Build your own Game module
How to build the module
To build the module you’ll need to inherit the Game class from the Game module documentation and implement all of the methods
How to test if the module was implemented correctly
Game module example (Freecell)
Module structure
- Among many files, our core functionality was split onto following files:
Board.py- containsBoardclass; represents game state and provides functionalities regarding performing movesCard.py- containsCardclass; provides card representation and functionalities regarding comparison and move validity checksDeck.py- containsDeckclass; definesDeckas a list ofCardobjects and provides shuffling functionalityFreecell.py- containsFreeCellclass; game entry point, equipped only with methods required by theGameabstract classGame.py- containsGameabstract class andStateenum classMove.py- containsMoveenum class
Game notation
Game abstract class, move in get_moves should be passed as a (preferably string) tuple.'F'for an empty free cell'S'for suit stack'{rank}{color}'for any other destination card'0'for empty columns- examples:
('TH', 'F')means moving the Ten of Hearts to a freecell('JS', 'S')means moving the Jack of Spades to a suit stack('AD', '2C')means moving the Ace of Diamonds to the 2 of Clubs('AD', '0')means moving the Ace of Diamonds to an empty column
FreeCell.py
Consider games listed here only as an example and a brief guide on how to implement the necesities provided by Game.
The part of showcased code is the most vital part of FreeCell in regards to entire project.
The rest of the code is available in our repository.
Let’s take a look at the FreeCell class, the entry point of the game:
from game.Game import Game, State
from game.Board import Board
from game.Deck import Deck
from game.Move import Move
from random import Random
class FreeCell(Game):
def __init__(self, seed: int = None):
if seed is None:
seed = Random().randint(0, 1000000)
self._move_count = 0
self.deck = Deck(seed)
self.board = Board(self.deck.cards_shuffled())
def increment_move_count(self):
self._move_count += 1
# Overridden functions from game class
def get_moves(self) -> list:
"""Get all possible moves from the current board state.
:return: A list of all possible moves from the current board state.
"""
moves = list()
# Moves onto empty columns
if [] in self.board.columns:
for card in self.board.free_cells + self.board.get_movable_cards():
if card:
moves.append((str(card), "0"))
# Get cards from the top of columns
suspected_moves = self.board.get_movable_cards()
# Check if at least one of freecells is empty
if None in self.board.free_cells:
# Append moving every from the top of column to a freecell
for card in suspected_moves:
moves.append((str(card), "F"))
for card in self.board.free_cells:
if card:
# Check for suit stack moves
if card.is_larger_and_same_suit(self.board.suit_stack[card.suit]):
moves.append((str(card), "S"))
# Check if any card from freecells can be moved onto a column
for card_destination in suspected_moves:
if card.is_smaller_and_different_color(card_destination):
moves.append((str(card), str(card_destination)))
for card in suspected_moves:
# Check if any card from columns can be moved onto a suit stack
if card.is_larger_and_same_suit(self.board.suit_stack[card.suit]):
moves.append((str(card), "S"))
# Check if any card from columns can be moved onto another column
for card_destination in suspected_moves:
if card != card_destination and (
card.is_smaller_and_different_color(card_destination)
):
moves.append((str(card), str(card_destination)))
return moves
def make_move(self, move: tuple) -> bool:
if move not in self.get_moves():
# return False
raise ValueError("Invalid move, not in get_moves()")
card = self.board.find_card_from_string(move[0])
match move[1]:
case Move.FREECELL.value:
move_completed = self.board.move_to_free_cell(card)
case Move.SUIT_STACK.value:
move_completed = self.board.move_to_stack(card)
case Move.EMPTY_COLUMN.value:
move_completed = self.board.move_to_free_column(card)
case _:
move_completed = self.board.move_to_card(
card, self.board.find_card_from_string(move[1])
)
if move_completed:
self.increment_move_count()
else:
raise ValueError("Invalid move, problem with execution")
return move_completed
def get_state(self) -> State:
"""Get the current state of the game.
:return: The current state of the game as State enum.
"""
suit_stack = list(self.board.suit_stack.values())
for card in suit_stack:
if card is None or card.rank != 13:
return State.ONGOING if bool(self.get_moves()) else State.LOST
return State.WON
def get_board(self) -> list:
"""Get the current board state.
:return: The current board state as a list of 10 lists:
* The first 8 lists are the columns.
* The next 4 element long list is the list of free cells.
* The last 4 element long list is
the list of the top cards on each suit stack.
"""
return (
self.board.columns,
self.board.free_cells,
list(self.board.suit_stack.values()),
)
def start_game(self) -> None:
self.__init__()
Game module example (2048)
Module structure
- Among many files, our core functionality was split onto following files:
board.py- containsBoardclass; represents game state and provides functionalities regarding performing moves and their validity checksgame.py- containsGameabstract class andStateenum classgame2048.py- containsGame2048class andDirectionenum class; game entry point, equipped only with methods required by theGameabstract class and an enum representing move directionsnode.py- contains a briefNodeclass; representation of a game tile
Game notation
Game abstract class, move in get_moves should be passed as a (preferably string) tuple.'w'for a move up's'for a move down'a'for a move left'd'for a move right
game2048.py
Consider games listed here only as an example and a brief guide on how to implement the necesities provided by Game.
The part of showcased code is the most vital part of 2048 in regards to entire project.
The rest of the code is available in our repository.
Let’s take a look at the Game2048 class, the entry point of the game:
from code2048.game import Game, State
from code2048.board import Board
class Game2048(Game):
def __init__(self, board: Board = None, rows: int = 4, cols: int = 4) -> None:
self.board = board if board else Board(rows, cols)
def get_moves(self) -> list:
"""
Provides possible moves as a list of w/s/a/d characters meaning up/down/left/right respectively
"""
return [key.value for key in self.board.possible_moves.keys()]
def make_move(self, move: tuple) -> bool:
"""
Returns True if move succeeded, False otherwise.
Requires move in form of one element tuple, containing character mentioned above.
Example: make_move('w',) will perform an upwards move.
"""
if move[0] in self.get_moves():
self.board.make_move(move[0])
return True
return False
def get_state(self) -> State:
"""
Returns game state enum: State.{ONGOING / WON / LOST}.
"""
return self.board.game_status()
def get_board(self) -> list:
"""
Returns current board state as a list of lists (rows).
"""
return self.board.board
def start_game(self) -> None:
"""
Overwrites current object, invoking constructor with default values and resetting every variable.
"""
self.board = Board()