Events¶
Introduction¶
ChessMaker uses a custom event system to allow altering and extending the game logic. This allows you to add custom logic to the game without having to modify the engine code.
The event system is inspired by Spigot's Event API.
Events¶
The event system defines a base Event
dataclass that all event types inherit from.
All attributes of the event are immutable by default, and the event exposes
one function called _set, which allows event types to make a specific attribute mutable.
Generally, things that happen expose a before and after event, with some only exposing an after event. A common pattern is for after events to be completely immutable, and for before events to have mutable attributes.
from dataclasses import dataclass
from chessmaker.events import Event
from chessmaker.chess.base.move_option import MoveOption
# An immutable event
@dataclass(frozen=True)
class AfterMoveEvent(Event):
piece: "Piece"
move_option: MoveOption
# A mutable event
class BeforeMoveEvent(AfterMoveEvent):
def set_move_option(self, move_option: MoveOption):
self._set("move_option", move_option)
Cancellable Events¶
Events can also inherit from the CancellableEvent
class,
which adds a cancelled
attribute and a set_cancelled
function to the event.
from chessmaker.events import CancellableEvent
class BeforeMoveEvent(AfterMoveEvent, CancellableEvent):
def set_move_option(self, move_option: MoveOption):
self._set("move_option", move_option)
Note
Most of the time, you're going to be subscribing to existing events, but if you are creating a new event, you should remember events are just dataclasses - and don't actually implement logic like cancelling or mutating. It is the publisher's responsibility to use the mutated event in the correct way.
Subscribing to events¶
To subscribe to events, you need to subscribe to a publisher with the event type and callback function. Events are subscribed to on a per-instance basis - when you subscribe to a Pawn moving, it will only subscribe to that specific pawn - not all pawns.
import random
board: Board = ...
def on_after_turn_change(event: BeforeTurnChangeEvent):
if random.random() < 0.5:
event.set_cancelled(True)
else:
event.set_next_player(event.board.players[1])
board.subscribe(BeforeTurnChangeEvent, on_after_turn_change)
Tip
In you event's callback function, you should use the arguments from the event,
rather than using ones from your outer scope (For example, board
in the above example).
This is related to Cloneables, and will be explained later.
Event Priorities¶
Events can be subscribed to with a priority, which determines the order in which they are called - a higher priority means the event is called earlier.
For most use cases, the default priority of 0
is fine,
but if you need to ensure your event is called before or after another event,
you can either use the EventPriority
enum to specify a priority, or use an integer for more fine-grained control.
from chessmaker.events import EventPriority
board.subscribe(BeforeTurnChangeEvent, on_after_turn_change)
board.subscribe(BeforeTurnChangeEvent, on_after_turn_change, priority=EventPriority.VERY_LOW)
board.subscribe(BeforeTurnChangeEvent, on_after_turn_change, 2000)
Subscribing to all events of an instance¶
You can also subscribe to all events of an instance by using the subscribe_all
function.
def on_any_event(_: Event):
print("Something happened to the board!")
board.subscribe_all(on_any_event)
Unsubscribing from events¶
To unsubscribe from events, you need to call the unsubscribe
function with the same arguments you used to subscribe.
Similarly, you can use unsubscribe_all
to unsubscribe from all events of an instance.
Publishing events¶
If you're adding new code, and want to make that code extendible - it is recommended to publish events.
For an instance to publish events, it needs to use the @event_publisher
decorator,
and specify the event types it publishes.
If it inherits from another publisher, you need use the same decorator to specify the additional event types it publishes, If it doesn't publish any additional events, you don't have to use the decorator at all.
For typing and completion purposes, a publisher should also inherit from EventPublisher
.
(If it doesn't inherit from another publisher).
from chessmaker.events import EventPublisher, event_publisher
@event_publisher(BeforePrintEvent, AfterPrintEvent)
class MyPrinter(EventPublisher):
def print_number(self):
number = str(random.randint(0, 100))
before_print_event = BeforePrintEvent(self, number)
self.publish(before_print_event)
if not before_print_event.cancelled:
print(before_print_event.number)
self.publish(AfterPrintEvent(self, number))
Propagating events¶
Sometimes, you may want to publish events from a publisher to another one. You can do this either to all event types, or to a specific one.
@event_publisher(BeforePrintEvent, AfterPrintEvent)
class MyPrinterManager(EventPublisher):
def __init__(self, my_printer: MyPrinter):
self.my_printer = my_printer
self.my_printer.propagate_all(self)
self.my_printer.propagate(BeforePrintEvent, self)
Now, every time the printer publishes an event, the manager will also publish it. Currently, you can not unpropagate events.
Info
The main use of this in the game is the board propagating all events of its pieces and squares to itself. This means that instead of subscribing to a specific piece move, you can subscribe to all pieces moving by subscribing to the board.