Mastodon Politics, Power, and Science: A State-Driven Game Description Language for Turn-Based Games

Tuesday, October 14, 2025

A State-Driven Game Description Language for Turn-Based Games

 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:

  1. All game knowledge is encoded in declarative JSON specifications
  2. A generic interpreter evaluates rules without game-specific code
  3. Game state is the single source of truth for all decisions
  4. 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) - equality
  • ne(a, b) - inequality
  • gt(a, b), lt(a, b) - ordering

3.2 Logical Operations

  • and(a, b, ...) - conjunction
  • or(a, b, ...) - disjunction
  • not(a) - negation

3.3 Arithmetic Operations

  • abs(x) - absolute value
  • sub(a, b) - subtraction
  • mul(a, b, ...) - multiplication

3.4 Game-Specific Functions

  • mid_pos(start, target) - midpoint calculation
  • path_clear(start, target) - line-of-sight checking
  • other_player(p) - opponent reference

3.5 Property Access

Dot notation for nested properties:

  • entity.owner.direction
  • entity.rank
  • state.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:

  1. Parse Input: Convert user input to start/target positions
  2. Identify Entity: Retrieve entity at start position
  3. Find Valid Action: For each allowed action in current phase:
    • Evaluate all conditions against current state
    • First action with all conditions satisfied is selected
  4. 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:

  1. Validates each segment of the path independently
  2. Applies effects cumulatively in simulation
  3. Commits all changes only if entire path is valid
  4. 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 forward
  • rank: '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

  1. Declarative Specifications: Game rules are data, not code
  2. Zero Code for New Games: Add games by writing JSON
  3. Formal Reasoning: Rules can be analyzed, verified, proven correct
  4. Variant Generation: Small JSON changes create game variants
  5. AI/Solver Ready: State space is explicit and queryable
  6. Tooling Potential: IDE support, rule validators, visualization

8.2 Current Limitations

  1. No Randomness: No dice, card shuffling, or probability
  2. No Hidden Information: All state is visible (no hands)
  3. Limited Topology: Only grids, no networks or zones
  4. No Resources: Can't track money, cards in hand, etc.
  5. Sequential Evaluation: Conditions evaluated in order, not optimized
  6. 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:

  1. Declarative game specifications separate from implementation
  2. State as the single source of truth for all rule evaluation
  3. Complex behaviors emerge from simple condition-effect primitives
  4. Zero code changes required to add new games
  5. 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

  1. Genesereth, M., Love, N., & Pell, B. (2005). General game playing: Overview of the AAAI competition. AI Magazine, 26(2), 62-72.

  2. Browne, C., et al. (2018). Ludii - The Ludemic General Game System. arXiv preprint arXiv:1905.05013.

  3. Parlett, D. (1999). The Oxford History of Board Games. Oxford University Press.

  4. Silver, D., et al. (2016). Mastering the game of Go with deep neural networks and tree search. Nature, 529(7587), 484-489.

  5. 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):

json
"topology": {
  "type": "discrete",
  "structure": "grid(8, 8)"
}

Hex Grid:

json
"topology": {
  "type": "discrete",
  "structure": "hex_grid(11, 11)",
  "coordinate_system": "axial"
}

Network (Risk):

json
"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:

json
"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:

json
"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:

python
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 server

The Beautiful Part: Zero Code Changes

Your evaluator doesn't change. You just add:

1. Topology Types

python
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

python
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 filtered

3. New Functions in Evaluator

python
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

json
{
  "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:

python
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:

python
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:

python
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:

json
"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

Progress on the campaign manager

You can see that you can build tactical maps automatically from the world map data.  You can place roads, streams, buildings. The framework ...