Compare commits

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

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

* Move March narrow recommended button into prefab

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

* Refine March narrow supply controls

* Polish March narrow supply controls

* Move March recommended button into units header

* Rework March narrow tabs and supply controls

* Center March narrow minus glyph

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

* Wire March unit selector scene controller

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

* Make Trade controls touch sized

* Refine Trade slider controls

* Balance Trade resource controls

* Hide Trade plus button backing images

* Use round Trade plus backgrounds

* Soften Trade plus artwork

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

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

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

* Place volley badge beside unit size

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

* Center Shardok volley tile indicator

* Raise Shardok volley tile indicator

* Add backing halo to volley indicators

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

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

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

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

* Fix stunned tile effect setup

* Avoid TMP outline on stunned effect stars

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

* Fix ransom offer panel header layout

* Keep ransom offer bottom controls inside panel

* Restructure ransom offer panel layout

* Increase ransom offer title row height

* Inset ransom offer content row bottom

* Make ransom offer content row shrinkable

* Constrain ransom offer main content height

* Move ransom offer height constraint to main row

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

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

* Avoid restarting active touch long presses

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

* Allow observing all unblocked battles

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

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

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

* Fit available battalion rows with larger buttons

* Cap Organize Troops table outside narrow layout

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

* Apply Organize Troops layout to scene panel

* Use prefab instance for Organize Troops panel

* Fix Organize Troops bottom controls layout

* Improve Organize Troops bottom strip spacing

* Set Organize Troops action button height

* Keep Organize Troops controls inside panel

* Place Organize Troops controls under battalion table

* Match Organize Troops reset button height

* Make Organize Troops controls responsive

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

* Limit chronicle history in LLM prompts

* Revert "Limit chronicle history in LLM prompts"

This reverts commit 7f22be26eb00b17aaf77982173688fc9e5c7e3d3.

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

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

* Refine Manage Prisoners info panel layout

* Simplify Manage Prisoners info layout

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

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

* Use Manage Prisoners prefab in Eagle scene

* Narrow Manage Prisoners scene prefab replacement

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

* Revert "Stop warning for replacement command tokens"

This reverts commit fab7fa47678b71f76076142f35465d36fb3fc456.

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

* Reserve selected light cavalry for mages

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

* Widen scene organize troops table

* Give organize troops table more room

* Tighten organize troops row layout

* Remove organize troops disclosure triangle

* Align organize troops header and row layout

* Center organize troops type icons

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

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

* Refine Shardok touch info panel behavior

* Suppress mouse hover after Shardok touch release

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

* Restore compact March narrow layout

* Stop inflating unit selector child icons

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

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

* Prevent Arm Troops name overlap

* Align Arm Troops action column headers

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

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

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

* Give Arm Troops row names layout width

* Restore Arm Troops panel layout widths

* Use compact Arm Troops table in touch layout

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

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

* Fit wider Arm Troops pointer rows

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

* Allow editor touch simulation for Arm Troops

* Constrain Arm Troops touch action strip

* Move Arm Troops touch controls beside table

* Compact Arm Troops touch controls

* Tighten Arm Troops touch table width

* Polish Arm Troops touch selection controls

* Clarify Arm Troops touch columns

* Align Arm Troops touch column headers

* Separate Arm Troops all-battalion controls

* Use column layout for Arm Troops touch controls

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

This reverts commit e6273c22f900e990c86fcac5e761723d54bd15d0.

* Use dedicated touch all-battalions controls

* Handle missing touch button style sources

* Revert "Handle missing touch button style sources"

This reverts commit 2a2b2204bbc0deede8e92ffa5d07cd86ccf3cb96.

* Revert "Use dedicated touch all-battalions controls"

This reverts commit 44fd112162518b9d0658f9374af5771196ccefbb.

* Fit Arm Troops all-battalion touch column

* Use correct Arm Troops all-controls parent

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

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

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

* Use direct GamesManager state deps

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

* Split game controller registry target

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

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

* Use inside for march command province lookups

* Use inside for diplomacy command lookups

* Use inside for captured hero deferred changes

* Use inside for applier relationship lookups

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

* Use inside for offer command selections

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

* Use inside for resolve availability Options

* Use inside for defend and train Options

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

* Widen Arm Troops table for touch controls

* Improve Arm Troops row button spacing

