Recently I have been wanting to get better with Python, in particular with Python 3.x features and Python OOP, so I wrote what I thought at first would be pretty simple, a TicTacToe game, which turned out to become pretty complex after adding all that I wanted.
Some of the features I added, besides the basic game logic:
Player names and cumulative scores
Ability to play multiple games without restarting the program
Informative game messages throughout the application
Brief overview of the different sections:
Player Section: Basic player classes
Board Section: Game board with multiple methods, including a
play
method with certain conditions that must be met for a move/play to be valid.Messages Section: I did not want the game logic code to get polluted with a bunch of
print
statements and such, so I made aGameMessages
class to handle all these. I usedTemplate
with a good number of the messages. They are mostly very simple, aside fromdisplay_scores
method which has some comparison logic to build the text.Game Section: The
Game
class contains the bulk of the game logic. Thestart
method is responsible for setting up the board, the players, and the logic for playing multiple games in one session while keeping cumulative score. Theplay_game
method contains all the logic used in actually playing games, and is called in a loop withinstart
.
The whole program if self-contained in one single file at the moment. Here is a working demo on repl.it
from typing import Dict
from enum import Enum
from string import Template
from random import randrange
# Note: The ttt/TTT acronym is used for TicTacToe throughout this code.
### Player Section
class PlayerNum(Enum):
one = 1
two = 2
class Token(Enum):
X = 'X'
O = 'O'
class Player:
def __init__(self, player_num: PlayerNum, token: Token, name: str):
self.player_num = player_num
self.token = token
self.name = name
self.score = 0
def add_win(self):
self.score += 1
### Board Section
class Cell(Enum):
empty = '-'
X = Token.X
O = Token.O
class Board:
def __init__(self):
"""Empty TTT board."""
self.cells = {
1: Cell.empty, 2: Cell.empty, 3: Cell.empty,
4: Cell.empty, 5: Cell.empty, 6: Cell.empty,
7: Cell.empty, 8: Cell.empty, 9: Cell.empty
}
def play(self, cell: int, token: Token) -> bool:
"""Attempt to place a token on a cell.
Returns False if the cell is invalid or already occupied.
"""
if not 1 <= cell <= 9:
print('Invalid cell "', cell, '"; cell must be a whole number from 1 to 9.', sep='')
return False
elif not self.cells[cell] == Cell.empty:
print('Cell "', cell, '" is already occupied.', sep='')
return False
else:
self.cells[cell] = token
return True
def display(self) -> None:
"""Display the board to the user using stdout."""
for cell in self.cells:
print(self.cells[cell].value, end=' ')
# get a new line/row after each 3rd cell:
if cell % 3 == 0:
print()
def clear(self) -> None:
"""Make the board empty."""
for cell in self.cells:
self.cells[cell] = Cell.empty
def get_token_cells(self, token: Token) -> list:
"""Return a list of all cell numbers/keys occupied by the provided token."""
return list(key for key, value in self.cells.items() if value == token)
def get_empty_cells(self) -> int:
"""Return a list of all cell numbers/keys not occupied by any token."""
return len(list(key for key, value in self.cells.items() if value == Cell.empty))
### Game Section
class Game:
def __init__(self):
self.game_number = 1
self.victory_conditions = [
# horizontal
[1,2,3],
[4,5,6],
[7,8,9],
# vertical
[1,4,7],
[2,5,8],
[3,6,9],
# diagonal
[1,5,9],
[3,5,7]
]
self.start()
def start(self):
msg = GameMessages(self)
msg.welcome()
# Prepare the players
player1 = self.setup_player(PlayerNum.one)
player2 = self.setup_player(PlayerNum.two)
msg.announce_player(player1)
msg.announce_player(player2)
msg.display_controls()
# Start playing
continue_playing = True
while continue_playing == True:
self.play_game(player1, player2)
msg.display_scores(player1, player2)
continue_selection = ''
while continue_selection.upper() != 'Y' or continue_selection.upper() != 'N':
msg.ask_to_play_again()
continue_selection = input()
if continue_selection.upper() == 'Y':
self.game_number += 1
continue_playing = True
break
elif continue_selection.upper() == 'N':
msg.final_scores(player1, player2)
continue_playing = False
break
else:
# print invalid selection
print('Invalid selection:', continue_selection)
def setup_player(self, player_num: PlayerNum) -> Player:
msg = GameMessages(self)
msg.ask_player_name(player_num.value)
name = input()
if player_num == PlayerNum.one:
token = Token.X
else:
token = Token.O
return Player(player_num, token, name)
def is_player_victorious(self, board: Board, token: Token) -> bool:
"""Determine if a token/player meets victory conditions."""
token_cells = board.get_token_cells(token)
victorious = False
for condition in self.victory_conditions:
if not victorious:
if len(set(token_cells).intersection(condition)) >= 3:
victorious = True
return victorious
def play_game(self, player1: Player, player2: Player) -> None:
"""Plays a TTT game."""
msg = GameMessages(self)
msg.game_start()
board = Board()
# coin_toss to decide first player
if randrange(1,3) == 1:
first_player = player1
second_player = player2
else:
first_player = player2
second_player = player1
msg.announce_first_player(first_player)
turn_number = 1
winning_player = None
game_over = False
current_player = first_player
# game loop
while game_over == False:
msg.display_current_turn_board(turn_number, board)
is_valid_move = False
while is_valid_move == False:
msg.ask_player_for_move(current_player)
target_cell = input()
try:
int(target_cell)
except ValueError:
print('Invalid input:', target_cell, 'is not a valid whole number.')
is_valid_move = False
else:
is_valid_move = board.play(int(target_cell), current_player.token)
# check victory conditions after most recent play
if self.is_player_victorious(board, current_player.token):
winning_player = current_player
msg.announce_player_win(winning_player)
board.display()
if winning_player == first_player:
if first_player == player1:
player1.add_win()
else:
player2.add_win()
else:
if second_player == player1:
player1.add_win()
else:
player2.add_win()
game_over = True
# game draw conditions:
if board.get_empty_cells() == 0 and winning_player == None:
msg.announce_draw()
game_over = True
turn_number += 1
if current_player == first_player:
current_player = second_player
else:
current_player = first_player
### Messages Section
class GameMessages:
"""A collection of message templates to be displayed during a TTT game."""
def __init__(self, game: Game):
self.game = game
def display_controls(self):
print('Controls: When asked to select a cell number (1-9) to play, ',
'type a number 1-9 to place your token on that cell:\n',
'1 2 3\n4 5 6\n7 8 9', sep='')
def welcome(self):
print('Welcome to TicTacToe.')
def game_start(self):
msg = Template('---\nNow playing game $num!')
print(msg.substitute(num = self.game.game_number))
def ask_player_name(self, player_num: int):
print('Player', player_num, 'enter your name: ', end='')
def announce_player(self, player: Player):
msg = Template('Player $num is $name and will use token "$token".')
print(msg.substitute(
num = player.player_num.value,
name = player.name,
token = player.token.value))
def announce_first_player(self, player: Player):
msg = Template('The coin toss results in $name playing the first turn.')
print(msg.substitute(name = player.name))
def display_current_turn_board(self, turn: int, board: Board):
print('Turn', turn, 'board:')
board.display()
def ask_player_for_move(self, player: Player):
msg = Template('$name, select a cell to place your "$token" token in: ')
print(msg.substitute(
name = player.name,
token = player.token.value), end='')
def announce_draw(self):
print('Game', self.game.game_number, 'is a draw.')
def announce_player_win(self, player: Player):
msg = Template('$name wins game $game_num!')
print(msg.substitute(
name = player.name,
game_num = self.game.game_number))
def display_scores(self, player1: Player, player2: Player):
if player1.score == player2.score:
msg = Template('$name1 and $name2 are tied with $score points each after $games game(s).')
print(msg.substitute(
name1 = player1.name,
name2 = player2.name,
score = player1.score,
games = self.game.game_number))
else:
if player1.score > player2.score:
leading_player = player1
trailing_player = player2
else:
leading_player = player2
trailing_player = player1
msg = Template('$name1 is in the lead with $score1 points! $name2 is trailing with $score2 points after $games game(s).')
print(msg.substitute(
name1 = leading_player.name,
score1 = leading_player.score,
name2 = trailing_player.name,
score2 = trailing_player.score,
games = self.game.game_number))
def ask_to_play_again(self):
print('Play again? (Y/N)')
def final_scores(self, player1: Player, player2: Player):
print('FINAL SCORE!')
self.display_scores(player1, player2)
def main():
game = Game()
main()