Compare commits

...
Author SHA1 Message Date
admin 2071176054 Use prefabs for remaining Handle Riot panels 2026-07-06 09:58:50 -07:00
admin d527d47010 Use prefab for Handle Riot Give panel 2026-07-06 09:30:28 -07:00
adminandGitHub c5c8252325 Polish March narrow prefab layout (#8303) 2026-07-06 09:15:55 -07:00
adminandGitHub 36124919b9 Stop resizing March recommended button (#8302)
* Stop resizing March recommended button

* Preserve March recommended button prefab height
2026-07-06 09:06:44 -07:00
adminandGitHub 4ffdca7c8d Use March narrow tab panes from prefab (#8301) 2026-07-06 08:27:32 -07:00
adminandGitHub 6eaf748d6f Log mac notarization submit failures (#8300) 2026-07-06 08:23:08 -07:00
adminandGitHub 5a346cd8df Remove dead March narrow battalions field (#8299)
* Remove dead March narrow battalions field

* Move March narrow recommended button into prefab

* Remove dead March narrow battalions table
2026-07-06 08:05:59 -07:00
adminandGitHub 585cbb135c Use occupy wording for border security quest (#8297) 2026-07-06 06:56:43 -07:00
adminandGitHub 292573e137 Use Rest command panel prefab (#8296) 2026-07-06 06:46:06 -07:00
adminandGitHub 3492e49433 Use Train command panel prefab (#8295) 2026-07-06 06:32:14 -07:00
adminandGitHub 4529e0fa2b Improve March iPad layout (#8294)
* Improve March iPad layout

* Refine March narrow supply controls

* Polish March narrow supply controls

* Move March recommended button into units header

* Rework March narrow tabs and supply controls

* Center March narrow minus glyph

* Move March supply glyphs into prefab
2026-07-05 22:04:07 -07:00
adminandGitHub 9a68094b07 Use March panel prefab in Eagle scene (#8293)
* Use March panel prefab in Eagle scene

* Wire March unit selector scene controller

* Move March scene controller override to March instance
2026-07-05 14:07:46 -07:00
adminandGitHub 1ca8e81062 Use handle captured hero panel prefab in Eagle scene (#8292) 2026-07-05 13:36:31 -07:00
adminandGitHub 9e430e786e Reopen closed Postgres client text connections (#8291) 2026-07-05 11:18:52 -07:00
adminandGitHub bb302001f2 Use Feast panel prefab in Eagle scene (#8290) 2026-07-05 07:26:54 -07:00
adminandGitHub 7c300733e0 Use Trade command panel prefab in scene (#8289)
* Use Trade command panel prefab in scene

* Make Trade controls touch sized

* Refine Trade slider controls

* Balance Trade resource controls

* Hide Trade plus button backing images

* Use round Trade plus backgrounds

* Soften Trade plus artwork

* Use plus-only Trade button icon
2026-07-04 21:43:36 -07:00
adminandGitHub 57b6f6f1eb Format low-loyalty commander tooltip (#8288)
* Format low-loyalty commander tooltip

* Improve commander warning tooltip layout
2026-07-04 11:00:49 -07:00
adminandGitHub 5bddf66240 Allow quest completion without faction leader present (#8287) 2026-07-03 19:49:16 -07:00
adminandGitHub c8fc611182 Guard battle navigation until Shardok model is ready (#8286) 2026-07-03 19:34:13 -07:00
adminandGitHub cbd1058523 Hide unknown volley counts on map (#8285)
* Hide unknown volley counts on map

* Use pattern matching for known volley count
2026-07-03 19:22:01 -07:00
adminandGitHub e0d67f4750 Reserve battle progress action button space (#8284) 2026-07-03 19:08:54 -07:00
adminandGitHub 5b2c0214db Use Send Supplies command panel prefab (#8282) 2026-07-03 18:55:00 -07:00
adminandGitHub 511c69b14e Move volley indicator off profession icon (#8283)
* Move volley indicator off profession icon

* Place volley badge beside unit size

* Shrink volley badge
2026-07-03 18:46:14 -07:00
adminandGitHub 9ebe6dc841 Update Zander headshot (#8281) 2026-07-03 18:06:26 -07:00
adminandGitHub f2bed66119 Let Organize Troops controls column fit warnings (#8280) 2026-07-03 18:06:01 -07:00
adminandGitHub 96eebb87af Show volley indicators on Shardok tiles (#8279)
* Show volley indicator on Shardok tiles

* Center Shardok volley tile indicator

* Raise Shardok volley tile indicator

* Add backing halo to volley indicators

* Remove unused volley quiver field
2026-07-03 17:24:33 -07:00
adminandGitHub 716ee0ca50 Animate low morale Shardok units (#8274)
* Animate low morale Shardok units

* Make low morale effect clearer
2026-07-03 16:56:51 -07:00
adminandGitHub 6492215005 Add morale input to custom battle units (#8278)
* Add optional common battalion morale

* Add morale input to custom battle units
2026-07-03 16:45:12 -07:00
adminandGitHub ab5c337b22 Honor custom battle battalion morale (#8277) 2026-07-03 16:44:54 -07:00
adminandGitHub 16d82332cd Add optional common battalion morale (#8276) 2026-07-03 16:43:12 -07:00
adminandGitHub 023ffbafc9 Animate tired Shardok heroes (#8273)
* Animate tired Shardok heroes

* Increase tired effect visibility
2026-07-03 15:56:56 -07:00
adminandGitHub 8ec8b902f6 Animate stunned Shardok units (#8272)
* Animate stunned Shardok units

* Fix stunned tile effect setup

* Avoid TMP outline on stunned effect stars

* Improve stunned effect contrast
2026-07-03 15:41:17 -07:00
adminandGitHub ae4916e7bf Lower ransom offer panel contents (#8271)
* Lower ransom offer panel contents

* Fix ransom offer panel header layout

* Keep ransom offer bottom controls inside panel

* Restructure ransom offer panel layout

* Increase ransom offer title row height

* Inset ransom offer content row bottom

* Make ransom offer content row shrinkable

* Constrain ransom offer main content height

* Move ransom offer height constraint to main row

* Match ransom offer main child heights
2026-07-03 09:09:18 -07:00
adminandGitHub 73699ae901 Use Exile Vassal command panel prefab in scene (#8269) 2026-07-01 15:55:37 -07:00
adminandGitHub 910c60236e Color province commanders by loyalty (#8270) 2026-07-01 15:55:26 -07:00
adminandGitHub 9b989dd35e Use Divine command panel prefab in scene (#8268) 2026-07-01 10:15:05 -07:00
adminandGitHub 9400dbeba2 Use Diplomacy command panel prefab in scene (#8267)
* Use Diplomacy command panel prefab in scene

* Restore Eagle scene YAML header
2026-07-01 09:12:59 -07:00
adminandGitHub 33f6c4dfc9 Teach water crossing AI about frozen crossings (#8247) 2026-07-01 08:55:12 -07:00
adminandGitHub 52941d9f50 Preserve captured units in Shardok game state views (#8266) 2026-07-01 08:42:23 -07:00
adminandGitHub 4cdda1fab6 Keep Shardok touch taps tolerant after long-press changes (#8265) 2026-07-01 06:57:19 -07:00
adminandGitHub 67749672fd Use Defend command panel prefab in scene (#8264) 2026-06-30 16:16:14 -07:00
adminandGitHub 23b6dfb0a9 Use Decline Quest command panel prefab in scene (#8260) 2026-06-30 10:39:13 -07:00
adminandGitHub c98eeb9f88 Improve Shardok touch long-press tracking (#8263)
* Improve Shardok touch long-press tracking

* Avoid restarting active touch long presses

* Lengthen Shardok long-press threshold
2026-06-30 10:34:44 -07:00
adminandGitHub ec0c391467 Use server battle access flags in client (#8262) 2026-06-30 07:55:29 -07:00
adminandGitHub 7305149729 Gate battle actions by server order (#8261)
* Gate battle actions by server battle order

* Allow observing all unblocked battles

* Require explicit battle access flags
2026-06-30 07:12:43 -07:00
adminandGitHub 556094d15d Make grid long press tolerant of finger jitter (#8259) 2026-06-30 06:44:38 -07:00
adminandGitHub 22c7965314 Order battle views server-side (#8257) 2026-06-30 06:42:22 -07:00
adminandGitHub 4fd8f81686 Use Control Weather command panel prefab in scene (#8258) 2026-06-30 06:35:13 -07:00
adminandGitHub 3d650ff5c8 Use Battle Aftermath command panel prefab in scene (#8256) 2026-06-29 21:24:44 -07:00
adminandGitHub 9e2bcdf8a5 Use Attack Decision command panel prefab in scene (#8255)
* Use Attack Decision command panel prefab in scene

* Fix Attack Decision button icon sizing
2026-06-29 21:11:33 -07:00
adminandGitHub d2d85f1a2d Use Apprehend Outlaw command panel prefab in scene (#8254) 2026-06-29 17:06:08 -07:00
adminandGitHub b90a5835da Use Alms command panel prefab in scene (#8253)
* Use Alms command panel prefab in scene

* Remove redundant Alms prefab overrides
2026-06-29 16:53:10 -07:00
adminandGitHub 54c418c07b Tighten Organize Troops wide layout (#8252)
* Tighten Organize Troops wide layout

* Fit available battalion rows with larger buttons

* Cap Organize Troops table outside narrow layout

* Narrow Organize Troops table cap
2026-06-29 14:07:11 -07:00
adminandGitHub d4083791c1 Reflow Organize Troops controls for iPad (#8246)
* Reflow Organize Troops controls for iPad

* Apply Organize Troops layout to scene panel

* Use prefab instance for Organize Troops panel

* Fix Organize Troops bottom controls layout

* Improve Organize Troops bottom strip spacing

* Set Organize Troops action button height

* Keep Organize Troops controls inside panel

* Place Organize Troops controls under battalion table

* Match Organize Troops reset button height

* Make Organize Troops controls responsive

* Fix Organize Troops wide controls layout
2026-06-29 12:58:59 -07:00
adminandGitHub 0ed403ded1 Add abnormal LLM diagnostics (#8251) 2026-06-29 12:38:04 -07:00
adminandGitHub f8b064f28b Fail incomplete LLM streams (#8250) 2026-06-29 10:44:57 -07:00
adminandGitHub 38973f07a0 Add LLM stream stall diagnostics (#8249) 2026-06-29 09:25:18 -07:00
adminandGitHub 1f5bfe9e4d Back off failed LLM text retries (#8248)
* Back off failed LLM text retries

* Limit chronicle history in LLM prompts

* Revert "Limit chronicle history in LLM prompts"

This reverts commit 7f22be26eb00b17aaf77982173688fc9e5c7e3d3.

* Add diagnostics for repeated LLM retries
2026-06-29 08:48:44 -07:00
adminandGitHub 1d0b1dafed Harden FlatbufferWrapper empty state handling (#8243)
* Harden FlatbufferWrapper empty state handling

* Tighten FlatbufferWrapper invalid input handling
2026-06-28 21:48:44 -07:00
adminandGitHub 2571f65ac4 Guard music source time reads (#8244) 2026-06-28 15:57:55 -07:00
adminandGitHub f52fd34879 Place prisoner move dropdown above Move (#8242) 2026-06-28 12:26:57 -07:00
adminandGitHub 3610c34b4a Extract pure LLM update batch planning (#8240) 2026-06-28 12:25:25 -07:00
adminandGitHub 30390816df Add Manage Prisoners info panel (#8233)
* Add Manage Prisoners info panel

* Refine Manage Prisoners info panel layout

* Simplify Manage Prisoners info layout

* Show Manage Prisoners move dropdown when selected
2026-06-28 08:50:57 -07:00
adminandGitHub 9c77f2f843 Use a blocking queue for LLM updates (#8239) 2026-06-28 08:33:31 -07:00
adminandGitHub c7fa4c5180 Ignore stale AI transposition table entries (#8238) 2026-06-28 08:24:23 -07:00
adminandGitHub 66d544fb0a Use immutable queue snapshots for LLM updates (#8237)
* Use immutable queue snapshots for LLM updates

* Name the LLM update queue monitor
2026-06-28 08:17:51 -07:00
adminandGitHub 7f62f07d0b Avoid early return when loading raw Postgres history (#8235) 2026-06-28 08:15:25 -07:00
adminandGitHub 2829ec1aae Harden MCTS legal action cache key (#8236) 2026-06-28 08:14:02 -07:00
adminandGitHub 97ffb0633d Build admin action JSON without mutable buffer (#8234) 2026-06-27 22:11:39 -07:00
adminandGitHub f378429786 Own castle coords in AI lookahead futures (#8230) 2026-06-27 22:06:36 -07:00
adminandGitHub f755d89b2a Remove early returns from game loading (#8232) 2026-06-27 22:05:46 -07:00
adminandGitHub a8e200ab03 Fix Manage Prisoners button icon sizing (#8229)
* Fix Manage Prisoners button icon sizing

* Use Manage Prisoners prefab in Eagle scene

* Narrow Manage Prisoners scene prefab replacement

* Fix Manage Prisoners prefab scene reference
2026-06-27 16:01:31 -07:00
adminandGitHub 68bec95772 Make pregenerated text population pure (#8231) 2026-06-27 15:43:52 -07:00
adminandGitHub fde9529a31 Fix command token advancement for automatic results (#8227)
* Stop warning for replacement command tokens

* Revert "Stop warning for replacement command tokens"

This reverts commit fab7fa47678b71f76076142f35465d36fb3fc456.

* Fix command token advancement for automatic results
2026-06-27 11:15:24 -07:00
adminandGitHub 0d044a7468 Prefer light cavalry for mage combat assignments (#8225)
* Prefer light cavalry for mage combat assignments

* Reserve selected light cavalry for mages

* Cover generic and ranger combat unit pairing
2026-06-27 11:09:18 -07:00
adminandGitHub ee4b9c1d8f Hide battle ended panel until battle can exit (#8228) 2026-06-27 09:35:38 -07:00
adminandGitHub a7bd3deb76 Allow Wardens from constitution gains (#8226) 2026-06-26 20:44:53 -07:00
adminandGitHub 9e69b213b0 Use correct article for singular beast events (#8223) 2026-06-26 18:54:50 -07:00
adminandGitHub f75ac01959 Widen Organize Troops table (#8222)
* Widen organize troops table

* Widen scene organize troops table

* Give organize troops table more room

* Tighten organize troops row layout

* Remove organize troops disclosure triangle

* Align organize troops header and row layout

* Center organize troops type icons

* Inset organize troops type column
2026-06-26 18:48:52 -07:00
adminandGitHub 3eafc2b31c Narrow GamesManagerState helper API (#8221) 2026-06-26 14:58:14 -07:00
adminandGitHub b39f48e8d6 Preserve copied Shardok history (#8219) 2026-06-26 14:54:06 -07:00
adminandGitHub 4346e2dc6f Move running games merge into state (#8220) 2026-06-26 14:53:35 -07:00
adminandGitHub 85122128bf Move flush marker mod time update into state (#8217) 2026-06-26 10:40:24 -07:00
adminandGitHub 5a58ea3ffb Preserve Shardok copy controller state (#8218) 2026-06-26 10:39:44 -07:00
adminandGitHub e1237da088 Keep copied Shardok command caches self-contained (#8213)
* Do not copy Shardok command cache

* Keep copied command caches self-contained
2026-06-26 10:03:54 -07:00
adminandGitHub b4eebf5ead Move timed state checks into transitions (#8215) 2026-06-26 10:02:28 -07:00
adminandGitHub 922a825648 Add waiting game player accessors (#8212) 2026-06-26 09:57:44 -07:00
adminandGitHub 3602d439a5 Make Shardok end turn button touch sized (#8214) 2026-06-26 09:55:20 -07:00
adminandGitHub 1718d8e885 Fix Arm Troops TMP wrapping warning (#8211) 2026-06-26 09:49:01 -07:00
adminandGitHub f58f3ac31a Move waiting game join transition into state (#8209) 2026-06-26 09:45:06 -07:00
adminandGitHub 6e65b556b4 Remove MCTS state engine cache (#8210) 2026-06-26 09:44:39 -07:00
adminandGitHub d86597b92e Show Shardok tile info after touch taps (#8184)
* Show Shardok tile info after touch taps

* Refine Shardok touch info panel behavior

* Suppress mouse hover after Shardok touch release

* Centralize Shardok touch info priority
2026-06-26 09:41:37 -07:00
adminandGitHub ce667c5784 Bake narrow March supply touch targets (#8194)
* Bake narrow March supply touch targets

* Restore compact March narrow layout

* Stop inflating unit selector child icons

* Tighten unit selector hero row layout
2026-06-26 09:38:33 -07:00
adminandGitHub 13a939f88e Move waiting game creation into state (#8208) 2026-06-26 09:37:53 -07:00
adminandGitHub 52ae7cad7f Cache immutable MCTS legal actions (#8206)
* Cache immutable MCTS legal actions

* Avoid mutating cached engines during legal action lookup
2026-06-26 09:32:10 -07:00
adminandGitHub eacad46c9b Move waiting game join helpers into state (#8207) 2026-06-26 09:31:37 -07:00
adminandGitHub 1bd8fc7154 Move waiting player removal into state (#8205) 2026-06-26 09:18:41 -07:00
adminandGitHub 50ccb1b7d8 Cooldown failing LLM model fallbacks (#8204) 2026-06-26 08:34:36 -07:00
adminandGitHub c693553365 Propagate MCTS worker exceptions (#8202) 2026-06-26 08:33:40 -07:00
adminandGitHub 6c3e4bc202 Move saved running games state transition (#8203) 2026-06-26 08:33:17 -07:00
adminandGitHub 92d9a2dbd8 Centralize GamesManager state reads (#8201) 2026-06-26 07:58:25 -07:00
adminandGitHub 5df24d23c0 Tune Arm Troops iPad header spacing (#8200)
* Tune Arm Troops iPad header spacing

* Prevent Arm Troops name overlap

* Align Arm Troops action column headers

* Tighten Arm Troops header slots
2026-06-26 07:57:49 -07:00
adminandGitHub 1215b5044c Remove retired Gemini fallback models (#8199)
* Remove retired Gemini fallback models

* Prefer cheaper Gemini fallbacks first
2026-06-26 07:46:55 -07:00
adminandGitHub b1263ec4df Include AI failure context in stream errors (#8193)
* Include AI failure context in stream errors

* Fix AI diagnostics end condition logging
2026-06-26 07:44:44 -07:00
adminandGitHub 19e85bde8b Document Unity scene churn guardrail (#8198) 2026-06-26 07:33:57 -07:00
adminandGitHub cbe71f4fd4 Use state loaded checks in raw rewind (#8195) 2026-06-26 07:29:18 -07:00
adminandGitHub 52afd836ac Prevent dangling settings accessors (#8192) 2026-06-26 07:28:40 -07:00
adminandGitHub 095a29a4a0 Centralize Shardok C++ standard settings (#8197) 2026-06-26 07:28:26 -07:00
adminandGitHub 49ff9f8b09 Fix Shardok clang-tidy macOS sysroot (#8196) 2026-06-26 07:18:55 -07:00
adminandGitHub 457c1e933a Tighten Shardok test fixture namespaces (#8191) 2026-06-26 04:52:35 -07:00
adminandGitHub f5e56f0da9 Use state controller lookup in rewind (#8190) 2026-06-26 04:52:09 -07:00
adminandGitHub 0d8df5e97c Add Shardok clang-tidy CI gate (#8189) 2026-06-26 04:51:41 -07:00
adminandGitHub 92af09660c Use state controller lookup in tutorial setup (#8188) 2026-06-25 21:54:09 -07:00
adminandGitHub 435e7e08f3 Use state lookups in GamesManager admin paths (#8187) 2026-06-25 21:41:59 -07:00
adminandGitHub 2516c42c08 Move waiting game lookup into state (#8185) 2026-06-25 21:38:29 -07:00
adminandGitHub 19dc4fd1b1 Use direct empty vectors in remaining helpers (#8186) 2026-06-25 21:37:58 -07:00
adminandGitHub 0305cc7506 Add widescreen Arm Troops column spacing (#8183) 2026-06-25 19:50:18 -07:00
adminandGitHub 5854929085 Use explicit casts for weather proto conversion (#8182) 2026-06-25 19:44:12 -07:00
adminandGitHub 232e560724 Use integral hero test constants (#8181) 2026-06-25 19:43:53 -07:00
adminandGitHub 994de5cdc2 Use state controller lookups in command flow (#8180) 2026-06-25 19:43:34 -07:00
adminandGitHub dcc303ad37 Reconnect Arm Troops panel to prefab (#8174)
* Reconnect Arm Troops scene panel to prefab

* Give Arm Troops row names layout width

* Restore Arm Troops panel layout widths

* Use compact Arm Troops table in touch layout

* Tighten Arm Troops touch table width
2026-06-25 19:41:54 -07:00
adminandGitHub 84ecc4d13d Use direct empty vector initialization (#8179) 2026-06-25 19:08:53 -07:00
adminandGitHub eb338b1d0b Replace broad Shardok namespace imports (#8178) 2026-06-25 19:06:22 -07:00
adminandGitHub 1b1261f4f6 Move loaded running game snapshots into state (#8177) 2026-06-25 18:58:47 -07:00
adminandGitHub 3fff156344 Evaluate flee commands with weighted odds (#8176) 2026-06-25 18:54:04 -07:00
adminandGitHub 3d39238045 Move lobby state queries into GamesManagerState (#8175) 2026-06-25 18:49:50 -07:00
adminandGitHub 705b176992 Prefer fleeing uncertain final-round battles (#8173) 2026-06-25 13:24:31 -07:00
adminandGitHub 1c2e150899 Synchronize Shardok AI choice diagnostics (#8169) 2026-06-25 13:19:21 -07:00
adminandGitHub 8af820c58e Use GamesManagerState controller queries in callbacks (#8170) 2026-06-25 13:18:43 -07:00
adminandGitHub 940a4836a7 Reserve MCTS legal action cache once (#8171) 2026-06-25 13:18:20 -07:00
adminandGitHub 1ef2fa0f84 Fall back across Gemini models on demand errors (#8172) 2026-06-25 13:17:54 -07:00
adminandGitHub 7efdf90100 Restore Shardok touch target overlays (#8157)
* Restore Shardok touch target overlays

* Handle touch taps on Shardok grid
2026-06-25 09:17:03 -07:00
adminandGitHub be930740cf Include SSE response details in failure logs (#8168) 2026-06-25 09:13:30 -07:00
adminandGitHub 241b49ac23 Make broadcast notification count expression-based (#8167) 2026-06-25 09:10:00 -07:00
adminandGitHub 77499dbdad Avoid sharing AI sequence random generators (#8164) 2026-06-25 08:21:10 -07:00
adminandGitHub a4c688f126 Widen Arm Troops pointer table (#8159)
* Widen Arm Troops pointer table

* Fit wider Arm Troops pointer rows

* Apply Arm Troops pointer layout to scene
2026-06-25 08:20:02 -07:00
adminandGitHub 5ee753e989 Move cache state queries into GamesManagerState (#8165) 2026-06-25 08:19:12 -07:00
adminandGitHub 77c6c16096 Handle legacy game snapshots without eagle maps (#8166) 2026-06-25 08:18:42 -07:00
adminandGitHub 2105ccc791 Move controller state queries into GamesManagerState (#8162) 2026-06-25 08:07:53 -07:00
adminandGitHub e03af03acb Move lobby updates into GamesManagerState (#8160) 2026-06-25 07:48:39 -07:00
adminandGitHub 041a43be97 Handle legacy game snapshots without dates (#8161) 2026-06-25 07:48:14 -07:00
adminandGitHub 53180baddd Move controller updates into GamesManagerState (#8158) 2026-06-25 07:18:49 -07:00
adminandGitHub b88613a6f1 Move controller removal into GamesManagerState (#8156) 2026-06-25 07:10:55 -07:00
adminandGitHub dbc5455de0 Add Shardok AI evaluator exception context (#8155) 2026-06-25 07:10:19 -07:00
adminandGitHub 17357eba33 Centralize GamesManager state reads (#8152) 2026-06-25 06:56:03 -07:00
adminandGitHub ff9ab1907b Stabilize Shardok AI lookahead command lookup (#8153) 2026-06-25 06:55:23 -07:00
adminandGitHub 0b9ebb04c8 Split Arm Troops controls by input mode (#8085)
* Split Arm Troops controls by input mode

* Allow editor touch simulation for Arm Troops

* Constrain Arm Troops touch action strip

* Move Arm Troops touch controls beside table

* Compact Arm Troops touch controls

* Tighten Arm Troops touch table width

* Polish Arm Troops touch selection controls

* Clarify Arm Troops touch columns

* Align Arm Troops touch column headers

* Separate Arm Troops all-battalion controls

* Use column layout for Arm Troops touch controls

* Revert "Use column layout for Arm Troops touch controls"

This reverts commit e6273c22f900e990c86fcac5e761723d54bd15d0.

* Use dedicated touch all-battalions controls

* Handle missing touch button style sources

* Revert "Handle missing touch button style sources"

This reverts commit 2a2b2204bbc0deede8e92ffa5d07cd86ccf3cb96.

* Revert "Use dedicated touch all-battalions controls"

This reverts commit 44fd112162518b9d0658f9374af5771196ccefbb.

* Fit Arm Troops all-battalion touch column

* Use correct Arm Troops all-controls parent

* Align Arm Troops touch column headers
2026-06-25 06:46:45 -07:00
adminandGitHub 10a3b3a046 Store evacuate prisoners settings getter by value (#8151)
* Store evacuate prisoners settings getter by value

* Keep evacuate settings getter const
2026-06-25 06:43:42 -07:00
adminandGitHub 7cf852ab52 Synchronize GamesManager state updates (#8150) 2026-06-25 06:40:04 -07:00
adminandGitHub 2341df8998 Synchronize GamesManager state snapshots (#8149) 2026-06-25 05:20:18 -07:00
adminandGitHub 190e71d3c4 Validate sequence random generator input (#8148) 2026-06-24 22:19:06 -07:00
adminandGitHub c0a9b90715 Decode Shardok unit bytes safely in tests (#8140) 2026-06-24 22:13:40 -07:00
adminandGitHub 6d13774fd0 Synchronize GamesManager creation paths (#8146) 2026-06-24 22:13:17 -07:00
adminandGitHub 41252ccaa6 Harden random generator input validation (#8147) 2026-06-24 22:12:56 -07:00
adminandGitHub ff05a35991 Centralize GamesManager state transitions (#8143) 2026-06-24 21:52:31 -07:00
adminandGitHub 8b7e9eaeb6 Print Shardok AI failure ids numerically (#8145) 2026-06-24 21:51:59 -07:00
adminandGitHub 7f80eb0ef2 Clamp random integer endpoint samples (#8144)
* Clamp random integer endpoint samples

* Cover endpoint undead command fallback
2026-06-24 21:51:18 -07:00
adminandGitHub ef151c2ef4 Log Shardok AI failure context (#8142) 2026-06-24 15:57:37 -07:00
adminandGitHub e7cad8dd36 Extract immutable GamesManager state (#8138)
* Extract immutable games manager state

* Use direct GamesManager state deps

* Add direct ControllerInfo deps
2026-06-24 15:53:15 -07:00
adminandGitHub c4edb2f6a0 Remove raw Unit byte casts in Shardok paths (#8141) 2026-06-24 15:43:42 -07:00
adminandGitHub 11709e6d48 Decode secured Shardok units safely (#8139) 2026-06-24 15:36:50 -07:00
adminandGitHub 0b0fcc411d Extract game controller registry (#8135)
* Extract game controller registry

* Split game controller registry target

* Make game controller registry immutable
2026-06-24 15:23:50 -07:00
adminandGitHub 1aadcc863f Copy action result unit bytes before reading (#8137) 2026-06-24 10:51:22 -07:00
adminandGitHub 9ac167e66c Add Shardok sanitizer CI (#8136) 2026-06-24 10:31:07 -07:00
adminandGitHub 1530873df5 Bound action filter leakage logging (#8134) 2026-06-24 09:41:59 -07:00
adminandGitHub 75a5b4fa21 Use timed logger in Shardok interface client (#8133) 2026-06-24 09:41:44 -07:00
adminandGitHub b6c45da7bc Use timed logger for history fallback messages (#8131) 2026-06-24 09:41:28 -07:00
adminandGitHub 5e30673281 Use timed logger in client text stores (#8132) 2026-06-24 09:41:14 -07:00
adminandGitHub d38c611a18 Use timed logger in exception interceptor (#8128) 2026-06-24 08:59:09 -07:00
adminandGitHub 570bc557f5 Remove temporary backstory visibility debug check (#8130) 2026-06-24 08:58:56 -07:00
adminandGitHub c1f4152cae Cache target indexes in attack grouping (#8129) 2026-06-24 08:58:04 -07:00
adminandGitHub 0472362abc Broaden ActionResultC copy coverage (#8127) 2026-06-24 08:53:05 -07:00
adminandGitHub b732b60520 Use indexed distances for CoordsSet scans (#8126) 2026-06-24 08:52:17 -07:00
adminandGitHub fc1f988654 Clarify command option validation (#8125) 2026-06-24 08:48:58 -07:00
adminandGitHub 7ef93c4a11 Optimize sparse CoordsSet filtering (#8124) 2026-06-24 08:45:55 -07:00
adminandGitHub 247aa3e685 Reject malformed ransom offer commands (#8122) 2026-06-24 08:26:44 -07:00
adminandGitHub aa0292bad6 Use CoordsSet index iterator for victory scoring (#8123) 2026-06-24 08:26:11 -07:00
adminandGitHub 08cd608ac2 Use CoordsSet index iterator for attack locations (#8121) 2026-06-24 08:05:03 -07:00
adminandGitHub f85a32dc08 Fix ActionResultC copy removed faction ids (#8118) 2026-06-24 07:54:12 -07:00
adminandGitHub b5deb69ef7 Template CoordsSet filter (#8120) 2026-06-24 07:53:22 -07:00
adminandGitHub 872883f710 Shrink CoordsSet dimension fields (#8119) 2026-06-24 07:10:35 -07:00
adminandGitHub 3d402e7bdd Add CoordsSet microbenchmark (#8117) 2026-06-24 07:04:38 -07:00
adminandGitHub 862480c470 Assert CoordsSet bounds in debug builds (#8100) 2026-06-24 06:54:40 -07:00
adminandGitHub 98b5d7f034 Standardize CoordsSet iterators (#8103)
* Standardize CoordsSet iterators

* Add CoordsSet iterator tests
2026-06-24 06:48:42 -07:00
adminandGitHub e5c5a929ec Use inside for action-result Option lookups (#8113)
* Use inside for feast changed hero lookups

* Use inside for march command province lookups

* Use inside for diplomacy command lookups

* Use inside for captured hero deferred changes

* Use inside for applier relationship lookups

* Refresh CI after CoordsSet main update
2026-06-24 06:47:05 -07:00
adminandGitHub d39ee392b9 Use inside for command-choice Option selections (#8114)
* Use inside for expand command choices

* Use inside for offer command selections

* Refresh CI after CoordsSet main update
2026-06-24 06:46:38 -07:00
adminandGitHub b574c6c950 Use inside for availability command Options (#8115)
* Use inside for diplomacy command Options

* Use inside for resolve availability Options

* Use inside for defend and train Options

* Refresh CI after CoordsSet main update
2026-06-24 06:43:40 -07:00
adminandGitHub 074c984f3b Fix CoordsSet test raw index helper name (#8116) 2026-06-24 06:04:02 -07:00
adminandGitHub 2817ca529d Clarify CoordsSet dimension naming (#8098) 2026-06-24 05:34:14 -07:00
adminandGitHub 4f932d0a56 Remove unsafe CoordsSet string conversion (#8097) 2026-06-24 05:33:42 -07:00
adminandGitHub 2dd931dc8f Expand CoordsSet test coverage (#8105) 2026-06-24 05:32:30 -07:00
adminandGitHub c7ab2ecb44 Rename CoordsSet raw index add (#8102) 2026-06-24 05:30:28 -07:00
adminandGitHub 8466ab6ed9 Use inside for decline quest availability Option (#8096) 2026-06-23 22:17:47 -07:00
adminandGitHub 7b74905973 Use inside for divine availability Options (#8095) 2026-06-23 22:17:33 -07:00
adminandGitHub ecfb5a0a94 Use inside for excess supplies Options (#8089) 2026-06-23 22:17:19 -07:00
adminandGitHub 967cb74aa5 Use inside for recruit heroes Options (#8094) 2026-06-23 22:16:54 -07:00
adminandGitHub 34daf8f38d Make byte_vector own its storage (#8086) 2026-06-23 22:14:23 -07:00
adminandGitHub e12359821c Use inside for apprehend outlaw Options (#8093) 2026-06-23 22:10:51 -07:00
adminandGitHub eea41e6c05 Use inside for march destination lookups (#8092) 2026-06-23 22:10:34 -07:00
adminandGitHub 36d1e75719 Use inside for manage prisoner lookups (#8091) 2026-06-23 22:10:13 -07:00
adminandGitHub 35e93c8ce1 Use inside for control weather availability Options (#8090) 2026-06-23 22:09:50 -07:00
adminandGitHub d603b01733 Use inside for game state view full info (#8088) 2026-06-23 22:09:33 -07:00
adminandGitHub 6c66b5df82 Use inside for game state view diffs (#8087) 2026-06-23 22:09:09 -07:00
adminandGitHub 3b59e21407 Normalize Shardok engine test suffixes (#8083) 2026-06-23 21:49:21 -07:00
adminandGitHub d0e74ac4cb Consolidate nodiscard helper annotations (#8078) 2026-06-23 21:49:08 -07:00
adminandGitHub c04aa3d753 Use inside for unaffiliated hero change lookups (#8077) 2026-06-23 21:48:45 -07:00
adminandGitHub f3fade7ed2 Use inside for new round action lookups (#8076) 2026-06-23 21:48:30 -07:00
adminandGitHub 79d8e837e8 Use inside for riot command choice new values (#8074) 2026-06-23 21:48:08 -07:00
adminandGitHub c39795fa9f Use inside for diplomacy availability Options (#8072) 2026-06-23 21:47:31 -07:00
adminandGitHub 4a7c55a382 Use inside for view test lookups (#8068) 2026-06-23 21:45:43 -07:00
adminandGitHub 7e4396552e Use inside for resource availability Options (#8063) 2026-06-23 21:45:30 -07:00
adminandGitHub 40219f2a67 Replace remaining numeric functional casts (#8079) 2026-06-23 21:45:09 -07:00
adminandGitHub ce35c176ee Make CoordsSet iterator casts explicit (#8082) 2026-06-23 21:44:53 -07:00
adminandGitHub a852f03214 Use from_chars for hex byte parsing (#8084) 2026-06-23 21:40:53 -07:00
adminandGitHub 7c8b43d91b Tidy AI unit score readability (#8080) 2026-06-23 19:26:43 -07:00
adminandGitHub ecf763b24b Tidy common byte helpers (#8081) 2026-06-23 19:25:24 -07:00
adminandGitHub 8ad4fc52e3 Use inside for province availability Options (#8060) 2026-06-23 19:24:16 -07:00
adminandGitHub 7e48f6fd2b Use inside for hero availability Options (#8058) 2026-06-23 19:24:00 -07:00
adminandGitHub 5c81832cde Align notification popup height with command panel (#8061)
* Align notification popup height with command panel

* Widen Arm Troops table for touch controls

* Improve Arm Troops row button spacing

* Add Arm Troops scrollbar gutter
2026-06-23 19:21:06 -07:00
adminandGitHub 569458d691 Use inside for attack decision new values (#8066) 2026-06-23 17:42:33 -07:00
adminandGitHub c825bf4606 Use inside for supplies and riot availability Options (#8070) 2026-06-23 17:41:23 -07:00
adminandGitHub 567846156c Mark action point distance helpers nodiscard (#8040)
* Mark action point distance helpers nodiscard

* Allow APD cache warmup calls
2026-06-23 17:27:06 -07:00
adminandGitHub 14f1ab992c Bake remaining iPad touch target sizes (#8007) 2026-06-23 17:23:17 -07:00
adminandGitHub 42cb9291fc Use inside for request battle test lookups (#8051) 2026-06-23 17:22:21 -07:00
adminandGitHub aa2bf97078 Use inside for tutorial test Options (#8050) 2026-06-23 17:22:07 -07:00
adminandGitHub 6976db961b Use inside for upgrade battalion quest Options (#8049) 2026-06-23 17:21:47 -07:00
adminandGitHub 1823543706 Use inside for alliance and army quest Options (#8048) 2026-06-23 17:21:31 -07:00
adminandGitHub c452b8be69 Use inside for recon quest result Options (#8047) 2026-06-23 17:21:13 -07:00
adminandGitHub 53612938b6 Mark victory score helpers nodiscard (#8046) 2026-06-23 17:20:57 -07:00
adminandGitHub ccb64480a8 Mark private AI score helpers nodiscard (#8045) 2026-06-23 17:20:44 -07:00
adminandGitHub 4f0a172036 Mark action result state helpers nodiscard (#8044) 2026-06-23 17:20:33 -07:00
adminandGitHub ca45eba76b Use inside for resolve battle claimant lookups (#8053) 2026-06-23 17:19:47 -07:00
adminandGitHub 8f58543a4c Use inside for action test Option lookups (#8052) 2026-06-23 17:18:55 -07:00
adminandGitHub 453242e06a Use inside for availability command Options (#8054) 2026-06-23 17:18:27 -07:00
adminandGitHub 249a564ac7 Use inside for combat availability Options (#8055) 2026-06-23 17:17:46 -07:00
adminandGitHub 701c822e1a Use inside for trade and recon availability Options (#8056) 2026-06-23 17:17:29 -07:00
adminandGitHub 2ca4b6e8d4 Mark game state guesser helpers nodiscard (#8043) 2026-06-23 16:20:50 -07:00
adminandGitHub fe060e4489 Mark AI planning helpers nodiscard (#8042) 2026-06-23 16:20:36 -07:00
adminandGitHub 6dc29ec746 Mark command factory helpers nodiscard (#8041) 2026-06-23 16:20:14 -07:00
adminandGitHub 8b9ac89205 Mark geometry weather helpers nodiscard (#8039) 2026-06-23 16:19:59 -07:00
adminandGitHub 74bbd490d7 Mark AI helper results nodiscard (#8038) 2026-06-23 16:19:45 -07:00
adminandGitHub 7a5f89ea87 Mark flatbuffer conversion helpers nodiscard (#8037) 2026-06-23 16:19:24 -07:00
adminandGitHub bdcddb88e4 Mark unit combat helpers nodiscard (#8036) 2026-06-23 16:19:08 -07:00
adminandGitHub cccc7971c1 Use inside for order and weather quest results (#8035) 2026-06-23 16:18:49 -07:00
adminandGitHub 82d2ca915d Mark map query helpers nodiscard (#8034) 2026-06-23 16:18:30 -07:00
adminandGitHub 319dc0b19e Use inside for quest chooser Option results (#8033) 2026-06-23 16:18:09 -07:00
adminandGitHub 2573f0f2af Initialize flatbuffer wrapper offsets (#8032) 2026-06-23 16:17:45 -07:00
adminandGitHub 641244103f Use nullopt default for tile modifiers (#8031) 2026-06-23 16:17:28 -07:00
adminandGitHub 1b7909d914 Simplify identity Option matches (#8030) 2026-06-23 16:17:15 -07:00
adminandGitHub 9d7dfd8f5f Mark remaining command accessors nodiscard (#8029) 2026-06-23 16:16:33 -07:00
adminandGitHub 42edef2313 Clean up simple collection idioms (#8028) 2026-06-23 16:16:16 -07:00
adminandGitHub 7ad6677b90 Mark more command accessors nodiscard (#8027) 2026-06-23 16:11:15 -07:00
adminandGitHub ce19c34b8b Clarify OAuth Try Either handling (#8026) 2026-06-23 16:06:17 -07:00
adminandGitHub d675e3781f Mark command accessors nodiscard (#8025) 2026-06-23 16:05:55 -07:00
adminandGitHub 2164a98abe Use Map.empty for empty map values (#8024) 2026-06-23 16:05:33 -07:00
adminandGitHub b61ce58f4c Use nullopt for absent optional values (#8023) 2026-06-23 16:01:55 -07:00
adminandGitHub 7a11c8fa9a Use nullopt in hex map utilities (#8022) 2026-06-23 16:01:41 -07:00
adminandGitHub 62438ec4f6 Use nullopt for empty optional returns (#8021) 2026-06-23 16:01:26 -07:00
adminandGitHub afcc6a9a3e Remove const from random generator return (#8020) 2026-06-23 16:00:48 -07:00
adminandGitHub b6a094064a Use braced empty ally pid return (#8019) 2026-06-23 16:00:30 -07:00
adminandGitHub 70b8eb6513 Default initialize hex map directions (#8018) 2026-06-23 16:00:16 -07:00
adminandGitHub e4c7a0c98b Clarify CoordsSet index arithmetic (#8017) 2026-06-23 16:00:02 -07:00
adminandGitHub 67573e7a8a Use braced ActionCost returns (#8016) 2026-06-23 15:59:43 -07:00
adminandGitHub c60dce4504 Default GameStateW special members (#8015) 2026-06-23 15:59:09 -07:00
adminandGitHub 8250d3d42f Use concatenated ai battle simulator namespace (#8013) 2026-06-23 15:58:47 -07:00
adminandGitHub 5407c8aa65 Use concatenated MCTS test namespace (#8012) 2026-06-23 15:58:34 -07:00
adminandGitHub d282634264 Use concatenated score helper namespace (#8011) 2026-06-23 15:58:19 -07:00
adminandGitHub fff0e6dd30 Use concatenated ai testing namespaces (#8010) 2026-06-23 15:58:06 -07:00
adminandGitHub a1ccdc672c Use Vector.empty for empty vector values (#8014) 2026-06-23 15:57:14 -07:00
adminandGitHub 6802da55ed Consolidate Shardok ranges algorithm cleanup (#8005) 2026-06-23 15:42:09 -07:00
adminandGitHub 57dae50cf7 Consolidate AI battle simulator tidy cleanup (#8006) 2026-06-23 15:38:44 -07:00
adminandGitHub 4bce809fab Use iota for MCTS action indices (#8000) 2026-06-23 15:37:55 -07:00
adminandGitHub 2ff2cdab79 Use Vector.empty in utility fallbacks (#7969) 2026-06-23 15:37:36 -07:00
adminandGitHub 76839dcd59 Use Vector.empty in command result fields (#7971) 2026-06-23 15:37:21 -07:00
adminandGitHub effd5451b6 Use Vector.empty in command choice helpers (#7973) 2026-06-23 15:37:04 -07:00
adminandGitHub 00ac74f944 Use Vector.empty in quest creation (#7977) 2026-06-23 15:36:47 -07:00
adminandGitHub 728f1969ce Consolidate Shardok pre-increment cleanup (#8004) 2026-06-23 15:36:07 -07:00
adminandGitHub fab757f9dd Preserve live clients when replacing game controllers (#7970) 2026-06-23 15:35:21 -07:00
adminandGitHub fa006ba947 Use Vector.empty in availability factories (#7975) 2026-06-23 15:34:45 -07:00
adminandGitHub 2b61fb0fcf Simplify boolean comparisons in command tests (#7968) 2026-06-23 15:34:06 -07:00
adminandGitHub 62bcd3b669 Fix ransom offer popup iPad layout (#7967) 2026-06-23 14:08:23 -07:00
adminandGitHub 0fc394174c Trim redundant AI comments (#7919) 2026-06-23 14:04:25 -07:00
adminandGitHub 14015aebc7 Use explicit casts in command odds (#7920) 2026-06-23 14:04:09 -07:00
adminandGitHub 5acdcea0b0 Use ranges sort for MCTS action ordering (#7923) 2026-06-23 14:03:52 -07:00
adminandGitHub 709d3b9219 Use ranges sort for hero ids (#7925) 2026-06-23 14:03:40 -07:00
adminandGitHub 0f028e73d3 Use ranges find in test helpers (#7926) 2026-06-23 14:03:25 -07:00
adminandGitHub f0fa1f1a1d Use ranges find in view differ (#7927) 2026-06-23 14:03:08 -07:00
adminandGitHub 0a52e41c3d Use ranges contains in AI filter tests (#7928) 2026-06-23 14:02:51 -07:00
adminandGitHub 9ef750956c Use ranges any_of for cube line scan (#7929) 2026-06-23 14:02:33 -07:00
adminandGitHub b70d541224 Use sys.env for Scala environment lookups (#7930) 2026-06-23 14:02:16 -07:00
adminandGitHub a8299738d2 Simplify recon quest candidate selection (#7932) 2026-06-23 14:01:41 -07:00
adminandGitHub e789309827 Match battalion options in combat commands (#7934) 2026-06-23 14:00:56 -07:00
adminandGitHub f8066454a8 Use Option.fold in history lookups (#7936) 2026-06-23 14:00:35 -07:00
adminandGitHub a80c8e6d1b Remove return from raw history lookup (#7938) 2026-06-23 14:00:11 -07:00
adminandGitHub 3dc8541aee Remove return from delete game failure path (#7940) 2026-06-23 13:59:51 -07:00
adminandGitHub b7cd003b4c Use keyed lookups for unaffiliated hero action (#7942) 2026-06-23 13:59:30 -07:00
adminandGitHub bae90633a5 Share required faction lookup helper (#7945) 2026-06-23 13:59:08 -07:00
adminandGitHub 56eff84f30 Avoid duplicate faction lookup in HeroUtils (#7947) 2026-06-23 13:58:50 -07:00
adminandGitHub 375ae7cecb Use Vector.empty in service result paths (#7949) 2026-06-23 13:58:29 -07:00
adminandGitHub ac8a9358ff Consolidate direct vector declaration cleanups (#7965)
* Use direct vector declarations in AI tests

* Use direct vector declarations in MCTS tests

* Use direct roll vector declarations

* Use direct unit vector declarations in command tests

* Use direct vector declarations in action tests

* Use direct vector declarations in status tests

* Use direct vector declarations in library tests

* Use direct vectors in game state helpers

* Use direct vectors in Shardok server setup

* Use direct fire roll vector declarations

* Use direct melee roll vector declarations

* Use direct combat factory vector declarations

* Use direct vector declarations in command tests

* Use direct vector declarations in special command tests

* Use direct vector declarations in command factory tests

* Use direct vector declarations in bridge factory tests

* Use direct vector declarations in AI score tests

* Use direct vector declarations in test helpers
2026-06-23 13:58:09 -07:00
adminandGitHub 8bc900eef9 Consolidate pre-increment cleanups (#7966)
* Use preincrement in test loops

* Use preincrement in production loops

* Use pre-increment in library tests

* Use pre-increment in hex map helper tests

* Use pre-increment in action point tests
2026-06-23 13:57:32 -07:00
adminandGitHub 845a85ed1f Serialize iPad touch target sizes (#7912) 2026-06-23 13:56:14 -07:00
adminandGitHub a1a7a8abf8 Use pre-increment in available command tests (#7961) 2026-06-23 13:50:08 -07:00
adminandGitHub 9cc4dca5b2 Use pre-increment in MCTS score tests (#7962) 2026-06-23 13:49:54 -07:00
adminandGitHub ea244506d3 Use direct vector declarations in factory tests (#7939) 2026-06-23 13:49:42 -07:00
adminandGitHub 9827414e92 Use pre-increment in map info loops (#7964) 2026-06-23 13:49:07 -07:00
adminandGitHub 549cf37127 Require eagle map on GameState (#7924) 2026-06-23 12:44:12 -07:00
adminandGitHub 237d5ce7c3 Tidy transposition table accessors (#7918) 2026-06-23 12:25:57 -07:00
adminandGitHub 05fc72c611 Tidy AI command evaluator result state (#7917) 2026-06-23 12:25:36 -07:00
adminandGitHub 5d889b71aa Remove GameState bootstrap date helper (#7916) 2026-06-23 12:24:57 -07:00
adminandGitHub 8641d15fc7 Remove single-future reserve hints (#7915) 2026-06-23 12:24:00 -07:00
adminandGitHub e4fc742990 Make GameState currentDate required (#7913) 2026-06-23 12:10:56 -07:00
adminandGitHub a27dfe895f Use span for odds other factors (#7914) 2026-06-23 12:07:11 -07:00
adminandGitHub 098d4b4dcc Tidy random battle runner setup (#7903) 2026-06-23 11:52:39 -07:00
adminandGitHub 765ca02255 Use clamped perf test unit counts (#7902) 2026-06-23 11:52:13 -07:00
adminandGitHub 186dec0ad9 Reserve controller update fields (#7901) 2026-06-23 11:50:01 -07:00
adminandGitHub 14a6d143bc Reserve odds helper factors (#7904) 2026-06-23 11:48:42 -07:00
adminandGitHub ade5a4fa10 Size game state guesser buffers first (#7905) 2026-06-23 11:47:38 -07:00
adminandGitHub 85d3ae8fce Tidy action point distance setup (#7906) 2026-06-23 11:47:22 -07:00
adminandGitHub fb200278ae Tidy available command assembly (#7907) 2026-06-23 11:47:02 -07:00
adminandGitHub 34da29bef0 Reserve reinforcement setup commands (#7908) 2026-06-23 11:46:47 -07:00
adminandGitHub f164fbb150 Add GameState required current date helper (#7910) 2026-06-23 11:46:03 -07:00
adminandGitHub fe1537bb12 Trim map names by extension (#7909) 2026-06-23 11:45:06 -07:00
adminandGitHub 69cc336cef Tidy map info position loop (#7911) 2026-06-23 11:44:48 -07:00
adminandGitHub d05030f99a Improve remaining iPad touch targets (#7900) 2026-06-23 11:03:39 -07:00
adminandGitHub 9699d095c5 Reserve command execution result buffers (#7883) 2026-06-23 11:03:01 -07:00
adminandGitHub 6ca2d7a37c Reserve action execution buffers (#7884) 2026-06-23 11:02:40 -07:00
adminandGitHub b0c4944a6c Remove Postgres text store early returns (#7881) 2026-06-23 11:02:13 -07:00
adminandGitHub d25925d525 Avoid implicit food lookup insertion (#7885) 2026-06-23 11:01:38 -07:00
adminandGitHub 07a6834216 Handle zero-length cube lines (#7880) 2026-06-23 11:00:47 -07:00
adminandGitHub 69f1f51460 Tighten shardok server request conversion (#7879) 2026-06-23 11:00:14 -07:00
adminandGitHub 8b30a2c5f2 Reserve tutorial reinforcement units (#7886) 2026-06-23 10:59:49 -07:00
adminandGitHub 65a534ebae Remove SQLite text store early returns (#7887) 2026-06-23 10:59:31 -07:00
adminandGitHub ac0a154fc4 Reserve game state view fields (#7888) 2026-06-23 10:59:10 -07:00
adminandGitHub 1b510007fd Use Await.result in server setup (#7889) 2026-06-23 10:58:23 -07:00
adminandGitHub 692c363264 Guard action filter actor lookup (#7890) 2026-06-23 10:58:02 -07:00
adminandGitHub cf5d88c69d Avoid copying filtered history (#7892) 2026-06-23 10:57:36 -07:00
adminandGitHub 109f49e522 Clarify custom battle player view lookup (#7891) 2026-06-23 10:57:05 -07:00
adminandGitHub 53e3f7a1b0 Use fixed adjacent tile offsets (#7893) 2026-06-23 10:56:51 -07:00
adminandGitHub c7d3e162b9 Clarify ResolveBattle required winners (#7894) 2026-06-23 10:56:23 -07:00
adminandGitHub 7b2bb34a36 Clarify mid-game AI required values (#7895) 2026-06-23 10:56:07 -07:00
adminandGitHub c7b1082954 Clarify GamesManager required loads (#7896) 2026-06-23 10:54:42 -07:00
adminandGitHub aa18d0dd1a Clarify chronicle required leaders (#7897) 2026-06-23 10:54:22 -07:00
adminandGitHub 711d0382f3 Make province distance lookup explicit (#7898) 2026-06-23 10:53:55 -07:00
adminandGitHub 7b02c39172 Make tutorial loyalty lookups explicit (#7899) 2026-06-23 10:53:04 -07:00
adminandGitHub b178f11bea Improve iPad touch targets in popup UI (#7882) 2026-06-23 10:49:29 -07:00
adminandGitHub 42da8a7d15 Clarify great person faction requirement (#7870) 2026-06-23 10:08:24 -07:00
adminandGitHub bd9b62d7de Reserve AI evaluator lookahead vectors (#7869) 2026-06-23 10:08:07 -07:00
adminandGitHub 98fcd94614 Reserve engine output vectors (#7868) 2026-06-23 10:07:49 -07:00
adminandGitHub 06e7112db1 Clarify battle participant lookups (#7847) 2026-06-23 10:07:32 -07:00
adminandGitHub 9ba023b0fd Reserve hex map proto fields (#7871) 2026-06-23 10:07:08 -07:00
adminandGitHub ff63c0fff2 Reserve game state helper collections (#7872) 2026-06-23 10:06:52 -07:00
adminandGitHub 421dbcae80 Clarify required quest request fields (#7873) 2026-06-23 10:06:34 -07:00
adminandGitHub 4c321967f4 Reserve geometry utility vectors (#7874) 2026-06-23 10:06:18 -07:00
adminandGitHub b3c0138074 Clarify required proto submessages (#7875) 2026-06-23 10:05:53 -07:00
adminandGitHub 4b7c862ac8 Clarify service invariant lookups (#7876) 2026-06-23 10:05:31 -07:00
adminandGitHub cf4b99710c Clarify Shardok interface required fields (#7877) 2026-06-23 10:05:11 -07:00
adminandGitHub d25e0121a6 Clarify required hero library conjunctions (#7878) 2026-06-23 10:04:52 -07:00
adminandGitHub 213a339d34 Delete stale editor log zip (#7845) 2026-06-23 08:38:58 -07:00
adminandGitHub d2205dc361 Reserve Shardok test helper vectors (#7846) 2026-06-23 08:38:46 -07:00
adminandGitHub 21e0586add Reserve AI test helper vectors (#7848) 2026-06-23 08:38:07 -07:00
adminandGitHub cbbe200c98 Avoid raw battle battalion options (#7862) 2026-06-23 08:37:50 -07:00
adminandGitHub fbeddc7b19 Avoid raw heads in string parser (#7849) 2026-06-23 08:29:08 -07:00
adminandGitHub 1ed6c85b5d Improve iPad touch targets in Eagle UI (#7850) 2026-06-23 08:28:25 -07:00
adminandGitHub c7f10dbcbc Clarify required engine history result (#7851) 2026-06-23 08:28:08 -07:00
adminandGitHub 81f747c75b Reserve simulator setup vectors (#7852) 2026-06-23 08:27:41 -07:00
adminandGitHub a76dc0515c Clarify required dates in round advancement (#7853) 2026-06-23 08:26:39 -07:00
adminandGitHub bb77ecbd73 Clarify province resource date requirement (#7854) 2026-06-23 08:26:21 -07:00
adminandGitHub 5d35e7d78b Extract MCTS action sorting helper (#7855) 2026-06-23 08:25:56 -07:00
adminandGitHub e20c4e7027 Avoid vector allocation in ranged bonus scoring (#7856) 2026-06-23 08:25:25 -07:00
adminandGitHub 3977df70f4 Clarify more current date requirements (#7857) 2026-06-23 08:24:52 -07:00
adminandGitHub 7f72a8bb39 Reserve AI strategy working vectors (#7858) 2026-06-23 08:24:26 -07:00
adminandGitHub de27e9fcb8 Right-size APD working vectors (#7859) 2026-06-23 08:24:05 -07:00
adminandGitHub 76bd94ddb5 Clarify config parse failures (#7860) 2026-06-23 08:23:35 -07:00
adminandGitHub 651e209f07 Move completed action results into vectors (#7861) 2026-06-23 08:23:03 -07:00
adminandGitHub 448ec1e0ee Reserve simulator CLI path vectors (#7863) 2026-06-23 08:21:32 -07:00
adminandGitHub 6e2afeee3c Clarify required battle ended result (#7864) 2026-06-23 08:21:08 -07:00
adminandGitHub edce49467c Reserve map info results (#7865) 2026-06-23 08:20:51 -07:00
adminandGitHub 2934027437 Clarify battle unit return lookups (#7866) 2026-06-23 08:20:35 -07:00
adminandGitHub b2250cda79 Reserve tutorial controller result vectors (#7867) 2026-06-23 08:20:17 -07:00
adminandGitHub 96d69b349f Reserve Shardok filter result vectors (#7839) 2026-06-23 07:49:53 -07:00
adminandGitHub 4a6ae0380a Clarify current date requirements in command helpers (#7840) 2026-06-23 07:49:38 -07:00
adminandGitHub e339ddd227 Require factions for leader-aware command choices (#7841) 2026-06-23 07:49:18 -07:00
adminandGitHub afaf6c9e19 Move Shardok vector temporaries (#7842) 2026-06-23 07:49:03 -07:00
adminandGitHub e21eaf8606 Add narrow March command flow (#7747) 2026-06-23 07:48:32 -07:00
adminandGitHub a43a650d17 Document PR review commit history preference (#7844) 2026-06-23 07:48:17 -07:00
adminandGitHub 17cb742964 Clarify province conquest required values (#7843) 2026-06-23 07:40:12 -07:00
adminandGitHub 9d8231b253 Clarify current date requirements in aftermath phases (#7836) 2026-06-23 07:00:16 -07:00
adminandGitHub 5bf2ea95e5 Clarify current date requirements in resolution events (#7837) 2026-06-23 07:00:02 -07:00
adminandGitHub 2f48330d74 Document cleanup PR batching guidance (#7838) 2026-06-23 06:59:31 -07:00
adminandGitHub b0b516a990 Clarify current date requirements in event phases (#7835) 2026-06-23 06:59:05 -07:00
adminandGitHub 1057271163 Reserve move destination vector (#7830) 2026-06-23 06:58:47 -07:00
adminandGitHub 296d5df16f Reserve fall into water helper vectors (#7829) 2026-06-23 06:58:33 -07:00
adminandGitHub b2abe00b14 Reserve possible chargee ids (#7828) 2026-06-23 06:58:20 -07:00
adminandGitHub 793d665d80 Reserve allied player id vector (#7826) 2026-06-23 06:58:07 -07:00
adminandGitHub bbbe596e72 Reserve game state guesser vectors (#7825) 2026-06-23 06:57:54 -07:00
adminandGitHub c3c148f5db Reserve Shardok game setup vectors (#7823) 2026-06-23 06:57:33 -07:00
adminandGitHub 4b8f1d2d20 Reserve hex cube result vectors (#7821) 2026-06-23 06:57:18 -07:00
adminandGitHub 8a3222ed54 Reserve map name vector capacity (#7819) 2026-06-23 06:56:56 -07:00
adminandGitHub 9f82af2e12 Make remaining Shardok test casts explicit (#7818) 2026-06-23 06:56:35 -07:00
adminandGitHub 2536e5146c Fix empty AI score reserve underflow (#7832) 2026-06-23 06:54:09 -07:00
adminandGitHub 98b0502e20 Use reinterpret cast for resolved unit bytes (#7811) 2026-06-23 06:08:33 -07:00
adminandGitHub 25c1198c0a Clarify captured hero command acting hero (#7790) 2026-06-23 06:05:58 -07:00
adminandGitHub a6e31596d6 Qualify flatbuffer wrapper memcpy calls (#7791) 2026-06-23 06:05:42 -07:00
adminandGitHub c9b3cd6fc0 Clarify beast info requirement (#7792) 2026-06-23 06:05:26 -07:00
adminandGitHub 2c18d1d39b Move AI command evaluator sample sequence (#7794) 2026-06-23 06:05:09 -07:00
adminandGitHub 28895e3e04 Clarify captured hero command choice requirement (#7793) 2026-06-23 06:04:54 -07:00
adminandGitHub f8ef044226 Include cstring in unit helpers (#7789) 2026-06-23 06:04:36 -07:00
adminandGitHub b8da5750c7 Clarify please recruit command choice requirements (#7795) 2026-06-23 06:02:59 -07:00
adminandGitHub fbdc0f79a8 Move AI battle simulator setup objects (#7796) 2026-06-23 06:02:35 -07:00
adminandGitHub ea7f666e36 Move game state helper unit structs (#7797) 2026-06-23 06:02:18 -07:00
adminandGitHub 48ff8a7e1b Bind march origin command choice (#7798) 2026-06-23 06:02:00 -07:00
adminandGitHub 033a378e14 Use static cast for game status conversion (#7799) 2026-06-23 06:01:41 -07:00
adminandGitHub a3d19f246e Move random battle runner units (#7800) 2026-06-23 06:01:25 -07:00
adminandGitHub ec67215bc2 Clean up percentile roll casts (#7801) 2026-06-23 06:00:59 -07:00
adminandGitHub f207bef4a8 Clarify armament cost lookup (#7802) 2026-06-23 06:00:34 -07:00
adminandGitHub 41bb944c26 Use static casts for action result sizes (#7803) 2026-06-23 06:00:19 -07:00
adminandGitHub 38eb33367d Bind attack command current date (#7804) 2026-06-23 05:59:45 -07:00
adminandGitHub 5ae59006fb Bind alms command current date (#7805) 2026-06-23 05:59:26 -07:00
adminandGitHub e66e7d4a03 Clarify update game status player id check (#7806) 2026-06-23 05:59:05 -07:00
adminandGitHub 34bbfaf256 Clarify backstory update counter parsing (#7807) 2026-06-23 05:58:30 -07:00
adminandGitHub abd323a232 Use static casts in hex map utilities (#7808) 2026-06-23 05:58:00 -07:00
adminandGitHub 97ba1d0be1 Use static casts in hex cube fixup (#7809) 2026-06-23 05:57:43 -07:00
adminandGitHub deb211a420 Clarify new round current date requirement (#7810) 2026-06-23 05:57:19 -07:00
adminandGitHub 9481eed7b8 Use static cast for new wind direction (#7812) 2026-06-23 05:51:51 -07:00
adminandGitHub 50c457d877 Use static cast for battalion type size check (#7813) 2026-06-23 05:51:34 -07:00
adminandGitHub a544a0d15e Use static casts in hex map direction arithmetic (#7814) 2026-06-22 23:52:15 -07:00
adminandGitHub 8c26ac5fca Clarify expand selector required hero checks (#7777) 2026-06-22 21:19:09 -07:00
adminandGitHub d56e0de9fd Clarify new hero province selection (#7778) 2026-06-22 21:18:40 -07:00
adminandGitHub 5132152334 Catch Shardok server exceptions by const reference (#7779) 2026-06-22 21:18:11 -07:00
adminandGitHub ef6ba7e585 Throw battle simulator CLI helper errors (#7780) 2026-06-22 21:17:51 -07:00
adminandGitHub 695a9d19d4 Return from AI performance argument parsing (#7781) 2026-06-22 21:17:30 -07:00
adminandGitHub 45682dff9b Parse real battle extractor integers with stoi (#7782) 2026-06-22 21:17:08 -07:00
adminandGitHub 47c5653bc0 Parse AI experiment id with from_chars (#7783) 2026-06-22 21:16:43 -07:00
adminandGitHub 033ebc802a Parse Shardok server config without strtok (#7784) 2026-06-22 21:16:12 -07:00
adminandGitHub 14e0fb66fb Drop unused random battle srand calls (#7785) 2026-06-22 21:15:28 -07:00
adminandGitHub 18d9756fc5 Make AI evaluation counter noncopyable (#7786) 2026-06-22 21:15:15 -07:00
adminandGitHub f976548fa1 Make attacker score timer noncopyable (#7787) 2026-06-22 21:14:58 -07:00
adminandGitHub a3fed6836d Include cstring in flatbuffer wrapper (#7788) 2026-06-22 21:14:38 -07:00
adminandGitHub 78d89f83da Scope remaining Shardok cpp tests to namespace (#7776) 2026-06-22 17:04:14 -07:00
adminandGitHub 66b93b0c83 Clarify tribute demand fallback (#7775) 2026-06-22 17:03:21 -07:00
adminandGitHub fe06ead2e2 Scope flee command tests to shardok namespace (#7767) 2026-06-22 17:03:09 -07:00
adminandGitHub 71e5b1b866 Clarify faction change victor lookups (#7774) 2026-06-22 17:02:38 -07:00
adminandGitHub 35ac2d6297 Scope ice action tests to shardok namespace (#7764) 2026-06-22 17:02:00 -07:00
adminandGitHub ff33d13822 Scope snow action tests to shardok namespace (#7762) 2026-06-22 17:01:47 -07:00
adminandGitHub ffb02d1d6c Scope build bridge command tests to shardok namespace (#7771) 2026-06-22 17:01:34 -07:00
adminandGitHub 86f7e85ce9 Clarify prisoner move required hero lookup (#7770) 2026-06-22 17:01:20 -07:00
adminandGitHub cd6318ba84 Clarify faction change required lookups (#7772) 2026-06-22 17:01:07 -07:00
adminandGitHub 7385b8ced2 Scope evacuate prisoners command tests to shardok namespace (#7773) 2026-06-22 17:00:54 -07:00
adminandGitHub a36ccd4157 Clarify captured hero recruitment requirements (#7765) 2026-06-22 17:00:40 -07:00
adminandGitHub 29a3459649 Scope blow bridge command tests to shardok namespace (#7766) 2026-06-22 17:00:27 -07:00
adminandGitHub 8dd7df989d Bind selected captured hero options once (#7763) 2026-06-22 17:00:09 -07:00
adminandGitHub d40b9a9ba6 Clarify captured hero return destination lookup (#7768) 2026-06-22 16:59:49 -07:00
adminandGitHub dd5d34f7aa Scope holy wave command tests to shardok namespace (#7769) 2026-06-22 16:58:50 -07:00
adminandGitHub 5685c8430f Clarify required CommandFactory province fields (#7761) 2026-06-22 16:58:37 -07:00
adminandGitHub 723a195d89 Scope ambush odds tests to shardok namespace (#7760) 2026-06-22 16:58:22 -07:00
adminandGitHub 68a20cba27 Scope percentile roll tests to shardok namespace (#7759) 2026-06-22 16:53:03 -07:00
adminandGitHub 27802e54e6 Move game settings test include inside guard (#7758) 2026-06-22 16:49:18 -07:00
adminandGitHub 3e91ca3bc3 Qualify standard library types in ShardokGameController (#7756) 2026-06-22 16:49:07 -07:00
adminandGitHub 5a4e98bd71 Remove unsafe runtime validator lookups (#7752) 2026-06-22 16:44:57 -07:00
adminandGitHub 225fe66afa Qualify standard library types in ShardokEngine (#7753) 2026-06-22 16:44:27 -07:00
adminandGitHub 619f0ca3e9 Report alliance destination lookup failures explicitly (#7754) 2026-06-22 16:44:09 -07:00
adminandGitHub fa3f4cd06b Report chronicle attack faction name lookup failures explicitly (#7755) 2026-06-22 16:43:54 -07:00
adminandGitHub 7694d2e676 Handle empty conjunction results without unsafe get (#7757) 2026-06-22 16:43:24 -07:00
adminandGitHub ea6792f21b Report quest creation lookup failures explicitly (#7751) 2026-06-22 16:42:10 -07:00
adminandGitHub 5e4dace755 Report battalion suitability type lookup failures explicitly (#7742) 2026-06-22 16:33:14 -07:00
adminandGitHub 276dbf5d42 Qualify vectors in hex map helpers (#7743)
* Qualify vectors in hex map helpers

* Qualify ZOC vector parameters
2026-06-22 16:32:57 -07:00
adminandGitHub 0ca2685bf3 Use province ruling faction helper for command availability (#7736) 2026-06-22 16:32:33 -07:00
adminandGitHub 4bab97ec62 Report arm troops availability cost lookup failures explicitly (#7741) 2026-06-22 16:32:11 -07:00
adminandGitHub 47bd5f6291 Qualify std iterators in Eagle gRPC server (#7740) 2026-06-22 16:31:57 -07:00
adminandGitHub 7ee1441b8e Report march neighbor lookup failures explicitly (#7739) 2026-06-22 16:31:43 -07:00
adminandGitHub 034270a119 Use province ruling faction helper for diplomacy availability (#7738) 2026-06-22 16:31:29 -07:00
adminandGitHub 9e4bff03d9 Qualify string constant in FearCommand (#7737) 2026-06-22 16:31:11 -07:00
adminandGitHub 29b9954954 Qualify std algorithms in AI unit scoring (#7735) 2026-06-22 16:30:48 -07:00
adminandGitHub 3aa62b9eeb Report quest fulfillment lookup failures explicitly (#7734) 2026-06-22 16:30:28 -07:00
adminandGitHub d8bd976204 Report unaffiliated hero province lookup failures explicitly (#7744) 2026-06-22 16:30:07 -07:00
adminandGitHub 3ccb8f5dc8 Qualify shared pointers in Eagle gRPC server (#7745) 2026-06-22 16:29:54 -07:00
adminandGitHub cb05990260 Report hero utils faction lookup failures explicitly (#7746) 2026-06-22 16:29:20 -07:00
adminandGitHub 8a2c21c88c Report combat unit suitability lookup failures explicitly (#7748) 2026-06-22 16:28:47 -07:00
adminandGitHub 204cd81462 Remove stale std aliases from Unit header (#7749) 2026-06-22 16:28:30 -07:00
adminandGitHub 71505b5419 Report manage prisoners return province lookup failures explicitly (#7750) 2026-06-22 16:27:59 -07:00
adminandGitHub 50e958e2e3 Use province ruling faction helper in actions (#7724) 2026-06-22 14:58:25 -07:00
adminandGitHub 3ffab58cbf Report LLM lookup failures explicitly (#7723) 2026-06-22 14:58:12 -07:00
adminandGitHub febb9e2fe0 Report organize troops lookup failures explicitly (#7722) 2026-06-22 14:57:52 -07:00
adminandGitHub b1af1b7254 Report quest ruling faction lookup failures explicitly (#7725) 2026-06-22 14:57:07 -07:00
adminandGitHub 32a4da1826 Report alliance resolution lookup failures explicitly (#7726) 2026-06-22 14:56:31 -07:00
adminandGitHub cc44eef174 Report invitation resolution lookup failures explicitly (#7727) 2026-06-22 14:55:41 -07:00
adminandGitHub c608a5890a Report diplomacy origin province lookup failures explicitly (#7728) 2026-06-22 14:53:43 -07:00
adminandGitHub 52253908eb Report AI diplomacy faction lookup failures explicitly (#7729) 2026-06-22 14:53:27 -07:00
adminandGitHub a50956cdb0 Report AI ransom hero lookup failures explicitly (#7730) 2026-06-22 14:53:14 -07:00
adminandGitHub ee55e4b216 Move deploy transport off Bazel runners (#7732) 2026-06-22 14:52:27 -07:00
adminandGitHub 9d041926d7 Preserve iOS scene manifest for TestFlight builds (#7733) 2026-06-22 14:51:36 -07:00
adminandGitHub 72ea2de25c Version Addressables by Unity editor (#7731) 2026-06-22 08:33:15 -07:00
adminandGitHub 4b559c2278 Report validated command lookup failures explicitly (#7720) 2026-06-22 07:38:14 -07:00
adminandGitHub 327ecf1aed Qualify std algorithms in hex map utils (#7721) 2026-06-22 07:37:26 -07:00
adminandGitHub 8fc893ce08 Qualify iterators in game state guesser (#7719) 2026-06-22 07:36:38 -07:00
adminandGitHub 0ec0e10c0f Remove stale iterator aliases from APD (#7718) 2026-06-22 07:34:02 -07:00
adminandGitHub 38c7a01743 Qualify std types in fixed APD test (#7717) 2026-06-22 07:33:50 -07:00
adminandGitHub a481b5ce60 Report recruit heroes ruling hero explicitly (#7716) 2026-06-22 07:32:35 -07:00
adminandGitHub 7786db1040 Qualify std types in Shardok games manager (#7715) 2026-06-22 07:30:48 -07:00
adminandGitHub 200aa0d319 Report exile vassal lookup failures explicitly (#7714) 2026-06-22 07:30:27 -07:00
adminandGitHub c8bb543907 Remove stale std aliases from AI headers (#7713) 2026-06-22 07:30:08 -07:00
adminandGitHub de7ecf45d1 Avoid repeated eligible gift get (#7712) 2026-06-22 07:27:18 -07:00
adminandGitHub f25298ed9e Report command factory lookup failures explicitly (#7711) 2026-06-22 07:27:01 -07:00
adminandGitHub d4ea2fbd93 Report arm troops lookup failures explicitly (#7710) 2026-06-22 07:24:24 -07:00
adminandGitHub 79c51fb21a Qualify STL containers in game state helpers (#7709) 2026-06-22 07:23:48 -07:00
adminandGitHub c57c9b5f18 Qualify std helpers in Shardok production code (#7708) 2026-06-22 07:20:09 -07:00
adminandGitHub 823cd73d33 Report missing hero descriptions explicitly (#7707) 2026-06-22 07:19:50 -07:00
adminandGitHub 54db4b6e34 Report missing suppress beasts effect text (#7706) 2026-06-22 07:18:56 -07:00
adminandGitHub 112399d67c Qualify smart pointer helpers in Shardok tests (#7705) 2026-06-22 07:18:42 -07:00
adminandGitHub 01b80d8798 Report missing province ruling faction explicitly (#7704) 2026-06-22 07:18:14 -07:00
adminandGitHub f6a3f36a13 Remove unused vector aliases in Shardok tests (#7703) 2026-06-22 07:18:01 -07:00
adminandGitHub d674e23987 Report missing common unit faction explicitly (#7702) 2026-06-22 07:17:46 -07:00
adminandGitHub 51aebf5e40 Qualify std vector in Shardok tests (#7701) 2026-06-22 07:15:59 -07:00
adminandGitHub 2ec6d2f1d7 Report missing seek-more-leaders faction explicitly (#7700) 2026-06-22 07:15:23 -07:00
adminandGitHub 9f94917a2d Report required lookup failures explicitly (#7687) 2026-06-22 07:08:10 -07:00
adminandGitHub 06f2104750 Report new game lookup failures explicitly (#7688) 2026-06-22 07:07:53 -07:00
adminandGitHub b1e1e36236 Report empty in-memory history explicitly (#7690) 2026-06-22 07:06:02 -07:00
adminandGitHub 9eb2056ac6 Report map load failures with context (#7691) 2026-06-22 07:05:40 -07:00
adminandGitHub 0c3cb22e83 Report unknown hex terrain types explicitly (#7692) 2026-06-22 07:05:24 -07:00
adminandGitHub 92f27ba273 Report missing faction utility lookups explicitly (#7693) 2026-06-22 07:05:04 -07:00
adminandGitHub 47d682e7c1 Report unaffiliated phase lookup failures explicitly (#7694) 2026-06-22 07:04:23 -07:00
adminandGitHub 75d84b67ff Report missing action ruling factions explicitly (#7695) 2026-06-22 07:03:52 -07:00
adminandGitHub be8f5ca5da Report missing beast fallback explicitly (#7696) 2026-06-22 07:03:24 -07:00
adminandGitHub c7910a6183 Report missing withdrawing flee province explicitly (#7697) 2026-06-22 07:03:08 -07:00
renovate[bot]andGitHub 98afc28762 Update alpine_linux:3.24 Docker digest to 28bd5fe (#7698) 2026-06-22 06:17:57 -07:00
adminandGitHub 4cd1349a5a Fix AI victory score test std types (#7699) 2026-06-22 05:44:05 -07:00
adminandGitHub cebac64264 Avoid unsafe diplomacy hero fallback head (#7668) 2026-06-21 21:48:23 -07:00
adminandGitHub 172125e03b Avoid unsafe AI game arg head (#7667) 2026-06-21 21:48:09 -07:00
adminandGitHub 15006091b4 Report missing AI commands explicitly (#7670) 2026-06-21 21:46:19 -07:00
adminandGitHub 2175f1d957 Report missing diplomacy offers explicitly (#7671) 2026-06-21 21:45:56 -07:00
adminandGitHub d445cbfe25 Avoid unsafe ruling hero head (#7673) 2026-06-21 21:44:07 -07:00
adminandGitHub 5fc56c342d Avoid unsafe shuffle tail heads (#7674) 2026-06-21 21:43:53 -07:00
adminandGitHub e5350d03a8 Report missing recon targets explicitly (#7676) 2026-06-21 21:43:40 -07:00
adminandGitHub 097daf3dc1 Report missing province ruling heroes explicitly (#7678) 2026-06-21 21:43:25 -07:00
adminandGitHub 3c3dc85187 Avoid standard aliases in command factory headers (#7675)
* Avoid standard aliases in command factory headers

* Fix available commands std type qualification
2026-06-21 21:42:47 -07:00
adminandGitHub a775a667f6 Remove unused vector alias from unit filter (#7677) 2026-06-21 21:41:27 -07:00
adminandGitHub a1a7a69aeb Remove unused vector alias from game settings (#7679) 2026-06-21 21:41:08 -07:00
adminandGitHub ff549ab337 Qualify shared pointers in cache and settings headers (#7686)
* Qualify shared pointers in cache and settings headers

* Fix map info test shared pointer spelling
2026-06-21 21:40:51 -07:00
adminandGitHub 464651503e Avoid vector aliases in utility headers (#7672) 2026-06-21 21:36:38 -07:00
adminandGitHub 2f93250c8e Avoid vector aliases in AI headers (#7669) 2026-06-21 21:36:14 -07:00
adminandGitHub 22ce7ba750 Report missing tutorial ruling heroes explicitly (#7680) 2026-06-21 21:35:54 -07:00
adminandGitHub acbb09f6a1 Qualify vector usage in action point distances (#7681) 2026-06-21 21:35:40 -07:00
adminandGitHub 900dd23ae7 Use explicit backstory text id accessors (#7683) 2026-06-21 21:35:14 -07:00
adminandGitHub 0f391776aa Qualify vector usage in duel utilities (#7682) 2026-06-21 21:34:42 -07:00
adminandGitHub 817fb613ab Clean up library implementation std aliases (#7684) 2026-06-21 21:34:16 -07:00
adminandGitHub 2f57ea0160 Qualify std types in available commands header (#7685) 2026-06-21 21:33:59 -07:00
adminandGitHub f5d5aedf9d Avoid std aliases in map loader header (#7647) 2026-06-21 19:56:47 -07:00
adminandGitHub c5114e9bf1 Avoid unsafe OpenAI choice head (#7648) 2026-06-21 19:56:35 -07:00
adminandGitHub 7514aed437 Remove unused vector alias from unit filter header (#7649) 2026-06-21 19:56:19 -07:00
adminandGitHub f4d381c9d4 Report missing headshot output path (#7650) 2026-06-21 19:56:09 -07:00
adminandGitHub 8698036f7c Avoid unsafe migration version lookup (#7651) 2026-06-21 19:55:51 -07:00
adminandGitHub 21f04cfd46 Avoid string alias in shardok exception header (#7652) 2026-06-21 19:55:30 -07:00
adminandGitHub 074cf62cc9 Avoid unsafe recruit province head (#7653) 2026-06-21 19:52:34 -07:00
adminandGitHub a66f833c90 Avoid unsafe aftermath auto-resolve head (#7654) 2026-06-21 19:51:53 -07:00
adminandGitHub d704d23d39 Avoid unsafe aftermath hostile error head (#7655) 2026-06-21 19:48:34 -07:00
adminandGitHub e2570aec4a Use explicit vectors in implementation files (#7656) 2026-06-21 19:48:06 -07:00
adminandGitHub 6b97fde9ab Report missing tutorial battle explicitly (#7657) 2026-06-21 19:47:50 -07:00
adminandGitHub 30adf1bcbf Avoid vector aliases in APD headers (#7658) 2026-06-21 19:46:25 -07:00
adminandGitHub f205b49a54 Avoid string alias in action result helper header (#7659) 2026-06-21 19:46:06 -07:00
adminandGitHub 5c7e18696e Avoid unsafe quest chooser hero heads (#7660) 2026-06-21 19:45:50 -07:00
adminandGitHub c78b00b0fc Avoid vector alias in coords set header (#7661) 2026-06-21 19:45:19 -07:00
adminandGitHub b05d6e1e1b Avoid unsafe recon candidate head (#7662) 2026-06-21 19:45:05 -07:00
adminandGitHub da0bce655c Avoid standard aliases in shardok action headers (#7663) 2026-06-21 19:44:41 -07:00
adminandGitHub d4dcbcc9d5 Report missing uncontested conqueror explicitly (#7664) 2026-06-21 19:44:26 -07:00
adminandGitHub f132e5b87b Remove unused string alias from hero header (#7665) 2026-06-21 19:42:05 -07:00
adminandGitHub 872475e82b Avoid map alias in unit header (#7666) 2026-06-21 19:41:48 -07:00
adminandGitHub 66d062bfda Avoid unsafe pregenerated hero head (#7631) 2026-06-21 15:55:51 -07:00
adminandGitHub a514ceb62f Cache repeated loop bounds (#7632) 2026-06-21 15:55:38 -07:00
adminandGitHub b0d8a5bc41 Avoid unsafe province distance frontier lookup (#7633) 2026-06-21 15:55:19 -07:00
adminandGitHub 8f4bb5130e Avoid namespace import in map info implementation (#7634) 2026-06-21 15:55:03 -07:00
adminandGitHub 9386589558 Avoid unsafe attack decision unit head (#7638) 2026-06-21 15:54:47 -07:00
adminandGitHub 5945289a81 Use explicit std types in map loader (#7639) 2026-06-21 15:54:31 -07:00
adminandGitHub 53346cc85b Use explicit std types in game over response populator (#7640) 2026-06-21 15:26:45 -07:00
adminandGitHub a6fad3ac4e Use explicit string type in shardok controller (#7641) 2026-06-21 15:26:32 -07:00
adminandGitHub cb7e2b5d5c Use explicit std types in shardok server main (#7642) 2026-06-21 15:26:15 -07:00
adminandGitHub 0f764416a3 Avoid namespace import in AI performance runner (#7643) 2026-06-21 15:25:56 -07:00
adminandGitHub c9e0012bf7 Avoid namespace import in random battle runner (#7644) 2026-06-21 15:25:37 -07:00
adminandGitHub 457f2a677a Avoid vector alias in game update receiver header (#7645) 2026-06-21 15:25:10 -07:00
adminandGitHub ced56ee41f Avoid string alias in server configuration header (#7646) 2026-06-21 15:24:56 -07:00
adminandGitHub 4e910f1f22 Report missing migration save files explicitly (#7637) 2026-06-21 15:16:53 -07:00
adminandGitHub 34bceebace Remove unused registrar header string import (#7636) 2026-06-21 15:16:34 -07:00
adminandGitHub b4f802ba77 Avoid unsafe LLM update group bounds (#7635) 2026-06-21 15:16:15 -07:00
adminandGitHub 3d9d464870 Avoid unsafe battle aftermath claimant head (#7630) 2026-06-21 15:04:00 -07:00
adminandGitHub a7e21427f9 Avoid namespace imports in map info header (#7627) 2026-06-21 15:03:43 -07:00
adminandGitHub bdbf10294b Avoid namespace import in map info runner (#7629) 2026-06-21 15:02:57 -07:00
adminandGitHub 8588430cf0 Avoid unsafe TSV header lookup (#7628) 2026-06-21 15:01:15 -07:00
adminandGitHub d920bb5c17 Report utility TSV load failures explicitly (#7626) 2026-06-21 14:59:01 -07:00
adminandGitHub 64fe9bf59d Cache map starting position count in tests (#7625) 2026-06-21 14:58:23 -07:00
adminandGitHub 101b0bf41f Cache hex map terrain count (#7624) 2026-06-21 14:54:31 -07:00
adminandGitHub 019865b5ee Reserve converted victory conditions (#7623) 2026-06-21 14:54:16 -07:00
adminandGitHub 799d34a1f1 Avoid unsafe Gemini response heads (#7622) 2026-06-21 14:54:03 -07:00
adminandGitHub 951d533ed3 Avoid unsafe rate limit header heads (#7619) 2026-06-21 14:53:49 -07:00
adminandGitHub 459eb0ef7b Cache Eagle interface player counts (#7621) 2026-06-21 14:53:15 -07:00
adminandGitHub f710105bd9 Cache action result repeated field counts (#7620) 2026-06-21 14:53:01 -07:00
adminandGitHub 4998e3ce40 Cache changed unit counts in gtest helpers (#7618) 2026-06-21 14:48:32 -07:00
adminandGitHub d739b374a4 Cache AI battle protobuf unit counts (#7617) 2026-06-21 14:48:15 -07:00
adminandGitHub bd61ebcbb6 Use flatbuffer-sized production knowledge indices (#7616) 2026-06-21 14:47:53 -07:00
adminandGitHub 3963275092 Report hero backstory load failures explicitly (#7569) 2026-06-21 14:40:52 -07:00
adminandGitHub a796e0fe67 Avoid unsafe battle request error message lookup (#7583) 2026-06-21 14:40:35 -07:00
adminandGitHub 52ac61fa1d Qualify flatbuffer wrapper abort calls (#7606) 2026-06-21 14:40:13 -07:00
adminandGitHub 29ca7665b1 Use size_t for AI integration unit loops (#7607) 2026-06-21 14:39:25 -07:00
adminandGitHub d9bdb814f4 Use size_t for MCTS reserve unit loops (#7608) 2026-06-21 14:39:09 -07:00
adminandGitHub 3dfb094a08 Use size_t for optimized score test unit loops (#7609) 2026-06-21 14:38:51 -07:00
adminandGitHub 7ce1818a7f Use flatbuffer-sized unit knowledge indices (#7610) 2026-06-21 14:37:45 -07:00
adminandGitHub 6e6eff7991 Use flatbuffer-sized retreat factory knowledge index (#7615) 2026-06-21 14:37:04 -07:00
adminandGitHub aa9c98f081 Use flatbuffer-sized game start knowledge indices (#7611) 2026-06-21 14:36:00 -07:00
adminandGitHub cdf0ad45f0 Use flatbuffer-sized move test knowledge index (#7612) 2026-06-21 14:35:14 -07:00
adminandGitHub 1b97b6673f Use flatbuffer-sized melee test knowledge index (#7613) 2026-06-21 14:34:10 -07:00
adminandGitHub 721d432bc6 Use flatbuffer-sized flee factory knowledge index (#7614) 2026-06-21 14:33:01 -07:00
adminandGitHub e513a784eb Report missing start game heroes explicitly (#7571) 2026-06-21 14:30:42 -07:00
adminandGitHub ff8da78992 Report missing tutorial battle players explicitly (#7577) 2026-06-21 14:30:11 -07:00
adminandGitHub 84a69ce982 Match prisoner leader faction options (#7572) 2026-06-21 14:28:27 -07:00
adminandGitHub 7d743f8678 Report missing proto converter fields explicitly (#7575) 2026-06-21 14:28:13 -07:00
adminandGitHub 3f24e15392 Bind custom battle human updates explicitly (#7578) 2026-06-21 14:27:49 -07:00
adminandGitHub 0f86bb6874 Match engine command lookup explicitly (#7580) 2026-06-21 14:27:29 -07:00
adminandGitHub 05ee7a2ab2 Report unruled defend flee provinces explicitly (#7582) 2026-06-21 14:25:46 -07:00
adminandGitHub b9321433d0 Report fixed hero load failures explicitly (#7567) 2026-06-21 14:24:29 -07:00
adminandGitHub 62791401e9 Use streams for raise dead setup output (#7581) 2026-06-21 14:18:59 -07:00
adminandGitHub 4d28bba9cc Use streams for MCTS decision investigation output (#7579) 2026-06-21 14:18:42 -07:00
adminandGitHub 1f3660e1f3 Use streams for reserve combat phase output (#7576) 2026-06-21 14:18:24 -07:00
adminandGitHub 95ff44e205 Use streams for reserve setup diagnostics (#7574) 2026-06-21 14:18:08 -07:00
adminandGitHub 987a164092 Report missing hostile army neighbor explicitly (#7584) 2026-06-21 14:17:51 -07:00
adminandGitHub 1b5b11fd5b Match pending aftermath info explicitly (#7586) 2026-06-21 14:17:35 -07:00
adminandGitHub ddcde52e27 Bind validated command options explicitly (#7587) 2026-06-21 14:12:54 -07:00
adminandGitHub ea2da84535 Report missing declined quest hero explicitly (#7589) 2026-06-21 14:12:04 -07:00
adminandGitHub abed8ee574 Bind control weather option explicitly (#7590) 2026-06-21 14:09:50 -07:00
adminandGitHub 556c59a9c7 Report S3 credential load failures explicitly (#7566) 2026-06-21 14:09:39 -07:00
adminandGitHub ad2d085b24 Stabilize sqlite history upload snapshot test (#7605) 2026-06-21 14:08:50 -07:00
adminandGitHub d7209102b8 Report missing battalion types explicitly (#7592) 2026-06-21 13:25:39 -07:00
adminandGitHub 14e1ba6795 Avoid unwrapping province distances (#7594) 2026-06-21 13:25:25 -07:00
adminandGitHub 65b56081ce Report beast info load failures explicitly (#7595) 2026-06-21 13:25:00 -07:00
adminandGitHub eafb51cbfa Report name generator load failures explicitly (#7564) 2026-06-21 13:24:37 -07:00
adminandGitHub 3444ae296e Report missing ransom offer prisoner explicitly (#7597) 2026-06-21 13:24:23 -07:00
adminandGitHub 4f2fff5da6 Use streams for MCTS start fire test output (#7565) 2026-06-21 13:23:50 -07:00
adminandGitHub 144483a45d Use streams for start fire cost explanation (#7573) 2026-06-21 13:21:53 -07:00
adminandGitHub 53345b2bb0 Use streams for MCTS rain fire output (#7568) 2026-06-21 13:17:20 -07:00
adminandGitHub 5d6bc429b0 Use streams for start fire cost summary (#7570) 2026-06-21 13:17:00 -07:00
adminandGitHub a4faf61613 Report missing divine acting province explicitly (#7599) 2026-06-21 13:16:41 -07:00
adminandGitHub fdd516598d Report missing apprehended outlaw explicitly (#7601) 2026-06-21 13:16:29 -07:00
adminandGitHub 70da6990c4 Match optional date protos explicitly (#7602) 2026-06-21 13:13:12 -07:00
adminandGitHub e8b18d0805 Use streams for raise dead filter output (#7585) 2026-06-21 13:12:03 -07:00
adminandGitHub 1ca9e8678c Use streams for MCTS investigation intro output (#7588) 2026-06-21 13:11:29 -07:00
adminandGitHub 94670149fe Use streams for MCTS end-turn score output (#7591) 2026-06-21 13:11:14 -07:00
adminandGitHub 8e253c2972 Use streams for MCTS start-fire failure output (#7596) 2026-06-21 13:11:00 -07:00
adminandGitHub 6e15c6d9cb Use streams for MCTS start-fire success output (#7593) 2026-06-21 13:10:21 -07:00
adminandGitHub ca00e90ad6 Use streams for AI command mismatch diagnostics (#7600) 2026-06-21 13:10:07 -07:00
adminandGitHub b1d76ea661 Use streams for MCTS path comparison output (#7598) 2026-06-21 13:09:52 -07:00
adminandGitHub 059d428cfa Make Shardok player id conversions explicit (#7603) 2026-06-21 13:08:19 -07:00
adminandGitHub a4b50eb357 Tighten map info index handling (#7604) 2026-06-21 13:07:43 -07:00
adminandGitHub 0dfd5aa11d Use streams for critical fire score output (#7563) 2026-06-21 10:32:50 -07:00
adminandGitHub 2b63ccd145 Report name format parse failures explicitly (#7562) 2026-06-21 10:32:38 -07:00
adminandGitHub 97cec5ec38 Add Shardok Linux ARM64 PR build (#7561) 2026-06-21 10:29:17 -07:00
adminandGitHub 31fb5a3616 Avoid partial battalion access in riot command errors (#7560) 2026-06-21 10:27:28 -07:00
adminandGitHub 7b7130d67d Use resource handling for Shardok map info (#7552) 2026-06-21 10:25:49 -07:00
adminandGitHub 6707ad55ff Report missing beasts event explicitly (#7559) 2026-06-21 10:25:37 -07:00
adminandGitHub 89aa73b63a Use resource handling for image paths (#7554) 2026-06-21 10:25:21 -07:00
adminandGitHub bb7453a002 Avoid partial Option access in HeroNameCache (#7555) 2026-06-21 10:25:09 -07:00
adminandGitHub 8cea29dcb0 Avoid partial command access in early AI (#7556) 2026-06-21 10:24:55 -07:00
adminandGitHub d5d74ec8d6 Avoid partial command access in mid-game AI (#7557) 2026-06-21 10:24:32 -07:00
adminandGitHub 9b000b94ac Report missing Shardok map info explicitly (#7558) 2026-06-21 10:24:13 -07:00
adminandGitHub 82ed9fcc83 Include array in CombatDamage (#7553) 2026-06-21 10:23:51 -07:00
adminandGitHub cd53283935 Use resource handling for LLM usage loads (#7551) 2026-06-21 10:17:08 -07:00
adminandGitHub 3faddf76f8 Use resource handling in JsonUtils (#7550) 2026-06-21 10:15:43 -07:00
adminandGitHub 6e24ae2f4f Use streams for MCTS archery debug output (#7538) 2026-06-21 10:10:27 -07:00
adminandGitHub 66912580c8 Use streams in C++ debug headers (#7540) 2026-06-21 10:10:13 -07:00
adminandGitHub 219dd830e1 Use streams for MCTS score sequence output (#7541) 2026-06-21 10:09:51 -07:00
adminandGitHub 3d5cdcc78e Use streams for combat damage strings (#7542) 2026-06-21 10:09:32 -07:00
adminandGitHub ab1d43fae3 Use streams for MCTS minimax debug output (#7543) 2026-06-21 10:09:19 -07:00
adminandGitHub 2d51f3c23d Use resource handling for name format reads (#7548) 2026-06-21 10:09:03 -07:00
adminandGitHub f9447cf7aa Use resource handling for map info migration (#7549) 2026-06-21 10:08:42 -07:00
adminandGitHub c6ed0bfc7d Use streams for AI choice diagnostics (#7534) 2026-06-21 10:06:57 -07:00
adminandGitHub 2d4621b1a6 Use pattern matching for parser literals (#7547) 2026-06-21 10:05:48 -07:00
adminandGitHub 3bf929b4a5 Use streams for MCTS fire score analysis (#7544) 2026-06-21 10:02:45 -07:00
adminandGitHub ebed01ba86 Use streams for abstract MCTS diagnostics (#7545) 2026-06-21 10:02:29 -07:00
adminandGitHub 880d005347 Use streams for Alah fire score analysis (#7546) 2026-06-21 10:02:12 -07:00
adminandGitHub e8ef216de8 Use streams for common C++ diagnostics (#7539) 2026-06-21 10:01:47 -07:00
adminandGitHub baa453594a Delete old dropped games automatically (#7535) 2026-06-21 09:46:01 -07:00
adminandGitHub 9c767d5c49 Use streams for MCTS score placement diagnostics (#7537) 2026-06-21 09:45:15 -07:00
adminandGitHub e0892bd2fa Use streams for MCTS setup diagnostics (#7536) 2026-06-21 09:44:55 -07:00
adminandGitHub 315be7c465 Use streams for ID AI performance logs (#7533) 2026-06-21 09:42:33 -07:00
adminandGitHub 9d34dc0954 Use streams for AI debug dump messages (#7532) 2026-06-21 09:42:14 -07:00
adminandGitHub bdedd43e3c Use streams for score timing diagnostic (#7531) 2026-06-21 09:42:02 -07:00
adminandGitHub 97e779675e Use streams in gtest contains diagnostics (#7528) 2026-06-21 09:41:43 -07:00
adminandGitHub 2ef4216886 Use cerr for flatbuffer allocator failures (#7530) 2026-06-21 09:41:28 -07:00
adminandGitHub 8c9764a57e Use cerr for APD thread diagnostic (#7529) 2026-06-21 09:41:09 -07:00
adminandGitHub eb94eb1a6d Use streams in available commands test diagnostics (#7527) 2026-06-21 09:40:53 -07:00
adminandGitHub 12e3fb99eb Keep dropped games in existing storage (#7525) 2026-06-21 09:32:34 -07:00
adminandGitHub 2bd90a26a8 Remove client text getTexts returns (#7526) 2026-06-21 09:32:04 -07:00
adminandGitHub 24db3a4cb1 Use streams for AI command count logging (#7524) 2026-06-21 09:31:02 -07:00
adminandGitHub 0d05ea1730 Use streams for iterative deepening diagnostics (#7523) 2026-06-21 09:30:45 -07:00
adminandGitHub 9c13b3c1bf Use streams for AI flee debug output (#7522) 2026-06-21 09:27:34 -07:00
adminandGitHub 492d9d16eb Remove StringConstructionParser returns (#7507) 2026-06-21 09:20:30 -07:00
adminandGitHub c4cd0eb683 Remove broken action point distance timer (#7509) 2026-06-21 09:15:53 -07:00
adminandGitHub 287f7b9818 Fix random battle runner build (#7521) 2026-06-21 09:15:35 -07:00
adminandGitHub 15b5a3e0c1 Remove SQLite client text guard returns (#7505) 2026-06-21 09:15:15 -07:00
adminandGitHub cf93c43ef2 Remove AI time budget debug print (#7506) 2026-06-21 09:14:55 -07:00
adminandGitHub 380c24537b Remove stale undead morale diagnostic (#7508) 2026-06-21 09:12:31 -07:00
adminandGitHub 6200bc7e02 Remove SqliteHistory flush return (#7510) 2026-06-21 09:12:05 -07:00
adminandGitHub e92465c894 Use explicit chrono imports in distance test (#7511) 2026-06-21 09:11:52 -07:00
adminandGitHub 6f432d221a Remove GameController debug return (#7512) 2026-06-21 09:11:28 -07:00
adminandGitHub b8ddca4e76 Route action point cache stats through streams (#7513) 2026-06-21 09:11:12 -07:00
adminandGitHub f343ee9f7f Send battalion type parse errors to stderr (#7514) 2026-06-21 09:10:53 -07:00
adminandGitHub 9a8c4c6fdd Remove GameController post battle return (#7515) 2026-06-21 09:10:38 -07:00
adminandGitHub 96f2dc0454 Send hex map migrator errors to stderr (#7516) 2026-06-21 09:10:21 -07:00
adminandGitHub e0988ef186 Remove GamesManager cache invalidation returns (#7517) 2026-06-21 09:07:20 -07:00
adminandGitHub a154174fa1 Remove broken Shardok inspector target (#7496) 2026-06-21 09:05:50 -07:00
adminandGitHub 042861f065 Remove GamesManager S3 archive return (#7520) 2026-06-21 09:04:54 -07:00
adminandGitHub 0f776af050 Remove GamesManager flush marker returns (#7518) 2026-06-21 09:04:18 -07:00
adminandGitHub 166e50924f Remove GamesManager LLM response return (#7519) 2026-06-21 09:04:00 -07:00
adminandGitHub efb34ede56 Remove Postgres client text guard returns (#7504) 2026-06-21 09:02:56 -07:00
adminandGitHub 22b55eb223 Allow routing transposition table stats output (#7503) 2026-06-21 08:41:33 -07:00
adminandGitHub f5d9027206 Allow routing MCTS cache stats output (#7502) 2026-06-21 08:39:52 -07:00
adminandGitHub 9dcbeff21a Avoid endl flushes in AI integration tests (#7501) 2026-06-21 08:39:29 -07:00
adminandGitHub 0cc7c4d0a2 Remove SQLite download recovery return (#7500) 2026-06-21 08:36:29 -07:00
adminandGitHub b0ca65b720 Use unique_ptr in attacker strategy selector test (#7499) 2026-06-21 08:36:09 -07:00
adminandGitHub e3eb3eb7bf Own nonstandard hex map adjacency cache entries (#7498) 2026-06-21 08:34:22 -07:00
adminandGitHub b8b2c92227 Remove SQLite legacy guard returns (#7497) 2026-06-21 08:33:33 -07:00
adminandGitHub 424dde5da6 Remove SQLite migration pending return (#7495) 2026-06-21 08:33:00 -07:00
adminandGitHub ce7e636987 Remove SQLite replay empty range return (#7494) 2026-06-21 08:31:12 -07:00
adminandGitHub c51f61ba56 Remove expand command selector return (#7492) 2026-06-21 08:28:42 -07:00
adminandGitHub 91623bcf7a Remove broken corrupt game checker (#7491) 2026-06-21 08:28:14 -07:00
adminandGitHub 34e14a3521 Check hex map migrator parse results (#7490) 2026-06-21 08:27:52 -07:00
adminandGitHub 6c1a79b342 Remove GameController AI guard returns (#7489) 2026-06-21 08:27:25 -07:00
adminandGitHub 6af5014866 Remove OAuth handler early returns (#7488) 2026-06-21 08:26:59 -07:00
adminandGitHub 16f53dc1b1 Use make_shared for battalion types (#7487) 2026-06-21 08:26:40 -07:00
adminandGitHub df09fe0684 Remove authorization interceptor early returns (#7486) 2026-06-21 08:26:09 -07:00
adminandGitHub e765764cea Remove SQLite shardok row early returns (#7485) 2026-06-21 08:25:13 -07:00
adminandGitHub 2e63bcb5c3 Write flatbuffer allocator diagnostics to stderr (#7484) 2026-06-21 08:23:55 -07:00
adminandGitHub e231f0c34e Use streams for Shardok controller diagnostics (#7483) 2026-06-21 08:23:35 -07:00
adminandGitHub 8155fe7b3f Remove Postgres shardok row early returns (#7482) 2026-06-21 08:23:17 -07:00
adminandGitHub 6fe1bcdff2 Use streams for map info diagnostics (#7481) 2026-06-21 08:23:00 -07:00
adminandGitHub b6b488bd46 Remove changed faction writer early returns (#7480) 2026-06-21 08:15:09 -07:00
adminandGitHub d97cc0cadc Use streams for Shardok server diagnostics (#7479) 2026-06-21 08:13:19 -07:00
adminandGitHub 2a3c742be9 Remove SQLite to Postgres migration admin flow (#7478) 2026-06-21 08:11:06 -07:00
adminandGitHub cdee86dbcb Remove S3 startup save sync (#7475) 2026-06-21 08:10:47 -07:00
adminandGitHub 60264e2bfc Make Shardok numeric casts explicit (#7477) 2026-06-21 08:09:58 -07:00
adminandGitHub 19fb315500 Make remaining unit byte casts explicit (#7476) 2026-06-21 08:09:34 -07:00
adminandGitHub 66710659c9 Tighten Shardok ownership cleanup (#7472) 2026-06-21 07:57:14 -07:00
adminandGitHub de5a34c485 Make game state dumper casts explicit (#7474) 2026-06-21 07:54:29 -07:00
adminandGitHub e0c7ba3874 Make Shardok casts explicit (#7473) 2026-06-21 07:54:04 -07:00
adminandGitHub 9e58fbee56 Remove S3 sync early returns (#7471) 2026-06-21 05:59:23 -07:00
adminandGitHub 5315941f88 Remove sqlite postgres migration return (#7470) 2026-06-21 05:59:01 -07:00
adminandGitHub 3ffeb0250b Remove battle progress early return (#7469) 2026-06-21 05:58:39 -07:00
adminandGitHub 451e6bb406 Avoid unnecessary endl flushes (#7465) 2026-06-21 05:58:06 -07:00
adminandGitHub 58d680bd59 Remove tutorial hero factions early return (#7464) 2026-06-20 21:47:23 -07:00
adminandGitHub 79eb708adb Normalize Shardok header guards (#7463) 2026-06-20 21:44:58 -07:00
adminandGitHub 1282e62781 Remove AsyncS3Persister early returns (#7466) 2026-06-20 21:44:29 -07:00
adminandGitHub 4af501feee Make gtest helper casts explicit (#7467) 2026-06-20 21:44:07 -07:00
adminandGitHub ef7c24f6de Make game over response casts explicit (#7468) 2026-06-20 21:43:29 -07:00
adminandGitHub 62edd02a22 Remove attack decision early returns (#7462) 2026-06-20 21:43:10 -07:00
adminandGitHub 6f22edc8f3 Remove GameMigrator early returns (#7461) 2026-06-20 18:43:08 -07:00
adminandGitHub 27bba1c2da Replace GNU alignment attributes with alignas (#7460) 2026-06-20 17:59:59 -07:00
adminandGitHub c95bc22f0e Remove simple Unit early returns (#7459) 2026-06-20 17:45:04 -07:00
adminandGitHub 844bab253d Rename test settings helper global (#7458) 2026-06-20 17:42:44 -07:00
adminandGitHub b8372487cf Remove backstory recovery early return (#7457) 2026-06-20 17:40:46 -07:00
adminandGitHub 8cc38466e6 Replace AI client flee debug macro (#7456) 2026-06-20 17:40:25 -07:00
adminandGitHub 14935b1a52 Remove march availability early returns (#7455) 2026-06-20 17:37:29 -07:00
adminandGitHub f7b14cd9ba Replace coords set index check macro (#7454) 2026-06-20 17:36:03 -07:00
adminandGitHub 9d1e61ef8d Remove defend availability early return (#7453) 2026-06-20 17:33:01 -07:00
adminandGitHub 8c912c1b3e Replace SuppressBeastsCommand type test (#7450) 2026-06-20 17:32:41 -07:00
adminandGitHub 1ff6ae0195 Replace test type predicates (#7452) 2026-06-20 17:32:12 -07:00
adminandGitHub 641bf456fa Replace iterative deepening debug macro (#7451) 2026-06-20 17:28:10 -07:00
adminandGitHub f21eaabb15 Modernize disabled C++ logging toggles (#7449) 2026-06-20 17:25:13 -07:00
adminandGitHub 7d38336e67 Remove EngineImpl applyActionResults return (#7448) 2026-06-20 17:23:56 -07:00
adminandGitHub f37ec78d10 Remove auth early returns (#7447) 2026-06-20 17:23:02 -07:00
adminandGitHub eb0f92dcd0 Remove simple utility early returns (#7446) 2026-06-20 17:21:20 -07:00
adminandGitHub ee06235cf4 Remove quest chooser early returns (#7445) 2026-06-20 17:20:53 -07:00
adminandGitHub 48dfe9054d Replace simple isInstanceOf predicates (#7443) 2026-06-20 17:20:33 -07:00
adminandGitHub c7e39e67aa Remove simple availability early returns (#7444) 2026-06-20 17:20:07 -07:00
adminandGitHub 86e51a11b7 Replace AI evaluator control macros with constexpr (#7442) 2026-06-20 16:35:44 -07:00
adminandGitHub 3c0e98280d Rename reserved test settings include guard (#7441) 2026-06-20 16:35:15 -07:00
adminandGitHub c54c9f7955 Remove Gemini parser early returns (#7440) 2026-06-20 16:34:50 -07:00
adminandGitHub 614c404bf4 Replace test roll macros with constexpr constants (#7439) 2026-06-20 16:15:09 -07:00
adminandGitHub c1ebcd49fc Replace combat macros with constexpr constants (#7437) 2026-06-20 16:11:00 -07:00
adminandGitHub 49551614fe Default simple C++ special members (#7436) 2026-06-20 16:10:21 -07:00
adminandGitHub b2b8603f23 Generalize FunctionalRandom sequence helpers (#7438) 2026-06-20 16:09:41 -07:00
adminandGitHub 4a6ecda8f8 Remove SettingsLoader asInstanceOf generation (#7432) 2026-06-20 15:58:17 -07:00
adminandGitHub b65d3c25e1 Remove observer asInstanceOf cast (#7427) 2026-06-20 15:56:58 -07:00
adminandGitHub 65f5ec1c38 Remove domain asInstanceOf casts (#7426) 2026-06-20 15:56:43 -07:00
adminandGitHub b8b225eb84 Rename reserved AI evaluator identifiers (#7433) 2026-06-20 14:47:37 -07:00
adminandGitHub c20239b9e8 Normalize static const declarations (#7434) 2026-06-20 14:47:12 -07:00
adminandGitHub a8df7228b3 Mark single-argument constructors explicit (#7431) 2026-06-20 14:46:47 -07:00
adminandGitHub b8ebc0efd8 Remove remaining inline body semicolons (#7428) 2026-06-20 14:46:21 -07:00
adminandGitHub 5ee86faf80 Remove redundant command constructor semicolons (#7425) 2026-06-20 14:44:41 -07:00
adminandGitHub 3d6524b2fa Remove key asInstanceOf casts (#7423) 2026-06-20 14:21:03 -07:00
adminandGitHub 82b671b234 Remove redundant factory constructor semicolons (#7422) 2026-06-20 14:20:16 -07:00
adminandGitHub 3ad6de784f Remove JSON asInstanceOf casts (#7421) 2026-06-20 14:19:46 -07:00
adminandGitHub 6a9e1d7ad7 Remove redundant virtual from retreat command (#7420) 2026-06-20 14:17:46 -07:00
adminandGitHub 84f182d74f Fail iOS build when Addressables runtime data is missing (#7424) 2026-06-20 14:16:26 -07:00
adminandGitHub d08402c502 Remove stale unused casts from Shardok server main (#7417) 2026-06-20 14:11:14 -07:00
adminandGitHub b6a7c38a70 Tidy unused MCTS parameters (#7419) 2026-06-20 14:10:54 -07:00
adminandGitHub 9513c1e404 Remove final test asInstanceOf uses (#7418) 2026-06-20 14:09:33 -07:00
adminandGitHub d67d8b9784 Use inside in attack decision hostile army fixtures (#7415) 2026-06-20 14:04:49 -07:00
adminandGitHub fdc643bac0 Use inside in attack decision allied army fixtures (#7413) 2026-06-20 14:04:36 -07:00
adminandGitHub 9e7890a1a6 Use inside in province events drought fixtures (#7411) 2026-06-20 14:04:23 -07:00
adminandGitHub e9d876e5c9 Use nullptr in server configuration parser (#7416) 2026-06-20 14:03:10 -07:00
adminandGitHub 524c15b824 Use using for remaining C++ aliases (#7414) 2026-06-20 14:02:48 -07:00
adminandGitHub 0891ccb0af Tidy remaining C++ include guards (#7412) 2026-06-20 14:02:22 -07:00
adminandGitHub 179288fa25 Use inside in province events conflict fixtures (#7410) 2026-06-20 13:11:31 -07:00
adminandGitHub 4a8b6df484 Use inside in province events riot condition fixtures (#7409) 2026-06-20 13:07:09 -07:00
adminandGitHub 73b5e1a8cc Use inside for resolve battle flee province change (#7407) 2026-06-20 13:06:08 -07:00
adminandGitHub 8fa012cb73 Tidy unit guard (#7408) 2026-06-20 13:05:21 -07:00
adminandGitHub 8561c606eb Tidy fire utils guard (#7406) 2026-06-20 13:05:06 -07:00
adminandGitHub abed097cad Tidy hex map utils guard (#7405) 2026-06-20 13:04:45 -07:00
adminandGitHub ccd7e6db18 Use inside in resolve battle quest province checks (#7404) 2026-06-20 13:00:40 -07:00
adminandGitHub e6c648c03e Use inside in resolve battle captured hero checks (#7403) 2026-06-20 13:00:21 -07:00
adminandGitHub a4278cb6f2 Use inside in attack decision battalion fixtures (#7402) 2026-06-20 13:00:02 -07:00
adminandGitHub ce9ddc049e Use inside in attack decision status fixtures (#7401) 2026-06-20 12:59:33 -07:00
adminandGitHub 3646fd868a Use inside in province events riot fixtures (#7398) 2026-06-20 12:59:11 -07:00
adminandGitHub 288428c874 Use using for tile modifier alias (#7399) 2026-06-20 12:58:03 -07:00
adminandGitHub 1f30d13454 Tidy zone control guard (#7400) 2026-06-20 12:57:48 -07:00
adminandGitHub 6712a495ff Tidy terrain guard (#7397) 2026-06-20 12:57:25 -07:00
adminandGitHub 8ac26578e3 Use inside in attack decision helper fixtures (#7396) 2026-06-20 12:44:50 -07:00
adminandGitHub 049c7abb81 Use inside in fulfill quests fixtures (#7395) 2026-06-20 12:44:17 -07:00
adminandGitHub 123e0066d7 Use inside in attack command chooser fixtures (#7394) 2026-06-20 12:43:56 -07:00
adminandGitHub 3cff277132 Use inside in seek more leaders fixtures (#7393) 2026-06-20 12:43:33 -07:00
adminandGitHub ebb6a55ef8 Use inside in province move resolution fixture (#7392) 2026-06-20 12:43:05 -07:00
adminandGitHub a2d371fcfc Use inside in manage prisoners availability fixtures (#7390) 2026-06-20 12:42:45 -07:00
adminandGitHub b1d30c4f03 Use inside in captured hero availability fixtures (#7389) 2026-06-20 12:42:21 -07:00
adminandGitHub 67911c12f6 Use inside in break alliance availability test (#7386) 2026-06-20 12:41:58 -07:00
adminandGitHub 25db6649f3 Use inside for new year fixture helpers (#7385) 2026-06-20 12:41:40 -07:00
adminandGitHub 6a5d9df1a4 Use inside for battle flee army assertions (#7383) 2026-06-20 12:41:18 -07:00
adminandGitHub 8254600b5c Tidy hex map direction guard (#7391) 2026-06-20 08:45:01 -07:00
adminandGitHub 8b0d43a5ee Tidy coordinates guard (#7388) 2026-06-20 08:44:45 -07:00
adminandGitHub 99b1827048 Tidy shardok c types guard (#7387) 2026-06-20 08:44:20 -07:00
adminandGitHub 8368f4f762 Tidy shardok action aliases (#7382) 2026-06-20 08:43:49 -07:00
adminandGitHub eda94bd881 Tidy percentile roll odds aliases (#7384) 2026-06-20 08:27:17 -07:00
adminandGitHub 2c8b3e6089 Pattern match attack decision phase filters (#7381) 2026-06-20 08:22:27 -07:00
adminandGitHub d9376ee870 Tidy battalion type helpers (#7380) 2026-06-20 08:21:55 -07:00
adminandGitHub 94473eaac6 Use inside for province event epidemic assertions (#7379) 2026-06-20 08:21:15 -07:00
adminandGitHub ff2caaac1c Use inside for forced turn back province assertions (#7378) 2026-06-20 08:20:51 -07:00
adminandGitHub 2fc7d91b6b Use inside in prisoner exchange test (#7375) 2026-06-20 08:20:27 -07:00
adminandGitHub f74ef641d8 Use inside for defender victory battle assertions (#7377) 2026-06-20 08:19:58 -07:00
adminandGitHub 1aa23d2992 Use inside for food consumption battalion assertions (#7374) 2026-06-20 08:19:01 -07:00
adminandGitHub 2fd86301ce Use inside for new year battalion assertions (#7372) 2026-06-20 08:16:17 -07:00
adminandGitHub 2c2a6a083b Tidy battalion damage helpers (#7376) 2026-06-20 08:15:13 -07:00
adminandGitHub 3462d7e6b3 Tidy sequence random generator header (#7370) 2026-06-20 08:14:45 -07:00
adminandGitHub d9a56d5cc5 Tidy tile modifier integrity bounds (#7373) 2026-06-20 08:14:11 -07:00
adminandGitHub db8a50edab Use inside in defense decision phase test (#7371) 2026-06-20 08:01:50 -07:00
adminandGitHub e815438212 Scope AttackOrientation enum (#7369) 2026-06-20 07:58:42 -07:00
adminandGitHub a31ce39009 Tidy ActionCost value type (#7366) 2026-06-20 07:58:20 -07:00
adminandGitHub 8d8abf0f90 Use inside in attack decision availability test (#7365) 2026-06-20 07:57:56 -07:00
adminandGitHub c9faf440ce Use inside for captured defender battle assertions (#7368) 2026-06-20 07:57:38 -07:00
adminandGitHub 14aec4f6d0 Use inside for outlawed defender battle assertions (#7367) 2026-06-20 07:57:07 -07:00
adminandGitHub 5b11135abb Use inside in captured hero availability test (#7364) 2026-06-20 07:56:25 -07:00
adminandGitHub 3ce4ad3770 Use inside in manage prisoner availability test (#7363) 2026-06-20 07:54:10 -07:00
adminandGitHub 65bc952a5a Use inside in forced turn back test (#7362) 2026-06-20 07:53:54 -07:00
adminandGitHub 01be354271 Use inside for new year province assertions (#7361) 2026-06-20 07:53:31 -07:00
adminandGitHub 2634c7c3c8 Tidy random generator header (#7358) 2026-06-20 07:49:34 -07:00
adminandGitHub 30789f86f6 Tidy Shardok unit helpers (#7355) 2026-06-20 07:49:09 -07:00
adminandGitHub 404028bb59 Always package Windows Addressables runtime data (#7360) 2026-06-20 07:47:26 -07:00
adminandGitHub ea25b9ea79 Use inside for new year hero assertions (#7359) 2026-06-20 07:43:03 -07:00
adminandGitHub 423940ad3b Use inside in send supplies test (#7357) 2026-06-20 07:42:47 -07:00
adminandGitHub 44d8ba6c40 Use inside for province event assertions (#7356) 2026-06-20 07:42:32 -07:00
adminandGitHub 87ea1f1727 Use inside in recon resolution tests (#7354) 2026-06-20 07:41:47 -07:00
adminandGitHub 76c70166ee Scope DamageType enum (#7353) 2026-06-20 07:30:13 -07:00
adminandGitHub 40706f96a3 Use inside for action result test casts (#7351) 2026-06-20 07:28:40 -07:00
adminandGitHub f04bcd61f7 Clean up CombatDamage storage (#7349) 2026-06-20 06:07:28 -07:00
adminandGitHub d415e97672 Use inside for selected Scala test casts (#7350) 2026-06-20 06:05:15 -07:00
adminandGitHub e22a6d987c Add clang-tidy runner script (#7347) 2026-06-19 22:23:03 -07:00
adminandGitHub ba60421024 Tighten Shardok C++ library tests (#7346) 2026-06-19 22:18:25 -07:00
adminandGitHub 0dd7d75878 Improve Shardok C++ safety checks (#7345) 2026-06-19 22:11:02 -07:00
adminandGitHub e77afba638 Increase handle riot give toggle tap targets (#7344) 2026-06-19 21:59:32 -07:00
adminandGitHub 2d69336245 Increase resource control tap targets (#7343) 2026-06-19 21:59:19 -07:00
adminandGitHub 934f3c594e Increase remaining command tap targets (#7342) 2026-06-19 21:59:04 -07:00
adminandGitHub 4cb56f80c6 Increase diplomacy tap targets (#7341) 2026-06-19 18:28:12 -07:00
adminandGitHub 212f19ba40 Increase attack decision tap targets (#7340) 2026-06-19 18:27:17 -07:00
adminandGitHub 9e4f052653 Increase command panel info tap target (#7339) 2026-06-19 18:20:05 -07:00
adminandGitHub afa85afebf Increase prisoner action tap targets (#7338) 2026-06-19 18:19:52 -07:00
adminandGitHub c6514be984 Increase ransom offer button tap targets (#7337) 2026-06-19 16:58:11 -07:00
adminandGitHub 719323d3c7 Restore stale text request invariant (#7336) 2026-06-19 14:20:23 -07:00
adminandGitHub 47fd3733b2 Prune generated text after rewind (#7335)
* Prune text store entries after rewind

* Fix rewind controller test text store expectation
2026-06-19 14:12:12 -07:00
adminandGitHub d7a6241a90 Include Shardok rows in Postgres save exports (#7334) 2026-06-19 13:51:12 -07:00
adminandGitHub ce37e37fb6 Resubmit orphaned outstanding Shardok battles (#7333) 2026-06-19 13:38:54 -07:00
adminandGitHub 536af1f547 Advance saved games after load or rewind (#7332) 2026-06-19 13:26:28 -07:00
adminandGitHub 7c69a9da0e Guard raw rewinds into missing battle state (#7331) 2026-06-19 13:23:38 -07:00
adminandGitHub 548f72e6fc Avoid reflection in raw Postgres history JSON (#7330) 2026-06-19 12:57:26 -07:00
adminandGitHub 1d5aa0c7aa Support raw Postgres history recovery (#7329) 2026-06-19 11:57:34 -07:00
adminandGitHub c00631d6dc Support raw rewind for Postgres saves (#7328) 2026-06-19 11:07:09 -07:00
adminandGitHub f3e6285828 Generate pleas for evacuated secured prisoners (#7327) 2026-06-19 07:47:06 -07:00
adminandGitHub 866c39283d Fix admin saved game transfer for Postgres (#7326) 2026-06-19 07:46:42 -07:00
adminandGitHub 6a98cd8d6d Upgrade Unity client to 6000.5.0f1 (#7325) 2026-06-19 07:46:13 -07:00
adminandGitHub 28000e389a Remove no profession icon (#7324) 2026-06-18 22:07:20 -07:00
adminandGitHub a336ee086c Replace fixed hero headshots (#7323)
* Replace fixed hero headshots

* Rework selected fixed hero portraits
2026-06-18 07:25:37 -07:00
adminandGitHub 08338c1910 Add Warden prisoner join bonus (#7322) 2026-06-17 16:29:47 -07:00
adminandGitHub 3fd9147567 Add Warden Great People (#7321) 2026-06-17 15:55:14 -07:00
adminandGitHub 1cd7f58817 Document Warden follow-up priorities (#7320) 2026-06-17 15:28:19 -07:00
adminandGitHub ebeb7cf2b1 Use shackles icon for Warden evacuation (#7319) 2026-06-17 13:26:00 -07:00
adminandGitHub 5ff379df96 Expose Warden evacuation command in Shardok UI (#7318) 2026-06-17 12:37:07 -07:00
adminandGitHub 4150f1de11 Carry Warden securement metadata through battle resolution (#7316) 2026-06-17 11:18:56 -07:00
adminandGitHub a4565196c2 Resolve secured prisoners from Warden evacuations (#7315) 2026-06-17 09:52:27 -07:00
adminandGitHub fb222665bf Limit Addressables rebuild detection to content inputs (#7314) 2026-06-17 09:50:08 -07:00
adminandGitHub 627f716eb4 Add Warden evacuation Shardok command (#7310) 2026-06-17 09:26:09 -07:00
adminandGitHub 1606a056b5 Guard Addressables groups against pack-together bundles (#7313) 2026-06-17 09:24:49 -07:00
adminandGitHub 3ac1225808 Guard against oversized Addressables uploads (#7312) 2026-06-17 08:44:37 -07:00
1037 changed files with 39982 additions and 151070 deletions
+29
View File
@@ -14,6 +14,7 @@ common --worker_sandboxing
common --local_test_jobs=64
common --jobs=64
# Keep these C++ standard flags in sync with tools/copts.bzl and scripts/shardok_cpp_config.sh.
common --cxxopt="--std=c++23"
common --cxxopt="-Wno-deprecated-non-prototype"
common --per_file_copt=src/test/cpp/.*@-Wno-character-conversion
@@ -22,6 +23,34 @@ common --per_file_copt=src/test/cpp/.*@-Wno-sign-compare
common --per_file_copt=src/test/cpp/.*@-Wno-unknown-warning-option
common --host_cxxopt="--std=c++23"
# C++ sanitizer configs for targeted Shardok safety checks.
# Example: bazel test --config=asan //src/test/cpp/net/eagle0/shardok/library/...
build:asan --compilation_mode=dbg
build:asan --strip=never
build:asan --copt=-fno-omit-frame-pointer
build:asan --copt=-Wno-macro-redefined
build:asan --copt=-fsanitize=address
build:asan --linkopt=-fsanitize=address
test:asan --test_env=ASAN_OPTIONS=detect_leaks=0:strict_init_order=1
build:ubsan --compilation_mode=dbg
build:ubsan --strip=never
build:ubsan --copt=-fno-omit-frame-pointer
build:ubsan --copt=-Wno-macro-redefined
build:ubsan --copt=-fsanitize=undefined
# Protobuf's arena implementation trips UBSan alignment checks in external code on macOS.
build:ubsan --copt=-fno-sanitize=alignment
build:ubsan --linkopt=-fsanitize=undefined
test:ubsan --test_env=UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
build:tsan --compilation_mode=dbg
build:tsan --strip=never
build:tsan --copt=-fno-omit-frame-pointer
build:tsan --copt=-Wno-macro-redefined
build:tsan --copt=-fsanitize=thread
build:tsan --linkopt=-fsanitize=thread
test:tsan --test_env=TSAN_OPTIONS=halt_on_error=1
common --javacopt="-Xlint:-options"
# Prefer protobuf's prebuilt protoc toolchain instead of building protoc from
+24
View File
@@ -0,0 +1,24 @@
Checks: >
-*,
bugprone-*,
performance-*,
modernize-use-nullptr,
modernize-use-override,
modernize-use-auto,
modernize-use-nodiscard,
readability-braces-around-statements,
readability-qualified-auto,
readability-redundant-member-init,
cppcoreguidelines-pro-type-cstyle-cast,
-bugprone-easily-swappable-parameters,
-bugprone-branch-clone,
-bugprone-signed-char-misuse
WarningsAsErrors: ''
HeaderFilterRegex: 'src/(main|test)/cpp/net/eagle0/(shardok|common)/.*'
FormatStyle: file
CheckOptions:
readability-braces-around-statements.ShortStatementLines: '1'
readability-function-size.LineThreshold: '160'
readability-function-size.StatementThreshold: '80'
readability-function-size.BranchThreshold: '20'
modernize-use-nullptr.NullMacros: 'NULL'
+8
View File
@@ -12,6 +12,9 @@ on:
- '.bazelrc'
- 'ci/github_actions/ensure_bazel_installed.sh'
- 'ci/github_actions/summarize_bazel_bep.py'
- '.clang-tidy'
- 'scripts/run-clang-tidy.sh'
- 'scripts/check_shardok_clang_tidy.sh'
- '.github/actions/setup-bazel/**'
- '.github/workflows/bazel_test.yml'
- '!src/main/csharp/**'
@@ -26,6 +29,9 @@ on:
- '.bazelrc'
- 'ci/github_actions/ensure_bazel_installed.sh'
- 'ci/github_actions/summarize_bazel_bep.py'
- '.clang-tidy'
- 'scripts/run-clang-tidy.sh'
- 'scripts/check_shardok_clang_tidy.sh'
- '.github/actions/setup-bazel/**'
- '.github/workflows/bazel_test.yml'
- '!src/main/csharp/**'
@@ -78,6 +84,8 @@ jobs:
node-version: 'lts/*'
- name: Check JavaScript syntax
run: node --check src/main/go/net/eagle0/admin_server/static/map_editor.js
- name: Check Shardok clang-tidy gate
run: ./scripts/check_shardok_clang_tidy.sh
test:
runs-on: [self-hosted, bazel]
+1 -15
View File
@@ -255,7 +255,7 @@ jobs:
echo "=== All images pushed successfully ==="
deploy:
runs-on: [self-hosted, bazel]
runs-on: ubuntu-latest
needs: [build-all]
if: needs.build-all.outputs.skip_build != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
environment: production
@@ -334,25 +334,11 @@ jobs:
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Download warmup binary
id: download-warmup
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: warmup-binary
path: scripts/bin/
- name: Build warmup binary fallback
if: steps.download-warmup.outcome != 'success'
run: |
echo "::warning::warmup-binary artifact was unavailable; rebuilding warmup binary before deploy"
./ci/github_actions/ensure_bazel_installed.sh
bazel build \
--platforms=//:linux_x86_64 \
--extra_toolchains=@llvm_toolchain_linux//:cc-toolchain-x86_64-linux \
//ci:warmup_tar
mkdir -p scripts/bin
tar -xf bazel-bin/ci/warmup_tar.tar -C scripts/bin --strip-components=1
- name: Copy config files to droplet
run: |
# Create directory structure on remote
+3 -1
View File
@@ -177,10 +177,12 @@ jobs:
env:
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
run: |
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
UNITY_MAJOR_MINOR=$(echo "$UNITY_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/\1.\2/')
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
-H "Authorization: Bearer $DO_CDN_PAT" \
-H "Content-Type: application/json" \
-d '{"files": ["addressables/iOS/*"]}' \
-d "{\"files\": [\"addressables/iOS/$UNITY_MAJOR_MINOR/*\"]}" \
--fail || echo "Warning: CDN purge failed (non-fatal)"
- name: Archive Build Log
+31 -3
View File
@@ -23,6 +23,7 @@ on:
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/ensure_bazel_installed.sh"
- ".github/actions/setup-bazel/**"
- "ci/github_actions/detect_addressables_changes.sh"
- "ci/github_actions/upload_addressables.sh"
- "ci/github_actions/ensure_unity_installed.sh"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
@@ -49,8 +50,11 @@ on:
- "scripts/notarize_wait.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/ensure_unity_installed.sh"
- "ci/github_actions/ensure_bazel_installed.sh"
- ".github/actions/setup-bazel/**"
- "ci/github_actions/detect_addressables_changes.sh"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectSettings.asset"
- ".bazelrc"
- "ci/mac/**"
@@ -121,11 +125,25 @@ jobs:
- name: Clean stale files
run: |
git clean -ffd
LIBRARY_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library"
VERSION_CACHE="$LIBRARY_DIR/.last_unity_version"
PROJECT_VERSION_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
CURRENT_VERSION=$(grep "m_EditorVersion:" "$PROJECT_VERSION_FILE" | head -1 | sed 's/m_EditorVersion: //')
# Nuke Library/ when Unity version changes to avoid import loops
if [ -f "$VERSION_CACHE" ]; then
CACHED_VERSION=$(cat "$VERSION_CACHE")
if [ "$CACHED_VERSION" != "$CURRENT_VERSION" ]; then
echo "Unity version changed ($CACHED_VERSION -> $CURRENT_VERSION) -- clearing Library/"
rm -rf "$LIBRARY_DIR"
fi
fi
# Only clear Bee/ when C# files were added/deleted/renamed (structural
# changes that stale the DAG). Content-only modifications are handled by
# Bee's incremental compilation. See persist_library.sh for background.
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
BEE_DIR="$LIBRARY_DIR/Bee"
SHA_FILE="$LIBRARY_DIR/.last_built_sha"
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
LAST_SHA=$(cat "$SHA_FILE")
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
@@ -182,6 +200,12 @@ jobs:
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
- name: Save Unity version for Library/ cache invalidation
if: success()
run: |
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //' > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_unity_version
- name: Check if should deploy
id: check-deploy
run: |
@@ -243,6 +267,7 @@ jobs:
runs-on: [self-hosted, macOS, notarize]
outputs:
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
deployed_unity_major_minor: ${{ steps.deploy-mac.outputs.deployed_unity_major_minor }}
concurrency:
group: mac-deploy-${{ github.ref }}
cancel-in-progress: false
@@ -471,6 +496,9 @@ jobs:
rm "$SPARKLE_PRIVATE_KEY_PATH"
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
UNITY_MAJOR_MINOR=$(echo "$UNITY_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/\1.\2/')
echo "deployed_unity_major_minor=$UNITY_MAJOR_MINOR" >> $GITHUB_OUTPUT
# Artifact cleanup is handled by the cleanup job on ubuntu-latest
- name: Cleanup build directory
@@ -491,7 +519,7 @@ jobs:
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
-H "Authorization: Bearer $DO_CDN_PAT" \
-H "Content-Type: application/json" \
-d '{"files": ["mac/*", "addressables/StandaloneOSX/*"]}' \
-d '{"files": ["mac/*", "addressables/StandaloneOSX/${{ needs.deploy-mac.outputs.deployed_unity_major_minor }}/*"]}' \
--fail || echo "Warning: CDN purge failed (non-fatal)"
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy-mac.outputs.deployed_version }}&required=false" \
+6
View File
@@ -20,6 +20,7 @@ on:
- 'ci/github_actions/ensure_bazel_installed.sh'
- 'scripts/build_eagle_ci.sh'
- 'scripts/build_shardok_ci.sh'
- 'scripts/build_shardok_linux_arm64_ci.sh'
- '.github/actions/setup-bazel/**'
- '.github/workflows/server_build.yml'
@@ -83,6 +84,7 @@ jobs:
'src/main/cpp/',
'src/main/protobuf/net/eagle0/shardok/',
'scripts/build_shardok_ci.sh',
'scripts/build_shardok_linux_arm64_ci.sh',
];
const shared = matches(sharedPatterns);
@@ -106,6 +108,10 @@ jobs:
name: 'Shardok server',
command: './scripts/build_shardok_ci.sh',
});
include.push({
name: 'Shardok Linux ARM64 server',
command: './scripts/build_shardok_linux_arm64_ci.sh',
});
}
core.setOutput('build_matrix', JSON.stringify({include}));
+3 -1
View File
@@ -196,7 +196,9 @@ jobs:
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
deploy-hetzner:
runs-on: [self-hosted, bazel]
# Keep this on a self-hosted Mac runner for IPv6 reachability to Hetzner,
# but do not require a Bazel runner slot for artifact transport/deploy work.
runs-on: [self-hosted, macOS, ARM64]
needs: [build-shardok-arm64]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
@@ -0,0 +1,92 @@
name: Shardok Sanitizer Test
on:
push:
branches: ["main"]
paths:
- "src/main/cpp/net/eagle0/shardok/**"
- "src/test/cpp/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/common/**"
- "WORKSPACE"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "BUILD.bazel"
- ".bazelrc"
- ".github/actions/setup-bazel/**"
- ".github/workflows/shardok_sanitizer_test.yml"
- "ci/github_actions/ensure_bazel_installed.sh"
- "ci/github_actions/summarize_bazel_bep.py"
pull_request:
paths:
- "src/main/cpp/net/eagle0/shardok/**"
- "src/test/cpp/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/shardok/**"
- "src/main/protobuf/net/eagle0/common/**"
- "WORKSPACE"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "BUILD.bazel"
- ".bazelrc"
- ".github/actions/setup-bazel/**"
- ".github/workflows/shardok_sanitizer_test.yml"
- "ci/github_actions/ensure_bazel_installed.sh"
- "ci/github_actions/summarize_bazel_bep.py"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
shardok-sanitizer-test:
name: Shardok ${{ matrix.name }}
runs-on: [self-hosted, bazel]
strategy:
fail-fast: false
matrix:
include:
- name: ASan
config: asan
- name: UBSan
config: ubsan
steps:
- name: Ensure Git LFS available for checkout
run: |
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
for path in "${COMMON_PATHS[@]}"; do
if [ -d "$path" ]; then
echo "$path" >> "$GITHUB_PATH"
export PATH="$path:$PATH"
fi
done
if command -v git-lfs >/dev/null 2>&1; then
git-lfs version
exit 0
fi
brew install git-lfs
git-lfs version
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
lfs: false
- name: Setup Bazel
uses: ./.github/actions/setup-bazel
- name: Run Shardok sanitizer tests
run: bazel test --config=${{ matrix.config }} --build_event_json_file=${{ matrix.config }}.json //src/test/cpp/net/eagle0/shardok/...
- name: Summarize Bazel build metrics
if: always()
run: python3 ci/github_actions/summarize_bazel_bep.py ${{ matrix.config }}.json
- name: Archive sanitizer test results
if: always()
uses: actions/upload-artifact@v7
with:
name: shardok-${{ matrix.config }}.json
path: ${{ matrix.config }}.json
retention-days: 3
+3 -1
View File
@@ -18,6 +18,7 @@ on:
- "ci/github_actions/test_unity_editmode.sh"
- "ci/github_actions/ensure_bazel_installed.sh"
- ".github/actions/setup-bazel/**"
- "ci/github_actions/detect_addressables_changes.sh"
- "ci/github_actions/upload_addressables.sh"
- "ci/github_actions/ensure_unity_installed.sh"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
@@ -42,6 +43,7 @@ on:
- "ci/github_actions/test_unity_editmode.sh"
- "ci/github_actions/ensure_bazel_installed.sh"
- ".github/actions/setup-bazel/**"
- "ci/github_actions/detect_addressables_changes.sh"
- "ci/github_actions/upload_addressables.sh"
- "ci/github_actions/ensure_unity_installed.sh"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
@@ -285,7 +287,7 @@ jobs:
./ci/github_actions/detect_addressables_changes.sh "$BASE_SHA" "${{ github.sha }}"
- name: Build Windows unity
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "${{ steps.addressables.outputs.changed }}" "${{ steps.addressables.outputs.changed }}"
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" true "${{ steps.addressables.outputs.changed }}"
- name: Save build SHA for Bee/ cache invalidation
if: success()
+25 -1
View File
@@ -11,6 +11,11 @@
**NEVER kill Unity without asking the user first.** The user might be actively using the Unity Editor. Do not kill,
force-quit, terminate, or otherwise stop Unity processes unless the user explicitly approves that specific action.
**NEVER change Unity's default/open scene as part of running checks.** Unity batchmode or editor validation may write
scene-selection churn such as `ProjectSettings/EditorBuildSettings.asset`, `ProjectSettings/SceneTemplateSettings.json`,
or scene files simply because a different scene was open or loaded. Treat those as unintended local environment changes:
do not stage them, and restore them before committing unless the user explicitly asked to change scenes/build settings.
## CRITICAL GIT RULES (NEVER VIOLATE)
**NEVER use `git -C`.** Just run `git` directly from the cwd — it finds the repo via `.git` discovery. Do not `cd` to the repo root either (see the no-`cd` bash rule above).
@@ -29,6 +34,16 @@ If you catch yourself about to run `git push origin main` or `git push origin <b
If you catch yourself about to run `gh pr merge`, STOP. Only the user merges PRs.
## Commit History During Review
Prefer new follow-up commits over amending existing commits once a PR is open and the user is actively reviewing or
testing it. This preserves working interim states so the user can inspect, compare, or return to a known-good point
while improvements continue.
Use `git commit --amend` and force-push only when the user explicitly asks for history cleanup, when fixing metadata
before review has started, or when repairing a local commit that has not been pushed. If you believe squashing is the
right choice despite an open review, explain why and ask first.
## Worktree Usage
Prefer using the current worktree for small requested fixes. Create a separate worktree only when the user asks for one,
@@ -58,6 +73,15 @@ For PRs expected to trigger substantial Bazel work in CI, run the relevant Bazel
workstation is fast, and local Bazel runs can help hydrate the remote cache for the CI builders while still giving
earlier signal on failures.
## Cleanup PR Batching
When making mechanical cleanup changes, group 3-5 files per PR when the files are receiving the same kind of change
and can be reviewed as one pattern. Do not open one PR per file for nearly identical few-line cleanups, such as replacing
the same unsafe accessor pattern with the same contextual failure pattern across multiple Scala files.
Keep separate PRs for changes that involve different semantics, materially different risk, unrelated subsystems, or
files whose tests/validation strategy makes them better reviewed independently.
## GitHub CLI PR Bodies
When creating or editing PR descriptions with multiline bodies, write the body to a temporary markdown file and pass it
@@ -294,7 +318,7 @@ to be used for different players or game situations within the same server proce
**C# (Unity Client):**
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
- Uses Unity 6 (6000.4.10f1) with comprehensive protobuf integration (100+ .proto files)
- Uses Unity 6.5 (6000.5.0f1) with comprehensive protobuf integration (100+ .proto files)
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
+1 -1
View File
@@ -282,7 +282,7 @@ oci.pull(
# Base image for Admin Server (Alpine for lightweight Go binary)
oci.pull(
name = "alpine_linux",
digest = "sha256:a2d49ea686c2adfe3c992e47dc3b5e7fa6e6b5055609400dc2acaeb241c829f4",
digest = "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
image = "docker.io/library/alpine",
platforms = ["linux/amd64"],
tag = "3.24",
+8 -8
View File
@@ -904,7 +904,7 @@
"@@rules_oci+//oci:extensions.bzl%oci": {
"general": {
"bzlTransitiveDigest": "H23W9dvwCQM6h+Wt8PjIahx08EkAU5eSbUxAyx9Mopc=",
"usagesDigest": "tbQ7wHHObzzrBzSRv652104GY3xxiaQDK5OUp2YeEbg=",
"usagesDigest": "k6DBHAkb5br/tpwCPUMJgqwb+7U4DXyCmBZQBgLnHJ8=",
"recordedInputs": [
"REPO_MAPPING:aspect_bazel_lib+,bazel_tools bazel_tools",
"REPO_MAPPING:bazel_features+,bazel_tools bazel_tools",
@@ -922,7 +922,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/eclipse-temurin",
"identifier": "sha256:db3011b348ad87ced055afe5145238d87a3d8f0e8f23eb4d0cd8cecb4659b085",
"identifier": "sha256:0beb258e10ad28d2bae0f74349a170bdfa67da0ef87c028cbf3d95c3e1170120",
"platform": "linux/amd64",
"target_name": "eclipse_temurin_25_jre_linux_amd64",
"bazel_tags": []
@@ -936,7 +936,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/eclipse-temurin",
"identifier": "sha256:db3011b348ad87ced055afe5145238d87a3d8f0e8f23eb4d0cd8cecb4659b085",
"identifier": "sha256:0beb258e10ad28d2bae0f74349a170bdfa67da0ef87c028cbf3d95c3e1170120",
"platforms": {
"@@platforms//cpu:x86_64": "@eclipse_temurin_25_jre_linux_amd64"
},
@@ -980,7 +980,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/ubuntu",
"identifier": "24.04",
"identifier": "26.04",
"platform": "linux/amd64",
"target_name": "ubuntu_24_04_linux_amd64",
"bazel_tags": []
@@ -993,7 +993,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/ubuntu",
"identifier": "24.04",
"identifier": "26.04",
"platform": "linux/arm64/v8",
"target_name": "ubuntu_24_04_linux_arm64_v8",
"bazel_tags": []
@@ -1007,7 +1007,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/ubuntu",
"identifier": "24.04",
"identifier": "26.04",
"platforms": {
"@@platforms//cpu:x86_64": "@ubuntu_24_04_linux_amd64",
"@@platforms//cpu:arm64": "@ubuntu_24_04_linux_arm64_v8"
@@ -1023,7 +1023,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/alpine",
"identifier": "sha256:a2d49ea686c2adfe3c992e47dc3b5e7fa6e6b5055609400dc2acaeb241c829f4",
"identifier": "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
"platform": "linux/amd64",
"target_name": "alpine_linux_linux_amd64",
"bazel_tags": []
@@ -1037,7 +1037,7 @@
"scheme": "https",
"registry": "index.docker.io",
"repository": "library/alpine",
"identifier": "sha256:a2d49ea686c2adfe3c992e47dc3b5e7fa6e6b5055609400dc2acaeb241c829f4",
"identifier": "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
"platforms": {
"@@platforms//cpu:x86_64": "@alpine_linux_linux_amd64"
},
+1 -3
View File
@@ -5,8 +5,7 @@ set -x
COMMAND_LOG="$(bazel info command_log)"
bazel build \
//src/main/cpp/net/eagle0/shardok:shardok-server \
//src/main/cpp/net/eagle0/shardok:shardok-inspector
//src/main/cpp/net/eagle0/shardok:shardok-server
STATUS="$?"
if [ $STATUS -eq 0 ]
@@ -26,7 +25,6 @@ echo "Setting up Shardok files..."
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server ./shardok/
/bin/mkdir -p ./shardok/shardok-server.runfiles/net_eagle0/src/main/
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server.runfiles/net_eagle0/src/main/resources ./shardok/shardok-server.runfiles/net_eagle0/src/main/
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-inspector ./shardok/
echo "tar & zip shardok files..."
/bin/tar cfh - shardok | /usr/bin/pigz > ./deploy/shardok.tar.gz
-13
View File
@@ -40,19 +40,6 @@ if [ -f "$INFO_PLIST" ]; then
/usr/libexec/PlistBuddy -c "Set :ITSAppUsesNonExemptEncryption false" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool false" "$INFO_PLIST"
echo "Set ITSAppUsesNonExemptEncryption=false in Info.plist"
# Remove UIApplicationSceneManifest if present.
# Unity 6 generates this key, which opts the app into the iOS scene-based
# lifecycle. This breaks Application.deepLinkActivated because iOS routes
# deep link URLs through UISceneDelegate instead of UIApplicationDelegate,
# and Unity doesn't implement the scene delegate path.
# See: https://issuetracker.unity3d.com/issues/in-135632
if /usr/libexec/PlistBuddy -c "Print :UIApplicationSceneManifest" "$INFO_PLIST" 2>/dev/null; then
echo "Found UIApplicationSceneManifest in Info.plist — removing to fix deep link handling"
/usr/libexec/PlistBuddy -c "Delete :UIApplicationSceneManifest" "$INFO_PLIST"
else
echo "No UIApplicationSceneManifest in Info.plist (deep links should work)"
fi
fi
# Unity always generates "Unity-iPhone" as the main app scheme
+14
View File
@@ -59,6 +59,20 @@ if [ $UNITY_EXIT_CODE -ne 0 ]; then
exit $UNITY_EXIT_CODE
fi
if [ ! -f "$LOG_PATH" ]; then
echo ""
echo "ERROR: Unity exited successfully but did not write an editor log at $LOG_PATH"
exit 1
fi
if [ ! -f "$BUILD_PATH/Data/Raw/aa/settings.json" ]; then
echo ""
echo "ERROR: iOS player is missing Addressables runtime data at:"
echo " $BUILD_PATH/Data/Raw/aa/settings.json"
echo "Build Addressables before packaging the player so Addressables can initialize at runtime."
exit 1
fi
# Fail the build if any prefab references are broken — this produces a player
# that launches but has null Inspector fields, which is hard to debug.
if grep -q "Missing Prefab" "$LOG_PATH"; then
+8
View File
@@ -57,6 +57,14 @@ if [ ! -f "$LOG_PATH" ]; then
exit 1
fi
if [ ! -f "$BUILD_DIR/eagle0_Data/StreamingAssets/aa/settings.json" ]; then
echo ""
echo "ERROR: Windows player is missing Addressables runtime data at:"
echo " $BUILD_DIR/eagle0_Data/StreamingAssets/aa/settings.json"
echo "Build Addressables before packaging the player so Addressables can initialize at runtime."
exit 1
fi
# Fail the build if any prefab references are broken — this produces a player
# that launches but has null Inspector fields, which is hard to debug.
if grep -q "Missing Prefab" "$LOG_PATH"; then
@@ -28,7 +28,6 @@ else
"$UNITY_PROJECT/Assets/AustraliaAnimalsPackv1/"
"$UNITY_PROJECT/Assets/Music/"
"$UNITY_PROJECT/Assets/Editor/BuildScript.cs"
"ci/github_actions/upload_addressables.sh"
)
is_addressables_input() {
+10 -2
View File
@@ -49,7 +49,7 @@ check_modules_installed() {
fi
;;
mac)
if ! has_il2cpp_variation "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Variations"; then
if ! has_mac_il2cpp_support "$unity_path" "$unity_app_contents_path"; then
echo "✗ Mac IL2CPP module not installed for Unity ${UNITY_VERSION}"
return 1
fi
@@ -67,7 +67,7 @@ check_modules_installed() {
echo "✗ iOS module missing"
missing=1
fi
if ! has_il2cpp_variation "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Variations"; then
if ! has_mac_il2cpp_support "$unity_path" "$unity_app_contents_path"; then
echo "✗ Mac IL2CPP module missing"
missing=1
fi
@@ -84,6 +84,14 @@ check_modules_installed() {
return 0
}
has_mac_il2cpp_support() {
local unity_path="$1"
local unity_app_contents_path="$2"
has_il2cpp_variation "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Variations" ||
has_il2cpp_variation "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations"
}
has_il2cpp_variation() {
local variations_path="$1"
local variation
+65 -12
View File
@@ -13,12 +13,19 @@ set -euxo pipefail
BUILD_TARGET=$1
WORKSPACE=$(pwd)
UNITY_PROJECT="$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET"
UNITY_VERSION=$(grep "m_EditorVersion:" "$UNITY_PROJECT/ProjectSettings/ProjectVersion.txt" | head -1 | sed 's/m_EditorVersion: //')
UNITY_MAJOR_MINOR=$(echo "$UNITY_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/\1.\2/')
ADDRESSABLES_PREFIX="addressables/$BUILD_TARGET/$UNITY_MAJOR_MINOR"
SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET/$UNITY_MAJOR_MINOR"
# DigitalOcean Spaces configuration (same region as other eagle0 buckets)
DO_ENDPOINT="https://sfo3.digitaloceanspaces.com"
DO_BUCKET="eagle0-assets"
UPLOAD_WARNING_BYTES=$((1024 * 1024 * 1024))
DEFAULT_MAX_UPLOAD_BYTES=$((1024 * 1024 * 1024))
DEFAULT_MAX_SINGLE_FILE_BYTES=$((512 * 1024 * 1024))
MAX_UPLOAD_BYTES=${ADDRESSABLES_MAX_UPLOAD_BYTES:-$DEFAULT_MAX_UPLOAD_BYTES}
MAX_SINGLE_FILE_BYTES=${ADDRESSABLES_MAX_SINGLE_FILE_BYTES:-$DEFAULT_MAX_SINGLE_FILE_BYTES}
TOP_UPLOAD_COUNT=${ADDRESSABLES_TOP_UPLOAD_COUNT:-20}
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export PATH="${COMMON_PATHS}:${PATH}"
@@ -51,6 +58,17 @@ file_size() {
fi
}
append_summary() {
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
echo "$1" >> "$GITHUB_STEP_SUMMARY"
fi
}
budget_enabled() {
local value=$1
[ "$value" -gt 0 ]
}
summarize_pending_uploads() {
local sync_args_file
sync_args_file=$(mktemp)
@@ -60,7 +78,7 @@ summarize_pending_uploads() {
sorted_uploads_file=$(mktemp)
trap 'rm -f "$sync_args_file" "$uploads_file" "$sorted_uploads_file"' RETURN
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/$ADDRESSABLES_PREFIX/" \
--endpoint-url "$DO_ENDPOINT" \
--acl public-read \
--size-only \
@@ -91,24 +109,59 @@ summarize_pending_uploads() {
fi
echo "Addressables upload preflight: $upload_count changed files, $(format_bytes "$upload_bytes") to upload."
append_summary "## Addressables upload preflight"
append_summary ""
append_summary "- Target: \`$BUILD_TARGET\`"
append_summary "- Changed files: \`$upload_count\`"
append_summary "- Changed upload bytes: \`$(format_bytes "$upload_bytes")\`"
append_summary "- Upload byte budget: \`$(format_bytes "$MAX_UPLOAD_BYTES")\`"
append_summary "- Single-file budget: \`$(format_bytes "$MAX_SINGLE_FILE_BYTES")\`"
if [ "$upload_bytes" -ge "$UPLOAD_WARNING_BYTES" ]; then
echo "WARNING: Addressables upload is expected to exceed $(format_bytes "$UPLOAD_WARNING_BYTES")."
echo "A small asset change may have invalidated an oversized bundle; inspect the largest pending uploads below."
fi
local guardrail_failed=0
if [ -s "$uploads_file" ]; then
echo "Largest pending Addressables uploads:"
append_summary ""
append_summary "### Largest pending uploads"
append_summary ""
append_summary "| Size | Path |"
append_summary "| ---: | --- |"
sort -nr "$uploads_file" > "$sorted_uploads_file"
local listed_uploads=0
while IFS=$'\t' read -r size path; do
if [ "$listed_uploads" -ge 20 ]; then
if [ "$listed_uploads" -ge "$TOP_UPLOAD_COUNT" ]; then
break
fi
local relative_path=${path#"$SERVER_DATA"/}
echo " $(format_bytes "$size") $relative_path"
append_summary "| $(format_bytes "$size") | \`$relative_path\` |"
listed_uploads=$((listed_uploads + 1))
done < "$sorted_uploads_file"
if budget_enabled "$MAX_SINGLE_FILE_BYTES"; then
while IFS=$'\t' read -r size path; do
if [ "$size" -gt "$MAX_SINGLE_FILE_BYTES" ]; then
local relative_path=${path#"$SERVER_DATA"/}
echo "ERROR: Pending Addressables upload '$relative_path' is $(format_bytes "$size"), exceeding single-file budget $(format_bytes "$MAX_SINGLE_FILE_BYTES")."
append_summary ""
append_summary ":x: \`$relative_path\` exceeds the single-file budget at \`$(format_bytes "$size")\`."
guardrail_failed=1
fi
done < "$uploads_file"
fi
fi
if budget_enabled "$MAX_UPLOAD_BYTES" && [ "$upload_bytes" -gt "$MAX_UPLOAD_BYTES" ]; then
echo "ERROR: Addressables upload is expected to send $(format_bytes "$upload_bytes"), exceeding budget $(format_bytes "$MAX_UPLOAD_BYTES")."
echo "A small asset change may have invalidated an oversized bundle; inspect the largest pending uploads above."
append_summary ""
append_summary ":x: Changed upload bytes exceed budget: \`$(format_bytes "$upload_bytes")\` > \`$(format_bytes "$MAX_UPLOAD_BYTES")\`."
guardrail_failed=1
fi
if [ "$guardrail_failed" -ne 0 ]; then
echo "Refusing to upload Addressables. Raise ADDRESSABLES_MAX_UPLOAD_BYTES or ADDRESSABLES_MAX_SINGLE_FILE_BYTES only after confirming the upload is intentional."
exit 1
fi
}
@@ -128,7 +181,7 @@ if [ ! -d "$SERVER_DATA" ]; then
fi
echo "Uploading Addressables bundles from $SERVER_DATA"
echo "Target: s3://$DO_BUCKET/addressables/$BUILD_TARGET/"
echo "Target: s3://$DO_BUCKET/$ADDRESSABLES_PREFIX/"
# Configure AWS CLI for DigitalOcean Spaces
export AWS_ACCESS_KEY_ID="$ACCESS_KEY_ID"
@@ -143,13 +196,13 @@ summarize_pending_uploads
#
# --delete removes files in destination that don't exist in source.
# --acl public-read makes files publicly accessible.
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/$ADDRESSABLES_PREFIX/" \
--endpoint-url "$DO_ENDPOINT" \
--acl public-read \
--size-only \
--delete
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/$ADDRESSABLES_PREFIX/" \
--endpoint-url "$DO_ENDPOINT" \
--acl public-read \
--exclude "*" \
@@ -158,4 +211,4 @@ aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
--include "*.json"
echo "Addressables upload complete"
echo "Files available at: https://assets.eagle0.net/addressables/$BUILD_TARGET/"
echo "Files available at: https://assets.eagle0.net/$ADDRESSABLES_PREFIX/"
+1 -1
View File
@@ -71,7 +71,7 @@ LLM settings can be changed at runtime via the admin console:
1. Navigate to Admin Console → Settings
2. Change `LlmProvider` to select vendor (gemini, openai, claude)
3. Change the corresponding model name setting:
- `GeminiModelName` (default: gemini-3.1-flash-lite-preview)
- `GeminiModelName` (default: gemini-3.1-flash-lite)
- `OpenAiModelName` (default: gpt-4.1-mini)
- `ClaudeModelName` (default: claude-3-5-haiku-20241022)
+12
View File
@@ -103,6 +103,18 @@ and protect fragile specialists.
a prisoner, add the Warden custody bonus to that prisoner's `factionBiases(rulingFactionId)`.
- For Shardok, start with a single-turn guard status before attempting more complex interception chains
**Current Follow-Up Priorities:**
- Teach the Shardok AI when to use the Warden evacuation command. The command is valuable when the Warden's side has
captured enemy heroes and can secure them by leaving the battle, especially when the battle outcome is uncertain or
trending against that side.
- Add the strategic passive custody bonus: each round, prisoners in a province guarded by a ruling-faction Warden should
get a small accumulated positive bias toward joining that faction. Prefer storing this as an addition to the existing
`factionBiases` entry rather than changing the global `roundsInType` multiplier, so the bonus only reflects rounds
actually spent under Warden custody.
- Make tactical AI more protective of heroes at risk of capture when a Warden may be present. Warden evacuation makes
captured heroes harder to recover, so the AI should treat exposing high-value heroes to capture as more dangerous than
before. This matters even when the AI does not have perfect knowledge that a Warden is in the battle.
### 2. HERALD (Morale & Communication Specialist)
**Fantasy:** The inspiring leader who rallies troops and carries messages across the battlefield.
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
bazel_cmd=(bazel)
if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then
bazel_cmd+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
fi
if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
bazel_cmd+=("--output_base=${BAZEL_OUTPUT_BASE}")
fi
"${bazel_cmd[@]}" build \
--platforms=//:linux_arm64 \
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
"$@" \
//src/main/cpp/net/eagle0/shardok:shardok-server
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# Runs the curated Shardok clang-tidy gate used by CI.
#
# This intentionally checks representative files instead of the whole C++ tree.
# Keep this list low-noise; expand it as cleanup PRs make more areas tidy-clean.
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
source "$repo_root/scripts/shardok_cpp_config.sh"
files=(
"src/main/cpp/net/eagle0/shardok/library/ActionCost.hpp"
"src/main/cpp/net/eagle0/shardok/library/CombatDamage.hpp"
)
compiler_args=(
"-I$repo_root"
"$SHARDOK_CXX_STANDARD_ARG"
)
if command -v xcrun >/dev/null 2>&1; then
sdk_path="$(xcrun --show-sdk-path)"
compiler_args+=("-isysroot" "$sdk_path")
fi
"$repo_root/scripts/run-clang-tidy.sh" --verify-config
"$repo_root/scripts/run-clang-tidy.sh" --quiet --warnings-as-errors="*" "${files[@]}" -- "${compiler_args[@]}"
+12 -2
View File
@@ -32,23 +32,33 @@ echo "=== Creating ZIP for notarization: $ZIP_PATH ===" >&2
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "=== Submitting to Apple for notarization ===" >&2
set +e
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" 2>&1)
SUBMIT_STATUS=$?
set -e
echo "$SUBMIT_OUTPUT" >&2
if [ "$SUBMIT_STATUS" -ne 0 ]; then
rm -f "$ZIP_PATH"
echo "ERROR: notarytool submit failed with exit code $SUBMIT_STATUS" >&2
exit "$SUBMIT_STATUS"
fi
# Extract submission ID
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | awk '/^[[:space:]]*id:/ {print $2; exit}')
if [ -z "$SUBMISSION_ID" ]; then
rm -f "$ZIP_PATH"
echo "ERROR: Failed to get submission ID" >&2
exit 1
fi
# Clean up the zip
rm "$ZIP_PATH"
rm -f "$ZIP_PATH"
echo "Submission ID: $SUBMISSION_ID" >&2
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# Runs the repository-pinned clang-tidy binary from the workspace root.
#
# Bazel's `run` command executes clang-tidy from its runfiles tree, which makes
# workspace-relative source paths and .clang-tidy discovery fail. This wrapper
# builds the tool and then invokes the produced binary directly.
set -e
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
repo_root="$(git rev-parse --show-toplevel)"
source "$repo_root/scripts/shardok_cpp_config.sh"
if [ "$#" -eq 0 ]; then
echo "Usage: scripts/run-clang-tidy.sh [clang-tidy args] <files> [-- <compiler args>]"
echo ""
echo "Examples:"
echo " scripts/run-clang-tidy.sh --verify-config"
echo " scripts/run-clang-tidy.sh src/main/cpp/net/eagle0/shardok/library/CombatDamage.hpp -- -I$repo_root $SHARDOK_CXX_STANDARD_ARG"
exit 2
fi
bazel build @llvm_toolchain//:clang-tidy
bazel_bin="$(bazel info bazel-bin)"
clang_tidy="$bazel_bin/external/toolchains_llvm++llvm+llvm_toolchain/clang-tidy"
if [ ! -x "$clang_tidy" ]; then
echo "Unable to find clang-tidy binary at $clang_tidy"
exit 1
fi
"$clang_tidy" --config-file="$repo_root/.clang-tidy" "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Shared C++ settings for shell scripts that invoke Shardok tooling outside Bazel.
SHARDOK_CXX_STANDARD="c++23"
SHARDOK_CXX_STANDARD_ARG="-std=${SHARDOK_CXX_STANDARD}"
+1
View File
@@ -1,6 +1,7 @@
#!/bin/zsh
/opt/homebrew/bin/s3cmd sync \
--acl-public \
--exclude="*dream_log.txt" \
--exclude="*.DS_Store" \
--delete-removed \
@@ -28,7 +28,7 @@ static inline auto HashBuffer(const uint8_t* data, size_t size) -> uint64_t {
// Process 8 bytes at a time
while (data + 8 <= end) {
uint64_t word;
uint64_t word = 0;
// Use memcpy to avoid alignment issues and let compiler optimize
__builtin_memcpy(&word, data, sizeof(word));
hash ^= word;
@@ -14,6 +14,7 @@
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
#include "tools/cpp/runfiles/runfiles.h"
@@ -37,7 +38,7 @@ auto rloc(const string& execPath) -> string {
const std::unique_ptr<Runfiles> runfiles(Runfiles::Create(execPath, &error));
if (runfiles == nullptr) {
fprintf(stderr, "Error! %s\n", error.c_str());
std::cerr << "Error! " << error << '\n';
abort();
// error handling
}
@@ -78,9 +79,9 @@ auto FilesystemUtils::MapFilesDirectory() -> string {
void FilesystemUtils::MakeDirectoryIfNecessary(const string& directoryPath) {
if (fs::create_directories(directoryPath))
fprintf(stderr, "Directory %s created\n", directoryPath.c_str());
std::cerr << "Directory " << directoryPath << " created\n";
else
fprintf(stderr, "No new directory created for %s\n", directoryPath.c_str());
std::cerr << "No new directory created for " << directoryPath << '\n';
}
auto FilesystemUtils::SaveFilesDirectory() -> string {
@@ -140,11 +141,11 @@ auto FilesystemUtils::AtomicallySaveToPath(const string& path, const byte_vector
if (ostr.good()) {
const int err = rename(tempPath.c_str(), path.c_str());
if (err == -1) {
fprintf(stderr, "Failed to move file to %s! Errno %d\n", path.c_str(), errno);
std::cerr << "Failed to move file to " << path << "! Errno " << errno << '\n';
return false;
}
} else {
fprintf(stderr, "Failed writing to %s!\n", tempPath.c_str());
std::cerr << "Failed writing to " << tempPath << "!\n";
return false;
}
@@ -6,8 +6,8 @@
// Copyright © 2017 none. All rights reserved.
//
#ifndef FilesystemUtils_hpp
#define FilesystemUtils_hpp
#ifndef EAGLE0_COMMON_FILESYSTEM_UTILS_HPP
#define EAGLE0_COMMON_FILESYSTEM_UTILS_HPP
#include <string>
#include <vector>
@@ -36,4 +36,4 @@ public:
static auto LoadFromPath(const string& path) -> byte_vector;
};
#endif /* FilesystemUtils_hpp */
#endif // EAGLE0_COMMON_FILESYSTEM_UTILS_HPP
@@ -9,6 +9,7 @@
#include <bit>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#define ITERABLE_BITSET_INDEX_CHECKS false
@@ -104,9 +105,11 @@ public:
class Iter {
public:
using iterator_category = std::forward_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = size_t;
using pointer = size_t*;
using reference = size_t&;
using pointer = void;
using reference = size_t;
private:
size_t storeIndex;
@@ -142,13 +145,20 @@ public:
auto operator!=(const Iter& rhs) const -> bool {
return storeIndex != rhs.storeIndex || bitIndex != rhs.bitIndex;
}
void operator++() {
auto operator++() -> Iter& {
bitIndex++;
if (bitIndex >= STORE_BITS) {
storeIndex++;
bitIndex = 0;
}
ToNext();
return *this;
}
auto operator++(int) -> Iter {
Iter previous = *this;
++(*this);
return previous;
}
auto operator*() const -> size_t { return (storeIndex << BIT_SHIFT) + bitIndex; }
+3 -3
View File
@@ -6,8 +6,8 @@
// Copyright © 2018 none. All rights reserved.
//
#ifndef MapUtils_hpp
#define MapUtils_hpp
#ifndef EAGLE0_COMMON_MAP_UTILS_HPP
#define EAGLE0_COMMON_MAP_UTILS_HPP
#include <string>
#include <unordered_map>
@@ -19,4 +19,4 @@ auto IntForKey(const std::unordered_map<std::string, std::string>& map, const st
auto BoolForKey(const std::unordered_map<std::string, std::string>& map, const std::string& key)
-> bool;
#endif /* MapUtils_hpp */
#endif // EAGLE0_COMMON_MAP_UTILS_HPP
@@ -6,8 +6,8 @@
// Copyright © 2019 none. All rights reserved.
//
#ifndef ProtobufWarningSuppression_h
#define ProtobufWarningSuppression_h
#ifndef EAGLE0_COMMON_PROTOBUF_WARNING_SUPPRESSION_HPP
#define EAGLE0_COMMON_PROTOBUF_WARNING_SUPPRESSION_HPP
#define SUPPRESS_PROTOBUF_WARNINGS \
_Pragma("GCC diagnostic ignored \"-Wpragmas\"") \
@@ -19,4 +19,4 @@
"GCC diagnostic ignored \"-Wshorten-64-to-32\"") \
_Pragma("GCC diagnostic ignored \"-Wnested-anon-types\"")
#endif /* ProtobufWarningSuppression_h */
#endif // EAGLE0_COMMON_PROTOBUF_WARNING_SUPPRESSION_HPP
@@ -8,7 +8,9 @@
#include "RandomGenerator.hpp"
#include <algorithm>
#include <ctime>
#include <stdexcept>
constexpr double kOpenEndedHighThreshold = 95.0;
constexpr double kOpenEndedLowThreshold = 5.0;
@@ -67,7 +69,8 @@ auto RandomGenerator::ReverseOpenEndedPercentile() -> double {
}
auto RandomGenerator::IntBetween(const int min, const int max) -> int {
return int(DoubleZeroToOne() * (max - min)) + min;
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
return std::min(int(DoubleZeroToOne() * (max - min)) + min, max - 1);
}
auto RandomGenerator::IntBelow(const int max) -> int { return IntBetween(0, max); }
@@ -94,6 +97,7 @@ StdLibraryGenerator::StdLibraryGenerator(const std::mt19937_64::result_type seed
}
auto StdLibraryGenerator::IntBetween(const int min, const int max) -> int {
if (min >= max) { throw std::invalid_argument("random integer range must be non-empty"); }
std::uniform_int_distribution<int> unifInt(min, max - 1);
return unifInt(engine);
}
@@ -6,11 +6,14 @@
// Copyright (c) 2015 none. All rights reserved.
//
#ifndef __eagle0__ShardokRandomGenerator__
#define __eagle0__ShardokRandomGenerator__
#ifndef EAGLE0_COMMON_RANDOM_GENERATOR_HPP
#define EAGLE0_COMMON_RANDOM_GENERATOR_HPP
#include <cstddef>
#include <memory>
#include <random>
#include <stdexcept>
#include <vector>
class RandomGenerator;
@@ -22,6 +25,10 @@ private:
public:
RandomGenerator() = default;
RandomGenerator(const RandomGenerator&) = default;
auto operator=(const RandomGenerator&) -> RandomGenerator& = default;
RandomGenerator(RandomGenerator&&) = default;
auto operator=(RandomGenerator&&) -> RandomGenerator& = default;
virtual ~RandomGenerator() = default;
@@ -35,7 +42,8 @@ public:
template<class T>
auto RandomElement(const std::vector<T>& vec) -> T {
return vec[IntBelow((int)vec.size())];
if (vec.empty()) { throw std::invalid_argument("cannot choose from an empty vector"); }
return vec.at(static_cast<std::size_t>(IntBelow(static_cast<int>(vec.size()))));
}
static auto ChanceOpenEndedPercentileAtOrAbove(double value) -> double;
@@ -56,4 +64,4 @@ public:
auto IntBetween(int min, int max) -> int override; // inclusive, exclusive (min<=return<max)
};
#endif /* defined(__eagle0__ShardokRandomGenerator__) */
#endif // EAGLE0_COMMON_RANDOM_GENERATOR_HPP
@@ -6,10 +6,13 @@
// Copyright © 2018 none. All rights reserved.
//
#ifndef SequenceRandomGenerator_h
#define SequenceRandomGenerator_h
#ifndef EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
#define EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
#include <cmath>
#include <memory>
#include <stdexcept>
#include <utility>
#include <vector>
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
@@ -24,22 +27,32 @@
// which produces: initial=2 (triggers open-ended), accumulated=52, final=2-52=-50
class SequenceRandomGenerator : public ::RandomGenerator {
private:
const std::vector<double> sequence;
std::vector<double> sequence;
size_t position = 0;
auto DoubleZeroToOne() -> double override {
const double nextVal = sequence[position];
position++;
if (position >= sequence.size()) { position = 0; }
const double nextVal = sequence.at(position);
position++;
return nextVal;
}
public:
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)), position(0) {}
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {
if (sequence.empty()) {
throw std::invalid_argument("sequence random generator requires at least one value");
}
for (const double value : sequence) {
if (!std::isfinite(value) || value < 0.0 || value > 1.0) {
throw std::invalid_argument(
"sequence random generator values must be between 0 and 1");
}
}
}
static auto WithSequence(const std::vector<double>& s)
-> std::shared_ptr<SequenceRandomGenerator> {
return std::make_shared<SequenceRandomGenerator>(s);
}
};
#endif /* SequenceRandomGenerator_h */
#endif // EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
@@ -80,7 +80,7 @@ void EmitShardokLatencyTrace(
out << "}";
std::scoped_lock lock(LogMutex());
std::cerr << out.str() << std::endl;
std::cerr << out.str() << '\n';
}
ScopedShardokLatencyTrace::ScopedShardokLatencyTrace(
@@ -10,6 +10,7 @@
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
@@ -171,7 +172,7 @@ public:
// Get detailed queue information for debugging
void debug_queue_state() const {
std::unique_lock<std::mutex> lock(const_cast<std::mutex&>(queue_mutex));
printf("ThreadPool: Queue size: %zu\n", tasks.size());
std::cout << "ThreadPool: Queue size: " << tasks.size() << '\n';
if (!tasks.empty()) {
// Create a copy to inspect priorities without modifying queue
auto queue_copy = tasks;
@@ -180,9 +181,9 @@ public:
priorities.push_back(queue_copy.top().priority);
queue_copy.pop();
}
printf("ThreadPool: Priorities in queue: ");
for (int p : priorities) { printf("%d ", p); }
printf("\n");
std::cout << "ThreadPool: Priorities in queue: ";
for (int p : priorities) { std::cout << p << ' '; }
std::cout << '\n';
}
}
@@ -197,4 +198,4 @@ public:
} // namespace eagle0::common
#endif // EAGLE0_THREADPOOL_HPP
#endif // EAGLE0_THREADPOOL_HPP
+4 -4
View File
@@ -6,14 +6,14 @@
// Copyright © 2018 none. All rights reserved.
//
#ifndef TsvParser_hpp
#define TsvParser_hpp
#ifndef EAGLE0_COMMON_TSV_PARSER_HPP
#define EAGLE0_COMMON_TSV_PARSER_HPP
#include <string>
#include <unordered_map>
#include <vector>
typedef std::unordered_map<std::string, std::string> StringMap;
using StringMap = std::unordered_map<std::string, std::string>;
// Parses a TSV, with the first column becoming map keys and subsequent columns becoming values
// for different maps in the returned vector.
@@ -23,4 +23,4 @@ public:
std::vector<StringMap> ParseColumnEntryTsv(std::string tsv);
};
#endif /* TsvParser_hpp */
#endif // EAGLE0_COMMON_TSV_PARSER_HPP
@@ -9,6 +9,7 @@ namespace shardok {
using Coords = net::eagle0::shardok::storage::fb::Coords;
constexpr double kDefaultMorale = 50.0;
constexpr float kExplicitMoraleSentinel = -1.0f;
auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) -> Battalion {
Battalion shardokBattalion{};
@@ -18,6 +19,10 @@ auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) ->
shardokBattalion.mutate_type(
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(battalion.type()));
shardokBattalion.mutate_morale(kDefaultMorale);
if (battalion.has_morale()) {
shardokBattalion.mutate_morale(static_cast<float>(battalion.morale()));
shardokBattalion.mutate_base_morale(kExplicitMoraleSentinel);
}
shardokBattalion.mutate_armament(static_cast<float>(battalion.armament()));
shardokBattalion.mutate_training(static_cast<float>(battalion.training()));
@@ -125,4 +130,4 @@ auto ConvertUnit(
return shardokUnit;
}
} // namespace shardok
} // namespace shardok
+93 -62
View File
@@ -6,21 +6,27 @@
// Copyright © 2017 none. All rights reserved.
//
#ifndef byte_vector_h
#define byte_vector_h
#ifndef EAGLE0_COMMON_BYTE_VECTOR_HPP
#define EAGLE0_COMMON_BYTE_VECTOR_HPP
#include <charconv>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <sstream>
#include <initializer_list>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
using std::string;
using std::vector;
class byte_vector : public vector<uint8_t> {
class byte_vector {
private:
std::vector<uint8_t> bytes_;
public:
class HexException : public std::exception {
const std::string description;
@@ -35,60 +41,78 @@ public:
};
private:
static auto HexNibbleToValue(const char nibble) -> uint8_t {
if (nibble >= '0' && nibble <= '9') {
return uint8_t(nibble - '0');
} else if (nibble >= 'A' && nibble <= 'F') {
return uint8_t(nibble - 'A' + 10);
} else if (nibble >= 'a' && nibble <= 'f') {
return uint8_t(nibble - 'a' + 10);
static auto HexCharsToByte(const std::string_view digits) -> uint8_t {
unsigned int parsedByte = 0;
const auto [ptr, ec] =
std::from_chars(digits.data(), digits.data() + digits.size(), parsedByte, 16);
if (ec != std::errc() || ptr != digits.data() + digits.size()) {
throw HexException(ptr == digits.data() + digits.size() ? digits.back() : *ptr);
}
throw HexException(nibble);
}
static auto HexCharsToByte(std::istringstream& str) -> uint8_t {
uint8_t b = 0;
char c = 0;
str >> c;
b |= (HexNibbleToValue(c) << 4u);
str >> c;
b |= HexNibbleToValue(c);
return b;
return static_cast<uint8_t>(parsedByte);
}
public:
byte_vector() = default;
byte_vector(const std::initializer_list<uint8_t> list) : vector<uint8_t>(list) {}
byte_vector(const std::initializer_list<uint8_t> list) : bytes_(list) {}
template<class InputIt>
byte_vector(InputIt begin, InputIt end) : vector<uint8_t>(begin, end) {}
byte_vector(InputIt begin, InputIt end) : bytes_(begin, end) {}
byte_vector(const byte_vector& rhs) = default;
explicit byte_vector(const std::string& s) : byte_vector(s.begin(), s.end()) {}
explicit byte_vector(const size_t size) : vector<uint8_t>(size) {}
explicit byte_vector(void* data, const size_t size) : vector<uint8_t>(size) {
this->assign((uint8_t*)data, (uint8_t*)data + size);
byte_vector(byte_vector&& rhs) noexcept = default;
explicit byte_vector(const std::string& s) : bytes_(s.begin(), s.end()) {}
explicit byte_vector(std::vector<uint8_t> bytes) : bytes_(std::move(bytes)) {}
explicit byte_vector(const size_t size) : bytes_(size) {}
explicit byte_vector(const void* data, const size_t size) {
auto* bytes = static_cast<const uint8_t*>(data);
bytes_.assign(bytes, bytes + size);
}
auto operator==(const vector<uint8_t>& vec) -> bool {
if (this->size() != vec.size()) return false;
for (size_t i = 0; i < size(); i++) {
if ((*this)[i] != vec[i]) return false;
}
return true;
auto operator=(const byte_vector& rhs) -> byte_vector& = default;
auto operator=(byte_vector&& rhs) noexcept -> byte_vector& = default;
[[nodiscard]] auto operator==(const std::vector<uint8_t>& vec) const -> bool {
return bytes_ == vec;
}
[[nodiscard]] auto operator==(const byte_vector& vec) const -> bool {
return bytes_ == vec.bytes_;
}
[[nodiscard]] auto data() -> uint8_t* { return bytes_.data(); }
[[nodiscard]] auto data() const -> const uint8_t* { return bytes_.data(); }
[[nodiscard]] auto size() const -> size_t { return bytes_.size(); }
[[nodiscard]] auto empty() const -> bool { return bytes_.empty(); }
void reserve(const size_t count) { bytes_.reserve(count); }
void resize(const size_t count) { bytes_.resize(count); }
void swap(byte_vector& other) noexcept { bytes_.swap(other.bytes_); }
[[nodiscard]] auto begin() -> std::vector<uint8_t>::iterator { return bytes_.begin(); }
[[nodiscard]] auto begin() const -> std::vector<uint8_t>::const_iterator {
return bytes_.begin();
}
[[nodiscard]] auto end() -> std::vector<uint8_t>::iterator { return bytes_.end(); }
[[nodiscard]] auto end() const -> std::vector<uint8_t>::const_iterator { return bytes_.end(); }
[[nodiscard]] auto cbegin() const -> std::vector<uint8_t>::const_iterator {
return bytes_.cbegin();
}
[[nodiscard]] auto cend() const -> std::vector<uint8_t>::const_iterator {
return bytes_.cend();
}
[[nodiscard]] auto operator[](const size_t index) -> uint8_t& { return bytes_[index]; }
[[nodiscard]] auto operator[](const size_t index) const -> uint8_t { return bytes_[index]; }
auto append(const char* bytes, const size_t length) {
const auto previousSize = this->size();
this->resize(previousSize + length);
std::memcpy(this->data() + previousSize, bytes, length);
const auto previousSize = size();
resize(previousSize + length);
std::memcpy(data() + previousSize, bytes, length);
}
template<class T>
auto append(const T* elts, const size_t count) {
append((char*)elts, count * sizeof(T));
append(reinterpret_cast<const char*>(elts), count * sizeof(T));
}
template<class T>
auto append(const vector<T> elts) {
auto append(const vector<T>& elts) {
append(elts.data(), elts.size());
}
@@ -98,7 +122,7 @@ public:
}
auto operator+=(const uint8_t c) -> byte_vector& {
this->insert(end(), c);
bytes_.push_back(c);
return *this;
}
@@ -111,7 +135,7 @@ public:
template<class Iterable>
auto operator+=(const Iterable& rhs) -> byte_vector& {
this->insert(end(), rhs.cbegin(), rhs.cend());
bytes_.insert(end(), rhs.cbegin(), rhs.cend());
return *this;
}
@@ -119,7 +143,7 @@ public:
if (size() < 8) {
uint64_t total = 0;
for (size_t i = 0; i < size(); i++) {
total <<= 8u;
total <<= 8U;
total += (*this)[i];
}
return total;
@@ -128,9 +152,9 @@ public:
uint64_t firstBytes = 0;
uint64_t lastBytes = 0;
for (unsigned i = 0; i < 8; i++) {
firstBytes <<= 8u;
firstBytes <<= 8U;
firstBytes += (*this)[i];
lastBytes <<= 8u;
lastBytes <<= 8U;
lastBytes += (*this)[i];
}
@@ -141,8 +165,8 @@ public:
std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
const std::streamsize size = inputFileStream.tellg();
inputFileStream.seekg(0, std::ios::beg);
this->resize((size_t(size)));
inputFileStream.read((char*)this->data(), size);
this->resize(static_cast<size_t>(size));
inputFileStream.read(reinterpret_cast<char*>(this->data()), size);
return *this;
}
@@ -153,35 +177,42 @@ public:
}
const std::streamsize size = inputFileStream.tellg();
inputFileStream.seekg(0, std::ios::beg);
byte_vector serializedGame((size_t(size)));
inputFileStream.read((char*)serializedGame.data(), size);
byte_vector serializedGame(static_cast<size_t>(size));
inputFileStream.read(reinterpret_cast<char*>(serializedGame.data()), size);
return serializedGame;
}
static auto FromHex(const char* hexString) -> byte_vector { return FromHex(string(hexString)); }
static auto FromHex(string hexString) -> byte_vector {
if (hexString[0] == '0' && (hexString[1] == 'x' || hexString[1] == 'X')) {
hexString = string(hexString.begin() + 1, hexString.end());
static auto FromHex(const string& hexString) -> byte_vector {
std::string_view digits(hexString);
if (digits.starts_with("0x") || digits.starts_with("0X")) {
digits.remove_prefix(2);
} else if (digits.starts_with("x") || digits.starts_with("X")) {
digits.remove_prefix(1);
}
if (hexString[0] == 'x' || hexString[0] == 'X') {
hexString = string(hexString.begin() + 1, hexString.end());
}
byte_vector vec;
const size_t len = hexString.size();
vec.reserve(len / 2);
std::istringstream hexStream(hexString);
while (size_t(hexStream.tellg()) < len) { vec += HexCharsToByte(hexStream); }
if (digits.size() % 2 != 0) { throw HexException(digits.empty() ? '\0' : digits.back()); }
byte_vector vec;
vec.reserve(digits.size() / 2);
for (size_t index = 0; index < digits.size(); index += 2) {
vec += HexCharsToByte(digits.substr(index, 2));
}
return vec;
}
explicit operator std::string() const { return std::string(begin(), end()); }
};
inline auto operator<<(std::ostream& ostr, byte_vector vec) -> std::ostream& {
ostr.write((char*)vec.data(), (std::streamsize)vec.size());
inline auto operator==(const std::vector<uint8_t>& lhs, const byte_vector& rhs) -> bool {
return rhs == lhs;
}
inline auto operator<<(std::ostream& ostr, const byte_vector& vec) -> std::ostream& {
ostr.write(reinterpret_cast<const char*>(vec.data()), static_cast<std::streamsize>(vec.size()));
return ostr;
}
#endif /* byte_vector_h */
#endif // EAGLE0_COMMON_BYTE_VECTOR_HPP
@@ -9,9 +9,11 @@
#include <fstream>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <mutex>
#include <random>
#include <sstream>
#include <stdexcept>
#include <thread>
@@ -207,8 +209,8 @@ auto AbstractMCTSAI::BuildMCTSTree(
}));
}
// Wait for all threads to complete
for (auto& future : futures) { future.wait(); }
// Wait for all threads to complete and propagate worker exceptions.
for (auto& future : futures) { future.get(); }
} else {
// Single-threaded MCTS
while (std::chrono::steady_clock::now() < deadline) {
@@ -862,12 +864,12 @@ auto AbstractMCTSAI::LogSearchResults(
// Log selected command
const std::string selectedDesc =
bestChild->action ? bestChild->action->getDescription() : "Unknown";
printf("MCTS: Selected %s (visit:%d, reward:%.2f, lookahead:%.2f) from %zu options\n",
selectedDesc.c_str(),
bestChild->visitCount,
bestChild->averageReward,
bestChild->lookaheadScore,
rootNode->children.size());
std::ostringstream selectedLine;
selectedLine << std::fixed << std::setprecision(2) << "MCTS: Selected " << selectedDesc
<< " (visit:" << bestChild->visitCount << ", reward:" << bestChild->averageReward
<< ", lookahead:" << bestChild->lookaheadScore << ") from "
<< rootNode->children.size() << " options";
std::cout << selectedLine.str() << '\n';
// Log top 3 actions by visits
std::vector<const MCTSNode*> sortedChildren;
@@ -879,20 +881,17 @@ auto AbstractMCTSAI::LogSearchResults(
// Show all actions if there are <= 10, otherwise top 5
const size_t numToShow = sortedChildren.size() <= 10 ? sortedChildren.size() : 5;
printf("MCTS: Top %zu actions by visits (out of %zu total):\n",
numToShow,
sortedChildren.size());
std::cout << "MCTS: Top " << numToShow << " actions by visits (out of " << sortedChildren.size()
<< " total):\n";
for (size_t i = 0; i < numToShow; ++i) {
const auto* child = sortedChildren[i];
printf(" [%zu] visits:%d avgReward:%.2f immediate:%.2f lookahead:%.2f",
i,
child->visitCount,
child->averageReward,
child->immediateScore,
child->lookaheadScore);
std::ostringstream line;
line << std::fixed << std::setprecision(2) << " [" << i << "] visits:" << child->visitCount
<< " avgReward:" << child->averageReward << " immediate:" << child->immediateScore
<< " lookahead:" << child->lookaheadScore;
// Show the action's own description
if (child->action) { printf(" %s", child->action->getDescription().c_str()); }
if (child->action) { line << ' ' << child->action->getDescription(); }
// Show sequence preview
if (!child->children.empty()) {
@@ -905,16 +904,15 @@ auto AbstractMCTSAI::LogSearchResults(
}
}
if (bestNext && bestNext->action) {
printf(" -> %s", bestNext->action->getDescription().c_str());
line << " -> " << bestNext->action->getDescription();
}
}
printf("\n");
std::cout << line.str() << '\n';
}
printf("MCTS: Tree stats - max depth:%d, total nodes:%d, root visits:%d\n",
result.searchDepth,
result.nodesEvaluated,
rootNode->visitCount);
std::cout << "MCTS: Tree stats - max depth:" << result.searchDepth
<< ", total nodes:" << result.nodesEvaluated
<< ", root visits:" << rootNode->visitCount << '\n';
// Log best sequence from chosen action (following most-visited path)
std::vector<const MCTSNode*> bestSequence;
@@ -940,7 +938,10 @@ auto AbstractMCTSAI::LogSearchResults(
}
if (!bestSequence.empty()) {
printf("MCTS: Best sequence from chosen action (final: %.2f):\n", sequenceScore);
std::ostringstream sequenceLine;
sequenceLine << std::fixed << std::setprecision(2)
<< "MCTS: Best sequence from chosen action (final: " << sequenceScore << "):";
std::cout << sequenceLine.str() << '\n';
int displayedStep = 0;
for (size_t i = 0; i < bestSequence.size(); ++i) {
const auto* node = bestSequence[i];
@@ -949,29 +950,31 @@ auto AbstractMCTSAI::LogSearchResults(
if (i > 0 && node->parent && node->parent->IsChanceNode()) { continue; }
displayedStep++;
printf(" %d.", displayedStep);
if (node->action) { printf(" %s", node->action->getDescription().c_str()); }
std::ostringstream line;
line << " " << displayedStep << ".";
if (node->action) { line << ' ' << node->action->getDescription(); }
// If this is a chance node, display outcome probabilities and scores
if (node->IsChanceNode() && !node->outcomeProbabilities.empty()) {
printf(" [");
line << " [";
for (size_t j = 0; j < node->outcomeProbabilities.size(); ++j) {
if (j > 0) printf(", ");
if (j > 0) { line << ", "; }
const double prob = node->outcomeProbabilities[j] * 100;
// Show lookahead score for each outcome if child exists
if (j < node->children.size() && node->children[j]->visitCount > 0) {
printf("%.0f%%->%.1f", prob, node->children[j]->lookaheadScore);
line << std::fixed << std::setprecision(0) << prob << "%->"
<< std::setprecision(1) << node->children[j]->lookaheadScore;
} else {
printf("%.0f%%->?", prob);
line << std::fixed << std::setprecision(0) << prob << "%->?";
}
}
printf("]");
line << "]";
}
printf(" (visits:%d, immediate:%.2f, lookahead:%.2f)\n",
node->visitCount,
node->immediateScore,
node->lookaheadScore);
line << std::fixed << std::setprecision(2) << " (visits:" << node->visitCount
<< ", immediate:" << node->immediateScore << ", lookahead:" << node->lookaheadScore
<< ")";
std::cout << line.str() << '\n';
// For non-root nodes in the sequence, show what the top alternatives were
// Skip showing alternatives for chance nodes (they have outcome children, not action
@@ -991,17 +994,17 @@ auto AbstractMCTSAI::LogSearchResults(
});
// Show top 3 alternatives at this decision point
printf(" Alternatives at this node (%zu total):\n", siblings.size());
std::cout << " Alternatives at this node (" << siblings.size() << " total):\n";
const size_t topN = std::min(siblings.size(), size_t(3));
for (size_t j = 0; j < topN; ++j) {
const auto* alt = siblings[j];
printf(" [%zu] visits:%d immediate:%.2f lookahead:%.2f",
j,
alt->visitCount,
alt->immediateScore,
alt->lookaheadScore);
if (alt->action) { printf(" %s", alt->action->getDescription().c_str()); }
printf("\n");
std::ostringstream altLine;
altLine << std::fixed << std::setprecision(2) << " [" << j
<< "] visits:" << alt->visitCount
<< " immediate:" << alt->immediateScore
<< " lookahead:" << alt->lookaheadScore;
if (alt->action) { altLine << ' ' << alt->action->getDescription(); }
std::cout << altLine.str() << '\n';
}
}
}
@@ -1013,7 +1016,7 @@ auto AbstractMCTSAI::DumpTreeToFile(const MCTSNode* root, const std::string& fil
std::ofstream out(filepath);
if (!out) {
fprintf(stderr, "Failed to open dump file: %s\n", filepath.c_str());
std::cerr << "Failed to open dump file: " << filepath << '\n';
return;
}
@@ -1042,7 +1045,7 @@ auto AbstractMCTSAI::DumpTreeToFile(const MCTSNode* root, const std::string& fil
out << "\n=== End of Tree Dump ===\n";
out.close();
printf("MCTS: Tree dumped to %s\n", filepath.c_str());
std::cout << "MCTS: Tree dumped to " << filepath << '\n';
}
auto AbstractMCTSAI::DumpNodeRecursive(
@@ -141,9 +141,8 @@ public:
// Default implementation: no filtering, so filtered index = original index
[[nodiscard]] virtual size_t mapFilteredIndexToOriginal(
size_t filteredIndex,
const MCTSGameState& state) const {
const MCTSGameState& /*state*/) const {
// Default: no filtering, index stays the same
(void)state; // Suppress unused parameter warning
return filteredIndex;
}
@@ -158,4 +157,4 @@ public:
} // namespace mcts
} // namespace shardok
#endif // EAGLE0_MCTS_GAME_ENGINE_HPP
#endif // EAGLE0_MCTS_GAME_ENGINE_HPP
@@ -6,8 +6,11 @@
#define EAGLE0_ABSTRACT_MCTSNODE_HPP
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>
#include <memory>
#include <sstream>
#include <vector>
#include "MCTSAction.hpp"
@@ -205,13 +208,13 @@ struct MCTSNode {
isMaximizingPlayer ? child->lookaheadScore : -child->lookaheadScore;
const double explorationValue =
explorationConstant * std::sqrt(std::log(visitCount) / child->visitCount);
printf(" UCB: %s lookahead=%.2f expl=%.2f (+%.2f) = %.2f [%s]\n",
isMaximizingPlayer ? "MAX" : "MIN",
child->lookaheadScore,
exploitationValue,
explorationValue,
value,
child->action ? child->action->getDescription().c_str() : "root");
std::ostringstream line;
line << std::fixed << std::setprecision(2)
<< " UCB: " << (isMaximizingPlayer ? "MAX" : "MIN")
<< " lookahead=" << child->lookaheadScore << " expl=" << exploitationValue
<< " (+" << explorationValue << ") = " << value << " ["
<< (child->action ? child->action->getDescription() : "root") << "]";
std::cout << line.str() << '\n';
}
if (value > bestValue) {
@@ -28,36 +28,6 @@ cc_binary(
deps = [":shardok-server-lib"],
)
cc_binary(
name = "shardok-inspector",
srcs = ["shardok_inspector.cpp"],
copts = COPTS,
data = [
"//src/main/resources/net/eagle0/shardok:battalion_types",
"//src/main/resources/net/eagle0/shardok:settings",
"//src/main/resources/net/eagle0/shardok/maps",
],
visibility = ["//visibility:public"],
deps = [
"//src/main/cpp/net/eagle0/common:byte_vector",
"//src/main/cpp/net/eagle0/common:filesystem_utils",
"//src/main/cpp/net/eagle0/common:tsv_parser",
"//src/main/cpp/net/eagle0/shardok/library:engine",
],
)
cc_binary(
name = "corrupt-game-checker",
srcs = ["CorruptGameChecker.cpp"],
copts = COPTS,
deps = [
"//src/main/cpp/net/eagle0/common:byte_vector",
"//src/main/cpp/net/eagle0/common:filesystem_utils",
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
],
)
cc_binary(
name = "hex-map-migrator",
srcs = ["HexMapMigrator.cpp"],
@@ -1,46 +0,0 @@
//
// Created by Dan Crosby on 1/9/22.
//
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/protobuf/net/eagle0/common/shardok_internal_interface.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/storage/game.pb.h"
auto main(int argc, char** argv) -> int {
char* path = argv[1];
printf("Checking file %s\n", path);
auto bytes = byte_vector::FromPath(path);
net::eagle0::shardok::storage::Game game;
game.ParseFromArray(bytes.data(), bytes.size());
printf("The game ID is %s\n", game.game_id().c_str());
printf("The size is %lu\n", bytes.size());
const int arCount = game.action_result_size();
printf("There are %d results\n", arCount);
for (int arIndex = 0; arIndex < arCount; arIndex++) {
shardok::GameStateW gameState =
shardok::GameStateW::FromByteString(game.action_result(arIndex).state_after_fb());
const auto* hexMap = gameState->hex_map();
for (int terrainIndex = 0; terrainIndex < hexMap->terrain()->size(); terrainIndex++) {
const auto* terrain = hexMap->terrain()->Get(terrainIndex);
if (terrain->type() == net::eagle0::shardok::storage::fb::Terrain_::Type_STILL_WATER)
continue;
if (terrain->type() == net::eagle0::shardok::storage::fb::Terrain_::Type_RIVER)
continue;
if (terrain->modifier().bridge().present()) {
printf("Caught one! arIndex %d, terrainIndex %d\n", arIndex, terrainIndex);
printf("Action result was %s\n",
game.action_result(arIndex).action_result().DebugString().c_str());
exit(0);
}
}
}
}
@@ -2,7 +2,7 @@
// Created by Dan Crosby on 3/19/20.
//
#include <cstdio>
#include <iostream>
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
@@ -14,7 +14,7 @@ void ModifyMap(HexMapProto& map);
int main(const int argc, char** argv) {
if (argc < 3) {
printf("Usage: %s input_path output_path\n", argv[0]);
std::cerr << "Usage: " << argv[0] << " input_path output_path\n";
return -1;
}
@@ -22,21 +22,25 @@ int main(const int argc, char** argv) {
const std::string outputPath = argv[2];
const byte_vector mapProtoBytes = byte_vector::FromPath(inputPath);
net::eagle0::shardok::common::HexMap mapProto;
mapProto.ParseFromArray(mapProtoBytes.data(), (int)mapProtoBytes.size());
if (!mapProto.ParseFromArray(mapProtoBytes.data(), static_cast<int>(mapProtoBytes.size()))) {
std::cerr << "Failed to parse hex map from " << inputPath << '\n';
return -1;
}
ModifyMap(mapProto);
const std::string outputString = mapProto.SerializeAsString();
if (FilesystemUtils::AtomicallySaveToPath(outputPath, byte_vector(outputString))) return 0;
printf("Failed to save.\n");
std::cerr << "Failed to save.\n";
return -1;
}
void ModifyMap(HexMapProto& map) {
for (int i = 0; i < map.terrain_size(); i++) {
const int terrainCount = map.terrain_size();
for (int i = 0; i < terrainCount; ++i) {
const auto existingTerrainType = map.terrain(i).type();
const auto newTerrain =
(net::eagle0::shardok::common::Terrain_Type)(existingTerrainType + 2);
static_cast<net::eagle0::shardok::common::Terrain_Type>(existingTerrainType + 2);
map.mutable_terrain(i)->set_type(newTerrain);
}
}
@@ -4,50 +4,52 @@
#include "MapInfoCalculator.hpp"
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include "src/main/cpp/net/eagle0/shardok/ai/AIWaterCrossingCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
using namespace shardok;
using std::endl;
auto LoadMapFb(const string& mapName) -> Wrapper<HexMap> {
auto LoadMapFb(const std::string& mapName) -> shardok::Wrapper<shardok::HexMap> {
const HexMapProto mapProto = LoadMap(mapName);
flatbuffers::FlatBufferBuilder fbb{};
const auto hexMapOffset = fb::ConvertHexMapProto(fbb, mapProto);
const auto hexMapOffset = shardok::fb::ConvertHexMapProto(fbb, mapProto);
fbb.Finish(hexMapOffset);
return Wrapper<HexMap>(fbb);
return shardok::Wrapper<shardok::HexMap>(fbb);
}
auto CalculateMap(
const string& mapName,
const std::shared_ptr<ActionPointDistancesCache>& apdCache,
const BattalionTypeSPtr& battalionType) -> OneMapInfo {
const std::string& mapName,
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo {
auto hexMap = LoadMapFb(mapName);
const auto mapId = ActionPointDistancesCache::GetMapId(hexMap);
const auto mapId = shardok::ActionPointDistancesCache::GetMapId(hexMap);
const CoordsSet criticalTileLocations = GetCriticalTileLocations(hexMap);
const shardok::CoordsSet criticalTileLocations = shardok::GetCriticalTileLocations(hexMap);
OneMapInfo info{
.castleCount = criticalTileLocations.size(),
.name = mapName,
.positionsRequiringCrossing = {}};
for (unsigned int i = 0; i < hexMap->attacker_starting_positions()->size(); i++) {
for (std::size_t i = 0; i < hexMap->attacker_starting_positions()->size(); ++i) {
const auto* positionList = hexMap->attacker_starting_positions()->Get(i);
if (positionList->positions()->size() < 1) continue;
if (positionList->positions()->size() != 10) {
printf("Should have 10 starting positions!");
abort();
const auto* positions = positionList->positions();
if (positions->empty()) continue;
if (positions->size() != 10) {
std::cerr << "Should have 10 starting positions!" << '\n';
std::abort();
}
int positionsRequiringCrossing = 0;
for (const auto* startingPosition : *positionList->positions()) {
for (const Coords& castlePosition : criticalTileLocations) {
if (!CanReach(
for (const auto* startingPosition : *positions) {
for (const shardok::Coords& castlePosition : criticalTileLocations) {
if (!shardok::CanReach(
*startingPosition,
castlePosition,
hexMap,
@@ -56,11 +58,11 @@ auto CalculateMap(
battalionType)) {
++positionsRequiringCrossing;
break;
};
}
}
}
info.positionsRequiringCrossing[i] = positionsRequiringCrossing;
info.positionsRequiringCrossing[static_cast<int>(i)] = positionsRequiringCrossing;
}
return info;
}
}
@@ -12,19 +12,15 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
using namespace shardok;
using std::map;
using std::string;
struct OneMapInfo {
struct alignas(64) OneMapInfo {
uint32_t castleCount;
string name;
map<int, int> positionsRequiringCrossing;
} __attribute__((aligned(64)));
std::string name;
std::map<int, int> positionsRequiringCrossing;
};
auto CalculateMap(
const string& mapName,
const std::shared_ptr<ActionPointDistancesCache>& apdCache,
const BattalionTypeSPtr& battalionType) -> OneMapInfo;
[[nodiscard]] auto CalculateMap(
const std::string& mapName,
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo;
#endif // EAGLE0_MAPINFOCALCULATOR_HPP
@@ -2,37 +2,42 @@
// Created by Dan Crosby on 4/18/22.
//
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include "MapInfoCalculator.hpp"
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
using namespace shardok;
using std::endl;
auto main(const int argc, char** argv) -> int {
FilesystemUtils::SetExecPath(argv[0]);
std::ostream* outputStreamPtr = &std::cout;
std::ofstream outputFile{};
if (argc > 1) {
const string outputPath = argv[1];
outputStreamPtr = new std::ofstream(outputPath);
const std::string outputPath = argv[1];
outputFile.open(outputPath);
outputStreamPtr = &outputFile;
}
std::ostream& outputStream = *outputStreamPtr;
const auto gameSettings = std::make_shared<GameSettings>();
const auto gameSettings = std::make_shared<shardok::GameSettings>();
auto setter = gameSettings->GetSetter();
BattalionTypeRegistrar::RegisterBattalionTypes(setter);
const BattalionTypeSPtr& battalionType = gameSettings->GetGetter().GetBattalionType(
shardok::BattalionTypeRegistrar::RegisterBattalionTypes(setter);
const shardok::BattalionTypeSPtr& battalionType = gameSettings->GetGetter().GetBattalionType(
net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
vector<OneMapInfo> mapInfos{};
for (const string& mapName : GetMapNames()) {
std::shared_ptr<ActionPointDistancesCache> apdCache =
std::make_shared<ActionPointDistancesCache>();
const std::vector<std::string> mapNames = GetMapNames();
std::vector<OneMapInfo> mapInfos{};
mapInfos.reserve(mapNames.size());
for (const std::string& mapName : mapNames) {
std::shared_ptr<shardok::ActionPointDistancesCache> apdCache =
std::make_shared<shardok::ActionPointDistancesCache>();
mapInfos.push_back(CalculateMap(mapName, apdCache, battalionType));
}
@@ -41,29 +46,29 @@ auto main(const int argc, char** argv) -> int {
bool firstMap = true;
for (const OneMapInfo& mapInfo : mapInfos) {
if (firstMap) {
outputStream << endl;
outputStream << '\n';
firstMap = false;
} else {
outputStream << "," << endl;
outputStream << "," << '\n';
}
outputStream << " {" << endl;
outputStream << R"( "name": ")" << mapInfo.name << "\"," << endl;
outputStream << " {" << '\n';
outputStream << R"( "name": ")" << mapInfo.name << "\"," << '\n';
outputStream << " \"castleCount\": " << mapInfo.castleCount << "," << endl;
outputStream << " \"castleCount\": " << mapInfo.castleCount << "," << '\n';
outputStream << " \"positions\": {";
bool firstPosition = true;
for (const auto& [position, count] : mapInfo.positionsRequiringCrossing) {
if (firstPosition) {
outputStream << endl;
outputStream << '\n';
firstPosition = false;
} else {
outputStream << "," << endl;
outputStream << "," << '\n';
}
outputStream << " \"" << position << "\": " << count;
}
outputStream << endl << " }" << endl << " }";
outputStream << '\n' << " }" << '\n' << " }";
}
outputStream << endl << "]" << endl;
outputStream << '\n' << "]" << '\n';
}
@@ -2,10 +2,17 @@
// Created by Dan Crosby on 3/29/21.
//
#include <algorithm>
#include <array>
#include <climits>
#include <iostream>
#include <memory>
#include <numeric>
#include <mutex>
#include <random>
#include <stdexcept>
#include <thread>
#include <utility>
#include <vector>
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
@@ -14,8 +21,6 @@
#include "src/main/protobuf/net/eagle0/common/random_units.pb.h"
using net::eagle0::common::RandomUnits;
using std::shared_ptr;
using std::vector;
constexpr int THREAD_COUNT = 16;
@@ -28,98 +33,132 @@ std::random_device rd;
std::mt19937 g(rd());
#endif
vector<string> fileNames{};
std::mutex randomMutex;
std::vector<std::string> fileNames{};
using namespace shardok;
ShardokGamesManager* manager;
std::unique_ptr<shardok::ShardokGamesManager> manager;
auto randInt(const int lowInc, const int hiInc) {
std::lock_guard<std::mutex> lock(randomMutex);
return std::uniform_int_distribution(lowInc, hiInc)(g);
}
void runBattle(const vector<shared_ptr<const UnitProto>>& units) {
auto mapName = fileNames[randInt(0, (int)fileNames.size() - 1)];
auto month = randInt(1, 12);
void runBattle(const std::vector<shardok::Unit>& units) {
if (fileNames.empty()) { throw std::runtime_error("No map files found"); }
if (units.size() < 2) { throw std::runtime_error("Need at least two units to run a battle"); }
auto p1UnitCount = randInt(1, 20);
vector<UnitProto> p1Units{};
for (int i = 0; i < p1UnitCount; i++) {
auto unit = *units[i];
unit.set_player_id(0);
p1Units.push_back(unit);
const auto mapName = fileNames[randInt(0, static_cast<int>(fileNames.size()) - 1)];
const auto month = randInt(1, 12);
const auto maxP1UnitCount = std::min<size_t>(20, units.size() - 1);
const auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
std::vector<shardok::Unit> p1Units{};
p1Units.reserve(p1UnitCount);
for (int i = 0; i < p1UnitCount; ++i) {
auto unit = units[i];
unit.mutate_player_id(0);
p1Units.push_back(std::move(unit));
}
auto p2UnitCount = randInt(1, 20);
vector<UnitProto> p2Units{};
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; i++) {
auto unit = *units[i];
unit.set_player_id(1);
p2Units.push_back(unit);
const auto maxP2UnitCount = std::min<size_t>(20, units.size() - p1UnitCount);
const auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
std::vector<shardok::Unit> p2Units{};
p2Units.reserve(p2UnitCount);
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; ++i) {
auto unit = units[i];
unit.mutate_player_id(1);
p2Units.push_back(std::move(unit));
}
printf("map name is %s, %d attacking %d\n", mapName.c_str(), p1UnitCount, p2UnitCount);
std::cout << "map name is " << mapName << ", " << p1UnitCount << " attacking " << p2UnitCount
<< '\n';
vector<VictoryConditionProto> attVictoryConditions{
std::vector<VictoryConditionProto> attVictoryConditions{
VictoryConditionProto::VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING};
vector<VictoryConditionProto> defVictoryConditions{
std::vector<VictoryConditionProto> defVictoryConditions{
VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING,
VictoryConditionProto::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS};
auto p1 = PlayerInfoWithUnits(0, true, 5, false, 10000, attVictoryConditions, {}, p1Units);
auto p2 = PlayerInfoWithUnits(1, false, 7, false, 10000, defVictoryConditions, {}, p2Units);
std::vector<shardok::PlayerInfoWithUnits> playerInfos;
playerInfos.reserve(2);
playerInfos.emplace_back(
0,
true,
5,
false,
10000,
attVictoryConditions,
std::vector<shardok::PlayerId>{},
p1Units,
false);
playerInfos.emplace_back(
1,
true,
7,
true,
10000,
defVictoryConditions,
std::vector<shardok::PlayerId>{},
p2Units,
false);
string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
const std::string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
manager->LockedCreateSpecifiedGame(gameId, {p1, p2}, mapName, "", month, "");
manager->UnlockedCreateSpecifiedGame(
gameId,
playerInfos,
mapName,
"",
month,
TutorialBattleConfigProto{},
"");
}
auto LoadUnits() -> vector<shared_ptr<const UnitProto>> {
auto LoadUnits() -> std::vector<shardok::Unit> {
const auto bytes = byte_vector::FromPath("/tmp/random_heroes.bin");
RandomUnits commonUnits;
commonUnits.ParseFromArray(bytes.data(), (int)bytes.size());
printf("Loaded %d units\n", commonUnits.units_size());
if (!commonUnits.ParseFromArray(bytes.data(), static_cast<int>(bytes.size()))) {
throw std::runtime_error("Failed to parse /tmp/random_heroes.bin");
}
std::cout << "Loaded " << commonUnits.units_size() << " units\n";
vector<shared_ptr<const UnitProto>> units{};
std::vector<shardok::Unit> units{};
units.reserve(commonUnits.units_size());
for (const auto& commonUnit : commonUnits.units()) {
// FIXME: assumes 2 players
units.push_back(std::make_shared<const UnitProto>(ConvertUnit(commonUnit, 0, {0, 1})));
units.push_back(shardok::ConvertUnit(commonUnit, 0, {0, 1}));
}
std::shuffle(units.begin(), units.end(), g);
return units;
}
[[noreturn]] void runBattlesThread(const vector<shared_ptr<const UnitProto>>& units) {
[[noreturn]] void runBattlesThread(const std::vector<shardok::Unit>& units) {
while (true) { runBattle(units); }
}
auto main(int argc, char** argv) -> int {
auto main(int /*argc*/, char** argv) -> int {
#if FIXED_SEED
std::srand(4564564);
g.seed(378473);
#else
std::srand(std::time(nullptr));
#endif
FilesystemUtils::SetExecPath(argv[0]);
auto randomUnits = LoadUnits();
printf("Map file directory is %s\n", FilesystemUtils::MapFilesDirectory().c_str());
std::cout << "Map file directory is " << FilesystemUtils::MapFilesDirectory() << '\n';
fileNames = FilesystemUtils::FilesInDirectory(FilesystemUtils::MapFilesDirectory(), ".e0m");
for (auto& fileName : fileNames) { fileName.resize(fileName.size() - 4); }
manager = new ShardokGamesManager(nullptr, std::vector<std::string>());
manager = std::make_unique<shardok::ShardokGamesManager>(std::vector<std::string>());
std::thread threads[THREAD_COUNT];
std::array<std::thread, THREAD_COUNT> threads;
for (int i = 0; i < THREAD_COUNT; i++) {
for (int i = 0; i < THREAD_COUNT; ++i) {
threads[i] = std::thread(runBattlesThread, randomUnits);
}
@@ -16,9 +16,6 @@ namespace shardok {
constexpr double kOverpowerRatio = 2.0;
constexpr double kBraveWaterCostMultiplier = 1.2;
using std::pair;
using std::shared_ptr;
DIST_T NormalizedCostWhenBraving(const DIST_T cost) {
if (cost >= static_cast<double>(ActionPointDistances::IMPOSSIBLE) / kBraveWaterCostMultiplier)
return ActionPointDistances::IMPOSSIBLE;
@@ -28,25 +25,44 @@ DIST_T NormalizedCostWhenBraving(const DIST_T cost) {
struct TargetAndDistance {
Coords target;
size_t targetIndex;
CoordsSet attackLocations;
double targetPower;
DIST_T distance;
TargetAndDistance(const Coords& t, const CoordsSet& al, const double tp, const DIST_T d)
TargetAndDistance(
const Coords& t,
const size_t ti,
const CoordsSet& al,
const double tp,
const DIST_T d)
: target(t),
targetIndex(ti),
attackLocations(al),
targetPower(tp),
distance(d) {}
};
auto CoordsIndex(const Coords& coords, const int columnCount) {
return coords.row() * columnCount + coords.column();
}
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
return Coords(
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
}
auto MinDistance(
const Coords& start,
const CoordsSet& destinations,
const ActionPointDistances* apd) -> DIST_T {
DIST_T minDistance = ActionPointDistances::IMPOSSIBLE;
const int startIndex = CoordsIndex(start, destinations.ColumnCount());
for (const Coords& dest : destinations) {
if (const DIST_T distance = apd->Distance(start, dest); distance < minDistance) {
for (const auto destIndex : destinations.indexIterator()) {
if (const DIST_T distance = apd->Distance(startIndex, static_cast<int>(destIndex));
distance < minDistance) {
minDistance = distance;
}
}
@@ -116,10 +132,6 @@ auto EffectiveDistance(
auto Power(const Unit* unit) -> double { return unit->battalion().size(); }
auto CoordsIndex(const Coords& coords, const int columnCount) {
return coords.row() * columnCount + coords.column();
}
// Chooses a list of targets in priority order for each unit.
auto GenerateTargetPriorities(
const vector<const Unit*>& occupants,
@@ -156,6 +168,7 @@ auto GenerateTargetPriorities(
vector<TargetAndAttackLocations>{});
vector<TargetAndDistance> targetsWithDistance;
targetsWithDistance.reserve(targets.size());
// Get APDs directly from cache (now with built-in thread-local optimization)
const auto& battType = battalionTypeGetter(unit->battalion().type());
@@ -165,10 +178,10 @@ auto GenerateTargetPriorities(
bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
}
for (const Coords& targetLocation : targets) {
const auto coordsIndex = CoordsIndex(targetLocation, cc);
const auto& occupant = occupants[coordsIndex];
for (const auto targetIndex : targets.indexIterator()) {
const auto& occupant = occupants[targetIndex];
const Coords targetLocation = CoordsFromIndex(targetIndex, cc);
const auto& attackLocations = alCache->CachedLocations(targetLocation, isLateGame);
if (occupant && occupant->player_id() != attackerId) {
@@ -181,12 +194,14 @@ auto GenerateTargetPriorities(
targetsWithDistance.emplace_back(
targetLocation,
targetIndex,
attackLocsForUnit,
occupantPower,
distance);
} else {
targetsWithDistance.emplace_back(
targetLocation,
targetIndex,
attackLocations.LocationsWithEnemyInRange(unit),
occupantPower,
ActionPointDistances::IMPOSSIBLE);
@@ -197,13 +212,11 @@ auto GenerateTargetPriorities(
// Sort by distance
std::ranges::sort(
targetsWithDistance,
[&powerAttackingEachTarget,
cc](const TargetAndDistance& left, const TargetAndDistance& right) {
const auto leftIndex = CoordsIndex(left.target, cc);
const auto rightIndex = CoordsIndex(right.target, cc);
const double powerAttackingLeft = powerAttackingEachTarget[leftIndex];
const double powerAttackingRight = powerAttackingEachTarget[rightIndex];
[&powerAttackingEachTarget](
const TargetAndDistance& left,
const TargetAndDistance& right) {
const double powerAttackingLeft = powerAttackingEachTarget[left.targetIndex];
const double powerAttackingRight = powerAttackingEachTarget[right.targetIndex];
const bool leftAlreadyOverwhelmed =
powerAttackingLeft >= kOverpowerRatio * left.targetPower;
@@ -217,8 +230,7 @@ auto GenerateTargetPriorities(
});
if (!targetsWithDistance.empty()) {
powerAttackingEachTarget[CoordsIndex(targetsWithDistance.front().target, cc)] +=
Power(unit);
powerAttackingEachTarget[targetsWithDistance.front().targetIndex] += Power(unit);
}
tpl.priorityOrder.reserve(targetsWithDistance.size());
@@ -235,4 +247,4 @@ auto GenerateTargetPriorities(
return allTargetsUnitsAndDistances;
}
} // namespace shardok
} // namespace shardok
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 6/19/23.
//
#ifndef EAGLE0_AIATTACKGROUPS_HPP
#define EAGLE0_AIATTACKGROUPS_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_ATTACK_GROUPS_HPP
#define EAGLE0_SHARDOK_AI_AI_ATTACK_GROUPS_HPP
#include <functional>
#include <vector>
@@ -20,7 +20,6 @@ namespace shardok {
using GameState = net::eagle0::shardok::storage::fb::GameState;
using Unit = net::eagle0::shardok::storage::fb::Unit;
using net::eagle0::shardok::storage::fb::PlayerInfo;
using std::vector;
using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
@@ -28,19 +27,19 @@ struct TargetAndAttackLocations {
Coords target;
CoordsSet attackLocations;
auto operator==(const TargetAndAttackLocations& rhs) const -> bool = default;
[[nodiscard]] auto operator==(const TargetAndAttackLocations& rhs) const -> bool = default;
};
struct TargetPriorityList {
UnitId attackingUnitId;
vector<TargetAndAttackLocations> priorityOrder;
std::vector<TargetAndAttackLocations> priorityOrder;
TargetPriorityList(const UnitId uid, const vector<TargetAndAttackLocations>& po)
TargetPriorityList(const UnitId uid, const std::vector<TargetAndAttackLocations>& po)
: attackingUnitId(uid),
priorityOrder(po) {}
};
auto EffectiveDistance(
[[nodiscard]] auto EffectiveDistance(
const Unit* unit,
const HexMap* map,
const AttackLocations& attackLocations,
@@ -48,7 +47,7 @@ auto EffectiveDistance(
const BattalionTypeGetter& battalionTypeGetter,
ActionPoints braveWaterCost) -> DIST_T;
auto EffectiveDistance(
[[nodiscard]] auto EffectiveDistance(
const Unit* unit,
const HexMap* map,
const CoordsSet& locations,
@@ -56,25 +55,25 @@ auto EffectiveDistance(
const BattalionTypeGetter& battalionTypeGetter,
ActionPoints braveWaterCost) -> DIST_T;
auto EffectiveDistance(
[[nodiscard]] auto EffectiveDistance(
const Unit* unit,
const ActionPointDistances* notBravingApd,
const ActionPointDistances* bravingApd,
const CoordsSet& locations) -> DIST_T;
// Chooses a list of targets in priority order for each unit.
auto GenerateTargetPriorities(
const vector<const Unit*>& occupants,
[[nodiscard]] auto GenerateTargetPriorities(
const std::vector<const Unit*>& occupants,
const HexMap* map,
const CoordsSet& targets,
PlayerId attackerId,
const vector<const Unit*>& remainingUnits,
const std::vector<const Unit*>& remainingUnits,
const APDCache& apdCache,
const ALCache& alCache,
const BattalionTypeGetter& battalionTypeGetter,
ActionPoints braveWaterCost,
bool isLateGame = false) -> vector<TargetPriorityList>;
bool isLateGame = false) -> std::vector<TargetPriorityList>;
} // namespace shardok
#endif // EAGLE0_AIATTACKGROUPS_HPP
#endif // EAGLE0_SHARDOK_AI_AI_ATTACK_GROUPS_HPP
@@ -53,8 +53,8 @@ AttackLocationsCache::AttackLocationsCache(const HexMap *hexMap, const SettingsG
auto emptySet = CoordsSet(columnCount, rowCount);
for (MapIndex r = 0; r < hexMap->row_count(); r++) {
for (MapIndex c = 0; c < hexMap->column_count(); c++) {
for (MapIndex r = 0; r < hexMap->row_count(); ++r) {
for (MapIndex c = 0; c < hexMap->column_count(); ++c) {
auto location = Coords(r, c);
const auto terrain = GetTerrain(hexMap, location);
@@ -114,8 +114,7 @@ auto AttackLocationsCache::CachedLocations(const CoordsSet &targets, const bool
-> AttackLocations {
AttackLocations al(rowCount, columnCount);
for (const auto &target : targets) {
const int index = target.row() * columnCount + target.column();
for (const auto index : targets.indexIterator()) {
if (lateGame) {
al += lateGameLocations[index];
} else {
@@ -137,4 +136,4 @@ auto AttackLocationsCache::CachedLocations(const vector<const Unit *> &enemies,
return CachedLocations(targets, lateGame);
}
} // namespace shardok
} // namespace shardok
@@ -17,10 +17,8 @@
namespace shardok {
using Unit = net::eagle0::shardok::storage::fb::Unit;
using std::shared_ptr;
using std::vector;
class AttackLocations {
class alignas(128) AttackLocations {
private:
const MapIndex rowCount;
const MapIndex columnCount;
@@ -147,9 +145,9 @@ public:
adjacentLocations += cs;
allLocations += cs;
}
} __attribute__((aligned(128)));
};
struct AttackLocationsCache {
struct alignas(128) AttackLocationsCache {
private:
const MapIndex rowCount;
const MapIndex columnCount;
@@ -165,8 +163,8 @@ public:
auto CachedLocations(const Coords& singleTarget, bool lateGame) -> const AttackLocations&;
auto CachedLocations(const CoordsSet& targets, bool lateGame) -> AttackLocations;
auto CachedLocations(const vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
} __attribute__((aligned(128)));
auto CachedLocations(const std::vector<const Unit*>& enemies, bool lateGame) -> AttackLocations;
};
using ALCache = std::unique_ptr<AttackLocationsCache>;
@@ -33,6 +33,7 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
bool canFlee = false;
vector<const Unit*> attackerUnits{};
attackerUnits.reserve(gameState->units()->size());
CoordsSet defenderPositions(gameState->hex_map());
for (const Unit* unit : *gameState->units()) {
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
@@ -131,4 +132,4 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
return chosenStrategy;
}
} // namespace shardok
} // namespace shardok
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 1/9/22.
//
#ifndef EAGLE0_AIATTACKERSTRATEGYSELECTOR_HPP
#define EAGLE0_AIATTACKERSTRATEGYSELECTOR_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_ATTACKER_STRATEGY_SELECTOR_HPP
#define EAGLE0_SHARDOK_AI_AI_ATTACKER_STRATEGY_SELECTOR_HPP
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
@@ -32,4 +32,4 @@ public:
};
} // namespace shardok
#endif // EAGLE0_AIATTACKERSTRATEGYSELECTOR_HPP
#endif // EAGLE0_SHARDOK_AI_AI_ATTACKER_STRATEGY_SELECTOR_HPP
@@ -8,7 +8,11 @@
#include <chrono>
#include <cmath>
#include <future>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include "AICommandFilter.hpp"
#include "TranspositionTable.hpp"
@@ -19,16 +23,49 @@
namespace shardok {
// No need to forward declare internal functions - use the public interface instead
constexpr bool kMultithread = true;
constexpr bool kLogging = false;
// Helper constants and static variables
static const std::vector<double> _averageSequence = {0.5};
static const auto _averageGenerator = std::make_shared<SequenceRandomGenerator>(_averageSequence);
[[nodiscard]] static auto AverageGenerator() -> std::shared_ptr<SequenceRandomGenerator> {
return std::make_shared<SequenceRandomGenerator>(std::vector{0.5});
}
#define MULTITHREAD true
#define LOGGING_ 0
[[nodiscard]] static auto DescribeCommandForLog(const CommandSPtr& command) -> std::string {
if (!command) { return "<null command>"; }
std::ostringstream out;
out << "type=" << command->GetCommandType()
<< " player=" << static_cast<int>(command->GetPlayerId())
<< " actor=" << command->GetActorUnitId() << " target=("
<< static_cast<int>(command->GetTargetRow()) << ','
<< static_cast<int>(command->GetTargetColumn()) << ')';
if (command->HasOdds()) { out << " odds_percentile=" << command->GetOddsPercentile(); }
return out.str();
}
[[nodiscard]] static auto DescribeCommandAt(
const CommandListSPtr& commands,
const size_t commandIndex) -> std::string {
if (!commands) { return "command=<null command list>"; }
if (commandIndex >= commands->size()) {
std::ostringstream out;
out << "command=<index " << commandIndex << " out of " << commands->size() << ">";
return out.str();
}
std::ostringstream out;
out << "command={" << DescribeCommandForLog(commands->at(commandIndex)) << '}';
return out.str();
}
[[nodiscard]] static auto WithExceptionContext(
const std::string& context,
const std::exception& exception) -> std::runtime_error {
return std::runtime_error(context + ": " + exception.what());
}
// Helper function to determine if a command type is deterministic
static auto IsDeterministic(const CommandType type) -> bool {
switch (type) {
case net::eagle0::shardok::common::MOVE_COMMAND:
@@ -41,14 +78,13 @@ static auto IsDeterministic(const CommandType type) -> bool {
case net::eagle0::shardok::common::PLACE_HIDDEN_UNIT_COMMAND:
case net::eagle0::shardok::common::UNIT_STOP_COMMAND:
case net::eagle0::shardok::common::UNIT_REST_COMMAND:
case net::eagle0::shardok::common::FLEE_COMMAND:
case net::eagle0::shardok::common::REINFORCE_COMMAND:
case net::eagle0::shardok::common::RETREAT_COMMAND:
case net::eagle0::shardok::common::END_PLAYER_SETUP_COMMAND:
case net::eagle0::shardok::common::HIDE_COMMAND:
case net::eagle0::shardok::common::FORTIFY_COMMAND:
case net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND:
case net::eagle0::shardok::common::HOLY_WAVE_COMMAND:
case net::eagle0::shardok::common::EVACUATE_PRISONERS_COMMAND:
case net::eagle0::shardok::common::REPAIR_COMMAND: return true;
default: return false;
}
@@ -60,7 +96,14 @@ static auto RandomnessSampleForRepeat(const int repeatIteration, const int maxRe
return static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1);
}
// Helper function to sort commands by score
static auto SuccessSampleForOdds(const double successChance) -> double {
return successChance / 2.0;
}
static auto FailureSampleForOdds(const double successChance) -> double {
return successChance + ((1.0 - successChance) / 2.0);
}
static auto CommandSorter(
const AICommandEvaluator::IndexAndScore& l,
const AICommandEvaluator::IndexAndScore& r) -> bool {
@@ -80,7 +123,7 @@ AICommandEvaluator::AICommandEvaluator(
BattalionTypeGetter battalionTypeGetter)
: scorer_(scorer),
apdCache_(apdCache),
battalionTypeGetter_(std::move(battalionTypeGetter)) {} // Move the function object
battalionTypeGetter_(std::move(battalionTypeGetter)) {}
auto AICommandEvaluator::PerformLookahead(
const PlayerId pid,
@@ -135,17 +178,34 @@ auto AICommandEvaluator::PerformLookahead(
std::launch::deferred,
[bestCommandFuture = std::move(bestCommandFuture),
innerEngine,
nextCommands,
pid,
nextUtility,
remainingLookahead]() mutable -> EvaluationResult {
const auto bestCommand = bestCommandFuture.get();
IndexAndScore bestCommand;
try {
bestCommand = bestCommandFuture.get();
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI PerformLookahead failed while resolving best command"
<< " player=" << static_cast<int>(pid)
<< " remaining_lookahead=" << remainingLookahead
<< " next_command_count=" << nextCommands->size();
throw WithExceptionContext(context.str(), e);
}
if (!bestCommand.completed) {
return EvaluationResult{.score = nextUtility, .completed = false};
}
if (bestCommand.index >= nextCommands->size()) {
std::cerr << "AI lookahead produced command index " << bestCommand.index
<< " with only " << nextCommands->size()
<< " available commands for player " << static_cast<int>(pid)
<< " at remaining lookahead " << remainingLookahead << "\n";
return EvaluationResult{.score = nextUtility, .completed = false};
}
ScoreValue resultScore;
if (auto& nextCommand = innerEngine->GetAvailableCommandsForAIPlayer(pid)->at(
bestCommand.index);
if (const auto& nextCommand = nextCommands->at(bestCommand.index);
nextCommand->GetCommandType() !=
net::eagle0::shardok::common::END_TURN_COMMAND) {
resultScore = bestCommand.immediateScore;
@@ -196,14 +256,36 @@ auto AICommandEvaluator::EvaluateWithRandomness(
return returnValue;
}
const auto guessedCommands = guessedEngine.GetAvailableCommandsForAIPlayer(pid);
auto innerEngine = std::make_shared<ShardokEngine>(guessedEngine, false);
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
try {
innerEngine->PostCommand(pid, commandIndex, randomGenerator);
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI EvaluateWithRandomness failed while posting simulated command"
<< " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
<< " guessed_command_count=" << guessedCommands->size()
<< " remaining_lookahead=" << remainingLookahead << ' '
<< DescribeCommandAt(guessedCommands, commandIndex);
throw WithExceptionContext(context.str(), e);
}
auto innerUtility = scorer_.GuessedStateScore(
isDefender,
innerEngine->GetCurrentGameState(),
attackerStrategy,
allCastleCoords);
ScoreValue innerUtility;
try {
innerUtility = scorer_.GuessedStateScore(
isDefender,
innerEngine->GetCurrentGameState(),
attackerStrategy,
allCastleCoords);
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI EvaluateWithRandomness failed while scoring simulated state"
<< " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
<< " guessed_command_count=" << guessedCommands->size()
<< " remaining_lookahead=" << remainingLookahead << ' '
<< DescribeCommandAt(guessedCommands, commandIndex);
throw WithExceptionContext(context.str(), e);
}
returnValue.immediateScore = innerUtility;
returnValue.completed = true;
@@ -218,33 +300,45 @@ auto AICommandEvaluator::EvaluateWithRandomness(
isDefender,
remainingLookahead,
maxRepeatCount,
commandIndex,
innerEngine,
attackerStrategy,
innerUtility,
&allCastleCoords,
guessedCommands,
allCastleCoords,
deadline]() -> EvaluationResult {
auto lookaheadFuture = PerformLookahead(
pid,
isDefender,
remainingLookahead,
maxRepeatCount,
innerEngine,
innerUtility,
attackerStrategy,
allCastleCoords,
deadline);
return lookaheadFuture.get();
try {
auto lookaheadFuture = PerformLookahead(
pid,
isDefender,
remainingLookahead,
maxRepeatCount,
innerEngine,
innerUtility,
attackerStrategy,
allCastleCoords,
deadline);
return lookaheadFuture.get();
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI EvaluateWithRandomness failed during recursive lookahead"
<< " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
<< " remaining_lookahead=" << remainingLookahead << ' '
<< DescribeCommandAt(guessedCommands, commandIndex);
throw WithExceptionContext(context.str(), e);
}
};
#if MULTITHREAD
auto launchPolicy = remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
#else
std::promise<EvaluationResult> p;
returnValue.lookaheadScore = p.get_future();
auto lambdaResult = lookaheadLambda();
p.set_value(lambdaResult);
#endif
if constexpr (kMultithread) {
auto launchPolicy =
remainingLookahead == 1 ? std::launch::async : std::launch::deferred;
returnValue.lookaheadScore = std::async(launchPolicy, lookaheadLambda);
} else {
std::promise<EvaluationResult> p;
returnValue.lookaheadScore = p.get_future();
auto lambdaResult = lookaheadLambda();
p.set_value(lambdaResult);
}
}
return returnValue;
@@ -301,39 +395,52 @@ auto AICommandEvaluator::FindBestCommand(
minDistToEnemies = 0.0; // No enemies found
}
#if LOGGING_
// Log command count and distance metrics for performance analysis
const auto allCommandCount = guessedDescriptors->size();
const auto filteredCommandCount = filteredIndices.size();
const int currentRound = gameState->current_round();
if constexpr (kLogging) {
// Log command count and distance metrics for performance analysis
const auto allCommandCount = guessedDescriptors->size();
const auto filteredCommandCount = filteredIndices.size();
const int currentRound = gameState->current_round();
printf("AI_COMMAND_COUNT: Round %d, Player %d, Defender %d, MinDist %.1f, Commands %zu -> %zu "
"(%.1f%% filtered)\n",
currentRound,
static_cast<int>(pid),
isDefender ? 1 : 0,
minDistToEnemies,
allCommandCount,
filteredCommandCount,
100.0 * (allCommandCount - filteredCommandCount) / allCommandCount);
#endif
const auto flags = std::cout.flags();
const auto precision = std::cout.precision();
std::cout << "AI_COMMAND_COUNT: Round " << currentRound << ", Player "
<< static_cast<int>(pid) << ", Defender " << (isDefender ? 1 : 0) << ", MinDist "
<< std::fixed << std::setprecision(1) << minDistToEnemies << ", Commands "
<< allCommandCount << " -> " << filteredCommandCount << " ("
<< 100.0 * (allCommandCount - filteredCommandCount) / allCommandCount
<< "% filtered)\n";
std::cout.flags(flags);
std::cout.precision(precision);
}
const auto commandCount = filteredIndices.size();
// Structure to hold all command evaluation data
struct CommandEvaluation {
size_t index;
CommandType type;
ScoreValue immediateScore;
size_t index{};
CommandType type{};
ScoreValue immediateScore{};
bool immediateCompleted = true;
std::vector<std::future<EvaluationResult>> lookaheadFutures;
};
std::vector<CommandEvaluation> commandEvaluations(commandCount);
for (uint32_t index = 0; index < commandCount; index++) {
for (uint32_t index = 0; index < commandCount; ++index) {
const auto originalIndex = filteredIndices[index];
const auto& guessedDescriptor = guessedDescriptors->at(originalIndex);
const CommandSPtr guessedDescriptor = [&] {
try {
return guessedDescriptors->at(originalIndex);
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI FindBestCommand failed while reading filtered command"
<< " player=" << static_cast<int>(pid) << " filtered_index=" << index
<< " original_index=" << originalIndex
<< " guessed_command_count=" << guessedDescriptors->size()
<< " filtered_command_count=" << filteredIndices.size();
throw WithExceptionContext(context.str(), e);
}
}();
const auto guessedCommandType = guessedDescriptor->GetCommandType();
commandEvaluations[index].index = originalIndex;
@@ -351,7 +458,7 @@ auto AICommandEvaluator::FindBestCommand(
originalIndex,
remainingLookahead,
maxRepeatCount,
_averageGenerator,
AverageGenerator(),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -365,7 +472,7 @@ auto AICommandEvaluator::FindBestCommand(
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
// Success attempt uses 1.0 - (successChance / 2) as the roll
// Success attempt uses the midpoint of the successful percentile range.
auto successEvaluation = EvaluateWithRandomness(
pid,
isDefender,
@@ -373,13 +480,13 @@ auto AICommandEvaluator::FindBestCommand(
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(
std::vector{1.0 - successChance / 2.0}),
std::vector{SuccessSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
deadline);
// Failure attempt uses the average of (1 - successChance) and 0 as the roll
// Failure attempt uses the midpoint of the failing percentile range.
auto failureEvaluation = EvaluateWithRandomness(
pid,
isDefender,
@@ -387,7 +494,7 @@ auto AICommandEvaluator::FindBestCommand(
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(
std::vector{(1.0 - successChance) / 2.0}),
std::vector{FailureSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -417,7 +524,8 @@ auto AICommandEvaluator::FindBestCommand(
} else {
ScoreValue sum = 0.0;
const int sampleCount = std::max(1, maxRepeatCount);
for (int repeatIteration = 0; repeatIteration < sampleCount; repeatIteration++) {
commandEvaluations[index].lookaheadFutures.reserve(sampleCount);
for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
// In each iteration, use a double from [0, 1] as the random roll
auto sequence =
std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
@@ -427,7 +535,7 @@ auto AICommandEvaluator::FindBestCommand(
originalIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(sequence),
std::make_shared<SequenceRandomGenerator>(std::move(sequence)),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -454,7 +562,17 @@ auto AICommandEvaluator::FindBestCommand(
ScoreValue totalLookaheadScore = 0.0;
bool completed = eval.immediateCompleted;
for (auto& future : eval.lookaheadFutures) {
const auto lookahead = future.get();
EvaluationResult lookahead;
try {
lookahead = future.get();
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI FindBestCommand failed while resolving lookahead future"
<< " command_index=" << eval.index
<< " command_type=" << eval.type
<< " lookahead_future_count=" << eval.lookaheadFutures.size();
throw WithExceptionContext(context.str(), e);
}
if (!lookahead.completed) { completed = false; }
totalLookaheadScore += lookahead.score;
}
@@ -506,26 +624,30 @@ auto AICommandEvaluator::EvaluateCommand(
}
const auto& guessedDescriptor = guessedDescriptors->at(commandIndex);
const auto guessedCommandType = guessedDescriptor->GetCommandType();
if (const auto guessedCommandType = guessedDescriptor->GetCommandType();
guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
if (guessedCommandType == net::eagle0::shardok::common::END_TURN_COMMAND) {
std::promise<EvaluationResult> p;
p.set_value(EvaluationResult{.score = currentUtility, .completed = true});
return p.get_future();
} else if (IsDeterministic(guessedCommandType)) {
}
if (IsDeterministic(guessedCommandType)) {
auto evaluation = EvaluateWithRandomness(
pid,
isDefender,
commandIndex,
remainingLookahead,
maxRepeatCount,
_averageGenerator,
AverageGenerator(),
guessedEngine,
attackerStrategy,
allCastleCoords,
deadline);
return std::move(evaluation.lookaheadScore);
} else if (guessedDescriptor->HasOdds()) {
}
if (guessedDescriptor->HasOdds()) {
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
@@ -536,7 +658,8 @@ auto AICommandEvaluator::EvaluateCommand(
commandIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(std::vector{1.0 - successChance / 2.0}),
std::make_shared<SequenceRandomGenerator>(
std::vector{SuccessSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -549,7 +672,8 @@ auto AICommandEvaluator::EvaluateCommand(
commandIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(std::vector{(1.0 - successChance) / 2.0}),
std::make_shared<SequenceRandomGenerator>(
std::vector{FailureSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -570,47 +694,47 @@ auto AICommandEvaluator::EvaluateCommand(
.score = std::lerp(failure.score, success.score, successChance),
.completed = true};
});
} else {
// For non-deterministic commands without odds, use multiple attempts
std::vector<std::future<EvaluationResult>> lookaheadFutures;
const int sampleCount = std::max(1, maxRepeatCount);
lookaheadFutures.reserve(sampleCount);
for (int repeatIteration = 0; repeatIteration < sampleCount; repeatIteration++) {
auto sequence = std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
auto evaluation = EvaluateWithRandomness(
pid,
isDefender,
commandIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(sequence),
guessedEngine,
attackerStrategy,
allCastleCoords,
deadline);
lookaheadFutures.push_back(std::move(evaluation.lookaheadScore));
}
// Return a future that computes the average when needed
return std::async(
std::launch::deferred,
[lookaheadFutures = std::move(lookaheadFutures),
sampleCount]() mutable -> EvaluationResult {
ScoreValue total = 0.0;
bool completed = true;
for (auto& future : lookaheadFutures) {
const auto result = future.get();
if (!result.completed) { completed = false; }
total += result.score;
}
if (!completed) { return EvaluationResult{.score = 0.0, .completed = false}; }
return EvaluationResult{
.score = total / static_cast<ScoreValue>(sampleCount),
.completed = true};
});
}
// For non-deterministic commands without odds, use multiple attempts
std::vector<std::future<EvaluationResult>> lookaheadFutures;
const int sampleCount = std::max(1, maxRepeatCount);
lookaheadFutures.reserve(sampleCount);
for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
auto sequence = std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
auto evaluation = EvaluateWithRandomness(
pid,
isDefender,
commandIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(std::move(sequence)),
guessedEngine,
attackerStrategy,
allCastleCoords,
deadline);
lookaheadFutures.push_back(std::move(evaluation.lookaheadScore));
}
// Return a future that computes the average when needed
return std::async(
std::launch::deferred,
[lookaheadFutures = std::move(lookaheadFutures),
sampleCount]() mutable -> EvaluationResult {
ScoreValue total = 0.0;
bool completed = true;
for (auto& future : lookaheadFutures) {
const auto result = future.get();
if (!result.completed) { completed = false; }
total += result.score;
}
if (!completed) { return EvaluationResult{.score = 0.0, .completed = false}; }
return EvaluationResult{
.score = total / static_cast<ScoreValue>(sampleCount),
.completed = true};
});
}
} // namespace shardok
@@ -3,8 +3,8 @@
// Separated from AIScoreCalculator to isolate pure state scoring from lookahead logic.
//
#ifndef EAGLE0_AICOMMANDEVALUATOR_HPP
#define EAGLE0_AICOMMANDEVALUATOR_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_COMMAND_EVALUATOR_HPP
#define EAGLE0_SHARDOK_AI_AI_COMMAND_EVALUATOR_HPP
#include <chrono>
#include <future>
@@ -57,11 +57,11 @@ public:
/// Find the best command among all available commands at the given depth.
struct IndexAndScore {
size_t index;
CommandType type;
ScoreValue lookaheadScore;
ScoreValue immediateScore;
bool completed;
size_t index{};
CommandType type{};
ScoreValue lookaheadScore{};
ScoreValue immediateScore{};
bool completed{};
};
[[nodiscard]] auto FindBestCommand(
@@ -78,11 +78,11 @@ public:
private:
const AIScoreCalculator& scorer_;
const APDCache& apdCache_;
BattalionTypeGetter battalionTypeGetter_; // Store by value
BattalionTypeGetter battalionTypeGetter_;
struct ImmediateAndLookaheadScore {
ScoreValue immediateScore;
bool completed;
ScoreValue immediateScore{};
bool completed{};
std::future<EvaluationResult> lookaheadScore;
};
@@ -114,4 +114,4 @@ private:
} // namespace shardok
#endif // EAGLE0_AICOMMANDEVALUATOR_HPP
#endif // EAGLE0_SHARDOK_AI_AI_COMMAND_EVALUATOR_HPP
@@ -2,8 +2,8 @@
// Filter obviously bad commands to reduce search space for AI
//
#ifndef EAGLE0_AICOMMANDFILTER_HPP
#define EAGLE0_AICOMMANDFILTER_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_COMMAND_FILTER_HPP
#define EAGLE0_SHARDOK_AI_AI_COMMAND_FILTER_HPP
#include <memory>
#include <vector>
@@ -116,4 +116,4 @@ private:
} // namespace shardok
#endif // EAGLE0_AICOMMANDFILTER_HPP
#endif // EAGLE0_SHARDOK_AI_AI_COMMAND_FILTER_HPP
@@ -3,8 +3,8 @@
// Common type definitions used across AI utility functions
//
#ifndef EAGLE0_AICOMMONTYPES_HPP
#define EAGLE0_AICOMMONTYPES_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_COMMON_TYPES_HPP
#define EAGLE0_SHARDOK_AI_AI_COMMON_TYPES_HPP
#include <functional>
@@ -20,4 +20,4 @@ using BattalionTypeGetter = std::function<BattalionTypeSPtr(BattalionTypeId)>;
} // namespace shardok
#endif // EAGLE0_AICOMMONTYPES_HPP
#endif // EAGLE0_SHARDOK_AI_AI_COMMON_TYPES_HPP
@@ -2,8 +2,8 @@
// AI System Types and Configuration
//
#ifndef EAGLE0_AI_CONFIG_HPP
#define EAGLE0_AI_CONFIG_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_CONFIG_HPP
#define EAGLE0_SHARDOK_AI_AI_CONFIG_HPP
namespace shardok {
@@ -22,4 +22,4 @@ enum class ScoringCalculatorType {
} // namespace shardok
#endif // EAGLE0_AI_CONFIG_HPP
#endif // EAGLE0_SHARDOK_AI_AI_CONFIG_HPP
@@ -32,6 +32,7 @@ auto AIDefenderStrategySelector::BestDefenderStrategy(
bool canFlee = false;
vector<UnitId> attackerUnitIdsRequiringWaterCrossing{};
attackerUnitIdsRequiringWaterCrossing.reserve(gameState->units()->size());
for (const auto& player : *gameState->player_infos()) {
if (!player->is_defender()) {
const auto& unitIdsRequiringWaterCrossing = UnitIdsRequiringWaterCrossing(
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 8/30/23.
//
#ifndef EAGLE0_AIDEFENDERSTRATEGYSELECTOR_HPP
#define EAGLE0_AIDEFENDERSTRATEGYSELECTOR_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_DEFENDER_STRATEGY_SELECTOR_HPP
#define EAGLE0_SHARDOK_AI_AI_DEFENDER_STRATEGY_SELECTOR_HPP
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
@@ -26,4 +26,4 @@ public:
};
} // namespace shardok
#endif // EAGLE0_AIDEFENDERSTRATEGYSELECTOR_HPP
#endif // EAGLE0_SHARDOK_AI_AI_DEFENDER_STRATEGY_SELECTOR_HPP
@@ -55,12 +55,12 @@ auto DefenderDistanceBuf(
const bool includeUndead) -> double {
const auto &locationsToAttackMe = alCache->CachedLocations(defenderLocation, lateGame);
struct WithoutAndWith {
struct alignas(8) WithoutAndWith {
int without;
int with;
WithoutAndWith(const int a, const int b) : without(a), with(b) {}
} __attribute__((aligned(8)));
};
vector<WithoutAndWith> pointCosts{};
pointCosts.reserve(attackerUnits.size());
@@ -104,23 +104,21 @@ auto DefenderDistanceBuf(
pointCostToDesiredTargetWithBraving);
}
std::sort(
begin(pointCosts),
end(pointCosts),
[](const WithoutAndWith &left, const WithoutAndWith &right) {
return left.without == right.without ? left.with < right.with
: left.without < right.without;
});
std::ranges::sort(pointCosts, [](const WithoutAndWith &left, const WithoutAndWith &right) {
return left.without == right.without ? left.with < right.with
: left.without < right.without;
});
// experiment: add dummy values to the end with very high cost, so there's never an advantage
// to leaving a defender unit alive in a castle.
for (size_t i = attackerUnits.size(); i < 20; i++) { pointCosts.emplace_back(999, 999); }
for (size_t i = attackerUnits.size(); i < 20; ++i) { pointCosts.emplace_back(999, 999); }
double sum = 0.0;
double decr = kPerUnitDebufDecay;
for (const WithoutAndWith &pointCost : pointCosts) {
const int effectiveCost =
(pointCost.without == 999) ? int(1.2 * pointCost.with) : pointCost.without;
const int effectiveCost = (pointCost.without == 999)
? static_cast<int>(1.2 * pointCost.with)
: pointCost.without;
sum += decr / (effectiveCost + 1);
decr *= kPerUnitDebufDecay;
}
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 5/10/21.
//
#ifndef EAGLE0_AIDISTANCEDEBUF_HPP
#define EAGLE0_AIDISTANCEDEBUF_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_DISTANCE_DEBUF_HPP
#define EAGLE0_SHARDOK_AI_AI_DISTANCE_DEBUF_HPP
#include "AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
@@ -13,14 +13,11 @@
namespace shardok {
using std::shared_ptr;
using std::vector;
auto DefenderDistanceBuf(
[[nodiscard]] auto DefenderDistanceBuf(
const Coords &defenderLocation,
const HexMap *hexMap,
const MapId &mapId,
const vector<const Unit *> &attackerUnits,
const std::vector<const Unit *> &attackerUnits,
const APDCache &apdCache,
const ALCache &alCache,
const BattalionTypeGetter &battalionTypeGetter,
@@ -29,4 +26,4 @@ auto DefenderDistanceBuf(
bool includeUndead) -> double;
} // namespace shardok
#endif // EAGLE0_AIDISTANCEDEBUF_HPP
#endif // EAGLE0_SHARDOK_AI_AI_DISTANCE_DEBUF_HPP
@@ -2,22 +2,36 @@
// Temporary AI experiment switches used by benchmark-only EXPERIMENTAL runs.
//
#ifndef EAGLE0_AI_EXPERIMENT_CONFIG_HPP
#define EAGLE0_AI_EXPERIMENT_CONFIG_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
#define EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
#include <charconv>
#include <cstdlib>
#include <string>
#include <string_view>
#include <system_error>
namespace shardok {
inline auto CurrentAIExperimentId() -> int {
[[nodiscard]] inline auto CurrentAIExperimentId() -> int {
const char* rawValue = std::getenv("SHARDOK_SCORING_EXPERIMENT_ID");
if (rawValue == nullptr || std::string(rawValue).empty()) { return 0; }
return std::atoi(rawValue);
if (rawValue == nullptr) { return 0; }
const std::string_view value(rawValue);
if (value.empty()) { return 0; }
int experimentId = 0;
const auto parseResult =
std::from_chars(value.data(), value.data() + value.size(), experimentId);
if (parseResult.ec != std::errc{} || parseResult.ptr != value.data() + value.size()) {
return 0;
}
return experimentId;
}
inline auto IsAIExperiment(const int id) -> bool { return CurrentAIExperimentId() == id; }
[[nodiscard]] inline auto IsAIExperiment(const int id) -> bool {
return CurrentAIExperimentId() == id;
}
} // namespace shardok
#endif // EAGLE0_AI_EXPERIMENT_CONFIG_HPP
#endif // EAGLE0_SHARDOK_AI_AI_EXPERIMENT_CONFIG_HPP
@@ -8,12 +8,31 @@
#include "AIFleeDecisionCalculator.hpp"
#include <iomanip>
#include <iostream>
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
namespace {
constexpr double kCertainCombatWinChance = 0.95;
void WritePercent(std::ostream& output, const double value) {
const auto flags = output.flags();
const auto precision = output.precision();
output << std::fixed << std::setprecision(1) << value;
output.flags(flags);
output.precision(precision);
}
} // namespace
auto AIFleeDecisionCalculator::GetFleeCommandIndex(
const CommandList::const_iterator& fleeCommand,
const CommandListSPtr& availableCommands) -> size_t {
@@ -137,77 +156,41 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
const CommandListSPtr& availableCommands,
const CommandList::const_iterator& fleeCommand,
int maxRounds,
int minimumFleeOddsThreshold,
int desperateFleeThreshold,
int /*minimumFleeOddsThreshold*/,
int /*desperateFleeThreshold*/,
bool enableDebugLogging) -> FleeDecision {
// Get flee success odds
const int fleeSuccessChance = (*fleeCommand)->GetOddsPercentile();
if (enableDebugLogging) {
printf("AI FinalRound: Evaluating flee (odds=%d%%)...\n", fleeSuccessChance);
std::cout << "AI FinalRound: Evaluating flee (odds=" << fleeSuccessChance << "%)...\n";
}
// Check if flee odds are good enough to attempt
if (fleeSuccessChance >= minimumFleeOddsThreshold) {
if (enableDebugLogging) {
printf("AI FinalRound: Good flee odds (%d%% >= %d%%), choosing flee\n",
fleeSuccessChance,
minimumFleeOddsThreshold);
}
return FleeDecision{
true,
GetFleeCommandIndex(fleeCommand, availableCommands),
"Good flee odds"};
}
// Low flee odds - evaluate if fighting might be better
const double combatWinChance = EstimateCombatSuccess(playerId, guessedState, maxRounds);
// If combat situation is hopeless, even bad flee odds are better than certain death
if (combatWinChance <= 0.05 && fleeSuccessChance >= desperateFleeThreshold) {
if (combatWinChance < kCertainCombatWinChance) {
if (enableDebugLogging) {
printf("AI FinalRound: Combat hopeless (%.1f%%), desperate flee attempt (%d%%)\n",
combatWinChance * 100,
fleeSuccessChance);
std::cout << "AI FinalRound: Combat not certain (";
WritePercent(std::cout, combatWinChance * 100);
std::cout << "% < ";
WritePercent(std::cout, kCertainCombatWinChance * 100);
std::cout << "%), choosing flee\n";
}
return FleeDecision{
true,
GetFleeCommandIndex(fleeCommand, availableCommands),
"Combat hopeless, desperate flee"};
"Last round combat not certain"};
}
// Detailed flee vs fight comparison
const double fleeChance = static_cast<double>(fleeSuccessChance) / 100.0;
// Compare expected outcomes:
// - Flee: fleeChance of survival (not victory, but avoiding loss)
// - Fight: combatWinChance of victory (better than survival)
constexpr double FLEE_VS_COMBAT_MARGIN =
0.8; // Require 80% of combat chance to prefer fighting
const double adjustedCombatThreshold = combatWinChance * FLEE_VS_COMBAT_MARGIN;
if (enableDebugLogging) {
printf("AI FinalRound: Flee=%d%%, Combat=%.1f%%, Threshold=%.1f%% -> ",
fleeSuccessChance,
combatWinChance * 100,
adjustedCombatThreshold * 100);
}
if (fleeChance > adjustedCombatThreshold) {
if (enableDebugLogging) { printf("FLEE (better odds)\n"); }
return FleeDecision{
true,
GetFleeCommandIndex(fleeCommand, availableCommands),
"Flee has better expected outcome"};
} else {
if (enableDebugLogging) { printf("FIGHT (better expected outcome)\n"); }
// Return 0 to indicate we should use standard command selection
return FleeDecision{
false,
0, // Will be replaced by StandardChooseCommandIndex
"Fighting has better expected outcome"};
std::cout << "AI FinalRound: Combat effectively certain (";
WritePercent(std::cout, combatWinChance * 100);
std::cout << "%), fighting\n";
}
return FleeDecision{
false,
0, // Will be replaced by StandardChooseCommandIndex
"Combat effectively certain"};
}
auto AIFleeDecisionCalculator::ShouldConsiderFleeing(
@@ -6,8 +6,8 @@
// and flee vs fight evaluation for final round scenarios
//
#ifndef AIFleeDecisionCalculator_hpp
#define AIFleeDecisionCalculator_hpp
#ifndef EAGLE0_SHARDOK_AI_AI_FLEE_DECISION_CALCULATOR_HPP
#define EAGLE0_SHARDOK_AI_AI_FLEE_DECISION_CALCULATOR_HPP
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
@@ -63,4 +63,4 @@ private:
} // namespace shardok
#endif /* AIFleeDecisionCalculator_hpp */
#endif // EAGLE0_SHARDOK_AI_AI_FLEE_DECISION_CALCULATOR_HPP
@@ -226,6 +226,7 @@ double AIHeuristicWeighting::GetCommandWeight(
// === ZERO WEIGHT - NEVER SELECT (0.0) ===
// Explicitly bad actions
case CommandType::FLEE_COMMAND: return 0.0; // Never flee in simulation
case CommandType::EVACUATE_PRISONERS_COMMAND: return 0.0;
case CommandType::RETREAT_COMMAND: return 0.0;
case CommandType::BECOME_OUTLAW_COMMAND: return 0.0; // Never become outlaw
case CommandType::DISMISS_UNIT_COMMAND:
@@ -3,8 +3,8 @@
// Provides O(1) weights based on command type and context
//
#ifndef EAGLE0_AI_HEURISTIC_WEIGHTING_HPP
#define EAGLE0_AI_HEURISTIC_WEIGHTING_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_HEURISTIC_WEIGHTING_HPP
#define EAGLE0_SHARDOK_AI_AI_HEURISTIC_WEIGHTING_HPP
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-redundant-constexpr-static-def"
@@ -37,4 +37,4 @@ public:
} // namespace shardok
#endif // EAGLE0_AI_HEURISTIC_WEIGHTING_HPP
#endif // EAGLE0_SHARDOK_AI_AI_HEURISTIC_WEIGHTING_HPP
@@ -5,15 +5,34 @@
#include "AIMinimumDistanceAndTarget.hpp"
namespace shardok {
namespace {
auto CoordsIndex(const Coords &coords, const int columnCount) -> int {
return coords.row() * columnCount + coords.column();
}
auto CoordsFromIndex(const size_t index, const int columnCount) -> Coords {
return Coords(
static_cast<int8_t>(index / static_cast<size_t>(columnCount)),
static_cast<int8_t>(index % static_cast<size_t>(columnCount)));
}
} // namespace
auto MinimumDistanceAndTarget(
const ActionPointDistances *apd,
const Coords &origin,
const CoordsSet &destinations) -> CoordsAndDistance {
CoordsAndDistance min{Coords(-1, -1), ActionPointDistances::IMPOSSIBLE};
for (const auto &dest : destinations) {
const DIST_T dist = apd->Distance(origin, dest);
const int columnCount = destinations.ColumnCount();
const int originIndex = CoordsIndex(origin, columnCount);
for (const auto destIndex : destinations.indexIterator()) {
const DIST_T dist = apd->Distance(originIndex, static_cast<int>(destIndex));
if (dist == ActionPointDistances::IMPOSSIBLE) continue;
if (dist < min.distance) min = CoordsAndDistance{dest, dist};
if (dist < min.distance) {
min = CoordsAndDistance{CoordsFromIndex(destIndex, columnCount), dist};
}
}
return min;
@@ -25,4 +44,4 @@ auto MinimumDistance(
const CoordsSet &destinations) -> int {
return MinimumDistanceAndTarget(apd, origin, destinations).distance;
}
} // namespace shardok
} // namespace shardok
@@ -2,11 +2,8 @@
// Created by Dan Crosby on 8/31/23.
//
#ifndef EAGLE0_AIMINIMUMDISTANCEANDTARGET_HPP
#define EAGLE0_AIMINIMUMDISTANCEANDTARGET_HPP
#include <utility>
#include <vector>
#ifndef EAGLE0_SHARDOK_AI_AI_MINIMUM_DISTANCE_AND_TARGET_HPP
#define EAGLE0_SHARDOK_AI_AI_MINIMUM_DISTANCE_AND_TARGET_HPP
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistances.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
@@ -15,23 +12,22 @@
namespace shardok {
using net::eagle0::shardok::storage::fb::Coords;
using std::shared_ptr;
struct CoordsAndDistance {
Coords coords;
int distance;
};
auto MinimumDistanceAndTarget(
[[nodiscard]] auto MinimumDistanceAndTarget(
const ActionPointDistances *apd,
const Coords &origin,
const CoordsSet &destinations) -> CoordsAndDistance;
auto MinimumDistance(
[[nodiscard]] auto MinimumDistance(
const ActionPointDistances *apd,
const Coords &origin,
const CoordsSet &destinations) -> int;
} // namespace shardok
#endif // EAGLE0_AIMINIMUMDISTANCEANDTARGET_HPP
#endif // EAGLE0_SHARDOK_AI_AI_MINIMUM_DISTANCE_AND_TARGET_HPP
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 4/7/21.
//
#ifndef EAGLE0_AISCOREUTILITIES_HPP
#define EAGLE0_AISCOREUTILITIES_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_SCORE_UTILITIES_HPP
#define EAGLE0_SHARDOK_AI_AI_SCORE_UTILITIES_HPP
#include <vector>
@@ -16,19 +16,18 @@ namespace shardok {
using ScoreValue = double;
using std::vector;
using GameState = net::eagle0::shardok::storage::fb::GameState;
using PlayerInfo = net::eagle0::shardok::storage::fb::PlayerInfo;
using Unit = net::eagle0::shardok::storage::fb::Unit;
auto HasAttachedHeroWithProfession(
[[nodiscard]] auto HasAttachedHeroWithProfession(
const Unit *unit,
net::eagle0::shardok::storage::fb::Profession profession) -> bool;
auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
auto PlayerInfoForPid(const GameStateW &, PlayerId pid) -> const PlayerInfo *;
[[nodiscard]] auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &gameState, PlayerId pid)
-> const PlayerInfo *;
} // namespace shardok
#endif // EAGLE0_AISCOREUTILITIES_HPP
#endif // EAGLE0_SHARDOK_AI_AI_SCORE_UTILITIES_HPP
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 1/10/22.
//
#ifndef EAGLE0_AISTRATEGY_HPP
#define EAGLE0_AISTRATEGY_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_STRATEGY_HPP
#define EAGLE0_SHARDOK_AI_AI_STRATEGY_HPP
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
@@ -26,12 +26,14 @@ struct AIStrategy {
};
extern AIStrategy FleeStrategy;
auto AttackUnitsStrategy(const vector<TargetPriorityList>& targetPriorities) -> AIStrategy;
auto AttackCastlesStrategy(const vector<TargetPriorityList>& targetPriorities) -> AIStrategy;
[[nodiscard]] auto AttackUnitsStrategy(const vector<TargetPriorityList>& targetPriorities)
-> AIStrategy;
[[nodiscard]] auto AttackCastlesStrategy(const vector<TargetPriorityList>& targetPriorities)
-> AIStrategy;
extern AIStrategy HoldCastlesStrategy;
extern AIStrategy ScatterStrategy;
auto CrossRiversStrategy(const CoordsSet& targetLocations) -> AIStrategy;
[[nodiscard]] auto CrossRiversStrategy(const CoordsSet& targetLocations) -> AIStrategy;
} // namespace shardok
#endif // EAGLE0_AISTRATEGY_HPP
#endif // EAGLE0_SHARDOK_AI_AI_STRATEGY_HPP
@@ -116,15 +116,6 @@ auto CalculateTimeBudget(
const auto clampedBudgetMs = std::clamp(budgetMs, 200.0, maxBudgetMs);
const auto remainingBudget = std::chrono::milliseconds(static_cast<int64_t>(clampedBudgetMs));
// TEMPORARY DEBUG OUTPUT
printf("[DEBUG CalculateTimeBudget] numCommands=%zu, msPerCommand=%.2f, budgetMs=%.2f, "
"clampedBudgetMs=%.2f, isClose=%d\n",
numCommands,
msPerCommand,
budgetMs,
clampedBudgetMs,
isClose);
// Get minimum depth requirement
const size_t minDepth = settingsGetter.Backing().min_lookahead_turns();
@@ -134,4 +125,4 @@ auto CalculateTimeBudget(
.isCloseToEnemy = isClose};
}
} // namespace shardok
} // namespace shardok
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 07/04/25.
//
#ifndef EAGLE0_AITIMEBUDGET_HPP
#define EAGLE0_AITIMEBUDGET_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_TIME_BUDGET_HPP
#define EAGLE0_SHARDOK_AI_AI_TIME_BUDGET_HPP
#include <atomic>
#include <chrono>
@@ -25,7 +25,12 @@ class AIEvaluationCounter {
public:
AIEvaluationCounter();
~AIEvaluationCounter();
static int GetCurrentCount();
AIEvaluationCounter(const AIEvaluationCounter&) = delete;
auto operator=(const AIEvaluationCounter&) -> AIEvaluationCounter& = delete;
AIEvaluationCounter(AIEvaluationCounter&&) = delete;
auto operator=(AIEvaluationCounter&&) -> AIEvaluationCounter& = delete;
[[nodiscard]] static int GetCurrentCount();
};
// Configuration structure for iterative deepening time budget
@@ -39,13 +44,13 @@ struct AITimeBudget {
// Time budget is calculated dynamically based on number of available commands:
// budget = msPerCommand × numCommands (clamped to 200-5000ms)
// If isAllAiBattle is true, uses allAiBattleTimeBudgetMaximum instead of the normal maximum.
auto CalculateTimeBudget(
[[nodiscard]] auto CalculateTimeBudget(
PlayerId playerId,
const GameSettingsSPtr &settings,
const GameStateW &state,
const GameSettingsSPtr& settings,
const GameStateW& state,
size_t numCommands,
bool isAllAiBattle) -> AITimeBudget;
} // namespace shardok
#endif // EAGLE0_AITIMEBUDGET_HPP
#endif // EAGLE0_SHARDOK_AI_AI_TIME_BUDGET_HPP
@@ -11,10 +11,6 @@
namespace shardok {
using std::begin;
using std::end;
using std::shared_ptr;
constexpr double kProfessionValue = 200;
constexpr double kVigorScoreMultiplier = 5.0;
constexpr double kCastleMultiplierBonus = 1.0;
@@ -92,8 +88,8 @@ auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue {
break;
}
const double battalionValue = battalionTypeMultiplier * (1.0 + armament / 100.0) *
(1.0 + training / 100.0) * (0.5 + morale / 100.0) *
const double battalionValue = battalionTypeMultiplier * (1.0 + (armament / 100.0)) *
(1.0 + (training / 100.0)) * (0.5 + (morale / 100.0)) *
unit->battalion().size();
const double heroValue =
@@ -124,15 +120,16 @@ auto archeryValue(const vector<const Unit *> &enemyUnits, const bool canShootNow
auto reduceValue(const Unit *unit, const Terrain *unitTerrain) -> double {
// TODO: make this depend on value of reducing castle too
if (unit->fortified() || unitTerrain->modifier().castle().present())
if (unit->fortified() || unitTerrain->modifier().castle().present()) {
return kReduceFortifiedPossibleValue;
else if (AllowsFortification(unitTerrain))
return kReduceUnfortifiedPossibleValue;
else
return 0.0;
}
if (AllowsFortification(unitTerrain)) { return kReduceUnfortifiedPossibleValue; }
return 0.0;
}
auto fearValue(const vector<const Unit *> &) -> double { return kFearPossibleValue; }
auto fearValue([[maybe_unused]] const vector<const Unit *> &enemyUnits) -> double {
return kFearPossibleValue;
}
auto lightningValue(const Unit *unit) -> double {
// TODO: make this depend on the value of the targets
@@ -239,9 +236,8 @@ auto meteorValue(
multiplier = kMeteorNoneRoundMultiplier * vigorFactor;
}
return maxValue * multiplier;
} else {
return 0.0;
}
return 0.0;
}
case net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET:
@@ -251,9 +247,8 @@ auto meteorValue(
map,
enemyOccupants,
friendlyOccupants);
} else {
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
}
return bestTargetValue(unit, map, enemyOccupants, friendlyOccupants, meteorRange);
case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
return kMeteorCastRoundMultiplier *
meteorDropRawValue(
@@ -280,7 +275,7 @@ auto GetRangedAttackBonus(
const int meteorRange,
const double minVigorToCast,
const ActionPoints archeryActionPointCost) -> double {
vector<double> rangedAttackValues{};
double bestRangedAttackValue = 0.0;
const auto &enemyUnits = isAttacker ? defenderUnits : attackerUnits;
const auto &friendlyUnits = isAttacker ? attackerUnits : defenderUnits;
@@ -296,7 +291,7 @@ auto GetRangedAttackBonus(
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE)) {
if (unit->attached_hero().profession_info().meteor_cast_state() ==
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE) {
rangedAttackValues.push_back(lightningValue(unit));
bestRangedAttackValue = std::max(bestRangedAttackValue, lightningValue(unit));
} else {
const double mv = meteorValue(
unit,
@@ -306,14 +301,14 @@ auto GetRangedAttackBonus(
friendlyUnits,
meteorRange,
minVigorToCast);
rangedAttackValues.push_back(mv);
bestRangedAttackValue = std::max(bestRangedAttackValue, mv);
}
}
if (attackLocations.EngineerLocations().Contains(unitLocation) && unit->has_attached_hero() &&
unit->attached_hero().profession_info().profession() ==
net::eagle0::shardok::storage::fb::Profession_ENGINEER) {
rangedAttackValues.push_back(reduceValue(unit, terrain));
bestRangedAttackValue = std::max(bestRangedAttackValue, reduceValue(unit, terrain));
}
if (attackLocations.NecromancerLocations().Contains(unitLocation) &&
@@ -321,23 +316,24 @@ auto GetRangedAttackBonus(
unit->attached_hero().profession_info().profession() ==
net::eagle0::shardok::storage::fb::Profession_NECROMANCER &&
unit->attached_hero().control_info().controlled_unit_id() != -1) {
rangedAttackValues.push_back(fearValue(enemyUnits));
bestRangedAttackValue = std::max(bestRangedAttackValue, fearValue(enemyUnits));
}
if (attackLocations.ArcheryLocations().Contains(unitLocation) &&
unit->volleys_remaining() > 0) {
rangedAttackValues.push_back(archeryValue(enemyUnits, canShootNow));
bestRangedAttackValue =
std::max(bestRangedAttackValue, archeryValue(enemyUnits, canShootNow));
}
// Longbowmen in castle can archer adjacent
if (attackLocations.AdjacentLocations().Contains(unitLocation) &&
unit->volleys_remaining() > 0 && terrain->modifier().castle().present() &&
unit->battalion().type() == net::eagle0::shardok::storage::fb::BattalionTypeId_LONGBOWMEN) {
rangedAttackValues.push_back(archeryValue(enemyUnits, canShootNow));
bestRangedAttackValue =
std::max(bestRangedAttackValue, archeryValue(enemyUnits, canShootNow));
}
if (rangedAttackValues.empty()) return 0.0;
return *std::max_element(begin(rangedAttackValues), end(rangedAttackValues));
return bestRangedAttackValue;
}
auto UnitValue(
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 4/7/21.
//
#ifndef EAGLE0_AIUNITSCORECALCULATOR_HPP
#define EAGLE0_AIUNITSCORECALCULATOR_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_UNIT_SCORE_CALCULATOR_HPP
#define EAGLE0_SHARDOK_AI_AI_UNIT_SCORE_CALCULATOR_HPP
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistances.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
@@ -14,34 +14,31 @@
namespace shardok {
using net::eagle0::shardok::storage::fb::Unit;
using std::shared_ptr;
using std::vector;
using ScoreValue = double;
class AttackLocations;
auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue;
[[nodiscard]] auto ContextFreeUnitValue(const Unit *unit) -> ScoreValue;
auto GetRangedAttackBonus(
[[nodiscard]] auto GetRangedAttackBonus(
const Unit *unit,
bool isAttacker,
const HexMap *map,
const Terrain *terrain,
const AttackLocations &attackLocations,
int roundsRemaining,
const vector<const Unit *> &attackerUnits,
const vector<const Unit *> &defenderUnits,
const std::vector<const Unit *> &attackerUnits,
const std::vector<const Unit *> &defenderUnits,
int meteorRange,
double minVigorToCast,
ActionPoints archeryActionPointCost = 0) -> double;
auto UnitValue(
[[nodiscard]] auto UnitValue(
const Unit *unit,
bool isAttacker,
const vector<const Unit *> &attackerUnits,
const std::vector<const Unit *> &attackerUnits,
bool attackerWantsCastles,
bool includeCastleBonus,
const vector<const Unit *> &defenderUnits,
const std::vector<const Unit *> &defenderUnits,
const HexMap *map,
int roundsRemaining,
const AttackLocations &locationsThisSideCanAttackFrom,
@@ -53,4 +50,4 @@ auto UnitValue(
} // namespace shardok
#endif // EAGLE0_AIUNITSCORECALCULATOR_HPP
#endif // EAGLE0_SHARDOK_AI_AI_UNIT_SCORE_CALCULATOR_HPP
@@ -6,9 +6,18 @@
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
namespace {
auto RecomputeModifierHash(HexMap *hexMap) -> MapId {
hexMap->mutate_modifier_hash(GetModifierHash(hexMap));
return ActionPointDistancesCache::GetMapId(hexMap);
}
} // namespace
auto UnitIdsRequiringWaterCrossing(
const GameStateW &gameState,
@@ -18,7 +27,7 @@ auto UnitIdsRequiringWaterCrossing(
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
// Put out all the fires, except on bridges
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
for (uint32_t index = 0; index < mapCopy->terrain()->size(); index++) {
for (uint32_t index = 0; index < mapCopy->terrain()->size(); ++index) {
if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (mapCopy)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
@@ -155,7 +164,7 @@ auto WaterCrossingTiles(
// 1) the tile is water
// 2) the tile does not already have a bridge (that is not on fire)
// 3) the tile is not already frozen
for (int index = 0; index < mapSize; index++) {
for (int index = 0; index < mapSize; ++index) {
const auto startTerrain = *hexMap->terrain()->Get(index);
if (!IsWater(startTerrain.type())) continue;
@@ -169,6 +178,7 @@ auto WaterCrossingTiles(
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index));
terr->mutable_modifier().mutable_bridge().mutate_present(true);
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
terr->mutable_modifier().mutable_fire().mutate_present(false);
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
@@ -185,6 +195,50 @@ auto WaterCrossingTiles(
return returnCoords;
}
// Returns currently frozen water tiles that can act as an existing crossing from origin to
// destination. The shared APD cache deliberately ignores ice, so this probes frozen tiles by
// temporarily treating one as bridged for the water-crossing strategy only.
auto FrozenWaterCrossingTiles(
const Coords &origin,
const Coords &destination,
const HexMap *hexMap,
const APDCache &apdCache,
const BattalionTypeSPtr &battalionType) -> CoordsSet {
AssertValid(origin, hexMap);
AssertValid(destination, hexMap);
auto returnCoords = CoordsSet(hexMap);
fb::HexMapW mapCopy = fb::CopyHexMap(hexMap);
const int mapSize = hexMap->row_count() * hexMap->column_count();
for (int index = 0; index < mapSize; ++index) {
const auto startTerrain = *hexMap->terrain()->Get(index);
if (!IsWater(startTerrain.type())) continue;
const auto modifier = startTerrain.modifier();
if (!modifier.ice().present() || modifier.fire().present()) continue;
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index));
terr->mutable_modifier().mutable_bridge().mutate_present(true);
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
terr->mutable_modifier().mutable_fire().mutate_present(false);
const MapId mapId = RecomputeModifierHash(mapCopy.Get());
if (const auto *distances = apdCache->GetRaw(mapCopy, mapId, battalionType, false);
distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
returnCoords.Add(index / hexMap->column_count(), index % hexMap->column_count());
}
terr->mutable_modifier().mutable_bridge().mutate_present(false);
terr->mutable_modifier().mutable_bridge().mutate_integrity(0.0);
}
return returnCoords;
}
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
auto IntendedCrossingStarts(
const GameStateW &gameState,
@@ -213,7 +267,6 @@ auto IntendedCrossingStarts(
using Unit = net::eagle0::shardok::storage::fb::Unit;
constexpr double kNoRequiredCrossingScore = std::numeric_limits<double>::max();
constexpr double kNoCrossingCreatorsScore = std::numeric_limits<double>::min();
auto WaterCrossingScore(
const PlayerId playerId,
@@ -256,12 +309,26 @@ auto WaterCrossingScore(
const auto unitIdsCreatingCrossing =
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
double totalScore = 0;
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
if (unitIdsCreatingCrossing.empty()) {
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
const Coords location = unit->location();
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
const int thisDistance =
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
totalScore -= thisDistance;
}
return totalScore;
}
// First put a big penalty on the distance for units that can create a crossing
for (const UnitId uid : unitIdsCreatingCrossing) {
const Unit *unit = gameState->units()->Get(uid);
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 12/29/21.
//
#ifndef EAGLE0_AIWATERCROSSINGCALCULATOR_HPP
#define EAGLE0_AIWATERCROSSINGCALCULATOR_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
#define EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
#include "src/main/cpp/net/eagle0/shardok/ai/AICommonTypes.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
@@ -23,14 +23,15 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
if (!valid) {
std::stringstream str;
str << "Invalid coords " << (int)c.row() << ", " << (int)c.column();
str << "Invalid coords " << static_cast<int>(c.row()) << ", "
<< static_cast<int>(c.column());
throw ShardokInternalErrorException(str.str());
}
}
// Units that need a water crossing to reach at least one of the destinations
auto UnitIdsRequiringWaterCrossing(
[[nodiscard]] auto UnitIdsRequiringWaterCrossing(
const GameStateW& gameState,
PlayerId pid,
const CoordsSet& destinations,
@@ -38,14 +39,14 @@ auto UnitIdsRequiringWaterCrossing(
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
// Units belonging to the player that are capable of creating water crossings
auto UnitIdsToCreateWaterCrossing(
[[nodiscard]] auto UnitIdsToCreateWaterCrossing(
const GameStateW& gameState,
PlayerId pid,
const BattalionTypeGetter& battalionTypeGetter) -> vector<UnitId>;
// Whether a unit of the given type can reach destination from origin, given the current state
// of the map
auto CanReach(
[[nodiscard]] auto CanReach(
const Coords& origin,
const Coords& destination,
const HexMap* hexMap,
@@ -55,11 +56,21 @@ auto CanReach(
// Returns the *non*-water tiles from which you could bridge/freeze water to allow a unit to cross
// from origin to destination
auto CrossingStartLocations(const HexMap* hexMap, const CoordsSet& waterCrossingTiles) -> CoordsSet;
[[nodiscard]] auto CrossingStartLocations(const HexMap* hexMap, const CoordsSet& waterCrossingTiles)
-> CoordsSet;
// Returns the water tiles that, if they were bridged/frozen, would allow a unit to cross from
// origin to destination
auto WaterCrossingTiles(
[[nodiscard]] auto WaterCrossingTiles(
const Coords& origin,
const Coords& destination,
const HexMap* hexMap,
const APDCache& apdCache,
const BattalionTypeSPtr& battalionType) -> CoordsSet;
// Returns currently frozen water tiles that can act as an existing crossing from origin to
// destination.
[[nodiscard]] auto FrozenWaterCrossingTiles(
const Coords& origin,
const Coords& destination,
const HexMap* hexMap,
@@ -67,7 +78,7 @@ auto WaterCrossingTiles(
const BattalionTypeSPtr& battalionType) -> CoordsSet;
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
auto IntendedCrossingStarts(
[[nodiscard]] auto IntendedCrossingStarts(
const GameStateW& gameState,
const vector<UnitId>& unitIdsCreatingCrossing,
const CoordsSet& tilesToStartCrossingFrom,
@@ -75,7 +86,7 @@ auto IntendedCrossingStarts(
const BattalionTypeGetter& battalionTypeGetter) -> CoordsSet;
// Calculate score based on water crossing strategy
auto WaterCrossingScore(
[[nodiscard]] auto WaterCrossingScore(
PlayerId playerId,
const BattalionTypeGetter& battalionTypeGetter,
const GameStateW& gameState,
@@ -85,4 +96,4 @@ auto WaterCrossingScore(
} // namespace shardok
#endif // EAGLE0_AIWATERCROSSINGCALCULATOR_HPP
#endif // EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_CALCULATOR_HPP
@@ -15,7 +15,6 @@ namespace shardok {
using Unit = net::eagle0::shardok::storage::fb::Unit;
constexpr ScoreValue kNoRequiredCrossingScore = std::numeric_limits<ScoreValue>::max();
constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>::min();
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
const BattalionTypeGetter &battalionTypeGetter,
@@ -56,12 +55,26 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
const auto unitIdsCreatingCrossing =
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
ScoreValue totalScore = 0;
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
if (unitIdsCreatingCrossing.empty()) {
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
const Coords location = unit->location();
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
const int thisDistance =
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
totalScore -= thisDistance;
}
return totalScore;
}
// First put a big penalty on the distance for units that can create a crossing
for (const UnitId uid : unitIdsCreatingCrossing) {
const Unit *unit = gameState->units()->Get(uid);
@@ -157,7 +170,6 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
const auto unitIdsCreatingCrossing =
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
if (unitIdsCreatingCrossing.empty()) return startCrossingFrom;
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
@@ -177,6 +189,17 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
Coords destination = *destinations.begin();
AssertValid(destination, gameState->hex_map());
auto frozenWaterCrossingTiles = FrozenWaterCrossingTiles(
origin,
destination,
gameState->hex_map(),
apdCache,
battalionType);
startCrossingFrom +=
CrossingStartLocations(gameState->hex_map(), frozenWaterCrossingTiles);
if (unitIdsCreatingCrossing.empty()) continue;
auto waterCrossingTiles = WaterCrossingTiles(
origin,
destination,
@@ -192,4 +215,4 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
return startCrossingFrom;
}
} // namespace shardok
} // namespace shardok
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 1/5/22.
//
#ifndef EAGLE0_AIWATERCROSSINGCOMMANDCHOOSER_HPP
#define EAGLE0_AIWATERCROSSINGCOMMANDCHOOSER_HPP
#ifndef EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_COMMAND_CHOOSER_HPP
#define EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_COMMAND_CHOOSER_HPP
#include <utility>
@@ -44,4 +44,4 @@ public:
} // namespace shardok
#endif // EAGLE0_AIWATERCROSSINGCOMMANDCHOOSER_HPP
#endif // EAGLE0_SHARDOK_AI_AI_WATER_CROSSING_COMMAND_CHOOSER_HPP
@@ -5,6 +5,8 @@
#include "IterativeDeepeningAI.hpp"
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <limits>
#include <numeric>
#include <utility>
@@ -19,7 +21,21 @@
namespace shardok {
#define DEBUG_ITERATIVE_DEEPENING_TIMINGS 1
constexpr bool kDebugIterativeDeepeningTimings = true;
namespace {
void WriteFixed(std::ostream& output, const double value, const int precision) {
const auto flags = output.flags();
const auto originalPrecision = output.precision();
output << std::fixed << std::setprecision(precision) << value;
output.flags(flags);
output.precision(originalPrecision);
}
} // namespace
IterativeDeepeningAI::IterativeDeepeningAI(
const PlayerId playerId,
@@ -35,7 +51,7 @@ IterativeDeepeningAI::IterativeDeepeningAI(
castleCoords(castleCoords),
scorer(scorer),
apdCache(apdCache),
battalionTypeGetter(std::move(battalionTypeGetter)) {} // Move the function object
battalionTypeGetter(std::move(battalionTypeGetter)) {}
auto IterativeDeepeningAI::IterativeSearch(
const GameSettingsSPtr& settings,
@@ -54,9 +70,9 @@ auto IterativeDeepeningAI::IterativeSearch(
// DEBUG: Clear TT to see if that's causing the suspicious depth reaching
// g_transpositionTable.clear(); // Uncomment to test without cross-search caching
if (commands->empty()) {
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("ID AI: Commands are empty, returning early\n");
#endif
if (kDebugIterativeDeepeningTimings) {
std::cout << "ID AI: Commands are empty, returning early\n";
}
result.searchCompleted = true;
return result;
}
@@ -168,23 +184,23 @@ auto IterativeDeepeningAI::IterativeSearch(
// Log if best command changed from previous depth
if (currentDepth > 1 && currentBestCommand != previousBestCommand) {
#if DEBUG_ITERATIVE_DEEPENING_TIMINGS
printf("ID AI: Best command changed at depth %lu:\n", currentDepth);
printf(" Depth %lu best: command %zu (score %.2f) - type: %s\n",
currentDepth - 1,
previousBestCommand,
scoresByDepth[previousBestCommand][currentDepth - 1],
net::eagle0::shardok::common::CommandType_Name(
(*commands)[previousBestCommand]->GetCommandType())
.c_str());
printf(" Depth %lu best: command %zu (score %.2f) - type: %s\n",
currentDepth,
currentBestCommand,
currentBestScore,
net::eagle0::shardok::common::CommandType_Name(
(*commands)[currentBestCommand]->GetCommandType())
.c_str());
#endif
if (kDebugIterativeDeepeningTimings) {
std::cout << "ID AI: Best command changed at depth " << currentDepth << ":\n"
<< " Depth " << currentDepth - 1 << " best: command "
<< previousBestCommand << " (score ";
WriteFixed(std::cout, scoresByDepth[previousBestCommand][currentDepth - 1], 2);
std::cout << ") - type: "
<< net::eagle0::shardok::common::CommandType_Name(
(*commands)[previousBestCommand]->GetCommandType())
<< '\n'
<< " Depth " << currentDepth << " best: command "
<< currentBestCommand << " (score ";
WriteFixed(std::cout, currentBestScore, 2);
std::cout << ") - type: "
<< net::eagle0::shardok::common::CommandType_Name(
(*commands)[currentBestCommand]->GetCommandType())
<< '\n';
}
}
previousBestCommand = currentBestCommand;
@@ -239,9 +255,9 @@ auto IterativeDeepeningAI::IterativeSearch(
static_cast<double>(initialBudgetMs.count());
if (budgetUsedPercent > 0.5) {
printf("ID AI: Stopping after depth %lu - used %.1f%% of time budget\n",
currentDepth,
budgetUsedPercent * 100);
std::cout << "ID AI: Stopping after depth " << currentDepth << " - used ";
WriteFixed(std::cout, budgetUsedPercent * 100, 1);
std::cout << "% of time budget\n";
completionReason = EvaluationCompletionReason::NOT_ENOUGH_TIME_TO_CONTINUE;
break;
}
@@ -271,9 +287,9 @@ auto IterativeDeepeningAI::IterativeSearch(
// Validation: if completion reason is RAN_OUT_OF_COMMANDS, evaluation should be 100%
if (completionReason == EvaluationCompletionReason::RAN_OUT_OF_COMMANDS &&
result.commandCountEvaluated < result.availableCommandCount) {
printf("ERROR: Completion reason RAN_OUT_OF_COMMANDS but evaluation %lu/%zu < 100%%\n",
result.commandCountEvaluated,
result.availableCommandCount);
std::cerr << "ERROR: Completion reason RAN_OUT_OF_COMMANDS but evaluation "
<< result.commandCountEvaluated << '/' << result.availableCommandCount
<< " < 100%\n";
}
// Print TranspositionTable statistics
@@ -2,8 +2,8 @@
// Created by Dan Crosby on 07/04/25.
//
#ifndef EAGLE0_ITERATIVEDEEPENINGAI_HPP
#define EAGLE0_ITERATIVEDEEPENINGAI_HPP
#ifndef EAGLE0_SHARDOK_AI_ITERATIVE_DEEPENING_AI_HPP
#define EAGLE0_SHARDOK_AI_ITERATIVE_DEEPENING_AI_HPP
#include <chrono>
#include <future>
@@ -79,7 +79,7 @@ private:
CoordsSet castleCoords;
const AIScoreCalculator& scorer;
const APDCache& apdCache;
BattalionTypeGetter battalionTypeGetter; // Store by value, not reference!
BattalionTypeGetter battalionTypeGetter;
// Reusable vectors to reduce memory allocations
mutable std::vector<std::vector<ScoreValue>> scoresByDepth;
@@ -111,4 +111,4 @@ private:
} // namespace shardok
#endif // EAGLE0_ITERATIVEDEEPENINGAI_HPP
#endif // EAGLE0_SHARDOK_AI_ITERATIVE_DEEPENING_AI_HPP
@@ -8,8 +8,6 @@
#include "ShardokAIClient.hpp"
#define DEBUG_FLEE_DECISIONS
// Enable to dump game state and debug tree to /tmp for debugging
// #define ENABLE_MCTS_DEBUG_DUMP
@@ -17,9 +15,15 @@
#include <chrono>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#endif
#include <array>
#include <atomic>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <set>
#include <unordered_map>
@@ -43,6 +47,7 @@ using net::eagle0::shardok::api::ActionResultView;
using net::eagle0::shardok::api::GameStateView;
static constexpr bool kPerformanceLogging = true;
static constexpr bool kDebugFleeDecisions = true;
void ApplyUpdate(GameStateView & /*currentView*/, const ActionResultView & /*update*/) {}
@@ -175,25 +180,20 @@ auto ShardokAIClient::StandardChooseCommandIndex(
// Verify that the AI's guessed state produces the same available commands as reality
if (commandCount != realAvailableCommands->size()) {
fprintf(stderr,
"AI command count mismatch: guessed=%zu real=%zu player=%d round=%d\n",
commandCount,
realAvailableCommands->size(),
playerId,
guessedState->current_round());
std::cerr << "AI command count mismatch: guessed=" << commandCount
<< " real=" << realAvailableCommands->size() << " player=" << playerId
<< " round=" << guessedState->current_round() << "\n";
const auto maxDump = std::max(commandCount, realAvailableCommands->size());
for (size_t i = 0; i < maxDump; i++) {
const auto *realName = i < realAvailableCommands->size()
? net::eagle0::shardok::common::CommandType_Name(
(*realAvailableCommands)[i]->GetCommandType())
.c_str()
: "(none)";
const auto *guessedName = i < commandCount
? net::eagle0::shardok::common::CommandType_Name(
(*guessedCommands)[i]->GetCommandType())
.c_str()
: "(none)";
for (size_t i = 0; i < maxDump; ++i) {
const auto realName = i < realAvailableCommands->size()
? net::eagle0::shardok::common::CommandType_Name(
(*realAvailableCommands)[i]->GetCommandType())
: "(none)";
const auto guessedName = i < commandCount
? net::eagle0::shardok::common::CommandType_Name(
(*guessedCommands)[i]->GetCommandType())
: "(none)";
const int realActor = i < realAvailableCommands->size()
? (*realAvailableCommands)[i]->GetActorUnitId()
: -1;
@@ -207,30 +207,23 @@ auto ShardokAIClient::StandardChooseCommandIndex(
: -1;
const int guessedRow = i < commandCount ? (*guessedCommands)[i]->GetTargetRow() : -1;
const int guessedCol = i < commandCount ? (*guessedCommands)[i]->GetTargetColumn() : -1;
fprintf(stderr,
" [%zu] real: %s unit=%d (%d,%d) guessed: %s unit=%d (%d,%d)\n",
i,
realName,
realActor,
realRow,
realCol,
guessedName,
guessedActor,
guessedRow,
guessedCol);
std::cerr << " [" << i << "] real: " << realName << " unit=" << realActor << " ("
<< realRow << "," << realCol << ") guessed: " << guessedName
<< " unit=" << guessedActor << " (" << guessedRow << "," << guessedCol
<< ")\n";
}
// Find positions in real commands that are missing from guessed commands
// Group by unit to find the missing position per unit
std::unordered_map<int, std::set<std::pair<int, int>>> realPositions;
std::unordered_map<int, std::set<std::pair<int, int>>> guessedPositions;
for (size_t i = 0; i < realAvailableCommands->size(); i++) {
for (size_t i = 0; i < realAvailableCommands->size(); ++i) {
const auto &cmd = (*realAvailableCommands)[i];
realPositions[cmd->GetActorUnitId()].emplace(
cmd->GetTargetRow(),
cmd->GetTargetColumn());
}
for (size_t i = 0; i < commandCount; i++) {
for (size_t i = 0; i < commandCount; ++i) {
const auto &cmd = (*guessedCommands)[i];
guessedPositions[cmd->GetActorUnitId()].emplace(
cmd->GetTargetRow(),
@@ -239,52 +232,41 @@ auto ShardokAIClient::StandardChooseCommandIndex(
for (const auto &[unitId, positions] : realPositions) {
for (const auto &[row, col] : positions) {
if (!guessedPositions[unitId].contains({row, col})) {
fprintf(stderr,
" Missing from guessed: unit=%d pos=(%d,%d)",
unitId,
row,
col);
std::cerr << " Missing from guessed: unit=" << unitId << " pos=(" << row << ","
<< col << ")";
const Coords missingCoords(row, col);
const auto *occupant = guessedState.GetOccupant(missingCoords);
if (occupant) {
fprintf(stderr,
" -> guessed occupant: unit_id=%d player=%d status=%d "
"loc=(%d,%d) hidden=%d battalion_size=%d\n",
occupant->unit_id(),
occupant->player_id(),
static_cast<int>(occupant->status()),
occupant->location().row(),
occupant->location().column(),
occupant->hidden(),
occupant->battalion().size());
std::cerr << " -> guessed occupant: unit_id=" << occupant->unit_id()
<< " player=" << occupant->player_id()
<< " status=" << static_cast<int>(occupant->status()) << " loc=("
<< occupant->location().row() << ","
<< occupant->location().column()
<< ") hidden=" << occupant->hidden()
<< " battalion_size=" << occupant->battalion().size() << "\n";
} else {
fprintf(stderr, " -> no occupant in guessed state\n");
std::cerr << " -> no occupant in guessed state\n";
}
}
}
}
// Dump all guessed state units for full picture
fprintf(stderr, " Guessed state units (%u total):\n", guessedState->units()->size());
for (size_t i = 0; i < guessedState->units()->size(); i++) {
std::cerr << " Guessed state units (" << guessedState->units()->size() << " total):\n";
for (size_t i = 0; i < guessedState->units()->size(); ++i) {
const auto *u = guessedState->units()->Get(static_cast<unsigned int>(i));
fprintf(stderr,
" unit_id=%d player=%d status=%d loc=(%d,%d) hidden=%d "
"starting_pos_idx=%d\n",
u->unit_id(),
u->player_id(),
static_cast<int>(u->status()),
u->location().row(),
u->location().column(),
u->hidden(),
u->starting_position_index());
std::cerr << " unit_id=" << u->unit_id() << " player=" << u->player_id()
<< " status=" << static_cast<int>(u->status()) << " loc=("
<< u->location().row() << "," << u->location().column()
<< ") hidden=" << u->hidden()
<< " starting_pos_idx=" << u->starting_position_index() << "\n";
}
throw ShardokInternalErrorException(
"AI command count mismatch: guessed=" + std::to_string(commandCount) +
" real=" + std::to_string(realAvailableCommands->size()));
}
for (size_t i = 0; i < commandCount; i++) {
for (size_t i = 0; i < commandCount; ++i) {
CheckCommand((*realAvailableCommands)[i], (*guessedCommands)[i]);
}
@@ -357,9 +339,9 @@ auto ShardokAIClient::StandardChooseCommandIndex(
// Write the flatbuffer game state to file using SaveTo method
if (guessedState.SaveTo(statePath)) {
printf("Game state dumped to: %s\n", statePath.c_str());
std::cout << "Game state dumped to: " << statePath << '\n';
} else {
printf("Failed to dump game state to: %s\n", statePath.c_str());
std::cout << "Failed to dump game state to: " << statePath << '\n';
}
#endif // ENABLE_MCTS_DEBUG_DUMP
@@ -397,19 +379,17 @@ auto ShardokAIClient::StandardChooseCommandIndex(
if constexpr (kPerformanceLogging) {
if (result.commandCountEvaluated < result.availableCommandCount) {
printf("ID AI: Depth %d - evaluated %lu/%zu commands\n",
result.depthAchieved,
result.commandCountEvaluated,
result.availableCommandCount);
std::cout << "ID AI: Depth " << result.depthAchieved << " - evaluated "
<< result.commandCountEvaluated << '/' << result.availableCommandCount
<< " commands\n";
}
const auto chosenCommandType =
(*realAvailableCommands)[result.chosenIndex]->GetCommandType();
printf("ID AI: Search complete - achieved depth %d for best command %zu (%s)\n",
result.depthAchieved,
result.chosenIndex,
net::eagle0::shardok::common::CommandType_Name(chosenCommandType).c_str());
std::cout << "ID AI: Search complete - achieved depth " << result.depthAchieved
<< " for best command " << result.chosenIndex << " ("
<< net::eagle0::shardok::common::CommandType_Name(chosenCommandType) << ")\n";
fflush(stdout);
std::cout << std::flush;
}
return result;
@@ -468,12 +448,7 @@ auto ShardokAIClient::FinalRoundAttackerChooseCommandIndex(
maxRounds,
minimumFleeOddsThreshold,
desperateFleeThreshold,
#ifdef DEBUG_FLEE_DECISIONS
true // Enable debug logging
#else
false
#endif
);
kDebugFleeDecisions);
if (fleeDecision.shouldFlee) {
CommandChoiceResults results{};
@@ -493,8 +468,10 @@ auto ShardokAIClient::ChooseCommandIndex(
const GameSettingsSPtr &settings,
const GameStateView &gsv,
const CommandListSPtr &realAvailableCommands) const -> CommandChoiceResults {
static int typeChosenCount[net::eagle0::shardok::common::CommandType_MAX + 1];
static int totalChoices = 0;
static std::array<std::atomic<int>, net::eagle0::shardok::common::CommandType_MAX + 1>
typeChosenCount;
static std::atomic<int> totalChoices = 0;
static std::mutex reportMutex;
CommandChoiceResults results{};
@@ -512,24 +489,27 @@ auto ShardokAIClient::ChooseCommandIndex(
}
const auto chosenType = (*realAvailableCommands)[results.chosenIndex]->GetCommandType();
typeChosenCount[static_cast<int>(chosenType)]++;
totalChoices++;
typeChosenCount[static_cast<size_t>(chosenType)].fetch_add(1, std::memory_order_relaxed);
const int updatedTotal = totalChoices.fetch_add(1, std::memory_order_relaxed) + 1;
if (totalChoices % 1000 == 0) {
printf("TYPES CHOSEN:\n");
if (updatedTotal % 1000 == 0) {
std::lock_guard lock(reportMutex);
std::cout << "TYPES CHOSEN:\n";
vector<std::pair<int, CommandType>> choices{};
for (int i = 0; i <= net::eagle0::shardok::common::CommandType_MAX; i++) {
if (typeChosenCount[i] != 0) {
choices.emplace_back(typeChosenCount[i], static_cast<CommandType>(i));
for (int i = 0; i <= net::eagle0::shardok::common::CommandType_MAX; ++i) {
if (const int count =
typeChosenCount[static_cast<size_t>(i)].load(std::memory_order_relaxed);
count != 0) {
choices.emplace_back(count, static_cast<CommandType>(i));
}
}
std::ranges::sort(choices);
std::ranges::reverse(choices);
for (const auto &[index, choice] : choices) {
printf("%5d %s\n", index, CommandType_Name(choice).c_str());
std::cout << std::setw(5) << index << ' ' << CommandType_Name(choice) << '\n';
}
printf("\n\n");
std::cout << "\n\n";
}
return results;
@@ -539,7 +519,7 @@ auto ShardokAIClient::ChooseCommandIndex(const ShardokEngine &engine) const
-> CommandChoiceResults {
if (const auto &availableCommands = engine.GetAvailableCommandsForAIPlayer(playerId);
availableCommands->empty()) {
printf("no commands for player %d\n", playerId);
std::cout << "no commands for player " << playerId << '\n';
throw ShardokInternalErrorException(
"Asked to choose a command, but there are none available");
} else {
@@ -6,8 +6,8 @@
// Copyright © 2017 none. All rights reserved.
//
#ifndef ShardokAIClient_hpp
#define ShardokAIClient_hpp
#ifndef EAGLE0_SHARDOK_AI_SHARDOK_AI_CLIENT_HPP
#define EAGLE0_SHARDOK_AI_SHARDOK_AI_CLIENT_HPP
#include <vector>
@@ -95,4 +95,4 @@ public:
};
} // namespace shardok
#endif /* ShardokAIClient_hpp */
#endif // EAGLE0_SHARDOK_AI_SHARDOK_AI_CLIENT_HPP
@@ -4,9 +4,9 @@
#include "TranspositionTable.hpp"
#include <cinttypes>
#include <cstdio>
#include <cstring>
#include <iomanip>
#include <iostream>
namespace shardok {
@@ -18,7 +18,7 @@ TranspositionTable::TranspositionTable() : table(TABLE_SIZE) {
clear();
}
uint64_t TranspositionTable::hashGameState(const GameStateW& state) const {
auto TranspositionTable::hashGameState(const GameStateW& state) const -> uint64_t {
// The FlatBuffer is contiguous in memory and units are sorted by ID,
// so we can just hash the raw bytes for order-independent hashing
// Use ComputeFNV1aHash to avoid creating a string copy
@@ -38,8 +38,10 @@ TranspositionTable::probe(const GameStateW& state, int depth, PlayerId player) {
uint64_t stored_hash = entry.hash_full.load(std::memory_order_relaxed);
uint8_t stored_depth = entry.depth.load(std::memory_order_relaxed);
uint8_t stored_player = entry.player_id.load(std::memory_order_relaxed);
uint16_t stored_age = entry.age.load(std::memory_order_relaxed);
if (stored_hash == hash && stored_depth >= depth && stored_player == player) {
if (stored_hash == hash && stored_depth >= depth && stored_player == player &&
stored_age == current_age.load(std::memory_order_relaxed)) {
stats.hits++;
float score = entry.score.load(std::memory_order_relaxed);
return static_cast<ScoreValue>(score);
@@ -100,15 +102,23 @@ void TranspositionTable::clear() {
current_age = 0;
}
void TranspositionTable::printStats() const {
printf("TranspositionTable Stats:\n");
printf(" Probes: %" PRIu64 "\n", stats.probes.load());
printf(" Hits: %" PRIu64 " (%.1f%%)\n", stats.hits.load(), stats.hitRate());
printf(" Stores: %" PRIu64 "\n", stats.stores.load());
printf(" Collisions: %" PRIu64 "\n", stats.collisions.load());
printf(" Table size: %zu entries (%.1f MB)\n",
TABLE_SIZE,
(TABLE_SIZE * sizeof(TTEntry)) / (1024.0 * 1024.0));
void TranspositionTable::printStats() const { printStats(std::cout); }
void TranspositionTable::printStats(std::ostream& output) const {
const auto flags = output.flags();
const auto precision = output.precision();
output << "TranspositionTable Stats:\n"
<< " Probes: " << stats.probes.load() << '\n'
<< " Hits: " << stats.hits.load() << " (" << std::fixed << std::setprecision(1)
<< stats.hitRate() << "%)\n"
<< " Stores: " << stats.stores.load() << '\n'
<< " Collisions: " << stats.collisions.load() << '\n'
<< " Table size: " << TABLE_SIZE << " entries ("
<< (TABLE_SIZE * sizeof(TTEntry)) / (1024.0 * 1024.0) << " MB)\n";
output.flags(flags);
output.precision(precision);
}
} // namespace shardok
} // namespace shardok
@@ -2,12 +2,13 @@
// TranspositionTable.hpp - Cache for game state evaluations to avoid redundant calculations
//
#ifndef EAGLE0_TRANSPOSITIONTABLE_HPP
#define EAGLE0_TRANSPOSITIONTABLE_HPP
#ifndef EAGLE0_SHARDOK_AI_TRANSPOSITION_TABLE_HPP
#define EAGLE0_SHARDOK_AI_TRANSPOSITION_TABLE_HPP
#include <atomic>
#include <cstdint>
#include <optional>
#include <ostream>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
@@ -27,8 +28,8 @@ public:
std::atomic<uint64_t> stores{0};
std::atomic<uint64_t> collisions{0};
double hitRate() const {
uint64_t p = probes.load();
[[nodiscard]] auto hitRate() const -> double {
const uint64_t p = probes.load();
return p > 0 ? (100.0 * hits.load() / p) : 0.0;
}
@@ -59,7 +60,7 @@ private:
std::atomic<uint16_t> current_age{0};
// Hash function for FlatBuffer game state
uint64_t hashGameState(const GameStateW& state) const;
[[nodiscard]] auto hashGameState(const GameStateW& state) const -> uint64_t;
public:
TranspositionTable();
@@ -77,10 +78,11 @@ public:
void incrementAge() { current_age++; }
// Get statistics
const Stats& getStats() const { return stats; }
[[nodiscard]] auto getStats() const -> const Stats& { return stats; }
// Print statistics to stdout
void printStats() const;
void printStats(std::ostream& output) const;
};
// Global instance for the AI to use
@@ -88,4 +90,4 @@ extern TranspositionTable g_transpositionTable;
} // namespace shardok
#endif // EAGLE0_TRANSPOSITIONTABLE_HPP
#endif // EAGLE0_SHARDOK_AI_TRANSPOSITION_TABLE_HPP
@@ -2,8 +2,8 @@
// Shardok-specific MCTS AI that wraps the abstract implementation
//
#ifndef EAGLE0_SHARDOK_MCTSAI_HPP
#define EAGLE0_SHARDOK_MCTSAI_HPP
#ifndef EAGLE0_SHARDOK_AI_MCTS_SHARDOK_MCTSAI_HPP
#define EAGLE0_SHARDOK_AI_MCTS_SHARDOK_MCTSAI_HPP
#include <memory>
#include <vector>
@@ -68,4 +68,4 @@ private:
} // namespace shardok
#endif // EAGLE0_SHARDOK_MCTSAI_HPP
#endif // EAGLE0_SHARDOK_AI_MCTS_SHARDOK_MCTSAI_HPP
@@ -2,8 +2,8 @@
// Shardok-specific action adapter for MCTS
//
#ifndef EAGLE0_SHARDOK_ACTION_HPP
#define EAGLE0_SHARDOK_ACTION_HPP
#ifndef EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_ACTION_HPP
#define EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_ACTION_HPP
#include <memory>
#include <string>
@@ -44,6 +44,7 @@ public:
[[nodiscard]] PlayerId getPlayer() const { return player_; }
[[nodiscard]] int getActorId() const { return actorId_; }
[[nodiscard]] std::pair<int, int> getTarget() const { return {targetRow_, targetCol_}; }
[[nodiscard]] bool hasOdds() const { return hasOdds_; }
private:
// Store only essential fields (~25 bytes, all POD, cache-friendly)
@@ -58,4 +59,4 @@ private:
} // namespace shardok::mcts
#endif // EAGLE0_SHARDOK_ACTION_HPP
#endif // EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_ACTION_HPP
@@ -6,6 +6,10 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <numeric>
#include "ShardokAction.hpp"
@@ -30,9 +34,47 @@ auto IsDefenderPlayer(const GameStateW& gameState, const PlayerId playerId) -> b
return false;
}
auto LegalActionsCacheKey(const uint64_t stateHash, const bool rootIsDefender) -> uint64_t {
auto MixCacheKey(uint64_t seed, const uint64_t value) -> uint64_t {
constexpr uint64_t kHashCombineConstant = 0x9e3779b97f4a7c15ULL;
seed ^= value + kHashCombineConstant + (seed << 6) + (seed >> 2);
return seed;
}
auto PointerIdentityHash(const void* ptr) -> uint64_t {
return static_cast<uint64_t>(reinterpret_cast<std::uintptr_t>(ptr));
}
auto LegalActionsCacheKey(
const uint64_t stateHash,
const bool rootIsDefender,
const GameSettings* gameSettings,
const APDCache* apdCache) -> uint64_t {
constexpr uint64_t kDefenderRoleHash = 0x9e3779b97f4a7c15ULL;
return rootIsDefender ? stateHash ^ kDefenderRoleHash : stateHash;
uint64_t key = rootIsDefender ? stateHash ^ kDefenderRoleHash : stateHash;
// The cached action set is the output of AICommandFilter, not just the raw command list.
// Include the context objects that can affect filtering so a process-wide cache entry from
// one search cannot be reused by a different settings/APD context with the same state hash.
key = MixCacheKey(key, PointerIdentityHash(gameSettings));
key = MixCacheKey(key, PointerIdentityHash(apdCache != nullptr ? apdCache->get() : nullptr));
return key;
}
auto SortActionsByWeight(
std::vector<std::unique_ptr<MCTSAction>> actions,
const std::vector<double>& weights) -> std::vector<std::unique_ptr<MCTSAction>> {
std::vector<size_t> sortedIndices(actions.size());
std::iota(sortedIndices.begin(), sortedIndices.end(), 0);
std::ranges::sort(sortedIndices, [&weights](size_t a, size_t b) {
return weights[a] > weights[b];
});
std::vector<std::unique_ptr<MCTSAction>> sortedActions;
sortedActions.reserve(actions.size());
for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }
return sortedActions;
}
} // namespace
@@ -53,6 +95,10 @@ std::atomic<uint64_t> ShardokGameEngine::cacheMisses_{0};
std::atomic<uint64_t> ShardokGameEngine::timeInHashComputation_{0};
std::atomic<uint64_t> ShardokGameEngine::timeInLegalActionsComputation_{0};
namespace {
std::once_flag legalActionsCacheReserveOnce;
} // namespace
ShardokGameEngine::ShardokGameEngine(
[[maybe_unused]] const ShardokEngine* engine,
const AIScoreCalculator* scoreCalculator,
@@ -73,7 +119,9 @@ ShardokGameEngine::ShardokGameEngine(
criticalTileCoords_(criticalTileCoords) {
// Thread-local cache is automatically initialized per thread
// Reserve space to reduce rehashing (based on profiling: ~30-50K unique states per search)
legalActionsCache_.reserve(100000);
std::call_once(legalActionsCacheReserveOnce, [] {
ShardokGameEngine::legalActionsCache_.reserve(100000);
});
}
std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
@@ -87,27 +135,15 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
// Use cached engine if available (avoids recomputing GetAvailableCommands for same state)
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
// Clone the cached engine to preserve command cache
engine = std::make_shared<ShardokEngine>(*cachedEngine);
} else {
// Create fresh engine and populate command cache
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate command cache (result intentionally unused, just populating cache)
[[maybe_unused]] const auto commands =
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Cache the engine for future use with this state
shardokState->setCachedEngine(engine);
// Clone it for applying the action (don't mutate the cached engine)
engine = std::make_shared<ShardokEngine>(*engine);
}
auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate the AI command list before posting so the MCTS action index maps to the
// no-follow-up command ordering used by getLegalActions().
[[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Create deterministic random generator if a specific roll is requested
// deterministicRoll of -1.0 (default) means use random generator
@@ -168,11 +204,6 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
*alCache_,
criticalTileCoords_);
// Cache the engine on the new state so score() can use it for END_TURN normalization
// The engine's command list may be stale after the action was applied, but that's OK -
// we'll refresh it when we call GetAvailableCommandsForAIPlayer() in score()
newState->setCachedEngine(engine);
// Don't pre-compute hash - let it be computed lazily on first use
// Many states (especially in simulation) never need their hash computed
return newState;
@@ -192,28 +223,18 @@ void ShardokGameEngine::applyActionMutable(
const auto currentPlayer = static_cast<PlayerId>(state->currentPlayerId());
// Use cached engine if available
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
engine = std::make_shared<ShardokEngine>(*cachedEngine);
} else {
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate command cache (result intentionally unused, just populating cache)
[[maybe_unused]] const auto commands =
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
shardokState->setCachedEngine(engine);
engine = std::make_shared<ShardokEngine>(*engine);
}
auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate the AI command list before posting so the MCTS action index maps to the
// no-follow-up command ordering used by getLegalActions().
[[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
engine->PostCommand(currentPlayer, shardokAction->getIndex(), nullptr);
shardokState->getMutableShardokState() = engine->GetCurrentGameState();
// Clear the cached engine and hash since the state has been mutated
shardokState->setCachedEngine(nullptr);
shardokState->invalidateHashCache();
}
@@ -239,7 +260,8 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Time hash computation
const auto hashStart = std::chrono::high_resolution_clock::now();
const uint64_t stateHash = shardokState->hash();
const uint64_t cacheKey = LegalActionsCacheKey(stateHash, isDefender_);
const uint64_t cacheKey =
LegalActionsCacheKey(stateHash, isDefender_, gameSettings_.get(), apdCache_);
const auto hashEnd = std::chrono::high_resolution_clock::now();
timeInHashComputation_.fetch_add(
std::chrono::duration_cast<std::chrono::microseconds>(hashEnd - hashStart).count(),
@@ -247,52 +269,49 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Check transposition table for cached legal actions
if (auto it = legalActionsCache_.find(cacheKey); it != legalActionsCache_.end()) {
cacheHits_.fetch_add(1, std::memory_order_relaxed);
const auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_);
// Use cached engine
shardokState->setCachedEngine(it->second.engine);
const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Get commands from the cached engine (Engine already caches these internally)
const CommandListSPtr commands =
it->second.engine->GetAvailableCommandsForAIPlayer(currentPlayer);
if (!commands || commands->empty()) { return {}; }
// Convert to MCTSActions using stored filtered indices
std::vector<std::unique_ptr<MCTSAction>> actions;
actions.reserve(it->second.filteredIndices.size());
for (const size_t origIdx : it->second.filteredIndices) {
if (origIdx < commands->size()) {
const auto& cmd = commands->at(origIdx);
// Extract essential fields directly from command (no proto conversion!)
actions.push_back(std::make_unique<ShardokAction>(
origIdx,
cmd->GetCommandType(),
cmd->GetPlayerId(),
cmd->GetActorUnitId(),
cmd->GetTargetRow(),
cmd->GetTargetColumn(),
cmd->HasOdds()));
bool cacheEntryMatches = commands != nullptr && commands->size() == it->second.commandCount;
for (const auto& cachedAction : it->second.actions) {
const size_t commandIndex = cachedAction.getIndex();
if (!cacheEntryMatches || commandIndex >= commands->size()) {
cacheEntryMatches = false;
break;
}
const auto& command = commands->at(commandIndex);
const auto [targetRow, targetColumn] = cachedAction.getTarget();
cacheEntryMatches = command->GetCommandType() ==
static_cast<net::eagle0::shardok::common::CommandType>(
cachedAction.getType()) &&
command->GetPlayerId() == cachedAction.getPlayer() &&
command->GetActorUnitId() == cachedAction.getActorId() &&
command->GetTargetRow() == targetRow &&
command->GetTargetColumn() == targetColumn &&
command->HasOdds() == cachedAction.hasOdds();
if (!cacheEntryMatches) { break; }
}
// Sort actions by weight (descending) to ensure MCTS explores high-value actions first
const std::vector<double> weights = getActionWeights(actions, state);
if (cacheEntryMatches) {
cacheHits_.fetch_add(1, std::memory_order_relaxed);
std::vector<size_t> sortedIndices(actions.size());
std::iota(sortedIndices.begin(), sortedIndices.end(), 0);
// Convert immutable cached action snapshots to MCTS action instances.
std::vector<std::unique_ptr<MCTSAction>> actions;
actions.reserve(it->second.actions.size());
std::sort(sortedIndices.begin(), sortedIndices.end(), [&weights](size_t a, size_t b) {
return weights[a] > weights[b];
});
for (const auto& cachedAction : it->second.actions) {
actions.push_back(cachedAction.clone());
}
std::vector<std::unique_ptr<MCTSAction>> sortedActions;
sortedActions.reserve(actions.size());
for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }
return sortedActions;
// Sort actions by weight (descending) to ensure MCTS explores high-value actions first
const std::vector<double> weights = getActionWeights(actions, state);
return SortActionsByWeight(std::move(actions), weights);
}
}
cacheMisses_.fetch_add(1, std::memory_order_relaxed);
@@ -300,17 +319,10 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Time legal actions computation
const auto actionsStart = std::chrono::high_resolution_clock::now();
// Use cached engine if available, otherwise create and cache it
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
engine = cachedEngine;
} else {
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_);
shardokState->setCachedEngine(engine);
}
const auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_);
const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
@@ -331,20 +343,24 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Convert only filtered commands to MCTSActions
std::vector<std::unique_ptr<MCTSAction>> actions;
actions.reserve(filteredIndices.size());
std::vector<ShardokAction> cachedActions;
cachedActions.reserve(filteredIndices.size());
for (const size_t idx : filteredIndices) {
if (idx < commands->size()) {
const auto& cmd = commands->at(idx);
// Extract essential fields directly from command (no proto conversion!)
actions.push_back(std::make_unique<ShardokAction>(
ShardokAction action(
idx,
cmd->GetCommandType(),
cmd->GetPlayerId(),
cmd->GetActorUnitId(),
cmd->GetTargetRow(),
cmd->GetTargetColumn(),
cmd->HasOdds()));
cmd->HasOdds());
actions.push_back(action.clone());
cachedActions.push_back(action);
}
}
@@ -353,20 +369,7 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// get explored deeply. Original indices are preserved in ShardokAction::getIndex()
const std::vector<double> weights = getActionWeights(actions, state);
// Create index vector for sorting
std::vector<size_t> sortedIndices(actions.size());
std::iota(sortedIndices.begin(), sortedIndices.end(), 0);
// Sort indices by weight (descending)
std::sort(sortedIndices.begin(), sortedIndices.end(), [&weights](size_t a, size_t b) {
return weights[a] > weights[b];
});
// Reorder actions according to sorted indices
std::vector<std::unique_ptr<MCTSAction>> sortedActions;
sortedActions.reserve(actions.size());
for (size_t idx : sortedIndices) { sortedActions.push_back(std::move(actions[idx])); }
actions = std::move(sortedActions);
actions = SortActionsByWeight(std::move(actions), weights);
const auto actionsEnd = std::chrono::high_resolution_clock::now();
timeInLegalActionsComputation_.fetch_add(
@@ -374,20 +377,20 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
.count(),
std::memory_order_relaxed);
// Store in transposition table for future lookups
// Note: We only store filtered indices and the engine (which caches commands internally)
// This avoids duplicating heavy protocol buffer objects
// Store immutable action snapshots in the transposition table for future lookups.
// Do not store ShardokEngine here: its command cache is mutable and not safe to share
// across MCTS worker threads.
// Use lazy_emplace_l to ensure thread-safe insertion (locks the bucket during construction)
legalActionsCache_.lazy_emplace_l(
cacheKey,
[&](typename decltype(legalActionsCache_)::value_type& v) {
// Update existing entry
v.second.filteredIndices = filteredIndices;
v.second.engine = engine;
v.second.commandCount = commands->size();
v.second.actions = cachedActions;
},
[&](const typename decltype(legalActionsCache_)::constructor& ctor) {
// Create new entry
ctor(cacheKey, LegalActionsCache{filteredIndices, engine});
ctor(cacheKey, LegalActionsCache{commands->size(), cachedActions});
});
return actions;
@@ -404,9 +407,8 @@ std::vector<size_t> ShardokGameEngine::filterActions(
const MCTSGameState& /*state*/) const {
// All filtering is already done in getLegalActions() using AICommandFilter
// This method is used by simulation policies and doesn't need additional filtering
std::vector<size_t> indices;
indices.reserve(actions.size());
for (size_t i = 0; i < actions.size(); ++i) { indices.push_back(i); }
std::vector<size_t> indices(actions.size());
std::iota(indices.begin(), indices.end(), 0);
return indices;
}
@@ -421,15 +423,7 @@ std::vector<double> ShardokGameEngine::getActionWeights(
"indicates a type mismatch in the MCTS adapter layer");
}
// Get cached engine and command list for looking up command protos
auto cachedEngine = shardokState->getCachedEngine();
if (!cachedEngine) {
throw MCTSInternalError(
"ShardokGameEngine::getActionWeights called with state that has no cached engine");
}
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
const CommandListSPtr commands = cachedEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Determine if current player is defender (not root player!)
// During simulation we need to use the correct perspective for action weighting
@@ -448,20 +442,13 @@ std::vector<double> ShardokGameEngine::getActionWeights(
"indicates a type mismatch in the MCTS adapter layer");
}
// Look up command proto from cached engine using action's index
const size_t cmdIndex = shardokAction->getIndex();
if (cmdIndex >= commands->size()) {
throw MCTSInternalError(
"ShardokGameEngine::getActionWeights: action index out of bounds");
}
const auto& cmd = commands->at(cmdIndex);
const auto [targetRow, targetColumn] = shardokAction->getTarget();
weights.push_back(AIHeuristicWeighting::GetCommandWeight(
cmd->GetCommandType(),
cmd->GetActorUnitId(),
cmd->GetPlayerId(),
Coords{cmd->GetTargetRow(), cmd->GetTargetColumn()},
static_cast<net::eagle0::shardok::common::CommandType>(shardokAction->getType()),
shardokAction->getActorId(),
shardokAction->getPlayer(),
Coords{static_cast<int8_t>(targetRow), static_cast<int8_t>(targetColumn)},
gameState,
castleCoords_,
apdCache_,
@@ -495,7 +482,7 @@ bool ShardokGameEngine::shouldStopSearch(
size_t ShardokGameEngine::mapFilteredIndexToOriginal(
size_t filteredIndex,
const MCTSGameState& state) const {
// Get the filtered actions (uses cached engine)
// Get the filtered actions.
auto actions = getLegalActions(state, state.currentPlayerId(), 0, 0);
// Check bounds
@@ -509,7 +496,9 @@ size_t ShardokGameEngine::mapFilteredIndexToOriginal(
return shardokAction->getIndex();
}
void ShardokGameEngine::reportCacheStatistics() const {
void ShardokGameEngine::reportCacheStatistics() const { reportCacheStatistics(std::cout); }
void ShardokGameEngine::reportCacheStatistics(std::ostream& output) const {
const uint64_t hits = cacheHits_.load(std::memory_order_relaxed);
const uint64_t misses = cacheMisses_.load(std::memory_order_relaxed);
const uint64_t hashTime = timeInHashComputation_.load(std::memory_order_relaxed);
@@ -523,27 +512,29 @@ void ShardokGameEngine::reportCacheStatistics() const {
const double avgActionsTimeUs =
misses > 0 ? static_cast<double>(actionsTime) / static_cast<double>(misses) : 0.0;
printf("Legal Actions Cache Stats:\n");
printf(" Lookups: %llu hits, %llu misses, %.1f%% hit rate, %zu entries\n",
static_cast<unsigned long long>(hits),
static_cast<unsigned long long>(misses),
hitRate * 100.0,
legalActionsCache_.size());
printf(" Timing: %.2f us avg hash, %.2f us avg actions (on miss)\n",
avgHashTimeUs,
avgActionsTimeUs);
printf(" Total time: %.2f ms in hash, %.2f ms in actions\n",
hashTime / 1000.0,
actionsTime / 1000.0);
// Calculate if transposition table is worth it
const double timeWithCache = hashTime + actionsTime;
const double timeWithoutCache =
avgActionsTimeUs * static_cast<double>(totalLookups); // All lookups recompute
const double savings = (timeWithoutCache - timeWithCache) / timeWithoutCache * 100.0;
printf(" Cache savings: %.1f%% vs. no cache (%.2f ms saved)\n",
savings,
(timeWithoutCache - timeWithCache) / 1000.0);
const auto flags = output.flags();
const auto precision = output.precision();
output << "Legal Actions Cache Stats:\n"
<< " Lookups: " << hits << " hits, " << misses << " misses, " << std::fixed
<< std::setprecision(1) << hitRate * 100.0 << "% hit rate, "
<< legalActionsCache_.size() << " entries\n"
<< " Timing: " << std::setprecision(2) << avgHashTimeUs << " us avg hash, "
<< avgActionsTimeUs << " us avg actions (on miss)\n"
<< " Total time: " << hashTime / 1000.0 << " ms in hash, " << actionsTime / 1000.0
<< " ms in actions\n"
<< " Cache savings: " << std::setprecision(1) << savings << "% vs. no cache ("
<< std::setprecision(2) << (timeWithoutCache - timeWithCache) / 1000.0
<< " ms saved)\n";
output.flags(flags);
output.precision(precision);
}
}
@@ -593,22 +584,12 @@ ChanceOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
// Get or create the engine for this state
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
engine = cachedEngine;
} else {
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate command cache
[[maybe_unused]] const auto commands =
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
shardokState->setCachedEngine(engine);
}
const auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Get command descriptors
const auto descriptors = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
@@ -2,18 +2,20 @@
// Shardok-specific game engine adapter for MCTS
//
#ifndef EAGLE0_SHARDOK_GAME_ENGINE_HPP
#define EAGLE0_SHARDOK_GAME_ENGINE_HPP
#ifndef EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_GAME_ENGINE_HPP
#define EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_GAME_ENGINE_HPP
#include <atomic>
#include <functional>
#include <gtl/phmap.hpp>
#include <memory>
#include <ostream>
#include <vector>
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/mcts/adapters/ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
@@ -92,16 +94,18 @@ public:
// Report transposition table statistics
void reportCacheStatistics() const;
void reportCacheStatistics(std::ostream& output) const;
// Reset cache statistics
void resetCacheStatistics();
private:
// Transposition table entry for caching legal actions
// Note: We don't store command protos since the Engine already caches them
// Transposition table entry for caching legal actions.
// Store immutable action snapshots only; ShardokEngine owns mutable command caches and
// must remain per-state/per-thread.
struct LegalActionsCache {
std::vector<size_t> filteredIndices;
std::shared_ptr<ShardokEngine> engine; // Engine with populated command cache
size_t commandCount;
std::vector<ShardokAction> actions;
};
const AIScoreCalculator* scoreCalculator_;
@@ -141,4 +145,4 @@ public:
} // namespace mcts
} // namespace shardok
#endif // EAGLE0_SHARDOK_GAME_ENGINE_HPP
#endif // EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_GAME_ENGINE_HPP
@@ -98,7 +98,6 @@ std::unique_ptr<MCTSGameState> ShardokGameState::clone() const {
apdCache_,
alCache_,
criticalTileCoords_);
// Don't copy the cached engine - each state needs its own
return cloned;
}
@@ -2,15 +2,14 @@
// Shardok-specific game state adapter for MCTS
//
#ifndef EAGLE0_SHARDOK_GAME_STATE_HPP
#define EAGLE0_SHARDOK_GAME_STATE_HPP
#ifndef EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_GAME_STATE_HPP
#define EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_GAME_STATE_HPP
#include <memory>
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameState.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -52,10 +51,6 @@ public:
[[nodiscard]] const GameSettings* getSettings() const { return settings_; }
[[nodiscard]] const CoordsSet& getCriticalTileCoords() const { return criticalTileCoords_; }
// Engine caching for performance (avoids recomputing available commands)
void setCachedEngine(std::shared_ptr<ShardokEngine> engine) const { cachedEngine_ = engine; }
[[nodiscard]] std::shared_ptr<ShardokEngine> getCachedEngine() const { return cachedEngine_; }
// Invalidate hash cache when state is mutated
void invalidateHashCache() const {
hashCached_ = false;
@@ -74,10 +69,9 @@ private:
mutable uint64_t cachedHash_ = 0;
mutable bool hashCached_ = false;
const CoordsSet criticalTileCoords_; // Own the data to avoid dangling references
mutable std::shared_ptr<ShardokEngine> cachedEngine_; // Engine with cached available commands
};
} // namespace mcts
} // namespace shardok
#endif // EAGLE0_SHARDOK_GAME_STATE_HPP
#endif // EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_GAME_STATE_HPP
@@ -2,8 +2,8 @@
// Factory for creating Shardok-specific MCTS components
//
#ifndef EAGLE0_SHARDOK_MCTS_FACTORY_HPP
#define EAGLE0_SHARDOK_MCTS_FACTORY_HPP
#ifndef EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_MCTS_FACTORY_HPP
#define EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_MCTS_FACTORY_HPP
#include <functional>
#include <memory>
@@ -67,4 +67,4 @@ public:
} // namespace mcts
} // namespace shardok
#endif // EAGLE0_SHARDOK_MCTS_FACTORY_HPP
#endif // EAGLE0_SHARDOK_AI_MCTS_ADAPTERS_SHARDOK_MCTS_FACTORY_HPP
@@ -2,10 +2,8 @@
// Created by dancrosby on 3/4/20.
//
#ifndef EAGLE0_AISCORECALCULATOR_HPP
#define EAGLE0_AISCORECALCULATOR_HPP
#include <future>
#ifndef EAGLE0_SHARDOK_AI_SCORE_AI_SCORE_CALCULATOR_HPP
#define EAGLE0_SHARDOK_AI_SCORE_AI_SCORE_CALCULATOR_HPP
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
@@ -16,8 +14,6 @@
namespace shardok {
using shardok::PlayerId;
using std::future;
using std::vector;
using ScoreValue = double;
@@ -53,4 +49,4 @@ public:
} // namespace shardok
#endif // EAGLE0_AISCORECALCULATOR_HPP
#endif // EAGLE0_SHARDOK_AI_SCORE_AI_SCORE_CALCULATOR_HPP
@@ -133,8 +133,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
const auto cc = gameState->hex_map()->column_count();
const auto occupants = Occupants(*gameState->units(), rc, cc);
for (const Coords& criticalTileLocation : criticalTileLocations) {
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
for (const auto index : criticalTileLocations.indexIterator()) {
const auto& occupant = occupants[index];
if (occupant && occupant->battalion().type() !=
@@ -160,6 +159,8 @@ auto AttackerHoldsCriticalTilesVictoryScore(
ActionPoints braveWaterCost) -> ScoreValue {
vector<const Unit*> playerUnits{};
vector<const Unit*> claimablePlayerUnits{};
playerUnits.reserve(gameState->units()->size());
claimablePlayerUnits.reserve(gameState->units()->size());
for (const Unit* unit : *gameState->units()) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->player_id() == player->player_id()) {
@@ -260,6 +261,8 @@ auto LastPlayerStandingVictoryScore(
vector<const Unit*> playerUnits{};
vector<const Unit*> otherUnits{};
playerUnits.reserve(gameState->units()->size());
otherUnits.reserve(gameState->units()->size());
for (const Unit* unit : *gameState->units()) {
if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;

Some files were not shown because too many files have changed in this diff Show More