Chapter 3: Rendering the Board
Chapter 3: Rendering the Board¶
π Previous: Chapter 2: Modeling Moves¶
In previous chapters, we used a simple ASCII display function. Now we'll create a higher quality renderer. We will use color and have a grid for the board.
Setup: Import from Previous Chapters¶
# Core constants and functions from previous chapters
EMPTY = 0
RED_PAWN = 1
WHITE_PAWN = 2
RED_KING = 3
WHITE_KING = 4
RED = 1
WHITE = 2
def map_1d_to_2d(i):
row = i // 4
col = 2 * (i % 4) + (1 - row % 2)
return row, col
def map_2d_to_1d(row, col):
return row * 4 + ((col - (1 - row % 2)) // 2)
class GameState:
def __init__(self, board, turn=RED):
self._board = tuple(board)
self._turn = turn
@property
def board(self):
return self._board
@property
def turn(self):
return self._turn
def at(self, row, col):
idx = map_2d_to_1d(row, col)
return self._board[idx]
def apply_move(state, move):
"""Apply a move from Chapter 2."""
new_board = list(state.board)
start_pos = move[0]
end_pos = move[-1]
new_board[end_pos] = new_board[start_pos]
new_board[start_pos] = EMPTY
for i in range(len(move) - 1):
r1, c1 = map_1d_to_2d(move[i])
r2, c2 = map_1d_to_2d(move[i + 1])
if abs(r2 - r1) > 1:
jumped_row = (r1 + r2) // 2
jumped_col = (c1 + c2) // 2
jumped_pos = map_2d_to_1d(jumped_row, jumped_col)
new_board[jumped_pos] = EMPTY
if new_board[end_pos] == RED_PAWN and end_pos in range(28, 32):
new_board[end_pos] = RED_KING
elif new_board[end_pos] == WHITE_PAWN and end_pos in range(0, 4):
new_board[end_pos] = WHITE_KING
next_turn = WHITE if state.turn == RED else RED
return GameState(new_board, next_turn)
print("✓ Setup complete")
✓ Setup complete
Step 1: ANSI Color Codes and Unicode Characters¶
Let's define ANSI escape codes that will give our pieces color in a terminal. We will continue using the same unicode characters as before.
# ANSI color codes for terminal output
RED_COLOR = '\033[91m' # Bright red
BLACK_COLOR = '\033[30m' # Black
GRAY_COLOR = '\033[37m' # Light gray for grid
RESET_COLOR = '\033[0m' # Reset to default
# Unicode symbols for different piece types
PIECE_SYMBOLS = {
EMPTY: ' ',
RED_PAWN: '●', # Filled circle
WHITE_PAWN: '○', # Hollow circle
RED_KING: '♚', # King symbol
WHITE_KING: '♔', # King symbol
}
# Piece colors
PIECE_COLORS = {
EMPTY: '',
RED_PAWN: RED_COLOR,
WHITE_PAWN: BLACK_COLOR,
RED_KING: RED_COLOR,
WHITE_KING: BLACK_COLOR,
}
# Test rendering pieces
print("Piece rendering:")
for piece_type, symbol in PIECE_SYMBOLS.items():
if piece_type == EMPTY:
continue
color = PIECE_COLORS[piece_type]
print(f" {color}{symbol}{RESET_COLOR} - Piece type {piece_type}")
Piece rendering: ● - Piece type 1 ○ - Piece type 2 ♚ - Piece type 3 ♔ - Piece type 4
Step 2: Rendering the Board¶
Let's draw the board with colored pieces and grid lines using state.at(row, col) directly.
def render_board(state):
"""Render the board with colored pieces and grid lines."""
output = []
# Top border
output.append(f"{GRAY_COLOR}+" + "---+" * 8 + f"{RESET_COLOR}")
# Each row
for row in range(8):
row_str = f"{GRAY_COLOR}|{RESET_COLOR} "
for col in range(8):
# Check if this is a playable square
if (row + col) % 2 == 0:
# Light square (not playable)
row_str += " " + f" {GRAY_COLOR}|{RESET_COLOR} "
else:
# Dark square (playable) - get piece and render
piece = state.at(row, col)
symbol = PIECE_SYMBOLS[piece]
color = PIECE_COLORS[piece]
row_str += f"{color}{symbol}{RESET_COLOR}" + f" {GRAY_COLOR}|{RESET_COLOR} "
output.append(row_str)
# Horizontal separator
output.append(f"{GRAY_COLOR}+" + "---+" * 8 + f"{RESET_COLOR}")
return "\n".join(output)
# Test rendering
board = [RED_PAWN] * 12 + [EMPTY] * 8 + [WHITE_PAWN] * 12
state = GameState(board, RED)
print(render_board(state))
Step 3: Renderer Interface and TextRenderer¶
We want to support different types of rendering (text, GUI, etc.), so we define a Renderer interface and implement TextRenderer as our first concrete renderer.
from abc import ABC, abstractmethod
class Renderer(ABC):
"""Abstract base class for all renderers."""
@abstractmethod
def render(self, state):
"""Render the game state."""
pass
class TextRenderer(Renderer):
"""Renders game boards as colored text with grid lines."""
def __init__(self):
self.piece_symbols = PIECE_SYMBOLS
self.piece_colors = PIECE_COLORS
def render(self, state):
"""Render the board as colored text with grid."""
output = []
output.append(f"{GRAY_COLOR}+" + "---+" * 8 + f"{RESET_COLOR}")
for row in range(8):
row_str = f"{GRAY_COLOR}|{RESET_COLOR} "
for col in range(8):
if (row + col) % 2 == 0:
row_str += " " + f" {GRAY_COLOR}|{RESET_COLOR} "
else:
piece = state.at(row, col)
symbol = self.piece_symbols[piece]
color = self.piece_colors[piece]
row_str += f"{color}{symbol}{RESET_COLOR}" + f" {GRAY_COLOR}|{RESET_COLOR} "
output.append(row_str)
output.append(f"{GRAY_COLOR}+" + "---+" * 8 + f"{RESET_COLOR}")
print("\n".join(output))
# Test the renderer
renderer = TextRenderer()
board = [RED_PAWN] * 12 + [EMPTY] * 8 + [WHITE_PAWN] * 12
state = GameState(board, RED)
print("Using TextRenderer:")
renderer.render(state)
Using TextRenderer: +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | ● | | ● | | ● | | ● | | +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | | | | | | | | | +---+---+---+---+---+---+---+---+ | | | | | | | | | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ | | ○ | | ○ | | ○ | | ○ | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+
Runnable Artifact: Enhanced Game Display¶
Let's use our renderer to display a game in progress with better visuals.
def display_game_with_renderer(moves):
"""Display a game using the enhanced renderer."""
renderer = TextRenderer()
board = [RED_PAWN] * 12 + [EMPTY] * 8 + [WHITE_PAWN] * 12
state = GameState(board, RED)
print("="*40)
print(" CHECKERS GAME - INITIAL POSITION")
print("="*40)
renderer.render(state)
for move_num, move in enumerate(moves, 1):
state = apply_move(state, move)
turn_name = "RED" if state.turn == RED else "WHITE"
print(f"\n{'='*40}")
print(f" Move {move_num}: {move}")
print(f" Next turn: {turn_name}")
print("="*40)
renderer.render(state)
print("\n✓ Game display complete!")
# Display a sample game
sample_moves = [
[9, 13],
[22, 18],
[10, 14],
[24, 19],
]
display_game_with_renderer(sample_moves)
======================================== CHECKERS GAME - INITIAL POSITION ======================================== +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | ● | | ● | | ● | | ● | | +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | | | | | | | | | +---+---+---+---+---+---+---+---+ | | | | | | | | | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ | | ○ | | ○ | | ○ | | ○ | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ ======================================== Move 1: [9, 13] Next turn: WHITE ======================================== +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | ● | | ● | | ● | | ● | | +---+---+---+---+---+---+---+---+ | | ● | | | | ● | | ● | +---+---+---+---+---+---+---+---+ | | | ● | | | | | | +---+---+---+---+---+---+---+---+ | | | | | | | | | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ | | ○ | | ○ | | ○ | | ○ | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ ======================================== Move 2: [22, 18] Next turn: RED ======================================== +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | ● | | ● | | ● | | ● | | +---+---+---+---+---+---+---+---+ | | ● | | | | ● | | ● | +---+---+---+---+---+---+---+---+ | | | ● | | | | | | +---+---+---+---+---+---+---+---+ | | | | | | ○ | | | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | | | ○ | | +---+---+---+---+---+---+---+---+ | | ○ | | ○ | | ○ | | ○ | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ ======================================== Move 3: [10, 14] Next turn: WHITE ======================================== +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | ● | | ● | | ● | | ● | | +---+---+---+---+---+---+---+---+ | | ● | | | | | | ● | +---+---+---+---+---+---+---+---+ | | | ● | | ● | | | | +---+---+---+---+---+---+---+---+ | | | | | | ○ | | | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | | | ○ | | +---+---+---+---+---+---+---+---+ | | ○ | | ○ | | ○ | | ○ | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ ======================================== Move 4: [24, 19] Next turn: RED ======================================== +---+---+---+---+---+---+---+---+ | | ● | | ● | | ● | | ● | +---+---+---+---+---+---+---+---+ | ● | | ● | | ● | | ● | | +---+---+---+---+---+---+---+---+ | | ● | | | | | | ● | +---+---+---+---+---+---+---+---+ | | | ● | | ● | | | | +---+---+---+---+---+---+---+---+ | | | | | | ○ | | ○ | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | | | ○ | | +---+---+---+---+---+---+---+---+ | | | | ○ | | ○ | | ○ | +---+---+---+---+---+---+---+---+ | ○ | | ○ | | ○ | | ○ | | +---+---+---+---+---+---+---+---+ ✓ Game display complete!
Summary¶
In this chapter, we built a rendering system for displaying checkers boards:
- ANSI colors and Unicode symbols: Terminal escape codes for colored output with distinctive piece characters
- Direct rendering: Using
state.at(row, col)to render the board without intermediate data structures - Renderer interface: Abstract base class defining the contract for all renderers
- TextRenderer: Concrete implementation for colored terminal output with grid lines
What's next?
In Chapter 4, we'll build player abstractions so both humans and AI can play the game.
Repo reference: The complete code is in renderer/renderer.py and renderer/text_renderer.py [Repo to be published soon.]
π Next: Chapter 4