Complete tic-tac-toe implementation using OttoChain script + state machine, demonstrating the oracle-centric architecture pattern.
- Overview
- Key Features Demonstrated
- Architecture
- Script Design
- State Machine Design
- Test Location
- Cell Numbering
This example demonstrates the oracle-centric architecture where:
- Script = Game engine (holds board, enforces rules, detects wins)
- State Machine = Lifecycle orchestrator (setup → playing → finished/cancelled)
The game provides a simple but complete example of:
- Stateful scripts maintaining game state
- State machines calling oracle methods via
_oracleCall - Guards checking oracle state for transitions
- Self-transitions for ongoing gameplay
- Multiple final states (finished vs cancelled)
- ✅ Deterministic rules: Oracle enforces valid moves, win detection
- ✅ Single source of truth: Board state in one place
- ✅ Atomic operations: makeMove updates board + checks winner in one call
- ✅ Simple state machine: Just lifecycle, no game logic
| Feature | Description |
|---|---|
| Script Pattern | Oracle holds all game state (board, players, history) |
| Oracle Method Dispatch | Single script with 6 methods: initialize, makeMove, checkWinner, getBoard, resetGame, cancelGame |
| Validation Logic | Oracle validates moves (turn, cell bounds, occupied check) |
| Win Detection | Deterministic check of all 8 winning patterns |
| Self-Transitions | State machine stays in playing during moves |
| Multiple Guards | Same event type (make_move) transitions to different states based on oracle status |
| Reset Support | Clear board without recreating oracle/machine |
| Structured Outputs | Emit game_completed output on win/draw |
┌──────────────────────────┐
│ State Machine │
│ (Lifecycle) │
│ │
│ setup → playing → │
│ ↓ ↑ │
│ finished/cancelled │
└──────────┬───────────────┘
│ _oracleCall
▼
┌──────────────────────────┐
│ Script │
│ (Game Engine) │
│ │
│ • Board [9 cells] │
│ • Turn (X/O) │
│ • Win detection │
│ • Move validation │
└──────────────────────────┘
┌───────┐
│ setup │ (initial state)
└───┬───┘
│ start_game
▼
┌─────────┐ make_move (self-transition)
│ playing │ ◄──────────┐
└─┬─┬─┬───┘ │
│ │ │ └──────────────┘
│ │ │
│ │ │ make_move (win/draw)
│ │ ├────────────► ┌──────────┐
│ │ │ │ finished │ (final)
│ │ │ └──────────┘
│ │ │
│ │ └─ reset_board (stays in playing)
│ │
│ └─ cancel_game
│ └────────────► ┌───────────┐
│ │ cancelled │ (final)
└────────────────────┘
{
"board": [null, null, "X", null, "O", null, null, null, null],
"currentTurn": "X",
"moveCount": 3,
"status": "InProgress",
"playerX": "DAG7L1...",
"playerO": "DAG3X2...",
"gameId": "550e8400-e29b-41d4-a716-446655440000",
"moveHistory": [
{"player": "X", "cell": 2, "moveNum": 1},
{"player": "O", "cell": 4, "moveNum": 2},
{"player": "X", "cell": 0, "moveNum": 3}
],
"winner": null,
"cancelledBy": null,
"cancelReason": null
}Sets up new game with two players.
Input:
{
"playerX": "DAG7L1...",
"playerO": "DAG3X2...",
"gameId": "550e8400-..."
}Output:
- Creates fresh board (all nulls)
- Sets status to "InProgress"
- Sets currentTurn to "X"
Validates and applies move, checks for win/draw.
Validations:
- Game status must be "InProgress"
- Correct player's turn
- Cell in bounds [0-8]
- Cell not occupied
Logic:
- Update
board[cell] = player - Increment
moveCount - Append to
moveHistory - Check for win condition (8 winning patterns)
- Check for draw condition (moveCount === 9)
- Toggle
currentTurnif game continues
Error Response (validation failure):
{
"_result": {
"valid": false,
"error": "Cell already occupied"
}
}Note: Returns _result only (no _state), so oracle state unchanged.
Returns current game status and winner (read-only).
Returns current board, turn, and move count (read-only).
Clears board for new round, keeps same players.
Logic:
- Clear board to all nulls
- Reset moveCount to 0
- Reset status to "InProgress"
- Reset currentTurn to "X"
- Clear moveHistory and winner
- Keep: playerX, playerO, gameId
Marks game as cancelled, preserves state for auditing.
A player wins if they occupy any of these 8 winning patterns:
Rows: Columns: Diagonals:
[0, 1, 2] [0, 3, 6] [0, 4, 8]
[3, 4, 5] [1, 4, 7] [2, 4, 6]
[6, 7, 8] [2, 5, 8]
Implementation Note: The win detection checks the board state AFTER the current move is applied by using conditional logic: "if this is the cell being played, use the player's mark; otherwise use the current board value".
Lifecycle Orchestrator: The state machine manages setup → playing → finished/cancelled transitions, while the oracle enforces game rules.
Why State Machine is Minimal:
- ✅ Oracle holds game state (board, moves, winner)
- ✅ State machine only tracks lifecycle phase
- ✅ Guards check oracle state for transitions
- ✅ Effects invoke oracle methods via
_oracleCall
- Initial state, waiting for players to be assigned
- Next States:
playing,cancelled
- Game is active, moves being made
- References oracle for querying game state
- Next States:
playing(self),finished,cancelled - Special: Self-transitions on both
make_moveandreset_board
- Game ended (won or draw)
- isFinal:
true - Contains winner and final board snapshot
- Game cancelled by user
- isFinal:
true - Contains cancellation reason and who cancelled
Event Payload:
{
"eventType": {"value": "start_game"},
"payload": {
"playerX": "DAG7L1...",
"playerO": "DAG3X2...",
"gameId": "550e8400-..."
}
}Effect: Calls oracle initialize method with player info.
Event Payload:
{
"eventType": {"value": "make_move"},
"payload": {
"player": "X",
"cell": 4
}
}Guard: Oracle status is "InProgress"
Effect: Calls oracle makeMove method, stays in playing state.
Same Event Type as transition #2, but different guard!
Guard: Oracle status is "Won" OR "Draw"
Effect:
- Captures final status, winner, and board from oracle state
- Emits structured output:
{
"_outputs": [{
"outputType": "game_completed",
"data": {
"gameId": "550e8400-...",
"winner": "X",
"status": "Won"
}
}]
}Guard: Oracle status is "Won" OR "Draw"
Effect:
- Increments state machine's
roundCount - Calls oracle
resetGamemethod - Stays in
playingstate for new round
Effect: Calls oracle cancelGame method with reason.
Every transition that reads oracle state must include the oracle CID in its dependencies array:
{
"from": {"value": "playing"},
"to": {"value": "playing"},
"eventType": {"value": "make_move"},
"dependencies": ["11111111-1111-1111-1111-111111111111"]
}This ensures the DeterministicEventProcessor loads oracle state before evaluating guards/effects.
The tic-tac-toe example is fully implemented and tested in the Scala test suite:
Test Suite:
modules/shared-data/src/test/scala/xyz/kd5ujc/shared_data/examples/TicTacToeGameSuite.scala
Test Resources:
- Script definition:
modules/shared-data/src/test/resources/tictactoe/script-definition.json - State machine definition:
modules/shared-data/src/test/resources/tictactoe/state-machine-definition.json
- Complete game flow - X wins: Play 5 moves, X wins with top row [0,1,2]
- Draw scenario: Play all 9 moves resulting in draw
- Invalid move rejected: Attempt to play occupied cell, verify rejection
- Reset and play another round: Complete game, reset board, play again
The test suite demonstrates:
- Creating oracle and state machine
- Starting game and making moves
- Oracle state updates and win detection
- Invalid move handling (event fails, state unchanged)
- Multi-round gameplay with reset
Board cells are indexed 0-8 in row-major order:
0 | 1 | 2
-----------
3 | 4 | 5
-----------
6 | 7 | 8
Possible extensions to explore:
- Stat tracking: Add
getStats()method for wins/losses/draws across rounds - Undo move: Add
undoLastMove()method using moveHistory - Timed moves: Add turn time limits with auto-forfeit
- Tournament mode: Chain multiple games with bracket progression
- AI opponent: Oracle method for computer player move selection
- State Machine Examples README - Overview of all examples
- Fuel Logistics - Multi-machine coordination example
- Clinical Trial - Complex multi-party workflows
- Test Resources - Actual JSON definitions
