The game manager is largely responsible for being an interface for the user to interact with the game state, by centralising and wrapping useful methods from the other components. It contains all other game components. It also has a few responsibilities relating to the visual state of the game:
Moves ask the game manager for an animator component that handles animating a particular move. Animators tell the move when they are done so that logical actions can be taken. Animators are generally responsible for making sure pieces are in the correct visual state for given moves. The game manager enforces positional state after moves are committed as well, to make sure visual piece components are in the correct place.
The game manager creates, contains and manages all visual pieces. Visual pieces map to their corresponding IPieceProperties object, and exist on a visual (rectangular for now) grid.
The move manager is a queue that processes submitted moves in order, and also remembers all moves ever committed to the state. The manager contains events that are subscribed to by the game rules (and could be utilised by users too) in order to hook into move-ending events to advance the game state.
The move manager can unwind its own state with ScheduleUndo, but this is not currently very robust. Importantly, any move knows how to play forwards and backwards, but the move manager won’t correctly maintain its stack of historical moves unless the correct moves are undone in the order in which they were submitted. ScheduleUndo will do this properly, but only when called one move at a time (since moves are not removed from history until they are complete). To schedule the undo for multiple moves at once, a more robust solution should be developed.
The turn manager is a straightforward state machine. It knows which player is currently taking their turn, knows the current turn state, and knows all players in the game (in turn order).
Players are simple objects to represent an agent in the game. Currently the only logic inside of HumanPlayer is one that automatically advances them from PlayerTurnState.Starting and PlayerTurnState.Ending.
The AI player object does that as well, and additionally contains its personal blackboard and delegates move selection to its behaviour tree.
The player (and thus its behaviour tree) is ticked by the Turn Manager in the following case:
- Every frame while
- the move queue is empty,
- and the player is in its Running state
Generally, the game rules will automatically advance the game state (therefore causing a player to move from Running to Ending) after a move completes, and so a behaviour tree is only ticked once per turn. However, in the case where players could make multiple moves per turn (such as in draughts), this can potentially happen a number of times in one turn. Every time it happens, the rules will have a list of all available actions that can be taken in that state.
The game state is a wrapper for the underlying logical grid. It is also responsible for all the types of moves that can operate on itself, as well as all fundamental actions that can operate on itself. Since moves only know how to operate on a given game state, all moves and the game state itself are strongly tied into each other.
Moves represent a single, reversible atomic change in the game state. Moves are equatable, such that different instances of moves that represent the same player action are equivalent. This is important for a number of reasons, but primarily for move validation.
The rules component is the most complex component in Abstract Strategy. It handles all the piece types available to all players, knows how those pieces can behave and move, and therefore is capable of producing an exhaustive collection of all possible moves that can be made from the current game state. It also maintains and checks for victory conditions, and advances the game state based on moves made by players, usually automatically after x-number of moves made by a player (counted once every time MoveManager fires OnAllMovesEnd, which happens whenever its move queue is depleted).
Given that the rules know a list of all available moves for the current player, the rules also use this information to validate moves, by comparing a given move against its known list.
The rules listen for various events from various other components in order to perform its logic.
All moves performed on the game state know how to undo what they have done. If registered and executed with the TurnManager, simply calling ScheduleUndo will revert the last move made in the game. You can also manually keep a stack of all changes made to the state and rewind them without using the turn manager (which needs to go through at least one update before it ticks the current move anyway).
However, complications arise with regards to animations. By default, a move will attempt to get an animator component when starting, even when starting backwards. It queries the game manager for the correct animator components which have been set up by the user. In order to test and rewind states for AI purposes, you will need a way to circumvent this behaviour so that moves can perform all of their logic in one frame rather than waiting for animations.
Cloning a state is also possible by simply copying the game state object’s contained grid, but it is likely prohibitively expensive and using the existing rewind behaviour will likely be simpler. However, moves ask a game manager for the game state object to modify, so if you’re performing operations on a clone, moves will need to know how to get a game state to change through some other interface instead.