J. Rogers, SE Ohio
Link to source code.
Abstract
We present a declarative, JSON-based game description language (GDL) for specifying turn-based board and card games. Unlike traditional game implementations that hardcode rules in imperative programming languages, our approach treats games as state machines where all rules emerge from state-dependent action validation. The system demonstrates that complex game mechanics—including dynamic rule changes, multi-step interactions, and conditional effects—can be expressed through a simple condition-effect paradigm evaluated against current game state.
1. Introduction
Traditional board game implementations suffer from tight coupling between game rules and implementation code. Adding a new game or variant requires modifying source code, and game logic becomes scattered across imperative functions. This makes games difficult to modify, test, and reason about formally.
We propose a state-driven game description language where:
- All game knowledge is encoded in declarative JSON specifications
- A generic interpreter evaluates rules without game-specific code
- Game state is the single source of truth for all decisions
- Rules emerge from validating actions against current state
1.1 Design Philosophy
The core principle is: Initial state + allowed actions → state changes → new rules
This creates a feedback loop where:
- State determines which actions are legal
- Actions modify state
- Modified state changes which actions become legal
- Complex behaviors emerge from simple primitives
2. Language Structure
A game specification consists of seven primary sections:
2.1 Metadata
Basic game information (name, description, version).
2.2 Players
Defines player roles and their attributes:
"players": {
"roles": [
{
"name": "White",
"attributes": {
"home_row": 0,
"pawn_direction": 1,
"promotion_row": 7
}
}
]
}
Player attributes enable parameterized rules that work symmetrically for all players despite different board positions or movement directions.
2.3 Entity Schemas
Defines game piece types and their attributes:
"entity_schemas": {
"types": {
"Piece": {
"attributes": {
"owner": { "type": "player_ref" },
"rank": { "type": "string", "default": "pawn" }
}
}
}
}
Entities are the atomic game objects (chess pieces, checkers pieces, cards, tokens).
2.4 Topology
Defines the game board structure:
"topology": {
"type": "discrete",
"structure": "grid(8, 8)"
}
Currently supports grid-based boards. Future extensions will include:
- Networks (for games like Risk)
- Tracks (for games like Monopoly)
- Zones (deck, hand, discard pile for card games)
2.5 State Schema
Defines global game state variables:
"state_schema": {
"global": {
"current_player": { "type": "player_ref", "initial": "player('White')" },
"turn_direction": { "type": "int", "initial": 1 }
}
}
State variables enable:
- Turn order tracking
- Phase management
- Dynamic rule modifications
- Game mode flags
2.6 Interactions
The heart of the system. Defines all possible actions:
"interactions": {
"list": {
"pawn_move": {
"conditions": [
"eq(entity.owner, state.current_player)",
"eq(entity.rank, 'pawn')",
"eq(board[target], null)",
"eq(sub(target.y, start.y), entity.owner.pawn_direction)"
],
"effects": [
"set(board[start], null)",
"set(board[target], entity)",
"set(entity.pos, target)"
]
}
}
}
Each interaction specifies:
- Conditions: Predicates evaluated against current state
- Effects: State modifications to apply
- Chainable: Whether action can be part of multi-step sequences
2.7 Game Flow
Defines turn structure and legal action sets:
"game_flow": {
"time_model": "turn_based",
"phases": {
"main_turn": {
"actors": "current_player",
"allowed_actions": ["pawn_move", "knight_move", "capture"]
}
}
}
3. Expression Language
The system uses a LISP-like functional expression language for conditions and effects:
3.1 Comparison Operations
eq(a, b)- equalityne(a, b)- inequalitygt(a, b),lt(a, b)- ordering
3.2 Logical Operations
and(a, b, ...)- conjunctionor(a, b, ...)- disjunctionnot(a)- negation
3.3 Arithmetic Operations
abs(x)- absolute valuesub(a, b)- subtractionmul(a, b, ...)- multiplication
3.4 Game-Specific Functions
mid_pos(start, target)- midpoint calculationpath_clear(start, target)- line-of-sight checkingother_player(p)- opponent reference
3.5 Property Access
Dot notation for nested properties:
entity.owner.directionentity.rankstate.current_player
3.6 Bracket Access
Array/dictionary indexing:
board[target]board[mid_pos(start, target)]
4. State-Driven Execution Model
4.1 Action Validation
When a player attempts an action:
- Parse Input: Convert user input to start/target positions
- Identify Entity: Retrieve entity at start position
- Find Valid Action: For each allowed action in current phase:
- Evaluate all conditions against current state
- First action with all conditions satisfied is selected
- Execute or Reject: Apply effects if valid, otherwise report error
4.2 Multi-Step Actions
Some games allow chaining actions (checkers jump sequences):
"man_jump": {
"chainable": true,
"conditions": [...],
"effects": [...]
}
The engine:
- Validates each segment of the path independently
- Applies effects cumulatively in simulation
- Commits all changes only if entire path is valid
- Requires all steps use chainable actions
4.3 Conditional Effects
Effects can be conditional:
"effects": [
"set(board[target], entity)",
"if(eq(target.y, entity.owner.promotion_row), set(entity.rank, 'queen'))"
]
This enables promotion, capture, and other context-dependent outcomes.
5. Key Design Patterns
5.1 Symmetric Rules via Player Attributes
Instead of separate rules for each player:
"conditions": [
"eq(sub(target.y, start.y), entity.owner.pawn_direction)"
]
White pawns move +1, Black pawns move -1, both using the same rule.
5.2 Dynamic Rule Changes
State variables control which actions are legal:
Uno Reverse Example:
"reverse_card": {
"effects": [
"set(state.turn_direction, mul(state.turn_direction, -1))"
]
}
Next player calculation now references state.turn_direction.
5.3 Phase-Based Constraints
Different action sets per phase:
"phases": {
"roll_phase": { "allowed_actions": ["roll_dice"] },
"move_phase": { "allowed_actions": ["move_piece", "pass"] },
"build_phase": { "allowed_actions": ["build", "end_turn"] }
}
5.4 Entity State as Rules
An entity's attributes determine its capabilities:
rank: 'man'→ can only move forwardrank: 'king'→ can move in any diagonal direction
The same piece type with different rank values has different legal moves.
6. Implementation Architecture
6.1 Components
GameState: Container for all mutable game data
- Players
- Entities
- Board (spatial mapping)
- Global state variables
- Topology metadata
ExpressionEvaluator: Pure functional expression interpreter
- Evaluates condition predicates
- Computes effect expressions
- No game-specific knowledge
- Context-based evaluation
GamePresenter: Orchestrates game execution
- Loads specifications
- Initializes state from setup
- Validates and executes actions
- Renders board state
- Manages turn flow
6.2 Separation of Concerns
The evaluator is deliberately isolated from game logic:
class ExpressionEvaluator:
"""Generic expression evaluator - knows NOTHING about game rules"""
def eval(self, expr, context):
# Parse and evaluate expression against context
# No chess, checkers, or game-specific code
This ensures:
- New games require zero code changes
- Game rules cannot "leak" into the engine
- Testing focuses on expression semantics, not game logic
7. Case Studies
7.1 Checkers
Demonstrates:
- Multi-step chainable jumps
- Promotion via conditional effects
- Mandatory capture rules (via action priority)
Key insight: Forced jumps are the only legal actions when jumps exist. This is expressed by making jump actions evaluate conditions first in the allowed actions list.
7.2 Chess
Demonstrates:
- Diverse piece movement patterns
- Path validation for sliding pieces
- Capture as move variant (same action, different target state)
- Pawn promotion
Key insight: Rooks, bishops, and queens all use the same path_clear() function. The difference in movement is purely in conditions, not in special-case code.
7.3 Potential Extensions
Uno (dynamic rules):
"state_schema": {
"global": {
"turn_direction": { "type": "int", "initial": 1 },
"draw_penalty": { "type": "int", "initial": 0 }
}
}
Monopoly (resource management):
"entity_schemas": {
"types": {
"Property": {
"attributes": {
"owner": { "type": "player_ref" },
"buildings": { "type": "int", "default": 0 }
}
}
}
}
8. Advantages and Limitations
8.1 Advantages
- Declarative Specifications: Game rules are data, not code
- Zero Code for New Games: Add games by writing JSON
- Formal Reasoning: Rules can be analyzed, verified, proven correct
- Variant Generation: Small JSON changes create game variants
- AI/Solver Ready: State space is explicit and queryable
- Tooling Potential: IDE support, rule validators, visualization
8.2 Current Limitations
- No Randomness: No dice, card shuffling, or probability
- No Hidden Information: All state is visible (no hands)
- Limited Topology: Only grids, no networks or zones
- No Resources: Can't track money, cards in hand, etc.
- Sequential Evaluation: Conditions evaluated in order, not optimized
- No Concurrency: Turn-based only, no real-time games
8.3 Future Extensions
Randomness:
"effects": [
"set(state.dice_roll, random_int(1, 6))"
]
Hidden Information:
"entity_schemas": {
"Card": {
"attributes": {
"visible_to": { "type": "player_ref_list" }
}
}
}
Zones:
"topology": {
"type": "zones",
"zones": {
"deck": { "type": "stack", "visible": false },
"hand_p1": { "type": "set", "owner": "player('P1')" }
}
}
9. Related Work
9.1 Game Description Languages
GDL (General Game Playing)
Developed for AI game-playing competitions. Uses logic programming (Datalog). Focus: automated reasoning and search.
Comparison: GDL emphasizes formal logic for AI. Our system prioritizes human readability and practical implementation.
Zillions of Games
Commercial system with custom scripting language. Focus: GUI and game variants.
Comparison: Proprietary, imperative scripting. Our system is open, declarative, and state-centric.
Ludii
Academic project using ludemes (game design patterns). Focus: game analysis and generation.
Comparison: Ludii has extensive game library and analysis tools. Our system emphasizes simplicity and extensibility for developers.
9.2 Rule Engines
Our approach resembles production rule systems (CLIPS, Drools):
- Condition evaluation against working memory (state)
- Action execution modifies working memory
- Forward chaining through state changes
Difference: We're specialized for turn-based games with spatial and ownership relationships.
10. Conclusion
We have presented a state-driven game description language that demonstrates:
- Declarative game specifications separate from implementation
- State as the single source of truth for all rule evaluation
- Complex behaviors emerge from simple condition-effect primitives
- Zero code changes required to add new games
- Dynamic rule modification through state variables
The system successfully implements chess and checkers with all standard rules, including promotion, multi-step moves, and diverse piece behaviors—all without game-specific code in the interpreter.
Future work will extend the language to support:
- Card games (randomness, hidden information, zones)
- Resource management (inventory, currency)
- Network topologies (territorial games)
- Simultaneous actions (real-time elements)
The fundamental insight is that treating games as state machines where rules are emergent properties of state validation creates a powerful, extensible framework for game implementation.
Appendix A: Complete Chess Specification
{
"metadata": {
"name": "Chess",
"description": "The classic game of chess",
"version": "1.0"
},
"players": {
"count": { "min": 2, "max": 2 },
"roles": [
{
"name": "White",
"attributes": {
"home_row": 0,
"pawn_direction": 1,
"promotion_row": 7
}
},
{
"name": "Black",
"attributes": {
"home_row": 7,
"pawn_direction": -1,
"promotion_row": 0
}
}
]
},
"entity_schemas": {
"types": {
"Piece": {
"attributes": {
"owner": { "type": "player_ref" },
"rank": { "type": "string", "default": "pawn" }
}
}
}
},
"topology": {
"type": "discrete",
"structure": "grid(8, 8)"
},
"state_schema": {
"global": {
"current_player": {
"type": "player_ref",
"initial": "player('White')"
}
}
},
"interactions": {
"list": {
"pawn_move": {
"conditions": [
"eq(entity.owner, state.current_player)",
"eq(entity.rank, 'pawn')",
"eq(board[target], null)",
"eq(target.x, start.x)",
"eq(sub(target.y, start.y), entity.owner.pawn_direction)"
],
"effects": [
"set(board[start], null)",
"set(board[target], entity)",
"set(entity.pos, target)",
"if(eq(target.y, entity.owner.promotion_row), set(entity.rank, 'queen'))"
]
},
"knight_move": {
"conditions": [
"eq(entity.owner, state.current_player)",
"eq(entity.rank, 'knight')",
"or(eq(board[target], null), ne(board[target].owner, entity.owner))",
"or(and(eq(abs(sub(target.x, start.x)), 2), eq(abs(sub(target.y, start.y)), 1)), and(eq(abs(sub(target.x, start.x)), 1), eq(abs(sub(target.y, start.y)), 2)))"
],
"effects": [
"if(ne(board[target], null), remove_entity(board[target]))",
"set(board[start], null)",
"set(board[target], entity)",
"set(entity.pos, target)"
]
}
}
},
"game_flow": {
"time_model": "turn_based",
"phases": {
"main_turn": {
"actors": "current_player",
"allowed_actions": ["pawn_move", "knight_move", "..."]
}
}
}
}
References
-
Genesereth, M., Love, N., & Pell, B. (2005). General game playing: Overview of the AAAI competition. AI Magazine, 26(2), 62-72.
-
Browne, C., et al. (2018). Ludii - The Ludemic General Game System. arXiv preprint arXiv:1905.05013.
-
Parlett, D. (1999). The Oxford History of Board Games. Oxford University Press.
-
Silver, D., et al. (2016). Mastering the game of Go with deep neural networks and tree search. Nature, 529(7587), 484-489.
-
Forgy, C. L. (1982). Rete: A fast algorithm for the many pattern/many object pattern match problem. Artificial Intelligence, 19(1), 17-37.
Notes:
Topology as Data, Not Code
Current (Grid):
"topology": {
"type": "discrete",
"structure": "grid(8, 8)"
}Hex Grid:
"topology": {
"type": "discrete",
"structure": "hex_grid(11, 11)",
"coordinate_system": "axial"
}Network (Risk):
"topology": {
"type": "network",
"nodes": {
"Alaska": {"continent": "North America"},
"Kamchatka": {"continent": "Asia"},
"Alberta": {"continent": "North America"}
},
"edges": [
{"from": "Alaska", "to": "Kamchatka"},
{"from": "Alaska", "to": "Alberta"},
{"from": "Alaska", "to": "Northwest Territory"}
]
}Now conditions can reference topology:
"conditions": [
"adjacent(start, target)",
"or(adjacent(start, target), special_connection(start, target))"
]State Visibility - The Critical Security Layer
This is essential for card games and is a separation of concerns problem:
State Schema with Visibility:
"state_schema": {
"global": {
"current_player": {...},
"deck": {"visibility": "none"},
"discard_pile": {"visibility": "all"}
},
"per_player": {
"hand": {"visibility": "owner_only"},
"played_cards": {"visibility": "all"},
"resources": {"visibility": "owner_only"}
}
}The Evaluator Must Respect Visibility:
class ExpressionEvaluator:
def __init__(self, game_state, viewing_player=None):
self.state = game_state
self.viewer = viewing_player # WHO is asking?
def eval(self, expr, context=None):
# When accessing state, filter by visibility
if accessing hidden state:
if not self.can_view(self.viewer, state_item):
return None # Or raise visibility error
```
### Network Architecture:
```
Server (authoritative state):
├── Full game state (all cards, all hands)
├── Rule validation (knows everything)
└── Sends filtered views to clients
Client A:
├── Receives state_view_for(player_A)
├── Sees: own hand, public info, opponent card backs
└── Sends moves to server
Client B:
├── Receives state_view_for(player_B)
├── Sees: own hand, public info, opponent card backs
└── Sends moves to serverThe Beautiful Part: Zero Code Changes
Your evaluator doesn't change. You just add:
1. Topology Types
class TopologyHandler:
def get_adjacent(self, node):
if self.topology['type'] == 'grid':
return grid_adjacent(node)
elif self.topology['type'] == 'network':
return self.topology['edges'][node]
elif self.topology['type'] == 'hex_grid':
return hex_adjacent(node)2. Visibility Filter
def filter_state_for_viewer(state, viewer):
"""Return state with hidden information removed"""
filtered = copy.deepcopy(state)
for entity in filtered.entities.values():
if entity.visibility == 'owner_only' and entity.owner != viewer:
entity.rank = 'hidden' # Show card back
entity.attributes = {} # Hide details
return filtered3. New Functions in Evaluator
elif func_name == 'adjacent':
return self.topology.is_adjacent(args[0], args[1])
elif func_name == 'special_connection':
return self.topology.has_special_edge(args[0], args[1])
elif func_name == 'visible_to':
entity, player = args
return self.check_visibility(entity, player)Example: Poker
{
"topology": {
"type": "zones",
"zones": {
"deck": {"type": "stack", "visibility": "none", "shuffle": true},
"community": {"type": "set", "visibility": "all"},
"hand_p1": {"type": "set", "visibility": "owner_only", "owner": "player('P1')"},
"hand_p2": {"type": "set", "visibility": "owner_only", "owner": "player('P2')"}
}
},
"interactions": {
"list": {
"deal": {
"conditions": [
"eq(state.phase, 'dealing')",
"gt(count(zone('deck')), 0)"
],
"effects": [
"move_entity(top(zone('deck')), zone('hand_p1'))",
"set(entity.visibility, 'owner_only')"
]
},
"bet": {
"conditions": [
"eq(entity.owner, state.current_player)",
"gte(player.chips, amount)"
],
"effects": [
"sub(player.chips, amount)",
"add(state.pot, amount)"
]
}
}
}
}Security Properties
Server knows truth:
server_state = {
"deck": [Card(rank="A", suit="♠"), ...],
"hand_p1": [Card(rank="K", suit="♥"), Card(rank="Q", suit="♥")],
"hand_p2": [Card(rank="2", suit="♣"), Card(rank="7", suit="♦")]
}Client A receives:
client_a_view = {
"deck": [{"count": 48, "back": "blue"}],
"hand_p1": [Card(rank="K", suit="♥"), Card(rank="Q", suit="♥")],
"hand_p2": [{"back": "blue"}, {"back": "blue"}]
}Client B receives:
client_b_view = {
"deck": [{"count": 48, "back": "blue"}],
"hand_p1": [{"back": "blue"}, {"back": "blue"}],
"hand_p2": [Card(rank="2", suit="♣"), Card(rank="7", suit="♦")]
}The Code Doesn't Leak
Because visibility is declarative:
"hand": {"visibility": "owner_only"}The filter function mechanically removes data before network transmission. There's no way for presentation code to "accidentally" show opponent cards - they literally aren't in the client's state object.
You're right: topology, zones, and visibility are the next separation layers needed. And just like with chess/checkers, they should be data-driven, not code.
No comments:
Post a Comment