diff --git a/2025-08-03.json b/2025-08-03.json new file mode 100644 index 0000000..d24cd08 --- /dev/null +++ b/2025-08-03.json @@ -0,0 +1,722 @@ +{ + "machine_file": "2025-08-03.json", + "rows": 3, + "steps": [ + { + "player": "W", + "moves": { + "WWW...BBB": [ + "a1-a2", + "b1-b2", + "c1-c2" + ] + } + }, + { + "player": "B", + "moves": { + ".WWW..BBB": [ + "b3-a2", + "b3-b2", + "c3-c2" + ], + "W.W.W.BBB": [ + "a3-b2", + "c3-b2", + "c3-c2" + ], + "WW...WBBB": [ + "a3-a2", + "b3-b2", + "b3-c2" + ] + } + }, + { + "player": "W", + "moves": { + ".WWB..B.B": [ + "b1-a2", + "b1-b2", + "c1-c2" + ], + ".WWW.BBB.": [ + "b1-b2", + "b1-c2", + "a2-b3" + ], + ".WWWB.B.B": [ + "c1-b2", + "c1-c2" + ], + "W.W.B..BB": [ + "a1-a2", + "a1-b2", + "c1-b2", + "c1-c2" + ], + "W.W.B.BB.": [ + "a1-a2", + "a1-b2", + "c1-b2", + "c1-c2" + ], + "W.W.WBBB.": [ + "a1-a2", + "b2-a3" + ], + "W.WBW..BB": [ + "c1-c2", + "b2-c3" + ], + "WW...BB.B": [ + "a1-a2", + "b1-b2", + "b1-c2" + ], + "WW..BWB.B": [ + "a1-a2", + "a1-b2" + ], + "WW.B.W.BB": [ + "b1-a2", + "b1-b2", + "c2-b3" + ] + } + }, + { + "player": "B", + "moves": { + "..W.W..BB": [ + "c3-b2", + "c3-c2" + ], + "..W.W.BB.": [ + "a3-a2", + "a3-b2" + ], + "..WBW.B.B": [ + "a2-a1", + "a3-b2", + "c3-b2", + "c3-c2" + ], + "..WW..B.B": [ + "c3-c2" + ], + "..WW.WBB.": [ + "b3-a2", + "b3-b2", + "b3-c2" + ], + "..WWB..BB": [ + "b2-b1", + "b2-c1", + "b3-a2", + "c3-c2" + ], + "..WWB.BB.": [ + "b2-b1", + "b2-c1", + "b3-a2" + ], + "..WWWBBB.": [ + "a3-b2" + ], + ".W..WWB.B": [ + "a3-a2", + "a3-b2", + "c3-b2" + ], + ".W.B.WB.B": [ + "a2-a1", + "a2-b1" + ], + ".W.W.BB.B": [ + "c2-b1", + "c2-c1" + ], + ".W.WW.B.B": [ + "a3-b2", + "c3-b2", + "c3-c2" + ], + ".WW..BBW.": [ + "c2-b1", + "a3-a2" + ], + "W....WB.B": [ + "a3-a2" + ], + "W...BW.BB": [ + "b2-a1", + "b2-b1", + "b3-c2" + ], + "W...BWBB.": [ + "b2-a1", + "b2-b1", + "a3-a2", + "b3-c2" + ], + "W...W..BB": [ + "c3-b2", + "c3-c2" + ], + "W...W.BB.": [ + "a3-a2", + "a3-b2" + ], + "W...WBB.B": [ + "c2-c1", + "a3-a2", + "a3-b2", + "c3-b2" + ], + "W..BWW.BB": [ + "c3-b2" + ], + "W..W.W.BB": [ + "b3-a2", + "b3-b2", + "b3-c2" + ], + "W.W..BWB.": [ + "b3-b2" + ], + "W.WB...BW": [ + "b3-b2" + ], + "WW.B...WB": [ + "a2-b1", + "c3-c2" + ] + } + }, + { + "player": "W", + "moves": { + "..BW...BB": [ + "a2-a3", + "a2-b3" + ], + "..BW..BB.": [ + "a2-b3" + ], + "..W.B..B.": [ + "c1-b2", + "c1-c2" + ], + "..WB.WB..": [ + "c2-c3" + ], + "..WBB...B": [ + "c1-b2", + "c1-c2" + ], + "..WBB.B..": [ + "c1-b2", + "c1-c2" + ], + "..WBW..B.": [ + "c1-c2" + ], + "..WBWBB..": [ + "b2-a3", + "b2-b3" + ], + "..WWBB.B.": [ + "c1-b2", + "a2-a3", + "a2-b3" + ], + "..WWBWB..": [ + "c1-b2", + "c2-c3" + ], + ".BW...BW.": [ + "c1-c2" + ], + ".BWW...BB": [ + "c1-c2", + "a2-a3", + "a2-b3" + ], + ".BWW..BB.": [ + "c1-c2", + "a2-b3" + ], + ".W..BWB..": [ + "c2-c3" + ], + ".W.BWW..B": [ + "b1-a2", + "b2-b3", + "b2-c3" + ], + ".W.WB...B": [ + "a2-a3" + ], + ".W.WWBB..": [ + "b1-c2", + "b2-a3", + "b2-b3" + ], + ".WBW..B.B": [ + "b1-b2" + ], + "B....W.BB": [ + "c2-b3" + ], + "B....WBB.": [ + "c2-b3", + "c2-c3" + ], + "B.W.W.B.B": [ + "c1-c2", + "b2-a3", + "b2-b3", + "b2-c3" + ], + "BW...WB.B": [ + "b1-b2" + ], + "W...B..B.": [ + "a1-a2", + "a1-b2" + ], + "W...BB..B": [ + "a1-a2", + "a1-b2" + ], + "W...BBB..": [ + "a1-a2", + "a1-b2" + ], + "W...WB.B.": [ + "a1-a2" + ], + "W..BBW.B.": [ + "a1-b2", + "c2-b3", + "c2-c3" + ], + "W..BWB..B": [ + "b2-b3", + "b2-c3" + ], + "W..W.B..B": [ + "a2-a3" + ], + "W..WBW..B": [ + "a1-b2", + "a2-a3" + ], + "W.B.W.B.B": [ + "a1-a2", + "b2-a3", + "b2-b3", + "b2-c3" + ], + "WB.....WB": [ + "a1-a2" + ], + "WB...W.BB": [ + "a1-a2", + "c2-b3" + ], + "WB...WBB.": [ + "a1-a2", + "c2-b3", + "c2-c3" + ] + } + }, + { + "player": "B", + "moves": { + "....BW.B.": [ + "b2-b1", + "b3-c2" + ], + "....WB..B": [ + "c2-c1", + "c3-b2" + ], + "....WBB..": [ + "c2-c1", + "a3-a2", + "a3-b2" + ], + "...BBW..B": [ + "a2-a1", + "b2-b1" + ], + "...BBWB..": [ + "a2-a1", + "b2-b1" + ], + "...BW...B": [ + "a2-a1", + "c3-b2", + "c3-c2" + ], + "...BW.B..": [ + "a2-a1", + "a3-b2" + ], + "...BWW.B.": [ + "a2-a1", + "b3-c2" + ], + "...WB..B.": [ + "b2-b1", + "b3-a2" + ], + "...WBB..B": [ + "b2-b1", + "c2-c1" + ], + "...WBBB..": [ + "b2-b1", + "c2-c1" + ], + "...WWB.B.": [ + "c2-c1", + "b3-a2" + ], + "...WWW..B": [ + "c3-b2" + ], + "...WWWB..": [ + "a3-b2" + ], + "..B....WB": [ + "c3-c2" + ], + "..B...BW.": [ + "a3-a2" + ], + "..B...WBB": [ + "b3-b2", + "c3-c2" + ], + "..W.BB.W.": [ + "b2-b1", + "b2-c1" + ], + "..W.BBWB.": [ + "b2-b1", + "b2-c1" + ], + "..WB..B.W": [ + "a2-a1" + ], + "..WB.BBW.": [ + "a2-a1" + ], + "..WB.BW..": [ + "a2-a1" + ], + "..WWB.B.W": [ + "b2-b1", + "b2-c1" + ], + ".BW...WBB": [ + "b3-b2", + "c3-c2" + ], + ".W..B.B.W": [ + "a3-a2" + ], + ".W..B.W.B": [ + "c3-c2" + ], + ".W.B.W..W": [ + "a2-a1", + "a2-b1" + ], + ".W.B.W.WB": [ + "a2-a1", + "a2-b1" + ], + ".W.W.BBW.": [ + "c2-b1", + "c2-c1" + ], + ".W.W.BW..": [ + "c2-b1", + "c2-c1" + ], + "W....BW.B": [ + "c2-c1" + ], + "W...BWW.B": [ + "b2-a1", + "b2-b1" + ], + "W..B.B..W": [ + "c2-c1" + ], + "W..B.B.WB": [ + "c2-c1" + ], + "W..BB..BW": [ + "b2-a1", + "b2-b1" + ], + "W..BB..W.": [ + "b2-a1", + "b2-b1" + ], + "W.B...BWB": [ + "a3-a2", + "c3-c2" + ], + "W.B...W.B": [ + "c3-c2" + ] + } + }, + { + "player": "W", + "moves": { + "...BWB...": [ + "b2-b3" + ], + "...WBW...": [ + "a2-a3", + "c2-c3" + ], + "..B.W...B": [ + "b2-b3", + "b2-c3" + ], + "..B.W.B..": [ + "b2-a3", + "b2-b3" + ], + "..BWB...B": [ + "a2-a3" + ], + "..BWW..B.": [ + "a2-a3", + "a2-b3" + ], + ".B...W.B.": [ + "c2-b3", + "c2-c3" + ], + ".B.B.WB..": [ + "c2-c3" + ], + ".B.W...B.": [ + "a2-a3", + "a2-b3" + ], + ".B.W.B..B": [ + "a2-a3" + ], + ".BWW..B.W": [ + "c1-c2" + ], + "B...BWB..": [ + "c2-c3" + ], + "B...W...B": [ + "b2-b3", + "b2-c3" + ], + "B...W.B..": [ + "b2-a3", + "b2-b3" + ], + "B...WW.B.": [ + "c2-b3", + "c2-c3" + ], + "B.W...B.W": [ + "c1-c2" + ], + "BW...W..W": [ + "b1-b2" + ], + "BW...W.WB": [ + "b1-b2" + ] + } + }, + { + "player": "B", + "moves": { + "....BWW..": [ + "b2-b1" + ], + "...B.B.W.": [ + "a2-a1", + "c2-c1" + ], + "...WB...W": [ + "b2-b1" + ], + "..B....WB": [ + "c3-c2" + ], + "..B...BW.": [ + "a3-a2" + ], + "..B.B.W.B": [ + "b2-b1", + "c3-c2" + ], + ".B....WB.": [ + "b3-b2" + ], + ".B...BW.B": [ + "c2-c1" + ] + } + }, + { + "player": "W", + "moves": { + ".B.W....W": [ + "a2-a3" + ] + } + }, + { + "player": "B", + "moves": {} + } + ], + "games_played": [ + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-a2" + }, + { + "board_str": "W.WBW..BB", + "move_str": "c1-c2" + }, + { + "board_str": "W..BWW.BB", + "move_str": "b3-c2" + }, + { + "board_str": "W..BWB..B", + "move_str": "b2-c3" + } + ], + "winner": "W" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-c2" + }, + { + "board_str": "W...BWBB.", + "move_str": "b3-c2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-a2" + }, + { + "board_str": "...WBBB..", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-c2" + }, + { + "board_str": "W.W.WBBB.", + "move_str": "a1-a2" + }, + { + "board_str": "..WWWBBB.", + "move_str": "b3-a2" + }, + { + "board_str": "..WBWBB..", + "move_str": "b2-b3" + } + ], + "winner": "W" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-a2" + }, + { + "board_str": "W.WBW..BB", + "move_str": "b2-c3" + } + ], + "winner": "W" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "a2-a1" + } + ], + "winner": "B" + } + ], + "Logger": {} +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2d24860 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development commands + +### Running the application +```bash +go run main.go # Run with default settings (3x3 board, 2 players, 20 games) +go run main.go -r 5 -p 1 -g 10 # Run with 5x5 board, 1 human player, 10 games +go run main.go --filename custom_machine.json # Use custom machine learning file +go run main.go --logfile custom_log.json # Use custom log file +``` + +### Visualization modes +```bash +go run main.go -g 10 -p 0 -v # Text summary with statistics (works everywhere) +go run main.go -g 5 -p 0 -i # Interactive TUI with auto-play (requires compatible terminal) +go run main.go -g 1 -p 0 -R # Step-by-step game replay +go run main.go -g 3 -p 0 -v -R # Combined text summary and replay +``` + +**Important for TUI mode (`-i`)**: Must be run as compiled binary directly in terminal (Terminal.app, iTerm2, Ghostty), NOT through IDEs or Claude Code: +```bash +go build +./hexapawn-go -g 5 -p 0 -i +``` + +### Testing +```bash +go test ./... # Run all tests +go test ./hexapawn # Run tests for hexapawn package only +go test -v ./hexapawn # Run tests with verbose output +go test -run TestNewBoard ./hexapawn # Run specific test +``` + +### Building +```bash +go build # Build executable in current directory +go build -o hexapawn-game # Build with custom executable name +``` + +### Code quality +```bash +go fmt ./... # Format all Go code +go vet ./... # Run Go static analysis +go mod tidy # Clean up module dependencies +``` + +## Architecture overview + +### Core components + +**Game engine (`hexapawn/hexapawn.go`)** +- `Board`: Represents the game board with NxN grid (default 3x3) +- `Game`: Manages game state, player turns, and win conditions +- `Position`: Represents board state with available moves for machine learning +- Move validation and execution system + +**Machine learning (`hexapawn/machine.go`)** +- `Machine`: Implements reinforcement learning that improves by removing losing moves +- `Step`: Represents decision points in the game tree with available moves per board state +- Training system that learns from losses by eliminating bad moves from future games +- Persistence to JSON files for learning continuity + +**Game types (`hexapawn/types.go`)** +- Core data structures for board representation, moves, and game state +- JSON serialization support for machine learning persistence +- Error handling types in `hexapawn/errors.go` + +**Visualization system (`hexapawn/ui.go`)** +- `GameViewer`: Interactive TUI using Bubbletea framework +- `ShowGamesText()`: Text-based summary with statistics +- `ShowGameReplay()`: Step-by-step game replay functionality +- TTY compatibility checking for robust cross-environment support + +### Game mechanics + +**Board representation**: String format like "WWW...BBB" where W=white pawns, B=black pawns, .=empty +**Move notation**: Algebraic notation like "a1-b2" (from square to square) +**Win conditions**: +1. Advance pawn to opposite end +2. Capture all enemy pieces +3. Block opponent from moving + +### Machine learning approach + +The AI uses a simplified reinforcement learning approach: +1. Generates all possible game positions and moves during initialization +2. Plays games by randomly selecting from available moves +3. After losing, removes the losing move from that board position +4. Over time, eliminates bad moves to improve play quality + +### File structure + +- `main.go`: CLI interface and application entry point +- `hexapawn/`: Core game logic package + - `hexapawn.go`: Game engine and board logic + - `machine.go`: Machine learning implementation + - `types.go`: Core data structures + - `ui.go`: Visualization and TUI components + - `conversions.go`: String/board conversion utilities + - `errors.go`: Custom error types +- `machine.json`: Default machine learning state persistence +- `hexapawn_log.json`: Game logging in JSON format +- `notes.md`: Development notes and game analysis +- `SESSION_SUMMARY.md`: Implementation session documentation + +## Visualization features + +The project includes multiple visualization modes to demonstrate machine learning progress: + +### Interactive TUI (`-i` flag) +- Full-screen terminal interface with Bubbletea framework +- Real-time game replay with auto-play functionality +- Controls: ←→ (moves), ↑↓ (games), Space (play/pause), r (reset), e (end), q (quit) +- Requires direct terminal execution (not through IDEs) + +### Text summary (`-v` flag) +- Statistics showing win percentages and learning progress +- Recent games with move sequences and final board states +- Works in all environments including IDEs and scripts + +### Step-by-step replay (`-R` flag) +- Manual advancement through moves of the most recent game +- Interactive demonstration of auto-play functionality +- Fallback when TUI is not available + +## Dependencies + +- `github.com/spf13/pflag`: Command-line flag parsing +- `github.com/charmbracelet/bubbletea`: TUI framework +- `github.com/charmbracelet/lipgloss`: Terminal styling \ No newline at end of file diff --git a/SESSION_SUMMARY.md b/SESSION_SUMMARY.md new file mode 100644 index 0000000..f45241c --- /dev/null +++ b/SESSION_SUMMARY.md @@ -0,0 +1,177 @@ +# Hexapawn Visualization Implementation Session Summary + +## Overview +This session focused on implementing user-facing visualization features for the hexapawn-go machine learning game project. The work involved creating both text-based and interactive Terminal User Interface (TUI) visualizations to help users understand the machine learning progress and replay games. + +## Issues Addressed + +### 1. Missing Win Statistics Display ✅ SOLVED +**Problem**: Win statistics were being displayed at the top of the output but getting scrolled out of view in the terminal. + +**Root Cause**: Statistics were shown before the game details, causing them to scroll off-screen with long output. + +**Solution**: Moved statistics to the end of the output with enhanced formatting: +``` +=== OVERALL STATISTICS === +Total games: 260 +White wins: 21 (8.1%) +Black wins: 239 (91.9%) +Machine learning progress: Black is winning 91.9% of games! +``` + +### 2. Non-Functional Space Key Auto-Play ✅ SOLVED +**Problem**: Space key wasn't working to toggle play/pause in the interactive TUI mode. + +**Root Cause**: TTY (terminal device) access limitations in certain execution environments prevented Bubbletea TUI framework from initializing properly. + +**Debugging Process**: +- Initial assumption: Key handling logic was incorrect +- Discovered: TUI framework couldn't access `/dev/tty` in some environments +- Key finding: `open /dev/tty: device not configured` error +- Solution: Added TTY availability checking and clear user guidance + +**Final Solution**: +- Added proper TTY compatibility checking +- Implemented graceful fallback to text mode when TTY unavailable +- Fixed key handling to use `" "` for space key detection +- Added clear instructions for users on how to run in compatible environments + +## Implementation Details + +### New Features Added + +#### 1. Interactive TUI Game Viewer (`-i` flag) +- **Full-screen terminal interface** with styled board display +- **Navigation controls**: + - `←→` (h/l): Navigate through moves + - `↑↓` (k/j): Switch between games + - `Space`: Toggle auto-play (plays moves every 1.2 seconds) + - `r`: Reset to start of game + - `e`: Jump to end of game + - `q`: Quit +- **Visual board representation** with colored pieces +- **Real-time game state display** showing current game, move, and play status + +#### 2. Enhanced Text Visualization (`-v` flag) +- **Statistics at bottom** for better visibility +- **Recent games summary** (last 5 games with details) +- **Final board states** for each recent game +- **Machine learning progress indicator** + +#### 3. Step-by-Step Replay (`-R` flag) +- **Interactive replay** of the most recent game +- **Manual advancement** through moves (press Enter) +- **Move-by-move board display** with player indicators +- **Fallback demonstration** of auto-play functionality + +### Technical Architecture + +#### TUI Framework +- **Bubbletea v1.3.6**: Modern Go TUI framework based on The Elm Architecture +- **Lipgloss**: Styling and layout for attractive terminal display +- **Event-driven model**: Update/View pattern with command handling + +#### Key Components +- `GameViewer`: Main TUI model managing game state and display +- `ShowGamesText()`: Text-based fallback with statistics +- `ShowGameReplay()`: Interactive step-by-step replay +- `RunGameViewerTUI()`: TUI initialization with compatibility checking + +### Environment Compatibility + +#### Working Environments ✅ +- **macOS**: Terminal.app, iTerm2, Ghostty with direct execution +- **Linux**: Most standard terminal emulators +- **Direct binary execution**: `./hexapawn-go -i` + +#### Non-Compatible Environments ❌ +- **IDE integrated terminals** (limited TTY access) +- **Script execution contexts** +- **CI/CD environments** +- **Containerized environments** without TTY +- **Claude Code execution context** + +## Debugging Insights + +### Key Discoveries +1. **TTY Access is Critical**: Bubbletea requires direct TTY access, not available in all environments +2. **Environment Detection**: Added `/dev/tty` accessibility check for better user experience +3. **Graceful Degradation**: Multiple fallback modes ensure functionality across environments +4. **Key Handling**: Space key detection required specific string matching approach + +### Error Patterns Identified +- `could not open a new TTY: open /dev/tty: device not configured` +- `all TUI modes failed` → indicates environment limitation, not code bug + +## Command Line Interface + +### New Flags Added +- `-v, --visualize`: Show text summary with statistics +- `-i, --interactive`: Launch TUI game viewer (TTY required) +- `-R, --replay`: Step-by-step replay of most recent game + +### Usage Examples +```bash +# Text summary (works everywhere) +./hexapawn-go -g 10 -p 0 -v + +# Interactive TUI (compatible terminals) +./hexapawn-go -g 5 -p 0 -i + +# Step-by-step replay +./hexapawn-go -g 1 -p 0 -R + +# Combined modes +./hexapawn-go -g 3 -p 0 -v -R +``` + +## Machine Learning Visualization Impact + +The implementation successfully demonstrates Martin Gardner's original matchbox learning concept: +- **Clear learning progression**: Black win percentage increases over time +- **Visual game evolution**: Users can see how move choices improve +- **Educational value**: Step-by-step replay helps understand strategy development + +### Typical Results Observed +- **Initial games**: More balanced outcomes +- **After training**: Black consistently wins 90%+ of games +- **Learning evidence**: Fewer available moves for White over time + +## Files Modified + +### Core Implementation +- `hexapawn/ui.go`: New TUI implementation with Bubbletea +- `main.go`: Added visualization flags and integration +- `go.mod`: Added Bubbletea and Lipgloss dependencies + +### Documentation +- `CLAUDE.md`: Updated with visualization commands +- `SESSION_SUMMARY.md`: This comprehensive summary + +## Future Enhancements Identified + +### Potential Improvements +1. **Move analysis**: Highlight losing moves that get removed +2. **Learning metrics**: Track win rate progression over time +3. **Export functionality**: Save games to different formats +4. **Multiple difficulty levels**: Different AI strategies +5. **Web interface**: Browser-based visualization for broader accessibility + +### Technical Debt +- TUI initialization could be more robust across different terminal types +- Error handling could provide more specific troubleshooting guidance + +## Testing Outcomes + +### Successful Validation +✅ **Statistics display**: Clearly visible at output end +✅ **Auto-play functionality**: Space key toggles play/pause correctly +✅ **Cross-platform compatibility**: Works in iTerm2 and Ghostty on macOS +✅ **Fallback behavior**: Graceful degradation to text mode when needed +✅ **Educational value**: Learning progression clearly demonstrated + +## Conclusion + +The visualization implementation successfully transforms the command-line hexapawn game into an engaging, educational tool that clearly demonstrates machine learning principles. The multi-modal approach (TUI, text, replay) ensures accessibility across different environments while maintaining the educational integrity of Martin Gardner's original concept. + +The debugging process revealed important insights about terminal compatibility and TUI framework limitations, leading to a robust solution with proper environment detection and graceful fallbacks. \ No newline at end of file diff --git a/docs/tasks/implement-visualization-system.md b/docs/tasks/implement-visualization-system.md new file mode 100644 index 0000000..2f0e2cc --- /dev/null +++ b/docs/tasks/implement-visualization-system.md @@ -0,0 +1,200 @@ +# Task: Implement Comprehensive Visualization System for Hexapawn Machine Learning Game + +## Objective +Add user-facing visualization features to the hexapawn-go machine learning game to help users understand the learning progress and replay games. The goal is to create multiple visualization modes that work across different environments while preserving the original Martin Gardner machine learning algorithm. + +## Background +The current hexapawn-go project implements Martin Gardner's matchbox learning machine algorithm where an AI learns by eliminating losing moves over time. The project needs visualization features to make the machine learning progress visible and engaging for educational purposes. + +## Requirements + +### 1. Text-Based Visualization Mode (`-v` flag) +**Purpose**: Provide comprehensive game statistics and recent game summaries that work in all environments. + +**Implementation Requirements**: +- Add `--visualize` / `-v` command-line flag +- Display overall statistics at the END of output (not beginning) to ensure visibility +- Show total games played, win percentages for both players +- Display recent games (last 5) with move sequences and final board states +- Include a "machine learning progress" message highlighting the learning effect +- Ensure output is properly formatted and easy to read + +**Expected Output Format**: +``` +=== HEXAPAWN GAMES SUMMARY === +Total games played: 250 + +Recent games (last 5): +[detailed game information with moves and final boards] + +=== OVERALL STATISTICS === +Total games: 250 +White wins: 20 (8.0%) +Black wins: 230 (92.0%) + +Machine learning progress: Black is winning 92.0% of games! +``` + +### 2. Interactive TUI Mode (`-i` flag) +**Purpose**: Create an interactive terminal user interface with auto-play functionality for real-time game viewing. + +**Technical Requirements**: +- Use Bubbletea framework (github.com/charmbracelet/bubbletea) for TUI +- Use Lipgloss (github.com/charmbracelet/lipgloss) for styling +- Implement TTY compatibility checking with clear error messages +- Provide graceful fallback to text mode when TUI unavailable + +**Functionality Requirements**: +- Navigate between games and moves with arrow keys +- **Space key auto-play**: Toggle between PAUSED and PLAYING modes +- When PLAYING: automatically advance through moves every 1.2 seconds +- Display current game number, move number, winner, and play status +- Show styled game board with colored pieces (White/Black pawns) +- Controls: ←→ (moves), ↑↓ (games), Space (play/pause), r (reset), e (end), q (quit) + +**Environment Compatibility**: +- Must work in direct terminal execution (Terminal.app, iTerm2, Ghostty) +- Should detect TTY availability and provide clear instructions when unavailable +- Should NOT work through IDEs or script execution contexts (by design) + +### 3. Step-by-Step Replay Mode (`-R` flag) +**Purpose**: Provide manual game replay functionality that works everywhere as a fallback demonstration. + +**Implementation Requirements**: +- Show initial board position +- Allow manual advancement through moves with Enter key +- Display each move with player indication and resulting board state +- Show game completion message with winner +- Work in all environments including those without TTY access + +### 4. Command-Line Integration +**Flag Specifications**: +- `-v, --visualize`: Text summary mode +- `-i, --interactive`: TUI mode with auto-play +- `-R, --replay`: Step-by-step replay mode +- Flags should be combinable (e.g., `-v -R` for both modes) + +**Help Documentation**: +- Update `--help` output with clear descriptions +- Include compatibility notes for TUI mode + +## Technical Implementation Details + +### Dependencies to Add +```bash +go get github.com/charmbracelet/bubbletea +go get github.com/charmbracelet/lipgloss +``` + +### Key Components to Implement + +#### 1. TUI Game Viewer (`hexapawn/ui.go`) +```go +type GameViewer struct { + games []GamePlayed + currentGame int + currentMove int + playing bool + // ... other fields +} +``` + +**Critical Implementation Notes**: +- Space key detection: Use `case " ":` in key switch statement +- Auto-play timing: Use `tea.Tick(time.Millisecond*1200, ...)` for consistent timing +- Board update logic: Start from initial board and apply moves sequentially (avoid double-applying moves) +- TTY checking: Use `os.OpenFile("/dev/tty", os.O_RDWR, 0)` to verify availability + +#### 2. Statistics Display Function +- Must place statistics at END of output for visibility +- Calculate win percentages with proper formatting +- Show learning progress indication + +#### 3. Replay Functionality +- Manual step-through with user input +- Clear move annotations with player indicators +- Board state display after each move + +### Error Handling and Fallbacks + +#### TTY Compatibility Issues +**Problem**: TUI frameworks require direct TTY access +**Solution**: Implement checking with clear user guidance: +``` +❌ TTY not available: open /dev/tty: device not configured + +🔧 To use interactive mode: + 1. Compile: go build + 2. Run directly in terminal: ./hexapawn-go -g 5 -p 0 -i + 3. NOT through IDE, Claude Code, or scripts +``` + +#### Environment Detection +- Check TTY availability before attempting TUI initialization +- Provide specific instructions for different environments +- Graceful fallback with explanation when TUI fails + +## Debugging Considerations + +### Common Issues to Anticipate +1. **Space key not working**: Usually due to TTY access problems, not key handling +2. **Statistics not visible**: Ensure they appear at END of output +3. **TUI not starting**: Environment doesn't provide TTY access +4. **Auto-play not advancing**: Check tick message handling and move update logic + +### Testing Strategy +- Test in multiple terminal environments (Terminal.app, iTerm2, third-party terminals) +- Verify graceful fallback behavior in non-compatible environments +- Test all key combinations and navigation +- Validate statistics accuracy and visibility + +## Success Criteria + +### Functional Requirements ✅ +- [ ] Text mode (`-v`) shows statistics at bottom of output +- [ ] TUI mode (`-i`) launches with proper TTY checking +- [ ] Space key toggles auto-play between PAUSED/PLAYING states +- [ ] Auto-play advances moves automatically every 1.2 seconds +- [ ] All navigation controls work (arrows, r, e, q) +- [ ] Replay mode (`-R`) works in all environments +- [ ] Graceful fallback when TUI unavailable + +### Educational Value ✅ +- [ ] Machine learning progress clearly visible (Black win percentage increasing) +- [ ] Game replay demonstrates strategy evolution +- [ ] Statistics show learning effect over time +- [ ] User can observe how AI eliminates losing moves + +### Cross-Platform Compatibility ✅ +- [ ] Works on macOS (Terminal.app, iTerm2, Ghostty) +- [ ] Provides clear error messages and instructions +- [ ] Fallback modes ensure functionality everywhere +- [ ] No breaking changes to existing functionality + +## Deliverables + +### Code Files +- `hexapawn/ui.go`: Complete TUI implementation +- `main.go`: Updated with new flags and integration +- `go.mod`: Updated dependencies + +### Documentation +- Updated `CLAUDE.md` with visualization commands +- Session summary documenting implementation details +- Clear usage examples and troubleshooting guide + +## Additional Context + +### Project Philosophy +- Preserve Martin Gardner's original learning algorithm unchanged +- Focus on educational value and learning demonstration +- Ensure accessibility across different environments +- Maintain clean, educational code that others can learn from + +### User Experience Goals +- Make machine learning progress immediately visible +- Provide engaging way to watch AI learn over time +- Ensure tool works for students, educators, and enthusiasts +- Create "wow factor" when users see Black's win rate climb to 90%+ + +This task represents a complete enhancement of the hexapawn project from a basic CLI tool to a comprehensive educational visualization system that effectively demonstrates machine learning principles through Martin Gardner's elegant matchbox algorithm. \ No newline at end of file diff --git a/go.mod b/go.mod index 67e61c9..bc1e5f6 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,30 @@ module github.com/pavelanni/hexapawn-go -go 1.22.3 +go 1.23.0 + +toolchain go1.24.5 require github.com/spf13/pflag v1.0.5 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum index 287f6fa..6ff197a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,45 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/hexapawn-go b/hexapawn-go new file mode 100755 index 0000000..946c2f9 Binary files /dev/null and b/hexapawn-go differ diff --git a/hexapawn/conversions.go b/hexapawn/conversions.go index 710ea86..a915344 100644 --- a/hexapawn/conversions.go +++ b/hexapawn/conversions.go @@ -8,6 +8,8 @@ import ( "strings" ) +const letters = "abcdefghijklmnopqrstuvwxyz" + // String is a stringer for Move func (m Move) String() string { return fmt.Sprintf("%s%d-%s%d", letters[m.FromCol:m.FromCol+1], m.FromRow+1, letters[m.ToCol:m.ToCol+1], m.ToRow+1) diff --git a/hexapawn/errors.go b/hexapawn/errors.go new file mode 100644 index 0000000..b5223b1 --- /dev/null +++ b/hexapawn/errors.go @@ -0,0 +1,39 @@ +// Package hexapawn provides the core game logic and machine learning capabilities +// for the Hexapawn game. +package hexapawn + +import "fmt" + +// GameError represents a game-specific error +type GameError struct { + Code ErrorCode + Message string +} + +func (e *GameError) Error() string { + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +// ErrorCode represents different types of game errors +type ErrorCode string + +const ( + // ErrInvalidMove indicates an illegal move attempt + ErrInvalidMove ErrorCode = "INVALID_MOVE" + // ErrInvalidBoard indicates an invalid board configuration + ErrInvalidBoard ErrorCode = "INVALID_BOARD" + // ErrInvalidPlayer indicates an invalid player number or type + ErrInvalidPlayer ErrorCode = "INVALID_PLAYER" + // ErrGameOver indicates an attempt to make a move in a finished game + ErrGameOver ErrorCode = "GAME_OVER" + // ErrMachineLearning indicates an error in the machine learning process + ErrMachineLearning ErrorCode = "MACHINE_LEARNING_ERROR" +) + +// NewGameError creates a new GameError with the given code and message +func NewGameError(code ErrorCode, message string) *GameError { + return &GameError{ + Code: code, + Message: message, + } +} diff --git a/hexapawn/hexapawn.go b/hexapawn/hexapawn.go index 2e544e5..f4d1dec 100644 --- a/hexapawn/hexapawn.go +++ b/hexapawn/hexapawn.go @@ -7,89 +7,120 @@ import ( ) const ( - letters = "abcdefghijklmnopqrstuvwxyz" + minBoardRows = 3 + maxBoardRows = 9 + minNumPlayers = 0 + maxNumPlayers = 2 + EmptyCell = '.' + WhitePlayer = 'W' + BlackPlayer = 'B' ) // Initialize a new board func NewBoard(boardRows int) *Board { b := &Board{ - Rows: boardRows, - Cols: boardRows, - Grid: make([][]string, boardRows), + Rows: boardRows, + Cols: boardRows, + Grid: make([][]string, boardRows), + State: GameInProgress, } for row := 0; row < b.Rows; row++ { b.Grid[row] = make([]string, b.Cols) for col := 0; col < b.Cols; col++ { - b.Grid[row][col] = "." // fill empty cells with dots + b.Grid[row][col] = string(EmptyCell) } } - // Place white pawns (W) at the bottom row + // Place white pawns at the bottom row for col := 0; col < b.Cols; col++ { - b.Grid[0][col] = "W" + b.Grid[0][col] = string(WhitePlayer) } - // Place black pawns (B) at the top row + // Place black pawns at the top row for col := 0; col < b.Cols; col++ { - b.Grid[b.Rows-1][col] = "B" + b.Grid[b.Rows-1][col] = string(BlackPlayer) } return b } // Initialize a new game func NewGame(boardRows, numPlayers int) (*Game, error) { - // check if the board dimensions are valid - if boardRows < 3 || boardRows > 9 { - return nil, fmt.Errorf("Invalid board dimensions. Rows and Cols must be equal and at least 3, at most 9.") + if boardRows < minBoardRows || boardRows > maxBoardRows { + return nil, NewGameError(ErrInvalidBoard, "board dimensions must be between 3 and 9") } - if numPlayers < 0 || numPlayers > 2 { - return nil, fmt.Errorf("Invalid number of players. Must be 0, 1, or 2.") + if numPlayers < minNumPlayers || numPlayers > maxNumPlayers { + return nil, NewGameError(ErrInvalidPlayer, "number of players must be 0, 1, or 2") } + g := &Game{ - NumPlayers: numPlayers, - Board: NewBoard(boardRows), + NumPlayers: numPlayers, + Board: NewBoard(boardRows), + CurrentPlayer: string(WhitePlayer), } return g, nil } // Check if a move is valid -func (b *Board) IsValidMove(ms string, player string) bool { +func (b *Board) IsValidMove(ms string, player string) error { + if b.State != GameInProgress { + return NewGameError(ErrGameOver, "game is already over") + } + m, err := b.MoveFromString(ms) if err != nil { - return false + return NewGameError(ErrInvalidMove, err.Error()) } - // Check if move's from position is in bounds - if m.FromRow < 0 || m.FromRow >= b.Rows || m.FromCol < 0 || m.FromCol >= b.Cols { - return false - } - // Check if move's to position is in bounds - if m.ToRow < 0 || m.ToRow >= b.Rows || m.ToCol < 0 || m.ToCol >= b.Cols { - return false + + // Check if move's positions are in bounds + if !b.isInBounds(m.FromRow, m.FromCol) || !b.isInBounds(m.ToRow, m.ToCol) { + return NewGameError(ErrInvalidMove, "move is out of bounds") } - // Check if we move our piece + + // Check if we're moving our own piece if b.Grid[m.FromRow][m.FromCol] != player { - return false - } - // Check if we move for one row only - if player == "W" && m.ToRow != m.FromRow+1 { - return false + return NewGameError(ErrInvalidMove, "cannot move opponent's piece") } - if player == "B" && m.ToRow != m.FromRow-1 { - return false + + // Check movement direction based on player + if player == string(WhitePlayer) && m.ToRow != m.FromRow+1 { + return NewGameError(ErrInvalidMove, "white can only move forward") } - // We can't move non-vertically on an empty space - if m.ToCol != m.FromCol && b.Grid[m.ToRow][m.ToCol] == "." { - return false + if player == string(BlackPlayer) && m.ToRow != m.FromRow-1 { + return NewGameError(ErrInvalidMove, "black can only move forward") } - // Check if we move to an empty position or it's a capture move - if b.Grid[m.ToRow][m.ToCol] != "." { - // check if the move is diagonal - if m.ToCol != m.FromCol+1 && m.ToCol != m.FromCol-1 { - return false + + // Check diagonal capture and forward movement rules + targetCell := b.Grid[m.ToRow][m.ToCol] + if m.ToCol != m.FromCol { + // Diagonal move must be a capture + if targetCell == string(EmptyCell) { + return NewGameError(ErrInvalidMove, "diagonal moves must capture") } - if b.Grid[m.ToRow][m.ToCol] == player { // we can't capture our own piece - return false + if abs(m.ToCol-m.FromCol) != 1 { + return NewGameError(ErrInvalidMove, "can only move diagonally one space") + } + if targetCell == player { + return NewGameError(ErrInvalidMove, "cannot capture own piece") + } + } else { + // Forward move must be to empty cell + if targetCell != string(EmptyCell) { + return NewGameError(ErrInvalidMove, "forward moves must be to empty space") } } - return true + + return nil +} + +// isInBounds checks if the given coordinates are within the board boundaries +func (b *Board) isInBounds(row, col int) bool { + return row >= 0 && row < b.Rows && col >= 0 && col < b.Cols +} + +// abs returns the absolute value of an integer +func abs(x int) int { + if x < 0 { + return -x + } + return x } // Apply a move to the board @@ -99,25 +130,78 @@ func (b *Board) ApplyMove(ms string) { log.Fatal(err) } b.Grid[m.ToRow][m.ToCol] = b.Grid[m.FromRow][m.FromCol] - b.Grid[m.FromRow][m.FromCol] = "." + b.Grid[m.FromRow][m.FromCol] = string(EmptyCell) } // Check for a win condition func (b *Board) CheckWin() string { for col := 0; col < b.Cols; col++ { - if b.Grid[0][col] == "B" { - return "B" + if b.Grid[0][col] == string(BlackPlayer) { + return string(BlackPlayer) } - if b.Grid[b.Rows-1][col] == "W" { - return "W" + if b.Grid[b.Rows-1][col] == string(WhitePlayer) { + return string(WhitePlayer) } } return "" } +// GenerateAvailableMoves generates all valid moves for the current position +func (p *Position) GenerateAvailableMoves() ([]Move, error) { + if p.Board == nil { + return nil, NewGameError(ErrInvalidBoard, "board is nil") + } + + var moves []Move + isWhite := p.Player == string(WhitePlayer) + + // For each square on the board + for row := 0; row < p.Board.Rows; row++ { + for col := 0; col < p.Board.Cols; col++ { + // If we find our piece + if p.Board.Grid[row][col] == p.Player { + // Forward move + newRow := row + 1 + if !isWhite { + newRow = row - 1 + } + + // Check forward move + if p.Board.isInBounds(newRow, col) && + p.Board.Grid[newRow][col] == string(EmptyCell) { + moves = append(moves, Move{ + FromRow: row, + FromCol: col, + ToRow: newRow, + ToCol: col, + }) + } + + // Check diagonal captures + for _, colOffset := range []int{-1, 1} { + newCol := col + colOffset + if p.Board.isInBounds(newRow, newCol) { + targetCell := p.Board.Grid[newRow][newCol] + if targetCell != string(EmptyCell) && targetCell != p.Player { + moves = append(moves, Move{ + FromRow: row, + FromCol: col, + ToRow: newRow, + ToCol: newCol, + }) + } + } + } + } + } + } + + return moves, nil +} + // Play the game func (g *Game) Play() { - currentPlayer := "W" + currentPlayer := string(WhitePlayer) winner := "" var move string var stepNumber int @@ -126,10 +210,10 @@ func (g *Game) Play() { fmt.Printf("Step %d\n", stepNumber+1) moves := g.Steps[stepNumber].Moves[g.Board.String()] if len(moves) == 0 { - if currentPlayer == "W" { - winner = "B" + if currentPlayer == string(WhitePlayer) { + winner = string(BlackPlayer) } else { - winner = "W" + winner = string(WhitePlayer) } fmt.Printf("Player %s has no moves; player %s wins!\n", currentPlayer, winner) break @@ -139,7 +223,7 @@ func (g *Game) Play() { fmt.Printf("Player %s, enter your move: ", currentPlayer) fmt.Scan(&move) case 1: - if currentPlayer == "W" { + if currentPlayer == string(WhitePlayer) { fmt.Print("Player W, enter your move: ") fmt.Scan(&move) } else { @@ -152,25 +236,27 @@ func (g *Game) Play() { continue } - if g.Board.IsValidMove(move, currentPlayer) { - fmt.Printf("Player %s moves %s\n", currentPlayer, move) - g.MovesPlayed = append(g.MovesPlayed, BoardMove{ - BoardStr: g.Board.String(), - MoveStr: move}) - g.Board.ApplyMove(move) - g.Board.Print() - winner = g.Board.CheckWin() - if winner != "" { - fmt.Printf("Player %s wins!\n", winner) - break - } - if currentPlayer == "W" { - currentPlayer = "B" - } else { - currentPlayer = "W" - } + err := g.Board.IsValidMove(move, currentPlayer) + if err != nil { + fmt.Println(err) + continue + } + + fmt.Printf("Player %s moves %s\n", currentPlayer, move) + g.MovesPlayed = append(g.MovesPlayed, BoardMove{ + BoardStr: g.Board.String(), + MoveStr: move}) + g.Board.ApplyMove(move) + g.Board.Print() + winner = g.Board.CheckWin() + if winner != "" { + fmt.Printf("Player %s wins!\n", winner) + break + } + if currentPlayer == string(WhitePlayer) { + currentPlayer = string(BlackPlayer) } else { - fmt.Println("Invalid move, try again.") + currentPlayer = string(WhitePlayer) } stepNumber++ if stepNumber >= len(g.Steps) { diff --git a/hexapawn/hexapawn_test.go b/hexapawn/hexapawn_test.go index b6e98a5..53bf19c 100644 --- a/hexapawn/hexapawn_test.go +++ b/hexapawn/hexapawn_test.go @@ -55,51 +55,146 @@ func TestNewGame(t *testing.T) { } func TestNewMove(t *testing.T) { - b := &Board{Cols: 8, Rows: 8} - - // Test valid move - move, err := b.MoveFromString("a1-b2") - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - expectedMove := Move{0, 0, 1, 1} - if move != expectedMove { - t.Errorf("Expected move %v, got %v", expectedMove, move) - } - - // Test invalid move string length - _, err = b.MoveFromString("a1-b2-c3") - if err == nil { - t.Errorf("Expected error for invalid move string length, got nil") - } - - // Test invalid move string format - _, err = b.MoveFromString("a1-b2-c3") - if err == nil { - t.Errorf("Expected error for invalid move string format, got nil") + tests := []struct { + name string + moveStr string + board *Board + expected Move + expectError bool + }{ + { + name: "Valid move", + moveStr: "a1-b2", + board: &Board{Cols: 8, Rows: 8}, + expected: Move{FromRow: 0, FromCol: 0, ToRow: 1, ToCol: 1}, + expectError: false, + }, + { + name: "Invalid move string length", + moveStr: "a1-b2-c3", + board: &Board{Cols: 8, Rows: 8}, + expectError: true, + }, + { + name: "Invalid from column", + moveStr: "i1-b2", + board: &Board{Cols: 8, Rows: 8}, + expectError: true, + }, + { + name: "Invalid from row", + moveStr: "a9-b2", + board: &Board{Cols: 8, Rows: 8}, + expectError: true, + }, + { + name: "Invalid to column", + moveStr: "a1-i2", + board: &Board{Cols: 8, Rows: 8}, + expectError: true, + }, + { + name: "Invalid to row", + moveStr: "a1-b9", + board: &Board{Cols: 8, Rows: 8}, + expectError: true, + }, } - // Test invalid fromCol - _, err = b.MoveFromString("i1-b2") - if err == nil { - t.Errorf("Expected error for invalid fromCol, got nil") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + move, err := tt.board.MoveFromString(tt.moveStr) + if tt.expectError { + if err == nil { + t.Errorf("Expected error for %s, but got none", tt.name) + } + } else { + if err != nil { + t.Errorf("Unexpected error for %s: %v", tt.name, err) + } + if move != tt.expected { + t.Errorf("Expected move %v, got %v", tt.expected, move) + } + } + }) } +} - // Test invalid fromRow - _, err = b.MoveFromString("a9-b2") - if err == nil { - t.Errorf("Expected error for invalid fromRow, got nil") - } +func TestPosition(t *testing.T) { + board := BoardFromString("WWW...BBB") - // Test invalid toCol - _, err = b.MoveFromString("a1-i2") - if err == nil { - t.Errorf("Expected error for invalid toCol, got nil") + tests := []struct { + name string + board *Board + player string + expectedMoves []Move + expectError bool + }{ + { + name: "Initial position white", + board: board, + player: "W", + expectedMoves: []Move{ + {FromRow: 0, FromCol: 0, ToRow: 1, ToCol: 0}, + {FromRow: 0, FromCol: 1, ToRow: 1, ToCol: 1}, + {FromRow: 0, FromCol: 2, ToRow: 1, ToCol: 2}, + }, + expectError: false, + }, + { + name: "Initial position black", + board: board, + player: "B", + expectedMoves: []Move{ + {FromRow: 2, FromCol: 0, ToRow: 1, ToCol: 0}, + {FromRow: 2, FromCol: 1, ToRow: 1, ToCol: 1}, + {FromRow: 2, FromCol: 2, ToRow: 1, ToCol: 2}, + }, + expectError: false, + }, + { + name: "Nil board", + board: nil, + player: "W", + expectError: true, + }, + { + name: "Position with capture moves", + board: BoardFromString("W.W.B.B.B"), + player: "W", + expectedMoves: []Move{ + {FromRow: 0, FromCol: 0, ToRow: 1, ToCol: 1}, // Diagonal capture + {FromRow: 0, FromCol: 2, ToRow: 1, ToCol: 1}, // Diagonal capture + }, + expectError: false, + }, } - // Test invalid toRow - _, err = b.MoveFromString("a1-b9") - if err == nil { - t.Errorf("Expected error for invalid toRow, got nil") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pos := &Position{ + Board: tt.board, + Player: tt.player, + } + moves, err := pos.GenerateAvailableMoves() + if tt.expectError { + if err == nil { + t.Errorf("Expected error for %s, but got none", tt.name) + } + } else { + if err != nil { + t.Errorf("Unexpected error for %s: %v", tt.name, err) + } + if len(moves) != len(tt.expectedMoves) { + t.Errorf("Expected %d moves, got %d", len(tt.expectedMoves), len(moves)) + } + // Compare moves + for i, move := range moves { + if move != tt.expectedMoves[i] { + t.Errorf("Move %d: expected %v, got %v", i, tt.expectedMoves[i], move) + } + } + } + }) } } diff --git a/hexapawn/machine.go b/hexapawn/machine.go index d34d158..3995fb8 100644 --- a/hexapawn/machine.go +++ b/hexapawn/machine.go @@ -8,55 +8,80 @@ import ( "slices" ) +// NewMachine creates a new machine learning instance func NewMachine() *Machine { - return &Machine{} + return &Machine{ + Steps: make([]Step, 0), + GamesPlayed: make([]GamePlayed, 0), + } } +// Play executes the specified number of games func (m *Machine) Play(numGames int) error { if m.Logger == nil { - return fmt.Errorf("slog.Logger is nil") + return NewGameError(ErrMachineLearning, "logger is not initialized") } + for i := 0; i < numGames; i++ { - fmt.Printf("Game %d/%d\n", i+1, numGames) + m.Logger.Info("starting game", slog.Int("game", i+1), slog.Int("total", numGames)) + g, err := NewGame(m.NumRows, 0) if err != nil { - panic(err) + return fmt.Errorf("failed to create game: %w", err) } + g.Steps = m.Steps g.Play() + m.GamesPlayed = append(m.GamesPlayed, GamePlayed{ MovesPlayed: g.MovesPlayed, Winner: g.Winner, }) - m.Logger.Info("game result", slog.Int("game", i+1), slog.String("winner", g.Winner)) - err = m.Train("B") - if err != nil { - return err + + m.Logger.Info("game completed", + slog.Int("game", i+1), + slog.String("winner", g.Winner), + slog.Int("moves", len(g.MovesPlayed))) + + if err := m.Train(string(BlackPlayer)); err != nil { + return fmt.Errorf("training failed: %w", err) } } return nil } +// Train updates the machine learning model based on game results func (m *Machine) Train(player string) error { lastGame := m.GamesPlayed[len(m.GamesPlayed)-1] if lastGame.Winner == player { - return nil // no need to train - } - lastMove := lastGame.MovesPlayed[len(lastGame.MovesPlayed)-2] // we need not the last move, but the move before the last - fmt.Printf("lastMove: %v\n", lastMove) - // remove the last move from the steps - lastStep := len(lastGame.MovesPlayed) - 2 // we need not the last step, but the step before the last because the last was the winning move - fmt.Printf("lastStep: %d\n", lastStep) // index of the last step - fmt.Printf("m.Steps[lastStep]: %v\n", m.Steps[lastStep]) - fmt.Printf("m.Steps[lastStep].Moves[lastMove.BoardStr]: %v\n", m.Steps[lastStep].Moves[lastMove.BoardStr]) - lastMoveIndex := slices.Index(m.Steps[lastStep].Moves[lastMove.BoardStr], lastMove.MoveStr) // index of the last (bad) move + return nil // no need to train on winning games + } + + if len(lastGame.MovesPlayed) < 2 { + return NewGameError(ErrMachineLearning, "insufficient moves for training") + } + + // Get the losing move (second to last move) + lastMove := lastGame.MovesPlayed[len(lastGame.MovesPlayed)-2] + lastStep := len(lastGame.MovesPlayed) - 2 + + m.Logger.Debug("training on losing move", + slog.String("board", lastMove.BoardStr), + slog.String("move", lastMove.MoveStr)) + + // Remove the losing move from available moves + moves, exists := m.Steps[lastStep].Moves[lastMove.BoardStr] + if !exists { + return NewGameError(ErrMachineLearning, "board state not found in training data") + } + + lastMoveIndex := slices.Index(moves, lastMove.MoveStr) if lastMoveIndex == -1 { - return fmt.Errorf("last move not found in steps") + return NewGameError(ErrMachineLearning, "move not found in training data") } - fmt.Printf("lastMoveIndex: %d\n", lastMoveIndex) - m.Steps[lastStep].Moves[lastMove.BoardStr] = slices.Delete(m.Steps[lastStep].Moves[lastMove.BoardStr], lastMoveIndex, lastMoveIndex+1) - return m.Save() + m.Steps[lastStep].Moves[lastMove.BoardStr] = slices.Delete(moves, lastMoveIndex, lastMoveIndex+1) + return m.Save() } func (m *Machine) Load() error { @@ -162,8 +187,10 @@ func (b *Board) ValidMoves(player string) []string { for row := 0; row < b.Rows; row++ { for col := 0; col < b.Cols; col++ { m := Move{FromRow: piece.Row, FromCol: piece.Col, ToRow: row, ToCol: col} - if b.IsValidMove(m.String(), player) { + if err := b.IsValidMove(m.String(), player); err == nil { moves = append(moves, m.String()) + } else { + continue } } } diff --git a/hexapawn/types.go b/hexapawn/types.go index b3d9d1d..f841355 100644 --- a/hexapawn/types.go +++ b/hexapawn/types.go @@ -1,14 +1,38 @@ +// Package hexapawn implements the core game logic for Hexapawn, a simplified chess variant +// played with only pawns. The package provides: +// - Board representation and game state management +// - Move validation and execution +// - Machine learning capabilities for AI players +// - Game state persistence package hexapawn import ( "log/slog" ) +// Player represents a game player +type Player string + +// GameState represents the current state of the game +type GameState string + +const ( + // GameInProgress indicates the game is still being played + GameInProgress GameState = "IN_PROGRESS" + // GameWonByWhite indicates white player has won + GameWonByWhite GameState = "WHITE_WON" + // GameWonByBlack indicates black player has won + GameWonByBlack GameState = "BLACK_WON" + // GameDrawn indicates the game ended in a draw + GameDrawn GameState = "DRAWN" +) + // Define the Board struct type Board struct { - Rows int `json:"rows"` - Cols int `json:"cols"` - Grid [][]string `json:"grid"` + Rows int `json:"rows"` + Cols int `json:"cols"` + Grid [][]string `json:"grid"` + State GameState `json:"stat"` } type BoardMove struct { @@ -17,11 +41,12 @@ type BoardMove struct { } type Game struct { - NumPlayers int `json:"num_players"` - Board *Board `json:"board"` - Steps []Step `json:"steps"` - MovesPlayed []BoardMove `json:"moves_played"` - Winner string `json:"winner"` + NumPlayers int `json:"num_players"` + Board *Board `json:"board"` + Steps []Step `json:"steps"` + MovesPlayed []BoardMove `json:"moves_played"` + CurrentPlayer string `json:"current_player"` + Winner string `json:"winner"` } type GamePlayed struct { @@ -51,6 +76,13 @@ type Move struct { ToCol int } +// Position represents a game position with a board state, current player, and available moves +type Position struct { + Board *Board `json:"board"` + Player string `json:"player"` + AvailableMoves []Move `json:"available_moves"` +} + type Step struct { Player string `json:"player"` // current player Moves map[string][]string `json:"moves"` // each position is a string like "BBB...WWW"; for each position, there is a list of possible moves diff --git a/hexapawn/ui.go b/hexapawn/ui.go new file mode 100644 index 0000000..a601240 --- /dev/null +++ b/hexapawn/ui.go @@ -0,0 +1,391 @@ +package hexapawn + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// UI colors and styles +var ( + boardStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("240")). + Padding(1) + + whiteStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("15")). + Background(lipgloss.Color("240")). + Bold(true) + + blackStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("0")). + Background(lipgloss.Color("252")). + Bold(true) + + emptyStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("240")) + + headerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("205")). + Bold(true). + Align(lipgloss.Center) + + moveStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("86")). + Bold(true) + + statusStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("214")). + Italic(true) +) + +// GameViewer represents the TUI model for viewing games +type GameViewer struct { + games []GamePlayed + currentGame int + currentMove int + board *Board + width int + height int + playing bool + speed time.Duration +} + +type tickMsg time.Time + +func tick() tea.Cmd { + return tea.Tick(time.Millisecond*1200, func(t time.Time) tea.Msg { + return tickMsg(t) + }) +} + +// NewGameViewer creates a new game viewer with the provided games +func NewGameViewer(games []GamePlayed, boardSize int) *GameViewer { + return &GameViewer{ + games: games, + currentGame: 0, + currentMove: -1, // Start before first move to show initial board + board: NewBoard(boardSize), + speed: time.Millisecond * 800, + } +} + +func (gv GameViewer) Init() tea.Cmd { + return nil +} + +func (gv GameViewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + gv.width = msg.Width + gv.height = msg.Height + return gv, nil + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return gv, tea.Quit + + case "left", "h": + if gv.currentMove > -1 { + gv.currentMove-- + gv.updateBoard() + } + return gv, nil + + case "right", "l": + if gv.currentGame < len(gv.games) && gv.currentMove < len(gv.games[gv.currentGame].MovesPlayed)-1 { + gv.currentMove++ + gv.updateBoard() + } + return gv, nil + + case "up", "k": + if gv.currentGame > 0 { + gv.currentGame-- + gv.currentMove = -1 + gv.updateBoard() + } + return gv, nil + + case "down", "j": + if gv.currentGame < len(gv.games)-1 { + gv.currentGame++ + gv.currentMove = -1 + gv.updateBoard() + } + return gv, nil + + case " ": + // Space key - toggle play/pause + gv.playing = !gv.playing + if gv.playing { + return gv, tick() + } + return gv, nil + + case "r": + gv.currentMove = -1 + gv.updateBoard() + return gv, nil + + case "e": + if gv.currentGame < len(gv.games) { + gv.currentMove = len(gv.games[gv.currentGame].MovesPlayed) - 1 + gv.updateBoard() + } + return gv, nil + } + + case tickMsg: + if gv.playing { + if gv.currentGame < len(gv.games) && gv.currentMove < len(gv.games[gv.currentGame].MovesPlayed)-1 { + gv.currentMove++ + gv.updateBoard() + return gv, tick() + } else { + gv.playing = false + } + } + return gv, nil + } + + return gv, nil +} + +func (gv *GameViewer) updateBoard() { + // Reset board to initial state + gv.board = NewBoard(gv.board.Rows) + + // Apply moves up to current position + if gv.currentGame < len(gv.games) && gv.currentMove >= 0 { + game := gv.games[gv.currentGame] + for i := 0; i <= gv.currentMove && i < len(game.MovesPlayed); i++ { + move := game.MovesPlayed[i] + // Apply the move to our board + gv.board.ApplyMove(move.MoveStr) + } + } +} + +func (gv GameViewer) View() string { + if len(gv.games) == 0 { + return "No games to display. Press 'q' to quit." + } + + var sections []string + + // Header + header := headerStyle.Render(fmt.Sprintf("Hexapawn Game Viewer - Game %d/%d", gv.currentGame+1, len(gv.games))) + sections = append(sections, header) + + // Game info + if gv.currentGame < len(gv.games) { + game := gv.games[gv.currentGame] + moveDisplay := "Start" + if gv.currentMove >= 0 { + moveDisplay = fmt.Sprintf("%d/%d", gv.currentMove+1, len(game.MovesPlayed)) + } else { + moveDisplay = fmt.Sprintf("0/%d", len(game.MovesPlayed)) + } + gameInfo := fmt.Sprintf("Winner: %s | Move: %s", game.Winner, moveDisplay) + if gv.playing { + gameInfo += " | ▶ PLAYING (auto-advance every 1.2s)" + } else { + gameInfo += " | ⏸ PAUSED (press SPACE to play)" + } + sections = append(sections, statusStyle.Render(gameInfo)) + } + + // Board + boardDisplay := gv.renderBoard() + sections = append(sections, boardStyle.Render(boardDisplay)) + + // Current move info + if gv.currentGame < len(gv.games) && gv.currentMove >= 0 && gv.currentMove < len(gv.games[gv.currentGame].MovesPlayed) { + move := gv.games[gv.currentGame].MovesPlayed[gv.currentMove] + moveInfo := fmt.Sprintf("Current move: %s", moveStyle.Render(move.MoveStr)) + sections = append(sections, moveInfo) + } + + // Controls + controls := `Controls: + ← → (h/l): Previous/Next move ↑ ↓ (k/j): Previous/Next game + Space: Play/Pause r: Reset to start e: End of game + q: Quit` + sections = append(sections, controls) + + return lipgloss.JoinVertical(lipgloss.Left, sections...) +} + +func (gv GameViewer) renderBoard() string { + var lines []string + + // Render from top to bottom (reverse row order for display) + for i := gv.board.Rows - 1; i >= 0; i-- { + line := fmt.Sprintf("%2d │ ", i+1) + for j := 0; j < gv.board.Cols; j++ { + cell := gv.board.Grid[i][j] + var styled string + switch cell { + case "W": + styled = whiteStyle.Render(" W ") + case "B": + styled = blackStyle.Render(" B ") + default: + styled = emptyStyle.Render(" · ") + } + line += styled + " " + } + lines = append(lines, line) + } + + // Add column labels + colLine := " └─" + for i := 0; i < gv.board.Cols; i++ { + colLine += "────" + } + lines = append(lines, colLine) + + labelLine := " " + for i := 0; i < gv.board.Cols; i++ { + labelLine += fmt.Sprintf(" %c ", 'a'+i) + } + lines = append(lines, labelLine) + + return strings.Join(lines, "\n") +} + +// RunGameViewerTUI starts the interactive TUI game viewer +func RunGameViewerTUI(games []GamePlayed, boardSize int) error { + if len(games) == 0 { + return fmt.Errorf("no games to display") + } + + // Check TTY availability first + fmt.Printf("Checking terminal compatibility...\n") + if _, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err != nil { + fmt.Printf("❌ TTY not available: %v\n", err) + fmt.Printf("\n🔧 To use interactive mode:\n") + fmt.Printf(" 1. Compile: go build\n") + fmt.Printf(" 2. Run directly in terminal: ./hexapawn-go -g 5 -p 0 -i\n") + fmt.Printf(" 3. NOT through IDE, Claude Code, or scripts\n\n") + return ShowGamesText(games, boardSize) + } + + fmt.Printf("✅ TTY available, starting TUI...\n") + + viewer := NewGameViewer(games, boardSize) + + fmt.Printf("Controls: ←→ (moves), ↑↓ (games), SPACE (play/pause), r (reset), e (end), q (quit)\n\n") + + // Try without alt screen first for better macOS compatibility + p := tea.NewProgram(viewer) + _, err := p.Run() + if err != nil { + fmt.Printf("TUI failed: %v\n", err) + return ShowGamesText(games, boardSize) + } + return err +} + +// ShowGamesText provides a simple text-based game viewer fallback +func ShowGamesText(games []GamePlayed, boardSize int) error { + fmt.Printf("\n=== HEXAPAWN GAMES SUMMARY ===\n") + fmt.Printf("Total games played: %d\n\n", len(games)) + + // Show last 5 games in detail first + startIdx := len(games) - 5 + if startIdx < 0 { + startIdx = 0 + } + + fmt.Printf("Recent games (last %d):\n", len(games)-startIdx) + for i := startIdx; i < len(games); i++ { + game := games[i] + fmt.Printf("\nGame %d: Winner = %s\n", i+1, game.Winner) + fmt.Printf("Moves: ") + for j, move := range game.MovesPlayed { + if j > 0 { + fmt.Print(" -> ") + } + fmt.Print(move.MoveStr) + } + fmt.Printf("\n") + + // Show final board state + if len(game.MovesPlayed) > 0 { + lastMove := game.MovesPlayed[len(game.MovesPlayed)-1] + finalBoard := BoardFromString(lastMove.BoardStr) + finalBoard.ApplyMove(lastMove.MoveStr) + fmt.Printf("Final board:\n") + finalBoard.Print() + } + } + + // Show statistics at the end so they're visible + wWins := 0 + bWins := 0 + for _, game := range games { + if game.Winner == "W" { + wWins++ + } else { + bWins++ + } + } + fmt.Printf("\n=== OVERALL STATISTICS ===\n") + fmt.Printf("Total games: %d\n", len(games)) + fmt.Printf("White wins: %d (%.1f%%)\n", wWins, float64(wWins)/float64(len(games))*100) + fmt.Printf("Black wins: %d (%.1f%%)\n", bWins, float64(bWins)/float64(len(games))*100) + fmt.Printf("\nMachine learning progress: Black is winning %.1f%% of games!\n", float64(bWins)/float64(len(games))*100) + + return nil +} + +// ShowGameReplay provides an animated text-based replay of the most recent game +func ShowGameReplay(games []GamePlayed, boardSize int) error { + if len(games) == 0 { + return fmt.Errorf("no games to display") + } + + // Get the most recent game + game := games[len(games)-1] + + fmt.Printf("\n=== GAME REPLAY ===\n") + fmt.Printf("Replaying Game %d (Winner: %s)\n\n", len(games), game.Winner) + + // Show initial board + board := NewBoard(boardSize) + fmt.Printf("Initial position:\n") + board.Print() + fmt.Printf("\nPress Enter to advance moves, or Ctrl+C to stop...\n\n") + + // Wait for user input to start + fmt.Scanln() + + // Show each move + for i, move := range game.MovesPlayed { + player := "W" + if i%2 == 1 { + player = "B" + } + + fmt.Printf("Move %d: Player %s plays %s\n", i+1, player, move.MoveStr) + board.ApplyMove(move.MoveStr) + board.Print() + + if i < len(game.MovesPlayed)-1 { + fmt.Printf("\nPress Enter for next move...\n") + fmt.Scanln() + } + fmt.Printf("\n") + } + + fmt.Printf("Game complete! Winner: %s\n", game.Winner) + return nil +} \ No newline at end of file diff --git a/hexapawn_log.json b/hexapawn_log.json index 33f6a1d..31492bf 100644 --- a/hexapawn_log.json +++ b/hexapawn_log.json @@ -151,3 +151,352 @@ {"time":"2024-07-13T20:06:25.597081662-04:00","level":"INFO","msg":"game result","game":48,"winner":"B"} {"time":"2024-07-13T20:06:25.597335757-04:00","level":"INFO","msg":"game result","game":49,"winner":"B"} {"time":"2024-07-13T20:06:25.597528113-04:00","level":"INFO","msg":"game result","game":50,"winner":"B"} +{"time":"2024-12-08T21:50:49.118350947-05:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2024-12-08T21:50:49.118378323-05:00","level":"INFO","msg":"starting game","game":1,"total":20} +{"time":"2024-12-08T21:50:49.11864137-05:00","level":"INFO","msg":"game completed","game":1,"winner":"W","moves":5} +{"time":"2024-12-08T21:50:49.119241933-05:00","level":"INFO","msg":"starting game","game":2,"total":20} +{"time":"2024-12-08T21:50:49.119554133-05:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2024-12-08T21:50:49.119563226-05:00","level":"INFO","msg":"starting game","game":3,"total":20} +{"time":"2024-12-08T21:50:49.119748035-05:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.119754398-05:00","level":"INFO","msg":"starting game","game":4,"total":20} +{"time":"2024-12-08T21:50:49.119940782-05:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.119946994-05:00","level":"INFO","msg":"starting game","game":5,"total":20} +{"time":"2024-12-08T21:50:49.120151-05:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.120157795-05:00","level":"INFO","msg":"starting game","game":6,"total":20} +{"time":"2024-12-08T21:50:49.120360083-05:00","level":"INFO","msg":"game completed","game":6,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.12036654-05:00","level":"INFO","msg":"starting game","game":7,"total":20} +{"time":"2024-12-08T21:50:49.12057533-05:00","level":"INFO","msg":"game completed","game":7,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.120583699-05:00","level":"INFO","msg":"starting game","game":8,"total":20} +{"time":"2024-12-08T21:50:49.120789829-05:00","level":"INFO","msg":"game completed","game":8,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.120796346-05:00","level":"INFO","msg":"starting game","game":9,"total":20} +{"time":"2024-12-08T21:50:49.120998318-05:00","level":"INFO","msg":"game completed","game":9,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.121004359-05:00","level":"INFO","msg":"starting game","game":10,"total":20} +{"time":"2024-12-08T21:50:49.12120506-05:00","level":"INFO","msg":"game completed","game":10,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.12122797-05:00","level":"INFO","msg":"starting game","game":11,"total":20} +{"time":"2024-12-08T21:50:49.121431043-05:00","level":"INFO","msg":"game completed","game":11,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.121437399-05:00","level":"INFO","msg":"starting game","game":12,"total":20} +{"time":"2024-12-08T21:50:49.121628522-05:00","level":"INFO","msg":"game completed","game":12,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.12163715-05:00","level":"INFO","msg":"starting game","game":13,"total":20} +{"time":"2024-12-08T21:50:49.121825207-05:00","level":"INFO","msg":"game completed","game":13,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.121831291-05:00","level":"INFO","msg":"starting game","game":14,"total":20} +{"time":"2024-12-08T21:50:49.122028469-05:00","level":"INFO","msg":"game completed","game":14,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.122034678-05:00","level":"INFO","msg":"starting game","game":15,"total":20} +{"time":"2024-12-08T21:50:49.122235637-05:00","level":"INFO","msg":"game completed","game":15,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.122241991-05:00","level":"INFO","msg":"starting game","game":16,"total":20} +{"time":"2024-12-08T21:50:49.122442722-05:00","level":"INFO","msg":"game completed","game":16,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.122449298-05:00","level":"INFO","msg":"starting game","game":17,"total":20} +{"time":"2024-12-08T21:50:49.122781038-05:00","level":"INFO","msg":"game completed","game":17,"winner":"B","moves":6} +{"time":"2024-12-08T21:50:49.122790933-05:00","level":"INFO","msg":"starting game","game":18,"total":20} +{"time":"2024-12-08T21:50:49.123006605-05:00","level":"INFO","msg":"game completed","game":18,"winner":"B","moves":4} +{"time":"2024-12-08T21:50:49.123014457-05:00","level":"INFO","msg":"starting game","game":19,"total":20} +{"time":"2024-12-08T21:50:49.123333644-05:00","level":"INFO","msg":"game completed","game":19,"winner":"B","moves":6} +{"time":"2024-12-08T21:50:49.123340736-05:00","level":"INFO","msg":"starting game","game":20,"total":20} +{"time":"2024-12-08T21:50:49.123558305-05:00","level":"INFO","msg":"game completed","game":20,"winner":"B","moves":4} +{"time":"2025-08-03T21:28:16.640836-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:28:16.641013-04:00","level":"INFO","msg":"starting game","game":1,"total":3} +{"time":"2025-08-03T21:28:16.641076-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:28:16.641082-04:00","level":"INFO","msg":"starting game","game":2,"total":3} +{"time":"2025-08-03T21:28:16.641126-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:28:16.641129-04:00","level":"INFO","msg":"starting game","game":3,"total":3} +{"time":"2025-08-03T21:28:16.641199-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":6} +{"time":"2025-08-03T21:28:57.774528-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:28:57.77465-04:00","level":"INFO","msg":"starting game","game":1,"total":2} +{"time":"2025-08-03T21:28:57.774716-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:28:57.77472-04:00","level":"INFO","msg":"starting game","game":2,"total":2} +{"time":"2025-08-03T21:28:57.774761-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:29:51.730629-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:29:51.730809-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:29:51.73091-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:29:51.730918-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:29:51.731048-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:29:51.731055-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:29:51.731144-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:29:51.73115-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:29:51.731242-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T21:29:51.731248-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:29:51.731379-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":6} +{"time":"2025-08-03T21:32:10.768402-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:32:10.768557-04:00","level":"INFO","msg":"starting game","game":1,"total":3} +{"time":"2025-08-03T21:32:10.768656-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:10.768665-04:00","level":"INFO","msg":"starting game","game":2,"total":3} +{"time":"2025-08-03T21:32:10.768751-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:10.768757-04:00","level":"INFO","msg":"starting game","game":3,"total":3} +{"time":"2025-08-03T21:32:10.768873-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":6} +{"time":"2025-08-03T21:32:47.741972-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:32:47.742089-04:00","level":"INFO","msg":"starting game","game":1,"total":10} +{"time":"2025-08-03T21:32:47.742241-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:32:47.742249-04:00","level":"INFO","msg":"starting game","game":2,"total":10} +{"time":"2025-08-03T21:32:47.742347-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.742353-04:00","level":"INFO","msg":"starting game","game":3,"total":10} +{"time":"2025-08-03T21:32:47.742446-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.742452-04:00","level":"INFO","msg":"starting game","game":4,"total":10} +{"time":"2025-08-03T21:32:47.742573-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.742578-04:00","level":"INFO","msg":"starting game","game":5,"total":10} +{"time":"2025-08-03T21:32:47.742664-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.742667-04:00","level":"INFO","msg":"starting game","game":6,"total":10} +{"time":"2025-08-03T21:32:47.742746-04:00","level":"INFO","msg":"game completed","game":6,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.742749-04:00","level":"INFO","msg":"starting game","game":7,"total":10} +{"time":"2025-08-03T21:32:47.742831-04:00","level":"INFO","msg":"game completed","game":7,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.742834-04:00","level":"INFO","msg":"starting game","game":8,"total":10} +{"time":"2025-08-03T21:32:47.742912-04:00","level":"INFO","msg":"game completed","game":8,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.742916-04:00","level":"INFO","msg":"starting game","game":9,"total":10} +{"time":"2025-08-03T21:32:47.742997-04:00","level":"INFO","msg":"game completed","game":9,"winner":"B","moves":4} +{"time":"2025-08-03T21:32:47.743-04:00","level":"INFO","msg":"starting game","game":10,"total":10} +{"time":"2025-08-03T21:32:47.743079-04:00","level":"INFO","msg":"game completed","game":10,"winner":"B","moves":4} +{"time":"2025-08-03T21:33:54.58292-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:33:54.583137-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:33:54.583283-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:33:54.583291-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:33:54.58342-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:33:54.583426-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:33:54.583515-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:33:54.583521-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:33:54.583607-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T21:33:54.583612-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:33:54.583738-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":6} +{"time":"2025-08-03T21:37:29.106401-04:00","level":"INFO","msg":"Starting hexapawn","filename":"2025-08-03.json"} +{"time":"2025-08-03T21:37:29.106558-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:37:29.106692-04:00","level":"INFO","msg":"game completed","game":1,"winner":"W","moves":5} +{"time":"2025-08-03T21:37:29.106867-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:37:29.107015-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:37:29.107022-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:37:29.107131-04:00","level":"INFO","msg":"game completed","game":3,"winner":"W","moves":5} +{"time":"2025-08-03T21:37:29.10727-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:37:29.107333-04:00","level":"INFO","msg":"game completed","game":4,"winner":"W","moves":3} +{"time":"2025-08-03T21:37:29.10746-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:37:29.107583-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":6} +{"time":"2025-08-03T21:41:15.025369-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:41:15.025559-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T21:41:15.025604-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:42:14.379383-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:42:14.379698-04:00","level":"INFO","msg":"starting game","game":1,"total":3} +{"time":"2025-08-03T21:42:14.379855-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:42:14.379867-04:00","level":"INFO","msg":"starting game","game":2,"total":3} +{"time":"2025-08-03T21:42:14.379996-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:42:14.380007-04:00","level":"INFO","msg":"starting game","game":3,"total":3} +{"time":"2025-08-03T21:42:14.380109-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:43:57.85123-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:43:57.851348-04:00","level":"INFO","msg":"starting game","game":1,"total":10} +{"time":"2025-08-03T21:43:57.851441-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:43:57.851448-04:00","level":"INFO","msg":"starting game","game":2,"total":10} +{"time":"2025-08-03T21:43:57.851575-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:43:57.85158-04:00","level":"INFO","msg":"starting game","game":3,"total":10} +{"time":"2025-08-03T21:43:57.85166-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:43:57.851665-04:00","level":"INFO","msg":"starting game","game":4,"total":10} +{"time":"2025-08-03T21:43:57.851795-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":6} +{"time":"2025-08-03T21:43:57.851801-04:00","level":"INFO","msg":"starting game","game":5,"total":10} +{"time":"2025-08-03T21:43:57.851887-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T21:43:57.851892-04:00","level":"INFO","msg":"starting game","game":6,"total":10} +{"time":"2025-08-03T21:43:57.851972-04:00","level":"INFO","msg":"game completed","game":6,"winner":"B","moves":4} +{"time":"2025-08-03T21:43:57.851976-04:00","level":"INFO","msg":"starting game","game":7,"total":10} +{"time":"2025-08-03T21:43:57.852051-04:00","level":"INFO","msg":"game completed","game":7,"winner":"B","moves":4} +{"time":"2025-08-03T21:43:57.852055-04:00","level":"INFO","msg":"starting game","game":8,"total":10} +{"time":"2025-08-03T21:43:57.852161-04:00","level":"INFO","msg":"game completed","game":8,"winner":"B","moves":6} +{"time":"2025-08-03T21:43:57.852165-04:00","level":"INFO","msg":"starting game","game":9,"total":10} +{"time":"2025-08-03T21:43:57.852238-04:00","level":"INFO","msg":"game completed","game":9,"winner":"B","moves":4} +{"time":"2025-08-03T21:43:57.852242-04:00","level":"INFO","msg":"starting game","game":10,"total":10} +{"time":"2025-08-03T21:43:57.852313-04:00","level":"INFO","msg":"game completed","game":10,"winner":"B","moves":4} +{"time":"2025-08-03T21:44:09.690352-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:44:09.690387-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:44:09.690488-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:44:09.690497-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:44:09.690582-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:44:09.690589-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:44:09.690675-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:44:09.69068-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:44:09.690809-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":6} +{"time":"2025-08-03T21:44:09.690814-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:44:09.690925-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T21:45:07.891787-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:45:07.891939-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:45:07.892008-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:45:07.892014-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:45:07.892078-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:45:07.892083-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:45:07.892144-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:45:07.892147-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:45:07.892215-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T21:45:07.89222-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:45:07.892281-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T21:46:34.948549-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:46:34.948676-04:00","level":"INFO","msg":"starting game","game":1,"total":2} +{"time":"2025-08-03T21:46:34.948725-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:46:34.948729-04:00","level":"INFO","msg":"starting game","game":2,"total":2} +{"time":"2025-08-03T21:46:34.948773-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:46:52.874362-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:46:52.874435-04:00","level":"INFO","msg":"starting game","game":1,"total":2} +{"time":"2025-08-03T21:46:52.874505-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:46:52.874512-04:00","level":"INFO","msg":"starting game","game":2,"total":2} +{"time":"2025-08-03T21:46:52.874572-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:47:22.055949-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:47:22.056082-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T21:47:22.056154-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:48:47.895671-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:48:47.895916-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T21:48:47.895992-04:00","level":"INFO","msg":"game completed","game":1,"winner":"W","moves":5} +{"time":"2025-08-03T21:49:26.333592-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:49:26.333878-04:00","level":"INFO","msg":"starting game","game":1,"total":10} +{"time":"2025-08-03T21:49:26.333973-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:26.33398-04:00","level":"INFO","msg":"starting game","game":2,"total":10} +{"time":"2025-08-03T21:49:26.334066-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:26.334071-04:00","level":"INFO","msg":"starting game","game":3,"total":10} +{"time":"2025-08-03T21:49:26.334144-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:26.334149-04:00","level":"INFO","msg":"starting game","game":4,"total":10} +{"time":"2025-08-03T21:49:26.334226-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:26.334231-04:00","level":"INFO","msg":"starting game","game":5,"total":10} +{"time":"2025-08-03T21:49:26.334353-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":6} +{"time":"2025-08-03T21:49:26.334356-04:00","level":"INFO","msg":"starting game","game":6,"total":10} +{"time":"2025-08-03T21:49:26.334428-04:00","level":"INFO","msg":"game completed","game":6,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:26.334431-04:00","level":"INFO","msg":"starting game","game":7,"total":10} +{"time":"2025-08-03T21:49:26.334504-04:00","level":"INFO","msg":"game completed","game":7,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:26.334508-04:00","level":"INFO","msg":"starting game","game":8,"total":10} +{"time":"2025-08-03T21:49:26.33458-04:00","level":"INFO","msg":"game completed","game":8,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:26.334583-04:00","level":"INFO","msg":"starting game","game":9,"total":10} +{"time":"2025-08-03T21:49:26.334685-04:00","level":"INFO","msg":"game completed","game":9,"winner":"B","moves":6} +{"time":"2025-08-03T21:49:26.334688-04:00","level":"INFO","msg":"starting game","game":10,"total":10} +{"time":"2025-08-03T21:49:26.33476-04:00","level":"INFO","msg":"game completed","game":10,"winner":"B","moves":4} +{"time":"2025-08-03T21:49:40.335864-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:49:40.336016-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T21:49:40.336142-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:50:08.980062-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:50:08.980286-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:50:08.980509-04:00","level":"INFO","msg":"game completed","game":1,"winner":"W","moves":7} +{"time":"2025-08-03T21:50:08.981417-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:50:08.981511-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:50:08.981518-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:50:08.981657-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":6} +{"time":"2025-08-03T21:50:08.981668-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:50:08.981755-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T21:50:08.981762-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:50:08.981858-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T21:51:16.174667-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:51:16.174851-04:00","level":"INFO","msg":"starting game","game":1,"total":3} +{"time":"2025-08-03T21:51:16.175018-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:51:16.175047-04:00","level":"INFO","msg":"starting game","game":2,"total":3} +{"time":"2025-08-03T21:51:16.175206-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:51:16.175228-04:00","level":"INFO","msg":"starting game","game":3,"total":3} +{"time":"2025-08-03T21:51:16.175325-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:52:09.544857-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:52:09.545067-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T21:52:09.54513-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:52:19.35594-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:52:19.355982-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T21:52:19.356065-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:53:04.238125-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:53:04.238326-04:00","level":"INFO","msg":"starting game","game":1,"total":3} +{"time":"2025-08-03T21:53:04.238429-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:53:04.238438-04:00","level":"INFO","msg":"starting game","game":2,"total":3} +{"time":"2025-08-03T21:53:04.23852-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:53:04.238526-04:00","level":"INFO","msg":"starting game","game":3,"total":3} +{"time":"2025-08-03T21:53:04.238608-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:53:11.552472-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:53:11.552522-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:53:11.552661-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:53:11.552671-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:53:11.552837-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:53:11.552844-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:53:11.553025-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":6} +{"time":"2025-08-03T21:53:11.553034-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:53:11.553193-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":6} +{"time":"2025-08-03T21:53:11.5532-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:53:11.553399-04:00","level":"INFO","msg":"game completed","game":5,"winner":"W","moves":7} +{"time":"2025-08-03T21:54:49.385205-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:54:49.385379-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:54:49.385498-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:54:49.38551-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:54:49.385633-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:54:49.385641-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:54:49.385771-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":6} +{"time":"2025-08-03T21:54:49.38578-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:54:49.385865-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T21:54:49.385872-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:54:49.385963-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T21:55:17.56622-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:55:17.566559-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T21:55:17.566646-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:55:30.55579-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:55:30.556169-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:55:30.556315-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:55:30.556325-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:55:30.556491-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T21:55:30.556498-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:55:30.556642-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:55:30.556648-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:55:30.55681-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":6} +{"time":"2025-08-03T21:55:30.556816-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:55:30.55699-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":6} +{"time":"2025-08-03T21:56:09.337833-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:56:09.338048-04:00","level":"INFO","msg":"starting game","game":1,"total":3} +{"time":"2025-08-03T21:56:09.338205-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T21:56:09.338225-04:00","level":"INFO","msg":"starting game","game":2,"total":3} +{"time":"2025-08-03T21:56:09.338307-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:56:09.338317-04:00","level":"INFO","msg":"starting game","game":3,"total":3} +{"time":"2025-08-03T21:56:09.338436-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":6} +{"time":"2025-08-03T21:56:55.801779-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:56:55.802012-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:56:55.802137-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:56:55.802152-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:56:55.80224-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:56:55.802249-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:56:55.802333-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T21:56:55.80234-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:56:55.802472-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":6} +{"time":"2025-08-03T21:56:55.802482-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:56:55.80257-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T21:58:00.374058-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T21:58:00.374503-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T21:58:00.374611-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T21:58:00.374621-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T21:58:00.37471-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T21:58:00.374716-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T21:58:00.374842-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":6} +{"time":"2025-08-03T21:58:00.374849-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T21:58:00.374974-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":6} +{"time":"2025-08-03T21:58:00.374979-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T21:58:00.375062-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":4} +{"time":"2025-08-03T22:00:23.436668-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:00:23.437003-04:00","level":"INFO","msg":"starting game","game":1,"total":2} +{"time":"2025-08-03T22:00:23.437282-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T22:00:23.437309-04:00","level":"INFO","msg":"starting game","game":2,"total":2} +{"time":"2025-08-03T22:00:23.437476-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":6} +{"time":"2025-08-03T22:00:31.380121-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:00:31.380156-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T22:00:31.380232-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T22:00:55.241791-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:00:55.241979-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T22:00:55.242037-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T22:01:28.826482-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:01:28.826819-04:00","level":"INFO","msg":"starting game","game":1,"total":2} +{"time":"2025-08-03T22:01:28.826989-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T22:01:28.827001-04:00","level":"INFO","msg":"starting game","game":2,"total":2} +{"time":"2025-08-03T22:01:28.827106-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T22:01:40.840399-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:01:40.840475-04:00","level":"INFO","msg":"starting game","game":1,"total":2} +{"time":"2025-08-03T22:01:40.840618-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":6} +{"time":"2025-08-03T22:01:40.840627-04:00","level":"INFO","msg":"starting game","game":2,"total":2} +{"time":"2025-08-03T22:01:40.840708-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T22:04:31.030201-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:04:31.030346-04:00","level":"INFO","msg":"starting game","game":1,"total":1} +{"time":"2025-08-03T22:04:31.030398-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:27.921063-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:05:27.921505-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T22:05:27.921657-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:27.921679-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T22:05:27.92176-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:27.92177-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T22:05:27.921846-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:27.921854-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T22:05:27.921939-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:27.921947-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T22:05:27.922064-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":6} +{"time":"2025-08-03T22:05:42.19349-04:00","level":"INFO","msg":"Starting hexapawn","filename":"machine.json"} +{"time":"2025-08-03T22:05:42.193793-04:00","level":"INFO","msg":"starting game","game":1,"total":5} +{"time":"2025-08-03T22:05:42.19406-04:00","level":"INFO","msg":"game completed","game":1,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:42.194089-04:00","level":"INFO","msg":"starting game","game":2,"total":5} +{"time":"2025-08-03T22:05:42.194179-04:00","level":"INFO","msg":"game completed","game":2,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:42.194189-04:00","level":"INFO","msg":"starting game","game":3,"total":5} +{"time":"2025-08-03T22:05:42.194389-04:00","level":"INFO","msg":"game completed","game":3,"winner":"B","moves":4} +{"time":"2025-08-03T22:05:42.194859-04:00","level":"INFO","msg":"starting game","game":4,"total":5} +{"time":"2025-08-03T22:05:42.195004-04:00","level":"INFO","msg":"game completed","game":4,"winner":"B","moves":6} +{"time":"2025-08-03T22:05:42.195011-04:00","level":"INFO","msg":"starting game","game":5,"total":5} +{"time":"2025-08-03T22:05:42.195132-04:00","level":"INFO","msg":"game completed","game":5,"winner":"B","moves":6} diff --git a/machine.json b/machine.json index 90bed19..f6981aa 100644 --- a/machine.json +++ b/machine.json @@ -87,8 +87,7 @@ "c3-c2" ], "..W.W.BB.": [ - "a3-a2", - "a3-b2" + "a3-a2" ], "..WBW.B.B": [ "a2-a1", @@ -105,8 +104,7 @@ "..WWB..BB": [ "b2-b1", "b2-c1", - "b3-a2", - "c3-c2" + "b3-a2" ], "..WWB.BB.": [ "b2-b1", @@ -335,7 +333,6 @@ ], "....WBB..": [ "c2-c1", - "a3-a2", "a3-b2" ], "...BBW..B": [ @@ -370,8 +367,7 @@ "c2-c1" ], "...WWB.B.": [ - "c2-c1", - "b3-a2" + "c2-c1" ], "...WWW..B": [ "c3-b2" @@ -3324,6 +3320,3556 @@ } ], "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W.BB.", + "move_str": "a3-b2" + }, + { + "board_str": "..W.B..B.", + "move_str": "c1-b2" + } + ], + "winner": "W" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-c2" + }, + { + "board_str": "W...BWBB.", + "move_str": "b3-c2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-a2" + }, + { + "board_str": "...WBBB..", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-c2" + }, + { + "board_str": "W...BW.BB", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a3-b2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-c2" + }, + { + "board_str": "...BBW..B", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a3-b2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-b2" + }, + { + "board_str": "...BW...B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W..BB", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a3-b2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-c2" + }, + { + "board_str": "...BBW..B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a3-b2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-c2" + }, + { + "board_str": "...BBW..B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-b2" + }, + { + "board_str": "...BW.B..", + "move_str": "a3-b2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c3-b2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-a2" + }, + { + "board_str": "...WBBB..", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-b2" + }, + { + "board_str": "W...W..BB", + "move_str": "c3-c2" + }, + { + "board_str": "W...WB.B.", + "move_str": "a1-a2" + }, + { + "board_str": "...WWB.B.", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-c2" + }, + { + "board_str": "W...BWBB.", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "a3-b2" + }, + { + "board_str": "W...BB..B", + "move_str": "a1-b2" + }, + { + "board_str": "....WB..B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c3-b2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-a2" + }, + { + "board_str": "...WBBB..", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB.BB.", + "move_str": "b3-a2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-b2" + }, + { + "board_str": "...BW.B..", + "move_str": "a3-b2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W..BB", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-c2" + }, + { + "board_str": "W...BWBB.", + "move_str": "b3-c2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-b2" + }, + { + "board_str": "....WBB..", + "move_str": "a3-b2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b3-a2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-c2" + }, + { + "board_str": "...BBW..B", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-b2" + }, + { + "board_str": "W...W..BB", + "move_str": "c3-c2" + }, + { + "board_str": "W...WB.B.", + "move_str": "a1-a2" + }, + { + "board_str": "...WWB.B.", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-b2" + }, + { + "board_str": "W...W..BB", + "move_str": "c3-c2" + }, + { + "board_str": "W...WB.B.", + "move_str": "a1-a2" + }, + { + "board_str": "...WWB.B.", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB.BB.", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a3-b2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-c2" + }, + { + "board_str": "...BBW..B", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-c2" + }, + { + "board_str": "W...BW.BB", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "c3-c2" + }, + { + "board_str": "..WWBB.B.", + "move_str": "a2-a3" + } + ], + "winner": "W" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W.BB.", + "move_str": "a3-a2" + }, + { + "board_str": "..WBW..B.", + "move_str": "c1-c2" + }, + { + "board_str": "...BWW.B.", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b3-a2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-b2" + }, + { + "board_str": "...BW...B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W.BB.", + "move_str": "a3-a2" + }, + { + "board_str": "..WBW..B.", + "move_str": "c1-c2" + }, + { + "board_str": "...BWW.B.", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c3-b2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-b2" + }, + { + "board_str": "....WBB..", + "move_str": "a3-a2" + }, + { + "board_str": "...BWB...", + "move_str": "b2-b3" + } + ], + "winner": "W" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "a3-b2" + }, + { + "board_str": "W...BB..B", + "move_str": "a1-a2" + }, + { + "board_str": "...WBB..B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-b2" + }, + { + "board_str": "W...W.BB.", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a3-b2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-b2" + }, + { + "board_str": "...BW...B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-c2" + }, + { + "board_str": "W...BW.BB", + "move_str": "b2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-b2" + }, + { + "board_str": "W...W..BB", + "move_str": "c3-c2" + }, + { + "board_str": "W...WB.B.", + "move_str": "a1-a2" + }, + { + "board_str": "...WWB.B.", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-c2" + }, + { + "board_str": "W...BW.BB", + "move_str": "b3-c2" + }, + { + "board_str": "W...BB..B", + "move_str": "a1-a2" + }, + { + "board_str": "...WBB..B", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W.BB.", + "move_str": "a3-a2" + }, + { + "board_str": "..WBW..B.", + "move_str": "c1-c2" + }, + { + "board_str": "...BWW.B.", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB.BB.", + "move_str": "b3-a2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB.BB.", + "move_str": "b3-a2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-b2" + }, + { + "board_str": "W...W..BB", + "move_str": "c3-c2" + }, + { + "board_str": "W...WB.B.", + "move_str": "a1-a2" + }, + { + "board_str": "...WWB.B.", + "move_str": "b3-a2" + }, + { + "board_str": "...BWB...", + "move_str": "b2-b3" + } + ], + "winner": "W" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W..BB", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a3-b2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-c2" + }, + { + "board_str": "...BBW..B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W..BB", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-c2" + }, + { + "board_str": "W...BW.BB", + "move_str": "b3-c2" + }, + { + "board_str": "W...BB..B", + "move_str": "a1-a2" + }, + { + "board_str": "...WBB..B", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-b2" + }, + { + "board_str": "W...W.BB.", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b3-a2" + }, + { + "board_str": "..WBB...B", + "move_str": "c1-b2" + }, + { + "board_str": "...BW...B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W.BB.", + "move_str": "a3-a2" + }, + { + "board_str": "..WBW..B.", + "move_str": "c1-c2" + }, + { + "board_str": "...BWW.B.", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c3-b2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-a2" + }, + { + "board_str": "...WBBB..", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-b2" + }, + { + "board_str": "W...W..BB", + "move_str": "c3-c2" + }, + { + "board_str": "W...WB.B.", + "move_str": "a1-a2" + }, + { + "board_str": "...WWB.B.", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W..BB", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c3-b2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-b2" + }, + { + "board_str": "....WBB..", + "move_str": "a3-b2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "c1-c2" + }, + { + "board_str": ".W.B.WB.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c3-b2" + }, + { + "board_str": "W...BBB..", + "move_str": "a1-b2" + }, + { + "board_str": "....WBB..", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "a3-b2" + }, + { + "board_str": "W...BB..B", + "move_str": "a1-a2" + }, + { + "board_str": "...WBB..B", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-c2" + }, + { + "board_str": "W...BW.BB", + "move_str": "b2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB.BB.", + "move_str": "b3-a2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-c2" + }, + { + "board_str": "...BBWB..", + "move_str": "b2-b1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "c1-b2" + }, + { + "board_str": "W...W..BB", + "move_str": "c3-c2" + }, + { + "board_str": "W...WB.B.", + "move_str": "a1-a2" + }, + { + "board_str": "...WWB.B.", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-b2" + }, + { + "board_str": "...BW.B..", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "a3-b2" + }, + { + "board_str": "W...BB..B", + "move_str": "a1-b2" + }, + { + "board_str": "....WB..B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "a3-b2" + }, + { + "board_str": "W.W.B..BB", + "move_str": "a1-a2" + }, + { + "board_str": "..WWB..BB", + "move_str": "b2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "a1-a2" + }, + { + "board_str": ".W.W.BB.B", + "move_str": "c2-c1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-b2" + }, + { + "board_str": "W...W.BB.", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "a1-b2" + }, + { + "board_str": "..W.W.BB.", + "move_str": "a3-a2" + }, + { + "board_str": "..WBW..B.", + "move_str": "c1-c2" + }, + { + "board_str": "...BWW.B.", + "move_str": "a2-a1" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-a2" + }, + { + "board_str": "..WW..B.B", + "move_str": "c3-c2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-c2" + }, + { + "board_str": "W....WB.B", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "b1-b2" + }, + { + "board_str": "W.W.W.BBB", + "move_str": "c3-b2" + }, + { + "board_str": "W.W.B.BB.", + "move_str": "c1-b2" + }, + { + "board_str": "W...W.BB.", + "move_str": "a3-a2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "c1-c2" + }, + { + "board_str": "WW...WBBB", + "move_str": "b3-c2" + }, + { + "board_str": "WW...BB.B", + "move_str": "b1-b2" + }, + { + "board_str": "W...WBB.B", + "move_str": "a3-b2" + }, + { + "board_str": "W...BB..B", + "move_str": "a1-b2" + }, + { + "board_str": "....WB..B", + "move_str": "c3-b2" + } + ], + "winner": "B" + }, + { + "moves_played": [ + { + "board_str": "WWW...BBB", + "move_str": "a1-a2" + }, + { + "board_str": ".WWW..BBB", + "move_str": "b3-a2" + }, + { + "board_str": ".WWB..B.B", + "move_str": "b1-b2" + }, + { + "board_str": "..WBW.B.B", + "move_str": "c3-b2" + }, + { + "board_str": "..WBB.B..", + "move_str": "c1-b2" + }, + { + "board_str": "...BW.B..", + "move_str": "a3-b2" + } + ], + "winner": "B" } ], "Logger": {} diff --git a/main.go b/main.go index 7727ac1..c838f28 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,6 @@ +// Package main implements a Hexapawn game with machine learning capabilities. +// Hexapawn is a simplified chess variant played on a 3x3 board (or larger) with only pawns. +// The game includes options for human players and AI opponents that learn from their mistakes. package main import ( @@ -9,47 +12,78 @@ import ( flag "github.com/spf13/pflag" ) -// Define constants for board dimensions -var boardRows, numPlayers, numGames int -var machineFile, logFile string +const ( + // Default configuration values + defaultBoardRows = 3 + defaultNumPlayers = 2 + defaultNumGames = 20 + defaultMachineFile = "machine.json" + defaultLogFile = "hexapawn_log.json" + + // Validation constraints + minBoardRows = 3 + maxBoardRows = 9 + minNumPlayers = 0 + maxNumPlayers = 2 + minNumGames = 1 +) + +// Configuration holds all game settings +type Config struct { + boardRows int + numPlayers int + numGames int + machineFile string + logFile string + visualize bool + interactive bool + replay bool +} func main() { - flag.IntVarP(&boardRows, "rows", "r", 3, "Number of rows in the board, must be at least 3, at most 9. Default is 3.") - flag.IntVarP(&numPlayers, "players", "p", 2, "Number of human players: 0, 1, or 2. Default is 2.") - flag.IntVarP(&numGames, "games", "g", 20, "Number of games to play: at least 1. Default is 20.") - flag.StringVarP(&machineFile, "filename", "f", "machine.json", "Load the machine from this file. If it doesn't exist, a new machine will be created and saved into this file. Default is 'machine.json'.") - flag.StringVarP(&logFile, "logfile", "l", "hexapawn_log.json", "Log the game into this file. Default is 'hexapawn_log.json'.") + var config Config + + flag.IntVarP(&config.boardRows, "rows", "r", defaultBoardRows, "Number of rows in the board, must be at least 3, at most 9. Default is 3.") + flag.IntVarP(&config.numPlayers, "players", "p", defaultNumPlayers, "Number of human players: 0, 1, or 2. Default is 2.") + flag.IntVarP(&config.numGames, "games", "g", defaultNumGames, "Number of games to play: at least 1. Default is 20.") + flag.StringVarP(&config.machineFile, "filename", "f", defaultMachineFile, "Load the machine from this file. If it doesn't exist, a new machine will be created and saved into this file. Default is 'machine.json'.") + flag.StringVarP(&config.logFile, "logfile", "l", defaultLogFile, "Log the game into this file. Default is 'hexapawn_log.json'.") + flag.BoolVarP(&config.visualize, "visualize", "v", false, "Show text summary of played games after completion.") + flag.BoolVarP(&config.interactive, "interactive", "i", false, "Show interactive TUI game viewer (requires compatible terminal).") + flag.BoolVarP(&config.replay, "replay", "R", false, "Show step-by-step replay of the most recent game.") flag.Parse() - if boardRows < 3 || boardRows > 9 { - log.Fatalf("Invalid board dimensions. Rows and Cols must be equal and at least 3, at most 9.") + if config.boardRows < minBoardRows || config.boardRows > maxBoardRows { + log.Fatalf("Invalid board dimensions. Rows and Cols must be equal and at least %d, at most %d.", minBoardRows, maxBoardRows) } - if numPlayers < 0 || numPlayers > 2 { - log.Fatalf("Invalid number of players. Must be 0, 1, or 2.") + if config.numPlayers < minNumPlayers || config.numPlayers > maxNumPlayers { + log.Fatalf("Invalid number of players. Must be %d, %d, or %d.", minNumPlayers, 1, maxNumPlayers) } - if numGames < 1 { - log.Fatalf("Invalid number of games. Must be at least 1.") + if config.numGames < minNumGames { + log.Fatalf("Invalid number of games. Must be at least %d.", minNumGames) } - if machineFile == "" { - log.Printf("Machine file is not specified. Using 'machine.json'.") + if config.machineFile == "" { + log.Printf("Machine file is not specified. Using '%s'.", defaultMachineFile) + config.machineFile = defaultMachineFile } - if logFile == "" { - log.Printf("Log file is not specified. Using 'hexapawn_log.json'.") + if config.logFile == "" { + log.Printf("Log file is not specified. Using '%s'.", defaultLogFile) + config.logFile = defaultLogFile } machine := hexapawn.NewMachine() // if machineFile exists, load it - machineFile = os.ExpandEnv(machineFile) - machine.MachineFile = machineFile - _, err := os.Stat(machineFile) + config.machineFile = os.ExpandEnv(config.machineFile) + machine.MachineFile = config.machineFile + _, err := os.Stat(machine.MachineFile) if !os.IsNotExist(err) && err != nil { log.Fatal(err) } if err != nil { // if it doesn't exist, create a new machine and save it - err := machine.Init(boardRows, numPlayers) + err := machine.Init(config.boardRows, config.numPlayers) if err != nil { log.Fatal(err) } @@ -57,17 +91,17 @@ func main() { if err != nil { log.Fatal(err) } - log.Printf("Machine saved to %s", machineFile) + log.Printf("Machine saved to %s", machine.MachineFile) } else { // if it exists, load it err := machine.Load() if err != nil { log.Fatal(err) } - log.Printf("Machine loaded from %s", machineFile) + log.Printf("Machine loaded from %s", machine.MachineFile) } - logFile = os.ExpandEnv(logFile) + logFile := os.ExpandEnv(config.logFile) l, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal(err) @@ -79,7 +113,7 @@ func main() { logger.Info("Starting hexapawn", slog.String("filename", machine.MachineFile)) machine.Logger = logger - err = machine.Play(numGames) + err = machine.Play(config.numGames) if err != nil { log.Fatal(err) } @@ -87,5 +121,32 @@ func main() { if err != nil { log.Fatal(err) } - log.Printf("Machine saved to %s", machineFile) + log.Printf("Machine saved to %s", machine.MachineFile) + + // Show visualization if requested + if config.visualize { + log.Printf("Starting game text summary...") + err = hexapawn.ShowGamesText(machine.GamesPlayed, config.boardRows) + if err != nil { + log.Printf("Visualization error: %v", err) + } + } + + // Show interactive TUI if requested + if config.interactive { + log.Printf("Starting interactive game viewer...") + err = hexapawn.RunGameViewerTUI(machine.GamesPlayed, config.boardRows) + if err != nil { + log.Printf("Interactive viewer error: %v", err) + } + } + + // Show step-by-step replay if requested + if config.replay { + log.Printf("Starting game replay...") + err = hexapawn.ShowGameReplay(machine.GamesPlayed, config.boardRows) + if err != nil { + log.Printf("Replay error: %v", err) + } + } }