19 KiB
AGENTS.md
CRITICAL BASH RULES (NEVER VIOLATE)
NEVER chain bash commands. Do not use &&, ||, or ; to combine commands. Each command must be a separate Bash tool call. Use parallel tool calls when commands are independent.
NEVER prefix a command with cd. Run git, gh, bazel, etc. directly from whatever the cwd already is. The cwd is a subdirectory of the worktree; git/gh find the repo via .git discovery and bazel finds the workspace via MODULE.bazel discovery — walking up from a subdir works fine. The shell resets cwd after every command anyway, so a cd never persists. (And cd <dir> && <cmd> violates the no-chaining rule above and forces an approval prompt every time.) Only cd in the rare case a tool genuinely cannot locate its root, and even then as its own Bash call, never a chain.
CRITICAL UNITY PROCESS RULES (NEVER VIOLATE)
NEVER kill Unity without asking the user first. The user might be actively using the Unity Editor. Do not kill, force-quit, terminate, or otherwise stop Unity processes unless the user explicitly approves that specific action.
CRITICAL GIT RULES (NEVER VIOLATE)
NEVER use git -C. Just run git directly from the cwd — it finds the repo via .git discovery. Do not cd to the repo root either (see the no-cd bash rule above).
NEVER push directly to main/master. No exceptions. Not for "small changes." Not for docs. Not ever.
NEVER merge PRs. You create PRs. The user merges them. No exceptions.
ALWAYS use this workflow:
- Create a feature branch from origin/main
- Commit to that branch
- Create a PR with
gh pr create - Wait for user to merge (DO NOT run
gh pr merge)
If you catch yourself about to run git push origin main or git push origin <branch>:main, STOP. You are about to violate a critical rule. Create a PR instead.
If you catch yourself about to run gh pr merge, STOP. Only the user merges PRs.
Commit History During Review
Prefer new follow-up commits over amending existing commits once a PR is open and the user is actively reviewing or testing it. This preserves working interim states so the user can inspect, compare, or return to a known-good point while improvements continue.
Use git commit --amend and force-push only when the user explicitly asks for history cleanup, when fixing metadata
before review has started, or when repairing a local commit that has not been pushed. If you believe squashing is the
right choice despite an open review, explain why and ask first.
Worktree Usage
Prefer using the current worktree for small requested fixes. Create a separate worktree only when the user asks for one, when the current branch/state would make the change unsafe, or when isolating a large/risky change is clearly beneficial.
NEVER leave the current worktree without explicit user approval. Do not create, switch to, edit, commit in, or run git commands from another worktree unless the user has specifically asked for that worktree or approved the move. If the current worktree is unsafe because of dirty state, report that and ask before using a different worktree.
Codex may edit code and run git commands in the current worktree, even when unrelated files are dirty, as long as it:
- does not revert, reset, clean, or overwrite unrelated changes
- stages only the files relevant to the requested change
- verifies the staged diff before committing
- creates feature branches and PRs as usual
- does not push to main/master or merge PRs
When the user says a Codex-created PR has been merged, delete the corresponding local branch and temporary worktree automatically, unless there is uncommitted work that would make cleanup unsafe.
PR Timing
When code changes appear correct locally, create the PR promptly even if long-running builds or tests are still running. Opening the PR starts CI on the build machines, uploads results to the Bazel remote cache, and can speed up subsequent local validation. Continue monitoring any already-started local and CI validation after opening the PR.
For PRs expected to trigger substantial Bazel work in CI, run the relevant Bazel builds and tests locally too. This workstation is fast, and local Bazel runs can help hydrate the remote cache for the CI builders while still giving earlier signal on failures.
Cleanup PR Batching
When making mechanical cleanup changes, group 3-5 files per PR when the files are receiving the same kind of change and can be reviewed as one pattern. Do not open one PR per file for nearly identical few-line cleanups, such as replacing the same unsafe accessor pattern with the same contextual failure pattern across multiple Scala files.
Keep separate PRs for changes that involve different semantics, materially different risk, unrelated subsystems, or files whose tests/validation strategy makes them better reviewed independently.
GitHub CLI PR Bodies
When creating or editing PR descriptions with multiline bodies, write the body to a temporary markdown file and pass it
with gh pr create --body-file <path> or gh pr edit <number> --body-file <path>. Do not use multiline $'...'
shell quoting for PR bodies; Codex's permission matching may treat that as a complex shell command instead of a clean
gh pr invocation, causing unnecessary approval prompts.
Write these temporary body files under /private/tmp/eagle0-pr-bodies/, creating that directory first when needed. This
directory is the canonical writable scratch location for PR descriptions. Do not write PR body files to protected paths,
repo-tracked paths, or ad hoc locations that might require additional user permission.
Use ordinary Bash file-writing commands for these temporary PR body files. First run a standalone command to create the
directory. Then run a standalone printf command that writes to /private/tmp/eagle0-pr-bodies/<name>.md. Do not use
apply_patch for temporary PR body files, because patch tools are for tracked workspace edits and may incorrectly
request user approval for scratch paths outside the repository.
Write other scratch files needed for commands under /private/tmp/eagle0-scratch/, creating that directory first when
needed. Do not use protected paths that require additional user permission just to create or edit temporary files.
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Project Overview
Eagle0 is a multi-language gaming system combining strategic turn-based gameplay (Eagle) with tactical hex-based combat (Shardok). The system integrates LLM-based narrative generation and supports both human and AI players.
Architecture
Three-Tier Game System:
- Unity Client (C#): Real-time strategy game client with integrated tactical combat UI
- Eagle (Scala): Strategic layer managing turn-based gameplay, diplomacy, hero progression, and province control
- Shardok (C++): Tactical layer handling real-time hex-based combat simulation with performance-critical battle resolution
Communication Flow:
Unity Client ↔ Eagle (gRPC streaming) ↔ Shardok (internal gRPC)
Key Entry Points:
/src/main/csharp/net/eagle0/clients/unity/eagle0/- Unity C# game client/src/main/scala/net/eagle0/eagle/Main.scala- Eagle strategic game server/src/main/cpp/net/eagle0/shardok/shardok_server_main.cpp- Shardok tactical server
Protocol Buffer Architecture:
- Extensive use of protobuf for type-safe communication
- Separate packages:
api/(client-facing),internal/(server state),views/(client projections) - Event sourcing pattern with immutable action history
Essential Commands
Building
# Build Eagle server using the same target as GitHub Actions
./scripts/build_eagle_ci.sh
# Build Shardok server using the same target and flags as GitHub Actions
./scripts/build_shardok_ci.sh
# Warm the remote cache for the main GitHub Actions Bazel builds
./scripts/hydrate_bazel_remote_cache.sh
# Build Unity/C# client
./scripts/build_protos.sh # Protocol buffer generation for Unity
./scripts/build_plugins.sh # Native plugins for all platforms
./scripts/build_windows_plugin.sh # Windows-specific plugin build
# Unity builds via CI: ci/github_actions/build_unity.sh
Running Services
# Eagle server (port 40032)
bazel run //src/main/scala/net/eagle0/eagle:eagle_server -- --eagle-grpc-port 40032
# Or: ./scripts/eagle_run.sh
# Shardok server
bazel run //src/main/cpp/net/eagle0/shardok:shardok-server --compilation_mode=opt
# Or: ./scripts/shardok_run.sh
Testing
# Run all tests
bazel test //src/test/... //src/main/go/...
# Component-specific tests
bazel test //src/test/scala/... # Scala Eagle tests
bazel test //src/test/cpp/... # C++ Shardok tests
Code Generation
bazel run gazelle # Update Go build files
./scripts/updateActionResultTypes.sh # Update protocol buffer mappings
Pre-Commit Checklist
MANDATORY: Before running git commit, verify:
- If you modified any BUILD.bazel file: Run
bazel run gazelleand stage any changes it makes - If you modified C++ or C# files: Run
clang-format -ion the modified files - If you modified Scala files: scalafmt will run automatically via pre-commit hook
- ALWAYS run the full test suite for your language changes before pushing:
- For Scala changes:
bazel test //src/test/scala/... - For C++ changes:
bazel test //src/test/cpp/... - This catches exhaustive pattern match errors and other compile-time failures that single-target builds miss
- For Scala changes:
The pre-commit hook runs gazelle but only checks if it succeeds - it does NOT verify the BUILD files are in canonical format. The gazelle_test will fail if deps are not alphabetically sorted. Always run gazelle manually after BUILD file changes.
Bazel Cache Hydration
When making changes that will trigger GitHub Actions Bazel builds, run the matching CI script instead of hand-writing
bazel build commands. The scripts keep local cache hydration aligned with CI targets, compilation modes, and flags:
./scripts/build_eagle_ci.sh
./scripts/build_shardok_ci.sh
./scripts/hydrate_bazel_remote_cache.sh
Use direct bazel build commands only for targeted debugging where CI cache hydration is not the goal.
Code Formatting
# ALWAYS run clang-format after making any C++ or C# code changes
clang-format -i <modified_files>
# Format all C++ files in a directory:
find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
# Format all C# files in a directory:
find . -name "*.cs" | xargs clang-format -i
Static Analysis
# Run clang-tidy static analysis on C++ files
# Note: This may show some header include errors but will still analyze the main file
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' <file_path> -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++23
# Example for AI files:
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' /Users/dancrosby/CodingProjects/github/eagle0/src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.cpp -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++23
AI Algorithm Selection
Eagle0 supports two AI algorithms for tactical combat decision-making:
Iterative Deepening AI (Default)
The original minimax-based AI with sophisticated randomness handling:
- Advantages: Proven, sophisticated randomness evaluation, comprehensive lookahead
- Use cases: Production builds, scenarios requiring precise evaluation
- Performance: Single-threaded, thorough evaluation
Monte Carlo Tree Search AI (MCTS)
Modern MCTS-based AI with multithreading support:
- Advantages: Multithreaded, better performance on modern CPUs, anytime algorithm
- Use cases: Performance testing, scenarios requiring fast decisions
- Performance: Multithreaded, adaptive depth based on time budget
Switching Between Algorithms
The algorithm is selected at runtime via the ShardokAIClient constructor:
// Using Iterative Deepening AI (default)
ShardokAIClient client(playerId, isDefender, hexMap, settings);
// OR explicitly:
ShardokAIClient client(playerId, isDefender, hexMap, settings, AIAlgorithmType::ITERATIVE_DEEPENING);
// Using MCTS AI
ShardokAIClient client(playerId, isDefender, hexMap, settings, AIAlgorithmType::MCTS);
# Build the server (includes both AI algorithms)
./scripts/build_shardok_ci.sh
# Test both algorithms
bazel test //src/test/cpp/net/eagle0/shardok/ai:ai_iterative_deepening_test
bazel test //src/test/cpp/net/eagle0/shardok/ai:ai_mcts_test # If available
# Performance tests
./scripts/ai_perf_test.sh # Uses whatever algorithm the server is configured to use
Both implementations are compatible with all existing interfaces and produce the same SearchResult structure.
Note: Both implementations are documented in src/main/cpp/net/eagle0/shardok/ai/AI_SCORING_SYSTEM.md, including
recommendations for improving MCTS randomness handling.
The AI algorithm selection is made at runtime when creating ShardokAIClient instances, allowing different AI strategies to be used for different players or game situations within the same server process.
Language-Specific Patterns
Scala (Strategic Layer):
- Use
EngineImpl.scalafor core game logic modifications - Follow event sourcing pattern - all changes through immutable actions
- gRPC streaming for real-time client updates via
EagleServiceImpl.scala - LLM integration in
/common/llm_integration/for narrative generation
C++ (Tactical Layer):
- Performance-critical combat in
ShardokEngine.hpp/.cpp - FlatBuffers for efficient serialization in
/flatbuffer/directory - AI systems in
/ai/subdirectory with pluggable strategy selectors - Extensive unit testing with Google Test framework
Protocol Buffers:
- Three-layer structure:
api/(client),internal/(server),views/(projections) - Use
shardok_internal_interface.protofor Eagle-Shardok communication - Maintain backward compatibility when modifying existing messages
C# (Unity Client):
- Located in
/src/main/csharp/net/eagle0/clients/unity/eagle0/ - Uses Unity 6.5 (6000.5.0f1) with comprehensive protobuf integration (100+ .proto files)
- Key components:
EagleConnection.cs(gRPC client),EagleGameController.cs(main game logic) - Real-time bidirectional streaming with server via
PersistentClientConnection.cs - Strategic map UI in
Assets/Eagle/, tactical battle UI inAssets/Shardok/ - Seamless transition between strategic gameplay and hex-based tactical combat
- NEVER add defensive null checks on Unity Inspector fields - these hide configuration bugs. If a field isn't linked in the editor, it should throw a NullReferenceException so the problem is immediately obvious. Silently skipping code when a required field is null makes bugs harder to find.
Go (Build Tools):
- Build automation and code generation utilities
- AWS S3 integration for deployment artifacts
Testing Strategy
- Comprehensive unit tests for both Scala and C++ components
- Integration tests for Eagle-Shardok communication
- Map validation tests ensure game content integrity
- Use
GameSettings_test_utils.cppandShardokEngineBasedTestData.cppfor C++ test helpers
Scala Testing Patterns
Use inside() instead of asInstanceOf for type matching in tests:
Never use asInstanceOf in tests. Instead, use ScalaTest's inside() pattern for safe type matching:
// BAD - don't do this
val changedHero = result.changedHeroes.head.asInstanceOf[ChangedHeroC]
changedHero.heroId shouldBe 19
// GOOD - use inside() pattern
import org.scalatest.Inside.inside
inside(result.changedHeroes.head) { case changedHero: ChangedHeroC =>
changedHero.heroId shouldBe 19
changedHero.vigorChange shouldBe StatDelta(17.2)
}
The inside() pattern:
- Provides better error messages when the type doesn't match
- Is idiomatic ScalaTest
- Works with pattern matching for more complex assertions
Performance Testing
When making performance-related changes to the AI or engine:
# 1. Commit your changes to a feature branch
git checkout -b performance-improvement-feature
git add . && git commit -m "Implement performance improvement"
# 2. Run performance tests multiple times on your branch to reduce noise
for i in 1 2 3; do
echo "=== Run $i ==="
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
done
# Save or note the results
# 3. Switch to main branch and run the same tests
git checkout main
for i in 1 2 3; do
echo "=== Run $i ==="
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
done
# 4. Compare the results between your branch and main
# Key metrics to compare:
# - Commands evaluated at each depth (e.g., "Depth 3: 169/523 commands")
# - Average search depth achieved
# - Completion rates at each depth
Important notes:
- Run tests multiple times (3-5) to account for performance variance
- Focus on commands evaluated at each depth rather than total commands
- Commands at different depths aren't directly comparable (depth 3 is more valuable than depth 2)
- Always test performance changes - what seems like an optimization may sometimes have unexpected overhead or behavior changes.
Troubleshooting Scala Build Errors
MissingType Errors
When you see errors like:
dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.eagle.internal.game_state.type.GameState
This is NOT a Scala compiler crash. This is a missing dependency in BUILD.bazel.
How to fix:
- Identify the missing type from the error message (e.g.,
game_state.GameState) - Find the Bazel target that provides this type (e.g.,
//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto) - Add it to the
depsof the failing target - If the type appears in a public method signature, also add it to
exportsso downstream targets can see it
Common pattern: When adding a method to a class that takes or returns a proto type, the proto dependency often needs to be added to both deps AND exports.
Bazel Clean
NEVER run bazel clean without asking first. It rarely fixes actual issues and wastes significant rebuild time. The issues that seem like they need bazel clean are usually:
- Missing imports in Scala code
- Missing dependencies in BUILD.bazel
- Missing exports for types used in public signatures
Game Content
Maps: .e0mj files in /src/main/resources/net/eagle0/shardok/maps/
Configuration: Game parameters in /src/main/resources/net/eagle0/eagle/game_parameters.json
Data Files: TSV format for battalions, heroes, and other game data
NEVER modify hero names. Do not change names in heroes.tsv, generated_heroes, or any other hero data files. The names are carefully chosen and are not to be altered.
Deployment
- Bazel handles multi-language builds and dependencies
- CI/CD via GitHub Actions with platform-specific build scripts in
/ci/github_actions/ - Docker containerization available via
ci/eagle_run.Dockerfile - Always run "bazel run //:gazelle" after editing any BUILD.bazel files
- ALWAYS ALWAYS run "bazel run gazelle" after any change that modifies a BUILD.bazel file