* Add Arm Troops scrollbar gutter
2026-06-23 19:21:06 -07:00
adminandGitHub 569458d691 Use inside for attack decision new values (#8066) 2026-06-23 17:42:33 -07:00
adminandGitHub c825bf4606 Use inside for supplies and riot availability Options (#8070) 2026-06-23 17:41:23 -07:00
289 changed files with 11896 additions and 141449 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[@]}"
+12 -2
View File
@@ -32,23 +32,33 @@ echo "=== Creating ZIP for notarization: $ZIP_PATH ===" >&2
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "=== Submitting to Apple for notarization ===" >&2
set +e
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" 2>&1)
SUBMIT_STATUS=$?
set -e
echo "$SUBMIT_OUTPUT" >&2
if [ "$SUBMIT_STATUS" -ne 0 ]; then
rm -f "$ZIP_PATH"
echo "ERROR: notarytool submit failed with exit code $SUBMIT_STATUS" >&2
exit "$SUBMIT_STATUS"
fi
# Extract submission ID
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | awk '/^[[:space:]]*id:/ {print $2; exit}')
if [ -z "$SUBMISSION_ID" ]; then
rm -f "$ZIP_PATH"
echo "ERROR: Failed to get submission ID" >&2
exit 1
fi
# Clean up the zip
rm "$ZIP_PATH"
rm -f "$ZIP_PATH"
echo "Submission ID: $SUBMISSION_ID" >&2
+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);
@@ -9,6 +9,7 @@ namespace shardok {
using Coords = net::eagle0::shardok::storage::fb::Coords;
constexpr double kDefaultMorale = 50.0;
constexpr float kExplicitMoraleSentinel = -1.0f;
auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) -> Battalion {
Battalion shardokBattalion{};
@@ -18,6 +19,10 @@ auto ConvertBattalion(const net::eagle0::common::CommonBattalion &battalion) ->
shardokBattalion.mutate_type(
static_cast<net::eagle0::shardok::storage::fb::BattalionTypeId>(battalion.type()));
shardokBattalion.mutate_morale(kDefaultMorale);
if (battalion.has_morale()) {
shardokBattalion.mutate_morale(static_cast<float>(battalion.morale()));
shardokBattalion.mutate_base_morale(kExplicitMoraleSentinel);
}
shardokBattalion.mutate_armament(static_cast<float>(battalion.armament()));
shardokBattalion.mutate_training(static_cast<float>(battalion.training()));
@@ -125,4 +130,4 @@ auto ConvertUnit(
return shardokUnit;
}
} // namespace shardok
} // namespace shardok
+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) {
@@ -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;
@@ -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());
@@ -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,12 +23,49 @@
namespace shardok {
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;
[[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:
@@ -39,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;
@@ -59,6 +96,14 @@ static auto RandomnessSampleForRepeat(const int repeatIteration, const int maxRe
return static_cast<double>(repeatIteration) / static_cast<double>(maxRepeatCount - 1);
}
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 {
@@ -133,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;
@@ -194,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;
@@ -216,22 +300,33 @@ auto AICommandEvaluator::EvaluateWithRandomness(
isDefender,
remainingLookahead,
maxRepeatCount,
commandIndex,
innerEngine,
attackerStrategy,
innerUtility,
&allCastleCoords,
guessedCommands,
allCastleCoords,
deadline]() -> EvaluationResult {
auto lookaheadFuture = PerformLookahead(
pid,
isDefender,
remainingLookahead,
maxRepeatCount,
innerEngine,
innerUtility,
attackerStrategy,
allCastleCoords,
deadline);
return lookaheadFuture.get();
try {
auto lookaheadFuture = PerformLookahead(
pid,
isDefender,
remainingLookahead,
maxRepeatCount,
innerEngine,
innerUtility,
attackerStrategy,
allCastleCoords,
deadline);
return lookaheadFuture.get();
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI EvaluateWithRandomness failed during recursive lookahead"
<< " player=" << static_cast<int>(pid) << " command_index=" << commandIndex
<< " remaining_lookahead=" << remainingLookahead << ' '
<< DescribeCommandAt(guessedCommands, commandIndex);
throw WithExceptionContext(context.str(), e);
}
};
if constexpr (kMultithread) {
@@ -333,7 +428,19 @@ auto AICommandEvaluator::FindBestCommand(
for (uint32_t index = 0; index < commandCount; ++index) {
const auto originalIndex = filteredIndices[index];
const auto& guessedDescriptor = guessedDescriptors->at(originalIndex);
const CommandSPtr guessedDescriptor = [&] {
try {
return guessedDescriptors->at(originalIndex);
} catch (const std::exception& e) {
std::ostringstream context;
context << "AI FindBestCommand failed while reading filtered command"
<< " player=" << static_cast<int>(pid) << " filtered_index=" << index
<< " original_index=" << originalIndex
<< " guessed_command_count=" << guessedDescriptors->size()
<< " filtered_command_count=" << filteredIndices.size();
throw WithExceptionContext(context.str(), e);
}
}();
const auto guessedCommandType = guessedDescriptor->GetCommandType();
commandEvaluations[index].index = originalIndex;
@@ -351,7 +458,7 @@ auto AICommandEvaluator::FindBestCommand(
originalIndex,
remainingLookahead,
maxRepeatCount,
kAverageGenerator,
AverageGenerator(),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -365,7 +472,7 @@ auto AICommandEvaluator::FindBestCommand(
const auto successChancePercentile = guessedDescriptor->GetOddsPercentile();
const double successChance = static_cast<double>(successChancePercentile) / 100.0;
// Success attempt uses 1.0 - (successChance / 2) as the roll
// Success attempt uses the midpoint of the successful percentile range.
auto successEvaluation = EvaluateWithRandomness(
pid,
isDefender,
@@ -373,13 +480,13 @@ auto AICommandEvaluator::FindBestCommand(
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(
std::vector{1.0 - successChance / 2.0}),
std::vector{SuccessSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
deadline);
// Failure attempt uses the average of (1 - successChance) and 0 as the roll
// Failure attempt uses the midpoint of the failing percentile range.
auto failureEvaluation = EvaluateWithRandomness(
pid,
isDefender,
@@ -387,7 +494,7 @@ auto AICommandEvaluator::FindBestCommand(
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(
std::vector{(1.0 - successChance) / 2.0}),
std::vector{FailureSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -455,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;
}
@@ -522,7 +639,7 @@ auto AICommandEvaluator::EvaluateCommand(
commandIndex,
remainingLookahead,
maxRepeatCount,
kAverageGenerator,
AverageGenerator(),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -541,7 +658,8 @@ auto AICommandEvaluator::EvaluateCommand(
commandIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(std::vector{1.0 - successChance / 2.0}),
std::make_shared<SequenceRandomGenerator>(
std::vector{SuccessSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -554,7 +672,8 @@ auto AICommandEvaluator::EvaluateCommand(
commandIndex,
remainingLookahead,
maxRepeatCount,
std::make_shared<SequenceRandomGenerator>(std::vector{(1.0 - successChance) / 2.0}),
std::make_shared<SequenceRandomGenerator>(
std::vector{FailureSampleForOdds(successChance)}),
guessedEngine,
attackerStrategy,
allCastleCoords,
@@ -116,8 +116,9 @@ auto DefenderDistanceBuf(
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;
}
@@ -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
@@ -25,7 +25,8 @@ using Unit = net::eagle0::shardok::storage::fb::Unit;
net::eagle0::shardok::storage::fb::Profession profession) -> bool;
[[nodiscard]] auto CastleClaimCapableAttackerUnitCount(const GameStateW &gameState) -> int;
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &, PlayerId pid) -> const PlayerInfo *;
[[nodiscard]] auto PlayerInfoForPid(const GameStateW &gameState, PlayerId pid)
-> const PlayerInfo *;
} // namespace shardok
@@ -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(
@@ -6,9 +6,18 @@
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
namespace {
auto RecomputeModifierHash(HexMap *hexMap) -> MapId {
hexMap->mutate_modifier_hash(GetModifierHash(hexMap));
return ActionPointDistancesCache::GetMapId(hexMap);
}
} // namespace
auto UnitIdsRequiringWaterCrossing(
const GameStateW &gameState,
@@ -169,6 +178,7 @@ auto WaterCrossingTiles(
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index));
terr->mutable_modifier().mutable_bridge().mutate_present(true);
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
terr->mutable_modifier().mutable_fire().mutate_present(false);
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
@@ -185,6 +195,50 @@ auto WaterCrossingTiles(
return returnCoords;
}
// Returns currently frozen water tiles that can act as an existing crossing from origin to
// destination. The shared APD cache deliberately ignores ice, so this probes frozen tiles by
// temporarily treating one as bridged for the water-crossing strategy only.
auto FrozenWaterCrossingTiles(
const Coords &origin,
const Coords &destination,
const HexMap *hexMap,
const APDCache &apdCache,
const BattalionTypeSPtr &battalionType) -> CoordsSet {
AssertValid(origin, hexMap);
AssertValid(destination, hexMap);
auto returnCoords = CoordsSet(hexMap);
fb::HexMapW mapCopy = fb::CopyHexMap(hexMap);
const int mapSize = hexMap->row_count() * hexMap->column_count();
for (int index = 0; index < mapSize; ++index) {
const auto startTerrain = *hexMap->terrain()->Get(index);
if (!IsWater(startTerrain.type())) continue;
const auto modifier = startTerrain.modifier();
if (!modifier.ice().present() || modifier.fire().present()) continue;
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index));
terr->mutable_modifier().mutable_bridge().mutate_present(true);
terr->mutable_modifier().mutable_bridge().mutate_integrity(100.0);
terr->mutable_modifier().mutable_fire().mutate_present(false);
const MapId mapId = RecomputeModifierHash(mapCopy.Get());
if (const auto *distances = apdCache->GetRaw(mapCopy, mapId, battalionType, false);
distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
returnCoords.Add(index / hexMap->column_count(), index % hexMap->column_count());
}
terr->mutable_modifier().mutable_bridge().mutate_present(false);
terr->mutable_modifier().mutable_bridge().mutate_integrity(0.0);
}
return returnCoords;
}
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
auto IntendedCrossingStarts(
const GameStateW &gameState,
@@ -213,7 +267,6 @@ auto IntendedCrossingStarts(
using Unit = net::eagle0::shardok::storage::fb::Unit;
constexpr double kNoRequiredCrossingScore = std::numeric_limits<double>::max();
constexpr double kNoCrossingCreatorsScore = std::numeric_limits<double>::min();
auto WaterCrossingScore(
const PlayerId playerId,
@@ -256,12 +309,26 @@ auto WaterCrossingScore(
const auto unitIdsCreatingCrossing =
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
double totalScore = 0;
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
if (unitIdsCreatingCrossing.empty()) {
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
const Coords location = unit->location();
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
const int thisDistance =
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
totalScore -= thisDistance;
}
return totalScore;
}
// First put a big penalty on the distance for units that can create a crossing
for (const UnitId uid : unitIdsCreatingCrossing) {
const Unit *unit = gameState->units()->Get(uid);
@@ -68,6 +68,15 @@ static inline void AssertValid(const Coords& c, const HexMap* hexMap) {
const APDCache& apdCache,
const BattalionTypeSPtr& battalionType) -> CoordsSet;
// Returns currently frozen water tiles that can act as an existing crossing from origin to
// destination.
[[nodiscard]] auto FrozenWaterCrossingTiles(
const Coords& origin,
const Coords& destination,
const HexMap* hexMap,
const APDCache& apdCache,
const BattalionTypeSPtr& battalionType) -> CoordsSet;
// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
[[nodiscard]] auto IntendedCrossingStarts(
const GameStateW& gameState,
@@ -15,7 +15,6 @@ namespace shardok {
using Unit = net::eagle0::shardok::storage::fb::Unit;
constexpr ScoreValue kNoRequiredCrossingScore = std::numeric_limits<ScoreValue>::max();
constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>::min();
[[nodiscard]] auto AIWaterCrossingCommandChooser::WaterCrossingScore(
const BattalionTypeGetter &battalionTypeGetter,
@@ -56,12 +55,26 @@ constexpr ScoreValue kNoCrossingCreatorsScore = std::numeric_limits<ScoreValue>:
const auto unitIdsCreatingCrossing =
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
if (unitIdsCreatingCrossing.empty()) return kNoCrossingCreatorsScore;
ScoreValue totalScore = 0;
const auto mapId = ActionPointDistancesCache::GetMapId(gameState->hex_map());
if (unitIdsCreatingCrossing.empty()) {
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
const auto &battalionType = battalionTypeGetter(unit->battalion().type());
const Coords location = unit->location();
const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);
const int thisDistance =
location.row() < 0 ? 1000 : MinimumDistance(apd, location, startCrossingFrom);
totalScore -= thisDistance;
}
return totalScore;
}
// First put a big penalty on the distance for units that can create a crossing
for (const UnitId uid : unitIdsCreatingCrossing) {
const Unit *unit = gameState->units()->Get(uid);
@@ -157,7 +170,6 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
const auto unitIdsCreatingCrossing =
UnitIdsToCreateWaterCrossing(gameState, playerId, battalionTypeGetter);
if (unitIdsCreatingCrossing.empty()) return startCrossingFrom;
for (const UnitId uid : unitIdsRequiringCrossing) {
const Unit *unit = gameState->units()->Get(uid);
@@ -177,6 +189,17 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
Coords destination = *destinations.begin();
AssertValid(destination, gameState->hex_map());
auto frozenWaterCrossingTiles = FrozenWaterCrossingTiles(
origin,
destination,
gameState->hex_map(),
apdCache,
battalionType);
startCrossingFrom +=
CrossingStartLocations(gameState->hex_map(), frozenWaterCrossingTiles);
if (unitIdsCreatingCrossing.empty()) continue;
auto waterCrossingTiles = WaterCrossingTiles(
origin,
destination,
@@ -192,4 +215,4 @@ auto AIWaterCrossingCommandChooser::StartCrossingFrom(
return startCrossingFrom;
}
} // namespace shardok
} // namespace shardok
@@ -19,8 +19,11 @@
#include <sstream>
#endif
#include <array>
#include <atomic>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <set>
#include <unordered_map>
@@ -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));
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));
}
}
@@ -38,8 +38,10 @@ TranspositionTable::probe(const GameStateW& state, int depth, PlayerId player) {
uint64_t stored_hash = entry.hash_full.load(std::memory_order_relaxed);
uint8_t stored_depth = entry.depth.load(std::memory_order_relaxed);
uint8_t stored_player = entry.player_id.load(std::memory_order_relaxed);
uint16_t stored_age = entry.age.load(std::memory_order_relaxed);
if (stored_hash == hash && stored_depth >= depth && stored_player == player) {
if (stored_hash == hash && stored_depth >= depth && stored_player == player &&
stored_age == current_age.load(std::memory_order_relaxed)) {
stats.hits++;
float score = entry.score.load(std::memory_order_relaxed);
return static_cast<ScoreValue>(score);
@@ -44,6 +44,7 @@ public:
[[nodiscard]] PlayerId getPlayer() const { return player_; }
[[nodiscard]] int getActorId() const { return actorId_; }
[[nodiscard]] std::pair<int, int> getTarget() const { return {targetRow_, targetCol_}; }
[[nodiscard]] bool hasOdds() const { return hasOdds_; }
private:
// Store only essential fields (~25 bytes, all POD, cache-friendly)
@@ -6,8 +6,10 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <numeric>
#include "ShardokAction.hpp"
@@ -32,9 +34,30 @@ auto IsDefenderPlayer(const GameStateW& gameState, const PlayerId playerId) -> b
return false;
}
auto LegalActionsCacheKey(const uint64_t stateHash, const bool rootIsDefender) -> uint64_t {
auto MixCacheKey(uint64_t seed, const uint64_t value) -> uint64_t {
constexpr uint64_t kHashCombineConstant = 0x9e3779b97f4a7c15ULL;
seed ^= value + kHashCombineConstant + (seed << 6) + (seed >> 2);
return seed;
}
auto PointerIdentityHash(const void* ptr) -> uint64_t {
return static_cast<uint64_t>(reinterpret_cast<std::uintptr_t>(ptr));
}
auto LegalActionsCacheKey(
const uint64_t stateHash,
const bool rootIsDefender,
const GameSettings* gameSettings,
const APDCache* apdCache) -> uint64_t {
constexpr uint64_t kDefenderRoleHash = 0x9e3779b97f4a7c15ULL;
return rootIsDefender ? stateHash ^ kDefenderRoleHash : stateHash;
uint64_t key = rootIsDefender ? stateHash ^ kDefenderRoleHash : stateHash;
// The cached action set is the output of AICommandFilter, not just the raw command list.
// Include the context objects that can affect filtering so a process-wide cache entry from
// one search cannot be reused by a different settings/APD context with the same state hash.
key = MixCacheKey(key, PointerIdentityHash(gameSettings));
key = MixCacheKey(key, PointerIdentityHash(apdCache != nullptr ? apdCache->get() : nullptr));
return key;
}
auto SortActionsByWeight(
@@ -72,6 +95,10 @@ std::atomic<uint64_t> ShardokGameEngine::cacheMisses_{0};
std::atomic<uint64_t> ShardokGameEngine::timeInHashComputation_{0};
std::atomic<uint64_t> ShardokGameEngine::timeInLegalActionsComputation_{0};
namespace {
std::once_flag legalActionsCacheReserveOnce;
} // namespace
ShardokGameEngine::ShardokGameEngine(
[[maybe_unused]] const ShardokEngine* engine,
const AIScoreCalculator* scoreCalculator,
@@ -92,7 +119,9 @@ ShardokGameEngine::ShardokGameEngine(
criticalTileCoords_(criticalTileCoords) {
// Thread-local cache is automatically initialized per thread
// Reserve space to reduce rehashing (based on profiling: ~30-50K unique states per search)
legalActionsCache_.reserve(100000);
std::call_once(legalActionsCacheReserveOnce, [] {
ShardokGameEngine::legalActionsCache_.reserve(100000);
});
}
std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
@@ -106,27 +135,15 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
// Use cached engine if available (avoids recomputing GetAvailableCommands for same state)
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
// Clone the cached engine to preserve command cache
engine = std::make_shared<ShardokEngine>(*cachedEngine);
} else {
// Create fresh engine and populate command cache
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate command cache (result intentionally unused, just populating cache)
[[maybe_unused]] const auto commands =
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Cache the engine for future use with this state
shardokState->setCachedEngine(engine);
// Clone it for applying the action (don't mutate the cached engine)
engine = std::make_shared<ShardokEngine>(*engine);
}
auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate the AI command list before posting so the MCTS action index maps to the
// no-follow-up command ordering used by getLegalActions().
[[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Create deterministic random generator if a specific roll is requested
// deterministicRoll of -1.0 (default) means use random generator
@@ -187,11 +204,6 @@ std::unique_ptr<MCTSGameState> ShardokGameEngine::applyAction(
*alCache_,
criticalTileCoords_);
// Cache the engine on the new state so score() can use it for END_TURN normalization
// The engine's command list may be stale after the action was applied, but that's OK -
// we'll refresh it when we call GetAvailableCommandsForAIPlayer() in score()
newState->setCachedEngine(engine);
// Don't pre-compute hash - let it be computed lazily on first use
// Many states (especially in simulation) never need their hash computed
return newState;
@@ -211,28 +223,18 @@ void ShardokGameEngine::applyActionMutable(
const auto currentPlayer = static_cast<PlayerId>(state->currentPlayerId());
// Use cached engine if available
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
engine = std::make_shared<ShardokEngine>(*cachedEngine);
} else {
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate command cache (result intentionally unused, just populating cache)
[[maybe_unused]] const auto commands =
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
shardokState->setCachedEngine(engine);
engine = std::make_shared<ShardokEngine>(*engine);
}
auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate the AI command list before posting so the MCTS action index maps to the
// no-follow-up command ordering used by getLegalActions().
[[maybe_unused]] const auto commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
engine->PostCommand(currentPlayer, shardokAction->getIndex(), nullptr);
shardokState->getMutableShardokState() = engine->GetCurrentGameState();
// Clear the cached engine and hash since the state has been mutated
shardokState->setCachedEngine(nullptr);
shardokState->invalidateHashCache();
}
@@ -258,7 +260,8 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Time hash computation
const auto hashStart = std::chrono::high_resolution_clock::now();
const uint64_t stateHash = shardokState->hash();
const uint64_t cacheKey = LegalActionsCacheKey(stateHash, isDefender_);
const uint64_t cacheKey =
LegalActionsCacheKey(stateHash, isDefender_, gameSettings_.get(), apdCache_);
const auto hashEnd = std::chrono::high_resolution_clock::now();
timeInHashComputation_.fetch_add(
std::chrono::duration_cast<std::chrono::microseconds>(hashEnd - hashStart).count(),
@@ -266,40 +269,49 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Check transposition table for cached legal actions
if (auto it = legalActionsCache_.find(cacheKey); it != legalActionsCache_.end()) {
cacheHits_.fetch_add(1, std::memory_order_relaxed);
const auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_);
// Use cached engine
shardokState->setCachedEngine(it->second.engine);
const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Get commands from the cached engine (Engine already caches these internally)
const CommandListSPtr commands =
it->second.engine->GetAvailableCommandsForAIPlayer(currentPlayer);
if (!commands || commands->empty()) { return {}; }
// Convert to MCTSActions using stored filtered indices
std::vector<std::unique_ptr<MCTSAction>> actions;
actions.reserve(it->second.filteredIndices.size());
for (const size_t origIdx : it->second.filteredIndices) {
if (origIdx < commands->size()) {
const auto& cmd = commands->at(origIdx);
// Extract essential fields directly from command (no proto conversion!)
actions.push_back(std::make_unique<ShardokAction>(
origIdx,
cmd->GetCommandType(),
cmd->GetPlayerId(),
cmd->GetActorUnitId(),
cmd->GetTargetRow(),
cmd->GetTargetColumn(),
cmd->HasOdds()));
bool cacheEntryMatches = commands != nullptr && commands->size() == it->second.commandCount;
for (const auto& cachedAction : it->second.actions) {
const size_t commandIndex = cachedAction.getIndex();
if (!cacheEntryMatches || commandIndex >= commands->size()) {
cacheEntryMatches = false;
break;
}
const auto& command = commands->at(commandIndex);
const auto [targetRow, targetColumn] = cachedAction.getTarget();
cacheEntryMatches = command->GetCommandType() ==
static_cast<net::eagle0::shardok::common::CommandType>(
cachedAction.getType()) &&
command->GetPlayerId() == cachedAction.getPlayer() &&
command->GetActorUnitId() == cachedAction.getActorId() &&
command->GetTargetRow() == targetRow &&
command->GetTargetColumn() == targetColumn &&
command->HasOdds() == cachedAction.hasOdds();
if (!cacheEntryMatches) { break; }
}
// Sort actions by weight (descending) to ensure MCTS explores high-value actions first
const std::vector<double> weights = getActionWeights(actions, state);
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 +319,10 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Time legal actions computation
const auto actionsStart = std::chrono::high_resolution_clock::now();
// Use cached engine if available, otherwise create and cache it
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
engine = cachedEngine;
} else {
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_);
shardokState->setCachedEngine(engine);
}
const auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_);
const CommandListSPtr commands = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
@@ -338,20 +343,24 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
// Convert only filtered commands to MCTSActions
std::vector<std::unique_ptr<MCTSAction>> actions;
actions.reserve(filteredIndices.size());
std::vector<ShardokAction> cachedActions;
cachedActions.reserve(filteredIndices.size());
for (const size_t idx : filteredIndices) {
if (idx < commands->size()) {
const auto& cmd = commands->at(idx);
// Extract essential fields directly from command (no proto conversion!)
actions.push_back(std::make_unique<ShardokAction>(
ShardokAction action(
idx,
cmd->GetCommandType(),
cmd->GetPlayerId(),
cmd->GetActorUnitId(),
cmd->GetTargetRow(),
cmd->GetTargetColumn(),
cmd->HasOdds()));
cmd->HasOdds());
actions.push_back(action.clone());
cachedActions.push_back(action);
}
}
@@ -368,20 +377,20 @@ std::vector<std::unique_ptr<MCTSAction>> ShardokGameEngine::getLegalActions(
.count(),
std::memory_order_relaxed);
// Store in transposition table for future lookups
// Note: We only store filtered indices and the engine (which caches commands internally)
// This avoids duplicating heavy protocol buffer objects
// Store immutable action snapshots in the transposition table for future lookups.
// Do not store ShardokEngine here: its command cache is mutable and not safe to share
// across MCTS worker threads.
// Use lazy_emplace_l to ensure thread-safe insertion (locks the bucket during construction)
legalActionsCache_.lazy_emplace_l(
cacheKey,
[&](typename decltype(legalActionsCache_)::value_type& v) {
// Update existing entry
v.second.filteredIndices = filteredIndices;
v.second.engine = engine;
v.second.commandCount = commands->size();
v.second.actions = cachedActions;
},
[&](const typename decltype(legalActionsCache_)::constructor& ctor) {
// Create new entry
ctor(cacheKey, LegalActionsCache{filteredIndices, engine});
ctor(cacheKey, LegalActionsCache{commands->size(), cachedActions});
});
return actions;
@@ -414,15 +423,7 @@ std::vector<double> ShardokGameEngine::getActionWeights(
"indicates a type mismatch in the MCTS adapter layer");
}
// Get cached engine and command list for looking up command protos
auto cachedEngine = shardokState->getCachedEngine();
if (!cachedEngine) {
throw MCTSInternalError(
"ShardokGameEngine::getActionWeights called with state that has no cached engine");
}
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
const CommandListSPtr commands = cachedEngine->GetAvailableCommandsForAIPlayer(currentPlayer);
// Determine if current player is defender (not root player!)
// During simulation we need to use the correct perspective for action weighting
@@ -441,20 +442,13 @@ std::vector<double> ShardokGameEngine::getActionWeights(
"indicates a type mismatch in the MCTS adapter layer");
}
// Look up command proto from cached engine using action's index
const size_t cmdIndex = shardokAction->getIndex();
if (cmdIndex >= commands->size()) {
throw MCTSInternalError(
"ShardokGameEngine::getActionWeights: action index out of bounds");
}
const auto& cmd = commands->at(cmdIndex);
const auto [targetRow, targetColumn] = shardokAction->getTarget();
weights.push_back(AIHeuristicWeighting::GetCommandWeight(
cmd->GetCommandType(),
cmd->GetActorUnitId(),
cmd->GetPlayerId(),
Coords{cmd->GetTargetRow(), cmd->GetTargetColumn()},
static_cast<net::eagle0::shardok::common::CommandType>(shardokAction->getType()),
shardokAction->getActorId(),
shardokAction->getPlayer(),
Coords{static_cast<int8_t>(targetRow), static_cast<int8_t>(targetColumn)},
gameState,
castleCoords_,
apdCache_,
@@ -488,7 +482,7 @@ bool ShardokGameEngine::shouldStopSearch(
size_t ShardokGameEngine::mapFilteredIndexToOriginal(
size_t filteredIndex,
const MCTSGameState& state) const {
// Get the filtered actions (uses cached engine)
// Get the filtered actions.
auto actions = getLegalActions(state, state.currentPlayerId(), 0, 0);
// Check bounds
@@ -590,22 +584,12 @@ ChanceOutcomeInfo ShardokGameEngine::getBinaryOutcomeInfo(
const auto currentPlayer = static_cast<PlayerId>(state.currentPlayerId());
// Get or create the engine for this state
std::shared_ptr<ShardokEngine> engine;
if (auto cachedEngine = shardokState->getCachedEngine()) {
engine = cachedEngine;
} else {
engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Populate command cache
[[maybe_unused]] const auto commands =
engine->GetAvailableCommandsForAIPlayer(currentPlayer);
shardokState->setCachedEngine(engine);
}
const auto engine = std::make_shared<ShardokEngine>(
gameSettings_,
shardokState->getShardokState(),
criticalTileCoords_,
0,
false);
// Get command descriptors
const auto descriptors = engine->GetAvailableCommandsForAIPlayer(currentPlayer);
@@ -15,6 +15,7 @@
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackLocations.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/mcts/adapters/ShardokAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
@@ -99,11 +100,12 @@ public:
void resetCacheStatistics();
private:
// Transposition table entry for caching legal actions
// Note: We don't store command protos since the Engine already caches them
// Transposition table entry for caching legal actions.
// Store immutable action snapshots only; ShardokEngine owns mutable command caches and
// must remain per-state/per-thread.
struct LegalActionsCache {
std::vector<size_t> filteredIndices;
std::shared_ptr<ShardokEngine> engine; // Engine with populated command cache
size_t commandCount;
std::vector<ShardokAction> actions;
};
const AIScoreCalculator* scoreCalculator_;
@@ -98,7 +98,6 @@ std::unique_ptr<MCTSGameState> ShardokGameState::clone() const {
apdCache_,
alCache_,
criticalTileCoords_);
// Don't copy the cached engine - each state needs its own
return cloned;
}
@@ -10,7 +10,6 @@
#include "src/main/cpp/net/eagle0/common/mcts/abstract/MCTSGameState.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIStrategy.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -52,10 +51,6 @@ public:
[[nodiscard]] const GameSettings* getSettings() const { return settings_; }
[[nodiscard]] const CoordsSet& getCriticalTileCoords() const { return criticalTileCoords_; }
// Engine caching for performance (avoids recomputing available commands)
void setCachedEngine(std::shared_ptr<ShardokEngine> engine) const { cachedEngine_ = engine; }
[[nodiscard]] std::shared_ptr<ShardokEngine> getCachedEngine() const { return cachedEngine_; }
// Invalidate hash cache when state is mutated
void invalidateHashCache() const {
hashCached_ = false;
@@ -74,7 +69,6 @@ private:
mutable uint64_t cachedHash_ = 0;
mutable bool hashCached_ = false;
const CoordsSet criticalTileCoords_; // Own the data to avoid dangling references
mutable std::shared_ptr<ShardokEngine> cachedEngine_; // Engine with cached available commands
};
} // namespace mcts
@@ -133,8 +133,7 @@ auto DefenderHoldsCriticalTilesVictoryScore(
const auto cc = gameState->hex_map()->column_count();
const auto occupants = Occupants(*gameState->units(), rc, cc);
for (const Coords& criticalTileLocation : criticalTileLocations) {
const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
for (const auto index : criticalTileLocations.indexIterator()) {
const auto& occupant = occupants[index];
if (occupant && occupant->battalion().type() !=
@@ -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,
@@ -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,
@@ -29,7 +29,7 @@ public:
*
* @return Shared pointer to initialized GameSettings
*/
static auto CreateDefault() -> GameSettingsSPtr;
[[nodiscard]] static auto CreateDefault() -> GameSettingsSPtr;
};
} // namespace shardok::ai_testing_common
@@ -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;
@@ -189,7 +189,8 @@ struct BattalionType {
}
[[nodiscard]] auto GetCostToEnterTerrain(const TerrainProto &terrain) const -> ActionCost {
if (HasBridge(terrain.modifier())) {
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 /
@@ -208,7 +209,7 @@ 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()) {
if (tm.bridge().present() && tm.bridge().integrity() > 0.0) {
return {ActionCost::standard,
ActionPoints(
std::ceil(100.0 * minimumCostToEnterBridge / tm.bridge().integrity()))};
@@ -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
@@ -140,7 +140,12 @@ ShardokEngine::ShardokEngine(const ShardokEngine &toCopy, const bool trackHistor
gameState(toCopy.gameState),
trackHistory(trackHistory),
startingHistoryCount(toCopy.startingHistoryCount),
startingHistoryState(trackHistory ? toCopy.startingHistoryState : GameStateW{}),
actionHistory(
trackHistory ? toCopy.actionHistory : std::vector<ShardokActionWithResultingState>{}),
criticalTileCoords(toCopy.criticalTileCoords),
tutorialController_(toCopy.tutorialController_),
pendingReinforcementPlacement_(toCopy.pendingReinforcementPlacement_),
cachedAvailableCommands(toCopy.cachedAvailableCommands) {}
auto ShardokEngine::GetGameStateAtStartOfAction(const ActionId startingActionId) const
@@ -718,8 +723,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;
@@ -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;
@@ -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
@@ -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;
}
@@ -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",
],
@@ -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;
@@ -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);
@@ -11,7 +11,7 @@
namespace shardok {
class EvacuatePrisonersCommandFactory : public CommandFactory {
const SettingsGetter& settings;
const SettingsGetter settings;
public:
explicit EvacuatePrisonersCommandFactory(const SettingsGetter& getter);
@@ -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);
}
}
}
@@ -22,7 +22,7 @@ struct AccumulatedMoveInfo {
Coords endLocation;
ActionPoints pointCost = 0;
bool willUnhide = false;
vector<Coords> targets = {};
vector<Coords> targets{};
AccumulatedMoveInfo(
const Coords &startLocation,
@@ -305,6 +305,7 @@ cc_library(
"//src/main/cpp/net/eagle0/shardok/library:percentile_roll_odds",
"//src/main/cpp/net/eagle0/shardok/library:shardok_command",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:odds_filter",
],
)
@@ -12,6 +12,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/PercentileRollOdds.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/OddsFilter.hpp"
namespace shardok {
auto BecomeOutlawCommand::InternalExecute(
@@ -59,7 +60,12 @@ auto BecomeOutlawCommand::GetCommandProto() const -> CommandProto {
proto.set_player(GetPlayerId());
proto.set_type(net::eagle0::shardok::common::BECOME_OUTLAW_COMMAND);
proto.mutable_actor()->set_value(fleeingUnitId);
*proto.mutable_odds() = OddsFilteredForPlayer(successOdds, GetPlayerId());
return proto;
}
auto BecomeOutlawCommand::GetOddsPercentile() const -> int32_t {
return OddsFilteredForPlayer(successOdds, GetPlayerId()).success_chance();
}
} // namespace shardok
@@ -41,6 +41,8 @@ public:
}
[[nodiscard]] auto GetCommandProto() const -> CommandProto override;
[[nodiscard]] auto HasOdds() const -> bool override { return true; }
[[nodiscard]] auto GetOddsPercentile() const -> int32_t override;
[[nodiscard]] auto CanBeFollowUp() const -> bool override { return false; }
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
@@ -41,6 +41,7 @@ public:
}
[[nodiscard]] auto GetCommandProto() const -> CommandProto override;
[[nodiscard]] auto HasOdds() const -> bool override { return true; }
[[nodiscard]] auto GetOddsPercentile() const -> int32_t override;
[[nodiscard]] auto CanBeFollowUp() const -> bool override { return false; }
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
@@ -52,7 +52,7 @@ auto FreezeWaterCommand::InternalExecute(
if (PercentileRollSucceeds(odds, roll)) {
freezeResult.set_type(ActionType::FREEZE_WATER);
TileModifierProto newTileModifier = fb::ToTileModifierProto(targetTerrain->modifier());
TileModifierProto newTileModifier = fb::ToTileModifierProto(targetTerrain.modifier());
IncreaseFrozen(newTileModifier, freezeWaterIntegrity);
*freezeResult.add_changed_tile_modifiers() = MakeTmc(target, newTileModifier);
} else {
@@ -29,7 +29,7 @@ protected:
const ActionCost cost;
const UnitId actorId;
const Coords target;
const Terrain* targetTerrain;
const Terrain targetTerrain;
const UnitId occupantId;
const PercentileRollOdds odds;
const int freezeWaterIntegrity;
@@ -51,7 +51,7 @@ public:
cost(cost),
actorId(actor->unit_id()),
target(target),
targetTerrain(targetTerrain),
targetTerrain(*targetTerrain),
occupantId(underwaterOccupant->unit_id()),
odds(std::move(odds)),
freezeWaterIntegrity(freezeWaterIntegrity),
@@ -71,7 +71,7 @@ public:
cost(cost),
actorId(actor->unit_id()),
target(target),
targetTerrain(targetTerrain),
targetTerrain(*targetTerrain),
occupantId(-1),
odds(std::move(odds)),
freezeWaterIntegrity(freezeWaterIntegrity),
@@ -59,7 +59,7 @@ auto HolyWaveDamageAction::InternalExecute(
resultProto.set_type(net::eagle0::shardok::common::HOLY_WAVE_DAMAGE);
const Unit *damagedUnit = currentState->units()->Get(damagedUnitId);
auto newSize = int32_t(damagedUnit->battalion().size() * (1.0 - damagePercentage));
auto newSize = static_cast<int32_t>(damagedUnit->battalion().size() * (1.0 - damagePercentage));
auto damagedUnitAfter = *damagedUnit;
damagedUnitAfter.mutable_battalion().mutate_size(newSize);
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <utility>
#include "src/main/cpp/net/eagle0/common/ByteHasher.hpp"
@@ -34,6 +35,7 @@ private:
uint8_t* buffer;
static auto CopyBuffer(const uint8_t* data, const size_t size) -> uint8_t* {
if (data == nullptr || size == 0) { return nullptr; }
auto* b = WrapperAllocator::GetDefaultAllocator()->allocate(size);
std::memcpy(b, data, size);
return b;
@@ -47,12 +49,29 @@ private:
return CopyBuffer(copiedFrom.buffer, copiedFrom.size);
}
static void ValidateBufferParams(const uint8_t* data, const size_t offset, const size_t size) {
if (data == nullptr) {
throw std::invalid_argument("Cannot construct wrapper from null bytes");
}
if (size == 0) { throw std::invalid_argument("Cannot construct wrapper from zero bytes"); }
if (offset >= size) {
throw std::invalid_argument("Cannot construct wrapper with offset outside buffer");
}
}
static auto CopyValidatedBuffer(const uint8_t* data, const size_t offset, const size_t size)
-> uint8_t* {
ValidateBufferParams(data, offset, size);
return CopyBuffer(data, size);
}
public:
explicit Wrapper(flatbuffers::FlatBufferBuilder& fbb) : size(0), offset(0) {
const uint8_t* releasedBuf = fbb.ReleaseRaw(size, offset);
if (releasedBuf == nullptr || size == 0) {
buffer = nullptr;
delete[] releasedBuf;
throw std::invalid_argument("Cannot construct wrapper from empty FlatBufferBuilder");
} else {
buffer = CopyBuffer(releasedBuf, size);
}
@@ -62,7 +81,7 @@ public:
explicit Wrapper(uint8_t* allocatedBytes, const size_t offset, const size_t size)
: size(size),
offset(offset),
buffer(CopyBuffer(allocatedBytes, size)) {}
buffer(CopyValidatedBuffer(allocatedBytes, offset, size)) {}
Wrapper() : size(0), offset(0), buffer(nullptr) {}
Wrapper(Wrapper&& movedFrom) noexcept
@@ -76,17 +95,10 @@ public:
auto operator=(const Wrapper& other) -> Wrapper& {
if (&other != this) {
if (other.size > size) {
if (buffer != nullptr) {
WrapperAllocator::GetDefaultAllocator()->deallocate(buffer, size);
}
buffer = CopyBuffer(other.buffer, other.size);
} else if (buffer != nullptr && other.buffer != nullptr) {
std::memcpy(buffer, other.buffer, other.size);
}
size = other.size;
offset = other.offset;
Wrapper copy(other);
std::swap(size, copy.size);
std::swap(offset, copy.offset);
std::swap(buffer, copy.buffer);
}
return *this;
@@ -110,10 +122,22 @@ public:
// ReSharper disable once CppNonExplicitConversionOperator
operator const FB*() const { return Get(); } // NOLINT(*-explicit-constructor)
auto Get() const -> const FB* { return flatbuffers::GetRoot<FB>(buffer + offset); }
auto Get() -> FB* { return flatbuffers::GetMutableRoot<FB>(buffer + offset); }
auto operator->() const -> const FB* { return flatbuffers::GetRoot<FB>(buffer + offset); }
auto operator->() -> FB* { return flatbuffers::GetMutableRoot<FB>(buffer + offset); }
auto Get() const -> const FB* {
if (buffer == nullptr) { return nullptr; }
return flatbuffers::GetRoot<FB>(buffer + offset);
}
auto Get() -> FB* {
if (buffer == nullptr) { return nullptr; }
return flatbuffers::GetMutableRoot<FB>(buffer + offset);
}
auto operator->() const -> const FB* {
if (const auto* root = Get(); root != nullptr) { return root; }
throw std::logic_error("Cannot dereference an empty wrapper");
}
auto operator->() -> FB* {
if (auto* root = Get(); root != nullptr) { return root; }
throw std::logic_error("Cannot dereference an empty wrapper");
}
[[nodiscard]] auto SaveTo(const std::string& path) const -> bool {
return FilesystemUtils::AtomicallySaveToPath(path, ToByteVector());
@@ -123,6 +147,7 @@ public:
}
[[nodiscard]] auto ToByteString() const -> string {
if (buffer == nullptr || size == 0) { return {}; }
string result;
result.reserve(sizeof(size_t) + size);
result.append(reinterpret_cast<const char*>(&offset), sizeof(size_t));
@@ -138,25 +163,23 @@ public:
}
static auto FromByteString(const string& str) -> Wrapper {
if (str.size() < sizeof(size_t)) {
return Wrapper(); // Return empty wrapper for invalid data
throw std::invalid_argument("Cannot construct wrapper from truncated bytes");
}
size_t offset = 0;
std::memcpy(&offset, str.data(), sizeof(size_t));
size_t size = str.size() - sizeof(size_t);
auto toReturn = Wrapper(
return Wrapper(
const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(str.data())) + sizeof(size_t),
offset,
size);
return toReturn;
}
[[nodiscard]] auto ToByteVector() const -> byte_vector { return byte_vector(ToByteString()); }
static auto FromByteVector(const byte_vector& bv) -> Wrapper {
if (bv.size() < sizeof(size_t)) {
return Wrapper(); // Return empty wrapper for invalid data
throw std::invalid_argument("Cannot construct wrapper from truncated bytes");
}
size_t offset = 0;
@@ -164,9 +187,7 @@ public:
size_t size = bv.size() - sizeof(size_t);
// Note: CopyBuffer will handle const_cast safely by making a copy
auto toReturn = Wrapper(const_cast<uint8_t*>(bv.data()) + sizeof(size_t), offset, size);
return toReturn;
return Wrapper(const_cast<uint8_t*>(bv.data()) + sizeof(size_t), offset, size);
}
};
@@ -17,7 +17,29 @@
namespace shardok::fb {
using namespace net::eagle0::shardok::storage::fb;
constexpr float kExplicitMoraleSentinel = -1.0f;
using flatbuffers::FlatBufferBuilder;
using flatbuffers::Offset;
using net::eagle0::shardok::storage::fb::AlliedPlayer;
using net::eagle0::shardok::storage::fb::CreateGameStatusDirect;
using net::eagle0::shardok::storage::fb::EndGameCondition;
using net::eagle0::shardok::storage::fb::GameStateBuilder;
using net::eagle0::shardok::storage::fb::HexMapDirection_NORTHEAST;
using net::eagle0::shardok::storage::fb::PlayerInfoBuilder;
using net::eagle0::shardok::storage::fb::Profession_NECROMANCER;
using net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT;
using net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT;
using net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT;
using net::eagle0::shardok::storage::fb::VictoryCondition;
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES;
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_LAST_ALLIANCE_STANDING;
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING;
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_MOST_TROOPS_STANDING;
using net::eagle0::shardok::storage::fb::VictoryCondition_VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS;
using net::eagle0::shardok::storage::fb::WeatherConditions_CLOUDS;
using net::eagle0::shardok::storage::fb::Wind;
namespace GameStatusNs = net::eagle0::shardok::storage::fb::GameStatus_;
using GameStatusProto = net::eagle0::shardok::common::GameStatus;
const std::vector<PlayerId> kWinningPids{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
@@ -136,7 +158,7 @@ auto SetupInitialGameState(
EndGameCondition endGameCondition;
auto gameStatusOffset = CreateGameStatusDirect(
fbb,
GameStatus_::State_SET_UP,
GameStatusNs::State_SET_UP,
nullptr,
&kWinningPids,
&endGameCondition);
@@ -171,7 +193,8 @@ auto SetupInitialGameState(
net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
if (modifiedUnit.can_archery()) {
modifiedUnit.mutate_volleys_remaining(
int32_t(modifiedUnit.battalion().armament() *
static_cast<int32_t>(
modifiedUnit.battalion().armament() *
settings.Backing().armament_to_volleys()) +
settings.GetBattalionType(modifiedUnit.battalion().type())->bonusVolleys);
}
@@ -179,8 +202,11 @@ auto SetupInitialGameState(
RequiredMonthlyFood(modifiedUnit, settings) *
foodPercentByPid[modifiedUnit.player_id()]);
auto baseMorale = BaseMorale(modifiedUnit);
auto startingMorale = modifiedUnit.battalion().base_morale() == kExplicitMoraleSentinel
? modifiedUnit.battalion().morale()
: baseMorale;
modifiedUnit.mutable_battalion().mutate_base_morale(baseMorale);
modifiedUnit.mutable_battalion().mutate_morale(baseMorale);
modifiedUnit.mutable_battalion().mutate_morale(startingMorale);
if (modifiedUnit.status() !=
net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT) {
@@ -293,9 +319,9 @@ auto ToPlayerInfoProto(const PlayerInfo* fbPI) -> PlayerInfoProto {
auto ToWeatherProto(const Weather* fbW) -> net::eagle0::shardok::common::Weather {
net::eagle0::shardok::common::Weather protoWeather;
protoWeather.set_conditions(
(net::eagle0::shardok::common::Weather_Conditions)fbW->conditions());
static_cast<net::eagle0::shardok::common::Weather_Conditions>(fbW->conditions()));
protoWeather.mutable_wind()->set_direction(
(net::eagle0::shardok::common::HexMapDirection)fbW->wind().direction());
static_cast<net::eagle0::shardok::common::HexMapDirection>(fbW->wind().direction()));
protoWeather.mutable_wind()->set_speed_in_mph(fbW->wind().speed_in_mph());
return protoWeather;
@@ -13,20 +13,28 @@
namespace shardok::fb {
using TerrainProto = net::eagle0::shardok::common::Terrain;
using net::eagle0::shardok::storage::fb::Coords;
using net::eagle0::shardok::storage::fb::CreateStartingPositionList;
using net::eagle0::shardok::storage::fb::CreateStartingPositionListDirect;
using net::eagle0::shardok::storage::fb::HexMapBuilder;
using net::eagle0::shardok::storage::fb::MonthlyWeather;
using net::eagle0::shardok::storage::fb::SingleModifier;
using net::eagle0::shardok::storage::fb::StartingPositionList;
using net::eagle0::shardok::storage::fb::Terrain;
using net::eagle0::shardok::storage::fb::TileModifier;
namespace TerrainNs = net::eagle0::shardok::storage::fb::Terrain_;
using namespace net::eagle0::shardok::storage::fb;
auto ConvertType(const net::eagle0::shardok::common::Terrain_Type protoType) -> Terrain_::Type {
using namespace Terrain_;
auto ConvertType(const net::eagle0::shardok::common::Terrain_Type protoType) -> TerrainNs::Type {
switch (protoType) {
case net::eagle0::shardok::common::Terrain_Type_CITY: return Type_CITY;
case net::eagle0::shardok::common::Terrain_Type_FOREST: return Type_FOREST;
case net::eagle0::shardok::common::Terrain_Type_HILL: return Type_HILL;
case net::eagle0::shardok::common::Terrain_Type_MOUNTAIN: return Type_MOUNTAIN;
case net::eagle0::shardok::common::Terrain_Type_PLAINS: return Type_PLAINS;
case net::eagle0::shardok::common::Terrain_Type_RIVER: return Type_RIVER;
case net::eagle0::shardok::common::Terrain_Type_STILL_WATER: return Type_STILL_WATER;
case net::eagle0::shardok::common::Terrain_Type_SWAMP: return Type_SWAMP;
case net::eagle0::shardok::common::Terrain_Type_CITY: return TerrainNs::Type_CITY;
case net::eagle0::shardok::common::Terrain_Type_FOREST: return TerrainNs::Type_FOREST;
case net::eagle0::shardok::common::Terrain_Type_HILL: return TerrainNs::Type_HILL;
case net::eagle0::shardok::common::Terrain_Type_MOUNTAIN: return TerrainNs::Type_MOUNTAIN;
case net::eagle0::shardok::common::Terrain_Type_PLAINS: return TerrainNs::Type_PLAINS;
case net::eagle0::shardok::common::Terrain_Type_RIVER: return TerrainNs::Type_RIVER;
case net::eagle0::shardok::common::Terrain_Type_STILL_WATER:
return TerrainNs::Type_STILL_WATER;
case net::eagle0::shardok::common::Terrain_Type_SWAMP: return TerrainNs::Type_SWAMP;
case net::eagle0::shardok::common::Terrain_Type_UNKNOWN:
case net::eagle0::shardok::common::Terrain_Type_NONE:
@@ -208,7 +216,7 @@ auto ConvertHexMapProto(
}
auto CopyStartingPositionsList(FlatBufferBuilder& fbb, const StartingPositionList* spl) {
std::vector<Coords> coordsVec = {};
std::vector<Coords> coordsVec{};
if (spl && spl->positions()) {
coordsVec.reserve(spl->positions()->size());
@@ -232,7 +240,7 @@ auto CopyHexMap(const HexMap* hexMap) -> HexMapW {
for (const auto* w : *hexMap->monthly_weather()) weatherVec.push_back(*w);
const auto weatherOffset = fbb.CreateVectorOfStructs(weatherVec);
std::vector<Offset<StartingPositionList>> asplVec = {};
std::vector<Offset<StartingPositionList>> asplVec{};
asplVec.reserve(hexMap->attacker_starting_positions()->size());
for (const auto* spl : *hexMap->attacker_starting_positions()) {
asplVec.push_back(CopyStartingPositionsList(fbb, spl));
@@ -1,4 +1,4 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
load("//tools:copts.bzl", "COPTS")
cc_library(
@@ -28,6 +28,14 @@ cc_library(
],
)
cc_binary(
name = "coords_set_benchmark",
srcs = ["CoordsSetBenchmark.cpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [":coords_set"],
)
cc_library(
name = "hex_map_direction",
hdrs = ["HexMapDirection.hpp"],
@@ -60,7 +60,7 @@ namespace std {
template<>
struct hash<net::eagle0::shardok::common::Coords> {
[[nodiscard]] auto operator()(const net::eagle0::shardok::common::Coords &c) const -> size_t {
return size_t(c.row() << 16 | c.column());
return static_cast<size_t>(c.row() << 16 | c.column());
}
};
} // namespace std
@@ -6,7 +6,10 @@
#define EAGLE0_SHARDOK_LIBRARY_MAP_COORDS_SET_HPP
#include <bit>
#include <functional>
#include <cassert>
#include <cstdint>
#include <iterator>
#include <limits>
#include <utility>
#include <vector>
@@ -17,7 +20,11 @@
namespace shardok {
#ifndef NDEBUG
constexpr bool kCoordsSetIndexChecks = true;
#else
constexpr bool kCoordsSetIndexChecks = false;
#endif
constexpr size_t STANDARD_SIZE = size_t{12} * 14;
@@ -28,13 +35,28 @@ class CoordsSet {
private:
BackingBitset store;
int mapWidth;
int mapHeight;
uint8_t columnCount;
uint8_t rowCount;
uint32_t indexCount;
[[maybe_unused]] uint16_t indexCount;
void AssertCoordsInBounds([[maybe_unused]] const int row, [[maybe_unused]] const int column)
const {
if constexpr (kCoordsSetIndexChecks) {
assert(row >= 0);
assert(column >= 0);
assert(row < rowCount);
assert(column < columnCount);
}
}
void AssertIndexInBounds([[maybe_unused]] const uint32_t idx) const {
if constexpr (kCoordsSetIndexChecks) { assert(idx < indexCount); }
}
[[nodiscard]] auto Index(const int row, const int column) const -> int {
return (row * mapWidth) + column;
AssertCoordsInBounds(row, column);
return (row * columnCount) + column;
}
[[nodiscard]] auto Index(const Coords& c) const -> int { return Index(c.row(), c.column()); }
@@ -49,18 +71,25 @@ private:
};
void Remove(uint32_t idx) {
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
AssertIndexInBounds(idx);
store.Unset(idx);
}
[[nodiscard]] auto Contains(uint32_t idx) const -> bool {
if (kCoordsSetIndexChecks) { assert(idx < indexCount); }
AssertIndexInBounds(idx);
return store.IsSet(idx);
}
public:
CoordsSet(int w, int h) : mapWidth(w), mapHeight(h), indexCount(w * h) {
if (kCoordsSetIndexChecks) {
assert(static_cast<size_t>(w) * static_cast<size_t>(h) < STANDARD_SIZE);
CoordsSet(const int w, const int h)
: columnCount(static_cast<uint8_t>(w)),
rowCount(static_cast<uint8_t>(h)),
indexCount(static_cast<uint16_t>(w * h)) {
if constexpr (kCoordsSetIndexChecks) {
assert(w >= 0);
assert(h >= 0);
assert(w <= std::numeric_limits<uint8_t>::max());
assert(h <= std::numeric_limits<uint8_t>::max());
assert(static_cast<size_t>(w) * static_cast<size_t>(h) <= STANDARD_SIZE);
}
clear();
}
@@ -74,15 +103,20 @@ public:
auto operator=(const CoordsSet& rhs) -> CoordsSet& = default;
auto operator=(CoordsSet&& rhs) -> CoordsSet& = default;
auto operator==(const CoordsSet& rhs) const -> bool {
if (mapWidth != rhs.mapWidth) return false;
if (mapHeight != rhs.mapHeight) return false;
[[nodiscard]] auto operator==(const CoordsSet& rhs) const -> bool {
if (columnCount != rhs.columnCount) return false;
if (rowCount != rhs.rowCount) return false;
return store == rhs.store;
}
void Add(const Coords& c) { AddIndex(Index(c)); }
void Add(const int row, const int column) { AddIndex(Index(row, column)); }
void AddIndex(const uint32_t idx) { store.Set(idx); }
void Add(const Coords& c) { AddMapIndexUnchecked(Index(c)); }
void Add(const int row, const int column) { AddMapIndexUnchecked(Index(row, column)); }
// For callers that already have row * column_count + column.
void AddMapIndexUnchecked(const uint32_t idx) {
AssertIndexInBounds(idx);
store.Set(idx);
}
// Always succeeds
void Remove(const Coords& c) { Remove(Index(c)); }
@@ -101,13 +135,15 @@ public:
[[nodiscard]] auto empty() const -> bool { return store.empty(); }
[[nodiscard]] auto ColumnCount() const -> int { return columnCount; }
// Should only be used for debugging.
[[nodiscard]] auto as_vector() const -> std::vector<Coords> {
return std::vector<Coords>{this->begin(), this->end()};
}
auto operator+=(const CoordsSet& rhs) -> CoordsSet& {
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
throw MismatchException("mismatched sizes");
}
@@ -115,14 +151,14 @@ public:
return *this;
}
auto operator+(const CoordsSet& rhs) const -> CoordsSet {
[[nodiscard]] auto operator+(const CoordsSet& rhs) const -> CoordsSet {
CoordsSet cs = *this;
cs += rhs;
return cs;
}
auto operator-=(const CoordsSet& rhs) -> CoordsSet& {
if (mapWidth != rhs.mapWidth || mapHeight != rhs.mapHeight) {
if (columnCount != rhs.columnCount || rowCount != rhs.rowCount) {
throw MismatchException("mismatched sizes");
}
@@ -130,15 +166,25 @@ public:
return *this;
}
auto operator-(const CoordsSet& rhs) const -> CoordsSet {
[[nodiscard]] auto operator-(const CoordsSet& rhs) const -> CoordsSet {
CoordsSet cs = *this;
cs -= rhs;
return cs;
}
void Filter(const std::function<bool(int, int)>& f) {
for (uint32_t i = 0; i < indexCount; ++i) {
if (Contains(i) && !f(i / mapWidth, i % mapWidth)) { Remove(i); }
template<class Func>
void Filter(Func&& f) {
if (size() * 4 > indexCount) {
for (uint32_t i = 0; i < indexCount; ++i) {
if (Contains(i) && !f(i / columnCount, i % columnCount)) { Remove(i); }
}
return;
}
for (auto iter = store.begin(); iter != store.end();) {
const uint32_t i = static_cast<uint32_t>(*iter);
++iter;
if (!f(i / columnCount, i % columnCount)) { Remove(i); }
}
}
@@ -146,9 +192,9 @@ public:
public:
using iterator_category = std::forward_iterator_tag;
using value_type = Coords;
using difference_type = int;
using pointer = Coords*;
using reference = Coords&;
using difference_type = std::ptrdiff_t;
using pointer = void;
using reference = Coords;
private:
BackingBitset::Iter iterator;
@@ -158,15 +204,28 @@ public:
public:
Iter(const CoordsSet& cs, BackingBitset::Iter iter) : iterator(iter), cs(cs) {}
auto operator==(const Iter& rhs) const -> bool { return iterator == rhs.iterator; }
auto operator!=(const Iter& rhs) const -> bool { return iterator != rhs.iterator; }
void operator++() { ++iterator; }
[[nodiscard]] auto operator==(const Iter& rhs) const -> bool {
return iterator == rhs.iterator;
}
[[nodiscard]] auto operator!=(const Iter& rhs) const -> bool {
return iterator != rhs.iterator;
}
auto operator++() -> Iter& {
++iterator;
return *this;
}
auto operator*() const -> Coords {
auto operator++(int) -> Iter {
Iter previous = *this;
++(*this);
return previous;
}
[[nodiscard]] auto operator*() const -> Coords {
const size_t index = *iterator;
const int row = index / cs.mapWidth;
const int column = index % cs.mapWidth;
const int row = static_cast<int>(index / static_cast<size_t>(cs.columnCount));
const int column = static_cast<int>(index % static_cast<size_t>(cs.columnCount));
return Coords(row, column);
}
};
@@ -185,25 +244,21 @@ public:
[[nodiscard]] auto end() const -> BackingBitset::Iter { return cs.store.end(); }
};
[[nodiscard]] auto indexIterator() const -> IndexIterator { return IndexIterator(*this); }
[[nodiscard]] auto ToString() const -> std::string {
return std::string(reinterpret_cast<char const*>(&store), STANDARD_SIZE);
}
};
} // namespace shardok
namespace common {
static inline auto Contains(const shardok::CoordsSet& container, const shardok::Coords& elt)
-> bool {
[[nodiscard]] static inline auto Contains(
const shardok::CoordsSet& container,
const shardok::Coords& elt) -> bool {
return container.Contains(elt);
}
template<class Func>
void FilterInPlace(shardok::CoordsSet& container, Func fn) {
for (const shardok::Coords& c : container) {
if (!fn(c)) container.Remove(c);
}
container.Filter(
[&fn](const int row, const int column) { return fn(shardok::Coords(row, column)); });
}
} // namespace common
@@ -0,0 +1,204 @@
#include <chrono>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string_view>
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
namespace shardok {
namespace {
constexpr int kColumnCount = 12;
constexpr int kRowCount = 14;
constexpr int kIndexCount = kColumnCount * kRowCount;
volatile uint64_t benchmarkSink = 0;
template<class T>
void DoNotOptimize(const T& value) {
#if defined(__GNUC__) || defined(__clang__)
asm volatile("" : : "g"(&value) : "memory");
#else
benchmarkSink ^= reinterpret_cast<uintptr_t>(&value);
#endif
}
struct BenchmarkResult {
std::string_view name;
int iterations;
std::chrono::nanoseconds elapsed;
uint64_t checksum;
};
void Consume(uint64_t checksum) { benchmarkSink = checksum; }
[[nodiscard]] auto MakePatternedCoordsSet(const int step, const int offset = 0) -> CoordsSet {
CoordsSet coords(kColumnCount, kRowCount);
for (int index = offset; index < kIndexCount; index += step) {
coords.AddMapIndexUnchecked(static_cast<uint32_t>(index));
}
return coords;
}
template<class Func>
[[nodiscard]] auto RunBenchmark(std::string_view name, const int iterations, Func func)
-> BenchmarkResult {
const auto start = std::chrono::steady_clock::now();
const uint64_t checksum = func(iterations);
const auto end = std::chrono::steady_clock::now();
Consume(checksum);
return BenchmarkResult{
name,
iterations,
std::chrono::duration_cast<std::chrono::nanoseconds>(end - start),
checksum};
}
void PrintResult(const BenchmarkResult& result) {
const double nsPerIteration =
static_cast<double>(result.elapsed.count()) / static_cast<double>(result.iterations);
std::cout << std::left << std::setw(28) << result.name << " iterations=" << std::right
<< std::setw(10) << result.iterations << " ns/op=" << std::fixed
<< std::setprecision(2) << std::setw(10) << nsPerIteration
<< " checksum=" << result.checksum << '\n';
}
[[nodiscard]] auto BenchmarkCopies(const int iterations) -> uint64_t {
const CoordsSet source = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet copy = source;
DoNotOptimize(copy);
checksum += copy.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkUnions(const int iterations) -> uint64_t {
const CoordsSet left = MakePatternedCoordsSet(3);
const CoordsSet right = MakePatternedCoordsSet(5, 1);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet combined = left;
combined += right;
DoNotOptimize(combined);
checksum += combined.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkSubtractions(const int iterations) -> uint64_t {
const CoordsSet full = MakePatternedCoordsSet(1);
const CoordsSet removed = MakePatternedCoordsSet(4);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet remaining = full;
remaining -= removed;
DoNotOptimize(remaining);
checksum += remaining.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkContains(const int iterations) -> uint64_t {
const CoordsSet coords = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
const int index = i % kIndexCount;
checksum += coords.Contains(index / kColumnCount, index % kColumnCount) ? 1 : 0;
}
return checksum;
}
[[nodiscard]] auto BenchmarkCoordinateIteration(const int iterations) -> uint64_t {
const CoordsSet coords = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
for (const auto& coord : coords) {
checksum += static_cast<uint64_t>(coord.row() + coord.column());
}
}
return checksum;
}
[[nodiscard]] auto BenchmarkIndexIteration(const int iterations) -> uint64_t {
const CoordsSet coords = MakePatternedCoordsSet(3);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
for (const auto index : coords.indexIterator()) { checksum += index; }
}
return checksum;
}
[[nodiscard]] auto BenchmarkDenseFilter(const int iterations) -> uint64_t {
const CoordsSet source = MakePatternedCoordsSet(1);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet filtered = source;
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
DoNotOptimize(filtered);
checksum += filtered.size();
}
return checksum;
}
[[nodiscard]] auto BenchmarkSparseFilter(const int iterations) -> uint64_t {
const CoordsSet source = MakePatternedCoordsSet(7);
uint64_t checksum = 0;
for (int i = 0; i < iterations; ++i) {
CoordsSet filtered = source;
filtered.Filter([](const int row, const int column) { return (row + column) % 3 == 0; });
DoNotOptimize(filtered);
checksum += filtered.size();
}
return checksum;
}
} // namespace
} // namespace shardok
auto main() -> int {
using shardok::BenchmarkContains;
using shardok::BenchmarkCoordinateIteration;
using shardok::BenchmarkCopies;
using shardok::BenchmarkDenseFilter;
using shardok::BenchmarkIndexIteration;
using shardok::BenchmarkSparseFilter;
using shardok::BenchmarkSubtractions;
using shardok::BenchmarkUnions;
using shardok::CoordsSet;
using shardok::PrintResult;
using shardok::RunBenchmark;
std::cout << "CoordsSet benchmark\n";
std::cout << "sizeof(CoordsSet)=" << sizeof(CoordsSet)
<< " alignof(CoordsSet)=" << alignof(CoordsSet) << '\n';
PrintResult(RunBenchmark("copy", 5'000'000, BenchmarkCopies));
PrintResult(RunBenchmark("union", 3'000'000, BenchmarkUnions));
PrintResult(RunBenchmark("subtraction", 3'000'000, BenchmarkSubtractions));
PrintResult(RunBenchmark("contains", 20'000'000, BenchmarkContains));
PrintResult(RunBenchmark("coord iteration", 1'000'000, BenchmarkCoordinateIteration));
PrintResult(RunBenchmark("index iteration", 1'000'000, BenchmarkIndexIteration));
PrintResult(RunBenchmark("dense filter", 500'000, BenchmarkDenseFilter));
PrintResult(RunBenchmark("sparse filter", 500'000, BenchmarkSparseFilter));
return 0;
}
@@ -25,7 +25,7 @@ using TileModifierProto = net::eagle0::shardok::common::TileModifier;
SingleModifier MakeSingleModifier(const std::optional<double>& integ = std::nullopt);
bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
[[nodiscard]] bool operator==(const SingleModifier& lhs, const SingleModifier& rhs);
TileModifierProto MakeTileModifier(bool onFire = false);
@@ -12,10 +12,10 @@
namespace shardok {
using TileModifierWithCoords = net::eagle0::shardok::common::TileModifierWithCoords;
auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
[[nodiscard]] auto MakeTmc(const Coords &coordinates, const TileModifierProto &modifier)
-> TileModifierWithCoords;
auto MakeTmc(
[[nodiscard]] auto MakeTmc(
const Coords &coordinates,
const net::eagle0::shardok::storage::fb::TileModifier &modifier) -> TileModifierWithCoords;
@@ -92,7 +92,8 @@ public:
net::eagle0::shardok::storage::fb::WeatherConditions conditions) const -> int;
};
[[nodiscard]] auto GetGetter() const -> Getter { return Getter(*this); }
[[nodiscard]] auto GetGetter() const & -> Getter { return Getter(*this); }
[[nodiscard]] auto GetGetter() const && -> Getter = delete;
class Setter {
private:
@@ -118,7 +119,8 @@ public:
void SetFirePropensityFromWeather(Weather::Conditions wc, int propensity);
};
auto GetSetter() -> Setter { return Setter(*this); }
[[nodiscard]] auto GetSetter() & -> Setter { return Setter(*this); }
[[nodiscard]] auto GetSetter() && -> Setter = delete;
virtual ~GameSettings() = default;
};
@@ -13,7 +13,7 @@ namespace shardok {
class SettingsLoader {
public:
static auto LoadSettings() -> std::shared_ptr<GameSettings>;
[[nodiscard]] static auto LoadSettings() -> std::shared_ptr<GameSettings>;
};
} // namespace shardok
@@ -5,7 +5,10 @@
#ifndef EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
#define EAGLE0_SHARDOK_LIBRARY_UTIL_ACTION_RESULT_FLATBUFFER_HELPERS_HPP
#include <cstring>
#include <stdexcept>
#include <string>
#include <type_traits>
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
@@ -16,13 +19,30 @@ using ActionResult = net::eagle0::shardok::storage::ActionResult;
using ResolvedUnit = net::eagle0::shardok::storage::ResolvedUnit;
using Unit = net::eagle0::shardok::storage::fb::Unit;
static_assert(std::is_trivially_copyable_v<Unit>);
[[nodiscard]] static inline auto UnitToBytes(const Unit& unit) -> std::string {
std::string bytes(sizeof(Unit), '\0');
std::memcpy(bytes.data(), &unit, sizeof(Unit));
return bytes;
}
[[nodiscard]] static inline auto UnitFromBytes(const std::string& unitBytes) -> Unit {
if (unitBytes.size() != sizeof(Unit)) {
throw std::invalid_argument("Unexpected unit byte length");
}
Unit unit{};
std::memcpy(&unit, unitBytes.data(), sizeof(Unit));
return unit;
}
static inline void AddChangedUnit(ActionResult& result, const Unit& unit) {
result.add_changed_units_fb(&unit, sizeof(Unit));
}
static inline void AddUnitToResolved(ResolvedUnit& resolvedUnit, const Unit& unit) {
*resolvedUnit.mutable_unit_bytes() =
std::string(reinterpret_cast<const char*>(&unit), sizeof(Unit));
*resolvedUnit.mutable_unit_bytes() = UnitToBytes(unit);
}
} // namespace shardok
@@ -6,6 +6,7 @@ cc_library(
hdrs = ["ActionResultFlatbufferHelpers.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
@@ -306,10 +306,9 @@ auto MakeActionResultFb(const ActionType type, const MeleeResultFb &results, con
ar.mutable_actor()->set_value(results.attackerAfter.unit_id());
ar.mutable_target_unit()->set_value(results.defenderAfter.unit_id());
AddChangedUnit(ar, results.attackerAfter);
auto *attCU = reinterpret_cast<Unit *>(
ar.mutable_changed_units_fb(ar.changed_units_fb_size() - 1)->data());
if (resetZoc) attCU->mutate_has_moved_in_zoc(false);
Unit attackerAfter = results.attackerAfter;
if (resetZoc) attackerAfter.mutate_has_moved_in_zoc(false);
AddChangedUnit(ar, attackerAfter);
AddChangedUnit(ar, results.defenderAfter);
@@ -104,7 +104,7 @@ const std::vector<CoordsSet> HexMapUtils::standardCoords =
auto HexMapUtils::ComputeAllStandardAdjacentCoords() -> std::vector<CoordsSet> {
std::vector<CoordsSet> arr;
arr.reserve(size_t(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
arr.reserve(static_cast<size_t>(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));
for (int index = 0; index < STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS; ++index) {
arr.push_back(ComputeAdjacentCoords(
@@ -127,25 +127,25 @@ auto HexMapUtils::ComputeAdjacentCoords(
// Row above
if (origin.row() > 0) {
cs.AddIndex(originIndex - columnCount);
cs.AddMapIndexUnchecked(originIndex - columnCount);
if (origin.row() % 2 == 0) {
if (origin.column() > 0) { cs.AddIndex(originIndex - columnCount - 1); }
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - columnCount - 1); }
} else if (origin.column() < columnCount - 1) {
cs.AddIndex(originIndex - columnCount + 1);
cs.AddMapIndexUnchecked(originIndex - columnCount + 1);
}
}
// This row
if (origin.column() > 0) { cs.AddIndex(originIndex - 1); }
if (origin.column() < columnCount - 1) { cs.AddIndex(originIndex + 1); }
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex - 1); }
if (origin.column() < columnCount - 1) { cs.AddMapIndexUnchecked(originIndex + 1); }
// Row below
if (origin.row() < rowCount - 1) {
cs.AddIndex(originIndex + columnCount);
cs.AddMapIndexUnchecked(originIndex + columnCount);
if (origin.row() % 2 == 0) {
if (origin.column() > 0) { cs.AddIndex(originIndex + columnCount - 1); }
if (origin.column() > 0) { cs.AddMapIndexUnchecked(originIndex + columnCount - 1); }
} else if (origin.column() < columnCount - 1) {
cs.AddIndex(originIndex + columnCount + 1);
cs.AddMapIndexUnchecked(originIndex + columnCount + 1);
}
}
@@ -36,7 +36,7 @@ struct alignas(16) AdjacentTile {
AdjacentTile(const Coords co, const HexMapDirection dir) : directionTo(dir), coords(co) {}
auto operator==(const AdjacentTile &rhs) const -> bool {
[[nodiscard]] auto operator==(const AdjacentTile &rhs) const -> bool {
return coords == rhs.coords && directionTo == rhs.directionTo;
}
};
@@ -73,21 +73,21 @@ public:
[[nodiscard]] auto AllCastleCoords(const HexMap *hexMap) -> CoordsSet;
auto HasForestAccess(
[[nodiscard]] auto HasForestAccess(
const Coords &coords,
const Units *units,
const HexMap *hexMap,
const std::vector<PlayerId> &allyPids,
PlayerId player) -> bool;
auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
[[nodiscard]] auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
[[nodiscard]] auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
// Overload for const HexMap - uses const_cast internally. Safe when the underlying buffer is
// mutable.
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
[[nodiscard]] auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
[[nodiscard]] auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
[[nodiscard]] auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;
[[nodiscard]] auto GetTilesAcrossWater(const HexMap *map, const Coords &coords) -> CoordsSet;
@@ -100,7 +100,8 @@ auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool
[[nodiscard]] auto TwoAwayTilesUnblockedByMountains(const HexMap *map, const Coords &coords)
-> CoordsSet;
static inline auto Occupant(const Units *units, const Coords &coords) -> const Unit * {
[[nodiscard]] static inline auto Occupant(const Units *units, const Coords &coords)
-> const Unit * {
for (const auto &unit : *units) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->location() == coords) {
@@ -111,7 +112,8 @@ static inline auto Occupant(const Units *units, const Coords &coords) -> const U
return nullptr;
}
static inline auto Occupant(const Units &units, const Coords &coords) -> const Unit * {
[[nodiscard]] static inline auto Occupant(const Units &units, const Coords &coords)
-> const Unit * {
for (const auto &unit : units) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->location() == coords) {
@@ -122,7 +124,7 @@ static inline auto Occupant(const Units &units, const Coords &coords) -> const U
return nullptr;
}
static inline auto Occupants(
[[nodiscard]] static inline auto Occupants(
const std::vector<const Unit *> &units,
const int rowCount,
const int columnCount) -> std::vector<const Unit *> {
@@ -138,7 +140,7 @@ static inline auto Occupants(
return positions;
}
static inline auto Occupants(
[[nodiscard]] static inline auto Occupants(
const flatbuffers::Vector<const Unit *> &units,
const int rowCount,
const int columnCount) -> std::vector<const Unit *> {
@@ -154,8 +156,9 @@ static inline auto Occupants(
return positions;
}
static inline auto Occupant(const std::vector<const Unit *> &units, const Coords &coords)
-> const Unit * {
[[nodiscard]] static inline auto Occupant(
const std::vector<const Unit *> &units,
const Coords &coords) -> const Unit * {
for (const auto &unit : units) {
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
unit->location() == coords) {
@@ -167,7 +170,7 @@ static inline auto Occupant(const std::vector<const Unit *> &units, const Coords
}
template<class UnitContainer>
auto Occupant(const UnitContainer &units, const Coords &coords)
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords)
-> std::optional<typename UnitContainer::value_type> {
for (const auto &unit : units) {
if (unit.location() == coords) { return unit; }
@@ -177,7 +180,8 @@ auto Occupant(const UnitContainer &units, const Coords &coords)
}
template<class U>
auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::optional<U> {
[[nodiscard]] auto Occupant(const std::map<UnitId, U> &units, const Coords &coords)
-> std::optional<U> {
for (const auto &[unitId, unit] : units) {
if (unit.location() == coords) { return unit; }
}
@@ -186,7 +190,7 @@ auto Occupant(const std::map<UnitId, U> &units, const Coords &coords) -> std::op
}
template<class U>
auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
[[nodiscard]] auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
-> std::optional<U> {
for (const auto &[unitId, unit] : units) {
if (unit.location() == coords) { return unit; }
@@ -196,7 +200,7 @@ auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coo
}
template<class U, class UnitContainer>
auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
for (const auto &unit : units) {
if (unit.location() == coords) { return unit; }
}
@@ -204,7 +208,7 @@ auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional
return std::nullopt;
}
static inline auto FriendlyOccupant(
[[nodiscard]] static inline auto FriendlyOccupant(
const PlayerId pid,
const Units *units,
const std::vector<PlayerId> &allyPids,
@@ -221,7 +225,7 @@ static inline auto FriendlyOccupant(
}
template<class U, class UnitContainer>
auto FriendlyOccupant(
[[nodiscard]] auto FriendlyOccupant(
PlayerId pid,
const UnitContainer &units,
const std::vector<PlayerId> &allyPids,
@@ -237,14 +241,14 @@ auto FriendlyOccupant(
return {};
}
auto KnownEnemyOccupant(
[[nodiscard]] auto KnownEnemyOccupant(
PlayerId playerId,
const Units *units,
const std::vector<PlayerId> &allyPids,
const Coords &coords) -> const Unit *;
template<class UnitContainer>
auto KnownEnemyOccupant(
[[nodiscard]] auto KnownEnemyOccupant(
PlayerId playerId,
const UnitContainer &units,
const std::vector<PlayerId> &allyPids,
@@ -277,31 +281,35 @@ auto KnownEnemyOccupant(
return nullptr;
}
auto KnownAdjacentEnemies(
[[nodiscard]] auto KnownAdjacentEnemies(
PlayerId playerId,
const Units *units,
const HexMap *map,
const std::vector<PlayerId> &allyPids,
const Coords &coords) -> std::vector<const Unit *>;
auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
[[nodiscard]] auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
-> HexMapDirectionsTo;
static inline auto DirectionsTo(const HexMap *map, const Coords &origin, const Coords &destination)
-> HexMapDirectionsTo {
[[nodiscard]] static inline auto DirectionsTo(
const HexMap *map,
const Coords &origin,
const Coords &destination) -> HexMapDirectionsTo {
return DirectionsTo(map->column_count(), origin, destination);
}
auto MaybeTileInDirection(const HexMap *map, const Coords &inCoords, HexMapDirection orientation)
-> std::optional<Coords>;
[[nodiscard]] auto MaybeTileInDirection(
const HexMap *map,
const Coords &inCoords,
HexMapDirection orientation) -> std::optional<Coords>;
auto MaybeOccupantInDirection(
[[nodiscard]] auto MaybeOccupantInDirection(
const HexMap *map,
const Units *units,
const Coords &coords,
HexMapDirection direction) -> const Unit *;
auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
[[nodiscard]] auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;
void MutatingSetTileModifier(
HexMapT *hexMap,
@@ -318,9 +326,10 @@ void MutatingSetTileModifier(
const CoordsProto &coords,
const TileModifierProto &TileModifierProto);
auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
[[nodiscard]] auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;
auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units) -> bool;
[[nodiscard]] auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units)
-> bool;
} // namespace shardok
@@ -15,10 +15,10 @@ namespace shardok {
using PlayerInfoFb = net::eagle0::shardok::storage::fb::PlayerInfo;
using GameState = net::eagle0::shardok::storage::fb::GameState;
auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
auto NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true)
-> PlayerId;
auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
[[nodiscard]] auto PlayerIsDefender(const GameState *gameState, int playerId) -> bool;
[[nodiscard]] auto
NextPlayerId(const GameState *gameState, PlayerId pid, bool allowUncontrolled = true) -> PlayerId;
[[nodiscard]] auto AlliedPids(const GameState *gameState, int playerId) -> std::vector<PlayerId>;
} // namespace shardok
@@ -8,6 +8,7 @@
#include <ranges>
#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/view_filters/GameStateFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateViewDiffer.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/action_type.pb.h"
@@ -87,7 +88,7 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
if (result.has_actor()) {
const auto actorId = result.actor().value();
for (const auto &unitData : result.changed_units_fb()) {
const auto &unit = *reinterpret_cast<const Unit *>(unitData.data());
const auto unit = UnitFromBytes(unitData);
if (unit.unit_id() == actorId) {
opponentKnowledgeOfActor = OpponentKnowledge(unit, askingPlayer);
hiddenActor = unit.hidden();
@@ -13,6 +13,7 @@ cc_library(
deps = [
":unit_filter",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_view_differ",
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_cc_proto",
@@ -45,6 +45,7 @@ auto GameStateFilteredForPlayerWithAllies(
int reserveUnitCount = 0;
int normalUnitCount = 0;
int capturedUnitCount = 0;
for (const auto& unit : *gameState->units()) {
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
@@ -56,8 +57,11 @@ auto GameStateFilteredForPlayerWithAllies(
++normalUnitCount;
break;
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT:
++capturedUnitCount;
break;
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
@@ -73,6 +77,7 @@ auto GameStateFilteredForPlayerWithAllies(
gsv.mutable_reserve_units()->Reserve(reserveUnitCount);
gsv.mutable_units()->Reserve(normalUnitCount);
gsv.mutable_captured_units()->Reserve(capturedUnitCount);
for (const auto& unit : *gameState->units()) {
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
@@ -85,8 +90,12 @@ auto GameStateFilteredForPlayerWithAllies(
*gsv.add_units() = UnitFilteredForPlayer(settings, unit, askingPlayer, alliedPids);
break;
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT:
*gsv.add_captured_units() =
UnitFilteredForPlayer(settings, unit, askingPlayer, alliedPids);
break;
case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
@@ -216,7 +216,8 @@ auto GameStateGuesser::GuessedState(
std::vector<UnitId> foundUnits{};
std::vector<Unit> unitsVec{};
const int knownUnitCount = gameStateView.units_size() + gameStateView.reserve_units_size();
const int knownUnitCount = gameStateView.units_size() + gameStateView.reserve_units_size() +
gameStateView.captured_units_size();
foundUnits.reserve(knownUnitCount);
unitsVec.reserve(knownUnitCount + 6);
for (const auto &uv : gameStateView.units()) {
@@ -257,6 +258,19 @@ auto GameStateGuesser::GuessedState(
foundUnits.push_back(uv.unit_id());
}
for (const auto &uv : gameStateView.captured_units()) {
unitsVec.push_back(GuessedUnit(
settings,
uv,
pid,
FromCoordsProto(uv.location()),
net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT,
playerAverages));
if (uv.player_id() == 1) foundDefender = true;
if (uv.unit_id() > maxId) maxId = uv.unit_id();
foundUnits.push_back(uv.unit_id());
}
for (int i = 0; i < maxId; ++i) {
auto entry = std::ranges::find(foundUnits, i);
if (entry == std::end(foundUnits)) {
@@ -283,6 +297,8 @@ auto GameStateGuesser::GuessedState(
newUnit.mutate_unit_id(maxId + 1);
newUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
newUnit.mutate_has_attached_hero(false);
newUnit.mutate_commanding_unit_id(-1);
for (const auto &startPosProto :
gameStateView.hex_map().defender_starting_positions().positions()) {
@@ -62,6 +62,12 @@ auto MakeGameStateViewDiff(const GameStateView &before, const GameStateView &aft
diff.mutable_changed_reserve_units(),
diff.mutable_removed_reserve_unit_ids(),
changed);
PopulateRepeatedUnitDiffs(
before.captured_units(),
after.captured_units(),
diff.mutable_changed_captured_units(),
diff.mutable_removed_captured_unit_ids(),
changed);
// tile modifiers
const int afterRowCount = after.hex_map().row_count();
@@ -70,7 +76,8 @@ auto MakeGameStateViewDiff(const GameStateView &before, const GameStateView &aft
for (int tileIndex = 0; tileIndex < afterCount; ++tileIndex) {
Coords coords =
Coords(int8_t(tileIndex / afterColumnCount), int8_t(tileIndex % afterColumnCount));
Coords(static_cast<int8_t>(tileIndex / afterColumnCount),
static_cast<int8_t>(tileIndex % afterColumnCount));
const auto afterModifier = after.hex_map().terrain(tileIndex).modifier();
if (tileIndex >= before.hex_map().terrain_size()) {
@@ -17,7 +17,7 @@ namespace shardok {
using net::eagle0::shardok::api::GameStateView;
using net::eagle0::shardok::api::GameStateViewDiff;
auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
[[nodiscard]] auto MakeGameStateViewDiff(const GameStateView& before, const GameStateView& after)
-> std::optional<GameStateViewDiff>;
} // namespace shardok
@@ -57,6 +57,7 @@ cc_library(
deps = [
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
"//src/main/cpp/net/eagle0/shardok/library:shardok_exception",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
"//src/main/protobuf/net/eagle0/common:common_unit_cc_proto",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
@@ -9,6 +9,7 @@
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
namespace shardok {
@@ -71,13 +72,13 @@ auto PopulateGameOverResponse(
// copy in all the resolvedUnits that correspond to this player
for (const auto &internalRu : resolvedUnits) {
const auto *unit = reinterpret_cast<const Unit *>(internalRu.unit_bytes().data());
if (!unit->has_attached_hero()) continue;
const auto unit = UnitFromBytes(internalRu.unit_bytes());
if (!unit.has_attached_hero()) continue;
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
if (unit->player_id() != shardokPid) continue;
if (unit.player_id() != shardokPid) continue;
auto *ru = resolutionInfo->add_units();
*ru->mutable_unit() = FromUnitFb(*unit);
*ru->mutable_unit() = FromUnitFb(unit);
ru->set_status(FromInternalStatus(internalRu.status()));
if (internalRu.status() ==
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_SECURED_UNIT) {
@@ -22,12 +22,12 @@ auto PopulateGameOverResponse(
const std::vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
auto IncludeUnitProtoInReturn(
[[nodiscard]] auto IncludeUnitProtoInReturn(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
[[nodiscard]] auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
auto FromInternalStatus(
[[nodiscard]] auto FromInternalStatus(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
-> net::eagle0::common::UnitStatus;
@@ -94,7 +94,7 @@ private:
* Read auth token from a file, stripping whitespace.
* Returns empty string if file doesn't exist or is empty.
*/
inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
[[nodiscard]] inline auto ReadAuthTokenFromFile(const std::string& filePath) -> std::string {
if (filePath.empty()) { return ""; }
std::ifstream file(filePath);
@@ -26,10 +26,10 @@ public:
[[nodiscard]] auto what() const noexcept -> const char* override { return description.c_str(); }
};
auto LoadMap(const std::string& mapName) -> HexMapProto;
[[nodiscard]] auto LoadMap(const std::string& mapName) -> HexMapProto;
auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
[[nodiscard]] auto LoadMapFromBytes(const std::string& mapBytes) -> HexMapProto;
auto GetMapNames() -> std::vector<std::string>;
[[nodiscard]] auto GetMapNames() -> std::vector<std::string>;
#endif // EAGLE0_SHARDOK_UTIL_MAP_LOADER_HPP
@@ -22,11 +22,8 @@ namespace eagle {
public void SetGoToBattleAction(Action<string> action) { _goToBattleAction = action; }
public void UpdateBattles(
IList<ShardokBattleView> battles,
IGameModel model,
int battleDay,
string fightableGameId) {
public void
UpdateBattles(IList<ShardokBattleView> battles, IGameModel model, int battleDay) {
dayLabel.text = $"Day {battleDay}";
// Clear stale children left over from scene/prefab (e.g. after reload)
@@ -49,7 +46,7 @@ namespace eagle {
}
for (int i = 0; i < battles.Count; i++) {
_rows[i].Populate(battles[i], model, _goToBattleAction, fightableGameId);
_rows[i].Populate(battles[i], model, _goToBattleAction);
}
}
}
@@ -22,11 +22,8 @@ namespace eagle {
private bool _registeredAsFightButton;
public void Populate(
ShardokBattleView battle,
IGameModel model,
Action<string> goToBattleAction,
string fightableGameId) {
public void
Populate(ShardokBattleView battle, IGameModel model, Action<string> goToBattleAction) {
// Province name
if (model.Provinces.TryGetValue(battle.DefenderProvince, out var pv) &&
model.PlayerId.HasValue) {
@@ -64,7 +61,7 @@ namespace eagle {
outcomeLabel.color =
isAlliedWin ? new Color(0.2f, 0.8f, 0.2f) : new Color(0.8f, 0.2f, 0.2f);
actionButton.gameObject.SetActive(false);
SetActionButtonVisible(false);
} else {
thermometer.gameObject.SetActive(true);
outcomeLabel.gameObject.SetActive(false);
@@ -100,32 +97,36 @@ namespace eagle {
actionButton.onClick.RemoveAllListeners();
var gameId = battle.ShardokGameId;
bool modelReady = model.ShardokGameModels.ContainsKey(gameId);
if (isSelf) {
bool isFightable = gameId == fightableGameId;
actionButton.gameObject.SetActive(true);
actionButton.interactable = isFightable;
actionButtonLabel.text = isFightable ? "Fight!" : "Waiting...";
if (isFightable) {
bool canFight = battle.CanFight && modelReady;
SetActionButtonVisible(true);
actionButton.interactable = canFight;
actionButtonLabel.text =
modelReady ? battle.CanFight ? "Fight!" : "Waiting..." : "Loading...";
if (canFight) {
actionButton.onClick.AddListener(() => goToBattleAction?.Invoke(gameId));
}
SetFightButtonTutorialRegistration(isFightable);
SetFightButtonTutorialRegistration(canFight);
} else if (isAllied) {
// ShardokBattles is populated when a NewBattle eagle update arrives, but the
// matching ShardokGameModel isn't created until the first action-result
// response for that battle reaches us. During that gap, GoToBattle would
// throw on Model.ShardokGameModels[gameId]; gate the button on the model
// existing so the click can never land in the gap.
bool modelReady = model.ShardokGameModels.ContainsKey(gameId);
actionButton.gameObject.SetActive(true);
actionButton.interactable = modelReady;
actionButtonLabel.text = modelReady ? "Observe" : "Loading...";
if (modelReady) {
bool canObserve = battle.CanObserve && modelReady;
SetActionButtonVisible(true);
actionButton.interactable = canObserve;
actionButtonLabel.text = modelReady
? battle.CanObserve ? "Observe" : "Waiting..."
: "Loading...";
if (canObserve) {
actionButton.onClick.AddListener(() => goToBattleAction?.Invoke(gameId));
}
SetFightButtonTutorialRegistration(false);
} else {
actionButton.gameObject.SetActive(false);
SetActionButtonVisible(false);
SetFightButtonTutorialRegistration(false);
}
}
@@ -133,6 +134,17 @@ namespace eagle {
if (hasWinner) { SetFightButtonTutorialRegistration(false); }
}
private void SetActionButtonVisible(bool visible) {
actionButton.gameObject.SetActive(true);
actionButton.interactable = visible;
actionButton.targetGraphic.enabled = visible;
actionButtonLabel.enabled = visible;
if (!visible) {
actionButton.onClick.RemoveAllListeners();
actionButtonLabel.text = "";
}
}
private void SetFightButtonTutorialRegistration(bool shouldRegister) {
var registry = TutorialManager.Instance?.TargetRegistry;
if (registry == null) return;
@@ -37,7 +37,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -2, y: 0}
m_SizeDelta: {x: -14, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!222 &8488118418423736121
CanvasRenderer:
@@ -1859,12 +1859,12 @@ RectTransform:
- {fileID: 5557391494505736092}
- {fileID: 6313361240299796939}
- {fileID: 5570932315451852925}
m_Father: {fileID: 2429126807788525227}
m_Father: {fileID: 910000000000000004}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 217.5914, y: -8}
m_SizeDelta: {x: 173.1829, y: 202}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &5247279438940124088
MonoBehaviour:
@@ -2268,14 +2268,13 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8329760851148613071}
- {fileID: 760782539024697097}
- {fileID: 910000000000000004}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -20}
m_SizeDelta: {x: 0, y: -40}
m_AnchoredPosition: {x: 0, y: -26}
m_SizeDelta: {x: 0, y: -52}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8056181741204377687
CanvasRenderer:
@@ -2334,6 +2333,70 @@ MonoBehaviour:
maxLabel: {fileID: 2383775986439252}
maxEvenLabel: {fileID: 2632144799086272744}
maxEvenButton: {fileID: 8627715712575757530}
--- !u!1 &910000000000000003
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 910000000000000004}
- component: {fileID: 910000000000000005}
m_Layer: 0
m_Name: Content
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &910000000000000004
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000000000000003}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8329760851148613071}
- {fileID: 760782539024697097}
m_Father: {fileID: 2429126807788525227}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -10, y: -10}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &910000000000000005
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000000000000003}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 10
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
m_ChildControlHeight: 1
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!1 &6366334277554995035
GameObject:
m_ObjectHideFlags: 0
@@ -2344,6 +2407,7 @@ GameObject:
m_Component:
- component: {fileID: 8329760851148613071}
- component: {fileID: 531306430670215360}
- component: {fileID: 910000000000000002}
m_Layer: 0
m_Name: Arm Troops Table
m_TagString: Untagged
@@ -2365,13 +2429,33 @@ RectTransform:
m_Children:
- {fileID: 1213384902002452052}
- {fileID: 2133514339961208833}
m_Father: {fileID: 2429126807788525227}
m_Father: {fileID: 910000000000000004}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: -79, y: 0}
m_SizeDelta: {x: 395, y: -20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &910000000000000002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6366334277554995035}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 600
m_MinHeight: -1
m_PreferredWidth: 800
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!114 &531306430670215360
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1198,8 +1198,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -17.5}
m_SizeDelta: {x: 24, y: 31}
m_AnchoredPosition: {x: 5, y: -16}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &1363051944519128355
CanvasRenderer:
@@ -1249,12 +1249,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &1708500215346604881
GameObject:
@@ -2278,8 +2278,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -17.5}
m_SizeDelta: {x: 24, y: 31}
m_AnchoredPosition: {x: 5, y: -16}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &1265686771602852415
CanvasRenderer:
@@ -2329,12 +2329,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &3265081575184054094
GameObject:
@@ -4955,8 +4955,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -17.5}
m_SizeDelta: {x: 24, y: 31}
m_AnchoredPosition: {x: 5, y: -16}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &3184427525695078776
CanvasRenderer:
@@ -5006,12 +5006,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &7946146202987969918
GameObject:
@@ -5048,8 +5048,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -17.5}
m_SizeDelta: {x: 24, y: 31}
m_AnchoredPosition: {x: 5, y: -16}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &7764612421372900914
CanvasRenderer:
@@ -5099,12 +5099,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &8017289266862744017
GameObject:
@@ -1277,7 +1277,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &4527995013667464316
RectTransform:
m_ObjectHideFlags: 0
@@ -1388,6 +1388,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::eagle.BattleAftermathDecisionCommandSelector
ButtonImage: {fileID: 2800000, guid: a2e380d318e0b49489106feb9292804b, type: 3}
unitsTable: {fileID: 5961362941522371876}
keepProvinceToggle: {fileID: 2962653562883339797}
withdrawToggle: {fileID: 4875145955270851034}
decisionToggleGroup: {fileID: 370780603706089160}
@@ -1,5 +1,161 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &217594625717164528
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6057945560984678582}
- component: {fileID: 315907523241977106}
- component: {fileID: 4416349416423580090}
- component: {fileID: 3403394604621129500}
m_Layer: 0
m_Name: Content
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6057945560984678582
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 217594625717164528}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2156765723954912222}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 180}
m_Pivot: {x: 0, y: 1}
--- !u!222 &315907523241977106
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 217594625717164528}
m_CullTransparentMesh: 1
--- !u!114 &4416349416423580090
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 217594625717164528}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: 'Captured from The Ninth Gate Compact.
Bridget is a faction leader.'
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 0, g: 0, b: 0, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 17
m_fontSizeBase: 17
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!114 &3403394604621129500
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 217594625717164528}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
m_HorizontalFit: 0
m_VerticalFit: 2
--- !u!1 &309590178533267660
GameObject:
m_ObjectHideFlags: 0
@@ -507,7 +663,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -12.923078}
m_SizeDelta: {x: 24, y: 24}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &2812250721490797743
CanvasRenderer:
@@ -557,12 +713,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &1211474509175590511
GameObject:
@@ -1045,13 +1201,14 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8962292495336518232}
- {fileID: 2156765723954912222}
- {fileID: 1910983489799631348}
m_Father: {fileID: 8727722510168669489}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 410.35156, y: -95}
m_SizeDelta: {x: 810.7031, y: 180}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -95}
m_SizeDelta: {x: -40, y: 180}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &8034445121229170357
MonoBehaviour:
@@ -1091,7 +1248,7 @@ MonoBehaviour:
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 1
m_Spacing: 0
m_Spacing: 24
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -1667,7 +1824,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -12.923078}
m_SizeDelta: {x: 24, y: 24}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &7293334478758891821
CanvasRenderer:
@@ -1717,12 +1874,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &3423825046551003777
GameObject:
@@ -2029,6 +2186,7 @@ RectTransform:
- {fileID: 4188743959489053307}
- {fileID: 8724010977067144246}
- {fileID: 5699403500788722507}
- {fileID: 8674504589829862267}
m_Father: {fileID: 8854617199350090050}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
@@ -2310,7 +2468,6 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5408870193947667072}
- {fileID: 8674504589829862267}
- {fileID: 8854617199350090050}
m_Father: {fileID: 3293749606495420917}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -2381,7 +2538,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -12.923078}
m_SizeDelta: {x: 24, y: 24}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &2561976136182974797
CanvasRenderer:
@@ -2431,12 +2588,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &5012345052674663814
GameObject:
@@ -2934,7 +3091,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -12.923078}
m_SizeDelta: {x: 24, y: 24}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &2936928241299030275
CanvasRenderer:
@@ -2984,12 +3141,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &5290798991585313877
GameObject:
@@ -3389,10 +3546,10 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4849531796595395475}
- {fileID: 7646253429728842762}
- {fileID: 3163624482538184363}
- {fileID: 2985415434360325071}
- {fileID: 4105688894330716876}
- {fileID: 7646253429728842762}
- {fileID: 3897688492057273019}
- {fileID: 1764692555304340678}
m_Father: {fileID: 8727722510168669489}
@@ -3400,7 +3557,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 410.35156, y: -230.07692}
m_SizeDelta: {x: 810.7031, y: 44}
m_SizeDelta: {x: 810.7031, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4496069533907013775
CanvasRenderer:
@@ -3461,7 +3618,7 @@ MonoBehaviour:
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 20
m_Spacing: 8
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -3505,7 +3662,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 5, y: -12.923078}
m_SizeDelta: {x: 24, y: 24}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &7125238889541449479
CanvasRenderer:
@@ -3555,12 +3712,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 24
m_MinHeight: 24
m_PreferredWidth: -1
m_PreferredHeight: -1
m_MinWidth: 32
m_MinHeight: 32
m_PreferredWidth: 32
m_PreferredHeight: 32
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &6613470828170084132
GameObject:
@@ -3918,7 +4075,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &8674504589829862267
RectTransform:
m_ObjectHideFlags: 0
@@ -3934,13 +4091,13 @@ RectTransform:
- {fileID: 6178109771874370456}
- {fileID: 7388385801072749764}
- {fileID: 8400884665445857398}
m_Father: {fileID: 8727722510168669489}
m_Father: {fileID: 7646253429728842762}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 410.35156, y: -201.07692}
m_SizeDelta: {x: 250, y: 22.153847}
m_Pivot: {x: 0.5, y: 0.5}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: 0, y: 8}
m_SizeDelta: {x: 190, y: 48}
m_Pivot: {x: 0.5, y: 0}
--- !u!222 &1220066650368134757
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -4056,11 +4213,11 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 250
m_PreferredHeight: 30
m_FlexibleWidth: 0
m_MinWidth: 160
m_MinHeight: 48
m_PreferredWidth: 190
m_PreferredHeight: 48
m_FlexibleWidth: 1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &7522094328753451038
@@ -4602,6 +4759,109 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &8258851023267795968
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2156765723954912222}
- component: {fileID: 2323458623403657949}
- component: {fileID: 2719519746693893967}
- component: {fileID: 6456472258987035459}
m_Layer: 0
m_Name: Prisoner Info
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2156765723954912222
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8258851023267795968}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6057945560984678582}
m_Father: {fileID: 5408870193947667072}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 520, y: 180}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2323458623403657949
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8258851023267795968}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3312d7739989d2b4e91e6319e9a96d76, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RectMask2D
m_Padding: {x: 0, y: 0, z: 0, w: 0}
m_Softness: {x: 0, y: 0}
--- !u!114 &2719519746693893967
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8258851023267795968}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ScrollRect
m_Content: {fileID: 6057945560984678582}
m_Horizontal: 0
m_Vertical: 1
m_MovementType: 2
m_Elasticity: 0.1
m_Inertia: 1
m_DecelerationRate: 0.135
m_ScrollSensitivity: 12
m_Viewport: {fileID: 2156765723954912222}
m_HorizontalScrollbar: {fileID: 0}
m_VerticalScrollbar: {fileID: 0}
m_HorizontalScrollbarVisibility: 0
m_VerticalScrollbarVisibility: 0
m_HorizontalScrollbarSpacing: 0
m_VerticalScrollbarSpacing: 0
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
--- !u!114 &6456472258987035459
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8258851023267795968}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 0
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &8552052792859198839
GameObject:
m_ObjectHideFlags: 0
@@ -4880,6 +5140,7 @@ MonoBehaviour:
moveToDropdown: {fileID: 9136850984056651894}
heroDetails: {fileID: 7671674037592426843}
MessageText: {fileID: 5923522230458371477}
PrisonerInfoText: {fileID: 4416349416423580090}
NextHeroButton: {fileID: 7463642923309765748}
--- !u!222 &3375450489895352050
CanvasRenderer:
@@ -5717,7 +5978,7 @@ PrefabInstance:
- target: {fileID: 4733968769770696120, guid: 5294d9ccdfeff49a7922b57958f1f36f,
type: 3}
propertyPath: m_SizeDelta.x
value: 335
value: 500
objectReference: {fileID: 0}
- target: {fileID: 4733968769770696120, guid: 5294d9ccdfeff49a7922b57958f1f36f,
type: 3}
@@ -5727,7 +5988,7 @@ PrefabInstance:
- target: {fileID: 4733968769770696120, guid: 5294d9ccdfeff49a7922b57958f1f36f,
type: 3}
propertyPath: m_AnchoredPosition.x
value: 332.5
value: 250
objectReference: {fileID: 0}
- target: {fileID: 4733968769770696120, guid: 5294d9ccdfeff49a7922b57958f1f36f,
type: 3}
@@ -451,12 +451,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinWidth: 60
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredWidth: 60
m_PreferredHeight: -1
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &488425550134300133
GameObject:
@@ -527,16 +527,16 @@ MonoBehaviour:
m_GameObject: {fileID: 488425550134300133}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 0
m_Spacing: 4
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -689,9 +689,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 110
m_MinWidth: 224
m_MinHeight: -1
m_PreferredWidth: 110
m_PreferredWidth: 224
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
@@ -884,12 +884,12 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinWidth: 60
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredWidth: 60
m_PreferredHeight: -1
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &1155012697468707981
GameObject:
@@ -983,7 +983,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &7586176169805169365
RectTransform:
m_ObjectHideFlags: 0
@@ -1282,7 +1282,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 74.94789, y: -76.28999}
m_SizeDelta: {x: 120, y: 20}
m_SizeDelta: {x: 120, y: 44}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8543794887947467143
CanvasRenderer:
@@ -1392,9 +1392,9 @@ MonoBehaviour:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 120
m_PreferredHeight: 20
m_MinHeight: 44
m_PreferredWidth: 100
m_PreferredHeight: 44
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -1901,16 +1901,16 @@ MonoBehaviour:
m_GameObject: {fileID: 2953883977394020486}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 0
m_Spacing: 4
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -2365,7 +2365,6 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6513069223245071550}
- {fileID: 7350415193282674024}
- {fileID: 7969189778685776836}
- {fileID: 3917383894856241933}
@@ -2378,7 +2377,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 209.96754, y: -7.5}
m_SizeDelta: {x: 459, y: 15}
m_SizeDelta: {x: 760, y: 15}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6612103987515864042
CanvasRenderer:
@@ -2431,7 +2430,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Padding:
m_Left: 5
m_Left: 13
m_Right: 5
m_Top: 0
m_Bottom: 0
@@ -2529,12 +2528,13 @@ RectTransform:
m_Children:
- {fileID: 4151450821939884441}
- {fileID: 3874727949806933351}
m_Father: {fileID: 1860981762561904428}
- {fileID: 7362096397021782730}
m_Father: {fileID: 912345678901234568}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 450.83984, y: -121.5}
m_SizeDelta: {x: 419.9351, y: 243}
m_SizeDelta: {x: 720.9351, y: 243}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &3117222806062181928
MonoBehaviour:
@@ -2605,9 +2605,9 @@ MonoBehaviour:
m_IgnoreLayout: 0
m_MinWidth: 0
m_MinHeight: -1
m_PreferredWidth: 459
m_PreferredWidth: 620
m_PreferredHeight: -1
m_FlexibleWidth: 0
m_FlexibleWidth: 1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &4443145359085870969
@@ -2648,7 +2648,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 74.94789, y: -56.289993}
m_SizeDelta: {x: 120, y: 20}
m_SizeDelta: {x: 120, y: 44}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &210610449393901007
CanvasRenderer:
@@ -2758,9 +2758,9 @@ MonoBehaviour:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 120
m_PreferredHeight: 20
m_MinHeight: 44
m_PreferredWidth: 100
m_PreferredHeight: 44
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -3045,9 +3045,7 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 748598348251160796}
- {fileID: 7750478506398315162}
- {fileID: 7362096397021782730}
- {fileID: 912345678901234568}
m_Father: {fileID: 839607184085740685}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@@ -3064,14 +3062,79 @@ MonoBehaviour:
m_GameObject: {fileID: 5010833061231567156}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_Padding:
m_Left: 5
m_Right: 5
m_Top: 0
m_Bottom: 5
m_ChildAlignment: 0
m_Spacing: 5
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
m_ChildControlHeight: 1
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!1 &912345678901234567
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 912345678901234568}
- component: {fileID: 912345678901234569}
- component: {fileID: 912345678901234570}
m_Layer: 0
m_Name: Tables Row
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &912345678901234568
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 912345678901234567}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 748598348251160796}
- {fileID: 7750478506398315162}
m_Father: {fileID: 1860981762561904428}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -121.5}
m_SizeDelta: {x: 0, y: 243}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &912345678901234569
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 912345678901234567}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 5
m_ChildForceExpandWidth: 0
@@ -3081,6 +3144,26 @@ MonoBehaviour:
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &912345678901234570
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 912345678901234567}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &5838670755870491000
GameObject:
m_ObjectHideFlags: 0
@@ -3341,7 +3424,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 74.94789, y: -36.289993}
m_SizeDelta: {x: 120, y: 20}
m_SizeDelta: {x: 120, y: 44}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5803585239292126491
CanvasRenderer:
@@ -3451,9 +3534,9 @@ MonoBehaviour:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 120
m_PreferredHeight: 20
m_MinHeight: 44
m_PreferredWidth: 100
m_PreferredHeight: 44
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -3716,9 +3799,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: 20
m_MinWidth: 0
m_MinHeight: -1
m_PreferredWidth: 20
m_PreferredWidth: 0
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
@@ -3825,6 +3908,12 @@ MonoBehaviour:
costLabel: {fileID: 3527621031723332124}
costWarningLabel: {fileID: 3629507779563399207}
troopCountLabel: {fileID: 2686329734541291421}
controlsPanel: {fileID: 7362096397021782730}
tablesRow: {fileID: 912345678901234568}
organizeTroopsTable: {fileID: 7750478506398315162}
controlsHorizontalLayout: {fileID: 7382413815305380344}
controlsLayoutElement: {fileID: 1563171855920966945}
organizeTroopsTableLayoutElement: {fileID: 4021893445488533329}
--- !u!1 &6701613514182487714
GameObject:
m_ObjectHideFlags: 0
@@ -3855,15 +3944,15 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 195834348705549749}
- {fileID: 209791553066742287}
- {fileID: 1942492033842436962}
- {fileID: 4434663922884638592}
- {fileID: 7936925421685254814}
- {fileID: 989737680149053438}
- {fileID: 7586176169805169365}
- {fileID: 4427285768326593240}
m_Father: {fileID: 1860981762561904428}
- {fileID: 195834348705549749}
- {fileID: 7586176169805169365}
m_Father: {fileID: 7750478506398315162}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
@@ -3879,16 +3968,16 @@ MonoBehaviour:
m_GameObject: {fileID: 6701613514182487714}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Left: 4
m_Right: 4
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 0
m_Spacing: 8
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -3912,9 +4001,9 @@ MonoBehaviour:
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: 0
m_FlexibleHeight: 1
m_PreferredHeight: 50
m_FlexibleWidth: 1
m_FlexibleHeight: 0
m_LayoutPriority: 1
--- !u!1 &6729763905510632408
GameObject:
@@ -3948,7 +4037,7 @@ RectTransform:
m_Children:
- {fileID: 922705621724578618}
- {fileID: 8590284293786068690}
m_Father: {fileID: 1860981762561904428}
m_Father: {fileID: 912345678901234568}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
@@ -4040,7 +4129,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 209.96754, y: -130.5}
m_SizeDelta: {x: 419.9351, y: 225}
m_SizeDelta: {x: 720.9351, y: 225}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8745097594797046594
CanvasRenderer:
@@ -4147,7 +4236,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &195834348705549749
RectTransform:
m_ObjectHideFlags: 0
@@ -196,7 +196,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 300, y: -112.509995}
m_AnchoredPosition: {x: 300, y: -120.509995}
m_SizeDelta: {x: 600, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &5334479731787923377
@@ -947,10 +947,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 69246690839682329}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: 26, y: 26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &697706423778571430
CanvasRenderer:
@@ -973,16 +973,16 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 0, g: 0, b: 0, a: 0.9}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300002, guid: 66995b6b61aed864daa956ae255d7fee, type: 3}
m_Sprite: {fileID: 21300000, guid: 26aa44d57374b4f3c8ba477d8bd86235, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
@@ -1022,10 +1022,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2623886979664758727}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: 26, y: 26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &122077270395423389
CanvasRenderer:
@@ -1048,16 +1048,16 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 0, g: 0, b: 0, a: 0.9}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300002, guid: 66995b6b61aed864daa956ae255d7fee, type: 3}
m_Sprite: {fileID: 21300000, guid: 26aa44d57374b4f3c8ba477d8bd86235, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
@@ -1238,10 +1238,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 7361033892154136433}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMin: {x: 0, y: 0.5}
m_AnchorMax: {x: 0, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7733795268603465078
CanvasRenderer:
@@ -1266,14 +1266,14 @@ MonoBehaviour:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_RaycastPadding: {x: -6, y: -6, z: -6, w: -6}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 0
m_PreserveAspect: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
@@ -1476,7 +1476,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 44, y: -16}
m_SizeDelta: {x: 32, y: 32}
m_SizeDelta: {x: 40, y: 40}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9056578123099038329
CanvasRenderer:
@@ -1531,8 +1531,8 @@ MonoBehaviour:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 32
m_PreferredHeight: 32
m_PreferredWidth: 40
m_PreferredHeight: 40
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -1951,16 +1951,16 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0.5019608}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
@@ -2037,10 +2037,10 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinWidth: 44
m_MinHeight: 44
m_PreferredWidth: 25
m_PreferredHeight: 25
m_PreferredWidth: 44
m_PreferredHeight: 44
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -2057,9 +2057,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 18
m_MinWidth: 44
m_MinHeight: 44
m_PreferredWidth: 44
m_PreferredHeight: 44
m_FlexibleWidth: 0
m_FlexibleHeight: 0
@@ -2322,16 +2322,16 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0.5019608}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
@@ -2408,10 +2408,10 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinWidth: 44
m_MinHeight: 44
m_PreferredWidth: 25
m_PreferredHeight: 25
m_PreferredWidth: 44
m_PreferredHeight: 44
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -2428,9 +2428,9 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 18
m_MinWidth: 44
m_MinHeight: 44
m_PreferredWidth: 44
m_PreferredHeight: 44
m_FlexibleWidth: 0
m_FlexibleHeight: 0
@@ -2903,7 +2903,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 556, y: -16}
m_SizeDelta: {x: 32, y: 32}
m_SizeDelta: {x: 40, y: 40}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7328892238930851044
CanvasRenderer:
@@ -2958,8 +2958,8 @@ MonoBehaviour:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 32
m_PreferredHeight: 32
m_PreferredWidth: 40
m_PreferredHeight: 40
m_FlexibleWidth: 0
m_FlexibleHeight: 0
m_LayoutPriority: 1
@@ -20,6 +20,43 @@ namespace eagle {
private int _availableGold;
private int _maxArmament;
private MaxAttainableEverywhereBattalions _maxAttainableEverywhereBattalions;
private int? _selectedBattalionIndex;
private GameObject _touchActionStrip;
private TMP_Text _touchActionStripLabel;
private Button _touchPlusButton;
private Button _touchMinusButton;
private Button _touchResetButton;
private Button _touchMaxButton;
private Image _touchPlusIcon;
private Image _touchMinusIcon;
private Image _touchResetIcon;
private Image _touchMaxIcon;
private Transform _touchActionButtonGrid;
private GameObject _allBattalionsHeader;
private GameObject _allBattalionsMaxButtonRow;
private bool _capturedTableLayout;
private bool _capturedAllBattalionsLayout;
private Vector2 _tableAnchoredPosition;
private Vector2 _tableSizeDelta;
private LayoutElement _tableLayoutElement;
private float _tableMinWidth;
private float _tablePreferredWidth;
private float _tableFlexibleWidth;
private HorizontalLayoutGroup _mainContentLayout;
private float _mainContentSpacing;
private Transform _touchActionStripParent;
private int _touchActionStripSiblingIndex;
private RectTransform _allBattalionsControlsRectTransform;
private Vector2 _allBattalionsControlsAnchoredPosition;
private Vector2 _allBattalionsControlsSizeDelta;
private VerticalLayoutGroup _allBattalionsControlsLayout;
private TextAnchor _allBattalionsControlsAlignment;
private RectTransform _maxAllButtonRectTransform;
private RectTransform _maxEvenButtonRectTransform;
private Transform _maxAllButtonParent;
private Transform _maxEvenButtonParent;
private int _maxAllButtonSiblingIndex;
private int _maxEvenButtonSiblingIndex;
public EventBasedTable armTroopsTable;
public TMP_Text totalCostText;
@@ -34,6 +71,9 @@ namespace eagle {
private ArmTroopsAvailableCommand ArmTroopsAvailableCommand =>
_availableCommand.ArmTroopsCommand;
private ProvinceId ActingProvinceId => ArmTroopsAvailableCommand.ActingProvinceId;
private bool UseTouchActionStrip =>
IsNarrowLayout && GameInput.HasTouchscreen() &&
(!GameInput.HasMousePointer() || GameInput.HasSimulatedTouchscreen());
private struct MaxAttainableEverywhereBattalions {
public int Armament;
@@ -107,6 +147,14 @@ namespace eagle {
}
protected override void SetUpUI() {
EnsureTouchActionStrip();
if (armTroopsTable.RowClickedHandler == null) {
armTroopsTable.RowClickedHandler = new EventBasedTable.RowClickedEvent();
}
armTroopsTable.WireRuntimeRowClickHandlers = true;
armTroopsTable.RowClickedHandler.RemoveListener(RowClicked);
armTroopsTable.RowClickedHandler.AddListener(RowClicked);
var province = _model.Provinces[ActingProvinceId];
_battalions =
ArmTroopsAvailableCommand.AvailableBattalions
@@ -126,16 +174,21 @@ namespace eagle {
_maxArmament = ArmTroopsAvailableCommand.MaxArmament;
_maxAttainableEverywhereBattalions = SpendRemainingGold(SpendEvenlyOnly());
if (_maxAttainableEverywhereBattalions.Armament == _maxArmament) {
maxEvenButton.gameObject.SetActive(false);
} else {
maxEvenLabel.text = _maxAttainableEverywhereBattalions.Armament.ToString();
maxEvenButton.gameObject.SetActive(true);
}
maxEvenLabel.text = _maxAttainableEverywhereBattalions.Armament.ToString();
maxEvenButton.gameObject.SetActive(true);
maxEvenButton.interactable =
_maxAttainableEverywhereBattalions.Armament != _maxArmament;
maxLabel.text = _maxArmament.ToString();
availableText.text = _availableGold.ToString();
ApplyInputModeLayout();
UpdateTableAndCost();
}
public override void OnLayoutModeChanged() {
if (_battalions == null) return;
ApplyInputModeLayout();
UpdateTableAndCost();
}
@@ -159,6 +212,12 @@ namespace eagle {
row.MinusButtonClickedCallback = () => MinusClicked(index);
row.ResetButtonClickedCallback = () => ResetClicked(index);
row.MaxButtonClickedCallback = () => MaxClicked(index);
row.RowClickedCallback = () => RowClicked(index);
row.SetActionButtonsVisible(!UseTouchActionStrip);
row.SetNameVisible(!UseTouchActionStrip);
row.SetContentRaycastTargets(!UseTouchActionStrip);
row.Selectable = true;
row.Selected = UseTouchActionStrip && _selectedBattalionIndex == index;
SetValuesAndButtons(index);
}
@@ -172,12 +231,13 @@ namespace eagle {
row.NewArmament = newArmament;
row.Name = battalion.Name;
row.AllowMax = battalion.Armament < _maxArmament;
row.AllowPlus = battalion.Armament < _maxArmament;
row.AllowMax = newArmament < _maxArmament;
row.AllowPlus = newArmament < _maxArmament;
row.AllowMinus = battalion.Armament < newArmament;
row.AllowReset = battalion.Armament != newArmament;
UpdateCost();
UpdateTouchActionStrip();
}
private void UpdateCost() {
@@ -198,11 +258,28 @@ namespace eagle {
}
private void UpdateTableAndCost() {
if (UseTouchActionStrip && _battalions.Count > 0) {
if (_selectedBattalionIndex == null || _selectedBattalionIndex < 0 ||
_selectedBattalionIndex >= _battalions.Count) {
_selectedBattalionIndex = 0;
}
} else {
_selectedBattalionIndex = null;
}
armTroopsTable.RowCount = _battalions.Count;
for (int i = 0; i < _battalions.Count; i++) { PopulateRow(i); }
armTroopsTable.RefreshRowEventHandlers();
UpdateCost();
UpdateTouchActionStrip();
}
public void RowClicked(int index) {
if (!UseTouchActionStrip) return;
SelectBattalion(index);
}
public void PlusClicked(int index) {
@@ -276,5 +353,378 @@ namespace eagle {
}
private int Cost() { return Cost(_newArmaments); }
private void SelectBattalion(int index) {
_selectedBattalionIndex = index;
for (int i = 0; i < _battalions.Count; i++) {
armTroopsTable.ComponentAt<ArmTroopsRowController>(i).Selected = i == index;
}
UpdateTouchActionStrip();
}
private bool TrySelectedBattalionIndex(out int index) {
index = _selectedBattalionIndex ?? -1;
return UseTouchActionStrip && index >= 0 && index < _battalions.Count;
}
private void UpdateTouchActionStrip() {
if (_touchActionStrip == null) return;
if (!TrySelectedBattalionIndex(out int index)) {
_touchActionStrip.SetActive(false);
return;
}
var battalion = _battalions[index];
var originalArmament = (int)(battalion.Armament ?? 0);
var newArmament = _newArmaments[index];
_touchActionStrip.SetActive(true);
_touchActionStripLabel.text = battalion.Name;
UpdateTouchActionButtonIcons();
_touchPlusButton.interactable = newArmament < _maxArmament;
_touchMinusButton.interactable = originalArmament < newArmament;
_touchResetButton.interactable = originalArmament != newArmament;
_touchMaxButton.interactable = newArmament < _maxArmament;
}
private void EnsureTouchActionStrip() {
if (_touchActionStrip != null) return;
_touchActionStrip = new GameObject(
"Touch Action Strip",
typeof(RectTransform),
typeof(LayoutElement),
typeof(Image),
typeof(VerticalLayoutGroup));
_touchActionStrip.transform.SetParent(transform, false);
_touchActionStripParent = transform;
_touchActionStripSiblingIndex = _touchActionStrip.transform.GetSiblingIndex();
var rectTransform = _touchActionStrip.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition = new Vector2(55f, -16f);
rectTransform.sizeDelta = new Vector2(176f, 186f);
var stripLayoutElement = _touchActionStrip.GetComponent<LayoutElement>();
stripLayoutElement.minWidth = 176f;
stripLayoutElement.preferredWidth = 176f;
stripLayoutElement.minHeight = 202f;
stripLayoutElement.preferredHeight = 202f;
stripLayoutElement.flexibleWidth = -1f;
stripLayoutElement.flexibleHeight = 1f;
var background = _touchActionStrip.GetComponent<Image>();
background.color = Color.clear;
var layout = _touchActionStrip.GetComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(0, 0, 0, 0);
layout.spacing = 6f;
layout.childAlignment = TextAnchor.UpperCenter;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
_touchActionStripLabel = CreateActionStripLabel();
_touchActionButtonGrid = CreateActionButtonGrid();
_touchMinusButton = CreateActionStripButton("Minus Button", out _touchMinusIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { MinusClicked(index); }
});
_touchPlusButton = CreateActionStripButton("Plus Button", out _touchPlusIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { PlusClicked(index); }
});
_touchResetButton = CreateActionStripButton("Reset Button", out _touchResetIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { ResetClicked(index); }
});
_touchMaxButton = CreateActionStripButton("Max Button", out _touchMaxIcon, () => {
if (TrySelectedBattalionIndex(out int index)) { MaxClicked(index); }
});
_touchActionStrip.SetActive(false);
EnsureAllBattalionsHeader();
}
private void ApplyInputModeLayout() {
var tableRectTransform = armTroopsTable.GetComponent<RectTransform>();
if (!_capturedTableLayout) {
_tableAnchoredPosition = tableRectTransform.anchoredPosition;
_tableSizeDelta = tableRectTransform.sizeDelta;
_tableLayoutElement = armTroopsTable.GetComponent<LayoutElement>();
_tableMinWidth = _tableLayoutElement.minWidth;
_tablePreferredWidth = _tableLayoutElement.preferredWidth;
_tableFlexibleWidth = _tableLayoutElement.flexibleWidth;
_mainContentLayout =
tableRectTransform.parent.GetComponent<HorizontalLayoutGroup>();
_mainContentSpacing = _mainContentLayout.spacing;
_allBattalionsControlsRectTransform =
maxEvenButton.transform.parent.GetComponent<RectTransform>();
_allBattalionsControlsAnchoredPosition =
_allBattalionsControlsRectTransform.anchoredPosition;
_allBattalionsControlsSizeDelta = _allBattalionsControlsRectTransform.sizeDelta;
_capturedTableLayout = true;
}
CaptureAllBattalionsLayout();
EnsureAllBattalionsHeader();
EnsureAllBattalionsMaxButtonRow();
if (UseTouchActionStrip) {
var touchTableWidth = 270f;
_mainContentLayout.spacing = _mainContentSpacing;
_tableLayoutElement.minWidth = touchTableWidth;
_tableLayoutElement.preferredWidth = touchTableWidth;
_tableLayoutElement.flexibleWidth = -1f;
tableRectTransform.anchoredPosition = _tableAnchoredPosition;
tableRectTransform.sizeDelta = _tableSizeDelta;
_allBattalionsControlsRectTransform.anchoredPosition =
_allBattalionsControlsAnchoredPosition;
_allBattalionsControlsRectTransform.sizeDelta = new Vector2(190f, 202f);
_allBattalionsControlsLayout.childAlignment = TextAnchor.UpperCenter;
_touchActionStrip.transform.SetParent(tableRectTransform.parent, false);
_touchActionStrip.transform.SetSiblingIndex(
tableRectTransform.GetSiblingIndex() + 1);
var touchActionStripRectTransform = _touchActionStrip.GetComponent<RectTransform>();
touchActionStripRectTransform.anchoredPosition = Vector2.zero;
touchActionStripRectTransform.sizeDelta = new Vector2(176f, 202f);
_allBattalionsHeader.transform.SetParent(
_allBattalionsControlsRectTransform,
false);
_allBattalionsHeader.transform.SetSiblingIndex(0);
_allBattalionsMaxButtonRow.transform.SetParent(
_allBattalionsControlsRectTransform,
false);
_allBattalionsMaxButtonRow.transform.SetSiblingIndex(1);
_maxAllButtonRectTransform.SetParent(_allBattalionsMaxButtonRow.transform, false);
_maxAllButtonRectTransform.SetSiblingIndex(0);
_maxEvenButtonRectTransform.SetParent(_allBattalionsMaxButtonRow.transform, false);
_maxEvenButtonRectTransform.SetSiblingIndex(1);
_allBattalionsHeader.SetActive(true);
_allBattalionsMaxButtonRow.SetActive(true);
} else {
_mainContentLayout.spacing = 18f;
_tableLayoutElement.minWidth = _tableMinWidth;
_tableLayoutElement.preferredWidth = _tablePreferredWidth;
_tableLayoutElement.flexibleWidth = _tableFlexibleWidth;
tableRectTransform.anchoredPosition = _tableAnchoredPosition;
tableRectTransform.sizeDelta = _tableSizeDelta;
_touchActionStrip.transform.SetParent(_touchActionStripParent, false);
_touchActionStrip.transform.SetSiblingIndex(_touchActionStripSiblingIndex);
_allBattalionsControlsRectTransform.anchoredPosition =
_allBattalionsControlsAnchoredPosition;
_allBattalionsControlsRectTransform.sizeDelta = _allBattalionsControlsSizeDelta;
_allBattalionsControlsLayout.childAlignment = _allBattalionsControlsAlignment;
RestoreAllBattalionsMaxButtons();
if (_allBattalionsHeader != null) { _allBattalionsHeader.SetActive(false); }
if (_allBattalionsMaxButtonRow != null) {
_allBattalionsMaxButtonRow.SetActive(false);
}
}
}
private void CaptureAllBattalionsLayout() {
if (_capturedAllBattalionsLayout) return;
_allBattalionsControlsLayout =
_allBattalionsControlsRectTransform.GetComponent<VerticalLayoutGroup>();
_allBattalionsControlsAlignment = _allBattalionsControlsLayout.childAlignment;
_maxAllButtonRectTransform =
_allBattalionsControlsRectTransform.GetChild(0).GetComponent<RectTransform>();
_maxEvenButtonRectTransform = maxEvenButton.GetComponent<RectTransform>();
_maxAllButtonParent = _maxAllButtonRectTransform.parent;
_maxEvenButtonParent = _maxEvenButtonRectTransform.parent;
_maxAllButtonSiblingIndex = _maxAllButtonRectTransform.GetSiblingIndex();
_maxEvenButtonSiblingIndex = _maxEvenButtonRectTransform.GetSiblingIndex();
_capturedAllBattalionsLayout = true;
}
private void RestoreAllBattalionsMaxButtons() {
_maxAllButtonRectTransform.SetParent(_maxAllButtonParent, false);
_maxAllButtonRectTransform.SetSiblingIndex(_maxAllButtonSiblingIndex);
_maxEvenButtonRectTransform.SetParent(_maxEvenButtonParent, false);
_maxEvenButtonRectTransform.SetSiblingIndex(_maxEvenButtonSiblingIndex);
}
private TMP_Text CreateActionStripLabel() {
var label = new GameObject(
"Selected Battalion Label",
typeof(RectTransform),
typeof(LayoutElement),
typeof(TextMeshProUGUI));
label.transform.SetParent(_touchActionStrip.transform, false);
var layoutElement = label.GetComponent<LayoutElement>();
layoutElement.minWidth = 176f;
layoutElement.preferredWidth = 176f;
layoutElement.minHeight = 56f;
layoutElement.preferredHeight = 56f;
var text = label.GetComponent<TextMeshProUGUI>();
text.alignment = TextAlignmentOptions.Top;
text.color = Color.black;
text.font = maxLabel.font;
text.fontSize = 16f;
text.textWrappingMode = TextWrappingModes.Normal;
text.overflowMode = TextOverflowModes.Ellipsis;
return text;
}
private Transform CreateActionButtonGrid() {
var gridObject = new GameObject(
"Action Button Grid",
typeof(RectTransform),
typeof(LayoutElement),
typeof(GridLayoutGroup));
gridObject.transform.SetParent(_touchActionStrip.transform, false);
var layoutElement = gridObject.GetComponent<LayoutElement>();
layoutElement.minWidth = 108f;
layoutElement.preferredWidth = 108f;
layoutElement.minHeight = 108f;
layoutElement.preferredHeight = 108f;
var grid = gridObject.GetComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(48f, 48f);
grid.spacing = new Vector2(6f, 6f);
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
grid.constraintCount = 2;
grid.childAlignment = TextAnchor.MiddleCenter;
return gridObject.transform;
}
private Button CreateActionStripButton(
string name,
out Image icon,
UnityEngine.Events.UnityAction action) {
var buttonObject = new GameObject(
name,
typeof(RectTransform),
typeof(LayoutElement),
typeof(Image),
typeof(Button));
buttonObject.transform.SetParent(_touchActionButtonGrid, false);
var layoutElement = buttonObject.GetComponent<LayoutElement>();
layoutElement.minWidth = 48f;
layoutElement.preferredWidth = 48f;
layoutElement.minHeight = 48f;
layoutElement.preferredHeight = 48f;
var image = buttonObject.GetComponent<Image>();
image.color = new Color(0.94f, 0.94f, 0.96f, 1f);
CopyButtonBackground(maxEvenButton, image);
var button = buttonObject.GetComponent<Button>();
button.targetGraphic = image;
button.onClick.AddListener(action);
var iconObject = new GameObject("Icon", typeof(RectTransform), typeof(Image));
iconObject.transform.SetParent(buttonObject.transform, false);
var iconRectTransform = iconObject.GetComponent<RectTransform>();
iconRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
iconRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
iconRectTransform.anchoredPosition = Vector2.zero;
iconRectTransform.sizeDelta = new Vector2(34f, 34f);
icon = iconObject.GetComponent<Image>();
icon.color = Color.black;
icon.preserveAspect = true;
return button;
}
private void UpdateTouchActionButtonIcons() {
if (_battalions == null || _battalions.Count == 0) return;
var row = armTroopsTable.ComponentAt<ArmTroopsRowController>(0);
_touchMinusIcon.sprite = ButtonIconSprite(row.minusButton);
_touchPlusIcon.sprite = ButtonIconSprite(row.plusButton);
_touchResetIcon.sprite = ButtonIconSprite(row.resetButton);
_touchMaxIcon.sprite = ButtonIconSprite(row.maxButton);
}
private Sprite ButtonIconSprite(Button button) {
foreach (var image in button.GetComponentsInChildren<Image>(includeInactive: true)) {
if (image.gameObject != button.gameObject && image.sprite != null) {
return image.sprite;
}
}
return null;
}
private void CopyButtonBackground(Button sourceButton, Image targetImage) {
var sourceImage = sourceButton.targetGraphic as Image;
if (sourceImage == null) return;
targetImage.sprite = sourceImage.sprite;
targetImage.type = sourceImage.type;
targetImage.pixelsPerUnitMultiplier = sourceImage.pixelsPerUnitMultiplier;
}
private void EnsureAllBattalionsMaxButtonRow() {
if (_allBattalionsMaxButtonRow != null) return;
_allBattalionsMaxButtonRow = new GameObject(
"All Battalions Max Button Row",
typeof(RectTransform),
typeof(LayoutElement),
typeof(HorizontalLayoutGroup));
_allBattalionsMaxButtonRow.transform.SetParent(
_allBattalionsControlsRectTransform,
false);
var layoutElement = _allBattalionsMaxButtonRow.GetComponent<LayoutElement>();
layoutElement.minWidth = 190f;
layoutElement.preferredWidth = 190f;
layoutElement.minHeight = 60f;
layoutElement.preferredHeight = 60f;
var layout = _allBattalionsMaxButtonRow.GetComponent<HorizontalLayoutGroup>();
layout.padding = new RectOffset(0, 0, 0, 0);
layout.spacing = 8f;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
_allBattalionsMaxButtonRow.SetActive(false);
}
private void EnsureAllBattalionsHeader() {
if (_allBattalionsHeader != null) return;
_allBattalionsHeader = new GameObject(
"All Battalions Header",
typeof(RectTransform),
typeof(LayoutElement),
typeof(TextMeshProUGUI));
_allBattalionsHeader.transform.SetParent(transform, false);
var layoutElement = _allBattalionsHeader.GetComponent<LayoutElement>();
layoutElement.minWidth = 190f;
layoutElement.preferredWidth = 190f;
layoutElement.minHeight = 56f;
layoutElement.preferredHeight = 56f;
var text = _allBattalionsHeader.GetComponent<TextMeshProUGUI>();
text.text = "All Battalions";
text.alignment = TextAlignmentOptions.Top;
text.color = Color.black;
text.font = maxLabel.font;
text.fontSize = 16f;
text.textWrappingMode = TextWrappingModes.NoWrap;
_allBattalionsHeader.SetActive(false);
}
}
}
}

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