Compare commits

...
Author SHA1 Message Date
admin e79e8fc61c Show tooltip for representative Shardok command 2026-06-28 15:58:59 -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
712 changed files with 27157 additions and 20667 deletions
+3
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
@@ -37,6 +38,8 @@ 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
+10 -18
View File
@@ -2,25 +2,17 @@ Checks: >
-*,
bugprone-*,
performance-*,
readability-*,
modernize-*,
cppcoreguidelines-*,
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,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-type-const-cast,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-vararg,
-modernize-use-trailing-return-type,
-readability-convert-member-functions-to-static,
-readability-function-cognitive-complexity,
-readability-identifier-length,
-readability-magic-numbers
-bugprone-branch-clone,
-bugprone-signed-char-misuse
WarningsAsErrors: ''
HeaderFilterRegex: 'src/(main|test)/cpp/net/eagle0/(shardok|common)/.*'
FormatStyle: file
+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
@@ -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
+24
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
-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
+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)
Binary file not shown.
+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[@]}"
+2 -1
View File
@@ -10,13 +10,14 @@ 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 -std=c++23"
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
+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}"
@@ -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;
@@ -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; }
@@ -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);
}
@@ -12,6 +12,7 @@
#include <cstddef>
#include <memory>
#include <random>
#include <stdexcept>
#include <vector>
class RandomGenerator;
@@ -41,6 +42,7 @@ public:
template<class T>
auto RandomElement(const std::vector<T>& vec) -> T {
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()))));
}
@@ -9,7 +9,9 @@
#ifndef EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
#define EAGLE0_COMMON_SEQUENCE_RANDOM_GENERATOR_HPP
#include <cmath>
#include <memory>
#include <stdexcept>
#include <utility>
#include <vector>
@@ -29,14 +31,24 @@ private:
size_t position = 0;
auto DoubleZeroToOne() -> double override {
if (position >= sequence.size()) { position = 0; }
const double nextVal = sequence.at(position);
position++;
if (position >= sequence.size()) { position = 0; }
return nextVal;
}
public:
explicit SequenceRandomGenerator(std::vector<double> s) : sequence(std::move(s)) {}
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);
+90 -59
View File
@@ -9,18 +9,24 @@
#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,34 +177,41 @@ 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;
}
@@ -209,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) {
@@ -37,7 +37,7 @@ int main(const int argc, char** argv) {
void ModifyMap(HexMapProto& map) {
const int terrainCount = map.terrain_size();
for (int i = 0; i < terrainCount; i++) {
for (int i = 0; i < terrainCount; ++i) {
const auto existingTerrainType = map.terrain(i).type();
const auto newTerrain =
static_cast<net::eagle0::shardok::common::Terrain_Type>(existingTerrainType + 2);
@@ -37,16 +37,17 @@ auto CalculateMap(
.name = mapName,
.positionsRequiringCrossing = {}};
for (std::size_t 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()->empty()) continue;
if (positionList->positions()->size() != 10) {
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 auto* startingPosition : *positions) {
for (const shardok::Coords& castlePosition : criticalTileLocations) {
if (!shardok::CanReach(
*startingPosition,
@@ -57,7 +58,7 @@ auto CalculateMap(
battalionType)) {
++positionsRequiringCrossing;
break;
};
}
}
}
info.positionsRequiringCrossing[static_cast<int>(i)] = positionsRequiringCrossing;
@@ -18,7 +18,7 @@ struct alignas(64) OneMapInfo {
std::map<int, int> positionsRequiringCrossing;
};
auto CalculateMap(
[[nodiscard]] auto CalculateMap(
const std::string& mapName,
const std::shared_ptr<shardok::ActionPointDistancesCache>& apdCache,
const shardok::BattalionTypeSPtr& battalionType) -> OneMapInfo;
@@ -32,8 +32,10 @@ auto main(const int argc, char** argv) -> int {
const shardok::BattalionTypeSPtr& battalionType = gameSettings->GetGetter().GetBattalionType(
net::eagle0::shardok::storage::fb::BattalionTypeId_LIGHT_INFANTRY);
const std::vector<std::string> mapNames = GetMapNames();
std::vector<OneMapInfo> mapInfos{};
for (const std::string& mapName : GetMapNames()) {
mapInfos.reserve(mapNames.size());
for (const std::string& mapName : mapNames) {
std::shared_ptr<shardok::ActionPointDistancesCache> apdCache =
std::make_shared<shardok::ActionPointDistancesCache>();
@@ -3,6 +3,7 @@
//
#include <algorithm>
#include <array>
#include <climits>
#include <iostream>
#include <memory>
@@ -10,6 +11,8 @@
#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"
@@ -44,25 +47,27 @@ 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 mapName = fileNames[randInt(0, (int)fileNames.size() - 1)];
auto month = randInt(1, 12);
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);
auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
const auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
std::vector<shardok::Unit> p1Units{};
for (int i = 0; i < p1UnitCount; i++) {
p1Units.reserve(p1UnitCount);
for (int i = 0; i < p1UnitCount; ++i) {
auto unit = units[i];
unit.mutate_player_id(0);
p1Units.push_back(unit);
p1Units.push_back(std::move(unit));
}
const auto maxP2UnitCount = std::min<size_t>(20, units.size() - p1UnitCount);
auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
const auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
std::vector<shardok::Unit> p2Units{};
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; i++) {
p2Units.reserve(p2UnitCount);
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; ++i) {
auto unit = units[i];
unit.mutate_player_id(1);
p2Units.push_back(unit);
p2Units.push_back(std::move(unit));
}
std::cout << "map name is " << mapName << ", " << p1UnitCount << " attacking " << p2UnitCount
@@ -75,32 +80,34 @@ void runBattle(const std::vector<shardok::Unit>& units) {
VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING,
VictoryConditionProto::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS};
auto p1 = shardok::PlayerInfoWithUnits(
std::vector<shardok::PlayerInfoWithUnits> playerInfos;
playerInfos.reserve(2);
playerInfos.emplace_back(
0,
true,
5,
false,
10000,
attVictoryConditions,
{},
std::vector<shardok::PlayerId>{},
p1Units,
false);
auto p2 = shardok::PlayerInfoWithUnits(
playerInfos.emplace_back(
1,
true,
7,
true,
10000,
defVictoryConditions,
{},
std::vector<shardok::PlayerId>{},
p2Units,
false);
std::string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
const std::string gameId = std::to_string(randInt(INT_MIN, INT_MAX));
manager->UnlockedCreateSpecifiedGame(
gameId,
{p1, p2},
playerInfos,
mapName,
"",
month,
@@ -135,10 +142,7 @@ auto LoadUnits() -> std::vector<shardok::Unit> {
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]);
@@ -152,9 +156,9 @@ auto main(int /*argc*/, char** argv) -> int {
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);
}
@@ -25,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;
}
}
@@ -113,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,
@@ -153,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());
@@ -162,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) {
@@ -178,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);
@@ -194,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;
@@ -214,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());
@@ -27,7 +27,7 @@ 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 {
@@ -39,7 +39,7 @@ struct TargetPriorityList {
priorityOrder(po) {}
};
auto EffectiveDistance(
[[nodiscard]] auto EffectiveDistance(
const Unit* unit,
const HexMap* map,
const AttackLocations& attackLocations,
@@ -47,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,
@@ -55,14 +55,14 @@ 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(
[[nodiscard]] auto GenerateTargetPriorities(
const std::vector<const Unit*>& occupants,
const HexMap* map,
const CoordsSet& targets,
@@ -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
@@ -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
@@ -11,6 +11,8 @@
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include "AICommandFilter.hpp"
#include "TranspositionTable.hpp"
@@ -21,16 +23,49 @@
namespace shardok {
// No need to forward declare internal functions - use the public interface instead
// Helper constants and static variables
static const std::vector<double> kAverageSequence = {0.5};
static const auto kAverageGenerator = std::make_shared<SequenceRandomGenerator>(kAverageSequence);
constexpr bool kMultithread = true;
constexpr bool kLogging = false;
// Helper function to determine if a command type is deterministic
[[nodiscard]] static auto AverageGenerator() -> std::shared_ptr<SequenceRandomGenerator> {
return std::make_shared<SequenceRandomGenerator>(std::vector{0.5});
}
[[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());
}
static auto IsDeterministic(const CommandType type) -> bool {
switch (type) {
case net::eagle0::shardok::common::MOVE_COMMAND:
@@ -43,13 +78,11 @@ 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;
@@ -63,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 {
@@ -83,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,
@@ -138,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;
@@ -199,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;
@@ -221,22 +300,33 @@ 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 constexpr (kMultithread) {
@@ -327,18 +417,30 @@ auto AICommandEvaluator::FindBestCommand(
// 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;
@@ -356,7 +458,7 @@ auto AICommandEvaluator::FindBestCommand(
originalIndex,
remainingLookahead,
maxRepeatCount,
kAverageGenerator,
AverageGenerator(),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -370,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,
@@ -378,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,
@@ -392,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,
@@ -422,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)};
@@ -432,7 +535,7 @@ auto AICommandEvaluator::FindBestCommand(
originalIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(sequence),
std::make_shared<SequenceRandomGenerator>(std::move(sequence)),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -459,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;
}
@@ -511,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,
kAverageGenerator,
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;
@@ -541,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,
@@ -554,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,
@@ -575,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
@@ -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;
};
@@ -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(
@@ -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;
}
@@ -13,7 +13,7 @@
namespace shardok {
auto DefenderDistanceBuf(
[[nodiscard]] auto DefenderDistanceBuf(
const Coords &defenderLocation,
const HexMap *hexMap,
const MapId &mapId,
@@ -5,18 +5,32 @@
#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
@@ -19,6 +19,8 @@ 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();
@@ -154,8 +156,8 @@ 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();
@@ -164,67 +166,31 @@ auto AIFleeDecisionCalculator::EvaluateFleeVsFight(
std::cout << "AI FinalRound: Evaluating flee (odds=" << fleeSuccessChance << "%)...\n";
}
// Check if flee odds are good enough to attempt
if (fleeSuccessChance >= minimumFleeOddsThreshold) {
if (enableDebugLogging) {
std::cout << "AI FinalRound: Good flee odds (" << fleeSuccessChance
<< "% >= " << minimumFleeOddsThreshold << "%), choosing flee\n";
}
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) {
std::cout << "AI FinalRound: Combat hopeless (";
std::cout << "AI FinalRound: Combat not certain (";
WritePercent(std::cout, combatWinChance * 100);
std::cout << "%), desperate flee attempt (" << fleeSuccessChance << "%)\n";
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) {
std::cout << "AI FinalRound: Flee=" << fleeSuccessChance << "%, Combat=";
std::cout << "AI FinalRound: Combat effectively certain (";
WritePercent(std::cout, combatWinChance * 100);
std::cout << "%, Threshold=";
WritePercent(std::cout, adjustedCombatThreshold * 100);
std::cout << "% -> ";
}
if (fleeChance > adjustedCombatThreshold) {
if (enableDebugLogging) { std::cout << "FLEE (better odds)\n"; }
return FleeDecision{
true,
GetFleeCommandIndex(fleeCommand, availableCommands),
"Flee has better expected outcome"};
} else {
if (enableDebugLogging) { std::cout << "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 << "%), fighting\n";
}
return FleeDecision{
false,
0, // Will be replaced by StandardChooseCommandIndex
"Combat effectively certain"};
}
auto AIFleeDecisionCalculator::ShouldConsiderFleeing(
@@ -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
@@ -18,12 +18,12 @@ struct CoordsAndDistance {
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;
@@ -20,12 +20,13 @@ 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
@@ -26,11 +26,13 @@ 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
@@ -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,10 +44,10 @@ 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;
@@ -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(
@@ -17,9 +17,9 @@ using net::eagle0::shardok::storage::fb::Unit;
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,
@@ -32,7 +32,7 @@ auto GetRangedAttackBonus(
double minVigorToCast,
ActionPoints archeryActionPointCost = 0) -> double;
auto UnitValue(
[[nodiscard]] auto UnitValue(
const Unit *unit,
bool isAttacker,
const std::vector<const Unit *> &attackerUnits,
@@ -18,7 +18,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 +155,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 +169,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);
@@ -31,7 +31,7 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
}
// 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,
@@ -39,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,
@@ -56,11 +56,12 @@ 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,
@@ -68,7 +69,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,
@@ -76,7 +77,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,
@@ -51,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,
@@ -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;
@@ -19,8 +19,11 @@
#include <sstream>
#endif
#include <array>
#include <atomic>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <set>
#include <unordered_map>
@@ -182,7 +185,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
<< " round=" << guessedState->current_round() << "\n";
const auto maxDump = std::max(commandCount, realAvailableCommands->size());
for (size_t i = 0; i < maxDump; i++) {
for (size_t i = 0; i < maxDump; ++i) {
const auto realName = i < realAvailableCommands->size()
? net::eagle0::shardok::common::CommandType_Name(
(*realAvailableCommands)[i]->GetCommandType())
@@ -214,13 +217,13 @@ auto ShardokAIClient::StandardChooseCommandIndex(
// 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(),
@@ -250,7 +253,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
// Dump all guessed state units for full picture
std::cerr << " Guessed state units (" << guessedState->units()->size() << " total):\n";
for (size_t i = 0; i < guessedState->units()->size(); i++) {
for (size_t i = 0; i < guessedState->units()->size(); ++i) {
const auto *u = guessedState->units()->Get(static_cast<unsigned int>(i));
std::cerr << " unit_id=" << u->unit_id() << " player=" << u->player_id()
<< " status=" << static_cast<int>(u->status()) << " loc=("
@@ -263,7 +266,7 @@ auto ShardokAIClient::StandardChooseCommandIndex(
"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]);
}
@@ -465,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{};
@@ -484,15 +489,18 @@ 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) {
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));
}
}
@@ -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);
@@ -28,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;
}
@@ -60,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();
@@ -78,7 +78,7 @@ 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;
@@ -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)
@@ -6,8 +6,10 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <numeric>
#include "ShardokAction.hpp"
@@ -32,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
@@ -55,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,
@@ -75,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(
@@ -89,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
@@ -170,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;
@@ -194,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();
}
@@ -241,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(),
@@ -249,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);
@@ -302,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);
@@ -333,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);
}
}
@@ -355,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(
@@ -376,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;
@@ -406,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;
}
@@ -423,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
@@ -450,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_,
@@ -497,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
@@ -599,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);
@@ -15,6 +15,7 @@
#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"
@@ -99,11 +100,12 @@ public:
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_;
@@ -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;
}
@@ -10,7 +10,6 @@
#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,7 +69,6 @@ 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
@@ -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;
@@ -23,7 +23,7 @@ using net::eagle0::shardok::storage::fb::PlayerInfo;
using ScoreValue = double;
auto AttackerHoldsCriticalTilesVictoryScore(
[[nodiscard]] auto AttackerHoldsCriticalTilesVictoryScore(
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const PlayerInfo* player,
@@ -32,12 +32,12 @@ auto AttackerHoldsCriticalTilesVictoryScore(
const BattalionTypeGetter& battalionTypeGetter,
ActionPoints braveWaterCost) -> ScoreValue;
auto DefenderHoldsCriticalTilesVictoryScore(
[[nodiscard]] auto DefenderHoldsCriticalTilesVictoryScore(
const GameStateW& gameState,
const CoordsSet& criticalTileLocations,
const PlayerInfo* player) -> ScoreValue;
auto LastPlayerStandingVictoryScore(
[[nodiscard]] auto LastPlayerStandingVictoryScore(
const GameStateW& gameState,
const PlayerInfo* player,
const APDCache& apdCache,
@@ -230,7 +230,7 @@ auto MakeMCTSOptimizedAIScoreCalculator(
std::vector<BattalionTypeSPtr> battalionTypes(BattalionTypeId::BattalionTypeId_MAX + 1);
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
typeId <= BattalionTypeId::BattalionTypeId_MAX;
typeId++) {
++typeId) {
auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
battalionTypes[battalionTypeId] = settingsGetter.GetBattalionType(battalionTypeId);
}
@@ -144,6 +144,10 @@ private:
public:
AttackerScoreTimer() : startTime(std::chrono::high_resolution_clock::now()) {}
AttackerScoreTimer(const AttackerScoreTimer &) = delete;
auto operator=(const AttackerScoreTimer &) -> AttackerScoreTimer & = delete;
AttackerScoreTimer(AttackerScoreTimer &&) = delete;
auto operator=(AttackerScoreTimer &&) -> AttackerScoreTimer & = delete;
~AttackerScoreTimer() {
auto endTime = std::chrono::high_resolution_clock::now();
@@ -352,7 +356,7 @@ auto MakeStandardAIScoreCalculatorWithExperiment(
std::vector<BattalionTypeSPtr> battalionTypes(BattalionTypeId::BattalionTypeId_MAX + 1);
for (int typeId = BattalionTypeId::BattalionTypeId_MIN;
typeId <= BattalionTypeId::BattalionTypeId_MAX;
typeId++) {
++typeId) {
auto battalionTypeId = static_cast<BattalionTypeId>(typeId);
battalionTypes[battalionTypeId] = settingsGetter.GetBattalionType(battalionTypeId);
}
@@ -7,8 +7,7 @@
#include "src/main/cpp/net/eagle0/shardok/ai/AIScoreUtilities.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
namespace shardok {
namespace score_calculator_internal {
namespace shardok::score_calculator_internal {
auto EffectiveDistanceCache::GetOrCompute(
const Unit* unit,
@@ -125,5 +124,4 @@ auto AttackerMultiplierForTargetDistance(
isLateGame);
}
} // namespace score_calculator_internal
} // namespace shardok
} // namespace shardok::score_calculator_internal
@@ -24,8 +24,7 @@
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map_generated.h"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit_generated.h"
namespace shardok {
namespace score_calculator_internal {
namespace shardok::score_calculator_internal {
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
using Unit = net::eagle0::shardok::storage::fb::Unit;
@@ -51,13 +50,13 @@ struct EffectiveDistanceCache {
struct CacheKey {
UnitId unitId;
Coords target;
bool operator==(const CacheKey& other) const {
[[nodiscard]] bool operator==(const CacheKey& other) const {
return unitId == other.unitId && target == other.target;
}
};
struct CacheKeyHash {
size_t operator()(const CacheKey& key) const {
[[nodiscard]] size_t operator()(const CacheKey& key) const {
return std::hash<UnitId>{}(key.unitId) ^ (std::hash<int>{}(key.target.row()) << 1) ^
(std::hash<int>{}(key.target.column()) << 2);
}
@@ -65,7 +64,7 @@ struct EffectiveDistanceCache {
mutable gtl::flat_hash_map<CacheKey, DIST_T, CacheKeyHash> cache;
DIST_T GetOrCompute(
[[nodiscard]] DIST_T GetOrCompute(
const Unit* unit,
const Coords& target,
const ActionPointDistances* notBravingApd,
@@ -75,11 +74,12 @@ struct EffectiveDistanceCache {
/// Calculate score for FLEE strategy
/// Returns negative score based on fleeing units
auto FleeStrategyScoreForState(const GameStateW& gameState, PlayerId playerId) -> ScoreValue;
[[nodiscard]] auto FleeStrategyScoreForState(const GameStateW& gameState, PlayerId playerId)
-> ScoreValue;
/// Calculate attacker multiplier based on distance to priority targets
/// This is used to weight attacker units by their proximity to objectives
auto AttackerMultiplierForTargetDistance(
[[nodiscard]] auto AttackerMultiplierForTargetDistance(
const Unit* attackingUnit,
const std::vector<TargetAndAttackLocations>& priorityList,
const std::vector<const Unit*>& occupants,
@@ -89,7 +89,6 @@ auto AttackerMultiplierForTargetDistance(
const ActionPointDistances* bravingApd,
bool isLateGame) -> double;
} // namespace score_calculator_internal
} // namespace shardok
} // namespace shardok::score_calculator_internal
#endif // EAGLE0_SHARDOK_AI_SCORE_PRIVATE_AI_SCORE_CALCULATOR_SHARED_UTILITIES_HPP
@@ -47,19 +47,19 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
// Memoization cache for EffectiveDistance calls
EffectiveDistanceCache distanceCache;
// Early return for empty game states
const size_t estimatedUnitCount = cachedUnits->size();
if (estimatedUnitCount == 0) { return UnitsScoreComponents{0.0, 0.0}; }
std::vector<const Unit *> attackerUnits{};
std::vector<const Unit *> defenderUnits{};
// Pre-allocate vectors based on estimated unit ratios to avoid reallocations
const size_t estimatedUnitCount = cachedUnits->size();
attackerUnits.reserve(estimatedUnitCount - 1);
defenderUnits.reserve(estimatedUnitCount - 1);
attackerUnits.reserve(estimatedUnitCount);
defenderUnits.reserve(estimatedUnitCount);
double attackerUnitsValue = 0;
double defenderUnitsValue = 0;
// Early return for empty game states
if (cachedUnits->size() == 0) { return UnitsScoreComponents{0.0, 0.0}; }
auto occupants = Occupants(*cachedUnits, cachedRowCount, cachedColumnCount);
for (const Unit *unit : *cachedUnits) {
@@ -10,8 +10,7 @@
#include <iostream>
#include <sstream>
namespace shardok {
namespace ai_battle_simulator {
namespace shardok::ai_battle_simulator {
using net::eagle0::shardok::ai_battle_simulator::AIAlgorithmType;
using net::eagle0::shardok::ai_battle_simulator::PlayerConfig;
@@ -110,5 +109,4 @@ std::string AiBattleConfigLoader::ToJsonString(const BattleConfigProto& config)
return jsonString;
}
} // namespace ai_battle_simulator
} // namespace shardok
} // namespace shardok::ai_battle_simulator
@@ -13,8 +13,7 @@
#include "src/main/protobuf/net/eagle0/shardok/ai_battle_simulator/ai_battle_config.pb.h"
#pragma clang diagnostic pop
namespace shardok {
namespace ai_battle_simulator {
namespace shardok::ai_battle_simulator {
using BattleConfigProto = net::eagle0::shardok::ai_battle_simulator::BattleConfig;
@@ -41,7 +40,6 @@ public:
[[nodiscard]] static std::string ToJsonString(const BattleConfigProto& config);
};
} // namespace ai_battle_simulator
} // namespace shardok
} // namespace shardok::ai_battle_simulator
#endif // EAGLE0_SHARDOK_AI_BATTLE_SIMULATOR_AI_BATTLE_CONFIG_HPP
@@ -8,6 +8,7 @@
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <utility>
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
@@ -27,8 +28,7 @@
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
#pragma clang diagnostic pop
namespace shardok {
namespace ai_battle_simulator {
namespace shardok::ai_battle_simulator {
namespace {
@@ -330,9 +330,6 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
// Load map
auto hexMapProto = LoadMap(config_.map_name());
// Create player info protos
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;
// Attacker info
net::eagle0::shardok::common::PlayerInfo attackerInfo;
attackerInfo.set_player_id(ATTACKER_ID);
@@ -342,7 +339,6 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
attackerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
playerInfoProtos.push_back(attackerInfo);
// Defender info
net::eagle0::shardok::common::PlayerInfo defenderInfo;
@@ -353,10 +349,14 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING);
defenderInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS);
playerInfoProtos.push_back(defenderInfo);
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos{
std::move(attackerInfo),
std::move(defenderInfo)};
// Create units from config
std::vector<net::eagle0::shardok::storage::fb::Unit> units;
units.reserve(config_.attacker().units_size() + config_.defender().units_size());
// Attacker units
const int attackerUnitCount = config_.attacker().units_size();
@@ -450,7 +450,7 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
unit.mutable_opponent_knowledge()->Mutate(0, 0);
unit.mutable_opponent_knowledge()->Mutate(1, 0);
units.push_back(unit);
units.push_back(std::move(unit));
}
// Defender units
@@ -546,7 +546,7 @@ GameStateW AiBattleSimulator::CreateInitialGameState() const {
unit.mutable_opponent_knowledge()->Mutate(0, 0);
unit.mutable_opponent_knowledge()->Mutate(1, 0);
units.push_back(unit);
units.push_back(std::move(unit));
}
// Create game state
@@ -914,6 +914,7 @@ BattleResult AiBattleSimulator::CreateResultFromGameState(
// Collect surviving units (for winner, or all units if draw)
const auto* units = state->units();
if (units) {
result.survivingUnits.reserve(units->size());
for (const auto* unit : *units) {
if (!unit) { continue; }
@@ -957,5 +958,4 @@ BattleResult AiBattleSimulator::CreateResultFromGameState(
return result;
}
} // namespace ai_battle_simulator
} // namespace shardok
} // namespace shardok::ai_battle_simulator
@@ -30,7 +30,9 @@ class ShardokEngine;
// Type alias for hex map
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
namespace ai_battle_simulator {
} // namespace shardok
namespace shardok::ai_battle_simulator {
using BattleConfigProto = net::eagle0::shardok::ai_battle_simulator::BattleConfig;
@@ -146,7 +148,6 @@ private:
CreateResultFromGameState(const GameStateW& state, int totalRounds, int totalCommands) const;
};
} // namespace ai_battle_simulator
} // namespace shardok
} // namespace shardok::ai_battle_simulator
#endif // EAGLE0_SHARDOK_AI_BATTLE_SIMULATOR_AI_BATTLE_SIMULATOR_HPP
@@ -88,8 +88,7 @@ void GenerateConfigFile(const std::string& outputPath) {
// Output to file
std::ofstream outFile(outputPath);
if (!outFile.is_open()) {
std::cerr << "Error: Failed to open output file: " << outputPath << "\n";
std::exit(1);
throw std::runtime_error("Failed to open output file: " + outputPath);
}
outFile << jsonString << "\n";
std::cout << "Sample config written to: " << outputPath << "\n";
@@ -295,8 +294,7 @@ BattleResult RunBattle(
if (!config) {
restoreOutput();
std::cerr << "Error: Failed to load config file\n";
std::exit(1);
throw std::runtime_error("Failed to load config file");
}
// Create simulator
@@ -328,6 +326,8 @@ int main(int argc, char* argv[]) {
std::vector<std::string> configPaths;
std::vector<std::string> statePaths;
configPaths.reserve(argc - 1);
statePaths.reserve(argc - 1);
std::string outputPath;
std::string tracePath;
std::string runLabel;
@@ -2,7 +2,6 @@
// Converts saved Eagle ShardokBattle/GameState protobuf blobs into AI battle configs.
//
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <optional>
@@ -146,9 +145,9 @@ int main(int argc, char *argv[]) {
} else if (arg.starts_with("--output=")) {
outputPath = arg.substr(9);
} else if (arg.starts_with("--seed=")) {
seed = std::atoi(arg.substr(7).c_str());
seed = std::stoi(arg.substr(7));
} else if (arg.starts_with("--max-rounds=")) {
maxRounds = std::atoi(arg.substr(13).c_str());
maxRounds = std::stoi(arg.substr(13));
} else if (arg.starts_with("--attacker-scoring=")) {
const auto scoring = ParseScoring(arg.substr(19));
if (!scoring.has_value()) { throw std::runtime_error("Unknown attacker scoring"); }
@@ -115,6 +115,7 @@ int main(int argc, char *argv[]) {
std::string resultPath;
std::string stateFbPath;
std::vector<std::string> actionViewPaths;
actionViewPaths.reserve(argc - 1);
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
@@ -145,6 +146,9 @@ int main(int argc, char *argv[]) {
std::vector<int> attackerFactions;
std::vector<int> defenderFactions;
std::vector<std::string> playerSides;
attackerFactions.reserve(battle.players_size());
defenderFactions.reserve(battle.players_size());
playerSides.reserve(battle.players_size());
bool aiVsAi = true;
for (const auto &player : battle.players()) {
if (player.eagle_fid() == 3 || player.eagle_fid() == 4) { aiVsAi = false; }
@@ -187,7 +191,8 @@ int main(int argc, char *argv[]) {
}
std::cout << "],\"has_game_status\":" << (hasGameStatus ? "true" : "false") << "}\n";
return 0;
} else if (!resultPath.empty()) {
}
if (!resultPath.empty()) {
ActionResult result;
if (!result.ParseFromString(ReadFile(resultPath))) {
throw std::runtime_error("Failed to parse Shardok action result proto");
@@ -4,9 +4,10 @@
#include "AIPerformanceRunner.hpp"
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include "PerformanceTestGameStateBuilder.hpp"
@@ -47,7 +48,7 @@ auto CompletionReasonToString(EvaluationCompletionReason reason) -> std::string
/**
* Parse command line arguments into a configuration struct.
*/
auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
auto ParseCommandLineArgs(int argc, char* argv[]) -> std::optional<PerformanceTestConfig> {
PerformanceTestConfig config;
for (int i = 1; i < argc; ++i) {
@@ -63,7 +64,7 @@ auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
<< " --defender=BOOL AI is defender (default: false)\n"
<< " --verbose Enable verbose output\n"
<< " --help, -h Show this help message\n";
std::exit(0);
return std::nullopt;
} else if (arg.starts_with("--map=")) {
config.mapName = arg.substr(6);
} else if (arg.starts_with("--turns=")) {
@@ -74,9 +75,8 @@ auto ParseCommandLineArgs(int argc, char* argv[]) -> PerformanceTestConfig {
} else if (arg == "--verbose") {
config.verbose = true;
} else {
std::cerr << "Unknown argument: " << arg << "\n";
std::cerr << "Use --help for usage information.\n";
std::exit(1);
throw std::invalid_argument(
"Unknown argument: " + arg + "\nUse --help for usage information.");
}
}
@@ -96,7 +96,9 @@ int main(int argc, char* argv[]) {
std::cout << "==============================\n";
// Parse command line arguments
auto config = ParseCommandLineArgs(argc, argv);
const auto parsedConfig = ParseCommandLineArgs(argc, argv);
if (!parsedConfig.has_value()) { return 0; }
const auto& config = parsedConfig.value();
if (config.verbose) {
std::cout << "Configuration:\n";
@@ -163,6 +165,7 @@ int main(int argc, char* argv[]) {
std::cout << "Running AI performance test for " << config.numTurns << " turns...\n";
std::vector<AIPerformanceMetrics> metrics;
if (config.numTurns > 0) { metrics.reserve(config.numTurns); }
for (int turn = 0; turn < config.numTurns; ++turn) {
// Check if AI can make a move
@@ -4,7 +4,9 @@
#include "PerformanceTestGameStateBuilder.hpp"
#include <algorithm>
#include <filesystem>
#include <utility>
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai_testing_common/GameSettingsFactory.hpp"
@@ -53,6 +55,7 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
// Create player info protos
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfoProtos;
playerInfoProtos.reserve(2);
// AI player
net::eagle0::shardok::common::PlayerInfo aiPlayerInfo;
@@ -68,7 +71,7 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
aiPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
}
playerInfoProtos.push_back(aiPlayerInfo);
playerInfoProtos.push_back(std::move(aiPlayerInfo));
// Human player
net::eagle0::shardok::common::PlayerInfo humanPlayerInfo;
@@ -84,13 +87,16 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
humanPlayerInfo.add_victory_conditions(
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
}
playerInfoProtos.push_back(humanPlayerInfo);
playerInfoProtos.push_back(std::move(humanPlayerInfo));
// Create units
const int clampedAiUnitCount = std::clamp(aiUnitCount, 0, 6);
const int clampedHumanUnitCount = std::clamp(humanUnitCount, 0, 6);
std::vector<net::eagle0::shardok::storage::fb::Unit> units;
units.reserve(clampedAiUnitCount + clampedHumanUnitCount);
// Create AI units in reserve (location -1, -1)
for (int i = 0; i < aiUnitCount && i < 6; ++i) {
for (int i = 0; i < clampedAiUnitCount; ++i) {
units.push_back(AddGenericUnit(
AI_PLAYER_ID,
i, // Unit ID
@@ -101,10 +107,10 @@ auto PerformanceTestGameStateBuilder::CreateCustomTestGameState(
}
// Create human units in reserve (location -1, -1)
for (int i = 0; i < humanUnitCount && i < 6; ++i) {
for (int i = 0; i < clampedHumanUnitCount; ++i) {
units.push_back(AddGenericUnit(
HUMAN_PLAYER_ID,
aiUnitCount + i, // Unit ID starting aiUnitCount
clampedAiUnitCount + i, // Unit ID starting after generated AI units
net::eagle0::shardok::storage::fb::Coords(-1, -1), // Reserve location
NO_PROFESSION,
HEAVY_INFANTRY_BATTALION_TYPE,
@@ -235,4 +241,4 @@ auto PerformanceTestGameStateBuilder::AddGenericUnit(
return unit;
}
} // namespace shardok
} // namespace shardok
@@ -32,7 +32,7 @@ public:
* Initialize game settings from the default configuration files.
* Must be called before creating game states.
*/
static auto InitializeGameSettings() -> GameSettingsSPtr;
[[nodiscard]] static auto InitializeGameSettings() -> GameSettingsSPtr;
/**
* Create the standard "Perf" test configuration:
@@ -44,7 +44,7 @@ public:
* @param defenderToggle If true, AI is defender; if false, AI is attacker
* @return A GameStateW with the configured battle
*/
static auto CreatePerfTestGameState(
[[nodiscard]] static auto CreatePerfTestGameState(
const GameSettingsSPtr& settings,
bool defenderToggle = false) -> GameStateW;
@@ -58,7 +58,7 @@ public:
* @param defenderToggle If true, AI is defender; if false, AI is attacker
* @return A GameStateW with the configured battle
*/
static auto CreateCustomTestGameState(
[[nodiscard]] static auto CreateCustomTestGameState(
const GameSettingsSPtr& settings,
const std::string& mapName,
int aiUnitCount,
@@ -67,11 +67,11 @@ public:
private:
// Helper functions for building game state components
static auto
[[nodiscard]] static auto
AddPlayerInfo(flatbuffers::FlatBufferBuilder& fbb, int playerId, bool isDefender, int food)
-> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo>;
static auto AddGenericUnit(
[[nodiscard]] static auto AddGenericUnit(
PlayerId playerId,
UnitId unitId,
const net::eagle0::shardok::storage::fb::Coords& location,
@@ -7,8 +7,7 @@
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSTypes.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
namespace shardok {
namespace ai_testing_common {
namespace shardok::ai_testing_common {
auto AIClientFactory::Create(
PlayerId playerId,
@@ -30,5 +29,4 @@ auto AIClientFactory::Create(
mctsConfig);
}
} // namespace ai_testing_common
} // namespace shardok
} // namespace shardok::ai_testing_common
@@ -54,7 +54,7 @@ public:
* @param mctsConfig MCTS settings, used only when algorithmType is MCTS
* @return Unique pointer to created ShardokAIClient
*/
static auto Create(
[[nodiscard]] static auto Create(
PlayerId playerId,
bool isDefender,
const net::eagle0::shardok::storage::fb::HexMap* hexMap,
@@ -8,8 +8,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
namespace shardok {
namespace ai_testing_common {
namespace shardok::ai_testing_common {
auto GamePhaseRunner::RunSetupPhase(
ShardokEngine& engine,
@@ -58,5 +57,4 @@ auto GamePhaseRunner::RunSingleTurn(ShardokEngine& engine, PlayerId playerId, Sh
return true;
}
} // namespace ai_testing_common
} // namespace shardok
} // namespace shardok::ai_testing_common
@@ -9,8 +9,7 @@
#include "src/main/cpp/net/eagle0/common/byte_vector.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/BattalionTypeRegistrar.hpp"
namespace shardok {
namespace ai_testing_common {
namespace shardok::ai_testing_common {
auto GameSettingsFactory::CreateDefault() -> GameSettingsSPtr {
auto settings = std::make_shared<GameSettings>();
@@ -31,5 +30,4 @@ auto GameSettingsFactory::CreateDefault() -> GameSettingsSPtr {
return settings;
}
} // namespace ai_testing_common
} // namespace shardok
} // namespace shardok::ai_testing_common
@@ -10,8 +10,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
namespace shardok {
namespace ai_testing_common {
namespace shardok::ai_testing_common {
/**
* Factory class for creating GameSettings instances with standard configuration.
@@ -30,10 +29,9 @@ public:
*
* @return Shared pointer to initialized GameSettings
*/
static auto CreateDefault() -> GameSettingsSPtr;
[[nodiscard]] static auto CreateDefault() -> GameSettingsSPtr;
};
} // namespace ai_testing_common
} // namespace shardok
} // namespace shardok::ai_testing_common
#endif // EAGLE0_SHARDOK_AI_TESTING_COMMON_GAME_SETTINGS_FACTORY_HPP
@@ -14,6 +14,7 @@
#include <iostream>
#include <iterator>
#include <ranges>
#include <sstream>
#include <string>
#include <thread>
@@ -24,14 +25,80 @@
namespace shardok {
using net::eagle0::common::ScopedShardokLatencyTrace;
using std::condition_variable;
using std::mutex;
using std::scoped_lock;
using std::unique_lock;
using std::weak_ptr;
using net::eagle0::shardok::common::GameStatus;
namespace {
[[nodiscard]] 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(); }
try {
out << " proto={" << command->GetCommandProto().ShortDebugString() << '}';
} catch (const std::exception &e) { out << " proto_error={" << e.what() << '}'; }
return out.str();
}
[[nodiscard]] auto DescribeCommandsForLog(const CommandList &commands) -> std::string {
std::ostringstream out;
out << "available_command_count=" << commands.size();
for (size_t i = 0; i < commands.size(); ++i) {
out << "\n [" << i << "] " << DescribeCommandForLog(commands[i]);
}
return out.str();
}
[[nodiscard]] auto DescribeCurrentStateForLog(const ShardokEngine &engine) -> std::string {
const auto *state = engine.GetCurrentGameState().Get();
if (state == nullptr) { return "state=<null>"; }
const auto vectorSize = [](const auto *vector) -> size_t {
return vector == nullptr ? 0 : vector->size();
};
std::ostringstream out;
out << "state={round=" << static_cast<int>(state->current_round())
<< " current_player=" << static_cast<int>(state->current_player())
<< " history_count=" << engine.GetUnfilteredHistoryCount();
if (const auto *status = state->status(); status != nullptr) {
out << " status=" << static_cast<int>(status->state()) << " end_condition=";
if (const auto *endCondition = status->end_game_condition(); endCondition != nullptr) {
out << "{type=" << static_cast<int>(endCondition->victory_type())
<< " victory_details=" << static_cast<int>(endCondition->victory_details())
<< " draw_details=" << static_cast<int>(endCondition->draw_details()) << '}';
} else {
out << "<null>";
}
} else {
out << " status=<null>";
}
out << " units=" << vectorSize(state->units())
<< " player_infos=" << vectorSize(state->player_infos());
if (const auto *hexMap = state->hex_map(); hexMap != nullptr) {
out << " map=" << hexMap->row_count() << 'x' << hexMap->column_count();
} else {
out << " map=<null>";
}
out << '}';
return out.str();
}
} // namespace
class InvalidTokenException : public std::exception {};
class MismatchedPlayerIdException : public std::exception {
@@ -66,9 +133,9 @@ public:
[[nodiscard]] auto what() const noexcept -> const char * override { return message.c_str(); }
};
vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
const unique_ptr<ShardokEngine> &e) {
vector<shared_ptr<ShardokAIClient>> aic;
std::vector<std::shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
const std::unique_ptr<ShardokEngine> &e) {
std::vector<std::shared_ptr<ShardokAIClient>> aic;
mcts::MCTSConfig mctsConfig;
mctsConfig.backpropagationPolicy = mcts::MCTSBackpropagationPolicy::MINIMAX;
@@ -78,6 +145,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
// Check if all players are AI - if so, use faster time budgets
const auto &playerInfos = e->GetPlayerInfos();
aic.reserve(playerInfos.size());
const bool isAllAiBattle =
std::ranges::all_of(playerInfos, [](const auto &pi) { return pi.is_ai(); });
@@ -107,7 +175,7 @@ vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
void ShardokGameController::LockedNotifyClients() const { updateCondition.notify_all(); }
auto ShardokGameController::LockedAIClientForPid(PlayerId pid) const
-> shared_ptr<ShardokAIClient> {
-> std::shared_ptr<ShardokAIClient> {
const auto it = std::ranges::find_if(aiClients, [pid](const auto &client) {
return client->GetPlayerId() == pid;
});
@@ -120,7 +188,7 @@ void ShardokGameController::DoAIThread() {
while (aiThreadKeepGoing) {
// Phase 1: Gather data for AI decision (brief lock)
shared_ptr<ShardokAIClient> aiClient;
std::shared_ptr<ShardokAIClient> aiClient;
PlayerId playerId;
GameSettingsSPtr settings;
net::eagle0::shardok::api::GameStateView gsv;
@@ -128,7 +196,7 @@ void ShardokGameController::DoAIThread() {
size_t expectedHistoryCount;
{
unique_lock lk(masterLock);
std::unique_lock lk(masterLock);
if (engine->GameIsOver()) {
aiThreadKeepGoing = false;
@@ -158,6 +226,8 @@ void ShardokGameController::DoAIThread() {
continue;
}
std::string aiPhase = "choosing command";
int64_t chosenIndex = -1;
try {
// Phase 2: AI thinks (NO LOCK - this is the slow part)
auto results = [&] {
@@ -169,10 +239,11 @@ void ShardokGameController::DoAIThread() {
{"available_command_count", std::to_string(availableCommands->size())}});
return aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
}();
chosenIndex = results.chosenIndex;
// Phase 3: Post the command (brief lock)
{
unique_lock lk(masterLock);
std::unique_lock lk(masterLock);
// Verify state hasn't changed while we were thinking
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
@@ -187,6 +258,7 @@ void ShardokGameController::DoAIThread() {
}
{
aiPhase = "posting chosen command";
ScopedShardokLatencyTrace trace(
"ai_engine_post_command",
cachedGameId,
@@ -197,6 +269,7 @@ void ShardokGameController::DoAIThread() {
}
LockedNotifyClients();
{
aiPhase = "posting forced follow-up commands";
ScopedShardokLatencyTrace trace(
"ai_engine_post_forced_commands",
cachedGameId,
@@ -208,16 +281,32 @@ void ShardokGameController::DoAIThread() {
aiThreadKeepGoing = !engine->GameIsOver();
}
} catch (const std::exception &e) {
std::ostringstream error;
error << "AI error in game " << cachedGameId << ", player " << playerId << ", round "
<< gsv.current_round() << ": " << e.what();
std::ostringstream context;
context << "AI failure context: phase=" << aiPhase
<< ", expected_history_count=" << expectedHistoryCount
<< ", current_history_count=" << engine->GetUnfilteredHistoryCount()
<< ", chosen_index=" << chosenIndex
<< ", current_player=" << static_cast<int>(engine->GetCurrentPlayerId()) << ", "
<< DescribeCurrentStateForLog(*engine);
const std::string commandsContext = availableCommands
? DescribeCommandsForLog(*availableCommands)
: "available_command_count=<cleared>";
std::cerr << "AI thread exception in game " << cachedGameId << ", player " << playerId
<< ", round " << gsv.current_round() << ": " << e.what() << '\n';
std::cerr << context.str() << '\n';
std::cerr << commandsContext << '\n';
void *backtraceArray[20];
const int backtraceSize = backtrace(backtraceArray, 20);
backtrace_symbols_fd(backtraceArray, backtraceSize, STDERR_FILENO);
aiThreadErrorMessage = "AI error in game " + cachedGameId + ", player " +
std::to_string(playerId) + ", round " +
std::to_string(gsv.current_round()) + ": " + e.what();
aiThreadErrorMessage = error.str() + "\n" + context.str() + "\n" + commandsContext;
aiThreadFailed.store(true);
updateCondition.notify_all();
break;
@@ -228,7 +317,7 @@ void ShardokGameController::DoAIThread() {
ShardokGameController::~ShardokGameController() {
{
unique_lock lk(masterLock);
std::unique_lock lk(masterLock);
aiThreadKeepGoing = false;
aiCondition.notify_one();
}
@@ -236,7 +325,7 @@ ShardokGameController::~ShardokGameController() {
}
void CheckFactionId(
const unique_ptr<ShardokEngine> &engine,
const std::unique_ptr<ShardokEngine> &engine,
const PlayerId shardokPlayerId,
const int eagleFactionId) {
bool found = false;
@@ -256,7 +345,7 @@ void ShardokGameController::PostCommand(
const int eagleFactionId,
const int64_t token,
const int64_t commandIndex,
const optional<int32_t> roll) {
const std::optional<int32_t> roll) {
ScopedShardokLatencyTrace totalTrace(
"controller_post_command_total",
cachedGameId,
@@ -264,7 +353,7 @@ void ShardokGameController::PostCommand(
{"eagle_faction_id", std::to_string(eagleFactionId)},
{"token", std::to_string(token)},
{"command_index", std::to_string(commandIndex)}});
scoped_lock<mutex> guard(masterLock);
std::scoped_lock<std::mutex> guard(masterLock);
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
@@ -303,7 +392,7 @@ void ShardokGameController::PostPlacementCommands(
const PlayerId shardokPlayerId,
const int eagleFactionId,
const int64_t token,
const vector<UnitPlacementInfo> &infos) {
const std::vector<UnitPlacementInfo> &infos) {
ScopedShardokLatencyTrace totalTrace(
"controller_post_placement_commands_total",
cachedGameId,
@@ -312,7 +401,7 @@ void ShardokGameController::PostPlacementCommands(
{"token", std::to_string(token)},
{"placement_count", std::to_string(infos.size())}});
{
scoped_lock<mutex> guard(masterLock);
std::scoped_lock<std::mutex> guard(masterLock);
CheckFactionId(engine, shardokPlayerId, eagleFactionId);
@@ -356,7 +445,7 @@ void ShardokGameController::PostPlacementCommands(
}
auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
scoped_lock<mutex> guard(masterLock);
std::scoped_lock<std::mutex> guard(masterLock);
return engine->GetCurrentGameStateBytes();
}
@@ -368,9 +457,9 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
AllUpdates updates{};
incomingRegistrations++;
vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
std::vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
{
unique_lock<mutex> guard(masterLock);
std::unique_lock<std::mutex> guard(masterLock);
updates.newUnfilteredCount = engine->GetUnfilteredHistoryCount();
@@ -400,7 +489,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
auto fid = pi.eagle_faction_id();
vector<net::eagle0::shardok::api::ActionResultView> actionResultViews;
std::vector<net::eagle0::shardok::api::ActionResultView> actionResultViews;
{
ScopedShardokLatencyTrace trace(
"engine_filter_new_results",
@@ -411,7 +500,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
actionResultViews = engine->FilterNewResults(pi.player_id(), startingActionId);
}
shared_ptr<AvailableCommands> acs = nullptr;
std::shared_ptr<AvailableCommands> acs = nullptr;
const auto availableCommands = [&] {
ScopedShardokLatencyTrace trace(
@@ -430,9 +519,11 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
if (!availableCommands.empty() || !previewCommands.empty()) {
acs = std::make_shared<AvailableCommands>();
acs->mutable_current_command()->Reserve(availableCommands.size());
*acs->mutable_current_command() = {
std::begin(availableCommands),
std::end(availableCommands)};
acs->mutable_preview_command()->Reserve(previewCommands.size());
*acs->mutable_preview_command() = {
std::begin(previewCommands),
std::end(previewCommands)};
@@ -483,6 +574,7 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
// Extract battle progress data from the FlatBuffer game state
const auto *gs = engine->GetCurrentGameState().Get();
updates.currentRound = gs->current_round();
updates.playerTroopCounts.reserve(gs->player_infos()->size());
for (const auto *player : *gs->player_infos()) {
int32_t troopCount = 0;
for (const auto *unit : *gs->units()) {
@@ -504,36 +596,37 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
}
auto ShardokGameController::GameIsOver() -> bool {
scoped_lock<mutex> guard(masterLock);
std::scoped_lock<std::mutex> guard(masterLock);
const auto &gameStatus = engine->GetGameStatus();
return ShardokEngine::GameIsOver(gameStatus);
}
auto ShardokGameController::GameOverStatus() -> net::eagle0::shardok::common::GameStatus {
scoped_lock<mutex> guard(masterLock);
std::scoped_lock<std::mutex> guard(masterLock);
return fb::ToProto(engine->GetGameStatus());
}
auto ShardokGameController::EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit> {
scoped_lock<mutex> guard(masterLock);
auto ShardokGameController::EndGameUnits()
-> std::vector<net::eagle0::shardok::storage::ResolvedUnit> {
std::scoped_lock<std::mutex> guard(masterLock);
return engine->EndGameUnits();
}
auto ShardokGameController::ResolvedPlayerInfos()
-> vector<net::eagle0::shardok::common::PlayerInfo> {
scoped_lock<mutex> guard(masterLock);
-> std::vector<net::eagle0::shardok::common::PlayerInfo> {
std::scoped_lock<std::mutex> guard(masterLock);
return engine->GetPlayerInfos();
}
void ShardokGameController::RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber) {
scoped_lock<mutex> guard(subscriberLock);
std::scoped_lock<std::mutex> guard(subscriberLock);
subscribers.push_back(subscriber);
}
void ShardokGameController::UnregisterSubscriber(const StreamSubscriber *subscriber) {
scoped_lock<mutex> guard(subscriberLock);
std::scoped_lock<std::mutex> guard(subscriberLock);
subscribers.erase(
std::remove_if(
subscribers.begin(),
@@ -556,7 +649,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
GameOverInfo gameOverInfo{};
{
unique_lock<mutex> guard(masterLock);
std::unique_lock<std::mutex> guard(masterLock);
// Wait for updates, game over, or AI thread failure
updateCondition.wait(guard, [this, lastPushedActionId] {
@@ -10,9 +10,14 @@
#define EAGLE0_SHARDOK_CONTROLLER_SHARDOK_GAME_CONTROLLER_HPP
#include <atomic>
#include <condition_variable>
#include <ctime>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <utility>
#include <vector>
@@ -26,31 +31,27 @@
namespace shardok {
using net::eagle0::shardok::api::ActionResultView;
using net::eagle0::shardok::api::AvailableCommands;
using std::optional;
using std::shared_ptr;
using std::unique_ptr;
using std::weak_ptr;
// Forward declaration
class ShardokGameController;
/// Info about a game that has ended, for notifying subscribers
struct GameOverInfo {
vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
std::vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
net::eagle0::shardok::common::GameStatus gameStatus;
};
/// Updates for a single player (includes faction ID for client routing)
struct OnePlayerUpdates {
int32_t eagleFactionId;
vector<ActionResultView> resultViews;
shared_ptr<AvailableCommands> availableCommands;
std::vector<ActionResultView> resultViews;
std::shared_ptr<AvailableCommands> availableCommands;
OnePlayerUpdates(
const int32_t fid,
const vector<ActionResultView>& arvs,
const shared_ptr<AvailableCommands>& acs)
const std::vector<ActionResultView>& arvs,
const std::shared_ptr<AvailableCommands>& acs)
: eagleFactionId(fid),
resultViews(arvs),
availableCommands(acs) {}
@@ -70,12 +71,12 @@ public:
/// Called when new game updates are available
virtual void OnUpdate(
const vector<ActionResult>& mainResults,
const vector<OnePlayerUpdates>& filteredResults,
const std::vector<ActionResult>& mainResults,
const std::vector<OnePlayerUpdates>& filteredResults,
int32_t newUnfilteredCount,
const byte_vector& currentGameState,
int32_t currentRound,
const vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
const std::vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
/// Called when the game ends
virtual void OnGameOver(const GameOverInfo& info) = 0;
@@ -101,51 +102,52 @@ private:
mutable std::mutex subscriberLock{};
std::vector<std::weak_ptr<StreamSubscriber>> subscribers{};
string serializedRequest;
std::string serializedRequest;
unique_ptr<ShardokEngine> engine;
std::unique_ptr<ShardokEngine> engine;
// Cached immutable data - safe to access without lock since it never changes after construction
const GameId cachedGameId;
std::atomic_int incomingRegistrations = 0;
const string mapName;
const string logFilePath;
const std::string mapName;
const std::string logFilePath;
// AI thread error state - written once by AI thread before setting atomic flag
std::atomic<bool> aiThreadFailed{false};
std::string aiThreadErrorMessage;
vector<shared_ptr<ShardokAIClient>> aiClients;
std::vector<std::shared_ptr<ShardokAIClient>> aiClients;
std::thread aiThread;
// Each entry maps an Eagle faction id of a non-participant watcher to the
// Shardok pids of participants whose allies list named this watcher. Used
// when generating per-watcher filtered views in GetUpdates.
const vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies;
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies;
static auto MakeLogFilePath() -> string {
const time_t timer = time(nullptr);
static auto MakeLogFilePath() -> std::string {
const std::time_t timer = std::time(nullptr);
char buf[255];
strftime(buf, 255, "/tmp/playthrough_%Y_%m_%d_%H%M%S.log", localtime(&timer));
return string(buf);
std::strftime(buf, 255, "/tmp/playthrough_%Y_%m_%d_%H%M%S.log", std::localtime(&timer));
return std::string(buf);
}
void LockedNotifyClients() const;
auto LockedAIClientForPid(PlayerId pid) const -> shared_ptr<ShardokAIClient>;
auto LockedAIClientForPid(PlayerId pid) const -> std::shared_ptr<ShardokAIClient>;
static vector<shared_ptr<ShardokAIClient>> MakeAIClients(const unique_ptr<ShardokEngine>& e);
static std::vector<std::shared_ptr<ShardokAIClient>> MakeAIClients(
const std::unique_ptr<ShardokEngine>& e);
void DoAIThread();
public:
ShardokGameController(
unique_ptr<ShardokEngine> e,
string mapName,
string serializedRequest = "",
vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies = {})
std::unique_ptr<ShardokEngine> e,
std::string mapName,
std::string serializedRequest = "",
std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies = {})
: serializedRequest(std::move(serializedRequest)),
engine(std::move(e)),
cachedGameId(engine->GetGameId()),
@@ -160,40 +162,40 @@ public:
~ShardokGameController();
auto SerializedRequest() const -> string { return serializedRequest; }
auto SerializedRequest() const -> std::string { return serializedRequest; }
void PostCommand(
PlayerId shardokPlayerId,
int eagleFactionId,
int64_t token,
int64_t commandIndex,
optional<int32_t> roll);
std::optional<int32_t> roll);
void PostPlacementCommands(
PlayerId shardokPlayerId,
int eagleFactionId,
int64_t token,
const vector<UnitPlacementInfo>& infos);
const std::vector<UnitPlacementInfo>& infos);
struct AllUpdates {
vector<ActionResult> mainResults;
vector<OnePlayerUpdates> filteredResults;
std::vector<ActionResult> mainResults;
std::vector<OnePlayerUpdates> filteredResults;
int32_t newUnfilteredCount;
byte_vector currentGameState;
int32_t currentRound = 0;
vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
std::vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
};
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
auto GetCurrentGameStateBytes() -> byte_vector;
auto GameIsOver() -> bool;
auto GameOverStatus() -> net::eagle0::shardok::common::GameStatus;
auto ResolvedPlayerInfos() -> vector<net::eagle0::shardok::common::PlayerInfo>;
auto EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit>;
auto ResolvedPlayerInfos() -> std::vector<net::eagle0::shardok::common::PlayerInfo>;
auto EndGameUnits() -> std::vector<net::eagle0::shardok::storage::ResolvedUnit>;
[[nodiscard]] auto GetGameId() const -> GameId { return cachedGameId; }
[[nodiscard]] auto GetLogFilePath() const -> string { return logFilePath; }
[[nodiscard]] auto GetLogFilePath() const -> std::string { return logFilePath; }
[[nodiscard]] auto HasAIThreadError() const -> bool { return aiThreadFailed.load(); }
[[nodiscard]] auto GetAIThreadError() const -> std::string { return aiThreadErrorMessage; }
@@ -9,6 +9,7 @@
#include "AvailableCommandsFactory.hpp"
#include <algorithm>
#include <iterator>
#include <memory>
#include <ranges>
@@ -31,23 +32,21 @@ private:
static auto CannotBecomeOutlaw(const GameStateW &gameState, PlayerId pid) -> bool {
if (pid == UNCONTROLLED_PLAYER_ID) return false;
const auto &player = std::find_if(
begin(*gameState->player_infos()),
end(*gameState->player_infos()),
const auto &player = std::ranges::find_if(
*gameState->player_infos(),
[pid](const PlayerInfoFb *pi) { return pi->player_id() == pid; });
return (player != end(*gameState->player_infos()) && player->cannot_become_outlaw());
return (player != std::end(*gameState->player_infos()) && player->cannot_become_outlaw());
}
static auto IsAttacker(const GameStateW &gameState, PlayerId pid) -> bool {
if (pid == UNCONTROLLED_PLAYER_ID) return false;
const auto &player = std::find_if(
begin(*gameState->player_infos()),
end(*gameState->player_infos()),
const auto &player = std::ranges::find_if(
*gameState->player_infos(),
[pid](const PlayerInfoFb *pi) { return pi->player_id() == pid; });
return (player == end(*gameState->player_infos()) || !player->is_defender());
return (player == std::end(*gameState->player_infos()) || !player->is_defender());
}
const std::vector<std::shared_ptr<const CommandFactory>> commandFactories;
@@ -116,6 +115,7 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
const bool isEligibleCharger = unit->unit_id() == gameState->eligible_charger_id();
std::vector<UnitId> possibleChargees{};
possibleChargees.reserve(gameState->possible_chargee_ids()->size());
for (const UnitId cid : *gameState->possible_chargee_ids()) {
if (cid != -1) { possibleChargees.push_back(cid); }
}
@@ -194,8 +194,6 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
CommandList commands{};
std::vector<AdjacentTile> adjacentTiles;
playerSetupCommandFactory
.AddAvailablePlayerSetupCommands(commands, playerId, gameState, reinforcementPositions);
if (!commands.empty()) return std::make_shared<CommandList>(commands);
@@ -238,6 +236,7 @@ auto AvailableCommandsFactoryImpl::GetAvailableCommands(
/* onlyFollowUps=*/true);
std::unordered_set<CommandType> followUpTypes{};
followUpTypes.reserve(followUpCommands.size());
for (const auto &followUpCmd : followUpCommands) {
if (followUpCmd->CanBeFollowUp()) {
followUpTypes.insert(followUpCmd->GetCommandType());
@@ -189,12 +189,13 @@ struct BattalionType {
}
[[nodiscard]] auto GetCostToEnterTerrain(const TerrainProto &terrain) const -> ActionCost {
if (HasBridge(terrain.modifier()))
return ActionCost(
ActionCost::standard,
if (HasBridge(terrain.modifier()) && terrain.modifier().bridge().has_integrity() &&
terrain.modifier().bridge().integrity().value() > 0.0) {
return {ActionCost::standard,
ActionPoints(std::ceil(
100.0 * minimumCostToEnterBridge /
terrain.modifier().bridge().integrity().value())));
terrain.modifier().bridge().integrity().value()))};
}
if (IsFrozen(terrain.modifier())) return costToEnterIce;
auto terrainCost = GetCostToEnterTerrainType(terrain.type());
@@ -208,11 +209,11 @@ struct BattalionType {
[[nodiscard]] auto GetCostToEnterTerrain(
const net::eagle0::shardok::storage::fb::Terrain_::Type terrainType,
const net::eagle0::shardok::storage::fb::TileModifier &tm) const -> ActionCost {
if (tm.bridge().present())
return ActionCost(
ActionCost::standard,
if (tm.bridge().present() && tm.bridge().integrity() > 0.0) {
return {ActionCost::standard,
ActionPoints(
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity())));
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity()))};
}
if (tm.ice().present()) return costToEnterIce;
auto terrainCost = GetCostToEnterTerrainType(terrainType);
@@ -261,13 +262,8 @@ struct BattalionType {
trainingDifferenceMultiplierForStunChanceOnCharge * trainingDifference;
OtherFactor trainingFactor =
MakeOtherFactor(static_cast<int16_t>(trainingValue), "training difference");
return MakeOdds(
static_cast<int16_t>(baseStunChanceOnCharge),
0,
0,
0,
{},
{trainingFactor});
const std::array otherFactors = {trainingFactor};
return MakeOdds(static_cast<int16_t>(baseStunChanceOnCharge), 0, 0, 0, {}, otherFactors);
}
[[nodiscard]] auto GetDailyFoodCostPerTroop() const -> double {
@@ -44,7 +44,7 @@ inline constexpr DamageType DamageType_aessence = DamageType::DamageType_aessenc
inline constexpr DamageType DamageType_impact = DamageType::DamageType_impact;
inline constexpr int NUM_DAMAGE_TYPES = static_cast<int>(DamageType::NUM_DAMAGE_TYPES);
static inline bool DamageTypeGetsTerrainDefense(const DamageType type) {
[[nodiscard]] static inline bool DamageTypeGetsTerrainDefense(const DamageType type) {
switch (type) {
case DamageType_slashing:
case DamageType_puncturing:
@@ -132,11 +132,13 @@ public:
return damageByType.at(PenetratingIndex(type));
};
bool operator==(const CombatDamage& cmpr) const { return damageByType == cmpr.damageByType; }
[[nodiscard]] bool operator==(const CombatDamage& cmpr) const {
return damageByType == cmpr.damageByType;
}
bool operator!=(const CombatDamage& cmpr) const { return !(*this == cmpr); }
[[nodiscard]] bool operator!=(const CombatDamage& cmpr) const { return !(*this == cmpr); }
bool operator<=(const CombatDamage& cmpr) const {
[[nodiscard]] bool operator<=(const CombatDamage& cmpr) const {
return std::ranges::equal(
damageByType,
cmpr.damageByType,
@@ -145,7 +147,7 @@ public:
});
}
bool operator>=(const CombatDamage& cmpr) const {
[[nodiscard]] bool operator>=(const CombatDamage& cmpr) const {
return std::ranges::equal(
damageByType,
cmpr.damageByType,
@@ -154,11 +156,15 @@ public:
});
}
bool operator>(const CombatDamage& cmpr) const { return ((*this >= cmpr) && !(*this == cmpr)); }
[[nodiscard]] bool operator>(const CombatDamage& cmpr) const {
return ((*this >= cmpr) && !(*this == cmpr));
}
bool operator<(const CombatDamage& cmpr) const { return ((*this <= cmpr) && !(*this == cmpr)); }
[[nodiscard]] bool operator<(const CombatDamage& cmpr) const {
return ((*this <= cmpr) && !(*this == cmpr));
}
CombatDamage operator+(const CombatDamage& add) const {
[[nodiscard]] CombatDamage operator+(const CombatDamage& add) const {
return Builder()
.SetSlashing(
GetNormalDamageOfType(DamageType_slashing) +
@@ -219,7 +225,7 @@ public:
.Build();
}
CombatDamage operator*(const double factor) const {
[[nodiscard]] CombatDamage operator*(const double factor) const {
return Builder()
.SetSlashing(GetNormalDamageOfType(DamageType_slashing) * factor)
.SetPuncturing(GetNormalDamageOfType(DamageType_puncturing) * factor)
@@ -442,7 +448,7 @@ public:
[[nodiscard]] auto ToBuilder() const -> Builder {
Builder builder = Builder();
for (int i = 0; i < static_cast<int>(NUM_DAMAGE_TYPES); i++) {
for (int i = 0; i < static_cast<int>(NUM_DAMAGE_TYPES); ++i) {
const auto type = static_cast<DamageType>(i);
builder.SetDamageByType(type, false, this->GetNormalDamageOfType(type));
builder.SetDamageByType(type, true, this->GetPenetratingDamageOfType(type));
@@ -451,7 +457,9 @@ public:
}
};
inline CombatDamage operator*(const double factor, const CombatDamage& dmg) { return dmg * factor; }
[[nodiscard]] inline CombatDamage operator*(const double factor, const CombatDamage& dmg) {
return dmg * factor;
}
} // namespace shardok
#endif // EAGLE0_SHARDOK_LIBRARY_COMBAT_DAMAGE_HPP
@@ -9,6 +9,7 @@
#include "FireUtils.hpp"
#include <algorithm>
#include <array>
#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifier.hpp"
@@ -103,14 +104,14 @@ auto GetExtinguishOdds(
const int16_t windFactor = -weather->wind().speed_in_mph();
std::vector<OtherFactor> others;
const std::vector<double> stats = {agility, agility, intelligence};
if (ownTile) {
others.push_back(MakeOtherFactor(settings.Backing().extinguish_own_tile_buf(), "ownTile"));
const std::array others = {
MakeOtherFactor(settings.Backing().extinguish_own_tile_buf(), "ownTile")};
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, others);
}
const std::vector<double> stats = {agility, agility, intelligence};
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, others);
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats);
}
auto GetStartFireOdds(
@@ -128,12 +129,13 @@ auto GetStartFireOdds(
const int16_t windFactor = -currentWeather.wind().speed_in_mph();
std::vector<OtherFactor> otherFactors;
if (isMage) { otherFactors.push_back(MakeOtherFactor(+20, "mage")); }
const std::vector<double> stats = {agility, intelligence, intelligence};
if (isMage) {
const std::array otherFactors = {MakeOtherFactor(+20, "mage")};
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, otherFactors);
}
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, otherFactors);
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats);
}
} // namespace shardok
@@ -21,19 +21,23 @@ using WeatherFb = net::eagle0::shardok::storage::fb::Weather;
using TerrainProto = net::eagle0::shardok::common::Terrain;
using Terrain = net::eagle0::shardok::storage::fb::Terrain;
auto PropensityByTerrain(const Terrain *terrain, const SettingsGetter &settings) -> int;
[[nodiscard]] auto PropensityByTerrain(const Terrain *terrain, const SettingsGetter &settings)
-> int;
auto PropensityByTerrain(const TerrainProto &terrain, const SettingsGetter &settings) -> int;
[[nodiscard]] auto PropensityByTerrain(const TerrainProto &terrain, const SettingsGetter &settings)
-> int;
auto PropensityByTerrain(
[[nodiscard]] auto PropensityByTerrain(
const TerrainType &terrainType,
const TileModifierProto &modifier,
const SettingsGetter &settings) -> int;
auto GetFireDamage(int troops, double openEndedPercentileRoll1, double openEndedPercentileRoll2)
-> CombatDamage;
[[nodiscard]] auto GetFireDamage(
int troops,
double openEndedPercentileRoll1,
double openEndedPercentileRoll2) -> CombatDamage;
auto GetExtinguishOdds(
[[nodiscard]] auto GetExtinguishOdds(
const SettingsGetter &settings,
const Terrain *terrain,
const WeatherFb *currentWeather,
@@ -41,7 +45,7 @@ auto GetExtinguishOdds(
double intelligence,
bool ownTile) -> PercentileRollOdds;
auto GetStartFireOdds(
[[nodiscard]] auto GetStartFireOdds(
const SettingsGetter &settings,
const Terrain &terrain,
const WeatherFb &currentWeather,
@@ -36,25 +36,19 @@ public:
using BaseType::BaseType;
// Default constructor
GameStateW() : BaseType() {}
GameStateW() = default;
// Copy constructor
GameStateW(const GameStateW& other) : BaseType(other) {}
GameStateW(const GameStateW& other) = default;
// Move constructor
GameStateW(GameStateW&& other) noexcept : BaseType(std::move(other)) {}
GameStateW(GameStateW&& other) noexcept = default;
// Copy assignment
GameStateW& operator=(const GameStateW& other) {
BaseType::operator=(other);
return *this;
}
GameStateW& operator=(const GameStateW& other) = default;
// Move assignment
GameStateW& operator=(GameStateW&& other) noexcept {
BaseType::operator=(std::move(other));
return *this;
}
GameStateW& operator=(GameStateW&& other) noexcept = default;
// Constructor from base type
GameStateW(const BaseType& base) : BaseType(base) {}
@@ -13,7 +13,7 @@
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
namespace shardok {
double BonusFromStat(const double stat) { return pow(((double)stat - 50.0), 3.0) / 5000.0; }
double BonusFromStat(const double stat) { return pow(stat - 50.0, 3.0) / 5000.0; }
int16_t AverageBonusFromStats(const std::vector<double> &stats) {
double totalBonus = 0.0;
@@ -23,7 +23,7 @@ int16_t AverageBonusFromStats(const std::vector<double> &stats) {
}
int16_t GetSuccessChance(const PercentileRollOdds &odds) {
return int16_t(std::floor(
return static_cast<int16_t>(std::floor(
100.0 * RandomGenerator::ChanceOpenEndedPercentileAtOrAbove(100 - GetTotalOdds(odds)) +
0.5));
}
@@ -42,7 +42,7 @@ PercentileRollOdds MakeOdds(
const int weath,
const int wind,
const int statF,
const std::vector<OtherFactor> &others) {
const std::span<const OtherFactor> others) {
PercentileRollOdds odds;
odds.set_base_difficulty(base);
@@ -61,7 +61,7 @@ PercentileRollOdds MakeOdds(
const int weath,
const int wind,
const std::vector<double> &stats,
const std::vector<OtherFactor> &others) {
const std::span<const OtherFactor> others) {
return MakeOdds(base, terr, weath, wind, AverageBonusFromStats(stats), others);
}
@@ -9,6 +9,7 @@
#ifndef EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
#define EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
#include <span>
#include <string>
#include <vector>
@@ -21,33 +22,33 @@ namespace shardok {
using PercentileRollOdds = net::eagle0::shardok::storage::Odds;
using OtherFactor = net::eagle0::shardok::storage::Odds_OtherFactor;
double BonusFromStat(double stat);
[[nodiscard]] double BonusFromStat(double stat);
int16_t AverageBonusFromStats(const std::vector<double> &stats);
[[nodiscard]] int16_t AverageBonusFromStats(const std::vector<double> &stats);
OtherFactor MakeOtherFactor(int value, const std::string &id);
[[nodiscard]] OtherFactor MakeOtherFactor(int value, const std::string &id);
PercentileRollOdds MakeOdds(
[[nodiscard]] PercentileRollOdds MakeOdds(
int base = 0,
int terr = 0,
int weath = 0,
int wind = 0,
int statF = 0,
const std::vector<OtherFactor> &others = std::vector<OtherFactor>());
std::span<const OtherFactor> others = {});
PercentileRollOdds MakeOdds(
[[nodiscard]] PercentileRollOdds MakeOdds(
int base,
int terr,
int weath,
int wind,
const std::vector<double> &stats,
const std::vector<OtherFactor> &others = std::vector<OtherFactor>());
std::span<const OtherFactor> others = {});
int16_t GetTotalOdds(const PercentileRollOdds &odds);
[[nodiscard]] int16_t GetTotalOdds(const PercentileRollOdds &odds);
bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
[[nodiscard]] bool PercentileRollSucceeds(const PercentileRollOdds &odds, double roll);
int16_t GetSuccessChance(const PercentileRollOdds &odds);
[[nodiscard]] int16_t GetSuccessChance(const PercentileRollOdds &odds);
} // namespace shardok
#endif // EAGLE0_SHARDOK_LIBRARY_PERCENTILE_ROLL_ODDS_HPP
@@ -42,7 +42,7 @@ private:
[[nodiscard]] virtual auto InternalExecute(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator) const -> std::vector<ActionResult> {
return InternalExecuteWithRoll(currentState, generator, std::optional<int32_t>());
return InternalExecuteWithRoll(currentState, generator, std::nullopt);
}
[[nodiscard]] virtual auto InternalExecuteWithRoll(
@@ -77,11 +77,11 @@ ShardokEngine::ShardokEngine(
ShardokEngine::ShardokEngine(
const GameSettingsSPtr &settings,
vector<Unit> &&unplacedUnorderedUnits,
std::vector<Unit> &&unplacedUnorderedUnits,
const HexMapProto &map,
const int month,
const GameId &existingGameId,
const vector<PlayerInfoProto> &playerInfos,
const std::vector<PlayerInfoProto> &playerInfos,
const bool trackHistory)
: gameSettings(settings),
settingsGetter(settings->GetGetter()),
@@ -106,7 +106,7 @@ ShardokEngine::ShardokEngine(
ApplyAndAddActionResult(setupStartAction);
}
void ShardokEngine::ApplyAndAddActionResults(const vector<ActionResultProto> &results) {
void ShardokEngine::ApplyAndAddActionResults(const std::vector<ActionResultProto> &results) {
for (const ActionResultProto &result : results) { ApplyAndAddActionResult(result); }
}
@@ -122,7 +122,7 @@ void ShardokEngine::ApplyAndAddActionResult(const ActionResultProto &result) {
ShardokEngine::ShardokEngine(
const GameSettingsSPtr &settings,
const vector<ShardokActionWithResultingState> &history,
const std::vector<ShardokActionWithResultingState> &history,
const bool trackHistory)
: gameSettings(settings),
settingsGetter(settings->GetGetter()),
@@ -140,7 +140,12 @@ ShardokEngine::ShardokEngine(const ShardokEngine &toCopy, const bool trackHistor
gameState(toCopy.gameState),
trackHistory(trackHistory),
startingHistoryCount(toCopy.startingHistoryCount),
startingHistoryState(trackHistory ? toCopy.startingHistoryState : GameStateW{}),
actionHistory(
trackHistory ? toCopy.actionHistory : std::vector<ShardokActionWithResultingState>{}),
criticalTileCoords(toCopy.criticalTileCoords),
tutorialController_(toCopy.tutorialController_),
pendingReinforcementPlacement_(toCopy.pendingReinforcementPlacement_),
cachedAvailableCommands(toCopy.cachedAvailableCommands) {}
auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId) const
@@ -156,11 +161,11 @@ auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId)
}
auto ShardokEngine::GetGameHistory(const ActionId lastUpdatedActionId) const
-> vector<ShardokActionWithResultingState> {
-> std::vector<ShardokActionWithResultingState> {
if (lastUpdatedActionId >= static_cast<int64_t>(actionHistory.size()) + startingHistoryCount) {
return vector<ShardokActionWithResultingState>();
return std::vector<ShardokActionWithResultingState>();
}
return vector<ShardokActionWithResultingState>(
return std::vector<ShardokActionWithResultingState>(
std::begin(actionHistory) + lastUpdatedActionId - startingHistoryCount,
std::end(actionHistory));
}
@@ -172,14 +177,17 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
[[nodiscard]] auto ShardokEngine::FilterNewResults(
PlayerId askingPlayer,
int64_t previousActionCount) const -> vector<net::eagle0::shardok::api::ActionResultView> {
int64_t previousActionCount) const
-> std::vector<net::eagle0::shardok::api::ActionResultView> {
if (previousActionCount < startingHistoryCount) {
throw ShardokInternalErrorException("Unable to fetch history before startingHistoryCount");
}
vector<ShardokActionWithResultingState> unfilteredHistory = GetGameHistory(previousActionCount);
std::vector<ShardokActionWithResultingState> unfilteredHistory =
GetGameHistory(previousActionCount);
vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
std::vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
filteredHistory.reserve(unfilteredHistory.size());
GameStateW startingState = (previousActionCount == 0)
? GameStateW{}
@@ -193,9 +201,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
GameState *previousStatePtr = nullptr;
GameStateView &previousView = startingView;
for (vector<ShardokActionWithResultingState> newHistory =
{unfilteredHistory.begin(), unfilteredHistory.end()};
const ShardokActionWithResultingState &awrs : newHistory) {
for (const ShardokActionWithResultingState &awrs : unfilteredHistory) {
GameStateView viewAfter = GameStateFilteredForPlayer(
settingsGetter,
GameStateW::FromByteString(awrs.state_after_fb()),
@@ -220,16 +226,18 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
[[nodiscard]] auto ShardokEngine::FilterNewResultsForWatcher(
const PlayerId askingPlayer,
const vector<PlayerId> &alliedPids,
const std::vector<PlayerId> &alliedPids,
const int64_t previousActionCount) const
-> vector<net::eagle0::shardok::api::ActionResultView> {
-> std::vector<net::eagle0::shardok::api::ActionResultView> {
if (previousActionCount < startingHistoryCount) {
throw ShardokInternalErrorException("Unable to fetch history before startingHistoryCount");
}
vector<ShardokActionWithResultingState> unfilteredHistory = GetGameHistory(previousActionCount);
std::vector<ShardokActionWithResultingState> unfilteredHistory =
GetGameHistory(previousActionCount);
vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
std::vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
filteredHistory.reserve(unfilteredHistory.size());
GameStateW startingState = (previousActionCount == 0)
? GameStateW{}
@@ -271,7 +279,7 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
}
auto ShardokEngine::GetFilteredGameHistory(const PlayerId askingPlayer) const
-> vector<net::eagle0::shardok::api::ActionResultView> {
-> std::vector<net::eagle0::shardok::api::ActionResultView> {
return FilterNewResults(askingPlayer, 0);
}
@@ -423,7 +431,7 @@ void ShardokEngine::ResumeAfterReinforcementPlacement(
void ShardokEngine::PostPlacementCommands(
const PlayerId player,
const vector<UnitPlacementInfo> &placementInfos,
const std::vector<UnitPlacementInfo> &placementInfos,
const std::shared_ptr<RandomGenerator> &randomGenerator) {
if (GetCurrentPlayerId() != player) {
throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
@@ -440,7 +448,7 @@ void ShardokEngine::PostPlacementCommands(
reinforcementPositions);
// first make sure they're all valid and there are no duplicates
for (size_t i = 0; i < placementInfos.size(); i++) {
for (size_t i = 0; i < placementInfos.size(); ++i) {
const UnitPlacementInfo &pi = placementInfos[i];
const auto it = std::ranges::find_if(*placementCommands, [pi](const CommandSPtr &cmd) {
@@ -452,7 +460,7 @@ void ShardokEngine::PostPlacementCommands(
}
// check that we're not double-filling any location or double-placing any unit
for (size_t j = i + 1; j < placementInfos.size(); j++) {
for (size_t j = i + 1; j < placementInfos.size(); ++j) {
const UnitPlacementInfo &other = placementInfos[j];
if (pi.unitId == other.unitId)
@@ -471,7 +479,8 @@ void ShardokEngine::PostPlacementCommands(
return cmd->GetCommandProto().actor().value() == pi.unitId &&
cmd->GetCommandProto().target() == pi.location;
});
for (vector<ActionResult> onePlacementResults = (*it)->Execute(gameState, randomGenerator);
for (std::vector<ActionResult> onePlacementResults =
(*it)->Execute(gameState, randomGenerator);
const ActionResultProto &oneResult : onePlacementResults) {
HandleActionResult(oneResult, randomGenerator);
}
@@ -564,7 +573,7 @@ void ShardokEngine::PostActionUnchecked(
const CommandSPtr &command,
const std::shared_ptr<RandomGenerator> &randomGenerator,
const std::optional<int32_t> roll) {
for (const vector<ActionResultProto> results =
for (const std::vector<ActionResultProto> results =
command->CanUseClientRoll()
? command->ExecuteWithRoll(gameState, randomGenerator, roll)
: command->Execute(gameState, randomGenerator);
@@ -614,7 +623,7 @@ void ShardokEngine::HandleActionResult(
auto ShardokEngine::HandleUnitFallingIntoWater(
const Terrain *terrain,
const Unit *unit,
std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResultProto> {
std::shared_ptr<RandomGenerator> randomGenerator) const -> std::vector<ActionResultProto> {
// Do nothing if this unit can travel across water
if (const auto &battType = settingsGetter.GetBattalionType(unit->battalion().type());
battType->GetCostToEnterTerrain(terrain).type != ActionCost::impossible) {
@@ -629,8 +638,9 @@ auto ShardokEngine::HandleUnitFallingIntoWater(
return fallAction.Execute(gameState, std::move(randomGenerator));
}
auto CommandProtosFromActionList(const CommandList &actionList) -> vector<CommandProto> {
vector<CommandProto> protos;
auto CommandProtosFromActionList(const CommandList &actionList) -> std::vector<CommandProto> {
std::vector<CommandProto> protos;
protos.reserve(actionList.size());
std::transform(
std::begin(actionList),
@@ -641,7 +651,7 @@ auto CommandProtosFromActionList(const CommandList &actionList) -> vector<Comman
return protos;
}
auto ShardokEngine::GetPreviewCommands(const PlayerId playerId) const -> vector<CommandProto> {
auto ShardokEngine::GetPreviewCommands(const PlayerId playerId) const -> std::vector<CommandProto> {
if (GameIsOver(GetGameStatus())) {
// The game is over
return {};
@@ -670,7 +680,7 @@ auto ShardokEngine::GetAvailableCommandsForAIPlayer(const PlayerId playerId) con
}
auto ShardokEngine::GetAvailableCommandProtos(const PlayerId playerId, const bool includeFollowUps)
const -> vector<CommandProto> {
const -> std::vector<CommandProto> {
if (GameIsOver(GetGameStatus())) {
// The game is over
return {};
@@ -703,7 +713,8 @@ auto ShardokEngine::UncachedGetAvailableCommands(
using SecuredByPlayerByUnitId = std::unordered_map<UnitId, PlayerId>;
auto SecuredByPlayerByUnitIdFromHistory(
const vector<ShardokActionWithResultingState> &actionHistory) -> SecuredByPlayerByUnitId {
const std::vector<ShardokActionWithResultingState> &actionHistory)
-> SecuredByPlayerByUnitId {
SecuredByPlayerByUnitId securedByPlayerByUnitId;
for (const auto &actionWithResultingState : actionHistory) {
for (const auto &resolvedUnit : actionWithResultingState.action_result().resolved_units()) {
@@ -712,15 +723,15 @@ auto SecuredByPlayerByUnitIdFromHistory(
continue;
}
if (!resolvedUnit.has_secured_by_player()) { continue; }
const auto *unit = reinterpret_cast<const Unit *>(resolvedUnit.unit_bytes().data());
securedByPlayerByUnitId[unit->unit_id()] = resolvedUnit.secured_by_player().value();
const auto unit = UnitFromBytes(resolvedUnit.unit_bytes());
securedByPlayerByUnitId[unit.unit_id()] = resolvedUnit.secured_by_player().value();
}
}
return securedByPlayerByUnitId;
}
void AddUnits(
vector<net::eagle0::shardok::storage::ResolvedUnit> &to,
std::vector<net::eagle0::shardok::storage::ResolvedUnit> &to,
const Units &from,
const SecuredByPlayerByUnitId &securedByPlayerByUnitId) {
for (const auto &unit : from) {
@@ -769,11 +780,12 @@ void AddUnits(
}
AddUnitToResolved(ru, *unit);
to.push_back(ru);
to.push_back(std::move(ru));
}
}
auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage::ResolvedUnit> {
auto ShardokEngine::EndGameUnits() const
-> std::vector<net::eagle0::shardok::storage::ResolvedUnit> {
if (const auto state = GetGameStatus()->state();
state != net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY) {
throw ShardokInternalErrorException(
@@ -782,7 +794,8 @@ auto ShardokEngine::EndGameUnits() const -> vector<net::eagle0::shardok::storage
const auto &gs = GetCurrentGameState();
vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
std::vector<net::eagle0::shardok::storage::ResolvedUnit> endgameUnits;
endgameUnits.reserve(gs->units()->size());
AddUnits(endgameUnits, *gs->units(), SecuredByPlayerByUnitIdFromHistory(actionHistory));
return endgameUnits;
@@ -11,7 +11,9 @@
#include <flatbuffers/flatbuffers.h>
#include <memory>
#include <optional>
#include <vector>
#include "ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
@@ -27,12 +29,6 @@
#include "src/main/protobuf/net/eagle0/shardok/storage/action_with_resulting_state.pb.h"
namespace shardok {
using std::begin;
using std::end;
using std::optional;
using std::shared_ptr;
using std::vector;
using net::eagle0::shardok::api::UnitView;
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
@@ -43,14 +39,14 @@ class ShardokEngine {
private:
const GameSettingsSPtr gameSettings;
const SettingsGetter settingsGetter;
const shared_ptr<AvailableCommandsFactory> availableCommandsFactory;
const std::shared_ptr<AvailableCommandsFactory> availableCommandsFactory;
GameStateW gameState;
const bool trackHistory;
const int32_t startingHistoryCount = 0;
const GameStateW startingHistoryState;
mutable vector<ShardokActionWithResultingState> actionHistory;
mutable std::vector<ShardokActionWithResultingState> actionHistory;
const CoordsSet criticalTileCoords;
@@ -61,12 +57,12 @@ private:
void ApplyAndAddActionResult(const ActionResult &result);
void ApplyAndAddActionResults(const vector<ActionResult> &results);
void ApplyAndAddActionResults(const std::vector<ActionResult> &results);
[[nodiscard]] auto HandleUnitFallingIntoWater(
const Terrain *terrain,
const fb::Unit *unit,
std::shared_ptr<RandomGenerator> randomGenerator) const -> vector<ActionResult>;
std::shared_ptr<RandomGenerator> randomGenerator) const -> std::vector<ActionResult>;
void HandleActionResult(
const ActionResult &actionResult,
@@ -105,17 +101,17 @@ public:
// Constructor for a brand new game
ShardokEngine(
const GameSettingsSPtr &settings,
vector<Unit> &&unplacedUnorderedUnits,
std::vector<Unit> &&unplacedUnorderedUnits,
const HexMapProto &map,
int month,
const GameId &existingGameId,
const vector<PlayerInfoProto> &playerInfos,
const std::vector<PlayerInfoProto> &playerInfos,
bool trackHistory = true);
// Constructor for a game with history
ShardokEngine(
const GameSettingsSPtr &settings,
const vector<ShardokActionWithResultingState> &history,
const std::vector<ShardokActionWithResultingState> &history,
bool trackHistory = true);
// Copy constructor
@@ -134,12 +130,12 @@ public:
}
[[nodiscard]] auto EndGameUnits() const
-> vector<net::eagle0::shardok::storage::ResolvedUnit>; // Throws if the game is not
// over
-> std::vector<net::eagle0::shardok::storage::ResolvedUnit>; // Throws if the game is
// not over
// Controller API
[[nodiscard]] auto GetGameHistory(ActionId lastUpdatedActionId) const
-> vector<ShardokActionWithResultingState>;
-> std::vector<ShardokActionWithResultingState>;
[[nodiscard]] auto GetUnfilteredHistoryCount() const -> size_t {
return actionHistory.size() + startingHistoryCount;
@@ -150,7 +146,7 @@ public:
-> net::eagle0::shardok::api::GameStateView;
[[nodiscard]] auto FilterNewResults(PlayerId askingPlayer, int64_t previousActionCount) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
-> std::vector<net::eagle0::shardok::api::ActionResultView>;
// Variant for non-participant watchers: askingPlayer is a sentinel (not in
// player_infos) and alliedPids names the Shardok pids whose units the
@@ -158,18 +154,18 @@ public:
// and hidden actions.
[[nodiscard]] auto FilterNewResultsForWatcher(
PlayerId askingPlayer,
const vector<PlayerId> &alliedPids,
const std::vector<PlayerId> &alliedPids,
int64_t previousActionCount) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
-> std::vector<net::eagle0::shardok::api::ActionResultView>;
[[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
-> std::vector<net::eagle0::shardok::api::ActionResultView>;
[[nodiscard]] auto GetUnitById(PlayerId askingPlayer, UnitId unitId) const -> UnitView;
void PostPlacementCommands(
PlayerId player,
const vector<UnitPlacementInfo> &placementInfos,
const std::vector<UnitPlacementInfo> &placementInfos,
const std::shared_ptr<RandomGenerator> &randomGenerator = nullptr);
void PostFinishedPlacementCommand(
PlayerId player,
@@ -186,11 +182,11 @@ public:
PlayerId playerId,
std::shared_ptr<RandomGenerator> randomGenerator) -> int;
[[nodiscard]] auto GetPreviewCommands(PlayerId playerId) const -> vector<CommandProto>;
[[nodiscard]] auto GetPreviewCommands(PlayerId playerId) const -> std::vector<CommandProto>;
[[nodiscard]] auto GetAvailableCommandsForAIPlayer(PlayerId playerId) const -> CommandListSPtr;
[[nodiscard]] auto GetAvailableCommandProtos(PlayerId playerId, bool includeFollowUps) const
-> vector<CommandProto>;
-> std::vector<CommandProto>;
[[nodiscard]] auto GetGameId() const -> GameId {
return GetCurrentGameState()->game_id()->str();
@@ -198,9 +194,9 @@ public:
[[nodiscard]] auto GetMonth() const -> int { return GetCurrentGameState()->month(); }
[[nodiscard]] auto GetPlayerInfos() const -> vector<PlayerInfoProto> {
[[nodiscard]] auto GetPlayerInfos() const -> std::vector<PlayerInfoProto> {
const auto &currentGameState = GetCurrentGameState();
vector<PlayerInfoProto> protos{};
std::vector<PlayerInfoProto> protos{};
for (const auto *const piFB : *currentGameState->player_infos()) {
protos.push_back(fb::ToPlayerInfoProto(piFB));
}
@@ -79,6 +79,8 @@ auto PlayerSetupCommandFactory::AddAvailablePlayerSetupCommands(
const auto unplacedUnits = ReserveUnitsForPlayer(gameState->units(), playerId);
if (unplacedUnits.empty()) return;
existingCommands.reserve(
existingCommands.size() + (unplacedUnits.size() * reinforcementPositions->size()));
for (const auto &[unitId, unit] : unplacedUnits) {
for (const Coords &position : *reinforcementPositions) {
if (!gameState.GetOccupant(position)) {
@@ -29,8 +29,9 @@ auto ActionPointDistances::BraveWaterPossibleCoords(const HexMap* hexMap) const
std::shared_ptr<BraveableTileInfo> info = std::make_shared<BraveableTileInfo>(hexMap);
CoordsSet& braveWaterPossibleCoords = info->cs;
const int indexCount = hexMap->row_count() * hexMap->column_count();
info->details.reserve(indexCount);
const auto indexToCoords = CreateIndexToCoords(hexMap);
for (int i = 0; i < indexCount; i++) {
for (int i = 0; i < indexCount; ++i) {
if (const Terrain* terrain = hexMap->terrain()->Get(i);
IsWater(terrain->type()) && !terrain->modifier().bridge().present() &&
!terrain->modifier().ice().present()) {
@@ -70,7 +71,7 @@ auto ActionPointDistances::CreateIndexToCoords(const HexMap* hexMap) -> std::vec
const int indexCount = hexMap->row_count() * columnCount;
std::vector<Coords> indexToCoords;
indexToCoords.reserve(indexCount);
for (int i = 0; i < indexCount; i++) {
for (int i = 0; i < indexCount; ++i) {
indexToCoords.emplace_back(
static_cast<int8_t>(i / columnCount),
static_cast<int8_t>(i % columnCount));
@@ -84,7 +85,7 @@ auto ActionPointDistances::CreateAdjacencyTable(const HexMap* hexMap)
const int indexCount = hexMap->row_count() * columnCount;
std::vector<std::array<int, 6>> adjacencyTable;
adjacencyTable.reserve(indexCount);
for (int i = 0; i < indexCount; i++) {
for (int i = 0; i < indexCount; ++i) {
const Coords coords(
static_cast<int8_t>(i / columnCount),
static_cast<int8_t>(i % columnCount));
@@ -118,7 +119,7 @@ void ActionPointDistances::PopulateOne(
// Find starting index (the one with distance 0)
const int distanceCount = static_cast<int>(ds.size());
for (int i = 0; i < distanceCount; i++) {
for (int i = 0; i < distanceCount; ++i) {
if (ds[i] == 0) {
pq.emplace(0, i);
break;
@@ -140,7 +141,7 @@ void ActionPointDistances::PopulateOne(
// Prefetch terrain data for all neighbors to reduce memory stalls
const std::array<int, 6>& neighbors = adjacencyTable[currentIndex];
for (int i = 0; i < 6 && neighbors[i] != -1; i++) {
for (int i = 0; i < 6 && neighbors[i] != -1; ++i) {
__builtin_prefetch(hexMap->terrain()->Get(neighbors[i]), 0, 3);
}
@@ -239,11 +240,10 @@ OnDemandActionPointDistances::OnDemandActionPointDistances(
battalionType(std::move(battTp)),
distances(static_cast<size_t>(map->row_count() * map->column_count())) {
const int indexCount = map->row_count() * map->column_count();
distances.resize(indexCount);
auto braveWaterPossibleCoords = includeBravingWater ? BraveWaterPossibleCoords(map) : nullptr;
for (int fromIndex = 0; fromIndex < indexCount; fromIndex++) {
for (int fromIndex = 0; fromIndex < indexCount; ++fromIndex) {
distances[fromIndex] = std::async(
std::launch::deferred,
&OnDemandActionPointDistances::GenerateDistances,
@@ -43,7 +43,7 @@ protected:
const std::shared_ptr<BraveableTileInfo> &braveWaterPossibleCoords,
const std::vector<Coords> &indexToCoords,
const std::vector<std::array<int, 6>> &adjacencyTable);
static auto GenerateDistances(
[[nodiscard]] static auto GenerateDistances(
int fromIndex,
const HexMap *hexMap,
bool includeBravingWater,
@@ -51,13 +51,15 @@ protected:
const BattalionTypeSPtr &battalionType,
const std::shared_ptr<BraveableTileInfo> &braveWaterPossibleCoords)
-> std::vector<DIST_T>;
auto BraveWaterPossibleCoords(const HexMap *hexMap) const -> std::shared_ptr<BraveableTileInfo>;
[[nodiscard]] auto BraveWaterPossibleCoords(const HexMap *hexMap) const
-> std::shared_ptr<BraveableTileInfo>;
// Create coordinate lookup table for efficient index->coords conversion
static auto CreateIndexToCoords(const HexMap *hexMap) -> std::vector<Coords>;
[[nodiscard]] static auto CreateIndexToCoords(const HexMap *hexMap) -> std::vector<Coords>;
// Create adjacency lookup table for efficient neighbor access
static auto CreateAdjacencyTable(const HexMap *hexMap) -> std::vector<std::array<int, 6>>;
[[nodiscard]] static auto CreateAdjacencyTable(const HexMap *hexMap)
-> std::vector<std::array<int, 6>>;
[[nodiscard]] auto ToIndex(const Coords &coords) const -> int {
return coords.row() * column_count + coords.column();
@@ -70,9 +72,9 @@ public:
virtual ~ActionPointDistances() = default;
virtual auto Distance(int fromIndex, int toIndex) const -> DIST_T = 0;
[[nodiscard]] virtual auto Distance(int fromIndex, int toIndex) const -> DIST_T = 0;
virtual auto Distance(const Coords &from, const Coords &to) const -> DIST_T = 0;
[[nodiscard]] virtual auto Distance(const Coords &from, const Coords &to) const -> DIST_T = 0;
};
class OnDemandActionPointDistances final : public ActionPointDistances {
@@ -91,11 +93,11 @@ public:
~OnDemandActionPointDistances() override = default;
auto Distance(const int fromIndex, const int toIndex) const -> int16_t override {
[[nodiscard]] auto Distance(const int fromIndex, const int toIndex) const -> int16_t override {
return distances[fromIndex].get()[toIndex];
}
auto Distance(const Coords &from, const Coords &to) const -> int16_t override {
[[nodiscard]] auto Distance(const Coords &from, const Coords &to) const -> int16_t override {
return Distance(ToIndex(from), ToIndex(to));
}
};
@@ -77,7 +77,7 @@ static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
auto* mutableMap = mapCopy.Get();
auto* terrainVec = mutableMap->mutable_terrain();
for (size_t i = 0; i < terrainVec->size(); i++) {
for (size_t i = 0; i < terrainVec->size(); ++i) {
// Only process tiles with ice
// const_cast is safe here because we own the mutable buffer (mapCopy)
if (auto* terrain = const_cast<Terrain*>(terrainVec->GetMutableObject(i));
@@ -22,7 +22,7 @@ struct MapId {
uint64_t terrainTypesId;
uint64_t modifierId;
auto operator==(const MapId& other) const -> bool {
[[nodiscard]] auto operator==(const MapId& other) const -> bool {
return terrainTypesId == other.terrainTypesId && modifierId == other.modifierId;
}
};
@@ -34,7 +34,7 @@ struct FullCacheKey {
bool includeBravingWater;
int braveWaterCost;
bool operator==(const FullCacheKey& other) const {
[[nodiscard]] bool operator==(const FullCacheKey& other) const {
return mapId == other.mapId && battalionTypeId == other.battalionTypeId &&
includeBravingWater == other.includeBravingWater &&
braveWaterCost == other.braveWaterCost;
@@ -43,7 +43,7 @@ struct FullCacheKey {
// Hash function for FullCacheKey
struct FullCacheKeyHash {
size_t operator()(const FullCacheKey& key) const {
[[nodiscard]] size_t operator()(const FullCacheKey& key) const {
// Pack small fields into a single 64-bit value
uint64_t packed = (static_cast<uint64_t>(key.battalionTypeId) << 32) |
(static_cast<uint64_t>(key.braveWaterCost) << 1) |
@@ -85,7 +85,7 @@ private:
// Epoch system removed - TLS cache uses size-based eviction instead
// Helper to build cache key
static auto MakeCacheKey(
[[nodiscard]] static auto MakeCacheKey(
const MapId& mapId,
const BattalionTypeSPtr& battalionType,
bool includeBravingWater,
@@ -107,7 +107,7 @@ public:
bool includeBravingWater,
int braveWaterActionPointCost = -1) -> const ActionPointDistances*;
static auto GetMapId(const HexMap* map) -> MapId;
[[nodiscard]] static auto GetMapId(const HexMap* map) -> MapId;
// Consolidate the thread-safe cache into the persistent cache and clear
// the current thread's local cache. This is only safe if we know reads
@@ -116,7 +116,7 @@ public:
// Cache management methods
static void ClearThreadLocalCache();
static size_t GetThreadLocalCacheSize();
[[nodiscard]] static size_t GetThreadLocalCacheSize();
};
using APDCache = std::shared_ptr<ActionPointDistancesCache>;
@@ -12,7 +12,7 @@
// Dynamic thread count based on hardware capabilities
static const int ASYNC_COUNT = []() {
const int cores = static_cast<int>(std::thread::hardware_concurrency());
// Use cores-2 to leave room for OS and other processes, minimum 4 threads
// Use cores-4 to leave room for OS and other processes, minimum 4 threads
const int threadCount = std::max(4, cores - 4);
std::cerr << "ActionPointDistances using " << threadCount << " threads (detected " << cores
<< " cores)\n";
@@ -35,20 +35,20 @@ auto FixedActionPointDistances::Create(
const int indexCount = map->row_count() * map->column_count();
vector<std::future<vector<vector<DIST_T>>>> futures(indexCount);
vector<std::future<vector<vector<DIST_T>>>> futures(ASYNC_COUNT);
auto braveWaterPossibleCoords =
includeBravingWater ? apd->BraveWaterPossibleCoords(map) : nullptr;
int chunkSize = (indexCount + ASYNC_COUNT - 1) / ASYNC_COUNT;
// Break into chunks for async
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; ++chunkIdx) {
futures[chunkIdx] = std::async(std::launch::async, [=]() -> vector<vector<DIST_T>> {
vector<vector<DIST_T>> chunkVec;
chunkVec.reserve(chunkSize);
const int chunkStartIndex = chunkIdx * chunkSize;
for (int i = 0; i < chunkSize; i++) {
for (int i = 0; i < chunkSize; ++i) {
const auto fromIndex = chunkStartIndex + i;
if (fromIndex >= indexCount) { continue; }
chunkVec.push_back(ActionPointDistances::GenerateDistances(
@@ -65,7 +65,7 @@ auto FixedActionPointDistances::Create(
apd->distances.reserve(indexCount);
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; ++chunkIdx) {
auto resultsVec = futures[chunkIdx].get();
apd->distances.insert(apd->distances.end(), resultsVec.begin(), resultsVec.end());
}
@@ -24,7 +24,7 @@ private:
public:
// Factory method to create FixedActionPointDistances
static auto Create(
[[nodiscard]] static auto Create(
const HexMap *map,
const BattalionTypeSPtr &battalionType,
bool includeBravingWater,
@@ -14,6 +14,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/UnitHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -30,10 +31,6 @@ inline auto GetMutableUnit(GameState *state, UnitId unitId) -> Unit * {
return const_cast<Unit *>(state->mutable_units()->GetMutableObject(unitId));
}
inline auto UnitFromBytes(const std::string &unitBytes) -> const Unit & {
return *reinterpret_cast<const Unit *>(unitBytes.data());
}
using net::eagle0::shardok::storage::fb::VictoryCondition;
using net::eagle0::shardok::storage::fb::VictoryType;
@@ -68,7 +65,7 @@ void ApplyResolvedUnit(
// If a VIP was captured, alter the morale of all this player's units
if (status == net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT &&
unit.has_attached_hero() && unit.attached_hero().is_vip()) {
for (uint32_t i = 0; i < inoutState->units()->size(); i++) {
for (uint32_t i = 0; i < inoutState->units()->size(); ++i) {
auto *playerUnit = GetMutableUnit(inoutState, i);
if (playerUnit->player_id() != unit.player_id()) continue;
if (playerUnit->unit_id() == unit.unit_id()) continue;
@@ -101,7 +98,7 @@ void ApplyResolvedUnit(
GameStateT &inoutState,
const ResolvedUnitProto &resolvedUnit,
const double vipCapturedMoraleAdjustment) {
const Unit &unit = UnitFromBytes(resolvedUnit.unit_bytes());
const Unit unit = UnitFromBytes(resolvedUnit.unit_bytes());
UnitId unitId = unit.unit_id();
if (unit.has_attached_hero() &&
@@ -169,15 +166,15 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
// First pass: check what kind of modifications we need
for (const auto &unitBytes : result.changed_units_fb()) {
const auto *unit = &UnitFromBytes(unitBytes);
maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());
const auto unit = UnitFromBytes(unitBytes);
maxChangedUnitId = std::max(maxChangedUnitId, unit.unit_id());
if (static_cast<unsigned int>(unit->unit_id()) >= mutatingState->units()->size()) {
if (static_cast<unsigned int>(unit.unit_id()) >= mutatingState->units()->size()) {
// Unit ID beyond vector size - must expand
needsVectorExpansion = true;
break; // No point checking further
} else if (
mutatingState->units()->Get(unit->unit_id())->status() ==
mutatingState->units()->Get(unit.unit_id())->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
// Unit wants to use a reserved slot
needsReservedSlotConversion = true;
@@ -199,8 +196,8 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
// Convert reserved slots to real units in place
// We only need to process the units that are being changed
for (const auto &unitBytes : result.changed_units_fb()) {
const auto *unit = &UnitFromBytes(unitBytes);
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit->unit_id());
const auto unit = UnitFromBytes(unitBytes);
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit.unit_id());
if (mutableUnit->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
// Convert this reserved slot to a real unit
@@ -248,12 +245,12 @@ void MutatingApplyResult(
static_cast<net::eagle0::shardok::storage::fb::GameStatus_::State>(
result.game_status().state())));
const int winningShardokIdCount = result.game_status().winning_shardok_ids_size();
for (int i = 0; i < winningShardokIdCount; i++) {
for (int i = 0; i < winningShardokIdCount; ++i) {
mutatingGameState->mutable_status()->mutable_winning_shardok_ids()->Mutate(
i,
result.game_status().winning_shardok_ids(i));
}
for (int i = winningShardokIdCount; i < 10; i++) {
for (int i = winningShardokIdCount; i < 10; ++i) {
mutatingGameState->mutable_status()->mutable_winning_shardok_ids()->Mutate(i, -1);
}
@@ -299,50 +296,48 @@ void MutatingApplyResult(
const double vipCapturedMoraleAdjustment = settings.Backing().vip_captured_morale_adjustment();
for (const auto &changedUnitBytes : result.changed_units_fb()) {
const Unit *changedUnit = &UnitFromBytes(changedUnitBytes);
const Unit changedUnit = UnitFromBytes(changedUnitBytes);
internalAssert(
changedUnit->unit_id() < (int32_t)mutatingGameState->units()->size(),
changedUnit.unit_id() < static_cast<int32_t>(mutatingGameState->units()->size()),
"Got a changed unit outside acceptable range");
const auto battalionSizeAfter = changedUnit->battalion().size();
const auto battalionSizeAfter = changedUnit.battalion().size();
const int battalionSizeBefore = (changedUnit->unit_id() < (int32_t)initialUnitCount)
? mutatingGameState->units()
->Get(changedUnit->unit_id())
->battalion()
.size()
: changedUnit->battalion().size();
const int battalionSizeBefore =
(changedUnit.unit_id() < static_cast<int32_t>(initialUnitCount))
? mutatingGameState->units()->Get(changedUnit.unit_id())->battalion().size()
: changedUnit.battalion().size();
auto status = changedUnit->status();
if (IsDestroyed(*changedUnit)) {
auto status = changedUnit.status();
if (IsDestroyed(changedUnit)) {
status =
changedUnit->has_attached_hero()
changedUnit.has_attached_hero()
? net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT
: net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT;
ApplyResolvedUnit(
mutatingGameState.Get(),
*changedUnit,
changedUnit,
status,
vipCapturedMoraleAdjustment,
settings);
}
// Capture old position before applying changes
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit->unit_id());
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit.unit_id());
const auto oldLocation = mutableUnit->location();
fb::ApplyUnit(mutableUnit, changedUnit, status);
fb::ApplyUnit(mutableUnit, &changedUnit, status);
// Update occupied tiles bitfield if position changed
const auto &newLocation = changedUnit->location();
const auto &newLocation = changedUnit.location();
if (oldLocation.row() != newLocation.row() ||
oldLocation.column() != newLocation.column()) {
mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
}
if (battalionSizeBefore != battalionSizeAfter) {
if (changedUnit->battalion().type() ==
if (changedUnit.battalion().type() ==
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
if (battalionSizeAfter > battalionSizeBefore) {
// Undead consumed some bodies
@@ -354,7 +349,7 @@ void MutatingApplyResult(
if (battalionSizeAfter > battalionSizeBefore) {
std::ostringstream stringStream;
stringStream << "Battalion got larger! Was " << battalionSizeBefore << ", now "
<< changedUnit << " result was " << result.DebugString();
<< &changedUnit << " result was " << result.DebugString();
throw ShardokInternalErrorException(stringStream.str());
}
@@ -364,7 +359,7 @@ void MutatingApplyResult(
if (newCount != 0) {
newArmament =
(mutatingGameState->dead_count() * mutatingGameState->dead_armament() +
casualties * changedUnit->battalion().armament()) /
casualties * changedUnit.battalion().armament()) /
newCount;
}
@@ -384,10 +379,10 @@ void MutatingApplyResult(
internalAssert(mutatingGameState->mutate_eligible_charger_id(-1));
}
const int possibleChargeeCount = result.possible_chargees_size();
for (int i = 0; i < possibleChargeeCount; i++) {
for (int i = 0; i < possibleChargeeCount; ++i) {
mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, result.possible_chargees(i));
}
for (int i = possibleChargeeCount; i < 6; i++) {
for (int i = possibleChargeeCount; i < 6; ++i) {
mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, -1);
}
@@ -23,11 +23,11 @@ void MutatingApplyResult(
GameStateW& mutatingGameState,
const ActionResultProto& actionResult,
const SettingsGetter& settings);
auto ApplyResult(
[[nodiscard]] auto ApplyResult(
GameStateW startingState,
const ActionResultProto& actionResult,
const SettingsGetter& settings) -> GameStateW;
auto ApplyResults(
[[nodiscard]] auto ApplyResults(
const GameStateW& startingState,
const std::vector<ActionResultProto>& actionResults,
const SettingsGetter& settings) -> GameStateW;
@@ -16,6 +16,7 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
"//src/main/cpp/net/eagle0/shardok/library/map:hex_map_hasher",
"//src/main/cpp/net/eagle0/shardok/library/unit",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
],

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