Compare commits

...
Author SHA1 Message Date
admin 03b68d744b Keep manage prisoners controls in panel 2026-06-26 10:04:27 -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
536 changed files with 8101 additions and 10306 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]
@@ -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
+5
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).
+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)
+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,7 +37,7 @@ 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);
const auto* positions = positionList->positions();
if (positions->empty()) continue;
@@ -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;
@@ -54,7 +54,7 @@ void runBattle(const std::vector<shardok::Unit>& units) {
const auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
std::vector<shardok::Unit> p1Units{};
p1Units.reserve(p1UnitCount);
for (int i = 0; i < p1UnitCount; i++) {
for (int i = 0; i < p1UnitCount; ++i) {
auto unit = units[i];
unit.mutate_player_id(0);
p1Units.push_back(std::move(unit));
@@ -64,7 +64,7 @@ void runBattle(const std::vector<shardok::Unit>& units) {
const auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
std::vector<shardok::Unit> p2Units{};
p2Units.reserve(p2UnitCount);
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; i++) {
for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; ++i) {
auto unit = units[i];
unit.mutate_player_id(1);
p2Units.push_back(std::move(unit));
@@ -158,7 +158,7 @@ auto main(int /*argc*/, char** argv) -> int {
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,
@@ -163,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) {
@@ -179,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);
@@ -195,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;
@@ -215,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
@@ -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,
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) {
@@ -336,9 +426,21 @@ auto AICommandEvaluator::FindBestCommand(
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,
@@ -423,7 +525,7 @@ auto AICommandEvaluator::FindBestCommand(
ScoreValue sum = 0.0;
const int sampleCount = std::max(1, maxRepeatCount);
commandEvaluations[index].lookaheadFutures.reserve(sampleCount);
for (int repeatIteration = 0; repeatIteration < sampleCount; repeatIteration++) {
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)};
@@ -460,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;
}
@@ -527,7 +639,7 @@ auto AICommandEvaluator::EvaluateCommand(
commandIndex,
remainingLookahead,
maxRepeatCount,
kAverageGenerator,
AverageGenerator(),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -546,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,
@@ -559,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,
@@ -587,7 +701,7 @@ auto AICommandEvaluator::EvaluateCommand(
const int sampleCount = std::max(1, maxRepeatCount);
lookaheadFutures.reserve(sampleCount);
for (int repeatIteration = 0; repeatIteration < sampleCount; repeatIteration++) {
for (int repeatIteration = 0; repeatIteration < sampleCount; ++repeatIteration) {
auto sequence = std::vector{RandomnessSampleForRepeat(repeatIteration, sampleCount)};
auto evaluation = EvaluateWithRandomness(
pid,
@@ -78,7 +78,7 @@ public:
private:
const AIScoreCalculator& scorer_;
const APDCache& apdCache_;
BattalionTypeGetter battalionTypeGetter_; // Store by value
BattalionTypeGetter battalionTypeGetter_;
struct ImmediateAndLookaheadScore {
ScoreValue immediateScore{};
@@ -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,
@@ -12,7 +12,7 @@
namespace shardok {
inline auto CurrentAIExperimentId() -> int {
[[nodiscard]] inline auto CurrentAIExperimentId() -> int {
const char* rawValue = std::getenv("SHARDOK_SCORING_EXPERIMENT_ID");
if (rawValue == nullptr) { return 0; }
@@ -28,7 +28,9 @@ inline auto CurrentAIExperimentId() -> int {
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
@@ -30,7 +30,7 @@ public:
AIEvaluationCounter(AIEvaluationCounter&&) = delete;
auto operator=(AIEvaluationCounter&&) -> AIEvaluationCounter& = delete;
static int GetCurrentCount();
[[nodiscard]] static int GetCurrentCount();
};
// Configuration structure for iterative deepening time budget
@@ -44,7 +44,7 @@ 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,
@@ -88,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 =
@@ -120,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
@@ -235,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:
@@ -247,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(
@@ -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));
}
}
@@ -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)
@@ -8,6 +8,7 @@
#include <chrono>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <numeric>
#include "ShardokAction.hpp"
@@ -43,7 +44,7 @@ auto SortActionsByWeight(
std::vector<size_t> sortedIndices(actions.size());
std::iota(sortedIndices.begin(), sortedIndices.end(), 0);
std::sort(sortedIndices.begin(), sortedIndices.end(), [&weights](size_t a, size_t b) {
std::ranges::sort(sortedIndices, [&weights](size_t a, size_t b) {
return weights[a] > weights[b];
});
@@ -72,6 +73,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,
@@ -92,7 +97,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(
@@ -106,27 +113,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
@@ -187,11 +182,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;
@@ -211,28 +201,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();
}
@@ -266,40 +246,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);
return SortActionsByWeight(std::move(actions), weights);
if (cacheEntryMatches) {
cacheHits_.fetch_add(1, std::memory_order_relaxed);
// Convert immutable cached action snapshots to MCTS action instances.
std::vector<std::unique_ptr<MCTSAction>> actions;
actions.reserve(it->second.actions.size());
for (const auto& cachedAction : it->second.actions) {
actions.push_back(cachedAction.clone());
}
// 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);
@@ -307,17 +296,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);
@@ -338,20 +320,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);
}
}
@@ -368,20 +354,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;
@@ -398,9 +384,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;
}
@@ -415,15 +400,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
@@ -442,20 +419,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_,
@@ -489,7 +459,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
@@ -591,22 +561,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() !=
@@ -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);
}
@@ -356,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
@@ -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
@@ -28,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 {
@@ -331,10 +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;
playerInfoProtos.reserve(2);
// Attacker info
net::eagle0::shardok::common::PlayerInfo attackerInfo;
attackerInfo.set_player_id(ATTACKER_ID);
@@ -344,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(std::move(attackerInfo));
// Defender info
net::eagle0::shardok::common::PlayerInfo defenderInfo;
@@ -355,7 +349,10 @@ 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(std::move(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;
@@ -961,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
@@ -191,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");
@@ -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>
@@ -27,6 +28,77 @@ using net::eagle0::common::ScopedShardokLatencyTrace;
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 {
@@ -154,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 = [&] {
@@ -165,6 +239,7 @@ 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)
{
@@ -183,6 +258,7 @@ void ShardokGameController::DoAIThread() {
}
{
aiPhase = "posting chosen command";
ScopedShardokLatencyTrace trace(
"ai_engine_post_command",
cachedGameId,
@@ -193,6 +269,7 @@ void ShardokGameController::DoAIThread() {
}
LockedNotifyClients();
{
aiPhase = "posting forced follow-up commands";
ScopedShardokLatencyTrace trace(
"ai_engine_post_forced_commands",
cachedGameId,
@@ -204,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;
@@ -32,9 +32,8 @@ private:
static auto CannotBecomeOutlaw(const GameStateW &gameState, PlayerId pid) -> bool {
if (pid == UNCONTROLLED_PLAYER_ID) return false;
const auto &player = std::find_if(
std::begin(*gameState->player_infos()),
std::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 != std::end(*gameState->player_infos()) && player->cannot_become_outlaw());
@@ -43,9 +42,8 @@ private:
static auto IsAttacker(const GameStateW &gameState, PlayerId pid) -> bool {
if (pid == UNCONTROLLED_PLAYER_ID) return false;
const auto &player = std::find_if(
std::begin(*gameState->player_infos()),
std::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 == std::end(*gameState->player_infos()) || !player->is_defender());
@@ -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);
@@ -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
@@ -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) {}
@@ -22,13 +22,13 @@ 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,
@@ -36,7 +36,7 @@ PercentileRollOdds MakeOdds(
int statF = 0,
std::span<const OtherFactor> others = {});
PercentileRollOdds MakeOdds(
[[nodiscard]] PercentileRollOdds MakeOdds(
int base,
int terr,
int weath,
@@ -44,11 +44,11 @@ PercentileRollOdds MakeOdds(
const std::vector<double> &stats,
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(
@@ -443,7 +443,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) {
@@ -455,7 +455,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)
@@ -718,8 +718,8 @@ 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;
@@ -31,7 +31,7 @@ auto ActionPointDistances::BraveWaterPossibleCoords(const HexMap* hexMap) const
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()) {
@@ -71,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));
@@ -85,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));
@@ -119,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;
@@ -141,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);
}
@@ -243,7 +243,7 @@ OnDemandActionPointDistances::OnDemandActionPointDistances(
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>;
@@ -42,13 +42,13 @@ auto FixedActionPointDistances::Create(
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,51 +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() < static_cast<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() < static_cast<int32_t>(initialUnitCount))
? mutatingGameState->units()
->Get(changedUnit->unit_id())
->battalion()
.size()
: changedUnit->battalion().size();
(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
@@ -355,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());
}
@@ -365,7 +359,7 @@ void MutatingApplyResult(
if (newCount != 0) {
newArmament =
(mutatingGameState->dead_count() * mutatingGameState->dead_armament() +
casualties * changedUnit->battalion().armament()) /
casualties * changedUnit.battalion().armament()) /
newCount;
}
@@ -385,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",
],
@@ -19,7 +19,7 @@ auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> Game
// Add the requested units plus some extra slack for future use
int extraSlack = std::max(5, additionalCount * 2);
for (int i = 0; i < additionalCount + extraSlack; i++) {
for (int i = 0; i < additionalCount + extraSlack; ++i) {
Unit unit;
unit.mutate_unit_id(static_cast<int16_t>(endGST.units.size()));
if (i < additionalCount) {
@@ -9,7 +9,8 @@
namespace shardok {
auto CopyWithExtraUnits(const GameStateW& original, int additionalCount) -> GameStateW;
[[nodiscard]] auto CopyWithExtraUnits(const GameStateW& original, int additionalCount)
-> GameStateW;
} // namespace shardok
@@ -8,7 +8,7 @@
namespace shardok::fb {
using namespace net::eagle0::shardok::storage::fb;
using net::eagle0::shardok::storage::fb::UnitStatus;
auto ApplyUnit(
Unit* toUnit,
@@ -17,7 +17,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
using namespace net::eagle0::shardok::common;
using net::eagle0::shardok::common::WATER_DAMAGE;
auto EscapeChance(
const int base,
@@ -65,7 +65,7 @@ auto EscapeChance(
}
auto ToVector(const Units &units) -> vector<const Unit *> {
auto v = vector<const Unit *>{};
vector<const Unit *> v{};
v.reserve(units.size());
for (const Unit *u : units) { v.push_back(u); }
return v;
@@ -90,7 +90,7 @@ auto BurnStructuresResult(const GameStateW &gameState, const SettingsGetter &set
*result.mutable_changed_tile_modifiers() = {burnedTmcs.begin(), burnedTmcs.end()};
return result;
} else {
return std::optional<ActionResultProto>();
return std::nullopt;
}
}
@@ -60,7 +60,8 @@ auto StartPlayerTurnAction::InternalExecute(
->Get(startingState->month() - 1)
->is_winter() &&
!inCastle) {
effectiveBase += float(settingsGetter.Backing().winter_morale_adjustment());
effectiveBase +=
static_cast<float>(settingsGetter.Backing().winter_morale_adjustment());
}
float moraleDiff = effectiveBase - updatedUnit.battalion().morale();
MutatingMaybeAdjustMorale(
@@ -34,7 +34,8 @@ auto UndeadChangeAction::InternalExecute(
int growth = newCount - decayedSize;
float newArmament =
float(unit->battalion().armament() * decayedSize + armament * growth) / newCount;
static_cast<float>(unit->battalion().armament() * decayedSize + armament * growth) /
newCount;
auto newUnit = *unit;
newUnit.mutable_battalion().mutate_size(newCount);
@@ -25,10 +25,9 @@ static inline auto GameIsOver(const GameStatusFb* status) -> bool {
}
auto UpdateGameStatusAction::GetPlayerInfo(int playerId) const -> const PlayerInfoFb* {
return *std::find_if(
std::begin(*gameState->player_infos()),
std::end(*gameState->player_infos()),
[playerId](const PlayerInfoFb* pi) { return pi->player_id() == playerId; });
return *std::ranges::find_if(*gameState->player_infos(), [playerId](const PlayerInfoFb* pi) {
return pi->player_id() == playerId;
});
}
static inline auto HasVictoryCondition(const PlayerInfoFb* pi, const VictoryCondition condition)
@@ -29,7 +29,7 @@ auto UpdateOpponentKnowledgeAction::InternalExecute(
auto unitAfter = *unit;
for (PlayerId pid = 0; pid < 10; pid++) {
for (PlayerId pid = 0; pid < 10; ++pid) {
if (pid == unitPid) continue;
if (static_cast<unsigned int>(pid) >= playerCount) continue;
int bump = PlayerIsDefender(currentState, pid) ? defenderKnowledgeGain
@@ -126,7 +126,7 @@ void BraveWaterCommandFactory::AddAvailableBraveWaterCommands(
}
for (const auto &target : braveWaterTargets) {
std::optional<UnitId> ambusher = std::optional<UnitId>();
std::optional<UnitId> ambusher = std::nullopt;
const auto &targetOccupant = Occupant(units, target);
if (targetOccupant) { ambusher = targetOccupant->unit_id(); }
commands.push_back(BraveWaterCommandFactory(settings)
@@ -19,7 +19,7 @@ private:
public:
explicit CommandFactoriesList(const SettingsGetter& settings);
auto GetFactories() const -> std::vector<std::shared_ptr<const CommandFactory>>;
[[nodiscard]] auto GetFactories() const -> std::vector<std::shared_ptr<const CommandFactory>>;
};
} // namespace shardok
@@ -41,7 +41,7 @@ public:
virtual void AddAvailableCommands(CommandList& commands, const CommandParams& params) const = 0;
virtual auto IncludeInFollowUps() const -> bool { return true; }
[[nodiscard]] virtual auto IncludeInFollowUps() const -> bool { return true; }
};
} // namespace shardok
@@ -11,7 +11,7 @@
namespace shardok {
class EvacuatePrisonersCommandFactory : public CommandFactory {
const SettingsGetter& settings;
const SettingsGetter settings;
public:
explicit EvacuatePrisonersCommandFactory(const SettingsGetter& getter);
@@ -27,7 +27,7 @@ void FearCommandFactory::AddAvailableFearCommands(
if (costForFear.IsPossible(remainingActionPoints)) {
const int range = settings.Backing().fear_range();
for (int distance = 1; distance <= range; distance++) {
for (int distance = 1; distance <= range; ++distance) {
const auto coordsSet = TilesWithExactDistance(hexMap, position, distance);
for (const Coords &fearCoords : coordsSet) {
@@ -61,9 +61,11 @@ void FleeCommandFactory::AddAvailableFleeCommands(
const auto *occupant = Occupant(allUnits, twoAwayCoords);
if (occupant && !occupant->hidden()) {
if (std::ranges::contains(allyPids, occupant->player_id())) {
adjacentFriendliesMod += int32_t(perAdjacentFriendly * adjustmentForTwoAway);
adjacentFriendliesMod +=
static_cast<int32_t>(perAdjacentFriendly * adjustmentForTwoAway);
} else {
adjacentEnemiesMod += int32_t(perAdjacentEnemy * adjustmentForTwoAway);
adjacentEnemiesMod +=
static_cast<int32_t>(perAdjacentEnemy * adjustmentForTwoAway);
}
}
}
@@ -46,7 +46,7 @@ auto LightningBoltCommandFactory::AddAvailableLightningBoltCommands(
ActionCost::UsesAllActionCost(settings.Backing().lightning_action_point_cost());
const int lightningRange = settings.Backing().lightning_range();
if (costForLightning.IsPossible(remainingActionPoints)) {
for (int distance = 1; distance <= lightningRange; distance++) {
for (int distance = 1; distance <= lightningRange; ++distance) {
CoordsSet coords = TilesWithExactDistance(hexMap, position, distance);
for (const Coords &lightningCoords : coords) {
@@ -22,7 +22,7 @@ struct AccumulatedMoveInfo {
Coords endLocation;
ActionPoints pointCost = 0;
bool willUnhide = false;
vector<Coords> targets = {};
vector<Coords> targets{};
AccumulatedMoveInfo(
const Coords &startLocation,
@@ -41,7 +41,7 @@ void RaiseDeadCommandFactory::AddAvailableRaiseDeadCommands(
const BattalionTypeSPtr &undeadType = settings.GetBattalionType(
net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD);
for (int distance = 1; distance <= range; distance++) {
for (int distance = 1; distance <= range; ++distance) {
const CoordsSet coordsSet = TilesWithExactDistance(hexMap, position, distance);
for (const Coords &raiseDeadCoords : coordsSet) {
@@ -34,7 +34,7 @@ void ScoutCommandFactory::AddAvailableScoutCommands(
const Coords unitLocation = unit->location();
if (costForScout.IsPossible(remainingActionPoints)) {
for (int distance = 1; distance <= scoutRange; distance++) {
for (int distance = 1; distance <= scoutRange; ++distance) {
CoordsSet coordsSet = TilesWithExactDistance(hexMap, unitLocation, distance);
for (const Coords& scoutCoords : coordsSet) {

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