Custom Chess Engine Mechanic Builder
1. System Architecture & Prerequisites
This skill builds a variant chess engine on top of python-chess while
keeping the logic layer completely pygame-free. The architecture splits into
four parts:
SkillChessBoard- a wrappedchess.Board. It mirrors the standard rule engine (push_uci,legal_moves,is_game_over) and addsturn_playerbookkeeping, a JSON-serialisablecooldownsdictionary, and alogofMoveEntryrecords so gameplay can be replayed/reported.- Skill mechanics -
apply_teleport(sq_from, sq_to)relocates a friendly piece instantly (legal moves route throughboard.push_uci; impossible movements use a variant fallback with manual turn advance) andapply_freeze(sq)marks a square frozen. A frozen square's occupant is skipped bylegal_moves()until its expiry ticks run out. - Cooldown scheduler -
cooldown_ticksdefines honest cooldowns per skill;can_cast(skill)gates casting,tick_cooldowns()decays both skill cooldowns and freeze expiries as the turn clock advances. - View layer -
rotate_view_180(board_ui)maps file/rank indices for a 180-degree board rotation,render_ascii()is a pure-text renderer that works without any GUI library, andrender()draws the same state on a pygame surface guarded by anImportErrorfallback.
Prerequisites:
| Dependency | Required? | Install |
|---|---|---|
| Python 3.9+ | yes | - |
python-chess |
yes (logic layer) | python -m pip install python-chess |
pygame 8.0+ |
only for render() |
python -m pip install pygame |
The core module imports cleanly with only python-chess installed; scene
rendering degrades to no-ops when pygame is absent.
2. Input/Output Data Contracts
2.1 Input JSON Schema - SkillChessBoard options
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "SkillChessBoardConfig",
"additionalProperties": false,
"properties": {
"start_fen": {
"type": "string",
"default": "",
"description": "Initial FEN; empty string starts the standard game"
},
"cooldown_ticks": {
"type": "object",
"properties": {
"teleport": { "type": "integer", "minimum": 0, "default": 6 },
"freeze": { "type": "integer", "minimum": 0, "default": 9 }
}
},
"freeze_duration": {
"type": "integer",
"minimum": 0,
"default": 3
}
}
}
2.2 Output contracts
cooldowns: Dict[str, int]- remaining ticks per skill after the last cast (0 means ready).frozen: Set[int]- chess square indices currently frozen.frozen_expiry: Dict[int, int]- remaining freeze ticks per frozen square.log: List[MoveEntry]- orderedMoveEntry(kind, detail, turn, cooldowns).render_ascii(board, view_rotated=False, frozen=None) -> str- 8 x 8 text board, uppercase = white, lowercase = black,.= empty, frozen cells shown as[p].render(screen, board, ...)- optional pygame draw, returnsNonewhen pygame is missing.
2.3 Output artifacts
| Artifact | Path | Format |
|---|---|---|
| Reference implementation | skills/chess-engine-mechanic-builder/chess_mechanics.py |
Python |
| Demo transcript | printed by main() (moves, teleport, freeze, cooldowns) |
stdout |
3. Production Reference Implementation
"""chess_mechanics.py - custom chess engine with skill cooldowns.
Wraps python-chess inside a SkillChessBoard that adds turn tracking,
per-skill cooldown ticks, a Teleport mechanic and a Freeze Square mechanic
with expiry timers. Rendering ships as a pygame-free ASCII view plus an
optional pygame surface renderer imported lazily, so the logic layer never
requires pygame.
Hard dependency (logic): python-chess (pip install python-chess)
Optional dependency: pygame 8.0+ (used only by render())
Run directly: python chess_mechanics.py
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
try:
import chess
from chess import Board, Piece
except ImportError as exc: # pragma: no cover - user-facing guard
raise ImportError(
"python-chess is required: python -m pip install python-chess"
) from exc
COOLDOWN_TICKS: Dict[str, int] = {"teleport": 6, "freeze": 9}
FREEZE_DURATION_TICKS = 3
SKILL_NAMES = tuple(COOLDOWN_TICKS)
PIECE_GLYPH = {
chess.PAWN: "p",
chess.KNIGHT: "n",
chess.BISHOP: "b",
chess.ROOK: "r",
chess.QUEEN: "q",
chess.KING: "k",
}
try:
import pygame
PYGAME_OK = True
except ImportError: # pragma: no cover - optional dependency
pygame = None # type: ignore[assignment]
PYGAME_OK = False
@dataclass
class MoveEntry:
"""A recorded action for replay / reporting."""
kind: str
detail: str
turn: str
cooldowns: Dict[str, int] = field(default_factory=dict)
class SkillChessBoard:
"""A chess.Board wrapper with fantasy mechanics.
Standard chess moves are pushed through ``board.push_uci`` (the rules
engine stays authoritative). Skill moves are applied as board-level
mutations and logged as ``kind in {teleport, freeze}`` entries so the
flavour never desyncs the main rule book.
"""
def __init__(
self,
start_fen: str = "",
cooldown_ticks: Optional[Dict[str, int]] = None,
freeze_duration: int = FREEZE_DURATION_TICKS,
) -> None:
self.board: Board = chess.Board(start_fen) if start_fen else chess.Board()
self.turn_player: str = "white" if self.board.turn else "black"
self.cooldown_ticks: Dict[str, int] = dict(COOLDOWN_TICKS)
if cooldown_ticks:
self.cooldown_ticks.update(cooldown_ticks)
self.cooldowns: Dict[str, int] = {name: 0 for name in SKILL_NAMES}
self.freeze_duration = freeze_duration
self.frozen: Set[int] = set()
self.frozen_expiry: Dict[int, int] = {}
self.log: List[MoveEntry] = []
# -- turn bookkeeping ----------------------------------------------------
def increment_turn(self) -> None:
"""Manually advance the player turn (used by variant teleports)."""
self.turn_player = "black" if self.turn_player == "white" else "white"
def _sync_turn(self) -> None:
"""Re-derive ``turn_player`` from the authoritative chess board."""
self.turn_player = "white" if self.board.turn else "black"
# -- standard moves ------------------------------------------------------
def push_uci(self, uci: str) -> None:
"""Push a standard move through the rule engine."""
self.board.push_uci(uci)
self._sync_turn()
self.log.append(MoveEntry("move", uci, self.turn_player,
dict(self.cooldowns)))
def legal_moves(self) -> Iterable[Any]:
"""All legal moves except those whose FROM square is frozen."""
for move in self.board.legal_moves:
if move.from_square in self.frozen:
continue
yield move
def can_move_from(self, square: int) -> bool:
"""True when the occupant of ``square`` may legally be moved."""
return square not in self.frozen
def piece_at(self, square: int) -> Optional[Piece]:
return self.board.piece_at(square)
# -- skill API -----------------------------------------------------------
def can_cast(self, skill: str) -> bool:
"""A skill is castable while its cooldown is spent and a game lives."""
if skill not in self.cooldowns:
return False
if self.board.is_game_over():
return False
return self.cooldowns[skill] <= 0
def tick_cooldowns(self, ticks: int = 1) -> None:
"""Advance every skill cooldown and freeze expiry by ``ticks``."""
for _ in range(max(0, ticks)):
for name in SKILL_NAMES:
self.cooldowns[name] = max(0, self.cooldowns[name] - 1)
for sq in list(self.frozen_expiry):
self.frozen_expiry[sq] -= 1
if self.frozen_expiry[sq] <= 0:
self.frozen.discard(sq)
del self.frozen_expiry[sq]
def apply_teleport(self, sq_from: int, sq_to: int) -> bool:
"""Instantly relocate a friendly piece from ``sq_from`` to ``sq_to``.
First the move is attempted as a legal chess move via
``board.push_uci`` (the turn flips naturally). When the board
rejects it as an impossible jump, a variant teleport mutates the
board directly with ``remove_piece_at`` / ``set_piece_at`` and the
turn advances manually. Returns False when the cast cannot happen.
"""
if not self.can_cast("teleport"):
return False
if not (0 <= sq_from < 64 and 0 <= sq_to < 64) or sq_from == sq_to:
return False
piece = self.board.piece_at(sq_from)
if piece is None or piece.color != self.board.turn:
return False
target = self.board.piece_at(sq_to)
if target is not None and target.color == piece.color:
return False
actor = self.turn_player
moved = False
try:
uci = chess.square_name(sq_from) + chess.square_name(sq_to)
candidate = chess.Move.from_uci(uci)
if candidate in self.board.legal_moves:
self.board.push_uci(uci)
moved = True
except ValueError:
moved = False
if moved:
self._sync_turn()
else:
self.board.remove_piece_at(sq_from)
if target is not None:
self.board.remove_piece_at(sq_to)
self.board.set_piece_at(sq_to, piece)
self.increment_turn()
self.cooldowns["teleport"] = self.cooldown_ticks["teleport"]
self.log.append(MoveEntry(
"teleport",
"%s->%s" % (chess.square_name(sq_from), chess.square_name(sq_to)),
actor,
dict(self.cooldowns),
))
return True
def apply_freeze(self, square: int) -> bool:
"""Freeze ``square`` so its occupant cannot move for the duration.
Freezing is a free action: it only costs the skill cooldown, not a
turn. The occupant's move options are skipped until the expiry ticks
elapse (handled by ``tick_cooldowns``).
"""
if not self.can_cast("freeze"):
return False
if not (0 <= square < 64):
return False
self.frozen.add(square)
self.frozen_expiry[square] = self.freeze_duration
self.cooldowns["freeze"] = self.cooldown_ticks["freeze"]
self.log.append(MoveEntry("freeze", chess.square_name(square),
self.turn_player, dict(self.cooldowns)))
return True
def rotate_coords_180(file_: int, rank: int, size: int = 8) -> Tuple[int, int]:
"""Map (file, rank) to its 180-degree rotated position on an 8-rank board."""
return (size - 1 - file_, size - 1 - rank)
def rotate_view_180(board_ui: Any) -> List[List[Any]]:
"""Rotate a square board view 180 degrees.
``board_ui`` is any 8 x 8 nested sequence (list of rows of piece glyphs,
ASCII grid, pygame colour keys ...). Rank order is reversed and every
row is reversed, mapping [file, rank] onto [7 - file, 7 - rank].
"""
rows: List[Any] = list(board_ui)
if len(rows) != 8 or any(len(row) != 8 for row in rows):
raise ValueError("board_ui must be an 8x8 nested sequence")
return [list(reversed(row)) for row in reversed(rows)]
def _board_rows(board: Any) -> List[List[str]]:
"""Rows of one-char piece symbols with rank 8 on top."""
if hasattr(board, "board"):
board = board.board
rows: List[List[str]] = []
for rank in range(7, -1, -1):
row: List[str] = []
for file_ in range(8):
piece = board.piece_at(chess.square(file_, rank))
if piece is None:
row.append(".")
elif piece.color == chess.WHITE:
row.append(PIECE_GLYPH[piece.piece_type].upper())
else:
row.append(PIECE_GLYPH[piece.piece_type])
rows.append(row)
return rows
def render_ascii(board: Any, view_rotated: bool = False,
frozen: Optional[Set[int]] = None) -> str:
"""Text board renderer (pygame-free).
Uppercase = white, lowercase = black, '.' = empty, frozen cells are
wrapped as ``[p]``. ``view_rotated=True`` renders the board from the
opponent's perspective (180-degree rotation).
"""
rows = _board_rows(board)
if view_rotated:
rows = rotate_view_180(rows)
lines: List[str] = []
for i, row in enumerate(rows):
rank_label = (8 - i) if not view_rotated else (i + 1)
cells: List[str] = []
for col, glyph in enumerate(row):
if frozen:
file_ = col if not view_rotated else 7 - col
rank = (7 - i) if not view_rotated else i
if chess.square(file_, rank) in frozen:
glyph = "[%s]" % glyph
cells.append(glyph)
lines.append("%d %s" % (rank_label, " ".join(cells)))
files = "abcdefgh" if not view_rotated else "hgfedcba"
lines.append(" %s" % " ".join(files))
return "\n".join(lines)
def render(screen: Any, board: Any, view_rotated: bool = False,
cell_px: int = 56, square_highlight: Optional[int] = None,
frozen: Optional[Set[int]] = None) -> None:
"""Optional pygame renderer. Silently no-ops when pygame is missing.
Drawing positions follow ``rotate_view_180`` coordinate mapping whenever
``view_rotated`` is set, while square lookups keep using real board
coordinates so pieces stay correct under rotation.
"""
if pygame is None:
return
if hasattr(board, "board"):
skill_ok = True
board = board.board
else:
skill_ok = False
palette = {
"light": (240, 217, 181),
"dark": (181, 136, 99),
"frozen": (220, 70, 80),
"sel": (90, 200, 90),
"piece": (20, 20, 20),
}
board_size = cell_px * 8
screen.fill((30, 30, 34))
for rank in range(8):
for file_ in range(8):
rx, ry = file_, rank
if view_rotated:
rx, ry = rotate_coords_180(file_, rank)
colour = palette["light"] if ((file_ + rank) % 2 == 0) else palette["dark"]
sq = chess.square(file_, rank)
if skill_ok and frozen and sq in frozen:
colour = palette["frozen"]
if sq == square_highlight:
colour = palette["sel"]
rect = pygame.Rect(rx * cell_px, ry * cell_px, cell_px, cell_px)
pygame.draw.rect(screen, colour, rect)
piece = board.piece_at(sq)
if piece:
font = pygame.font.Font(None, int(cell_px * 0.8))
glyph = PIECE_GLYPH[piece.piece_type]
if piece.color == chess.WHITE:
glyph = glyph.upper()
label = font.render(glyph, True, palette["piece"])
screen.blit(label, label.get_rect(center=rect.center))
pygame.display.flip()
def main() -> None:
"""Demo: a short opening line, then Teleport and Freeze casts showing
the cooldown and freeze-expiry lifecycle, plus both board views."""
skill = SkillChessBoard()
print("== Initial position ==")
print(render_ascii(skill))
line = ["e2e4", "e7e5", "g1f3", "b8c6"]
for uci in line:
skill.push_uci(uci)
print("\n== After %s ==" % ", ".join(line))
print(render_ascii(skill))
e4 = chess.parse_square("e4")
e7 = chess.parse_square("e7")
teleported = skill.apply_teleport(e4, e7)
print("\n== Teleport e4 -> e7 cast %s ==" % ("OK" if teleported else "FAILED"))
print(render_ascii(skill, frozen=skill.frozen))
froze = skill.apply_freeze(e7)
print("\n== Freeze e7 cast %s ==" % ("OK" if froze else "FAILED"))
print(render_ascii(skill, frozen=skill.frozen))
if skill.frozen:
print("Frozen squares: %s"
% ", ".join(chess.square_name(s) for s in sorted(skill.frozen)))
print("\n== Cooldowns right after cast ==")
print(json.dumps(skill.cooldowns, indent=2, sort_keys=True))
print("can_cast('teleport')=%s can_cast('freeze')=%s"
% (skill.can_cast("teleport"), skill.can_cast("freeze")))
frozen_sq = e7
before = skill.can_move_from(frozen_sq)
skill.tick_cooldowns(ticks=6)
after = skill.can_move_from(frozen_sq)
print("\ncan move from e7 while frozen: %s -> after 6 ticks: %s"
% (before, after))
print("cooldowns after ticking: teleport=%d freeze=%d"
% (skill.cooldowns["teleport"], skill.cooldowns["freeze"]))
print("\n== 180-degree rotated view of the same moment ==")
print(render_ascii(skill, view_rotated=True, frozen=skill.frozen))
print("\n== Move log ==")
for entry in skill.log:
print("%-10s %-14s %s" % (entry.kind, entry.detail, entry.turn))
if PYGAME_OK:
pygame.init()
screen = pygame.display.set_mode((8 * 56, 8 * 56))
pygame.display.set_caption("Skill Chess render() demo")
render(screen, skill, view_rotated=False, frozen=skill.frozen)
print("\npygame window shown; close it to exit the demo.")
if __name__ == "__main__":
main()
4. Execution Protocol & Step-by-Step Workflow
- Install logic dependency. Run
python -m pip install python-chess. pygame is optional and only needed forrender(). - Bring up a skill board. Create
SkillChessBoard(), optionally passingstart_fen, customcooldown_ticksandfreeze_duration. - Play standard moves. Call
board.push_uci("e2e4"). The wrapper keepsturn_playerin sync with the authoritativechess.Boardand appends aMoveEntrytolog. - Gate skill use. Before casting, assert
can_cast(skill)is True andcooldowns[skill] == 0; otherwise the cast is rejected and returns False. - Cast Teleport.
apply_teleport(sq_from, sq_to)relocates a friendly piece. Legal jumps route throughpush_uci; impossible jumps use the variant path and manually advance the turn. Verify success via the return value and the newcooldowns["teleport"]. - Cast Freeze.
apply_freeze(square)locks that square; its occupant is now skipped bylegal_moves()andcan_move_from()returns False. - Advance the clock. Call
tick_cooldowns(ticks=N)once per turn. This decays skill cooldowns and freeze expiry; squares whose expiry reaches zero are removed fromfrozenand unblocked again. - Render. Use
render_ascii(board, view_rotated=..., frozen=...)for console/debug output. For a GUI, callrender(screen, board, ...)afterpygame.display.set_mode(...). - Rotate the view. Wrap any 8 x 8 board view with
rotate_view_180(board_ui)or passview_rotated=Trueto the renderers. - Sanity check (agent). Run
main()and confirm the transcript shows two legal pushes per side, the teleport relocating the e4 pawn, the e7 square freezing, cooldowns descending from{"teleport":6,"freeze":9}, and e7 becoming movable again after the ticks.
5. Edge Cases & Error Handling
- Missing
python-chess. Module import raises a descriptiveImportErrorwith the install command; the logic layer cannot run without the rule engine. - Missing pygame.
render()and the GUI demo branch become no-ops;render_asciikeeps delivering full fidelity, so the demo still runs. - Teleport on cooldown or game over.
can_castreturns False andapply_teleportreturns False without touching the board. - Invalid teleport targets. Out-of-range squares, self-squares, absent
source pieces, and destinations holding a friendly piece are all rejected
before any mutation.
chess.square_name/Move.from_ucifailures are swallowed and routed to the variant fallback. - Frozen occupants. A frozen piece remains on the board but every move
whose
from_squareis frozen is omitted fromlegal_moves(), andcan_move_fromreports False, splitting the checks cleanly for any AI. - Expiry bookkeeping.
tick_cooldownssnaps cooldowns at 0 (never negative) and removes expired freezes from bothfrozenandfrozen_expiryatomically, so the two structures never disagree. - Variant-move legality gap. Direct board mutation bypasses push-side
validity (e.g., a teleported king may sit in attack). Callers that need
strict safety can re-run
board.is_valid()after a variant cast and undo the cast throughlogwhen it fails. - Ragged board views.
rotate_view_180and_board_rowsvalidate the 8 x 8 shape and raiseValueErrorinstead of producing garbled mirrors.