Check OPENAI_API_KEY and ANTHROPIC_API_KEY environment variables first,
fall back to api_keys.txt file if not set. This allows the Docker
container to receive API keys via environment without needing a file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane binary discovery in CI workflow
- Add set -ex for better error visibility
- Try finding crane in bazel-bin/external first
- Fall back to bazel cquery if not found
- Add debug output for digest and crane path
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane path - use runfiles directory
Crane binary is in the runfiles directory after bazel run:
bazel-bin/ci/push_*.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use crane push directly instead of bazel run
The registry converts OCI to Docker format immediately, changing the
digest. We can't reference the image by its original OCI digest after
push.
Solution: Use crane push directly to a tag (not by digest). Bazel is
still used to build the image and get crane in runfiles.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AttackCommandChooser and dependencies to use Scala types
- AttackCommandChooser: Use Scala GameState, HeroT, ProvinceT instead of proto
- CombatUnitSelector: Use Scala HeroT, BattalionT, BattalionType
- BattalionSuitability: Use Scala HeroT, BattalionT, BattalionType
- MarchSuppliesHelpers: Use Scala BattalionT (fixes pre-existing broken dep)
- CommandChoiceHelpers: Add GameStateConverter.fromProto() at call boundary
- AttackCommandChooserTest: Rewrite to use Scala types (GameState, HeroC, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix type conversion for callers of CombatUnitSelector and BattalionSuitability
After deproto-ing CombatUnitSelector to use Scala types, all callers need
to convert proto types to Scala at call sites:
- AvailableDefendCommandsFactory: Add converter imports and calls
- AvailableMarchCommandFactory: Add converter imports and calls
- CommandChoiceHelpers: Add converter imports and calls in defendingUnits
BUILD file updates:
- Add converter deps to availability factories
- Add battalion/battalion_type state deps for return types
- Update visibility on battalion_type_converter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert BattalionSuitabilityTest to use Scala types
BattalionSuitability now expects Scala types (HeroT, BattalionT, BattalionType),
so its test needs to:
- Use HeroC and BattalionC instead of proto Hero and Battalion
- Use Scala Profession enum
- Convert BattalionTypesTestData (proto) via BattalionTypeConverter.fromProto
Keep BattalionTypesTestData returning proto types since other tests
(like AttackDecisionCommandChooserTest) pass it to proto GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix MidGameAIClient to convert proto GameState when calling AttackCommandChooser
AttackCommandChooser now expects Scala GameState, so MidGameAIClient needs
to convert via GameStateConverter.fromProto() at the call sites.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Convert AlmsCommandSelector from proto types (GameState, Hero, Province) to Scala model types (GameState, HeroT, ProvinceT)
- Add backward-compatible proto type overloads that convert to Scala types internally
- Add Scala type overloads to FoodConsumptionUtils for foodConsumptionMonthsToHold
- Add fatigue() method to HeroUtils for Scala types
- Convert AlmsCommandSelectorTest to use Scala model types (ProvinceC, HeroC, BattalionC)
- Update BUILD.bazel dependencies, exports, and visibility for proper type resolution
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
DigitalOcean registry converts OCI manifests to Docker format, causing
digest mismatch when Bazel's oci_push tries to tag by digest.
Solution:
- Remove remote_tags from oci_push in BUILD.bazel (push by digest only)
- Use Bazel for the push, then crane copy/tag for tagging (handles format conversion)
This keeps Bazel as the primary build/push tool while working around
the registry's format conversion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update AlmsCommandSelector to use new FoodConsumptionUtils
- Replace LegacyFoodConsumptionUtils with FoodConsumptionUtils
- Add private foodConsumptionMonthsToHold helper that converts proto
types to Scala types using DateConverter and RoundPhaseConverter
- Update visibility of DateConverter and RoundPhaseConverter to allow
access from command_choice_helpers
- Update tests to set currentPhase = PLAYER_COMMANDS (required by
RoundPhaseConverter)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix quest command chooser tests to set currentPhase
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add parameter names to foodConsumptionMonthsToHold call
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Docker deployment connectivity issues
- Shardok: Listen on 0.0.0.0:40042 instead of localhost for container networking
- Shardok: Add env var fallback for resource paths (SHARDOK_RESOURCES_PATH, SHARDOK_MAPS_PATH)
- Eagle: Use plaintext gRPC for internal container-to-container communication
- docker-compose: Pass CLI args directly instead of env vars, default to gpt-5.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document Docker networking and resource configuration
Added section explaining:
- Why Shardok binds to 0.0.0.0 instead of localhost for container networking
- Why Eagle uses .usePlaintext() for internal gRPC
- How env var fallbacks replace Bazel runfiles in Docker
- Future considerations for multi-host deployment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Rename FoodConsumptionUtils to LegacyFoodConsumptionUtils, create new Scala-types version
- Renamed proto-based FoodConsumptionUtils to LegacyFoodConsumptionUtils
- Created new FoodConsumptionUtils that uses native Scala model types (Date, ProvinceT, GameState, RoundPhase) instead of proto types
- Updated all callers (AlmsCommandSelector, CommandChoiceHelpers, ExpandCommandSelector, MidGameAIClient) to use LegacyFoodConsumptionUtils
- Renamed test to LegacyFoodConsumptionUtilsTest
- Updated BUILD.bazel files with new targets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move FoodConsumptionUtils to dedicated food_consumption package
- Move FoodConsumptionUtils.scala and LegacyFoodConsumptionUtils.scala
from command_choice_helpers to new food_consumption directory
- Add FoodConsumptionUtilsTest using pure Scala types instead of protos
- Update all dependent BUILD.bazel files with new import paths
- Update importing Scala files to use new package location
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rename food_consumption_utils target to food_consumption
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix missing dependency and visibility for food_consumption package
- Add legacy_food_consumption_utils dependency to command_choice_helpers
- Fix visibility to use __pkg__ instead of __subpackages__
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Next steps for productionization
* Run deploy job on self-hosted runner for secure SSH
* Temporarily disable production environment to debug runner
* Use ubuntu-latest for deploy job
* Add remote_tags to oci_push for latest tag
The push script runs on the host (macOS) and needs native tools like jq.
Using --platforms=//:linux_x86_64 caused it to try running Linux binaries.
The image is already built for Linux; the push just uploads it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds NewFactionHeadDetailsNotificationGenerator to display a notification
when a faction's leader changes. Shows both the new and previous leader
headshots, with appropriate text for player vs other factions.
Features:
- 10 randomized notification titles (New Leadership, Succession, etc.)
- Shows province where new leader is located (if player's faction)
- Includes previous leader name in text when available
- References LLM-generated text for full narrative
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The NewFactionHeadMessage LLM request was added in #4785 but without a
corresponding notification. This adds the notification type so clients
can display the event.
Changes:
- Add NewFactionHeadDetails proto message with new_head_hero_id,
faction_id, and previous_head_hero_id fields
- Add NewFactionHead case class to NotificationDetails
- Add toProto/fromProto converters in NotificationConverter
- Create notification alongside LLM request in CheckForFactionChangesAction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix sysroot .so plugins and registry auth
- Exclude GCC plugin .so files from sysroot (Bazel can't handle them)
- Only copy GCC headers and static libs needed for cross-compilation
- Add tarball structure verification to build script
- Fix DO registry auth by setting DOCKER_CONFIG env var for Bazel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add triple symlinks to sysroot for clang compatibility
Clang uses --target=x86_64-unknown-linux-gnu but Ubuntu's GCC uses
x86_64-linux-gnu (without "unknown"). This caused clang to fail to detect
the GCC installation and couldn't find C++ standard library headers.
Add symlinks in the sysroot:
- /usr/lib/gcc/x86_64-unknown-linux-gnu -> x86_64-linux-gnu
- /lib/x86_64-unknown-linux-gnu -> x86_64-linux-gnu
- /include/x86_64-unknown-linux-gnu -> x86_64-linux-gnu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix sysroot tarball structure for toolchains_llvm
The tarball was wrapping contents in sysroot/ which caused double nesting
when extracted by the sysroot() repo rule. Changed to tar from inside the
sysroot directory so usr/, lib/, etc. are at the root of the archive.
Before: sysroot/usr/...
After: ./usr/... (or usr/...)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add libgcc_s.so symlink to sysroot
The linker looks for libgcc_s.so but Ubuntu only provides libgcc_s.so.1.
Added a symlink to make -lgcc_s work.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix sysroot for cross-compilation
1. Add ld-linux-x86-64.so.2 symlink in lib/ since libc.so linker script
references /lib64/ld-linux-x86-64.so.2 as an absolute path
2. Change linkopt to host_linkopt in .bazelrc to avoid passing macOS-specific
linker flags (-no_warn_duplicate_libraries) to Linux cross-compilation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update sysroot to v3.4 with all required symlinks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add missing includes for cross-compilation portability
These files relied on platform-specific transitive includes that work on
macOS but not on Linux. Added explicit includes for:
- <cstdint> for uint8_t, uint64_t
- <stdexcept> for std::out_of_range
- <cinttypes> for PRIu64 portable format specifier
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change return type from Vector[EventForChronicle] (proto) to Vector[ChronicleEvent] (Scala)
- Remove intermediate EventForChronicleDetails layer - create Scala types directly
- Update all 23 event type mappings to use Scala *ChronicleEvent types
- Add DateConverter.fromProto() to convert proto dates to Scala dates
- Update NewRoundAction to use Scala types directly (remove ChronicleEventConverter)
- Update DEPROTO_PLAN.md: now 47/52 (90%) action files are fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM notification when new faction head is declared
When a hero becomes the new head of a faction, generate an LLM message
where they declare their new leadership to the world. If the faction
is also being renamed (because the new head has a ledFactionName),
the declaration includes explanation of the new faction name.
Changes:
- Add NewFactionHeadMessage proto and Scala case to LlmRequestT
- Add NewFactionHeadPromptGenerator for generating the LLM prompt
- Update CheckForFactionChangesAction to create LLM requests
- Update LlmRequestConverter to handle the new message type
- Update LlmResolver to use the new prompt generator
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Include previous faction head's execution in LLM notification
The notification now mentions that the previous faction head was executed
and includes their name, providing context for the new leader's declaration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Include previous faction head's description in LLM prompt
Add the executed leader's full description (including backstory) to the
prompt, allowing the LLM to potentially reference their history when
generating the new leader's declaration message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a faction head changes (e.g., due to leader death), if the new head
hero has a ledFactionName set, the faction is automatically renamed to
that name. This enables "great person" heroes to rename factions they lead.
Changes:
- Add new_name field to ChangedFaction proto and ChangedFactionC
- Add new_name field to FactionViewDiff for client updates
- Update ChangedFactionConverter for new field
- Update GameStateFactionExtensions applier to apply name changes
- Update GameStateViewDiffer to diff faction names
- Update CheckForFactionChangesAction to set newName from hero's ledFactionName
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The v2 sysroot includes GCC installation directories that clang needs
to find libstdc++ headers for cross-compilation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Skip AWS CLI install if already present
- Use eagle0-windows bucket (same as other workflows, credentials have access)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a new field to the Hero proto and Scala models to store the name of the
faction that a hero would lead, if they became a faction leader. This is
populated from the "faction_name" column in the heroes TSV for "great person"
heroes, and is empty (or None in Scala) for other heroes.
This will be used in the future to rename factions when a great person
becomes the leader of a different faction.
Changes:
- Add led_faction_name (string) to Hero proto
- Add ledFactionName: Option[String] to HeroT trait and HeroC case class
- Add ledFactionName to LoadedHero intermediate type
- Update HeroConverter, LoadedHeroConversion, and FixedHeroes to handle the new field
- Update FixedHeroesTest to include the new field
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use existing ACCESS_KEY_ID and SECRET_KEY secrets instead of
non-existent DO_SPACES_KEY and DO_SPACES_SECRET.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Issues fixed:
1. Sysroot hosted on GitHub releases but repo is private, causing 404s
2. Sysroot missing GCC directories that clang needs to find libstdc++ headers
Changes:
- Add GCC installation directories to sysroot (clang uses these to locate C++ headers)
- Update workflow to upload sysroot to DO Spaces instead of GitHub releases
- Versioned sysroot paths (v2, v3, etc.) for easier updates
NOTE: After merging, run the "Build Linux Sysroot" workflow with version "v2",
then update MODULE.bazel with the sha256 from the workflow output.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add a "Bottom Line Up Front" section after the title with a short prose
paragraph highlighting the most important changes and what to look for
when testing
- Wrap HTML output in proper document with UTF-8 charset declaration to
fix Unicode character rendering (em-dashes, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the Claude prompt to generate a "PR Details" section after the synopsis,
with the same thematic groupings but listing actual PR numbers, titles, and
clickable GitHub links.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Switch from Mac Mail AppleScript to Fastmail JMAP API
- Generate HTML synopsis instead of plain text for better formatting
- Auto-fetch account ID, identity ID, and drafts mailbox from API
- Support config files in ~/.config/eagle0/:
- fastmail_token: API token (required)
- changelog_recipient: Email recipients, one per line (optional)
- Support multiple recipients (one email address per line, # for comments)
- Fall back to FASTMAIL_API_TOKEN environment variable for token
- Only require token when not in --dry-run mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable Shardok cross-compilation with sysroot
- Update sha256 with actual value from sysroot release
- Re-enable build-shardok job in docker_build.yml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add DigitalOcean registry authentication for image push
The oci_push rule needs credentials to push to the registry.
Creates ~/.docker/config.json with the auth token before pushing.
Requires DO_REGISTRY_TOKEN_BASE64 secret to be configured:
echo -n "username:token" | base64
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use existing DO_REGISTRY_TOKEN secret for registry auth
Base64 encode the token on the fly instead of requiring a
separate pre-encoded secret.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add cross-compilation support for Shardok Docker builds
This enables building Shardok on the self-hosted Mac runner while
targeting Linux x86_64, avoiding the need for slow GitHub-hosted
Ubuntu runners.
Changes:
- Add Ubuntu 24.04 (Noble) sysroot generation scripts
- Add GitHub Actions workflow to build and release the sysroot
- Configure toolchains_llvm for cross-compilation with sysroot
- Update docker_build.yml to use cross-compilation
- Add linux_x86_64 platform definition
The sysroot contains libstdc++-13 which provides C++23 support
needed by the codebase.
To complete setup:
1. Run the "Build Linux Sysroot" workflow to create the sysroot
2. Update MODULE.bazel with the actual sha256 from the release
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Temporarily disable Shardok build until sysroot is ready
The cross-compilation sysroot needs to be built and uploaded before
Shardok can be built. Steps to re-enable:
1. Run "Build Linux Sysroot" workflow
2. Update sha256 in MODULE.bazel
3. Uncomment build-shardok job
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Creates scripts/generate_changelog.sh that:
- Fetches merged PRs since last run (tracked via git tag) or previous Friday 4pm
- Uses Claude CLI to generate a themed synopsis of changes
- Opens an email draft in Mac Mail with the synopsis
- Updates the changelog-last-run tag for next run
Usage: ./scripts/generate_changelog.sh [--dry-run]
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove Docker dependency by using `bazel run //ci:*_push` instead of
oci_load + docker tag + docker push
- Build Shardok on ubuntu-latest to produce Linux binary for container
- Add Bazel caching for GitHub-hosted runner
The self-hosted Mac runner doesn't have Docker running, and even if it
did, the Shardok binary would be macOS, not Linux.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Fix custom battle shardokGameId collision
Previously, all custom battles for the same eagleGameId used the same
shardokGameId ("${eagleGameId}_1"), causing token mismatch exceptions
when starting a second custom battle while another was running.
The Shardok server would return the OLD game's controller (with its
higher token count), while the Eagle client had a fresh controller
(token=0), resulting in InvalidTokenException.
Fix: Add a counter to generate unique shardokGameIds for each custom
battle: "custom_${eagleGameId}_${counter}".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix duplicate streaming updates causing InvalidTokenException
The SubscribeToGame RPC was sending results twice:
1. Initial response sent results from known_result_count onwards
2. WaitForUpdatesAndPush started from known_result_count, immediately
satisfying the wait condition and re-sending the same results
This caused Eagle to receive duplicate results, inflating its count
above Shardok's actual count, leading to token > expectedToken.
Fix: Track total_action_result_count from the initial response and
use that as the starting point for WaitForUpdatesAndPush.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change FactionT.reconnedProvinces from proto ProvinceView to Scala ProvinceView
- Change FactionC.reconnedProvinces from proto ProvinceView to Scala ProvinceView
- Change ChangedFactionC.updatedReconnedProvinces from proto to Scala ProvinceView
- Update FactionConverter and ChangedFactionConverter to convert at boundary
- Remove ProvinceViewConverter.toProto calls from PerformReconResolutionAction
and EndBattleAftermathPhaseAction
- Update GameStateFactionExtensions import to use Scala ProvinceView
- Update test assertions to use Scala Date type
Progress: 45/52 action files (87%) are now fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add rules_oci to MODULE.bazel for OCI container support
- Create ci/BUILD.bazel with oci_image targets for both servers
- Add docker-compose.prod.yml for local testing
- Add GitHub Actions workflow for building and pushing images
- Update resource BUILD files with //ci visibility
Build images: bazel build //ci:eagle_server_image //ci:shardok_server_image
Load locally: bazel run //ci:eagle_server_load && bazel run //ci:shardok_server_load
Push to DO: bazel run //ci:eagle_server_push && bazel run //ci:shardok_server_push
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the streaming connection to Shardok fails, Eagle now uses
exponential backoff for reconnection attempts, starting at 1 second
and doubling up to 10 seconds max. This is more robust for handling
transient network issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
These font files were committed as regular blobs before LFS tracking
was set up for *.ttf files. Adding explicit exclusions prevents the
"files that should have been pointers" warning.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents the architecture and migration plan to move Eagle and Shardok
servers from home Mac to DigitalOcean cloud infrastructure:
- On-demand Shardok with Eagle lifecycle management
- Docker containerization strategy
- GitHub Actions CI/CD pipeline
- Cost estimates and scaling options
- Migration phases and rollback procedures
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Delete UnaffiliatedHeroMovedAction: was never called from production code;
PerformUnaffiliatedHeroesAction.heroMovedResult constructs ActionResultC directly
- Delete HeroBackstoryUpdateActionGenerator.fromGameState: dead method that
converted proto to Scala; only apply(GameState) is used
- Update DEPROTO_PLAN.md: now 44/52 (85%) action files are fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Shows "Your vassal {name} in {province} became a {profession}" instead
of just "Your vassal {name} became a {profession}".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update PerformReconResolutionAction and EndBattleAftermathPhaseAction
to use the new Scala ProvinceViewFilter.filteredProvinceView overload,
eliminating the need for lazy proto conversion.
- PerformReconResolutionAction: Remove proto GameState conversion entirely
- EndBattleAftermathPhaseAction: Use Scala overload for DidBattle case
(Withdrew case still needs proto for withdrawnFromProvinceView)
- Remove unused deps from BUILD.bazel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replaces the polling-based gameStatusRunner with server-side streaming via
SubscribeToGame. Updates are now pushed immediately by Shardok instead of
being polled, reducing latency and eliminating polling overhead.
- Replace gameStatusRunner with subscribeToGame using StreamObserver
- Add handleStreamingResponse to process pushed updates
- Add scheduleReconnect for automatic reconnection on stream errors
- Update postCommand/postPlacementCommands to not handle responses
(updates come via stream)
- Remove dead code: handleBattleResponse, waitingForHumanPlayer
Requires: PR #4753 (server-side streaming implementation)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. Deadlock: WaitForUpdatesAndPush was calling GetUpdates while holding
masterLock, but GetUpdates also tries to acquire masterLock.
Fix: Release lock before calling GetUpdates.
2. Missing eagle_faction_id: Streaming OnUpdate wasn't setting the faction
ID on filtered responses, so clients couldn't route updates correctly.
Fix: Move OnePlayerUpdates struct before StreamSubscriber, update OnUpdate
to take vector<OnePlayerUpdates>, and properly iterate to set faction IDs.
Also added currentGameState to AllUpdates struct.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds SubscribeToGame streaming RPC to Shardok server, replacing the need for
Eagle to poll via GetGameStatus. Updates are pushed to subscribers when the
game state changes, reducing latency and eliminating continuous polling.
- Add GameSubscriptionRequest message and SubscribeToGame streaming RPC
- Add StreamSubscriber interface for push-based update delivery
- Implement subscriber registration in ShardokGameController
- Add WaitForUpdatesAndPush loop that blocks until updates are available
- Implement GrpcStreamSubscriber to write updates to gRPC stream
- Use separate subscriberLock to avoid deadlock with masterLock
Eagle client-side changes will be in a follow-up PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Scala overloads for ProvinceViewFilter and dependencies
Enable ProvinceViewFilter to accept Scala ProvinceT and GameState types
instead of proto types, supporting the ongoing deproto migration for
internal logic. This unblocks dependent actions like EndBattleAftermathPhaseAction.
Changes:
- Add pure Scala filterArmy overload to ArmyFilter
- Add filteredProvinceView(ProvinceT, ScalaGameState) to ProvinceViewFilter
- Add monthlyFoodConsumption Scala overload to ProvinceUtils
- Update BUILD.bazel files with required deps, exports, and visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN with ProvinceViewFilter progress
- Mark ProvinceViewFilter server-side overload as complete (PR #4752)
- Update View Filters section to show partial completion status
- Mark EndBattleAftermathPhaseAction and PerformReconResolutionAction as unblocked
- Add validation checkbox for server-side ProvinceViewFilter
- Update estimated remaining effort
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Adds a "Retry" button next to connection status that appears when:
- Circuit breaker is in Open state (server down)
- Connection is counting down to a retry attempt
- Clicking the button forces an immediate reconnection attempt,
bypassing timeouts
- Button is hidden when connected or actively connecting
The button must be wired up in the Unity scene to the ConnectionStatusUI
component's retryButton field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The AI thread was holding the master lock for the entire duration of
AI decision-making (which can take seconds). This blocked all polls
from getting updates, causing batching.
New architecture with three phases:
1. Phase 1 (brief lock): Get copies of game state, settings, commands
2. Phase 2 (NO LOCK): AI thinks on the copies - polls can get through
3. Phase 3 (brief lock): Verify state unchanged, post command
If the state changed while thinking (e.g., human posted a command),
we discard the AI decision and re-evaluate with fresh data.
This reduces lock hold time from seconds to milliseconds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- For faction leaders: "Your sworn {sibling} {name} became a {profession}"
- For vassals: "Your vassal {name} became a {profession}"
- For other factions: "{name} of {faction} became a {profession}" (unchanged)
- Only highlights the province where the hero is located (falls back to
all faction provinces if hero not found)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Added UpdateAction?.Invoke() to HandleAvailableCommands so the UI
refreshes when available commands are updated
- Initialize AvailableCommands to empty list to prevent null reference
when UpdateAction triggers before first HandleAvailableCommands call
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous fix (PR #4732) added mutex protection to GetGameId() and
GetHexMap(), but this caused stalls because GetUpdates() holds the lock
for extended periods while waiting for AI updates.
This fix takes a different approach: since game_id never changes after
game creation, we cache it at construction time. This eliminates the
race condition without any locking overhead.
Also removes the unused GetHexMap() method which had the same thread
safety issue.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add exception handling to Shardok polling to prevent client freeze
When handleBattleResponse throws an exception, the polling loop would
stop completely, causing both clients to freeze at the same point.
Exceptions were silently swallowed by the async Future callback.
This fix:
- Wraps the processing in try-catch
- Logs exception details to console for debugging
- Continues polling even on error to prevent freeze
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve comments explaining Shardok polling logic
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The server was setting ransomPaidByFactionId and ransomPaidToFactionId
backwards in ResolveRansomOfferCommand. The acting faction (captor)
should receive the payment, and the originating faction (offering)
should pay.
Also added missing notification for the accepting faction (captor)
in the client, matching the pattern from RansomRejected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The content RectTransform was only grown to fit text, never shrunk.
If a previous text was longer, scrolling to the bottom would show
blank space past where the text ends. Now the content height is
synced in both directions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The actual retry timeout remains 60 seconds, but the UI now shows
a maximum of "10s" to avoid overwhelming users with long countdowns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The hasHumanPlayerCommands method was checking if ANY player (human or AI)
had available Shardok commands. When an AI player had commands that Shardok
handles internally, this would return true, causing Eagle to stop polling
Shardok for updates until a human posted a command.
This caused Shardok battle updates to batch up and arrive all at once
instead of streaming in real-time.
Fix: Only check human faction IDs when determining if we should wait for
a command to be posted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update ProvinceViewFilter to return Scala types instead of proto
- ProvinceViewFilter now returns Scala ProvinceView instead of proto
- Added Scala helper methods in ArmyFilter, LegacyBattalionViewFilter, StatWithConditionUtils
- Updated callers (EndBattleAftermathPhaseAction, PerformReconResolutionAction,
GameStateViewFilter) to convert back to proto using ProvinceViewConverter.toProto()
- Added default values to Scala case classes (ProvinceView, FullProvinceInfo,
IncomingArmyView, UnaffiliatedHeroBasics)
- Fixed recruitmentInfo handling to use fold/getOrElse with RecruitmentInfo.Unknown
- Updated ProvinceViewFilterTest to use proto type aliases for Faction.reconnedProvinces
- Added tests for view converters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add lastCommand field to ProvinceT/ProvinceC
Add the lastCommand field that was present in province.proto but missing
from the Scala types. Uses the proto SelectedCommand type directly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ProvinceConverter to use typed lastCommand
Update ProvinceConverter to use Option[SelectedCommand] instead of Any,
and properly convert Empty to None in fromProto.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unnecessary default arguments from view case classes
Defaults can mask missing fields at compile time. Removed defaults from
FullProvinceInfo, ProvinceView, and UnaffiliatedHeroBasics. Updated tests
to explicitly provide all required fields.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add lastCommandTypeForActingProvince to ActionResultT and apply in ActionResultApplierImpl
This adds the equivalent of applyLastCommand from ActionResultProtoApplierImpl
to the Scala-based action result applier, ensuring lastCommand is properly
persisted when using Scala GameState types.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ActionResultProtoConverter to include lastCommandTypeForActingProvince
Added the new field to the pattern match and proto conversion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Move Application.logMessageReceivedThreaded registration from Start()
to Awake() so exceptions during initialization are captured.
Also add fallback for when MainQueue isn't ready yet - errors are
queued and displayed in Update() once the UI is available.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When subscribing with count=0 (fresh start), the server sends thousands of
historical results. Before the client can process them all, heartbeat runs
and detects a sync mismatch, triggering reconnect. This creates an endless
loop where the client never catches up.
Added a 60-second grace period after successful connect during which sync
mismatches are logged but don't trigger reconnects. This allows time to
receive and process historical results.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ModelUpdated() and SetModifiers() were wrapping their work in
MainQueue.Q.Enqueue(), but they're already called from the MainQueue
via the update processing chain:
MainQueue → ReceiveGameUpdate → HandleUpdates → UpdateAction → ModelUpdated
This double/triple-enqueuing caused UI updates to be pushed to the end
of the queue during rapid updates (like AI turns), making moves appear
delayed or batched instead of in real-time.
By removing the unnecessary enqueues, UI updates now happen immediately
when the update is processed, restoring real-time display of moves.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ShardokViewStatuses was accessed from the heartbeat timer thread while
ShardokGameModels (a regular Dictionary) could be modified on the
MainQueue thread. This race condition could cause enumeration errors
or incorrect sync status being reported.
Changes:
- Convert ShardokGameModels from Dictionary to ConcurrentDictionary
- Replace Remove() calls with TryRemove() for ConcurrentDictionary API
- Remove non-thread-safe History.Count fallback in ShardokViewStatuses,
now falls back to 0 if count not yet tracked
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ProvinceHovered iterated over MovingArmies and accessed table rows by
index. If the data changed after the table was built (e.g., due to a
game update), this could throw ArgumentOutOfRangeException.
Now checks RowCount before accessing to skip stale indices.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
On fresh client start with an ongoing battle:
1. Client subscribes with no ShardokViewStatuses (doesn't know about battles)
2. Server sends StartingState with OutstandingBattles
3. Server sends ShardokActionResultResponses but may start from recent point
4. Client has partial battle history
The fix:
- After receiving StartingState, check for battles not in ShardokGameModels
- Create ShardokGameModel for each new battle
- Mark for resync (requestFullResync=true)
- Re-subscribe to request full state with the correct ShardokViewStatuses
This ensures fresh clients get complete Shardok state for ongoing battles.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
OnApplicationPause had an asymmetry:
- Pause: StopListeningForUpdates() synchronously removed the subscriber
- Resume: StartListeningForUpdates() was fire-and-forget async
If the app paused again before the async subscribe completed, or if the
subscribe failed, the subscriber was permanently lost. This caused
"heartbeat with 0 games" even though data was still being received on
the stream.
The fix is to not unsubscribe on pause at all. With MainQueue rate-limiting
(from #4659), keeping the subscription during pause is safe - updates will
queue up and be processed on resume. Reconnects will continue to work
since the subscriber stays in the dictionary.
Added logging to track pause/resume events for debugging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When UpdateText() was called after a listener fired for one hero name,
it would start from the raw template and only replace placeholders that
were in placeholderValues. Other placeholders that hadn't loaded yet
would appear as literal "{HeroName}" text.
Now UpdateText() applies fallback values for any placeholder that hasn't
been loaded yet, ensuring the notification always shows either the actual
hero name or a readable fallback like "the hero".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Make masterLock mutable and add lock protection to GetGameId() and
GetHexMap() which were accessing the engine without synchronization.
This fixes crashes where the game state buffer was being read while
another thread was modifying it, resulting in invalid memory access
(address 0x9a0 = offset from null pointer).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When MainQueue has a backlog (e.g., after resuming from background),
the main thread could overwrite the gRPC thread's accurate result count
with a stale value from an older queued action. This caused sync
mismatches where the client's reported count was behind the server's,
triggering repeated reconnection loops.
The count is already updated on the gRPC thread in UpdateResultCounts()
before enqueueing, so the redundant update in ReceiveGameUpdate() is
removed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
GetCurrentGameStateBytes was reading from the game engine without
acquiring masterLock, causing crashes when the AI thread was
simultaneously modifying the game state through PostCommand.
The fix adds scoped_lock protection to prevent concurrent access.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Hold either Shift key to pause auto-scrolling, letting the user
read at their own pace. Releasing Shift resumes from current position.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Backstories now grow at a normal rate (+30 words) until they reach 225
words (~1350 characters), then slow to +8 words per update. This
encourages the LLM to tell the hero's story more efficiently once it
reaches a reasonable length, rather than growing indefinitely.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust UI layout anchors and positions
Various RectTransform adjustments in the Gameplay scene.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix race condition in streaming text with proper lock
ConcurrentDictionary doesn't make the read-modify-write in
HandleNewStreamingText atomic. Two concurrent updates for the same
text ID could interleave and corrupt the text.
Changed to use a lock around the dictionary to ensure atomicity.
Listener notifications happen outside the lock to avoid deadlocks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Create converters to translate between proto and Scala view types:
- StatWithConditionConverter
- ArmyViewConverter
- IncomingArmyViewConverter
- UnaffiliatedHeroBasicsConverter
- FullProvinceInfoConverter
- ProvinceViewConverter
Also updates:
- StatWithCondition enum to include all proto condition values
- UnaffiliatedHeroBasics to use Scala Profession type
- Various visibility settings to allow cross-package access
- Make recruitmentInfoFromProto public in UnaffiliatedHeroConverter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When receiving 100s of streaming text updates at once, this was causing
performance issues by notifying listeners for every single update.
Changes:
- Make ClientTextProvider thread-safe with ConcurrentDictionary
- Handle StreamingTextResponse directly on gRPC thread (no MainQueue)
- Track pending text IDs and batch listener notifications
- ProcessPendingUpdates() called once per frame from EagleGameController
This ensures each listener is only notified once per frame per text ID,
regardless of how many updates arrive between frames.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add auto-scroll speed setting to Settings panel
- Add GlobalScrollSpeedMultiplier static property to AutoScrollingText
that persists via PlayerPrefs
- Add slider and label fields to SettingsPanelController
- Speed range: 0.0 (paused) to 2.0 (double speed), default 1.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add scroll speed slider to Gameplay scene
Wire up the auto-scroll speed slider and label in the Settings panel.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Foundation for converting ProvinceViewFilter to use Scala types.
New Scala view models:
- StatWithCondition - condition enum (Low/Medium/High) with stat value
- ArmyView - faction army with units
- IncomingArmyView - incoming army details with optional unit info
- UnaffiliatedHeroBasics - unaffiliated hero info for province views
- FullProvinceInfo - detailed province information
- ProvinceView - top-level province view combining all the above
DEPROTO_PLAN.md updates:
- Mark Phase 6 Part 1 (ActionResultApplier) as complete
- Update ActionResultProto Consumer Inventory with current status
- Add table of remaining proto usage in actions with blockers
- Update estimated effort and validation checkboxes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add AutoScrollingText component for overflow text in tooltips
A reusable component that automatically scrolls text content that
overflows its container. Features:
- Detects content overflow via ScrollRect
- Shows optional fade gradient at bottom when content overflows
- Waits configurable delay (default 1.5s) before starting to scroll
- Scrolls at configurable speed (default 0.15 normalized units/sec)
- Pauses at bottom, then resets to top and repeats
- Automatically resets when enabled/disabled (e.g., when tooltip opens)
To use on the hero description popup:
1. Ensure the backstory text is inside a ScrollRect
2. Add AutoScrollingText component to the popup panel
3. Assign the ScrollRect reference
4. Optionally create a gradient image for the fade effect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add dynamic height sizing to AutoScrollingText
The component now supports dynamic sizing:
- ScrollRect grows to fit content height
- Caps at available screen space (bottom of panel to top of screen)
- Only scrolls when content exceeds available space
New configuration:
- dynamicHeight: Enable/disable dynamic sizing (default true)
- topMargin: Margin from top of screen in pixels
- layoutElement: LayoutElement to adjust (usually on ScrollRect)
Also fix for text starting partway down: ensure Content pivot is (0.5, 1).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix dynamic height calculation and add debug logging
* Use TMP_Text.preferredHeight for accurate content measurement
The Content RectTransform's rect.height wasn't reflecting the actual
text size, causing the panel to be too small. Now we measure the
TMP_Text's preferredHeight directly and resize the Content to match,
ensuring the ScrollRect can scroll properly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Hide panel during layout to prevent visual jump
- Reset scroll position immediately on enable (both horizontal and vertical)
- Reset content's anchored position to prevent slide-in from right
- Use CanvasGroup to hide panel until layout is complete, preventing
jumpy resize when hovering
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Configure AutoScrollingText in Gameplay scene
Set up the hero description popup with AutoScrollingText component,
including ScrollRect, LayoutElement, otherContent, and CanvasGroup
references for dynamic height and smooth appearance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Set up the hero description popup with AutoScrollingText component,
including ScrollRect, LayoutElement, otherContent, and CanvasGroup
references for dynamic height and smooth appearance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Responses API is OpenAI's newer API that offers:
- Better performance with reasoning models (3% improvement on SWE-bench)
- Lower costs through improved cache utilization (40-80% improvement)
- Semantic streaming events with clear lifecycle events
- Built-in tools support (web search, file search, etc.)
Changes:
- Create OpenAIResponsesServiceImpl that implements ExternalTextGenerationServiceImpl
- Handle semantic streaming events (response.output_text.delta, response.output_text.done, etc.)
- Add to chat_gpt_binary for testing
- Update ExternalTextGenerationCallerApp with option to select Responses API
The implementation uses the /v1/responses endpoint and parses the new
event-based streaming format with typed events like response.output_text.delta.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Optimize OrganizeTroopsCommandSelector for better responsiveness
Performance improvements:
- Remove redundant Update() calls in PlusClickedImpl methods - the caller
(UpdateTable or MaxClickedImpl) calls Update() when needed
- Remove unused Update() call in MinusClicked (result was never used)
- Cache extraTroops counts by type to avoid repeated LINQ queries on each
battalion row
- Fix somethingChanged check to inspect fields directly instead of calling
expensive Update() method inside Exists()
- Remove duplicate maxAllButton.SetActive(false) call
These changes reduce the number of object allocations and iterations
performed on each button click, improving UI responsiveness.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use array instead of Dictionary for extraTroopsByType
Since BattalionTypeId is an enum with sequential values, an array
provides O(1) access without hashing overhead. The array size is
determined dynamically from Enum.GetValues to support future
battalion types.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Reuse table rows instead of destroying/recreating them
Instead of setting RowCount=0 (which destroys all rows) then adding
new rows, we now:
1. Calculate total rows needed
2. Set RowCount to target (adds/removes only as needed)
3. Update existing rows in place with ComponentAt<T>()
This avoids expensive GameObject destruction and instantiation
on every button click, significantly improving responsiveness
with 8+ battalions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Java HttpClient lacks read timeout support for streaming connections.
If the server stops sending data without closing the connection, the
client waits forever. This is a known limitation with no workaround.
OkHttp supports read timeouts via `readTimeout()` on the client builder.
If no data is received for the configured timeout (60s by default), the
connection will timeout with an IOException, allowing proper error
handling and retry.
Changes:
- Add OkHttp and okhttp-sse dependencies to MODULE.bazel
- Create OkHttpSseListener to handle SSE events with CompletableFuture
- Convert ExternalTextGenerationCaller to use OkHttp instead of Java HttpClient
- Add toOkHttpRequest helper to convert Java HttpRequest to OkHttp Request
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The consumer thread had no exception handling. If any exception was
thrown while processing LLM updates (e.g., game not found, null pointer),
the thread would die and ALL future LLM streaming updates would queue
but never be processed - causing every incomplete text to stall.
Now exceptions are caught, logged with the affected update IDs, and the
consumer continues processing. This prevents a single bad update from
killing the entire LLM processing pipeline.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Shardok resync flag cleared before updates received
The resync flag was being cleared immediately after subscription
acknowledgment, but BEFORE the Shardok updates actually arrived.
If the connection dropped between acknowledgment and update delivery,
the flag would already be cleared, so the next reconnect wouldn't
request a resync, leaving the client with stale Shardok state.
The fix removes the premature flag clearing - flags are now only
cleared in EagleGameModel.HandleOneGameUpdate after updates are
actually received.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Optimize MainQueue: skip Stopwatch when queue is empty
Added a fast path to avoid Stopwatch creation when the action queue
is empty, reducing per-frame overhead during normal gameplay. Also
removed unused actionsProcessed variable.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When incomplete texts can't resume due to unsatisfied dependencies
(e.g., the prompt generator needs another text that's also incomplete),
they would get stuck forever. The code detected stalled texts and
logged a warning, but never actually fixed them.
Now, stalled incomplete texts (waiting > 3 minutes) that return
LlmResolverDependencyNotSatisfied are moved back to unrequested state.
This breaks dependency cycles and allows the system to recover by
regenerating prompts with fresh dependency resolution.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert PerformProvinceMoveResolutionAction to use Scala types
- Extend ProtolessRandomSequentialResultsAction instead of TRandomSequentialResultsAction
- Accept ActionResultApplier as constructor parameter
- Use RandomStateSequencer for state tracking with Scala types
- Remove proto conversions (GameStateConverter, ArmyConverter, etc.)
- Update RoundPhaseAdvancer to pass applier to constructor
- Remove unused ActionResultTApplierImpl from RoundPhaseAdvancer
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update test to assert on ActionResultT directly
Remove proto conversion from test - now tests ActionResultC/ChangedProvinceC
directly instead of converting to proto format.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use Scala types for test game state instead of proto
Remove all proto dependencies from test - now uses:
- GameState (Scala case class)
- FactionC, HeroC, ProvinceC (Scala concrete types)
- MovingArmy, Army, CombatUnit, Supplies (Scala types)
- RoundPhase, ProvinceOrderType, Date (Scala enums/types)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of a fixed 10 actions per frame, process actions for up to 8ms
per frame. This allows much faster catch-up when there's a large backlog
while still leaving time for rendering within the 16ms frame budget.
Also increased the logging threshold from 10 to 100 to reduce log noise.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
RedrawCommandOverlays was called with null grid indices when selecting
a reserve unit, but MapCoordsToGridIndex was still called with the
resulting null mapMouseCoords.
Add null check before calling MapCoordsToGridIndex.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove classes that are no longer used after the protoless migration:
- Command.scala - legacy base trait for proto-based commands
- RandomSingleResultCommand.scala - no subclasses remaining
- SimpleActionWrapper.scala - replaced by protoless patterns
- DeterministicSequentialResultsAction.scala - no subclasses remaining
- RandomStateProtoSequencer.scala - replaced by RandomStateSequencer
Also removes Command from CommandFactory's makeCommandInternal return type.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Delete LegacyRandomStateTSequencer and migrate ProtolessSequentialResultsActionWrapper
- Migrate ProtolessSequentialResultsActionWrapper to use protoless RandomStateSequencer
- Delete LegacyRandomStateTSequencer.scala (no longer used)
- Remove legacy_random_state_trait_sequencer target from BUILD.bazel
- Clean up unnecessary proto dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate postCommand to protoless flow and delete wrapper classes
- Add withTCommand and protoless action methods to RandomStateSequencer
- Migrate EngineImpl.postCommand to use protoless RandomStateSequencer
- Delete CommandFactory.makeCommand (no longer used)
- Delete ProtolessSequentialResultsActionWrapper (no longer used)
- Delete ProtolessSimpleActionWrapper (no longer used)
- Delete ProtolessRandomSimpleActionWrapper (no longer used)
The postCommand flow now uses:
1. RandomStateSequencer (protoless) instead of RandomStateProtoSequencer
2. makeTCommand instead of makeCommand
3. withTCommand to execute commands without proto wrapping
4. appliedResultsScala to process results
Proto conversion now only happens at the very end via appliedResultsScala.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate EngineImpl.recursiveTransformT to use protoless RandomStateSequencer
This removes the proto conversion roundtrip in recursiveTransformT by using
the new RandomStateSequencer which works with Scala GameState throughout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md with EngineImpl.recursiveTransformT migration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate EndDiplomacyResolutionPhaseAction to protoless RandomStateSequencer
- Use ActionResultApplier instead of ActionResultTApplier
- Use RandomStateSequencer instead of LegacyRandomStateTSequencer
- All helper methods now accept GameState instead of GameStateProto
- Removed all proto converter calls
- Updated test to use ActionResultApplierImpl and provide a date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert PerformUnaffiliatedHeroesAction to use protoless RandomStateSequencer
- Migrated from LegacyRandomStateTSequencer to RandomStateSequencer
- Changed from ActionResultTApplier to ActionResultApplier
- Updated RoundPhaseAdvancer to pass actionResultApplier
- Updated test to call .results() directly and convert to proto (matching other migrated action tests)
- Updated DEPROTO_PLAN.md to mark action as migrated
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update PerformUnaffiliatedHeroesActionTest to use Scala types directly
- Use .results(SeededRandom(...)) instead of resultsOfExecute()
- Assert on ActionResultT types (HeroChangedResultType, ChangedHeroC, ChangedProvinceC)
- Use inside() pattern for safe type matching instead of asInstanceOf
- Add Scala testing patterns guidance to CLAUDE.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor PerformUnaffiliatedHeroesActionTest to use Scala GameState directly
Instead of constructing proto GameState and converting to Scala,
the test now creates Scala GameState directly with all required fields.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When TextId was set but the text entry hadn't arrived yet, the
TMP_Text component still displayed whatever was previously there.
This caused a brief flash of old text before the new streaming
text started appearing.
Now UpdateView() is called immediately when TextId changes,
clearing any stale content even if the new text hasn't arrived yet.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add tracking and detection for incomplete texts that have been waiting
for LLM responses for longer than 3 minutes:
- Add `requestedAtMillis` field to `IncompleteClientText` to track when
the LLM request was submitted
- Add `requested_at_millis` field to proto message for persistence
- Add `stalledIncompleteTexts` method to `ClientTextStore` to find texts
that have exceeded the threshold
- Log warnings in `clientTextStoreWithHandledIncompleteTexts` when
stalled texts are detected, showing text ID, wait time, and partial
content
This helps diagnose issues where LLM responses are not being received
or processed properly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When clientTextStoreWithHandledIncompleteTexts is called at startup to
resume incomplete texts, if LlmResolverTooManyRequestsInFlight is
returned for any text, those texts were silently dropped and never
retried. This happened because:
1. They stayed in "incomplete" state (not picked up by unrequested handler)
2. The method only runs once at startup
3. No callback would ever come since the LLM was never called
Fix: Move texts that couldn't be submitted back to unrequested state
so they get retried via the normal handler loop.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate EndBattleAftermathPhaseAction to protoless RandomStateSequencer
- Replace LegacyRandomStateTSequencer with RandomStateSequencer
- Convert deferredChangeAR to use Scala DeferredChangeT types instead of proto
- Update allDeferredChanges and convertToUnaffiliated to take Scala GameState
- Replace ActionResultTApplier with ActionResultApplier
- Keep lazy proto conversion for ProvinceViewFilter calls in revelationChange
- Remove unused proto converter imports and dependencies
- Update tests to use ActionResultApplierImpl and Scala GameState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN with migration progress and ProvinceView needs
- Add NewRoundAction and EndBattleAftermathPhaseAction to completed migrations
- Add EndDiplomacyResolutionPhaseAction and PerformUnaffiliatedHeroesAction as pending
- Add View Filters section documenting ProvinceViewFilter blocking full deproto
- Document need for Scala ProvinceViewT model
- Update Open Questions about view generation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate EndPlayerCommandsPhaseAction to protoless RandomStateSequencer
- Use Scala DeferredChangeT types instead of proto DeferredChange
- Accept ActionResultApplier instead of ActionResultTApplier
- Use RandomStateSequencer which passes Scala GameState to callbacks
- Update test to use ActionResultApplierImpl
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate NewRoundAction to protoless RandomStateSequencer
- Changed NewRoundAction to extend ProtolessRandomSequentialResultsAction
- Added actionResultApplier parameter to NewRoundAction constructor
- Updated RoundPhaseAdvancer to pass actionResultApplier to NewRoundAction
- Updated test to use new API with helper to convert results to proto for assertions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace isInstanceOf with pattern matching in EndPlayerCommandsPhaseAction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
OnEnable() calls AddListener() which calls TextId(), and SetUp() accesses
CurrentEntry - both throw IndexOutOfRangeException when _entries is empty.
Add guards to return early/null when there are no entries.
Also fix the logic for jumping to the last entry - previously it only
checked if gameObject was inactive, but now that OnEnable doesn't crash,
the object is active before entries are populated. Check if entries were
previously empty as well.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Revert to using the proven iterative deepening AI algorithm instead of
MCTS for tactical combat decisions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Migrate EndVassalCommandsPhaseAction to protoless RandomStateSequencer
- Change from TRandomSequentialResultsAction to ProtolessRandomSequentialResultsAction
- Use ActionResultApplier instead of ActionResultTApplier
- Use RandomStateSequencer instead of LegacyRandomStateTSequencer
- Update RoundPhaseAdvancer to pass ActionResultApplier directly
- Add BattalionTypeConverter for proto conversion of battalionTypes parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate PerformReconResolutionAction to protoless RandomStateSequencer
- Change from TRandomSequentialResultsAction to ProtolessRandomSequentialResultsAction
- Use ActionResultApplier instead of ActionResultTApplier
- Use RandomStateSequencer instead of LegacyRandomStateTSequencer
- Convert from proto IncomingEndTurnAction to Scala IncomingEndTurnAction
- Update RoundPhaseAdvancer to pass ActionResultApplier directly
- Keep lazy proto GameState conversion only for ProvinceViewFilter.filteredProvinceView
- Update test to use ActionResultApplierImpl and Scala types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The warning was logged when a Shardok update arrived for a battle that
Eagle had already removed via RemovedBattleIds. This is expected behavior
and handled correctly - the UI shows "Back to Eagle" via MarkBattleEnded().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change both actions to use RandomStateSequencer instead of LegacyRandomStateTSequencer
- Use ActionResultApplier instead of ActionResultTApplier
- Use TCommandFactory instead of CommandFactory
- Use Scala ProvinceUtils instead of LegacyProvinceUtils
- Update RoundPhaseAdvancer callers to use new parameter names
- Update PerformVassalCommandsPhaseActionTest to use new types and chooseCommand signature
- Update BUILD.bazel dependencies for both actions and test
- Mark both actions as migrated in DEPROTO_PLAN.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add null check in Triangulate() to guard against HexGrid.Update()
calling overlayMesh.Triangulate() before SetUp() has initialized
the hexMesh field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
EndPlayerCommandsPhaseAction was using gameStateProto.deferredNotifications
(the initial state) instead of gs.deferredNotifications (the current state
from the sequencer). This caused notifications to not be properly removed
and accumulate across phases.
This is the same bug that was fixed in EndVassalCommandsPhaseAction in PR #4686.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When reconnecting after being backgrounded, many AddNote calls queue up
in MainQueue. If the user clicked Dismiss All, it would clear the current
notes but the queued AddNote calls would immediately add more.
Fix: Use a generation counter that increments on Dismiss All. Pending
AddNote calls capture the generation when enqueued and skip if it changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Server sends ServerGameStatus in ActionResultResponse
Include server-reported game status in every ActionResultResponse:
- YOUR_TURN: when availableCommands is present with commands
- WAITING_FOR_PLAYERS: when no commands available
This allows the client to display accurate server state rather than
inferring it from local data. Detecting mismatches between server
status and client state can reveal desync issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Report YOUR_TURN or WAITING_FOR_PLAYERS status
Change from previous approach: now always report a status instead of
returning None when no commands. This gives the client useful information:
- YOUR_TURN when player has commands available
- WAITING_FOR_PLAYERS when player doesn't have commands
GENERATING_TEXT would require threading clientTextStore access through
to HumanPlayerClientConnectionState, which is a larger refactoring.
For now, WAITING_FOR_PLAYERS covers the common case.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Calculate ServerGameStatus properly based on actual game state
GameController now calculates status based on what we actually know:
- YOUR_TURN: when this player has commands available
- GENERATING_TEXT: when there are incomplete LLM texts for this player
- WAITING_FOR_PLAYERS: when other human players have commands
- None: when we don't know (e.g., waiting for AI or battle resolution)
This is more accurate than always returning WAITING_FOR_PLAYERS when
the player has no commands.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add mock expectation for incompleteTexts in GameControllerTest
The test was failing because humanClientsAfterPostingResults now calls
clientTextStore.incompleteTexts to check for in-progress LLM text generation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Same fix as Eagle counts - track Shardok result counts in a thread-safe
dictionary updated immediately on the gRPC thread before enqueueing to
MainQueue. This ensures heartbeats report accurate counts even when
MainQueue is blocked (e.g., Unity backgrounded).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When Unity is backgrounded and a Shardok battle ends, there's a race
condition where the Eagle update (removing the battle from ShardokBattles)
may arrive before the Shardok Victory update. This caused the user to be
stuck on the Shardok canvas with no "Back to Eagle" button.
Fix: When processing RemovedBattleIds, check if there's an active
ShardokGameModel and call MarkBattleEnded() to trigger the controller's
return-to-Eagle logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
EndHandleRiotsPhaseAction and EndVassalCommandsPhaseAction were using
the initial gameState's deferredNotifications instead of the current
state from the sequencer. This bug was introduced in PR #2679 (May 2023).
While this was a latent bug, it could cause issues if notifications were
added/removed during sequencer operations before the end-phase result.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate EndHandleRiotsPhaseAction to protoless RandomStateSequencer
- Use RandomStateSequencer instead of LegacyRandomStateTSequencer
- Use ActionResultApplier instead of ActionResultTApplier
- Use ProvinceUtils.hasImminentRiot instead of LegacyProvinceUtils
- Match on TCommand cases to execute commands properly
- Update test to include rulingFactionHeroIds and hero in game state
- Update DEPROTO_PLAN.md with migration progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Extract TCommandFactory trait for lightweight mocking
- Create TCommandFactory trait with just makeTCommand method
- CommandFactory now extends TCommandFactory
- EndHandleRiotsPhaseAction accepts TCommandFactory instead of CommandFactory
- Test mocks TCommandFactory to avoid pulling in 40+ command dependencies
- Update DEPROTO_PLAN.md with migration progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix: Use current game state for deferred notifications
Was using initial gameState instead of current gs from sequencer,
causing deferred notifications to not be properly tracked.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Scala ActionResultApplierImpl.applyNewNotifications was incorrectly
adding ALL notifications to deferredNotifications, including ones with
deferred=false. This caused notifications to be delivered repeatedly.
The proto path handled this correctly by checking the deferred flag and
routing non-deferred notifications to notificationsToDeliver instead.
This regression was introduced in PR #4661 when ActionResultApplierImpl
was created, and became visible when actions started using the protoless
RandomStateSequencer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This function was missing from ProvinceUtils but present in
LegacyProvinceUtils. Adding it enables EndHandleRiotsPhaseAction
to be migrated away from proto dependencies.
Also updates DEPROTO_PLAN.md to document LegacyProvinceUtils
migration progress.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change base class from TRandomSequentialResultsAction to ProtolessRandomSequentialResultsAction
- Change constructor parameter from ActionResultTApplier to ActionResultApplier
- Use RandomStateSequencer instead of LegacyRandomStateTSequencer
- Update RoundPhaseAdvancer call site to pass ActionResultApplier
- Rewrite test to use pure Scala types (ProvinceC, FactionC, GameState) instead of proto types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add DEVELOPER_DIR repo_env to .bazelrc so bazel always uses the current
Xcode installation rather than caching the version. This avoids the need
for `bazel clean --expunge` after Xcode updates.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Rename RandomStateTSequencer to LegacyRandomStateTSequencer
- Create new fully protoless RandomStateSequencer in its own package
- Update all 13 action usages to import LegacyRandomStateTSequencer
- The new sequencer uses Scala GameState throughout (no proto conversions)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert RoundPhaseAdvancer to accept Scala GameState instead of proto
This eliminates unnecessary proto conversions since EngineImpl already has
Scala GameState. Previously it converted to proto just to call
checkForPhaseAdvancement, and inside that method most actions immediately
converted back to Scala.
Changes:
- RoundPhaseAdvancer.checkForPhaseAdvancement now takes Scala GameState and
ActionResultApplier (returns ActionResultWithResultingState)
- Added lazy proto conversion only for AvailableCommandsFactory calls
- Updated match cases to use Scala RoundPhase values (NewRound, etc.)
- Added EngineImpl.appliedResultsScala and recursiveTransformScala helpers
- Added GameHistory.withNewResultsScala default method
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use Scala RoundPhase instead of proto for timing map
- Added RoundPhase.allValues to enumerate all round phases
- Removed RoundPhaseProto import from RoundPhaseAdvancer
- Updated times map to use Scala RoundPhase
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete recursiveTransform, have recursiveTransformT use RandomStateTSequencer
- recursiveTransform was only called by recursiveTransformT
- recursiveTransformT now uses recursiveTransformScala with RandomStateTSequencer
- Converts ActionResultTWithResultingState (proto GameState) to
ActionResultWithResultingState (Scala GameState) at the boundary
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Accept Scala GameState in constructor instead of proto
- Use RandomStateTSequencer.apply() which takes Scala GameState
- Update helper methods to use GameStateProto type alias for clarity
- Update test to construct Scala GameState directly
- Update DEPROTO_PLAN.md with Phase 5c progress and sequencer migration plan
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change action to accept Scala GameState, convert to proto internally
- Update internal methods to use GameStateProto explicitly
- Add game_state_converter dependency to BUILD files
- Update tests to use GameStateConverter.fromProto and randomResults
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changes:
- Rewrote RequestFreeForAllBattlesAction to take Scala GameState
- Extends ProtolessSequentialResultsAction instead of DeterministicSequentialResultsAction
- Uses Scala types: ShardokBattle, ShardokPlayer, HostileArmyGroup, BattleType, VictoryCondition
- Uses BattalionUtils instead of LegacyBattalionUtils for food calculation
- Updated RoundPhaseAdvancer to use the protoless action
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert PerformHeroDeparturesAction to use Scala types
Changes:
- Rewrote PerformHeroDeparturesAction to take Scala GameState and return ActionResultT
- Added effectiveLoyalty method to HeroUtils (Scala version)
- Added afterHeroDeparture method to ProvinceUtils
- Updated RoundPhaseAdvancer to use the protoless action
- Rewrote tests to use pure Scala types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use inside() pattern instead of asInstanceOf in tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change action to take Scala GameState instead of proto
- Implement ProtolessSequentialResultsAction trait
- Update internal logic to use Scala model types (Army, MovingArmy, MovingSupplies, etc.)
- Rewrite tests to use pure Scala model objects
- RoundPhaseAdvancer converts to/from proto at the boundary
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace proto GameState with Scala GameState
- Replace proto ActionResult with ActionResultT/ActionResultC
- Replace proto ChangedHero/ChangedProvince with Scala versions
- Use NotificationDetails.PrisonerExchange for notifications
- Implement ProtolessSequentialResultsAction trait
- Rewrite test to use pure Scala model objects
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous log only showed whether eagle/shardok were in sync (true/false).
Now it shows the actual counts from both client and server, making it easier
to diagnose the cause of sync mismatches.
Example output:
[HEARTBEAT] Detected sync mismatches for user: game 123: eagle: client=50 server=52
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add `apply(gameState: GameState)` method as the preferred entry point
- Use FactionUtils.alliedFactions instead of LegacyFactionUtils
- Thread Scala GameState through the generator
- Maintain fromGameState(GameStateProto) for backwards compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace LegacyFactionUtils.hasTruceOrAlliance with FactionUtils.hasTruceOrAlliance
- Use Scala FactionT and ProvinceT instead of proto types
- Remove GameStateConverter.toProto() call (was converting Scala to proto unnecessarily)
- Update BUILD.bazel deps: remove legacy_faction_utils and proto_converters/game_state,
add faction_utils and state/faction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert three more actions to take Scala GameState
- EndFreeForAllDecisionPhaseAction: now takes Scala GameState directly
- EndBattleRequestPhaseAction: renamed fromProtoState to apply, takes Scala GameState
- EndDefenseDecisionPhaseAction: renamed fromProtoState to apply, takes Scala GameState
Updated RoundPhaseAdvancer callers to use GameStateConverter.fromProto().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document Scala 3 compiler crash blocker for EndPleaseRecruitMePhaseAction
When attempting to convert EndPleaseRecruitMePhaseAction to take Scala GameState,
the Scala 3.7.2 compiler crashes during the lambdaLift phase.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert EndPleaseRecruitMePhaseAction to Scala GameState and fix test
- Convert EndPleaseRecruitMePhaseAction to take Scala GameState directly
- Rewrite EndDefenseDecisionPhaseActionTest to use pure Scala model objects
(instead of creating proto GameState and converting)
- Fix test to expect correct phase transition (TruceTurnBack, not BattleRequest)
- Update BUILD.bazel deps for both action and test
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md - mark EndPleaseRecruitMePhaseAction complete
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Update EndBattleAftermathPhaseAction case class to take Scala GameState
- Add private gameStateProto field for internal proto conversion
- Rename companion object method parameters to clarify proto vs Scala types
- Update RoundPhaseAdvancer caller to convert proto to Scala
- Update tests to use GameStateConverter.fromProto()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate callers from LegacyUnaffiliatedHeroUtils to UnaffiliatedHeroUtils
This is Phase 6 of the deproto migration, cleaning up legacy utility files.
Changes:
- Add willPleaseRecruitMe convenience method to UnaffiliatedHeroUtils
- Add updatedForQuest and maybeUpdatedForQuest methods to UnaffiliatedHeroUtils
- Convert EndBattleAftermathPhaseAction to use Scala types
- Convert UnaffiliatedHeroMovedAction to use Scala types
- Convert AvailablePleaseRecruitMeCommandFactory to use Scala types
- Delete LegacyUnaffiliatedHeroUtils (no more callers)
- Update test fixtures to include required roundPhase/currentPhase
- Update BUILD.bazel visibility and deps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify AvailablePleaseRecruitMeCommandFactory to avoid dual GameState params
Remove the pattern of passing both proto and Scala GameState to internal
methods. Now forOneProvince takes only proto GameState and converts to
Scala internally where needed for willPleaseRecruitMe.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AvailablePleaseRecruitMeCommandFactory to use Scala GameState
Changed the factory to accept Scala GameState instead of proto GameState,
moving toward the deproto goal. The conversion flow is now:
- Caller passes Scala GameState
- Factory works with Scala types directly
- Only converts to proto for ExpandedUnaffiliatedHeroUtils (still proto-based)
Updated:
- AvailablePleaseRecruitMeCommandFactory to take Scala types
- AvailableCommandsFactory to convert proto->Scala before calling
- Test to pass Scala GameState
- BUILD.bazel files with required deps and visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make validator optional in ActionResultApplierImpl with type class generics
- Change ActionResultApplierImpl to take Option[ScalaValidator]
- Use Scala 3 type classes (Validatable, ValidatableWithGameState) for generic validation
- Single generic validate[T] method handles HeroT, GameState, ActionResultT
- Single generic validate[T](value, gs) method handles BattalionT with GameState context
- Update ActionResultTApplierImpl to wrap validator in Some()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ProtolessSequentialResultsActionWrapper to use ScalaRuntimeValidator
- Update to use ActionResultTApplierImpl with ScalaRuntimeValidator
- Export action_result_applier from action_result_trait_applier_impl
- Remove unused ActionResultApplierImpl import
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix test failures after ActionResultTApplierImpl changes
- Create TestingNoopScalaValidator for tests that don't need real validation
- Update tests to use TestingNoopScalaValidator instead of ScalaRuntimeValidator
- Add currentPhase to test GameState objects to fix proto-to-Scala conversion
- Add valid date fields to BackstoryVersion in test data
- Update BUILD.bazel files with correct dependencies
- Export game_state from action_result_trait_applier_impl
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use no-validation applier in TRandomSequentialResultsAction for tests
- Change TRandomSequentialResultsAction.execute() to use ActionResultTApplierImpl()
instead of ActionResultTApplierImpl(ScalaRuntimeValidator) to avoid validation
errors on synthetic test data
- Add apply() factory method to ActionResultTApplierImpl that creates an applier
with no validation (Option[ScalaValidator] = None)
- Export scala_validator from action_result_trait_applier_impl so the type is
visible to dependents
- Update test files to use ActionResultTApplierImpl() instead of
ActionResultTApplierImpl(TestingNoopScalaValidator)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused TestingNoopScalaValidator
Use None instead of TestingNoopScalaValidator for tests that don't need validation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add required date field to BackstoryVersion in test
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ScalaValidator and use in ActionResultApplierImpl
Introduce a Scala-native validation interface (ScalaValidator) and its
implementation (ScalaRuntimeValidator) for validating game state during
action result application.
Changes:
- Add ScalaValidator trait with methods to validate heroes, battalions,
provinces, and action results using Scala types
- Add ScalaRuntimeValidator implementing validation logic
- Update ActionResultApplierImpl to accept an optional ScalaValidator
- Add visibility rules for validations package to access required types
- Add ScalaRuntimeValidatorTest
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove stale testing_noop_scala_validator target
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ActionResultApplierImplTest to pass None for validator
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ActionResultApplier for direct Scala GameState manipulation
Phase 6 of deproto migration: Create ActionResultApplier infrastructure
that applies ActionResultT directly to Scala GameState without proto
conversion.
New components:
- ActionResultApplier trait - interface for applying action results
- ActionResultApplierImpl - implementation using extension methods
- GameState extension methods split across multiple files:
- GameStateProvinceExtensions - province operations
- GameStateBattalionExtensions - battalion operations
- GameStateHeroExtensions - hero operations
- GameStateFactionExtensions - faction operations
- GameStateBattleExtensions - battle operations
- GameStateMiscExtensions - notifications, seed, chronicle, etc.
- GameStateExtensions - aggregator that re-exports all extensions
- ProvinceUpdateHelpers/2 - complex province update logic
Note: ActionResultProtoApplier is still used throughout the codebase
(EngineImpl, RoundPhaseAdvancer, Actions, Commands). This new applier
is infrastructure for future migration when we switch to Scala GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add ActionResultApplierImplTest for Scala GameState ActionResultApplier
Adds comprehensive test coverage for ActionResultApplierImpl that matches
the proto-based ActionResultProtoApplierImplTest:
- Basic state updates (round id, phase, date, seed, game ended, victor)
- Battalion operations (changed, zero size/destroy, new, removed)
- Hero operations (vigor delta/absolute, new, removed, stat deltas, XP)
- Faction operations (new, changed head, trust levels, removed, outgoing offers)
- Battle operations (new battle)
- XP for stat bump calculations
- Multiple results in sequence
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update client to use ServerGameStatus from ActionResultResponse:
- IGameStateProvider now has ServerStatus instead of inferring state
- GameModelUpdater stores ServerStatus when receiving ActionResultResponse
- ConnectionStatusUI displays server-reported status:
- YOUR_TURN -> "Your turn"
- WAITING_FOR_PLAYERS -> "Waiting for other players"
- GENERATING_TEXT -> "Generating..."
- PROCESSING_ACTION -> "Processing..."
Client-side IsProcessingCommand still takes priority (for responsive
feedback when submitting commands, before server responds).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Completed phases (1-5b) are now summarized in a table. The plan now
focuses on Phase 6: migrating from ActionResultProto consumers to
ActionResultT consumers throughout the engine.
Key finding: No code directly produces ActionResultProto anymore - all
production goes through ActionResultProtoConverter.toProto() from
ActionResultT. The next step is eliminating internal consumption.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Rate-limit MainQueue to prevent blocking when resuming from background
When Unity is backgrounded during a Shardok game, the gRPC stream
continues receiving updates which queue up in MainQueue. Previously,
Update() would process all queued actions in a single frame, causing
the UI to freeze/spin when resuming.
This change limits processing to 10 actions per frame, spreading the
work across multiple frames and keeping the UI responsive. Also adds
logging when the queue has built up, to help diagnose similar issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix duplicate updates when reconnecting while Unity is backgrounded
Root cause: When Unity is backgrounded, MainQueue.Update() doesn't run,
so ReceiveGameUpdate() never processes updates and _lastUnfilteredResultCount
never advances. When the connection times out and reconnects, it sends the
stale count, causing the server to re-send all the same updates. This
repeats with each reconnect, accumulating duplicates.
Fix: Call UpdateResultCounts() immediately on the gRPC thread when updates
arrive, BEFORE enqueueing to MainQueue. This ensures reconnects always use
accurate counts regardless of MainQueue state.
Also adds duplicate detection in Notification.Append() as a defense-in-depth
measure to prevent the same text from being appended multiple times.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Implement UpdateResultCounts in CustomBattleHandler
CustomBattleHandler only handles Shardok updates, so the implementation
is a no-op.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert PerformProvinceEventsAction to pure Scala types and delete RandomSequentialResultsAction
- Convert PerformProvinceEventsAction to use ProtolessRandomSequentialResultsAction
with pure Scala types (zero proto dependencies in action logic)
- Add BeastUtils.beastInfosT for T-type BeastInfo access
- Update RoundPhaseAdvancer to pass both GameStateProto and applier to execute()
- Delete RandomSequentialResultsAction base class (no longer used)
- Update PerformProvinceEventsActionTest to use T-types with proper casting
- Move Actions and ActionResultT to "What's Done" in DEPROTO_PLAN.md
All 10 RandomSequentialResultsAction subclasses are now converted to T-type base classes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove proto BeastInfo from BeastUtils and update tests to use T-types
- BeastUtils.beastInfos now returns T-type BeastInfo (removed proto version)
- SuppressBeastsPromptGenerator updated to use T-type BeastInfo
- PerformProvinceEventsAction: replace isInstanceOf with pattern matching
- PerformProvinceEventsActionTest: construct T-type test data directly
instead of proto data that gets converted
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move province utility methods to ProvinceUtils
- Move effectiveEconomy and effectiveInfrastructure usage from local methods
to existing ProvinceUtils implementations
- Add hasBlizzard, hasDrought, hasFlood, hasFestival, hasEpidemic, hasBeasts
predicates to ProvinceUtils
- Remove duplicate local methods from PerformProvinceEventsAction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add game state to connection status indicator
When connected, the status indicator now shows game-specific state:
- "Generating..." - LLM text generation in progress (highest priority)
- "Processing..." - Command submitted, awaiting response (only if > 500ms)
- "Your turn" - Player has available commands
- "Waiting for other players" - No commands, waiting for opponents
Implementation:
- Add IGameStateProvider interface in ConnectionStatusUI.cs
- Implement interface in GameModelUpdater with:
- HasAvailableCommands: check AvailableCommandsByProvince and CommandToken
- IsStreamingTextInProgress: check ClientTextProvider for incomplete entries
- IsProcessingCommand: track command submission time (500ms delay to avoid flash)
- Wire up in EagleGameController when entering/leaving game
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add ServerGameStatus proto for server-reported game state
Add ServerGameStatus message to ActionResultResponse:
- YOUR_TURN: Player has commands available
- WAITING_FOR_PLAYERS: Waiting for other player(s) to act
- GENERATING_TEXT: LLM text generation in progress
- PROCESSING_ACTION: Server is processing an action
Includes waiting_for_faction_ids and generating_llm_id for additional context.
This allows the client to display accurate server state rather than
inferring it from local data, which enables detecting desync issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- handleHeartbeat now checks client's reported counts against server's
- Compares Eagle unfiltered_result_count and Shardok filtered counts
- Returns GameSyncResult/ShardokSyncResult only for mismatched games
- Logs detected mismatches for debugging
Backwards compatible: old client sends HeartbeatRequest without
GameSyncStatuses, server handles empty list (no sync checks).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert remaining RandomSequentialResultsAction subclasses to TRandomSequentialResultsAction
- Convert EndHandleRiotsPhaseAction to TRandomSequentialResultsAction
- Convert PerformVassalCommandsPhaseAction to TRandomSequentialResultsAction
- Convert PerformVassalDefenseDecisionsAction to TRandomSequentialResultsAction
- Add withRandomAction and withOptionalRandomAction to RandomStateTSequencer
- Create ActionResultProtoWrapper to wrap proto ActionResult as ActionResultT
- Update VigorXPApplier to skip proto-wrapped results
- Expose protoApplier on ActionResultTApplierImpl for sequencer access
This enables executing proto Actions from CommandFactory.makeCommand() within
the T-based sequencer by wrapping results in ActionResultProtoWrapper.
9/10 RandomSequentialResultsAction subclasses now converted. Only
PerformProvinceEventsAction remains (heavily proto-based internally).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert vassal command Actions to use T-type commands via TCommand sealed trait
- Create TCommand sealed trait unifying Simple, RandomSimple, and Sequential T-type actions
- Add makeTCommand method to CommandFactory returning T-type actions directly
- Add withTCommand/withOptionalTCommand helpers to RandomStateTSequencer
- Convert PerformVassalCommandsPhaseAction, PerformVassalDefenseDecisionsAction,
and EndHandleRiotsPhaseAction to use T-type commands
- Add executeProtolessAction helper in RoundPhaseAdvancer to bridge T-type actions
with proto-based engine interface
- Delete ActionResultProtoWrapper (no longer needed after T-type conversion)
- Add exports to action_result_trait for interface types to support ScalaMock mocking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add VigorXPApplier.withVigorXp to test helper to match production behavior
Addresses Copilot review comment about test executeAction helper missing
vigor XP application that RoundPhaseAdvancer.executeProtolessAction does.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove actionResultProtoApplier from TRandomSequentialResultsAction.randomResults
TRandomSequentialResultsAction subclasses should only use ActionResultTApplier,
not both appliers. The execute() method creates the T-type applier internally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add execute method to ProtolessRandomSequentialResultsAction
Move the duplicate executeAction/executeProtolessAction helper code
into a proper execute() method on ProtolessRandomSequentialResultsAction.
This eliminates code duplication between tests and RoundPhaseAdvancer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add troubleshooting guidance for Scala MissingType errors
Document that MissingType errors are BUILD.bazel dependency issues,
not compiler crashes. Also note to never run bazel clean without asking.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add heartbeat timer (10s interval) that sends HeartbeatRequest with:
- GameSyncStatus per subscribed game (unfiltered_result_count)
- ShardokSyncStatus per tactical battle (filtered_result_count)
Handle HeartbeatResponse with sync results:
- Log detailed mismatch information for debugging
- Trigger reconnect when server reports sync mismatch
- Reconnect will re-subscribe and server sends missing updates
Backwards compatible: old server ignores new request fields,
new client handles empty sync results (no reconnect triggered).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Extend heartbeat messages to support sync verification:
HeartbeatRequest now includes:
- GameSyncStatus per subscribed game with unfiltered_result_count
- ShardokSyncStatus per tactical battle with filtered_result_count
HeartbeatResponse now includes:
- GameSyncResult per game indicating if counts match
- ShardokSyncResult per battle with server's counts for comparison
This allows client to report its known action counts, and server to
detect desync and trigger resync if needed. Fields are optional so
this is backwards-compatible with existing clients/servers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Reconnect on subscription failure instead of silently proceeding
Previously, when StreamOneGameAsync() failed (timeout or server rejection),
we logged "subscribe_partial_failure" but still set state to Connected.
This left users with a green status light but no game updates - a silent
failure that's confusing and unrecoverable without manual intervention.
Now when subscription fails:
- Log "subscribe_failed" (clearer than "partial_failure")
- Record circuit breaker failure
- Schedule reconnect with exponential backoff
- Do NOT proceed to Connected state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add resource cleanup before reconnect on subscription failure
Copilot correctly identified that returning early without cleanup
could leave the streaming call and background thread running. When
Connect() later disposes the streaming call, HandleStreamingCall
would catch an exception and schedule its own reconnect - causing
a race condition.
Now we clean up consistently with other failure paths:
- Dispose streaming call and cancel thread token
- Mark Shardok games for resync
- Cancel pending subscription acks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Clarifies the method name to indicate it accepts a proto GameState directly,
distinguishing it from the other apply() that takes a T-type GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Client changes:
- Added SubscriptionPending state shown as "Subscribing..." in status UI
- Subscribe() now returns Task<bool> to indicate success/failure
- Wait for server ack with 10-second timeout using CancellationTokenSource
- Handle OperationCanceledException separately from other errors
- Move TrySetResult outside lock to avoid potential deadlock
- Clear resync flags only after successful acknowledgment
- Cancel pending acks on connection drop
API changes:
- Subscribe() returns Task<bool> instead of Task
- StartListeningForUpdates() returns Task<bool> instead of Task
- Callers using fire-and-forget pattern still work (failures logged)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
If responseObserver.onNext() throws when trying to send a failure ack
(e.g., because the observer is already closed), we don't want that
exception to propagate and potentially cause duplicate ack attempts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Add SubscriptionAck message for server to confirm subscriptions
Server now sends SubscriptionAck after processing StreamGameRequest:
- Success=true with confirmedResultCount on successful subscription
- Success=false with error message on failure
This is backward compatible - existing clients will ignore the new message.
Client-side handling will be added in a follow-up PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Address Copilot review comments
- Remove errorMessage from success case (per proto contract)
- Handle null getMessage() with Option().getOrElse("")
- Remove confirmedResultCount from error cases (not needed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Previously, StreamOneGame used fire-and-forget for subscription writes,
meaning if the write failed (network issue, server not ready), the client
would never know and would wait forever for updates that never arrive.
Changes:
- Convert StreamOneGame to StreamOneGameAsync that returns Task<bool>
- Restructure Connect() to collect subscribers under lock, then await
subscription writes outside the lock
- Make Subscribe() async and await the subscription write
- Move resync flag clearing to AFTER successful send (if send fails,
flags remain set for next reconnect attempt)
- Add diagnostic logging for subscription success/failure
This addresses the root cause of connection instability where clients
would "connect" successfully but never receive data because the
subscription write silently failed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The previous hook ran gazelle but didn't check if it modified any files.
This meant commits could go through with non-canonical BUILD files, causing
gazelle_test to fail in CI.
The new wrapper script:
1. Runs gazelle
2. Checks if any BUILD files were modified
3. Fails with a helpful message if they were, instructing the user to stage changes
Also adds a Pre-Commit Checklist section to CLAUDE.md documenting this behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Check if the stream is cancelled before calling onNext/onError/onCompleted
to prevent IllegalStateException when client disconnects while server is
sending messages.
The ServerCallStreamObserver.isCancelled() method detects when the client
has cancelled the stream, allowing us to silently skip sends rather than
throwing an exception.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When ScheduleReconnect schedules a Connect() call in 2 seconds, but then
a connection succeeds before that timer fires (e.g., through immediate
retry), the scheduled reconnect would still fire and dispose the working
connection, causing:
1. connect_success (connection works)
2. 2 seconds later: scheduled Connect() fires
3. Connect() disposes the working streaming call
4. Working thread catches Cancelled, calls ScheduleReconnect
5. But new connection also succeeds immediately
6. 2 seconds later, repeat forever...
The fix cancels and disposes the retry timer when a connection succeeds,
preventing stale scheduled reconnects from killing working connections.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The notification deduplication logic used SequenceEqual on HeroView objects,
but HeroView is a protobuf-generated class that uses reference equality.
Each time an ActionResultView is processed, new HeroView instances are created,
so even notifications about the same heroes were treated as different.
Changed to compare hero lists by their Id field instead of by object reference.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
During reconnection, PopupPanelController.Start() or SetUpPanel() runs
before MapController.Model has been set. When clearing OverrideTargetedProvinces,
SetDefaultProvinceColor tries to access Model.Provinces which is null.
Added null check in SetDefaultProvinceColor to handle the case where Model
hasn't been initialized yet during reconnection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When a connection drops (e.g., DeadlineExceeded after 300s), the reconnect
logic would create a new connection but immediately kill it:
1. Old connection times out, _lastResponseReceived is ~5 minutes old
2. ScheduleReconnect() schedules Connect() with backoff
3. Connect() creates new streaming call, logs connect_success
4. Connect() calls StartIdleCheckTimer()
5. IdleCheckTimer fires within 5s, checks _lastResponseReceived
6. idleTime > MaxIdleSeconds (30s) because timestamp is from OLD connection
7. CheckForIdleTimeout() disposes the NEW connection
8. Triggers "Cancelled" exception, ScheduleReconnect again
9. Loop repeats forever
The fix resets _lastResponseReceived to DateTime.UtcNow when a new
connection is established, before starting the idle check timer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When the server sends Eagle results containing a date change before Shardok
results, the client clears its ShardokGameModels on the date change, then
receives Shardok updates for battles that no longer have models. This causes
the client to create fresh models with empty history and trigger unnecessary
resyncs.
Fix by sending Shardok results first, so they land in existing models before
the Eagle date change clears them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Change base class from RandomSequentialResultsAction to TRandomSequentialResultsAction
- Use T-based types: ActionResultC, ChangedProvinceC, ChangedHeroC, ChangedFactionC
- Use LlmRequestT.ChronicleUpdateMessage for chronicle requests
- Use ChronicleEventConverter.fromProto to convert proto events to T-types
- Use UnaffiliatedHeroConverter.fromProto for unaffiliated hero updates
- Add newChronicleEntry field to ActionResultT/ActionResultC
- Update BUILD.bazel dependencies and visibility for chronicle_entry, unaffiliated_hero, quest
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Unity 6.3 adds HTTP/2 support on Windows, Mac, Linux, and Android,
which may allow us to remove the YetAnotherHttpHandler dependency
in a future PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Change base class from RandomSequentialResultsAction to TRandomSequentialResultsAction
- Replace proto ActionResult with ActionResultC
- Replace proto ChangedProvince/ChangedFaction/ClientTextVisibilityExtension with T-based equivalents
- Update test to use T-based types
- Update DEPROTO_PLAN.md: 5/10 actions now converted
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Add Phase 8: Create Scala-Native Sequencer to deproto plan
Documents the future goal of creating a ScalaOnlySequencer that operates
entirely on Scala GameState, eliminating per-callback proto conversions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert PerformUnaffiliatedHeroesAction and PerformProvinceMoveResolutionAction to TRandomSequentialResultsAction
- PerformUnaffiliatedHeroesAction: Was already mostly T-based internally,
now extends TRandomSequentialResultsAction and uses RandomStateTSequencer
- PerformProvinceMoveResolutionAction: Uses T-based sub-actions
(FriendlyMoveAction, ShipmentArrivedAction), converted to use
ActionResultTApplier and ActionResultTWithResultingState
- Updated BUILD.bazel dependencies for both actions
- Updated DEPROTO_PLAN.md with progress (4/10 actions converted)
Phase 5b progress: 4/10 RandomSequentialResultsAction subclasses converted.
Remaining 6 actions blocked on CommandFactory or direct proto construction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add TRandomSequentialResultsAction and convert first two actions
- Create TRandomSequentialResultsAction base class for actions that:
- Take Scala GameState as constructor parameter
- Extend Action trait (provides execute())
- Use ActionResultTApplier for applying results
- Use RandomStateTSequencer for sequencing operations
- Convert EndVassalCommandsPhaseAction to TRandomSequentialResultsAction
- Convert TruceTurnBackPhaseAction to TRandomSequentialResultsAction
Both converted actions now return ActionResultT instead of proto ActionResult,
eliminating proto usage in their result construction.
Part of Phase 5b: deleting RandomSequentialResultsAction base class.
8 more actions remain to be converted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Sort BUILD.bazel deps alphabetically
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The Authorization header was being sent to S3 signed URLs after redirect,
causing HTTP 400 errors. Now the auth header is only added to requests
going to eagle0.net hosts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Convert EndVassalCommandsPhaseAction to use Scala GameState
- Change constructor to take Scala GameState instead of proto
- Pass GameStateConverter.toProto() to parent RandomSequentialResultsAction
- Use ActionResultC with EndVassalCommandsPhaseResultType for final result
- Handle notifications with Scala types (withDeferred for delivery)
- Update RoundPhaseAdvancer to convert proto to Scala GameState
- Add required BUILD.bazel dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert EndHandleRiotsPhaseAction to use Scala GameState
- Change constructor to take Scala GameState instead of proto
- Pass GameStateConverter.toProto(gameState) to parent class
- Use withActionResultT with ActionResultC for endPhaseResult
- Update RoundPhaseAdvancer to convert proto to Scala GameState
- Add generated_text_request dependency to BUILD.bazel
- Update test to pass converted Scala GameState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert more RandomSequentialResultsAction subclasses to Scala GameState
- PerformProvinceMoveResolutionAction: takes Scala GameState, converts to proto internally
- PerformProvinceEventsAction: takes Scala GameState, converts to proto internally
- TruceTurnBackPhaseAction: takes Scala GameState, uses RandomStateProtoSequencer with initialState
- PerformVassalCommandsPhaseAction: takes Scala GameState, uses gameStateProto for internal proto operations
Updated RoundPhaseAdvancer to convert proto to Scala GameState for each action.
Fixed tests to use GameStateConverter.fromProto().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert PerformVassalDefenseDecisionsAction to Scala GameState
Also updates related tests to use GameStateConverter.fromProto() where needed.
Note: PerformProvinceEventsActionTest has 10 failing tests that need
their expectations updated to account for complete beast data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert NewRoundAction and PerformReconResolutionAction to Scala GameState
Continue the deproto conversion of RandomSequentialResultsAction subclasses:
- Convert PerformReconResolutionAction to use Scala GameState
- Convert NewRoundAction to use Scala GameState
- Fix test fixtures to provide required fields for proto-to-Scala conversion
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The request_full_resync field exists in eagle.proto but is not read by the server.
The actual resync mechanism uses filteredResultCount = 0 instead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Request resync instead of crashing on missing Shardok results
When HandleUpdates detects missing results (expected > existing + new),
likely due to dropped packets on bad network, request a full resync
instead of throwing an exception.
Changes:
- ShardokGameModel.HandleUpdates now returns bool (true=ok, false=need resync)
- EagleGameModel marks game for resync and clears history on mismatch
- CustomBattleHandler clears history and continues on mismatch
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add missing UnityEngine using statement for Debug.Log
Fixes build error: error CS0103: The name 'Debug' does not exist in the current context
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add 10-second timeout (was 100s default) - fail fast on bad network
- Reduce retry delays from [1s, 2s, 4s, 8s, 16s] to [500ms, 1s, 2s, 3s, 5s]
- Total retry delay reduced from 31s to 11.5s per hop
On bad networks, this should significantly improve responsiveness by
failing fast and retrying sooner rather than waiting for long timeouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Debug.LogWarning shows as popups in Unity which is too intrusive for
routine retry messages. Use Debug.Log instead for informational
messages about network retries.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Replace two-phase fetch with generic hop-following loop
- Each hop (whether redirect or content) gets its own 5 retry attempts
- Works regardless of backend implementation:
- Direct content response: works
- Single redirect: works
- Multiple redirects: works (up to 5 hops)
- Remove unused _httpClient field
- Add MaxRedirectHops constant (5) to prevent infinite redirect loops
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Use FirstOrDefault instead of First to avoid InvalidOperationException
- Return null and skip processing if battle was already removed
- Remove model from ShardokGameModels when:
- Battle not found (can't create model)
- Game state transitions out of Running/SetUp (battle ended)
- This ensures the UI properly reflects that the battle is over
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Disable auto-redirect and manually handle the eagle0.net -> signed URL redirect
- Each phase (redirect + image fetch) gets its own 5 retry attempts
- If phase 1 succeeds, we don't waste it when phase 2 fails
- Increase retry count from 3 to 5 with delays: 1s, 2s, 4s, 8s, 16s
- Add catch blocks for WebException and IOException (covers "Remote prematurely closed connection")
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
All actions that previously extended DeterministicSingleResultAction have
been converted to ProtolessSimpleAction. The base class is no longer used.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add 3 retry attempts with 1s, 2s, 4s exponential backoff delays
- Check HTTP status codes before processing responses
- Handle HttpRequestException, TaskCanceledException, and unexpected exceptions
- Track failed paths and retry them every 30 seconds via Timer
- Skip 4xx client errors (except 408/429) since retrying won't help
- Fix Prefetch to skip empty paths and avoid duplicate fetches
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Convert the final 3 DeterministicSingleResultAction classes to ProtolessSimpleAction:
- PerformFoodConsumptionPhaseAction
- PerformHostileArmySetupAction
- NewYearAction
All actions now use Scala GameState internally and return ActionResultT.
RoundPhaseAdvancer updated to convert via GameStateConverter at boundaries.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When an attacker wins an assault province battle, outlawed defenders
were being added to both unaffiliatedHeroes (as outlaws) AND to
capturedDefenderIds (as prisoners). This caused a validation error
because the same hero appeared in multiple province hero lists.
The fix filters outlawed defenders from notFledDefenders, matching
the existing behavior for attackers (line 368). Semantically, an
outlawed hero deserted during battle and is not present to be captured.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Logs warnings at 10s and 20s thresholds before the 30s idle timeout
triggers. This helps diagnose whether connection issues are gradual
slowdowns or sudden drops during testing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Allow clicking Free Heroes panel to select hero in RecruitHeroesCommand
## Summary
Enable clicking on recruitable heroes in the Free Heroes panel to directly
select them, eliminating the need to cycle through heroes using the "Next Hero"
button.
## Problem
RecruitHeroesCommandSelector was the only command selector with hero selection
that didn't support clicking heroes in the Free Heroes panel. Users had to:
- Click "Next Hero" button repeatedly to cycle through all available heroes
- No way to directly select a specific hero they wanted to recruit
- Inconsistent UX compared to other command selectors
## Solution
Implement the missing `AddTargetedHero()` method following the same pattern
used by all other command selectors (ManagePrisonersCommand, ImproveCommand,
DiplomacyCommand, etc.).
## Changes
### RecruitHeroesCommandSelector.cs
Added `AddTargetedHero(HeroId heroId)` override:
- Finds the hero in `RecruitHeroesCommand.AvailableHeroes` list
- Sets `_selectedHeroIndex` to that hero's index
- Calls `DisplayHero()` to update UI with hero details and backstory
Existing methods already supported Free Heroes integration:
- ✅ `HeroIsTargetable()` - marks recruitable heroes as selectable
- ✅ `TargetedHeroIds` - marks currently selected hero
## Behavior
**Before:**
- Recruitable heroes appeared in Free Heroes panel but weren't highlighted
- No indication which heroes were selectable
- Must use "Next Hero" button to cycle through sequentially
- Many clicks needed to find a specific hero
**After:**
- All recruitable heroes highlighted as selectable in Free Heroes panel
- Currently selected hero highlighted as selected
- Click any recruitable hero to instantly select them
- Hero details and backstory update immediately
- "Next Hero" button still works for sequential navigation
## User Experience
This completes the Free Heroes panel integration across ALL command selectors:
- ✅ Consistent interaction pattern everywhere
- ✅ Visual feedback about which heroes can be recruited
- ✅ Faster selection - click the hero you want
- ✅ Fewer clicks needed to recruit specific heroes
## Testing
Manual testing scenarios:
1. Select province with multiple recruitable heroes
2. Click RecruitHeroes command
3. Verify heroes appear highlighted in Free Heroes panel
4. Click different heroes, verify UI updates instantly
5. Verify backstory text updates correctly
6. Verify "Next Hero" button still works
7. Test with single recruitable hero (no "Next Hero" button)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add null safety check to HeroIsTargetable in RecruitHeroesCommandSelector
## Fix
Add null check before accessing _availableCommand.RecruitHeroesCommand to
prevent NullReferenceException when HeroIsTargetable() is called before
the command selector is fully initialized.
## Issue
HeroIsTargetable() is called by FreeHeroesTableController during table setup,
which can happen before _availableCommand is set. Without null checking:
- Throws NullReferenceException
- Prevents Free Heroes table from rendering
- Breaks the UI when switching commands
## Solution
Follow the same pattern used in ManagePrisonersCommandSelector (PR #4609):
- Check if _availableCommand is null
- Check if _availableCommand.RecruitHeroesCommand is null
- Return false instead of crashing
- Allow graceful handling when command data isn't ready yet
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Allow clicking Free Heroes panel to select prisoner in ManagePrisonersCommand
## Changes
Enable clicking on a hero in the Free Heroes panel to directly select that
hero in the ManagePrisonersCommand selector, eliminating the need to cycle
through prisoners using the "Next Hero" button.
## Implementation
- Override `HeroIsTargetable()` to return true for any hero in the prisoners list
- Override `AddTargetedHero()` to find the prisoner by heroId and update `_selectedHeroIndex`
- Override `TargetedHeroIds` to return the currently selected hero's ID
- Call `DisplaySelectedHero()` after selection to update UI
## Behavior
**Before:**
- User must click "Next Hero" button to cycle through prisoners
- No visual indication in Free Heroes panel
**After:**
- Prisoners in Free Heroes panel are highlighted as selectable
- Currently selected prisoner is highlighted as selected
- Clicking any prisoner directly selects them in ManagePrisonersCommand
- UI immediately updates to show selected prisoner's details and options
## User Experience
This follows the existing pattern used by other command selectors
(ImproveCommand, DiplomacyCommand, etc.) where clicking a hero in the Free
Heroes panel selects that hero for the active command.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix prisoner selection in Free Heroes panel
## Bug Fix
Prisoners in the Free Heroes panel were always grayed out and unclickable
because the Free Heroes table wasn't being updated after the command
selector was set.
## Root Causes
1. **Null reference**: HeroIsTargetable() was called before _availableCommand
was initialized, causing it to crash or return false
2. **Missing update**: After SetAvailableCommandAndSelector(), the Free Heroes
table wasn't notified to refresh its row selections
## Changes
### ManagePrisonersCommandSelector.cs
- Add null check in HeroIsTargetable() to handle early calls before
_availableCommand is set
- Return false instead of crashing when command data isn't ready yet
### EagleGameController.cs
- Add freeHeroesTableController.UpdateUnaffiliatedHeroSelections() call
after setting command selector
- This refreshes the Free Heroes table to show correct selectable/selected
states for the new command
## How It Works Now
1. User selects ManagePrisonersCommand
2. Command selector is set up with prisoner data
3. **NEW**: Free Heroes table is notified to update
4. Table calls HeroIsTargetable() for each hero
5. **NEW**: Returns true for prisoners (with null check)
6. Prisoner rows become highlighted as selectable
7. Clicking a prisoner calls AddTargetedHero()
8. Selected prisoner's index is updated
9. UI refreshes to show that prisoner's details
## Result
✅ Prisoners appear as selectable (highlighted) in Free Heroes panel
✅ Currently selected prisoner appears as selected
✅ Clicking any prisoner immediately selects them
✅ ManagePrisonersCommand UI updates instantly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Make UpdateUnaffiliatedHeroSelections public
Fix compilation error: UpdateUnaffiliatedHeroSelections() was private but
called from EagleGameController. Making it public allows the game controller
to refresh hero selection states when the command selector changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Phase 4: Implement Shardok state resync mechanism
## Summary
Add full state resync mechanism for Shardok games to prevent state inconsistencies after connection drops. When a connection is lost during Shardok gameplay, the client may have partially processed updates leading to desynced state. This change ensures full state consistency on reconnect.
## Changes
### 1. Protocol Extension
- **eagle.proto**: Add `request_full_resync` field to `ShardokViewStatus` message
- Allows client to request full state instead of delta updates
### 2. Client-Side Tracking
- **IClientConnectionSubscriber.cs**: Add `requestFullResync` field to struct
- **EagleGameModel.cs**:
- Add `_shardokNeedsResync` dictionary to track games requiring resync
- Add `MarkShardokForResync()` to flag individual games
- Add `MarkAllShardokForResync()` to flag all active games (on disconnect)
- Add `ClearShardokResyncFlag()` to clear flag after successful update
- Update `ShardokViewStatuses` property to set `requestFullResync` flag and `filteredResultCount = 0` when resync needed
### 3. Connection Integration
- **PersistentClientConnection.cs**:
- Add `MarkAllShardokGamesForResync()` helper method
- Call on disconnect in both RpcException and ObjectDisposedException handlers
- Update `StreamGameRequest` building to include `RequestFullResync` field
### 4. Auto-Clear on Success
- **EagleGameModel.cs**: Clear resync flag after successfully receiving and processing Shardok updates
## Behavior
**On Connection Drop:**
1. All active Shardok games are marked for resync
2. Client logs: `[RESYNC] Marked Shardok game {id} for full state resync`
**On Reconnect:**
1. Client sends `StreamGameRequest` with `request_full_resync = true` and `filtered_result_count = 0`
2. Server sends full current state instead of delta
3. Client processes full state update
4. Resync flag is cleared
5. Client logs: `[RESYNC] Cleared resync flag for Shardok game {id}`
**Subsequent Updates:**
- Normal delta updates resume with correct result counts
- State guaranteed to be consistent with server
## Testing
- Manual: Force disconnect during Shardok combat, verify state consistency after reconnect
- Manual: Multiple simultaneous Shardok games, verify all marked for resync
- Manual: Check logs for [RESYNC] messages during disconnect/reconnect cycles
## Related
- Implements Priority 2.1 from connection resilience plan (docs/CONNECTION_ARCHITECTURE.md)
- Complements Phase 2 exponential backoff and Phase 3 circuit breaker
- Addresses risk of state corruption from partial delta updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* CRITICAL FIX: Clear resync flag immediately after sending request
## Bug
Units were randomly moving around during Shardok placement because:
1. Resync flag was only cleared AFTER receiving server response
2. Multiple StreamGameRequests sent BEFORE first response arrived
3. Each request sent filtered_result_count=0 with resync=true
4. Server sent full state multiple times
5. Client replayed all placement actions repeatedly
## Root Cause
The `ShardokViewStatuses` property is called every time a `StreamGameRequest`
is built. If the resync flag is set, EVERY request sends filtered_result_count=0
until a response clears the flag. This creates a window where multiple requests
can ask for full state.
## Fix
Clear resync flags immediately AFTER building the request, BEFORE sending it.
This ensures only the FIRST request after disconnect has resync=true.
Sequence now:
1. Disconnect → mark games for resync
2. First StreamGameRequest reads flags → builds request with resync=true
3. **Immediately clear flags** ← THE FIX
4. Send request
5. Subsequent requests have resync=false (flags already cleared)
6. Server only sends full state once
## Changes
- PersistentClientConnection.StreamOneGame(): Clear resync flags after reading
but before sending request
- Keep defensive clear in EagleGameModel.ReceiveGameUpdate() as safety net
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Address Copilot review: Thread safety and code style improvements
## Changes
### 1. Thread Safety Fix (Critical)
**Issue**: _shardokNeedsResync dictionary accessed from multiple threads:
- Connection thread marks games for resync on disconnect
- Unity main thread reads/clears flags when building requests
- No synchronization → race conditions and potential exceptions
**Fix**: Replace Dictionary<string, bool> with ConcurrentDictionary<string, bool>
- Thread-safe for concurrent reads and writes
- Use TryRemove() instead of Remove() for atomic removal
- Add comment documenting thread-safety requirement
### 2. Code Style Improvements
**Issue**: Implicit filtering in foreach loops (Copilot warnings)
**Fixes**:
- Use `.Where(s => s.requestFullResync)` to explicitly filter resync statuses
- Use `.OfType<GameModelUpdater>()` instead of foreach with type checking
- Both changes improve readability and make intent explicit
### 3. Timing Clarification
**Copilot concern**: Clearing resync flag before request is sent/confirmed
**Resolution**: Current implementation is correct
- Flag cleared after reading but before sending ensures only ONE request has resync=true
- If send fails, connection drops again → MarkAllShardokForResync() called again
- Added comment explaining this reasoning to prevent future confusion
## Testing
- No functional changes, only thread safety and style improvements
- Existing behavior preserved: flag clearing still prevents duplicate resync requests
- ConcurrentDictionary is drop-in replacement for Dictionary in this use case
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Convert PerformUnaffiliatedHeroesAction to accept Scala GameState
This is part of the Phase 5 deproto plan. Changes:
- PerformUnaffiliatedHeroesAction now accepts Scala GameState instead of proto
- Internally converts to proto for legacy utilities and base class
- Updated RoundPhaseAdvancer to convert proto to Scala before calling
- Updated tests to use GameStateConverter and add currentPhase to test fixtures
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use Scala types internally in PerformUnaffiliatedHeroesAction
- Add hasBlizzard method to ProvinceUtils that takes ProvinceT
- Add closestNeighborToFaction overload to ProvinceDistances for Scala Map
- Refactor PerformUnaffiliatedHeroesAction to use Scala provinces/factions
internally rather than converting from proto for each operation
- Update test to use Scala types directly for blizzard event fixture
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Complete deproto of PerformUnaffiliatedHeroesAction internal logic
- Use Scala types (ActionResultT, ChangedHeroC, ChangedProvinceC, UnaffiliatedHeroT)
internally throughout the action
- Add ChangedHeroConverter.fromProto for boundary conversion
- Replace proto .update() with Scala .copy()
- Only remaining proto usage is at boundaries:
- RandomSequentialResultsAction base class returns ActionResultProto
- UnaffiliatedHeroMovedAction still uses proto (requires separate deproto)
- All 10 tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Inline UnaffiliatedHeroMovedAction and use Scala-typed utilities
- Replace LegacyUnaffiliatedHeroUtils with UnaffiliatedHeroUtils (Scala types)
- Add heroMovedResult method using Scala types instead of proto-based
UnaffiliatedHeroMovedAction
- Remove unused proto converter deps (changed_hero_converter,
notification_converter, unaffiliated_hero_converter)
- Add notification_concrete and free_hero_move_vigor_cost deps
Remaining proto deps are structural (RandomSequentialResultsAction,
RandomStateProtoSequencer) and would require architectural changes to remove.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix HasQuest comparison - use pattern matching instead of companion object
The comparison `recruitmentInfo == RecruitmentInfo.HasQuest` always
returned false because HasQuest is a case class and we were comparing
an instance like HasQuest(quest) to the companion object.
Use pattern matching to correctly check if recruitmentInfo is an
instance of HasQuest, preserving the quest data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove unnecessary asInstanceOf and isInstanceOf usage
- Use explicit Vector[ActionResultT] type parameter instead of asInstanceOf cast
- Use collectFirst pattern match instead of isInstanceOf in hasBlizzard
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor newRecruitmentInfo to use tuple pattern matching
Replace cascading if-else chain with cleaner tuple match on
(isFactionLeader, unaffiliatedHeroType, recruitmentInfo) with guards
for odds-based conditions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Phase 2: Add exponential backoff, state resync logging, and connection health UI
Implements Priority 2 (State Consistency & Recovery) from the connection resilience plan.
## Changes
### 1. Exponential Backoff for Reconnection (`PersistentClientConnection.cs`)
Replaced fixed-delay and immediate reconnection with intelligent exponential backoff.
**Implementation:**
- `_consecutiveFailures`: Tracks sequential connection failures
- `GetBackoffSeconds()`: Calculates backoff with exponential growth
- `ScheduleReconnect()`: Unified retry scheduler for all disconnect scenarios
**Backoff Sequence:**
```
Attempt 1: 2.0s delay
Attempt 2: 4.0s delay
Attempt 3: 8.0s delay
Attempt 4: 16.0s delay
Attempt 5+: 32.0s delay (capped)
```
**Applied to all disconnect scenarios:**
- `Cancelled`: Now uses backoff (was immediate retry)
- `Internal`: Now uses backoff (was immediate retry)
- `DeadlineExceeded`: Now uses backoff (was immediate retry)
- `Unavailable`: Now uses backoff (was fixed 5s retry)
- `ObjectDisposed`: Now uses backoff (was immediate retry)
- `Unknown`: Now uses backoff (was no retry)
**Benefits:**
- Reduces server load during outages (no immediate retry storm)
- Prevents client-side reconnection thrashing
- Progressive backoff gives transient issues time to resolve
- Resets to 2s on successful connection
**Logging:**
```
[CONNECTION] ... event=schedule_reconnect details="Unavailable, backoff=4.0s, attempt=2"
```
### 2. State Resync Logging (`EagleGameModel.cs`)
Added structured logging for state resynchronization events.
**Note:** State resync mechanism was already fully implemented in the protocol!
- Protocol field: `GameUpdate.starting_state` (eagle.proto line 151)
- Client handling: `HandleStartingState()` fully functional since original implementation
- This PR only adds observability
**New Logging:**
```
[STATE_RESYNC] timestamp=YYYY-MM-DD HH:mm:ss.fff round=<n> actions=<count> factions=<count>
```
Logs when server sends full state snapshot after reconnection, allowing diagnosis of:
- How often resyncs occur
- Game state at resync time (round, action count)
- Whether resync is triggered appropriately
### 3. Connection Health Monitoring (`ConnectionStatusUI.cs`)
NEW FILE: Simple Unity UI component for visual connection status display.
**Features:**
- Real-time connection state display
- Countdown timer during reconnection backoff
- Color-coded status indicator
- Low-overhead polling (0.5s update interval)
**Connection States:**
- `Connected`: Green indicator, normal operation
- `Connecting`: Yellow indicator, initial connection
- `Reconnecting`: Orange indicator with countdown "Retry in Xs"
- `Disconnected`: Red indicator, connection lost
**Usage:**
```csharp
// Attach ConnectionStatusUI to a TextMeshProUGUI GameObject
var statusUI = gameObject.AddComponent<ConnectionStatusUI>();
statusUI.SetConnection(persistentConnection);
```
**Display Examples:**
```
● Connected (green)
● Connecting... (yellow)
● Retry in 8s (orange)
● Disconnected (red)
```
**Implementation Details:**
- `ConnectionState` enum: Tracks current connection phase
- `NextReconnectAttempt`: DateTime for countdown calculation
- `CurrentState` property: Public accessor for UI monitoring
- Non-intrusive: Updates via polling, no event subscriptions
### 4. Connection State Tracking (`PersistentClientConnection.cs`)
Added public API for connection health monitoring:
**New Public API:**
```csharp
public enum ConnectionState { Disconnected, Connecting, Connected, Reconnecting }
public ConnectionState CurrentState { get; }
public DateTime? NextReconnectAttempt { get; }
```
**State Transitions:**
- `Disconnected` → `Connecting`: Initial connection or first reconnect
- `Connecting` → `Connected`: Connection established
- `Connected` → `Reconnecting`: Connection lost, scheduling retry
- `Reconnecting` → `Connecting`: Retry timer fired, attempting connection
- `Connecting` → `Reconnecting`: Connection failed, scheduling next retry
## Testing Strategy
### Exponential Backoff Verification
**Monitor logs for backoff progression:**
```bash
grep 'schedule_reconnect' logfile.txt
```
Expected output:
```
... event=schedule_reconnect details="Unavailable, backoff=2.0s, attempt=1"
... event=schedule_reconnect details="Unavailable, backoff=4.0s, attempt=2"
... event=schedule_reconnect details="Unavailable, backoff=8.0s, attempt=3"
```
**Test scenarios:**
1. Kill server during active session → observe progressive backoff
2. Successful reconnect → verify backoff resets to 2s on next failure
3. Server unavailable for 2+ minutes → verify cap at 32s
### State Resync Logging
**Trigger resync:**
1. Start game and play several rounds
2. Kill client (not server) to lose connection
3. Restart client and reconnect
4. Check logs for `[STATE_RESYNC]` event
**Verify:**
- Round number matches current game state
- Action count is non-zero and reasonable
- Faction count matches game setup
### Connection Status UI
**Manual testing:**
1. Add ConnectionStatusUI component to Unity scene
2. Observe status during: connection, gameplay, disconnect, reconnect
3. Verify countdown timer accuracy during backoff
4. Confirm color coding matches connection state
## Success Criteria
- ✅ Exponential backoff applied to all reconnection scenarios
- ✅ Backoff resets to 2s on successful connection
- ✅ State resync events logged with game state details
- ✅ Connection status UI displays current state accurately
- ✅ Retry countdown shows correct time remaining
- ✅ No performance degradation from status polling
## Known Limitations
**Not addressed in this PR:**
- ❌ Server-side state tracking (not needed - protocol already handles this!)
- ❌ Circuit breaker pattern (Priority 3)
- ❌ Server-side metrics (Priority 3)
- ❌ Adaptive parameters (Priority 4)
**State Resync Note:**
The protocol already has full state resync support via `GameUpdate.starting_state`. The server decides when to send a full snapshot (typically after reconnection). This PR only adds logging for observability - no protocol or logic changes were needed.
## Rollback Plan
If issues arise:
1. Revert exponential backoff: Replace `ScheduleReconnect()` calls with `Task.Run(() => Connect())`
2. Remove state resync logging if it impacts performance (unlikely)
3. Disable ConnectionStatusUI component via Unity inspector
4. All changes are backward compatible and independently revertible
## Related Documentation
- Connection Architecture Analysis: `docs/CONNECTION_ARCHITECTURE.md`
- Implementation Plan (Priority 2): PR #4599
- Phase 1 (Diagnostics): PR #4601🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix GameStateView field names for state resync logging
Corrected field names to match actual protobuf definition:
- RoundId → CurrentRoundId
- ActionCount → removed (not present in GameStateView)
- ActiveFactions → Factions
- Added Heroes.Count for additional context
Fixes Unity build error:
CS1061: 'GameStateView' does not contain a definition for 'RoundId'/'ActionCount'/'ActiveFactions'
* Add Unity metadata files for new C# files
Unity auto-generated files:
- Assembly-CSharp.csproj: Updated to include ConnectionStatusUI.cs
- .meta files: Unity asset metadata for ConnectionStatusUI and prisoner notifications
* Integrate ConnectionStatusUI into EagleGameController
Wire up the ConnectionStatusUI component to display connection status in the game UI.
Implementation:
- Added ConnectionStatusUI component to connectionStatusLabel
- Initializes once when PersistentClientConnection is available
- Accesses connection through errorHandler.PersistentClientConnection
- Only initializes once using _connectionStatusUIInitialized flag
The status UI will now automatically display:
- ● Connected (green)
- ● Connecting... (yellow)
- ● Retry in Xs (orange) during backoff
- ● Disconnected (red)
* Use GetComponent instead of AddComponent for ConnectionStatusUI
Changed to use GetComponent to find the existing ConnectionStatusUI component
that was already added in the Unity editor, rather than creating it in code.
This follows proper Unity patterns: configure components in the editor, wire
them up in code.
* Add ConnectionStatusUI support to Shardok canvas
Integrated connection status display into the Shardok battle UI.
Changes to ShardokGameController.cs:
- Added connectionStatusLabel field for TextMeshProUGUI
- Added _connectionStatusUIInitialized flag
- Added SetConnection() method to wire up ConnectionStatusUI component
Changes to EagleGameController.cs:
- Call SetConnection() when activating Shardok canvas
- Passes PersistentClientConnection from errorHandler
Both Eagle and Shardok canvases now display real-time connection status.
---------
Co-authored-by: Claude <noreply@anthropic.com>
When unfilteredCount == 0 (fresh client), start from position 1 instead
of 0 to avoid diffing against the invalid initial state which has
UNKNOWN_PHASE. Send stateAfter(1) as the starting state to the client
and filter results from position 1 onwards.
This replaces the previous fix (#4604) which used an empty GameStateProto
but still caused issues when GameStateViewDiffer tried to diff against it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Instead of emitting one ActionResult per hero, batch all status changes
into a single HERO_CHANGED ActionResult per round. This significantly
reduces the number of actions in game history.
Changes:
- Add BatchedHeroChanges and HeroProcessingResult helper classes
- Refactor prisonerChanges, residentChanges, travelerChanges, outlawChanges
to return HeroProcessingResult instead of calling UnaffiliatedHeroesChangedAction
- Remove UnaffiliatedHeroesChangedAction (now unused)
- Add tests for batching behavior, resident→traveler, and traveler→resident transitions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When unfilteredCountBefore is 0 (fresh client), use an empty GameStateProto
for filtering action results instead of calling stateAfter(0), which returns
an invalid state with UNKNOWN_PHASE.
This allows fresh clients to receive the full history of action results
from an empty starting state, letting the diffs build up the complete
game state.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Implements Priority 1 (Critical Fixes & Diagnostics) from the connection resilience plan.
## Changes
### Comprehensive Connection Logging (PersistentClientConnection.cs)
Added structured logging to track complete connection lifecycle:
**New metrics tracked:**
- `_lastConnectAttempt`: Timestamp of last connection attempt
- `_lastSuccessfulConnect`: Timestamp of last successful connection
- `_lastDisconnect`: Timestamp of last disconnection
- `_lastDisconnectReason`: StatusCode of last disconnect (if from RpcException)
**New helper methods:**
- `GetTotalShardokGames()`: Counts active Shardok games across all subscribers
- `LogConnectionEvent()`: Structured logging with key-value pairs for easy parsing
**Structured log format:**
```
[CONNECTION] timestamp=YYYY-MM-DD HH:mm:ss.fff event=<event_type> shardok_games=<count> status=<StatusCode> details="<details>" seconds_since_connect=<seconds>
```
**Events logged:**
- `connect_attempt`: When Connect() is called
- `connect_success`: When connection is established and streaming thread started
- `connect_failed`: When connection setup fails with exception type
- `disconnect_explicit`: When Disconnect() is explicitly called
- `disconnect`: When connection drops with StatusCode (Cancelled, Internal, DeadlineExceeded, Unavailable, ObjectDisposed, Unknown)
**Key insights this enables:**
- Correlate disconnections with Shardok gameplay (shardok_games counter)
- Measure connection lifetime (seconds_since_connect)
- Identify disconnect patterns by StatusCode
- Track connection stability over time
### HTTP/2 Keepalive Reduction (EagleConnection.cs)
Reduced HTTP/2 keepalive interval from 45s to 15s for better NAT/firewall traversal.
**Rationale:**
- Typical NAT/firewall timeout: 60-120 seconds
- Previous 45s keepalive was insufficient to prevent timeouts
- 15s keepalive provides 4x safety margin below 60s timeout
- Minimal bandwidth overhead (~4 bytes every 15s)
**Expected impact:**
- Prevents connection drops during idle periods (e.g., thinking during Shardok battles)
- Maintains connection through home routers and ISP NAT devices
- Should significantly reduce ~2-minute disconnection issues
## Testing Strategy
**Logging verification:**
- Monitor ConnectionLogger output for structured [CONNECTION] events
- Verify all event types appear in appropriate scenarios
- Confirm shardok_games counter tracks active battles
**Keepalive verification:**
- Test connection stability during 5+ minute Shardok battles
- Monitor network traffic to confirm 15s PING intervals
- Verify no disconnections during idle periods with remote players
## Success Criteria
- Structured connection logs appear for all lifecycle events
- Shardok game count accurately reflects active battles
- Connection remains stable during 5-minute idle periods
- Disconnect events include clear StatusCode and timing information
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Add Go admin server for Eagle game management
- Add GetRunningGames and GetGameHistory RPC endpoints to eagle.proto
- Implement admin methods in EagleServiceImpl.scala
- Create Go HTTP admin server at src/main/go/net/eagle0/admin_server/
- Add gRPC dependency to go.mod and MODULE.bazel
- Fix Go proto compilation with gazelle-compatible '# keep' directives:
- api_go_proto uses go_grpc (not go_grpc_v2) to generate message types
- common_go_proto uses go_proto and excludes shardok_internal_interface_proto
- admin_server_lib keeps proto dependency that gazelle doesn't detect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use hex format for game IDs in admin server
- /games endpoint returns game_id in hex format
- /games/{id}/history expects game ID in hex format
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix hex game ID format and restore full game info
- Use unsigned hex format (uint64 cast) to avoid negative values
- Restore all RunningGameInfo fields: current_round, action_count, players, run_status
- Include full player info: faction_id, faction_name, leader_name, is_human, user_name
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix hex game ID parsing for large unsigned values
Use ParseUint instead of ParseInt to handle game IDs that exceed
max signed int64 when represented as unsigned hex.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The client would fail to detect dead connections because the heartbeat timer was never recreated after sending a heartbeat.
Root cause:
In TimerFired() (lines 666-690), the timer is always disposed when it fires (lines 666-668). If no response has been received for 10-20 seconds, the code sends a heartbeat (line 685) but then returns WITHOUT creating a new timer. This means if the server never responds to the heartbeat (dead connection), the client waits forever because there's no timer to detect the timeout.
The timer only gets recreated when SetUpTimer() is called in HandleStreamingCall after receiving a response (line 482). But if the connection is dead, no response ever comes, so SetUpTimer() is never called again.
Timeline of the bug:
1. No response for 10 seconds → timer fires
2. Code sends heartbeat, disposes timer, returns
3. Timer is gone, no response ever comes
4. Client waits forever, never detects dead connection
5. No automatic reconnection happens
Fix:
Call SetUpTimer() after sending a heartbeat (line 688):
- Creates new 10-second timer after heartbeat is sent
- If still no response after another 10 seconds (20 seconds total), next timer fires
- Detects > 20 seconds since last response, forces reconnection via Connect()
This was more noticeable during Shardok gameplay because dead connections are more disruptive to fast-paced tactical combat.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The client wasn't automatically reconnecting when dropped during Shardok gameplay due to a race condition in PersistentClientConnection.
Root causes:
1. Connect() was being called without await from multiple places (exception handlers, timers), dropping the returned Task
2. Multiple concurrent Connect() calls could happen simultaneously, creating conflicting state
3. The old HandleStreamingCall thread would check _currentThreadToken.IsCancellationRequested and return without reconnecting, even though that token gets cancelled during normal reconnection
Fixes:
- Add _isConnecting flag to prevent concurrent connection attempts
- Wrap Connect() body in try/finally to always reset the flag
- Change all Connect() calls to use Task.Run(() => Connect()) to properly handle the async method
- Only check _cancellationToken (not _currentThreadToken) in StatusCode.Cancelled handler
- Move Connect() call outside the lock in TimerFired to prevent blocking
This was more noticeable in Shardok because of more frequent updates and timing-sensitive gameplay.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Create notification generators for three prisoner management actions that now have LLM-generated narrative text:
- PrisonerReleasedDetailsNotificationGenerator
- PrisonerExiledDetailsNotificationGenerator
- PrisonerReturnedDetailsNotificationGenerator
Each follows the established pattern using StreamingDynamicNotification to display LLM-generated text as it arrives via llmId.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
This commit fixes two related issues in the MCTS implementation:
1. Initial expansion guarantee: Ensures at least one child is expanded
before entering the time-bounded loop. Previously, if the deadline
had already passed (e.g., debugger pause, system load), we might
enter the loop with zero children and crash when selecting the best.
2. Terminal node expansion fix: Changes the order of checks in selection
and expansion to allow expanding terminal nodes that still have untried
actions (e.g., final round where we need to pick an action). Previously,
the isTerminal check would prevent expansion even when actions remained.
Also stubs two broken integration tests that manually constructed incomplete
FlatBuffer game states - proper testing is done in shardok_mcts_ai_basic_test.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add proto messages for PrisonerReleasedMessage, PrisonerExiledMessage,
PrisonerReturnedMessage in generated_text_request.proto
- Add notification details for the three new prisoner management types
- Create prompt generators for release, exile, and return actions
- Update ManagePrisonersCommand to emit LLM requests and notifications
for Release, Exile, and Return options (matching Execute behavior)
- Update LlmResolver to handle the new prompt generators
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When a defending hero becomes outlawed during battle:
- They were correctly added to newUnaffiliatedHeroes via newOutlaws()
- But they were NOT removed from rulingFactionHeroIds because
unitReturned() returns false for Outlawed status
This caused the same hero to appear in both rulingFactionHeroIds and
unaffiliatedHeroes, failing RuntimeValidator.scala:206 validation.
Fix: Also remove outlawed heroes from removedRulingPlayerHeroIds and
their battalions from removedBattalionIds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Update PrisonerExecutedDetailsNotificationGenerator to use StreamingDynamicNotification instead of static DynamicTextNotification, enabling LLM-generated "last words" text to appear as it arrives.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Implement LLM-generated "last words" for prisoners when they are executed
via ManagePrisonersCommand, following the same pattern as CapturedHeroExecuted.
Changes:
- Add PrisonerExecutedMessage to proto and LlmRequestT enum
- Create PrisonerExecutedPromptGenerator for generating prompts
- Update ManagePrisonersCommand to create LLM request when executing
- Link notification to LLM request via NotificationT.Llm.Id
- Add test verifying LLM request creation and notification linking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Clamp fire damage to prevent negative casualties
Extreme negative open-ended percentile rolls (as low as -475) could
produce negative damage values in GetFireDamage, leading to negative
casualties in MutatingInternalTakeDamage.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add tests for fire damage with extreme negative rolls
Tests verify that GetFireDamage produces non-negative damage values
even with extreme negative open-ended percentile rolls (as low as -475).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Convert EndPleaseRecruitMePhaseAction to ActionResultT
- Add fromProtoState factory to convert proto deferredNotifications
- Use NotificationConverter to convert notifications to Scala model
- Update call site in RoundPhaseAdvancer to use ActionResultProtoConverter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert EndDefenseDecisionPhaseAction to ActionResultT
- Migrate from DeterministicSingleResultAction to ProtolessSimpleAction
- Add fromProtoState factory method to convert proto GameState to Scala models
- Use ArmyConverter for MovingArmy conversion
- Extract PayingProvinceResolution data class for tribute-paid army tracking
- Update call site in RoundPhaseAdvancer
- Update test to use new API pattern
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update DEPROTO_PLAN.md with Phase 5 progress
- Mark 6 DeterministicSingleResultAction conversions as complete
- Update overall progress to ~75% complete
- Document remaining 4 actions to convert:
- PerformFoodConsumptionPhaseAction
- PerformHostileArmySetupAction
- UnaffiliatedHeroesChangedAction
- NewYearAction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Throws ShardokInternalErrorException if MutatingInternalTakeDamage
calculates negative casualties, which would indicate a bug in damage
calculation logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Convert EndFreeForAllBattleRequestPhaseAction to case object with ProtolessSimpleAction
- Convert EndFreeForAllBattleResolutionPhaseAction to case object with ProtolessSimpleAction
- Update call sites in RoundPhaseAdvancer to use ActionResultProtoConverter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Combat commands use OpenEndedPercentile rolls that affect damage dealt.
Without chance nodes, MCTS only sees one possible outcome, which can
lead to suboptimal decisions when roll variance significantly affects
combat results.
Commands now treated as multi-outcome chance nodes:
- MELEE_COMMAND: attacker roll affects damage
- ARCHERY_COMMAND: attacker roll affects damage
- CHARGE_COMMAND: attacker roll affects damage
- CHALLENGE_DUEL_COMMAND: multiple rolls affect duel outcome
- REDUCE_COMMAND: roll affects structure/unit damage
Each uses 5 fixed-seed outcomes (rolls: 10, 30, 50, 70, 90) to sample
the distribution of possible results.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Update DEPROTO_PLAN.md: Phase 4 is already complete
Assessment shows ActionResultT infrastructure is 86% complete:
- ActionResultT trait and ActionResultC implementation exist
- ActionResultTApplier exists for gradual migration
- ActionResultProtoConverter is complete
- 51/59 actions already use ActionResultT
- Only ~10 actions still use proto ActionResult
Phase 5 will cover:
- Converting remaining proto actions to ActionResultT
- Converting RoundPhaseAdvancer to use Scala GameState
- Converting action parameters to Scala GameState
Updated effort estimates: ~40% complete (was 10%)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert EndBattleRequestPhaseAction to ActionResultT
- Convert EndBattleRequestPhaseAction to use ProtolessSimpleAction
- Return ActionResultT instead of proto ActionResult
- Use Scala model types (RoundPhase.FoodConsumption, ChangedProvinceC)
- Add factory method fromProtoState() for call sites using proto GameState
- Update RoundPhaseAdvancer call site to use ActionResultProtoConverter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Convert EndBattleResolutionPhaseAction to ActionResultT
- Convert from case class with GameState to case object extending ProtolessSimpleAction
- Update call site in RoundPhaseAdvancer to use ActionResultProtoConverter
- Update test to use Scala model types instead of proto types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The starting_position_index field was not being included in the
UnitView for hidden/unplaced enemy units, causing GameStateGuesser
to default it to -1. This caused crashes in PlayerSetupCommandFactory
when the AI tried to generate setup commands for attacker units.
starting_position_index is public information (defenders know which
direction attackers will spawn from), so it should always be visible.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Implement AddTargetedHero() in DivineCommandSelector to allow direct
selection of heroes from the Free Heroes panel. When a hero is clicked,
find their index in the divinable heroes list and update the selection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
END_TURN has random effects (fire spread/extinguish, weather changes)
that caused MCTS to sometimes prefer START_FIRE over END_TURN because
the random outcomes created inconsistent scoring.
This change:
- Generalizes BinaryOutcomeInfo to ChanceOutcomeInfo supporting N outcomes
- Adds multiOutcome(int) factory for END_TURN with 5 fixed-seed outcomes
- Updates ShardokAction::requiresChanceNode() to return true for END_TURN
- Adds test verifying AI doesn't prefer START_FIRE when not beneficial
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Two bugs in chance node handling:
1. lookaheadScore not updated for binary outcomes: The code only updated
lookaheadScore when children.size() == 1, which never happened for
binary outcomes (2 children). Chance nodes kept their initial score
from the parent state, giving them unfair UCB advantage.
2. Simulation ran on wrong state: When creating a chance node, we returned
it for simulation. But chance nodes store the parent state, so simulation
ran on the pre-action state instead of an outcome state. Now we recursively
expand the first outcome and return that instead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Add variable beast power with min/max range
- Split relativePower into minRelativePower and maxRelativePower
- SuppressBeastsCommand now randomly selects power within range
- CommandChoiceHelpers uses average power for AI decisions
- Fix CRLF line endings in TSV download scripts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* clown variance
* Fix SuppressBeastsCommandTest for min/max relativePower
Update test BeastInfo instances to use minRelativePower and
maxRelativePower instead of the old relativePower field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use worst-case beast power for AI decision-making
The AI should assume max relativePower when deciding whether to
suppress beasts, to be cautious about high-variance beasts like clowns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Extract relativePower method and add tests
Create a public SuppressBeastsCommand.relativePower method that takes
BeastInfo and FunctionalRandom, returning RandomState[Double]. This
makes the random power calculation reusable and testable.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use cubic distribution for beast relativePower
Change from uniform to cubic distribution (roll^3) so that most
encounters are closer to minRelativePower, while still allowing
rare high-power encounters up to maxRelativePower.
For clowns (5-50 power range):
- Median outcome: ~10.6 (vs 27.5 with uniform)
- 75th percentile: ~24 (vs 38.75 with uniform)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use quartic distribution and P90 for AI decisions
- Change from cubic (roll^3) to quartic (roll^4) distribution for
even more skew toward minRelativePower
- AI now uses P90 (0.9^4 = 0.6561) instead of worst-case when
deciding whether to suppress beasts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Phase 3: Update GameHistory to return Scala models
- GameHistory.stateAfter now returns Scala GameState instead of proto
- GameHistory.sinceDate now accepts Scala Date instead of proto Date
- Updated InMemoryHistory and PersistedHistory implementations
- Updated callers (EngineImpl, UnrequestedTextHandler, HumanPlayerClientConnectionState)
to convert to proto only at boundaries where needed
- Updated tests to use Scala models for mock expectations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update DEPROTO_PLAN with Phase 3 completion and RoundPhaseAdvancer strategy
- Mark Phase 2 and Phase 3 as complete (PRs #4563 and #4576)
- Update rollout diagram to show progress
- Restructure Phase 5 to prioritize RoundPhaseAdvancer actions
- Add strategic insight about RoundPhaseAdvancer as central orchestrator
- Add Lessons Learned appendix from Phases 2-3
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Google Sheets exports TSV files with Windows-style CRLF line endings.
This causes spurious git diffs when the download scripts are run.
Pipe curl output through `tr -d '\r'` to strip carriage returns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Phase 2: Update EngineImpl to use Scala GameState internally
This is part of the deproto migration plan to limit proto usage to the
edges (network/disk) in the Eagle game engine.
Key changes:
- Engine.currentState now returns Scala GameState instead of proto
- EngineImpl uses Scala GameState internally, converting to/from proto
at boundaries when calling proto-expecting functions
- Updated AIClient, GameController, and GamesManager to use
GameStateConverter at boundaries
- Added necessary transitive exports in BUILD files for Scala model types
- Updated GamesManagerTest to use GameStateConverter for test mocks
Known issue: GamesManagerTest has 2 failing test cases due to incomplete
mock hero data (heroes lack factionId). This is a test data issue, not
a code issue - the test mocks need to be updated with proper hero setup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use Scala GameState directly in tests instead of converting from proto
Update GameControllerTest and GamesManagerTest to create GameState objects
directly using the Scala model types, rather than creating GameStateProto
and converting. This simplifies the tests and removes unnecessary proto
dependencies.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Improve ProfessionGained notification wording
Change from 'gained the {profession} profession' to 'became a {profession}'
for more natural and concise text.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix article grammar for profession names
Add GetArticle() helper to use 'an' for vowel-starting professions
(Engineer) and 'a' for consonant-starting ones.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-27 16:56:25 -08:00
416 changed files with 28797 additions and 11157 deletions
**MANDATORY: Before running `git commit`, verify:**
1.**If you modified any BUILD.bazel file:** Run `bazel run gazelle` and stage any changes it makes
2.**If you modified C++ or C# files:** Run `clang-format -i` on the modified files
3.**If you modified Scala files:** scalafmt will run automatically via pre-commit hook
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.**
### Code Formatting
```bash
@@ -206,6 +216,31 @@ to be used for different players or game situations within the same server proce
- Map validation tests ensure game content integrity
- Use `GameSettings_test_utils.cpp` and `ShardokEngineBasedTestData.cpp` for 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:
- 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:
@@ -244,6 +279,32 @@ done
- **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:**
1. Identify the missing type from the error message (e.g., `game_state.GameState`)
2. Find the Bazel target that provides this type (e.g., `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`)
3. Add it to the `deps` of the failing target
4. If the type appears in a public method signature, also add it to `exports` so 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/`
**Protocol buffers should only be used at the edges** — for network serialization (gRPC) and disk persistence. Inside the Eagle game engine, all logic should operate on native Scala models.
| Phase 5: Action Base Classes | **Complete** | All `RandomSequentialResultsAction` and `DeterministicSingleResultAction` converted to T-type base classes |
| Phase 5b: Base Class Cleanup | **Complete** | `RandomSequentialResultsAction` and `DeterministicSingleResultAction` deleted |
| Phase 5c: RoundPhaseAdvancer Actions | **Complete** | All actions called by RoundPhaseAdvancer accept Scala GameState |
`ActionResultApplier` applies `ActionResultT` directly to Scala `GameState`. The legacy `ActionResultTApplierImpl` wraps it and converts to/from proto for callers that still need proto types.
Modify `RandomStateTSequencer` to thread Scala `GameState` throughout (currently converts to proto internally). Then evaluate whether `RandomStateProtoSequencer` is still needed at all.
**Current State**: `RandomStateTSequencer` accepts Scala `GameState` via its `apply()` method but internally converts to proto. All callback methods (`withRandomActionResult`, `withActionResults`, etc.) pass `GameStateProto` to callers, forcing actions that use the sequencer to work with proto types internally.
**Target State**: Create a fully protoless sequencer where:
To enable lightweight mocking of command creation in tests, `TCommandFactory` trait was extracted from `CommandFactory`. This allows tests to mock just the `makeTCommand` method without pulling in all 40+ command dependencies that `CommandFactory` requires.
-`TCommandFactory` - lightweight trait with just `makeTCommand`
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions. `PersistedHistory` converts to proto internally for disk persistence; `InMemoryHistory` doesn't need proto at all.
| Second biggest blocker | `CommandChoiceHelpers` (blocks 4 actions) |
### Validation
- [x]`ActionResultApplier` created and tested
- [x]`RandomStateSequencer` threads Scala GameState throughout
- [x]`RoundPhaseAdvancer` uses T-types internally
- [x]`ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
- [x]`FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` use Scala `ProvinceView`
- [x]`ProvinceViewFilter.withdrawnFromProvinceView` has Scala overload
- [ ]`ProvinceViewFilter` faction-filtered views use Scala types
- [ ]`CommandChoiceHelpers` uses Scala types
- [ ] History APIs vend Scala types
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
- [ ] All tests pass
---
## Phase 7: Clean Up Legacy Utilities
### Objective
Remove remaining direct proto imports from utility classes.
### Files to Modify
| File | Status |
|------|--------|
| `CommandChoiceHelpers.scala` | Accepts proto `GameState`; blocks full deproto of `PerformVassalCommandsPhaseAction` and `PerformVassalDefenseDecisionsAction` |
-`/library/util/` - Pure Scala models (except loaders)
-`/model/state/` - Pure Scala models
---
## Open Questions
1.**Persistence Format**: Currently game state is persisted as proto. Should we keep proto for persistence (good for schema evolution) or switch to a different format?
2.**Shardok Integration**: `ResolveBattleAction` communicates with Shardok. Should the Shardok interface use protos (external service) or Scala models?
3.**View Generation**: `GameStateViewDiffer` works with view protos for client updates. Views need Scala models (`ProvinceViewT`, etc.) to allow actions like `EndBattleAftermathPhaseAction` to be fully protoless. The Scala views would be converted to proto only at the gRPC boundary when sending updates to clients.
---
## Success Criteria
### Code Quality
- [ ] Zero proto imports in `/library/actions/` (except boundaries)
- [ ] Zero proto imports in `/library/` utilities (except loaders)
- [ ]`GameStateT` used throughout engine internals
- [ ] Proto usage limited to: `EagleServiceImpl`, loaders, converters, persistence
### Architecture
- [ ] Clear separation: Scala models (internal) vs Proto (boundaries)
- [ ] Converters as the only bridge between domains
You are summarizing changes for a weekly engineering update email.
Read the following list of merged PRs and create a concise synopsis grouped by theme/feature/area of the codebase.
Structure:
1. <h1> title (e.g., "Eagle0 Weekly Update")
2. <h2>BLUF</h2> (Bottom Line Up Front) - A short prose paragraph (2-4 sentences) highlighting the 1-3 most important changes this week and what to look for when testing. This should be conversational and help readers quickly understand what matters most.
3. Synopsis sections (<h2> headings with bullet point summaries)
4. <hr> divider
5. <h2>PR Details</h2> with the same groupings, but smaller (<h3> headings) and listing PR links
- Format each PR as: <a href="${repo_url}/pull/NUMBER">#NUMBER</a>: Title
Guidelines for the SYNOPSIS sections:
- Group related changes together under clear headings (use <h2> tags)
- Use bullet points (<ul><li>) for individual changes
- Highlight any significant new features, breaking changes, or important fixes
- Keep the tone professional but accessible
- Don't include PR numbers in the synopsis - focus on what changed and why it matters
IMPORTANT: Output valid HTML that can be used directly in an email body. Do NOT wrap in \`\`\`html code blocks - just output the raw HTML.
Here are the merged PRs:
PROMPT_HEADER
cat "$input_file" >> "$prompt_file"
echo"" >> "$prompt_file"
echo"Generate the synopsis now:" >> "$prompt_file"
# Use Claude CLI to generate the synopsis, wrapped in proper HTML with charset
localraw_output="/tmp/eagle0_raw_$$.html"
cat "$prompt_file"| claude --print > "$raw_output"
# Wrap in HTML document with UTF-8 charset
cat > "$output_file"<<'HTML_HEAD'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
HTML_HEAD
cat "$raw_output" >> "$output_file"
echo"</body></html>" >> "$output_file"
rm -f "$prompt_file""$raw_output"
echo"Synopsis generated at: $output_file"
}
# Get Fastmail session info (account ID, identity ID, drafts mailbox ID)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.