This guide is not a general Python course.
It teaches only the Python you need to start doing useful work in CynMeith:
- defining a piece
- checking the board
- writing a custom move rule
- attaching a side effect
If you can follow the examples here, you are already far enough to prototype games with this engine.
This page is for you if:
- you are curious about making your own board-game rules
- you can read some code, but do not want a full Python textbook first
- you want the smallest possible set of ideas that unlock CynMeith
This page is not for advanced Python users. If you are already comfortable with functions, loops, and dataclasses, skim this and move on to Your First Custom Game.
Before syntax, understand the shape of the engine.
- A
PieceStateis data: what kind of piece, whose it is. - A
BoardStatemaps positions to pieces. You read it; you never edit it. - A move generator is a function that looks at the board and describes what one piece could do.
- The
Enginetakes aGameStateand aMoveand produces the nextGameState.
When you write custom rules in CynMeith, you are usually doing one of two things:
- describing which moves exist (generators, filters)
- describing what extra should happen when a move lands (effects)
That is why the Python you need is mostly small functions that return or yield values. There is almost no class-writing: pieces are built by calling functions, not by subclassing.
In Python, you give a value a name with =.
distance = 2
symbol = "S"
allowed = TrueYou will do this constantly in move logic:
dr = destination.r - origin.r
dc = destination.c - origin.cHere dr means "change in row" and dc "change in column". These two values
are often enough to describe a move.
A function is a named block of code that does one job.
def is_forward_step(start, end):
dr = end.r - start.r
dc = end.c - start.c
return dr == 1 and dc == 0Important parts:
defstarts a function- the indented block is the function body
returnsends the result back
In CynMeith, small functions like this become move predicates and pieces of generators.
These three values matter a lot in the engine.
True: yes, this condition holdsFalse: no, it does notNone: there is nothing here
Typical CynMeith usage:
def handle(position):
piece = state.board.at(position)
if piece is None:
return # empty square — nothing to doboard.at(...) returns the piece at a position, or None when the square is
empty. Checking is None is the single most common line in rule code.
An if statement lets you make decisions. Most rule logic is a sequence of
small filters:
def can_strike(engine, state, origin, target, piece):
if not engine.rules.geometry.contains(target):
return False
victim = state.board.at(target)
if victim is None:
return False
if victim.side == piece.side:
return False
return TrueThis reads as:
- if the target is off the board, reject it
- if the target square is empty, reject it
- if the target holds a friend, reject it
- otherwise allow it
This style is common in CynMeith because it is easy to read and debug.
A for loop repeats something. In CynMeith, loops usually walk candidate
squares, and yield hands each legal move to the engine one at a time:
from cynmeith import Move
def guard_moves(engine, state, origin, piece):
for destination in engine.rules.geometry.neighbors(origin):
occupant = state.board.at(destination)
if occupant is not None and occupant.side == piece.side:
continue # skip squares held by friends
yield Move(origin, destination)A function that uses yield is called a generator. Read it as: "produce these
moves, one by one." You do not need to understand generators deeply — copying
this shape is enough.
Imports let you use code from elsewhere. Almost everything you need comes from one place:
from cynmeith import Coord, Move, PieceDef, Remove, leaper, rider, stepperRead it as "I need these tools from CynMeith."
A "side" in CynMeith is an index: 0 is the first player, 1 is the second.
So piece.side is 0 or 1, and comparisons are just:
if occupant.side != piece.side:
... # it's an enemyIn a FEN setup string, uppercase symbols belong to side 0 and lowercase to
side 1. You do not need anything fancier than that to start writing rules.
Positions on a square board are Coord(row, col) values:
Coord(2, 3) # row 2, column 3You can add them like vectors — origin + Coord(1, 0) is one row up. Useful
helpers include manhattan_to, chebyshev_to, is_orthogonal,
is_diagonal, and the direction constants ORTHOGONAL_DIRECTIONS,
DIAGONAL_DIRECTIONS, ALL_DIRECTIONS.
Hex boards use Hex(q, r) the same way, with HEX_DIRECTIONS for the six
neighbors. Your code mostly won't care which one it has, because it asks the
geometry.
When writing rules, these are the calls to know first:
state.board.at(position) # piece there, or None
engine.rules.geometry.contains(position) # on the board?
engine.rules.geometry.neighbors(position) # adjacent squares
engine.rules.geometry.line(position, step) # slide outward until the edge
engine.rules.geometry.distance(a, b) # steps between two squaresAnd on pieces:
piece.kind # "N", "S", ...
piece.side # 0 or 1
piece.get("has_moved") # a stored attribute, or NoneMost pieces need zero custom code — describe them with combinators:
from cynmeith import ORTHOGONAL_DIRECTIONS, PieceDef, rider, stepper
GUARD = PieceDef("G", "Guard", moves=stepper(*ORTHOGONAL_DIRECTIONS))
LANCE = PieceDef("L", "Lance", moves=rider(*ORTHOGONAL_DIRECTIONS))stepper(...): one step in each given directionrider(...): slide until something blocks; enemies are capturedleaper(...): fixed jumps (knights), given as offsets
They already handle bounds, friends, and captures.
Write a generator function. The guard_moves example above is the full
pattern: loop over candidate squares, filter, yield Move(...). The engine
passes your function four things:
engine— ask it for the geometry and rulesstate— the currentGameState;state.boardis the boardorigin— where the piece standspiece— thePieceStateitself
Plug it in exactly like a combinator:
GUARD = PieceDef("G", "Guard", moves=guard_moves)Effects describe extra consequences of a move — the engine applies them after the piece moves:
from cynmeith import Move, Remove
def sniper_shots(engine, state, origin, piece):
for target, victim in state.board.items():
if victim.side != piece.side and engine.rules.geometry.distance(origin, target) == 2:
yield Move.make(
origin,
origin, # the sniper stays put...
"shoot",
move_actor=False,
effects=(Remove(target),), # ...the victim is removed
)Built-in effects: Remove (captures), Relocate (move another piece),
Promote (replace a piece), Place (spawn a new one).
The board is immutable. You cannot do state.board[x] = ... — and you never
need to. Your job is to describe moves and effects; the engine applies them
and hands everyone a new state. This is also why undo "just works" in every
game you build.
If a piece needs memory (say, has_moved), store it as an attribute via the
after_move hook:
ROOK = PieceDef(
"R",
"Rook",
moves=rider(*ORTHOGONAL_DIRECTIONS),
after_move=lambda piece: piece.with_attr("has_moved", True),
)Good piece attributes: has_moved, remaining charges, a transformed flag.
Bad piece attributes: whose turn it is, the score, the phase — those belong to
turn/scoring/phase rules.
If you want to build confidence, try these in order:
- make a piece that steps one square orthogonally (
stepper) - make a piece that slides diagonally any distance (
rider) - make a piece that moves but never captures (
no_captures(...)) - write a generator for a piece that jumps to any empty corner of the board
- add a stationary "strike" move that removes an adjacent enemy (effects)
If you can do those five things, you are already beyond the beginner stage for this engine.
- Writing a generator for something a combinator already does. Check
leaper/rider/stepper/filteredfirst. - Trying to mutate the board. Describe changes with effects instead.
- Forgetting bounds checks in custom generators — use
geometry.contains(position). - Putting game-wide state on a piece. Turn, score, and phase live in rule components.
- Over-optimizing too early. A clear rule beats a clever rule while you are still discovering the game.
You do not need to master these before building a game:
- classes and inheritance (you mostly won't write any)
- advanced typing and generics
- decorators, metaclasses, async code
- packaging theory
Those are not the bottleneck for CynMeith work.
Once this guide feels comfortable, continue with: