Game Object Methods - JohnAD/turn_based_game GitHub Wiki
The Game object is associated (in this module) with a variety of pre-defined methods.
But before we being with those methods, let's examine the variables that come with the Game
object.
Game
:
Variables inherited from -
players
-
players*: seq[Player]
-
this sequence contains the player objects that are actually playing the game. The default
Player
class simply asks the end-user to choose what move to make via the local shell/terminal. But other players can be created; such as AI players and framework-specific players.This sequence also determines position. So, the player at index 0 is player 1, the player at index 1 is player 2, etc.
-
-
current_player_number
current_player_number*: int
- This is the player currently playing a turn in the game. It defaults to 1 (
players[0]
), but that can be overridden with thesetup
method. Seesetup
below.
-
winner_player_number
-
winner_player_number*: int
-
This is the player number of the winner of the current game.
If the value is 0 (const
NO_WINNER_YET
), it means that there is not currently a winner.If the value is -1 (const
STALEMATE
), it means that the game reached the point where neither player can possibly win. A tie, for example, can create a stalemate condition.By default, a value of 0 also means the game is not over yet. But that behavior can be changed by overriding the
'is_over
method. Seeis_over
below.
-
Methods That Must Be Overridden
The following four methods MUST be redefined in your inherited object in order for the game rules engine to work.
setup
method setup*(self: Game, players: seq[Player])
By itself, the Game
object has no information about the game play itself. For example, if writing a checkers game, it doesn't know about the playing pieces or the 8x8 board. When inheriting from the Game object, you generally add those properties. The setup method is where you "setup" those properties to the conditions expected at the beginning of the game. In the case of checkers, for example, you would establish that the where the individual checker pieces are placed on the board using the setup
method.
To get the expect default behavior for the inherited properties of Game
, call the default_setup
method at the start of your setup
. For example:
method setup*(self: Checkers, players: seq[Player]) =
self.default_setup(players)
self.board = [
-1 # position 0 is ignored
B, B, B, B,
B, B, B, B,
B, B, B, B,
e, e, e, e,
e, e, e, e,
R, R, R, R,
R, R, R, R,
R, R, R, R
]
set_possible_moves
method set_possible_moves*(self: Game, moves: var seq[string])
or
method set_possible_moves*(self: Game, moves: var OrderedTable[string, string])
One of these two methods must be re-defined. If you overload the seq version and it returns a non-empty sequence, the OrderedTable version is ignored; preferring the faster simple sequence.
Both methods have the same purpose, to list the possible moves available to the player at the current turn of the game. Each move must be encoded as a unique string. var seq[string]
is a list of strings as a simple sequence. var OrderedTable[string, string]
sets a table, where the key to each entry of the table is a move string, and the value for each entry is a human readable description of that move.
For example, for the first move of a game of checkers, the player playing the black pieces might receive the following list of moves from the seq version:
@["9-13", "10-14", "11-15", "12-16"]
Or you can have set_possible_moves
, set the following table:
{
"9-13": "move man at 9 ahead to 13",
"10-14": "move man at 10 ahead to 14",
"11-15": "move man at 11 ahead to 15",
"12-16": "move man at 12 ahead to 16"
}
This procedure, regardless of version, should return at least one possible move. To not have a move implies that the game is over. It is certainly valid to have a move string labelled "do_nothing" or "skip_turn", etc. to flag that the player can't do anything other than skip their turn. This happens, for example, in the game of reversi, when one of the players has no valid place to put a chip.
Note: if the game is using the generic shell-based Player
objects, then any move with a string of "quit" will be ignored as the user typing "quit" will cause the game to end instead.
Note: when using with most AI algorithms, the var seq[string]
version must be functioning as the other version is ignored when it is considering future moves.
make_move
method make_move*(self: Game, move: string): string
From the list of possible move strings (see set_possible_moves
), this method applies the one chosen move string to the game. This method is where most of the game rules are encoded.
This method is not expected to determine if the game is over or if there is a winner. Those functions are encoded in determine_winner
and is_over
respectively.
determine_winner
method determine_winner(self: Game)
This method is always called AFTER the make_move
method, but BEFORE the next player is chosen. So, if
you are going to write the method in a way that calls the 'scoring' method, the 'scoring' value might
be the opposite of what is expected. Think this through carefully.
Depending on the game rules, it is possible that it could be called more often than that.
This method would set the winner_player_number
property to either:
0
: there is no winner,-1
: there is no winner and there cannot be a winner (such as in a tie), or- n where n is the player number of the winner.
Please note that, by default, the is_over
method looks at the winner_player_number
property to determine if the game is over. If it is non-zero, it returns true
, meaning the game is over. If your game uses differing conditions for end-of-game, then you will need to also override the is_over
method.
Methods That Are Good to Override
status
method status*(self: Game): string
The method returns the current state of the game. By default, it simply states whether the game is over or not.
Overriding this method will allow you to place better/more information into the status string. For example, if programming a game of checkers, you could return a text-rendition of the state of the checkerboard and show the current score.
Other Methods
For most games, you will not need to override the following methods. However, if your game has a unique set of rules that create a need for such, they are made available for override.
next_player_number
method next_player_number*(self: Game): int
This procedure returns the player number that is to play next.
By default, this procedure returns the numbers in a round-robin sequence. So, if player_number 1 is the current player, then this returns 2. If the player number is 2 and there are more players, this returns 3, otherwise, it rotates back to 1. And so on.
Override this method if you need a different or more complex turn sequence.
finish_turn
method finish_turn*(self: Game)
This procedure is called after make_move
is called to finish up the current turn and start the next turn.
Be default, finish turn's code is simply:
method finish_turn*(self: Game) =
self.current_player_number = self.next_player_number()
Override this method if you need more done when finishing a turn. Be sure to set self.current_player_number
to the correct next player.
is_over
method is_over*(self: Game): bool
This procedure returns a boolean indicating whether the game is over.
The default is to simply see if a winner (or stalemate) has been declared yet (self.winner_player_number
). It returns true
if it has, otherwise false
. See determine_winner
.
If the game has other conditions for ending that are not determined by knowing the state of self.winner_player_number
, then override this method with what will make that determination.
default_setup
method default_setup*(self: Game, players: seq[Player])
This procedure is a convenience for a plain default setup of the Game variables. It sets:
self.players = players
self.player_count = len(self.players)
self.current_player_number = 1
self.winner_player_number = 0
I don't recommend overriding this method. I recommend you simply create your setup
method and ignore this if it is not what you want.
play
method play*(self: Game) : seq[string]
Methods Required By AIs (and possibly other libraries)
scoring
method scoring*(self: Game): float
The returned floating point number represents the current "score" of the game from the point of view of the current player. More positive numbers are always considered better than less positive numbers. The number need not reflect the "real" score; it is just a value that allows different game states to be judged against each other. For example, in a checkers game, there is no "score" in the true sense. One can only win or lose. So, one might numerically weigh such items as:
- How many of my pieces are left?
- How many of my opponents pieces are left?
- How well defended are my pieces?
- How many moves are possible for my pieces?
- etc.
Such a score might return a value between -1000.0 to 1000.0. But, if the game is over, then the score should be a more extreme number such as +9000.0 for a win or -9000.0 for a loss.
get_state
method get_state*(self: Game): string
The current state of the game is "encoded" into a single string. The actual format of the encoding does not matter as long as the corresponding restore_state
can use it to restore a game.
The encoding does need to include who the current player is.
For example, for a game of checkers, all 64 game board spaces could be represented as a string of 64 characters, where each character is a symbol of the content of that space. Plus a 65th character containing "B" or "R" to store who's turn it is (Black or Red).
This get_state/restore_state is used heavily by AIs such as Negamax. It could also be a convenient way to "save games".
restore_state
method restore_state*(self: Game, state: string): void
This method corresponds to the get_state
method. It should take the state
string and decode it in such as ways as to restore the game. This includes setting the current player.