From c4c84439dc9140bc05315879d5b57ca1359808af Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:32:22 -0700 Subject: [PATCH 01/41] wip --- drool/imgs/attacks/attack_spritesheet.json | 52 ++++++++++++++------- drool/imgs/attacks/attack_spritesheet.png | Bin 131657 -> 146015 bytes drool/imgs/attacks/loop.gif | Bin 0 -> 3299 bytes drool/imgs/attacks/loop.png | Bin 0 -> 4321 bytes drool/imgs/attacks/triple_think.gif | Bin 0 -> 5860 bytes drool/imgs/attacks/triple_think.png | Bin 0 -> 6682 bytes drool/imgs/attacks/unexpected_carrot.gif | Bin 0 -> 15999 bytes drool/imgs/attacks/unexpected_carrot.png | Bin 0 -> 9220 bytes processing/generateMonsTypeScript.py | 2 + src/game-layer/GachaTeamRegistry.sol | 9 ++-- 10 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 drool/imgs/attacks/loop.gif create mode 100644 drool/imgs/attacks/loop.png create mode 100644 drool/imgs/attacks/triple_think.gif create mode 100644 drool/imgs/attacks/triple_think.png create mode 100644 drool/imgs/attacks/unexpected_carrot.gif create mode 100644 drool/imgs/attacks/unexpected_carrot.png diff --git a/drool/imgs/attacks/attack_spritesheet.json b/drool/imgs/attacks/attack_spritesheet.json index ddf009c8..e24c27ec 100644 --- a/drool/imgs/attacks/attack_spritesheet.json +++ b/drool/imgs/attacks/attack_spritesheet.json @@ -3,102 +3,120 @@ "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[0, 0], [96, 0], [192, 0], [288, 0], [384, 0], [480, 0], [576, 0], [672, 0], [768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [1440, 0], [1536, 0], [1632, 0], [1728, 0], [1824, 0], [0, 96], [96, 96], [192, 96], [288, 96]] + "frames": [[0, 0], [96, 0], [192, 0], [288, 0], [384, 0], [480, 0], [576, 0], [672, 0], [768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [1440, 0], [1536, 0], [1632, 0], [1728, 0], [1824, 0], [1920, 0], [0, 96], [96, 96], [192, 96]] }, "bubble_bop": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[384, 96], [480, 96], [576, 96], [672, 96], [768, 96], [864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [1440, 96], [1536, 96], [1632, 96], [1728, 96], [1824, 96], [0, 192], [96, 192], [192, 192], [288, 192], [384, 192], [480, 192], [576, 192], [672, 192]] + "frames": [[288, 96], [384, 96], [480, 96], [576, 96], [672, 96], [768, 96], [864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [1440, 96], [1536, 96], [1632, 96], [1728, 96], [1824, 96], [1920, 96], [0, 192], [96, 192], [192, 192], [288, 192], [384, 192], [480, 192]] }, "contagious_slumber": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[768, 192], [864, 192], [960, 192], [1056, 192], [1152, 192], [1248, 192], [1344, 192], [1440, 192], [1536, 192], [1632, 192], [1728, 192], [1824, 192], [0, 288], [96, 288], [192, 288], [288, 288], [384, 288], [480, 288], [576, 288], [672, 288], [768, 288], [864, 288], [960, 288], [1056, 288]] + "frames": [[576, 192], [672, 192], [768, 192], [864, 192], [960, 192], [1056, 192], [1152, 192], [1248, 192], [1344, 192], [1440, 192], [1536, 192], [1632, 192], [1728, 192], [1824, 192], [1920, 192], [0, 288], [96, 288], [192, 288], [288, 288], [384, 288], [480, 288], [576, 288], [672, 288], [768, 288]] }, "deep_freeze": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1152, 288], [1248, 288], [1344, 288], [1440, 288], [1536, 288], [1632, 288], [1728, 288], [1824, 288], [0, 384], [96, 384], [192, 384], [288, 384], [384, 384], [480, 384], [576, 384], [672, 384], [768, 384], [864, 384], [960, 384], [1056, 384], [1152, 384], [1248, 384], [1344, 384], [1440, 384]] + "frames": [[864, 288], [960, 288], [1056, 288], [1152, 288], [1248, 288], [1344, 288], [1440, 288], [1536, 288], [1632, 288], [1728, 288], [1824, 288], [1920, 288], [0, 384], [96, 384], [192, 384], [288, 384], [384, 384], [480, 384], [576, 384], [672, 384], [768, 384], [864, 384], [960, 384], [1056, 384]] }, "infernal_flame": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1536, 384], [1632, 384], [1728, 384], [1824, 384], [0, 480], [96, 480], [192, 480], [288, 480], [384, 480], [480, 480], [576, 480], [672, 480], [768, 480], [864, 480], [960, 480], [1056, 480], [1152, 480], [1248, 480], [1344, 480], [1440, 480], [1536, 480], [1632, 480], [1728, 480], [1824, 480], [0, 576]] + "frames": [[1152, 384], [1248, 384], [1344, 384], [1440, 384], [1536, 384], [1632, 384], [1728, 384], [1824, 384], [1920, 384], [0, 480], [96, 480], [192, 480], [288, 480], [384, 480], [480, 480], [576, 480], [672, 480], [768, 480], [864, 480], [960, 480], [1056, 480], [1152, 480], [1248, 480], [1344, 480], [1440, 480]] }, "infinite_love": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[96, 576], [192, 576], [288, 576], [384, 576], [480, 576], [576, 576], [672, 576], [768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576], [1344, 576], [1440, 576], [1536, 576], [1632, 576], [1728, 576], [1824, 576], [0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672]] + "frames": [[1536, 480], [1632, 480], [1728, 480], [1824, 480], [1920, 480], [0, 576], [96, 576], [192, 576], [288, 576], [384, 576], [480, 576], [576, 576], [672, 576], [768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576], [1344, 576], [1440, 576], [1536, 576], [1632, 576], [1728, 576], [1824, 576]] + }, + "loop": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[1920, 576], [0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672], [576, 672], [672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672], [1440, 672], [1536, 672]] }, "mega_star_blast": { "msPerFrame": 50, "width": 96, "height": 96, - "frames": [[576, 672], [672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672], [1440, 672], [1536, 672], [1632, 672], [1728, 672], [1824, 672], [0, 768], [96, 768], [192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768], [768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [1440, 768], [1536, 768], [1632, 768], [1728, 768], [1824, 768], [0, 864], [96, 864], [192, 864], [288, 864], [384, 864], [480, 864]] + "frames": [[1632, 672], [1728, 672], [1824, 672], [1920, 672], [0, 768], [96, 768], [192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768], [768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [1440, 768], [1536, 768], [1632, 768], [1728, 768], [1824, 768], [1920, 768], [0, 864], [96, 864], [192, 864], [288, 864], [384, 864], [480, 864], [576, 864], [672, 864], [768, 864], [864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864]] }, "pound_ground": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[576, 864], [672, 864], [768, 864], [864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864], [1440, 864], [1536, 864], [1632, 864], [1728, 864], [1824, 864], [0, 960], [96, 960], [192, 960], [288, 960], [384, 960], [480, 960], [576, 960]] + "frames": [[1440, 864], [1536, 864], [1632, 864], [1728, 864], [1824, 864], [1920, 864], [0, 960], [96, 960], [192, 960], [288, 960], [384, 960], [480, 960], [576, 960], [672, 960], [768, 960], [864, 960], [960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960]] }, "q5": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[672, 960], [768, 960], [864, 960], [960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960], [1440, 960], [1536, 960], [1632, 960], [1728, 960], [1824, 960], [0, 1056], [96, 1056], [192, 1056], [288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056], [768, 1056], [864, 1056], [960, 1056]] + "frames": [[1440, 960], [1536, 960], [1632, 960], [1728, 960], [1824, 960], [1920, 960], [0, 1056], [96, 1056], [192, 1056], [288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056], [768, 1056], [864, 1056], [960, 1056], [1056, 1056], [1152, 1056], [1248, 1056], [1344, 1056], [1440, 1056], [1536, 1056], [1632, 1056]] }, "scary_numbers": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1056, 1056], [1152, 1056], [1248, 1056], [1344, 1056], [1440, 1056], [1536, 1056], [1632, 1056], [1728, 1056], [1824, 1056], [0, 1152], [96, 1152], [192, 1152], [288, 1152], [384, 1152], [480, 1152], [576, 1152], [672, 1152], [768, 1152], [864, 1152], [960, 1152], [1056, 1152], [1152, 1152], [1248, 1152], [1344, 1152]] + "frames": [[1728, 1056], [1824, 1056], [1920, 1056], [0, 1152], [96, 1152], [192, 1152], [288, 1152], [384, 1152], [480, 1152], [576, 1152], [672, 1152], [768, 1152], [864, 1152], [960, 1152], [1056, 1152], [1152, 1152], [1248, 1152], [1344, 1152], [1440, 1152], [1536, 1152], [1632, 1152], [1728, 1152], [1824, 1152], [1920, 1152]] }, "sneak_attack": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1440, 1152], [1536, 1152], [1632, 1152], [1728, 1152], [1824, 1152], [0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248], [480, 1248], [576, 1248], [672, 1248], [768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248]] + "frames": [[0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248], [480, 1248], [576, 1248], [672, 1248], [768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248], [1248, 1248], [1344, 1248], [1440, 1248], [1536, 1248], [1632, 1248]] }, "stat_boost_enemy": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1248, 1248], [1344, 1248], [1440, 1248], [1536, 1248], [1632, 1248], [1728, 1248], [1824, 1248], [0, 1344], [96, 1344], [192, 1344], [288, 1344], [384, 1344], [480, 1344], [576, 1344]] + "frames": [[1728, 1248], [1824, 1248], [1920, 1248], [0, 1344], [96, 1344], [192, 1344], [288, 1344], [384, 1344], [480, 1344], [576, 1344], [672, 1344], [768, 1344], [864, 1344], [960, 1344]] }, "stat_debuff_enemy": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[672, 1344], [768, 1344], [864, 1344], [960, 1344], [1056, 1344], [1152, 1344], [1248, 1344], [1344, 1344], [1440, 1344], [1536, 1344], [1632, 1344], [1728, 1344], [1824, 1344], [0, 1440]] + "frames": [[1056, 1344], [1152, 1344], [1248, 1344], [1344, 1344], [1440, 1344], [1536, 1344], [1632, 1344], [1728, 1344], [1824, 1344], [1920, 1344], [0, 1440], [96, 1440], [192, 1440], [288, 1440]] + }, + "triple_think": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[384, 1440], [480, 1440], [576, 1440], [672, 1440], [768, 1440], [864, 1440], [960, 1440], [1056, 1440], [1152, 1440], [1248, 1440]] + }, + "unexpected_carrot": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[1344, 1440], [1440, 1440], [1536, 1440], [1632, 1440], [1728, 1440], [1824, 1440], [1920, 1440], [0, 1536], [96, 1536], [192, 1536], [288, 1536], [384, 1536], [480, 1536], [576, 1536], [672, 1536], [768, 1536]] }, "vital_siphon": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[96, 1440], [192, 1440], [288, 1440], [384, 1440], [480, 1440], [576, 1440], [672, 1440], [768, 1440], [864, 1440], [960, 1440], [1056, 1440], [1152, 1440], [1248, 1440], [1344, 1440], [1440, 1440], [1536, 1440], [1632, 1440], [1728, 1440], [1824, 1440], [0, 1536], [96, 1536], [192, 1536], [288, 1536], [384, 1536]] + "frames": [[864, 1536], [960, 1536], [1056, 1536], [1152, 1536], [1248, 1536], [1344, 1536], [1440, 1536], [1536, 1536], [1632, 1536], [1728, 1536], [1824, 1536], [1920, 1536], [0, 1632], [96, 1632], [192, 1632], [288, 1632], [384, 1632], [480, 1632], [576, 1632], [672, 1632], [768, 1632], [864, 1632], [960, 1632], [1056, 1632]] }, "gachachacha_bunnies": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[480, 1536], [576, 1536], [672, 1536], [672, 1536], [768, 1536], [864, 1536], [480, 1536], [480, 1536], [480, 1536], [960, 1536], [1056, 1536], [1152, 1536], [1248, 1536], [960, 1536], [1344, 1536], [1632, 1536], [0, 1632], [288, 1632], [576, 1632], [864, 1632], [1152, 1632], [1440, 1632], [1728, 1632], [96, 1728], [384, 1728], [384, 1728], [384, 1728]] + "frames": [[1152, 1632], [1248, 1632], [1344, 1632], [1344, 1632], [1440, 1632], [1536, 1632], [1152, 1632], [1152, 1632], [1152, 1632], [1632, 1632], [1728, 1632], [1824, 1632], [1920, 1632], [1632, 1632], [0, 1728], [288, 1728], [576, 1728], [864, 1728], [1152, 1728], [1440, 1728], [1728, 1728], [0, 1824], [288, 1824], [576, 1824], [864, 1824], [864, 1824], [864, 1824]] }, "gachachacha_carrots": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[480, 1536], [576, 1536], [672, 1536], [672, 1536], [768, 1536], [864, 1536], [480, 1536], [480, 1536], [480, 1536], [960, 1536], [1056, 1536], [1152, 1536], [1248, 1536], [960, 1536], [1440, 1536], [1728, 1536], [96, 1632], [384, 1632], [672, 1632], [960, 1632], [1248, 1632], [1536, 1632], [1824, 1632], [192, 1728], [384, 1728], [384, 1728], [384, 1728]] + "frames": [[1152, 1632], [1248, 1632], [1344, 1632], [1344, 1632], [1440, 1632], [1536, 1632], [1152, 1632], [1152, 1632], [1152, 1632], [1632, 1632], [1728, 1632], [1824, 1632], [1920, 1632], [1632, 1632], [96, 1728], [384, 1728], [672, 1728], [960, 1728], [1248, 1728], [1536, 1728], [1824, 1728], [96, 1824], [384, 1824], [672, 1824], [864, 1824], [864, 1824], [864, 1824]] }, "gachachacha_skulls": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[480, 1536], [576, 1536], [672, 1536], [672, 1536], [768, 1536], [864, 1536], [480, 1536], [480, 1536], [480, 1536], [960, 1536], [1056, 1536], [1152, 1536], [1248, 1536], [960, 1536], [1536, 1536], [1824, 1536], [192, 1632], [480, 1632], [768, 1632], [1056, 1632], [1344, 1632], [1632, 1632], [0, 1728], [288, 1728], [384, 1728], [384, 1728], [384, 1728]] + "frames": [[1152, 1632], [1248, 1632], [1344, 1632], [1344, 1632], [1440, 1632], [1536, 1632], [1152, 1632], [1152, 1632], [1152, 1632], [1632, 1632], [1728, 1632], [1824, 1632], [1920, 1632], [1632, 1632], [192, 1728], [480, 1728], [768, 1728], [1056, 1728], [1344, 1728], [1632, 1728], [1920, 1728], [192, 1824], [480, 1824], [768, 1824], [864, 1824], [864, 1824], [864, 1824]] } } \ No newline at end of file diff --git a/drool/imgs/attacks/attack_spritesheet.png b/drool/imgs/attacks/attack_spritesheet.png index 28051ea505ad41bcd1319cd9077644fab330c344..0963d04460bc42188687199edc37e1ea122699ef 100644 GIT binary patch literal 146015 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

l3h<$a#N6|K6hl7~8A^SUhPZ&K1@*M{(+eNlV!* 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 z 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 UTMyLBmk@uhT(=@^+0LW?54c7xEdT%j literal 131657 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�R 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%0D{)&o4sp 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 diff --git a/drool/imgs/attacks/loop.gif b/drool/imgs/attacks/loop.gif new file mode 100644 index 0000000000000000000000000000000000000000..01f3469392aa112a7080a33d35b56c74980bebbb GIT binary patch literal 3299 zcmchZc{tm79>;$Yal{qGQFSB|{18W7bvIffB8(%7-PT>>?6_uXy1T7VMX0*1qZ-k= zm7t1gx2dCoP^Q|hwx&iewU2#dY!%y){h?2pXPY=?&ayIzfA-Kzw%%OQl2g}-(yZ*E@sB2a1rP= z?1rc$5)c4{?N7FU3rMg4fuuUQgLyAQLeby^x1)A23x%Upli94{TvhX!=UjH($Ysni zhPqqlwb5eGxB0|q=k@V&)9|rsx32n$D(mE>=c8Q>53Uk3kQ$Wk#_2k8vBAl)?xu$g zUUif!l$*_uS^~Oa=EiQeJZTRZWH{(EsxuvtFn$h%KNq}l(@%&2Mhr% zKtTu=UjQLNSZUu;u8RhlfKw(4w-vrPRXdc6G;|!|vYCT$aa)F{qz~S;22nP8#+-VnFzC7PX?#qA|iWS5`Z3{3m}EyqV70ac|p&RkXMEr zg^Iu>ailmhKbW4Yg7u+Bb%Z(PsW_O~PZesD3$^8J32B|D+|prT{V8rqAyg6b=*PD! zk|QT-9OOhr_khG8AVuJS5J3JrKwk6o^Wuui_|*CNfh-B^(G)NdJIsKagjw{mUy?3q zIfcb79;)Lq@b237IYqCX%jJBVOWv`u6ZuwtHwa!ty$4ry&LfR5%tcbI3!;&#hT6|& z4$%oyP&7d9#`EfF)6E5IL5N#$^cVFwRu zE=rh13>V@&YMh&rPK}kR2M;)B|HR>4v^wx6COP4L<4n zJHQCKQXN262zMTz0G-ki3I?>)j?h6k3Ts>qGFgLJ^5(X!;5_SIp13{o75I}Dw+NJs zp;jnpkC$VE2TDf2*YPMh^i>(JX_W;5(#!TMGlPggp?Z?MON6-xhsLvg2&61S3a3jT z@@ri#N*Qg{alLWQm*s4fr?{wVBl)uKmG=6~Q%)BQ{d>>}rjfh~f$7ud6~zq?_6<=h z2u9eN-`N%0{Rrs^*dC&i;>OjzE43~c#7z{c8@S zPl}D%8(x(lW?`r)`7I9yOW&+MK;ul6x$Lj`!w{CPQ1`YwC<^w#1}{p=7+0(Ee;i~e z>zMIC6wWnE%3Sl~IzNpLL%9~=1DQHw`G(=Lar#}lPL)=%3%mmlb-t*uJAV#Mq=9ZM zSe~rT{y~9&*!%%iA!_oL2%hr-56)!oiTG<9MV%qaR?;0&&rtF7qmR)0H$#Ok;rA3 zCRMc~s4q>j)xqmS0_}Fb)X_-w4KRqEs25K9Zl%rVj_qn^eDvCCR1xcLy|nb1)+aOW z9hfRwm*!?R&Mr;JU2d5e&I@T*XjAY-V6EP%5RX~4hE`aIX-(ue&N+VLa1@L zw!(dCA||qL6$o!}W!GW|Jp=K-x48C4WNGwFy^C=vy$Dfh6z9p`XWG8mSn|kyY(nOo;~SL3u1C#rY{nVO>wjGi9QC_S zxk*(>d^}QWuJS%EWz~;oDN^hiUrAWHYUk3v_0O|Lympt%~|kmOGT`haoeWF?97of`H?cJYrp$* zu=@`dmpZ>b-CrE|{Z`*Wc6^7ft88$yeSJjZgoa)z)ef__rNp2W;0LG((XcVQBbUj6 zi)|-jQ|!RSokT3Za4bhfFS4~U0kl%oHr!2d|(&-#Zjfa`A%@X@~v_|>0|^ZyF?8ty%< Tvt3aSn@|3yfRBfUzumt8#PMLm literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/loop.png b/drool/imgs/attacks/loop.png new file mode 100644 index 0000000000000000000000000000000000000000..34be7b342fccf93cd4af86e76e700b7ab2bbe2f8 GIT binary patch literal 4321 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]Rt`XJ*~>f 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 P16KdrsQ&-js2=?n7b@+m literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/triple_think.png b/drool/imgs/attacks/triple_think.png new file mode 100644 index 0000000000000000000000000000000000000000..d950b246a24c4de90687e67d2a9547e24ce4086d GIT binary patch literal 6682 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/unexpected_carrot.png b/drool/imgs/attacks/unexpected_carrot.png new file mode 100644 index 0000000000000000000000000000000000000000..4767a23468778cacd7500e50dbe1c8f909c8d5b2 GIT binary patch literal 9220 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 Date: Tue, 14 Jul 2026 20:15:11 -0700 Subject: [PATCH 02/41] engine fix --- src/Engine.sol | 33 +++++++++---- test/mons/XmonTest.sol | 103 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 9 deletions(-) diff --git a/src/Engine.sol b/src/Engine.sol index ad10bd99..37acfeb4 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -1366,13 +1366,14 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Stamina regen decision is encapsulated in _inlineStaminaRegen, which reads the move FRESH // — required because effects (SleepStatus) can rewrite the move to a resting NO_OP mid-turn. if (inlineStaminaRegen) { - _inlineStaminaRegen( + playerSwitchForTurnFlag = _inlineStaminaRegen( config, EffectStep.AfterMove, priorityPlayerIndex, _unpackActiveMonIndex(battle.activeMonIndex, priorityPlayerIndex), 0, - 0 + 0, + playerSwitchForTurnFlag ); } @@ -1433,13 +1434,14 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } if (inlineStaminaRegen) { - _inlineStaminaRegen( + playerSwitchForTurnFlag = _inlineStaminaRegen( config, EffectStep.AfterMove, otherPlayerIndex, _unpackActiveMonIndex(battle.activeMonIndex, otherPlayerIndex), 0, - 0 + 0, + playerSwitchForTurnFlag ); } @@ -1491,7 +1493,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (inlineStaminaRegen) { uint256 p0Mon = _unpackActiveMonIndex(battle.activeMonIndex, 0); uint256 p1Mon = _unpackActiveMonIndex(battle.activeMonIndex, 1); - _inlineStaminaRegen(config, EffectStep.RoundEnd, 0, 0, p0Mon, p1Mon); + playerSwitchForTurnFlag = + _inlineStaminaRegen(config, EffectStep.RoundEnd, 0, 0, p0Mon, p1Mon, playerSwitchForTurnFlag); } } @@ -4482,11 +4485,13 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 playerIndex, uint256 monIndex, uint256 p0ActiveMonIndex, - uint256 p1ActiveMonIndex - ) private { + uint256 p1ActiveMonIndex, + uint256 prevPlayerSwitchForTurnFlag + ) private returns (uint256 playerSwitchForTurnFlag) { + playerSwitchForTurnFlag = prevPlayerSwitchForTurnFlag; if (round == EffectStep.RoundEnd) { if (!StaminaRegenLogic._shouldRegenOnRoundEnd(battleData[battleKeyForWrite].playerSwitchForTurnFlag)) { - return; + return playerSwitchForTurnFlag; } _inlineRegenStaminaForMon(config, 0, p0ActiveMonIndex); _inlineRegenStaminaForMon(config, 1, p1ActiveMonIndex); @@ -4494,10 +4499,20 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Fetch packedMoveIndex via helper - resolves to transient during executeWithMoves, storage otherwise. uint8 packedMoveIndex = _getCurrentTurnMove(config, playerIndex).packedMoveIndex; if (!StaminaRegenLogic._isRestingMove(packedMoveIndex)) { - return; + return playerSwitchForTurnFlag; } _inlineRegenStaminaForMon(config, playerIndex, monIndex); } + + // A regen tick fires OnUpdateMonState, which an effect (e.g. Somniphobia's nightmare) can + // turn into dealDamage and KO the mon that just gained stamina. Mirror the koOccurredFlag + // handling in _handleMove / _handleEffects: without it, a KO landing on the round-end regen + // (the last thing in the turn) is never observed, so playerSwitchForTurnFlag stays 2 and the + // engine wrongly runs the next turn as a normal both-sides-act turn instead of a forced switch. + if (koOccurredFlag != 0) { + koOccurredFlag = 0; + (playerSwitchForTurnFlag,) = _checkForGameOverOrKO(config, battleData[battleKeyForWrite], 2); + } } /// @dev Mirrors the storage write that StaminaRegenLogic used to do, then fires diff --git a/test/mons/XmonTest.sol b/test/mons/XmonTest.sol index 3946ecf9..7d481b2e 100644 --- a/test/mons/XmonTest.sol +++ b/test/mons/XmonTest.sol @@ -309,6 +309,109 @@ contract XmonTest is Test, BattleHelper { ); } + // Regression: a KO delivered by Somniphobia's nightmare tick, fired off the inline round-end + // stamina regen (INLINE_STAMINA_REGEN_RULESET is production's default path), must still set + // playerSwitchForTurnFlag for the next turn. _handleMove / _handleEffects recompute the flag via + // _checkForGameOverOrKO whenever koOccurredFlag is set; _inlineStaminaRegen must do the same, + // because _inlineRegenStaminaForMon's OnUpdateMonState hook can reach dealDamage (exactly what + // the nightmare effect does). When the killing tick is the round-end regen -- the last thing in + // the turn -- nothing downstream observes the KO, so pre-fix the flag stayed at 2 and the engine + // wrongly ran the next turn as a normal two-sided turn instead of a one-sided forced switch. + function test_somniphobiaInlineRegenKOSetsForcedSwitchFlag() public { + Somniphobia somniphobia = new Somniphobia(); + + uint32 maxHp = uint32(somniphobia.DAMAGE_DENOM()) * 10; // 80 HP -> 10 dmg/tick at 1 stack + + // Chips Bob down to 5 HP (below one 10-dmg nightmare tick) but does NOT KO on its own, + // so the killing blow is unambiguously the regen-triggered nightmare tick. + StandardAttack bigAttack = attackFactory.createAttack( + ATTACK_PARAMS({ + BASE_POWER: maxHp - 15, + STAMINA_COST: 1, + ACCURACY: 100, + PRIORITY: DEFAULT_PRIORITY, + MOVE_TYPE: Type.Cosmic, + EFFECT_ACCURACY: 0, + MOVE_CLASS: MoveClass.Physical, + CRIT_RATE: 0, + VOLATILITY: 0, + NAME: "Big Attack", + EFFECT: IEffect(address(0)) + }) + ); + StandardAttack staminaBurn = _staminaBurn(4); + + uint256[] memory aliceMoves = new uint256[](2); + aliceMoves[0] = uint256(uint160(address(somniphobia))); + aliceMoves[1] = uint256(uint160(address(bigAttack))); + + uint256[] memory bobMoves = new uint256[](1); + bobMoves[0] = uint256(uint160(address(staminaBurn))); + + Mon memory aliceMon = _createMon(); + aliceMon.moves = aliceMoves; + aliceMon.stats.hp = maxHp; + aliceMon.stats.stamina = 10; + aliceMon.stats.speed = 2; + + Mon memory bobMon = _createMon(); + bobMon.moves = bobMoves; + bobMon.stats.hp = maxHp; + bobMon.stats.stamina = 10; + bobMon.stats.speed = 1; + + Mon[] memory aliceTeam = new Mon[](1); + aliceTeam[0] = aliceMon; + // Bob gets a second mon so the forced switch this bug should trigger is meaningful. + Mon[] memory bobTeam = new Mon[](2); + bobTeam[0] = bobMon; + bobTeam[1] = bobMon; + + defaultRegistry.setTeam(ALICE, aliceTeam); + defaultRegistry.setTeam(BOB, bobTeam); + + bytes32 battleKey = _startBattle( + engine, + mockOracle, + defaultRegistry, + matchmaker, + new IEngineHook[](0), + IRuleset(INLINE_STAMINA_REGEN_RULESET), + address(commitManager) + ); + + // Both players select their first mon + _commitRevealExecuteForAliceAndBob( + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) + ); + + // Turn 1: Alice casts Somniphobia; Bob burns 4 stamina so its later rest regens it back. + // The round-end regen tick already lands one nightmare hit (Bob -> 70 HP). + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); + assertEq( + engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), 0, "Bob alive after turn 1" + ); + + // Turn 2: Alice chips Bob to 5 HP; Bob burns stamina (does NOT rest, so its only regen is + // the round-end top-up -- the LAST inline-regen call in the turn, after every _handleEffects + // pass). That regen fires Somniphobia's nightmare tick (10 dmg), the killing blow: a KO that + // originates inside _inlineStaminaRegen with nothing running afterwards to notice it. + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, 0, 0, 0); + + assertEq( + engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), + 1, + "Bob's active mon should be KOed by the regen-triggered nightmare tick" + ); + + // The bug: Bob's active mon just died, so the next turn must be a one-sided forced switch + // for Bob (playerSwitchForTurnFlag == 1). Because the KO happened inside _inlineStaminaRegen + // -- whose call sites never re-check koOccurredFlag / call _checkForGameOverOrKO -- the flag + // is left at 2, so the engine wrongly runs the next turn as a normal both-sides-act turn. + (, BattleData memory state) = engine.getBattle(battleKey); + assertEq(state.playerSwitchForTurnFlag, 1, "Bob should be forced to switch after the nightmare KO"); + } + // Re-casting Somniphobia raises the stack, scaling the per-gain damage (1/8 max HP per stack). function test_somniphobiaStacks() public { Somniphobia somniphobia = new Somniphobia(); From b89651d0b5321c3a0031fa3871e3f1d67c43e675 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:26:51 -0700 Subject: [PATCH 03/41] fix engine test --- test/EngineTest.sol | 74 +++++++++++++++++++++ test/mocks/StaminaGainKOEffect.sol | 41 ++++++++++++ test/mons/XmonTest.sol | 103 ----------------------------- 3 files changed, 115 insertions(+), 103 deletions(-) create mode 100644 test/mocks/StaminaGainKOEffect.sol diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 21a73a56..3ad09c23 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -36,6 +36,7 @@ import {IEngineHook} from "../src/IEngineHook.sol"; import {OneTurnStatBoost} from "./mocks/OneTurnStatBoost.sol"; import {SingleInstanceEffect} from "./mocks/SingleInstanceEffect.sol"; import {SkipTurnMove} from "./mocks/SkipTurnMove.sol"; +import {StaminaGainKOEffect} from "./mocks/StaminaGainKOEffect.sol"; import {TempStatBoostEffect} from "./mocks/TempStatBoostEffect.sol"; import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; @@ -2733,6 +2734,79 @@ contract EngineTest is Test, BattleHelper { assertEq(effects[0].data, bytes32(uint256(69))); } + // Regression: a KO delivered by an effect's OnUpdateMonState -> dealDamage hook, fired off the + // inline round-end stamina regen (INLINE_STAMINA_REGEN_RULESET is production's default path), must + // still set playerSwitchForTurnFlag for the next turn. _handleMove / _handleEffects recompute the + // flag via _checkForGameOverOrKO whenever koOccurredFlag is set; _inlineStaminaRegen must do the + // same, because a regen tick's OnUpdateMonState listener can reach dealDamage. When the killing + // tick is the round-end regen -- the last thing in the turn -- nothing downstream observes the KO, + // so pre-fix the flag stayed at 2 and the engine wrongly ran the next turn as a normal two-sided turn. + function test_inlineRegenKOSetsForcedSwitchFlag() public { + // Bob's mon carries a KO-on-stamina-gain effect via its ability; the effect's OnUpdateMonState + // hook deals lethal damage on any positive stamina gain -- a mon-agnostic stand-in for an + // effect (e.g. a nightmare tick) that reaches dealDamage from a regen. + StaminaGainKOEffect koOnStaminaGain = new StaminaGainKOEffect(); + IAbility koAbility = new EffectAbility(koOnStaminaGain); + + // Non-resting stamina burn: costs stamina (so a round-end regen has something to top up) but + // deals no damage, so the killing blow is unambiguously the regen-triggered effect. + IMoveSet staminaBurn = new CustomAttack( + typeCalc, CustomAttack.Args({TYPE: Type.Liquid, BASE_POWER: 0, ACCURACY: 100, STAMINA_COST: 2, PRIORITY: 0}) + ); + uint256[] memory burnMoves = new uint256[](1); + burnMoves[0] = uint256(uint160(address(staminaBurn))); + + Mon memory aliceMon = _createMon(); + aliceMon.moves = burnMoves; + aliceMon.stats.hp = 100; + aliceMon.stats.speed = 2; // faster, so move order is deterministic + + Mon memory bobMon = _createMon(); + bobMon.moves = burnMoves; + bobMon.ability = uint160(address(koAbility)); + bobMon.stats.hp = 100; + bobMon.stats.speed = 1; + + Mon[] memory aliceTeam = new Mon[](1); + aliceTeam[0] = aliceMon; + // Bob gets a second mon so the KO forces a switch rather than ending the game. + Mon[] memory bobTeam = new Mon[](2); + bobTeam[0] = bobMon; + bobTeam[1] = bobMon; + + defaultRegistry.setTeam(ALICE, aliceTeam); + defaultRegistry.setTeam(BOB, bobTeam); + + bytes32 battleKey = _startBattle( + engine, + defaultOracle, + defaultRegistry, + matchmaker, + new IEngineHook[](0), + IRuleset(INLINE_STAMINA_REGEN_RULESET), + address(commitManager) + ); + + // Both select mon 0; Bob's ability registers the KO-on-stamina-gain effect on switch-in. + _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); + + // Alice rests; Bob burns stamina (does NOT rest, so its only regen is the round-end top-up -- + // the LAST inline-regen call in the turn). That regen fires the KO effect, the killing blow: a + // KO that originates inside _inlineStaminaRegen with nothing running afterwards to notice it. + _commitRevealExecuteForAliceAndBob(battleKey, NO_OP_MOVE_INDEX, 0, 0, 0); + + assertEq( + engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), + 1, + "Bob's active mon should be KOed by the regen-triggered effect" + ); + + // Bob's active mon just died inside _inlineStaminaRegen, so the next turn must be a one-sided + // forced switch for Bob (playerSwitchForTurnFlag == 1). Pre-fix the flag was left at 2. + (, BattleData memory state) = engine.getBattle(battleKey); + assertEq(state.playerSwitchForTurnFlag, 1, "Bob should be forced to switch after the regen KO"); + } + function test_maxBattleDuration() public { Mon memory mon = _createMon(); mon.moves = new uint256[](0); diff --git a/test/mocks/StaminaGainKOEffect.sol b/test/mocks/StaminaGainKOEffect.sol new file mode 100644 index 00000000..00f06782 --- /dev/null +++ b/test/mocks/StaminaGainKOEffect.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../src/Enums.sol"; +import "../../src/Structs.sol"; + +import {IEngine} from "../../src/IEngine.sol"; +import {BasicEffect} from "../../src/effects/BasicEffect.sol"; + +/// @notice On any positive stamina gain, deals lethal damage to the mon that gained it. A generic, +/// mon-agnostic stand-in for effects whose OnUpdateMonState hook reaches dealDamage and can therefore +/// KO a mon off an inline stamina-regen tick. +contract StaminaGainKOEffect is BasicEffect { + function name() external pure override returns (string memory) { + return "Stamina Gain KO"; + } + + // Steps: OnUpdateMonState + function getStepsBitmap() external pure override returns (uint16) { + return 0x100; + } + + function onUpdateMonState( + IEngine engine, + bytes32 battleKey, + uint256, + bytes32 extraData, + uint256 playerIndex, + uint256 monIndex, + uint256, + MonStateIndexName stateVarIndex, + int32 valueToAdd + ) external override returns (bytes32, bool) { + if (stateVarIndex == MonStateIndexName.Stamina && valueToAdd > 0) { + uint32 maxHp = engine.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + engine.dealDamage(playerIndex, monIndex, int32(uint32(maxHp))); + } + return (extraData, false); + } +} diff --git a/test/mons/XmonTest.sol b/test/mons/XmonTest.sol index 7d481b2e..3946ecf9 100644 --- a/test/mons/XmonTest.sol +++ b/test/mons/XmonTest.sol @@ -309,109 +309,6 @@ contract XmonTest is Test, BattleHelper { ); } - // Regression: a KO delivered by Somniphobia's nightmare tick, fired off the inline round-end - // stamina regen (INLINE_STAMINA_REGEN_RULESET is production's default path), must still set - // playerSwitchForTurnFlag for the next turn. _handleMove / _handleEffects recompute the flag via - // _checkForGameOverOrKO whenever koOccurredFlag is set; _inlineStaminaRegen must do the same, - // because _inlineRegenStaminaForMon's OnUpdateMonState hook can reach dealDamage (exactly what - // the nightmare effect does). When the killing tick is the round-end regen -- the last thing in - // the turn -- nothing downstream observes the KO, so pre-fix the flag stayed at 2 and the engine - // wrongly ran the next turn as a normal two-sided turn instead of a one-sided forced switch. - function test_somniphobiaInlineRegenKOSetsForcedSwitchFlag() public { - Somniphobia somniphobia = new Somniphobia(); - - uint32 maxHp = uint32(somniphobia.DAMAGE_DENOM()) * 10; // 80 HP -> 10 dmg/tick at 1 stack - - // Chips Bob down to 5 HP (below one 10-dmg nightmare tick) but does NOT KO on its own, - // so the killing blow is unambiguously the regen-triggered nightmare tick. - StandardAttack bigAttack = attackFactory.createAttack( - ATTACK_PARAMS({ - BASE_POWER: maxHp - 15, - STAMINA_COST: 1, - ACCURACY: 100, - PRIORITY: DEFAULT_PRIORITY, - MOVE_TYPE: Type.Cosmic, - EFFECT_ACCURACY: 0, - MOVE_CLASS: MoveClass.Physical, - CRIT_RATE: 0, - VOLATILITY: 0, - NAME: "Big Attack", - EFFECT: IEffect(address(0)) - }) - ); - StandardAttack staminaBurn = _staminaBurn(4); - - uint256[] memory aliceMoves = new uint256[](2); - aliceMoves[0] = uint256(uint160(address(somniphobia))); - aliceMoves[1] = uint256(uint160(address(bigAttack))); - - uint256[] memory bobMoves = new uint256[](1); - bobMoves[0] = uint256(uint160(address(staminaBurn))); - - Mon memory aliceMon = _createMon(); - aliceMon.moves = aliceMoves; - aliceMon.stats.hp = maxHp; - aliceMon.stats.stamina = 10; - aliceMon.stats.speed = 2; - - Mon memory bobMon = _createMon(); - bobMon.moves = bobMoves; - bobMon.stats.hp = maxHp; - bobMon.stats.stamina = 10; - bobMon.stats.speed = 1; - - Mon[] memory aliceTeam = new Mon[](1); - aliceTeam[0] = aliceMon; - // Bob gets a second mon so the forced switch this bug should trigger is meaningful. - Mon[] memory bobTeam = new Mon[](2); - bobTeam[0] = bobMon; - bobTeam[1] = bobMon; - - defaultRegistry.setTeam(ALICE, aliceTeam); - defaultRegistry.setTeam(BOB, bobTeam); - - bytes32 battleKey = _startBattle( - engine, - mockOracle, - defaultRegistry, - matchmaker, - new IEngineHook[](0), - IRuleset(INLINE_STAMINA_REGEN_RULESET), - address(commitManager) - ); - - // Both players select their first mon - _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) - ); - - // Turn 1: Alice casts Somniphobia; Bob burns 4 stamina so its later rest regens it back. - // The round-end regen tick already lands one nightmare hit (Bob -> 70 HP). - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); - assertEq( - engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), 0, "Bob alive after turn 1" - ); - - // Turn 2: Alice chips Bob to 5 HP; Bob burns stamina (does NOT rest, so its only regen is - // the round-end top-up -- the LAST inline-regen call in the turn, after every _handleEffects - // pass). That regen fires Somniphobia's nightmare tick (10 dmg), the killing blow: a KO that - // originates inside _inlineStaminaRegen with nothing running afterwards to notice it. - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, 0, 0, 0); - - assertEq( - engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), - 1, - "Bob's active mon should be KOed by the regen-triggered nightmare tick" - ); - - // The bug: Bob's active mon just died, so the next turn must be a one-sided forced switch - // for Bob (playerSwitchForTurnFlag == 1). Because the KO happened inside _inlineStaminaRegen - // -- whose call sites never re-check koOccurredFlag / call _checkForGameOverOrKO -- the flag - // is left at 2, so the engine wrongly runs the next turn as a normal both-sides-act turn. - (, BattleData memory state) = engine.getBattle(battleKey); - assertEq(state.playerSwitchForTurnFlag, 1, "Bob should be forced to switch after the nightmare KO"); - } - // Re-casting Somniphobia raises the stack, scaling the per-gain damage (1/8 max HP per stack). function test_somniphobiaStacks() public { Somniphobia somniphobia = new Somniphobia(); From 30b3e1b19ebf9b243bc3d038762218b352b72e78 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:02:19 -0700 Subject: [PATCH 04/41] clean up certain moves --- docs/stomp_2.md | 51 +++++- src/effects/status/BurnStatus.sol | 19 +- src/mons/ekineki/SneakAttack.sol | 8 +- src/mons/embursa/HeatBeacon.sol | 5 +- src/mons/embursa/Tinderclaws.sol | 10 +- src/mons/inutia/ChainExpansion.sol | 10 +- src/mons/inutia/Interweaving.sol | 14 +- src/mons/pengym/PostWorkout.sol | 12 +- test/MainnetReplayGasTest.t.sol | 268 +++++++++++++++++++++++++++++ test/mons/IblivionTest.sol | 6 +- 10 files changed, 342 insertions(+), 61 deletions(-) create mode 100644 test/MainnetReplayGasTest.t.sol diff --git a/docs/stomp_2.md b/docs/stomp_2.md index e4c6f26a..230314df 100644 --- a/docs/stomp_2.md +++ b/docs/stomp_2.md @@ -1,4 +1,43 @@ -Remember when we dreamed of fully on-chain battles with collectibles. +ONCHAIN IS FUN NOW + +Prior reading: Make Onchain Fun Again + +TLDR: Stomp.gg is fully live, and there is a A LOT of content, you should try it out, and it all costs under $0.01 in ETH. + +I genuinely believe Stomp is one of the most interesting, complex, and fun experiences you can have onchain today. If you want to see what's possible with ultra fast blocks, ultra low fees, and ultra insane overengineering, I urge you to try it out. Now, the more perceptive of you in the audience may worry that I may not be entirely biased. Not to worry. + +Here is what is ALREADY LIVE TODAY, THAT YOU CAN TRY OUT, ENTIRELY ONCHAIN: + +First, the core game loop. + +Stomp is a turn based game 2 player game. It borrows heavy inspiration from Pokemon. Each turn, you choose a move, your opponent chooses a move, and then they resolve. You might do some damage, you might heal some damage, you might change which monster you have out. + +You can play a full game across a variety of CPU difficulties, with basically zero perceived latency. The game engine is transpiled to TypeScript (from Solidity!!!) so you get instant feedback. There's a rich and varied set of mechanics, and it all costs less than $0.01 in ETH! (Around 2-5 million gas for the nerds.) + +If you've played JRPGs or turn-based games, a lot of the mechanics may be familiar to you. There's a resource system, various types of damage (damage over time, delayed damage, elemental charts), and also a few unique ideas (some mons can resurrect, others can target any mon, etc. etc.) + +INSERT GAME GIF + +Okay, but playing against bots might be boring. What if you want to play against a friend? + +You can do that! Stomp lets you run 1v1 games against anyone! Play live against another friend, moves still settle onchain, and everyone else can spectate! + +INSERT 1v1 GAME GIF + + + +------- + + + + + + + + + + +Remember when we dreamed of fully on-chain battles with collectibles, expansive worlds, and It's live! @@ -10,11 +49,15 @@ It's live! Play live against another friend, moves settle onchain, everyone else Okay, but what if you're a huge friend group and want to play 2v2? -It's live! Even more strategy and depth. +It's live! Even more strategy and depth, probably lots of busted combos. Help me break the game! + +Okay, but what if you want to play co-op with just one other friend instead of PVP? + +That's live too! -Okay, but what if you want unlockables and more customization? +Okay, but what if you don't care about the game modes. You just want unlockables and customization to theory-craft? -It's live! 13 mons to choose from, all wholly unique, unlockable stat upgrades, new moves, it's all there! +It's live! 13 mons to choose from, all wholly unique, unlockable stat upgrades, unlockables moves, it's all there! Okay, but what if you only care if you and your friend can bet $10 on the outcome? diff --git a/src/effects/status/BurnStatus.sol b/src/effects/status/BurnStatus.sol index 34ab2ab4..f94c5f7e 100644 --- a/src/effects/status/BurnStatus.sol +++ b/src/effects/status/BurnStatus.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; -import {EffectInstance, StatBoostToApply} from "../../Structs.sol"; +import {StatBoostToApply} from "../../Structs.sol"; import {StatusEffect} from "./StatusEffect.sol"; import {StatusEffectLib} from "./StatusEffectLib.sol"; @@ -77,18 +77,11 @@ contract BurnStatus is StatusEffect { }); engine.addStatBoost(targetIndex, monIndex, statBoosts, StatBoostFlag.Perm); } else { - (EffectInstance[] memory effects, uint256[] memory indices) = - engine.getEffects(battleKey, targetIndex, monIndex); - uint256 indexOfBurnEffect; - uint256 burnDegree; - bytes32 newExtraData; - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == address(this)) { - indexOfBurnEffect = indices[i]; - burnDegree = uint256(effects[i].data) & 0xFF; - newExtraData = effects[i].data; - } - } + // Single burn per mon (one-status invariant), so the first by-address match is the one. + (, uint256 indexOfBurnEffect, bytes32 burnData) = + engine.getEffectData(battleKey, targetIndex, monIndex, address(this)); + uint256 burnDegree = uint256(burnData) & 0xFF; + bytes32 newExtraData = burnData; if (burnDegree < MAX_BURN_DEGREE) { newExtraData = bytes32((uint256(newExtraData) & ~uint256(0xFF)) | (burnDegree + 1)); } diff --git a/src/mons/ekineki/SneakAttack.sol b/src/mons/ekineki/SneakAttack.sol index ad616761..6c5f0f4e 100644 --- a/src/mons/ekineki/SneakAttack.sol +++ b/src/mons/ekineki/SneakAttack.sol @@ -40,11 +40,9 @@ contract SneakAttack is IMoveSet, BasicEffect { uint256 rng ) external { // Check if already used this switch-in (effect present = already used) - (EffectInstance[] memory effects,) = engine.getEffects(battleKey, attackerPlayerIndex, attackerMonIndex); - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == address(this)) { - return; - } + (bool alreadyUsed,,) = engine.getEffectData(battleKey, attackerPlayerIndex, attackerMonIndex, address(this)); + if (alreadyUsed) { + return; } uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; diff --git a/src/mons/embursa/HeatBeacon.sol b/src/mons/embursa/HeatBeacon.sol index 2302dd0a..4098bcfc 100644 --- a/src/mons/embursa/HeatBeacon.sol +++ b/src/mons/embursa/HeatBeacon.sol @@ -45,10 +45,7 @@ contract HeatBeacon is IMoveSet { } } - // Grant +1 priority to next turn's move (refresh if already set). - if (HeatBeaconLib._getPriorityBoost(engine, battleKey, attackerPlayerIndex) == 1) { - HeatBeaconLib._clearPriorityBoost(engine, attackerPlayerIndex); - } + // Grant +1 priority to next turn's move (idempotent refresh; consumed by the payoff move). HeatBeaconLib._setPriorityBoost(engine, attackerPlayerIndex); } diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index 83d27c85..7396dedc 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -122,13 +122,9 @@ contract Tinderclaws is IAbility, BasicEffect { } function _removeBurnIfPresent(IEngine engine, bytes32 battleKey, uint256 targetIndex, uint256 monIndex) internal { - (EffectInstance[] memory effects, uint256[] memory indices) = - engine.getEffects(battleKey, targetIndex, monIndex); - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == address(BURN_STATUS)) { - engine.removeEffect(targetIndex, monIndex, indices[i]); - return; - } + (bool exists, uint256 index,) = engine.getEffectData(battleKey, targetIndex, monIndex, address(BURN_STATUS)); + if (exists) { + engine.removeEffect(targetIndex, monIndex, index); } } } diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index 1c76f930..d7c94cfc 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -43,12 +43,10 @@ contract ChainExpansion is IMoveSet, BasicEffect { uint16, uint256 ) external { - // Check if the ability is already applied globally - (EffectInstance[] memory effects,) = engine.getEffects(battleKey, 2, 2); - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == address(this)) { - return; - } + // Skip if the ability is already applied globally (single instance by address) + (bool exists,,) = engine.getEffectData(battleKey, 2, 2, address(this)); + if (exists) { + return; } // Otherwise, add this effect globally engine.addEffect(2, attackerPlayerIndex, this, _encodeState(CHARGES, uint128(attackerPlayerIndex))); diff --git a/src/mons/inutia/Interweaving.sol b/src/mons/inutia/Interweaving.sol index 0c31f3c0..83016799 100644 --- a/src/mons/inutia/Interweaving.sol +++ b/src/mons/inutia/Interweaving.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import {EMPTY_ACTIVE_LANE, NO_SLOT} from "../../Constants.sol"; import "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; -import {EffectInstance, StatBoostToApply} from "../../Structs.sol"; +import {StatBoostToApply} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; @@ -37,15 +37,11 @@ contract Interweaving is IAbility, BasicEffect { }); engine.addStatBoost(otherPlayerIndex, otherPlayerActiveMonIndex, statBoosts, StatBoostFlag.Temp); - // Check if the effect has already been set for this mon - (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == address(this)) { - return; - } + // Skip if the switch-out effect is already registered on this mon + (bool exists,,) = engine.getEffectData(battleKey, playerIndex, monIndex, address(this)); + if (exists) { + return; } - // Otherwise, add this effect to the mon when it switches in - // This way we can trigger on switch out engine.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); } diff --git a/src/mons/pengym/PostWorkout.sol b/src/mons/pengym/PostWorkout.sol index 40548f95..8681837f 100644 --- a/src/mons/pengym/PostWorkout.sol +++ b/src/mons/pengym/PostWorkout.sol @@ -50,16 +50,8 @@ contract PostWorkout is IAbility, BasicEffect { if (statusAddress != 0) { IEffect statusEffect = IEffect(address(uint160(statusAddress))); - // Get the index of the effect and remove it - uint256 effectIndex; - (EffectInstance[] memory effects, uint256[] memory indices) = - engine.getEffects(battleKey, targetIndex, monIndex); - for (uint256 i; i < effects.length; i++) { - if (address(effects[i].effect) == address(statusEffect)) { - effectIndex = indices[i]; - break; - } - } + // Get the index of the status effect and remove it (one status per mon) + (, uint256 effectIndex,) = engine.getEffectData(battleKey, targetIndex, monIndex, address(statusEffect)); engine.removeEffect(targetIndex, monIndex, effectIndex); // Boost stamina by 1 diff --git a/test/MainnetReplayGasTest.t.sol b/test/MainnetReplayGasTest.t.sol new file mode 100644 index 00000000..1fa8613f --- /dev/null +++ b/test/MainnetReplayGasTest.t.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../src/Constants.sol"; +import "../src/Enums.sol"; +import "../src/Structs.sol"; +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; + +import {SetupMons} from "../script/SetupMons.s.sol"; +import {Engine} from "../src/Engine.sol"; +import {IEngine} from "../src/IEngine.sol"; +import {GachaTeamRegistry} from "../src/game-layer/GachaTeamRegistry.sol"; +import {IGachaRNG} from "../src/rng/IGachaRNG.sol"; + +import {Overclock} from "../src/effects/battlefield/Overclock.sol"; +import {BlessedStatus} from "../src/effects/status/BlessedStatus.sol"; +import {BurnStatus} from "../src/effects/status/BurnStatus.sol"; +import {FrostbiteStatus} from "../src/effects/status/FrostbiteStatus.sol"; +import {PanicStatus} from "../src/effects/status/PanicStatus.sol"; +import {SleepStatus} from "../src/effects/status/SleepStatus.sol"; +import {ZapStatus} from "../src/effects/status/ZapStatus.sol"; +import {TypeCalculator} from "../src/types/TypeCalculator.sol"; + +import {IEngineHook} from "../src/IEngineHook.sol"; +import {IRuleset} from "../src/IRuleset.sol"; +import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; +import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; +import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; +import {IRandomnessOracle} from "../src/rng/IRandomnessOracle.sol"; + +import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; + +/// @notice Bit-faithful replay of the first real mainnet game we captured — MegaETH tx +/// 0x5c5daa0edaaa79ac05a6915c36c393c03b172f0336a25917d96b811a5f1283e9 (CPU::executeGame, +/// 27 batched turns, receipt 4,875,113 gas). Both sides ran an Embursa burn kit, so this +/// exercises the burn apply/escalate + switch-guard paths that Tier A/B of the gas audit +/// target. Teams (ids + facet-adjusted stats) and the 27 packed turn entries were read +/// off-chain from the real config (getBattle) + tx calldata; the turn RNG is keccak(p0Salt, +/// p1Salt) with NO battleKey dependency, so replaying the same salts reproduces the exact +/// battle here. Measured through engine.executeBatchedTurns (the same path the tx took) on +/// a VIRGIN storageKey and again on a REUSED (steady-state) one. +contract MainnetReplayGasTest is Test, SetupMons { + uint256 constant P0_PK = 0xA11CE; + uint256 constant P1_PK = 0xB0B; + uint256 constant TX_BASE = 21000; + address p0; + address p1; + + Engine engine; + SignedCommitManager mgr; + SignedMatchmaker maker; + GachaTeamRegistry gachaReg; + TestTeamRegistry registry; + + // Real rosters from the mainnet config (getBattle). p1 is the CPU side. + uint256[4] P0_IDS = [uint256(1), 5, 7, 6]; // Inutia, Sofabbi, Embursa, Pengym + uint256[4] P1_IDS = [uint256(5), 7, 4, 7]; // Sofabbi, Embursa, Gorillax, Embursa + + function setUp() public { + p0 = vm.addr(P0_PK); + p1 = vm.addr(P1_PK); + _deployStack(); + registry.setTeam(p0, _buildTeam(P0_IDS, _p0Stats())); + registry.setTeam(p1, _buildTeam(P1_IDS, _p1Stats())); + } + + function _deployStack() internal { + engine = new Engine(4, 4); + TypeCalculator tc = new TypeCalculator(); + Overclock oc = new Overclock(); + SleepStatus sleepStatus = new SleepStatus(); + PanicStatus panicStatus = new PanicStatus(); + FrostbiteStatus frost = new FrostbiteStatus(); + BurnStatus burn = new BurnStatus(); + ZapStatus zap = new ZapStatus(); + BlessedStatus blessed = new BlessedStatus(); + gachaReg = + new GachaTeamRegistry(4, 4, IEngine(address(engine)), IGachaRNG(address(0)), GachaTeamRegistry(address(0))); + vm.setEnv("TYPE_CALCULATOR", vm.toString(address(tc))); + vm.setEnv("OVERCLOCK", vm.toString(address(oc))); + vm.setEnv("SLEEP_STATUS", vm.toString(address(sleepStatus))); + vm.setEnv("PANIC_STATUS", vm.toString(address(panicStatus))); + vm.setEnv("FROSTBITE_STATUS", vm.toString(address(frost))); + vm.setEnv("BURN_STATUS", vm.toString(address(burn))); + vm.setEnv("ZAP_STATUS", vm.toString(address(zap))); + vm.setEnv("BLESSED_STATUS", vm.toString(address(blessed))); + vm.setEnv("GACHA_TEAM_REGISTRY", vm.toString(address(gachaReg))); + deployGhouliath(gachaReg); + deployInutia(gachaReg); + deployMalalien(gachaReg); + deployIblivion(gachaReg); + deployGorillax(gachaReg); + deploySofabbi(gachaReg); + deployPengym(gachaReg); + deployEmbursa(gachaReg); + deployVolthare(gachaReg); + deployAurox(gachaReg); + deployXmon(gachaReg); + mgr = new SignedCommitManager(IEngine(address(engine))); + maker = new SignedMatchmaker(engine); + registry = new TestTeamRegistry(); + } + + function _buildTeam(uint256[4] memory ids, MonStats[4] memory logStats) internal view returns (Mon[] memory team) { + team = new Mon[](4); + for (uint256 i; i < 4; i++) { + (, uint256[] memory moves, uint256[] memory abilities) = gachaReg.getMonData(ids[i]); + team[i] = Mon({stats: logStats[i], ability: abilities.length > 0 ? abilities[0] : 0, moves: moves}); + } + } + + function _mk(uint32 hp, uint32 stam, uint32 spe, uint32 atk, uint32 def, uint32 spa, uint32 spd, Type t1, Type t2) + internal + pure + returns (MonStats memory) + { + return MonStats({ + hp: hp, + stamina: stam, + speed: spe, + attack: atk, + defense: def, + specialAttack: spa, + specialDefense: spd, + type1: t1, + type2: t2 + }); + } + + // Facet-adjusted stats straight from the mainnet config (getBattle). + function _p0Stats() internal pure returns (MonStats[4] memory s) { + s[0] = _mk(371, 5, 218, 179, 189, 183, 192, Type(9), Type(14)); + s[1] = _mk(333, 5, 167, 180, 211, 120, 282, Type(7), Type(14)); + s[2] = _mk(420, 5, 106, 141, 231, 190, 169, Type(4), Type(14)); + s[3] = _mk(371, 5, 156, 191, 191, 210, 172, Type(6), Type(14)); + } + + function _p1Stats() internal pure returns (MonStats[4] memory s) { + s[0] = _mk(333, 5, 167, 189, 201, 126, 269, Type(7), Type(14)); + s[1] = _mk(420, 5, 106, 148, 220, 199, 161, Type(4), Type(14)); + s[2] = _mk(407, 5, 123, 317, 175, 117, 176, Type(2), Type(14)); + s[3] = _mk(420, 5, 106, 148, 220, 199, 161, Type(4), Type(14)); + } + + // The 27 packed turn entries from the tx calldata (verbatim wire words: + // p0Move 8 | p0Extra 16 | p0Salt 104 | p1Move 8 | p1Extra 16 | p1Salt 104). + function _entries() internal pure returns (uint256[] memory e) { + e = new uint256[](27); + e[0] = 0x17def5ffbebed718e42ad8d5437b700007d; + e[1] = 0x17cc1b6f203a7394f49cc2cbe26000002; + e[2] = 0x1491cabc6f3f9f098a351da75a6000303; + e[3] = 0x2d351b8789b948ba4d526e455d0000001; + e[4] = 0x28ea2c7b0d748d1437c53a9d7cb00007e; + e[5] = 0x17e454fd4f7ab2d549f0ee2ab9000027d; + e[6] = 0x27dd5e77a28073a9e5ee44929aab3000001; + e[7] = 0x173a3d8262dc65be7dbf730c590000001; + e[8] = 0x1643e54852af9503f2e8077ebf700007e; + e[9] = 0x7ef094b6975e53293948e1203cc700037d; + e[10] = 0x31feeeddb68b4b17088085a1080000002; + e[11] = 0x339b37ca50816ee03f076911342000001; + e[12] = 0x3e201cd1cb941d52bc67fec615d00007e; + e[13] = 0x17d2888d001183e07bcc3f0a8e8e800017d; + e[14] = 0x37d0000000000000000000000000000007e; + e[15] = 0x1977f57d82dc43af2a2448c2f91000002; + e[16] = 0x1a18f414979e79dece646b3ed36000101; + e[17] = 0x2c444b8009fc7e49a768d6a26eb000003; + e[18] = 0x2ff3f0d7e0548d4c90746470428000003; + e[19] = 0x1a90ef652a7be14013561d2f88f00007e; + e[20] = 0x7e1c013c0ff1e95c94cef042b98a00007d; + e[21] = 0x22d9ec4faf7e71b4cbe49adbe55000002; + e[22] = 0x2a2c344c9bc853f445b3c8df0cc00007e; + e[23] = 0x7d0000000000000000000000000000007e; + e[24] = 0xe0eb713b94ee96ba68b8c238c3000001; + e[25] = 0x7e474f7f0d702737a8ad3337d43000037d; + e[26] = 0x53d475e14cb30342bbd9209f1f000002; + } + + function _startBattle() internal returns (bytes32 key) { + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(maker); + address[] memory makersToRemove = new address[](0); + vm.prank(p0); + engine.updateMatchmakers(makersToAdd, makersToRemove); + vm.prank(p1); + engine.updateMatchmakers(makersToAdd, makersToRemove); + bytes32 pairHash; + (key, pairHash) = engine.computeBattleKey(p0, p1); + uint256 nonce = engine.pairHashNonces(pairHash); + // moveManager == the tx sender of executeBatchedTurns (mirrors the CPU relayer role). + BattleOffer memory offer = BattleOffer({ + battle: Battle({ + p0: p0, + p0TeamIndex: 0, + p1: p1, + p1TeamIndex: 0, + p2: address(0), + p2TeamIndex: 0, + p3: address(0), + p3TeamIndex: 0, + teamRegistry: registry, + rngOracle: IRandomnessOracle(address(0)), + ruleset: IRuleset(INLINE_STAMINA_REGEN_RULESET), + moveManager: address(mgr), + matchmaker: maker, + engineHooks: new IEngineHook[](0) + }), + pairHashNonce: nonce, + battleMode: BATTLE_MODE_SINGLES + }); + bytes32 digest = maker.hashTypedData(BattleOfferLib.hashBattleOfferForSigning(offer, 0)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(P0_PK, digest); + vm.prank(p1); + bytes[4] memory seatSigs; + seatSigs[0] = abi.encodePacked(r, s, v); + maker.startGame(offer, 0, seatSigs); + } + + /// @dev Prod-faithful per-tx gas = on-chain exec + EIP-2028 calldata. The ABI encode is done by + /// the caller OUTSIDE the bracket (the relayer encodes off-chain); cold-starts storage first. + function _replayOneTx(bytes32 battleKey, bool measure) internal returns (uint256 execGas) { + uint256[] memory entries = _entries(); + bytes memory cd = abi.encodeCall(engine.executeBatchedTurns, (battleKey, entries)); + if (measure) { + vm.cool(address(engine)); + } + vm.prank(address(mgr)); + uint256 g0 = gasleft(); + (bool ok,) = address(engine).call(cd); + uint256 raw = g0 - gasleft(); + require(ok, "replay reverted"); + if (measure) { + execGas = raw + _calldataCost(cd) + TX_BASE; + } + engine.resetCallContext(); + } + + function _calldataCost(bytes memory cd) internal pure returns (uint256 cost) { + for (uint256 i; i < cd.length; i++) { + cost += cd[i] == bytes1(0) ? 4 : 16; + } + } + + function test_mainnetReplay_batchedExecute() public { + vm.warp(vm.getBlockTimestamp() + 1); + + // ---- VIRGIN storageKey (fresh, every execute SSTORE is 0->nonzero) ---- + bytes32 key1 = _startBattle(); + vm.warp(vm.getBlockTimestamp() + 1); + uint256 virginGas = _replayOneTx(key1, true); + address winner1 = engine.getWinner(key1); + require(winner1 != address(0), "replay must reach game over (faithful)"); + + // Reaching game over freed key1's storageKey back into the pool. + // ---- REUSED storageKey (steady state, execute SSTOREs are nonzero->nonzero) ---- + bytes32 key2 = _startBattle(); + require(engine.getStorageKey(key1) == engine.getStorageKey(key2), "storageKey must be reused"); + vm.warp(vm.getBlockTimestamp() + 1); + uint256 reusedGas = _replayOneTx(key2, true); + require(engine.getWinner(key2) != address(0), "reused replay must also reach game over"); + + console.log(""); + console.log("=== MAINNET replay (27 batched turns), prod config (inline regen) ==="); + console.log(" VIRGIN storageKey (cold, first battle) execGas :", virginGas); + console.log(" REUSED storageKey (steady state) execGas :", reusedGas); + console.log(" virgin - reused (recycling saving) :", virginGas - reusedGas); + } +} diff --git a/test/mons/IblivionTest.sol b/test/mons/IblivionTest.sol index dd7c6045..ccf2dcb5 100644 --- a/test/mons/IblivionTest.sol +++ b/test/mons/IblivionTest.sol @@ -486,7 +486,7 @@ contract IblivionTest is Test, BattleHelper { engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); - // At Baselight 1 (seed), Loop should give a 15% boost. + // At Baselight 1 (seed), Loop should give a % boost. assertEq(baselight.getBaselightLevel(engine, battleKey, 0, 0), 1, "Should have 1 stack"); // Get stats before Loop @@ -495,9 +495,9 @@ contract IblivionTest is Test, BattleHelper { // Alice uses Loop _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, 0, 0); - // Check stats are boosted (15% of 100 = 15) + // Check stats are boosted int32 attackAfter = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack); - assertEq(attackAfter - attackBefore, 15, "Attack should be boosted by 15%"); + assertEq(attackAfter - attackBefore, int32(int8(loop.BOOST_PERCENT_LEVEL_1())), "Attack should be boosted by %"); // Loop should be marked as active assertTrue(loop.isLoopActive(engine, battleKey, 0, 0), "Loop should be active"); From 171ab909524e83563e74e955d6a6a6e6090af89a Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:30:44 -0700 Subject: [PATCH 05/41] wip --- CLAUDE.md | 4 +- processing/deploy.py | 5 + processing/validateEffectBitmaps.py | 145 ++++++++++++++++++++++++++++ src/mons/nirvamma/Adaptor.sol | 4 +- src/mons/nirvamma/HardReset.sol | 4 +- 5 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 processing/validateEffectBitmaps.py diff --git a/CLAUDE.md b/CLAUDE.md index 6367e134..d20d56ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,6 +110,7 @@ chomp/ │ ├── generateSolidity.py # Generate SetupMons.s.sol from CSV data │ ├── generateSetupCPU.py # Generate SetupCPU.s.sol team config from cpu-teams.json │ ├── validateMoves.py # Validate move contracts match CSV data +│ ├── validateEffectBitmaps.py # Validate IEffect contracts set ALWAYS_APPLIES_BIT correctly │ ├── deploy.py # Full deployment pipeline orchestrator │ ├── deploy_addresses.py # Inline ability/move address packing for the deploy pipeline │ ├── buildTypeChart.py # Build type effectiveness chart @@ -527,7 +528,7 @@ Effects fall into several categories depending on scope: To implement a new effect: 1. Extend `BasicEffect` (or `StatusEffect` for status conditions) 2. Override the relevant lifecycle hooks (`onRoundEnd`, `onAfterDamage`, `onPreDamage`, …) — each receives `activesPacked` for local slot resolution via `TargetLib` -3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call +3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call — if the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `StatusEffect` subclasses) must NOT set this bit. `python processing/validateEffectBitmaps.py` checks this invariant across `src/` and is run as part of `deploy.py`. 4. Return `(updatedExtraData, removeAfterRun)` from hooks — use `extraData` (bytes32) to carry state between turns (counters, degrees, flags) 5. Shared effects are injected into moves/abilities via constructor parameters at deploy time — there is no runtime effect registry @@ -537,6 +538,7 @@ To implement a new effect: ```bash python processing/validateMoves.py # Validate contracts vs CSV +python processing/validateEffectBitmaps.py # Validate ALWAYS_APPLIES_BIT usage on IEffect contracts python processing/generateSolidity.py # Generate SetupMons.s.sol python processing/generateSetupCPU.py # Generate SetupCPU.s.sol from cpu-teams.json python processing/deploy.py --testnet # Full deployment (forge scripts + codegen) diff --git a/processing/deploy.py b/processing/deploy.py index e93f8479..7e85bf9f 100755 --- a/processing/deploy.py +++ b/processing/deploy.py @@ -338,6 +338,11 @@ def run_deploy(args, network, rpc_url, password, chomp_dir, env_path): if not run_validate(): raise RuntimeError("Move validation failed. Fix issues before deploying.") + print_banner("Validating effect getStepsBitmap() ALWAYS_APPLIES_BIT usage") + from validateEffectBitmaps import run as run_validate_effect_bitmaps + if not run_validate_effect_bitmaps(): + raise RuntimeError("Effect bitmap validation failed. Fix issues before deploying.") + print_banner("Generating Solidity deployment script (SetupMons.s.sol)") from generateSolidity import run as run_solidity if not run_solidity(): diff --git a/processing/validateEffectBitmaps.py b/processing/validateEffectBitmaps.py new file mode 100644 index 00000000..007bb395 --- /dev/null +++ b/processing/validateEffectBitmaps.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Static check that every IEffect contract's getStepsBitmap() correctly sets +ALWAYS_APPLIES_BIT (0x8000) when-and-only-when the contract has no real +shouldApply() gating logic of its own. + +Background: Engine._addEffectInternal skips the external shouldApply() call +entirely when ALWAYS_APPLIES_BIT is set on the returned bitmap (see +src/Constants.sol / src/Engine.sol). BasicEffect's default shouldApply() +unconditionally returns true, so any contract that inherits BasicEffect +directly and does not override shouldApply() can safely set the bit to skip +a wasted external call. Contracts that inherit StatusEffect (or otherwise +override shouldApply() with real gating logic) must NOT set the bit, since +that would make the Engine skip their gating entirely. + +This script has no CSV to check against - it's pure Solidity source +inspection, run over every contract under src/ that participates in the +IEffect hierarchy (declares `is ... BasicEffect ...` or `is ... StatusEffect ...`). +""" + +import os +import re +import sys + +ALWAYS_APPLIES_BIT = 0x8000 + +CONTRACT_RE = re.compile(r"\bcontract\s+(\w+)\s+is\s+([^{]+?)\s*\{", re.DOTALL) +STEPS_FN_RE = re.compile(r"function\s+getStepsBitmap\s*\([^)]*\)[^{]*\{(.*?)\}", re.DOTALL) +RETURN_RE = re.compile(r"return\s+([^;]+);") +SHOULD_APPLY_FN_RE = re.compile(r"\bfunction\s+shouldApply\s*\(") + + +def find_sol_files(src_path): + for root, _dirs, files in os.walk(src_path): + for f in files: + if f.endswith(".sol"): + yield os.path.join(root, f) + + +def parse_steps_expr(expr): + """Return (has_always_bit, is_literal) for a getStepsBitmap() return expression.""" + expr = expr.strip() + if "ALWAYS_APPLIES_BIT" in expr: + return True, True + # Try to parse the whole expression as a constant int (hex or decimal literal, + # optionally OR'd together, e.g. "0x8000 | 0x04"). + parts = [p.strip() for p in expr.split("|")] + total = 0 + for p in parts: + try: + total |= int(p, 0) + except ValueError: + return False, False + return (total & ALWAYS_APPLIES_BIT) != 0, True + + +def check_file(path): + """Return a list of (severity, message) issues found in this file.""" + with open(path, "r") as f: + text = f.read() + + issues = [] + for match in CONTRACT_RE.finditer(text): + name, bases_str = match.group(1), match.group(2) + bases = [b.strip() for b in bases_str.split(",")] + + is_status = "StatusEffect" in bases + is_basic = "BasicEffect" in bases + if not is_status and not is_basic: + continue + + steps_match = STEPS_FN_RE.search(text) + if not steps_match: + # Abstract base / interface with no getStepsBitmap body - not our concern. + continue + return_match = RETURN_RE.search(steps_match.group(1)) + if not return_match: + issues.append(("warn", f"{name}: could not find a return statement in getStepsBitmap()")) + continue + + has_bit, parsed_ok = parse_steps_expr(return_match.group(1)) + if not parsed_ok: + issues.append(( + "warn", + f"{name}: getStepsBitmap() returns a non-literal expression " + f"({return_match.group(1).strip()!r}) - could not verify ALWAYS_APPLIES_BIT usage", + )) + continue + + has_own_should_apply = bool(SHOULD_APPLY_FN_RE.search(text)) + + if is_status or has_own_should_apply: + # Real gating logic (either inherited from StatusEffect or defined locally) - + # the bit must NOT be set, or the Engine will bypass that logic entirely. + if has_bit: + issues.append(( + "error", + f"{name}: has custom shouldApply() gating but ALWAYS_APPLIES_BIT is set - " + f"this makes the Engine skip the gate entirely", + )) + else: + # Relies on BasicEffect's default (unconditional true) shouldApply() - + # the bit should be set to skip the wasted external call. + if not has_bit: + issues.append(( + "error", + f"{name}: relies on BasicEffect's default shouldApply() (always true) but " + f"ALWAYS_APPLIES_BIT is not set in getStepsBitmap() - missed optimization " + f"(see src/Constants.sol ALWAYS_APPLIES_BIT)", + )) + + return issues + + +def run(src_path="src/"): + all_issues = [] + for path in sorted(find_sol_files(src_path)): + rel = os.path.relpath(path) + for severity, message in check_file(path): + all_issues.append((severity, rel, message)) + + errors = [i for i in all_issues if i[0] == "error"] + warnings = [i for i in all_issues if i[0] == "warn"] + + for _severity, rel, message in warnings: + print(f"WARNING {rel}: {message}") + for _severity, rel, message in errors: + print(f"ERROR {rel}: {message}") + + if errors: + print(f"\n❌ {len(errors)} effect bitmap issue(s) found.") + return False + + print(f"\n✅ All {sum(1 for _ in find_sol_files(src_path))} Solidity files checked - effect bitmaps OK.") + return True + + +def main(): + src_path = sys.argv[1] if len(sys.argv) > 1 else "src/" + if not run(src_path): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/mons/nirvamma/Adaptor.sol b/src/mons/nirvamma/Adaptor.sol index 75d4080b..ae0581c6 100644 --- a/src/mons/nirvamma/Adaptor.sol +++ b/src/mons/nirvamma/Adaptor.sol @@ -33,9 +33,9 @@ contract Adaptor is IAbility, BasicEffect { engine.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); } - // Steps: AfterDamage, PreDamage + // Steps: AfterDamage, PreDamage, ALWAYS_APPLIES (0x8000) function getStepsBitmap() external pure override returns (uint16) { - return 0x240; + return 0x8240; } function onPreDamage(IEngine engine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256 source) diff --git a/src/mons/nirvamma/HardReset.sol b/src/mons/nirvamma/HardReset.sol index d048d439..0a613f96 100644 --- a/src/mons/nirvamma/HardReset.sol +++ b/src/mons/nirvamma/HardReset.sol @@ -82,9 +82,9 @@ contract HardReset is IMoveSet, BasicEffect { }); } - // Steps: AfterMove + // Steps: AfterMove, ALWAYS_APPLIES (0x8000) function getStepsBitmap() external pure override returns (uint16) { - return 0x80; + return 0x8080; } function onAfterMove( From 432f87508c20c008e082c31464aaefbdf8f7d3b7 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:11:49 -0700 Subject: [PATCH 06/41] fix rust flow --- transpiler/codegen_rs/symbols.py | 48 +++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/transpiler/codegen_rs/symbols.py b/transpiler/codegen_rs/symbols.py index e3222462..cb3d1c4a 100644 --- a/transpiler/codegen_rs/symbols.py +++ b/transpiler/codegen_rs/symbols.py @@ -378,12 +378,58 @@ def visit(node): found[0] = True elif cls == 'FunctionCall': fn = node.function - if type(fn).__name__ == 'Identifier': + fcls = type(fn).__name__ + if fcls == 'Identifier': if fn.name in getattr(self, 'noop_calls', set()): return sig = self.lookup_function(leaf, fn.name) or self.functions.get((None, fn.name)) if sig is not None and sig.needs_world: found[0] = True + elif fcls == 'MemberAccess': + # Mirrors compute_needs_world's member-call classification, but + # consults final needs_world flags directly (this scan runs after + # the phase-2 fixed point) instead of building call edges. + base = fn.expression + bcls = type(base).__name__ + if bcls == 'Identifier' and base.name == 'super': + for b in self.linearize_bases(leaf): + sig = self.functions.get((b, fn.member)) + if sig is not None and sig.needs_world: + found[0] = True + break + elif bcls == 'Identifier' and ( + base.name in self.libraries or base.name in self.contracts + ): + sig = self.functions.get((base.name, fn.member)) + if sig is not None and sig.needs_world: + found[0] = True + else: + # Interface-value method call: external/dispatch interfaces + # always route via world; aliased dispatch inherits the + # impl's neediness (a public-state-var getter through an + # alias is a world field read). + for iface in (self.external_interfaces + | getattr(self, 'dispatch_interfaces', set())): + if (iface, fn.member) in self.functions: + found[0] = True + break + else: + for iface, alias in self.interface_aliases.items(): + if (iface, fn.member) in self.functions: + if fn.member in self.state_vars.get(alias, {}) \ + and (alias, fn.member) not in self.functions: + found[0] = True + else: + sig = self.functions.get((alias, fn.member)) + if sig is not None and sig.needs_world: + found[0] = True + break + else: + for cname in self.contracts: + sig = self.functions.get((cname, fn.member)) + if sig is not None and sig.needs_world: + found[0] = True + break def walk(node): if node is None or found[0]: From 3166763151f6448aea6d2bd0dc39c70b9879532e Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:19:54 -0700 Subject: [PATCH 07/41] update some effects and also status effect privilege in engine --- CLAUDE.md | 21 +-- processing/validateEffectBitmaps.py | 213 +++++++++++++++++-------- src/Constants.sol | 12 ++ src/Engine.sol | 150 ++++++++++++++++- src/IEngine.sol | 5 + src/Structs.sol | 6 + src/effects/status/BlessedStatus.sol | 20 ++- src/effects/status/BurnStatus.sol | 99 ++++-------- src/effects/status/FrostbiteStatus.sol | 21 +-- src/effects/status/IStatusEffect.sol | 23 +++ src/effects/status/PanicStatus.sol | 23 ++- src/effects/status/SleepStatus.sol | 28 ++-- src/effects/status/StatusEffect.sol | 60 ++----- src/effects/status/StatusEffectLib.sol | 9 -- src/effects/status/ZapStatus.sol | 10 +- src/game-layer/SignedAssigner.sol | 56 +++++++ src/mons/aurox/GildedRecovery.sol | 17 +- src/mons/embursa/HeatBeacon.sol | 6 +- src/mons/embursa/Tinderclaws.sol | 25 ++- src/mons/ghouliath/GraveAffliction.sol | 7 +- src/mons/pengym/DeepFreeze.sol | 16 +- src/mons/pengym/PostWorkout.sol | 18 +-- test/MainnetReplayGasTest.t.sol | 82 ++++++++++ test/SignedAssignerTest.sol | 172 ++++++++++++++++++++ test/StatBoostMicrobench.t.sol | 73 +++++++++ test/mocks/BoostBenchMove.sol | 88 ++++++++++ test/mocks/SpAtkDebuffEffect.sol | 29 ++-- test/mocks/TestStatusClass.sol | 7 + transpiler/strategies-rs/src/sim.rs | 2 + transpiler/strategies-rs/src/view.rs | 14 +- transpiler/transpiler-config-rust.json | 1 + 31 files changed, 966 insertions(+), 347 deletions(-) create mode 100644 src/effects/status/IStatusEffect.sol delete mode 100644 src/effects/status/StatusEffectLib.sol create mode 100644 src/game-layer/SignedAssigner.sol create mode 100644 test/SignedAssignerTest.sol create mode 100644 test/StatBoostMicrobench.t.sol create mode 100644 test/mocks/BoostBenchMove.sol create mode 100644 test/mocks/TestStatusClass.sol diff --git a/CLAUDE.md b/CLAUDE.md index d20d56ff..0db371bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ chomp/ │ │ ├── IEffect.sol # Effect interface with lifecycle hooks │ │ ├── BasicEffect.sol │ │ ├── StaminaRegen.sol -│ │ ├── status/ # Status effects (Blessed, Burn, Frostbite, Panic, Sleep, Zap) + StatusEffect(Lib) +│ │ ├── status/ # Status effects (Blessed, Burn, Frostbite, Panic, Sleep, Zap) + StatusEffect marker base + IStatusEffect (onReapply) │ │ ├── battlefield/ # Battlefield effects (Overclock) │ ├── game-layer/ # Team / mon registry, gacha, exp, facets, quests, gifts │ │ ├── GachaTeamRegistry.sol # Concrete leaf: composes the abstracts below @@ -124,15 +124,18 @@ chomp/ │ ├── packMoves.py # Pack move data for distribution │ ├── inputToEnv.py # Parse forge output to .env │ └── removeUnusedImports.py # Clean up unused Solidity imports -├── transpiler/ # Solidity-to-TypeScript/Rust transpiler (Python) -│ ├── sol2ts.py # Main entry point +├── transpiler/ # Solidity-to-TypeScript/Rust transpiler (Python; run via `python3 -m transpiler`) +│ ├── sol2ts.py # TypeScript backend driver (not directly runnable — use `-m transpiler`) +│ ├── sol2rs.py # Rust backend driver (`--target rust`) │ ├── lexer/ # Tokenizer │ ├── parser/ # AST construction │ ├── type_system/ # Type registry │ ├── codegen/ # TypeScript code generation +│ ├── codegen_rs/ # Rust code generation │ ├── runtime/ # TypeScript runtime library +│ ├── runtime-rs/, strategies-rs/ # Hand-written Rust crates (rsynced into rs-output/) │ ├── dependency_resolver/ # Dependency resolution -│ └── test/ # TypeScript integration tests (vitest) +│ └── test_transpiler.py # Transpiler unit tests (the TS integration tests live in munch) ├── drool/ # Game data (CSV) and frontend assets │ ├── mons.csv # Mon stats (HP, attack, speed, types, etc.) │ ├── moves.csv # Move definitions (power, stamina, accuracy, etc.) @@ -520,7 +523,7 @@ For once-per-battle abilities (e.g., `RiseFromTheGrave`), use a `globalKV` flag Effects fall into several categories depending on scope: -- **Status effects** (`src/effects/status/`): Extend `StatusEffect` which enforces one-status-per-mon via a KV flag. Shared across mons — deployed once, injected into moves via constructor parameters. (e.g., `BurnStatus`, `FrostbiteStatus`, `SleepStatus`) +- **Status effects** (`src/effects/status/`): Extend the `StatusEffect` marker base. One-status-per-mon is **Engine-native**: each status folds a unique class id into `getStepsBitmap()` bits 10-13 (an internal `STATUS_CLASS` constant; deployable ids 1-14, 15 reserved for test mocks), and the Engine tracks it in a per-mon lane (`BattleConfig.monStatusLanes`, slot 3) — gating adds before any external call, setting the lane on apply, clearing it on removal. Readers use `engine.getMonStatusClass` (identity/existence) and `engine.clearMonStatus(player, mon, expectedClass)` (remove; 0 = any class); reader contracts cache the class id as a constructor `immutable` derived from the injected status's `getStepsBitmap()`. Escalating statuses (Burn) set `HAS_REAPPLY_BIT` (bit 14) and implement `IStatusEffect.onReapply`, which the Engine calls on a same-class re-apply; without the bit a re-apply is a zero-call no-op. Extra apply conditions beyond exclusivity (Sleep's single-sleeper rule) live in a `shouldApply` override (no `ALWAYS_APPLIES_BIT`). Shared across mons — deployed once, injected into moves via constructor parameters. (e.g., `BurnStatus`, `FrostbiteStatus`, `SleepStatus`) - **Battlefield effects** (`src/effects/battlefield/`): Extend `BasicEffect`, use `targetIndex=2` for global scope. (e.g., `Overclock`) - **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StaminaRegen` for per-turn recovery). NOTE: stat modifiers are **not** an effect — they are inlined Engine functions (`addStatBoost`/`removeStatBoost`/…); see "Stat Boosts" above. - **Mon-local effects** (`src/mons//`): Abilities or move-effect hybrids that only apply to one mon. These live in the mon's directory, not in `src/effects/`. @@ -528,7 +531,7 @@ Effects fall into several categories depending on scope: To implement a new effect: 1. Extend `BasicEffect` (or `StatusEffect` for status conditions) 2. Override the relevant lifecycle hooks (`onRoundEnd`, `onAfterDamage`, `onPreDamage`, …) — each receives `activesPacked` for local slot resolution via `TargetLib` -3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call — if the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `StatusEffect` subclasses) must NOT set this bit. `python processing/validateEffectBitmaps.py` checks this invariant across `src/` and is run as part of `deploy.py`. +3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call — if the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `SleepStatus`'s single-sleeper rule) must NOT set this bit. Statuses additionally carry their class id in bits 10-13 and `HAS_REAPPLY_BIT` (bit 14) iff they implement `onReapply`. `python processing/validateEffectBitmaps.py` checks all of these invariants (bit pairing, class range 1-14, cross-src uniqueness) and is run as part of `deploy.py`. 4. Return `(updatedExtraData, removeAfterRun)` from hooks — use `extraData` (bytes32) to carry state between turns (counters, degrees, flags) 5. Shared effects are injected into moves/abilities via constructor parameters at deploy time — there is no runtime effect registry @@ -553,10 +556,10 @@ Converts Solidity contracts to TypeScript for local battle simulation: ```bash # Transpile all contracts -python3 transpiler/sol2ts.py src/ -o transpiler/ts-output -d src --emit-metadata +python3 -m transpiler src/ -o transpiler/ts-output -d src --emit-metadata -# Run transpiler tests -cd transpiler && npm install && npx vitest run +# Run transpiler unit tests (the TS runtime/integration tests moved to munch with the codegen sync) +python3 transpiler/test_transpiler.py ``` **Rust target (sim performance).** The same front-end also drives a Rust diff --git a/processing/validateEffectBitmaps.py b/processing/validateEffectBitmaps.py index 007bb395..02c31a57 100644 --- a/processing/validateEffectBitmaps.py +++ b/processing/validateEffectBitmaps.py @@ -1,21 +1,24 @@ #!/usr/bin/env python3 """ -Static check that every IEffect contract's getStepsBitmap() correctly sets -ALWAYS_APPLIES_BIT (0x8000) when-and-only-when the contract has no real -shouldApply() gating logic of its own. - -Background: Engine._addEffectInternal skips the external shouldApply() call -entirely when ALWAYS_APPLIES_BIT is set on the returned bitmap (see -src/Constants.sol / src/Engine.sol). BasicEffect's default shouldApply() -unconditionally returns true, so any contract that inherits BasicEffect -directly and does not override shouldApply() can safely set the bit to skip -a wasted external call. Contracts that inherit StatusEffect (or otherwise -override shouldApply() with real gating logic) must NOT set the bit, since -that would make the Engine skip their gating entirely. - -This script has no CSV to check against - it's pure Solidity source -inspection, run over every contract under src/ that participates in the -IEffect hierarchy (declares `is ... BasicEffect ...` or `is ... StatusEffect ...`). +Static checks on every IEffect contract's getStepsBitmap() under src/: + +1. ALWAYS_APPLIES_BIT (0x8000) is set when-and-only-when the contract has no shouldApply() + gating logic of its own. Engine._addEffectInternal skips the external shouldApply() call + entirely when the bit is set; BasicEffect's default shouldApply() unconditionally returns + true, so any contract relying on it should set the bit (skipping a wasted external call), + and any contract with a real shouldApply() override must NOT set it. + +2. Status class bits (10-13): deployable ids are 1..14, unique across src/ (two statuses + sharing an id would make the Engine treat one as a re-apply of the other). Id 15 is + reserved for test-only mocks (test/mocks/TestStatusClass.sol) and rejected in src/. + +3. HAS_REAPPLY_BIT (0x4000) is set when-and-only-when the contract implements onReapply() + (the Engine calls it on a same-class re-apply iff the bit is set), and requires a nonzero + status class. + +Pure Solidity source inspection over every contract in the IEffect hierarchy (declares +`is ... BasicEffect ...` or `is ... StatusEffect ...`). Bitmap expressions may reference +constants declared in the same file (e.g. STATUS_CLASS) plus the shared Constants.sol names. """ import os @@ -23,38 +26,69 @@ import sys ALWAYS_APPLIES_BIT = 0x8000 +HAS_REAPPLY_BIT = 0x4000 +STATUS_CLASS_SHIFT = 10 +STATUS_CLASS_MASK = 0xF + +# Shared names resolvable inside bitmap expressions (mirrors src/Constants.sol and +# test/mocks/TestStatusClass.sol). +KNOWN_CONSTANTS = { + "ALWAYS_APPLIES_BIT": ALWAYS_APPLIES_BIT, + "HAS_REAPPLY_BIT": HAS_REAPPLY_BIT, + "STATUS_CLASS_SHIFT": STATUS_CLASS_SHIFT, + "STATUS_CLASS_MASK": STATUS_CLASS_MASK, + "TEST_STATUS_CLASS": 15, +} CONTRACT_RE = re.compile(r"\bcontract\s+(\w+)\s+is\s+([^{]+?)\s*\{", re.DOTALL) STEPS_FN_RE = re.compile(r"function\s+getStepsBitmap\s*\([^)]*\)[^{]*\{(.*?)\}", re.DOTALL) RETURN_RE = re.compile(r"return\s+([^;]+);") SHOULD_APPLY_FN_RE = re.compile(r"\bfunction\s+shouldApply\s*\(") - - -def find_sol_files(src_path): - for root, _dirs, files in os.walk(src_path): - for f in files: - if f.endswith(".sol"): - yield os.path.join(root, f) - - -def parse_steps_expr(expr): - """Return (has_always_bit, is_literal) for a getStepsBitmap() return expression.""" +ON_REAPPLY_FN_RE = re.compile(r"\bfunction\s+onReapply\s*\(") +CONSTANT_DECL_RE = re.compile( + r"\buint\d*\s+(?:public\s+|private\s+|internal\s+)?constant\s+(\w+)\s*=\s*([^;]+);" +) +SAFE_EXPR_RE = re.compile(r"^[\s0-9a-fA-FxX|&()<>+]*$") + + +def _collect_constants(text): + """Same-file constant declarations, resolved iteratively so one constant may + reference another (or the shared KNOWN_CONSTANTS).""" + consts = dict(KNOWN_CONSTANTS) + decls = CONSTANT_DECL_RE.findall(text) + for _ in range(len(decls) + 1): + progressed = False + for name, expr in decls: + if name in consts and name not in KNOWN_CONSTANTS: + continue + val = _eval_expr(expr, consts) + if val is not None: + if consts.get(name) != val: + progressed = True + consts[name] = val + if not progressed: + break + return consts + + +def _eval_expr(expr, consts): + """Evaluate a Solidity constant expression of hex/dec literals, |, &, <<, >>, and + parentheses. uintN(...) casts are stripped. Returns None if unresolvable.""" expr = expr.strip() - if "ALWAYS_APPLIES_BIT" in expr: - return True, True - # Try to parse the whole expression as a constant int (hex or decimal literal, - # optionally OR'd together, e.g. "0x8000 | 0x04"). - parts = [p.strip() for p in expr.split("|")] - total = 0 - for p in parts: - try: - total |= int(p, 0) - except ValueError: - return False, False - return (total & ALWAYS_APPLIES_BIT) != 0, True - - -def check_file(path): + expr = re.sub(r"\buint\d*\s*\(", "(", expr) + for name, val in sorted(consts.items(), key=lambda kv: -len(kv[0])): + expr = re.sub(r"\b" + re.escape(name) + r"\b", str(val), expr) + # Unresolved identifier = a letter-initiated token (hex literals like 0x801D start with a + # digit, so their letter digits don't trip this). + if not SAFE_EXPR_RE.match(expr) or re.search(r"(?> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK + has_own_should_apply = bool(SHOULD_APPLY_FN_RE.search(text)) + has_own_on_reapply = bool(ON_REAPPLY_FN_RE.search(text)) - if is_status or has_own_should_apply: - # Real gating logic (either inherited from StatusEffect or defined locally) - - # the bit must NOT be set, or the Engine will bypass that logic entirely. - if has_bit: - issues.append(( - "error", - f"{name}: has custom shouldApply() gating but ALWAYS_APPLIES_BIT is set - " - f"this makes the Engine skip the gate entirely", - )) - else: - # Relies on BasicEffect's default (unconditional true) shouldApply() - - # the bit should be set to skip the wasted external call. - if not has_bit: + # Rule 1: ALWAYS_APPLIES_BIT <=> no shouldApply override. + if has_own_should_apply and has_always: + issues.append(( + "error", + f"{name}: has custom shouldApply() gating but ALWAYS_APPLIES_BIT is set - " + f"this makes the Engine skip the gate entirely", + )) + elif not has_own_should_apply and not has_always: + issues.append(( + "error", + f"{name}: relies on BasicEffect's default shouldApply() (always true) but " + f"ALWAYS_APPLIES_BIT is not set in getStepsBitmap() - missed optimization " + f"(see src/Constants.sol ALWAYS_APPLIES_BIT)", + )) + + # Rule 2: status class range + uniqueness (deployable ids are 1..14; 15 is test-only). + if status_class == 15: + issues.append(( + "error", + f"{name}: status class 15 is reserved for test mocks " + f"(test/mocks/TestStatusClass.sol) - deployable statuses use 1..14", + )) + elif status_class != 0: + prior = class_registry.get(status_class) + if prior is not None: issues.append(( "error", - f"{name}: relies on BasicEffect's default shouldApply() (always true) but " - f"ALWAYS_APPLIES_BIT is not set in getStepsBitmap() - missed optimization " - f"(see src/Constants.sol ALWAYS_APPLIES_BIT)", + f"{name}: status class {status_class} already used by {prior} - the Engine " + f"would treat one as a re-apply of the other", )) + else: + class_registry[status_class] = name + + # Rule 3: HAS_REAPPLY_BIT <=> onReapply implemented, and requires a class. + if has_reapply_bit and not has_own_on_reapply: + issues.append(( + "error", + f"{name}: HAS_REAPPLY_BIT is set but onReapply() is not implemented - the " + f"Engine's same-class re-apply call would revert", + )) + elif has_own_on_reapply and not has_reapply_bit: + issues.append(( + "error", + f"{name}: implements onReapply() but HAS_REAPPLY_BIT is not set - the Engine " + f"would never call it", + )) + if has_reapply_bit and status_class == 0: + issues.append(( + "error", + f"{name}: HAS_REAPPLY_BIT requires a nonzero status class (bits 10-13)", + )) return issues def run(src_path="src/"): all_issues = [] + class_registry = {} for path in sorted(find_sol_files(src_path)): rel = os.path.relpath(path) - for severity, message in check_file(path): + for severity, message in check_file(path, class_registry): all_issues.append((severity, rel, message)) errors = [i for i in all_issues if i[0] == "error"] @@ -131,10 +201,19 @@ def run(src_path="src/"): print(f"\n❌ {len(errors)} effect bitmap issue(s) found.") return False + used = ", ".join(f"{c}={n}" for c, n in sorted(class_registry.items())) print(f"\n✅ All {sum(1 for _ in find_sol_files(src_path))} Solidity files checked - effect bitmaps OK.") + print(f" Status classes in use: {used or '(none)'}") return True +def find_sol_files(src_path): + for root, _dirs, files in os.walk(src_path): + for f in files: + if f.endswith(".sol"): + yield os.path.join(root, f) + + def main(): src_path = sys.argv[1] if len(sys.argv) > 1 else "src/" if not run(src_path): diff --git a/src/Constants.sol b/src/Constants.sol index 48170223..0c7195d1 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -68,6 +68,18 @@ address constant BUILTIN_DUAL_SIGNED_MANAGER = address(0x5165); // "SIGS" - buil // Bit 15 of stepsBitmap: when set, Engine skips the external shouldApply() call uint16 constant ALWAYS_APPLIES_BIT = 0x8000; +// Bits 10-13 of stepsBitmap: exclusive-status class (0 = not a status; 1-14 = a deployable +// status class; 15 is reserved for test-only mocks and rejected in src/ by the validator). +// Each status declares its own id (an internal STATUS_CLASS constant folded into its +// getStepsBitmap()); validateEffectBitmaps.py asserts uniqueness across src/. The Engine keys +// the per-mon status lane (BattleConfig.monStatusLanes) off these bits — it holds no status list. +uint256 constant STATUS_CLASS_SHIFT = 10; +uint256 constant STATUS_CLASS_MASK = 0xF; + +// Bit 14 of stepsBitmap: when set, Engine calls onReapply() on a same-class re-apply +// (escalating statuses, e.g. Burn). Clear = a same-class re-apply is a zero-call no-op. +uint16 constant HAS_REAPPLY_BIT = 0x4000; + uint256 constant MAX_BATTLE_DURATION = 1 hours; bytes32 constant MOVE_MISS_EVENT_TYPE = sha256(abi.encode("MoveMiss")); diff --git a/src/Engine.sol b/src/Engine.sol index 37acfeb4..c4ca7761 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -9,6 +9,7 @@ import "./moves/IMoveSet.sol"; import {IEngine} from "./IEngine.sol"; import {IAbility} from "./abilities/IAbility.sol"; +import {IStatusEffect} from "./effects/status/IStatusEffect.sol"; import {SignedCommitLib} from "./commit-manager/SignedCommitLib.sol"; import {ECDSA} from "./lib/ECDSA.sol"; import {EIP712} from "./lib/EIP712.sol"; @@ -130,6 +131,10 @@ contract Engine is IEngine, MappingAllocator, EIP712 { /// @param _DEFAULT_MONS_PER_TEAM Default mons per team for inline validation /// @param _DEFAULT_MOVES_PER_MON Default moves per mon for inline validation constructor(uint256 _DEFAULT_MONS_PER_TEAM, uint256 _DEFAULT_MOVES_PER_MON) { + // Hard cap shared by koBitmaps (8 KO bits/side) and monStatusLanes (8 nibbles/side). + if (_DEFAULT_MONS_PER_TEAM > 8) { + revert InvalidBattleConfig(); + } DEFAULT_MONS_PER_TEAM = _DEFAULT_MONS_PER_TEAM; DEFAULT_MOVES_PER_MON = _DEFAULT_MOVES_PER_MON; } @@ -431,6 +436,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { config.playerEffectStepsUnion = 0; config.engineHookStepsUnion = hookStepsUnion; config.battleMode = battleMode; + config.monStatusLanes = 0; if ((hookStepsUnion & (1 << uint8(EngineHookStep.OnBattleStart))) != 0) { for (uint256 i = 0; i < numHooks;) { @@ -1843,8 +1849,26 @@ contract Engine is IEngine, MappingAllocator, EIP712 { function _addEffectInternal(uint256 targetIndex, uint256 monIndex, IEffect effect, bytes32 extraData) internal { bytes32 battleKey = battleKeyForWrite; - // Fetch steps bitmap once (reused for storage and ALWAYS_APPLIES check) + // Fetch steps bitmap once (reused for the status gate, ALWAYS_APPLIES check, and storage) uint16 stepsBitmap = effect.getStepsBitmap(); + BattleConfig storage config = battleConfig[storageKeyForWrite]; + + // Exclusive-status gate (class-bearing player effects only), ahead of any external call: + // a different status blocks, and a same-class re-apply is a no-op unless HAS_REAPPLY + // routes it to the existing entry's onReapply. Both blocked paths cost zero external + // calls; the free-lane path falls through to the normal gate below. + { + uint256 statusClass = (stepsBitmap >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; + if (statusClass != 0 && targetIndex != 2) { + uint256 lane = (config.monStatusLanes >> _statusLaneShift(targetIndex, monIndex)) & 0xF; + if (lane != 0) { + if (lane == statusClass && (stepsBitmap & HAS_REAPPLY_BIT) != 0) { + _reapplyStatus(config, battleKey, targetIndex, monIndex, statusClass); + } + return; + } + } + } // Skip external shouldApply() call if ALWAYS_APPLIES_BIT is set bool applies; @@ -1872,9 +1896,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ); } if (!removeAfterRun) { - // Add to the appropriate effects mapping based on targetIndex - BattleConfig storage config = battleConfig[storageKeyForWrite]; - // INVARIANT: every path that adds a player effect MUST fold its steps bitmap into // playerEffectStepsUnion here. The step-skip guards in _executeInternal / _handleSwitch // treat a clear union bit as "no player effect listens at this step" and skip the whole @@ -1884,6 +1905,17 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Global effects (targetIndex == 2) are gated by globalEffectsLength instead, not the union. if (targetIndex != 2) { config.playerEffectStepsUnion |= stepsBitmap; + // Status-lane set: same slot as the union write, so both coalesce into one + // SSTORE. INVARIANT: lane nonzero ⇔ exactly one class-bearing entry on the + // mon; the only clears are _removeEffectAtSlot + startBattle's reset. + uint256 statusClass = (stepsBitmap >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; + if (statusClass != 0) { + uint256 laneShift = _statusLaneShift(targetIndex, monIndex); + config.monStatusLanes = uint64( + (uint256(config.monStatusLanes) & ~(uint256(0xF) << laneShift)) + | (statusClass << laneShift) + ); + } } if (targetIndex == 2) { @@ -1924,6 +1956,48 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } } + /// @dev Same-class re-apply with HAS_REAPPLY set: find the live entry and let it rewrite + /// (or remove) itself. The scan is bounded by the mon's short effect list and only + /// runs for escalating statuses (Burn) — plain re-applies never get here. + function _reapplyStatus( + BattleConfig storage config, + bytes32 battleKey, + uint256 targetIndex, + uint256 monIndex, + uint256 statusClass + ) private { + uint96 packedCounts = targetIndex == 0 ? config.packedP0EffectsCount : config.packedP1EffectsCount; + uint256 monEffectCount = _getMonEffectCount(packedCounts, monIndex); + mapping(uint256 => EffectInstance) storage effects = targetIndex == 0 ? config.p0Effects : config.p1Effects; + uint256 baseSlot = _getEffectSlotIndex(monIndex, 0); + for (uint256 i; i < monEffectCount;) { + EffectInstance storage e = effects[baseSlot + i]; + if ( + address(e.effect) != TOMBSTONE_ADDRESS + && ((e.stepsBitmap >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK) == statusClass + ) { + (bytes32 newData, bool removeAfterRun) = IStatusEffect(address(e.effect)).onReapply( + IEngine(address(this)), + battleKey, + tempRNG, + e.data, + targetIndex, + monIndex, + _hookActivesWord(battleData[battleKey]) + ); + if (removeAfterRun) { + _removeEffectAtSlot(config, battleKey, targetIndex, monIndex, baseSlot + i); + } else { + e.data = newData; + } + return; + } + unchecked { + ++i; + } + } + } + function addEffect(uint256 targetIndex, uint256 monIndex, IEffect effect, bytes32 extraData) external { if (battleKeyForWrite == bytes32(0)) { revert NoWriteAllowed(); @@ -1959,6 +2033,49 @@ contract Engine is IEngine, MappingAllocator, EIP712 { _removeEffectAtSlot(battleConfig[storageKeyForWrite], battleKey, targetIndex, monIndex, indexToRemove); } + /// @dev Bit offset of a mon's 4-bit status-class nibble in monStatusLanes (stride 8/side). + function _statusLaneShift(uint256 targetIndex, uint256 monIndex) private pure returns (uint256) { + return ((targetIndex << 3) | monIndex) << 2; + } + + /// @notice Remove a mon's exclusive status (running its onRemove) and clear its lane. + /// @param expectedClass 0 = clear any status; nonzero = only a matching class. + /// @return cleared Whether a status entry was removed. + function clearMonStatus(uint256 targetIndex, uint256 monIndex, uint256 expectedClass) + external + returns (bool cleared) + { + bytes32 battleKey = battleKeyForWrite; + if (battleKey == bytes32(0)) { + revert NoWriteAllowed(); + } + BattleConfig storage config = battleConfig[storageKeyForWrite]; + uint256 lane = (config.monStatusLanes >> _statusLaneShift(targetIndex, monIndex)) & 0xF; + if (lane == 0 || (expectedClass != 0 && lane != expectedClass)) { + return false; + } + // Scan the mon's short effect list for the class-bearing entry (cold path; the lane + // answers the hot existence/identity reads without a scan). + uint96 packedCounts = targetIndex == 0 ? config.packedP0EffectsCount : config.packedP1EffectsCount; + uint256 monEffectCount = _getMonEffectCount(packedCounts, monIndex); + mapping(uint256 => EffectInstance) storage effects = targetIndex == 0 ? config.p0Effects : config.p1Effects; + uint256 baseSlot = _getEffectSlotIndex(monIndex, 0); + for (uint256 i; i < monEffectCount;) { + EffectInstance storage e = effects[baseSlot + i]; + if ( + address(e.effect) != TOMBSTONE_ADDRESS + && ((e.stepsBitmap >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK) == lane + ) { + _removeEffectAtSlot(config, battleKey, targetIndex, monIndex, baseSlot + i); + return true; + } + unchecked { + ++i; + } + } + return false; + } + function _removeEffectAtSlot( BattleConfig storage config, bytes32 battleKey, @@ -1979,8 +2096,20 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (address(effect) == TOMBSTONE_ADDRESS) { return; } + uint16 stepsBitmap = eff.stepsBitmap; - if ((eff.stepsBitmap & (1 << uint8(EffectStep.OnRemove))) != 0) { + // Status-lane clear (free: stepsBitmap shares the slot just loaded). Cleared before + // onRemove so a nested add during the hook sees the lane free. This is the SOLE + // class-bearing removal chokepoint — the direct tombstone writers elsewhere are all + // stat-boost slots, which never carry class bits. + if (targetIndex != 2) { + uint256 statusClass = (stepsBitmap >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; + if (statusClass != 0) { + config.monStatusLanes &= ~uint64(uint256(0xF) << _statusLaneShift(targetIndex, monIndex)); + } + } + + if ((stepsBitmap & (1 << uint8(EffectStep.OnRemove))) != 0) { effect.onRemove( IEngine(address(this)), battleKey, @@ -4796,6 +4925,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { p1Salt: config.p1Salt, p0TeamIndex: data.p0TeamIndex, p1TeamIndex: data.p1TeamIndex, + monStatusLanes: config.monStatusLanes, p0Move: config.p0Move, p1Move: config.p1Move, globalEffects: globalEffects, @@ -5156,6 +5286,16 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return _getEffectsForTarget(storageKey, targetIndex, monIndex); } + /// @notice Class id (stepsBitmap bits 10-13) of the mon's exclusive status; 0 = none. + function getMonStatusClass(bytes32 battleKey, uint256 targetIndex, uint256 monIndex) + external + view + returns (uint256) + { + bytes32 storageKey = _resolveStorageKey(battleKey); + return (battleConfig[storageKey].monStatusLanes >> _statusLaneShift(targetIndex, monIndex)) & 0xF; + } + /// @notice Targeted single-effect lookup. Scans a mon's (or the global) effect list for /// `effectAddr` and returns its slot index + data, WITHOUT materializing the full /// `EffectInstance[]` array. For abilities / move-effects that only need one known diff --git a/src/IEngine.sol b/src/IEngine.sol index f4e2a535..e10c2a07 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -23,6 +23,7 @@ interface IEngine { external; function addEffect(uint256 targetIndex, uint256 monIndex, IEffect effect, bytes32 extraData) external; function removeEffect(uint256 targetIndex, uint256 monIndex, uint256 effectIndex) external; + function clearMonStatus(uint256 targetIndex, uint256 monIndex, uint256 expectedClass) external returns (bool); function editEffect(uint256 targetIndex, uint256 effectIndex, bytes32 newExtraData) external; function setGlobalKV(uint64 key, uint192 value) external; // Inlined stat boosts (formerly the StatBoosts effect contract). Keyed by msg.sender. @@ -198,6 +199,10 @@ interface IEngine { external view returns (bool exists, uint256 effectIndex, bytes32 data); + function getMonStatusClass(bytes32 battleKey, uint256 targetIndex, uint256 monIndex) + external + view + returns (uint256); function getWinner(bytes32 battleKey) external view returns (address); function getKOBitmap(bytes32 battleKey, uint256 playerIndex) external view returns (uint256); function getBattleContext(bytes32 battleKey) external view returns (BattleContext memory); diff --git a/src/Structs.sol b/src/Structs.sol index 415bf052..24b67e25 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -192,6 +192,11 @@ struct BattleConfig { // piggybacks on the engineHookStepsUnion SLOAD; rewritten every startBattle (recycled // storage must never leak a previous battle's mode). uint8 battleMode; // 8 + // Per-mon exclusive-status lanes: one 4-bit class nibble per mon, laneIndex = side*8 + + // monIndex (stride 8 = the Engine's hard team-size cap, NOT MONS_PER_TEAM). 0 = no status. + // Engine-owned: set in _addEffectInternal, cleared in _removeEffectAtSlot / startBattle's + // consolidated reset. Rides slot 3 so both writes coalesce with SSTOREs already paid. + uint64 monStatusLanes; // 64 MoveDecision p0Move; MoveDecision p1Move; // Stored at startBattle so Engine.getBattle can passthrough to level/exp/facet getters. @@ -232,6 +237,7 @@ struct BattleConfigView { uint104 p1Salt; uint16 p0TeamIndex; uint16 p1TeamIndex; + uint64 monStatusLanes; // Per-mon exclusive-status class nibbles (see BattleConfig) MoveDecision p0Move; MoveDecision p1Move; EffectInstance[] globalEffects; diff --git a/src/effects/status/BlessedStatus.sol b/src/effects/status/BlessedStatus.sol index 4e09bd61..fde4b000 100644 --- a/src/effects/status/BlessedStatus.sol +++ b/src/effects/status/BlessedStatus.sol @@ -1,12 +1,15 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; +import {ALWAYS_APPLIES_BIT, STATUS_CLASS_SHIFT} from "../../Constants.sol"; import {MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {StatusEffect} from "./StatusEffect.sol"; contract BlessedStatus is StatusEffect { + uint256 constant STATUS_CLASS = 6; + // Healed on removal, as a fraction of max HP. int32 public constant HEAL_DENOM = 16; @@ -16,7 +19,7 @@ contract BlessedStatus is StatusEffect { // Steps: OnApply (0x01), OnRemove (0x08), PreDamage (0x200) function getStepsBitmap() external pure override returns (uint16) { - return 0x209; + return 0x209 | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } // Absorb the next incoming damage source entirely, then remove (which grants the heal). @@ -32,17 +35,12 @@ contract BlessedStatus is StatusEffect { return (extraData, false); } - // On removal, grant the heal and clear the status flag. - function onRemove( - IEngine engine, - bytes32 battleKey, - bytes32 extraData, - uint256 targetIndex, - uint256 monIndex, - uint256 activesPacked - ) public override { + // On removal, grant the heal (the engine clears the status lane). + function onRemove(IEngine engine, bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex, uint256) + public + override + { _heal(engine, battleKey, targetIndex, monIndex); - super.onRemove(engine, battleKey, extraData, targetIndex, monIndex, activesPacked); } // Heal maxHp/HEAL_DENOM, clamped so we never overheal (copy of the ChainExpansion clamp). diff --git a/src/effects/status/BurnStatus.sol b/src/effects/status/BurnStatus.sol index f94c5f7e..23a763a4 100644 --- a/src/effects/status/BurnStatus.sol +++ b/src/effects/status/BurnStatus.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; +import {ALWAYS_APPLIES_BIT, HAS_REAPPLY_BIT, STATUS_CLASS_SHIFT} from "../../Constants.sol"; import "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {StatBoostToApply} from "../../Structs.sol"; +import {IStatusEffect} from "./IStatusEffect.sol"; import {StatusEffect} from "./StatusEffect.sol"; -import {StatusEffectLib} from "./StatusEffectLib.sol"; -contract BurnStatus is StatusEffect { +contract BurnStatus is StatusEffect, IStatusEffect { + uint256 constant STATUS_CLASS = 1; + uint256 public constant MAX_BURN_DEGREE = 3; uint8 public constant ATTACK_PERCENT = 50; @@ -22,31 +25,15 @@ contract BurnStatus is StatusEffect { } // Steps: OnApply, RoundEnd, OnRemove (no RoundStart behavior — the bit would only buy a - // no-op external call per burned turn) + // no-op external call per burned turn). HAS_REAPPLY routes same-class re-applies to the + // degree escalation in onReapply. function getStepsBitmap() external pure override returns (uint16) { - return 0x0D; + return 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | HAS_REAPPLY_BIT | ALWAYS_APPLIES_BIT; } // extraData layout: [burn degree: bits 0-7 | cached max HP: bits 32-63]. Base HP is fixed // for the battle, so caching it at apply saves the per-tick engine read. - function shouldApply(IEngine engine, bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex) - public - view - override - returns (bool) - { - uint64 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); - - // Get value from engine KV - uint192 monStatusFlag = engine.getGlobalKV(battleKey, keyForMon); - - // Check if a status already exists for the mon (or if it's already burned) - bool noStatus = monStatusFlag == 0; - bool hasBurnAlready = monStatusFlag == uint192(uint160(address(this))); - return (noStatus || hasBurnAlready); - } - function onApply( IEngine engine, bytes32 battleKey, @@ -56,58 +43,38 @@ contract BurnStatus is StatusEffect { uint256 monIndex, uint256 ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bool hasBurnAlready; - { - uint64 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); - uint192 monStatusFlag = engine.getGlobalKV(battleKey, keyForMon); - hasBurnAlready = monStatusFlag == uint192(uint160(address(this))); - // Set the burn flag only when fresh — this single read replaces both super.onApply's - // guard re-read and its redundant same-value write on the escalation path. - if (!hasBurnAlready) { - engine.setGlobalKV(keyForMon, uint192(uint160(address(this)))); - } - } - - // Set stat debuff or increase burn degree - if (!hasBurnAlready) { - // Reduce attack by 1/ATTACK_DENOM of base attack stat - StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); - statBoosts[0] = StatBoostToApply({ - stat: MonStateIndexName.Attack, boostPercent: ATTACK_PERCENT, boostType: StatBoostType.Divide - }); - engine.addStatBoost(targetIndex, monIndex, statBoosts, StatBoostFlag.Perm); - } else { - // Single burn per mon (one-status invariant), so the first by-address match is the one. - (, uint256 indexOfBurnEffect, bytes32 burnData) = - engine.getEffectData(battleKey, targetIndex, monIndex, address(this)); - uint256 burnDegree = uint256(burnData) & 0xFF; - bytes32 newExtraData = burnData; - if (burnDegree < MAX_BURN_DEGREE) { - newExtraData = bytes32((uint256(newExtraData) & ~uint256(0xFF)) | (burnDegree + 1)); - } - engine.editEffect(targetIndex, indexOfBurnEffect, newExtraData); - return (bytes32(0), true); - } + // Fresh apply only — the Engine routes same-class re-applies to onReapply instead. + // Reduce attack by 1/ATTACK_DENOM of base attack stat + StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); + statBoosts[0] = StatBoostToApply({ + stat: MonStateIndexName.Attack, boostPercent: ATTACK_PERCENT, boostType: StatBoostType.Divide + }); + engine.addStatBoost(targetIndex, monIndex, statBoosts, StatBoostFlag.Perm); uint256 maxHp = uint256(engine.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp)); return (bytes32(uint256(1) | (maxHp << 32)), false); } - function onRemove( - IEngine engine, - bytes32 battleKey, - bytes32, - uint256 targetIndex, - uint256 monIndex, - uint256 activesPacked - ) public override { - // Remove the base status flag - super.onRemove(engine, battleKey, bytes32(0), targetIndex, monIndex, activesPacked); + // Same-class re-apply (IStatusEffect): escalate the degree in place, preserving the cached + // max HP in the upper bits. + function onReapply(IEngine, bytes32, uint256, bytes32 existingData, uint256, uint256, uint256) + external + pure + returns (bytes32 newData, bool removeAfterRun) + { + uint256 burnDegree = uint256(existingData) & 0xFF; + if (burnDegree < MAX_BURN_DEGREE) { + existingData = bytes32((uint256(existingData) & ~uint256(0xFF)) | (burnDegree + 1)); + } + return (existingData, false); + } - // Reset the attack reduction + function onRemove(IEngine engine, bytes32, bytes32, uint256 targetIndex, uint256 monIndex, uint256) + public + override + { + // Lane clear is engine-owned (_removeEffectAtSlot); only the attack debuff is ours. engine.removeStatBoost(targetIndex, monIndex, StatBoostFlag.Perm); - // NOTE: no burn-degree KV reset — the degree lives in effect extraData (see onApply / - // onRoundEnd), and the old per-mon degree key was never written nor read anywhere. } // Deal damage over time diff --git a/src/effects/status/FrostbiteStatus.sol b/src/effects/status/FrostbiteStatus.sol index 870b1704..1dd3b60f 100644 --- a/src/effects/status/FrostbiteStatus.sol +++ b/src/effects/status/FrostbiteStatus.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; +import {ALWAYS_APPLIES_BIT, STATUS_CLASS_SHIFT} from "../../Constants.sol"; import "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {StatBoostToApply} from "../../Structs.sol"; @@ -8,6 +9,8 @@ import {StatBoostToApply} from "../../Structs.sol"; import {StatusEffect} from "./StatusEffect.sol"; contract FrostbiteStatus is StatusEffect { + uint256 constant STATUS_CLASS = 2; + int32 constant DAMAGE_DENOM = 16; uint8 constant SP_ATTACK_PERCENT = 50; @@ -17,7 +20,7 @@ contract FrostbiteStatus is StatusEffect { // Steps: OnApply, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint16) { - return 0x0D; + return 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } function onApply( @@ -29,8 +32,6 @@ contract FrostbiteStatus is StatusEffect { uint256 monIndex, uint256 activesPacked ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(engine, battleKey, rng, extraData, targetIndex, monIndex, activesPacked); - // Reduce special attack by half StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ @@ -43,16 +44,10 @@ contract FrostbiteStatus is StatusEffect { return (bytes32(maxHp << 32), false); } - function onRemove( - IEngine engine, - bytes32 battleKey, - bytes32 data, - uint256 targetIndex, - uint256 monIndex, - uint256 activesPacked - ) public override { - super.onRemove(engine, battleKey, data, targetIndex, monIndex, activesPacked); - + function onRemove(IEngine engine, bytes32, bytes32, uint256 targetIndex, uint256 monIndex, uint256) + public + override + { // Reset the special attack reduction engine.removeStatBoost(targetIndex, monIndex, StatBoostFlag.Perm); } diff --git a/src/effects/status/IStatusEffect.sol b/src/effects/status/IStatusEffect.sol new file mode 100644 index 00000000..15ba7866 --- /dev/null +++ b/src/effects/status/IStatusEffect.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {IEngine} from "../../IEngine.sol"; + +/// @dev Implemented only by statuses that set HAS_REAPPLY_BIT in their steps bitmap: the Engine +/// calls onReapply when the status is applied to a mon already carrying the same class +/// (e.g. Burn's degree escalation). `existingData` is the live entry's extraData; the +/// return either rewrites it in place or removes the entry. Statuses with the bit clear +/// never receive this call — a same-class re-apply is a zero-call no-op. +/// RULE (validator-enforced): a status's onApply must not apply another status to the same +/// mon — the lane is written once per stored entry, so a nested same-mon apply would alias. +interface IStatusEffect { + function onReapply( + IEngine engine, + bytes32 battleKey, + uint256 rng, + bytes32 existingData, + uint256 targetIndex, + uint256 monIndex, + uint256 activesPacked + ) external returns (bytes32 newData, bool removeAfterRun); +} diff --git a/src/effects/status/PanicStatus.sol b/src/effects/status/PanicStatus.sol index 6a3a3d8d..be07ee6d 100644 --- a/src/effects/status/PanicStatus.sol +++ b/src/effects/status/PanicStatus.sol @@ -1,12 +1,15 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; +import {ALWAYS_APPLIES_BIT, STATUS_CLASS_SHIFT} from "../../Constants.sol"; import {MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {StatusEffect} from "./StatusEffect.sol"; contract PanicStatus is StatusEffect { + uint256 constant STATUS_CLASS = 4; + uint256 constant DURATION = 3; function name() public pure override returns (string memory) { @@ -15,7 +18,7 @@ contract PanicStatus is StatusEffect { // Steps: OnApply, RoundStart, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint16) { - return 0x0F; + return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } // At the start of the turn, check to see if we should apply stamina debuff or end early @@ -37,17 +40,13 @@ contract PanicStatus is StatusEffect { return (extraData, false); } - // On apply, checks to apply the flag, and then sets the extraData to be the duration - function onApply( - IEngine engine, - bytes32 battleKey, - uint256 rng, - bytes32 data, - uint256 targetIndex, - uint256 monIndex, - uint256 activesPacked - ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(engine, battleKey, rng, data, targetIndex, monIndex, activesPacked); + // On apply, sets the extraData to be the duration + function onApply(IEngine, bytes32, uint256, bytes32, uint256, uint256, uint256) + public + pure + override + returns (bytes32 updatedExtraData, bool removeAfterRun) + { return (bytes32(DURATION), false); } diff --git a/src/effects/status/SleepStatus.sol b/src/effects/status/SleepStatus.sol index aeb10d03..6bc23d4b 100644 --- a/src/effects/status/SleepStatus.sol +++ b/src/effects/status/SleepStatus.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; -import {MOVE_INDEX_MASK, NO_OP_MOVE_INDEX, NO_SLOT, SWITCH_MOVE_INDEX} from "../../Constants.sol"; +import {MOVE_INDEX_MASK, NO_OP_MOVE_INDEX, NO_SLOT, STATUS_CLASS_SHIFT, SWITCH_MOVE_INDEX} from "../../Constants.sol"; import {MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {MoveDecision} from "../../Structs.sol"; @@ -10,6 +10,8 @@ import {TargetLib} from "../../lib/TargetLib.sol"; import {StatusEffect} from "./StatusEffect.sol"; contract SleepStatus is StatusEffect { + uint256 constant STATUS_CLASS = 3; + uint256 constant DURATION = 3; function name() public pure override returns (string memory) { @@ -18,25 +20,23 @@ contract SleepStatus is StatusEffect { // Steps: OnApply, RoundStart, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint16) { - return 0x0F; + return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT); } function _globalSleepKey(uint256 targetIndex) internal pure returns (uint64) { return uint64(uint256(keccak256(abi.encodePacked(name(), targetIndex)))); } - // Whether or not to add the effect if the step condition is met. + // Extra apply condition beyond the Engine's lane gate (which already guarantees the mon is + // status-free before this is called — hence no ALWAYS_APPLIES_BIT on this status). // Enforces one sleeper per player: the global sleep key stores // [monIndex+1 (bits 160+) | status address (lower 160 bits)] for the player's current sleeper. - function shouldApply(IEngine engine, bytes32 battleKey, bytes32 data, uint256 targetIndex, uint256 monIndex) + function shouldApply(IEngine engine, bytes32 battleKey, bytes32, uint256 targetIndex, uint256) public view override returns (bool) { - if (!super.shouldApply(engine, battleKey, data, targetIndex, monIndex)) { - return false; - } uint192 sleeperFlag = engine.getGlobalKV(battleKey, _globalSleepKey(targetIndex)); if (sleeperFlag == 0) { return true; @@ -95,7 +95,6 @@ contract SleepStatus is StatusEffect { uint256 monIndex, uint256 activesPacked ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(engine, battleKey, rng, data, targetIndex, monIndex, activesPacked); // Register this mon as the player's single sleeper (read by the shouldApply gate). engine.setGlobalKV( _globalSleepKey(targetIndex), uint192(uint160(address(this))) | (uint192(monIndex + 1) << 160) @@ -122,15 +121,10 @@ contract SleepStatus is StatusEffect { } } - function onRemove( - IEngine engine, - bytes32 battleKey, - bytes32 extraData, - uint256 targetIndex, - uint256 monIndex, - uint256 activesPacked - ) public override { - super.onRemove(engine, battleKey, extraData, targetIndex, monIndex, activesPacked); + function onRemove(IEngine engine, bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex, uint256) + public + override + { // Clear the sleeper flag only if it still points at this mon: after a KO'd sleeper // releases the gate, a successor sleeper owns the flag and must keep it. uint192 sleeperFlag = engine.getGlobalKV(battleKey, _globalSleepKey(targetIndex)); diff --git a/src/effects/status/StatusEffect.sol b/src/effects/status/StatusEffect.sol index 2eef5831..4f3a930b 100644 --- a/src/effects/status/StatusEffect.sol +++ b/src/effects/status/StatusEffect.sol @@ -1,56 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; -import {IEngine} from "../../IEngine.sol"; import {BasicEffect} from "../BasicEffect.sol"; -import {StatusEffectLib} from "./StatusEffectLib.sol"; -abstract contract StatusEffect is BasicEffect { - // Whether or not to add the effect if the step condition is met - function shouldApply(IEngine engine, bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex) - public - view - virtual - override - returns (bool) - { - uint64 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); - - // Get value from ENGINE KV - uint192 monStatusFlag = engine.getGlobalKV(battleKey, keyForMon); - - // Check if a status already exists for the mon - if (monStatusFlag == 0) { - return true; - } else { - // Otherwise return false - return false; - } - } - - /// @dev ENGINE-ONLY INVARIANT: the engine calls onApply immediately after a passing - /// shouldApply with nothing in between (_addEffectInternal), so the per-mon status flag - /// is guaranteed clear (or already ours, for statuses whose shouldApply allows that) - /// whenever this runs — the old guard re-read of the same key was a pure ~700-gas - /// round-trip. No non-engine caller exists (and a direct caller could already write the - /// flag via setGlobalKV, so this is not a trust boundary). - function onApply(IEngine engine, bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256) - public - virtual - override - returns (bytes32 extraData, bool removeAfterRun) - { - // Set the global status flag to be the address of the status (unconditional — see above) - engine.setGlobalKV(StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex), uint192(uint160(address(this)))); - return (extraData, removeAfterRun); - } - - function onRemove(IEngine engine, bytes32, bytes32, uint256 targetIndex, uint256 monIndex, uint256) - public - virtual - override - { - // On remove, reset the status flag - engine.setGlobalKV(StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex), 0); - } -} +/// @dev Marker base for exclusive statuses (one per mon). Exclusivity, re-apply routing, and +/// lane bookkeeping are Engine-native, keyed off the class id each status folds into its +/// getStepsBitmap() (bits 10-13): the Engine gates adds on the per-mon status lane before +/// any external call, sets the lane on apply, and clears it on removal. Statuses with +/// EXTRA apply conditions (SleepStatus's single-sleeper rule) override shouldApply and +/// must not set ALWAYS_APPLIES_BIT; all others set the bit and are gated by the lane +/// alone. Escalating statuses (BurnStatus) set HAS_REAPPLY_BIT and implement +/// IStatusEffect.onReapply. +abstract contract StatusEffect is BasicEffect {} diff --git a/src/effects/status/StatusEffectLib.sol b/src/effects/status/StatusEffectLib.sol deleted file mode 100644 index f8f654df..00000000 --- a/src/effects/status/StatusEffectLib.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; - -library StatusEffectLib { - function getKeyForMonIndex(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - bytes32 STATUS_EFFECT = "STATUS_EFFECT"; - return uint64(uint256(keccak256(abi.encodePacked(STATUS_EFFECT, playerIndex, monIndex)))); - } -} diff --git a/src/effects/status/ZapStatus.sol b/src/effects/status/ZapStatus.sol index 9a06faaa..6935385e 100644 --- a/src/effects/status/ZapStatus.sol +++ b/src/effects/status/ZapStatus.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; -import {MOVE_INDEX_MASK, NO_SLOT, SWITCH_MOVE_INDEX} from "../../Constants.sol"; +import { + ALWAYS_APPLIES_BIT, MOVE_INDEX_MASK, NO_SLOT, STATUS_CLASS_SHIFT, SWITCH_MOVE_INDEX +} from "../../Constants.sol"; import {MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {MoveDecision} from "../../Structs.sol"; @@ -10,6 +12,8 @@ import {TargetLib} from "../../lib/TargetLib.sol"; import {StatusEffect} from "./StatusEffect.sol"; contract ZapStatus is StatusEffect { + uint256 constant STATUS_CLASS = 5; + uint8 private constant ALREADY_SKIPPED = 1; function name() public pure override returns (string memory) { @@ -18,7 +22,7 @@ contract ZapStatus is StatusEffect { // Steps: OnApply, RoundStart, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint16) { - return 0x0F; + return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } function onApply( @@ -30,8 +34,6 @@ contract ZapStatus is StatusEffect { uint256 monIndex, uint256 activesPacked ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(engine, battleKey, rng, extraData, targetIndex, monIndex, activesPacked); - uint8 state; // If the target hasn't acted yet this turn, skip it immediately; otherwise wait for diff --git a/src/game-layer/SignedAssigner.sol b/src/game-layer/SignedAssigner.sol new file mode 100644 index 00000000..46cfa45a --- /dev/null +++ b/src/game-layer/SignedAssigner.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {ECDSA} from "../lib/ECDSA.sol"; +import {EIP712} from "../lib/EIP712.sol"; +import {IGachaPointsAssigner} from "./IGachaPointsAssigner.sol"; + +contract SignedAssigner is EIP712 { + address public immutable SIGNER; + IGachaPointsAssigner public immutable GACHA; + + mapping(uint256 word => uint256 bits) public claimBitmap; + + error ClaimAlreadySpent(); + + event Claimed(address indexed recipient, uint256 amount, uint256 indexed claimId); + + constructor(address _SIGNER, address _GACHA) { + SIGNER = _SIGNER; + GACHA = IGachaPointsAssigner(_GACHA); + } + + function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) { + name = "SignedAssigner"; + version = "1"; + } + + /// @notice Redeem a signed grant. Callable by anyone; `recipient` is paid either way. + function claim(address recipient, uint256 amount, uint256 claimId, bytes calldata signature) external { + bytes32 digest = _hashTypedData( + keccak256( + abi.encode( + keccak256("Claim(address recipient,uint256 amount,uint256 claimId)"), recipient, amount, claimId + ) + ) + ); + // Reverts InvalidSignature() itself on a malformed signature. + if (ECDSA.recoverCalldata(digest, signature) != SIGNER) { + revert ECDSA.InvalidSignature(); + } + uint256 word = claimId >> 8; + uint256 bit = 1 << (claimId & 0xFF); + uint256 bits = claimBitmap[word]; + if (bits & bit != 0) { + revert ClaimAlreadySpent(); + } + claimBitmap[word] = bits | bit; + + GACHA.assignPoints(recipient, amount); + emit Claimed(recipient, amount, claimId); + } + + function isSpent(uint256 claimId) external view returns (bool) { + return claimBitmap[claimId >> 8] & (1 << (claimId & 0xFF)) != 0; + } +} diff --git a/src/mons/aurox/GildedRecovery.sol b/src/mons/aurox/GildedRecovery.sol index 59af09dd..1d6181d4 100644 --- a/src/mons/aurox/GildedRecovery.sol +++ b/src/mons/aurox/GildedRecovery.sol @@ -7,7 +7,6 @@ import "../../Enums.sol"; import {MoveMeta} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; -import {StatusEffectLib} from "../../effects/status/StatusEffectLib.sol"; import {IMoveSet} from "../../moves/IMoveSet.sol"; contract GildedRecovery is IMoveSet { @@ -31,20 +30,8 @@ contract GildedRecovery is IMoveSet { // extraData contains the mon index as raw uint16 uint256 targetMonIndex = uint256(extraData); - // Check if the target mon has a status effect - uint64 statusKey = StatusEffectLib.getKeyForMonIndex(attackerPlayerIndex, targetMonIndex); - uint192 statusFlag = engine.getGlobalKV(battleKey, statusKey); - - // If the mon has a status effect, remove it and heal - if (statusFlag != 0) { - // Find and remove the status effect - // Targeted lookup: engine scans for the status effect internally, no full-array build. - address statusEffectAddress = address(uint160(statusFlag)); - (bool exists, uint256 idx,) = - engine.getEffectData(battleKey, attackerPlayerIndex, targetMonIndex, statusEffectAddress); - if (exists) { - engine.removeEffect(attackerPlayerIndex, targetMonIndex, idx); - } + // Remove the target mon's status (any class); heal + grant stamina only on success + if (engine.clearMonStatus(attackerPlayerIndex, targetMonIndex, 0)) { // Give +1 stamina engine.updateMonState(attackerPlayerIndex, targetMonIndex, MonStateIndexName.Stamina, STAMINA_BONUS); diff --git a/src/mons/embursa/HeatBeacon.sol b/src/mons/embursa/HeatBeacon.sol index 4098bcfc..e76e5b92 100644 --- a/src/mons/embursa/HeatBeacon.sol +++ b/src/mons/embursa/HeatBeacon.sol @@ -8,16 +8,17 @@ import "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {MoveMeta} from "../../Structs.sol"; import {IEffect} from "../../effects/IEffect.sol"; -import {StatusEffectLib} from "../../effects/status/StatusEffectLib.sol"; import {TargetLib} from "../../lib/TargetLib.sol"; import {IMoveSet} from "../../moves/IMoveSet.sol"; import {HeatBeaconLib} from "./HeatBeaconLib.sol"; contract HeatBeacon is IMoveSet { IEffect immutable BURN_STATUS; + uint256 immutable BURN_CLASS; constructor(IEffect _BURN_STATUS) { BURN_STATUS = _BURN_STATUS; + BURN_CLASS = (uint256(_BURN_STATUS.getStepsBitmap()) >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; } function name() public pure override returns (string memory) { @@ -35,8 +36,7 @@ contract HeatBeacon is IMoveSet { uint256 ) external { // Only spread Burn to the opponent if Embursa is itself Burned. - uint64 selfStatusKey = StatusEffectLib.getKeyForMonIndex(attackerPlayerIndex, attackerMonIndex); - if (engine.getGlobalKV(battleKey, selfStatusKey) == uint192(uint160(address(BURN_STATUS)))) { + if (engine.getMonStatusClass(battleKey, attackerPlayerIndex, attackerMonIndex) == BURN_CLASS) { uint256 targetSlot = TargetLib.lowestSlot(targetBits); if (targetSlot != NO_SLOT) { uint256 defenderPlayerIndex = TargetLib.sideOf(targetSlot); diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index 7396dedc..ac1406a2 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -4,13 +4,19 @@ pragma solidity ^0.8.0; // @inline-ability: singleton-local -import {MOVE_INDEX_MASK, NO_OP_MOVE_INDEX, NO_SLOT, SWITCH_MOVE_INDEX} from "../../Constants.sol"; +import { + MOVE_INDEX_MASK, + NO_OP_MOVE_INDEX, + NO_SLOT, + STATUS_CLASS_MASK, + STATUS_CLASS_SHIFT, + SWITCH_MOVE_INDEX +} from "../../Constants.sol"; import {MonStateIndexName, StatBoostFlag, StatBoostType} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {EffectInstance, IEffect, MoveDecision, StatBoostToApply} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; -import {StatusEffectLib} from "../../effects/status/StatusEffectLib.sol"; import {TargetLib} from "../../lib/TargetLib.sol"; contract Tinderclaws is IAbility, BasicEffect { @@ -18,9 +24,11 @@ contract Tinderclaws is IAbility, BasicEffect { uint8 constant SP_ATTACK_BOOST_PERCENT = 50; IEffect immutable BURN_STATUS; + uint256 immutable BURN_CLASS; constructor(IEffect _BURN_STATUS) { BURN_STATUS = _BURN_STATUS; + BURN_CLASS = (uint256(_BURN_STATUS.getStepsBitmap()) >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; } function name() public pure override(IAbility, BasicEffect) returns (string memory) { @@ -63,7 +71,7 @@ contract Tinderclaws is IAbility, BasicEffect { // If resting, remove burn if (moveIndex == NO_OP_MOVE_INDEX) { - _removeBurnIfPresent(engine, battleKey, targetIndex, monIndex); + engine.clearMonStatus(targetIndex, monIndex, BURN_CLASS); } // If used a move (not switch), 1/3 chance to self-burn else if (moveIndex != SWITCH_MOVE_INDEX) { @@ -116,15 +124,6 @@ contract Tinderclaws is IAbility, BasicEffect { view returns (bool) { - uint64 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); - uint192 monStatusFlag = engine.getGlobalKV(battleKey, keyForMon); - return monStatusFlag == uint192(uint160(address(BURN_STATUS))); - } - - function _removeBurnIfPresent(IEngine engine, bytes32 battleKey, uint256 targetIndex, uint256 monIndex) internal { - (bool exists, uint256 index,) = engine.getEffectData(battleKey, targetIndex, monIndex, address(BURN_STATUS)); - if (exists) { - engine.removeEffect(targetIndex, monIndex, index); - } + return engine.getMonStatusClass(battleKey, targetIndex, monIndex) == BURN_CLASS; } } diff --git a/src/mons/ghouliath/GraveAffliction.sol b/src/mons/ghouliath/GraveAffliction.sol index c026e4f6..08e4a094 100644 --- a/src/mons/ghouliath/GraveAffliction.sol +++ b/src/mons/ghouliath/GraveAffliction.sol @@ -7,7 +7,6 @@ import "../../Enums.sol"; import {MoveMeta} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; -import {StatusEffectLib} from "../../effects/status/StatusEffectLib.sol"; import {TargetLib} from "../../lib/TargetLib.sol"; import {IMoveSet} from "../../moves/IMoveSet.sol"; @@ -36,10 +35,8 @@ contract GraveAffliction is IMoveSet { uint256 defenderPlayerIndex = TargetLib.sideOf(targetSlot); uint256 defenderMonIndex = TargetLib.activeAt(activesPacked, targetSlot); - // Only fires if the opposing mon currently has a status condition. The StatusEffect base sets - // this per-mon flag in onApply for every status (Sleep/Panic/Burn/Frostbite/Zap/Blessed). - uint64 statusKey = StatusEffectLib.getKeyForMonIndex(defenderPlayerIndex, defenderMonIndex); - if (engine.getGlobalKV(battleKey, statusKey) == 0) { + // Only fires if the opposing mon currently has a status condition (any class). + if (engine.getMonStatusClass(battleKey, defenderPlayerIndex, defenderMonIndex) == 0) { return; } diff --git a/src/mons/pengym/DeepFreeze.sol b/src/mons/pengym/DeepFreeze.sol index 4de473a2..2d6086c0 100644 --- a/src/mons/pengym/DeepFreeze.sol +++ b/src/mons/pengym/DeepFreeze.sol @@ -17,10 +17,12 @@ contract DeepFreeze is IMoveSet { uint32 public constant BASE_POWER = 90; IEffect immutable FROSTBITE; + uint256 immutable FROSTBITE_CLASS; ITypeCalculator immutable TYPE_CALCULATOR; constructor(ITypeCalculator _TYPE_CALCULATOR, IEffect _FROSTBITE_STATUS) { FROSTBITE = _FROSTBITE_STATUS; + FROSTBITE_CLASS = (uint256(_FROSTBITE_STATUS.getStepsBitmap()) >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; TYPE_CALCULATOR = _TYPE_CALCULATOR; } @@ -28,16 +30,6 @@ contract DeepFreeze is IMoveSet { return "Deep Freeze"; } - function _frostbiteExists(IEngine engine, bytes32 battleKey, uint256 targetIndex, uint256 monIndex) - internal - view - returns (int32) - { - // Targeted lookup: engine scans for FROSTBITE internally, no full-array build. - (bool exists, uint256 idx,) = engine.getEffectData(battleKey, targetIndex, monIndex, address(FROSTBITE)); - return exists ? int32(int256(idx)) : int32(-1); - } - function move( IEngine engine, bytes32 battleKey, @@ -55,10 +47,8 @@ contract DeepFreeze is IMoveSet { uint256 otherPlayerIndex = TargetLib.sideOf(targetSlot); uint256 defenderMonIndex = TargetLib.activeAt(activesPacked, targetSlot); uint32 damageToDeal = BASE_POWER; - int32 frostbiteIndex = _frostbiteExists(engine, battleKey, otherPlayerIndex, defenderMonIndex); // Remove frostbite if it exists, and double the damage dealt - if (frostbiteIndex != -1) { - engine.removeEffect(otherPlayerIndex, defenderMonIndex, uint256(uint32(frostbiteIndex))); + if (engine.clearMonStatus(otherPlayerIndex, defenderMonIndex, FROSTBITE_CLASS)) { damageToDeal = damageToDeal * 2; } // Deal damage diff --git a/src/mons/pengym/PostWorkout.sol b/src/mons/pengym/PostWorkout.sol index 8681837f..773180ca 100644 --- a/src/mons/pengym/PostWorkout.sol +++ b/src/mons/pengym/PostWorkout.sol @@ -11,8 +11,6 @@ import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; -import {StatusEffectLib} from "../../effects/status/StatusEffectLib.sol"; - contract PostWorkout is IAbility, BasicEffect { function name() public pure override(IAbility, BasicEffect) returns (string memory) { return "Post-Workout"; @@ -36,25 +34,15 @@ contract PostWorkout is IAbility, BasicEffect { function onMonSwitchOut( IEngine engine, - bytes32 battleKey, + bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256 ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - uint64 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); - uint192 statusAddress = engine.getGlobalKV(battleKey, keyForMon); - - // Check if a status exists - if (statusAddress != 0) { - IEffect statusEffect = IEffect(address(uint160(statusAddress))); - - // Get the index of the status effect and remove it (one status per mon) - (, uint256 effectIndex,) = engine.getEffectData(battleKey, targetIndex, monIndex, address(statusEffect)); - engine.removeEffect(targetIndex, monIndex, effectIndex); - - // Boost stamina by 1 + // Clear any status (one per mon); on success boost stamina by 1 + if (engine.clearMonStatus(targetIndex, monIndex, 0)) { engine.updateMonState(targetIndex, monIndex, MonStateIndexName.Stamina, 1); } return (bytes32(0), false); diff --git a/test/MainnetReplayGasTest.t.sol b/test/MainnetReplayGasTest.t.sol index 1fa8613f..1e005af6 100644 --- a/test/MainnetReplayGasTest.t.sol +++ b/test/MainnetReplayGasTest.t.sol @@ -241,6 +241,88 @@ contract MainnetReplayGasTest is Test, SetupMons { } } + // --------------------------------------------------------------------------------------------- + // R2.0 — turn-pipeline floor: same rosters/config, but the turns carry ZERO content + // (turn 0 = send-ins, then NO_OPs: no moves, no effects, no damage). Prices the fixed + // per-turn engine cost so the replay's per-turn self-gas can be split into floor vs content. + // Raw exec gas, warm storage, no vm.cool — comparable to trace frame-self numbers. + // --------------------------------------------------------------------------------------------- + + function _packEntry(uint8 p0Move, uint16 p0Extra, uint104 p0Salt, uint8 p1Move, uint16 p1Extra, uint104 p1Salt) + internal + pure + returns (uint256) + { + return uint256(p0Move) | (uint256(p0Extra) << 8) | (uint256(p0Salt) << 24) | (uint256(p1Move) << 128) + | (uint256(p1Extra) << 136) | (uint256(p1Salt) << 152); + } + + function _noopEntries(uint256 n, uint256 saltBase) internal pure returns (uint256[] memory e) { + e = new uint256[](n); + for (uint256 i; i < n; i++) { + e[i] = + _packEntry(NO_OP_MOVE_INDEX, 0, uint104(saltBase + 2 * i), NO_OP_MOVE_INDEX, 0, uint104(saltBase + 2 * i + 1)); + } + } + + function _execBatch(bytes32 battleKey, uint256[] memory entries) internal returns (uint256 raw) { + bytes memory cd = abi.encodeCall(engine.executeBatchedTurns, (battleKey, entries)); + vm.prank(address(mgr)); + uint256 g0 = gasleft(); + (bool ok,) = address(engine).call(cd); + raw = g0 - gasleft(); + require(ok, "batch reverted"); + engine.resetCallContext(); + } + + function test_noopFloor_batchedExecute() public { + vm.warp(vm.getBlockTimestamp() + 1); + + // Battle 1: the real replay, unmeasured — completes and frees the storageKey. + bytes32 key1 = _startBattle(); + vm.warp(vm.getBlockTimestamp() + 1); + _replayOneTx(key1, false); + require(engine.getWinner(key1) != address(0), "warmup replay must finish"); + + // Battle 2 (reused key): send-ins, then 1-noop and 25-noop batches to split + // per-batch fixed cost from per-turn cost. + bytes32 key2 = _startBattle(); + require(engine.getStorageKey(key1) == engine.getStorageKey(key2), "storageKey must be reused"); + vm.warp(vm.getBlockTimestamp() + 1); + uint256[] memory sw = new uint256[](1); + sw[0] = _packEntry(SWITCH_MOVE_INDEX, 0, 11, SWITCH_MOVE_INDEX, 0, 12); + uint256 switchTurn = _execBatch(key2, sw); + uint256 t1 = _execBatch(key2, _noopEntries(1, 100)); + uint256 t25 = _execBatch(key2, _noopEntries(25, 300)); + uint256 perTurn = t25 / 25; + + // Battle 3 (reused again after forfeit): the whole no-op game in ONE batch, mirroring + // the replay's shape (1 tx, 27 entries) for a like-for-like total. Capture the storage + // key BEFORE forfeit — freeing deletes the battleKey mapping (identity fallback after). + bytes32 liveStorageKey = engine.getStorageKey(key2); + vm.prank(p0); + engine.forfeit(key2); + bytes32 key3 = _startBattle(); + require(engine.getStorageKey(key3) == liveStorageKey, "storageKey must be reused (3)"); + vm.warp(vm.getBlockTimestamp() + 1); + uint256[] memory whole = new uint256[](27); + whole[0] = _packEntry(SWITCH_MOVE_INDEX, 0, 13, SWITCH_MOVE_INDEX, 0, 14); + uint256[] memory noops = _noopEntries(26, 500); + for (uint256 i; i < 26; i++) { + whole[i + 1] = noops[i]; + } + uint256 t27 = _execBatch(key3, whole); + + console.log(""); + console.log("=== R2.0 no-op floor (reused key, raw exec gas, warm) ==="); + console.log(" send-in turn (batch of 1) :", switchTurn); + console.log(" 1 no-op turn (batch of 1) :", t1); + console.log(" 25 no-op turns (one batch) :", t25); + console.log(" floor per no-op turn (t25/25) :", perTurn); + console.log(" per-batch fixed (t1 - floor) :", t1 - perTurn); + console.log(" whole no-op game, 1 batch of 27 :", t27); + } + function test_mainnetReplay_batchedExecute() public { vm.warp(vm.getBlockTimestamp() + 1); diff --git a/test/SignedAssignerTest.sol b/test/SignedAssignerTest.sol new file mode 100644 index 00000000..deeaa3e3 --- /dev/null +++ b/test/SignedAssignerTest.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../lib/forge-std/src/Test.sol"; + +import "../src/Constants.sol"; + +import {Engine} from "../src/Engine.sol"; +import {GachaTeamRegistry} from "../src/game-layer/GachaTeamRegistry.sol"; +import {PlayerProfile} from "../src/game-layer/PlayerProfile.sol"; +import {SignedAssigner} from "../src/game-layer/SignedAssigner.sol"; +import {ECDSA} from "../src/lib/ECDSA.sol"; + +import {MockGachaRNG} from "./mocks/MockGachaRNG.sol"; + +contract SignedAssignerTest is Test { + address constant ALICE = address(0xA1); + address constant BOB = address(0xB0); + + uint256 constant SIGNER_PK = 0x5160E; + uint256 constant IMPOSTOR_PK = 0xBAD; + uint256 constant AMOUNT = 16; + + /// @dev Order of the secp256k1 curve; used to malleate a signature. + uint256 constant SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141; + + bytes32 constant DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 constant CLAIM_TYPEHASH = keccak256("Claim(address recipient,uint256 amount,uint256 claimId)"); + + GachaTeamRegistry registry; + SignedAssigner assigner; + Engine engine; + MockGachaRNG mockRNG; + address signer; + + function setUp() public { + signer = vm.addr(SIGNER_PK); + engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); + mockRNG = new MockGachaRNG(); + registry = new GachaTeamRegistry(4, 1, engine, mockRNG, GachaTeamRegistry(address(0))); + assigner = new SignedAssigner(signer, address(registry)); + _setAssigner(address(assigner), true); + } + + function _setAssigner(address who, bool allowed) internal { + address[] memory on = new address[](allowed ? 1 : 0); + address[] memory off = new address[](allowed ? 0 : 1); + (allowed ? on : off)[0] = who; + registry.setAssigners(on, off); + } + + // Rebuilt here rather than read off the contract, so a domain or typehash change fails. + function _digest(address recipient, uint256 amount, uint256 claimId) internal view returns (bytes32) { + bytes32 domainSeparator = keccak256( + abi.encode( + DOMAIN_TYPEHASH, keccak256("SignedAssigner"), keccak256("1"), block.chainid, address(assigner) + ) + ); + bytes32 structHash = keccak256(abi.encode(CLAIM_TYPEHASH, recipient, amount, claimId)); + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function _signParts(uint256 pk, address recipient, uint256 amount, uint256 claimId) + internal + view + returns (uint8 v, bytes32 r, bytes32 s) + { + return vm.sign(pk, _digest(recipient, amount, claimId)); + } + + function _sign(uint256 pk, address recipient, uint256 amount, uint256 claimId) + internal + view + returns (bytes memory) + { + (uint8 v, bytes32 r, bytes32 s) = _signParts(pk, recipient, amount, claimId); + return abi.encodePacked(r, s, v); + } + + function _sign(address recipient, uint256 amount, uint256 claimId) internal view returns (bytes memory) { + return _sign(SIGNER_PK, recipient, amount, claimId); + } + + /// Anyone may submit; the signed recipient is paid either way (BOB here drives the relay path). + function test_claimPaysSignedRecipient() public { + vm.expectEmit(true, true, true, true); + emit SignedAssigner.Claimed(ALICE, AMOUNT, 1); + vm.prank(BOB); + assigner.claim(ALICE, AMOUNT, 1, _sign(ALICE, AMOUNT, 1)); + + assertEq(registry.pointsBalance(ALICE), AMOUNT); + assertEq(registry.pointsBalance(BOB), 0); + assertTrue(assigner.isSpent(1)); + } + + /// Regression: the nullifier must key on claimId, not the signature bytes. ECDSA signatures + /// are malleable, so a signature-keyed nullifier lets a flipped s replay the same grant. + function test_malleatedSignatureCannotReplay() public { + (uint8 v, bytes32 r, bytes32 s) = _signParts(SIGNER_PK, ALICE, AMOUNT, 1); + assigner.claim(ALICE, AMOUNT, 1, abi.encodePacked(r, s, v)); + + bytes memory malleated = abi.encodePacked(r, bytes32(SECP256K1_N - uint256(s)), v == 27 ? uint8(28) : uint8(27)); + assertTrue( + keccak256(malleated) != keccak256(abi.encodePacked(r, s, v)), "malleated signature bytes must differ" + ); + + vm.expectRevert(SignedAssigner.ClaimAlreadySpent.selector); + assigner.claim(ALICE, AMOUNT, 1, malleated); + assertEq(registry.pointsBalance(ALICE), AMOUNT); + } + + /// The same (recipient, amount) must stay grantable under a fresh claimId. + function test_sameAmountRepeatableWithNewClaimId() public { + assigner.claim(ALICE, AMOUNT, 1, _sign(ALICE, AMOUNT, 1)); + assigner.claim(ALICE, AMOUNT, 2, _sign(ALICE, AMOUNT, 2)); + + assertEq(registry.pointsBalance(ALICE), AMOUNT * 2); + } + + /// Every field is inside the digest, and only SIGNER's key opens it. + function test_signedFieldsAreBound() public { + bytes memory sig = _sign(ALICE, AMOUNT, 1); + + vm.expectRevert(ECDSA.InvalidSignature.selector); + assigner.claim(BOB, AMOUNT, 1, sig); + + vm.expectRevert(ECDSA.InvalidSignature.selector); + assigner.claim(ALICE, AMOUNT * 100, 1, sig); + + vm.expectRevert(ECDSA.InvalidSignature.selector); + assigner.claim(ALICE, AMOUNT, 2, sig); + + vm.expectRevert(ECDSA.InvalidSignature.selector); + assigner.claim(ALICE, AMOUNT, 1, _sign(IMPOSTOR_PK, ALICE, AMOUNT, 1)); + } + + /// Signer-compromise recovery: revoking the allowlist entry kills unspent claims. + function test_removedAssignerReverts() public { + bytes memory sig = _sign(ALICE, AMOUNT, 1); + _setAssigner(address(assigner), false); + + vm.expectRevert(PlayerProfile.NotAssigner.selector); + assigner.claim(ALICE, AMOUNT, 1, sig); + } + + /// Ids sharing a word (0/255, 256/257) and crossing one (255/256) stay independent. + function test_bitmapWordBoundary() public { + uint256[4] memory ids = [uint256(0), 255, 256, 257]; + for (uint256 i; i < ids.length; ++i) { + assigner.claim(ALICE, AMOUNT, ids[i], _sign(ALICE, AMOUNT, ids[i])); + assertTrue(assigner.isSpent(ids[i])); + } + assertEq(registry.pointsBalance(ALICE), AMOUNT * ids.length); + + for (uint256 i; i < ids.length; ++i) { + vm.expectRevert(SignedAssigner.ClaimAlreadySpent.selector); + assigner.claim(ALICE, AMOUNT, ids[i], _sign(ALICE, AMOUNT, ids[i])); + } + } + + function testFuzz_claimIdSpendableOnce(uint256 claimId, uint128 amount) public { + vm.assume(amount > 0); + bytes memory sig = _sign(ALICE, amount, claimId); + + assigner.claim(ALICE, amount, claimId, sig); + assertEq(registry.pointsBalance(ALICE), amount); + + vm.expectRevert(SignedAssigner.ClaimAlreadySpent.selector); + assigner.claim(ALICE, amount, claimId, sig); + } +} diff --git a/test/StatBoostMicrobench.t.sol b/test/StatBoostMicrobench.t.sol new file mode 100644 index 00000000..149a4c4d --- /dev/null +++ b/test/StatBoostMicrobench.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../lib/forge-std/src/Test.sol"; + +import "../src/Constants.sol"; +import "../src/Enums.sol"; +import "../src/Structs.sol"; + +import {Engine} from "../src/Engine.sol"; +import {IEngine} from "../src/IEngine.sol"; +import {DefaultCommitManager} from "../src/commit-manager/DefaultCommitManager.sol"; +import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; +import {BattleHelper} from "./abstract/BattleHelper.sol"; +import {BoostBenchMove} from "./mocks/BoostBenchMove.sol"; +import {MockRandomnessOracle} from "./mocks/MockRandomnessOracle.sol"; +import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; + +/// @notice R1.0 — cost of one addStatBoost / removeStatBoost as a function of K existing +/// sources on the mon (fresh engine per K, so adds are virgin-lane; the K-slope is +/// the scan/unpack/aggregate growth, the K=0 intercept is entry-write + telescope + +/// call overhead). +contract StatBoostMicrobench is Test, BattleHelper { + function _runK(uint256 k) internal returns (uint256 addGas, uint256 removeGas) { + MockRandomnessOracle oracle = new MockRandomnessOracle(); + TestTeamRegistry registry = new TestTeamRegistry(); + Engine engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); + DefaultCommitManager mgr = new DefaultCommitManager(IEngine(address(engine))); + DefaultMatchmaker maker = new DefaultMatchmaker(engine); + BoostBenchMove bench = new BoostBenchMove(); + + uint256[] memory moves = new uint256[](1); + moves[0] = uint256(uint160(address(bench))); + Mon memory mon = Mon({ + stats: MonStats({ + hp: 100, + stamina: 100, + speed: 100, + attack: 100, + defense: 100, + specialAttack: 100, + specialDefense: 100, + type1: Type.Fire, + type2: Type.None + }), + moves: moves, + ability: 0 + }); + Mon[] memory team = new Mon[](1); + team[0] = mon; + registry.setTeam(ALICE, team); + registry.setTeam(BOB, team); + + bytes32 battleKey = _startBattle(engine, oracle, registry, maker, address(mgr)); + _commitRevealExecuteForAliceAndBob( + engine, mgr, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) + ); + _commitRevealExecuteForAliceAndBob(engine, mgr, battleKey, 0, NO_OP_MOVE_INDEX, uint16(k), 0); + addGas = bench.lastAddGas(); + removeGas = bench.lastRemoveGas(); + } + + function test_microbench_addRemoveByExistingSources() public { + console.log(""); + console.log("=== R1.0 stat-boost microbench (virgin lanes; K = existing sources) ==="); + for (uint256 k; k <= 4; k++) { + (uint256 a, uint256 r) = _runK(k); + console.log(" K =", k); + console.log(" addStatBoost :", a); + console.log(" removeStatBoost:", r); + } + } +} diff --git a/test/mocks/BoostBenchMove.sol b/test/mocks/BoostBenchMove.sol new file mode 100644 index 00000000..599e834c --- /dev/null +++ b/test/mocks/BoostBenchMove.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../src/Constants.sol"; +import "../../src/Enums.sol"; +import "../../src/Structs.sol"; + +import {IEngine} from "../../src/IEngine.sol"; +import {IMoveSet} from "../../src/moves/IMoveSet.sol"; + +/// @dev Distinct msg.sender per boost source (sources are keyed by caller). Memory prep happens +/// before the gas bracket so the measurement is the engine call alone (+ ~constant CALL +/// overhead, identical across K). +contract BoostSatellite { + function boost(IEngine engine, uint256 playerIndex, uint256 monIndex, uint8 pct) external returns (uint256 g) { + StatBoostToApply[] memory b = new StatBoostToApply[](1); + b[0] = StatBoostToApply({stat: MonStateIndexName.Attack, boostPercent: pct, boostType: StatBoostType.Multiply}); + uint256 g0 = gasleft(); + engine.addStatBoost(playerIndex, monIndex, b, StatBoostFlag.Perm); + g = g0 - gasleft(); + } + + function unboost(IEngine engine, uint256 playerIndex, uint256 monIndex) external returns (uint256 g) { + uint256 g0 = gasleft(); + engine.removeStatBoost(playerIndex, monIndex, StatBoostFlag.Perm); + g = g0 - gasleft(); + } +} + +/// @dev R1.0 microbench driver: extraData = K (0..5) existing sources to install, then measures +/// one more add (the K+1th source) and its removal. Results parked in public storage. +contract BoostBenchMove is IMoveSet { + BoostSatellite[6] public sats; + uint256 public lastAddGas; + uint256 public lastRemoveGas; + + constructor() { + for (uint256 i; i < 6; i++) { + sats[i] = new BoostSatellite(); + } + } + + function name() external pure returns (string memory) { + return "Boost Bench"; + } + + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint256, uint16 extraData, uint256) + external + { + uint256 k = uint256(extraData) & 0x7; + for (uint256 i; i < k; i++) { + sats[i].boost(engine, attackerPlayerIndex, 0, uint8(10 + i)); + } + lastAddGas = sats[k].boost(engine, attackerPlayerIndex, 0, 50); + lastRemoveGas = sats[k].unboost(engine, attackerPlayerIndex, 0); + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return 0; + } + + function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { + return 0; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Air; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Self; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + pure + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: 0 + }); + } +} diff --git a/test/mocks/SpAtkDebuffEffect.sol b/test/mocks/SpAtkDebuffEffect.sol index 1d03f4a4..0615eb0e 100644 --- a/test/mocks/SpAtkDebuffEffect.sol +++ b/test/mocks/SpAtkDebuffEffect.sol @@ -1,13 +1,17 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; +import {ALWAYS_APPLIES_BIT, STATUS_CLASS_SHIFT} from "../../src/Constants.sol"; import {MonStateIndexName, StatBoostFlag, StatBoostType} from "../../src/Enums.sol"; import {IEngine} from "../../src/IEngine.sol"; import {StatBoostToApply} from "../../src/Structs.sol"; import {StatusEffect} from "../../src/effects/status/StatusEffect.sol"; +import {TEST_STATUS_CLASS} from "./TestStatusClass.sol"; contract SpAtkDebuffEffect is StatusEffect { + uint256 constant STATUS_CLASS = TEST_STATUS_CLASS; + uint8 constant SP_ATTACK_PERCENT = 50; function name() public pure override returns (string memory) { @@ -16,21 +20,18 @@ contract SpAtkDebuffEffect is StatusEffect { // Steps: OnApply, OnRemove function getStepsBitmap() external pure override returns (uint16) { - return 0x09; + return 0x09 | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } function onApply( IEngine engine, - bytes32 battleKey, - uint256 rng, + bytes32, + uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, - uint256 activesPacked + uint256 ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - // Call parent to set status flag - super.onApply(engine, battleKey, rng, extraData, targetIndex, monIndex, activesPacked); - // Reduce special attack by half StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ @@ -42,16 +43,10 @@ contract SpAtkDebuffEffect is StatusEffect { return (extraData, false); } - function onRemove( - IEngine engine, - bytes32 battleKey, - bytes32 data, - uint256 targetIndex, - uint256 monIndex, - uint256 activesPacked - ) public override { - super.onRemove(engine, battleKey, data, targetIndex, monIndex, activesPacked); - + function onRemove(IEngine engine, bytes32, bytes32, uint256 targetIndex, uint256 monIndex, uint256) + public + override + { // Reset the special attack reduction engine.removeStatBoost(targetIndex, monIndex, StatBoostFlag.Perm); } diff --git a/test/mocks/TestStatusClass.sol b/test/mocks/TestStatusClass.sol new file mode 100644 index 00000000..50a2a8dc --- /dev/null +++ b/test/mocks/TestStatusClass.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +// Status class id reserved for test-only statuses: prod ids are 1..14 (validateEffectBitmaps.py +// rejects 15 in src/), so mocks can never collide with a deployed status. Mocks share this id; +// if a test ever needs two distinct mock statuses in one battle, extend the reservation to a band. +uint256 constant TEST_STATUS_CLASS = 15; diff --git a/transpiler/strategies-rs/src/sim.rs b/transpiler/strategies-rs/src/sim.rs index cccdc2a7..51f484e5 100644 --- a/transpiler/strategies-rs/src/sim.rs +++ b/transpiler/strategies-rs/src/sim.rs @@ -349,6 +349,7 @@ impl Sim { world.Engine._turnP1Packed = pack_turn(m.move_index, m.salt, m.extra_data); } world.Engine.storageKeyForWrite = fork; + world.Engine.battleKeyForWrite = fork; // write-gates (addEffect etc.) check this transient Engine::_executeInternal(world, fork, fork, false, false, false); // slotPacked=false (singles) world.reset_transient(); // fork tx over; capture reads boundary-clean fork @@ -372,6 +373,7 @@ impl Sim { world.Engine._turnP0Packed = Engine::_packSideTurn(side0_packed); world.Engine._turnP1Packed = Engine::_packSideTurn(side1_packed); world.Engine.storageKeyForWrite = fork; + world.Engine.battleKeyForWrite = fork; // write-gates (addEffect etc.) check this transient Engine::_executeInternal(world, fork, fork, false, false, true); // slotPacked=true, silent world.reset_transient(); fork diff --git a/transpiler/strategies-rs/src/view.rs b/transpiler/strategies-rs/src/view.rs index ad9b6578..0c0a8112 100644 --- a/transpiler/strategies-rs/src/view.rs +++ b/transpiler/strategies-rs/src/view.rs @@ -158,9 +158,19 @@ pub fn move_slot(sim: &mut Sim, seat: Seat, bk: B256, vp: u8, mon: usize, mi: us } /// Damage context between the two ACTIVE mons (proxied: both player-index -/// args seat-mapped). +/// args seat-mapped; the engine now takes explicit mon indices, so each +/// side's active is resolved here). pub fn damage_calc_context(sim: &mut Sim, seat: Seat, bk: B256, atk_vp: u8, def_vp: u8) -> DamageCalcContext { - Engine::getDamageCalcContext(&mut sim.world, bk, seat.phys(atk_vp), seat.phys(def_vp)) + let (opp_active, cpu_active) = active_mon_indices(sim, seat, bk); + let active_of = |vp: u8| if vp == VCPU { cpu_active } else { opp_active }; + Engine::getDamageCalcContext( + &mut sim.world, + bk, + seat.phys(atk_vp), + U256::from(active_of(atk_vp) as u64), + seat.phys(def_vp), + U256::from(active_of(def_vp) as u64), + ) } /// `TYPE_CALC.getTypeEffectiveness` — TypeCalculator delegates straight to diff --git a/transpiler/transpiler-config-rust.json b/transpiler/transpiler-config-rust.json index cd193277..a57a649e 100644 --- a/transpiler/transpiler-config-rust.json +++ b/transpiler/transpiler-config-rust.json @@ -74,6 +74,7 @@ "dispatchInterfaces": [ "IMoveSet", "IEffect", + "IStatusEffect", "IAbility" ], "duckDispatchMethods": [ From 2767055ba8e77710a98d7a85f451696f4f784183 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:03:57 -0700 Subject: [PATCH 08/41] make stat boosts cheaper --- src/Engine.sol | 370 +++++++++++++++++----------------- src/Structs.sol | 16 ++ src/lib/StatBoostLib.sol | 100 +++++++++ test/StatBoostAccParity.t.sol | 95 +++++++++ test/effects/EffectTest.sol | 38 ++-- test/effects/StatBoosts.t.sol | 59 ++---- test/mons/AuroxTest.sol | 9 +- 7 files changed, 443 insertions(+), 244 deletions(-) create mode 100644 test/StatBoostAccParity.t.sol diff --git a/src/Engine.sol b/src/Engine.sol index c4ca7761..651c0cae 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -422,6 +422,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } config.koBitmaps = 0; config.globalKVCount = 0; + config.p0BoostCounts = 0; config.teamSizes = newTeamSizes; config.hasInlineStaminaRegen = hasInlineRegen; config.globalEffectsLength = newGlobalEffectsLength; @@ -437,6 +438,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { config.engineHookStepsUnion = hookStepsUnion; config.battleMode = battleMode; config.monStatusLanes = 0; + config.p1BoostCounts = 0; if ((hookStepsUnion & (1 << uint8(EngineHookStep.OnBattleStart))) != 0) { for (uint256 i = 0; i < numHooks;) { @@ -2204,31 +2206,15 @@ contract Engine is IEngine, MappingAllocator, EIP712 { revert NoWriteAllowed(); } BattleConfig storage config = battleConfig[storageKeyForWrite]; - uint96 packedCounts = targetIndex == 0 ? config.packedP0EffectsCount : config.packedP1EffectsCount; - mapping(uint256 => EffectInstance) storage effects = targetIndex == 0 ? config.p0Effects : config.p1Effects; - uint256 monEffectCount = _getMonEffectCount(packedCounts, monIndex); - uint256 baseSlot = _getEffectSlotIndex(monIndex, 0); - - uint256 removeCount; - for (uint256 i; i < monEffectCount; ++i) { - EffectInstance storage eff = effects[baseSlot + i]; - if (address(eff.effect) == STAT_BOOST_ADDRESS) { - eff.effect = IEffect(TOMBSTONE_ADDRESS); - unchecked { - ++removeCount; - } - } - } - if (removeCount == 0) { + if (_boostCountOf(config, targetIndex, monIndex) == 0) { return; } - _compactTrailingTombstones(config, targetIndex, monIndex); + _setBoostCountOf(config, targetIndex, monIndex, 0); + config.statBoostAcc[(targetIndex << 3) | monIndex] = 0; // also clears a DISABLED flag - // Reset to base by applying with empty aggregation. + // Telescope back to base stats. uint32[5] memory baseStats = _getStatBoostBaseStats(config, targetIndex, monIndex); - uint32[5] memory numBoostsPerStat; - uint256[5] memory accumulatedNumeratorPerStat; - _applyStatBoosts(config, targetIndex, monIndex, baseStats, numBoostsPerStat, accumulatedNumeratorPerStat); + _applyBoostedStats(config, targetIndex, monIndex, baseStats, baseStats); } function _getStatBoostBaseStats(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) @@ -2244,6 +2230,37 @@ contract Engine is IEngine, MappingAllocator, EIP712 { stats[4] = monStats.speed; } + // Boost-source store: one packed word per source at slot monIndex * 16 + i (i < the mon's + // 4-bit count nibble). 15 sources/mon is far above the plausible distinct-caller ceiling. + + function _boostWordsOf(BattleConfig storage config, uint256 targetIndex) + private + view + returns (mapping(uint256 => bytes32) storage) + { + return targetIndex == 0 ? config.p0BoostWords : config.p1BoostWords; + } + + function _boostCountOf(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) + private + view + returns (uint256) + { + uint32 packed = targetIndex == 0 ? config.p0BoostCounts : config.p1BoostCounts; + return (packed >> (monIndex * 4)) & 0xF; + } + + function _setBoostCountOf(BattleConfig storage config, uint256 targetIndex, uint256 monIndex, uint256 newCount) + private + { + uint256 shift = monIndex * 4; + if (targetIndex == 0) { + config.p0BoostCounts = uint32((config.p0BoostCounts & ~(uint256(0xF) << shift)) | (newCount << shift)); + } else { + config.p1BoostCounts = uint32((config.p1BoostCounts & ~(uint256(0xF) << shift)) | (newCount << shift)); + } + } + function _addStatBoostWithKey( uint256 targetIndex, uint256 monIndex, @@ -2253,158 +2270,127 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ) private { BattleConfig storage config = battleConfig[storageKeyForWrite]; uint32[5] memory baseStats = _getStatBoostBaseStats(config, targetIndex, monIndex); + mapping(uint256 => bytes32) storage words = _boostWordsOf(config, targetIndex); + uint256 count = _boostCountOf(config, targetIndex, monIndex); + uint256 baseSlot = monIndex * 16; - uint96 packedCounts = targetIndex == 0 ? config.packedP0EffectsCount : config.packedP1EffectsCount; - mapping(uint256 => EffectInstance) storage effects = targetIndex == 0 ? config.p0Effects : config.p1Effects; - uint256 monEffectCount = _getMonEffectCount(packedCounts, monIndex); - uint256 baseSlot = _getEffectSlotIndex(monIndex, 0); - + // Find an existing same-key/same-permanence source (header-only reads — no lane unpack). bool found; - uint256 foundSlot; - bytes32 existingData; - uint256 tombstoneSlot = type(uint256).max; - uint32[5] memory numBoostsPerStat; - uint256[5] memory accumulatedNumeratorPerStat; - - // Single pass: find the matching key, aggregate every OTHER boost source on the mon, and - // remember the first tombstoned slot — a fresh source reuses it instead of appending, so - // temp-boost churn (Overclock re-applying every switch-in) stops growing the list. - for (uint256 i; i < monEffectCount; ++i) { - uint256 slotIndex = baseSlot + i; - EffectInstance storage eff = effects[slotIndex]; - address effAddr = address(eff.effect); - if (effAddr == TOMBSTONE_ADDRESS && tombstoneSlot == type(uint256).max) { - tombstoneSlot = slotIndex; - continue; - } - if (effAddr != STAT_BOOST_ADDRESS) { - continue; - } - bytes32 data = eff.data; - (bool effIsPerm, uint168 existingKey, uint8[5] memory bp, uint8[5] memory bc, bool[5] memory im) = - StatBoostLib.unpackBoostData(data); - if (existingKey == key && effIsPerm == isPerm) { + uint256 foundIdx; + bytes32 oldWord; + for (uint256 i; i < count; ++i) { + bytes32 w = words[baseSlot + i]; + (bool wPerm, uint168 wKey) = StatBoostLib.unpackBoostHeader(w); + if (wKey == key && wPerm == isPerm) { found = true; - foundSlot = slotIndex; - existingData = data; - continue; // excluded from aggregation; merged version is added below + foundIdx = i; + oldWord = w; + break; } - StatBoostLib.accumulateBoosts(baseStats, bp, bc, im, numBoostsPerStat, accumulatedNumeratorPerStat); } - // Compute the new/merged source and add its contribution. Fresh sources accumulate - // straight from the caller's entries (no pack -> unpack round-trip). - bytes32 newData; + bytes32 newWord; if (found) { - (,, uint8[5] memory ep, uint8[5] memory ec, bool[5] memory em) = StatBoostLib.unpackBoostData(existingData); + (,, uint8[5] memory ep, uint8[5] memory ec, bool[5] memory em) = StatBoostLib.unpackBoostData(oldWord); (uint8[5] memory fp, uint8[5] memory fc, bool[5] memory fm) = StatBoostLib.mergeExistingAndNewBoosts(ep, ec, em, statBoostsToApply); - newData = StatBoostLib.packBoostDataWithArrays(key, isPerm, fp, fc, fm); - StatBoostLib.accumulateBoosts(baseStats, fp, fc, fm, numBoostsPerStat, accumulatedNumeratorPerStat); + newWord = StatBoostLib.packBoostDataWithArrays(key, isPerm, fp, fc, fm); + words[baseSlot + foundIdx] = newWord; } else { - newData = StatBoostLib.packBoostData(key, isPerm, statBoostsToApply); - StatBoostLib.accumulateBoostsToApply( - baseStats, statBoostsToApply, numBoostsPerStat, accumulatedNumeratorPerStat - ); + if (count == 15) { + revert InvalidBattleConfig(); + } + newWord = StatBoostLib.packBoostData(key, isPerm, statBoostsToApply); + words[baseSlot + count] = newWord; + _setBoostCountOf(config, targetIndex, monIndex, count + 1); } - // Persist the source entry. - if (found) { - effects[foundSlot].data = newData; + // Accumulator update: O(changed lanes), replacing the legacy all-source rescan. A merge + // divides the old word out and multiplies the merged word in (always correct, order-free). + // count-before == 0 initializes — stale recycled-key accumulators are never read. + uint256 lane = (targetIndex << 3) | monIndex; + uint256 acc = count == 0 ? 0 : config.statBoostAcc[lane]; + if (acc & StatBoostLib.ACC_DISABLED_BIT == 0) { + bool ok = true; + if (found) { + (acc, ok) = StatBoostLib.applyWordToAcc(acc, oldWord, baseStats, false); + } + if (ok) { + (acc, ok) = StatBoostLib.applyWordToAcc(acc, newWord, baseStats, true); + } + if (!ok) { + acc = StatBoostLib.ACC_DISABLED_BIT; // overflow: recompute-from-sources from here on + } + config.statBoostAcc[lane] = acc; } else { - _addStatBoostEffectSlot( - config, targetIndex, monIndex, packedCounts, effects, monEffectCount, newData, tombstoneSlot - ); + acc = StatBoostLib.ACC_DISABLED_BIT; } - _applyStatBoosts(config, targetIndex, monIndex, baseStats, numBoostsPerStat, accumulatedNumeratorPerStat); + _applyStatBoostAggregates(config, targetIndex, monIndex, baseStats, acc); } function _removeStatBoostWithKey(uint256 targetIndex, uint256 monIndex, uint168 key, bool isPerm) private { BattleConfig storage config = battleConfig[storageKeyForWrite]; - uint32[5] memory baseStats = _getStatBoostBaseStats(config, targetIndex, monIndex); - - uint96 packedCounts = targetIndex == 0 ? config.packedP0EffectsCount : config.packedP1EffectsCount; - mapping(uint256 => EffectInstance) storage effects = targetIndex == 0 ? config.p0Effects : config.p1Effects; - uint256 monEffectCount = _getMonEffectCount(packedCounts, monIndex); - uint256 baseSlot = _getEffectSlotIndex(monIndex, 0); - - bool found; - uint256 foundSlot; - uint32[5] memory numBoostsPerStat; - uint256[5] memory accumulatedNumeratorPerStat; - - for (uint256 i; i < monEffectCount; ++i) { - uint256 slotIndex = baseSlot + i; - EffectInstance storage eff = effects[slotIndex]; - if (address(eff.effect) != STAT_BOOST_ADDRESS) { + mapping(uint256 => bytes32) storage words = _boostWordsOf(config, targetIndex); + uint256 count = _boostCountOf(config, targetIndex, monIndex); + uint256 baseSlot = monIndex * 16; + + for (uint256 i; i < count; ++i) { + bytes32 w = words[baseSlot + i]; + (bool wPerm, uint168 wKey) = StatBoostLib.unpackBoostHeader(w); + if (wKey != key || wPerm != isPerm) { continue; } - (bool effIsPerm, uint168 existingKey, uint8[5] memory bp, uint8[5] memory bc, bool[5] memory im) = - StatBoostLib.unpackBoostData(eff.data); - if (existingKey == key && effIsPerm == isPerm) { - found = true; - foundSlot = slotIndex; - continue; // removed: excluded from aggregation + // Swap-remove (aggregation is commutative; source order is meaningless). + uint256 last = count - 1; + if (i != last) { + words[baseSlot + i] = words[baseSlot + last]; + } + _setBoostCountOf(config, targetIndex, monIndex, last); + + uint32[5] memory baseStats = _getStatBoostBaseStats(config, targetIndex, monIndex); + uint256 lane = (targetIndex << 3) | monIndex; + uint256 acc = config.statBoostAcc[lane]; + if (acc & StatBoostLib.ACC_DISABLED_BIT == 0) { + bool ok; + (acc, ok) = StatBoostLib.applyWordToAcc(acc, w, baseStats, false); + if (!ok) { + acc = StatBoostLib.ACC_DISABLED_BIT; + } + config.statBoostAcc[lane] = acc; } - StatBoostLib.accumulateBoosts(baseStats, bp, bc, im, numBoostsPerStat, accumulatedNumeratorPerStat); - } - - if (!found) { + _applyStatBoostAggregates(config, targetIndex, monIndex, baseStats, acc); return; } - effects[foundSlot].effect = IEffect(TOMBSTONE_ADDRESS); - _compactTrailingTombstones(config, targetIndex, monIndex); - _applyStatBoosts(config, targetIndex, monIndex, baseStats, numBoostsPerStat, accumulatedNumeratorPerStat); } - /// @dev Write a fresh stat-boost source into the next free per-mon effect slot. Mirrors the - /// player-effect storage block in _addEffectInternal (count bump + dirty bit) but writes - /// the sentinel address and fixed steps bitmap directly — no external IEffect calls. - function _addStatBoostEffectSlot( + /// @dev Telescope from either the fast-path accumulator or (when DISABLED) a full + /// recompute over the mon's source words — the recompute IS the legacy aggregation, + /// so overflow behavior degrades to exactly the old math. + function _applyStatBoostAggregates( BattleConfig storage config, uint256 targetIndex, uint256 monIndex, - uint96 packedCounts, - mapping(uint256 => EffectInstance) storage effects, - uint256 monEffectCount, - bytes32 data, - uint256 tombstoneSlot + uint32[5] memory baseStats, + uint256 acc ) private { - // Reuse a tombstoned slot when the caller's scan found one: the effect word goes - // TOMBSTONE -> STAT_BOOST (nz->nz, 2.9k instead of a fresh 20k on virgin keys), the - // count is NOT bumped (the slot is already inside it — no packed-count SSTORE, no dirty - // TSTORE), and the list stops growing monotonically. Position shift is safe: boost - // entries only act at OnMonSwitchOut and aggregation order is commutative; a boost - // landing in an already-passed slot mid-_runEffects-iteration is skipped for that pass - // (only reachable via OnMonSwitchOut listeners adding boosts, and harmless then). - if (tombstoneSlot != type(uint256).max) { - EffectInstance storage reused = effects[tombstoneSlot]; - reused.effect = IEffect(STAT_BOOST_ADDRESS); - reused.stepsBitmap = STAT_BOOST_STEPS; - reused.data = data; - // Same playerEffectStepsUnion invariant as the append path below. - config.playerEffectStepsUnion |= STAT_BOOST_STEPS; - return; - } - uint256 slotIndex = _getEffectSlotIndex(monIndex, monEffectCount); - EffectInstance storage effectSlot = effects[slotIndex]; - effectSlot.effect = IEffect(STAT_BOOST_ADDRESS); - effectSlot.stepsBitmap = STAT_BOOST_STEPS; - effectSlot.data = data; - // Upholds the playerEffectStepsUnion invariant documented in _addEffectInternal: this is the - // one player-effect add-path that bypasses _addEffectInternal, so it must fold its steps in too. - // Without this the union under-reports OnMonSwitchOut and _handleSwitch would skip dropping - // temp boosts on switch-out. - config.playerEffectStepsUnion |= STAT_BOOST_STEPS; - uint96 newCounts = _setMonEffectCount(packedCounts, monIndex, monEffectCount + 1); - if (targetIndex == 0) { - config.packedP0EffectsCount = newCounts; - effectsDirtyBitmap |= (1 << (1 + monIndex)); + uint32[5] memory newBoostedStats; + if (acc & StatBoostLib.ACC_DISABLED_BIT != 0) { + mapping(uint256 => bytes32) storage words = _boostWordsOf(config, targetIndex); + uint256 count = _boostCountOf(config, targetIndex, monIndex); + uint256 baseSlot = monIndex * 16; + uint32[5] memory numBoostsPerStat; + uint256[5] memory accumulatedNumeratorPerStat; + for (uint256 i; i < count; ++i) { + (,, uint8[5] memory bp, uint8[5] memory bc, bool[5] memory im) = + StatBoostLib.unpackBoostData(words[baseSlot + i]); + StatBoostLib.accumulateBoosts(baseStats, bp, bc, im, numBoostsPerStat, accumulatedNumeratorPerStat); + } + newBoostedStats = StatBoostLib.finalizeBoostedStats(baseStats, numBoostsPerStat, accumulatedNumeratorPerStat); } else { - config.packedP1EffectsCount = newCounts; - effectsDirtyBitmap |= (1 << (9 + monIndex)); + newBoostedStats = StatBoostLib.finalizeAccStats(acc, baseStats); } + _applyBoostedStats(config, targetIndex, monIndex, baseStats, newBoostedStats); } /// @dev Re-apply a mon's aggregated stat boosts by telescoping its monState deltas. The @@ -2413,17 +2399,13 @@ contract Engine is IEngine, MappingAllocator, EIP712 { /// base + currentDelta. We compute the new boosted stat and feed the difference through /// _updateMonStateInternal, which fires OnUpdateMonState for listeners exactly as before. /// No globalKV snapshot is kept — being inside the Engine we read the delta back directly. - function _applyStatBoosts( + function _applyBoostedStats( BattleConfig storage config, uint256 targetIndex, uint256 monIndex, uint32[5] memory baseStats, - uint32[5] memory numBoostsPerStat, - uint256[5] memory accumulatedNumeratorPerStat + uint32[5] memory newBoostedStats ) private { - uint32[5] memory newBoostedStats = StatBoostLib.finalizeBoostedStats( - baseStats, numBoostsPerStat, accumulatedNumeratorPerStat - ); MonState storage st = _getMonState(config, targetIndex, monIndex); // Listener gate hoisted ONCE per apply (was paid inside _updateMonStateInternal per stat). @@ -2482,36 +2464,45 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return d == CLEARED_MON_STATE_SENTINEL ? int32(0) : d; } - /// @dev Switch-out handling for stat-boost entries: in a single pass drop EVERY temp source on - /// the mon (they all expire on switch-out) and re-aggregate the surviving permanent sources, - /// applying the result once. _runEffects only routes here when it hits a temp entry; that - /// first hit does all the work and tombstones its siblings, so the remaining temp slots are - /// skipped by the loop's tombstone guard (collapses the legacy per-instance O(n^2) recompute). + /// @dev Switch-out expiry: drop EVERY temp source on the mon in one pass (swap-compacting the + /// store) and apply the surviving aggregate once. Called directly from the switch path — + /// boost sources no longer live in the effect list, so no OnMonSwitchOut pass is involved. function _inlineStatBoostSwitchOut(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) private { - mapping(uint256 => EffectInstance) storage effects = targetIndex == 0 ? config.p0Effects : config.p1Effects; - uint96 packedCounts = targetIndex == 0 ? config.packedP0EffectsCount : config.packedP1EffectsCount; - uint256 monEffectCount = _getMonEffectCount(packedCounts, monIndex); - uint256 baseSlot = _getEffectSlotIndex(monIndex, 0); + mapping(uint256 => bytes32) storage words = _boostWordsOf(config, targetIndex); + uint256 count = _boostCountOf(config, targetIndex, monIndex); + uint256 baseSlot = monIndex * 16; uint32[5] memory baseStats = _getStatBoostBaseStats(config, targetIndex, monIndex); - uint32[5] memory numBoostsPerStat; - uint256[5] memory accumulatedNumeratorPerStat; - for (uint256 i; i < monEffectCount; ++i) { - EffectInstance storage eff = effects[baseSlot + i]; - if (address(eff.effect) != STAT_BOOST_ADDRESS) { + uint256 lane = (targetIndex << 3) | monIndex; + uint256 acc = config.statBoostAcc[lane]; + bool live = acc & StatBoostLib.ACC_DISABLED_BIT == 0; + uint256 kept = count; + uint256 i; + while (i < kept) { + bytes32 w = words[baseSlot + i]; + if (StatBoostLib.isPerm(w)) { + ++i; continue; } - (bool effIsPerm,, uint8[5] memory bp, uint8[5] memory bc, bool[5] memory im) = - StatBoostLib.unpackBoostData(eff.data); - if (!effIsPerm) { - eff.effect = IEffect(TOMBSTONE_ADDRESS); // temp sources all expire on switch-out - continue; + if (live) { + bool ok; + (acc, ok) = StatBoostLib.applyWordToAcc(acc, w, baseStats, false); + if (!ok) { + acc = StatBoostLib.ACC_DISABLED_BIT; + live = false; + } + } + --kept; + if (i != kept) { + words[baseSlot + i] = words[baseSlot + kept]; } - StatBoostLib.accumulateBoosts(baseStats, bp, bc, im, numBoostsPerStat, accumulatedNumeratorPerStat); } - - _compactTrailingTombstones(config, targetIndex, monIndex); - _applyStatBoosts(config, targetIndex, monIndex, baseStats, numBoostsPerStat, accumulatedNumeratorPerStat); + if (kept == count) { + return; // nothing expired: no writes, no telescope + } + _setBoostCountOf(config, targetIndex, monIndex, kept); + config.statBoostAcc[lane] = acc; + _applyStatBoostAggregates(config, targetIndex, monIndex, baseStats, acc); } function setGlobalKV(uint64 key, uint192 value) external { @@ -3121,6 +3112,13 @@ contract Engine is IEngine, MappingAllocator, EIP712 { _runEffects(battleKey, tempRNG, playerIndex, playerIndex, EffectStep.OnMonSwitchOut, "", outCount); } + // Temp stat-boost expiry, called directly (boost sources live in their own store, not + // the effect list — mons with only boosts skip the whole OnMonSwitchOut pass above). + // After the pass so switch-out effects observe the outgoing mon's boosted stats. + if (_boostCountOf(config, playerIndex, currentActiveMonIndex) != 0) { + _inlineStatBoostSwitchOut(config, playerIndex, currentActiveMonIndex); + } + // Then run the global on mon switch out hook as well if (config.globalEffectsLength > 0) { _runEffects(battleKey, tempRNG, 2, playerIndex, EffectStep.OnMonSwitchOut, "", type(uint256).max); @@ -3447,16 +3445,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return; } - // Inline execution for stat-boost sentinel entries. Only OnMonSwitchOut is in their steps - // bitmap; a temp boost is dropped on switch-out and the mon's stats recomputed from the - // remaining permanent sources (mirrors the legacy StatBoosts.onMonSwitchOut path). - if (address(effect) == STAT_BOOST_ADDRESS) { - if (!StatBoostLib.isPerm(data)) { - _inlineStatBoostSwitchOut(config, effectIndex, monIndex); - } - return; - } - // Run the effect and get result (bytes32 updatedExtraData, bool removeAfterRun) = _executeEffectHook( battleKeyForWrite, effect, rng, data, playerIndex, monIndex, round, extraEffectsData, activesPacked @@ -4929,6 +4917,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { p0Move: config.p0Move, p1Move: config.p1Move, globalEffects: globalEffects, + p0StatBoosts: _boostWordsView(config, 0), + p1StatBoosts: _boostWordsView(config, 1), p0Effects: p0Effects, p1Effects: p1Effects, teams: teams, @@ -5286,6 +5276,26 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return _getEffectsForTarget(storageKey, targetIndex, monIndex); } + /// @dev View shape of a side's stat-boost store: per-mon arrays of packed source words + /// (StatBoostLib layout — clients decode key/perm/lanes off-chain). + function _boostWordsView(BattleConfig storage config, uint256 side) + private + view + returns (bytes32[][] memory out) + { + uint256 teamSize = side == 0 ? (config.teamSizes & 0x0F) : (config.teamSizes >> 4); + mapping(uint256 => bytes32) storage words = _boostWordsOf(config, side); + out = new bytes32[][](teamSize); + for (uint256 m; m < teamSize; ++m) { + uint256 cnt = _boostCountOf(config, side, m); + bytes32[] memory ws = new bytes32[](cnt); + for (uint256 i; i < cnt; ++i) { + ws[i] = words[m * 16 + i]; + } + out[m] = ws; + } + } + /// @notice Class id (stepsBitmap bits 10-13) of the mon's exclusive status; 0 = none. function getMonStatusClass(bytes32 battleKey, uint256 targetIndex, uint256 monIndex) external diff --git a/src/Structs.sol b/src/Structs.sol index 24b67e25..ffad6f35 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -173,6 +173,9 @@ struct BattleConfig { uint40 startTimestamp; // 40 — battle start time; overflows in year ~36825 (shrunk from uint48 for slot-2 packing) bool hasInlineStaminaRegen; // 8 uint8 globalKVCount; // 8 — live entry count in the current battle's globalKV key buffer + // Per-mon stat-boost source counts, 4 bits per mon (max 15 sources/mon — distinct callers). + // MUST reset every startBattle (recycled keys); rides the slot-2 consolidated write. + uint32 p0BoostCounts; // 32 uint104 p0Salt; uint104 p1Salt; // OR of every player (per-mon) effect's stepsBitmap added this battle. Lets the hot step @@ -197,6 +200,7 @@ struct BattleConfig { // Engine-owned: set in _addEffectInternal, cleared in _removeEffectAtSlot / startBattle's // consolidated reset. Rides slot 3 so both writes coalesce with SSTOREs already paid. uint64 monStatusLanes; // 64 + uint32 p1BoostCounts; // 32 — see p0BoostCounts; rides slot 3 MoveDecision p0Move; MoveDecision p1Move; // Stored at startBattle so Engine.getBattle can passthrough to level/exp/facet getters. @@ -209,6 +213,16 @@ struct BattleConfig { mapping(uint256 => EffectInstance) p0Effects; mapping(uint256 => EffectInstance) p1Effects; mapping(uint256 => EngineHookInstance) engineHooks; + // Stat-boost sources: ONE packed word per source (StatBoostLib layout — key/perm/5 lanes), + // slot = monIndex * 16 + i, valid for i < that mon's 4-bit count in p{0,1}BoostCounts. + // No reset needed on recycled keys: words are only read below the (reset) count. + mapping(uint256 => bytes32) p0BoostWords; + mapping(uint256 => bytes32) p1BoostWords; + // Per-mon aggregation cache, laneIndex = side*8 + monIndex: 5 stat lanes of + // [numerator:48 | count:3] (bits k*51..) + bit 255 = disabled (overflow fallback). + // Never read when the mon's source count is 0 (initialize-on-first-add), so recycled-key + // staleness is unobservable; count > 0 implies this battle wrote it. + mapping(uint256 => uint256) statBoostAcc; } struct EffectInstance { @@ -241,6 +255,8 @@ struct BattleConfigView { MoveDecision p0Move; MoveDecision p1Move; EffectInstance[] globalEffects; + bytes32[][] p0StatBoosts; // Per-mon packed stat-boost source words (StatBoostLib layout) + bytes32[][] p1StatBoosts; EffectInstance[][] p0Effects; // Returns effects per mon in team EffectInstance[][] p1Effects; Mon[][] teams; diff --git a/src/lib/StatBoostLib.sol b/src/lib/StatBoostLib.sol index dc6a18f7..087e0cb8 100644 --- a/src/lib/StatBoostLib.sol +++ b/src/lib/StatBoostLib.sol @@ -34,6 +34,106 @@ library StatBoostLib { return uint8(uint256(data) >> PERM_FLAG_OFFSET) != 0; } + // --------------------------------------------------------------------------------------------- + // Aggregation accumulator (one uint256 per mon, cached by the Engine). + // + // 5 stat lanes of [numerator:48 | count:3] at bit k*51, plus bit 255 = DISABLED. A lane + // mirrors _accumulateOne's running product exactly: numerator = base × ∏(100 ± pct) with + // count total instances, finalized as numerator / 100^count (single divide — so incremental + // multiply-in / divide-out is bit-identical to a full recompute). An update that would + // overflow a lane's 48-bit numerator or 3-bit count returns ok = false; the Engine then sets + // DISABLED and falls back to recompute-from-sources for the rest of the battle. + // --------------------------------------------------------------------------------------------- + + uint256 internal constant ACC_LANE_BITS = 51; + uint256 internal constant ACC_NUM_MASK = (1 << 48) - 1; + uint256 internal constant ACC_CNT_MASK = 0x7; + uint256 internal constant ACC_DISABLED_BIT = 1 << 255; + + /// @dev Lane k of a packed source word: (pct, count, isMul). count == 0 means "no boost". + function laneAt(bytes32 data, uint256 k) internal pure returns (uint256 pct, uint256 cnt, bool mul) { + uint256 inst = (uint256(data) >> (k * 16)) & 0xFFFF; + pct = inst >> 8; + cnt = (inst >> 1) & 0x7F; + mul = (inst & 0x1) == 1; + } + + /// @dev Fold a source word into (mulIn = true) or out of (mulIn = false) the accumulator. + /// Divide-out is exact by construction (the factors were multiplied in); a lane drained + /// to count 0 resets its numerator to 0 (canonical empty — finalize reads base then). + function applyWordToAcc(uint256 acc, bytes32 word, uint32[5] memory baseStats, bool mulIn) + internal + pure + returns (uint256 newAcc, bool ok) + { + for (uint256 k; k < 5; ++k) { + (uint256 pct, uint256 cnt, bool mul) = laneAt(word, k); + if (cnt == 0) { + continue; + } + uint256 factor = mul ? DENOM + pct : DENOM - pct; + uint256 shift = k * ACC_LANE_BITS; + uint256 num = (acc >> shift) & ACC_NUM_MASK; + uint256 laneCnt = (acc >> (shift + 48)) & ACC_CNT_MASK; + if (mulIn) { + if (laneCnt + cnt > ACC_CNT_MASK) { + return (acc, false); + } + if (laneCnt == 0) { + num = baseStats[k]; + } + for (uint256 j; j < cnt; ++j) { + num *= factor; + if (num > ACC_NUM_MASK) { + return (acc, false); + } + } + laneCnt += cnt; + } else { + // factor 0 lanes (pct == 100 Divide) zero the product permanently; the Engine + // never reaches here for them — mulIn already failed the fit check (num 0 is + // fine to multiply but a later divide-out would div-by-zero), so guard: + if (factor == 0 || laneCnt < cnt) { + return (acc, false); + } + for (uint256 j; j < cnt; ++j) { + num /= factor; + } + laneCnt -= cnt; + if (laneCnt == 0) { + num = 0; + } + } + acc = (acc & ~(((ACC_NUM_MASK) | (ACC_CNT_MASK << 48)) << shift)) | (num << shift) + | (laneCnt << (shift + 48)); + } + return (acc, true); + } + + /// @dev finalizeBoostedStats over the packed accumulator — same divide + clamp semantics. + function finalizeAccStats(uint256 acc, uint32[5] memory baseStats) + internal + pure + returns (uint32[5] memory newBoostedStats) + { + for (uint256 k; k < 5; ++k) { + uint256 shift = k * ACC_LANE_BITS; + uint256 laneCnt = (acc >> (shift + 48)) & ACC_CNT_MASK; + if (laneCnt == 0) { + newBoostedStats[k] = baseStats[k]; + continue; + } + uint256 raw = ((acc >> shift) & ACC_NUM_MASK) / denomPower(laneCnt); + if (raw > MAX_BOOSTED_STAT) { + newBoostedStats[k] = MAX_BOOSTED_STAT; + } else if (raw == 0) { + newBoostedStats[k] = 1; + } else { + newBoostedStats[k] = uint32(raw); + } + } + } + // Pack a fresh boost instance from the caller-supplied boosts. function packBoostData(uint168 key, bool perm, StatBoostToApply[] memory statBoostsToApply) internal diff --git a/test/StatBoostAccParity.t.sol b/test/StatBoostAccParity.t.sol new file mode 100644 index 00000000..78d3a611 --- /dev/null +++ b/test/StatBoostAccParity.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../lib/forge-std/src/Test.sol"; + +import {MonStateIndexName, StatBoostType} from "../src/Enums.sol"; +import {StatBoostToApply} from "../src/Structs.sol"; +import {StatBoostLib} from "../src/lib/StatBoostLib.sol"; + +/// @notice R1.b parity invariant: the packed accumulator (incremental multiply-in / divide-out) +/// must finalize to EXACTLY the stats the legacy full recompute produces, for any source +/// set that fits the lanes — including after removing an arbitrary source. +contract StatBoostAccParity is Test { + function _mkWord(uint256 seed, uint168 key) internal pure returns (bytes32) { + // 1-2 boosted lanes per source, pct 1..100, count 1..2, mixed mul/div — the realistic + // envelope (prod uses 15-50% single-lane boosts). + uint256 numLanes = 1 + (seed & 1); + StatBoostToApply[] memory boosts = new StatBoostToApply[](numLanes); + for (uint256 i; i < numLanes; ++i) { + uint256 s = uint256(keccak256(abi.encode(seed, i))); + boosts[i] = StatBoostToApply({ + // Distinct stat per lane (well-formed input, like prod callers): packBoostData + // ORs lanes, so a same-stat collision would corrupt the word. + stat: MonStateIndexName(uint8(2 + ((seed + i) % 5))), + boostPercent: uint8(1 + (s >> 8) % 100), + boostType: (s >> 16) & 1 == 0 ? StatBoostType.Multiply : StatBoostType.Divide + }); + } + return StatBoostLib.packBoostData(key, seed & 2 == 0, boosts); + } + + function _referenceStats(bytes32[] memory words, uint256 skipIdx, uint32[5] memory baseStats) + internal + pure + returns (uint32[5] memory) + { + uint32[5] memory numBoosts; + uint256[5] memory accNum; + for (uint256 i; i < words.length; ++i) { + if (i == skipIdx) { + continue; + } + (,, uint8[5] memory bp, uint8[5] memory bc, bool[5] memory im) = StatBoostLib.unpackBoostData(words[i]); + StatBoostLib.accumulateBoosts(baseStats, bp, bc, im, numBoosts, accNum); + } + return StatBoostLib.finalizeBoostedStats(baseStats, numBoosts, accNum); + } + + function test_repro() public pure { + _run(3, 12, 41968); + } + + function testFuzz_accMatchesRecompute(uint256 seed, uint8 numSourcesRaw, uint16 baseRaw) public pure { + _run(seed, numSourcesRaw, baseRaw); + } + + function _run(uint256 seed, uint8 numSourcesRaw, uint16 baseRaw) internal pure { + uint256 numSources = 1 + (numSourcesRaw % 5); + uint32 base = 1 + uint32(baseRaw) % 1000; + uint32[5] memory baseStats = [base, base + 7, base + 13, base + 29, base + 51]; + + bytes32[] memory words = new bytes32[](numSources); + uint256 acc; + bool allOk = true; + for (uint256 i; i < numSources; ++i) { + words[i] = _mkWord(uint256(keccak256(abi.encode(seed, "w", i))), uint168(uint160(i + 1))); + bool ok; + (acc, ok) = StatBoostLib.applyWordToAcc(acc, words[i], baseStats, true); + allOk = allOk && ok; + } + // Realistic envelopes always fit; if a pathological draw overflowed, the Engine would + // have disabled + recomputed (parity by construction) — nothing to compare here. + vm.assume(allOk); + + uint32[5] memory fast = StatBoostLib.finalizeAccStats(acc, baseStats); + uint32[5] memory ref = _referenceStats(words, type(uint256).max, baseStats); + for (uint256 k; k < 5; ++k) { + assertEq(fast[k], ref[k], "add parity"); + } + + // Divide an arbitrary source back out: must equal recompute-without-it. The one designed + // refusal is a factor-0 lane (100% Divide) — the Engine disables the accumulator there + // and recomputes from sources, which IS the reference; nothing to compare. + uint256 removeIdx = uint256(keccak256(abi.encode(seed, "r"))) % numSources; + (uint256 acc2, bool ok2) = StatBoostLib.applyWordToAcc(acc, words[removeIdx], baseStats, false); + if (!ok2) { + return; + } + uint32[5] memory fast2 = StatBoostLib.finalizeAccStats(acc2, baseStats); + uint32[5] memory ref2 = _referenceStats(words, removeIdx, baseStats); + for (uint256 k; k < 5; ++k) { + assertEq(fast2[k], ref2[k], "remove parity"); + } + } +} diff --git a/test/effects/EffectTest.sol b/test/effects/EffectTest.sol index f5537719..bde86180 100644 --- a/test/effects/EffectTest.sol +++ b/test/effects/EffectTest.sol @@ -145,11 +145,12 @@ contract EffectTest is Test, BattleHelper { // Alice and Bob both select attacks, both of them are move index 0 (do frostbite damage) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); - // Check that both mons have an effect length of 2 (including stat boost) + // Both mons carry the frostbite status; its SpAtk debuff lives in the boost store, not + // the effect list (boost sources are no longer effect entries). (EffectInstance[] memory effects0,) = engine.getEffects(battleKey, 0, 0); (EffectInstance[] memory effects1,) = engine.getEffects(battleKey, 1, 0); - assertEq(effects0.length, 2); - assertEq(effects1.length, 2); + assertEq(effects0.length, 1); + assertEq(effects1.length, 1); // Check that both mons took 1 damage (we should round down) assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp), -1); @@ -162,11 +163,11 @@ contract EffectTest is Test, BattleHelper { // Alice and Bob both select attacks, both of them are move index 0 (do frostbite damage) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); - // Check that both mons still have an effect length of 2 (including stat boost) + // Check that both mons still carry exactly the status entry (effects0,) = engine.getEffects(battleKey, 0, 0); (effects1,) = engine.getEffects(battleKey, 1, 0); - assertEq(effects0.length, 2); - assertEq(effects1.length, 2); + assertEq(effects0.length, 1); + assertEq(effects1.length, 1); assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp), -2); assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp), -2); @@ -472,11 +473,12 @@ contract EffectTest is Test, BattleHelper { // Alice and Bob both select attacks, both of them are move index 0 (apply burn status) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); - // Check that both mons have an effect length of 2 (including stat boost) + // Both mons carry the burn status; its attack debuff lives in the boost store, not the + // effect list (boost sources are no longer effect entries). (EffectInstance[] memory burnEffects0,) = engine.getEffects(battleKey, 0, 0); (EffectInstance[] memory burnEffects1,) = engine.getEffects(battleKey, 1, 0); - assertEq(burnEffects0.length, 2); - assertEq(burnEffects1.length, 2); + assertEq(burnEffects0.length, 1); + assertEq(burnEffects1.length, 1); // Check that the attack of both mons was reduced by 50% (32/2 = 16) assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack), -16); @@ -489,11 +491,11 @@ contract EffectTest is Test, BattleHelper { // Alice and Bob both select attacks again to increase burn degree _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); - // Check that both mons still have an effect length of 2 (including stat boost) + // Check that both mons still carry exactly the status entry (debuff is store-side) (EffectInstance[] memory effects0,) = engine.getEffects(battleKey, 0, 0); (EffectInstance[] memory effects1,) = engine.getEffects(battleKey, 1, 0); - assertEq(effects0.length, 2); - assertEq(effects1.length, 2); + assertEq(effects0.length, 1); + assertEq(effects1.length, 1); // Check that both mons took additional 1/8 damage (256/8 = 32) // Total damage should be 16 (first round) + 32 (second round) = 48 @@ -503,11 +505,11 @@ contract EffectTest is Test, BattleHelper { // Alice and Bob both select attacks again to increase burn degree to maximum _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); - // Check that both mons still have an effect length of 2 + // Check that both mons still carry exactly the status entry (effects0,) = engine.getEffects(battleKey, 0, 0); (effects1,) = engine.getEffects(battleKey, 1, 0); - assertEq(effects0.length, 2); - assertEq(effects1.length, 2); + assertEq(effects0.length, 1); + assertEq(effects1.length, 1); // Check that both mons took additional 1/4 damage (256/4 = 64) // Total damage should be 16 (first round) + 32 (second round) + 64 (third round) = 112 @@ -517,11 +519,11 @@ contract EffectTest is Test, BattleHelper { // Alice and Bob both select attacks again to increase burn degree to maximum _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); - // Check that both mons still have an effect length of 2 + // Check that both mons still carry exactly the status entry (effects0,) = engine.getEffects(battleKey, 0, 0); (effects1,) = engine.getEffects(battleKey, 1, 0); - assertEq(effects0.length, 2); - assertEq(effects1.length, 2); + assertEq(effects0.length, 1); + assertEq(effects1.length, 1); // Check that both mons took another 1/4 damage (max burn degree) // Total damage should be 16 (first round) + 32 (second round) + 64 (third round) + 64 (fourth round) = 176 diff --git a/test/effects/StatBoosts.t.sol b/test/effects/StatBoosts.t.sol index 28298890..76bb70f3 100644 --- a/test/effects/StatBoosts.t.sol +++ b/test/effects/StatBoosts.t.sol @@ -138,17 +138,11 @@ contract StatBoostsTest is Test, BattleHelper { int32 boostedStat = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName(statIndex)); assertEq(boostedStat, initialStat + 10, "Stat should be boosted by 10%"); - // Verify the effect was added to Alice's mon - (EffectInstance[] memory effects,) = engine.getEffects(battleKey, 0, 0); - bool foundEffect = false; - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == STAT_BOOST_ADDRESS) { - foundEffect = true; - break; - } - } - assertTrue(foundEffect, "Stat Boost effect should be added to mon's effects"); - uint256 effectCount = effects.length; + // Verify the source landed in the mon's boost store (boost sources are no longer + // effect-list entries) + (BattleConfigView memory cfgView,) = engine.getBattle(battleKey); + assertEq(cfgView.p0StatBoosts[0].length, 1, "Stat Boost source should be in the boost store"); + uint256 sourceCount = cfgView.p0StatBoosts[0].length; // 2. Apply another boost (+10) to the same stat console.log("2. Applying additional 1% boost to Alice's mon"); @@ -167,9 +161,9 @@ contract StatBoostsTest is Test, BattleHelper { int32 furtherBoostedStat = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName(statIndex)); assertEq(furtherBoostedStat, initialStat + 21, "Stat should be boosted by 21% total"); - // Verify no duplicate effect was added - (effects,) = engine.getEffects(battleKey, 0, 0); - assertEq(effects.length, effectCount, "No duplicate effect should be added"); + // Verify no duplicate source was added (same caller merges into one source) + (cfgView,) = engine.getBattle(battleKey); + assertEq(cfgView.p0StatBoosts[0].length, sourceCount, "No duplicate source should be added"); // Switch out the mon console.log("4. Switching out Alice's mon"); @@ -184,16 +178,9 @@ contract StatBoostsTest is Test, BattleHelper { 0 // Bob does nothing ); - // Verify the effect was removed - (effects,) = engine.getEffects(battleKey, 0, 1); - foundEffect = false; - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == STAT_BOOST_ADDRESS) { - foundEffect = true; - break; - } - } - assertFalse(foundEffect, "Stat Boost effect should be removed after switching out"); + // Verify the temp source expired off the switched-out mon's boost store + (cfgView,) = engine.getBattle(battleKey); + assertEq(cfgView.p0StatBoosts[0].length, 0, "Stat Boost source should be removed after switching out"); // 5. Switch back to the original mon and verify stat is reset _commitRevealExecuteForAliceAndBob( @@ -310,16 +297,9 @@ contract StatBoostsTest is Test, BattleHelper { int32 boostedStat = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName(statIndices[i])); assertEq(boostedStat, 2, "Stat should be boosted by +2"); - // Verify the effect was added - (EffectInstance[] memory statEffects,) = engine.getEffects(battleKey, 0, 0); - bool foundStatEffect = false; - for (uint256 j = 0; j < statEffects.length; j++) { - if (address(statEffects[j].effect) == STAT_BOOST_ADDRESS) { - foundStatEffect = true; - break; - } - } - assertTrue(foundStatEffect, "Stat Boost effect should be added for each stat"); + // Verify the source is in the boost store + (BattleConfigView memory statView,) = engine.getBattle(battleKey); + assertTrue(statView.p0StatBoosts[0].length > 0, "Stat Boost source should be added for each stat"); } // Switch out and verify all effects are removed @@ -333,14 +313,9 @@ contract StatBoostsTest is Test, BattleHelper { 0 // Bob does nothing ); - // Verify all effects were removed - (EffectInstance[] memory effectsAfterSwitch,) = engine.getEffects(battleKey, 0, 1); - for (uint256 i = 0; i < effectsAfterSwitch.length; i++) { - assertFalse( - address(effectsAfterSwitch[i].effect) == STAT_BOOST_ADDRESS, - "No Stat Boost effects should remain after switching out" - ); - } + // Verify all temp sources expired off the switched-out mon + (BattleConfigView memory afterView,) = engine.getBattle(battleKey); + assertEq(afterView.p0StatBoosts[0].length, 0, "No Stat Boost sources should remain after switching out"); } function test_permanentTempStatBoostInteraction() public { diff --git a/test/mons/AuroxTest.sol b/test/mons/AuroxTest.sol index f233a06d..86737c27 100644 --- a/test/mons/AuroxTest.sol +++ b/test/mons/AuroxTest.sol @@ -809,10 +809,11 @@ contract AuroxTest is Test, BattleHelper { // Alice uses volatile punch, Bob does nothing _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); - // Verify that Bob's mon index 0 has frostbite (first effect is stat boost) + // Verify that Bob's mon index 0 has frostbite (its debuff lives in the boost store, + // so the status is the only effect entry) (EffectInstance[] memory effects,) = engine.getEffects(battleKey, 1, 0); - assertEq(address(effects[1].effect), address(frostbiteStatus), "Bob's mon should have frostbite"); + assertEq(address(effects[0].effect), address(frostbiteStatus), "Bob's mon should have frostbite"); // Precomputed seed triggers burn for Bob (player 1) mockOracle.setRNG(1); @@ -820,8 +821,8 @@ contract AuroxTest is Test, BattleHelper { // Alice does nothing, Bob uses volatile punch _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, 0); - // Verify that Alice's mon index 0 has burn (first effect is stat boost) + // Verify that Alice's mon index 0 has burn (effects,) = engine.getEffects(battleKey, 0, 0); - assertEq(address(effects[1].effect), address(burnStatus), "Alice's mon should have burn"); + assertEq(address(effects[0].effect), address(burnStatus), "Alice's mon should have burn"); } } From db520bbc424b5a81b7cf71b544cb17c0e75109be Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:04:23 -0700 Subject: [PATCH 09/41] wip --- docs/stomp_2.md | 61 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/docs/stomp_2.md b/docs/stomp_2.md index 230314df..3939e4ec 100644 --- a/docs/stomp_2.md +++ b/docs/stomp_2.md @@ -8,60 +8,81 @@ I genuinely believe Stomp is one of the most interesting, complex, and fun exper Here is what is ALREADY LIVE TODAY, THAT YOU CAN TRY OUT, ENTIRELY ONCHAIN: -First, the core game loop. +First, the core game loop. What even **is** Stomp? -Stomp is a turn based game 2 player game. It borrows heavy inspiration from Pokemon. Each turn, you choose a move, your opponent chooses a move, and then they resolve. You might do some damage, you might heal some damage, you might change which monster you have out. +Stomp is a turn-based RPG game. It borrows heavy inspiration from Pokemon. Each turn, you choose a move, your opponent chooses a move, and then they resolve. You might do some damage, you might heal some damage, you might change which monster you have out. -You can play a full game across a variety of CPU difficulties, with basically zero perceived latency. The game engine is transpiled to TypeScript (from Solidity!!!) so you get instant feedback. There's a rich and varied set of mechanics, and it all costs less than $0.01 in ETH! (Around 2-5 million gas for the nerds.) +SCREENSHOT HERE + +You can play a full game across a variety of CPU difficulties, with basically zero perceived latency. The game engine run both locally and onchain so you get instant feedback. There's a rich and varied set of mechanics, and it all costs less than $0.01 in ETH to get set up and play! *1 If you've played JRPGs or turn-based games, a lot of the mechanics may be familiar to you. There's a resource system, various types of damage (damage over time, delayed damage, elemental charts), and also a few unique ideas (some mons can resurrect, others can target any mon, etc. etc.) INSERT GAME GIF -Okay, but playing against bots might be boring. What if you want to play against a friend? +GETTING STARTED -You can do that! Stomp lets you run 1v1 games against anyone! Play live against another friend, moves still settle onchain, and everyone else can spectate! +To make it as easy as possible to get started trying out Stomp, I've snapshotted a list of Twitter mutuals to give you FREE GAS to start. -INSERT 1v1 GAME GIF +If you're on the list here (LINK), just link your Twitter during onboarding, and it'll automatically faucet you up! +Otherwise, you only need around 0.00001 ETH or less to get started. +Once you're set up, you can pick your initial starting mon: -------- +STARTER IMAGES + +- Ghouliath allows you to spam various status effects, and it has a powerful ability to resurrect itself. +- Inutia is a supporter that also boosts up the rest of your team. +- Malalien is a glass cannon that will flip your opponents off. +Then, you'll immediately get to roll the gacha for the remaining 3 mons. Once you have a team of 4, you're ready to start battling! +GACHA IMAGES +For your first match, Battle CPU will let you play immediately. If you want a challenge, you can select higher tiers, but Easy CPU is best if you haven't played before. +During your turn, you'll first decide which mon to send out. Then, you have a variety of moves to choose from, which can affect the battle in different ways. Hovering over will give you a preview of what the effects will be. All moves have a stamina cost, similar to mana in other RPGs. So unlike the PP system in Pokemon, a shared resource system means you decide when to use your strongest moves. If you're out of stamina, you can take a Rest action for your turn to regain some more. +You win if you can KO all of the opposing team's mons. Once you finish, your mons will gain some experience, and you'll earn enough gacha points for a another roll! +BATTLE RESULTS SCREEN +Okay, but playing against bots is boring. What if you want to play against a friend? +We got you covered! Stomp lets you run 1v1 games against anyone! Play live against another friend, moves settle onchain, and everyone else can spectate! +INSERT 1v1 GAME GIF + +Okay, but you're on mobile. What if you want to play on the go? -Remember when we dreamed of fully on-chain battles with collectibles, expansive worlds, and +We got you covered! Stomp is mobile-friendly, and you can play every mode everywhere. -It's live! +INSERT MOBILE GAME -Play against CPU, instant feedback, settle onchain. Full expressiveness, weird mechanics. And an entire game costs less than $0.01 in gas! +Okay, but you've already played 1v1 games a lot. What if you want more strategy and depth? What if you want to battle your opponents 2 at a time? -Okay, but what if you want to play against your friends? +We got you covered! Stomp supports both singles and doubles game modes! With doubles, you have even more depth and strategies, and probably lots of busted combos. Help me break the game! -It's live! Play live against another friend, moves settle onchain, everyone else can spectate, hook into the events. +INSERT DOUBLES GIF -Okay, but what if you're a huge friend group and want to play 2v2? +Okay, but games are only fun if they have something **at stake**. What if you want to wager on the outcome with your friends to both earn their glory and their riches? -It's live! Even more strategy and depth, probably lots of busted combos. Help me break the game! +We got you covered! Anyone can participate on wagering for PVP battles. Grab your friends and run your own tournament brackets! -Okay, but what if you want to play co-op with just one other friend instead of PVP? +INSERT WAGERS GIF -That's live too! +Okay, but the real depth is the progression. What if you want unlockables and customization to theorycraft all sorts of combos? -Okay, but what if you don't care about the game modes. You just want unlockables and customization to theory-craft? +We got you covered! There's 13 mons live today (with more coming soon!), each with their own completely unique mechanics, unlockable moves, stat upgrades, quests, ranked ladder, it's all there! -It's live! 13 mons to choose from, all wholly unique, unlockable stat upgrades, unlockables moves, it's all there! +FOR THE NERDS -Okay, but what if you only care if you and your friend can bet $10 on the outcome? +How do we keep Stomp fast and cheap? What actually happens under the hood? -It's live! Wagers are shipped, and anyone can participate. +*1 (around 2-4 million gas, for the nerds) + +------- Okay, but what if you want to make your own CPU, run agents and have it all happen automatically? From e6ed65a6884aa9d595ae832fe22df47d0046e081 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:52:04 -0700 Subject: [PATCH 10/41] cap stat boosts --- drool/abilities.csv | 4 +- snapshots/DoublesGasBenchmark.json | 40 +- snapshots/EngineOptimizationTest.json | 4 +- snapshots/FullyOptimizedInlineGasTest.json | 44 +- snapshots/MatchmakerTest.json | 6 +- snapshots/ProdPvPGasBenchmark.json | 12 +- snapshots/StartBattleGasTest.json | 32 +- src/Engine.sol | 15 +- src/lib/StatBoostLib.sol | 31 +- test/EngineTest.sol | 32 ++ test/StatBoostAccParity.t.sol | 43 ++ test/mocks/RawDamageMove.sol | 62 +++ transpiler/strategies-rs/src/analysis.rs | 105 +++- transpiler/strategies-rs/src/arena.rs | 163 ++++++- transpiler/strategies-rs/src/bin/analyze.rs | 106 ++-- transpiler/strategies-rs/src/bin/findpanic.rs | 78 +++ transpiler/strategies-rs/src/bin/montrace.rs | 99 ++++ transpiler/strategies-rs/src/bin/statpeak.rs | 71 +++ transpiler/strategies-rs/src/doubles.rs | 456 +++++++++++++++++- transpiler/strategies-rs/src/game.rs | 39 +- transpiler/strategies-rs/src/sim.rs | 7 + 21 files changed, 1318 insertions(+), 131 deletions(-) create mode 100644 test/mocks/RawDamageMove.sol create mode 100644 transpiler/strategies-rs/src/bin/findpanic.rs create mode 100644 transpiler/strategies-rs/src/bin/montrace.rs create mode 100644 transpiler/strategies-rs/src/bin/statpeak.rs diff --git a/drool/abilities.csv b/drool/abilities.csv index df29cf2c..8950a531 100644 --- a/drool/abilities.csv +++ b/drool/abilities.csv @@ -3,8 +3,8 @@ Rise From The Grave,Ghouliath,"Once per game, after being KO'ed, Ghouliath reviv Tinderclaws,Embursa,"After every move, Embursa has a 33% chance of burning itself. If burned, gain a 50% SpATK boost. When resting, Embursa heals from burn." Actus Reus,Malalien,"If Malalien KO's an opposing mon, they gain an Indictment. Upon KO, if they have an Indictment, Malalien cripples their murderer." Baselight,Iblivion,"Whenever Iblivion takes damage, it gains a Baselight point. Points can be used to empower moves." -Carrot Harvest,Sofabbi,"At the end of every turn, Sofabbi has a 50% chance of regaining 1 stamina. This can bank stamina past the maximum." -Post-Workout,Pengym,"On swap out, if Pengym has a status condition, heal it (any status). If healed, Pengym regains 1 stamina, which can exceed the maximum." +Carrot Harvest,Sofabbi,"At the end of every turn, Sofabbi has a 50% chance of regaining 1 stamina." +Post-Workout,Pengym,"On swap out, if Pengym has a status condition, heal it (any status). If healed, Pengym regains 1 stamina." Angery,Gorillax,"Each time Gorillax takes damage, they get Angerier. At 3 stacks, they heal for 16.6% of max HP." Preemptive Shock,Volthare,"When Volthare swaps in, they deal a small amount of damage to the opposing mon." Interweaving,Inutia,"When Inutia swaps in, the opposing mon's ATK decreases 15%. When Inutia swaps out, the opposing mon's SpATK decreases 15%." diff --git a/snapshots/DoublesGasBenchmark.json b/snapshots/DoublesGasBenchmark.json index 94b52077..06149a4b 100644 --- a/snapshots/DoublesGasBenchmark.json +++ b/snapshots/DoublesGasBenchmark.json @@ -1,32 +1,32 @@ { - "Doubles_Batch12_coldGas": "829428", - "Doubles_Batch12_coldSload": "36", + "Doubles_Batch12_coldGas": "772506", + "Doubles_Batch12_coldSload": "34", "Doubles_Batch12_extraAccounts": "6", "Doubles_Batch12_noop": "23", "Doubles_Batch12_nzToNz": "33", - "Doubles_Batch12_prodStorageGas": "543300", - "Doubles_Batch12_totalSload": "1698", - "Doubles_Batch12_totalSstore": "67", - "Doubles_Batch12_zToNz": "9", - "Doubles_Drain6_coldGas": "538845", - "Doubles_Drain6_coldSload": "42", + "Doubles_Batch12_prodStorageGas": "498900", + "Doubles_Batch12_totalSload": "1694", + "Doubles_Batch12_totalSstore": "65", + "Doubles_Batch12_zToNz": "7", + "Doubles_Drain6_coldGas": "487103", + "Doubles_Drain6_coldSload": "40", "Doubles_Drain6_extraAccounts": "7", "Doubles_Drain6_noop": "11", "Doubles_Drain6_nzToNz": "28", - "Doubles_Drain6_prodStorageGas": "462600", - "Doubles_Drain6_totalSload": "902", - "Doubles_Drain6_totalSstore": "50", - "Doubles_Drain6_zToNz": "9", - "Doubles_KitBatch10_coldGas": "1091153", - "Doubles_KitBatch10_coldSload": "29", + "Doubles_Drain6_prodStorageGas": "418200", + "Doubles_Drain6_totalSload": "898", + "Doubles_Drain6_totalSstore": "48", + "Doubles_Drain6_zToNz": "7", + "Doubles_KitBatch10_coldGas": "1026002", + "Doubles_KitBatch10_coldSload": "26", "Doubles_KitBatch10_extraAccounts": "3", "Doubles_KitBatch10_noop": "21", - "Doubles_KitBatch10_nzToNz": "61", - "Doubles_KitBatch10_prodStorageGas": "658000", - "Doubles_KitBatch10_totalSload": "2061", - "Doubles_KitBatch10_totalSstore": "93", - "Doubles_KitBatch10_zToNz": "10", - "Doubles_Stage6_coldGas": "174477", + "Doubles_KitBatch10_nzToNz": "60", + "Doubles_KitBatch10_prodStorageGas": "609600", + "Doubles_KitBatch10_totalSload": "2045", + "Doubles_KitBatch10_totalSstore": "90", + "Doubles_KitBatch10_zToNz": "8", + "Doubles_Stage6_coldGas": "174663", "Doubles_Stage6_coldSload": "18", "Doubles_Stage6_extraAccounts": "6", "Doubles_Stage6_noop": "0", diff --git a/snapshots/EngineOptimizationTest.json b/snapshots/EngineOptimizationTest.json index d0824947..7fad3bc2 100644 --- a/snapshots/EngineOptimizationTest.json +++ b/snapshots/EngineOptimizationTest.json @@ -1,4 +1,4 @@ { - "ExternalStaminaRegen": "340993", - "InlineStaminaRegen": "798337" + "ExternalStaminaRegen": "341383", + "InlineStaminaRegen": "802104" } \ No newline at end of file diff --git a/snapshots/FullyOptimizedInlineGasTest.json b/snapshots/FullyOptimizedInlineGasTest.json index 0d94466d..001ef295 100644 --- a/snapshots/FullyOptimizedInlineGasTest.json +++ b/snapshots/FullyOptimizedInlineGasTest.json @@ -1,29 +1,29 @@ { - "Fast_Battle1_coldGas": "1245198", - "Fast_Battle1_coldSload": "215", + "Fast_Battle1_coldGas": "1103813", + "Fast_Battle1_coldSload": "197", "Fast_Battle1_extraAccounts": "52", - "Fast_Battle1_noop": "28", - "Fast_Battle1_nzToNz": "68", - "Fast_Battle1_prodStorageGas": "1423700", - "Fast_Battle1_totalSload": "1017", - "Fast_Battle1_totalSstore": "123", - "Fast_Battle1_zToNz": "27", - "Fast_Battle2_coldGas": "1111541", - "Fast_Battle2_coldSload": "225", + "Fast_Battle1_noop": "23", + "Fast_Battle1_nzToNz": "69", + "Fast_Battle1_prodStorageGas": "1292800", + "Fast_Battle1_totalSload": "981", + "Fast_Battle1_totalSstore": "114", + "Fast_Battle1_zToNz": "22", + "Fast_Battle2_coldGas": "1017400", + "Fast_Battle2_coldSload": "205", "Fast_Battle2_extraAccounts": "52", - "Fast_Battle2_noop": "35", - "Fast_Battle2_nzToNz": "87", - "Fast_Battle2_prodStorageGas": "1273700", - "Fast_Battle2_totalSload": "1109", - "Fast_Battle2_totalSstore": "138", - "Fast_Battle2_zToNz": "15", - "Fast_Battle3_coldGas": "781622", - "Fast_Battle3_coldSload": "215", + "Fast_Battle2_noop": "25", + "Fast_Battle2_nzToNz": "90", + "Fast_Battle2_prodStorageGas": "1205000", + "Fast_Battle2_totalSload": "1061", + "Fast_Battle2_totalSstore": "129", + "Fast_Battle2_zToNz": "13", + "Fast_Battle3_coldGas": "738346", + "Fast_Battle3_coldSload": "197", "Fast_Battle3_extraAccounts": "52", - "Fast_Battle3_noop": "39", + "Fast_Battle3_noop": "30", "Fast_Battle3_nzToNz": "81", - "Fast_Battle3_prodStorageGas": "965400", - "Fast_Battle3_totalSload": "1017", - "Fast_Battle3_totalSstore": "123", + "Fast_Battle3_prodStorageGas": "931200", + "Fast_Battle3_totalSload": "981", + "Fast_Battle3_totalSstore": "114", "Fast_Battle3_zToNz": "2" } \ No newline at end of file diff --git a/snapshots/MatchmakerTest.json b/snapshots/MatchmakerTest.json index 7569d0fa..aac0ba5a 100644 --- a/snapshots/MatchmakerTest.json +++ b/snapshots/MatchmakerTest.json @@ -1,5 +1,5 @@ { - "Accept1": "331714", - "Accept2": "33834", - "Propose1": "175456" + "Accept1": "332643", + "Accept2": "33887", + "Propose1": "175509" } \ No newline at end of file diff --git a/snapshots/ProdPvPGasBenchmark.json b/snapshots/ProdPvPGasBenchmark.json index d019bc97..b64921b6 100644 --- a/snapshots/ProdPvPGasBenchmark.json +++ b/snapshots/ProdPvPGasBenchmark.json @@ -1,5 +1,5 @@ { - "BuiltinBare_5Turns_coldGas": "290006", + "BuiltinBare_5Turns_coldGas": "291871", "BuiltinBare_5Turns_coldSload": "70", "BuiltinBare_5Turns_extraAccounts": "15", "BuiltinBare_5Turns_noop": "10", @@ -8,7 +8,7 @@ "BuiltinBare_5Turns_totalSload": "435", "BuiltinBare_5Turns_totalSstore": "45", "BuiltinBare_5Turns_zToNz": "2", - "BuiltinHooked_5Turns_coldGas": "290824", + "BuiltinHooked_5Turns_coldGas": "292689", "BuiltinHooked_5Turns_coldSload": "70", "BuiltinHooked_5Turns_extraAccounts": "15", "BuiltinHooked_5Turns_noop": "10", @@ -17,16 +17,16 @@ "BuiltinHooked_5Turns_totalSload": "435", "BuiltinHooked_5Turns_totalSstore": "45", "BuiltinHooked_5Turns_zToNz": "2", - "Builtin_Drain3_coldGas": "144889", + "Builtin_Drain3_coldGas": "146377", "Builtin_Drain3_coldSload": "20", "Builtin_Drain3_extraAccounts": "3", "Builtin_Drain3_noop": "7", "Builtin_Drain3_nzToNz": "15", - "Builtin_Drain3_prodStorageGas": "155400", - "Builtin_Drain3_totalSload": "234", + "Builtin_Drain3_prodStorageGas": "155600", + "Builtin_Drain3_totalSload": "236", "Builtin_Drain3_totalSstore": "24", "Builtin_Drain3_zToNz": "2", - "Builtin_Stage3_coldGas": "90154", + "Builtin_Stage3_coldGas": "90313", "Builtin_Stage3_coldSload": "9", "Builtin_Stage3_extraAccounts": "3", "Builtin_Stage3_noop": "0", diff --git a/snapshots/StartBattleGasTest.json b/snapshots/StartBattleGasTest.json index 51d9e509..2e4f59c7 100644 --- a/snapshots/StartBattleGasTest.json +++ b/snapshots/StartBattleGasTest.json @@ -1,29 +1,29 @@ { - "StartBattle_Cold_coldGas": "1278992", + "StartBattle_Cold_coldGas": "1282545", "StartBattle_Cold_coldSload": "119", "StartBattle_Cold_extraAccounts": "3", "StartBattle_Cold_noop": "8", - "StartBattle_Cold_nzToNz": "2", - "StartBattle_Cold_prodStorageGas": "1494600", - "StartBattle_Cold_totalSload": "140", - "StartBattle_Cold_totalSstore": "67", + "StartBattle_Cold_nzToNz": "18", + "StartBattle_Cold_prodStorageGas": "1542600", + "StartBattle_Cold_totalSload": "156", + "StartBattle_Cold_totalSstore": "83", "StartBattle_Cold_zToNz": "57", - "StartBattle_WarmSame_coldGas": "225986", + "StartBattle_WarmSame_coldGas": "229539", "StartBattle_WarmSame_coldSload": "129", "StartBattle_WarmSame_extraAccounts": "3", - "StartBattle_WarmSame_noop": "56", + "StartBattle_WarmSame_noop": "72", "StartBattle_WarmSame_nzToNz": "5", - "StartBattle_WarmSame_prodStorageGas": "473700", - "StartBattle_WarmSame_totalSload": "146", - "StartBattle_WarmSame_totalSstore": "66", + "StartBattle_WarmSame_prodStorageGas": "476900", + "StartBattle_WarmSame_totalSload": "162", + "StartBattle_WarmSame_totalSstore": "82", "StartBattle_WarmSame_zToNz": "4", - "StartBattle_WarmSteady_coldGas": "225986", + "StartBattle_WarmSteady_coldGas": "229539", "StartBattle_WarmSteady_coldSload": "129", "StartBattle_WarmSteady_extraAccounts": "3", - "StartBattle_WarmSteady_noop": "32", - "StartBattle_WarmSteady_nzToNz": "29", - "StartBattle_WarmSteady_prodStorageGas": "540900", - "StartBattle_WarmSteady_totalSload": "146", - "StartBattle_WarmSteady_totalSstore": "66", + "StartBattle_WarmSteady_noop": "40", + "StartBattle_WarmSteady_nzToNz": "37", + "StartBattle_WarmSteady_prodStorageGas": "566500", + "StartBattle_WarmSteady_totalSload": "162", + "StartBattle_WarmSteady_totalSstore": "82", "StartBattle_WarmSteady_zToNz": "4" } \ No newline at end of file diff --git a/src/Engine.sol b/src/Engine.sol index 651c0cae..28342ce6 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -2604,11 +2604,22 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return; } - // If sentinel, replace with -damage; otherwise subtract damage - monState.hpDelta = (monState.hpDelta == CLEARED_MON_STATE_SENTINEL) ? -damage : monState.hpDelta - damage; + // If sentinel, replace with -damage; otherwise subtract damage. The damage formula clamps at + // type(int32).max, so a large hit on an already-damaged mon can carry the delta past int32 — + // a revert that would wedge the battle. Do the subtraction unchecked and detect the wrap: + // damage is positive, so a result that did not decrease means it overflowed. + int32 priorDelta = monState.hpDelta; + int32 newDelta; + unchecked { + newDelta = (priorDelta == CLEARED_MON_STATE_SENTINEL) ? -damage : priorDelta - damage; + } // Set KO flag if the total hpDelta is greater than the original mon HP uint32 baseHp = _getTeamMon(config, playerIndex, monIndex).stats.hp; + if (priorDelta != CLEARED_MON_STATE_SENTINEL && newDelta > priorDelta) { + newDelta = -int32(baseHp); // past fully-dead; the exact magnitude carries no meaning + } + monState.hpDelta = newDelta; if (monState.hpDelta + int32(baseHp) <= 0) { monState.isKnockedOut = true; _setMonKO(config, playerIndex, monIndex); diff --git a/src/lib/StatBoostLib.sol b/src/lib/StatBoostLib.sol index 087e0cb8..b3506b98 100644 --- a/src/lib/StatBoostLib.sol +++ b/src/lib/StatBoostLib.sol @@ -58,6 +58,20 @@ library StatBoostLib { mul = (inst & 0x1) == 1; } + /// @dev Scaling factor for one boost application, as a numerator over DENOM. A Divide can never + /// drive this to 0: the aggregation stores a stat as (numerator, count), so a zeroed + /// numerator is indistinguishable from an empty lane, and a divide-out of a 0 factor cannot + /// be inverted. A reduction of DENOM% or more therefore saturates at the smallest + /// representable factor rather than reverting (a revert here would wedge the battle — note + /// `DENOM - boostPercent` underflows for the uint8 range above 100). Every live move uses + /// 15-50%, so this is defensive only. + function boostFactor(uint256 boostPercent, bool isMul) internal pure returns (uint256) { + if (isMul) { + return DENOM + boostPercent; + } + return boostPercent >= DENOM ? 1 : DENOM - boostPercent; + } + /// @dev Fold a source word into (mulIn = true) or out of (mulIn = false) the accumulator. /// Divide-out is exact by construction (the factors were multiplied in); a lane drained /// to count 0 resets its numerator to 0 (canonical empty — finalize reads base then). @@ -71,7 +85,7 @@ library StatBoostLib { if (cnt == 0) { continue; } - uint256 factor = mul ? DENOM + pct : DENOM - pct; + uint256 factor = boostFactor(pct, mul); uint256 shift = k * ACC_LANE_BITS; uint256 num = (acc >> shift) & ACC_NUM_MASK; uint256 laneCnt = (acc >> (shift + 48)) & ACC_CNT_MASK; @@ -90,9 +104,8 @@ library StatBoostLib { } laneCnt += cnt; } else { - // factor 0 lanes (pct == 100 Divide) zero the product permanently; the Engine - // never reaches here for them — mulIn already failed the fit check (num 0 is - // fine to multiply but a later divide-out would div-by-zero), so guard: + // `boostFactor` floors at 1, so the div-by-zero arm is unreachable; kept as a cheap + // assertion that the invariant holds for anything folded in here. if (factor == 0 || laneCnt < cnt) { return (acc, false); } @@ -258,10 +271,12 @@ library StatBoostLib { uint32[5] memory numBoostsPerStat, uint256[5] memory accumulatedNumeratorPerStat ) private pure { - uint256 existingStatValue = (accumulatedNumeratorPerStat[k] == 0) - ? baseStats[k] - : accumulatedNumeratorPerStat[k]; - uint256 scalingFactor = isMul ? DENOM + boostPercent : DENOM - boostPercent; + // Seed off the boost COUNT, not the numerator: a 100% Divide gives factor 0, so a numerator + // of 0 is a legitimately zeroed stat, not an empty lane. Reading it as empty would re-seed + // from base and resurrect the stat, diverging from the accumulator (which seeds off count). + uint256 existingStatValue = + (numBoostsPerStat[k] == 0) ? baseStats[k] : accumulatedNumeratorPerStat[k]; + uint256 scalingFactor = boostFactor(boostPercent, isMul); unchecked { accumulatedNumeratorPerStat[k] = existingStatValue * (scalingFactor ** boostCount); numBoostsPerStat[k] += uint32(boostCount); diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 3ad09c23..8c2dac23 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -29,6 +29,7 @@ import {GlobalEffectAttack} from "./mocks/GlobalEffectAttack.sol"; import {InstantDeathEffect} from "./mocks/InstantDeathEffect.sol"; import {InstantDeathOnSwitchInEffect} from "./mocks/InstantDeathOnSwitchInEffect.sol"; import {MockRandomnessOracle} from "./mocks/MockRandomnessOracle.sol"; +import {RawDamageMove} from "./mocks/RawDamageMove.sol"; import {SelfSwitchAndDamageMove} from "./mocks/SelfSwitchAndDamageMove.sol"; import {IEngineHook} from "../src/IEngineHook.sol"; @@ -2705,6 +2706,37 @@ contract EngineTest is Test, BattleHelper { assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); } + // `AttackCalculator._calculateDamageCore` clamps a huge rawDamage to `type(int32).max` and hands + // it back as a legitimate damage value, so `_dealDamageInternal` must absorb it. Subtracting it + // from an already-negative hpDelta underflows int32 — which in 0.8 reverts, wedging the battle. + function test_dealDamageAbsorbsClampedMaxWithoutUnderflow() public { + uint256[] memory moves = new uint256[](2); + moves[0] = uint256(uint160(address(new RawDamageMove(2)))); + moves[1] = uint256(uint160(address(new RawDamageMove(type(int32).max)))); + Mon memory mon = _createMon(); + mon.moves = moves; + mon.stats.hp = 100; + Mon[] memory team = new Mon[](2); + team[0] = mon; + team[1] = mon; + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + bytes32 battleKey = _startBattle(engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); + _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); + + // Chip both actives so neither hpDelta is the cleared sentinel any more. + _commitRevealExecuteForAliceAndBob(battleKey, 0, 0, uint16(0), uint16(0)); + assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp), -2); + + // The clamped max lands on a mon already sitting at a negative hpDelta. + _commitRevealExecuteForAliceAndBob(battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); + + // It should read as a KO, not revert and not wrap to a positive hpDelta. + assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), 1); + // Floored at exactly -baseHp: fully dead, and still a value int32 can hold. + assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp), -int32(uint32(mon.stats.hp))); + } + function test_editEffect() public { EditEffectAttack editEffectAttack = new EditEffectAttack(); DummyStatus d = new DummyStatus(); diff --git a/test/StatBoostAccParity.t.sol b/test/StatBoostAccParity.t.sol index 78d3a611..c1222fa9 100644 --- a/test/StatBoostAccParity.t.sol +++ b/test/StatBoostAccParity.t.sol @@ -50,6 +50,49 @@ contract StatBoostAccParity is Test { _run(3, 12, 41968); } + /// Regression: a 100% Divide zeroes a lane's numerator. The recompute used to read a 0 + /// numerator as "lane not seeded yet" and restart from baseStats, resurrecting the stat and + /// diverging from the accumulator (which seeds off the boost count). + function test_zeroedLaneDoesNotReseedFromBase() public pure { + _run(14, 179, 9452); + } + + /// boostPercent is a uint8, so a Divide can be handed anything up to 255. `DENOM - pct` would + /// underflow and revert (wedging the battle mid-move), and a factor of 0 would zero the numerator + /// into a state the (numerator, count) aggregation cannot represent or invert. Both saturate. + function test_divideSaturatesInsteadOfRevertingOrZeroing() public pure { + assertEq(StatBoostLib.boostFactor(50, false), 50, "ordinary divide"); + assertEq(StatBoostLib.boostFactor(99, false), 1, "max representable reduction"); + assertEq(StatBoostLib.boostFactor(100, false), 1, "100% divide floors, not zeroes"); + assertEq(StatBoostLib.boostFactor(255, false), 1, "uint8 max divide floors, not underflows"); + assertEq(StatBoostLib.boostFactor(50, true), 150, "multiply unaffected"); + + // End to end: a 255% Divide folds in, stays invertible, and both paths agree. + uint32[5] memory baseStats = [uint32(100), 100, 100, 100, 100]; + StatBoostToApply[] memory boosts = new StatBoostToApply[](1); + boosts[0] = StatBoostToApply({ + stat: MonStateIndexName(2), boostPercent: 255, boostType: StatBoostType.Divide + }); + bytes32[] memory words = new bytes32[](1); + words[0] = StatBoostLib.packBoostData(uint168(1), true, boosts); + + (uint256 acc, bool ok) = StatBoostLib.applyWordToAcc(0, words[0], baseStats, true); + assertTrue(ok, "extreme divide folds in"); + uint32[5] memory fast = StatBoostLib.finalizeAccStats(acc, baseStats); + uint32[5] memory ref = _referenceStats(words, type(uint256).max, baseStats); + for (uint256 k; k < 5; ++k) { + assertEq(fast[k], ref[k], "extreme divide parity"); + } + + // And it divides back out cleanly rather than refusing. + (uint256 acc2, bool ok2) = StatBoostLib.applyWordToAcc(acc, words[0], baseStats, false); + assertTrue(ok2, "extreme divide inverts"); + uint32[5] memory restored = StatBoostLib.finalizeAccStats(acc2, baseStats); + for (uint256 k; k < 5; ++k) { + assertEq(restored[k], baseStats[k], "removing the source restores base"); + } + } + function testFuzz_accMatchesRecompute(uint256 seed, uint8 numSourcesRaw, uint16 baseRaw) public pure { _run(seed, numSourcesRaw, baseRaw); } diff --git a/test/mocks/RawDamageMove.sol b/test/mocks/RawDamageMove.sol new file mode 100644 index 00000000..eb17f0b6 --- /dev/null +++ b/test/mocks/RawDamageMove.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../src/Constants.sol"; +import "../../src/Enums.sol"; +import "../../src/Structs.sol"; + +import {IEngine} from "../../src/IEngine.sol"; +import {TargetLib} from "../../src/lib/TargetLib.sol"; +import {IMoveSet} from "../../src/moves/IMoveSet.sol"; + +/// Deals an exact `int32` to the target via `dealDamage` — lets a test drive the raw damage value +/// the engine has to absorb, including the clamped `type(int32).max` the damage formula can produce. +contract RawDamageMove is IMoveSet { + int32 immutable DAMAGE; + + constructor(int32 damage) { + DAMAGE = damage; + } + + function name() external pure returns (string memory) { + return "Raw Damage Move"; + } + + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 targetBits, uint256 activesPacked, uint16, uint256) + external + { + uint256 defenderMonIndex = TargetLib.activeAt(activesPacked, TargetLib.lowestSlot(targetBits)); + engine.dealDamage((attackerPlayerIndex + 1) % 2, defenderMonIndex, DAMAGE); + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return DEFAULT_PRIORITY; + } + + function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { + return 0; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Fire; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Physical; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + pure + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: 0 + }); + } +} diff --git a/transpiler/strategies-rs/src/analysis.rs b/transpiler/strategies-rs/src/analysis.rs index 961d6df1..0c361050 100644 --- a/transpiler/strategies-rs/src/analysis.rs +++ b/transpiler/strategies-rs/src/analysis.rs @@ -9,7 +9,8 @@ use std::collections::HashMap; -use crate::arena::{build_specs, build_specs_with}; +use crate::arena::{build_doubles_specs, build_specs, build_specs_full, build_specs_with, STRAT_PAIRS}; +use crate::doubles::run_doubles_games_instrumented; use crate::game::{run_games_instrumented, InstrRecord, StrategyKind}; use crate::roster::{self, Roster}; @@ -55,6 +56,10 @@ pub struct MonAnalysis { pub decided: u32, pub draws: u32, pub errors: u32, + /// Doubles only: KOs both opposing slots aimed at, so no single attacker earns matrix credit. + pub ko_shared: u32, + /// Doubles only: KOs no opposing move aimed at (status, recoil, ally damage). + pub ko_incidental: u32, } /// Draw the same 4v4 field as the plain arena, run it instrumented, fold per-mon. @@ -80,6 +85,102 @@ pub fn run_mon_analysis_with( fold(&records) } +/// Singles analysis over the standard pilot basket with loadout rotation — each drafted slot equips +/// a uniform 4-subset of that mon's catalog instead of always lanes 0-3. +pub fn run_mon_analysis_rotated( + roster: &Roster, + games: usize, + wseed: u32, + seed_base: u32, + threads: usize, + pairs: Option<&[(StrategyKind, StrategyKind)]>, +) -> MonAnalysis { + let book = roster::address_book(); + let default_pairs: Vec<(StrategyKind, StrategyKind)> = STRAT_PAIRS + .iter() + .map(|&(p1, p0)| (StrategyKind::parse(p1).unwrap(), StrategyKind::parse(p0).unwrap())) + .collect(); + let pairs = pairs.unwrap_or(&default_pairs); + let (specs, _) = build_specs_full(roster, games, wseed, seed_base, pairs, true); + let records = run_games_instrumented(&specs, &book, threads); + fold(&records) +} + +/// Doubles per-mon attribution over the DIFF_PAIRS field. KOs are credited from the killer side's +/// target nibble; `shared` (both slots aimed at the victim) and `incidental` (nothing aimed at it) +/// are counted but left out of the matrix rather than guessed at. +pub fn run_doubles_mon_analysis( + roster: &Roster, + games: usize, + wseed: u32, + seed_base: u32, + threads: usize, + rotate: bool, + search_depth: u32, +) -> MonAnalysis { + let book = roster::address_book(); + let (specs, _) = build_doubles_specs(roster, games, wseed, seed_base, rotate, search_depth); + let records = run_doubles_games_instrumented(&specs, &book, threads); + + let mut per_mon: HashMap = HashMap::new(); + let mut ko_matrix: HashMap<(u32, u32), u32> = HashMap::new(); + let (mut n_games, mut decided, mut draws) = (0u32, 0u32, 0u32); + let (mut shared, mut incidental) = (0u32, 0u32); + + for rec in &records { + n_games += 1; + let (p0_res, p1_res): (i8, i8) = match rec.winner_side { + Some(0) => { decided += 1; (1, -1) } + Some(1) => { decided += 1; (-1, 1) } + _ => { draws += 1; (0, 0) } + }; + shared += rec.kos_shared; + incidental += rec.kos_incidental; + + for (slot, &id) in rec.p0_ids.iter().enumerate() { + let st = per_mon.entry(id).or_default(); + tally_game(st, p0_res); + add_active(st, p0_res, rec.active_turns_p0.get(slot).copied().unwrap_or(0) as u64); + } + for (slot, &id) in rec.p1_ids.iter().enumerate() { + let st = per_mon.entry(id).or_default(); + tally_game(st, p1_res); + add_active(st, p1_res, rec.active_turns_p1.get(slot).copied().unwrap_or(0) as u64); + } + + for ko in &rec.kos { + *ko_matrix.entry((ko.killer_id, ko.victim_id)).or_insert(0) += 1; + let killer_res = if ko.killer_seat == 0 { p0_res } else { p1_res }; + { + let ks = per_mon.entry(ko.killer_id).or_default(); + match killer_res { + 1 => ks.kos_dealt_won += 1, + -1 => ks.kos_dealt_lost += 1, + _ => {} + } + } + { + let vs = per_mon.entry(ko.victim_id).or_default(); + match -killer_res { + 1 => vs.kos_taken_won += 1, + -1 => vs.kos_taken_lost += 1, + _ => {} + } + } + } + } + MonAnalysis { + per_mon, + ko_matrix, + games: n_games, + decided, + draws, + errors: 0, + ko_shared: shared, + ko_incidental: incidental, + } +} + pub fn fold(records: &[Result]) -> MonAnalysis { let mut per_mon: HashMap = HashMap::new(); let mut ko_matrix: HashMap<(u32, u32), u32> = HashMap::new(); @@ -137,7 +238,7 @@ pub fn fold(records: &[Result]) -> MonAnalysis { } } - MonAnalysis { per_mon, ko_matrix, games: n_games, decided, draws, errors } + MonAnalysis { per_mon, ko_matrix, games: n_games, decided, draws, errors, ko_shared: 0, ko_incidental: 0 } } fn tally_game(st: &mut MonStat, res: i8) { diff --git a/transpiler/strategies-rs/src/arena.rs b/transpiler/strategies-rs/src/arena.rs index c7c5771f..72d0ef6b 100644 --- a/transpiler/strategies-rs/src/arena.rs +++ b/transpiler/strategies-rs/src/arena.rs @@ -81,6 +81,28 @@ pub fn build_specs(roster: &Roster, games: usize, wseed: u32, seed_base: u32) -> build_specs_with(roster, games, wseed, seed_base, &pairs) } +/// Every legal 4-move loadout for a mon: each 4-subset of its catalog, in lane order. Mons with a +/// catalog of 4 or fewer yield exactly one loadout (identical to [`build_team_mon`]), so rotation is +/// a no-op for them. +pub fn loadouts(m: &RosterMon) -> Vec { + if m.catalog.len() <= 4 { + return vec![build_team_mon(m)]; + } + crate::teams::combos(m.catalog.len(), 4) + .into_iter() + .map(|pick| Mon { + stats: m.stats.clone(), + ability: m.ability, + moves: pick.iter().map(|&i| m.catalog[i].word).collect(), + }) + .collect() +} + +/// Per-mon loadout tables for a rotated draft. Indexed by mon id. +fn loadout_table(roster: &Roster) -> std::collections::HashMap> { + roster.mons.iter().map(|m| (m.id, loadouts(m))).collect() +} + /// Like `build_specs` but with an explicit [p1, p0] strategy rotation (e.g. a single no-peek-vs-peek /// pair). Team draws stay identical per seed regardless of the pilots — matched drafts. pub fn build_specs_with( @@ -90,10 +112,37 @@ pub fn build_specs_with( seed_base: u32, pairs: &[(StrategyKind, StrategyKind)], ) -> (Vec, Vec) { - // Build each mon's default loadout once (13 mons), then clone per draft — avoids an O(n) find + build_specs_full(roster, games, wseed, seed_base, pairs, false) +} + +/// `build_specs_with` plus a `rotate` switch: when set, each drafted slot draws a uniform loadout +/// from that mon's full 4-subset set instead of always equipping catalog lanes 0-3. The loadout draw +/// runs on its own rng stream so team drafts stay bit-identical to an unrotated run at the same seed. +/// +/// Callers that map an equipped lane index back to a move name (trace narration via +/// `Roster::move_name`) must leave this off — with rotation the lane→catalog mapping is per-game. +pub fn build_specs_full( + roster: &Roster, + games: usize, + wseed: u32, + seed_base: u32, + pairs: &[(StrategyKind, StrategyKind)], + rotate: bool, +) -> (Vec, Vec) { + // Build each mon's loadout set once (13 mons), then clone per draft — avoids an O(n) find // and a fresh build_team_mon on every drafted slot across tens of thousands of games. - let built: std::collections::HashMap = roster.mons.iter().map(|m| (m.id, build_team_mon(m))).collect(); + let table = loadout_table(roster); let mut rng = Wrand::new(wseed); + let mut lrng = Wrand::new(wseed ^ 0x9e37_79b9); + let equip = |ids: &[u32], lrng: &mut Wrand| -> Vec { + ids.iter() + .map(|id| { + let opts = &table[id]; + let k = if rotate && opts.len() > 1 { (lrng.next() * opts.len() as f64) as usize } else { 0 }; + opts[k.min(opts.len() - 1)].clone() + }) + .collect() + }; let mut specs = Vec::with_capacity(games); let mut pair_of = Vec::with_capacity(games); for i in 0..games { @@ -102,12 +151,14 @@ pub fn build_specs_with( // workload draw order: teams[0] (→ p0) first, teams[1] (→ p1) second. let p0_ids = draw_team(roster, &mut rng); let p1_ids = draw_team(roster, &mut rng); + let p0_team = equip(&p0_ids, &mut lrng); + let p1_team = equip(&p1_ids, &mut lrng); specs.push(GameSpec { seed: seed_base.wrapping_add(i as u32), max_turns: MAX_TURNS, mons_per_team: TEAM_SIZE as u64, - p0_team: p0_ids.iter().map(|&id| built[&id].clone()).collect(), - p1_team: p1_ids.iter().map(|&id| built[&id].clone()).collect(), + p0_team, + p1_team, p0_ids, p1_ids, p0_strategy: p0s, @@ -265,11 +316,28 @@ pub fn doubles_search_winrate(roster: &Roster, depth: u32, games: usize, wseed: if decisive == 0 { 0.0 } else { p1 as f64 / decisive as f64 } } -pub fn run_doubles_arena(roster: &Roster, games: usize, wseed: u32, seed_base: u32, threads: usize) -> Vec { - let book = roster::address_book(); - let mon = |id: u32| roster.mons.iter().find(|m| m.id == id).expect("drawn mon id in roster"); - +/// Build the doubles field (DIFF_PAIRS rotation, two random team draws each). `rotate` draws each +/// slot's loadout from that mon's full 4-subset set; see [`build_specs_full`]. +pub fn build_doubles_specs( + roster: &Roster, + games: usize, + wseed: u32, + seed_base: u32, + rotate: bool, + search_depth: u32, +) -> (Vec, Vec) { + let table = loadout_table(roster); let mut rng = Wrand::new(wseed); + let mut lrng = Wrand::new(wseed ^ 0x9e37_79b9); + let equip = |ids: &[u32], lrng: &mut Wrand| -> Vec { + ids.iter() + .map(|id| { + let opts = &table[id]; + let k = if rotate && opts.len() > 1 { (lrng.next() * opts.len() as f64) as usize } else { 0 }; + opts[k.min(opts.len() - 1)].clone() + }) + .collect() + }; let mut specs = Vec::with_capacity(games); let mut pair_of = Vec::with_capacity(games); for i in 0..games { @@ -277,22 +345,29 @@ pub fn run_doubles_arena(roster: &Roster, games: usize, wseed: u32, seed_base: u let (p1d, p0d) = DIFF_PAIRS[pi]; let p0_ids = draw_team(roster, &mut rng); let p1_ids = draw_team(roster, &mut rng); + let p0_team = equip(&p0_ids, &mut lrng); + let p1_team = equip(&p1_ids, &mut lrng); specs.push(DoublesSpec { seed: seed_base.wrapping_add(i as u32), max_turns: MAX_TURNS, mons_per_team: TEAM_SIZE as u64, - p0_team: p0_ids.iter().map(|&id| build_team_mon(mon(id))).collect(), - p1_team: p1_ids.iter().map(|&id| build_team_mon(mon(id))).collect(), + p0_team, + p1_team, p0_ids, p1_ids, p0_difficulty: p0d, p1_difficulty: p1d, - p0_search_depth: 0, - p1_search_depth: 0, + p0_search_depth: search_depth, + p1_search_depth: search_depth, }); pair_of.push(pi); } + (specs, pair_of) +} +pub fn run_doubles_arena(roster: &Roster, games: usize, wseed: u32, seed_base: u32, threads: usize) -> Vec { + let book = roster::address_book(); + let (specs, pair_of) = build_doubles_specs(roster, games, wseed, seed_base, false, 0); let outcomes = run_doubles_games(&specs, &book, threads); let mut stats: Vec = DIFF_PAIRS @@ -310,3 +385,67 @@ pub fn run_doubles_arena(roster: &Roster, games: usize, wseed: u32, seed_base: u } stats } + +#[cfg(test)] +mod tests { + use super::*; + use crate::roster::load_roster; + use std::collections::HashSet; + + fn chomp_root() -> std::path::PathBuf { + std::env::var("CHOMP_ROOT").map(std::path::PathBuf::from).unwrap_or_else(|_| { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }) + } + + /// Each mon's loadout set is exactly C(catalog, 4), and every catalog move reaches some loadout — + /// the level-6 moves that the default lanes-0-3 loadout can never equip. + #[test] + fn loadouts_cover_every_catalog_move() { + let roster = load_roster(&chomp_root()); + for m in &roster.mons { + let ls = loadouts(m); + let expected = if m.catalog.len() <= 4 { 1 } else { combos_len(m.catalog.len(), 4) }; + assert_eq!(ls.len(), expected, "{}: expected {expected} loadouts, got {}", m.name, ls.len()); + for l in &ls { + assert_eq!(l.moves.len(), 4, "{}: loadout must equip 4 lanes", m.name); + } + let seen: HashSet<_> = ls.iter().flat_map(|l| l.moves.iter().copied()).collect(); + for c in &m.catalog { + assert!(seen.contains(&c.word), "{}: catalog move {} unreachable", m.name, c.name); + } + } + } + + fn combos_len(n: usize, k: usize) -> usize { + (0..k).fold(1usize, |acc, i| acc * (n - i) / (i + 1)) + } + + /// Rotation leaves the team draft untouched (matched drafts) but does vary the equipped moves. + #[test] + fn rotation_preserves_drafts_and_varies_loadouts() { + let roster = load_roster(&chomp_root()); + let pairs = [(StrategyKind::Greedy, StrategyKind::Greedy)]; + let (plain, _) = build_specs_full(&roster, 300, 0xbeefcafe, 10_000, &pairs, false); + let (rot, _) = build_specs_full(&roster, 300, 0xbeefcafe, 10_000, &pairs, true); + + for (a, b) in plain.iter().zip(rot.iter()) { + assert_eq!(a.p0_ids, b.p0_ids, "rotation must not disturb the p0 draft"); + assert_eq!(a.p1_ids, b.p1_ids, "rotation must not disturb the p1 draft"); + } + // The plain field equips one fixed loadout per mon; the rotated field must show more. + let variants = |specs: &[GameSpec]| -> usize { + let mut s: HashSet<(u32, Vec)> = HashSet::new(); + for sp in specs { + for (ids, team) in [(&sp.p0_ids, &sp.p0_team), (&sp.p1_ids, &sp.p1_team)] { + for (id, mon) in ids.iter().zip(team.iter()) { + s.insert((*id, mon.moves.clone())); + } + } + } + s.len() + }; + assert_eq!(variants(&plain), roster.mons.len(), "unrotated field should equip one loadout per mon"); + assert!(variants(&rot) > variants(&plain), "rotated field should equip more than one loadout per mon"); + } +} diff --git a/transpiler/strategies-rs/src/bin/analyze.rs b/transpiler/strategies-rs/src/bin/analyze.rs index 21fec0a5..377c239e 100644 --- a/transpiler/strategies-rs/src/bin/analyze.rs +++ b/transpiler/strategies-rs/src/bin/analyze.rs @@ -5,7 +5,9 @@ //! //! Data (drool/*.csv, src/mons/*.json) is read relative to CHOMP_ROOT. -use chomp_strategies::analysis::{run_mon_analysis, run_mon_analysis_with}; +use chomp_strategies::analysis::{ + run_doubles_mon_analysis, run_mon_analysis, run_mon_analysis_rotated, run_mon_analysis_with, +}; use chomp_strategies::game::StrategyKind; use chomp_strategies::roster::load_roster; use std::path::PathBuf; @@ -45,17 +47,39 @@ fn main() { let p1 = arg(&args, "--p1"); let p0 = arg(&args, "--p0"); + // --rotate equips a uniform 4-subset of each mon's catalog per drafted slot (default: lanes 0-3). + let rotate = args.iter().any(|a| a == "--rotate"); + let mode = arg(&args, "--mode").unwrap_or_else(|| "singles".to_string()); + // Doubles only: 0 = epsilon-greedy pilot (damaging moves only), >=1 = maximin search (also + // enumerates setup moves), so this toggles whether setup mons can be piloted at all. + let search_depth = arg_u(&args, "--search-depth", 0) as u32; + if mode == "doubles" && search_depth >= 1 { + eprintln!(" pilot: doubles maximin search d{search_depth} (setup moves enumerated)"); + } + if rotate { + eprintln!(" loadouts: rotated (uniform 4-subset per drafted slot)"); + } + let started = std::time::Instant::now(); - let a = match (&p1, &p0) { - (Some(p1s), Some(p0s)) => { - let pair = ( - StrategyKind::parse(p1s).expect("bad --p1 strategy"), - StrategyKind::parse(p0s).expect("bad --p0 strategy"), - ); - eprintln!(" pilots: p1={p1s} vs p0={p0s}"); - run_mon_analysis_with(&roster, games, seed, seed_base, threads, &[pair]) + let a = if mode == "doubles" { + run_doubles_mon_analysis(&roster, games, seed, seed_base, threads, rotate, search_depth) + } else { + match (&p1, &p0) { + (Some(p1s), Some(p0s)) => { + let pair = ( + StrategyKind::parse(p1s).expect("bad --p1 strategy"), + StrategyKind::parse(p0s).expect("bad --p0 strategy"), + ); + eprintln!(" pilots: p1={p1s} vs p0={p0s}"); + if rotate { + run_mon_analysis_rotated(&roster, games, seed, seed_base, threads, Some(&[pair])) + } else { + run_mon_analysis_with(&roster, games, seed, seed_base, threads, &[pair]) + } + } + _ if rotate => run_mon_analysis_rotated(&roster, games, seed, seed_base, threads, None), + _ => run_mon_analysis(&roster, games, seed, seed_base, threads), } - _ => run_mon_analysis(&roster, games, seed, seed_base, threads), }; let elapsed = started.elapsed().as_secs_f64(); @@ -63,12 +87,21 @@ fn main() { let mut ids: Vec = a.per_mon.keys().copied().collect(); ids.sort_by(|&x, &y| a.per_mon[&y].win_rate().partial_cmp(&a.per_mon[&x].win_rate()).unwrap()); - println!( - "\n{:<12} {:>6} {:>7} | {:>8} {:>7} {:>7} | {:>8} {:>8}", - "mon", "win%", "games", "act.t/g", "KOd/g", "KOt/g", "KOd/g·W", "KOd/g·L" - ); + let attributed = !a.ko_matrix.is_empty(); + if attributed { + println!( + "\n{:<12} {:>6} {:>7} | {:>8} {:>7} {:>7} | {:>8} {:>8}", + "mon", "win%", "games", "act.t/g", "KOd/g", "KOt/g", "KOd/g·W", "KOd/g·L" + ); + } else { + println!("\n{:<12} {:>6} {:>7}", "mon", "win%", "games"); + } for id in &ids { let s = &a.per_mon[id]; + if !attributed { + println!("{:<12} {:>5.1}% {:>7}", roster.mon_name(*id), s.win_rate() * 100.0, s.games()); + continue; + } let g = s.games().max(1) as f64; let wg = s.games_won.max(1) as f64; let lg = s.games_lost.max(1) as f64; @@ -86,22 +119,39 @@ fn main() { } // Mon-vs-mon KO matrix (row KOs col), raw attributed counts. - println!("\nKO matrix (row mon KOs column mon — attributed counts):"); - print!("{:<9}", ""); - for id in &ids { - print!("{:>7}", short(&roster.mon_name(*id))); - } - println!(); - for kid in &ids { - print!("{:<9}", short(&roster.mon_name(*kid))); - for vid in &ids { - if kid == vid { - print!("{:>7}", "·"); - } else { - print!("{:>7}", a.ko_matrix.get(&(*kid, *vid)).copied().unwrap_or(0)); - } + if a.ko_matrix.is_empty() { + println!("\nKO matrix: n/a (no attributed KOs)"); + } else { + println!("\nKO matrix (row mon KOs column mon — attributed counts):"); + print!("{:<9}", ""); + for id in &ids { + print!("{:>7}", short(&roster.mon_name(*id))); } println!(); + for kid in &ids { + print!("{:<9}", short(&roster.mon_name(*kid))); + for vid in &ids { + if kid == vid { + print!("{:>7}", "·"); + } else { + print!("{:>7}", a.ko_matrix.get(&(*kid, *vid)).copied().unwrap_or(0)); + } + } + println!(); + } + } + + // Doubles credits a KO from the killer side's target nibble, so state the coverage rather than + // letting the matrix read as the full KO count. + if a.ko_shared > 0 || a.ko_incidental > 0 { + let credited: u32 = a.ko_matrix.values().sum(); + let total = credited + a.ko_shared + a.ko_incidental; + println!( + "\nKO attribution: {credited} credited ({:.0}%) · {} co-kills (both slots aimed) · {} incidental (status/recoil)", + 100.0 * credited as f64 / total.max(1) as f64, + a.ko_shared, + a.ko_incidental + ); } eprintln!( diff --git a/transpiler/strategies-rs/src/bin/findpanic.rs b/transpiler/strategies-rs/src/bin/findpanic.rs new file mode 100644 index 00000000..c46a2169 --- /dev/null +++ b/transpiler/strategies-rs/src/bin/findpanic.rs @@ -0,0 +1,78 @@ +//! Locate the game(s) in an arena batch that panic the engine, and print enough to replay one. +//! cargo run --release -p chomp-strategies --bin findpanic -- --games 20000 + +use chomp_strategies::arena::{build_specs, build_specs_full, STRAT_PAIRS}; +use chomp_strategies::game::{run_games_instrumented, StrategyKind}; +use chomp_strategies::roster::{self, load_roster}; +use std::path::PathBuf; + +fn arg(args: &[String], flag: &str) -> Option { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned() +} +fn arg_u(args: &[String], flag: &str, def: u64) -> u64 { + match arg(args, flag) { + Some(v) if v.starts_with("0x") => u64::from_str_radix(&v[2..], 16).unwrap_or(def), + Some(v) => v.parse().unwrap_or(def), + None => def, + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + let games = arg_u(&args, "--games", 20_000) as usize; + let seed = arg_u(&args, "--seed", 0xbeefcafe) as u32; + let seed_base = arg_u(&args, "--seed-base", 10_000) as u32; + let rotate = args.iter().any(|a| a == "--rotate"); + + let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }); + let roster = load_roster(&chomp_root); + let book = roster::address_book(); + + let pairs: Vec<(StrategyKind, StrategyKind)> = STRAT_PAIRS + .iter() + .map(|&(p1, p0)| (StrategyKind::parse(p1).unwrap(), StrategyKind::parse(p0).unwrap())) + .collect(); + let (specs, _) = if rotate { + build_specs_full(&roster, games, seed, seed_base, &pairs, true) + } else { + build_specs(&roster, games, seed, seed_base) + }; + + eprintln!("findpanic: {games} games · rotate={rotate} · seed {seed:#x}"); + // Single-threaded so the panic message interleaves with the game it came from. + let results = run_games_instrumented(&specs, &book, 1); + + let mut found = 0; + for (i, r) in results.iter().enumerate() { + let Err(e) = r else { continue }; + found += 1; + let s = &specs[i]; + let names = |ids: &[u32]| ids.iter().map(|&x| roster.mon_name(x)).collect::>().join(", "); + println!("\n=== PANIC at spec index {i} ==="); + println!(" error {e}"); + println!(" game seed {} (arena seed {seed:#x}, seed_base {seed_base})", s.seed); + println!(" p0 {:?} [{}]", s.p0_ids, names(&s.p0_ids)); + println!(" p1 {:?} [{}]", s.p1_ids, names(&s.p1_ids)); + println!(" pilots p0={:?} p1={:?}", s.p0_strategy, s.p1_strategy); + for (side, team) in [("p0", &s.p0_team), ("p1", &s.p1_team)] { + for (k, mon) in team.iter().enumerate() { + let id = if side == "p0" { s.p0_ids[k] } else { s.p1_ids[k] }; + let mv: Vec = mon + .moves + .iter() + .map(|w| { + roster + .mon_by_id(id) + .and_then(|m| m.catalog.iter().find(|c| c.word == *w)) + .map(|c| c.name.clone()) + .unwrap_or_else(|| "?".into()) + }) + .collect(); + println!(" {side}[{k}] {:<11} {}", roster.mon_name(id), mv.join(" / ")); + } + } + } + println!("\n{found} panicking game(s) out of {games}"); +} diff --git a/transpiler/strategies-rs/src/bin/montrace.rs b/transpiler/strategies-rs/src/bin/montrace.rs new file mode 100644 index 00000000..fe28bb0b --- /dev/null +++ b/transpiler/strategies-rs/src/bin/montrace.rs @@ -0,0 +1,99 @@ +//! Per-mon doubles diagnostic — what does one mon actually DO in its doubles games? +//! Reports move usage, survival, and whether its setup ever lands. +//! cargo run --release -p chomp-strategies --bin montrace -- --mon Iblivion --games 400 + +use chomp_strategies::arena::build_doubles_specs; +use chomp_strategies::doubles::play_doubles_game_instrumented; +use chomp_strategies::roster::{self, load_roster}; +use std::path::PathBuf; + +fn arg(args: &[String], flag: &str) -> Option { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned() +} +fn arg_u(args: &[String], flag: &str, def: u64) -> u64 { + match arg(args, flag) { + Some(v) if v.starts_with("0x") => u64::from_str_radix(&v[2..], 16).unwrap_or(def), + Some(v) => v.parse().unwrap_or(def), + None => def, + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + let games = arg_u(&args, "--games", 400) as usize; + let seed = arg_u(&args, "--seed", 0xbeefcafe) as u32; + let seed_base = arg_u(&args, "--seed-base", 10_000) as u32; + let target = arg(&args, "--mon").unwrap_or_else(|| "Iblivion".to_string()); + let search_depth = arg_u(&args, "--search-depth", 0) as u32; + let rotate = args.iter().any(|a| a == "--rotate"); + + let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }); + let roster = load_roster(&chomp_root); + let book = roster::address_book(); + let tid = roster.mons.iter().find(|m| m.name == target).expect("unknown --mon").id; + + let (specs, _) = build_doubles_specs(&roster, games, seed, seed_base, rotate, search_depth); + + // Move-index usage over the tracked mon's active turns (125 = switch, 126 = no-op/rest). + let mut usage = [0u64; 128]; + let (mut n, mut wins, mut losses) = (0u64, 0u64, 0u64); + let (mut sum_active, mut sum_turns) = (0u64, 0u64); + let mut ever_boosted = 0u64; // games where the mon's Attack delta ever went positive + let mut sum_min_hp = 0.0f64; + + for spec in &specs { + for side in 0u8..2 { + let ids = if side == 0 { &spec.p0_ids } else { &spec.p1_ids }; + let Some(mon) = ids.iter().position(|&x| x == tid) else { continue }; + let tr = play_doubles_game_instrumented(spec, &book, Some((side, mon))); + n += 1; + match tr.winner_side { + Some(w) if w == side => wins += 1, + Some(_) => losses += 1, + None => {} + } + sum_active += tr.tracked_active_turns as u64; + sum_turns += tr.turns as u64; + let mut boosted = false; + let mut min_hp = 100.0f64; + for r in &tr.rows { + if r.active { + usage[r.move_index as usize] += 1; + } + if r.atk_delta > 0 { + boosted = true; + } + if r.hp_pct < min_hp { + min_hp = r.hp_pct; + } + } + if boosted { + ever_boosted += 1; + } + sum_min_hp += min_hp; + } + } + + let nf = n.max(1) as f64; + println!("\n{target} (id {tid}) · {n} appearances over {games} doubles games · search d{search_depth}"); + println!(" win/loss {wins}-{losses} ({:.1}%)", wins as f64 / (wins + losses).max(1) as f64 * 100.0); + println!(" active turns/game {:.2} (game length {:.2})", sum_active as f64 / nf, sum_turns as f64 / nf); + println!(" min HP reached {:.1}% (mean over appearances)", sum_min_hp / nf); + println!(" setup ever landed {:.1}% of appearances (Attack delta > 0 at any point)", ever_boosted as f64 / nf * 100.0); + + let total: u64 = usage.iter().sum(); + println!("\n move usage over {total} acting turns:"); + for (mi, &c) in usage.iter().enumerate() { + if c == 0 { + continue; + } + let label = match mi { + 125 => "".to_string(), + 126 => "".to_string(), + _ => roster.move_name(tid, mi as u8), + }; + println!(" {:<20} {:>7} {:>5.1}%", label, c, c as f64 / total.max(1) as f64 * 100.0); + } +} diff --git a/transpiler/strategies-rs/src/bin/statpeak.rs b/transpiler/strategies-rs/src/bin/statpeak.rs new file mode 100644 index 00000000..ad913cd0 --- /dev/null +++ b/transpiler/strategies-rs/src/bin/statpeak.rs @@ -0,0 +1,71 @@ +//! Distribution of the largest combat stat any mon reaches — stat boosts are multiplicative and +//! stack per source, and MAX_BOOSTED_STAT lets one reach int32::max. +//! cargo run --release -p chomp-strategies --bin statpeak -- --games 20000 + +use chomp_strategies::arena::{build_specs, build_specs_full, STRAT_PAIRS}; +use chomp_strategies::game::{run_games_instrumented, StrategyKind}; +use chomp_strategies::roster::{self, load_roster}; +use std::path::PathBuf; + +fn arg(args: &[String], flag: &str) -> Option { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned() +} +fn arg_u(args: &[String], flag: &str, def: u64) -> u64 { + match arg(args, flag) { + Some(v) if v.starts_with("0x") => u64::from_str_radix(&v[2..], 16).unwrap_or(def), + Some(v) => v.parse().unwrap_or(def), + None => def, + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + let games = arg_u(&args, "--games", 20_000) as usize; + let seed = arg_u(&args, "--seed", 0xbeefcafe) as u32; + let seed_base = arg_u(&args, "--seed-base", 10_000) as u32; + let rotate = args.iter().any(|a| a == "--rotate"); + + let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }); + let roster = load_roster(&chomp_root); + let book = roster::address_book(); + let pairs: Vec<(StrategyKind, StrategyKind)> = STRAT_PAIRS + .iter() + .map(|&(p1, p0)| (StrategyKind::parse(p1).unwrap(), StrategyKind::parse(p0).unwrap())) + .collect(); + let (specs, _) = if rotate { + build_specs_full(&roster, games, seed, seed_base, &pairs, true) + } else { + build_specs(&roster, games, seed, seed_base) + }; + let recs = run_games_instrumented(&specs, &book, 8); + + let mut peaks: Vec<(i64, u32)> = recs.iter().filter_map(|r| r.as_ref().ok()).map(|r| (r.peak_stat, r.peak_stat_mon)).collect(); + peaks.sort_by_key(|p| p.0); + let n = peaks.len(); + let pct = |q: f64| peaks[((n as f64 - 1.0) * q) as usize]; + println!("\npeak combat stat per game ({n} games)"); + for (lbl, q) in [("p50", 0.50), ("p90", 0.90), ("p99", 0.99), ("p99.9", 0.999)] { + println!(" {lbl:>6} {:>16}", pct(q).0); + } + let (mx, mon) = peaks[n - 1]; + println!(" {:>6} {:>16} ({})", "max", mx, roster.mon_name(mon)); + + const I32MAX: i64 = 2_147_483_647; + for thresh in [10_000i64, 1_000_000, 100_000_000, I32MAX] { + let c = peaks.iter().filter(|p| p.0 >= thresh).count(); + println!(" games with a stat >= {thresh:>13}: {c:>6} ({:.3}%)", 100.0 * c as f64 / n as f64); + } + // Which mon holds the peak in the most extreme games. + let mut worst: Vec<(i64, u32)> = peaks.iter().rev().take(200).copied().collect(); + worst.sort_by_key(|p| p.1); + let mut counts: std::collections::HashMap = std::collections::HashMap::new(); + for (_, m) in &worst { *counts.entry(*m).or_default() += 1; } + let mut cv: Vec<_> = counts.into_iter().collect(); + cv.sort_by_key(|c| std::cmp::Reverse(c.1)); + println!("\n peak holder in the 200 most extreme games:"); + for (m, c) in cv.iter().take(6) { + println!(" {:<12} {c:>4}", roster.mon_name(*m)); + } +} diff --git a/transpiler/strategies-rs/src/doubles.rs b/transpiler/strategies-rs/src/doubles.rs index d20ed2c5..a9a0e913 100644 --- a/transpiler/strategies-rs/src/doubles.rs +++ b/transpiler/strategies-rs/src/doubles.rs @@ -155,15 +155,56 @@ fn damaging_options(sim: &mut Sim, bk: B256, cpu_side: u8, my_mon: u32) -> Vec<( if meta.moveClass != MoveClass::Physical && meta.moveClass != MoveClass::Special { continue; // only weigh damaging moves } - let dmg = estimate_damage_meta(&mut ctx, meta); - if dmg > 0 { - options.push((SlotMove { move_index: *mi, extra_data: target_bits(t_abs) }, dmg)); - } + let mv = SlotMove { move_index: *mi, extra_data: target_bits(t_abs) }; + // A hand-written getMeta() may quote 0 for a move that does deal damage, so 0 means + // "unknown", not "harmless" — dropping it here empties the option set and leaves the slot + // resting all game. Simulate instead of trusting a declared number: nominal power is not + // damage-this-turn for delayed (Q5), multi-hit (Bubble Bop), or spent (Sneak Attack) moves. + let dmg = if meta.basePower != 0 { + estimate_damage_meta(&mut ctx, meta) + } else { + probe_damage(sim, bk, cpu_side, my_mon, mv, opp_side, t_mon) + }; + options.push((mv, dmg)); } } options } +/// Exact damage for one (move, target), by simulating it: fork a turn where only `my_mon`'s slot +/// acts and read how much HP the target actually lost. Used for moves whose power the static quote +/// can't know; costs one fork, so it runs only on moves whose `getMeta()` quotes no power. +fn probe_damage( + sim: &mut Sim, + bk: B256, + cpu_side: u8, + my_mon: u32, + mv: SlotMove, + opp_side: u8, + t_mon: usize, +) -> i64 { + let slots = active_slots(sim, bk); + let [a0, a1] = side_slots(cpu_side); + // Place the probed action on whichever of my slots holds the acting mon; everything else rests, + // so the observed HP change is attributable to this move alone. + let (m0, m1) = if slots[a0] == my_mon { + (mv, NO_OP_MOVE) + } else if slots[a1] == my_mon { + (NO_OP_MOVE, mv) + } else { + return 0; // not an active slot this turn + }; + let mine = pack_side(m0.move_index, m0.extra_data, m1.move_index, m1.extra_data, 0); + let theirs = pack_side(NO_OP, 0, NO_OP, 0, 0); + let (side0, side1) = if cpu_side == 0 { (mine, theirs) } else { (theirs, mine) }; + + let before = mon_state(sim, OBS, bk, opp_side, t_mon, MonStateIndexName::Hp); + let fork = sim.apply_hypothetical_slot(bk, side0, side1); + let after = mon_state(sim, OBS, fork, opp_side, t_mon, MonStateIndexName::Hp); + sim.dispose_fork(fork); + (before - after).max(0) // hp deltas run negative; damage is how much further it fell +} + /// Greedy attack for one acting mon: the affordable damaging (move, target-slot) with the highest /// estimated damage, softened by `difficulty` (epsilon-greedy). None when nothing damaging is /// affordable (caller rests). @@ -711,3 +752,410 @@ pub fn run_doubles_games( }); slots.into_inner().unwrap().into_iter().map(|r| r.expect("slot filled")).collect() } + +// ── Diagnostic: per-turn trace of one mon's doubles game ──────────────────── + +/// What a tracked mon did / had on one doubles turn. +pub struct MonTurnTrace { + pub turn: u32, + pub active: bool, + pub move_index: u8, + pub hp_pct: f64, + pub stamina: i64, + /// Attack stat delta — non-zero once Loop's boost lands (the setup payoff). + pub atk_delta: i64, +} + +/// A KO credited to one attacker. Doubles can't reuse singles' "opposing active" proxy — there are +/// two of them — so credit comes from the target nibble the killer's side actually aimed. +pub struct DoublesKoEvent { + pub turn: u32, + pub killer_seat: u8, + pub killer_id: u32, + pub victim_id: u32, +} + +pub struct DoublesInstr { + pub winner_side: Option, + pub turns: u32, + pub p0_ids: Vec, + pub p1_ids: Vec, + /// Active-turn count per team slot (parallel to p0_ids / p1_ids). + pub active_turns_p0: Vec, + pub active_turns_p1: Vec, + pub kos: Vec, + /// KOs both opposing slots aimed at — a genuine co-kill, so no single attacker earns the credit. + pub kos_shared: u32, + /// KOs no opposing move aimed at (status, recoil, ally damage). + pub kos_incidental: u32, + /// Per-turn rows for the `track`ed mon, if any. + pub rows: Vec, + pub tracked_active_turns: u32, +} + +/// Was `mv` a real attack aimed at absolute slot `abs`? Switch / rest carry no target nibble. +fn aimed_at(mv: SlotMove, abs: usize) -> bool { + mv.move_index != SWITCH && mv.move_index != NO_OP && (mv.extra_data >> 12) & (1u16 << abs) != 0 +} + +/// Counterfactual: replayed from `snap` with only `mv` on `atk_side`'s slot `atk_i` and everything +/// else resting, does `victim` still go down? Lets a KO both opposing slots aimed at be credited to +/// whichever one was actually lethal — usually only one is, the other being redundant overkill. +/// +/// The real turn's salts are reused so accuracy/crit land as close to the observed turn as a +/// one-sided replay can; it is still a counterfactual, not a replay of what happened. +fn would_ko_alone( + sim: &mut Sim, + snap: B256, + atk_side: u8, + atk_i: usize, + mv: SlotMove, + victim_side: u8, + victim_mon: usize, + salts: (u128, u128), +) -> bool { + let (m0, m1) = if atk_i == 0 { (mv, NO_OP_MOVE) } else { (NO_OP_MOVE, mv) }; + let mine = pack_side(m0.move_index, m0.extra_data, m1.move_index, m1.extra_data, if atk_side == 0 { salts.0 } else { salts.1 }); + let theirs = pack_side(NO_OP, 0, NO_OP, 0, if atk_side == 0 { salts.1 } else { salts.0 }); + let (side0, side1) = if atk_side == 0 { (mine, theirs) } else { (theirs, mine) }; + let fork = sim.apply_hypothetical_slot(snap, side0, side1); + let down = ko_bitmap(sim, fork, victim_side) & (1u32 << victim_mon) != 0; + sim.dispose_fork(fork); + down +} + +/// Replay `spec` recording per-mon active turns and attributed KOs, plus optional per-turn rows for +/// one tracked `(side, mon)`. Mirrors `play_doubles_game`; the hot path stays uninstrumented. +pub fn play_doubles_game_instrumented( + spec: &DoublesSpec, + book: &HashMap, + track: Option<(u8, usize)>, +) -> DoublesInstr { + let mut rng = JsRng::new(spec.seed); + let mut sim = Sim::new_doubles( + spec.mons_per_team, + spec.p0_team.clone(), + spec.p1_team.clone(), + spec.p0_ids.clone(), + spec.p1_ids.clone(), + book, + ); + let mut out = DoublesInstr { + winner_side: None, + turns: spec.max_turns, + p0_ids: spec.p0_ids.clone(), + p1_ids: spec.p1_ids.clone(), + active_turns_p0: vec![0; spec.p0_ids.len()], + active_turns_p1: vec![0; spec.p1_ids.len()], + kos: Vec::new(), + kos_shared: 0, + kos_incidental: 0, + rows: Vec::new(), + tracked_active_turns: 0, + }; + + for t in 0..spec.max_turns { + let w = sim.winner_index(); + if w != 2 { + out.winner_side = Some(w); + out.turns = t; + return out; + } + let bk = sim.battle_key; + let (p0a, p0b) = if spec.p0_search_depth > 0 { + search_side_moves(&mut sim, bk, 0, spec.p0_search_depth) + } else { + pick_side_moves(&mut sim, bk, 0, spec.p0_difficulty, &mut rng) + }; + let (p1a, p1b) = if spec.p1_search_depth > 0 { + search_side_moves(&mut sim, bk, 1, spec.p1_search_depth) + } else { + pick_side_moves(&mut sim, bk, 1, spec.p1_difficulty, &mut rng) + }; + + // Slot occupancy + KO state before the turn — the victim's slot must be read pre-execute, + // since a KO'd slot may already have been vacated by the time we look. + let slots = active_slots(&mut sim, bk); + for side in 0u8..2 { + for abs in side_slots(side) { + let mon = slots[abs]; + if mon == EMPTY_LANE { + continue; + } + let counts = if side == 0 { &mut out.active_turns_p0 } else { &mut out.active_turns_p1 }; + if let Some(c) = counts.get_mut(mon as usize) { + *c += 1; + } + } + } + let ko_before = [ko_bitmap(&mut sim, bk, 0), ko_bitmap(&mut sim, bk, 1)]; + + if let Some((side, mon)) = track { + let [s0, s1] = side_slots(side); + let picked = if side == 0 { (p0a, p0b) } else { (p1a, p1b) }; + let (active, move_index) = if slots[s0] == mon as u32 { + (true, picked.0.move_index) + } else if slots[s1] == mon as u32 { + (true, picked.1.move_index) + } else { + (false, NO_OP) + }; + if active { + out.tracked_active_turns += 1; + } + let mhp = mon_max_hp(&mut sim, OBS, bk, side, mon).max(1); + let hp = mhp + mon_state(&mut sim, OBS, bk, side, mon, MonStateIndexName::Hp); + out.rows.push(MonTurnTrace { + turn: t, + active, + move_index, + hp_pct: (hp.max(0) * 100) as f64 / mhp as f64, + stamina: mon_current_stamina(&mut sim, OBS, bk, side, mon), + atk_delta: mon_state(&mut sim, OBS, bk, side, mon, MonStateIndexName::Attack), + }); + } + + let salt0 = random_salt(&mut rng); + let salt1 = random_salt(&mut rng); + let side0 = pack_side(p0a.move_index, p0a.extra_data, p0b.move_index, p0b.extra_data, salt0); + let side1 = pack_side(p1a.move_index, p1a.extra_data, p1b.move_index, p1b.extra_data, salt1); + // Rollback point for the co-kill counterfactuals below — the real turn is about to advance + // the battle past the state they need to be posed from. + let snap = sim.snapshot(bk); + sim.execute_slot_turn(side0, side1); + + // Fresh KOs → credit whichever opposing slot aimed at the victim's slot. Exactly one aimer + // is a clean attribution; two gets resolved by replaying each alone; none means it wasn't a + // targeted move that did it. + let bk2 = sim.battle_key; + for side in 0u8..2 { + let fresh = ko_bitmap(&mut sim, bk2, side) & !ko_before[side as usize]; + if fresh == 0 { + continue; + } + let opp = 1 - side; + let opp_moves = if opp == 0 { [p0a, p0b] } else { [p1a, p1b] }; + let (victim_ids, killer_ids) = if side == 0 { + (&spec.p0_ids, &spec.p1_ids) + } else { + (&spec.p1_ids, &spec.p0_ids) + }; + for v in 0..victim_ids.len() { + if fresh & (1u32 << v) == 0 { + continue; + } + let Some(v_abs) = side_slots(side).into_iter().find(|&a| slots[a] == v as u32) else { + out.kos_incidental += 1; // victim wasn't an active slot this turn + continue; + }; + let aimers: Vec = (0..2).filter(|&i| aimed_at(opp_moves[i], v_abs)).collect(); + if aimers.is_empty() { + out.kos_incidental += 1; // nothing targeted it — status, recoil, or ally damage + continue; + } + // Both aimed → replay each alone; the one lethal by itself earns the credit. If + // neither or both are, the KO is genuinely shared and stays out of the matrix. + let lethal: Vec = if aimers.len() == 2 { + aimers + .iter() + .copied() + .filter(|&i| { + would_ko_alone(&mut sim, snap, opp, i, opp_moves[i], side, v, (salt0, salt1)) + }) + .collect() + } else { + aimers + }; + let [i] = lethal.as_slice() else { + out.kos_shared += 1; + continue; + }; + let k_abs = side_slots(opp)[*i]; + match killer_ids.get(slots[k_abs] as usize) { + Some(&killer_id) => out.kos.push(DoublesKoEvent { + turn: t, + killer_seat: opp, + killer_id, + victim_id: victim_ids[v], + }), + None => out.kos_incidental += 1, + } + } + } + sim.dispose_fork(snap); + } + let fw = sim.winner_index(); + out.winner_side = if fw != 2 { Some(fw) } else { None }; + out +} + +#[cfg(test)] +mod pilot_tests { + use super::*; + use crate::arena::build_doubles_specs; + use crate::roster::{self, load_roster}; + + fn chomp_root() -> std::path::PathBuf { + std::env::var("CHOMP_ROOT").map(std::path::PathBuf::from).unwrap_or_else(|_| { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }) + } + + /// Every mon must be able to take a real action in doubles. A move whose hand-written + /// `getMeta()` quotes `basePower: 0` used to be dropped from the option set, which left a mon + /// with an all-custom kit (Iblivion) with nothing to pick and resting every turn of every game. + #[test] + fn every_mon_can_act_in_doubles() { + let roster = load_roster(&chomp_root()); + let book = roster::address_book(); + let (specs, _) = build_doubles_specs(&roster, 240, 0xbeefcafe, 10_000, true, 0); + + for m in &roster.mons { + let mut appearances = 0; + let mut acted = false; + for spec in &specs { + for side in 0u8..2 { + let ids = if side == 0 { &spec.p0_ids } else { &spec.p1_ids }; + let Some(mon) = ids.iter().position(|&x| x == m.id) else { continue }; + appearances += 1; + let tr = play_doubles_game_instrumented(spec, &book, Some((side, mon))); + if tr.rows.iter().any(|r| r.active && r.move_index != NO_OP && r.move_index != SWITCH) { + acted = true; + break; + } + } + if acted { + break; + } + } + assert!(appearances > 0, "{} never drafted — widen the sample", m.name); + assert!(acted, "{} never used a move in {appearances} doubles appearances", m.name); + } + } + + /// KO attribution must stay conservative and well-covered: every KO lands in exactly one of + /// credited / co-kill / incidental (no double-counting), and the co-kill replay must recover the + /// bulk of the turns where both slots aimed at the same victim — without it, coverage sits near + /// half and focus-fired mons are systematically under-counted as victims. + #[test] + fn ko_attribution_is_covered_and_conserved() { + let roster = load_roster(&chomp_root()); + let book = roster::address_book(); + let (specs, _) = build_doubles_specs(&roster, 300, 0xbeefcafe, 10_000, true, 0); + let recs = run_doubles_games_instrumented(&specs, &book, 1); + + let credited: usize = recs.iter().map(|r| r.kos.len()).sum(); + let shared: u32 = recs.iter().map(|r| r.kos_shared).sum(); + let incidental: u32 = recs.iter().map(|r| r.kos_incidental).sum(); + let total = credited as u32 + shared + incidental; + assert!(total > 0, "no KOs observed — widen the sample"); + + // A credited KO names a killer on the crediting side and a victim on the other. (killer_id + // == victim_id is legal: the two teams are drawn independently, so mirrors happen.) + for r in &recs { + for ko in &r.kos { + let (killers, victims) = if ko.killer_seat == 0 { + (&r.p0_ids, &r.p1_ids) + } else { + (&r.p1_ids, &r.p0_ids) + }; + assert!(killers.contains(&ko.killer_id), "killer not on the crediting side"); + assert!(victims.contains(&ko.victim_id), "victim not on the opposing side"); + } + } + + let coverage = credited as f64 / total as f64; + assert!(coverage > 0.6, "KO attribution coverage fell to {:.0}% — co-kill replay regressed", coverage * 100.0); + } + + /// The probe must actually price a zero-quote move. Iblivion's kit is entirely hand-written + /// `IMoveSet`, so every one of its damage options comes from simulation — if the probe silently + /// returned 0 the pilot would be picking blind even though it has candidates. + #[test] + fn probe_prices_a_zero_quote_kit() { + let roster = load_roster(&chomp_root()); + let book = roster::address_book(); + let iblivion = roster.mons.iter().find(|m| m.name == "Iblivion").expect("Iblivion in roster"); + let (specs, _) = build_doubles_specs(&roster, 240, 0xbeefcafe, 10_000, true, 0); + + let mut priced = false; + 'outer: for spec in &specs { + for side in 0u8..2 { + let ids = if side == 0 { &spec.p0_ids } else { &spec.p1_ids }; + if !ids.contains(&iblivion.id) { + continue; + } + let mut sim = Sim::new_doubles( + spec.mons_per_team, + spec.p0_team.clone(), + spec.p1_team.clone(), + spec.p0_ids.clone(), + spec.p1_ids.clone(), + &book, + ); + let bk = sim.battle_key; + // Turn 0 is the send-in; step once so both sides have live actives to price against. + sim.execute_slot_turn( + pack_side(SWITCH, 0, SWITCH, 1, 0), + pack_side(SWITCH, 0, SWITCH, 1, 0), + ); + let mon = ids.iter().position(|&x| x == iblivion.id).unwrap() as u32; + let opts = damaging_options(&mut sim, bk, side, mon); + if opts.iter().any(|&(_, d)| d > 0) { + priced = true; + break 'outer; + } + } + } + assert!(priced, "probe never produced a non-zero damage estimate for Iblivion's all-custom kit"); + } +} + +/// One instrumented doubles game, with the same engine-panic guard as the plain runner (a panic +/// yields an empty record, counted as a draw, rather than taking down the batch). +fn run_one_doubles_instrumented(spec: &DoublesSpec, book: &HashMap) -> DoublesInstr { + catch_unwind(AssertUnwindSafe(|| play_doubles_game_instrumented(spec, book, None))).unwrap_or_else(|_| { + DoublesInstr { + winner_side: None, + turns: 0, + p0_ids: spec.p0_ids.clone(), + p1_ids: spec.p1_ids.clone(), + active_turns_p0: vec![0; spec.p0_ids.len()], + active_turns_p1: vec![0; spec.p1_ids.len()], + kos: Vec::new(), + kos_shared: 0, + kos_incidental: 0, + rows: Vec::new(), + tracked_active_turns: 0, + } + }) +} + +/// Instrumented counterpart of [`run_doubles_games`]; results come back in spec order. +pub fn run_doubles_games_instrumented( + specs: &[DoublesSpec], + book: &HashMap, + threads: usize, +) -> Vec { + if threads <= 1 || specs.len() <= 1 { + return specs.iter().map(|s| run_one_doubles_instrumented(s, book)).collect(); + } + let n = threads.min(specs.len()); + let mut slots: Vec> = Vec::with_capacity(specs.len()); + slots.resize_with(specs.len(), || None); + let slots = std::sync::Mutex::new(slots); + let next = std::sync::atomic::AtomicUsize::new(0); + std::thread::scope(|scope| { + for _ in 0..n { + scope.spawn(|| loop { + let idx = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + if idx >= specs.len() { + break; + } + let r = run_one_doubles_instrumented(&specs[idx], book); + slots.lock().unwrap()[idx] = Some(r); + }); + } + }); + slots.into_inner().unwrap().into_iter().map(|r| r.expect("slot filled")).collect() +} diff --git a/transpiler/strategies-rs/src/game.rs b/transpiler/strategies-rs/src/game.rs index 45f18ac1..0985c8a2 100644 --- a/transpiler/strategies-rs/src/game.rs +++ b/transpiler/strategies-rs/src/game.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::panic::{catch_unwind, AssertUnwindSafe}; use chomp_engine::Engine; +use chomp_engine::Enums::MonStateIndexName; use chomp_engine::Structs::Mon; use chomp_rt::{Address, B256}; @@ -24,8 +25,8 @@ use crate::override_cpu::{self, OverrideState}; use crate::search; use crate::sim::Sim; use crate::view::{ - active_mon_indices, capture_view, ko_bitmap, mon_current_hp, mon_current_stamina, mon_max_hp, Mv, - Seat, NO_OP_INDEX, SWITCH_MOVE_INDEX, VCPU, VOPP, + active_mon_indices, capture_view, ko_bitmap, mon_current_hp, mon_current_stamina, mon_max_hp, + mon_state, mon_value, Mv, Seat, NO_OP_INDEX, SWITCH_MOVE_INDEX, VCPU, VOPP, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -743,6 +744,10 @@ pub struct InstrRecord { pub active_turns_p0: Vec, pub active_turns_p1: Vec, pub kos: Vec, + /// Largest (base + delta) any combat stat reached, and which mon-id held it — stat boosts are + /// multiplicative, so this shows how far compounding actually runs in a real game. + pub peak_stat: i64, + pub peak_stat_mon: u32, } pub fn play_game_instrumented(spec: &GameSpec, book: &HashMap) -> InstrRecord { @@ -764,14 +769,39 @@ pub fn play_game_instrumented(spec: &GameSpec, book: &HashMap) let mut active_turns_p0 = vec![0u32; spec.p0_ids.len()]; let mut active_turns_p1 = vec![0u32; spec.p1_ids.len()]; let mut kos: Vec = Vec::new(); + let (mut peak_stat, mut peak_stat_mon) = (0i64, 0u32); for t in 0..spec.max_turns { + // Boosts are multiplicative and stack per source, so track how far the combat stats actually + // compound; MAX_BOOSTED_STAT lets a stat reach int32::max, which the damage formula then + // scales by base power. + { + let bk_now: B256 = sim.battle_key; + for (vp, ids) in [(VOPP, &spec.p0_ids), (VCPU, &spec.p1_ids)] { + for (mi, &id) in ids.iter().enumerate() { + for ix in [ + MonStateIndexName::Attack, + MonStateIndexName::SpecialAttack, + MonStateIndexName::Defense, + MonStateIndexName::SpecialDefense, + ] { + let v = mon_value(&mut sim, obs, bk_now, vp, mi, ix) + + mon_state(&mut sim, obs, bk_now, vp, mi, ix); + if v > peak_stat { + peak_stat = v; + peak_stat_mon = id; + } + } + } + } + } let winner = sim.winner_index(); if winner != 2 { return InstrRecord { winner_seat: Some(winner), turns: t, p0_ids: spec.p0_ids.clone(), p1_ids: spec.p1_ids.clone(), active_turns_p0, active_turns_p1, kos, + peak_stat, peak_stat_mon, }; } @@ -843,6 +873,7 @@ pub fn play_game_instrumented(spec: &GameSpec, book: &HashMap) turns: spec.max_turns, p0_ids: spec.p0_ids.clone(), p1_ids: spec.p1_ids.clone(), active_turns_p0, active_turns_p1, kos, + peak_stat, peak_stat_mon, } } @@ -884,7 +915,7 @@ pub fn play_game_mock( for t in 0..spec.max_turns { let winner = sim.winner_index(); if winner != 2 { - return InstrRecord { winner_seat: Some(winner), turns: t, p0_ids: spec.p0_ids.clone(), p1_ids: spec.p1_ids.clone(), active_turns_p0, active_turns_p1, kos }; + return InstrRecord { winner_seat: Some(winner), turns: t, p0_ids: spec.p0_ids.clone(), p1_ids: spec.p1_ids.clone(), active_turns_p0, active_turns_p1, kos, peak_stat: 0, peak_stat_mon: 0 }; } let bk: B256 = sim.battle_key; @@ -956,7 +987,7 @@ pub fn play_game_mock( } let fw = sim.winner_index(); - InstrRecord { winner_seat: if fw != 2 { Some(fw) } else { None }, turns: spec.max_turns, p0_ids: spec.p0_ids.clone(), p1_ids: spec.p1_ids.clone(), active_turns_p0, active_turns_p1, kos } + InstrRecord { winner_seat: if fw != 2 { Some(fw) } else { None }, turns: spec.max_turns, p0_ids: spec.p0_ids.clone(), p1_ids: spec.p1_ids.clone(), active_turns_p0, active_turns_p1, kos, peak_stat: 0, peak_stat_mon: 0 } } /// Threaded mock batch (see `run_batch`); the mock's word is repacked each turn in play_game_mock. diff --git a/transpiler/strategies-rs/src/sim.rs b/transpiler/strategies-rs/src/sim.rs index 51f484e5..84d2a409 100644 --- a/transpiler/strategies-rs/src/sim.rs +++ b/transpiler/strategies-rs/src/sim.rs @@ -379,6 +379,13 @@ impl Sim { fork } + /// Clone `src`'s state under a fresh key WITHOUT running a turn — a rollback point for + /// counterfactuals that can only be posed after the real turn has already advanced the battle. + /// Dispose it like any other fork. + pub fn snapshot(&mut self, src: B256) -> B256 { + self.fork_battle_from(src) + } + /// Reclaim a fork's cloned state. pub fn dispose_fork(&mut self, fork: B256) { self.world.Engine.battleData.remove(&fork); From 53778bec536d1cab26ee69e18c2d32bab2b3c450 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:03:00 -0700 Subject: [PATCH 11/41] small improvement to onboarding flow --- CLAUDE.md | 2 +- snapshots/ProdPvPGasBenchmark.json | 18 ------------ src/game-layer/GachaTeamRegistry.sol | 13 ++++++++ test/GachaTeamRegistryTest.sol | 4 +++ test/GachaTest.sol | 44 ++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0db371bc..350d90d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -339,7 +339,7 @@ The leaf adds: the `onBattleEnd` orchestration (streak / quest / points / exp lo **Rolling.** Mon ids are sequential starting at 0 (`createMon` enforces `monId == monIds.length()`). Ids `[0, NUM_STARTERS)` (= 3) are *starter* mons. -- `firstRoll(uint256 starterId)` — one-shot per player. Caller picks `starterId ∈ {0,1,2}`; the contract guarantees that mon at slot 0 of the result and rolls `INITIAL_ROLLS - 1` (= 3) more uniformly from `[NUM_STARTERS, numMons)`. Free. +- `firstRoll(uint256 starterId)` — one-shot per player, onboarding in a single tx. Caller picks `starterId ∈ {0,1,2}`; the contract guarantees that mon at slot 0 of the result and rolls `INITIAL_ROLLS - 1` (= 3) more uniformly from `[NUM_STARTERS, numMons)`. Free. Also creates the player's first team (slot 0) from the first `MONS_PER_TEAM` rolled ids, via the same internal path `createTeam` uses (`_createTeamForUser`) — skipping the redundant ownership check since those ids were just written. The constructor reverts `MonsPerTeamExceedsInitialRolls` if `MONS_PER_TEAM > INITIAL_ROLLS`, since a deployment configured that way could never satisfy this. - `roll(uint256 numRolls)` — paid (`ROLL_COST` per roll, default 16 points). Uniform across the entire pool. Reverts `NoMoreStock` once the caller owns every mon. - Linear-probing dedup keeps draws inside their window so `firstRoll`'s 3 random picks never land on a starter. diff --git a/snapshots/ProdPvPGasBenchmark.json b/snapshots/ProdPvPGasBenchmark.json index b64921b6..ced33fd2 100644 --- a/snapshots/ProdPvPGasBenchmark.json +++ b/snapshots/ProdPvPGasBenchmark.json @@ -1,22 +1,4 @@ { - "BuiltinBare_5Turns_coldGas": "291871", - "BuiltinBare_5Turns_coldSload": "70", - "BuiltinBare_5Turns_extraAccounts": "15", - "BuiltinBare_5Turns_noop": "10", - "BuiltinBare_5Turns_nzToNz": "33", - "BuiltinBare_5Turns_prodStorageGas": "359200", - "BuiltinBare_5Turns_totalSload": "435", - "BuiltinBare_5Turns_totalSstore": "45", - "BuiltinBare_5Turns_zToNz": "2", - "BuiltinHooked_5Turns_coldGas": "292689", - "BuiltinHooked_5Turns_coldSload": "70", - "BuiltinHooked_5Turns_extraAccounts": "15", - "BuiltinHooked_5Turns_noop": "10", - "BuiltinHooked_5Turns_nzToNz": "33", - "BuiltinHooked_5Turns_prodStorageGas": "359200", - "BuiltinHooked_5Turns_totalSload": "435", - "BuiltinHooked_5Turns_totalSstore": "45", - "BuiltinHooked_5Turns_zToNz": "2", "Builtin_Drain3_coldGas": "146377", "Builtin_Drain3_coldSload": "20", "Builtin_Drain3_extraAccounts": "3", diff --git a/src/game-layer/GachaTeamRegistry.sol b/src/game-layer/GachaTeamRegistry.sol index 23d7f861..a8cd85f1 100644 --- a/src/game-layer/GachaTeamRegistry.sol +++ b/src/game-layer/GachaTeamRegistry.sol @@ -103,6 +103,7 @@ contract GachaTeamRegistry is error NotEngine(); error NoPreviousRegistry(); error AlreadyMigrated(); + error MonsPerTeamExceedsInitialRolls(); // ----- Events ----- event Roll(address indexed player, uint256[] monIds, uint256 pointsSpent); @@ -151,6 +152,9 @@ contract GachaTeamRegistry is IGachaRNG _RNG, GachaTeamRegistry _PREVIOUS_REGISTRY ) PackedTeamStore(_MONS_PER_TEAM, _MOVES_PER_MON) { + if (_MONS_PER_TEAM > INITIAL_ROLLS) { + revert MonsPerTeamExceedsInitialRolls(); + } ENGINE = _ENGINE; RNG = address(_RNG) == address(0) ? IGachaRNG(address(this)) : _RNG; PREVIOUS_REGISTRY = _PREVIOUS_REGISTRY; @@ -583,6 +587,15 @@ contract GachaTeamRegistry is // Remaining rolls are uniform across non-starter pool [NUM_STARTERS, numMons). _rollInto(rolledIds, 1, NUM_STARTERS); emit Roll(msg.sender, rolledIds, 0); + + uint256[] memory starterTeam = rolledIds; + if (MONS_PER_TEAM != INITIAL_ROLLS) { + starterTeam = new uint256[](MONS_PER_TEAM); + for (uint256 i; i < MONS_PER_TEAM; ++i) { + starterTeam[i] = rolledIds[i]; + } + } + _createTeamForUser(msg.sender, starterTeam); } function roll(uint256 numRolls) external returns (uint256[] memory rolledIds) { diff --git a/test/GachaTeamRegistryTest.sol b/test/GachaTeamRegistryTest.sol index 11b597f1..4fc3aebc 100644 --- a/test/GachaTeamRegistryTest.sol +++ b/test/GachaTeamRegistryTest.sol @@ -100,6 +100,10 @@ contract GachaTeamRegistryTest is Test { // Use single-shot prank so setUp leaves no lingering prank state — tests opt in. vm.prank(ALICE); gachaTeamRegistry.firstRoll(0); + // firstRoll now seeds a starter team at slot 0; delete it so slot allocation in the + // tests below (which assume no team exists yet) is unaffected. + vm.prank(ALICE); + gachaTeamRegistry.deleteTeam(0); // Mon id 1 is a starter Alice didn't pick → unowned. Mon id 2 is also unowned. unownedMonId = 1; diff --git a/test/GachaTest.sol b/test/GachaTest.sol index f699bb18..09b723c4 100644 --- a/test/GachaTest.sol +++ b/test/GachaTest.sol @@ -152,6 +152,50 @@ contract GachaTest is Test, BattleHelper { assertTrue(found, "Roll event emitted"); } + function test_firstRoll_createsStarterTeam() public { + GachaTeamRegistry gachaRegistry = new GachaTeamRegistry( + GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON, engine, mockRNG, GachaTeamRegistry(address(0)) + ); + uint256 poolSize = gachaRegistry.NUM_STARTERS() + gachaRegistry.INITIAL_ROLLS() - 1; + for (uint256 i = 0; i < poolSize; i++) { + gachaRegistry.createMon( + i, + MonStats({ + hp: 10, + stamina: 2, + speed: 2, + attack: 1, + defense: 1, + specialAttack: 1, + specialDefense: 1, + type1: Type.Fire, + type2: Type.None + }), + new uint256[](0), + new uint256[](0), + new bytes32[](0), + new bytes32[](0) + ); + } + + vm.prank(ALICE); + uint256[] memory monIds = gachaRegistry.firstRoll(0); + + assertEq(gachaRegistry.getTeamCount(ALICE), 1); + uint256[] memory teamMonIds = gachaRegistry.getMonRegistryIndicesForTeam(ALICE, 0); + assertEq(teamMonIds.length, monIds.length); + for (uint256 i; i < monIds.length; i++) { + assertEq(teamMonIds[i], monIds[i]); + } + } + + function test_firstRoll_revertsWhenMonsPerTeamExceedsInitialRolls() public { + GachaTeamRegistry probe = new GachaTeamRegistry(0, 0, engine, mockRNG, GachaTeamRegistry(address(0))); + uint256 tooMany = probe.INITIAL_ROLLS() + 1; + vm.expectRevert(GachaTeamRegistry.MonsPerTeamExceedsInitialRolls.selector); + new GachaTeamRegistry(tooMany, GAME_MOVES_PER_MON, engine, mockRNG, GachaTeamRegistry(address(0))); + } + function test_assignPoints() public { GachaTeamRegistry gachaRegistry = new GachaTeamRegistry(0, 0, engine, mockRNG, GachaTeamRegistry(address(0))); From d4999e8637ba6f42bf8628f10594edc925136c7f Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:08:11 -0700 Subject: [PATCH 12/41] clean up stray Engine functions --- CLAUDE.md | 10 +++-- sims/arena/bench-search.ts | 3 +- sims/src/arena/transpose.ts | 14 ++++--- sims/src/cpu/doubles-search.ts | 4 +- sims/src/cpu/engine-view.ts | 15 +++---- snapshots/DoublesGasBenchmark.json | 6 +-- snapshots/EngineOptimizationTest.json | 4 +- snapshots/FullyOptimizedInlineGasTest.json | 6 +-- snapshots/MatchmakerTest.json | 6 +-- snapshots/ProdPvPGasBenchmark.json | 22 +++++++++- snapshots/StartBattleGasTest.json | 6 +-- src/Constants.sol | 9 ---- src/Engine.sol | 49 ++++++---------------- src/IEngine.sol | 6 --- src/lib/StaminaRegenLogic.sol | 2 +- test/EngineOptimizationTest.sol | 13 +++--- test/EngineTest.sol | 14 +++---- test/SignedCommitManager.t.sol | 22 +++++----- test/effects/EffectTest.sol | 8 ++-- test/mons/EmbursaTest.sol | 4 +- test/mons/NirvammaTest.sol | 8 ++-- test/mons/PengymTest.sol | 10 ++--- transpiler/strategies-rs/src/doubles.rs | 6 +-- transpiler/strategies-rs/src/sim.rs | 5 +-- transpiler/strategies-rs/src/view.rs | 8 ++-- 25 files changed, 120 insertions(+), 140 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 350d90d6..72069179 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -304,11 +304,13 @@ The packing/aggregation math lives in `src/lib/StatBoostLib.sol`. (Historically round-trips.) How it works: -- Each boost **source** is one packed entry stored in the mon's normal effect mapping under the - `STAT_BOOST_ADDRESS` sentinel (steps bitmap `STAT_BOOST_STEPS` = `OnMonSwitchOut | ALWAYS_APPLIES`). +- Each boost **source** is one packed word in a dedicated per-mon boost store + (`p0BoostWords`/`p1BoostWords`; 4-bit counts in `p0/p1BoostCounts`, aggregation cache in + `statBoostAcc`) — NOT an effect-list entry, so effect passes never iterate boost sources. Sources are keyed by `msg.sender`, so each move/ability/effect stacks independently and can remove its own boost. Boosts are **multiplicative** per source; `Temp` - boosts are dropped automatically on switch-out (`_inlineStatBoostSwitchOut`), `Perm` ones persist. + boosts are dropped on switch-out by a direct `_inlineStatBoostSwitchOut` call in `_handleSwitch`, + `Perm` ones persist. - Boosts apply only to the 5 stat deltas: `Speed`, `Attack`, `Defense`, `SpecialAttack`, `SpecialDefense`. There is **no globalKV snapshot** — the Engine telescopes off the live `monState` delta (`new boosted − base − currentDelta`) and fires `OnUpdateMonState` like any other delta write. @@ -398,7 +400,7 @@ Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws - `BattleData` and `BattleConfig` are stored per battle key (derived from player addresses) - `MonState` tracks deltas from base stats (hpDelta, staminaDelta, etc.). The 5 stat deltas are written only by the inlined stat-boost path (see "Stat Boosts"); other deltas via `updateMonState`. -- Effects stored in per-mon mappings with stride-based indexing (64 slots per mon). Stat-boost sources reuse these same mappings under the `STAT_BOOST_ADDRESS` sentinel. +- Effects stored in per-mon mappings with stride-based indexing (64 slots per mon). Stat-boost sources live in dedicated per-mon boost-word mappings (`p0/p1BoostWords` + packed counts), not the effect mappings. - Heavy use of bit packing for gas efficiency (KO bitmaps, effect counts, active mon indices) - Battle mode is a `uint8` in `BattleConfig.battleMode` (mirrored to `BattleData.isTwoSlotMode`/`isMultiMode`). Slot-0 active lanes pack into `BattleData.activeMonIndex` (side 0 = low byte, side 1 = high byte); 2-slot modes add slot-1 lanes in `activeMonExt`. Multi's extra seats (p2/p3) live in `multiSeats[battleKey]` (`MultiSeatData`), written only when `battleMode == MULTI`. - Transient storage used for per-transaction state (`battleKeyForWrite`, `tempRNG`, in-flight `PreDamage`) diff --git a/sims/arena/bench-search.ts b/sims/arena/bench-search.ts index 27d32c6d..a408934a 100644 --- a/sims/arena/bench-search.ts +++ b/sims/arena/bench-search.ts @@ -36,7 +36,8 @@ executeTurn(ctx, battleKey, { p0MoveIndex: SWITCH, p1MoveIndex: SWITCH, p0ExtraD const NO_OP = Number(Constants.NO_OP_MOVE_INDEX); const firstBench = (side: 0 | 1): bigint => { const ko = Number(engine.getKOBitmap(battleKey, BigInt(side))); - const active = Number(engine.getActiveMonIndexForBattleState(battleKey)[side]); + const ctx = engine.getBattleContext(battleKey); + const active = Number(side === 0 ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex); for (let i = 0; i < 4; i++) if (i !== active && (ko & (1 << i)) === 0) return BigInt(i); return 0n; }; diff --git a/sims/src/arena/transpose.ts b/sims/src/arena/transpose.ts index 2c0d85a8..59e9ee68 100644 --- a/sims/src/arena/transpose.ts +++ b/sims/src/arena/transpose.ts @@ -23,8 +23,7 @@ const FLIP_ARG_POSITIONS: Record = { getMonValueForBattle: [1], getMoveForMonForBattle: [1], getKOBitmap: [1], - getTeamSize: [1], - getMoveDecisionForBattleState: [1], + getMoveDecisionForSlot: [1], validatePlayerMoveForBattle: [2], getDamageCalcContext: [1, 3], // (bk, atkPlayer, atkMon, defPlayer, defMon) — player args at 1 & 3 // forward-model fork submission: _setMoveInternal(config, playerIndex, ...) @@ -72,10 +71,13 @@ export function transposeEngine(engine: any): any { out = (...args: any[]) => { const ctx = val.apply(target, args); const flag = Number(ctx.playerSwitchForTurnFlag); - if (flag === 0 || flag === 1) { - return { ...ctx, playerSwitchForTurnFlag: BigInt(1 - flag) }; - } - return ctx; + // Seat-swap the active indices; flip the singles switch flag (0/1). + return { + ...ctx, + p0ActiveMonIndex: ctx.p1ActiveMonIndex, + p1ActiveMonIndex: ctx.p0ActiveMonIndex, + playerSwitchForTurnFlag: flag === 0 || flag === 1 ? BigInt(1 - flag) : ctx.playerSwitchForTurnFlag, + }; }; } else { out = val.bind(target); diff --git a/sims/src/cpu/doubles-search.ts b/sims/src/cpu/doubles-search.ts index 7ae229cb..61ec531a 100644 --- a/sims/src/cpu/doubles-search.ts +++ b/sims/src/cpu/doubles-search.ts @@ -18,7 +18,7 @@ import { Hex } from './hex'; import { MoveClass, MonStateIndexName } from '../../../transpiler/ts-output/Enums'; import { moveSlotLib } from '../../../transpiler/ts-output/moves/MoveSlotLib'; import { buildDamageCalcContext, estimateDamageMeta } from './heuristic-shared'; -import { koBitmap, monCurrentHp, monCurrentStamina, moveSlot, moveInputTypeOf, moveTargetSpecOf } from './engine-view'; +import { koBitmap, monCurrentHp, monCurrentStamina, moveSlot, moveInputTypeOf, moveTargetSpecOf, teamSize } from './engine-view'; import { applyHypotheticalSlotMoveKeyed, disposeFork, packSide } from './forward-model'; const SWITCH = 125; @@ -51,7 +51,7 @@ function activeSlots(e: any, bk: Hex): number[] { } function teamSizeOf(e: any, bk: Hex, side: 0 | 1): number { - return Number(e.getTeamSize(bk, BigInt(side))); + return teamSize(e, bk, BigInt(side)); } function monMaxHp(e: any, bk: Hex, side: 0 | 1, mon: number): number { diff --git a/sims/src/cpu/engine-view.ts b/sims/src/cpu/engine-view.ts index 01db9a72..27f3574b 100644 --- a/sims/src/cpu/engine-view.ts +++ b/sims/src/cpu/engine-view.ts @@ -59,14 +59,10 @@ export function makeRng(seed: number = (Math.random() * 0x100000000) >>> 0): () // Small readers (the state CPU.sol's `_buildCPUContext` pulls off the engine) // --------------------------------------------------------------------------------------------- -/** - * Active mon indices [p0, p1]. The transpiled `getActiveMonIndexForBattleState` already UNPACKS to - * a 2-element array `[p0Index, p1Index]` (it is NOT a packed byte pair in the local engine — the - * on-chain `data.activeMonIndex` packing is hidden behind the getter), so we read it directly. - */ +/** Active mon indices [p0, p1], off getBattleContext (the transpose proxy seat-swaps them). */ export function activeMonIndices(e: any, bk: Hex): [number, number] { - const r = e.getActiveMonIndexForBattleState(bk); - return [Number(r[0]), Number(r[1])]; + const ctx = e.getBattleContext(bk); + return [Number(ctx.p0ActiveMonIndex), Number(ctx.p1ActiveMonIndex)]; } export function cpuActiveMonIndex(e: any, bk: Hex): number { @@ -78,7 +74,8 @@ export function oppActiveMonIndex(e: any, bk: Hex): number { } export function teamSize(e: any, bk: Hex, playerIndex: bigint): number { - return Number(e.getTeamSize(bk, playerIndex)); + const packed = Number(e.battleConfig[e._getStorageKey(bk)].teamSizes); + return playerIndex === 0n ? packed & 0x0f : packed >> 4; } export function cpuTeamSize(e: any, bk: Hex): number { @@ -185,7 +182,7 @@ export function moveTargetSpecOf(slot: bigint): string { // The opponent's (p0's) revealed move this turn — BetterCPU peeks at this. { moveIndex, extraData } // with the on-chain packing decoded (low 7 bits = move index; bit 7 = isRealTurn). export function opponentRevealedMove(e: any, bk: Hex): RevealedMove { - const d = e.getMoveDecisionForBattleState(bk, OPP_PLAYER_INDEX); + const d = e.getMoveDecisionForSlot(bk, OPP_PLAYER_INDEX, 0n); return { moveIndex: Number(d.packedMoveIndex) & 0x7f, extraData: Number(d.extraData) }; } diff --git a/snapshots/DoublesGasBenchmark.json b/snapshots/DoublesGasBenchmark.json index 06149a4b..c0c38da8 100644 --- a/snapshots/DoublesGasBenchmark.json +++ b/snapshots/DoublesGasBenchmark.json @@ -1,5 +1,5 @@ { - "Doubles_Batch12_coldGas": "772506", + "Doubles_Batch12_coldGas": "772352", "Doubles_Batch12_coldSload": "34", "Doubles_Batch12_extraAccounts": "6", "Doubles_Batch12_noop": "23", @@ -8,7 +8,7 @@ "Doubles_Batch12_totalSload": "1694", "Doubles_Batch12_totalSstore": "65", "Doubles_Batch12_zToNz": "7", - "Doubles_Drain6_coldGas": "487103", + "Doubles_Drain6_coldGas": "486927", "Doubles_Drain6_coldSload": "40", "Doubles_Drain6_extraAccounts": "7", "Doubles_Drain6_noop": "11", @@ -17,7 +17,7 @@ "Doubles_Drain6_totalSload": "898", "Doubles_Drain6_totalSstore": "48", "Doubles_Drain6_zToNz": "7", - "Doubles_KitBatch10_coldGas": "1026002", + "Doubles_KitBatch10_coldGas": "1025452", "Doubles_KitBatch10_coldSload": "26", "Doubles_KitBatch10_extraAccounts": "3", "Doubles_KitBatch10_noop": "21", diff --git a/snapshots/EngineOptimizationTest.json b/snapshots/EngineOptimizationTest.json index 7fad3bc2..fbda3132 100644 --- a/snapshots/EngineOptimizationTest.json +++ b/snapshots/EngineOptimizationTest.json @@ -1,4 +1,4 @@ { - "ExternalStaminaRegen": "341383", - "InlineStaminaRegen": "802104" + "ExternalStaminaRegen": "340364", + "InlineStaminaRegen": "800695" } \ No newline at end of file diff --git a/snapshots/FullyOptimizedInlineGasTest.json b/snapshots/FullyOptimizedInlineGasTest.json index 001ef295..a1299948 100644 --- a/snapshots/FullyOptimizedInlineGasTest.json +++ b/snapshots/FullyOptimizedInlineGasTest.json @@ -1,5 +1,5 @@ { - "Fast_Battle1_coldGas": "1103813", + "Fast_Battle1_coldGas": "1102097", "Fast_Battle1_coldSload": "197", "Fast_Battle1_extraAccounts": "52", "Fast_Battle1_noop": "23", @@ -8,7 +8,7 @@ "Fast_Battle1_totalSload": "981", "Fast_Battle1_totalSstore": "114", "Fast_Battle1_zToNz": "22", - "Fast_Battle2_coldGas": "1017400", + "Fast_Battle2_coldGas": "1015486", "Fast_Battle2_coldSload": "205", "Fast_Battle2_extraAccounts": "52", "Fast_Battle2_noop": "25", @@ -17,7 +17,7 @@ "Fast_Battle2_totalSload": "1061", "Fast_Battle2_totalSstore": "129", "Fast_Battle2_zToNz": "13", - "Fast_Battle3_coldGas": "738346", + "Fast_Battle3_coldGas": "736630", "Fast_Battle3_coldSload": "197", "Fast_Battle3_extraAccounts": "52", "Fast_Battle3_noop": "30", diff --git a/snapshots/MatchmakerTest.json b/snapshots/MatchmakerTest.json index aac0ba5a..f6c22c66 100644 --- a/snapshots/MatchmakerTest.json +++ b/snapshots/MatchmakerTest.json @@ -1,5 +1,5 @@ { - "Accept1": "332643", - "Accept2": "33887", - "Propose1": "175509" + "Accept1": "332599", + "Accept2": "33821", + "Propose1": "175443" } \ No newline at end of file diff --git a/snapshots/ProdPvPGasBenchmark.json b/snapshots/ProdPvPGasBenchmark.json index ced33fd2..c2828b64 100644 --- a/snapshots/ProdPvPGasBenchmark.json +++ b/snapshots/ProdPvPGasBenchmark.json @@ -1,5 +1,23 @@ { - "Builtin_Drain3_coldGas": "146377", + "BuiltinBare_5Turns_coldGas": "291871", + "BuiltinBare_5Turns_coldSload": "70", + "BuiltinBare_5Turns_extraAccounts": "15", + "BuiltinBare_5Turns_noop": "10", + "BuiltinBare_5Turns_nzToNz": "33", + "BuiltinBare_5Turns_prodStorageGas": "359200", + "BuiltinBare_5Turns_totalSload": "435", + "BuiltinBare_5Turns_totalSstore": "45", + "BuiltinBare_5Turns_zToNz": "2", + "BuiltinHooked_5Turns_coldGas": "292689", + "BuiltinHooked_5Turns_coldSload": "70", + "BuiltinHooked_5Turns_extraAccounts": "15", + "BuiltinHooked_5Turns_noop": "10", + "BuiltinHooked_5Turns_nzToNz": "33", + "BuiltinHooked_5Turns_prodStorageGas": "359200", + "BuiltinHooked_5Turns_totalSload": "435", + "BuiltinHooked_5Turns_totalSstore": "45", + "BuiltinHooked_5Turns_zToNz": "2", + "Builtin_Drain3_coldGas": "146311", "Builtin_Drain3_coldSload": "20", "Builtin_Drain3_extraAccounts": "3", "Builtin_Drain3_noop": "7", @@ -8,7 +26,7 @@ "Builtin_Drain3_totalSload": "236", "Builtin_Drain3_totalSstore": "24", "Builtin_Drain3_zToNz": "2", - "Builtin_Stage3_coldGas": "90313", + "Builtin_Stage3_coldGas": "90115", "Builtin_Stage3_coldSload": "9", "Builtin_Stage3_extraAccounts": "3", "Builtin_Stage3_noop": "0", diff --git a/snapshots/StartBattleGasTest.json b/snapshots/StartBattleGasTest.json index 2e4f59c7..6a45c92d 100644 --- a/snapshots/StartBattleGasTest.json +++ b/snapshots/StartBattleGasTest.json @@ -1,5 +1,5 @@ { - "StartBattle_Cold_coldGas": "1282545", + "StartBattle_Cold_coldGas": "1282501", "StartBattle_Cold_coldSload": "119", "StartBattle_Cold_extraAccounts": "3", "StartBattle_Cold_noop": "8", @@ -8,7 +8,7 @@ "StartBattle_Cold_totalSload": "156", "StartBattle_Cold_totalSstore": "83", "StartBattle_Cold_zToNz": "57", - "StartBattle_WarmSame_coldGas": "229539", + "StartBattle_WarmSame_coldGas": "229495", "StartBattle_WarmSame_coldSload": "129", "StartBattle_WarmSame_extraAccounts": "3", "StartBattle_WarmSame_noop": "72", @@ -17,7 +17,7 @@ "StartBattle_WarmSame_totalSload": "162", "StartBattle_WarmSame_totalSstore": "82", "StartBattle_WarmSame_zToNz": "4", - "StartBattle_WarmSteady_coldGas": "229539", + "StartBattle_WarmSteady_coldGas": "229495", "StartBattle_WarmSteady_coldSload": "129", "StartBattle_WarmSteady_extraAccounts": "3", "StartBattle_WarmSteady_noop": "40", diff --git a/src/Constants.sol b/src/Constants.sol index 0c7195d1..311e0512 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -47,15 +47,6 @@ uint256 constant EFFECT_COUNT_MASK = 0x3F; // 6 bits = max count of 63 address constant TOMBSTONE_ADDRESS = address(0xdead); -// Sentinel effect address used for inlined stat-boost entries. Boost sources are stored in the -// normal per-mon effect mappings under this address; the Engine recognizes it and runs the -// inlined stat-boost switch-out logic instead of making an external IEffect call (mirrors the -// address(0) StaminaRegen inline path). It is never a real deployed contract. -address constant STAT_BOOST_ADDRESS = address(0x57B); // "STB" - stat boost -// Steps bitmap stored on inlined stat-boost effect entries: ALWAYS_APPLIES | OnMonSwitchOut (bit 5). -// Matches the legacy StatBoosts.getStepsBitmap() (0x8020) so view/round-trip behavior is unchanged. -uint16 constant STAT_BOOST_STEPS = 0x8020; - // Sentinel ruleset address: when passed as battle.ruleset, the Engine adds // inline StaminaRegen as a global effect without calling an external contract. address constant INLINE_STAMINA_REGEN_RULESET = address(0x57A); // "STA"mina diff --git a/src/Engine.sol b/src/Engine.sol index 28342ce6..81621fc8 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -1902,8 +1902,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // playerEffectStepsUnion here. The step-skip guards in _executeInternal / _handleSwitch // treat a clear union bit as "no player effect listens at this step" and skip the whole // _runEffects shell, so a missed update would silently drop live effects. This is the - // canonical chokepoint (addEffect -> _addEffectInternal); the ONLY other player-effect - // writer is _addStatBoostEffectSlot, which mirrors this with `|= STAT_BOOST_STEPS`. + // SOLE add chokepoint for player effects (stat-boost sources live in their own + // boost-word store, not the effect list). // Global effects (targetIndex == 2) are gated by globalEffectsLength instead, not the union. if (targetIndex != 2) { config.playerEffectStepsUnion |= stepsBitmap; @@ -2165,14 +2165,14 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // --------------------------------------------------------------------------------------------- // Inlined stat boosts // - // Formerly the standalone `StatBoosts` effect contract. Boost sources are stored in the normal - // per-mon effect mappings under the STAT_BOOST_ADDRESS sentinel (stepsBitmap = STAT_BOOST_STEPS), - // and the aggregated multiplier snapshot lives in globalKV — both already recycled across battles - // by the MappingAllocator-managed storageKey, so no new storage is introduced. Callers (moves, - // abilities, shared effects) invoke these directly during execute, so the boost-source key is - // still derived from msg.sender exactly as it was when StatBoosts saw the caller. The math lives - // in StatBoostLib; here we only touch storage and fire the OnUpdateMonState pipeline via - // _updateMonStateInternal (matching the legacy updateMonState path). + // Formerly the standalone `StatBoosts` effect contract. Boost sources live in their own + // per-mon store — one packed word per source in p0/p1BoostWords, 4-bit counts in + // p0/p1BoostCounts, aggregation cache in statBoostAcc — NOT in the effect mappings, so effect + // passes never iterate them; Temp expiry is a direct _inlineStatBoostSwitchOut call in + // _handleSwitch. Callers (moves, abilities, shared effects) invoke these directly during + // execute, so the boost-source key is still derived from msg.sender exactly as it was when + // StatBoosts saw the caller. The math lives in StatBoostLib; here we only touch storage and + // fire the OnUpdateMonState pipeline via _updateMonStateInternal (matching the legacy path). // --------------------------------------------------------------------------------------------- /// @notice Apply a stat-boost source keyed by msg.sender (no salt). Merges into an existing @@ -3113,8 +3113,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // If so, remove the effect and the extra data. // Guard each _runEffects so we don't pay its setup (storage re-resolution + count load) when // there's provably nothing to run: the switching mon has no effects (count == 0) or no effect - // anywhere listens at this step (union bit clear — stat boosts now fold their OnMonSwitchOut - // step into the union via _addStatBoostEffectSlot), and the global list is empty. + // anywhere listens at this step (union bit clear), and the global list is empty. if (!currentMonState.isKnockedOut) { uint256 outCount = playerIndex == 0 ? _getMonEffectCount(config.packedP0EffectsCount, currentActiveMonIndex) @@ -3406,8 +3405,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Skip tombstones AND entries that don't listen at this step. The step filter is // hoisted here on purpose: stepsBitmap shares slot 0 with the effect address (free // check), while letting _runSingleEffect do it would first pay the eff.data SLOAD - // (slot 1) + 13-arg call setup. Stat-boost sentinel entries (OnMonSwitchOut-only) - // otherwise tax every RoundStart/RoundEnd/AfterMove/AfterDamage pass over their mon. + // (slot 1) + 13-arg call setup. if (address(eff.effect) != TOMBSTONE_ADDRESS && (eff.stepsBitmap & (1 << uint8(round))) != 0) { _runSingleEffect( config, @@ -5125,12 +5123,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } } - function getTeamSize(bytes32 battleKey, uint256 playerIndex) external view returns (uint256) { - bytes32 storageKey = _resolveStorageKey(battleKey); - uint8 teamSizes = battleConfig[storageKey].teamSizes; - return (playerIndex == 0) ? (teamSizes & 0x0F) : (teamSizes >> 4); - } - function getMoveForMonForBattle(bytes32 battleKey, uint256 playerIndex, uint256 monIndex, uint256 moveIndex) external view @@ -5155,15 +5147,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return _getCurrentTurnMove(battleConfig[_resolveStorageKey(battleKey)], playerIndex); } - function getMoveDecisionForBattleState(bytes32 battleKey, uint256 playerIndex) - external - view - returns (MoveDecision memory) - { - BattleConfig storage config = battleConfig[_resolveStorageKey(battleKey)]; - return _getCurrentTurnMove(config, playerIndex); - } - function getMonStatsForBattle(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external view @@ -5253,14 +5236,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return battleData[battleKey].turnId; } - function getActiveMonIndexForBattleState(bytes32 battleKey) external view returns (uint256[] memory) { - uint16 packed = battleData[battleKey].activeMonIndex; - uint256[] memory result = new uint256[](2); - result[0] = _unpackActiveMonIndex(packed, 0); - result[1] = _unpackActiveMonIndex(packed, 1); - return result; - } - function getGlobalKV(bytes32 battleKey, uint64 key) external view returns (uint192) { return _getGlobalKVValue(_resolveStorageKey(battleKey), key); } diff --git a/src/IEngine.sol b/src/IEngine.sol index e10c2a07..5089110d 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -180,13 +180,7 @@ interface IEngine { external view returns (uint256); - function getMoveDecisionForBattleState(bytes32 battleKey, uint256 playerIndex) - external - view - returns (MoveDecision memory); - function getTeamSize(bytes32 battleKey, uint256 playerIndex) external view returns (uint256); function getTurnIdForBattleState(bytes32 battleKey) external view returns (uint256); - function getActiveMonIndexForBattleState(bytes32 battleKey) external view returns (uint256[] memory); function getGlobalKV(bytes32 battleKey, uint64 key) external view returns (uint192); function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external diff --git a/src/lib/StaminaRegenLogic.sol b/src/lib/StaminaRegenLogic.sol index c72e1c46..e5e11d46 100644 --- a/src/lib/StaminaRegenLogic.sol +++ b/src/lib/StaminaRegenLogic.sol @@ -59,7 +59,7 @@ library StaminaRegenLogic { /// @notice External AfterMove entry point — gated on the same NoOp check as the inline path. function onAfterMoveExternal(IEngine engine, bytes32 battleKey, uint256 targetIndex, uint256 monIndex) internal { - MoveDecision memory moveDecision = engine.getMoveDecisionForBattleState(battleKey, targetIndex); + MoveDecision memory moveDecision = engine.getMoveDecisionForSlot(battleKey, targetIndex, 0); if (!_isRestingMove(moveDecision.packedMoveIndex)) { return; } diff --git a/test/EngineOptimizationTest.sol b/test/EngineOptimizationTest.sol index ee1b4c44..c0790b25 100644 --- a/test/EngineOptimizationTest.sol +++ b/test/EngineOptimizationTest.sol @@ -38,8 +38,9 @@ contract KOOpponentOnSwitchAbility is IAbility { function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256) external { uint256 opponentIndex = 1 - playerIndex; - uint256[] memory activeMons = engine.getActiveMonIndexForBattleState(battleKey); - engine.dealDamage(opponentIndex, activeMons[opponentIndex], 1000); + BattleContext memory ctx = engine.getBattleContext(battleKey); + uint256 oppActive = opponentIndex == 0 ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex; + engine.dealDamage(opponentIndex, oppActive, 1000); } } @@ -392,8 +393,8 @@ contract EngineOptimizationTest is Test, BattleHelper { _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint16(1)); - uint256[] memory activeMons = testEngine.getActiveMonIndexForBattleState(battleKey); - assertEq(activeMons[1], 1, "P1 should switch to mon 1"); + BattleContext memory activeMons = testEngine.getBattleContext(battleKey); + assertEq(activeMons.p1ActiveMonIndex, 1, "P1 should switch to mon 1"); assertEq( uint256(testEngine.getBattleContext(battleKey).playerSwitchForTurnFlag), 2, @@ -452,8 +453,8 @@ contract EngineOptimizationTest is Test, BattleHelper { signedManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); testEngine.resetCallContext(); - uint256[] memory activeMons = testEngine.getActiveMonIndexForBattleState(battleKey); - assertEq(activeMons[1], 1, "P1 should switch through normal reveal fallback"); + BattleContext memory activeMons = testEngine.getBattleContext(battleKey); + assertEq(activeMons.p1ActiveMonIndex, 1, "P1 should switch through normal reveal fallback"); assertEq( uint256(testEngine.getBattleContext(battleKey).playerSwitchForTurnFlag), 2, diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 8c2dac23..2f08584d 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -574,7 +574,7 @@ contract EngineTest is Test, BattleHelper { // Assert that mon index for Alice is 1 // Assert that the mon state for Alice has -5 applied to the switched in mon - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); + assertEq(engine.getBattleContext(battleKey).p0ActiveMonIndex, 1); assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp), -5); } @@ -623,7 +623,7 @@ contract EngineTest is Test, BattleHelper { // Assert that mon index for Alice is 1 // Assert that the mon state for Alice has -5 applied to the previous mon - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); + assertEq(engine.getBattleContext(battleKey).p0ActiveMonIndex, 1); assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp), -5); } @@ -1381,7 +1381,7 @@ contract EngineTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, _packForceSwitch(1, 1), 0); // Verify that Bob's mon is now index 1 - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 1); + assertEq(engine.getBattleContext(battleKey).p1ActiveMonIndex, 1); // Verify that Alice's mon took damage assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp), -5); @@ -1455,7 +1455,7 @@ contract EngineTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, _packForceSwitch(0, 1), 0); // Assert that Alice's mon is now index 1 - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); + assertEq(engine.getBattleContext(battleKey).p0ActiveMonIndex, 1); // Assert that Alice's new mon took damage assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp), -5); @@ -1545,7 +1545,7 @@ contract EngineTest is Test, BattleHelper { engine.execute(battleKey); // Check that the active mon index for Alice is still 0 (no switch happened) - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 0); + assertEq(engine.getBattleContext(battleKey).p0ActiveMonIndex, 0); } function test_forceSwitchMoveIgnoresInvalidSwitchTargetNonPriorityPlayerAfterAttacking() public { @@ -1631,7 +1631,7 @@ contract EngineTest is Test, BattleHelper { engine.execute(battleKey); // Check that the active mon index for Alice is still 0 (no switch happened) - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 0); + assertEq(engine.getBattleContext(battleKey).p0ActiveMonIndex, 0); } // environmental effect kills mon after switch in from player move and forces switch @@ -2703,7 +2703,7 @@ contract EngineTest is Test, BattleHelper { // Switch for turn flag should be 0, Bob's active mon index should now be 0 (, BattleData memory state) = engine.getBattle(battleKey); assertEq(state.playerSwitchForTurnFlag, 0); - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); + assertEq(engine.getBattleContext(battleKey).p1ActiveMonIndex, 0); } // `AttackCalculator._calculateDamageCore` clamps a huge rawDamage to `type(int32).max` and hands diff --git a/test/SignedCommitManager.t.sol b/test/SignedCommitManager.t.sol index 50b8009f..cd1d51c7 100644 --- a/test/SignedCommitManager.t.sol +++ b/test/SignedCommitManager.t.sol @@ -754,9 +754,9 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint16(1), NO_OP_MOVE_INDEX, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 1, "turn should advance"); - uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); - assertEq(active[0], 1, "p0 should have switched in mon 1 via valid SWITCH"); - assertEq(active[1], 0, "p1 should have been force-switched to mon 0"); + BattleContext memory active = engine.getBattleContext(battleKey); + assertEq(active.p0ActiveMonIndex, 1, "p0 should have switched in mon 1 via valid SWITCH"); + assertEq(active.p1ActiveMonIndex, 0, "p1 should have been force-switched to mon 0"); } /// @notice Turn 0 with a regular (attack) moveIndex must also coerce, not try to run the attack. @@ -767,9 +767,9 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { _executeDualSigned(battleKey, 0, 0, 0, 0, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 1, "turn should advance"); - uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); - assertEq(active[0], 0, "p0 force-switched to mon 0"); - assertEq(active[1], 0, "p1 force-switched to mon 0"); + BattleContext memory active = engine.getBattleContext(battleKey); + assertEq(active.p0ActiveMonIndex, 0, "p0 force-switched to mon 0"); + assertEq(active.p1ActiveMonIndex, 0, "p1 force-switched to mon 0"); } /// @notice Regular move with an out-of-bounds moveIndex must silently no-op rather than revert @@ -806,8 +806,8 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { _executeDualSigned(battleKey, 1, SWITCH_MOVE_INDEX, uint16(99), NO_OP_MOVE_INDEX, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 2, "turn should advance"); - uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); - assertEq(active[1], 0, "p1 still on mon 0 - OOB switch silently no-oped"); + BattleContext memory active = engine.getBattleContext(battleKey); + assertEq(active.p1ActiveMonIndex, 0, "p1 still on mon 0 - OOB switch silently no-oped"); } /// @notice Switch to the same mon on a non-turn-0 must silently no-op. @@ -816,14 +816,14 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { // Turn 0: p0 switches to mon 1, p1 to mon 0. _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint16(1), SWITCH_MOVE_INDEX, 0); - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); + assertEq(engine.getBattleContext(battleKey).p0ActiveMonIndex, 1); // Turn 1: p1 is committer. p1 tries to switch to their own active mon (0). p0 NO_OP. _executeDualSigned(battleKey, 1, SWITCH_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 2, "turn should advance"); - uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); - assertEq(active[1], 0, "p1 still on mon 0 - same-mon switch silent no-op"); + BattleContext memory active = engine.getBattleContext(battleKey); + assertEq(active.p1ActiveMonIndex, 0, "p1 still on mon 0 - same-mon switch silent no-op"); } function test_revertBattleNotCommiter() public { diff --git a/test/effects/EffectTest.sol b/test/effects/EffectTest.sol index bde86180..e709b6b5 100644 --- a/test/effects/EffectTest.sol +++ b/test/effects/EffectTest.sol @@ -307,7 +307,7 @@ contract EffectTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); + assertEq(engine.getBattleContext(battleKey).p0ActiveMonIndex, 1); } /** @@ -631,13 +631,13 @@ contract EffectTest is Test, BattleHelper { // The move should outspeed the swap, so the swap doesn't happen // So Bob's active mon index should still be 0 - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); + assertEq(engine.getBattleContext(battleKey).p1ActiveMonIndex, 0); // Alice uses slower Zap, Bob switches to mon index 1 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, SWITCH_MOVE_INDEX, 0, uint16(1)); // Bob's active mon index should be 1 (swap goes before getting Zapped) - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 1); + assertEq(engine.getBattleContext(battleKey).p1ActiveMonIndex, 1); // Bob's active mon should have the Zap effect (EffectInstance[] memory zapEffects,) = engine.getEffects(battleKey, 1, 1); @@ -649,7 +649,7 @@ contract EffectTest is Test, BattleHelper { ); // Check that Bob's active mon index is now 0, and the effect is still there - assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); + assertEq(engine.getBattleContext(battleKey).p1ActiveMonIndex, 0); (EffectInstance[] memory zapEffectsAfter,) = engine.getEffects(battleKey, 1, 1); assertEq(zapEffectsAfter.length, 1); diff --git a/test/mons/EmbursaTest.sol b/test/mons/EmbursaTest.sol index fe73967b..44a8c012 100644 --- a/test/mons/EmbursaTest.sol +++ b/test/mons/EmbursaTest.sol @@ -425,11 +425,11 @@ contract EmbursaTest is Test, BattleHelper { } function _aliceActive(bytes32 battleKey) internal view returns (uint256) { - return engine.getActiveMonIndexForBattleState(battleKey)[0]; + return engine.getBattleContext(battleKey).p0ActiveMonIndex; } function _bobActive(bytes32 battleKey) internal view returns (uint256) { - return engine.getActiveMonIndexForBattleState(battleKey)[1]; + return engine.getBattleContext(battleKey).p1ActiveMonIndex; } /** diff --git a/test/mons/NirvammaTest.sol b/test/mons/NirvammaTest.sol index 015ccd2b..01a802f0 100644 --- a/test/mons/NirvammaTest.sol +++ b/test/mons/NirvammaTest.sol @@ -186,8 +186,8 @@ contract NirvammaTest is Test, BattleHelper { "Nirvamma HP after own trigger mismatch" ); // Alice should have force-swapped to filler (mon 1). - uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); - assertEq(active[0], 1, "Alice should have force-swapped to filler"); + BattleContext memory active = engine.getBattleContext(battleKey); + assertEq(active.p0ActiveMonIndex, 1, "Alice should have force-swapped to filler"); } } @@ -215,8 +215,8 @@ contract NirvammaTest is Test, BattleHelper { int32 hpAfter = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); assertLe(hpAfter, bobHpBefore - 10, "Bob's mon should take >= 10 extra damage from opp trigger"); // Bob should have force-swapped. - uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); - assertEq(active[1], 1, "Bob should have force-swapped to filler"); + BattleContext memory active = engine.getBattleContext(battleKey); + assertEq(active.p1ActiveMonIndex, 1, "Bob should have force-swapped to filler"); } function test_hardReset_selfRemovesAfterBothFire() public { diff --git a/test/mons/PengymTest.sol b/test/mons/PengymTest.sol index 8b4cb6a7..b57d7008 100644 --- a/test/mons/PengymTest.sol +++ b/test/mons/PengymTest.sol @@ -590,20 +590,20 @@ contract PengymTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); // Active mon for Bob should be 1 - uint256 bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; + uint256 bobActiveMonIndex = engine.getBattleContext(battleKey).p1ActiveMonIndex; assertEq(bobActiveMonIndex, 1, "Swap succeeded (0 -> 1)"); // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); // Active mon for Bob should be 0 - bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; + bobActiveMonIndex = engine.getBattleContext(battleKey).p1ActiveMonIndex; assertEq(bobActiveMonIndex, 0, "Swap succeeded (1 -> 0)"); // Alice selects pistol squat, Bob does nothing (and dies) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); - bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; + bobActiveMonIndex = engine.getBattleContext(battleKey).p1ActiveMonIndex; assertEq(bobActiveMonIndex, 0, "No swap because KO"); // Bob sends in mon index 1 @@ -622,7 +622,7 @@ contract PengymTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); // Now Bob has mon index 2 (already took damage) and mon index 3 - bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; + bobActiveMonIndex = engine.getBattleContext(battleKey).p1ActiveMonIndex; assertEq(bobActiveMonIndex, 3, "Switch forced"); // Bob switches back to mon index 2, Alice does nothing @@ -639,7 +639,7 @@ contract PengymTest is Test, BattleHelper { engine.resetCallContext(); // Alice tries to force a switch, but active mon should not change _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); - bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; + bobActiveMonIndex = engine.getBattleContext(battleKey).p1ActiveMonIndex; assertEq(bobActiveMonIndex, 3, "No mons left"); } } diff --git a/transpiler/strategies-rs/src/doubles.rs b/transpiler/strategies-rs/src/doubles.rs index a9a0e913..1e598365 100644 --- a/transpiler/strategies-rs/src/doubles.rs +++ b/transpiler/strategies-rs/src/doubles.rs @@ -251,7 +251,7 @@ pub fn pick_side_moves( let is_forced = flag != 2; // 2 == both slots take a normal action let mask = flag & 0x0f; // which absolute slots must forced-switch let slots = active_slots(sim, bk); - let team_size = u64::try_from(Engine::getTeamSize(&mut sim.world, bk, U256::from(cpu_side))).unwrap_or(0) as usize; + let team_size = sim.team_size_phys(U256::from(cpu_side)); let m0 = decide_slot(sim, bk, cpu_side, difficulty, is_forced, mask, &slots, team_size, a0, rng); let m1 = decide_slot(sim, bk, cpu_side, difficulty, is_forced, mask, &slots, team_size, a1, rng); @@ -306,8 +306,8 @@ const D_W_HP: f64 = 1.0; const D_W_KO: f64 = 150.0; const D_W_STAMINA: f64 = 2.0; -fn team_size_of(sim: &mut Sim, bk: B256, side: u8) -> usize { - u64::try_from(Engine::getTeamSize(&mut sim.world, bk, U256::from(side))).unwrap_or(0) as usize +fn team_size_of(sim: &mut Sim, _bk: B256, side: u8) -> usize { + sim.team_size_phys(U256::from(side)) } /// Σ hp% over a side's whole roster. diff --git a/transpiler/strategies-rs/src/sim.rs b/transpiler/strategies-rs/src/sim.rs index 84d2a409..e67fb8b8 100644 --- a/transpiler/strategies-rs/src/sim.rs +++ b/transpiler/strategies-rs/src/sim.rs @@ -81,9 +81,8 @@ pub struct Sim { pub world: World, pub battle_key: B256, pub engine_addr: Address, - /// (p0, p1) team sizes, immutable after `startBattle` — cached because - /// the transpiled `getTeamSize` getter deep-clones the whole - /// BattleConfig per call, and view captures read sizes per fork. + /// (p0, p1) team sizes, immutable after `startBattle` — cached so a size + /// read doesn't deep-clone BattleConfig; view captures read sizes per fork. team_sizes: (usize, usize), /// (p0, p1) global mon-ids per team slot — carried from the drafted teams /// so the CPU can look up per-mon config by identity (the engine only diff --git a/transpiler/strategies-rs/src/view.rs b/transpiler/strategies-rs/src/view.rs index 0c0a8112..2fd61c3b 100644 --- a/transpiler/strategies-rs/src/view.rs +++ b/transpiler/strategies-rs/src/view.rs @@ -75,11 +75,11 @@ pub fn switch_flag(sim: &mut Sim, seat: Seat, bk: B256) -> u8 { } } -/// Virtual [opp active, cpu active] (the proxied getActiveMonIndex swap). +/// Virtual [opp active, cpu active] — active indices off getBattleContext, seat-swapped. pub fn active_mon_indices(sim: &mut Sim, seat: Seat, bk: B256) -> (usize, usize) { - let r = Engine::getActiveMonIndexForBattleState(&mut sim.world, bk); - let p0 = u64::try_from(r[0]).unwrap() as usize; - let p1 = u64::try_from(r[1]).unwrap() as usize; + let ctx = Engine::getBattleContext(&mut sim.world, bk); + let p0 = ctx.p0ActiveMonIndex as usize; + let p1 = ctx.p1ActiveMonIndex as usize; if seat.flipped() { (p1, p0) } else { From c0fa04bed5851a6262a2d1e6ecd875598d12762a Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:16:24 -0700 Subject: [PATCH 13/41] wip transpiler fixes --- transpiler/strategies-rs/src/arena.rs | 151 ++++++++++++++++++---- transpiler/strategies-rs/src/bin/arena.rs | 32 ++++- transpiler/strategies-rs/src/doubles.rs | 110 ++++++++++++---- 3 files changed, 241 insertions(+), 52 deletions(-) diff --git a/transpiler/strategies-rs/src/arena.rs b/transpiler/strategies-rs/src/arena.rs index 72d0ef6b..7a3639da 100644 --- a/transpiler/strategies-rs/src/arena.rs +++ b/transpiler/strategies-rs/src/arena.rs @@ -3,7 +3,7 @@ //! aggregates win rates. No bun, no chomp_run_games. use chomp_engine::Structs::Mon; -use crate::doubles::{run_doubles_games, Difficulty, DoublesSpec}; +use crate::doubles::{run_doubles_games, Difficulty, DoublesEvalW, DoublesSpec}; use crate::evaluator::{Weights, DEFAULT_WEIGHTS}; use crate::game::{run_games, GameSpec, StrategyKind}; use crate::roster::{self, Roster, RosterMon}; @@ -281,25 +281,20 @@ impl DiffPairStats { /// pilot (side 0) over `games` matched drafts. The Phase-3 substrate exit check. pub fn doubles_search_winrate(roster: &Roster, depth: u32, games: usize, wseed: u32, seed_base: u32, threads: usize) -> f64 { let book = roster::address_book(); - let mon = |id: u32| roster.mons.iter().find(|m| m.id == id).expect("drawn mon id in roster"); let mut rng = Wrand::new(wseed); let mut specs = Vec::with_capacity(games); for i in 0..games { - let p0_ids = draw_team(roster, &mut rng); - let p1_ids = draw_team(roster, &mut rng); - specs.push(DoublesSpec { - seed: seed_base.wrapping_add(i as u32), - max_turns: MAX_TURNS, - mons_per_team: TEAM_SIZE as u64, - p0_team: p0_ids.iter().map(|&id| build_team_mon(mon(id))).collect(), - p1_team: p1_ids.iter().map(|&id| build_team_mon(mon(id))).collect(), + let (p0_team, p0_ids) = draw_built_team(roster, &mut rng); + let (p1_team, p1_ids) = draw_built_team(roster, &mut rng); + specs.push(doubles_spec( + seed_base.wrapping_add(i as u32), + p0_team, p0_ids, + DoublesSideCfg::default(), // side 0 = epsilon-greedy Hard baseline + p1_team, p1_ids, - p0_difficulty: Difficulty::Hard, // side 0 = epsilon-greedy Hard baseline - p1_difficulty: Difficulty::Hard, // unused (side 1 searches) - p0_search_depth: 0, - p1_search_depth: depth, - }); + DoublesSideCfg { depth, ..Default::default() }, + )); } let outcomes = run_doubles_games(&specs, &book, threads); let (mut p1, mut decisive) = (0u32, 0u32); @@ -347,19 +342,15 @@ pub fn build_doubles_specs( let p1_ids = draw_team(roster, &mut rng); let p0_team = equip(&p0_ids, &mut lrng); let p1_team = equip(&p1_ids, &mut lrng); - specs.push(DoublesSpec { - seed: seed_base.wrapping_add(i as u32), - max_turns: MAX_TURNS, - mons_per_team: TEAM_SIZE as u64, + specs.push(doubles_spec( + seed_base.wrapping_add(i as u32), p0_team, - p1_team, p0_ids, + DoublesSideCfg { difficulty: p0d, depth: search_depth, ..Default::default() }, + p1_team, p1_ids, - p0_difficulty: p0d, - p1_difficulty: p1d, - p0_search_depth: search_depth, - p1_search_depth: search_depth, - }); + DoublesSideCfg { difficulty: p1d, depth: search_depth, ..Default::default() }, + )); pair_of.push(pi); } (specs, pair_of) @@ -386,6 +377,116 @@ pub fn run_doubles_arena(roster: &Roster, games: usize, wseed: u32, seed_base: u stats } +/// One side's doubles driver config: pilot difficulty (used at depth 0), search depth, eval weights. +#[derive(Clone, Copy)] +pub struct DoublesSideCfg { + pub difficulty: Difficulty, + pub depth: u32, + pub eval: DoublesEvalW, +} +impl Default for DoublesSideCfg { + fn default() -> Self { + Self { difficulty: Difficulty::Hard, depth: 0, eval: DoublesEvalW::default() } + } +} + +/// Draw a team and materialize its default-loadout mons. +fn draw_built_team(roster: &Roster, rng: &mut Wrand) -> (Vec, Vec) { + let ids = draw_team(roster, rng); + let team = ids.iter().map(|&id| build_team_mon(roster.mon_by_id(id).expect("drawn mon id in roster"))).collect(); + (team, ids) +} + +/// Fill a DoublesSpec from per-side configs — the one place the spec's field mapping lives. +#[allow(clippy::too_many_arguments)] +fn doubles_spec( + seed: u32, + p0_team: Vec, + p0_ids: Vec, + p0: DoublesSideCfg, + p1_team: Vec, + p1_ids: Vec, + p1: DoublesSideCfg, +) -> DoublesSpec { + DoublesSpec { + seed, + max_turns: MAX_TURNS, + mons_per_team: TEAM_SIZE as u64, + p0_team, + p1_team, + p0_ids, + p1_ids, + p0_difficulty: p0.difficulty, + p1_difficulty: p1.difficulty, + p0_search_depth: p0.depth, + p1_search_depth: p1.depth, + p0_eval: p0.eval, + p1_eval: p1.eval, + } +} + +pub struct DoublesAbResult { + /// Candidate's win share of decisive games. + pub share: f64, + pub cand_wins: u32, + pub base_wins: u32, + pub draws: u32, +} + +/// A/B two search configs: every drawn team pair plays twice with the same teams and seats but the +/// CONFIGS exchanged, so team-matchup luck and any engine side bias cancel per pair. Returns the +/// candidate (`cand`) side's record vs the baseline (`base`). +pub fn doubles_ab_winrate( + roster: &Roster, + cand: DoublesSideCfg, + base: DoublesSideCfg, + games: usize, + wseed: u32, + seed_base: u32, + threads: usize, +) -> DoublesAbResult { + let book = roster::address_book(); + let mut rng = Wrand::new(wseed); + // Both seats search in an A/B (depth 0 would compare the un-evaluated greedy pilot instead). + let cand = DoublesSideCfg { depth: cand.depth.max(1), ..cand }; + let base = DoublesSideCfg { depth: base.depth.max(1), ..base }; + let pairs = (games / 2).max(1); + let mut specs = Vec::with_capacity(pairs * 2); + for i in 0..pairs { + let (p0_team, p0_ids) = draw_built_team(roster, &mut rng); + let (p1_team, p1_ids) = draw_built_team(roster, &mut rng); + // Game A: candidate in the p1 seat; game B: same everything, configs swapped. + for &(c1, c0) in &[(cand, base), (base, cand)] { + specs.push(doubles_spec( + seed_base.wrapping_add(i as u32), + p0_team.clone(), + p0_ids.clone(), + c0, + p1_team.clone(), + p1_ids.clone(), + c1, + )); + } + } + let outcomes = run_doubles_games(&specs, &book, threads); + let (mut cand_wins, mut base_wins, mut draws) = (0u32, 0u32, 0u32); + for (i, o) in outcomes.iter().enumerate() { + let cand_seat = if i % 2 == 0 { 1 } else { 0 }; + match o.winner_side { + Some(s) if s == cand_seat => cand_wins += 1, + Some(_) => base_wins += 1, + None => draws += 1, + } + } + let decisive = cand_wins + base_wins; + DoublesAbResult { + share: if decisive == 0 { 0.0 } else { cand_wins as f64 / decisive as f64 }, + cand_wins, + base_wins, + draws, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/transpiler/strategies-rs/src/bin/arena.rs b/transpiler/strategies-rs/src/bin/arena.rs index 2ea77fb9..a32d0415 100644 --- a/transpiler/strategies-rs/src/bin/arena.rs +++ b/transpiler/strategies-rs/src/bin/arena.rs @@ -5,7 +5,10 @@ //! Data (drool/*.csv, src/mons/*.json) is read relative to CHOMP_ROOT (default: the repo root //! inferred from the crate location). -use chomp_strategies::arena::{doubles_search_winrate, eval_weights_winrate, run_arena, run_doubles_arena}; +use chomp_strategies::arena::{ + doubles_ab_winrate, doubles_search_winrate, eval_weights_winrate, run_arena, run_doubles_arena, DoublesSideCfg, +}; +use chomp_strategies::doubles::DoublesEvalW; use chomp_strategies::evaluator::{Weights, DEFAULT_WEIGHTS, N_FEATURES}; use chomp_strategies::game::StrategyKind; use chomp_strategies::roster::load_roster; @@ -56,6 +59,33 @@ fn main() { let search_depth = arg_u(&args, "--search-depth", 0) as u32; let peek = args.iter().any(|a| a == "--peek"); // peek-at-root best-response for the search seat + // Doubles eval-weight A/B: candidate (side flags below) vs the default-weight baseline, both + // searching. Same teams/seats per pair with configs exchanged, so only the config differs. + // --mode doubles-ab [--depth N] [--base-depth N] [--wboost F] [--wstatus F] [--wskip F] [--gateko] + if mode == "doubles-ab" { + let argf = |flag: &str, def: f64| arg(&args, flag).map(|v| v.parse().expect("float flag")).unwrap_or(def); + let depth = arg_u(&args, "--depth", 1) as u32; + let base_depth = arg_u(&args, "--base-depth", depth as u64) as u32; + let eval = DoublesEvalW { + w_boost: argf("--wboost", 0.0), + w_status: argf("--wstatus", 0.0), + w_skip: argf("--wskip", 0.0), + gate_ko: args.iter().any(|a| a == "--gateko"), + ..DoublesEvalW::default() + }; + let cand = DoublesSideCfg { depth, eval, ..Default::default() }; + let base = DoublesSideCfg { depth: base_depth, ..Default::default() }; + let started = std::time::Instant::now(); + let r = doubles_ab_winrate(&roster, cand, base, games, seed, seed_base, threads); + let elapsed = started.elapsed().as_secs_f64(); + println!( + "cand d{depth} boost={} status={} skip={} gateko={} vs base d{base_depth} default · {} games · win {:.1}% ({}-{}-{} draws)", + eval.w_boost, eval.w_status, eval.w_skip, eval.gate_ko, games, r.share * 100.0, r.cand_wins, r.base_wins, r.draws + ); + eprintln!("{games} games in {elapsed:.2}s ({:.0} games/s)", games as f64 / elapsed); + return; + } + // Doubles maximin search vs the epsilon-greedy Hard baseline (Phase-3 substrate check). if mode == "doubles" && search_depth >= 1 { let started = std::time::Instant::now(); diff --git a/transpiler/strategies-rs/src/doubles.rs b/transpiler/strategies-rs/src/doubles.rs index 1e598365..c2bb8ac8 100644 --- a/transpiler/strategies-rs/src/doubles.rs +++ b/transpiler/strategies-rs/src/doubles.rs @@ -24,7 +24,9 @@ use crate::jsrng::{random_salt, JsRng}; use crate::roster::{input_type_of, target_spec_of, InputType, TargetSpec}; use crate::shared::{build_damage_calc_context, estimate_damage_meta}; use crate::sim::{pack_side, Sim}; -use crate::view::{decode_meta, mon_current_stamina, mon_max_hp, mon_state, move_slot, turn_id, Seat}; +use crate::view::{ + decode_meta, mon_current_stamina, mon_max_hp, mon_skip_turn, mon_state, move_slot, stat_delta_score, turn_id, Seat, +}; const SWITCH: u8 = 125; const NO_OP: u8 = 126; @@ -306,6 +308,38 @@ const D_W_HP: f64 = 1.0; const D_W_KO: f64 = 150.0; const D_W_STAMINA: f64 = 2.0; +/// Doubles eval weights. Defaults reproduce the frozen baseline (hp/ko/stamina, corpses counted), +/// so every existing caller is unchanged; the extra terms exist to be A/B'd in the arena. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct DoublesEvalW { + pub w_hp: f64, + pub w_ko: f64, + pub w_stamina: f64, + /// Per Σ(delta/base) unit over a live active's five combat stats (the TS twin's 0.4-per-% ≡ 40 here). + pub w_boost: f64, + /// Per live opposing active carrying a status class, minus ours (statuses hurt their carrier). + pub w_status: f64, + /// Per live opposing active flagged ShouldSkipTurn, minus ours. + pub w_skip: f64, + /// Exclude KO'd actives from the per-active terms — a corpse holds its lane (and stat deltas) + /// until the forced switch, but its stamina/boosts/status are worth nothing. + pub gate_ko: bool, +} + +impl Default for DoublesEvalW { + fn default() -> Self { + Self { + w_hp: D_W_HP, + w_ko: D_W_KO, + w_stamina: D_W_STAMINA, + w_boost: 0.0, + w_status: 0.0, + w_skip: 0.0, + gate_ko: false, + } + } +} + fn team_size_of(sim: &mut Sim, _bk: B256, side: u8) -> usize { sim.team_size_phys(U256::from(side)) } @@ -324,28 +358,49 @@ fn side_roster_hp(sim: &mut Sim, bk: B256, side: u8, team_size: usize) -> f64 { sum } -/// Σ current stamina over a side's two active slots. -fn side_active_stamina(sim: &mut Sim, bk: B256, side: u8, slots: &[u32; 4]) -> i64 { - let mut sum = 0; +/// Per-active term sums (stamina, boost, status, skip) over a side's two active slots. Feature +/// reads are skipped while their weight is 0 so the default eval costs what it always did. +fn side_active_terms(sim: &mut Sim, bk: B256, side: u8, slots: &[u32; 4], w: &DoublesEvalW, ko: u32) -> (f64, f64, f64, f64) { + let (mut stam, mut boost, mut status, mut skip) = (0.0f64, 0.0f64, 0.0f64, 0.0f64); for &abs in &side_slots(side) { let mon = slots[abs]; - if mon != EMPTY_LANE { - sum += mon_current_stamina(sim, OBS, bk, side, mon as usize); + if mon == EMPTY_LANE || (w.gate_ko && ko & (1 << mon) != 0) { + continue; + } + stam += mon_current_stamina(sim, OBS, bk, side, mon as usize) as f64; + if w.w_boost != 0.0 { + boost += stat_delta_score(sim, OBS, bk, side, mon as usize); + } + if w.w_status != 0.0 + && Engine::getMonStatusClass(&mut sim.world, bk, U256::from(side), U256::from(mon)) != U256::from(0u64) + { + status += 1.0; + } + if w.w_skip != 0.0 && mon_skip_turn(sim, OBS, bk, side, mon as usize) { + skip += 1.0; } } - sum + (stam, boost, status, skip) } /// Linear 2-slot position value, `cpu_side`-perspective (higher = better). -fn doubles_eval(sim: &mut Sim, bk: B256, cpu_side: u8) -> f64 { +fn doubles_eval(sim: &mut Sim, bk: B256, cpu_side: u8, w: &DoublesEvalW) -> f64 { let opp_side = 1 - cpu_side; let cpu_ts = team_size_of(sim, bk, cpu_side); let opp_ts = team_size_of(sim, bk, opp_side); let hp = side_roster_hp(sim, bk, cpu_side, cpu_ts) - side_roster_hp(sim, bk, opp_side, opp_ts); - let ko = (ko_bitmap(sim, bk, opp_side).count_ones() as i64 - ko_bitmap(sim, bk, cpu_side).count_ones() as i64) as f64; + let my_ko = ko_bitmap(sim, bk, cpu_side); + let op_ko = ko_bitmap(sim, bk, opp_side); + let ko = (op_ko.count_ones() as i64 - my_ko.count_ones() as i64) as f64; let slots = active_slots(sim, bk); - let stam = (side_active_stamina(sim, bk, cpu_side, &slots) - side_active_stamina(sim, bk, opp_side, &slots)) as f64; - D_W_HP * hp + D_W_KO * ko + D_W_STAMINA * stam + let (my_stam, my_boost, my_status, my_skip) = side_active_terms(sim, bk, cpu_side, &slots, w, my_ko); + let (op_stam, op_boost, op_status, op_skip) = side_active_terms(sim, bk, opp_side, &slots, w, op_ko); + w.w_hp * hp + + w.w_ko * ko + + w.w_stamina * (my_stam - op_stam) + + w.w_boost * (my_boost - op_boost) + + w.w_status * (op_status - my_status) + + w.w_skip * (op_skip - my_skip) } /// Terminal value if a side is fully KO'd, else None. Mate-distance discounted: @@ -483,12 +538,12 @@ fn forced_joint_model(sim: &mut Sim, bk: B256, side: u8, mask: u32, slots: &[u32 /// Recursive maximin value at `bk`, cpu_side-perspective. Forced half-turns are resolved /// deterministically and recursed WITHOUT consuming depth (they don't burn horizon; the chain is /// bounded by roster size / the terminal check). -fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> f64 { +fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW) -> f64 { if let Some(v) = terminal(sim, bk, cpu_side, depth) { return v; } if depth == 0 { - return doubles_eval(sim, bk, cpu_side); + return doubles_eval(sim, bk, cpu_side, w); } let flag = Engine::getBattleContext(&mut sim.world, bk).playerSwitchForTurnFlag as u32; if flag != 2 { @@ -497,10 +552,10 @@ fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> f64 { let mine = forced_joint_model(sim, bk, cpu_side, mask, &slots); let theirs = forced_joint_model(sim, bk, 1 - cpu_side, mask, &slots); if mine == (NO_OP_MOVE, NO_OP_MOVE) && theirs == (NO_OP_MOVE, NO_OP_MOVE) { - return doubles_eval(sim, bk, cpu_side); // no legal resolution — don't loop + return doubles_eval(sim, bk, cpu_side, w); // no legal resolution — don't loop } let child = fork_joint(sim, bk, cpu_side, mine, theirs); - let v = search_value(sim, child, cpu_side, depth); + let v = search_value(sim, child, cpu_side, depth, w); sim.dispose_fork(child); return v; } @@ -512,7 +567,7 @@ fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> f64 { let mut worst = f64::INFINITY; for &theirs in &opp { let child = fork_joint(sim, bk, cpu_side, mine, theirs); - let v = search_value(sim, child, cpu_side, depth - 1); + let v = search_value(sim, child, cpu_side, depth - 1, w); sim.dispose_fork(child); if v < worst { worst = v; @@ -531,9 +586,9 @@ fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> f64 { /// Pick `cpu_side`'s two slot moves by depth-`depth` joint maximin (turn 0 → leads; forced-switch → /// first-legal bench; normal turn → search). A reverting hypothetical mid-search is contained to /// this decision (fall back to resting both slots) rather than aborting the game. -pub fn search_side_moves(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> (SlotMove, SlotMove) { +pub fn search_side_moves(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW) -> (SlotMove, SlotMove) { let saved_fc = sim.fork_counter(); - match catch_unwind(AssertUnwindSafe(|| search_side_moves_inner(sim, bk, cpu_side, depth))) { + match catch_unwind(AssertUnwindSafe(|| search_side_moves_inner(sim, bk, cpu_side, depth, w))) { Ok(m) => m, Err(_) => { sim.set_fork_counter(saved_fc); @@ -542,7 +597,7 @@ pub fn search_side_moves(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> ( } } -fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> (SlotMove, SlotMove) { +fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW) -> (SlotMove, SlotMove) { let [a0, a1] = side_slots(cpu_side); let sw = |i: u16| SlotMove { move_index: SWITCH, extra_data: i }; @@ -567,7 +622,7 @@ fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> let mut worst = f64::INFINITY; for &theirs in &opp { let child = fork_joint(sim, bk, cpu_side, mine, theirs); - let v = search_value(sim, child, cpu_side, depth.saturating_sub(1)); + let v = search_value(sim, child, cpu_side, depth.saturating_sub(1), w); sim.dispose_fork(child); if v < worst { worst = v; @@ -618,7 +673,7 @@ fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> let mut best_val = f64::NEG_INFINITY; for &mine in &combos { let child = fork_joint(sim, bk, cpu_side, mine, theirs); - let v = search_value(sim, child, cpu_side, depth); + let v = search_value(sim, child, cpu_side, depth, w); sim.dispose_fork(child); if v > best_val { best_val = v; @@ -638,7 +693,7 @@ fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32) -> let mut worst = f64::INFINITY; for &theirs in &opp { let child = fork_joint(sim, bk, cpu_side, mine, theirs); - let v = search_value(sim, child, cpu_side, depth - 1); + let v = search_value(sim, child, cpu_side, depth - 1, w); sim.dispose_fork(child); if v < worst { worst = v; @@ -670,6 +725,9 @@ pub struct DoublesSpec { /// Per-side search depth: 0 = epsilon-greedy at the difficulty; ≥1 = joint maximin search. pub p0_search_depth: u32, pub p1_search_depth: u32, + /// Per-side eval weights for the search tiers (ignored at depth 0). + pub p0_eval: DoublesEvalW, + pub p1_eval: DoublesEvalW, } pub struct DoublesOutcome { @@ -698,12 +756,12 @@ pub fn play_doubles_game(spec: &DoublesSpec, book: &HashMap) -> } let bk = sim.battle_key; let (p0a, p0b) = if spec.p0_search_depth > 0 { - search_side_moves(&mut sim, bk, 0, spec.p0_search_depth) + search_side_moves(&mut sim, bk, 0, spec.p0_search_depth, &spec.p0_eval) } else { pick_side_moves(&mut sim, bk, 0, spec.p0_difficulty, &mut rng) }; let (p1a, p1b) = if spec.p1_search_depth > 0 { - search_side_moves(&mut sim, bk, 1, spec.p1_search_depth) + search_side_moves(&mut sim, bk, 1, spec.p1_search_depth, &spec.p1_eval) } else { pick_side_moves(&mut sim, bk, 1, spec.p1_difficulty, &mut rng) }; @@ -863,12 +921,12 @@ pub fn play_doubles_game_instrumented( } let bk = sim.battle_key; let (p0a, p0b) = if spec.p0_search_depth > 0 { - search_side_moves(&mut sim, bk, 0, spec.p0_search_depth) + search_side_moves(&mut sim, bk, 0, spec.p0_search_depth, &spec.p0_eval) } else { pick_side_moves(&mut sim, bk, 0, spec.p0_difficulty, &mut rng) }; let (p1a, p1b) = if spec.p1_search_depth > 0 { - search_side_moves(&mut sim, bk, 1, spec.p1_search_depth) + search_side_moves(&mut sim, bk, 1, spec.p1_search_depth, &spec.p1_eval) } else { pick_side_moves(&mut sim, bk, 1, spec.p1_difficulty, &mut rng) }; From facd40850532197f82e9730841178674ca84d7f3 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:22:40 -0700 Subject: [PATCH 14/41] optimizing singles/doubles --- .gitignore | 2 + CLAUDE.md | 12 ++ foundry.toml | 14 +- snapshots/DoublesGasBenchmark.json | 68 ++++-- snapshots/MainnetReplayGasTest.json | 32 +++ snapshots/ProdPvPGasBenchmark.json | 18 +- src/Engine.sol | 322 ++++++++++++++++++++-------- src/Structs.sol | 5 + test/DoublesGasBenchmark.t.sol | 122 ++++++++++- test/MainnetReplayGasTest.t.sol | 310 +++++++++++++++++++++++++- test/abstract/GasMeasure.sol | 131 ++++++++++- 11 files changed, 914 insertions(+), 122 deletions(-) create mode 100644 snapshots/MainnetReplayGasTest.json diff --git a/.gitignore b/.gitignore index bf75d77c..6f6bd932 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Compiler files cache/ out/ +cache-fast/ +out-fast/ # Ignores development broadcast logs !/broadcast diff --git a/CLAUDE.md b/CLAUDE.md index 72069179..ce55d179 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,18 @@ forge test # Run all tests forge test -vvv # Run tests with verbose output ``` +Engine edits invalidate most of the production-profile dependency graph. For correctness-only +iteration, use the separate fast profile and a narrow path; it keeps via-IR (required by Engine) +but disables optimization/build-info, enables sparse compilation, and uses a separate cache: + +```bash +FOUNDRY_PROFILE=fast forge test --match-path test/EngineTest.sol +``` + +Do not use `profile.fast` for gas comparisons or snapshots. Run those and the final full-suite +gate under the default profile (`forge test ...`); the fast profile disables snapshot emission so +an accidental gas-test run cannot overwrite production-profile numbers. + ## Repository Structure ``` diff --git a/foundry.toml b/foundry.toml index c6adec91..5d6714fb 100644 --- a/foundry.toml +++ b/foundry.toml @@ -24,9 +24,21 @@ yul = true [profile.default.optimizer_details.yulDetails] stackAllocation = true +# Fast correctness iteration for Engine-heavy changes. This deliberately does NOT produce +# comparable gas numbers: keep the default profile for snapshots and final benchmarks. +# Separate artifacts preserve both caches when switching profiles. +[profile.fast] +out = 'out-fast' +cache_path = 'cache-fast' +optimizer = false +via_ir = true +sparse_mode = true +build_info = false +gas_snapshot_emit = false + [fmt] sort_imports = true # forge 1.5.x's formatter DROPS THE BRACES of one-line multi-statement if bodies under the # default/preserve/single modes ("if (c) { a; break; }" -> unconditional break). "multi" is # the one mode whose rewrite is brace-correct — verified on if+break / if+else / for shapes. -single_line_statement_blocks = "multi" \ No newline at end of file +single_line_statement_blocks = "multi" diff --git a/snapshots/DoublesGasBenchmark.json b/snapshots/DoublesGasBenchmark.json index c0c38da8..1c42baf9 100644 --- a/snapshots/DoublesGasBenchmark.json +++ b/snapshots/DoublesGasBenchmark.json @@ -1,31 +1,63 @@ { - "Doubles_Batch12_coldGas": "772352", - "Doubles_Batch12_coldSload": "34", + "DoublesFloor_AllAttack8_rawGas": "926951", + "DoublesFloor_AllAttack_perTurn": "115868", + "DoublesFloor_AllRest8_rawGas": "389089", + "DoublesFloor_AllRest_perTurn": "48636", + "DoublesFloor_OneAttack8_rawGas": "695767", + "DoublesFloor_OneAttack_perTurn": "86970", + "Doubles_Batch12_afterDamageCalls": "0", + "Doubles_Batch12_afterMoveCalls": "0", + "Doubles_Batch12_coldGas": "790395", + "Doubles_Batch12_coldSload": "35", + "Doubles_Batch12_effectDataReads": "2", + "Doubles_Batch12_effectDataWrites": "1", + "Doubles_Batch12_effectHeaderReads": "6", + "Doubles_Batch12_effectHeaderWrites": "3", "Doubles_Batch12_extraAccounts": "6", + "Doubles_Batch12_lifecycleCalls": "1", + "Doubles_Batch12_moveDecisionCallbacks": "0", + "Doubles_Batch12_nonHookHeaderReadUpperBound": "5", "Doubles_Batch12_noop": "23", "Doubles_Batch12_nzToNz": "33", - "Doubles_Batch12_prodStorageGas": "498900", - "Doubles_Batch12_totalSload": "1694", - "Doubles_Batch12_totalSstore": "65", - "Doubles_Batch12_zToNz": "7", - "Doubles_Drain6_coldGas": "486927", - "Doubles_Drain6_coldSload": "40", + "Doubles_Batch12_preDamageCalls": "0", + "Doubles_Batch12_prodStorageGas": "521900", + "Doubles_Batch12_roundEndCalls": "1", + "Doubles_Batch12_roundStartCalls": "0", + "Doubles_Batch12_totalSload": "1675", + "Doubles_Batch12_totalSstore": "67", + "Doubles_Batch12_updateStateCalls": "0", + "Doubles_Batch12_zToNz": "8", + "Doubles_Drain6_coldGas": "506626", + "Doubles_Drain6_coldSload": "41", "Doubles_Drain6_extraAccounts": "7", "Doubles_Drain6_noop": "11", "Doubles_Drain6_nzToNz": "28", - "Doubles_Drain6_prodStorageGas": "418200", - "Doubles_Drain6_totalSload": "898", - "Doubles_Drain6_totalSstore": "48", - "Doubles_Drain6_zToNz": "7", - "Doubles_KitBatch10_coldGas": "1025452", - "Doubles_KitBatch10_coldSload": "26", + "Doubles_Drain6_prodStorageGas": "442400", + "Doubles_Drain6_totalSload": "891", + "Doubles_Drain6_totalSstore": "50", + "Doubles_Drain6_zToNz": "8", + "Doubles_KitBatch10_afterDamageCalls": "0", + "Doubles_KitBatch10_afterMoveCalls": "0", + "Doubles_KitBatch10_coldGas": "1042457", + "Doubles_KitBatch10_coldSload": "27", + "Doubles_KitBatch10_effectDataReads": "9", + "Doubles_KitBatch10_effectDataWrites": "1", + "Doubles_KitBatch10_effectHeaderReads": "11", + "Doubles_KitBatch10_effectHeaderWrites": "2", "Doubles_KitBatch10_extraAccounts": "3", + "Doubles_KitBatch10_lifecycleCalls": "9", + "Doubles_KitBatch10_moveDecisionCallbacks": "0", + "Doubles_KitBatch10_nonHookHeaderReadUpperBound": "2", "Doubles_KitBatch10_noop": "21", "Doubles_KitBatch10_nzToNz": "60", - "Doubles_KitBatch10_prodStorageGas": "609600", - "Doubles_KitBatch10_totalSload": "2045", - "Doubles_KitBatch10_totalSstore": "90", - "Doubles_KitBatch10_zToNz": "8", + "Doubles_KitBatch10_preDamageCalls": "0", + "Doubles_KitBatch10_prodStorageGas": "628100", + "Doubles_KitBatch10_roundEndCalls": "9", + "Doubles_KitBatch10_roundStartCalls": "0", + "Doubles_KitBatch10_totalSload": "2010", + "Doubles_KitBatch10_totalSstore": "91", + "Doubles_KitBatch10_updateStateCalls": "0", + "Doubles_KitBatch10_zToNz": "9", "Doubles_Stage6_coldGas": "174663", "Doubles_Stage6_coldSload": "18", "Doubles_Stage6_extraAccounts": "6", diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json new file mode 100644 index 00000000..07e6c05e --- /dev/null +++ b/snapshots/MainnetReplayGasTest.json @@ -0,0 +1,32 @@ +{ + "MainnetDoublesReplay1_Reused_afterDamageCalls": "8", + "MainnetDoublesReplay1_Reused_afterMoveCalls": "5", + "MainnetDoublesReplay1_Reused_effectDataReads": "36", + "MainnetDoublesReplay1_Reused_effectDataWrites": "14", + "MainnetDoublesReplay1_Reused_effectHeaderReads": "54", + "MainnetDoublesReplay1_Reused_effectHeaderWrites": "16", + "MainnetDoublesReplay1_Reused_execGas": "1276607", + "MainnetDoublesReplay1_Reused_lifecycleCalls": "34", + "MainnetDoublesReplay1_Reused_moveDecisionCallbacks": "0", + "MainnetDoublesReplay1_Reused_nonHookHeaderReadUpperBound": "20", + "MainnetDoublesReplay1_Reused_preDamageCalls": "0", + "MainnetDoublesReplay1_Reused_roundEndCalls": "19", + "MainnetDoublesReplay1_Reused_roundStartCalls": "0", + "MainnetDoublesReplay1_Reused_statusClassCallbacks": "0", + "MainnetDoublesReplay1_Reused_updateStateCalls": "2", + "MainnetReplay_Reused_afterDamageCalls": "9", + "MainnetReplay_Reused_afterMoveCalls": "19", + "MainnetReplay_Reused_effectDataReads": "98", + "MainnetReplay_Reused_effectDataWrites": "36", + "MainnetReplay_Reused_effectHeaderReads": "186", + "MainnetReplay_Reused_effectHeaderWrites": "31", + "MainnetReplay_Reused_execGas": "2377783", + "MainnetReplay_Reused_lifecycleCalls": "84", + "MainnetReplay_Reused_moveDecisionCallbacks": "19", + "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "102", + "MainnetReplay_Reused_preDamageCalls": "0", + "MainnetReplay_Reused_roundEndCalls": "56", + "MainnetReplay_Reused_roundStartCalls": "0", + "MainnetReplay_Reused_statusClassCallbacks": "23", + "MainnetReplay_Reused_updateStateCalls": "0" +} \ No newline at end of file diff --git a/snapshots/ProdPvPGasBenchmark.json b/snapshots/ProdPvPGasBenchmark.json index c2828b64..e7a2abe7 100644 --- a/snapshots/ProdPvPGasBenchmark.json +++ b/snapshots/ProdPvPGasBenchmark.json @@ -1,28 +1,28 @@ { - "BuiltinBare_5Turns_coldGas": "291871", - "BuiltinBare_5Turns_coldSload": "70", + "BuiltinBare_5Turns_coldGas": "291616", + "BuiltinBare_5Turns_coldSload": "75", "BuiltinBare_5Turns_extraAccounts": "15", "BuiltinBare_5Turns_noop": "10", "BuiltinBare_5Turns_nzToNz": "33", - "BuiltinBare_5Turns_prodStorageGas": "359200", + "BuiltinBare_5Turns_prodStorageGas": "369200", "BuiltinBare_5Turns_totalSload": "435", "BuiltinBare_5Turns_totalSstore": "45", "BuiltinBare_5Turns_zToNz": "2", - "BuiltinHooked_5Turns_coldGas": "292689", - "BuiltinHooked_5Turns_coldSload": "70", + "BuiltinHooked_5Turns_coldGas": "292434", + "BuiltinHooked_5Turns_coldSload": "75", "BuiltinHooked_5Turns_extraAccounts": "15", "BuiltinHooked_5Turns_noop": "10", "BuiltinHooked_5Turns_nzToNz": "33", - "BuiltinHooked_5Turns_prodStorageGas": "359200", + "BuiltinHooked_5Turns_prodStorageGas": "369200", "BuiltinHooked_5Turns_totalSload": "435", "BuiltinHooked_5Turns_totalSstore": "45", "BuiltinHooked_5Turns_zToNz": "2", - "Builtin_Drain3_coldGas": "146311", - "Builtin_Drain3_coldSload": "20", + "Builtin_Drain3_coldGas": "146209", + "Builtin_Drain3_coldSload": "21", "Builtin_Drain3_extraAccounts": "3", "Builtin_Drain3_noop": "7", "Builtin_Drain3_nzToNz": "15", - "Builtin_Drain3_prodStorageGas": "155600", + "Builtin_Drain3_prodStorageGas": "157600", "Builtin_Drain3_totalSload": "236", "Builtin_Drain3_totalSstore": "24", "Builtin_Drain3_zToNz": "2", diff --git a/src/Engine.sol b/src/Engine.sol index 81621fc8..7088b465 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -57,6 +57,9 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // signal — and the salt read collapses from two transient loads to one. uint256 private transient _turnP0Packed; uint256 private transient _turnP1Packed; + // Two-slot action scheduler invalidation. Speed-mutating API chokepoints mark active lanes; + // the next greedy pick refreshes only those lanes from mon storage. + uint256 private transient _speedDirtySlots; // Errors error NoWriteAllowed(); @@ -439,6 +442,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { config.battleMode = battleMode; config.monStatusLanes = 0; config.p1BoostCounts = 0; + config.playerEffectStepsByMon = 0; if ((hookStepsUnion & (1 << uint8(EngineHookStep.OnBattleStart))) != 0) { for (uint256 i = 0; i < numHooks;) { @@ -1578,6 +1582,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { koOccurredFlag = 0; tempPreDamage = 0; effectsDirtyBitmap = 0; + _speedDirtySlots = 0; } /// @notice Forcibly end a stalled battle once it has run past MAX_BATTLE_DURATION. @@ -1686,6 +1691,9 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } else if (stateVarIndex == MonStateIndexName.Speed) { monState.speedDelta = (monState.speedDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.speedDelta + valueToAdd; + if (config.battleMode != BATTLE_MODE_SINGLES) { + _markActiveMonSpeedDirty(playerIndex, monIndex); + } } else if (stateVarIndex == MonStateIndexName.Attack) { monState.attackDelta = (monState.attackDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.attackDelta + valueToAdd; @@ -1717,19 +1725,9 @@ contract Engine is IEngine, MappingAllocator, EIP712 { monState.shouldSkipTurn = (valueToAdd % 2) == 1; } - // Trigger OnUpdateMonState lifecycle hook only if some player effect actually listens at it - // (battle-wide union) AND this mon has effects. OnUpdateMonState has a single listener in the - // whole game (Dreamcatcher), so the union bit is unset in almost every battle — skipping the - // abi.encode(4-tuple) + _runEffects shell entirely. Stat-boost delta writes hit this path a lot. - // Union bit FIRST: OnUpdateMonState has a single listener game-wide, so the bit is clear - // in almost every battle — short-circuiting on it skips the per-mon count SLOAD entirely - // (this path fires on every stat-boost delta write, up to 5 per boost application). - if ( - (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.OnUpdateMonState))) != 0 - && (playerIndex == 0 - ? _getMonEffectCount(config.packedP0EffectsCount, monIndex) - : _getMonEffectCount(config.packedP1EffectsCount, monIndex)) > 0 - ) { + // Exact per-mon step lane skips abi.encode + the whole effect shell unless THIS mon has an + // OnUpdateMonState listener. This path is hot for stamina and stat-boost delta writes. + if (_monListensAt(config, playerIndex, monIndex, EffectStep.OnUpdateMonState)) { _runEffectsPipeline( config, playerIndex, @@ -1898,15 +1896,15 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ); } if (!removeAfterRun) { - // INVARIANT: every path that adds a player effect MUST fold its steps bitmap into - // playerEffectStepsUnion here. The step-skip guards in _executeInternal / _handleSwitch - // treat a clear union bit as "no player effect listens at this step" and skip the whole - // _runEffects shell, so a missed update would silently drop live effects. This is the - // SOLE add chokepoint for player effects (stat-boost sources live in their own - // boost-word store, not the effect list). + // INVARIANT: every player-effect add folds its bitmap into BOTH routing caches here: + // the over-approximate battle-wide outer union and this mon's exact lane. A missed + // update would silently skip a live hook. This is the sole player-effect add chokepoint + // (stat boosts use their own store). // Global effects (targetIndex == 2) are gated by globalEffectsLength instead, not the union. if (targetIndex != 2) { config.playerEffectStepsUnion |= stepsBitmap; + uint256 stepLaneShift = _effectStepLaneShift(targetIndex, monIndex); + config.playerEffectStepsByMon |= uint256(stepsBitmap) << stepLaneShift; // Status-lane set: same slot as the union write, so both coalesce into one // SSTORE. INVARIANT: lane nonzero ⇔ exactly one class-bearing entry on the // mon; the only clears are _removeEffectAtSlot + startBattle's reset. @@ -2040,6 +2038,35 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return ((targetIndex << 3) | monIndex) << 2; } + /// @dev Bit offset of a mon's uint16 exact lifecycle-step union (stride 8/side). + function _effectStepLaneShift(uint256 targetIndex, uint256 monIndex) private pure returns (uint256) { + return ((targetIndex << 3) | monIndex) << 4; + } + + function _monEffectSteps(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) + private + view + returns (uint16) + { + return uint16(config.playerEffectStepsByMon >> _effectStepLaneShift(targetIndex, monIndex)); + } + + function _monListensAt(BattleConfig storage config, uint256 targetIndex, uint256 monIndex, EffectStep step) + private + view + returns (bool) + { + return _stepsWordListens(config.playerEffectStepsByMon, targetIndex, monIndex, step); + } + + function _stepsWordListens(uint256 stepsWord, uint256 targetIndex, uint256 monIndex, EffectStep step) + private + pure + returns (bool) + { + return ((stepsWord >> _effectStepLaneShift(targetIndex, monIndex)) & (1 << uint8(step))) != 0; + } + /// @notice Remove a mon's exclusive status (running its onRemove) and clear its lane. /// @param expectedClass 0 = clear any status; nonzero = only a matching class. /// @return cleared Whether a status entry was removed. @@ -2136,9 +2163,32 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } } else { _compactTrailingTombstones(config, targetIndex, monIndex); + _rebuildMonEffectSteps(config, targetIndex, monIndex); } } + /// @dev Exact-clear the changed mon's step lane after a removal. Removal is cold and already + /// scans/compacts this short list; the hot turn and damage paths consume the exact cache. + function _rebuildMonEffectSteps(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) private { + uint96 packedCounts = targetIndex == 0 ? config.packedP0EffectsCount : config.packedP1EffectsCount; + uint256 count = _getMonEffectCount(packedCounts, monIndex); + mapping(uint256 => EffectInstance) storage effects = targetIndex == 0 ? config.p0Effects : config.p1Effects; + uint256 baseSlot = _getEffectSlotIndex(monIndex, 0); + uint16 rebuilt; + for (uint256 i; i < count;) { + EffectInstance storage e = effects[baseSlot + i]; + if (address(e.effect) != TOMBSTONE_ADDRESS) { + rebuilt |= e.stepsBitmap; + } + unchecked { + ++i; + } + } + uint256 shift = _effectStepLaneShift(targetIndex, monIndex); + uint256 mask = uint256(type(uint16).max) << shift; + config.playerEffectStepsByMon = (config.playerEffectStepsByMon & ~mask) | (uint256(rebuilt) << shift); + } + /// @dev Shrink a mon's packed effect count over any trailing tombstones so the count-gated /// passes (and the all-actives-clean early-out) regain their zero fast path. Only the /// tail can shrink — a live entry above a tombstone pins the count. @@ -2414,10 +2464,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // reverts StatRequiresStatBoost for them), so the stored delta after any telescoped op is // exactly newBoosted - base — writing that directly is bit-identical to the dispatcher's // sentinel-aware add, minus its 2 TLOADs + 2 mapping keccaks + enum chain per stat. - bool hasListener = (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.OnUpdateMonState))) != 0 - && (targetIndex == 0 - ? _getMonEffectCount(config.packedP0EffectsCount, monIndex) - : _getMonEffectCount(config.packedP1EffectsCount, monIndex)) > 0; + bool hasListener = _monListensAt(config, targetIndex, monIndex, EffectStep.OnUpdateMonState); for (uint256 i; i < 5; ++i) { // old boosted = base + current stat-boost delta (sentinel reads as 0 / "no boost") @@ -2441,6 +2488,9 @@ contract Engine is IEngine, MappingAllocator, EIP712 { st.specialDefenceDelta = newDelta; } else { st.speedDelta = newDelta; + if (config.battleMode != BATTLE_MODE_SINGLES) { + _markActiveMonSpeedDirty(targetIndex, monIndex); + } } } } @@ -2582,18 +2632,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // PreDamage pipeline: victim-side mon-local effects can mutate the in-flight damage by // calling engine.setPreDamage(). Reuses the standard _runEffects loop; running damage is // threaded through the transient `tempPreDamage` slot so the iteration logic doesn't change. - // Union bits FIRST (PreDamage and AfterDamage each have a single listener game-wide): - // one shared union read covers both gates in the common no-listener case, and the per-mon - // count SLOAD is deferred until a bit actually passes — counts never shrink (tombstones), - // so the lazy >0 check stays fresh-safe. - uint16 stepsUnion = config.playerEffectStepsUnion; + // One exact per-mon lane read covers both gates, preventing another mon's listener from + // routing this victim through a non-matching scan. + uint16 monSteps = _monEffectSteps(config, playerIndex, monIndex); bool ranPreDamage; - if ( - (stepsUnion & uint16(1 << uint8(EffectStep.PreDamage))) != 0 - && (playerIndex == 0 - ? _getMonEffectCount(config.packedP0EffectsCount, monIndex) - : _getMonEffectCount(config.packedP1EffectsCount, monIndex)) > 0 - ) { + if ((monSteps & uint16(1 << uint8(EffectStep.PreDamage))) != 0) { tempPreDamage = damage; _runEffectsPipeline(config, playerIndex, monIndex, EffectStep.PreDamage, abi.encode(source)); damage = tempPreDamage; @@ -2628,18 +2671,13 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Lock in winner immediately if this KO ends the game _checkAndSetWinnerIfGameOver(config, playerIndex); } - // AfterDamage gate. The union is re-read ONLY when the PreDamage pipeline actually ran + // AfterDamage gate. The mon lane is re-read ONLY when the PreDamage pipeline actually ran // (its effects may have added an AfterDamage listener mid-call); otherwise the shared // read above is still authoritative — nothing else can mutate it inside this function. if (ranPreDamage) { - stepsUnion = config.playerEffectStepsUnion; + monSteps = _monEffectSteps(config, playerIndex, monIndex); } - if ( - (stepsUnion & uint16(1 << uint8(EffectStep.AfterDamage))) != 0 - && (playerIndex == 0 - ? _getMonEffectCount(config.packedP0EffectsCount, monIndex) - : _getMonEffectCount(config.packedP1EffectsCount, monIndex)) > 0 - ) { + if ((monSteps & uint16(1 << uint8(EffectStep.AfterDamage))) != 0) { _runEffectsPipeline(config, playerIndex, monIndex, EffectStep.AfterDamage, abi.encode(damage, source)); } } @@ -3118,7 +3156,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 outCount = playerIndex == 0 ? _getMonEffectCount(config.packedP0EffectsCount, currentActiveMonIndex) : _getMonEffectCount(config.packedP1EffectsCount, currentActiveMonIndex); - if (outCount > 0 && (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.OnMonSwitchOut))) != 0) { + if (outCount > 0 && _monListensAt(config, playerIndex, currentActiveMonIndex, EffectStep.OnMonSwitchOut)) { _runEffects(battleKey, tempRNG, playerIndex, playerIndex, EffectStep.OnMonSwitchOut, "", outCount); } @@ -3142,7 +3180,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 inCount = playerIndex == 0 ? _getMonEffectCount(config.packedP0EffectsCount, monToSwitchIndex) : _getMonEffectCount(config.packedP1EffectsCount, monToSwitchIndex); - if (inCount > 0 && (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.OnMonSwitchIn))) != 0) { + if (inCount > 0 && _monListensAt(config, playerIndex, monToSwitchIndex, EffectStep.OnMonSwitchIn)) { _runEffects(battleKey, tempRNG, playerIndex, playerIndex, EffectStep.OnMonSwitchIn, "", inCount); } @@ -3299,7 +3337,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { staminaCost = int32(moveStamina); } _deductStamina(currentMonState, staminaCost); - moveSet.move( self, battleKey, @@ -3556,6 +3593,12 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } else { uint256 monIndex = _unpackActiveMonIndex(battle.activeMonIndex, playerIndex); + // The battle-wide union is only the cheap outer gate. This exact mon lane prevents + // one listener (including a benched mon's) from routing unrelated active lists. + if (!_monListensAt(config, effectIndex, monIndex, round)) { + return playerSwitchForTurnFlag; + } + // Check if mon is KOed (reuse monIndex we already computed) if (condition == EffectRunCondition.SkipIfGameOverOrMonKO) { if (_getMonState(config, playerIndex, monIndex).isKnockedOut) { @@ -3883,6 +3926,23 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } } + /// @dev Mark any active lane currently occupied by this mon. All authored speed changes route + /// through the Engine's stat-boost/state chokepoints before reaching this helper. + function _markActiveMonSpeedDirty(uint256 side, uint256 monIndex) private { + BattleData storage battle = battleData[battleKeyForWrite]; + uint256 slot = side << 1; + uint256 dirty; + if (_slotActive(battle, slot) == monIndex) { + dirty = 1 << slot; + } + if (_slotActive(battle, slot + 1) == monIndex) { + dirty |= 1 << (slot + 1); + } + if (dirty != 0) { + _speedDirtySlots |= dirty; + } + } + /// @dev The packed actives word passed to effects/moves: one 8-bit lane per absolute slot. /// 2-slot only — singles never writes activeMonExt, so its slot-1 lanes would read /// 0x00 ("mon 0") instead of EMPTY_ACTIVE_LANE; singles paths use TargetLib.singlesActives. @@ -4000,8 +4060,14 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 actedMask; uint256 actionOrder; uint256 numActed; + uint256 cachedSpeeds; + // RoundStart mutations are captured by the first full load; only later mutations need + // invalidation. Clear stale bits from a prior batched sub-turn here. + _speedDirtySlots = 0; for (uint256 pick; pick < 4;) { - uint256 slot = _pickNextSlot(config, battle, lockedPriorities, actedMask, rng, pick, turnZero); + uint256 slot; + (slot, cachedSpeeds) = + _pickNextSlot(config, battle, lockedPriorities, actedMask, rng, pick, turnZero, cachedSpeeds); if (slot == NO_SLOT) { break; } @@ -4021,7 +4087,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 side = slot >> 1; uint256 actorMon = _slotActive(battle, slot); bool actorAlive = actorMon != EMPTY_ACTIVE_LANE && !_getMonState(config, side, actorMon).isKnockedOut; - if (actorAlive && (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.AfterMove))) != 0) { + if (actorAlive && _monListensAt(config, side, actorMon, EffectStep.AfterMove)) { uint256 cnt = side == 0 ? _getMonEffectCount(config.packedP0EffectsCount, actorMon) : _getMonEffectCount(config.packedP1EffectsCount, actorMon); @@ -4135,12 +4201,24 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 actedMask, uint256 rng, uint256 pick, - bool turnZero - ) private view returns (uint256 best) { + bool turnZero, + uint256 cachedSpeeds + ) private returns (uint256 best, uint256 newSpeeds) { best = NO_SLOT; + newSpeeds = cachedSpeeds; uint256 bestPrio; uint256 bestSpeed; uint256 bestJitter; + uint256 dirtyMask; + if (pick == 0) { + dirtyMask = 0xF; + } else { + dirtyMask = _speedDirtySlots; + if (dirtyMask != 0) { + _speedDirtySlots = 0; + } + } + uint256 koBitmaps = config.koBitmaps; uint256 h = uint256(keccak256(abi.encode(rng, pick))); for (uint256 s; s < 4;) { if (actedMask & (1 << s) == 0) { @@ -4151,13 +4229,18 @@ contract Engine is IEngine, MappingAllocator, EIP712 { candidate = turnZero; } else { uint256 side = s >> 1; - MonState storage st = _getMonState(config, side, mon); - if (!st.isKnockedOut) { + if ((koBitmaps & (1 << ((side << 3) | mon))) == 0) { candidate = true; - int32 spdDelta = st.speedDelta; - int256 spd = int256(uint256(_getTeamMon(config, side, mon).stats.speed)) - + (spdDelta == CLEARED_MON_STATE_SENTINEL ? int256(0) : int256(spdDelta)); - speed = spd > 0 ? uint256(spd) : 0; + uint256 shift = s << 6; + if (dirtyMask & (1 << s) != 0) { + int32 spdDelta = _getMonState(config, side, mon).speedDelta; + int256 spd = int256(uint256(_getTeamMon(config, side, mon).stats.speed)) + + (spdDelta == CLEARED_MON_STATE_SENTINEL ? int256(0) : int256(spdDelta)); + speed = spd > 0 ? uint256(spd) : 0; + newSpeeds = (newSpeeds & ~(uint256(type(uint64).max) << shift)) | (speed << shift); + } else { + speed = uint64(newSpeeds >> shift); + } } } if (candidate) { @@ -4182,6 +4265,82 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } } + /// @dev Effect-pass picker with the same rank-dependent jitter as `_pickNextSlot`, but it + /// reads each remaining slot's live speed only once while advancing over consecutive + /// non-listeners. The caller invokes it again after every listener runs, preserving D29: + /// effects may change speed, KO a mon, or replace an active before the next hook. + function _pickNextEffectSlot( + BattleConfig storage config, + BattleData storage battle, + uint256 stepsWord, + uint256 doneMask, + uint256 rng, + uint256 pickBase, + uint256 pick, + EffectStep step + ) private view returns (uint256 slot, uint256 newDoneMask, uint256 nextPick) { + uint256 candidateMask; + uint256 listenerMask; + uint256 speeds; + for (uint256 s; s < 4;) { + if (doneMask & (1 << s) == 0) { + uint256 side = s >> 1; + uint256 mon = _slotActive(battle, s); + if (mon != EMPTY_ACTIVE_LANE) { + MonState storage st = _getMonState(config, side, mon); + if (!st.isKnockedOut) { + candidateMask |= 1 << s; + int32 spdDelta = st.speedDelta; + int256 spd = int256(uint256(_getTeamMon(config, side, mon).stats.speed)) + + (spdDelta == CLEARED_MON_STATE_SENTINEL ? int256(0) : int256(spdDelta)); + if (spd > 0) { + speeds |= uint256(spd) << (s << 6); + } + if (_stepsWordListens(stepsWord, side, mon, step)) { + listenerMask |= 1 << s; + } + } + } + } + unchecked { + ++s; + } + } + + newDoneMask = doneMask; + nextPick = pick; + while (nextPick < 4 && candidateMask != 0) { + uint256 best = NO_SLOT; + uint256 bestSpeed; + uint256 bestJitter; + uint256 h = uint256(keccak256(abi.encode(rng, pickBase + nextPick))); + for (uint256 s; s < 4;) { + if (candidateMask & (1 << s) != 0) { + uint256 speed = uint64(speeds >> (s << 6)); + uint256 jitter = uint64(h >> (s << 6)); + if (best == NO_SLOT || speed > bestSpeed || (speed == bestSpeed && jitter > bestJitter)) { + best = s; + bestSpeed = speed; + bestJitter = jitter; + } + } + unchecked { + ++s; + } + } + uint256 bit = 1 << best; + candidateMask &= ~bit; + newDoneMask |= bit; + unchecked { + ++nextPick; + } + if (listenerMask & bit != 0) { + return (best, newDoneMask, nextPick); + } + } + return (NO_SLOT, newDoneMask, nextPick); + } + /// @dev Resolve one slot's action: coercion (turn-0 / KO'd -> switch), switch legality /// (bounds, KO'd target, ally-slot collision), NO_OP, or move execution with the /// engine-level fizzle rule (D2: stamina spent, dead/empty chosen target skips the move). @@ -4405,7 +4564,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 outCount = side == 0 ? _getMonEffectCount(config.packedP0EffectsCount, currentActive) : _getMonEffectCount(config.packedP1EffectsCount, currentActive); - if (outCount > 0 && (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.OnMonSwitchOut))) != 0) { + if (outCount > 0 && _monListensAt(config, side, currentActive, EffectStep.OnMonSwitchOut)) { _runEffectsForMon( config, battle, rng, side, side, currentActive, EffectStep.OnMonSwitchOut, "", outCount ); @@ -4421,7 +4580,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 inCount = side == 0 ? _getMonEffectCount(config.packedP0EffectsCount, monToSwitchIndex) : _getMonEffectCount(config.packedP1EffectsCount, monToSwitchIndex); - if (inCount > 0 && (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.OnMonSwitchIn))) != 0) { + if (inCount > 0 && _monListensAt(config, side, monToSwitchIndex, EffectStep.OnMonSwitchIn)) { _runEffectsForMon(config, battle, rng, side, side, monToSwitchIndex, EffectStep.OnMonSwitchIn, "", inCount); } uint256 inGlobals = config.globalEffectsLength; @@ -4446,31 +4605,28 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 rng, EffectStep step ) private { - // The steps union only says SOME mon (possibly benched or replaced) listens at this - // step; when none of the four current actives carries any effect the ordering pass has - // nothing to run. Empty lanes (0xFF) shift out to a zero count. - { - uint96 c0 = config.packedP0EffectsCount; - uint96 c1 = config.packedP1EffectsCount; - if ( - _getMonEffectCount(c0, _slotActive(battle, 0)) == 0 - && _getMonEffectCount(c0, _slotActive(battle, 1)) == 0 - && _getMonEffectCount(c1, _slotActive(battle, 2)) == 0 - && _getMonEffectCount(c1, _slotActive(battle, 3)) == 0 - ) { - return; - } + // One exact word answers both "has effects" and "listens at this step" for all actives. + // Empty lanes (0xFF) naturally shift beyond the word and read false. + uint256 stepsWord = config.playerEffectStepsByMon; + if ( + !_stepsWordListens(stepsWord, 0, _slotActive(battle, 0), step) + && !_stepsWordListens(stepsWord, 0, _slotActive(battle, 1), step) + && !_stepsWordListens(stepsWord, 1, _slotActive(battle, 2), step) + && !_stepsWordListens(stepsWord, 1, _slotActive(battle, 3), step) + ) { + return; } uint256 doneMask; // Distinct jitter-seed ranges per pass (and from the action scheduler's 0-3) so the // RoundStart and RoundEnd orderings roll independently. uint256 pickBase = step == EffectStep.RoundStart ? 16 : 24; - for (uint256 pick; pick < 4;) { - uint256 slot = _pickNextSlot(config, battle, 0, doneMask, rng, pickBase + pick, false); + uint256 pick; + while (pick < 4) { + uint256 slot; + (slot, doneMask, pick) = _pickNextEffectSlot(config, battle, stepsWord, doneMask, rng, pickBase, pick, step); if (slot == NO_SLOT) { break; } - doneMask |= (1 << slot); if (battle.winnerIndex != 2) { return; } @@ -4479,12 +4635,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 cnt = side == 0 ? _getMonEffectCount(config.packedP0EffectsCount, mon) : _getMonEffectCount(config.packedP1EffectsCount, mon); - if (cnt > 0) { - _runEffectsForMon(config, battle, rng, side, side, mon, step, "", cnt); - } - unchecked { - ++pick; - } + _runEffectsForMon(config, battle, rng, side, side, mon, step, "", cnt); } } @@ -4650,14 +4801,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return; } monState.staminaDelta += 1; - // Union bit first (single OnUpdateMonState listener game-wide) — skips the count SLOAD - // on the up-to-two regen ticks every round end. - if ( - (config.playerEffectStepsUnion & uint16(1 << uint8(EffectStep.OnUpdateMonState))) != 0 - && (playerIndex == 0 - ? _getMonEffectCount(config.packedP0EffectsCount, monIndex) - : _getMonEffectCount(config.packedP1EffectsCount, monIndex)) > 0 - ) { + // Exact lane skips unrelated mons without loading their effect count. + if (_monListensAt(config, playerIndex, monIndex, EffectStep.OnUpdateMonState)) { _runEffectsPipeline( config, playerIndex, @@ -4923,6 +5068,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { p0TeamIndex: data.p0TeamIndex, p1TeamIndex: data.p1TeamIndex, monStatusLanes: config.monStatusLanes, + playerEffectStepsByMon: config.playerEffectStepsByMon, p0Move: config.p0Move, p1Move: config.p1Move, globalEffects: globalEffects, diff --git a/src/Structs.sol b/src/Structs.sol index ffad6f35..2fa7eabe 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -223,6 +223,10 @@ struct BattleConfig { // Never read when the mon's source count is 0 (initialize-on-first-add), so recycled-key // staleness is unobservable; count > 0 implies this battle wrote it. mapping(uint256 => uint256) statBoostAcc; + // Exact OR of live EffectInstance.stepsBitmap values for each player mon. + // Lane index = side*8 + monIndex; one uint16 lane per mon fills one word exactly. + // Appended after mappings so existing BattleConfig mapping roots remain stable. + uint256 playerEffectStepsByMon; } struct EffectInstance { @@ -252,6 +256,7 @@ struct BattleConfigView { uint16 p0TeamIndex; uint16 p1TeamIndex; uint64 monStatusLanes; // Per-mon exclusive-status class nibbles (see BattleConfig) + uint256 playerEffectStepsByMon; // Exact uint16 lifecycle-step union per mon MoveDecision p0Move; MoveDecision p1Move; EffectInstance[] globalEffects; diff --git a/test/DoublesGasBenchmark.t.sol b/test/DoublesGasBenchmark.t.sol index 0424036b..310dfb0d 100644 --- a/test/DoublesGasBenchmark.t.sol +++ b/test/DoublesGasBenchmark.t.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.0; import "../lib/forge-std/src/Test.sol"; +import {Vm} from "../lib/forge-std/src/Vm.sol"; import "../src/Constants.sol"; import "../src/Enums.sol"; import "../src/Structs.sol"; import {Engine} from "../src/Engine.sol"; +import {IEngine} from "../src/IEngine.sol"; import {IRuleset} from "../src/IRuleset.sol"; import {IEffect} from "../src/effects/IEffect.sol"; import {BurnStatus} from "../src/effects/status/BurnStatus.sol"; @@ -51,6 +53,51 @@ contract DoublesGasBenchmark is BatchHelper, GasMeasure { Tally private _acc; uint256 private _accGas; + function _snapshotEffectCensus(string memory name, Vm.AccountAccess[] memory accesses, bytes32 storageKey) private { + EffectStorageTally memory effectStorage = _effectStorageTally(accesses, address(engine), storageKey); + uint256 roundStartCalls = _countCalls(accesses, address(engine), address(0), IEffect.onRoundStart.selector); + uint256 roundEndCalls = _countCalls(accesses, address(engine), address(0), IEffect.onRoundEnd.selector); + uint256 afterMoveCalls = _countCalls(accesses, address(engine), address(0), IEffect.onAfterMove.selector); + uint256 preDamageCalls = _countCalls(accesses, address(engine), address(0), IEffect.onPreDamage.selector); + uint256 afterDamageCalls = _countCalls(accesses, address(engine), address(0), IEffect.onAfterDamage.selector); + uint256 updateStateCalls = _countCalls(accesses, address(engine), address(0), IEffect.onUpdateMonState.selector); + uint256 moveDecisionCallbacks = + _countCalls(accesses, address(0), address(engine), IEngine.getMoveDecisionForSlot.selector); + uint256 lifecycleCalls = + roundStartCalls + roundEndCalls + afterMoveCalls + preDamageCalls + afterDamageCalls + updateStateCalls; + uint256 nonHookHeaderReadUpperBound = + effectStorage.headerReads > lifecycleCalls ? effectStorage.headerReads - lifecycleCalls : 0; + + vm.snapshotValue(string.concat(name, "_effectHeaderReads"), effectStorage.headerReads); + vm.snapshotValue(string.concat(name, "_effectDataReads"), effectStorage.dataReads); + vm.snapshotValue(string.concat(name, "_effectHeaderWrites"), effectStorage.headerWrites); + vm.snapshotValue(string.concat(name, "_effectDataWrites"), effectStorage.dataWrites); + vm.snapshotValue(string.concat(name, "_roundStartCalls"), roundStartCalls); + vm.snapshotValue(string.concat(name, "_roundEndCalls"), roundEndCalls); + vm.snapshotValue(string.concat(name, "_afterMoveCalls"), afterMoveCalls); + vm.snapshotValue(string.concat(name, "_preDamageCalls"), preDamageCalls); + vm.snapshotValue(string.concat(name, "_afterDamageCalls"), afterDamageCalls); + vm.snapshotValue(string.concat(name, "_updateStateCalls"), updateStateCalls); + vm.snapshotValue(string.concat(name, "_moveDecisionCallbacks"), moveDecisionCallbacks); + vm.snapshotValue(string.concat(name, "_lifecycleCalls"), lifecycleCalls); + vm.snapshotValue(string.concat(name, "_nonHookHeaderReadUpperBound"), nonHookHeaderReadUpperBound); + + console.log( + string.concat(name, " effect header/data reads:"), effectStorage.headerReads, effectStorage.dataReads + ); + console.log( + string.concat(name, " RoundStart/RoundEnd/AfterMove:"), roundStartCalls, roundEndCalls, afterMoveCalls + ); + console.log( + string.concat(name, " PreDamage/AfterDamage/UpdateState:"), + preDamageCalls, + afterDamageCalls, + updateStateCalls + ); + console.log(string.concat(name, " moveDecision callbacks:"), moveDecisionCallbacks); + console.log(string.concat(name, " non-hook header read upper bound:"), nonHookHeaderReadUpperBound); + } + function setUp() public { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -173,6 +220,7 @@ contract DoublesGasBenchmark is BatchHelper, GasMeasure { function test_doublesBatchedReplayGas() public { engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); bytes32 battleKey = _startDoubles(address(this)); + bytes32 storageKey = engine.getStorageKey(battleKey); uint256[] memory entries = new uint256[](24); for (uint64 t = 0; t < 12; t++) { @@ -185,9 +233,11 @@ contract DoublesGasBenchmark is BatchHelper, GasMeasure { uint256 g0 = gasleft(); engine.executeBatchedSlotTurns(battleKey, entries); _accGas += g0 - gasleft(); - _acc = _addTally(_acc, _tally(vm.stopAndReturnStateDiff())); + Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); + _acc = _addTally(_acc, _tally(accesses)); engine.resetCallContext(); _endMeasure("Doubles_Batch12"); + _snapshotEffectCensus("Doubles_Batch12", accesses, storageKey); assertEq(engine.getTurnIdForBattleState(battleKey), 12, "all twelve turns executed"); } @@ -271,6 +321,7 @@ contract DoublesGasBenchmark is BatchHelper, GasMeasure { battle.ruleset = IRuleset(INLINE_STAMINA_REGEN_RULESET); (bytes32 battleKey,) = engine.computeBattleKey(p0, p1); engine.startBattleWithMode(battle, BATTLE_MODE_DOUBLES); + bytes32 storageKey = engine.getStorageKey(battleKey); vm.warp(vm.getBlockTimestamp() + 1); // T0 send-ins; T1 burn B0 + siphon B1 while B rests; T2-T9 siphon every turn while @@ -297,13 +348,80 @@ contract DoublesGasBenchmark is BatchHelper, GasMeasure { uint256 g0 = gasleft(); engine.executeBatchedSlotTurns(battleKey, entries); _accGas += g0 - gasleft(); - _acc = _addTally(_acc, _tally(vm.stopAndReturnStateDiff())); + Vm.AccountAccess[] memory accesses = vm.stopAndReturnStateDiff(); + _acc = _addTally(_acc, _tally(accesses)); engine.resetCallContext(); _endMeasure("Doubles_KitBatch10"); + _snapshotEffectCensus("Doubles_KitBatch10", accesses, storageKey); assertEq(engine.getTurnIdForBattleState(battleKey), 10, "all ten turns executed"); } + function _tailEntries(uint256 n, uint256 shape) private pure returns (uint256[] memory entries) { + entries = new uint256[](n * 2); + for (uint256 t; t < n; t++) { + uint104 s0 = uint104(10_000 + 2 * t); + uint104 s1 = uint104(10_001 + 2 * t); + if (shape == 0) { + entries[t * 2] = sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, s0); + entries[t * 2 + 1] = sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, s1); + } else if (shape == 1) { + entries[t * 2] = sideWord(0, targetBits(2), NO_OP_MOVE_INDEX, 0, s0); + entries[t * 2 + 1] = sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, s1); + } else { + entries[t * 2] = sideWord(0, targetBits(2), 0, targetBits(3), s0); + entries[t * 2 + 1] = sideWord(0, targetBits(0), 0, targetBits(1), s1); + } + } + } + + function _measureDoublesTail(uint256 shape, uint256 n) private returns (uint256 rawGas) { + bytes32 battleKey = _startDoubles(address(this)); + (uint256 side0, uint256 side1) = _scriptSides(0); + uint256[] memory sendIns = new uint256[](2); + sendIns[0] = side0; + sendIns[1] = side1; + engine.executeBatchedSlotTurns(battleKey, sendIns); + engine.resetCallContext(); + + uint256[] memory entries = _tailEntries(n, shape); + vm.cool(address(engine)); + uint256 g0 = gasleft(); + engine.executeBatchedSlotTurns(battleKey, entries); + rawGas = g0 - gasleft(); + engine.resetCallContext(); + vm.prank(p0); + engine.forfeit(battleKey); + } + + /// @notice Differential 2-slot pipeline matrix on the same recycled storage key and teams. + /// The tails vary only action shape: four rests, one attack, or four attacks. + function test_doublesTurnFloorMatrix() public { + engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); + + // Seed and free the one-key pool so every measured scenario is reused-key steady state. + bytes32 warmup = _startDoubles(address(this)); + vm.prank(p0); + engine.forfeit(warmup); + + uint256 n = 8; + uint256 allRest = _measureDoublesTail(0, n); + uint256 oneAttack = _measureDoublesTail(1, n); + uint256 allAttack = _measureDoublesTail(2, n); + + vm.snapshotValue("DoublesFloor_AllRest8_rawGas", allRest); + vm.snapshotValue("DoublesFloor_OneAttack8_rawGas", oneAttack); + vm.snapshotValue("DoublesFloor_AllAttack8_rawGas", allAttack); + vm.snapshotValue("DoublesFloor_AllRest_perTurn", allRest / n); + vm.snapshotValue("DoublesFloor_OneAttack_perTurn", oneAttack / n); + vm.snapshotValue("DoublesFloor_AllAttack_perTurn", allAttack / n); + + console.log("Doubles floor matrix, 8-turn tails:"); + console.log(" all rest total/per turn:", allRest, allRest / n); + console.log(" one attack total/per turn:", oneAttack, oneAttack / n); + console.log(" all attack total/per turn:", allAttack, allAttack / n); + } + // ----- GasMeasure plumbing (mirrors ProdPvPGasBenchmark) ----- function _beginMeasure() internal { diff --git a/test/MainnetReplayGasTest.t.sol b/test/MainnetReplayGasTest.t.sol index 1e005af6..c0264fe2 100644 --- a/test/MainnetReplayGasTest.t.sol +++ b/test/MainnetReplayGasTest.t.sol @@ -5,11 +5,13 @@ import "../src/Constants.sol"; import "../src/Enums.sol"; import "../src/Structs.sol"; import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; import {console} from "forge-std/console.sol"; import {SetupMons} from "../script/SetupMons.s.sol"; import {Engine} from "../src/Engine.sol"; import {IEngine} from "../src/IEngine.sol"; +import {IEffect} from "../src/effects/IEffect.sol"; import {GachaTeamRegistry} from "../src/game-layer/GachaTeamRegistry.sol"; import {IGachaRNG} from "../src/rng/IGachaRNG.sol"; @@ -29,6 +31,8 @@ import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; import {IRandomnessOracle} from "../src/rng/IRandomnessOracle.sol"; +import {GasMeasure} from "./abstract/GasMeasure.sol"; +import {sideWord} from "./abstract/SlotWire.sol"; import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// @notice Bit-faithful replay of the first real mainnet game we captured — MegaETH tx @@ -40,7 +44,7 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// p1Salt) with NO battleKey dependency, so replaying the same salts reproduces the exact /// battle here. Measured through engine.executeBatchedTurns (the same path the tx took) on /// a VIRGIN storageKey and again on a REUSED (steady-state) one. -contract MainnetReplayGasTest is Test, SetupMons { +contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { uint256 constant P0_PK = 0xA11CE; uint256 constant P1_PK = 0xB0B; uint256 constant TX_BASE = 21000; @@ -53,6 +57,20 @@ contract MainnetReplayGasTest is Test, SetupMons { GachaTeamRegistry gachaReg; TestTeamRegistry registry; + struct ReplayCensus { + EffectStorageTally effectStorage; + uint256 roundStartCalls; + uint256 roundEndCalls; + uint256 afterMoveCalls; + uint256 preDamageCalls; + uint256 afterDamageCalls; + uint256 updateStateCalls; + uint256 moveDecisionCallbacks; + uint256 statusClassCallbacks; + uint256 lifecycleCalls; + uint256 nonHookHeaderReadUpperBound; + } + // Real rosters from the mainnet config (getBattle). p1 is the CPU side. uint256[4] P0_IDS = [uint256(1), 5, 7, 6]; // Inutia, Sofabbi, Embursa, Pengym uint256[4] P1_IDS = [uint256(5), 7, 4, 7]; // Sofabbi, Embursa, Gorillax, Embursa @@ -235,12 +253,200 @@ contract MainnetReplayGasTest is Test, SetupMons { engine.resetCallContext(); } + /// @dev Recorded analog of `_replayOneTx`. State-diff collection perturbs Foundry's raw gas + /// scalar, so this intentionally returns census data only and cannot become a KPI. + function _replayOneTxRecorded(bytes32 battleKey) internal returns (Vm.AccountAccess[] memory accesses) { + uint256[] memory entries = _entries(); + bytes memory cd = abi.encodeCall(engine.executeBatchedTurns, (battleKey, entries)); + vm.cool(address(engine)); + vm.startStateDiffRecording(); + vm.prank(address(mgr)); + (bool ok,) = address(engine).call(cd); + require(ok, "recorded replay reverted"); + accesses = vm.stopAndReturnStateDiff(); + engine.resetCallContext(); + } + function _calldataCost(bytes memory cd) internal pure returns (uint256 cost) { for (uint256 i; i < cd.length; i++) { cost += cd[i] == bytes1(0) ? 4 : 16; } } + function _recordReplayCensus( + string memory name, + Vm.AccountAccess[] memory accesses, + bytes32 storageKey, + uint256 reusedGas + ) internal returns (ReplayCensus memory c) { + c.effectStorage = _effectStorageTally(accesses, address(engine), storageKey); + c.roundStartCalls = _countCalls(accesses, address(engine), address(0), IEffect.onRoundStart.selector); + c.roundEndCalls = _countCalls(accesses, address(engine), address(0), IEffect.onRoundEnd.selector); + c.afterMoveCalls = _countCalls(accesses, address(engine), address(0), IEffect.onAfterMove.selector); + c.preDamageCalls = _countCalls(accesses, address(engine), address(0), IEffect.onPreDamage.selector); + c.afterDamageCalls = _countCalls(accesses, address(engine), address(0), IEffect.onAfterDamage.selector); + c.updateStateCalls = _countCalls(accesses, address(engine), address(0), IEffect.onUpdateMonState.selector); + c.moveDecisionCallbacks = + _countCalls(accesses, address(0), address(engine), IEngine.getMoveDecisionForSlot.selector); + c.statusClassCallbacks = _countCalls(accesses, address(0), address(engine), IEngine.getMonStatusClass.selector); + c.lifecycleCalls = c.roundStartCalls + c.roundEndCalls + c.afterMoveCalls + c.preDamageCalls + + c.afterDamageCalls + c.updateStateCalls; + c.nonHookHeaderReadUpperBound = + c.effectStorage.headerReads > c.lifecycleCalls ? c.effectStorage.headerReads - c.lifecycleCalls : 0; + + vm.snapshotValue(string.concat(name, "_execGas"), reusedGas); + vm.snapshotValue(string.concat(name, "_effectHeaderReads"), c.effectStorage.headerReads); + vm.snapshotValue(string.concat(name, "_effectDataReads"), c.effectStorage.dataReads); + vm.snapshotValue(string.concat(name, "_effectHeaderWrites"), c.effectStorage.headerWrites); + vm.snapshotValue(string.concat(name, "_effectDataWrites"), c.effectStorage.dataWrites); + vm.snapshotValue(string.concat(name, "_roundStartCalls"), c.roundStartCalls); + vm.snapshotValue(string.concat(name, "_roundEndCalls"), c.roundEndCalls); + vm.snapshotValue(string.concat(name, "_afterMoveCalls"), c.afterMoveCalls); + vm.snapshotValue(string.concat(name, "_preDamageCalls"), c.preDamageCalls); + vm.snapshotValue(string.concat(name, "_afterDamageCalls"), c.afterDamageCalls); + vm.snapshotValue(string.concat(name, "_updateStateCalls"), c.updateStateCalls); + vm.snapshotValue(string.concat(name, "_moveDecisionCallbacks"), c.moveDecisionCallbacks); + vm.snapshotValue(string.concat(name, "_statusClassCallbacks"), c.statusClassCallbacks); + vm.snapshotValue(string.concat(name, "_lifecycleCalls"), c.lifecycleCalls); + vm.snapshotValue(string.concat(name, "_nonHookHeaderReadUpperBound"), c.nonHookHeaderReadUpperBound); + + assertGt(c.effectStorage.headerReads, 0, "effect header census must be live"); + assertGt(c.roundEndCalls, 0, "round-end census must be live"); + assertGt(c.afterMoveCalls, 0, "after-move census must be live"); + } + + function _setFirstDoublesReplayTeams() private { + uint256[] memory p0Ids = new uint256[](4); + p0Ids[0] = 4; // Gorillax + p0Ids[1] = 2; // Malalien + p0Ids[2] = 5; // Sofabbi + p0Ids[3] = 11; // Ekineki + uint256[] memory p1Ids = new uint256[](4); + p1Ids[0] = 0; // Ghouliath + p1Ids[1] = 3; // Iblivion + p1Ids[2] = 10; // Xmon + p1Ids[3] = 5; // Sofabbi + uint8[] memory p0Facets = new uint8[](4); + uint8[] memory p1Facets = new uint8[](4); + for (uint256 i; i < 4; i++) { + p0Facets[i] = 6; + } + gachaReg.setTeamForUser(p0, 0, p0Ids, p0Facets); + gachaReg.setTeamForUser(p1, 0, p1Ids, p1Facets); + } + + function _startDoublesBattle() private returns (bytes32 key) { + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(maker); + vm.prank(p0); + engine.updateMatchmakers(makersToAdd, new address[](0)); + vm.prank(p1); + engine.updateMatchmakers(makersToAdd, new address[](0)); + bytes32 pairHash; + (key, pairHash) = engine.computeBattleKey(p0, p1); + BattleOffer memory offer = BattleOffer({ + battle: Battle({ + p0: p0, + p0TeamIndex: 0, + p1: p1, + p1TeamIndex: 0, + p2: address(0), + p2TeamIndex: 0, + p3: address(0), + p3TeamIndex: 0, + teamRegistry: gachaReg, + rngOracle: IRandomnessOracle(address(0)), + ruleset: IRuleset(INLINE_STAMINA_REGEN_RULESET), + moveManager: address(mgr), + matchmaker: maker, + engineHooks: new IEngineHook[](0) + }), + pairHashNonce: engine.pairHashNonces(pairHash), + battleMode: BATTLE_MODE_DOUBLES + }); + bytes32 digest = maker.hashTypedData(BattleOfferLib.hashBattleOfferForSigning(offer, 0)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(P0_PK, digest); + bytes[4] memory seatSigs; + seatSigs[0] = abi.encodePacked(r, s, v); + vm.prank(p1); + maker.startGame(offer, 0, seatSigs); + } + + function _firstDoublesReplayEntries() private pure returns (uint256[] memory e) { + e = new uint256[](20); + e[0] = sideWord(SWITCH_MOVE_INDEX, 1, SWITCH_MOVE_INDEX, 3, 629152483201830402240141); + e[1] = sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, 0); + e[2] = sideWord(1, 32768, 0, 32768, 288092756437916069539032); + e[3] = sideWord(1, 4096, 1, 0, 0); + e[4] = sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, 0); + e[5] = sideWord(NO_OP_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 2, 0); + e[6] = sideWord(1, 32768, 0, 32768, 874957430843907341896228); + e[7] = sideWord(1, 4096, 1, 4096, 0); + e[8] = sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, 0); + e[9] = sideWord(NO_OP_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 3, 0); + e[10] = sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, 1115499015655134625153807); + e[11] = sideWord(3, 4096, 2, 8192, 0); + e[12] = sideWord(SWITCH_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, 74943448586572800650145); + e[13] = sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, 0); + e[14] = sideWord(2, 32768, 0, 16384, 315825775983314630690957); + e[15] = sideWord(3, 8192, NO_OP_MOVE_INDEX, 0, 0); + e[16] = sideWord(2, 32768, NO_OP_MOVE_INDEX, 0, 363940942254872708793045); + e[17] = sideWord(3, 4096, 3, 0, 0); + e[18] = sideWord(2, 32768, 0, 32768, 191897147809779057662496); + e[19] = sideWord(3, 4096, 2, 4096, 0); + } + + function _replayDoublesOneTx(bytes32 battleKey, bool measure) private returns (uint256 execGas) { + uint256[] memory entries = _firstDoublesReplayEntries(); + bytes memory cd = abi.encodeCall(engine.executeBatchedSlotTurns, (battleKey, entries)); + if (measure) { + vm.cool(address(engine)); + } + vm.prank(address(mgr)); + uint256 g0 = gasleft(); + (bool ok,) = address(engine).call(cd); + uint256 raw = g0 - gasleft(); + require(ok, "doubles replay reverted"); + if (measure) { + execGas = raw + _calldataCost(cd) + TX_BASE; + } + engine.resetCallContext(); + } + + function _replayDoublesRecorded(bytes32 battleKey) private returns (Vm.AccountAccess[] memory accesses) { + uint256[] memory entries = _firstDoublesReplayEntries(); + bytes memory cd = abi.encodeCall(engine.executeBatchedSlotTurns, (battleKey, entries)); + vm.cool(address(engine)); + vm.startStateDiffRecording(); + vm.prank(address(mgr)); + (bool ok,) = address(engine).call(cd); + require(ok, "recorded doubles replay reverted"); + accesses = vm.stopAndReturnStateDiff(); + engine.resetCallContext(); + } + + function _finishIncompleteReplay(bytes32 battleKey) private { + if (engine.getWinner(battleKey) == address(0)) { + vm.prank(p0); + engine.forfeit(battleKey); + } + } + + function _assertExactEffectStepLanes(bytes32 battleKey) private view { + (BattleConfigView memory cfg,) = engine.getBattle(battleKey); + for (uint256 side; side < 2; side++) { + EffectInstance[][] memory effects = side == 0 ? cfg.p0Effects : cfg.p1Effects; + for (uint256 mon; mon < effects.length; mon++) { + uint16 expected; + for (uint256 i; i < effects[mon].length; i++) { + expected |= effects[mon][i].stepsBitmap; + } + uint256 shift = ((side << 3) | mon) << 4; + assertEq(uint16(cfg.playerEffectStepsByMon >> shift), expected, "effect-step lane mismatch"); + } + } + } + // --------------------------------------------------------------------------------------------- // R2.0 — turn-pipeline floor: same rosters/config, but the turns carry ZERO content // (turn 0 = send-ins, then NO_OPs: no moves, no effects, no damage). Prices the fixed @@ -260,8 +466,9 @@ contract MainnetReplayGasTest is Test, SetupMons { function _noopEntries(uint256 n, uint256 saltBase) internal pure returns (uint256[] memory e) { e = new uint256[](n); for (uint256 i; i < n; i++) { - e[i] = - _packEntry(NO_OP_MOVE_INDEX, 0, uint104(saltBase + 2 * i), NO_OP_MOVE_INDEX, 0, uint104(saltBase + 2 * i + 1)); + e[i] = _packEntry( + NO_OP_MOVE_INDEX, 0, uint104(saltBase + 2 * i), NO_OP_MOVE_INDEX, 0, uint104(saltBase + 2 * i + 1) + ); } } @@ -313,6 +520,13 @@ contract MainnetReplayGasTest is Test, SetupMons { } uint256 t27 = _execBatch(key3, whole); + vm.snapshotValue("SinglesFloor_SendIn_rawGas", switchTurn); + vm.snapshotValue("SinglesFloor_OneNoop_rawGas", t1); + vm.snapshotValue("SinglesFloor_25Noops_rawGas", t25); + vm.snapshotValue("SinglesFloor_Noop_perTurn", perTurn); + vm.snapshotValue("SinglesFloor_BatchFixed", t1 - perTurn); + vm.snapshotValue("SinglesFloor_Whole27_rawGas", t27); + console.log(""); console.log("=== R2.0 no-op floor (reused key, raw exec gas, warm) ==="); console.log(" send-in turn (batch of 1) :", switchTurn); @@ -341,10 +555,100 @@ contract MainnetReplayGasTest is Test, SetupMons { uint256 reusedGas = _replayOneTx(key2, true); require(engine.getWinner(key2) != address(0), "reused replay must also reach game over"); + // State-diff recording perturbs Foundry's raw gas scalar, so collect the census on a + // third, storage-congruent replay and never use its gas result as a KPI. + bytes32 recycledStorageKey = engine.getStorageKey(key1); + bytes32 key3 = _startBattle(); + require(engine.getStorageKey(key3) == recycledStorageKey, "storageKey must be reused for census"); + vm.warp(vm.getBlockTimestamp() + 1); + Vm.AccountAccess[] memory accesses = _replayOneTxRecorded(key3); + require(engine.getWinner(key3) != address(0), "census replay must also reach game over"); + ReplayCensus memory census = + _recordReplayCensus("MainnetReplay_Reused", accesses, recycledStorageKey, reusedGas); + console.log(""); console.log("=== MAINNET replay (27 batched turns), prod config (inline regen) ==="); console.log(" VIRGIN storageKey (cold, first battle) execGas :", virginGas); console.log(" REUSED storageKey (steady state) execGas :", reusedGas); console.log(" virgin - reused (recycling saving) :", virginGas - reusedGas); + console.log( + " player effect header/data reads :", + census.effectStorage.headerReads, + census.effectStorage.dataReads + ); + console.log( + " hooks RoundStart/RoundEnd/AfterMove :", + census.roundStartCalls, + census.roundEndCalls, + census.afterMoveCalls + ); + console.log( + " hooks PreDamage/AfterDamage/UpdateState :", + census.preDamageCalls, + census.afterDamageCalls, + census.updateStateCalls + ); + console.log( + " callbacks moveDecision/statusClass :", + census.moveDecisionCallbacks, + census.statusClassCallbacks + ); + console.log(" non-hook effect header read upper bound :", census.nonHookHeaderReadUpperBound); + } + + /// @dev Real submitted move/target/salt sequence from `replays.txt`. The historical deployed + /// catalog state is not yet available locally, so this is an action-mix gas replay rather + /// than a winner-faithful replay. It intentionally receives the same reused-key+census + /// treatment as singles; forfeit only recycles the key after the measured bracket. + function test_mainnetDoublesReplay1_batchedExecute() public { + _setFirstDoublesReplayTeams(); + vm.warp(vm.getBlockTimestamp() + 1); + + bytes32 key1 = _startDoublesBattle(); + bytes32 recycledStorageKey = engine.getStorageKey(key1); + vm.warp(vm.getBlockTimestamp() + 1); + _replayDoublesOneTx(key1, false); + assertEq(engine.getTurnIdForBattleState(key1), 10, "all real doubles turns execute"); + _finishIncompleteReplay(key1); + + bytes32 key2 = _startDoublesBattle(); + require(engine.getStorageKey(key2) == recycledStorageKey, "doubles storageKey must be reused"); + vm.warp(vm.getBlockTimestamp() + 1); + uint256 reusedGas = _replayDoublesOneTx(key2, true); + assertEq(engine.getTurnIdForBattleState(key2), 10, "all reused doubles turns execute"); + _finishIncompleteReplay(key2); + + bytes32 key3 = _startDoublesBattle(); + require(engine.getStorageKey(key3) == recycledStorageKey, "doubles storageKey must be reused for census"); + (BattleConfigView memory freshCfg,) = engine.getBattle(key3); + assertEq(freshCfg.playerEffectStepsByMon, 0, "recycled step lanes must reset"); + vm.warp(vm.getBlockTimestamp() + 1); + Vm.AccountAccess[] memory accesses = _replayDoublesRecorded(key3); + assertEq(engine.getTurnIdForBattleState(key3), 10, "all census doubles turns execute"); + _assertExactEffectStepLanes(key3); + ReplayCensus memory census = + _recordReplayCensus("MainnetDoublesReplay1_Reused", accesses, recycledStorageKey, reusedGas); + _finishIncompleteReplay(key3); + + console.log(""); + console.log("=== MAINNET doubles replay 1 (10 submitted turns, reused key) ==="); + console.log(" execGas :", reusedGas); + console.log( + " player effect header/data reads :", + census.effectStorage.headerReads, + census.effectStorage.dataReads + ); + console.log( + " hooks RoundStart/RoundEnd/AfterMove :", + census.roundStartCalls, + census.roundEndCalls, + census.afterMoveCalls + ); + console.log( + " callbacks moveDecision/statusClass :", + census.moveDecisionCallbacks, + census.statusClassCallbacks + ); + console.log(" non-hook effect header read upper bound :", census.nonHookHeaderReadUpperBound); } } diff --git a/test/abstract/GasMeasure.sol b/test/abstract/GasMeasure.sol index 95fc4ead..376a8996 100644 --- a/test/abstract/GasMeasure.sol +++ b/test/abstract/GasMeasure.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.0; import {CommonBase} from "../../lib/forge-std/src/Base.sol"; -import {Vm} from "../../lib/forge-std/src/Vm.sol"; +import {Vm, VmSafe} from "../../lib/forge-std/src/Vm.sol"; + +import {EFFECT_SLOTS_PER_MON} from "../../src/Constants.sol"; /// @notice Shared production-faithful gas measurement: a deterministic storage/account access /// tally per measured window (one prod-tx-equivalent unit), plus a synthetic @@ -17,6 +19,14 @@ import {Vm} from "../../lib/forge-std/src/Vm.sol"; /// pure compute (non-semantic), polluting cross-version scalar diffs. The tally is /// immune to all three: opcode counts are warmth- and layout-independent. abstract contract GasMeasure is CommonBase { + // Engine storage roots and BattleConfig-relative mapping offsets are compiler-layout + // facts, not production constants. Keep them test-only: the census tests assert the + // expected reads are non-zero, so a layout change fails loudly instead of silently + // emitting a misleading zero. See `forge inspect Engine storage-layout`. + uint256 private constant ENGINE_BATTLE_CONFIG_ROOT = 7; + uint256 private constant CONFIG_P0_EFFECTS_OFFSET = 12; + uint256 private constant CONFIG_P1_EFFECTS_OFFSET = 13; + struct Tally { uint256 totalSload; uint256 coldSload; // slot's first touch in the window is this READ @@ -30,6 +40,13 @@ abstract contract GasMeasure is CommonBase { uint256 extraAccounts; // unique non-precompile accounts beyond the call target (2,600 cold each in prod) } + struct EffectStorageTally { + uint256 headerReads; + uint256 dataReads; + uint256 headerWrites; + uint256 dataWrites; + } + /// @dev Classify a state-diff window as ONE transaction: first touch of a slot is cold, /// subsequent touches warm. Call once per prod-tx-equivalent unit (e.g. once per turn) /// so cold/warm reflects a fresh cold-start access list. @@ -124,6 +141,118 @@ abstract contract GasMeasure is CommonBase { o.extraAccounts = a.extraAccounts + b.extraAccounts; } + /// @notice Count calls matching an exact `(accessor, account, selector)` tuple. + /// @dev Address zero is a wildcard for either address. Resume records and reverted calls + /// are excluded. This operates after the measured bracket, so it cannot perturb gas. + function _countCalls(Vm.AccountAccess[] memory accesses, address accessor, address account, bytes4 selector) + internal + pure + returns (uint256 count) + { + for (uint256 i; i < accesses.length; i++) { + Vm.AccountAccess memory a = accesses[i]; + if ( + !a.reverted && _isCallKind(a.kind) && a.data.length >= 4 + && (accessor == address(0) || a.accessor == accessor) + && (account == address(0) || a.account == account) && bytes4(a.data) == selector + ) { + count++; + } + } + } + + function _isCallKind(VmSafe.AccountAccessKind kind) private pure returns (bool) { + return kind == VmSafe.AccountAccessKind.Call || kind == VmSafe.AccountAccessKind.StaticCall + || kind == VmSafe.AccountAccessKind.DelegateCall || kind == VmSafe.AccountAccessKind.CallCode; + } + + /// @notice Count reads/writes to player EffectInstance header/data slots for one Engine + /// storage key. Player effect arrays are mappings at BattleConfig offsets 12/13; + /// each EffectInstance occupies two consecutive slots. + function _effectStorageTally(Vm.AccountAccess[] memory accesses, address engine, bytes32 storageKey) + internal + pure + returns (EffectStorageTally memory t) + { + bytes32 configBase = keccak256(abi.encode(storageKey, ENGINE_BATTLE_CONFIG_ROOT)); + bytes32 p0Root = bytes32(uint256(configBase) + CONFIG_P0_EFFECTS_OFFSET); + bytes32 p1Root = bytes32(uint256(configBase) + CONFIG_P1_EFFECTS_OFFSET); + (bytes32[] memory slotKeys, uint8[] memory slotKinds) = _buildPlayerEffectSlotTable(p0Root, p1Root); + + for (uint256 i; i < accesses.length; i++) { + Vm.StorageAccess[] memory sa = accesses[i].storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.account != engine || a.reverted) { + continue; + } + uint256 kind = _playerEffectSlotKind(a.slot, slotKeys, slotKinds); + if (kind == 1) { + if (a.isWrite) { + t.headerWrites++; + } else { + t.headerReads++; + } + } else if (kind == 2) { + if (a.isWrite) { + t.dataWrites++; + } else { + t.dataReads++; + } + } + } + } + } + + /// @dev Precompute the player EffectInstance header/data slots for O(1) census lookup. + function _buildPlayerEffectSlotTable(bytes32 p0Root, bytes32 p1Root) + private + pure + returns (bytes32[] memory keys, uint8[] memory kinds) + { + // 8 mons/side, 64 stable effect indices per mon. The mapping key is already the + // flattened `monIndex * EFFECT_SLOTS_PER_MON + effectIndex` value. A 4096-entry + // open-addressed table stays at 50% load for the 2048 header+data slots. + keys = new bytes32[](4096); + kinds = new uint8[](4096); + uint256 slotsPerSide = 8 * EFFECT_SLOTS_PER_MON; + for (uint256 i; i < slotsPerSide; i++) { + bytes32 p0Header = keccak256(abi.encode(i, p0Root)); + bytes32 p1Header = keccak256(abi.encode(i, p1Root)); + _insertEffectSlot(keys, kinds, p0Header, 1); + _insertEffectSlot(keys, kinds, bytes32(uint256(p0Header) + 1), 2); + _insertEffectSlot(keys, kinds, p1Header, 1); + _insertEffectSlot(keys, kinds, bytes32(uint256(p1Header) + 1), 2); + } + } + + function _insertEffectSlot(bytes32[] memory keys, uint8[] memory kinds, bytes32 key, uint8 kind) private pure { + uint256 mask = keys.length - 1; + uint256 i = uint256(key) & mask; + while (kinds[i] != 0) { + i = (i + 1) & mask; + } + keys[i] = key; + kinds[i] = kind; + } + + /// @return kind 0 = unrelated, 1 = EffectInstance header, 2 = EffectInstance data. + function _playerEffectSlotKind(bytes32 slot, bytes32[] memory keys, uint8[] memory kinds) + private + pure + returns (uint256 kind) + { + uint256 mask = keys.length - 1; + uint256 i = uint256(slot) & mask; + while (kinds[i] != 0) { + if (keys[i] == slot) { + return kinds[i]; + } + i = (i + 1) & mask; + } + return 0; + } + /// @notice Conservative production storage+account cost for a window, priced from the tally /// as if the window were ONE fresh production transaction. Assumptions, all chosen so /// a code change's benefit is never overstated: From 2c797f295e4c598c78ccc3a2885aa6ab5ca0372b Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:04:16 -0700 Subject: [PATCH 15/41] gas optimizations --- snapshots/MainnetReplayGasTest.json | 26 +-- src/Constants.sol | 5 + src/Engine.sol | 236 +++++++++++++++----- src/IEngine.sol | 5 +- src/Structs.sol | 2 +- src/effects/BasicEffect.sol | 8 +- src/effects/IEffect.sol | 18 +- src/effects/StaminaRegen.sol | 2 +- src/effects/battlefield/Overclock.sol | 2 +- src/effects/status/BlessedStatus.sol | 2 +- src/effects/status/BurnStatus.sol | 2 +- src/effects/status/FrostbiteStatus.sol | 2 +- src/effects/status/PanicStatus.sol | 2 +- src/effects/status/SleepStatus.sol | 2 +- src/effects/status/ZapStatus.sol | 2 +- src/lib/TargetLib.sol | 14 ++ src/mons/aurox/IronWall.sol | 2 +- src/mons/aurox/UpOnly.sol | 2 +- src/mons/ekineki/SneakAttack.sol | 2 +- src/mons/embursa/Q5.sol | 2 +- src/mons/embursa/Tinderclaws.sol | 14 +- src/mons/ghouliath/RiseFromTheGrave.sol | 2 +- src/mons/gorillax/Angery.sol | 2 +- src/mons/iblivion/Baselight.sol | 2 +- src/mons/iblivion/Loop.sol | 2 +- src/mons/inutia/ChainExpansion.sol | 2 +- src/mons/inutia/Initialize.sol | 2 +- src/mons/inutia/Interweaving.sol | 2 +- src/mons/malalien/ActusReus.sol | 2 +- src/mons/nirvamma/Adaptor.sol | 2 +- src/mons/nirvamma/HardReset.sol | 2 +- src/mons/pengym/PostWorkout.sol | 2 +- src/mons/sofabbi/CarrotHarvest.sol | 2 +- src/mons/xmon/Dreamcatcher.sol | 2 +- src/mons/xmon/InvokeTaboo.sol | 2 +- src/mons/xmon/OldVengeance.sol | 2 +- src/mons/xmon/Somniphobia.sol | 2 +- test/EngineTest.sol | 68 ++++++ test/effects/PreDamageHookTest.sol | 8 +- test/mocks/AfterDamageReboundEffect.sol | 2 +- test/mocks/AlwaysRejectsEffect.sol | 2 +- test/mocks/DummyStatus.sol | 2 +- test/mocks/FreshMoveContextAbility.sol | 64 ++++++ test/mocks/InstantDeathEffect.sol | 2 +- test/mocks/InstantDeathOnSwitchInEffect.sol | 2 +- test/mocks/MockSingletonAbility.sol | 2 +- test/mocks/NeverAppliesEffect.sol | 2 +- test/mocks/OnUpdateMonStateHealEffect.sol | 2 +- test/mocks/OneTurnStatBoost.sol | 2 +- test/mocks/OrderRecorderAbility.sol | 2 +- test/mocks/SetEffectDataAttack.sol | 59 +++++ test/mocks/SingleInstanceEffect.sol | 2 +- test/mocks/SpAtkDebuffEffect.sol | 2 +- test/mocks/StaminaGainKOEffect.sol | 2 +- test/mocks/TempStatBoostEffect.sol | 2 +- test/mocks/WideDataEffect.sol | 27 +++ 56 files changed, 502 insertions(+), 136 deletions(-) create mode 100644 test/mocks/FreshMoveContextAbility.sol create mode 100644 test/mocks/SetEffectDataAttack.sol create mode 100644 test/mocks/WideDataEffect.sol diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json index 07e6c05e..66a81c59 100644 --- a/snapshots/MainnetReplayGasTest.json +++ b/snapshots/MainnetReplayGasTest.json @@ -1,14 +1,14 @@ { "MainnetDoublesReplay1_Reused_afterDamageCalls": "8", "MainnetDoublesReplay1_Reused_afterMoveCalls": "5", - "MainnetDoublesReplay1_Reused_effectDataReads": "36", - "MainnetDoublesReplay1_Reused_effectDataWrites": "14", - "MainnetDoublesReplay1_Reused_effectHeaderReads": "54", - "MainnetDoublesReplay1_Reused_effectHeaderWrites": "16", - "MainnetDoublesReplay1_Reused_execGas": "1276607", + "MainnetDoublesReplay1_Reused_effectDataReads": "0", + "MainnetDoublesReplay1_Reused_effectDataWrites": "0", + "MainnetDoublesReplay1_Reused_effectHeaderReads": "45", + "MainnetDoublesReplay1_Reused_effectHeaderWrites": "14", + "MainnetDoublesReplay1_Reused_execGas": "1220542", "MainnetDoublesReplay1_Reused_lifecycleCalls": "34", "MainnetDoublesReplay1_Reused_moveDecisionCallbacks": "0", - "MainnetDoublesReplay1_Reused_nonHookHeaderReadUpperBound": "20", + "MainnetDoublesReplay1_Reused_nonHookHeaderReadUpperBound": "11", "MainnetDoublesReplay1_Reused_preDamageCalls": "0", "MainnetDoublesReplay1_Reused_roundEndCalls": "19", "MainnetDoublesReplay1_Reused_roundStartCalls": "0", @@ -16,14 +16,14 @@ "MainnetDoublesReplay1_Reused_updateStateCalls": "2", "MainnetReplay_Reused_afterDamageCalls": "9", "MainnetReplay_Reused_afterMoveCalls": "19", - "MainnetReplay_Reused_effectDataReads": "98", - "MainnetReplay_Reused_effectDataWrites": "36", - "MainnetReplay_Reused_effectHeaderReads": "186", - "MainnetReplay_Reused_effectHeaderWrites": "31", - "MainnetReplay_Reused_execGas": "2377783", + "MainnetReplay_Reused_effectDataReads": "0", + "MainnetReplay_Reused_effectDataWrites": "0", + "MainnetReplay_Reused_effectHeaderReads": "191", + "MainnetReplay_Reused_effectHeaderWrites": "39", + "MainnetReplay_Reused_execGas": "2198697", "MainnetReplay_Reused_lifecycleCalls": "84", - "MainnetReplay_Reused_moveDecisionCallbacks": "19", - "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "102", + "MainnetReplay_Reused_moveDecisionCallbacks": "0", + "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "107", "MainnetReplay_Reused_preDamageCalls": "0", "MainnetReplay_Reused_roundEndCalls": "56", "MainnetReplay_Reused_roundStartCalls": "0", diff --git a/src/Constants.sol b/src/Constants.sol index 311e0512..a03d2475 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -71,6 +71,11 @@ uint256 constant STATUS_CLASS_MASK = 0xF; // (escalating statuses, e.g. Burn). Clear = a same-class re-apply is a zero-call no-op. uint16 constant HAS_REAPPLY_BIT = 0x4000; +// getStepsBitmap metadata: lifecycle steps remain in the low 16 bits; the high 16 bits opt an +// effect into fresh per-step hook context. The EVM selector is unchanged from the legacy uint16 +// return, and legacy deployed effects decode as metadata with a zero context half. +uint256 constant EFFECT_CONTEXT_SHIFT = 16; + uint256 constant MAX_BATTLE_DURATION = 1 hours; bytes32 constant MOVE_MISS_EVENT_TYPE = sha256(abi.encode("MoveMiss")); diff --git a/src/Engine.sol b/src/Engine.sol index 7088b465..f0fe367e 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -9,8 +9,8 @@ import "./moves/IMoveSet.sol"; import {IEngine} from "./IEngine.sol"; import {IAbility} from "./abilities/IAbility.sol"; -import {IStatusEffect} from "./effects/status/IStatusEffect.sol"; import {SignedCommitLib} from "./commit-manager/SignedCommitLib.sol"; +import {IStatusEffect} from "./effects/status/IStatusEffect.sol"; import {ECDSA} from "./lib/ECDSA.sol"; import {EIP712} from "./lib/EIP712.sol"; import {MappingAllocator} from "./lib/MappingAllocator.sol"; @@ -388,9 +388,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { (IEffect[] memory effects, bytes32[] memory data) = battle.ruleset.getInitialGlobalEffects(); uint256 numEffects = effects.length; for (uint256 i = 0; i < numEffects;) { - config.globalEffects[i].effect = effects[i]; - config.globalEffects[i].stepsBitmap = effects[i].getStepsBitmap(); - config.globalEffects[i].data = data[i]; + IEffect effect = effects[i]; + uint32 metadata = effect.getStepsBitmap(); + _writeEffectInstance( + config.globalEffects[i], effect, uint16(metadata), data[i], uint16(metadata >> EFFECT_CONTEXT_SHIFT) + ); unchecked { ++i; } @@ -568,7 +570,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (msg.sender != config.moveManager) { revert WrongCaller(); } - for (uint256 i = 0; i < entries.length; i++) { uint256 entry = entries[i]; uint8 p0Move = uint8(entry); @@ -1139,6 +1140,17 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return playerIndex == 0 ? config.p0Move : config.p1Move; } + /// @dev Allocation-free current-turn lane with the same transient/storage fallback as + /// _getCurrentTurnMove. Used to build fresh external hook context. + function _currentTurnMoveWord(BattleConfig storage config, uint256 playerIndex) private view returns (uint256) { + uint256 encoded = (playerIndex == 0 ? _turnP0Packed : _turnP1Packed) >> 104; + if (encoded != 0) { + return encoded & 0xFFFFFF; + } + MoveDecision storage move = playerIndex == 0 ? config.p0Move : config.p1Move; + return uint256(move.packedMoveIndex) | (uint256(move.extraData) << 8); + } + /// @notice Internal execution logic shared by execute() and executeWithMoves() /// @dev Two independent event controls. `emitMonMoves`: per-turn MonMoves at the top (legacy /// per-turn execute only — the batched/buffer paths announce moves elsewhere). `emitBattleComplete`: @@ -1850,7 +1862,9 @@ contract Engine is IEngine, MappingAllocator, EIP712 { function _addEffectInternal(uint256 targetIndex, uint256 monIndex, IEffect effect, bytes32 extraData) internal { bytes32 battleKey = battleKeyForWrite; // Fetch steps bitmap once (reused for the status gate, ALWAYS_APPLIES check, and storage) - uint16 stepsBitmap = effect.getStepsBitmap(); + uint32 effectMetadata = effect.getStepsBitmap(); + uint16 stepsBitmap = uint16(effectMetadata); + uint16 hookContextBitmap = uint16(effectMetadata >> EFFECT_CONTEXT_SHIFT); BattleConfig storage config = battleConfig[storageKeyForWrite]; // Exclusive-status gate (class-bearing player effects only), ahead of any external call: @@ -1912,8 +1926,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (statusClass != 0) { uint256 laneShift = _statusLaneShift(targetIndex, monIndex); config.monStatusLanes = uint64( - (uint256(config.monStatusLanes) & ~(uint256(0xF) << laneShift)) - | (statusClass << laneShift) + (uint256(config.monStatusLanes) & ~(uint256(0xF) << laneShift)) | (statusClass << laneShift) ); } } @@ -1922,9 +1935,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Global effects use simple sequential indexing uint256 effectIndex = config.globalEffectsLength; EffectInstance storage effectSlot = config.globalEffects[effectIndex]; - effectSlot.effect = effect; - effectSlot.stepsBitmap = stepsBitmap; - effectSlot.data = extraDataToUse; + _writeEffectInstance(effectSlot, effect, stepsBitmap, extraDataToUse, hookContextBitmap); config.globalEffectsLength = uint8(effectIndex + 1); // Set dirty bit 0 for global effects effectsDirtyBitmap |= 1; @@ -1933,9 +1944,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 monEffectCount = _getMonEffectCount(config.packedP0EffectsCount, monIndex); uint256 slotIndex = _getEffectSlotIndex(monIndex, monEffectCount); EffectInstance storage effectSlot = config.p0Effects[slotIndex]; - effectSlot.effect = effect; - effectSlot.stepsBitmap = stepsBitmap; - effectSlot.data = extraDataToUse; + _writeEffectInstance(effectSlot, effect, stepsBitmap, extraDataToUse, hookContextBitmap); config.packedP0EffectsCount = _setMonEffectCount(config.packedP0EffectsCount, monIndex, monEffectCount + 1); // Set dirty bit (1 + monIndex) for P0 effects @@ -1944,9 +1953,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 monEffectCount = _getMonEffectCount(config.packedP1EffectsCount, monIndex); uint256 slotIndex = _getEffectSlotIndex(monIndex, monEffectCount); EffectInstance storage effectSlot = config.p1Effects[slotIndex]; - effectSlot.effect = effect; - effectSlot.stepsBitmap = stepsBitmap; - effectSlot.data = extraDataToUse; + _writeEffectInstance(effectSlot, effect, stepsBitmap, extraDataToUse, hookContextBitmap); config.packedP1EffectsCount = _setMonEffectCount(config.packedP1EffectsCount, monIndex, monEffectCount + 1); // Set dirty bit (9 + monIndex) for P1 effects @@ -1976,19 +1983,20 @@ contract Engine is IEngine, MappingAllocator, EIP712 { address(e.effect) != TOMBSTONE_ADDRESS && ((e.stepsBitmap >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK) == statusClass ) { - (bytes32 newData, bool removeAfterRun) = IStatusEffect(address(e.effect)).onReapply( - IEngine(address(this)), - battleKey, - tempRNG, - e.data, - targetIndex, - monIndex, - _hookActivesWord(battleData[battleKey]) - ); + (bytes32 newData, bool removeAfterRun) = IStatusEffect(address(e.effect)) + .onReapply( + IEngine(address(this)), + battleKey, + tempRNG, + _effectData(e), + targetIndex, + monIndex, + _hookActivesWord(battleData[battleKey]) + ); if (removeAfterRun) { _removeEffectAtSlot(config, battleKey, targetIndex, monIndex, baseSlot + i); } else { - e.data = newData; + _setEffectData(e, newData); } return; } @@ -2022,7 +2030,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { effectInstance = config.p1Effects[effectIndex]; } - effectInstance.data = newExtraData; + _setEffectData(effectInstance, newExtraData); } function removeEffect(uint256 targetIndex, uint256 monIndex, uint256 indexToRemove) public { @@ -2043,6 +2051,84 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return ((targetIndex << 3) | monIndex) << 4; } + uint256 private constant EFFECT_COMPACT_DATA_BITS = 79; + uint256 private constant EFFECT_COMPACT_DATA_MASK = (uint256(1) << EFFECT_COMPACT_DATA_BITS) - 1; + uint256 private constant EFFECT_HOOK_CONTEXT_FLAG = uint256(1) << 255; + uint256 private constant EFFECT_HEADER_BASE_MASK = uint256(type(uint176).max) | EFFECT_HOOK_CONTEXT_FLAG; + uint256 private constant FRESH_HOOK_CONTEXT_REQUEST = uint256(1) << 255; + + /// @dev Effect header layout: address[0:160], steps[160:176], compact-data marker[176:255], + /// fresh-hook-context capability[255]. A nonzero marker stores `data + 1`; zero means the + /// full bytes32 lives in slot 1. Reserving one capability bit leaves 79 compact data bits. + /// A nonzero marker stores `data + 1`; zero means the full bytes32 lives in slot 1. The offset + /// represents zero without giving up a tag bit and leaves arbitrary wide data fully supported. + function _effectData(EffectInstance storage effectInstance) private view returns (bytes32 data) { + uint256 header; + assembly ("memory-safe") { + header := sload(effectInstance.slot) + } + uint256 encoded = (header >> 176) & EFFECT_COMPACT_DATA_MASK; + if (encoded != 0) { + return bytes32(encoded - 1); + } + return effectInstance.data; + } + + function _setEffectData(EffectInstance storage effectInstance, bytes32 data) private { + uint256 header; + assembly ("memory-safe") { + header := sload(effectInstance.slot) + } + uint256 raw = uint256(data); + if (raw < EFFECT_COMPACT_DATA_MASK) { + header = (header & EFFECT_HEADER_BASE_MASK) | ((raw + 1) << 176); + assembly ("memory-safe") { + sstore(effectInstance.slot, header) + } + } else { + if (((header >> 176) & EFFECT_COMPACT_DATA_MASK) != 0) { + header &= EFFECT_HEADER_BASE_MASK; + assembly ("memory-safe") { + sstore(effectInstance.slot, header) + } + } + effectInstance.data = data; + } + } + + function _writeEffectInstance( + EffectInstance storage effectInstance, + IEffect effect, + uint16 stepsBitmap, + bytes32 data, + uint16 hookContextBitmap + ) private { + uint256 raw = uint256(data); + uint256 encoded = raw < EFFECT_COMPACT_DATA_MASK ? raw + 1 : 0; + uint256 header = uint256(uint160(address(effect))) | (uint256(stepsBitmap) << 160) | (encoded << 176) + | (hookContextBitmap & (1 << uint8(EffectStep.AfterMove)) != 0 ? EFFECT_HOOK_CONTEXT_FLAG : 0); + assembly ("memory-safe") { + sstore(effectInstance.slot, header) + } + if (encoded == 0) { + effectInstance.data = data; + } + } + + function _effectDataAndHookContext(EffectInstance storage effectInstance) + private + view + returns (bytes32 data, bool needsFreshContext) + { + uint256 header; + assembly ("memory-safe") { + header := sload(effectInstance.slot) + } + uint256 encoded = (header >> 176) & EFFECT_COMPACT_DATA_MASK; + data = encoded == 0 ? effectInstance.data : bytes32(encoded - 1); + needsFreshContext = (header & EFFECT_HOOK_CONTEXT_FLAG) != 0; + } + function _monEffectSteps(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) private view @@ -2142,7 +2228,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { effect.onRemove( IEngine(address(this)), battleKey, - eff.data, + _effectData(eff), targetIndex, monIndex, _hookActivesWord(battleData[battleKey]) @@ -2436,7 +2522,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { StatBoostLib.unpackBoostData(words[baseSlot + i]); StatBoostLib.accumulateBoosts(baseStats, bp, bc, im, numBoostsPerStat, accumulatedNumeratorPerStat); } - newBoostedStats = StatBoostLib.finalizeBoostedStats(baseStats, numBoostsPerStat, accumulatedNumeratorPerStat); + newBoostedStats = + StatBoostLib.finalizeBoostedStats(baseStats, numBoostsPerStat, accumulatedNumeratorPerStat); } else { newBoostedStats = StatBoostLib.finalizeAccStats(acc, baseStats); } @@ -3444,6 +3531,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // check), while letting _runSingleEffect do it would first pay the eff.data SLOAD // (slot 1) + 13-arg call setup. if (address(eff.effect) != TOMBSTONE_ADDRESS && (eff.stepsBitmap & (1 << uint8(round))) != 0) { + (bytes32 effectData, bool needsFreshContext) = _effectDataAndHookContext(eff); + uint256 hookContext = activesPacked; + if (round == EffectStep.AfterMove && needsFreshContext) { + hookContext |= FRESH_HOOK_CONTEXT_REQUEST; + } _runSingleEffect( config, rng, @@ -3454,9 +3546,9 @@ contract Engine is IEngine, MappingAllocator, EIP712 { extraEffectsData, eff.effect, eff.stepsBitmap, - eff.data, + effectData, uint96(slotIndex), - activesPacked + hookContext ); // Re-read count if a new effect was added during this iteration @@ -3493,7 +3585,15 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Run the effect and get result (bytes32 updatedExtraData, bool removeAfterRun) = _executeEffectHook( - battleKeyForWrite, effect, rng, data, playerIndex, monIndex, round, extraEffectsData, activesPacked + battleKeyForWrite, + effect, + rng, + data, + playerIndex, + monIndex, + round, + extraEffectsData, + activesPacked ); // If we need to remove or update the effect @@ -3532,6 +3632,25 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 source = abi.decode(extraEffectsData, (uint256)); return effect.onPreDamage(self, battleKey, rng, data, playerIndex, monIndex, activesPacked, source); } else if (round == EffectStep.AfterMove) { + // Enrich the existing active-lane word with a fresh current-turn snapshot immediately + // before each callback. An earlier effect may rewrite a move, so this cannot be cached + // once per pass. Low 32 bits remain the long-standing active-mon ABI; move lanes occupy + // bits 32..127 and the acted-slot mask bits 128..131. + if (activesPacked & FRESH_HOOK_CONTEXT_REQUEST != 0) { + activesPacked &= ~FRESH_HOOK_CONTEXT_REQUEST; + BattleData storage battle = battleData[battleKey]; + if (battle.isTwoSlotMode) { + activesPacked |= _currentTurnMoveWordForSlot(0, 0) << 32; + activesPacked |= _currentTurnMoveWordForSlot(0, 1) << 56; + activesPacked |= _currentTurnMoveWordForSlot(1, 0) << 80; + activesPacked |= _currentTurnMoveWordForSlot(1, 1) << 104; + } else { + BattleConfig storage config = battleConfig[storageKeyForWrite]; + activesPacked |= _currentTurnMoveWord(config, 0) << 32; + activesPacked |= _currentTurnMoveWord(config, 1) << 80; + } + activesPacked |= actedSlotsThisTurnMask << 128; + } return effect.onAfterMove(self, battleKey, rng, data, playerIndex, monIndex, activesPacked); } else if (round == EffectStep.OnUpdateMonState) { (uint256 statePlayerIndex, uint256 stateMonIndex, MonStateIndexName stateVarIndex, int32 valueToAdd) = @@ -3558,11 +3677,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } else { // Update the data at the slot if (effectIndex == 2) { - config.globalEffects[slotIndex].data = updatedExtraData; + _setEffectData(config.globalEffects[slotIndex], updatedExtraData); } else if (effectIndex == 0) { - config.p0Effects[slotIndex].data = updatedExtraData; + _setEffectData(config.p0Effects[slotIndex], updatedExtraData); } else { - config.p1Effects[slotIndex].data = updatedExtraData; + _setEffectData(config.p1Effects[slotIndex], updatedExtraData); } } } @@ -4395,8 +4514,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // turn — can't be left vacant by an illegal pick: a colliding switch would re-mask // the lane every turn (an infinite-stall vector). Coerce to the first legal target; // a genuinely empty bench (one survivor) finds none and falls to the no-op guard. - bool laneMustFill = activeMon == EMPTY_ACTIVE_LANE - || _getMonState(config, side, activeMon).isKnockedOut; + bool laneMustFill = activeMon == EMPTY_ACTIVE_LANE || _getMonState(config, side, activeMon).isKnockedOut; if (laneMustFill && !_isLegalSlotSwitchTarget(config, battle, absSlot, monToSwitchIndex)) { (monToSwitchIndex,) = _firstLegalSwitchTarget(config, battle, absSlot); } @@ -4904,8 +5022,13 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256[] memory globalIndices = new uint256[](globalEffectsLength); uint256 globalIdx = 0; for (uint256 i = 0; i < globalEffectsLength;) { - if (address(config.globalEffects[i].effect) != TOMBSTONE_ADDRESS) { - globalResult[globalIdx] = config.globalEffects[i]; + EffectInstance storage storedEffect = config.globalEffects[i]; + if (address(storedEffect.effect) != TOMBSTONE_ADDRESS) { + globalResult[globalIdx] = EffectInstance({ + effect: storedEffect.effect, + stepsBitmap: storedEffect.stepsBitmap, + data: _effectData(storedEffect) + }); globalIndices[globalIdx] = i; unchecked { ++globalIdx; @@ -4934,8 +5057,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 idx = 0; for (uint256 i = 0; i < monEffectCount;) { uint256 slotIndex = baseSlot + i; - if (address(effects[slotIndex].effect) != TOMBSTONE_ADDRESS) { - result[idx] = effects[slotIndex]; + EffectInstance storage storedEffect = effects[slotIndex]; + if (address(storedEffect.effect) != TOMBSTONE_ADDRESS) { + result[idx] = EffectInstance({ + effect: storedEffect.effect, stepsBitmap: storedEffect.stepsBitmap, data: _effectData(storedEffect) + }); indices[idx] = slotIndex; unchecked { ++idx; @@ -4967,8 +5093,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { EffectInstance[] memory globalEffects = new EffectInstance[](globalLen); uint256 gIdx = 0; for (uint256 i = 0; i < globalLen;) { - if (address(config.globalEffects[i].effect) != TOMBSTONE_ADDRESS) { - globalEffects[gIdx] = config.globalEffects[i]; + EffectInstance storage storedEffect = config.globalEffects[i]; + if (address(storedEffect.effect) != TOMBSTONE_ADDRESS) { + globalEffects[gIdx] = EffectInstance({ + effect: storedEffect.effect, stepsBitmap: storedEffect.stepsBitmap, data: _effectData(storedEffect) + }); unchecked { ++gIdx; } @@ -5124,8 +5253,13 @@ contract Engine is IEngine, MappingAllocator, EIP712 { EffectInstance[] memory monEffects = new EffectInstance[](monCount); uint256 idx = 0; for (uint256 i = 0; i < monCount;) { - if (address(effects[baseSlot + i].effect) != TOMBSTONE_ADDRESS) { - monEffects[idx] = effects[baseSlot + i]; + EffectInstance storage storedEffect = effects[baseSlot + i]; + if (address(storedEffect.effect) != TOMBSTONE_ADDRESS) { + monEffects[idx] = EffectInstance({ + effect: storedEffect.effect, + stepsBitmap: storedEffect.stepsBitmap, + data: _effectData(storedEffect) + }); unchecked { ++idx; } @@ -5410,11 +5544,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { /// @dev View shape of a side's stat-boost store: per-mon arrays of packed source words /// (StatBoostLib layout — clients decode key/perm/lanes off-chain). - function _boostWordsView(BattleConfig storage config, uint256 side) - private - view - returns (bytes32[][] memory out) - { + function _boostWordsView(BattleConfig storage config, uint256 side) private view returns (bytes32[][] memory out) { uint256 teamSize = side == 0 ? (config.teamSizes & 0x0F) : (config.teamSizes >> 4); mapping(uint256 => bytes32) storage words = _boostWordsOf(config, side); out = new bytes32[][](teamSize); @@ -5455,7 +5585,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { for (uint256 i; i < len;) { EffectInstance storage e = config.globalEffects[i]; if (address(e.effect) == effectAddr) { - return (true, i, e.data); + return (true, i, _effectData(e)); } unchecked { ++i; @@ -5471,7 +5601,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 slotIndex = baseSlot + i; EffectInstance storage e = effects[slotIndex]; if (address(e.effect) == effectAddr) { - return (true, slotIndex, e.data); + return (true, slotIndex, _effectData(e)); } unchecked { ++i; diff --git a/src/IEngine.sol b/src/IEngine.sol index 5089110d..b43fe17f 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -193,10 +193,7 @@ interface IEngine { external view returns (bool exists, uint256 effectIndex, bytes32 data); - function getMonStatusClass(bytes32 battleKey, uint256 targetIndex, uint256 monIndex) - external - view - returns (uint256); + function getMonStatusClass(bytes32 battleKey, uint256 targetIndex, uint256 monIndex) external view returns (uint256); function getWinner(bytes32 battleKey) external view returns (address); function getKOBitmap(bytes32 battleKey, uint256 playerIndex) external view returns (uint256); function getBattleContext(bytes32 battleKey) external view returns (BattleContext memory); diff --git a/src/Structs.sol b/src/Structs.sol index 2fa7eabe..d214bae0 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -232,7 +232,7 @@ struct BattleConfig { struct EffectInstance { IEffect effect; // 160 bits uint16 stepsBitmap; // 16 bits - packs with effect in slot 0 (bit i = runs at EffectStep(i)) - // 80 bits unused in slot 0 + // High 80 bits carry Engine's offset-tagged compact data, or zero for slot-1 fallback. bytes32 data; // 256 bits in slot 1 } diff --git a/src/effects/BasicEffect.sol b/src/effects/BasicEffect.sol index 0c8aa460..39332ed6 100644 --- a/src/effects/BasicEffect.sol +++ b/src/effects/BasicEffect.sol @@ -6,11 +6,9 @@ import "../IEngine.sol"; import "../Structs.sol"; abstract contract BasicEffect is IEffect { - // Each subclass must override getStepsBitmap() to return a static constant - // Bit layout: OnApply=0x01, RoundStart=0x02, RoundEnd=0x04, OnRemove=0x08, - // OnMonSwitchIn=0x10, OnMonSwitchOut=0x20, AfterDamage=0x40, AfterMove=0x80, - // OnUpdateMonState=0x100, PreDamage=0x200 - function getStepsBitmap() external pure virtual returns (uint16); + // Each subclass returns static metadata: lifecycle steps in the low uint16 and optional + // fresh-context step requests in the high uint16 (see IEffect). + function getStepsBitmap() external pure virtual returns (uint32); function name() external virtual returns (string memory) { return ""; diff --git a/src/effects/IEffect.sol b/src/effects/IEffect.sol index bea48ac5..31688029 100644 --- a/src/effects/IEffect.sol +++ b/src/effects/IEffect.sol @@ -8,11 +8,12 @@ import "../Structs.sol"; interface IEffect { function name() external returns (string memory); - // Returns pre-computed bitmap of steps this effect runs at (set at deploy time) - // Bit layout: OnApply=0x01, RoundStart=0x02, RoundEnd=0x04, OnRemove=0x08, - // OnMonSwitchIn=0x10, OnMonSwitchOut=0x20, AfterDamage=0x40, AfterMove=0x80, - // OnUpdateMonState=0x100, PreDamage=0x200 - function getStepsBitmap() external view returns (uint16); + // Returns static metadata: lifecycle steps in bits 0-15 and requested fresh-context steps in + // bits 16-31. Lifecycle layout: OnApply=0x01, RoundStart=0x02, RoundEnd=0x04, + // OnRemove=0x08, OnMonSwitchIn=0x10, OnMonSwitchOut=0x20, AfterDamage=0x40, + // AfterMove=0x80, OnUpdateMonState=0x100, PreDamage=0x200. Legacy deployed effects returning + // only the low uint16 remain EVM-compatible and simply request no fresh context. + function getStepsBitmap() external view returns (uint32); // Whether or not to add the effect if some condition is met function shouldApply(IEngine engine, bytes32 battleKey, bytes32 extraData, uint256 targetIndex, uint256 monIndex) @@ -20,9 +21,10 @@ interface IEffect { returns (bool); // Lifecycle hooks during normal battle flow. - // `activesPacked` carries every slot's active roster index (one 8-bit lane per absolute - // slot, EMPTY_ACTIVE_LANE = no mon there) — passed to avoid external calls back to Engine. - // Decode via TargetLib (sideActive / activeAt). + // `activesPacked` carries every slot's active roster index in its low 32 bits (one 8-bit lane + // per absolute slot, EMPTY_ACTIVE_LANE = no mon there). For an opted-in AfterMove hook, Engine + // also embeds fresh 24-bit move lanes at bits 32..127 and the acted-slot mask at bits 128..131. + // Existing effects remain compatible; opt-in readers decode the high context via TargetLib. function onRoundStart( IEngine engine, bytes32 battleKey, diff --git a/src/effects/StaminaRegen.sol b/src/effects/StaminaRegen.sol index 72e8ab82..dfec7b51 100644 --- a/src/effects/StaminaRegen.sol +++ b/src/effects/StaminaRegen.sol @@ -14,7 +14,7 @@ contract StaminaRegen is BasicEffect { } // Steps: RoundEnd, AfterMove - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8084; } diff --git a/src/effects/battlefield/Overclock.sol b/src/effects/battlefield/Overclock.sol index a6b630f5..4de2f21e 100644 --- a/src/effects/battlefield/Overclock.sol +++ b/src/effects/battlefield/Overclock.sol @@ -21,7 +21,7 @@ contract Overclock is BasicEffect { } // Steps: OnApply, RoundEnd, OnRemove, OnMonSwitchIn - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x801D; } diff --git a/src/effects/status/BlessedStatus.sol b/src/effects/status/BlessedStatus.sol index fde4b000..312e1f70 100644 --- a/src/effects/status/BlessedStatus.sol +++ b/src/effects/status/BlessedStatus.sol @@ -18,7 +18,7 @@ contract BlessedStatus is StatusEffect { } // Steps: OnApply (0x01), OnRemove (0x08), PreDamage (0x200) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x209 | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } diff --git a/src/effects/status/BurnStatus.sol b/src/effects/status/BurnStatus.sol index 23a763a4..3e645cee 100644 --- a/src/effects/status/BurnStatus.sol +++ b/src/effects/status/BurnStatus.sol @@ -27,7 +27,7 @@ contract BurnStatus is StatusEffect, IStatusEffect { // Steps: OnApply, RoundEnd, OnRemove (no RoundStart behavior — the bit would only buy a // no-op external call per burned turn). HAS_REAPPLY routes same-class re-applies to the // degree escalation in onReapply. - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | HAS_REAPPLY_BIT | ALWAYS_APPLIES_BIT; } diff --git a/src/effects/status/FrostbiteStatus.sol b/src/effects/status/FrostbiteStatus.sol index 1dd3b60f..a170e131 100644 --- a/src/effects/status/FrostbiteStatus.sol +++ b/src/effects/status/FrostbiteStatus.sol @@ -19,7 +19,7 @@ contract FrostbiteStatus is StatusEffect { } // Steps: OnApply, RoundEnd, OnRemove - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } diff --git a/src/effects/status/PanicStatus.sol b/src/effects/status/PanicStatus.sol index be07ee6d..014eba47 100644 --- a/src/effects/status/PanicStatus.sol +++ b/src/effects/status/PanicStatus.sol @@ -17,7 +17,7 @@ contract PanicStatus is StatusEffect { } // Steps: OnApply, RoundStart, RoundEnd, OnRemove - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } diff --git a/src/effects/status/SleepStatus.sol b/src/effects/status/SleepStatus.sol index 6bc23d4b..e6e2b1fc 100644 --- a/src/effects/status/SleepStatus.sol +++ b/src/effects/status/SleepStatus.sol @@ -19,7 +19,7 @@ contract SleepStatus is StatusEffect { } // Steps: OnApply, RoundStart, RoundEnd, OnRemove - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT); } diff --git a/src/effects/status/ZapStatus.sol b/src/effects/status/ZapStatus.sol index 6935385e..5a52537f 100644 --- a/src/effects/status/ZapStatus.sol +++ b/src/effects/status/ZapStatus.sol @@ -21,7 +21,7 @@ contract ZapStatus is StatusEffect { } // Steps: OnApply, RoundStart, RoundEnd, OnRemove - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } diff --git a/src/lib/TargetLib.sol b/src/lib/TargetLib.sol index 5c22bce6..28f617d0 100644 --- a/src/lib/TargetLib.sol +++ b/src/lib/TargetLib.sol @@ -8,6 +8,9 @@ import "../Constants.sol"; /// 2 = side1/slot0, 3 = side1/slot1). `activesPacked` carries one 8-bit lane per /// absolute slot holding that slot's active roster index (EMPTY_ACTIVE_LANE = no mon). library TargetLib { + uint256 internal constant HOOK_MOVE_LANES_SHIFT = 32; + uint256 internal constant HOOK_ACTED_MASK_SHIFT = 128; + function sideOf(uint256 absSlot) internal pure returns (uint256) { return absSlot >> 1; } @@ -57,6 +60,17 @@ library TargetLib { return NO_SLOT; } + /// @notice Fresh 24-bit current-move lane embedded by Engine immediately before an + /// AfterMove callback: [extraData:16 | packedMoveIndex:8]. Other hook steps leave + /// these context lanes unset. The low 32 active-mon lanes remain ABI-compatible. + function hookMoveWordAt(uint256 hookContext, uint256 absSlot) internal pure returns (uint256) { + return (hookContext >> (HOOK_MOVE_LANES_SHIFT + absSlot * 24)) & 0xFFFFFF; + } + + function hookSlotActed(uint256 hookContext, uint256 absSlot) internal pure returns (bool) { + return ((hookContext >> (HOOK_ACTED_MASK_SHIFT + absSlot)) & 1) != 0; + } + /// @dev Kit-audit ruling for untargeted "the opposing active" effects (switch-in chips, /// KO-triggered debuffs...): the mirror of `ownSlot` on the opposing side, falling back /// to its partner when the mirror lane is empty; NO_SLOT when the opposing side is vacant. diff --git a/src/mons/aurox/IronWall.sol b/src/mons/aurox/IronWall.sol index 3beda519..e65d2527 100644 --- a/src/mons/aurox/IronWall.sol +++ b/src/mons/aurox/IronWall.sol @@ -82,7 +82,7 @@ contract IronWall is IMoveSet, BasicEffect { // IEffect implementation // Steps: OnMonSwitchOut, AfterDamage - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8060; } diff --git a/src/mons/aurox/UpOnly.sol b/src/mons/aurox/UpOnly.sol index 633026f2..2ce731f2 100644 --- a/src/mons/aurox/UpOnly.sol +++ b/src/mons/aurox/UpOnly.sol @@ -32,7 +32,7 @@ contract UpOnly is IAbility, BasicEffect { // IEffect implementation // Steps: AfterDamage - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8040; } diff --git a/src/mons/ekineki/SneakAttack.sol b/src/mons/ekineki/SneakAttack.sol index 6c5f0f4e..83932644 100644 --- a/src/mons/ekineki/SneakAttack.sol +++ b/src/mons/ekineki/SneakAttack.sol @@ -116,7 +116,7 @@ contract SneakAttack is IMoveSet, BasicEffect { // IEffect implementation — local effect that cleans up on switch-out // Steps: OnMonSwitchOut - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8020; } diff --git a/src/mons/embursa/Q5.sol b/src/mons/embursa/Q5.sol index 1188f75a..9e77bff2 100644 --- a/src/mons/embursa/Q5.sol +++ b/src/mons/embursa/Q5.sol @@ -104,7 +104,7 @@ contract Q5 is IMoveSet, BasicEffect { // Effect implementation // Steps: RoundStart - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8002; } diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index ac1406a2..707792a8 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -14,7 +14,7 @@ import { } from "../../Constants.sol"; import {MonStateIndexName, StatBoostFlag, StatBoostType} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; -import {EffectInstance, IEffect, MoveDecision, StatBoostToApply} from "../../Structs.sol"; +import {EffectInstance, IEffect, StatBoostToApply} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {TargetLib} from "../../lib/TargetLib.sol"; @@ -47,8 +47,9 @@ contract Tinderclaws is IAbility, BasicEffect { } // Steps: RoundEnd, AfterMove - function getStepsBitmap() external pure override returns (uint16) { - return 0x8084; + function getStepsBitmap() external pure override returns (uint32) { + // Low 16 bits: lifecycle steps. High 16 bits: requested fresh-context steps. + return 0x00808084; // AfterMove context | ALWAYS_APPLIES | AfterMove | RoundEnd } // extraData: 0 = no SpATK boost applied, 1 = SpATK boost applied @@ -65,9 +66,10 @@ contract Tinderclaws is IAbility, BasicEffect { if (ownSlot == NO_SLOT) { return (updatedExtraData, removeAfterRun); } - MoveDecision memory moveDecision = engine.getMoveDecisionForSlot(battleKey, targetIndex, ownSlot & 1); - // Unpack the move index from packedMoveIndex - uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; + // Engine embeds a fresh current-move lane immediately before every AfterMove callback. + // Reading it here avoids a warm external round-trip while remaining correct when an + // earlier effect rewrites the move during the same pass. + uint8 moveIndex = uint8(TargetLib.hookMoveWordAt(activesPacked, ownSlot)) & MOVE_INDEX_MASK; // If resting, remove burn if (moveIndex == NO_OP_MOVE_INDEX) { diff --git a/src/mons/ghouliath/RiseFromTheGrave.sol b/src/mons/ghouliath/RiseFromTheGrave.sol index 184c3832..e13a8801 100644 --- a/src/mons/ghouliath/RiseFromTheGrave.sol +++ b/src/mons/ghouliath/RiseFromTheGrave.sol @@ -36,7 +36,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { // IEffect implementation // Steps: RoundEnd, AfterDamage - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8044; } diff --git a/src/mons/gorillax/Angery.sol b/src/mons/gorillax/Angery.sol index 68bce810..530ac196 100644 --- a/src/mons/gorillax/Angery.sol +++ b/src/mons/gorillax/Angery.sol @@ -34,7 +34,7 @@ contract Angery is IAbility, BasicEffect { // IEffect implementation // Steps: RoundEnd, AfterDamage - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8044; } diff --git a/src/mons/iblivion/Baselight.sol b/src/mons/iblivion/Baselight.sol index d94216aa..c6eff34b 100644 --- a/src/mons/iblivion/Baselight.sol +++ b/src/mons/iblivion/Baselight.sol @@ -85,7 +85,7 @@ contract Baselight is IAbility, BasicEffect { } // Steps: AfterDamage - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8040; } diff --git a/src/mons/iblivion/Loop.sol b/src/mons/iblivion/Loop.sol index 89ed7510..79ba592e 100644 --- a/src/mons/iblivion/Loop.sol +++ b/src/mons/iblivion/Loop.sol @@ -121,7 +121,7 @@ contract Loop is IMoveSet, BasicEffect { } // ALWAYS_APPLIES | OnMonSwitchOut (bit 5): the marker only listens for switch-out to re-arm - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8020; } diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index d7c94cfc..ccf02689 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -72,7 +72,7 @@ contract ChainExpansion is IMoveSet, BasicEffect { * Effect implementation */ // Steps: OnMonSwitchIn - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8010; } diff --git a/src/mons/inutia/Initialize.sol b/src/mons/inutia/Initialize.sol index 93ea1df8..9ddc4cd8 100644 --- a/src/mons/inutia/Initialize.sol +++ b/src/mons/inutia/Initialize.sol @@ -79,7 +79,7 @@ contract Initialize is IMoveSet, BasicEffect { * Effect implementation */ // Steps: OnMonSwitchIn, OnMonSwitchOut - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8030; } diff --git a/src/mons/inutia/Interweaving.sol b/src/mons/inutia/Interweaving.sol index 83016799..c8673749 100644 --- a/src/mons/inutia/Interweaving.sol +++ b/src/mons/inutia/Interweaving.sol @@ -46,7 +46,7 @@ contract Interweaving is IAbility, BasicEffect { } // Steps: OnApply, OnMonSwitchOut - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8021; } diff --git a/src/mons/malalien/ActusReus.sol b/src/mons/malalien/ActusReus.sol index 4eebafcd..e1a2e67d 100644 --- a/src/mons/malalien/ActusReus.sol +++ b/src/mons/malalien/ActusReus.sol @@ -33,7 +33,7 @@ contract ActusReus is IAbility, BasicEffect { } // Steps: AfterDamage, AfterMove - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x80C0; } diff --git a/src/mons/nirvamma/Adaptor.sol b/src/mons/nirvamma/Adaptor.sol index ae0581c6..c0a8df48 100644 --- a/src/mons/nirvamma/Adaptor.sol +++ b/src/mons/nirvamma/Adaptor.sol @@ -34,7 +34,7 @@ contract Adaptor is IAbility, BasicEffect { } // Steps: AfterDamage, PreDamage, ALWAYS_APPLIES (0x8000) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8240; } diff --git a/src/mons/nirvamma/HardReset.sol b/src/mons/nirvamma/HardReset.sol index 0a613f96..37c049ee 100644 --- a/src/mons/nirvamma/HardReset.sol +++ b/src/mons/nirvamma/HardReset.sol @@ -83,7 +83,7 @@ contract HardReset is IMoveSet, BasicEffect { } // Steps: AfterMove, ALWAYS_APPLIES (0x8000) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8080; } diff --git a/src/mons/pengym/PostWorkout.sol b/src/mons/pengym/PostWorkout.sol index 773180ca..7618de61 100644 --- a/src/mons/pengym/PostWorkout.sol +++ b/src/mons/pengym/PostWorkout.sol @@ -28,7 +28,7 @@ contract PostWorkout is IAbility, BasicEffect { } // Steps: OnMonSwitchOut - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8020; } diff --git a/src/mons/sofabbi/CarrotHarvest.sol b/src/mons/sofabbi/CarrotHarvest.sol index 182d14ec..a38fc602 100644 --- a/src/mons/sofabbi/CarrotHarvest.sol +++ b/src/mons/sofabbi/CarrotHarvest.sol @@ -35,7 +35,7 @@ contract CarrotHarvest is IAbility, BasicEffect { } // Steps: RoundEnd - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8004; } diff --git a/src/mons/xmon/Dreamcatcher.sol b/src/mons/xmon/Dreamcatcher.sol index 0bad683f..ebb56d1b 100644 --- a/src/mons/xmon/Dreamcatcher.sol +++ b/src/mons/xmon/Dreamcatcher.sol @@ -31,7 +31,7 @@ contract Dreamcatcher is IAbility, BasicEffect { } // Steps: OnUpdateMonState - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8100; } diff --git a/src/mons/xmon/InvokeTaboo.sol b/src/mons/xmon/InvokeTaboo.sol index 73fdad04..8e5d5248 100644 --- a/src/mons/xmon/InvokeTaboo.sol +++ b/src/mons/xmon/InvokeTaboo.sol @@ -75,7 +75,7 @@ contract InvokeTaboo is IMoveSet, BasicEffect { } // Steps: OnMonSwitchOut (0x20), AfterMove (0x80), ALWAYS_APPLIES (0x8000) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return ALWAYS_APPLIES_BIT | 0x00A0; } diff --git a/src/mons/xmon/OldVengeance.sol b/src/mons/xmon/OldVengeance.sol index 29159cd4..64794dba 100644 --- a/src/mons/xmon/OldVengeance.sol +++ b/src/mons/xmon/OldVengeance.sol @@ -96,7 +96,7 @@ contract OldVengeance is IMoveSet, BasicEffect { } // Steps: RoundEnd (0x04), OnMonSwitchOut (0x20), ALWAYS_APPLIES (0x8000) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8024; } diff --git a/src/mons/xmon/Somniphobia.sol b/src/mons/xmon/Somniphobia.sol index e5f47e43..914b9298 100644 --- a/src/mons/xmon/Somniphobia.sol +++ b/src/mons/xmon/Somniphobia.sol @@ -85,7 +85,7 @@ contract Somniphobia is IMoveSet, BasicEffect { } // Steps: RoundEnd (0x04), OnMonSwitchIn (0x10), OnMonSwitchOut (0x20), OnUpdateMonState (0x100), ALWAYS_APPLIES (0x8000) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return ALWAYS_APPLIES_BIT | 0x0134; } diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 2f08584d..1d893068 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -45,7 +45,10 @@ import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; import {DummyStatus} from "./mocks/DummyStatus.sol"; import {EditEffectAttack} from "./mocks/EditEffectAttack.sol"; +import {SetEffectDataAttack} from "./mocks/SetEffectDataAttack.sol"; import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; +import {WideDataEffect} from "./mocks/WideDataEffect.sol"; +import {FreshMoveContextAbility} from "./mocks/FreshMoveContextAbility.sol"; contract EngineTest is Test, BattleHelper { DefaultCommitManager commitManager; @@ -129,6 +132,29 @@ contract EngineTest is Test, BattleHelper { assertEq(state.turnId, 2); } + function test_afterMoveContextIsFreshAfterEarlierEffectRewrite() public { + FreshMoveContextAbility observer = new FreshMoveContextAbility(); + Mon memory aliceMon = dummyMon; + aliceMon.ability = uint256(uint160(address(observer))); + + Mon[] memory aliceTeam = new Mon[](1); + Mon[] memory bobTeam = new Mon[](1); + aliceTeam[0] = aliceMon; + bobTeam[0] = dummyMon; + defaultRegistry.setTeam(ALICE, aliceTeam); + defaultRegistry.setTeam(BOB, bobTeam); + + bytes32 battleKey = _startBattle(engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); + _commitRevealExecuteForAliceAndBob( + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) + ); + + // Alice authors move 0. The first AfterMove effect rewrites it to rest; the context-aware + // observer runs second and must see the rewritten lane, not a pass-level snapshot. + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); + assertEq(observer.observedMoveIndex(), NO_OP_MOVE_INDEX, "context must reflect prior hook rewrite"); + } + function test_fasterSpeedKOsGameOver() public { // Initialize mons IMoveSet normalAttack = new CustomAttack( @@ -2766,6 +2792,48 @@ contract EngineTest is Test, BattleHelper { assertEq(effects[0].data, bytes32(uint256(69))); } + function test_effectDataCompactAndWideFallbackTransitions() public { + WideDataEffect wideEffect = new WideDataEffect(); + // Exact representation boundary: max-1 is offset-tagged; max falls back to slot 1. + uint256 compactMarkerMax = (uint256(1) << 79) - 1; + bytes32 compactData = bytes32(compactMarkerMax - 1); + bytes32 secondWideData = bytes32(compactMarkerMax); + SetEffectDataAttack setCompact = new SetEffectDataAttack(compactData); + SetEffectDataAttack setWide = new SetEffectDataAttack(secondWideData); + + Mon memory mon = _createMon(); + mon.ability = uint160(address(new EffectAbility(wideEffect))); + mon.moves = new uint256[](2); + mon.moves[0] = uint256(uint160(address(setCompact))); + mon.moves[1] = uint256(uint160(address(setWide))); + Mon[] memory team = new Mon[](1); + team[0] = mon; + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + bytes32 battleKey = _startBattle(engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); + + _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, 0); + (EffectInstance[] memory effects, uint256[] memory indices) = engine.getEffects(battleKey, 1, 0); + assertEq(effects[0].data, wideEffect.INITIAL_DATA()); + (bool exists,, bytes32 targetedData) = engine.getEffectData(battleKey, 1, 0, address(wideEffect)); + assertTrue(exists); + assertEq(targetedData, wideEffect.INITIAL_DATA()); + + uint16 editExtraData = uint16(uint256(1) | (indices[0] << 2)); + _commitRevealExecuteForAliceAndBob(battleKey, 0, NO_OP_MOVE_INDEX, editExtraData, 0); + (effects,) = engine.getEffects(battleKey, 1, 0); + assertEq(effects[0].data, compactData); + (BattleConfigView memory battleView,) = engine.getBattle(battleKey); + assertEq(battleView.p1Effects[0][0].data, compactData); + + _commitRevealExecuteForAliceAndBob(battleKey, 1, NO_OP_MOVE_INDEX, editExtraData, 0); + (effects,) = engine.getEffects(battleKey, 1, 0); + assertEq(effects[0].data, secondWideData); + (exists,, targetedData) = engine.getEffectData(battleKey, 1, 0, address(wideEffect)); + assertTrue(exists); + assertEq(targetedData, secondWideData); + } + // Regression: a KO delivered by an effect's OnUpdateMonState -> dealDamage hook, fired off the // inline round-end stamina regen (INLINE_STAMINA_REGEN_RULESET is production's default path), must // still set playerSwitchForTurnFlag for the next turn. _handleMove / _handleEffects recompute the diff --git a/test/effects/PreDamageHookTest.sol b/test/effects/PreDamageHookTest.sol index 046e1616..391de9a1 100644 --- a/test/effects/PreDamageHookTest.sol +++ b/test/effects/PreDamageHookTest.sol @@ -25,7 +25,7 @@ import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; // PreDamage that halves the running damage. contract PreDamageHalveEffect is BasicEffect { - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x200; // PreDamage } @@ -41,7 +41,7 @@ contract PreDamageHalveEffect is BasicEffect { // PreDamage that fully absorbs damage (sets running to 0). contract PreDamageAbsorbEffect is BasicEffect { - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x200; } @@ -57,7 +57,7 @@ contract PreDamageAbsorbEffect is BasicEffect { // PreDamage that doubles the running damage. contract PreDamageDoubleEffect is BasicEffect { - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x200; } @@ -80,7 +80,7 @@ contract SourceCaptureEffect is BasicEffect { uint256 public preDamageCallCount; uint256 public afterDamageCallCount; - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x240; // PreDamage | AfterDamage } diff --git a/test/mocks/AfterDamageReboundEffect.sol b/test/mocks/AfterDamageReboundEffect.sol index fd0767d1..17407c66 100644 --- a/test/mocks/AfterDamageReboundEffect.sol +++ b/test/mocks/AfterDamageReboundEffect.sol @@ -10,7 +10,7 @@ import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract AfterDamageReboundEffect is BasicEffect { // Steps: AfterDamage - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x40; } diff --git a/test/mocks/AlwaysRejectsEffect.sol b/test/mocks/AlwaysRejectsEffect.sol index 3adb1bd7..44a78bd4 100644 --- a/test/mocks/AlwaysRejectsEffect.sol +++ b/test/mocks/AlwaysRejectsEffect.sol @@ -16,7 +16,7 @@ contract AlwaysRejectsEffect is BasicEffect { } // ALWAYS_APPLIES_BIT (0x8000) | RoundEnd (0x04) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8004; } diff --git a/test/mocks/DummyStatus.sol b/test/mocks/DummyStatus.sol index f13b5515..ee618b70 100644 --- a/test/mocks/DummyStatus.sol +++ b/test/mocks/DummyStatus.sol @@ -11,7 +11,7 @@ contract DummyStatus is BasicEffect { } // Steps: None - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x00; } } diff --git a/test/mocks/FreshMoveContextAbility.sol b/test/mocks/FreshMoveContextAbility.sol new file mode 100644 index 00000000..4ea4d559 --- /dev/null +++ b/test/mocks/FreshMoveContextAbility.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {MOVE_INDEX_MASK, NO_OP_MOVE_INDEX, NO_SLOT} from "../../src/Constants.sol"; +import {IAbility} from "../../src/abilities/IAbility.sol"; +import {BasicEffect} from "../../src/effects/BasicEffect.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {IEngine} from "../../src/IEngine.sol"; +import {TargetLib} from "../../src/lib/TargetLib.sol"; + +contract RewriteMoveAfterMove is BasicEffect { + function getStepsBitmap() external pure override returns (uint32) { + return 0x8080; + } + + function onAfterMove( + IEngine engine, + bytes32 battleKey, + uint256, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 activesPacked + ) external override returns (bytes32, bool) { + uint256 ownSlot = TargetLib.slotOfMon(activesPacked, targetIndex, monIndex); + if (ownSlot != NO_SLOT) { + engine.setMoveForSlot(battleKey, targetIndex, ownSlot & 1, NO_OP_MOVE_INDEX, 0); + } + return (extraData, false); + } +} + +contract FreshMoveContextAbility is IAbility, BasicEffect { + IEffect private immutable REWRITER; + uint8 public observedMoveIndex; + + constructor() { + REWRITER = IEffect(address(new RewriteMoveAfterMove())); + } + + function name() public pure override(IAbility, BasicEffect) returns (string memory) { + return "Fresh Move Context"; + } + + function activateOnSwitch(IEngine engine, bytes32, uint256 playerIndex, uint256 monIndex) external override { + // Stable effect-array order: the rewriter runs before this observer. + engine.addEffect(playerIndex, monIndex, REWRITER, bytes32(0)); + engine.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); + } + + function getStepsBitmap() external pure override returns (uint32) { + return 0x00808080; // AfterMove context | ALWAYS_APPLIES | AfterMove + } + + function onAfterMove(IEngine, bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256 context) + external + override + returns (bytes32, bool) + { + uint256 ownSlot = TargetLib.slotOfMon(context, targetIndex, monIndex); + observedMoveIndex = uint8(TargetLib.hookMoveWordAt(context, ownSlot)) & MOVE_INDEX_MASK; + return (extraData, false); + } +} diff --git a/test/mocks/InstantDeathEffect.sol b/test/mocks/InstantDeathEffect.sol index 7e3a3f92..07fa8d52 100644 --- a/test/mocks/InstantDeathEffect.sol +++ b/test/mocks/InstantDeathEffect.sol @@ -14,7 +14,7 @@ contract InstantDeathEffect is BasicEffect { } // Steps: RoundEnd - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x04; } diff --git a/test/mocks/InstantDeathOnSwitchInEffect.sol b/test/mocks/InstantDeathOnSwitchInEffect.sol index 2d5bf221..dedf8830 100644 --- a/test/mocks/InstantDeathOnSwitchInEffect.sol +++ b/test/mocks/InstantDeathOnSwitchInEffect.sol @@ -14,7 +14,7 @@ contract InstantDeathOnSwitchInEffect is BasicEffect { } // Steps: OnMonSwitchIn - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x10; } diff --git a/test/mocks/MockSingletonAbility.sol b/test/mocks/MockSingletonAbility.sol index 0aaba565..730ac840 100644 --- a/test/mocks/MockSingletonAbility.sol +++ b/test/mocks/MockSingletonAbility.sol @@ -26,7 +26,7 @@ contract MockSingletonAbility is IAbility, BasicEffect { } // Steps: AfterDamage (just to verify hooks run) - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x8040; // ALWAYS_APPLIES | AfterDamage } diff --git a/test/mocks/NeverAppliesEffect.sol b/test/mocks/NeverAppliesEffect.sol index 4d7469f7..8b5d286d 100644 --- a/test/mocks/NeverAppliesEffect.sol +++ b/test/mocks/NeverAppliesEffect.sol @@ -15,7 +15,7 @@ contract NeverAppliesEffect is BasicEffect { } // RoundEnd (0x04), NO ALWAYS_APPLIES_BIT - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x04; } diff --git a/test/mocks/OnUpdateMonStateHealEffect.sol b/test/mocks/OnUpdateMonStateHealEffect.sol index c562c9d2..7beb117e 100644 --- a/test/mocks/OnUpdateMonStateHealEffect.sol +++ b/test/mocks/OnUpdateMonStateHealEffect.sol @@ -17,7 +17,7 @@ contract OnUpdateMonStateHealEffect is BasicEffect { int32 public constant HEAL_AMOUNT = 5; // Steps: OnUpdateMonState - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x100; } diff --git a/test/mocks/OneTurnStatBoost.sol b/test/mocks/OneTurnStatBoost.sol index 508abcad..f9c446de 100644 --- a/test/mocks/OneTurnStatBoost.sol +++ b/test/mocks/OneTurnStatBoost.sol @@ -14,7 +14,7 @@ contract OneTurnStatBoost is BasicEffect { } // Steps: OnApply, RoundEnd - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x05; } diff --git a/test/mocks/OrderRecorderAbility.sol b/test/mocks/OrderRecorderAbility.sol index e7c39248..64a38a70 100644 --- a/test/mocks/OrderRecorderAbility.sol +++ b/test/mocks/OrderRecorderAbility.sol @@ -27,7 +27,7 @@ contract OrderRecorderAbility is IAbility, BasicEffect { } } - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return uint16( ALWAYS_APPLIES_BIT | (1 << uint8(EffectStep.RoundStart)) | (1 << uint8(EffectStep.RoundEnd)) | (1 << uint8(EffectStep.AfterMove)) diff --git a/test/mocks/SetEffectDataAttack.sol b/test/mocks/SetEffectDataAttack.sol new file mode 100644 index 00000000..cb212376 --- /dev/null +++ b/test/mocks/SetEffectDataAttack.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {MoveClass, Type} from "../../src/Enums.sol"; +import {IEngine} from "../../src/IEngine.sol"; +import {MoveMeta} from "../../src/Structs.sol"; +import {IMoveSet} from "../../src/moves/IMoveSet.sol"; + +contract SetEffectDataAttack is IMoveSet { + bytes32 immutable DATA; + + constructor(bytes32 data) { + DATA = data; + } + + function name() external pure returns (string memory) { + return "Set Effect Data"; + } + + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint256, uint16 extraData, uint256) external { + uint256 targetIndex = uint256(extraData) & 0x3; + uint256 effectIndex = uint256(extraData) >> 2; + engine.editEffect(targetIndex, effectIndex, DATA); + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return 1; + } + + function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { + return 0; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Fire; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Physical; + } + + function basePower(bytes32) external pure returns (uint32) { + return 0; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + pure + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: 0 + }); + } +} diff --git a/test/mocks/SingleInstanceEffect.sol b/test/mocks/SingleInstanceEffect.sol index 06103886..8798c7f3 100644 --- a/test/mocks/SingleInstanceEffect.sol +++ b/test/mocks/SingleInstanceEffect.sol @@ -14,7 +14,7 @@ contract SingleInstanceEffect is BasicEffect { } // Steps: OnApply - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x01; } diff --git a/test/mocks/SpAtkDebuffEffect.sol b/test/mocks/SpAtkDebuffEffect.sol index 0615eb0e..bf731c05 100644 --- a/test/mocks/SpAtkDebuffEffect.sol +++ b/test/mocks/SpAtkDebuffEffect.sol @@ -19,7 +19,7 @@ contract SpAtkDebuffEffect is StatusEffect { } // Steps: OnApply, OnRemove - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x09 | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } diff --git a/test/mocks/StaminaGainKOEffect.sol b/test/mocks/StaminaGainKOEffect.sol index 00f06782..93227664 100644 --- a/test/mocks/StaminaGainKOEffect.sol +++ b/test/mocks/StaminaGainKOEffect.sol @@ -17,7 +17,7 @@ contract StaminaGainKOEffect is BasicEffect { } // Steps: OnUpdateMonState - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x100; } diff --git a/test/mocks/TempStatBoostEffect.sol b/test/mocks/TempStatBoostEffect.sol index 6464ab24..31fe6968 100644 --- a/test/mocks/TempStatBoostEffect.sol +++ b/test/mocks/TempStatBoostEffect.sol @@ -14,7 +14,7 @@ contract TempStatBoostEffect is BasicEffect { } // Steps: OnApply - function getStepsBitmap() external pure override returns (uint16) { + function getStepsBitmap() external pure override returns (uint32) { return 0x01; } diff --git a/test/mocks/WideDataEffect.sol b/test/mocks/WideDataEffect.sol new file mode 100644 index 00000000..a09188e9 --- /dev/null +++ b/test/mocks/WideDataEffect.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {ALWAYS_APPLIES_BIT} from "../../src/Constants.sol"; +import {IEngine} from "../../src/IEngine.sol"; +import {BasicEffect} from "../../src/effects/BasicEffect.sol"; + +contract WideDataEffect is BasicEffect { + bytes32 public constant INITIAL_DATA = bytes32((uint256(1) << 200) | 123); + + function name() external pure override returns (string memory) { + return "Wide Data"; + } + + function getStepsBitmap() external pure override returns (uint32) { + return uint16(1) | ALWAYS_APPLIES_BIT; + } + + function onApply(IEngine, bytes32, uint256, bytes32, uint256, uint256, uint256) + external + pure + override + returns (bytes32 updatedExtraData, bool removeAfterRun) + { + return (INITIAL_DATA, false); + } +} From bae7fb38808f3be2505fc83f0f4ce5b21f491a88 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:52:05 -0700 Subject: [PATCH 16/41] migrate moves to optimized access patterns --- CLAUDE.md | 31 ++++++++++++- snapshots/MainnetReplayGasTest.json | 6 +-- src/Engine.sol | 45 ++++++++++++------- src/effects/IEffect.sol | 13 ++++-- src/lib/TargetLib.sol | 19 ++++++++ src/mons/embursa/Tinderclaws.sol | 18 +++----- src/mons/nirvamma/HardReset.sol | 11 ++--- src/mons/xmon/Dreamcatcher.sol | 11 ++--- src/mons/xmon/InvokeTaboo.sol | 12 ++--- src/mons/xmon/Somniphobia.sol | 8 ++-- test/EngineTest.sol | 32 +++++++++++++- test/mocks/FreshMoveContextAbility.sol | 61 ++++++++++++++++++++++++++ 12 files changed, 210 insertions(+), 57 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ce55d179..0a027fb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -305,6 +305,35 @@ into the Engine. Effects can be per-mon (local; `targetIndex` = the owning player index) or global (battlefield-wide; side index `2`). The `StaminaRegen` effect is a global default that regenerates 1 stamina per turn. +#### Opt-in fresh hook context (gas path) + +Before adding an Engine getter inside a lifecycle hook, check whether the value is already available +in that hook's packed context. `getStepsBitmap()` returns `uint32`: the low 16 bits are lifecycle +subscriptions and the corresponding high 16 bits request fresh context. For example, an +`AfterMove` listener (`0x80`) requests context with `0x0080_0000`, so a typical bitmap is +`0x0080_8080` (`AfterMove` context + `ALWAYS_APPLIES_BIT` + `AfterMove`). Legacy effects that only +return low bits remain compatible. + +Supported layouts, decoded through `TargetLib`: + +- `AfterMove`: current 24-bit move words via `hookMoveWordAt(context, absSlot)` and acted bits via + `hookSlotActed(context, absSlot)`. +- `RoundEnd`: current status classes via `hookStatusClass(context, side, monIndex)`. +- `OnUpdateMonState`: the changed mon's max HP and normalized signed HP delta via + `hookMonMaxHp(context)` / `hookMonHpDelta(context)`. + +Snapshots are built immediately before each requesting callback, after earlier effects in the same +pass, so hooks must not cache them across calls. Context replaces read-only callbacks; mutations +still go through Engine functions. Prefer it when a hook would otherwise call an Engine getter, and +add a mutation-order regression when an earlier hook can change the observed value. + +The stored effect header currently collapses any nonzero high-half request to one capability bit. +Consequently, a multi-hook effect receives every context supported for each hook it subscribes to, +even if it requested only one step. Benchmark multi-hook migrations: an unused builder at a frequent +hook can outweigh the removed callback. Ordinary `IMoveSet.move()` execution does not yet receive +these lifecycle layouts; only moves/abilities that also register as effects can use them in their +effect hooks. Do not add address-specific Engine dispatch to work around that boundary. + ### Stat Boosts (inlined into the Engine) Stat modifiers are **not** a separate effect contract — they are native Engine functions: @@ -545,7 +574,7 @@ Effects fall into several categories depending on scope: To implement a new effect: 1. Extend `BasicEffect` (or `StatusEffect` for status conditions) 2. Override the relevant lifecycle hooks (`onRoundEnd`, `onAfterDamage`, `onPreDamage`, …) — each receives `activesPacked` for local slot resolution via `TargetLib` -3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call — if the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `SleepStatus`'s single-sleeper rule) must NOT set this bit. Statuses additionally carry their class id in bits 10-13 and `HAS_REAPPLY_BIT` (bit 14) iff they implement `onReapply`. `python processing/validateEffectBitmaps.py` checks all of these invariants (bit pairing, class range 1-14, cross-src uniqueness) and is run as part of `deploy.py`. +3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call. Before adding callback getters, opt into a supported fresh context by mirroring the applicable low lifecycle bit into bits 16-31 as described above. If the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the low bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `SleepStatus`'s single-sleeper rule) must NOT set this bit. Statuses additionally carry their class id in bits 10-13 and `HAS_REAPPLY_BIT` (bit 14) iff they implement `onReapply`. `python processing/validateEffectBitmaps.py` checks all of these invariants (bit pairing, class range 1-14, cross-src uniqueness) and is run as part of `deploy.py`. 4. Return `(updatedExtraData, removeAfterRun)` from hooks — use `extraData` (bytes32) to carry state between turns (counters, degrees, flags) 5. Shared effects are injected into moves/abilities via constructor parameters at deploy time — there is no runtime effect registry diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json index 66a81c59..ad3487e8 100644 --- a/snapshots/MainnetReplayGasTest.json +++ b/snapshots/MainnetReplayGasTest.json @@ -5,7 +5,7 @@ "MainnetDoublesReplay1_Reused_effectDataWrites": "0", "MainnetDoublesReplay1_Reused_effectHeaderReads": "45", "MainnetDoublesReplay1_Reused_effectHeaderWrites": "14", - "MainnetDoublesReplay1_Reused_execGas": "1220542", + "MainnetDoublesReplay1_Reused_execGas": "1213930", "MainnetDoublesReplay1_Reused_lifecycleCalls": "34", "MainnetDoublesReplay1_Reused_moveDecisionCallbacks": "0", "MainnetDoublesReplay1_Reused_nonHookHeaderReadUpperBound": "11", @@ -20,13 +20,13 @@ "MainnetReplay_Reused_effectDataWrites": "0", "MainnetReplay_Reused_effectHeaderReads": "191", "MainnetReplay_Reused_effectHeaderWrites": "39", - "MainnetReplay_Reused_execGas": "2198697", + "MainnetReplay_Reused_execGas": "2152334", "MainnetReplay_Reused_lifecycleCalls": "84", "MainnetReplay_Reused_moveDecisionCallbacks": "0", "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "107", "MainnetReplay_Reused_preDamageCalls": "0", "MainnetReplay_Reused_roundEndCalls": "56", "MainnetReplay_Reused_roundStartCalls": "0", - "MainnetReplay_Reused_statusClassCallbacks": "23", + "MainnetReplay_Reused_statusClassCallbacks": "5", "MainnetReplay_Reused_updateStateCalls": "0" } \ No newline at end of file diff --git a/src/Engine.sol b/src/Engine.sol index f0fe367e..1451f87f 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -2058,10 +2058,9 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 private constant FRESH_HOOK_CONTEXT_REQUEST = uint256(1) << 255; /// @dev Effect header layout: address[0:160], steps[160:176], compact-data marker[176:255], - /// fresh-hook-context capability[255]. A nonzero marker stores `data + 1`; zero means the - /// full bytes32 lives in slot 1. Reserving one capability bit leaves 79 compact data bits. - /// A nonzero marker stores `data + 1`; zero means the full bytes32 lives in slot 1. The offset - /// represents zero without giving up a tag bit and leaves arbitrary wide data fully supported. + /// fresh-hook-context capability[255]. A nonzero marker stores `data + 1`; zero means the full + /// bytes32 lives in slot 1. Reserving one capability bit leaves 79 compact data bits. Once opted + /// in, an effect receives whichever supported fresh context corresponds to its current hook. function _effectData(EffectInstance storage effectInstance) private view returns (bytes32 data) { uint256 header; assembly ("memory-safe") { @@ -2106,7 +2105,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 raw = uint256(data); uint256 encoded = raw < EFFECT_COMPACT_DATA_MASK ? raw + 1 : 0; uint256 header = uint256(uint160(address(effect))) | (uint256(stepsBitmap) << 160) | (encoded << 176) - | (hookContextBitmap & (1 << uint8(EffectStep.AfterMove)) != 0 ? EFFECT_HOOK_CONTEXT_FLAG : 0); + | (hookContextBitmap != 0 ? EFFECT_HOOK_CONTEXT_FLAG : 0); assembly ("memory-safe") { sstore(effectInstance.slot, header) } @@ -2126,7 +2125,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } uint256 encoded = (header >> 176) & EFFECT_COMPACT_DATA_MASK; data = encoded == 0 ? effectInstance.data : bytes32(encoded - 1); - needsFreshContext = (header & EFFECT_HOOK_CONTEXT_FLAG) != 0; + needsFreshContext = header & EFFECT_HOOK_CONTEXT_FLAG != 0; } function _monEffectSteps(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) @@ -3533,8 +3532,16 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (address(eff.effect) != TOMBSTONE_ADDRESS && (eff.stepsBitmap & (1 << uint8(round))) != 0) { (bytes32 effectData, bool needsFreshContext) = _effectDataAndHookContext(eff); uint256 hookContext = activesPacked; - if (round == EffectStep.AfterMove && needsFreshContext) { - hookContext |= FRESH_HOOK_CONTEXT_REQUEST; + if (needsFreshContext) { + if (round == EffectStep.RoundEnd) { + // Built immediately before this effect call, after every earlier effect in + // the same pass, so status mutations cannot stale the snapshot. + hookContext |= uint256(config.monStatusLanes) << 132; + } else if (round == EffectStep.AfterMove || round == EffectStep.OnUpdateMonState) { + // These hook-specific builders need data resolved at dispatch time. Keep + // the request out of the public low-32 active lanes until then. + hookContext |= FRESH_HOOK_CONTEXT_REQUEST; + } } _runSingleEffect( config, @@ -3545,7 +3552,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { round, extraEffectsData, eff.effect, - eff.stepsBitmap, effectData, uint96(slotIndex), hookContext @@ -3573,16 +3579,10 @@ contract Engine is IEngine, MappingAllocator, EIP712 { EffectStep round, bytes memory extraEffectsData, IEffect effect, - uint16 stepsBitmap, bytes32 data, uint96 slotIndex, uint256 activesPacked ) private { - // Use stored bitmap instead of external call to shouldRunAtStep() - if ((stepsBitmap & (1 << uint8(round))) == 0) { - return; - } - // Run the effect and get result (bytes32 updatedExtraData, bool removeAfterRun) = _executeEffectHook( battleKeyForWrite, @@ -3655,6 +3655,21 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } else if (round == EffectStep.OnUpdateMonState) { (uint256 statePlayerIndex, uint256 stateMonIndex, MonStateIndexName stateVarIndex, int32 valueToAdd) = abi.decode(extraEffectsData, (uint256, uint256, MonStateIndexName, int32)); + // The state mutation has already landed before this lifecycle pass. Build the snapshot + // immediately before each opted-in callback so it observes nested/prior hook writes. + // Hook-specific context can reuse the AfterMove lanes: max HP is bits 32..63 and the + // normalized signed HP delta is bits 64..95. + if (activesPacked & FRESH_HOOK_CONTEXT_REQUEST != 0) { + activesPacked &= ~FRESH_HOOK_CONTEXT_REQUEST; + BattleConfig storage config = battleConfig[storageKeyForWrite]; + uint32 maxHp = _getTeamMon(config, statePlayerIndex, stateMonIndex).stats.hp; + int32 hpDelta = _getMonState(config, statePlayerIndex, stateMonIndex).hpDelta; + if (hpDelta == CLEARED_MON_STATE_SENTINEL) { + hpDelta = 0; + } + activesPacked |= uint256(maxHp) << 32; + activesPacked |= uint256(uint32(hpDelta)) << 64; + } return effect.onUpdateMonState( self, battleKey, rng, data, statePlayerIndex, stateMonIndex, activesPacked, stateVarIndex, valueToAdd ); diff --git a/src/effects/IEffect.sol b/src/effects/IEffect.sol index 31688029..1fab611d 100644 --- a/src/effects/IEffect.sol +++ b/src/effects/IEffect.sol @@ -9,7 +9,9 @@ interface IEffect { function name() external returns (string memory); // Returns static metadata: lifecycle steps in bits 0-15 and requested fresh-context steps in - // bits 16-31. Lifecycle layout: OnApply=0x01, RoundStart=0x02, RoundEnd=0x04, + // bits 16-31. Storage collapses any nonzero context half to one capability bit; opted-in effects + // receive all context types supported for the hook currently running. Lifecycle layout: + // OnApply=0x01, RoundStart=0x02, RoundEnd=0x04, // OnRemove=0x08, OnMonSwitchIn=0x10, OnMonSwitchOut=0x20, AfterDamage=0x40, // AfterMove=0x80, OnUpdateMonState=0x100, PreDamage=0x200. Legacy deployed effects returning // only the low uint16 remain EVM-compatible and simply request no fresh context. @@ -22,9 +24,12 @@ interface IEffect { // Lifecycle hooks during normal battle flow. // `activesPacked` carries every slot's active roster index in its low 32 bits (one 8-bit lane - // per absolute slot, EMPTY_ACTIVE_LANE = no mon there). For an opted-in AfterMove hook, Engine - // also embeds fresh 24-bit move lanes at bits 32..127 and the acted-slot mask at bits 128..131. - // Existing effects remain compatible; opt-in readers decode the high context via TargetLib. + // per absolute slot, EMPTY_ACTIVE_LANE = no mon there). Opted-in AfterMove hooks also receive + // fresh 24-bit move lanes at bits 32..127 and the acted-slot mask at bits 128..131. Opted-in + // RoundEnd hooks receive fresh 4-bit status lanes at bits 132..195. Opted-in OnUpdateMonState + // hooks receive target max HP at bits 32..63 and normalized signed HP delta at bits 64..95. + // Context layouts are hook-specific; existing effects remain compatible and opt-in readers + // decode the high context via TargetLib. function onRoundStart( IEngine engine, bytes32 battleKey, diff --git a/src/lib/TargetLib.sol b/src/lib/TargetLib.sol index 28f617d0..cd096444 100644 --- a/src/lib/TargetLib.sol +++ b/src/lib/TargetLib.sol @@ -10,6 +10,9 @@ import "../Constants.sol"; library TargetLib { uint256 internal constant HOOK_MOVE_LANES_SHIFT = 32; uint256 internal constant HOOK_ACTED_MASK_SHIFT = 128; + uint256 internal constant HOOK_STATUS_LANES_SHIFT = 132; + uint256 internal constant HOOK_MON_MAX_HP_SHIFT = 32; + uint256 internal constant HOOK_MON_HP_DELTA_SHIFT = 64; function sideOf(uint256 absSlot) internal pure returns (uint256) { return absSlot >> 1; @@ -71,6 +74,22 @@ library TargetLib { return ((hookContext >> (HOOK_ACTED_MASK_SHIFT + absSlot)) & 1) != 0; } + /// @notice Fresh 4-bit exclusive-status class for any roster mon, embedded immediately + /// before an opted-in lifecycle callback. Lanes use side*8+monIndex, matching Engine. + function hookStatusClass(uint256 hookContext, uint256 side, uint256 monIndex) internal pure returns (uint256) { + return (hookContext >> (HOOK_STATUS_LANES_SHIFT + (((side << 3) | monIndex) << 2))) & 0xF; + } + + /// @notice Fresh target-mon state embedded for an opted-in OnUpdateMonState callback. Context + /// layouts are hook-specific, so these lanes intentionally overlap AfterMove's moves. + function hookMonMaxHp(uint256 hookContext) internal pure returns (uint32) { + return uint32(hookContext >> HOOK_MON_MAX_HP_SHIFT); + } + + function hookMonHpDelta(uint256 hookContext) internal pure returns (int32) { + return int32(uint32(hookContext >> HOOK_MON_HP_DELTA_SHIFT)); + } + /// @dev Kit-audit ruling for untargeted "the opposing active" effects (switch-in chips, /// KO-triggered debuffs...): the mirror of `ownSlot` on the opposing side, falling back /// to its partner when the mirror lane is empty; NO_SLOT when the opposing side is vacant. diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index 707792a8..8ce091de 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -49,13 +49,13 @@ contract Tinderclaws is IAbility, BasicEffect { // Steps: RoundEnd, AfterMove function getStepsBitmap() external pure override returns (uint32) { // Low 16 bits: lifecycle steps. High 16 bits: requested fresh-context steps. - return 0x00808084; // AfterMove context | ALWAYS_APPLIES | AfterMove | RoundEnd + return 0x00848084; // RoundEnd+AfterMove context | ALWAYS_APPLIES | AfterMove | RoundEnd } // extraData: 0 = no SpATK boost applied, 1 = SpATK boost applied function onAfterMove( IEngine engine, - bytes32 battleKey, + bytes32, uint256 rng, bytes32 extraData, uint256 targetIndex, @@ -92,14 +92,14 @@ contract Tinderclaws is IAbility, BasicEffect { function onRoundEnd( IEngine engine, - bytes32 battleKey, + bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, - uint256 + uint256 hookContext ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bool isBurned = _isBurned(engine, battleKey, targetIndex, monIndex); + bool isBurned = TargetLib.hookStatusClass(hookContext, targetIndex, monIndex) == BURN_CLASS; bool hasBoost = uint256(extraData) == 1; if (isBurned && !hasBoost) { @@ -120,12 +120,4 @@ contract Tinderclaws is IAbility, BasicEffect { return (extraData, false); } - - function _isBurned(IEngine engine, bytes32 battleKey, uint256 targetIndex, uint256 monIndex) - internal - view - returns (bool) - { - return engine.getMonStatusClass(battleKey, targetIndex, monIndex) == BURN_CLASS; - } } diff --git a/src/mons/nirvamma/HardReset.sol b/src/mons/nirvamma/HardReset.sol index 37c049ee..deb4ebd0 100644 --- a/src/mons/nirvamma/HardReset.sol +++ b/src/mons/nirvamma/HardReset.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {DEFAULT_PRIORITY, MOVE_INDEX_MASK, NO_OP_MOVE_INDEX, NO_SLOT} from "../../Constants.sol"; import {MonStateIndexName, MoveClass, Type} from "../../Enums.sol"; -import {EffectInstance, MoveDecision, MoveMeta} from "../../Structs.sol"; +import {EffectInstance, MoveMeta} from "../../Structs.sol"; import {IEngine} from "../../IEngine.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; @@ -82,9 +82,10 @@ contract HardReset is IMoveSet, BasicEffect { }); } - // Steps: AfterMove, ALWAYS_APPLIES (0x8000) + // Steps: AfterMove, ALWAYS_APPLIES (0x8000). Request fresh AfterMove context in the + // high 16-bit metadata lane so rest detection does not re-enter Engine. function getStepsBitmap() external pure override returns (uint32) { - return 0x8080; + return 0x00808080; } function onAfterMove( @@ -100,8 +101,8 @@ contract HardReset is IMoveSet, BasicEffect { if (restSlot == NO_SLOT) { return (extraData, false); } - MoveDecision memory dec = engine.getMoveDecisionForSlot(battleKey, targetIndex, restSlot & 1); - if ((dec.packedMoveIndex & MOVE_INDEX_MASK) != NO_OP_MOVE_INDEX) { + uint8 moveIndex = uint8(TargetLib.hookMoveWordAt(activesPacked, restSlot)) & MOVE_INDEX_MASK; + if (moveIndex != NO_OP_MOVE_INDEX) { return (extraData, false); } diff --git a/src/mons/xmon/Dreamcatcher.sol b/src/mons/xmon/Dreamcatcher.sol index ebb56d1b..d0087fdf 100644 --- a/src/mons/xmon/Dreamcatcher.sol +++ b/src/mons/xmon/Dreamcatcher.sol @@ -11,6 +11,7 @@ import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; contract Dreamcatcher is IAbility, BasicEffect { int32 public constant HEAL_DENOM = 16; @@ -32,27 +33,27 @@ contract Dreamcatcher is IAbility, BasicEffect { // Steps: OnUpdateMonState function getStepsBitmap() external pure override returns (uint32) { - return 0x8100; + return 0x01008100; // OnUpdateMonState context | ALWAYS_APPLIES | OnUpdateMonState } function onUpdateMonState( IEngine engine, - bytes32 battleKey, + bytes32, uint256, bytes32 extraData, uint256 playerIndex, uint256 monIndex, - uint256, + uint256 context, MonStateIndexName stateVarIndex, int32 valueToAdd ) external override returns (bytes32, bool) { // Only trigger if Stamina is being increased (positive valueToAdd) if (stateVarIndex == MonStateIndexName.Stamina && valueToAdd > 0) { - uint32 maxHp = engine.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + uint32 maxHp = TargetLib.hookMonMaxHp(context); int32 healAmount = int32(uint32(maxHp)) / HEAL_DENOM; // Prevent overhealing - int32 existingHpDelta = engine.getMonStateForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + int32 existingHpDelta = TargetLib.hookMonHpDelta(context); if (existingHpDelta + healAmount > 0) { healAmount = 0 - existingHpDelta; } diff --git a/src/mons/xmon/InvokeTaboo.sol b/src/mons/xmon/InvokeTaboo.sol index 8e5d5248..9f68dd5b 100644 --- a/src/mons/xmon/InvokeTaboo.sol +++ b/src/mons/xmon/InvokeTaboo.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import {ALWAYS_APPLIES_BIT, DEFAULT_PRIORITY, MOVE_INDEX_MASK, NO_SLOT, SWITCH_MOVE_INDEX} from "../../Constants.sol"; +import {DEFAULT_PRIORITY, MOVE_INDEX_MASK, NO_SLOT, SWITCH_MOVE_INDEX} from "../../Constants.sol"; import {MoveClass, Type} from "../../Enums.sol"; import {MoveDecision, MoveMeta} from "../../Structs.sol"; @@ -74,14 +74,15 @@ contract InvokeTaboo is IMoveSet, BasicEffect { return MoveClass.Other; } - // Steps: OnMonSwitchOut (0x20), AfterMove (0x80), ALWAYS_APPLIES (0x8000) + // Steps: OnMonSwitchOut (0x20), AfterMove (0x80), ALWAYS_APPLIES (0x8000). + // Request fresh AfterMove context in the high 16-bit metadata lane. function getStepsBitmap() external pure override returns (uint32) { - return ALWAYS_APPLIES_BIT | 0x00A0; + return 0x008080A0; } function onAfterMove( IEngine engine, - bytes32 battleKey, + bytes32, uint256, bytes32 extraData, uint256 targetIndex, @@ -92,8 +93,7 @@ contract InvokeTaboo is IMoveSet, BasicEffect { if (ownSlot == NO_SLOT) { return (extraData, false); } - MoveDecision memory moveDecision = engine.getMoveDecisionForSlot(battleKey, targetIndex, ownSlot & 1); - uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; + uint8 moveIndex = uint8(TargetLib.hookMoveWordAt(activesPacked, ownSlot)) & MOVE_INDEX_MASK; uint8 tabooMoveIndex = uint8(uint256(extraData)); if (moveIndex == tabooMoveIndex) { diff --git a/src/mons/xmon/Somniphobia.sol b/src/mons/xmon/Somniphobia.sol index 914b9298..c6f981c4 100644 --- a/src/mons/xmon/Somniphobia.sol +++ b/src/mons/xmon/Somniphobia.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import {ALWAYS_APPLIES_BIT, DEFAULT_PRIORITY, EMPTY_ACTIVE_LANE} from "../../Constants.sol"; +import {DEFAULT_PRIORITY, EMPTY_ACTIVE_LANE} from "../../Constants.sol"; import {MonStateIndexName, MoveClass, Type} from "../../Enums.sol"; import {MoveMeta} from "../../Structs.sol"; @@ -86,7 +86,7 @@ contract Somniphobia is IMoveSet, BasicEffect { // Steps: RoundEnd (0x04), OnMonSwitchIn (0x10), OnMonSwitchOut (0x20), OnUpdateMonState (0x100), ALWAYS_APPLIES (0x8000) function getStepsBitmap() external pure override returns (uint32) { - return ALWAYS_APPLIES_BIT | 0x0134; + return 0x01008134; // OnUpdateMonState context | ALWAYS_APPLIES | lifecycle steps } function onUpdateMonState( @@ -96,7 +96,7 @@ contract Somniphobia is IMoveSet, BasicEffect { bytes32 extraData, uint256 playerIndex, uint256 monIndex, - uint256, + uint256 context, MonStateIndexName stateVarIndex, int32 valueToAdd ) external override returns (bytes32, bool) { @@ -105,7 +105,7 @@ contract Somniphobia is IMoveSet, BasicEffect { uint256 lane = exists ? _lane(data, playerIndex ^ 1) : 0; if (lane != 0) { int32 stack = int32(uint32(lane >> 8)); - uint32 maxHp = engine.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); + uint32 maxHp = TargetLib.hookMonMaxHp(context); int32 damage = int32(uint32(maxHp)) / DAMAGE_DENOM * stack; if (damage > 0) { engine.dealDamage(playerIndex, monIndex, damage); diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 1d893068..661cf14d 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -48,7 +48,11 @@ import {EditEffectAttack} from "./mocks/EditEffectAttack.sol"; import {SetEffectDataAttack} from "./mocks/SetEffectDataAttack.sol"; import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; import {WideDataEffect} from "./mocks/WideDataEffect.sol"; -import {FreshMoveContextAbility} from "./mocks/FreshMoveContextAbility.sol"; +import { + FreshMoveContextAbility, + FreshStatusContextAbility, + TEST_CONTEXT_STATUS_CLASS +} from "./mocks/FreshMoveContextAbility.sol"; contract EngineTest is Test, BattleHelper { DefaultCommitManager commitManager; @@ -155,6 +159,32 @@ contract EngineTest is Test, BattleHelper { assertEq(observer.observedMoveIndex(), NO_OP_MOVE_INDEX, "context must reflect prior hook rewrite"); } + function test_roundEndContextIsFreshAfterEarlierEffectAddsStatus() public { + FreshStatusContextAbility observer = new FreshStatusContextAbility(); + Mon memory aliceMon = dummyMon; + aliceMon.ability = uint256(uint160(address(observer))); + + Mon[] memory aliceTeam = new Mon[](1); + Mon[] memory bobTeam = new Mon[](1); + aliceTeam[0] = aliceMon; + bobTeam[0] = dummyMon; + defaultRegistry.setTeam(ALICE, aliceTeam); + defaultRegistry.setTeam(BOB, bobTeam); + + bytes32 battleKey = _startBattle(engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); + _commitRevealExecuteForAliceAndBob( + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) + ); + + // Ability registration installs a writer before the observer. During RoundEnd the writer + // adds a status, and the following opted-in hook must receive the new lane value. + assertEq( + observer.observedStatusClass(), + TEST_CONTEXT_STATUS_CLASS, + "context must reflect prior hook status mutation" + ); + } + function test_fasterSpeedKOsGameOver() public { // Initialize mons IMoveSet normalAttack = new CustomAttack( diff --git a/test/mocks/FreshMoveContextAbility.sol b/test/mocks/FreshMoveContextAbility.sol index 4ea4d559..4891c45e 100644 --- a/test/mocks/FreshMoveContextAbility.sol +++ b/test/mocks/FreshMoveContextAbility.sol @@ -8,6 +8,8 @@ import {IEffect} from "../../src/effects/IEffect.sol"; import {IEngine} from "../../src/IEngine.sol"; import {TargetLib} from "../../src/lib/TargetLib.sol"; +uint256 constant TEST_CONTEXT_STATUS_CLASS = 15; + contract RewriteMoveAfterMove is BasicEffect { function getStepsBitmap() external pure override returns (uint32) { return 0x8080; @@ -62,3 +64,62 @@ contract FreshMoveContextAbility is IAbility, BasicEffect { return (extraData, false); } } + +contract ContextTestStatus is BasicEffect { + function getStepsBitmap() external pure override returns (uint32) { + return uint32(0x8000 | (TEST_CONTEXT_STATUS_CLASS << 10)); + } +} + +contract AddStatusAtRoundEnd is BasicEffect { + IEffect private immutable STATUS; + + constructor(IEffect status) { + STATUS = status; + } + + function getStepsBitmap() external pure override returns (uint32) { + return 0x8004; + } + + function onRoundEnd(IEngine engine, bytes32, uint256, bytes32 extraData, uint256 side, uint256 mon, uint256) + external + override + returns (bytes32, bool) + { + engine.addEffect(side, mon, STATUS, bytes32(0)); + return (extraData, false); + } +} + +contract FreshStatusContextAbility is IAbility, BasicEffect { + IEffect private immutable WRITER; + uint8 public observedStatusClass; + + constructor() { + IEffect status = IEffect(address(new ContextTestStatus())); + WRITER = IEffect(address(new AddStatusAtRoundEnd(status))); + } + + function name() public pure override(IAbility, BasicEffect) returns (string memory) { + return "Fresh Status Context"; + } + + function activateOnSwitch(IEngine engine, bytes32, uint256 playerIndex, uint256 monIndex) external override { + engine.addEffect(playerIndex, monIndex, WRITER, bytes32(0)); + engine.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); + } + + function getStepsBitmap() external pure override returns (uint32) { + return 0x00048004; // RoundEnd context | ALWAYS_APPLIES | RoundEnd + } + + function onRoundEnd(IEngine, bytes32, uint256, bytes32 extraData, uint256 side, uint256 mon, uint256 context) + external + override + returns (bytes32, bool) + { + observedStatusClass = uint8(TargetLib.hookStatusClass(context, side, mon)); + return (extraData, false); + } +} From 259e08ab8a0cc097a8f4db450cf3f20a043d0a00 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:46:31 -0700 Subject: [PATCH 17/41] clean up some moves --- CLAUDE.md | 15 ++++- processing/generateSolidity.py | 8 ++- script/SetupMons.s.sol | 2 +- snapshots/MainnetReplayGasTest.json | 6 +- src/Constants.sol | 3 +- src/Engine.sol | 73 ++++++++++++++++--------- src/IEngine.sol | 5 ++ src/effects/IEffect.sol | 12 ++-- src/effects/status/BlessedStatus.sol | 27 +++++---- src/effects/status/BurnStatus.sol | 9 +-- src/effects/status/FrostbiteStatus.sol | 9 +-- src/lib/TargetLib.sol | 12 ++++ src/mons/aurox/GildedRecovery.sol | 8 +-- src/mons/aurox/IronWall.sol | 18 +++--- src/mons/embursa/HeatBeacon.sol | 6 +- src/mons/embursa/HoneyBribe.sol | 5 +- src/mons/ghouliath/RiseFromTheGrave.sol | 9 +-- src/mons/iblivion/Baselight.sol | 14 ++--- src/mons/inutia/ChainExpansion.sol | 5 +- src/mons/nirvamma/Adaptor.sol | 22 +++++--- src/mons/nirvamma/HardReset.sol | 4 +- src/mons/sofabbi/SnackBreak.sol | 5 +- test/mons/EmbursaTest.sol | 3 +- 23 files changed, 172 insertions(+), 108 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0a027fb5..f0ea8bbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -316,8 +316,12 @@ return low bits remain compatible. Supported layouts, decoded through `TargetLib`: +- `OnApply`: the target mon's max HP via `hookMonMaxHp(context)`. This request is consumed before + installation and is not persisted as a recurring context capability. - `AfterMove`: current 24-bit move words via `hookMoveWordAt(context, absSlot)` and acted bits via `hookSlotActed(context, absSlot)`. +- `AfterDamage`: the damaged mon's finalized KO state via `hookMonIsKnockedOut(context)`. +- `PreDamage`: current signed in-flight damage via `hookPreDamage(context)`. - `RoundEnd`: current status classes via `hookStatusClass(context, side, monIndex)`. - `OnUpdateMonState`: the changed mon's max HP and normalized signed HP delta via `hookMonMaxHp(context)` / `hookMonHpDelta(context)`. @@ -332,7 +336,16 @@ Consequently, a multi-hook effect receives every context supported for each hook even if it requested only one step. Benchmark multi-hook migrations: an unused builder at a frequent hook can outweigh the removed callback. Ordinary `IMoveSet.move()` execution does not yet receive these lifecycle layouts; only moves/abilities that also register as effects can use them in their -effect hooks. Do not add address-specific Engine dispatch to work around that boundary. +effect hooks. Ordinary tagged deployed moves may separately opt into supported move context through +metadata capabilities: `@move-context: status-lanes` makes the deployment generator set +`MOVE_CONTEXT_STATUS_LANES`, and `move()` can then use `TargetLib.hookStatusClass`. Keep these +capabilities generic; do not add address-specific Engine dispatch. + +For ordinary moves/effects outside a supported packed hook context, use the narrowest typed bundled +getter matching the data shape. In particular, overheal clamps should call +`getMonHpState(battleKey, player, mon)` for `(maxHp, hpDelta)` rather than making separate +`getMonValueForBattle(Hp)` and `getMonStateForBattle(Hp)` external calls. Do not add raw-slot or +large generic snapshot APIs; add a new bundle only when multiple mechanics share the exact shape. ### Stat Boosts (inlined into the Engine) diff --git a/processing/generateSolidity.py b/processing/generateSolidity.py index 7b85eabb..f24b0097 100644 --- a/processing/generateSolidity.py +++ b/processing/generateSolidity.py @@ -289,6 +289,7 @@ def load_json_move(mon_name: str, move_name: str, base_path: str) -> Optional[di MOVE_META_TAG = 1 << 160 +MOVE_CONTEXT_STATUS_LANES = 1 << 161 MOVE_META_DYNAMIC = 0xF DEFAULT_PRIORITY = 3 @@ -326,16 +327,19 @@ def deployed_move_meta(mon_name: str, move_name: str, base_path: str) -> int: contract_name = contract_name_from_move_or_ability(move_name) sol_path = os.path.join(base_path, "src", "mons", mon_dir, f"{contract_name}.sol") stamina_val = priority_val = None + context_bits = 0 if os.path.exists(sol_path): with open(sol_path, "r", encoding="utf-8") as f: content = f.read() + if re.search(r"@move-context:\s*status-lanes\b", content): + context_bits |= MOVE_CONTEXT_STATUS_LANES stamina_val = _extract_static_int(content, "stamina", "STAMINA_COST") priority_val = _extract_static_int(content, "priority", "PRIORITY") if stamina_val is None or not 0 <= stamina_val < MOVE_META_DYNAMIC: stamina_val = MOVE_META_DYNAMIC if priority_val is None or not 0 <= priority_val < MOVE_META_DYNAMIC: priority_val = MOVE_META_DYNAMIC - return MOVE_META_TAG | (stamina_val << 236) | (priority_val << 244) + return MOVE_META_TAG | context_bits | (stamina_val << 236) | (priority_val << 244) def effect_name_to_env_var(effect_name: str) -> str: @@ -806,4 +810,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/script/SetupMons.s.sol b/script/SetupMons.s.sol index b76f36a2..d366adc1 100644 --- a/script/SetupMons.s.sol +++ b/script/SetupMons.s.sol @@ -544,7 +544,7 @@ contract SetupMons is Script { uint256[] memory moves = new uint256[](4); moves[0] = 0x00f0200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); moves[1] = 0x00f0300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x00f0000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[2] = 0x00f0000000000000000000030000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); moves[3] = 0x00f0200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); uint256[] memory abilities = new uint256[](1); abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[4])); diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json index ad3487e8..06a0a5b4 100644 --- a/snapshots/MainnetReplayGasTest.json +++ b/snapshots/MainnetReplayGasTest.json @@ -5,7 +5,7 @@ "MainnetDoublesReplay1_Reused_effectDataWrites": "0", "MainnetDoublesReplay1_Reused_effectHeaderReads": "45", "MainnetDoublesReplay1_Reused_effectHeaderWrites": "14", - "MainnetDoublesReplay1_Reused_execGas": "1213930", + "MainnetDoublesReplay1_Reused_execGas": "1210530", "MainnetDoublesReplay1_Reused_lifecycleCalls": "34", "MainnetDoublesReplay1_Reused_moveDecisionCallbacks": "0", "MainnetDoublesReplay1_Reused_nonHookHeaderReadUpperBound": "11", @@ -20,13 +20,13 @@ "MainnetReplay_Reused_effectDataWrites": "0", "MainnetReplay_Reused_effectHeaderReads": "191", "MainnetReplay_Reused_effectHeaderWrites": "39", - "MainnetReplay_Reused_execGas": "2152334", + "MainnetReplay_Reused_execGas": "2136527", "MainnetReplay_Reused_lifecycleCalls": "84", "MainnetReplay_Reused_moveDecisionCallbacks": "0", "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "107", "MainnetReplay_Reused_preDamageCalls": "0", "MainnetReplay_Reused_roundEndCalls": "56", "MainnetReplay_Reused_roundStartCalls": "0", - "MainnetReplay_Reused_statusClassCallbacks": "5", + "MainnetReplay_Reused_statusClassCallbacks": "0", "MainnetReplay_Reused_updateStateCalls": "0" } \ No newline at end of file diff --git a/src/Constants.sol b/src/Constants.sol index a03d2475..700176d0 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -124,12 +124,13 @@ uint8 constant BATTLE_MODE_DOUBLES = 1; uint8 constant BATTLE_MODE_MULTI = 2; // Deployed-move word with packed static metadata: [address 0-159 | tag bit 160 | -// stamina 236-239 | priority 244-247]. The nibble value 0xF means "battle/state-dependent — +// context capabilities 161+ | stamina 236-239 | priority 244-247]. The nibble value 0xF means "battle/state-dependent — // staticcall the move live" (Rock Pull's punisher priority, HeatBeacon-boosted casts, // Unbounded Strike's stamina). Inline move words never set bit 160 (their 160-227 range is // unused), so the tag cleanly three-ways the discriminator: tagged = deployed+meta, // untagged with high bits = inline, bare address = deployed without meta (legacy/tests). uint256 constant MOVE_META_TAG = 1 << 160; +uint256 constant MOVE_CONTEXT_STATUS_LANES = 1 << 161; uint256 constant MOVE_META_DYNAMIC = 0xF; // The 16-bit move extraData splits [targetBits 4 | movePayload 12]. The nibble is a bitmask diff --git a/src/Engine.sol b/src/Engine.sol index 1451f87f..d6cf116b 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -1898,15 +1898,15 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Check if we have to run an onApply state update (use bitmap instead of external call) if ((stepsBitmap & (1 << uint8(EffectStep.OnApply))) != 0) { + uint256 applyContext = _hookActivesWord(battleData[battleKey]); + if ((hookContextBitmap & (1 << uint8(EffectStep.OnApply))) != 0 && targetIndex < 2) { + // OnApply is executed before the effect is stored, so its exact request bit is + // still available. Max HP is immutable for the battle and occupies bits 32..63. + applyContext |= uint256(_getTeamMon(config, targetIndex, monIndex).stats.hp) << 32; + } // If so, we run the effect first, and get updated extraData if necessary (extraDataToUse, removeAfterRun) = effect.onApply( - IEngine(address(this)), - battleKey, - tempRNG, - extraData, - targetIndex, - monIndex, - _hookActivesWord(battleData[battleKey]) + IEngine(address(this)), battleKey, tempRNG, extraData, targetIndex, monIndex, applyContext ); } if (!removeAfterRun) { @@ -2104,8 +2104,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ) private { uint256 raw = uint256(data); uint256 encoded = raw < EFFECT_COMPACT_DATA_MASK ? raw + 1 : 0; + // OnApply has already run before installation. Do not persist an OnApply-only request as + // the broad capability flag, which would make multi-hook statuses build unused contexts. + uint16 storedContextBitmap = hookContextBitmap & ~uint16(1 << uint8(EffectStep.OnApply)); uint256 header = uint256(uint160(address(effect))) | (uint256(stepsBitmap) << 160) | (encoded << 176) - | (hookContextBitmap != 0 ? EFFECT_HOOK_CONTEXT_FLAG : 0); + | (storedContextBitmap != 0 ? EFFECT_HOOK_CONTEXT_FLAG : 0); assembly ("memory-safe") { sstore(effectInstance.slot, header) } @@ -3423,15 +3426,19 @@ contract Engine is IEngine, MappingAllocator, EIP712 { staminaCost = int32(moveStamina); } _deductStamina(currentMonState, staminaCost); + uint256 moveContext = TargetLib.singlesActives( + _unpackActiveMonIndex(battle.activeMonIndex, 0), _unpackActiveMonIndex(battle.activeMonIndex, 1) + ); + if (rawMoveSlot & MOVE_CONTEXT_STATUS_LANES != 0) { + moveContext |= uint256(config.monStatusLanes) << 132; + } moveSet.move( self, battleKey, playerIndex, activeMonIndex, TargetLib.impliedSinglesTargetBits(playerIndex), - TargetLib.singlesActives( - _unpackActiveMonIndex(battle.activeMonIndex, 0), _unpackActiveMonIndex(battle.activeMonIndex, 1) - ), + moveContext, move.extraData & EXTRA_DATA_PAYLOAD_MASK, tempRNG ); @@ -3525,10 +3532,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { eff = config.p1Effects[slotIndex]; } - // Skip tombstones AND entries that don't listen at this step. The step filter is - // hoisted here on purpose: stepsBitmap shares slot 0 with the effect address (free - // check), while letting _runSingleEffect do it would first pay the eff.data SLOAD - // (slot 1) + 13-arg call setup. + // Skip tombstones AND entries that don't listen at this step before reading slot-1 data. if (address(eff.effect) != TOMBSTONE_ADDRESS && (eff.stepsBitmap & (1 << uint8(round))) != 0) { (bytes32 effectData, bool needsFreshContext) = _effectDataAndHookContext(eff); uint256 hookContext = activesPacked; @@ -3537,6 +3541,16 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Built immediately before this effect call, after every earlier effect in // the same pass, so status mutations cannot stale the snapshot. hookContext |= uint256(config.monStatusLanes) << 132; + } else if (round == EffectStep.AfterDamage) { + // Damage has landed and KO state is finalized. The loop already has the + // exact effect owner and config, so this needs no dispatch-time lookup. + if (_getMonState(config, playerIndex, monIndex).isKnockedOut) { + hookContext |= uint256(1) << 32; + } + } else if (round == EffectStep.PreDamage) { + // This transient is updated by every earlier PreDamage effect, so reading + // it here gives each callback the current composed signed damage value. + hookContext |= uint256(uint32(tempPreDamage)) << 32; } else if (round == EffectStep.AfterMove || round == EffectStep.OnUpdateMonState) { // These hook-specific builders need data resolved at dispatch time. Keep // the request out of the public low-32 active lanes until then. @@ -3585,15 +3599,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ) private { // Run the effect and get result (bytes32 updatedExtraData, bool removeAfterRun) = _executeEffectHook( - battleKeyForWrite, - effect, - rng, - data, - playerIndex, - monIndex, - round, - extraEffectsData, - activesPacked + battleKeyForWrite, effect, rng, data, playerIndex, monIndex, round, extraEffectsData, activesPacked ); // If we need to remove or update the effect @@ -4606,6 +4612,10 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (isInlineAttack) { _inlineStandardAttack(config, rawMoveSlot, side, activeMon, tSlot >> 1, tMon, actionRng); } else { + uint256 moveContext = _buildActivesWord(battle); + if (rawMoveSlot & MOVE_CONTEXT_STATUS_LANES != 0) { + moveContext |= uint256(config.monStatusLanes) << 132; + } IMoveSet(address(uint160(rawMoveSlot))) .move( IEngine(address(this)), @@ -4613,7 +4623,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { side, activeMon, targetBits, - _buildActivesWord(battle), + moveContext, uint16(moveExtra & EXTRA_DATA_PAYLOAD_MASK), actionRng ); @@ -5462,6 +5472,19 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return _readMonStateDelta(config, playerIndex, monIndex, stateVarIndex); } + function getMonHpState(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) + external + view + returns (uint32 maxHp, int32 hpDelta) + { + BattleConfig storage config = battleConfig[_resolveStorageKey(battleKey)]; + maxHp = _getTeamMon(config, playerIndex, monIndex).stats.hp; + hpDelta = _getMonState(config, playerIndex, monIndex).hpDelta; + if (hpDelta == CLEARED_MON_STATE_SENTINEL) { + hpDelta = 0; + } + } + /// @notice Current (base + delta, sentinel-aware) value of a mon stat in one call — the /// merged form of the getMonValueForBattle + getMonStateForBattle pair. Only /// meaningful for Hp/Stamina and the five stats (booleans have no base value). diff --git a/src/IEngine.sol b/src/IEngine.sol index b43fe17f..de04616e 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -168,6 +168,11 @@ interface IEngine { uint256 monIndex, MonStateIndexName stateVarIndex ) external view returns (int32); + // Focused overheal-clamp snapshot: both HP quantities in one external frame. + function getMonHpState(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) + external + view + returns (uint32 maxHp, int32 hpDelta); // Current (base + delta, sentinel-aware) stat value in one call — the merged form of the // getMonValueForBattle + getMonStateForBattle pair. function getMonCurrentValue( diff --git a/src/effects/IEffect.sol b/src/effects/IEffect.sol index 1fab611d..a55a94e7 100644 --- a/src/effects/IEffect.sol +++ b/src/effects/IEffect.sol @@ -26,10 +26,11 @@ interface IEffect { // `activesPacked` carries every slot's active roster index in its low 32 bits (one 8-bit lane // per absolute slot, EMPTY_ACTIVE_LANE = no mon there). Opted-in AfterMove hooks also receive // fresh 24-bit move lanes at bits 32..127 and the acted-slot mask at bits 128..131. Opted-in - // RoundEnd hooks receive fresh 4-bit status lanes at bits 132..195. Opted-in OnUpdateMonState - // hooks receive target max HP at bits 32..63 and normalized signed HP delta at bits 64..95. - // Context layouts are hook-specific; existing effects remain compatible and opt-in readers - // decode the high context via TargetLib. + // OnApply hooks receive target max HP at bits 32..63. RoundEnd hooks receive fresh 4-bit status + // lanes at bits 132..195. Opted-in AfterDamage hooks receive the target KO bit at bit 32. + // Opted-in OnUpdateMonState hooks receive target max HP at bits 32..63 and normalized signed HP + // delta at bits 64..95. Context layouts are hook-specific; existing effects remain compatible + // and opt-in readers decode the high context via TargetLib. function onRoundStart( IEngine engine, bytes32 battleKey, @@ -88,7 +89,8 @@ interface IEffect { // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) // Runs before damage is applied; effects can mutate the in-flight damage by calling - // `engine.setPreDamage(int32)`. Read the current running damage via `engine.getPreDamage()`. + // `engine.setPreDamage(int32)`. Opted-in effects read the fresh current running damage via + // `TargetLib.hookPreDamage(activesPacked)`; legacy effects may use `engine.getPreDamage()`. // Multiple subscribed effects compose sequentially in effect-array order, each observing // the post-mutation value from prior effects. function onPreDamage( diff --git a/src/effects/status/BlessedStatus.sol b/src/effects/status/BlessedStatus.sol index 312e1f70..4e19647a 100644 --- a/src/effects/status/BlessedStatus.sol +++ b/src/effects/status/BlessedStatus.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {ALWAYS_APPLIES_BIT, STATUS_CLASS_SHIFT} from "../../Constants.sol"; import {MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; import {StatusEffect} from "./StatusEffect.sol"; @@ -17,18 +18,23 @@ contract BlessedStatus is StatusEffect { return "Blessed"; } - // Steps: OnApply (0x01), OnRemove (0x08), PreDamage (0x200) + // Steps: OnApply (0x01), OnRemove (0x08), PreDamage (0x200); fresh PreDamage context (0x0200_0000) function getStepsBitmap() external pure override returns (uint32) { - return 0x209 | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; + return uint32(0x02000209) | uint32(uint16(STATUS_CLASS << STATUS_CLASS_SHIFT)) | uint32(ALWAYS_APPLIES_BIT); } // Absorb the next incoming damage source entirely, then remove (which grants the heal). - function onPreDamage(IEngine engine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) - external - override - returns (bytes32, bool removeAfterRun) - { - if (engine.getPreDamage() > 0) { + function onPreDamage( + IEngine engine, + bytes32, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256 hookContext, + uint256 + ) external override returns (bytes32, bool removeAfterRun) { + if (TargetLib.hookPreDamage(hookContext) > 0) { engine.setPreDamage(0); return (extraData, true); } @@ -45,9 +51,8 @@ contract BlessedStatus is StatusEffect { // Heal maxHp/HEAL_DENOM, clamped so we never overheal (copy of the ChainExpansion clamp). function _heal(IEngine engine, bytes32 battleKey, uint256 targetIndex, uint256 monIndex) internal { - int32 amtToHeal = - int32(engine.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp)) / HEAL_DENOM; - int32 hpDelta = engine.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp); + (uint32 maxHp, int32 hpDelta) = engine.getMonHpState(battleKey, targetIndex, monIndex); + int32 amtToHeal = int32(maxHp) / HEAL_DENOM; // hpDelta is negative when damaged; cap the heal to the damage taken so we can't exceed max HP. if (amtToHeal > (-1 * hpDelta)) { amtToHeal = -1 * hpDelta; diff --git a/src/effects/status/BurnStatus.sol b/src/effects/status/BurnStatus.sol index 3e645cee..3dea880c 100644 --- a/src/effects/status/BurnStatus.sol +++ b/src/effects/status/BurnStatus.sol @@ -8,6 +8,7 @@ import {StatBoostToApply} from "../../Structs.sol"; import {IStatusEffect} from "./IStatusEffect.sol"; import {StatusEffect} from "./StatusEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; contract BurnStatus is StatusEffect, IStatusEffect { uint256 constant STATUS_CLASS = 1; @@ -28,7 +29,7 @@ contract BurnStatus is StatusEffect, IStatusEffect { // no-op external call per burned turn). HAS_REAPPLY routes same-class re-applies to the // degree escalation in onReapply. function getStepsBitmap() external pure override returns (uint32) { - return 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | HAS_REAPPLY_BIT | ALWAYS_APPLIES_BIT; + return 0x00010000 | 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | HAS_REAPPLY_BIT | ALWAYS_APPLIES_BIT; } // extraData layout: [burn degree: bits 0-7 | cached max HP: bits 32-63]. Base HP is fixed @@ -36,12 +37,12 @@ contract BurnStatus is StatusEffect, IStatusEffect { function onApply( IEngine engine, - bytes32 battleKey, + bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, - uint256 + uint256 context ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { // Fresh apply only — the Engine routes same-class re-applies to onReapply instead. // Reduce attack by 1/ATTACK_DENOM of base attack stat @@ -51,7 +52,7 @@ contract BurnStatus is StatusEffect, IStatusEffect { }); engine.addStatBoost(targetIndex, monIndex, statBoosts, StatBoostFlag.Perm); - uint256 maxHp = uint256(engine.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp)); + uint256 maxHp = TargetLib.hookMonMaxHp(context); return (bytes32(uint256(1) | (maxHp << 32)), false); } diff --git a/src/effects/status/FrostbiteStatus.sol b/src/effects/status/FrostbiteStatus.sol index a170e131..5b73376d 100644 --- a/src/effects/status/FrostbiteStatus.sol +++ b/src/effects/status/FrostbiteStatus.sol @@ -5,6 +5,7 @@ import {ALWAYS_APPLIES_BIT, STATUS_CLASS_SHIFT} from "../../Constants.sol"; import "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {StatBoostToApply} from "../../Structs.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; import {StatusEffect} from "./StatusEffect.sol"; @@ -20,17 +21,17 @@ contract FrostbiteStatus is StatusEffect { // Steps: OnApply, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint32) { - return 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; + return 0x00010000 | 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; } function onApply( IEngine engine, - bytes32 battleKey, + bytes32, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex, - uint256 activesPacked + uint256 context ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { // Reduce special attack by half StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); @@ -40,7 +41,7 @@ contract FrostbiteStatus is StatusEffect { engine.addStatBoost(targetIndex, monIndex, statBoosts, StatBoostFlag.Perm); // Cache max HP (bits 32-63; fixed for the battle) so each tick skips the engine read. - uint256 maxHp = uint256(engine.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp)); + uint256 maxHp = TargetLib.hookMonMaxHp(context); return (bytes32(maxHp << 32), false); } diff --git a/src/lib/TargetLib.sol b/src/lib/TargetLib.sol index cd096444..aa460158 100644 --- a/src/lib/TargetLib.sol +++ b/src/lib/TargetLib.sol @@ -13,6 +13,8 @@ library TargetLib { uint256 internal constant HOOK_STATUS_LANES_SHIFT = 132; uint256 internal constant HOOK_MON_MAX_HP_SHIFT = 32; uint256 internal constant HOOK_MON_HP_DELTA_SHIFT = 64; + uint256 internal constant HOOK_MON_KO_SHIFT = 32; + uint256 internal constant HOOK_PRE_DAMAGE_SHIFT = 32; function sideOf(uint256 absSlot) internal pure returns (uint256) { return absSlot >> 1; @@ -90,6 +92,16 @@ library TargetLib { return int32(uint32(hookContext >> HOOK_MON_HP_DELTA_SHIFT)); } + /// @notice Fresh target KO state embedded for an opted-in AfterDamage callback. + function hookMonIsKnockedOut(uint256 hookContext) internal pure returns (bool) { + return ((hookContext >> HOOK_MON_KO_SHIFT) & 1) != 0; + } + + /// @notice Fresh signed in-flight damage embedded for an opted-in PreDamage callback. + function hookPreDamage(uint256 hookContext) internal pure returns (int32) { + return int32(uint32(hookContext >> HOOK_PRE_DAMAGE_SHIFT)); + } + /// @dev Kit-audit ruling for untargeted "the opposing active" effects (switch-in chips, /// KO-triggered debuffs...): the mirror of `ownSlot` on the opposing side, falling back /// to its partner when the mirror lane is empty; NO_SLOT when the opposing side is vacant. diff --git a/src/mons/aurox/GildedRecovery.sol b/src/mons/aurox/GildedRecovery.sol index 1d6181d4..412e61c9 100644 --- a/src/mons/aurox/GildedRecovery.sol +++ b/src/mons/aurox/GildedRecovery.sol @@ -36,14 +36,12 @@ contract GildedRecovery is IMoveSet { engine.updateMonState(attackerPlayerIndex, targetMonIndex, MonStateIndexName.Stamina, STAMINA_BONUS); // Heal 50% of max HP for self - int32 maxHp = int32( - engine.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp) - ); + (uint32 maxHpRaw, int32 currentHpDelta) = + engine.getMonHpState(battleKey, attackerPlayerIndex, attackerMonIndex); + int32 maxHp = int32(maxHpRaw); int32 healAmount = (maxHp * HEAL_PERCENT) / 100; // Don't overheal - int32 currentHpDelta = - engine.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentHpDelta + healAmount > 0) { healAmount = -currentHpDelta; } diff --git a/src/mons/aurox/IronWall.sol b/src/mons/aurox/IronWall.sol index e65d2527..a40acdfa 100644 --- a/src/mons/aurox/IronWall.sol +++ b/src/mons/aurox/IronWall.sol @@ -8,6 +8,7 @@ import {IEngine} from "../../IEngine.sol"; import {MoveMeta} from "../../Structs.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; import {IMoveSet} from "../../moves/IMoveSet.sol"; contract IronWall is IMoveSet, BasicEffect { @@ -38,12 +39,10 @@ contract IronWall is IMoveSet, BasicEffect { engine.addEffect(attackerPlayerIndex, attackerMonIndex, IEffect(address(this)), bytes32(0)); // Also, heal for INITIAL_HEAL_PERCENT - int32 maxHp = - int32(engine.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp)); + (uint32 maxHpRaw, int32 currentHpDelta) = engine.getMonHpState(battleKey, attackerPlayerIndex, attackerMonIndex); + int32 maxHp = int32(maxHpRaw); int32 healAmount = (maxHp * INITIAL_HEAL_PERCENT) / 100; // Prevent overhealing - int32 currentHpDelta = - engine.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentHpDelta + healAmount > 0) { healAmount = -currentHpDelta; } @@ -83,17 +82,17 @@ contract IronWall is IMoveSet, BasicEffect { // IEffect implementation // Steps: OnMonSwitchOut, AfterDamage function getStepsBitmap() external pure override returns (uint32) { - return 0x8060; + return 0x00408060; // AfterDamage context | ALWAYS_APPLIES | lifecycle steps } function onAfterDamage( IEngine engine, - bytes32 battleKey, + bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, - uint256, + uint256 context, int32 damageDealt, uint256 ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { @@ -103,10 +102,7 @@ contract IronWall is IMoveSet, BasicEffect { // <= damageDealt/2 <= int32.max, so the cast back is safe. int32 healAmount = int32((int256(damageDealt) * HEAL_PERCENT) / 100); // Heal only if not KO'ed - if ( - healAmount > 0 - && engine.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut) == 0 - ) { + if (healAmount > 0 && !TargetLib.hookMonIsKnockedOut(context)) { engine.updateMonState(targetIndex, monIndex, MonStateIndexName.Hp, healAmount); } return (extraData, false); diff --git a/src/mons/embursa/HeatBeacon.sol b/src/mons/embursa/HeatBeacon.sol index e76e5b92..69ee1b3c 100644 --- a/src/mons/embursa/HeatBeacon.sol +++ b/src/mons/embursa/HeatBeacon.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; +// @move-context: status-lanes + import "../../Constants.sol"; import "../../Enums.sol"; @@ -27,7 +29,7 @@ contract HeatBeacon is IMoveSet { function move( IEngine engine, - bytes32 battleKey, + bytes32, uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 targetBits, @@ -36,7 +38,7 @@ contract HeatBeacon is IMoveSet { uint256 ) external { // Only spread Burn to the opponent if Embursa is itself Burned. - if (engine.getMonStatusClass(battleKey, attackerPlayerIndex, attackerMonIndex) == BURN_CLASS) { + if (TargetLib.hookStatusClass(activesPacked, attackerPlayerIndex, attackerMonIndex) == BURN_CLASS) { uint256 targetSlot = TargetLib.lowestSlot(targetBits); if (targetSlot != NO_SLOT) { uint256 defenderPlayerIndex = TargetLib.sideOf(targetSlot); diff --git a/src/mons/embursa/HoneyBribe.sol b/src/mons/embursa/HoneyBribe.sol index 774d9d05..fb857bea 100644 --- a/src/mons/embursa/HoneyBribe.sol +++ b/src/mons/embursa/HoneyBribe.sol @@ -57,11 +57,8 @@ contract HoneyBribe is IMoveSet { uint256 defenderMonIndex = TargetLib.activeAt(activesPacked, targetSlot); // Heal active mon by max HP / 2**bribeLevel uint256 bribeLevel = _getBribeLevel(engine, battleKey, attackerPlayerIndex, attackerMonIndex); - uint32 maxHp = - engine.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); + (uint32 maxHp, int32 currentDamage) = engine.getMonHpState(battleKey, attackerPlayerIndex, attackerMonIndex); int32 healAmount = int32(uint32(maxHp / (DEFAULT_HEAL_DENOM * (2 ** bribeLevel)))); - int32 currentDamage = - engine.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentDamage + healAmount > 0) { healAmount = -1 * currentDamage; } diff --git a/src/mons/ghouliath/RiseFromTheGrave.sol b/src/mons/ghouliath/RiseFromTheGrave.sol index e13a8801..688b8c14 100644 --- a/src/mons/ghouliath/RiseFromTheGrave.sol +++ b/src/mons/ghouliath/RiseFromTheGrave.sol @@ -7,6 +7,7 @@ import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; contract RiseFromTheGrave is IAbility, BasicEffect { uint64 public constant REVIVAL_DELAY = 3; @@ -37,17 +38,17 @@ contract RiseFromTheGrave is IAbility, BasicEffect { // IEffect implementation // Steps: RoundEnd, AfterDamage function getStepsBitmap() external pure override returns (uint32) { - return 0x8044; + return 0x00408044; // AfterDamage context | ALWAYS_APPLIES | lifecycle steps } function onAfterDamage( IEngine engine, - bytes32 battleKey, + bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, - uint256, + uint256 context, int32, uint256 ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { @@ -56,7 +57,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { and remove this effect (so we stop hooking into it on future applications) */ // If the mon is KO'd, add this effect to the global effects list and remove the mon effect - if (engine.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut) == 1) { + if (TargetLib.hookMonIsKnockedOut(context)) { uint64 v1 = REVIVAL_DELAY; uint64 v2 = uint64(targetIndex) & 0x3F; // player index (masked to 6 bits) uint64 v3 = uint64(monIndex) & 0x3F; // mon index (masked to 6 bits) diff --git a/src/mons/iblivion/Baselight.sol b/src/mons/iblivion/Baselight.sol index c6eff34b..41e8b88b 100644 --- a/src/mons/iblivion/Baselight.sol +++ b/src/mons/iblivion/Baselight.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.0; -import {MonStateIndexName} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; /** * Baselight Ability for Iblivion @@ -86,22 +86,22 @@ contract Baselight is IAbility, BasicEffect { // Steps: AfterDamage function getStepsBitmap() external pure override returns (uint32) { - return 0x8040; + return 0x00408040; // AfterDamage context | ALWAYS_APPLIES | AfterDamage } // Gain 1 Baselight per damage event (capped), but never from a lethal hit. function onAfterDamage( - IEngine engine, - bytes32 battleKey, + IEngine, + bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, - uint256, + uint256 context, int32, uint256 - ) external view override returns (bytes32 updatedExtraData, bool removeAfterRun) { - if (engine.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut) != 0) { + ) external pure override returns (bytes32 updatedExtraData, bool removeAfterRun) { + if (TargetLib.hookMonIsKnockedOut(context)) { return (extraData, false); } uint256 currentLevel = uint256(extraData); diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index ccf02689..d6404bee 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -97,9 +97,8 @@ contract ChainExpansion is IMoveSet, BasicEffect { (uint256 chargesLeft, uint256 ownerIndex) = _decodeState(extraData); // If it's a friendly mon, then we heal (flat 1/8 of max HP) if (targetIndex == ownerIndex) { - int32 amtToHeal = - int32(engine.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp)) / HEAL_DENOM; - int32 damageReceived = engine.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp); + (uint32 maxHp, int32 damageReceived) = engine.getMonHpState(battleKey, targetIndex, monIndex); + int32 amtToHeal = int32(maxHp) / HEAL_DENOM; // Prevent overhealing if (amtToHeal > (-1 * damageReceived)) { amtToHeal = -1 * damageReceived; diff --git a/src/mons/nirvamma/Adaptor.sol b/src/mons/nirvamma/Adaptor.sol index c0a8df48..30536786 100644 --- a/src/mons/nirvamma/Adaptor.sol +++ b/src/mons/nirvamma/Adaptor.sol @@ -10,6 +10,7 @@ import {IEngine} from "../../IEngine.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; /// @dev Source identity: low 160 bits = msg.sender for external dealDamage callers; full packed /// move slot for the inline-StandardAttack path. Two attackers wielding the same @@ -33,18 +34,23 @@ contract Adaptor is IAbility, BasicEffect { engine.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); } - // Steps: AfterDamage, PreDamage, ALWAYS_APPLIES (0x8000) + // Steps: AfterDamage, PreDamage, ALWAYS_APPLIES (0x8000); fresh PreDamage context (0x0200_0000) function getStepsBitmap() external pure override returns (uint32) { - return 0x8240; + return 0x02008240; } - function onPreDamage(IEngine engine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256 source) - external - override - returns (bytes32, bool) - { + function onPreDamage( + IEngine engine, + bytes32, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256 hookContext, + uint256 source + ) external override returns (bytes32, bool) { if (extraData == bytes32(source)) { - int32 running = engine.getPreDamage(); + int32 running = TargetLib.hookPreDamage(hookContext); engine.setPreDamage(running / DAMAGE_DENOM); } return (extraData, false); diff --git a/src/mons/nirvamma/HardReset.sol b/src/mons/nirvamma/HardReset.sol index deb4ebd0..967abdde 100644 --- a/src/mons/nirvamma/HardReset.sol +++ b/src/mons/nirvamma/HardReset.sol @@ -118,9 +118,9 @@ contract HardReset is IMoveSet, BasicEffect { } else if (cur < 0) { engine.updateMonState(targetIndex, monIndex, MonStateIndexName.Stamina, 1); } - int32 maxHp = int32(engine.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp)); + (uint32 maxHpRaw, int32 curHp) = engine.getMonHpState(battleKey, targetIndex, monIndex); + int32 maxHp = int32(maxHpRaw); int32 healAmt = maxHp / HP_DENOM; - int32 curHp = engine.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp); if (curHp + healAmt > 0) { healAmt = -curHp; } diff --git a/src/mons/sofabbi/SnackBreak.sol b/src/mons/sofabbi/SnackBreak.sol index 1734bab3..a18a36a4 100644 --- a/src/mons/sofabbi/SnackBreak.sol +++ b/src/mons/sofabbi/SnackBreak.sol @@ -47,13 +47,10 @@ contract SnackBreak is IMoveSet { uint256 ) external { uint256 snackLevel = _getSnackLevel(engine, battleKey, attackerPlayerIndex, attackerMonIndex); - uint32 maxHp = - engine.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); + (uint32 maxHp, int32 currentDamage) = engine.getMonHpState(battleKey, attackerPlayerIndex, attackerMonIndex); // Heal active mon by max HP / 2**snackLevel int32 healAmount = int32(uint32(maxHp / (DEFAULT_HEAL_DENOM * (2 ** snackLevel)))); - int32 currentDamage = - engine.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentDamage + healAmount > 0) { healAmount = -1 * currentDamage; } diff --git a/test/mons/EmbursaTest.sol b/test/mons/EmbursaTest.sol index 44a8c012..9a5e1de6 100644 --- a/test/mons/EmbursaTest.sol +++ b/test/mons/EmbursaTest.sol @@ -13,6 +13,7 @@ import {DefaultCommitManager} from "../../src/commit-manager/DefaultCommitManage import {IEngine} from "../../src/IEngine.sol"; import {IEffect} from "../../src/effects/IEffect.sol"; import {IMoveSet} from "../../src/moves/IMoveSet.sol"; +import {MoveSlotLib} from "../../src/moves/MoveSlotLib.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; import {BattleHelper} from "../abstract/BattleHelper.sol"; @@ -138,7 +139,7 @@ contract EmbursaTest is Test, BattleHelper { ); uint256[] memory moves = new uint256[](4); - moves[0] = uint256(uint160(address(heatBeacon))); + moves[0] = MoveSlotLib.packDeployed(address(heatBeacon), 0, MOVE_META_DYNAMIC) | MOVE_CONTEXT_STATUS_LANES; moves[1] = uint256(uint160(address(burnMove))); moves[2] = uint256(uint160(address(burnMove))); moves[3] = uint256(uint160(address(burnMove))); From c2851292afd3fd74f9e8cdb4b01370069ef27cb2 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:15:47 -0700 Subject: [PATCH 18/41] wip kernel tests --- CLAUDE.md | 10 +- drool/moves.csv | 2 +- processing/generateSolidity.py | 3 + script/SetupMons.s.sol | 2 +- snapshots/MainnetReplayGasTest.json | 158 +++++- src/Constants.sol | 1 + src/Engine.sol | 253 ++++++++- src/effects/EffectCommandLib.sol | 23 + src/effects/IEffectResolver.sol | 17 + src/kernel/PackedHeaderFrameLib.sol | 178 ++++++ src/mons/embursa/Tinderclaws.sol | 37 +- src/mons/inutia/HitAndDip.sol | 38 +- src/moves/IMoveResolver.sol | 17 + src/moves/MoveCommandLib.sol | 30 + test/EngineTest.sol | 2 +- test/MainnetReplayGasTest.t.sol | 311 ++++++++++- test/abstract/GasMeasure.sol | 683 +++++++++++++++++++++++ test/kernel/PackedHeaderFrameLibTest.sol | 192 +++++++ test/mons/DoublesKitTest.sol | 1 + 19 files changed, 1913 insertions(+), 45 deletions(-) create mode 100644 src/effects/EffectCommandLib.sol create mode 100644 src/effects/IEffectResolver.sol create mode 100644 src/kernel/PackedHeaderFrameLib.sol create mode 100644 src/moves/IMoveResolver.sol create mode 100644 src/moves/MoveCommandLib.sol create mode 100644 test/kernel/PackedHeaderFrameLibTest.sol diff --git a/CLAUDE.md b/CLAUDE.md index f0ea8bbb..3b614249 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,8 @@ ```bash forge install # Install dependencies (forge-std) forge build # Compile contracts -forge test # Run all tests -forge test -vvv # Run tests with verbose output +FOUNDRY_PROFILE=fast forge test # Run all tests +FOUNDRY_PROFILE=fast forge test -vvv # Run tests with verbose output ``` Engine edits invalidate most of the production-profile dependency graph. For correctness-only @@ -32,9 +32,9 @@ but disables optimization/build-info, enables sparse compilation, and uses a sep FOUNDRY_PROFILE=fast forge test --match-path test/EngineTest.sol ``` -Do not use `profile.fast` for gas comparisons or snapshots. Run those and the final full-suite -gate under the default profile (`forge test ...`); the fast profile disables snapshot emission so -an accidental gas-test run cannot overwrite production-profile numbers. +Always use `profile.fast` for correctness, regression, and full-suite test gates. Use the default +profile only for explicit gas comparisons and snapshots; the fast profile disables snapshot +emission so an accidental gas-test run cannot overwrite production-profile numbers. ## Repository Structure diff --git a/drool/moves.csv b/drool/moves.csv index 1ce57c1c..3f69901a 100644 --- a/drool/moves.csv +++ b/drool/moves.csv @@ -43,7 +43,7 @@ Volatile Punch,Aurox,40,3,100,0,Metal,Physical,Deals damage. 25% chance of Burn Gilded Recovery,Aurox,0,2,100,0,Faith,Self,"Heals a target friendly mon (Aurox included) from a status condition. If successful, Aurox heals {HEAL_PERCENT} of max HP and the target gains +1 stamina (this can exceed the stamina maximum). If the target has no status, the cast does nothing but still costs stamina.",,,self-mon,0,HEAL_PERCENT=50,none Iron Wall,Aurox,0,3,100,0,Metal,Self,"Until Aurox switches out, regenerate {HEAL_PERCENT} of all damage taken, its own recoil included. When activated for the first time each switch-in, Aurox heals {INITIAL_HEAL_PERCENT} of max HP.",,,none,0,HEAL_PERCENT=50;INITIAL_HEAL_PERCENT=20,self-only Bull Rush,Aurox,140,2,100,0,Metal,Physical,Deals damage. Also deals {SELF_DAMAGE_PERCENT} of max HP to self.,,,none,0,SELF_DAMAGE_PERCENT=20,opponent-slot -Big Bellow,Aurox,0,1,100,0,Metal,Other,Lowers the target's SpATK and Speed by 50%.,,,opponent-mon,6,,any-other-slot +Big Bellow,Aurox,0,1,100,0,Metal,Other,Lowers the target's SpATK and Speed by 50%.,,,none,6,,any-other-slot Contagious Slumber,Xmon,0,2,100,0,Cosmic,Other,Inflicts Sleep on self and opponent.,,,none,0,,opponent-slot Vital Siphon,Xmon,40,2,90,0,Cosmic,Special,"Deals damage, {STAMINA_STEAL_PERCENT} chance to steal 1 stamina from opponent.",,,none,0,STAMINA_STEAL_PERCENT=50,opponent-slot Somniphobia,Xmon,0,1,100,0,Cosmic,Other,"For 4 rounds, any opposing mon that gains stamina takes {DAMAGE_DENOM:frac} of its max HP per stack as damage. Repeated casts while active increase the damage but do not reset the cooldown.",,,none,0,DAMAGE_DENOM=8,self-only diff --git a/processing/generateSolidity.py b/processing/generateSolidity.py index f24b0097..26484482 100644 --- a/processing/generateSolidity.py +++ b/processing/generateSolidity.py @@ -290,6 +290,7 @@ def load_json_move(mon_name: str, move_name: str, base_path: str) -> Optional[di MOVE_META_TAG = 1 << 160 MOVE_CONTEXT_STATUS_LANES = 1 << 161 +MOVE_RESOLVER_TAG = 1 << 162 MOVE_META_DYNAMIC = 0xF DEFAULT_PRIORITY = 3 @@ -333,6 +334,8 @@ def deployed_move_meta(mon_name: str, move_name: str, base_path: str) -> int: content = f.read() if re.search(r"@move-context:\s*status-lanes\b", content): context_bits |= MOVE_CONTEXT_STATUS_LANES + if re.search(r"@move-resolver\b", content): + context_bits |= MOVE_RESOLVER_TAG stamina_val = _extract_static_int(content, "stamina", "STAMINA_COST") priority_val = _extract_static_int(content, "priority", "PRIORITY") if stamina_val is None or not 0 <= stamina_val < MOVE_META_DYNAMIC: diff --git a/script/SetupMons.s.sol b/script/SetupMons.s.sol index d366adc1..1e9c0a1d 100644 --- a/script/SetupMons.s.sol +++ b/script/SetupMons.s.sol @@ -228,7 +228,7 @@ contract SetupMons is Script { moves[0] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); moves[2] = 0x5009200000000000000000000000000000000000000000000000000000000000; - moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[3] = 0x0030200000000000000000050000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); uint256[] memory abilities = new uint256[](1); abilities[0] = uint256(uint160(addrs[4])); diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json index 06a0a5b4..800141e4 100644 --- a/snapshots/MainnetReplayGasTest.json +++ b/snapshots/MainnetReplayGasTest.json @@ -5,14 +5,91 @@ "MainnetDoublesReplay1_Reused_effectDataWrites": "0", "MainnetDoublesReplay1_Reused_effectHeaderReads": "45", "MainnetDoublesReplay1_Reused_effectHeaderWrites": "14", - "MainnetDoublesReplay1_Reused_execGas": "1210530", + "MainnetDoublesReplay1_Reused_effectResolverCalls": "0", + "MainnetDoublesReplay1_Reused_engineCallbackStorageReads": "251", + "MainnetDoublesReplay1_Reused_engineCallbackStorageWrites": "43", + "MainnetDoublesReplay1_Reused_engineRootStorageReads": "1819", + "MainnetDoublesReplay1_Reused_engineRootStorageWrites": "118", + "MainnetDoublesReplay1_Reused_engineStorageLoadCommitFloorOps": "83", + "MainnetDoublesReplay1_Reused_engineStorageReads": "2070", + "MainnetDoublesReplay1_Reused_engineStorageRemovableOps": "2148", + "MainnetDoublesReplay1_Reused_engineStorageUniqueTouchedSlots": "55", + "MainnetDoublesReplay1_Reused_engineStorageUniqueWrittenSlots": "28", + "MainnetDoublesReplay1_Reused_engineStorageWrites": "161", + "MainnetDoublesReplay1_Reused_execGas": "1208482", + "MainnetDoublesReplay1_Reused_externalStorageReads": "6", + "MainnetDoublesReplay1_Reused_externalStorageWrites": "0", + "MainnetDoublesReplay1_Reused_headerFrameCallbackPassthroughOps": "144", + "MainnetDoublesReplay1_Reused_headerFrameCleanInvalidationBoundaries": "31", + "MainnetDoublesReplay1_Reused_headerFrameCommitsWithLegacy": "27", + "MainnetDoublesReplay1_Reused_headerFrameDirtyFlushBoundaries": "13", + "MainnetDoublesReplay1_Reused_headerFrameLoadsWithLegacy": "191", + "MainnetDoublesReplay1_Reused_headerFrameReloadedWords": "184", + "MainnetDoublesReplay1_Reused_headerFrameRemovableStorageOpsWithLegacy": "909", + "MainnetDoublesReplay1_Reused_headerFrameStorageOpsWithLegacy": "362", + "MainnetDoublesReplay1_Reused_legacyAbilityBoundaries": "2", + "MainnetDoublesReplay1_Reused_legacyBoundaryUpperBound": "44", + "MainnetDoublesReplay1_Reused_legacyEffectBoundaries": "35", + "MainnetDoublesReplay1_Reused_legacyMoveBoundaries": "7", "MainnetDoublesReplay1_Reused_lifecycleCalls": "34", + "MainnetDoublesReplay1_Reused_monStateCleanInvalidationBoundaries": "14", + "MainnetDoublesReplay1_Reused_monStateDirtyFlushBoundaries": "30", + "MainnetDoublesReplay1_Reused_monStateFrameCommitsWithLegacy": "55", + "MainnetDoublesReplay1_Reused_monStateFrameLoadsWithLegacy": "145", + "MainnetDoublesReplay1_Reused_monStateFrameReloadedLanes": "137", + "MainnetDoublesReplay1_Reused_monStateFrameStorageOps": "16", + "MainnetDoublesReplay1_Reused_monStateFrameStorageOpsWithLegacy": "200", + "MainnetDoublesReplay1_Reused_monStateReads": "611", + "MainnetDoublesReplay1_Reused_monStateRemovableStorageOps": "668", + "MainnetDoublesReplay1_Reused_monStateRemovableStorageOpsWithLegacy": "484", + "MainnetDoublesReplay1_Reused_monStateUniqueTouchedLanes": "8", + "MainnetDoublesReplay1_Reused_monStateUniqueWrittenLanes": "8", + "MainnetDoublesReplay1_Reused_monStateWrites": "73", "MainnetDoublesReplay1_Reused_moveDecisionCallbacks": "0", + "MainnetDoublesReplay1_Reused_moveResolverCalls": "0", + "MainnetDoublesReplay1_Reused_moveResolverContinuations": "0", "MainnetDoublesReplay1_Reused_nonHookHeaderReadUpperBound": "11", "MainnetDoublesReplay1_Reused_preDamageCalls": "0", "MainnetDoublesReplay1_Reused_roundEndCalls": "19", "MainnetDoublesReplay1_Reused_roundStartCalls": "0", "MainnetDoublesReplay1_Reused_statusClassCallbacks": "0", + "MainnetDoublesReplay1_Reused_storage_battleDataReads": "788", + "MainnetDoublesReplay1_Reused_storage_battleDataUniqueTouched": "2", + "MainnetDoublesReplay1_Reused_storage_battleDataUniqueWritten": "2", + "MainnetDoublesReplay1_Reused_storage_battleDataWrites": "38", + "MainnetDoublesReplay1_Reused_storage_boostsReads": "0", + "MainnetDoublesReplay1_Reused_storage_boostsUniqueTouched": "2", + "MainnetDoublesReplay1_Reused_storage_boostsUniqueWritten": "2", + "MainnetDoublesReplay1_Reused_storage_boostsWrites": "2", + "MainnetDoublesReplay1_Reused_storage_configHeaderReads": "415", + "MainnetDoublesReplay1_Reused_storage_configHeaderUniqueTouched": "5", + "MainnetDoublesReplay1_Reused_storage_configHeaderUniqueWritten": "5", + "MainnetDoublesReplay1_Reused_storage_configHeaderWrites": "30", + "MainnetDoublesReplay1_Reused_storage_effectsReads": "45", + "MainnetDoublesReplay1_Reused_storage_effectsUniqueTouched": "8", + "MainnetDoublesReplay1_Reused_storage_effectsUniqueWritten": "8", + "MainnetDoublesReplay1_Reused_storage_effectsWrites": "14", + "MainnetDoublesReplay1_Reused_storage_firstOtherSlot": "0", + "MainnetDoublesReplay1_Reused_storage_globalKVReads": "7", + "MainnetDoublesReplay1_Reused_storage_globalKVUniqueTouched": "3", + "MainnetDoublesReplay1_Reused_storage_globalKVUniqueWritten": "3", + "MainnetDoublesReplay1_Reused_storage_globalKVWrites": "4", + "MainnetDoublesReplay1_Reused_storage_monStateReads": "611", + "MainnetDoublesReplay1_Reused_storage_monStateUniqueTouched": "8", + "MainnetDoublesReplay1_Reused_storage_monStateUniqueWritten": "8", + "MainnetDoublesReplay1_Reused_storage_monStateWrites": "73", + "MainnetDoublesReplay1_Reused_storage_otherReads": "0", + "MainnetDoublesReplay1_Reused_storage_otherUniqueTouched": "0", + "MainnetDoublesReplay1_Reused_storage_otherUniqueWritten": "0", + "MainnetDoublesReplay1_Reused_storage_otherWrites": "0", + "MainnetDoublesReplay1_Reused_storage_shellReads": "1", + "MainnetDoublesReplay1_Reused_storage_shellUniqueTouched": "1", + "MainnetDoublesReplay1_Reused_storage_shellUniqueWritten": "0", + "MainnetDoublesReplay1_Reused_storage_shellWrites": "0", + "MainnetDoublesReplay1_Reused_storage_staticCatalogReads": "203", + "MainnetDoublesReplay1_Reused_storage_staticCatalogUniqueTouched": "26", + "MainnetDoublesReplay1_Reused_storage_staticCatalogUniqueWritten": "0", + "MainnetDoublesReplay1_Reused_storage_staticCatalogWrites": "0", "MainnetDoublesReplay1_Reused_updateStateCalls": "2", "MainnetReplay_Reused_afterDamageCalls": "9", "MainnetReplay_Reused_afterMoveCalls": "19", @@ -20,13 +97,90 @@ "MainnetReplay_Reused_effectDataWrites": "0", "MainnetReplay_Reused_effectHeaderReads": "191", "MainnetReplay_Reused_effectHeaderWrites": "39", - "MainnetReplay_Reused_execGas": "2136527", + "MainnetReplay_Reused_effectResolverCalls": "19", + "MainnetReplay_Reused_engineCallbackStorageReads": "1052", + "MainnetReplay_Reused_engineCallbackStorageWrites": "199", + "MainnetReplay_Reused_engineRootStorageReads": "1956", + "MainnetReplay_Reused_engineRootStorageWrites": "212", + "MainnetReplay_Reused_engineStorageLoadCommitFloorOps": "135", + "MainnetReplay_Reused_engineStorageReads": "3008", + "MainnetReplay_Reused_engineStorageRemovableOps": "3284", + "MainnetReplay_Reused_engineStorageUniqueTouchedSlots": "84", + "MainnetReplay_Reused_engineStorageUniqueWrittenSlots": "51", + "MainnetReplay_Reused_engineStorageWrites": "411", + "MainnetReplay_Reused_execGas": "2107241", + "MainnetReplay_Reused_externalStorageReads": "18", + "MainnetReplay_Reused_externalStorageWrites": "0", + "MainnetReplay_Reused_headerFrameCallbackPassthroughOps": "535", + "MainnetReplay_Reused_headerFrameCleanInvalidationBoundaries": "68", + "MainnetReplay_Reused_headerFrameCommitsWithLegacy": "60", + "MainnetReplay_Reused_headerFrameDirtyFlushBoundaries": "29", + "MainnetReplay_Reused_headerFrameLoadsWithLegacy": "318", + "MainnetReplay_Reused_headerFrameReloadedWords": "310", + "MainnetReplay_Reused_headerFrameRemovableStorageOpsWithLegacy": "975", + "MainnetReplay_Reused_headerFrameStorageOpsWithLegacy": "913", + "MainnetReplay_Reused_legacyAbilityBoundaries": "2", + "MainnetReplay_Reused_legacyBoundaryUpperBound": "97", + "MainnetReplay_Reused_legacyEffectBoundaries": "78", + "MainnetReplay_Reused_legacyMoveBoundaries": "17", "MainnetReplay_Reused_lifecycleCalls": "84", + "MainnetReplay_Reused_monStateCleanInvalidationBoundaries": "28", + "MainnetReplay_Reused_monStateDirtyFlushBoundaries": "69", + "MainnetReplay_Reused_monStateFrameCommitsWithLegacy": "98", + "MainnetReplay_Reused_monStateFrameLoadsWithLegacy": "158", + "MainnetReplay_Reused_monStateFrameReloadedLanes": "150", + "MainnetReplay_Reused_monStateFrameStorageOps": "16", + "MainnetReplay_Reused_monStateFrameStorageOpsWithLegacy": "256", + "MainnetReplay_Reused_monStateReads": "779", + "MainnetReplay_Reused_monStateRemovableStorageOps": "911", + "MainnetReplay_Reused_monStateRemovableStorageOpsWithLegacy": "671", + "MainnetReplay_Reused_monStateUniqueTouchedLanes": "8", + "MainnetReplay_Reused_monStateUniqueWrittenLanes": "8", + "MainnetReplay_Reused_monStateWrites": "148", "MainnetReplay_Reused_moveDecisionCallbacks": "0", + "MainnetReplay_Reused_moveResolverCalls": "4", + "MainnetReplay_Reused_moveResolverContinuations": "2", "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "107", "MainnetReplay_Reused_preDamageCalls": "0", "MainnetReplay_Reused_roundEndCalls": "56", "MainnetReplay_Reused_roundStartCalls": "0", "MainnetReplay_Reused_statusClassCallbacks": "0", + "MainnetReplay_Reused_storage_battleDataReads": "814", + "MainnetReplay_Reused_storage_battleDataUniqueTouched": "2", + "MainnetReplay_Reused_storage_battleDataUniqueWritten": "1", + "MainnetReplay_Reused_storage_battleDataWrites": "92", + "MainnetReplay_Reused_storage_boostsReads": "46", + "MainnetReplay_Reused_storage_boostsUniqueTouched": "18", + "MainnetReplay_Reused_storage_boostsUniqueWritten": "18", + "MainnetReplay_Reused_storage_boostsWrites": "39", + "MainnetReplay_Reused_storage_configHeaderReads": "899", + "MainnetReplay_Reused_storage_configHeaderUniqueTouched": "6", + "MainnetReplay_Reused_storage_configHeaderUniqueWritten": "5", + "MainnetReplay_Reused_storage_configHeaderWrites": "83", + "MainnetReplay_Reused_storage_effectsReads": "191", + "MainnetReplay_Reused_storage_effectsUniqueTouched": "14", + "MainnetReplay_Reused_storage_effectsUniqueWritten": "14", + "MainnetReplay_Reused_storage_effectsWrites": "39", + "MainnetReplay_Reused_storage_firstOtherSlot": "0", + "MainnetReplay_Reused_storage_globalKVReads": "27", + "MainnetReplay_Reused_storage_globalKVUniqueTouched": "3", + "MainnetReplay_Reused_storage_globalKVUniqueWritten": "2", + "MainnetReplay_Reused_storage_globalKVWrites": "7", + "MainnetReplay_Reused_storage_monStateReads": "779", + "MainnetReplay_Reused_storage_monStateUniqueTouched": "8", + "MainnetReplay_Reused_storage_monStateUniqueWritten": "8", + "MainnetReplay_Reused_storage_monStateWrites": "148", + "MainnetReplay_Reused_storage_otherReads": "0", + "MainnetReplay_Reused_storage_otherUniqueTouched": "0", + "MainnetReplay_Reused_storage_otherUniqueWritten": "0", + "MainnetReplay_Reused_storage_otherWrites": "0", + "MainnetReplay_Reused_storage_shellReads": "2", + "MainnetReplay_Reused_storage_shellUniqueTouched": "3", + "MainnetReplay_Reused_storage_shellUniqueWritten": "3", + "MainnetReplay_Reused_storage_shellWrites": "3", + "MainnetReplay_Reused_storage_staticCatalogReads": "250", + "MainnetReplay_Reused_storage_staticCatalogUniqueTouched": "30", + "MainnetReplay_Reused_storage_staticCatalogUniqueWritten": "0", + "MainnetReplay_Reused_storage_staticCatalogWrites": "0", "MainnetReplay_Reused_updateStateCalls": "0" } \ No newline at end of file diff --git a/src/Constants.sol b/src/Constants.sol index 700176d0..f419117e 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -131,6 +131,7 @@ uint8 constant BATTLE_MODE_MULTI = 2; // untagged with high bits = inline, bare address = deployed without meta (legacy/tests). uint256 constant MOVE_META_TAG = 1 << 160; uint256 constant MOVE_CONTEXT_STATUS_LANES = 1 << 161; +uint256 constant MOVE_RESOLVER_TAG = 1 << 162; uint256 constant MOVE_META_DYNAMIC = 0xF; // The 16-bit move extraData splits [targetBits 4 | movePayload 12]. The nibble is a bitmask diff --git a/src/Engine.sol b/src/Engine.sol index d6cf116b..cc9b9c54 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -10,6 +10,8 @@ import "./moves/IMoveSet.sol"; import {IEngine} from "./IEngine.sol"; import {IAbility} from "./abilities/IAbility.sol"; import {SignedCommitLib} from "./commit-manager/SignedCommitLib.sol"; +import {EffectCommandLib} from "./effects/EffectCommandLib.sol"; +import {IEffectResolver} from "./effects/IEffectResolver.sol"; import {IStatusEffect} from "./effects/status/IStatusEffect.sol"; import {ECDSA} from "./lib/ECDSA.sol"; import {EIP712} from "./lib/EIP712.sol"; @@ -19,6 +21,8 @@ import {StatBoostLib} from "./lib/StatBoostLib.sol"; import {TargetLib} from "./lib/TargetLib.sol"; import {ValidatorLogic} from "./lib/ValidatorLogic.sol"; import {AttackCalculator} from "./moves/AttackCalculator.sol"; +import {IMoveResolver} from "./moves/IMoveResolver.sol"; +import {MoveCommandLib} from "./moves/MoveCommandLib.sol"; import {TypeCalcLib} from "./types/TypeCalcLib.sol"; contract Engine is IEngine, MappingAllocator, EIP712 { @@ -2051,16 +2055,18 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return ((targetIndex << 3) | monIndex) << 4; } - uint256 private constant EFFECT_COMPACT_DATA_BITS = 79; + uint16 private constant EFFECT_RESOLVER_METADATA_BIT = uint16(1) << 15; + uint256 private constant EFFECT_COMPACT_DATA_BITS = 78; uint256 private constant EFFECT_COMPACT_DATA_MASK = (uint256(1) << EFFECT_COMPACT_DATA_BITS) - 1; + uint256 private constant EFFECT_RESOLVER_FLAG = uint256(1) << 254; uint256 private constant EFFECT_HOOK_CONTEXT_FLAG = uint256(1) << 255; - uint256 private constant EFFECT_HEADER_BASE_MASK = uint256(type(uint176).max) | EFFECT_HOOK_CONTEXT_FLAG; + uint256 private constant EFFECT_HEADER_BASE_MASK = + uint256(type(uint176).max) | EFFECT_RESOLVER_FLAG | EFFECT_HOOK_CONTEXT_FLAG; uint256 private constant FRESH_HOOK_CONTEXT_REQUEST = uint256(1) << 255; - /// @dev Effect header layout: address[0:160], steps[160:176], compact-data marker[176:255], - /// fresh-hook-context capability[255]. A nonzero marker stores `data + 1`; zero means the full - /// bytes32 lives in slot 1. Reserving one capability bit leaves 79 compact data bits. Once opted - /// in, an effect receives whichever supported fresh context corresponds to its current hook. + /// @dev Effect header layout: address[0:160], steps[160:176], compact-data marker[176:254], + /// resolver capability[254], fresh-hook-context capability[255]. A nonzero marker stores + /// `data + 1`; zero means the full bytes32 lives in slot 1. function _effectData(EffectInstance storage effectInstance) private view returns (bytes32 data) { uint256 header; assembly ("memory-safe") { @@ -2106,8 +2112,11 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 encoded = raw < EFFECT_COMPACT_DATA_MASK ? raw + 1 : 0; // OnApply has already run before installation. Do not persist an OnApply-only request as // the broad capability flag, which would make multi-hook statuses build unused contexts. - uint16 storedContextBitmap = hookContextBitmap & ~uint16(1 << uint8(EffectStep.OnApply)); + bool usesResolver = hookContextBitmap & EFFECT_RESOLVER_METADATA_BIT != 0; + uint16 storedContextBitmap = hookContextBitmap + & ~uint16((1 << uint8(EffectStep.OnApply)) | EFFECT_RESOLVER_METADATA_BIT); uint256 header = uint256(uint160(address(effect))) | (uint256(stepsBitmap) << 160) | (encoded << 176) + | (usesResolver ? EFFECT_RESOLVER_FLAG : 0) | (storedContextBitmap != 0 ? EFFECT_HOOK_CONTEXT_FLAG : 0); assembly ("memory-safe") { sstore(effectInstance.slot, header) @@ -2120,7 +2129,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { function _effectDataAndHookContext(EffectInstance storage effectInstance) private view - returns (bytes32 data, bool needsFreshContext) + returns (bytes32 data, bool needsFreshContext, bool usesResolver) { uint256 header; assembly ("memory-safe") { @@ -2129,6 +2138,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 encoded = (header >> 176) & EFFECT_COMPACT_DATA_MASK; data = encoded == 0 ? effectInstance.data : bytes32(encoded - 1); needsFreshContext = header & EFFECT_HOOK_CONTEXT_FLAG != 0; + usesResolver = header & EFFECT_RESOLVER_FLAG != 0; } function _monEffectSteps(BattleConfig storage config, uint256 targetIndex, uint256 monIndex) @@ -2162,10 +2172,17 @@ contract Engine is IEngine, MappingAllocator, EIP712 { external returns (bool cleared) { - bytes32 battleKey = battleKeyForWrite; - if (battleKey == bytes32(0)) { + if (battleKeyForWrite == bytes32(0)) { revert NoWriteAllowed(); } + return _clearMonStatus(targetIndex, monIndex, expectedClass); + } + + function _clearMonStatus(uint256 targetIndex, uint256 monIndex, uint256 expectedClass) + private + returns (bool cleared) + { + bytes32 battleKey = battleKeyForWrite; BattleConfig storage config = battleConfig[storageKeyForWrite]; uint256 lane = (config.monStatusLanes >> _statusLaneShift(targetIndex, monIndex)) & 0xF; if (lane == 0 || (expectedClass != 0 && lane != expectedClass)) { @@ -2882,6 +2899,85 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ); } + function _executeResolvedMove( + BattleConfig storage config, + BattleData storage battle, + uint256 rawMoveSlot, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint256 activesPacked, + uint16 extraData, + uint256 rng + ) private { + IMoveResolver resolver = IMoveResolver(address(uint160(rawMoveSlot))); + (uint256 command, uint32 continuation) = resolver.resolveMove( + 0, 0, attackerPlayerIndex, attackerMonIndex, targetBits, activesPacked, extraData, rng + ); + int32 result = _executeResolvedMoveCommand( + config, battle, rawMoveSlot, attackerPlayerIndex, attackerMonIndex, targetBits, command, rng + ); + if (continuation != 0) { + (command,) = resolver.resolveMove( + continuation, + result, + attackerPlayerIndex, + attackerMonIndex, + targetBits, + activesPacked, + extraData, + rng + ); + _executeResolvedMoveCommand( + config, battle, rawMoveSlot, attackerPlayerIndex, attackerMonIndex, targetBits, command, rng + ); + } + } + + function _executeResolvedMoveCommand( + BattleConfig storage config, + BattleData storage battle, + uint256 rawMoveSlot, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint256 command, + uint256 rng + ) private returns (int32 result) { + uint256 op = uint8(command); + if (op == MoveCommandLib.OP_ATTACK) { + uint256 targetSlot = TargetLib.lowestSlot(targetBits); + if (targetSlot == NO_SLOT) { + return 0; + } + uint256 defenderMonIndex = _slotActive(battle, targetSlot); + if (defenderMonIndex == EMPTY_ACTIVE_LANE) { + return 0; + } + (result,) = _dispatchStandardAttackInternal( + config, + attackerPlayerIndex, + attackerMonIndex, + targetSlot >> 1, + defenderMonIndex, + uint32(command >> 8), + uint8(command >> 40), + uint8(command >> 48), + Type(uint8(command >> 56)), + MoveClass(uint8(command >> 64)), + uint8(command >> 72), + uint8(command >> 80), + IEffect(address(uint160(command >> 88))), + rng, + uint256(uint160(rawMoveSlot)) + ); + } else if (op == MoveCommandLib.OP_SWITCH) { + _switchActiveMonForSlotInternal( + uint8(command >> 8), uint8(command >> 16), uint8(command >> 24), config, battle + ); + } + } + function dispatchStandardAttack( uint256 attackerPlayerIndex, uint256 attackerMonIndex, @@ -3009,6 +3105,17 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } BattleConfig storage config = battleConfig[storageKeyForWrite]; BattleData storage battle = battleData[battleKey]; + _switchActiveMonForSlotInternal(playerIndex, slotIndex, monToSwitchIndex, config, battle); + } + + function _switchActiveMonForSlotInternal( + uint256 playerIndex, + uint256 slotIndex, + uint256 monToSwitchIndex, + BattleConfig storage config, + BattleData storage battle + ) private { + bytes32 battleKey = battleKeyForWrite; // Flow-aware like the other slot surfaces: singles routes through the legacy switch // (whose validation has no ally lane to collide with). if (!battle.isTwoSlotMode) { @@ -3432,16 +3539,31 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (rawMoveSlot & MOVE_CONTEXT_STATUS_LANES != 0) { moveContext |= uint256(config.monStatusLanes) << 132; } - moveSet.move( - self, - battleKey, - playerIndex, - activeMonIndex, - TargetLib.impliedSinglesTargetBits(playerIndex), - moveContext, - move.extraData & EXTRA_DATA_PAYLOAD_MASK, - tempRNG - ); + uint256 targetBits = TargetLib.impliedSinglesTargetBits(playerIndex); + if (rawMoveSlot & MOVE_RESOLVER_TAG != 0) { + _executeResolvedMove( + config, + battle, + rawMoveSlot, + playerIndex, + activeMonIndex, + targetBits, + moveContext, + move.extraData & EXTRA_DATA_PAYLOAD_MASK, + tempRNG + ); + } else { + moveSet.move( + self, + battleKey, + playerIndex, + activeMonIndex, + targetBits, + moveContext, + move.extraData & EXTRA_DATA_PAYLOAD_MASK, + tempRNG + ); + } } } @@ -3534,7 +3656,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Skip tombstones AND entries that don't listen at this step before reading slot-1 data. if (address(eff.effect) != TOMBSTONE_ADDRESS && (eff.stepsBitmap & (1 << uint8(round))) != 0) { - (bytes32 effectData, bool needsFreshContext) = _effectDataAndHookContext(eff); + (bytes32 effectData, bool needsFreshContext, bool usesResolver) = _effectDataAndHookContext(eff); uint256 hookContext = activesPacked; if (needsFreshContext) { if (round == EffectStep.RoundEnd) { @@ -3568,7 +3690,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { eff.effect, effectData, uint96(slotIndex), - hookContext + hookContext, + usesResolver ); // Re-read count if a new effect was added during this iteration @@ -3595,12 +3718,20 @@ contract Engine is IEngine, MappingAllocator, EIP712 { IEffect effect, bytes32 data, uint96 slotIndex, - uint256 activesPacked + uint256 activesPacked, + bool usesResolver ) private { // Run the effect and get result - (bytes32 updatedExtraData, bool removeAfterRun) = _executeEffectHook( - battleKeyForWrite, effect, rng, data, playerIndex, monIndex, round, extraEffectsData, activesPacked - ); + bytes32 updatedExtraData; + bool removeAfterRun; + if (round == EffectStep.AfterMove && usesResolver) { + (updatedExtraData, removeAfterRun) = + _executeResolvedEffect(effect, rng, data, playerIndex, monIndex, round, activesPacked); + } else { + (updatedExtraData, removeAfterRun) = _executeEffectHook( + battleKeyForWrite, effect, rng, data, playerIndex, monIndex, round, extraEffectsData, activesPacked + ); + } // If we need to remove or update the effect if (removeAfterRun || updatedExtraData != data) { @@ -3610,6 +3741,59 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } } + function _executeResolvedEffect( + IEffect effect, + uint256 rng, + bytes32 data, + uint256 playerIndex, + uint256 monIndex, + EffectStep round, + uint256 activesPacked + ) private returns (bytes32 updatedExtraData, bool removeAfterRun) { + uint256 command; + (updatedExtraData, removeAfterRun, command) = IEffectResolver(address(effect)).resolveEffect( + round, rng, data, playerIndex, monIndex, _freshHookContext(round, activesPacked) + ); + _applyEffectCommand(command); + } + + /// @dev Builds the dispatch-time portion shared by the legacy hook path and resolver prototype. + function _freshHookContext(EffectStep round, uint256 activesPacked) private view returns (uint256) { + if (activesPacked & FRESH_HOOK_CONTEXT_REQUEST == 0) { + return activesPacked; + } + activesPacked &= ~FRESH_HOOK_CONTEXT_REQUEST; + if (round == EffectStep.AfterMove) { + BattleData storage battle = battleData[battleKeyForWrite]; + if (battle.isTwoSlotMode) { + activesPacked |= _currentTurnMoveWordForSlot(0, 0) << 32; + activesPacked |= _currentTurnMoveWordForSlot(0, 1) << 56; + activesPacked |= _currentTurnMoveWordForSlot(1, 0) << 80; + activesPacked |= _currentTurnMoveWordForSlot(1, 1) << 104; + } else { + BattleConfig storage config = battleConfig[storageKeyForWrite]; + activesPacked |= _currentTurnMoveWord(config, 0) << 32; + activesPacked |= _currentTurnMoveWord(config, 1) << 80; + } + activesPacked |= actedSlotsThisTurnMask << 128; + } + return activesPacked; + } + + function _applyEffectCommand(uint256 command) private { + uint256 op = uint8(command); + if (op == 0) { + return; + } + uint256 targetIndex = uint8(command >> 8); + uint256 monIndex = uint8(command >> 16); + if (op == EffectCommandLib.OP_CLEAR_STATUS) { + _clearMonStatus(targetIndex, monIndex, uint8(command >> 24)); + } else if (op == EffectCommandLib.OP_ADD_EFFECT) { + _addEffectInternal(targetIndex, monIndex, IEffect(address(uint160(command >> 24))), bytes32(0)); + } + } + function _executeEffectHook( bytes32 battleKey, IEffect effect, @@ -4616,8 +4800,20 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (rawMoveSlot & MOVE_CONTEXT_STATUS_LANES != 0) { moveContext |= uint256(config.monStatusLanes) << 132; } - IMoveSet(address(uint160(rawMoveSlot))) - .move( + if (rawMoveSlot & MOVE_RESOLVER_TAG != 0) { + _executeResolvedMove( + config, + battle, + rawMoveSlot, + side, + activeMon, + targetBits, + moveContext, + uint16(moveExtra & EXTRA_DATA_PAYLOAD_MASK), + actionRng + ); + } else { + IMoveSet(address(uint160(rawMoveSlot))).move( IEngine(address(this)), battleKey, side, @@ -4627,6 +4823,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint16(moveExtra & EXTRA_DATA_PAYLOAD_MASK), actionRng ); + } } } diff --git a/src/effects/EffectCommandLib.sol b/src/effects/EffectCommandLib.sol new file mode 100644 index 00000000..af5a7347 --- /dev/null +++ b/src/effects/EffectCommandLib.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {IEffect} from "./IEffect.sol"; + +/// @notice Compact prototype command encoding for opted-in read-only effect resolvers. +library EffectCommandLib { + uint256 internal constant OP_CLEAR_STATUS = 1; + uint256 internal constant OP_ADD_EFFECT = 2; + + function clearStatus(uint256 targetIndex, uint256 monIndex, uint256 statusClass) + internal + pure + returns (uint256) + { + return OP_CLEAR_STATUS | (targetIndex << 8) | (monIndex << 16) | (statusClass << 24); + } + + function addEffect(uint256 targetIndex, uint256 monIndex, IEffect effect) internal pure returns (uint256) { + return OP_ADD_EFFECT | (targetIndex << 8) | (monIndex << 16) | (uint256(uint160(address(effect))) << 24); + } + +} diff --git a/src/effects/IEffectResolver.sol b/src/effects/IEffectResolver.sol new file mode 100644 index 00000000..088053a6 --- /dev/null +++ b/src/effects/IEffectResolver.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {EffectStep} from "../Enums.sol"; + +/// @notice Optional read-only effect hook that returns one Engine-validated mutation command. +interface IEffectResolver { + function resolveEffect( + EffectStep step, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 hookContext + ) external view returns (bytes32 updatedExtraData, bool removeAfterRun, uint256 command); +} + diff --git a/src/kernel/PackedHeaderFrameLib.sol b/src/kernel/PackedHeaderFrameLib.sol new file mode 100644 index 00000000..35c0034c --- /dev/null +++ b/src/kernel/PackedHeaderFrameLib.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {BattleConfig, BattleData} from "../Structs.sol"; + +/// @notice Lazy memory frame for the persistent packed battle headers. +/// @dev Word indices 0..1 are BattleData; 2..7 are BattleConfig offsets 0..5; index 8 is +/// BattleConfig offset 18 (`playerEffectStepsByMon`). This prototype deliberately exposes +/// raw words: typed accessors can be generated once the routing experiment fixes the schema. +library PackedHeaderFrameLib { + uint256 internal constant WORD_COUNT = 9; + uint256 internal constant BATTLE_STATIC_WORD = 0; + uint256 internal constant BATTLE_STATE_WORD = 1; + + uint256 private constant ADDRESS_MASK = type(uint160).max; + uint256 private constant WINNER_SHIFT = 160; + uint256 private constant SWITCH_FLAG_SHIFT = 168; + uint256 private constant ACTIVE_MON_SHIFT = 176; + uint256 private constant TIMESTAMP_SHIFT = 192; + uint256 private constant TURN_ID_SHIFT = 232; + uint256 private constant NUM_BUFFERED_SHIFT = 248; + + error HeaderWordOutOfBounds(); + error DirtyHeaderFrame(); + + struct Frame { + uint256 battleBaseSlot; + uint256 configBaseSlot; + uint256[WORD_COUNT] words; + uint16 loadedMask; + uint16 dirtyMask; + } + + function init(BattleData storage battle, BattleConfig storage config) + internal + pure + returns (Frame memory frame) + { + assembly ("memory-safe") { + mstore(frame, battle.slot) + mstore(add(frame, 0x20), config.slot) + } + } + + function read(Frame memory frame, uint256 wordIndex) internal view returns (uint256 value) { + uint16 bit = _wordBit(wordIndex); + if (frame.loadedMask & bit == 0) { + value = _sload(_storageSlot(frame, wordIndex)); + frame.words[wordIndex] = value; + frame.loadedMask |= bit; + } else { + value = frame.words[wordIndex]; + } + } + + /// @notice Replace a complete packed word without paying a load first. + function write(Frame memory frame, uint256 wordIndex, uint256 value) internal pure { + uint16 bit = _wordBit(wordIndex); + frame.words[wordIndex] = value; + frame.loadedMask |= bit; + frame.dirtyMask |= bit; + } + + function p1(Frame memory frame) internal view returns (address) { + return address(uint160(read(frame, BATTLE_STATIC_WORD))); + } + + function p0(Frame memory frame) internal view returns (address) { + return address(uint160(read(frame, BATTLE_STATE_WORD))); + } + + function winnerIndex(Frame memory frame) internal view returns (uint8) { + return uint8(read(frame, BATTLE_STATE_WORD) >> WINNER_SHIFT); + } + + function playerSwitchForTurnFlag(Frame memory frame) internal view returns (uint8) { + return uint8(read(frame, BATTLE_STATE_WORD) >> SWITCH_FLAG_SHIFT); + } + + function activeMonIndex(Frame memory frame) internal view returns (uint16) { + return uint16(read(frame, BATTLE_STATE_WORD) >> ACTIVE_MON_SHIFT); + } + + function lastExecuteTimestamp(Frame memory frame) internal view returns (uint40) { + return uint40(read(frame, BATTLE_STATE_WORD) >> TIMESTAMP_SHIFT); + } + + function turnId(Frame memory frame) internal view returns (uint16) { + return uint16(read(frame, BATTLE_STATE_WORD) >> TURN_ID_SHIFT); + } + + function numBuffered(Frame memory frame) internal view returns (uint8) { + return uint8(read(frame, BATTLE_STATE_WORD) >> NUM_BUFFERED_SHIFT); + } + + function setWinnerIndex(Frame memory frame, uint8 value) internal view { + _replaceBattleStateBits(frame, uint256(value), type(uint8).max, WINNER_SHIFT); + } + + function setActiveMonIndex(Frame memory frame, uint16 value) internal view { + _replaceBattleStateBits(frame, uint256(value), type(uint16).max, ACTIVE_MON_SHIFT); + } + + /// @notice Apply the normal non-game-over end-of-turn BattleData transition in one word. + function advanceTurn(Frame memory frame, uint8 switchFlag, uint40 timestamp) internal view { + uint256 word = read(frame, BATTLE_STATE_WORD); + uint16 nextTurnId = uint16(word >> TURN_ID_SHIFT) + 1; + uint256 clearMask = ~( + (uint256(type(uint8).max) << SWITCH_FLAG_SHIFT) + | (uint256(type(uint40).max) << TIMESTAMP_SHIFT) + | (uint256(type(uint16).max) << TURN_ID_SHIFT) + ); + word = (word & clearMask) | (uint256(switchFlag) << SWITCH_FLAG_SHIFT) + | (uint256(timestamp) << TIMESTAMP_SHIFT) | (uint256(nextTurnId) << TURN_ID_SHIFT); + write(frame, BATTLE_STATE_WORD, word); + } + + function flush(Frame memory frame) internal { + uint16 dirty = frame.dirtyMask; + while (dirty != 0) { + uint256 wordIndex = _leastSignificantBitIndex(dirty); + _sstore(_storageSlot(frame, wordIndex), frame.words[wordIndex]); + dirty &= dirty - 1; + } + frame.dirtyMask = 0; + } + + /// @notice Invalidate after a legacy mechanic returns. Dirty words must be flushed first. + function invalidate(Frame memory frame) internal pure { + if (frame.dirtyMask != 0) revert DirtyHeaderFrame(); + frame.loadedMask = 0; + } + + function flushAndInvalidate(Frame memory frame) internal { + flush(frame); + frame.loadedMask = 0; + } + + function _replaceBattleStateBits(Frame memory frame, uint256 value, uint256 valueMask, uint256 shift) + private + view + { + uint256 word = read(frame, BATTLE_STATE_WORD); + uint256 shiftedMask = valueMask << shift; + write(frame, BATTLE_STATE_WORD, (word & ~shiftedMask) | ((value & valueMask) << shift)); + } + + function _storageSlot(Frame memory frame, uint256 wordIndex) private pure returns (uint256 slot) { + if (wordIndex < 2) return frame.battleBaseSlot + wordIndex; + if (wordIndex < 8) return frame.configBaseSlot + wordIndex - 2; + if (wordIndex == 8) return frame.configBaseSlot + 18; + revert HeaderWordOutOfBounds(); + } + + function _wordBit(uint256 wordIndex) private pure returns (uint16 bit) { + if (wordIndex >= WORD_COUNT) revert HeaderWordOutOfBounds(); + bit = uint16(1 << wordIndex); + } + + function _leastSignificantBitIndex(uint16 value) private pure returns (uint256 index) { + while (value & 1 == 0) { + value >>= 1; + index++; + } + } + + function _sload(uint256 slot) private view returns (uint256 value) { + assembly ("memory-safe") { + value := sload(slot) + } + } + + function _sstore(uint256 slot, uint256 value) private { + assembly ("memory-safe") { + sstore(slot, value) + } + } +} diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index 8ce091de..136fe663 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -12,14 +12,16 @@ import { STATUS_CLASS_SHIFT, SWITCH_MOVE_INDEX } from "../../Constants.sol"; -import {MonStateIndexName, StatBoostFlag, StatBoostType} from "../../Enums.sol"; +import {EffectStep, MonStateIndexName, StatBoostFlag, StatBoostType} from "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; import {EffectInstance, IEffect, StatBoostToApply} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; import {BasicEffect} from "../../effects/BasicEffect.sol"; +import {EffectCommandLib} from "../../effects/EffectCommandLib.sol"; +import {IEffectResolver} from "../../effects/IEffectResolver.sol"; import {TargetLib} from "../../lib/TargetLib.sol"; -contract Tinderclaws is IAbility, BasicEffect { +contract Tinderclaws is IAbility, BasicEffect, IEffectResolver { uint256 constant BURN_CHANCE = 3; // 1 in 3 chance uint8 constant SP_ATTACK_BOOST_PERCENT = 50; @@ -49,7 +51,36 @@ contract Tinderclaws is IAbility, BasicEffect { // Steps: RoundEnd, AfterMove function getStepsBitmap() external pure override returns (uint32) { // Low 16 bits: lifecycle steps. High 16 bits: requested fresh-context steps. - return 0x00848084; // RoundEnd+AfterMove context | ALWAYS_APPLIES | AfterMove | RoundEnd + // Bit 31 opts into the read-only command resolver; bits 18/23 request fresh hook context. + return 0x80848084; // resolver | RoundEnd+AfterMove context | ALWAYS_APPLIES | hooks + } + + function resolveEffect( + EffectStep step, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 hookContext + ) external view returns (bytes32 updatedExtraData, bool removeAfterRun, uint256 command) { + if (step == EffectStep.AfterMove) { + uint256 ownSlot = TargetLib.slotOfMon(hookContext, targetIndex, monIndex); + if (ownSlot == NO_SLOT) { + return (extraData, false, 0); + } + uint8 moveIndex = uint8(TargetLib.hookMoveWordAt(hookContext, ownSlot)) & MOVE_INDEX_MASK; + if (moveIndex == NO_OP_MOVE_INDEX) { + command = EffectCommandLib.clearStatus(targetIndex, monIndex, BURN_CLASS); + } else if (moveIndex != SWITCH_MOVE_INDEX) { + rng = uint256(keccak256(abi.encode(rng, targetIndex, monIndex, address(this)))); + if (rng % BURN_CHANCE == BURN_CHANCE - 1) { + command = EffectCommandLib.addEffect(targetIndex, monIndex, BURN_STATUS); + } + } + return (extraData, false, command); + } + + return (extraData, false, 0); } // extraData: 0 = no SpATK boost applied, 1 = SpATK boost applied diff --git a/src/mons/inutia/HitAndDip.sol b/src/mons/inutia/HitAndDip.sol index 4242a31b..eec0132d 100644 --- a/src/mons/inutia/HitAndDip.sol +++ b/src/mons/inutia/HitAndDip.sol @@ -9,11 +9,14 @@ import {IEngine} from "../../IEngine.sol"; import {IEffect} from "../../effects/IEffect.sol"; import {TargetLib} from "../../lib/TargetLib.sol"; +import {IMoveResolver} from "../../moves/IMoveResolver.sol"; +import {MoveCommandLib} from "../../moves/MoveCommandLib.sol"; import {StandardAttack} from "../../moves/StandardAttack.sol"; import {ATTACK_PARAMS} from "../../moves/StandardAttackStructs.sol"; import {ITypeCalculator} from "../../types/ITypeCalculator.sol"; -contract HitAndDip is StandardAttack { +// @move-resolver +contract HitAndDip is StandardAttack, IMoveResolver { constructor(ITypeCalculator TYPE_CALCULATOR) StandardAttack( address(msg.sender), @@ -34,6 +37,39 @@ contract HitAndDip is StandardAttack { ) {} + function resolveMove( + uint32 continuation, + int32 priorResult, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint256 activesPacked, + uint16 extraData, + uint256 + ) external view returns (uint256 command, uint32 nextContinuation) { + if (continuation == 0) { + return ( + MoveCommandLib.attack( + uint32(basePower(bytes32(0))), + uint8(accuracy(bytes32(0))), + uint8(volatility(bytes32(0))), + moveType(IEngine(address(0)), bytes32(0)), + moveClass(IEngine(address(0)), bytes32(0)), + uint8(critRate(bytes32(0))), + uint8(effectAccuracy(bytes32(0))), + effect(bytes32(0)) + ), + 1 + ); + } + if (continuation == 1 && priorResult > 0) { + uint256 slot = TargetLib.slotOfMon(activesPacked, attackerPlayerIndex, attackerMonIndex); + if (slot != NO_SLOT) { + command = MoveCommandLib.switchMon(attackerPlayerIndex, slot & 1, uint256(extraData)); + } + } + } + function move( IEngine engine, bytes32 battleKey, diff --git a/src/moves/IMoveResolver.sol b/src/moves/IMoveResolver.sol new file mode 100644 index 00000000..a9e19607 --- /dev/null +++ b/src/moves/IMoveResolver.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +/// @notice Prototype continuation resolver for external moves. +interface IMoveResolver { + function resolveMove( + uint32 continuation, + int32 priorResult, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint256 activesPacked, + uint16 extraData, + uint256 rng + ) external view returns (uint256 command, uint32 nextContinuation); +} + diff --git a/src/moves/MoveCommandLib.sol b/src/moves/MoveCommandLib.sol new file mode 100644 index 00000000..b0274887 --- /dev/null +++ b/src/moves/MoveCommandLib.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {MoveClass, Type} from "../Enums.sol"; +import {IEffect} from "../effects/IEffect.sol"; + +library MoveCommandLib { + uint256 internal constant OP_ATTACK = 1; + uint256 internal constant OP_SWITCH = 2; + + function attack( + uint32 basePower, + uint8 accuracy, + uint8 volatility, + Type moveType, + MoveClass moveClass, + uint8 critRate, + uint8 effectAccuracy, + IEffect effect + ) internal pure returns (uint256) { + return OP_ATTACK | (uint256(basePower) << 8) | (uint256(accuracy) << 40) | (uint256(volatility) << 48) + | (uint256(uint8(moveType)) << 56) | (uint256(uint8(moveClass)) << 64) | (uint256(critRate) << 72) + | (uint256(effectAccuracy) << 80) | (uint256(uint160(address(effect))) << 88); + } + + function switchMon(uint256 playerIndex, uint256 slotIndex, uint256 monIndex) internal pure returns (uint256) { + return OP_SWITCH | (playerIndex << 8) | (slotIndex << 16) | (monIndex << 24); + } +} + diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 661cf14d..f6bcbb91 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -2825,7 +2825,7 @@ contract EngineTest is Test, BattleHelper { function test_effectDataCompactAndWideFallbackTransitions() public { WideDataEffect wideEffect = new WideDataEffect(); // Exact representation boundary: max-1 is offset-tagged; max falls back to slot 1. - uint256 compactMarkerMax = (uint256(1) << 79) - 1; + uint256 compactMarkerMax = (uint256(1) << 78) - 1; bytes32 compactData = bytes32(compactMarkerMax - 1); bytes32 secondWideData = bytes32(compactMarkerMax); SetEffectDataAttack setCompact = new SetEffectDataAttack(compactData); diff --git a/test/MainnetReplayGasTest.t.sol b/test/MainnetReplayGasTest.t.sol index c0264fe2..1cc1881b 100644 --- a/test/MainnetReplayGasTest.t.sol +++ b/test/MainnetReplayGasTest.t.sol @@ -11,7 +11,9 @@ import {console} from "forge-std/console.sol"; import {SetupMons} from "../script/SetupMons.s.sol"; import {Engine} from "../src/Engine.sol"; import {IEngine} from "../src/IEngine.sol"; +import {IAbility} from "../src/abilities/IAbility.sol"; import {IEffect} from "../src/effects/IEffect.sol"; +import {IEffectResolver} from "../src/effects/IEffectResolver.sol"; import {GachaTeamRegistry} from "../src/game-layer/GachaTeamRegistry.sol"; import {IGachaRNG} from "../src/rng/IGachaRNG.sol"; @@ -30,6 +32,8 @@ import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; import {IRandomnessOracle} from "../src/rng/IRandomnessOracle.sol"; +import {IMoveResolver} from "../src/moves/IMoveResolver.sol"; +import {IMoveSet} from "../src/moves/IMoveSet.sol"; import {GasMeasure} from "./abstract/GasMeasure.sol"; import {sideWord} from "./abstract/SlotWire.sol"; @@ -59,6 +63,12 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { struct ReplayCensus { EffectStorageTally effectStorage; + StorageWorkingSet engineWorkingSet; + CallBoundaryStorageTally callBoundaryStorage; + EngineStorageCategoryTally storageCategories; + MonStateStorageTally monStateStorage; + MonStateFrameModel monStateFrame; + PackedHeaderFrameModel packedHeaderFrame; uint256 roundStartCalls; uint256 roundEndCalls; uint256 afterMoveCalls; @@ -69,6 +79,13 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { uint256 statusClassCallbacks; uint256 lifecycleCalls; uint256 nonHookHeaderReadUpperBound; + uint256 moveResolverCalls; + uint256 moveResolverContinuations; + uint256 effectResolverCalls; + uint256 legacyMoveBoundaries; + uint256 legacyAbilityBoundaries; + uint256 legacyEffectBoundaries; + uint256 legacyBoundaryUpperBound; } // Real rosters from the mainnet config (getBattle). p1 is the CPU side. @@ -275,14 +292,39 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { function _recordReplayCensus( string memory name, + bytes32 battleKey, Vm.AccountAccess[] memory accesses, bytes32 storageKey, uint256 reusedGas ) internal returns (ReplayCensus memory c) { c.effectStorage = _effectStorageTally(accesses, address(engine), storageKey); + c.engineWorkingSet = _storageWorkingSet(accesses, address(engine)); + c.callBoundaryStorage = _callBoundaryStorageTally(accesses, address(engine), address(mgr)); + c.storageCategories = _engineStorageCategoryTally(accesses, address(engine), battleKey, storageKey); + c.monStateStorage = _monStateStorageTally(accesses, address(engine), storageKey); + c.monStateFrame = + _monStateFrameModel(accesses, address(engine), storageKey, _legacyBoundarySelectors()); + c.packedHeaderFrame = _packedHeaderFrameModel( + accesses, + address(engine), + address(mgr), + battleKey, + storageKey, + _legacyBoundarySelectors() + ); + uint256 classifiedHeaderOps = c.storageCategories.reads[STORAGE_BATTLE_DATA] + + c.storageCategories.writes[STORAGE_BATTLE_DATA] + + c.storageCategories.reads[STORAGE_CONFIG_HEADER] + + c.storageCategories.writes[STORAGE_CONFIG_HEADER]; + require(c.packedHeaderFrame.currentStorageOps == classifiedHeaderOps, "header frame census mismatch"); + require( + c.storageCategories.reads[STORAGE_OTHER] + c.storageCategories.writes[STORAGE_OTHER] == 0, + "unclassified Engine storage" + ); c.roundStartCalls = _countCalls(accesses, address(engine), address(0), IEffect.onRoundStart.selector); c.roundEndCalls = _countCalls(accesses, address(engine), address(0), IEffect.onRoundEnd.selector); - c.afterMoveCalls = _countCalls(accesses, address(engine), address(0), IEffect.onAfterMove.selector); + c.afterMoveCalls = _countCalls(accesses, address(engine), address(0), IEffect.onAfterMove.selector) + + _countCalls(accesses, address(engine), address(0), IEffectResolver.resolveEffect.selector); c.preDamageCalls = _countCalls(accesses, address(engine), address(0), IEffect.onPreDamage.selector); c.afterDamageCalls = _countCalls(accesses, address(engine), address(0), IEffect.onAfterDamage.selector); c.updateStateCalls = _countCalls(accesses, address(engine), address(0), IEffect.onUpdateMonState.selector); @@ -293,12 +335,60 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { + c.afterDamageCalls + c.updateStateCalls; c.nonHookHeaderReadUpperBound = c.effectStorage.headerReads > c.lifecycleCalls ? c.effectStorage.headerReads - c.lifecycleCalls : 0; + c.moveResolverCalls = _countCalls(accesses, address(engine), address(0), IMoveResolver.resolveMove.selector); + c.moveResolverContinuations = _countCallsWithNonzeroFirstArg( + accesses, address(engine), address(0), IMoveResolver.resolveMove.selector + ); + c.effectResolverCalls = + _countCalls(accesses, address(engine), address(0), IEffectResolver.resolveEffect.selector); + c.legacyMoveBoundaries = _countCalls(accesses, address(engine), address(0), IMoveSet.move.selector); + c.legacyAbilityBoundaries = + _countCalls(accesses, address(engine), address(0), IAbility.activateOnSwitch.selector); + c.legacyEffectBoundaries = _legacyEffectBoundaryCount(accesses); + c.legacyBoundaryUpperBound = + c.legacyMoveBoundaries + c.legacyAbilityBoundaries + c.legacyEffectBoundaries; vm.snapshotValue(string.concat(name, "_execGas"), reusedGas); vm.snapshotValue(string.concat(name, "_effectHeaderReads"), c.effectStorage.headerReads); vm.snapshotValue(string.concat(name, "_effectDataReads"), c.effectStorage.dataReads); vm.snapshotValue(string.concat(name, "_effectHeaderWrites"), c.effectStorage.headerWrites); vm.snapshotValue(string.concat(name, "_effectDataWrites"), c.effectStorage.dataWrites); + vm.snapshotValue(string.concat(name, "_engineStorageReads"), c.engineWorkingSet.reads); + vm.snapshotValue(string.concat(name, "_engineStorageWrites"), c.engineWorkingSet.writes); + vm.snapshotValue( + string.concat(name, "_engineStorageUniqueTouchedSlots"), c.engineWorkingSet.uniqueTouchedSlots + ); + vm.snapshotValue( + string.concat(name, "_engineStorageUniqueWrittenSlots"), c.engineWorkingSet.uniqueWrittenSlots + ); + vm.snapshotValue( + string.concat(name, "_engineStorageLoadCommitFloorOps"), c.engineWorkingSet.loadCommitFloorOps + ); + vm.snapshotValue(string.concat(name, "_engineStorageRemovableOps"), c.engineWorkingSet.removableOps); + vm.snapshotValue( + string.concat(name, "_engineRootStorageReads"), c.callBoundaryStorage.engineRootReads + ); + vm.snapshotValue( + string.concat(name, "_engineRootStorageWrites"), c.callBoundaryStorage.engineRootWrites + ); + vm.snapshotValue( + string.concat(name, "_engineCallbackStorageReads"), c.callBoundaryStorage.engineCallbackReads + ); + vm.snapshotValue( + string.concat(name, "_engineCallbackStorageWrites"), c.callBoundaryStorage.engineCallbackWrites + ); + vm.snapshotValue(string.concat(name, "_externalStorageReads"), c.callBoundaryStorage.externalReads); + vm.snapshotValue(string.concat(name, "_externalStorageWrites"), c.callBoundaryStorage.externalWrites); + for (uint256 category; category < STORAGE_CATEGORY_COUNT; category++) { + string memory prefix = string.concat(name, "_storage_", _storageCategoryName(category)); + vm.snapshotValue(string.concat(prefix, "Reads"), c.storageCategories.reads[category]); + vm.snapshotValue(string.concat(prefix, "Writes"), c.storageCategories.writes[category]); + vm.snapshotValue(string.concat(prefix, "UniqueTouched"), c.storageCategories.uniqueTouched[category]); + vm.snapshotValue(string.concat(prefix, "UniqueWritten"), c.storageCategories.uniqueWritten[category]); + } + vm.snapshotValue( + string.concat(name, "_storage_firstOtherSlot"), uint256(c.storageCategories.firstOtherSlot) + ); vm.snapshotValue(string.concat(name, "_roundStartCalls"), c.roundStartCalls); vm.snapshotValue(string.concat(name, "_roundEndCalls"), c.roundEndCalls); vm.snapshotValue(string.concat(name, "_afterMoveCalls"), c.afterMoveCalls); @@ -309,10 +399,223 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { vm.snapshotValue(string.concat(name, "_statusClassCallbacks"), c.statusClassCallbacks); vm.snapshotValue(string.concat(name, "_lifecycleCalls"), c.lifecycleCalls); vm.snapshotValue(string.concat(name, "_nonHookHeaderReadUpperBound"), c.nonHookHeaderReadUpperBound); + vm.snapshotValue(string.concat(name, "_monStateReads"), c.monStateStorage.reads); + vm.snapshotValue(string.concat(name, "_monStateWrites"), c.monStateStorage.writes); + vm.snapshotValue(string.concat(name, "_monStateUniqueTouchedLanes"), c.monStateStorage.uniqueTouchedLanes); + vm.snapshotValue(string.concat(name, "_monStateUniqueWrittenLanes"), c.monStateStorage.uniqueWrittenLanes); + vm.snapshotValue(string.concat(name, "_monStateFrameStorageOps"), c.monStateStorage.frameStorageOps); + vm.snapshotValue(string.concat(name, "_monStateRemovableStorageOps"), c.monStateStorage.removableStorageOps); + vm.snapshotValue(string.concat(name, "_monStateFrameLoadsWithLegacy"), c.monStateFrame.loads); + vm.snapshotValue(string.concat(name, "_monStateFrameCommitsWithLegacy"), c.monStateFrame.commits); + vm.snapshotValue(string.concat(name, "_monStateFrameReloadedLanes"), c.monStateFrame.reloadedLanes); + vm.snapshotValue(string.concat(name, "_monStateDirtyFlushBoundaries"), c.monStateFrame.dirtyFlushBoundaries); + vm.snapshotValue( + string.concat(name, "_monStateCleanInvalidationBoundaries"), c.monStateFrame.cleanInvalidationBoundaries + ); + vm.snapshotValue( + string.concat(name, "_monStateFrameStorageOpsWithLegacy"), c.monStateFrame.modeledStorageOps + ); + vm.snapshotValue( + string.concat(name, "_monStateRemovableStorageOpsWithLegacy"), c.monStateFrame.removableStorageOps + ); + vm.snapshotValue(string.concat(name, "_headerFrameLoadsWithLegacy"), c.packedHeaderFrame.loads); + vm.snapshotValue(string.concat(name, "_headerFrameCommitsWithLegacy"), c.packedHeaderFrame.commits); + vm.snapshotValue( + string.concat(name, "_headerFrameCallbackPassthroughOps"), + c.packedHeaderFrame.callbackPassthroughOps + ); + vm.snapshotValue(string.concat(name, "_headerFrameReloadedWords"), c.packedHeaderFrame.reloadedWords); + vm.snapshotValue( + string.concat(name, "_headerFrameDirtyFlushBoundaries"), + c.packedHeaderFrame.dirtyFlushBoundaries + ); + vm.snapshotValue( + string.concat(name, "_headerFrameCleanInvalidationBoundaries"), + c.packedHeaderFrame.cleanInvalidationBoundaries + ); + vm.snapshotValue( + string.concat(name, "_headerFrameStorageOpsWithLegacy"), c.packedHeaderFrame.modeledStorageOps + ); + vm.snapshotValue( + string.concat(name, "_headerFrameRemovableStorageOpsWithLegacy"), + c.packedHeaderFrame.removableStorageOps + ); + vm.snapshotValue(string.concat(name, "_moveResolverCalls"), c.moveResolverCalls); + vm.snapshotValue(string.concat(name, "_moveResolverContinuations"), c.moveResolverContinuations); + vm.snapshotValue(string.concat(name, "_effectResolverCalls"), c.effectResolverCalls); + vm.snapshotValue(string.concat(name, "_legacyMoveBoundaries"), c.legacyMoveBoundaries); + vm.snapshotValue(string.concat(name, "_legacyAbilityBoundaries"), c.legacyAbilityBoundaries); + vm.snapshotValue(string.concat(name, "_legacyEffectBoundaries"), c.legacyEffectBoundaries); + vm.snapshotValue(string.concat(name, "_legacyBoundaryUpperBound"), c.legacyBoundaryUpperBound); assertGt(c.effectStorage.headerReads, 0, "effect header census must be live"); assertGt(c.roundEndCalls, 0, "round-end census must be live"); assertGt(c.afterMoveCalls, 0, "after-move census must be live"); + assertGt(c.monStateStorage.reads, 0, "mon-state census must be live"); + } + + /// @dev Every mutating legacy effect hook is a conservative frame flush/invalidation boundary. + /// This is an upper bound: consecutive calls with no intervening dirty lane need not flush. + function _legacyEffectBoundaryCount(Vm.AccountAccess[] memory accesses) private view returns (uint256 count) { + count = _countCalls(accesses, address(engine), address(0), IEffect.shouldApply.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onRoundStart.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onRoundEnd.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onMonSwitchIn.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onMonSwitchOut.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onAfterDamage.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onAfterMove.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onUpdateMonState.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onPreDamage.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onApply.selector); + count += _countCalls(accesses, address(engine), address(0), IEffect.onRemove.selector); + } + + function _legacyBoundarySelectors() private pure returns (bytes4[] memory selectors) { + selectors = new bytes4[](13); + selectors[0] = IMoveSet.move.selector; + selectors[1] = IAbility.activateOnSwitch.selector; + selectors[2] = IEffect.shouldApply.selector; + selectors[3] = IEffect.onRoundStart.selector; + selectors[4] = IEffect.onRoundEnd.selector; + selectors[5] = IEffect.onMonSwitchIn.selector; + selectors[6] = IEffect.onMonSwitchOut.selector; + selectors[7] = IEffect.onAfterDamage.selector; + selectors[8] = IEffect.onAfterMove.selector; + selectors[9] = IEffect.onUpdateMonState.selector; + selectors[10] = IEffect.onPreDamage.selector; + selectors[11] = IEffect.onApply.selector; + selectors[12] = IEffect.onRemove.selector; + } + + function _logFrameCensus(ReplayCensus memory census) private pure { + console.log( + " Engine storage reads/writes :", + census.engineWorkingSet.reads, + census.engineWorkingSet.writes + ); + console.log( + " Engine unique touched/written slots :", + census.engineWorkingSet.uniqueTouchedSlots, + census.engineWorkingSet.uniqueWrittenSlots + ); + console.log( + " Engine current/floor/removable storage ops :", + census.engineWorkingSet.reads + census.engineWorkingSet.writes, + census.engineWorkingSet.loadCommitFloorOps, + census.engineWorkingSet.removableOps + ); + console.log( + " Engine root reads/writes :", + census.callBoundaryStorage.engineRootReads, + census.callBoundaryStorage.engineRootWrites + ); + console.log( + " Engine callback reads/writes :", + census.callBoundaryStorage.engineCallbackReads, + census.callBoundaryStorage.engineCallbackWrites + ); + console.log( + " mechanic-owned storage reads/writes :", + census.callBoundaryStorage.externalReads, + census.callBoundaryStorage.externalWrites + ); + console.log(" storage categories: reads / writes / unique / unique-written"); + _logStorageCategory(" other/unclassified", census.storageCategories, STORAGE_OTHER); + _logStorageCategory(" shell/allocator", census.storageCategories, STORAGE_SHELL); + _logStorageCategory(" BattleData", census.storageCategories, STORAGE_BATTLE_DATA); + _logStorageCategory(" config header", census.storageCategories, STORAGE_CONFIG_HEADER); + _logStorageCategory(" static catalog/team", census.storageCategories, STORAGE_STATIC_CATALOG); + _logStorageCategory(" MonState", census.storageCategories, STORAGE_MON_STATE); + _logStorageCategory(" effects/hooks", census.storageCategories, STORAGE_EFFECTS); + _logStorageCategory(" stat boosts", census.storageCategories, STORAGE_BOOSTS); + _logStorageCategory(" global KV", census.storageCategories, STORAGE_GLOBAL_KV); + if (census.storageCategories.firstOtherSlot != bytes32(0)) { + console.log(" first unclassified slot"); + console.logBytes32(census.storageCategories.firstOtherSlot); + } + console.log( + " packed-header current/modeled/removable ops :", + census.packedHeaderFrame.currentStorageOps, + census.packedHeaderFrame.modeledStorageOps, + census.packedHeaderFrame.removableStorageOps + ); + console.log( + " packed-header loads/commits/callback passthrough :", + census.packedHeaderFrame.loads, + census.packedHeaderFrame.commits, + census.packedHeaderFrame.callbackPassthroughOps + ); + console.log( + " packed-header reloads / dirty flushes / clean bars:", + census.packedHeaderFrame.reloadedWords, + census.packedHeaderFrame.dirtyFlushBoundaries, + census.packedHeaderFrame.cleanInvalidationBoundaries + ); + console.log( + " MonState reads/writes :", + census.monStateStorage.reads, + census.monStateStorage.writes + ); + console.log( + " MonState unique touched/written lanes :", + census.monStateStorage.uniqueTouchedLanes, + census.monStateStorage.uniqueWrittenLanes + ); + console.log( + " MonState current/frame/removable storage ops :", + census.monStateStorage.currentStorageOps, + census.monStateStorage.frameStorageOps, + census.monStateStorage.removableStorageOps + ); + console.log( + " frame loads/commits with legacy boundaries :", + census.monStateFrame.loads, + census.monStateFrame.commits + ); + console.log( + " frame reloaded lanes / dirty flushes / clean bars :", + census.monStateFrame.reloadedLanes, + census.monStateFrame.dirtyFlushBoundaries, + census.monStateFrame.cleanInvalidationBoundaries + ); + console.log( + " with-legacy frame/removable storage ops :", + census.monStateFrame.modeledStorageOps, + census.monStateFrame.removableStorageOps + ); + console.log( + " resolver move/continuation/effect calls :", + census.moveResolverCalls, + census.moveResolverContinuations, + census.effectResolverCalls + ); + console.log( + " legacy move/ability/effect boundary upper bound :", + census.legacyMoveBoundaries, + census.legacyAbilityBoundaries, + census.legacyEffectBoundaries + ); + console.log(" total legacy boundary upper bound :", census.legacyBoundaryUpperBound); + } + + function _logStorageCategory(string memory label, EngineStorageCategoryTally memory t, uint256 category) + private + pure + { + console.log(label, t.reads[category], t.writes[category]); + console.log(" unique touched/written", t.uniqueTouched[category], t.uniqueWritten[category]); + } + + function _storageCategoryName(uint256 category) private pure returns (string memory) { + if (category == STORAGE_SHELL) return "shell"; + if (category == STORAGE_BATTLE_DATA) return "battleData"; + if (category == STORAGE_CONFIG_HEADER) return "configHeader"; + if (category == STORAGE_STATIC_CATALOG) return "staticCatalog"; + if (category == STORAGE_MON_STATE) return "monState"; + if (category == STORAGE_EFFECTS) return "effects"; + if (category == STORAGE_BOOSTS) return "boosts"; + if (category == STORAGE_GLOBAL_KV) return "globalKV"; + return "other"; } function _setFirstDoublesReplayTeams() private { @@ -564,7 +867,7 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { Vm.AccountAccess[] memory accesses = _replayOneTxRecorded(key3); require(engine.getWinner(key3) != address(0), "census replay must also reach game over"); ReplayCensus memory census = - _recordReplayCensus("MainnetReplay_Reused", accesses, recycledStorageKey, reusedGas); + _recordReplayCensus("MainnetReplay_Reused", key3, accesses, recycledStorageKey, reusedGas); console.log(""); console.log("=== MAINNET replay (27 batched turns), prod config (inline regen) ==="); @@ -594,6 +897,7 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { census.statusClassCallbacks ); console.log(" non-hook effect header read upper bound :", census.nonHookHeaderReadUpperBound); + _logFrameCensus(census); } /// @dev Real submitted move/target/salt sequence from `replays.txt`. The historical deployed @@ -627,7 +931,7 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { assertEq(engine.getTurnIdForBattleState(key3), 10, "all census doubles turns execute"); _assertExactEffectStepLanes(key3); ReplayCensus memory census = - _recordReplayCensus("MainnetDoublesReplay1_Reused", accesses, recycledStorageKey, reusedGas); + _recordReplayCensus("MainnetDoublesReplay1_Reused", key3, accesses, recycledStorageKey, reusedGas); _finishIncompleteReplay(key3); console.log(""); @@ -650,5 +954,6 @@ contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { census.statusClassCallbacks ); console.log(" non-hook effect header read upper bound :", census.nonHookHeaderReadUpperBound); + _logFrameCensus(census); } } diff --git a/test/abstract/GasMeasure.sol b/test/abstract/GasMeasure.sol index 376a8996..b88b44e8 100644 --- a/test/abstract/GasMeasure.sol +++ b/test/abstract/GasMeasure.sol @@ -24,6 +24,8 @@ abstract contract GasMeasure is CommonBase { // expected reads are non-zero, so a layout change fails loudly instead of silently // emitting a misleading zero. See `forge inspect Engine storage-layout`. uint256 private constant ENGINE_BATTLE_CONFIG_ROOT = 7; + uint256 private constant CONFIG_P0_STATES_OFFSET = 9; + uint256 private constant CONFIG_P1_STATES_OFFSET = 10; uint256 private constant CONFIG_P0_EFFECTS_OFFSET = 12; uint256 private constant CONFIG_P1_EFFECTS_OFFSET = 13; @@ -47,6 +49,84 @@ abstract contract GasMeasure is CommonBase { uint256 dataWrites; } + /// @notice Exact accesses to the one-slot packed MonState lanes for one battle config. + /// @dev The bitmaps use lanes 0..7 for p0 and 8..15 for p1. A write-back frame would load + /// each touched lane at most once and commit each written lane at most once. + struct MonStateStorageTally { + uint256 reads; + uint256 writes; + uint16 readLanes; + uint16 writtenLanes; + uint256 uniqueTouchedLanes; + uint256 uniqueWrittenLanes; + uint256 frameStorageOps; + uint256 currentStorageOps; + uint256 removableStorageOps; + } + + /// @notice Storage traffic predicted for a lazy frame with legacy flush/invalidate barriers. + struct MonStateFrameModel { + uint256 loads; + uint256 commits; + uint256 reloadedLanes; + uint256 dirtyFlushBoundaries; + uint256 cleanInvalidationBoundaries; + uint256 modeledStorageOps; + uint256 removableStorageOps; + } + + /// @notice Conservative packed-header frame model. Root Engine execution uses a lazy + /// memory frame; legacy mechanic callbacks retain their actual storage operations. + struct PackedHeaderFrameModel { + uint256 loads; + uint256 commits; + uint256 callbackPassthroughOps; + uint256 reloadedWords; + uint256 dirtyFlushBoundaries; + uint256 cleanInvalidationBoundaries; + uint256 currentStorageOps; + uint256 modeledStorageOps; + uint256 removableStorageOps; + } + + /// @notice Whole-account storage working set for an onchain-kernel ceiling calculation. + struct StorageWorkingSet { + uint256 reads; + uint256 writes; + uint256 uniqueTouchedSlots; + uint256 uniqueWrittenSlots; + uint256 loadCommitFloorOps; + uint256 removableOps; + } + + struct CallBoundaryStorageTally { + uint256 engineRootReads; + uint256 engineRootWrites; + uint256 engineCallbackReads; + uint256 engineCallbackWrites; + uint256 externalReads; + uint256 externalWrites; + } + + uint256 internal constant STORAGE_CATEGORY_COUNT = 9; + uint8 internal constant STORAGE_OTHER = 0; + uint8 internal constant STORAGE_SHELL = 1; + uint8 internal constant STORAGE_BATTLE_DATA = 2; + uint8 internal constant STORAGE_CONFIG_HEADER = 3; + uint8 internal constant STORAGE_STATIC_CATALOG = 4; + uint8 internal constant STORAGE_MON_STATE = 5; + uint8 internal constant STORAGE_EFFECTS = 6; + uint8 internal constant STORAGE_BOOSTS = 7; + uint8 internal constant STORAGE_GLOBAL_KV = 8; + + struct EngineStorageCategoryTally { + uint256[STORAGE_CATEGORY_COUNT] reads; + uint256[STORAGE_CATEGORY_COUNT] writes; + uint256[STORAGE_CATEGORY_COUNT] uniqueTouched; + uint256[STORAGE_CATEGORY_COUNT] uniqueWritten; + bytes32 firstOtherSlot; + } + /// @dev Classify a state-diff window as ONE transaction: first touch of a slot is cold, /// subsequent touches warm. Call once per prod-tx-equivalent unit (e.g. once per turn) /// so cold/warm reflects a fresh cold-start access list. @@ -161,6 +241,34 @@ abstract contract GasMeasure is CommonBase { } } + /// @notice Count matching ABI calls whose first argument is nonzero. + /// @dev Used to distinguish continuation calls from initial resolver calls without changing + /// production contracts or adding instrumentation to the measured execution path. + function _countCallsWithNonzeroFirstArg( + Vm.AccountAccess[] memory accesses, + address accessor, + address account, + bytes4 selector + ) internal pure returns (uint256 count) { + for (uint256 i; i < accesses.length; i++) { + Vm.AccountAccess memory a = accesses[i]; + if ( + !a.reverted && _isCallKind(a.kind) && a.data.length >= 36 + && (accessor == address(0) || a.accessor == accessor) + && (account == address(0) || a.account == account) && bytes4(a.data) == selector + ) { + uint256 firstArg; + bytes memory data = a.data; + assembly ("memory-safe") { + firstArg := mload(add(data, 36)) + } + if (firstArg != 0) { + count++; + } + } + } + } + function _isCallKind(VmSafe.AccountAccessKind kind) private pure returns (bool) { return kind == VmSafe.AccountAccessKind.Call || kind == VmSafe.AccountAccessKind.StaticCall || kind == VmSafe.AccountAccessKind.DelegateCall || kind == VmSafe.AccountAccessKind.CallCode; @@ -204,6 +312,581 @@ abstract contract GasMeasure is CommonBase { } } + /// @notice Count reads/writes to the 16 possible packed MonState slots for one Engine config. + function _monStateStorageTally(Vm.AccountAccess[] memory accesses, address engine, bytes32 storageKey) + internal + pure + returns (MonStateStorageTally memory t) + { + bytes32 configBase = keccak256(abi.encode(storageKey, ENGINE_BATTLE_CONFIG_ROOT)); + bytes32 p0Root = bytes32(uint256(configBase) + CONFIG_P0_STATES_OFFSET); + bytes32 p1Root = bytes32(uint256(configBase) + CONFIG_P1_STATES_OFFSET); + bytes32[16] memory laneSlots; + for (uint256 mon; mon < 8; mon++) { + laneSlots[mon] = keccak256(abi.encode(mon, p0Root)); + laneSlots[8 + mon] = keccak256(abi.encode(mon, p1Root)); + } + + for (uint256 i; i < accesses.length; i++) { + Vm.StorageAccess[] memory sa = accesses[i].storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.account != engine || a.reverted) { + continue; + } + for (uint256 lane; lane < 16; lane++) { + if (a.slot == laneSlots[lane]) { + uint16 laneBit = uint16(1 << lane); + if (a.isWrite) { + t.writes++; + t.writtenLanes |= laneBit; + } else { + t.reads++; + t.readLanes |= laneBit; + } + break; + } + } + } + } + + t.uniqueTouchedLanes = _popcount16(t.readLanes | t.writtenLanes); + t.uniqueWrittenLanes = _popcount16(t.writtenLanes); + t.currentStorageOps = t.reads + t.writes; + t.frameStorageOps = t.uniqueTouchedLanes + t.uniqueWrittenLanes; + t.removableStorageOps = + t.currentStorageOps > t.frameStorageOps ? t.currentStorageOps - t.frameStorageOps : 0; + } + + /// @notice Count one account's exact storage working set in the recorded transaction. + /// @dev `loadCommitFloorOps` models loading each touched slot once and committing each written + /// slot once. It is an architectural ceiling: static catalog and shell slots cannot all be + /// collapsed into a mutable kernel state, but the result bounds the available storage win. + function _storageWorkingSet(Vm.AccountAccess[] memory accesses, address account) + internal + pure + returns (StorageWorkingSet memory w) + { + uint256 accessCount; + for (uint256 i; i < accesses.length; i++) { + Vm.StorageAccess[] memory sa = accesses[i].storageAccesses; + for (uint256 j; j < sa.length; j++) { + if (sa[j].account == account && !sa[j].reverted) { + accessCount++; + } + } + } + uint256 cap = 1; + while (cap < accessCount * 2 + 1) { + cap <<= 1; + } + bytes32[] memory slots = new bytes32[](cap); + bool[] memory occupied = new bool[](cap); + bool[] memory written = new bool[](cap); + uint256 mask = cap - 1; + + for (uint256 i; i < accesses.length; i++) { + Vm.StorageAccess[] memory sa = accesses[i].storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.account != account || a.reverted) { + continue; + } + if (a.isWrite) { + w.writes++; + } else { + w.reads++; + } + uint256 idx = uint256(a.slot) & mask; + while (occupied[idx] && slots[idx] != a.slot) { + idx = (idx + 1) & mask; + } + if (!occupied[idx]) { + occupied[idx] = true; + slots[idx] = a.slot; + w.uniqueTouchedSlots++; + } + if (a.isWrite && !written[idx]) { + written[idx] = true; + w.uniqueWrittenSlots++; + } + } + } + w.loadCommitFloorOps = w.uniqueTouchedSlots + w.uniqueWrittenSlots; + uint256 currentOps = w.reads + w.writes; + w.removableOps = currentOps > w.loadCommitFloorOps ? currentOps - w.loadCommitFloorOps : 0; + } + + /// @notice Split recorded storage operations between the root Engine frame, reentrant Engine + /// callback frames entered by mechanics, and mechanic-owned storage. + function _callBoundaryStorageTally(Vm.AccountAccess[] memory accesses, address engine, address rootCaller) + internal + pure + returns (CallBoundaryStorageTally memory t) + { + for (uint256 i; i < accesses.length; i++) { + Vm.AccountAccess memory frame = accesses[i]; + Vm.StorageAccess[] memory sa = frame.storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.reverted) { + continue; + } + if (a.account == engine) { + bool callbackFrame = frame.accessor != rootCaller && frame.accessor != engine; + if (callbackFrame) { + if (a.isWrite) t.engineCallbackWrites++; + else t.engineCallbackReads++; + } else { + if (a.isWrite) t.engineRootWrites++; + else t.engineRootReads++; + } + } else if (uint160(a.account) > 0xff) { + if (a.isWrite) t.externalWrites++; + else t.externalReads++; + } + } + } + } + + /// @notice Classify the Engine storage working set into kernel-state and shell/static families. + function _engineStorageCategoryTally( + Vm.AccountAccess[] memory accesses, + address engine, + bytes32 battleKey, + bytes32 storageKey + ) internal pure returns (EngineStorageCategoryTally memory t) { + uint256 cap = 8192; + bytes32[] memory slots = new bytes32[](cap); + uint8[] memory categories = new uint8[](cap); + bool[] memory occupied = new bool[](cap); + bool[] memory seen = new bool[](cap); + bool[] memory writtenSeen = new bool[](cap); + + _insertStorageCategory(slots, categories, occupied, bytes32(uint256(1)), STORAGE_SHELL); + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(battleKey, uint256(2))), STORAGE_SHELL + ); + for (uint256 i; i < 16; i++) { + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(i, uint256(0))), STORAGE_SHELL + ); + } + + bytes32 battleBase = keccak256(abi.encode(battleKey, uint256(5))); + _insertStorageCategory(slots, categories, occupied, battleBase, STORAGE_BATTLE_DATA); + _insertStorageCategory( + slots, categories, occupied, bytes32(uint256(battleBase) + 1), STORAGE_BATTLE_DATA + ); + bytes32 multiBase = keccak256(abi.encode(battleKey, uint256(6))); + _insertStorageCategory(slots, categories, occupied, multiBase, STORAGE_BATTLE_DATA); + _insertStorageCategory(slots, categories, occupied, bytes32(uint256(multiBase) + 1), STORAGE_BATTLE_DATA); + + bytes32 configBase = keccak256(abi.encode(storageKey, ENGINE_BATTLE_CONFIG_ROOT)); + for (uint256 offset; offset <= 5; offset++) { + _insertStorageCategory( + slots, + categories, + occupied, + bytes32(uint256(configBase) + offset), + STORAGE_CONFIG_HEADER + ); + } + _insertStorageCategory( + slots, categories, occupied, bytes32(uint256(configBase) + 18), STORAGE_CONFIG_HEADER + ); + _insertStorageCategory( + slots, categories, occupied, bytes32(uint256(configBase) + 6), STORAGE_STATIC_CATALOG + ); + + _insertTeamAndStateCategories(slots, categories, occupied, configBase); + _insertEffectCategories(slots, categories, occupied, configBase); + _insertBoostCategories(slots, categories, occupied, configBase); + _insertGlobalKVCategories(slots, categories, occupied, accesses, engine, storageKey); + + uint256 mask = cap - 1; + for (uint256 i; i < accesses.length; i++) { + Vm.StorageAccess[] memory sa = accesses[i].storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.account != engine || a.reverted) { + continue; + } + uint256 idx = uint256(a.slot) & mask; + while (occupied[idx] && slots[idx] != a.slot) { + idx = (idx + 1) & mask; + } + if (!occupied[idx]) { + occupied[idx] = true; + slots[idx] = a.slot; + categories[idx] = STORAGE_OTHER; + } + uint256 category = categories[idx]; + if (category == STORAGE_OTHER && t.firstOtherSlot == bytes32(0)) { + t.firstOtherSlot = a.slot; + } + if (a.isWrite) t.writes[category]++; + else t.reads[category]++; + if (!seen[idx]) { + seen[idx] = true; + t.uniqueTouched[category]++; + } + if (a.isWrite && !writtenSeen[idx]) { + writtenSeen[idx] = true; + t.uniqueWritten[category]++; + } + } + } + } + + function _insertTeamAndStateCategories( + bytes32[] memory slots, + uint8[] memory categories, + bool[] memory occupied, + bytes32 configBase + ) private pure { + bytes32 p0TeamRoot = bytes32(uint256(configBase) + 7); + bytes32 p1TeamRoot = bytes32(uint256(configBase) + 8); + bytes32 p0StateRoot = bytes32(uint256(configBase) + CONFIG_P0_STATES_OFFSET); + bytes32 p1StateRoot = bytes32(uint256(configBase) + CONFIG_P1_STATES_OFFSET); + for (uint256 mon; mon < 8; mon++) { + bytes32 p0Mon = keccak256(abi.encode(mon, p0TeamRoot)); + bytes32 p1Mon = keccak256(abi.encode(mon, p1TeamRoot)); + for (uint256 word; word < 6; word++) { + _insertStorageCategory( + slots, + categories, + occupied, + bytes32(uint256(p0Mon) + word), + STORAGE_STATIC_CATALOG + ); + _insertStorageCategory( + slots, + categories, + occupied, + bytes32(uint256(p1Mon) + word), + STORAGE_STATIC_CATALOG + ); + } + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(mon, p0StateRoot)), STORAGE_MON_STATE + ); + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(mon, p1StateRoot)), STORAGE_MON_STATE + ); + } + } + + function _insertEffectCategories( + bytes32[] memory slots, + uint8[] memory categories, + bool[] memory occupied, + bytes32 configBase + ) private pure { + bytes32 globalRoot = bytes32(uint256(configBase) + 11); + bytes32 p0Root = bytes32(uint256(configBase) + CONFIG_P0_EFFECTS_OFFSET); + bytes32 p1Root = bytes32(uint256(configBase) + CONFIG_P1_EFFECTS_OFFSET); + for (uint256 i; i < 256; i++) { + bytes32 header = keccak256(abi.encode(i, globalRoot)); + _insertStorageCategory(slots, categories, occupied, header, STORAGE_EFFECTS); + _insertStorageCategory( + slots, categories, occupied, bytes32(uint256(header) + 1), STORAGE_EFFECTS + ); + } + uint256 playerSlots = 8 * EFFECT_SLOTS_PER_MON; + for (uint256 i; i < playerSlots; i++) { + bytes32 p0Header = keccak256(abi.encode(i, p0Root)); + bytes32 p1Header = keccak256(abi.encode(i, p1Root)); + _insertStorageCategory(slots, categories, occupied, p0Header, STORAGE_EFFECTS); + _insertStorageCategory( + slots, categories, occupied, bytes32(uint256(p0Header) + 1), STORAGE_EFFECTS + ); + _insertStorageCategory(slots, categories, occupied, p1Header, STORAGE_EFFECTS); + _insertStorageCategory( + slots, categories, occupied, bytes32(uint256(p1Header) + 1), STORAGE_EFFECTS + ); + } + bytes32 hookRoot = bytes32(uint256(configBase) + 14); + for (uint256 i; i < 256; i++) { + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(i, hookRoot)), STORAGE_EFFECTS + ); + } + } + + function _insertBoostCategories( + bytes32[] memory slots, + uint8[] memory categories, + bool[] memory occupied, + bytes32 configBase + ) private pure { + bytes32 p0Root = bytes32(uint256(configBase) + 15); + bytes32 p1Root = bytes32(uint256(configBase) + 16); + bytes32 accRoot = bytes32(uint256(configBase) + 17); + for (uint256 i; i < 8 * 16; i++) { + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(i, p0Root)), STORAGE_BOOSTS + ); + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(i, p1Root)), STORAGE_BOOSTS + ); + } + for (uint256 lane; lane < 16; lane++) { + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(lane, accRoot)), STORAGE_BOOSTS + ); + } + } + + function _insertGlobalKVCategories( + bytes32[] memory slots, + uint8[] memory categories, + bool[] memory occupied, + Vm.AccountAccess[] memory accesses, + address engine, + bytes32 storageKey + ) private pure { + bytes32 valuesRoot = keccak256(abi.encode(storageKey, uint256(8))); + bytes32 keysRoot = keccak256(abi.encode(storageKey, uint256(9))); + bytes32[16] memory keySlots; + for (uint256 i; i < 16; i++) { + keySlots[i] = keccak256(abi.encode(i, keysRoot)); + _insertStorageCategory(slots, categories, occupied, keySlots[i], STORAGE_GLOBAL_KV); + } + // Zero-valued probes are deliberately absent from globalKVKeySlots. Decode the getter's + // key argument so those read-only value slots are still categorized exactly. + bytes4 getGlobalKVSelector = bytes4(keccak256("getGlobalKV(bytes32,uint64)")); + for (uint256 i; i < accesses.length; i++) { + Vm.AccountAccess memory frame = accesses[i]; + if (frame.account != engine || frame.data.length < 68 || bytes4(frame.data) != getGlobalKVSelector) { + continue; + } + uint256 rawKey; + bytes memory data = frame.data; + assembly ("memory-safe") { + rawKey := mload(add(data, 68)) + } + _insertStorageCategory( + slots, + categories, + occupied, + keccak256(abi.encode(uint64(rawKey), valuesRoot)), + STORAGE_GLOBAL_KV + ); + } + for (uint256 i; i < accesses.length; i++) { + Vm.StorageAccess[] memory sa = accesses[i].storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.account != engine || a.reverted) continue; + for (uint256 k; k < keySlots.length; k++) { + if (a.slot != keySlots[k]) continue; + _insertPackedGlobalKeys(slots, categories, occupied, valuesRoot, uint256(a.previousValue)); + _insertPackedGlobalKeys(slots, categories, occupied, valuesRoot, uint256(a.newValue)); + break; + } + } + } + } + + function _insertPackedGlobalKeys( + bytes32[] memory slots, + uint8[] memory categories, + bool[] memory occupied, + bytes32 valuesRoot, + uint256 packed + ) private pure { + for (uint256 lane; lane < 4; lane++) { + uint64 key = uint64(packed >> (lane * 64)); + _insertStorageCategory( + slots, categories, occupied, keccak256(abi.encode(key, valuesRoot)), STORAGE_GLOBAL_KV + ); + } + } + + function _insertStorageCategory( + bytes32[] memory slots, + uint8[] memory categories, + bool[] memory occupied, + bytes32 slot, + uint8 category + ) private pure { + uint256 mask = slots.length - 1; + uint256 idx = uint256(slot) & mask; + while (occupied[idx] && slots[idx] != slot) { + idx = (idx + 1) & mask; + } + if (!occupied[idx]) { + occupied[idx] = true; + slots[idx] = slot; + categories[idx] = category; + } + } + + /// @notice Replay the recorded access order through a lazy write-back frame model. + /// @dev Before every legacy mechanic call, dirty lanes are committed and all loaded lanes are + /// invalidated. Resolver calls are deliberately absent from `boundarySelectors` and retain + /// the frame. This remains test-only and does not perturb the measured Engine path. + function _monStateFrameModel( + Vm.AccountAccess[] memory accesses, + address engine, + bytes32 storageKey, + bytes4[] memory boundarySelectors + ) internal pure returns (MonStateFrameModel memory m) { + bytes32 configBase = keccak256(abi.encode(storageKey, ENGINE_BATTLE_CONFIG_ROOT)); + bytes32 p0Root = bytes32(uint256(configBase) + CONFIG_P0_STATES_OFFSET); + bytes32 p1Root = bytes32(uint256(configBase) + CONFIG_P1_STATES_OFFSET); + bytes32[16] memory laneSlots; + for (uint256 mon; mon < 8; mon++) { + laneSlots[mon] = keccak256(abi.encode(mon, p0Root)); + laneSlots[8 + mon] = keccak256(abi.encode(mon, p1Root)); + } + + uint16 loaded; + uint16 dirty; + uint16 everLoaded; + uint256 currentStorageOps; + for (uint256 i; i < accesses.length; i++) { + Vm.AccountAccess memory accountAccess = accesses[i]; + if (_isLegacyBoundary(accountAccess, engine, boundarySelectors)) { + if (dirty != 0) { + m.commits += _popcount16(dirty); + m.dirtyFlushBoundaries++; + } else { + m.cleanInvalidationBoundaries++; + } + loaded = 0; + dirty = 0; + } + + Vm.StorageAccess[] memory sa = accountAccess.storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.account != engine || a.reverted) { + continue; + } + for (uint256 lane; lane < 16; lane++) { + if (a.slot == laneSlots[lane]) { + uint16 laneBit = uint16(1 << lane); + currentStorageOps++; + if (loaded & laneBit == 0) { + m.loads++; + if (everLoaded & laneBit != 0) { + m.reloadedLanes++; + } + loaded |= laneBit; + everLoaded |= laneBit; + } + if (a.isWrite) { + dirty |= laneBit; + } + break; + } + } + } + } + if (dirty != 0) { + m.commits += _popcount16(dirty); + } + m.modeledStorageOps = m.loads + m.commits; + m.removableStorageOps = + currentStorageOps > m.modeledStorageOps ? currentStorageOps - m.modeledStorageOps : 0; + } + + /// @notice Model a lazy memory frame for the two BattleData words and seven direct + /// BattleConfig header words (offsets 0..5 and 18). + /// @dev Legacy external mechanics are barriers: dirty root words flush before the call, root + /// words invalidate, and Engine storage touched by callback frames remains passthrough. + /// Resolver calls are deliberately not barriers. + function _packedHeaderFrameModel( + Vm.AccountAccess[] memory accesses, + address engine, + address rootCaller, + bytes32 battleKey, + bytes32 storageKey, + bytes4[] memory boundarySelectors + ) internal pure returns (PackedHeaderFrameModel memory m) { + bytes32 battleBase = keccak256(abi.encode(battleKey, uint256(5))); + bytes32 configBase = keccak256(abi.encode(storageKey, ENGINE_BATTLE_CONFIG_ROOT)); + bytes32[9] memory headerSlots; + headerSlots[0] = battleBase; + headerSlots[1] = bytes32(uint256(battleBase) + 1); + for (uint256 offset; offset <= 5; offset++) { + headerSlots[2 + offset] = bytes32(uint256(configBase) + offset); + } + headerSlots[8] = bytes32(uint256(configBase) + 18); + + uint16 loaded; + uint16 dirty; + uint16 everLoaded; + for (uint256 i; i < accesses.length; i++) { + Vm.AccountAccess memory frame = accesses[i]; + if (_isLegacyBoundary(frame, engine, boundarySelectors)) { + if (dirty != 0) { + m.commits += _popcount16(dirty); + m.dirtyFlushBoundaries++; + } else { + m.cleanInvalidationBoundaries++; + } + loaded = 0; + dirty = 0; + } + + bool callbackFrame = frame.accessor != rootCaller && frame.accessor != engine; + Vm.StorageAccess[] memory sa = frame.storageAccesses; + for (uint256 j; j < sa.length; j++) { + Vm.StorageAccess memory a = sa[j]; + if (a.account != engine || a.reverted) continue; + for (uint256 word; word < headerSlots.length; word++) { + if (a.slot != headerSlots[word]) continue; + m.currentStorageOps++; + if (callbackFrame) { + m.callbackPassthroughOps++; + break; + } + uint16 wordBit = uint16(1 << word); + if (loaded & wordBit == 0) { + m.loads++; + if (everLoaded & wordBit != 0) m.reloadedWords++; + loaded |= wordBit; + everLoaded |= wordBit; + } + if (a.isWrite) dirty |= wordBit; + break; + } + } + } + if (dirty != 0) m.commits += _popcount16(dirty); + m.modeledStorageOps = m.loads + m.commits + m.callbackPassthroughOps; + m.removableStorageOps = m.currentStorageOps > m.modeledStorageOps + ? m.currentStorageOps - m.modeledStorageOps + : 0; + } + + function _isLegacyBoundary(Vm.AccountAccess memory a, address engine, bytes4[] memory selectors) + private + pure + returns (bool) + { + if (a.reverted || !_isCallKind(a.kind) || a.accessor != engine || a.data.length < 4) { + return false; + } + bytes4 selector = bytes4(a.data); + for (uint256 i; i < selectors.length; i++) { + if (selector == selectors[i]) { + return true; + } + } + return false; + } + + function _popcount16(uint16 value) private pure returns (uint256 count) { + while (value != 0) { + value &= value - 1; + count++; + } + } + /// @dev Precompute the player EffectInstance header/data slots for O(1) census lookup. function _buildPlayerEffectSlotTable(bytes32 p0Root, bytes32 p1Root) private diff --git a/test/kernel/PackedHeaderFrameLibTest.sol b/test/kernel/PackedHeaderFrameLibTest.sol new file mode 100644 index 00000000..e6e226ad --- /dev/null +++ b/test/kernel/PackedHeaderFrameLibTest.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {Test} from "../../lib/forge-std/src/Test.sol"; + +import {BattleConfig, BattleData} from "../../src/Structs.sol"; +import {PackedHeaderFrameLib} from "../../src/kernel/PackedHeaderFrameLib.sol"; + +contract PackedHeaderFrameHarness { + using PackedHeaderFrameLib for PackedHeaderFrameLib.Frame; + + BattleData private battle; + BattleConfig private config; + + function setRaw(uint256 wordIndex, uint256 value) external { + uint256 slot = _slot(wordIndex); + assembly ("memory-safe") { + sstore(slot, value) + } + } + + function getRaw(uint256 wordIndex) external view returns (uint256 value) { + uint256 slot = _slot(wordIndex); + assembly ("memory-safe") { + value := sload(slot) + } + } + + function probeTwice(uint256 wordIndex) + external + view + returns (uint256 first, uint256 second, uint16 loadedMask, uint16 dirtyMask) + { + PackedHeaderFrameLib.Frame memory frame = PackedHeaderFrameLib.init(battle, config); + first = frame.read(wordIndex); + second = frame.read(wordIndex); + loadedMask = frame.loadedMask; + dirtyMask = frame.dirtyMask; + } + + function writeAndFlush(uint256 wordIndex, uint256 value) + external + returns (uint16 loadedBeforeFlush, uint16 dirtyBeforeFlush, uint16 dirtyAfterFlush) + { + PackedHeaderFrameLib.Frame memory frame = PackedHeaderFrameLib.init(battle, config); + frame.write(wordIndex, value); + loadedBeforeFlush = frame.loadedMask; + dirtyBeforeFlush = frame.dirtyMask; + frame.flush(); + dirtyAfterFlush = frame.dirtyMask; + } + + function readBattleState() + external + view + returns ( + address p0, + address p1, + uint8 winner, + uint8 switchFlag, + uint16 active, + uint40 timestamp, + uint16 turn, + uint8 buffered + ) + { + PackedHeaderFrameLib.Frame memory frame = PackedHeaderFrameLib.init(battle, config); + return ( + frame.p0(), + frame.p1(), + frame.winnerIndex(), + frame.playerSwitchForTurnFlag(), + frame.activeMonIndex(), + frame.lastExecuteTimestamp(), + frame.turnId(), + frame.numBuffered() + ); + } + + function advanceTurnAndFlush(uint8 switchFlag, uint40 timestamp) external { + PackedHeaderFrameLib.Frame memory frame = PackedHeaderFrameLib.init(battle, config); + frame.advanceTurn(switchFlag, timestamp); + frame.flush(); + } + + function dirtyInvalidateReverts() external returns (bool reverted) { + PackedHeaderFrameLib.Frame memory frame = PackedHeaderFrameLib.init(battle, config); + frame.write(0, 1); + try this.invalidate(frame) {} catch { + reverted = true; + } + } + + function invalidate(PackedHeaderFrameLib.Frame memory frame) external pure { + frame.invalidate(); + } + + function _slot(uint256 wordIndex) private view returns (uint256 slot) { + uint256 battleSlot; + uint256 configSlot; + assembly ("memory-safe") { + battleSlot := battle.slot + configSlot := config.slot + } + if (wordIndex < 2) return battleSlot + wordIndex; + if (wordIndex < 8) return configSlot + wordIndex - 2; + if (wordIndex == 8) return configSlot + 18; + revert(); + } +} + +contract PackedHeaderFrameLibTest is Test { + PackedHeaderFrameHarness private harness; + + function setUp() public { + harness = new PackedHeaderFrameHarness(); + for (uint256 word; word < 9; word++) { + harness.setRaw(word, 1000 + word); + } + } + + function test_lazyReadLoadsWordOnce() public view { + (uint256 first, uint256 second, uint16 loadedMask, uint16 dirtyMask) = harness.probeTwice(5); + assertEq(first, 1005); + assertEq(second, 1005); + assertEq(loadedMask, uint16(1 << 5)); + assertEq(dirtyMask, 0); + } + + function test_fullWordWriteDoesNotRequirePriorReadAndFlushesOnlyDirtyWord() public { + (uint16 loaded, uint16 dirtyBefore, uint16 dirtyAfter) = harness.writeAndFlush(8, 7777); + assertEq(loaded, uint16(1 << 8)); + assertEq(dirtyBefore, uint16(1 << 8)); + assertEq(dirtyAfter, 0); + assertEq(harness.getRaw(8), 7777); + for (uint256 word; word < 8; word++) { + assertEq(harness.getRaw(word), 1000 + word); + } + } + + function test_dirtyFrameCannotInvalidateWithoutFlush() public { + assertTrue(harness.dirtyInvalidateReverts()); + } + + function test_wordBounds() public { + vm.expectRevert(PackedHeaderFrameLib.HeaderWordOutOfBounds.selector); + harness.probeTwice(9); + } + + function test_battleStateTypedAccessorsMatchStoragePacking() public { + address p0 = address(0x1234567890123456789012345678901234567890); + address p1 = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); + uint256 staticWord = uint256(uint160(p1)) | (uint256(9) << 160) | (uint256(11) << 176); + uint256 stateWord = uint256(uint160(p0)) | (uint256(2) << 160) | (uint256(1) << 168) + | (uint256(0x0703) << 176) | (uint256(123456) << 192) | (uint256(41) << 232) + | (uint256(6) << 248); + harness.setRaw(0, staticWord); + harness.setRaw(1, stateWord); + + ( + address gotP0, + address gotP1, + uint8 winner, + uint8 switchFlag, + uint16 active, + uint40 timestamp, + uint16 turn, + uint8 buffered + ) = harness.readBattleState(); + assertEq(gotP0, p0); + assertEq(gotP1, p1); + assertEq(winner, 2); + assertEq(switchFlag, 1); + assertEq(active, 0x0703); + assertEq(timestamp, 123456); + assertEq(turn, 41); + assertEq(buffered, 6); + } + + function test_advanceTurnPreservesUnrelatedPackedFields() public { + uint256 original = uint256(uint160(address(0x1234))) | (uint256(2) << 160) | (uint256(1) << 168) + | (uint256(0x0703) << 176) | (uint256(99) << 192) | (uint256(41) << 232) + | (uint256(6) << 248); + harness.setRaw(1, original); + harness.advanceTurnAndFlush(2, 777); + + uint256 expected = uint256(uint160(address(0x1234))) | (uint256(2) << 160) | (uint256(2) << 168) + | (uint256(0x0703) << 176) | (uint256(777) << 192) | (uint256(42) << 232) + | (uint256(6) << 248); + assertEq(harness.getRaw(1), expected); + } +} diff --git a/test/mons/DoublesKitTest.sol b/test/mons/DoublesKitTest.sol index ec50f4f6..5b4b0e07 100644 --- a/test/mons/DoublesKitTest.sol +++ b/test/mons/DoublesKitTest.sol @@ -135,6 +135,7 @@ contract DoublesKitTest is Test { Mon[] memory aTeam = new Mon[](3); aTeam[0] = _mkMon(IMoveSet(address(hitAndDip)), 0); aTeam[1] = _mkMon(IMoveSet(address(hitAndDip)), 0); + aTeam[1].moves[0] |= MOVE_META_TAG | MOVE_RESOLVER_TAG | (uint256(2) << 236) | (DEFAULT_PRIORITY << 244); aTeam[2] = _mkMon(IMoveSet(address(hitAndDip)), 0); Mon[] memory bTeam = new Mon[](2); bTeam[0] = _mkMon(IMoveSet(address(hitAndDip)), 0); From b46005621ca262e58f6280e905aa13ad484f4594 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:17:01 -0700 Subject: [PATCH 19/41] wip new sprites --- drool/imgs/attacks/chill_out.gif | Bin 0 -> 4880 bytes drool/imgs/attacks/chill_out.png | Bin 0 -> 6175 bytes drool/imgs/attacks/federal_investigation.gif | Bin 0 -> 12177 bytes drool/imgs/attacks/federal_investigation.png | Bin 0 -> 15897 bytes drool/imgs/attacks/sanctify.gif | Bin 0 -> 2936 bytes drool/imgs/attacks/sanctify.png | Bin 0 -> 3220 bytes drool/imgs/attacks/throw_pebble.gif | Bin 0 -> 1869 bytes drool/imgs/attacks/throw_pebble.png | Bin 0 -> 2079 bytes drool/imgs/attacks/wither_away.gif | Bin 0 -> 2990 bytes drool/imgs/attacks/wither_away.png | Bin 0 -> 3522 bytes 10 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 drool/imgs/attacks/chill_out.gif create mode 100644 drool/imgs/attacks/chill_out.png create mode 100644 drool/imgs/attacks/federal_investigation.gif create mode 100644 drool/imgs/attacks/federal_investigation.png create mode 100644 drool/imgs/attacks/sanctify.gif create mode 100644 drool/imgs/attacks/sanctify.png create mode 100644 drool/imgs/attacks/throw_pebble.gif create mode 100644 drool/imgs/attacks/throw_pebble.png create mode 100644 drool/imgs/attacks/wither_away.gif create mode 100644 drool/imgs/attacks/wither_away.png diff --git a/drool/imgs/attacks/chill_out.gif b/drool/imgs/attacks/chill_out.gif new file mode 100644 index 0000000000000000000000000000000000000000..5d79f63e6c62952b0ebd345f6d19bac76e24d823 GIT binary patch literal 4880 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 Nx|!A0DwBIL{{^$oDY^gv literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/federal_investigation.gif b/drool/imgs/attacks/federal_investigation.gif new file mode 100644 index 0000000000000000000000000000000000000000..2e9d2e61c28f8c67478c7f614e2f1e9a1a42d818 GIT binary patch literal 12177 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/sanctify.gif b/drool/imgs/attacks/sanctify.gif new file mode 100644 index 0000000000000000000000000000000000000000..155884ec2f5cc3882ccd7dbe038a18b2e438003e GIT binary patch literal 2936 zcmaKue>~Is9>+gpm>Gu2uPHXo%&(z3LVnHIW-GrEavVy(q!Z)Oq10x|kBP=86Yi#^ zSx&KXlH=a;BR`fxPTjhvI*}rE&m-(~a&PyzU)&D;argcA`~5z?&(G`idcWS^5PGoZ z=A=ZB2zUT2EG+ynne^}1#NUtLz^co3L2oNHDv}ZqMs;@~Yr>#gw)1dZBv=4Wi;pe7 z4iHMPpo^t7i#ziWCZzcOW=VGe8qcVqwbb7!Hup$*+TU`a?~DzFkEJUnf&d{h7=GIr z&!{AuDRL|#B>+Y^@F*xNXOa!5=mUdw8~{$^Abc{$-;d+Xc3H*eR5uN=<` zAb=Mz2E6~L4dTGcu?>-EL9>LCi&{sBCql>OV_esqmW7W8mReI1;0_8Y1%Tk{jzV^F zZ>1qjfT=QWVC4}H8l+}loAj$&*!!<{vJ6=`FYrvtCmDv&FJ&FN?_^m?9F|)~MzmU> zSE#Ik7Nop7s`y1JA{qeRvKcQr^U-*hoJA_gjJOT5G6|QDbv^OUGYnA6iIEMTh>220 zv9oS_&YScJX0_4I=RLY^grUFq=WX(djgV355#J$e8Y{*E01UPQVhpr_AHb-QC+kmn z;4quTR8aB(&bNj(BL*0os~Tk_^paw2-8SaH2OhcQnVQH>(%t)B3FkDa}!YVGOZKi%{u96?8 zOX+oZltGK z&G>$yhGXf;70zvwv$b5D6Ho59^FKyLoFiRkVhlxO4-9q*8KA+Xq1v$^KU?D~7>Jq{^*2H2B+ zMb6u2Q;Dj{9x=7RK&3k}HNc@CW@8>>#;7F`qG&CL7Q08~QMp%Ch!%V-%x6eWy^OhZ zFaOIBV1c7rX%8>Krir;wxogLm+t_yT(6(Nf+s(M{JX3pQp}A2KwG{12fYTMy0UFBzKG9vM zVTyD*pb+;|APxVPdb9kM-$}CBebna2ivi^(nOng~hy{@01!4J?lFe-N$m z$m)pWY!qT8i&nq|g3?u!#TcN4`1rdkSl)((WNs%%1MihgYuQDCyhNsb9Ig-Hnb;yR z7A-b?rRz-CJ~8Fu-eL#C{7jLNB)G&rV>+I$Xy`icfBXZ=Qn+XMf^%^+yxQGn1;P?+ zfFh<2B7PAfg!~y|_=R6CTq_XeUj`|&K=Q4C>MWRV;KoM{xb5o8wWN~ZjV&)bg&MJP zpDOl+pki|DwA5iVnO;WDh&VYl`f9K~Kxn*0iFp(8MSL(2h$ld)Q9}TMVBTL%raK~-Mt(u?^4u&`D&$=_=VSDAl9l~!$ z<Uq+`Mo7?^KkC|_$TX!Bg+|6B4(E`{(1*~qDel9>L`&tNQ*86o(_VJ9I zeu0F-H(Q^Ats0he7N8wTPt6thffAc-E~&EFWM%o9kRR+E$}iAF9$&|G6nb15s&qg0 zhbk7@@O6YUubZaA-$b0XsP(m;k03DwUu`+kB%f&rl==VlglXv6w^96^*>V3wZrZo1 z04)I)A~jMY^NFQN2orNTI{L_%enAq%g*RY6MkfUp>zQuqAi9kQlv?ZKoDlBPJ7omc zS*FNsPYB;p_lL{@#W?fyy2B5eii|Gaul3${0?ts`_Ek~ya4g2$ctnG7E&rrt)I^w^ zJHr`&Cz!N9>~f*6a{yVnxvLzLo(d^VKv=9j#qPW3^uxPq i=yS;|q0OG>PY34kp|xA|w7=e;HV(vJmE4Dl#QVR@=y!Mk literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/sanctify.png b/drool/imgs/attacks/sanctify.png new file mode 100644 index 0000000000000000000000000000000000000000..926ad5bef23db9dffe5c542618205c85329e6551 GIT binary patch literal 3220 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/throw_pebble.gif b/drool/imgs/attacks/throw_pebble.gif new file mode 100644 index 0000000000000000000000000000000000000000..8aa550cc2fa41df4890c2390c5ef75f307a1e9d2 GIT binary patch literal 1869 zcmZ?wbhEHbOkhZ0XkuVEaQaSa;h8j-xxN-D3`junKewN2NU*bGfUA+70W%{|O7SNP zCl|wi1|5(xkah-+1q}Z=Wjr=4IM~b~tQB)&!@|Su0?J-$>vfkU)+}xaTc~$J`ZEJ6DFL-=vuJ`tJ zcXwBOes%YCa0A2ox}RKfK07u%Jlr9y9k<6o0pVy)27d-7AZ)_rV-7Y3Rt^yX0fZJV z20v1@a5MOls)dKahg2;L49*OE4E%Wf;tGlwi3JZ1G_!L{xy%SiWN2fO)yi2B(7@2n zq8+s-BjAx+AOHW%KNf*04gJ#IRxX^E7Q6MRg>O5fQK)!$mVolKKjKJ1!NOq3Aj%+( z$Hr1{Qkam?z`!c3Wm6Hb(7BCAR;@*2!NMbrJgga8Dn7;^>lKs_VmY}{;YhQTXP3{( zO-Y{3^3iQ`I*ingw5l^tsL?c7?%&Sblr)F4#Nl|0W2>C%j4LmK8hz*5<^B@My0q&5 zqOhyGroIYU=Y6p4>#xwvs||?A;$zTc5MvO;EQba3kx)UT59EKEE!QN!EFrDKwc$7J>3RVo$>)ZAyXW#1BcS-9-nd>Q?J zzkeD(ab9GV$hM$!OVYt*elxH2WFenTib$54H-(TD9y5 zc;wu~Y?S3w5zye?$z<9kbMxckqZ2ixt70;Z7`!I_57}hmc`@PSOy2BMA{Ppkj?T7m ze6{68@rt9fSc~IyB#`1vgFz4I22nh2$YEgT5c#nllzsX6VrFbuc&L>H=mv{`MJ`=} zx@IjaJ}MpVla^%TNGMS8?B@1e6>_t1>FJ4LzFYzd#VThf^QE41c^Qv7a6$xbDoVtIH1}MU@tVE`tn%6ds37Ku%0C6&Dl^ zweoSBiBx=0bmvN}+~+%$Xq75nUB$whaaF5eb;IPi!(D%86}$;s zocO;|H$!aIp)FbG#Y|6&YR7I=djD$g?ZW(9NRC%yuwc+)5W?g5Do8q8aDah>Sprlj z9&F)|1r~}1iVfW&#z99`Y+QVRQGvZ;j)c$?_sRdgx_D*=EpeZ05P2+Q=cJ`aX6dy#fG7P+L#bg>Z9O_n%^eXxJUe%+?F!hzojN<2~=Gg(OqN+(R{NYtm jr^9@)o?jDIU0EH`pSAVX#O#38alNblh+j)_V6X-Nv6QKq literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/throw_pebble.png b/drool/imgs/attacks/throw_pebble.png new file mode 100644 index 0000000000000000000000000000000000000000..db6141dabcbbbeea1465403ba51d6ee16a011568 GIT binary patch literal 2079 zcmd5+_g5447XKy?Mpz;uL(33F5U?mn4I_a>mQq%NvQ$z}M4v#FK$M-wgD5Cq1sV`i zNJVMbh=7W0D`2q95_ZHGb^s-d0MhU$yr15?KiqTAJ$HQW`Fzez^Y(OAf$PEn04nZo zXM6#G7E9wc1v#moxG>NFfc&ic87JJGLf%w!)v$}U+{cS=VI{q=5@+i(hc3d$i8Agz zS%bc_$F8ZP2rnUl=_OE8>w7i{CfE|xFTZ$D5LOMMiY}{$BG1iE!U z9V-UlD8;LN-F`Q+vNXJIyITN3bGLAg5b&2m5ryMb3WWnpbax>YZUWlwF@qXW>}Rq# zjF2NMEqyTOwQ`V%k1}}(r+4lq(jw_ZlLwKxSET(_rdsycTEHswV(fZ`~5L6K!te>D`WStB3Y4o*~;+0t0zNwWN7vMO&WsqS>}ADCE4O|1K5MsU7>& zL^lAe)g4oRjrQTng0qMb1sa~a{&j>`{hLPo`&TbF4aW3$saF-Sy_2H(IwgoqT?5`M z{U`CVekoc-@Sg=7={ZyQdk4< z%up=wu7!^Cw3{YM8@;M!2fmFgp&3IbDqTmDqzYJq&U$w*v+4;!&b@1Pj$HVBG>MYz z(G-bNEo=@6z$<8;p0Ife*G1{eVVCyS$k+Kf5_9uj!fkOXPaW_muH?NcF@TZ&hj$=* zH1=V6Vp%#gF6uliDy&pQaNSt>vqYuJpiWOxRuwUo&T9RZ*FRhlrrAI$E0(}ant3r% zt6Xl~52AwUX9w@!?UOw)CHynt^PAN0bXMo&LY*(4pu=KZTU2T&Q9@1K%U?ZpU*6k_ zh>uC`lSn$ghTfMf2KlE3VVpB_`;xVg^oWn{w17kL%M}`e=yt5wywx*B`V+=p9}f|V znifMFdz;$EzK@$BZX~)nOn)}N)2=aoav$2%%s%XhVDhWQc-C-aV=q(5jE0x@7czfS zldLAO69w_i_SP{9gvDNX)^T;wT#lqVx4GTd`p9X$@xso*#BU1)-;3L`-&8 zC*lZ0gx7m&tZ})qI_h|s*uOs*Xdn=R;GpQz$MgidHCIcT3HolOM$;Y>i`~LwaxjX5PF;S|FU_i*T+id__@| zL6}>F_9cOov;5MT!x_V^*;@ML``N1*4*JaIF8tj{&7S?4nhz%YIX_!@!uF9oKqY4q zOH-f^L4DHJYUj%4by4MeUVmq+Zqsc_UcCF^IF5u_t4`cD+9*6bU!#j@y`tqm=39?@ z_{8Qia@1rF6O4?1XBfGx-A|`W9BvRk^&qH)5drb>p*&$&KA|v_-bPWONm7P#{_vlL z@}@D@%pg(cFsvXrhQVbIhTdRUSk#e$i}{IQy>BXz7tQpQ{S#5?lN$)N1BU#{tiPan zF^>iqkijp-Jh_(hL>;`$GlAx{Ax0Nna+xRven8HU-&%V`-^nh&)V1nl$5#SvE*B4@9cExOQr!Q$2l>&mX$RO5#y=(F`RuxSdvl9*{ z6fsf?p>%8^((+PW)YQC_(|b7Qy6U{=I=|=o{q=jE-~D;M_kG>>*T&8oW$Z-+L=X=E zo6QbHu)hNKCBO!29u>BrBi6+kZArjx*3;+V;(StB<`sZ(cIp1u_TxB!76G<^f^ALF z-2|wFwtHty@x3#mYPj>ZwI%Ij8H0e4&f3xknab!?1-rVkt{kLuwR=}xd3T<^5A(cT zeMQeXl)8vaR{g6-7c3HBins>GlTzCpZ4YKcRbM5c6i3H3Ru5cstq*v~Y`i#lm3S{z z(f-<{;d=kRYLD(~mtQm;nPk%Ko36ZU316Ih+1+$?^iC{~pc1~hX8c~VgwB3MO%heqfLn-pTc+@71;B}7lXuc3 z)goDpqh9yUA|UzOW@~*PQ1!6hBh3pF_;e}$w2>uo4M!5vk{e6QF6}AWo}zTW3a)}p z;Nn`n%61M0ob&!#49^viv7aC)4oHHKy!kd{kvEnkA?!ASEFxN`DwxIaMjS=7rK>u) zQT!K1u~fA^ZLFnZ(-@kT!9>Gs&4g9_rfBOjEx6i~B-@lQ-A;qzhg3NTLI}Fbf(Ft+ zlq2o0S^QTLQ#96^02Po@$PyiIOA_XnJ0;_!f=(4yaq~qHVnH0AWdLi&31LH4w9mlU zJ)L}TR>mVTQMRt!z5wbMaEW$o`hE_7h&iuZ{|vKaW2UO#aIbn_g|*Iemx8{^{;F-o zdTXIJRBAXr_>xbL@`uVwpSfOlL-W1-7s*DB_5A z51Gxqru^)I8i4&4Y4^X7Ac7)r*dm#Yn0m(CFM;ng^QK8lf_L*Wz7VHP4;@D3vdRZ^$tbGVCN}XojQgp z9(bU?i{Rs!uB0h`GlhU`i(Rc)c_0MLfa7muVE>4|(tX){NX$z2g-YVwp|vI0GZF?O z&7AJrk*0{YVlkY`>=MDw?6UBMvNno7S60yMf*+o?{!qcqh~pKG8QY@1up%}g9~NUa z$nDy@S*y3)fxa1Tu`9N<9Hoh)yA8})lqxqV&AWXH?5*(@&Mpry)Og+i8LTtY5!2gK z$9`ScATJ%V|3c`p>V3hmt7CC7f+5>Rn&v%QN$alOs?OVVw<|Nr&05!J@kwX=lv{ZV zZPD#%IV6>I{>$dQv|#Bs_+`=9=T~aBOOz{S&&Lkc2|lN$$u|#InvhRZdsD zZ)U9O#c7d3Q;NA_RM>l=#-@o-v&e`KbmT2Edymvo1jTO9)Nu61rm0KsogW{B-S`;8 z!o^9`_k7&dpyc%SkHJw(ev$CeeP7fW#2Bg_YCLGb8#yS)tJ2iJG z+h0B#S#IYtk?))7=p;~<>yDcm%zGz$y^QQjJyc3`b!e{eIDBKd!u2R=Yo*K4NB#$P z_BYD9x*QtruZ-e;dp*~|PbyN=J$XO$wO8_$2BfF)luKc;350iTIS2qtu>0?nLp+bT z(m#hWXDTazAH$ei{d*}xlrxOQ(ZTTLTEzVglgS9?_85mozA@d4s(feQa*!q(pyU; zO~WofD8RpT>(@G|H*`bOXMelB!Q}qi{No*v^T+3jZQG6KuP#=JUo z*5|>MiO8p2y2fbUtld+dab-4@j@9DYu>;kmO6p5D#si03GM`o4`tTY1h;Cmo@~yWC zI~HVTh&AtBvYWa_-gn|VYxAzTb;Zt(ry^TC-|*Llqz=3zu$JBl^~^k*kNyKcVRm^U zG1EEbYiH4|(OV%)<}si1>Db{Q*0)3@uW!pX#3)rR=urHZx6&#RT-Vx*Vw3NNDtpQ; z3@V3*TS95cYI1zu9yWBObP80P>PgyB#h|2WFQKV*P(IyMm~;n)u~6PqAai)`#Pxd3dkI^kOI&EquN}@cWDBOxZykA{ zdU=aYzns9yB*jk#*car=iSJdB?}MA}#Jwr|MqBDW+Y*%{qUT<_FmOl5pzwle#D^JI z*%!}Z=qAClFKEY&PwIUq`c#c~M`dPEPJL~$tbIcng-3t-;zoOZvbqJ)@lCb-vvl+# zHnH!RRBpd#{6R7GoM2ul_!_6C0Js1Kc>YG^Yk&31(_A7eP8EGdVEtcCRo439e>m0c z!T43Dx;VisKpmeO>8jVfd(M1~Q^mrz9O^6oH>Vn~D;AJ>9AiRX=U&`>eE+9mT(xiWY92KP<6cmAg2vHG` z3sQyT2%$FtMIr$NAt1d-2`Lxfd-vZR@8{iPtg+UZWA8bCtU1=&_ie1s3S#zS004sL z&zb!J01WySXw)9^qU{}d55Irj%;fUzoJBezWwcfT)qlKs?EI#t(WJ|5>`3Ce$7V)u zFoY2|<13hDE2wn@L9GuP7DF9>)XRsTKVL4j$Mc_#u`n4=>|3#AhJc)(iyK4)Xs34b zFL~_gQVcZ9Pd|kLU~H|arun~*+o$G#?g+>~<>(Rq6Wf0iqnsJDj`n!MWHY0RiMu-y zyE1o7n;tVCxCZ+2-Qzti?*XcFl^QVAJULK?U4a?9u%TWA>gP1Q6h=0Ft$(HB~h)>mHW zH<19CeY6+BXVU<9IXZ%LhXETAg8F~cJpa~OSlRvM=OR2WMYf5)=Ax&BfF#jS#vNdz z4|_ZLKW3jQk8K)T{%1LQ!VvCuR}1ln7tFW^K{44&ClHh*d48&)AgmI)wa^()Kv1Zs zg*{&+;?WdcIOHoeeaRH0hyh-jagw!wmq(xwEHG_S?7>fI9cz%|ARq~!?rWC!#ACEf zz|~O^v+(MfRyTXQXaohPWMi}){sh@lxH4=BK+QtSb3^zaFQ3T=@y}S~CW)j)@_ru4b=bR!9*e4q zqm3jV0{gP%f*ZZ_?cft$@VLC4o$?2u)_woN-Ml;pR`T3MSth63(kF{^q#pwd6srUg z9_p84*&pF>e**VQ@)ao<=s&jC-D3723TAxi4Yj|%{6M+6D9}2yszzoH>b{(J{YNOw zR>cn;u^zI1ut5d{>fK&szIq0LRk8tzir+mlu3JmkJOQBQhpewU960#m=kwiX1J9sk znY+1VAh&cDiTd7nFFv;7XW|b z9yB6v|GT~aU(T<^$L>U3Afo_=c?b;v#65Wi$Q?W097{r4{O#Ve^{E{GjlC$pytNTz zX`;Rr37dsvV8#h4Rk1TPbCAW3i8h=%J2yPQPtDP6%4J@9ducxc{!KUR=O*%e%%Rje zq@rAgB#T(S6}kO$dwey>9Pd_W{ttnKy1qrWtK@^-m8DYju1N~wx$U3c>mP8~7+nr&1O$)El_qitXoA9>WjK>NUT`}HFm-RB-L ziV|b|OWj!9zsf7LoeQH)`sErR(Q4J1%FCl|PmMYDW=>xip~V7tqO;rK)FUb3QUO(c zj9A{KWqmF+$;QyK;92#bYvkjo&w#eMRPdG+qNrW-!T8&K3Dgc=&>3MyufGmckzf|N zN&E0peFb&pr%CO;##hB%HMr)`G)3r?0}XaiIMw^VE;OsPMr*xZmV``zG? zXUlkR-E~7Sb|0&luOhCA_$_QZ)_xeDEd&j@nB+@8&gFZ^T{d{+{;@{Zh1gy)&lB-+ z$3YC*4bkM;j)*p#Yg?(qR>u83aD#%@BGw%Yz$y3((ezAY>-nHpT8Pc6KdV2ZZs(=P zV5{!`==TEx10$4)i)iWSOujfDX&oWY+I{){bOa@Jco!Sh9qmVxqC*j{0A0)NY(xqIpTlLiZy>Yaza|XylPlvIrS2ayxEYE_CZ5>*p!awQoh=zP z4>ZtFg7cYFS0!Ud$v6*x{!={nOhAr6TvR#ghnD1yBBEF+MiY`oP&$OSN%G^q%Q9wi z9_e^r9@{)n8`yOZE0jTEiM3S4b!Hz}!7JzO#&pb8K-r6Y@R@fKDmrTbTXFJU* zfLn(pgqyZ*;hS#n(L{9(%_^kR|ec-JiV2YG#IWsxbKCDtY- zuV#f3w=M>zAR}3o_TGARC|#PVY%dr{zvdKNmNJ$q`F4YYwdag2e|nS^sVrM#$MZWW z(>qJ058mYOy55Ps-oZSRCCM!rpX%HiVagHk*4CS&g-S|Bs<#%mh45eX^1^zXirZF0 zG;8h_w+OsB?dT1{9*VauS8PPzdV{QLM;mV5(^y-2U3jn;6_cYpVgLF4LG(NR*_8Mu zQkq|4J3SUJZqMxrldB;)2U{@T%C>yt$v2DZxiae;$Uue!DUptxwGOgJk}>+b0cFuI zWXy2~cy7Jvxg!KR^~BpBUuxkPFmvLf4}8GFGSnC2^+8|59Zja-U)TS&MO6mpG7h*ujP^`8l-CI15S6~+nNes&6ZCc;3REpMwSeX_~C*uk< z^vo7joBHA2e-n z-HkM%Eb z(I2chJ1xiBl+inLzN_+RXswhuw@-obJ4&VCOSZm-wK%sl*Dwhe!{h|Igb_eOW8*J@ zu7L#CtJ_z4ucqS1$@_3kj-RW8Q_8BM{=isqY{{Y4zd0F%5K96Y>6szRQy2-Z!;IY8 zNx4|5$Z74A6%vx#PJUA~MHU}&5r114wtAW6@o_no`jRY*>jS+8IisbrlRtMEO_^gW z$^R_mY!HTuI<)&`&0}9R=FdJ_+)B;4E1gKJ_x9<@>8}(bIM!#7o?q&DcHTMJ{-;=w2!3rOQyvAos6pBu99=c@WTrr$OehG~|s zNnMjwnN(%|u&g8igA=m!zQs|E6Ai;QX^fVV7953~P4hSA=crZJg~(P$S)7Qu^DY=}H9kc|m}+N|%^yEU hyfn17bETt#BNfx2#H&kE&inPfIB#xcR%YrJ|1V^PDo_9b literal 0 HcmV?d00001 From 53b5b50f2decbb90db2e0dd72f38064b961c86d2 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:23:19 -0700 Subject: [PATCH 20/41] new attacks --- .claude/skills/add-game-content/SKILL.md | 94 ++++++++ .claude/skills/deploy-chomp/SKILL.md | 72 ++++++ .gitignore | 7 +- CLAUDE.md | 229 +------------------- drool/imgs/attacks/attack_spritesheet.json | 124 +++++++++-- drool/imgs/attacks/attack_spritesheet.png | Bin 146015 -> 199115 bytes drool/imgs/attacks/dual_shock.gif | Bin 0 -> 2353 bytes drool/imgs/attacks/dual_shock.png | Bin 0 -> 3089 bytes drool/imgs/attacks/electrocute.gif | Bin 0 -> 4919 bytes drool/imgs/attacks/electrocute.png | Bin 0 -> 7777 bytes drool/imgs/attacks/modal_bolt_fire.gif | Bin 0 -> 3742 bytes drool/imgs/attacks/modal_bolt_fire.png | Bin 0 -> 4606 bytes drool/imgs/attacks/modal_bolt_ice.gif | Bin 0 -> 4056 bytes drool/imgs/attacks/modal_bolt_ice.png | Bin 0 -> 5159 bytes drool/imgs/attacks/modal_bolt_lightning.gif | Bin 0 -> 4255 bytes drool/imgs/attacks/modal_bolt_lightning.png | Bin 0 -> 5770 bytes drool/imgs/attacks/negative_thoughts.gif | Bin 0 -> 6021 bytes drool/imgs/attacks/negative_thoughts.png | Bin 0 -> 8214 bytes drool/imgs/attacks/quickstorm.gif | Bin 0 -> 1890 bytes drool/imgs/attacks/quickstorm.png | Bin 0 -> 2411 bytes drool/imgs/attacks/round_trip.gif | Bin 0 -> 1709 bytes drool/imgs/attacks/round_trip.png | Bin 0 -> 1796 bytes drool/imgs/attacks/set_ablaze.gif | Bin 0 -> 3687 bytes drool/imgs/attacks/set_ablaze.png | Bin 0 -> 5152 bytes drool/imgs/mon_switch.png | Bin 47935 -> 47978 bytes processing/generateMonsTypeScript.py | 20 +- src/game-layer/CLAUDE.md | 72 ++++++ 27 files changed, 366 insertions(+), 252 deletions(-) create mode 100644 .claude/skills/add-game-content/SKILL.md create mode 100644 .claude/skills/deploy-chomp/SKILL.md create mode 100644 drool/imgs/attacks/dual_shock.gif create mode 100644 drool/imgs/attacks/dual_shock.png create mode 100644 drool/imgs/attacks/electrocute.gif create mode 100644 drool/imgs/attacks/electrocute.png create mode 100644 drool/imgs/attacks/modal_bolt_fire.gif create mode 100644 drool/imgs/attacks/modal_bolt_fire.png create mode 100644 drool/imgs/attacks/modal_bolt_ice.gif create mode 100644 drool/imgs/attacks/modal_bolt_ice.png create mode 100644 drool/imgs/attacks/modal_bolt_lightning.gif create mode 100644 drool/imgs/attacks/modal_bolt_lightning.png create mode 100644 drool/imgs/attacks/negative_thoughts.gif create mode 100644 drool/imgs/attacks/negative_thoughts.png create mode 100644 drool/imgs/attacks/quickstorm.gif create mode 100644 drool/imgs/attacks/quickstorm.png create mode 100644 drool/imgs/attacks/round_trip.gif create mode 100644 drool/imgs/attacks/round_trip.png create mode 100644 drool/imgs/attacks/set_ablaze.gif create mode 100644 drool/imgs/attacks/set_ablaze.png create mode 100644 src/game-layer/CLAUDE.md diff --git a/.claude/skills/add-game-content/SKILL.md b/.claude/skills/add-game-content/SKILL.md new file mode 100644 index 00000000..a6562843 --- /dev/null +++ b/.claude/skills/add-game-content/SKILL.md @@ -0,0 +1,94 @@ +--- +name: add-game-content +description: Workflow for adding a new mon, move, ability, or effect to chomp — CSV data, contract patterns, and validation steps. Use when adding/creating a new mon, move, ability, or status/battlefield effect. +--- + +### Adding a New Mon + +1. Add mon stats to `drool/mons.csv` (HP, Attack, Defense, SpAtk, SpDef, Speed, Types) +2. Add 4 moves to `drool/moves.csv` (Name, Mon, Power, Stamina, Accuracy, Priority, Type, Class, etc.) +3. Add ability to `drool/abilities.csv` (Name, Mon, Effect description) +4. Create directory `src/mons//` (lowercase, e.g., `src/mons/aurox/`) +5. Implement 4 move contracts as `PascalCase.sol` files (see "Move Implementation Patterns" below) +6. Implement 1 ability contract as `PascalCase.sol` (see "Ability Patterns" below) +7. Run `python processing/validateMoves.py` to validate contracts match CSV data +8. Run `python processing/generateSolidity.py` to regenerate `SetupMons.s.sol` +9. Add tests in `test/mons/Test.sol` extending `BattleHelper` + +### Move Implementation Patterns + +Choose the simplest pattern that fits the move's behavior: + +**1. Pure `StandardAttack`** — constructor-only, no `move()` override. For straightforward damaging moves or simple effect-applying moves. Pass `EFFECT` + `EFFECT_ACCURACY` for probabilistic status application (e.g., 30% chance to burn): + +```solidity +contract Blow is StandardAttack { + constructor(IEngine _ENGINE, ITypeCalculator _TYPE_CALCULATOR) + StandardAttack(msg.sender, _ENGINE, _TYPE_CALCULATOR, ATTACK_PARAMS({ + BASE_POWER: 70, STAMINA_COST: 2, ACCURACY: DEFAULT_ACCURACY, + PRIORITY: DEFAULT_PRIORITY, MOVE_TYPE: Type.Air, + EFFECT_ACCURACY: 0, MOVE_CLASS: MoveClass.Physical, + CRIT_RATE: DEFAULT_CRIT_RATE, VOLATILITY: DEFAULT_VOL, + NAME: "Blow", EFFECT: IEffect(address(0)) + })) + {} +} +``` + +**2. `StandardAttack` + `move()` override** — for moves with side effects after damage (recoil, self-switch, self-status, multi-hit). Override the full `move(...)` signature and call `_move()`, which returns `(int32 damage, bytes32 eventType)`, then add custom logic: + +```solidity +function move( + IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex, + uint256 targetBits, uint256 activesPacked, uint16 extraData, uint256 rng +) public override { + (int32 damage,) = _move(engine, battleKey, attackerPlayerIndex, targetBits, rng); + if (damage > 0) { + engine.dealDamage(attackerPlayerIndex, attackerMonIndex, selfDamage); // recoil + } +} +``` + +A move's target domain is client-facing metadata in `moves.csv`'s `TargetSpec` column (→ TS `Move.targetSpec`), not a Solidity declaration; `validateMoves.py` checks each move's `targetBits` behavior against it. The player's chosen target arrives as `targetBits` (resolved against `activesPacked` via `TargetLib`); the old `targetSpec()` / `extraDataType()` / `ExtraDataType` selector layers have been removed. + +**3. Custom `IMoveSet`** — for complex conditional moves (variable power, healing, stat manipulation, reading opponent state). Implement the `IMoveSet` functions directly (`move` + metadata getters + `getMeta`). Deal damage via `engine.dispatchStandardAttack` / `dispatchCustomAttack` (passing `targetBits`); `AttackCalculator._calculateDamage()` now just delegates to `dispatchCustomAttack`. Store dependencies as `immutable`. + +**4. `IMoveSet` + `BasicEffect` hybrid** — for moves that persist as effects across turns (traps, delayed damage, per-turn modifiers). Implement both interfaces in one contract. The `move()` function calls `ENGINE.addEffect(playerIndex, monIndex, IEffect(address(this)), ...)`. + +**Shared libraries** — when multiple moves in the same mon share logic, extract into a `library` contract in the same directory (e.g., `NineNineNineLib.sol`, `HeatBeaconLib.sol`). + +### Ability Patterns + +Each mon has exactly one ability, implemented in its own `PascalCase.sol` file within the mon directory. + +**Pure `IAbility`** — for one-time switch-in actions (deal damage, apply a stat boost). Implement `name()` and `activateOnSwitch()`. No persistent state. (e.g., `PreemptiveShock`, `SaviorComplex`) + +**`IAbility` + `BasicEffect`** (most common) — for abilities with ongoing lifecycle effects. `activateOnSwitch()` registers `address(this)` as an effect on the mon, then hooks into effect lifecycle via `getStepsBitmap()`. Must override `name()` with `override(IAbility, BasicEffect)`. Uses an idempotency guard to prevent duplicate registration: + +```solidity +function activateOnSwitch(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { + (EffectInstance[] memory effects,) = ENGINE.getEffects(battleKey, playerIndex, monIndex); + for (uint256 i; i < effects.length; i++) { + if (address(effects[i].effect) == address(this)) return; + } + ENGINE.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); +} +``` + +For once-per-battle abilities (e.g., `RiseFromTheGrave`), use a `globalKV` flag instead of the effect-list check. + +### Adding a New Effect + +Effects fall into several categories depending on scope: + +- **Status effects** (`src/effects/status/`): Extend the `StatusEffect` marker base. One-status-per-mon is **Engine-native**: each status folds a unique class id into `getStepsBitmap()` bits 10-13 (an internal `STATUS_CLASS` constant; deployable ids 1-14, 15 reserved for test mocks), and the Engine tracks it in a per-mon lane (`BattleConfig.monStatusLanes`, slot 3) — gating adds before any external call, setting the lane on apply, clearing it on removal. Readers use `engine.getMonStatusClass` (identity/existence) and `engine.clearMonStatus(player, mon, expectedClass)` (remove; 0 = any class); reader contracts cache the class id as a constructor `immutable` derived from the injected status's `getStepsBitmap()`. Escalating statuses (Burn) set `HAS_REAPPLY_BIT` (bit 14) and implement `IStatusEffect.onReapply`, which the Engine calls on a same-class re-apply; without the bit a re-apply is a zero-call no-op. Extra apply conditions beyond exclusivity (Sleep's single-sleeper rule) live in a `shouldApply` override (no `ALWAYS_APPLIES_BIT`). Shared across mons — deployed once, injected into moves via constructor parameters. (e.g., `BurnStatus`, `FrostbiteStatus`, `SleepStatus`) +- **Battlefield effects** (`src/effects/battlefield/`): Extend `BasicEffect`, use `targetIndex=2` for global scope. (e.g., `Overclock`) +- **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StaminaRegen` for per-turn recovery). NOTE: stat modifiers are **not** an effect — they are inlined Engine functions (`addStatBoost`/`removeStatBoost`/…); see "Stat Boosts" in the root CLAUDE.md. +- **Mon-local effects** (`src/mons//`): Abilities or move-effect hybrids that only apply to one mon. These live in the mon's directory, not in `src/effects/`. + +To implement a new effect: +1. Extend `BasicEffect` (or `StatusEffect` for status conditions) +2. Override the relevant lifecycle hooks (`onRoundEnd`, `onAfterDamage`, `onPreDamage`, …) — each receives `activesPacked` for local slot resolution via `TargetLib` +3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call. Before adding callback getters, opt into a supported fresh context by mirroring the applicable low lifecycle bit into bits 16-31 as described in the root CLAUDE.md. If the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the low bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `SleepStatus`'s single-sleeper rule) must NOT set this bit. Statuses additionally carry their class id in bits 10-13 and `HAS_REAPPLY_BIT` (bit 14) iff they implement `onReapply`. `python processing/validateEffectBitmaps.py` checks all of these invariants (bit pairing, class range 1-14, cross-src uniqueness) and is run as part of `deploy.py`. +4. Return `(updatedExtraData, removeAfterRun)` from hooks — use `extraData` (bytes32) to carry state between turns (counters, degrees, flags) +5. Shared effects are injected into moves/abilities via constructor parameters at deploy time — there is no runtime effect registry diff --git a/.claude/skills/deploy-chomp/SKILL.md b/.claude/skills/deploy-chomp/SKILL.md new file mode 100644 index 00000000..ae4ff871 --- /dev/null +++ b/.claude/skills/deploy-chomp/SKILL.md @@ -0,0 +1,72 @@ +--- +name: deploy-chomp +description: Build and deploy pipeline for chomp — processing scripts, the Solidity-to-TS/Rust transpiler, deployment order, and CI/CD reference. Use when running codegen/validation scripts, transpiling, or deploying to testnet/mainnet. +--- + +### Processing Scripts (Python 3.11+) + +```bash +python processing/validateMoves.py # Validate contracts vs CSV +python processing/validateEffectBitmaps.py # Validate ALWAYS_APPLIES_BIT usage on IEffect contracts +python processing/generateSolidity.py # Generate SetupMons.s.sol +python processing/generateSetupCPU.py # Generate SetupCPU.s.sol from cpu-teams.json +python processing/deploy.py --testnet # Full deployment (forge scripts + codegen) +python processing/deploy.py --mainnet # Production deployment +``` + +Python dependencies: `numpy`, `pexpect`, `pillow` (managed via `uv`, see `pyproject.toml`) + +### Transpiler (Solidity to TypeScript / Rust) + +Converts Solidity contracts to TypeScript for local battle simulation: + +```bash +# Transpile all contracts +python3 -m transpiler src/ -o transpiler/ts-output -d src --emit-metadata + +# Run transpiler unit tests (the TS runtime/integration tests moved to munch with the codegen sync) +python3 transpiler/test_transpiler.py +``` + +**Rust target (sim performance).** The same front-end also drives a Rust +backend (`codegen_rs/`, native ints instead of bigint — see +`transpiler/codegen_rs/README.md`): + +```bash +python3 -m transpiler src/ --target rust # emit transpiler/rs-output (cargo workspace) +cd transpiler/rs-output && cargo build --release # engine + runtime + strategies (+ its bins) +CHOMP_ROOT=$(pwd)/../.. ./target/release/arena --games 6000 # whole-game batches, win-rate table +CHOMP_ROOT=$(pwd)/../.. ./target/release/trace --games 6000 --narrate 0 # trace analysis + greedy counterfactual +``` + +The full engine, all mons/effects, and the three CPU strategies (hard, +greedy, override) are transpiled/ported. The arena is now a **standalone, +pure-Rust binary** (`chomp-strategies`' `arena`/`trace` bins): it loads the +roster from `drool/*.csv` + `src/mons/*.json` at runtime and runs whole games +natively (~2,000 games/s at 8 threads, deterministic across thread counts). +The stacks are DECOUPLED — the Rust side was ported move-for-move against TS +and then cut loose; it may diverge freely (including its rng stream and CPU +decisions) as the prototyping substrate, and port-backs to the TS game carry +no bit-identicality requirement. Emission is allowlisted in +`transpiler-config-rust.json`. `rs-output/` is regenerated (gitignored); +hand-written crates live in `transpiler/{runtime-rs,strategies-rs}`. The +former bun↔Rust FFI seam (`chomp_run_games`, the `ffi` crate, and +`scripts/batch_benchmark.ts`) was removed once the pure-Rust arena replaced +it; the verification-era machinery (golden-vector suites, replay fixtures, +the drive-mode adapter, lockstep gates) lives in git history if parity ever +needs re-proving. The TS arena driver (`sims/src/arena/game.ts`) remains as +the port-back reference. + +**Known limitation — TODO when a second codebase needs it.** The transpiler hardcodes three TS namespace names (`Enums`, `Structs`, `Constants`) for type-only Solidity files. File-type detection is content-based (a file with only enums maps to the `Enums` namespace regardless of filename), but the *namespace name itself* is fixed, and only structs are tracked per source path. A codebase that splits types across multiple files (e.g. `PoolStructs.sol` + `OrderStructs.sol`, or `Errors.sol`) would produce colliding imports. To generalize: add `enum_paths` / `constant_paths` to `transpiler/type_system/registry.py` mirroring `struct_paths`, derive the namespace name from each source file's basename, and have `imports.py:_generate_module_imports` emit one `import * as ` per actually-referenced source module instead of three blanket imports. + +### Deployment Order + +1. `EngineAndPeriphery.s.sol` - Engine, type calculator, `SignedCommitManager`, `GachaTeamRegistry`, `SignedMatchmaker`, CPU host, `SimplePM`, shared effects +2. `SetupMons.s.sol` - All mon contracts (moves, abilities) +3. `SetupCPU.s.sol` - CPU players + +### CI/CD + +GitHub Actions runs on pull requests (`.github/workflows/main.yml`): +- `forge build` +- `forge test -vvv` diff --git a/.gitignore b/.gitignore index 6f6bd932..fad6d74d 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,12 @@ transpiler/ts-output/ .vscode/ .pytest_cache/ .venv/ -.claude/ + +# Ignore everything in .claude except shared skills +.claude/* +!.claude/skills +!.claude/skills/** + node_modules/ .deploys/ diff --git a/CLAUDE.md b/CLAUDE.md index 3b614249..fdaaf82d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -379,76 +379,7 @@ How it works: ### Gacha & Progression System -`GachaTeamRegistry` is the concrete leaf — it's an `IEngineHook` (subscribes to `OnBattleEnd`) and composes seven abstract bases that each own one slice of state and behavior: - -| Abstract | Owns | -|---|---| -| `MonOwnership` | `monsOwned` per-player set; ownership view + bulk-check helpers (`_isOwnerBatch`, `_validateOwnership`). Satisfies the `_isFacetMonOwned` hook on `Facets`. | -| `MonRegistry` | Owner-managed mon catalog (`monStats`, `monMoves`, `monAbilities`, `monMetadata`, sequential `monIds` set). Inherits `ITeamRegistry` so its `createMon`/`modifyMon`/batched-getters bind directly. Backs the `_getMonStatsForFacets` hook used by facet delta computation. | -| `PlayerProfile` | Packed `playerData[address]` (one uint256 per player) + the owner-managed `isAssigner` allowlist. Exposes `pointsBalance`, `isWhitelistedOpponent`, the bulk admin flag setters, and `assignPoints` (IGachaPointsAssigner). | -| `PackedTeamStore` | Bit-packed team CRUD: `teamGroupsPacked` (4 teams × 64 bits per slot), `teamOrderPacked` (16-slot live bitmap + display order). Constructor takes `MONS_PER_TEAM` / `MOVES_PER_MON` immutables. Subclass hooks: `_packedTeamValidateOwnership`, `_packedTeamIsCpuOpponent`, `_packedTeamGetMonData`. | -| `Facets` | 12-facet ±5% stat tradeoff system (see below). Pure helpers, packed per-mon facet data, `assignFacets`. | -| `MonExp` | Packed per-mon exp (`packedExpForMon`), level curve, level-up facet draws (`_processLevelUps`), public `getExp` / `getLevel` / `getExpAndLevelsFor*`, assigner-gated `assignExp`. Inherits `Facets` + `PackedTeamStore` because level-ups draw facets and team views need the lane helpers. Subclass hooks: `_assertExpAssigner`, `_monRegistrySize`. | -| `Quests` | Daily quest pool + packed predicate evaluator. Subclass hook: `_extract` (opcode dispatch implemented by the leaf since it sees Engine + all sub-systems). | - -The leaf adds: the `onBattleEnd` orchestration (streak / quest / points / exp loop, plus a `_applyExpAndFacetDraws` walk that mirrors `assignExp` but with KO bitmap + streak + event packing), gacha rolling (`firstRoll`/`roll`/`_rollInto`), the per-(user, opponent) CPU phantom facet config, the `getTeams` variant that folds facet deltas in, the `_extract` quest opcode dispatch, and the wiring overrides for every subclass hook. With this split the leaf is ~750 LOC of integration and event-shape concerns; each base is independently auditable. - -**Rolling.** Mon ids are sequential starting at 0 (`createMon` enforces `monId == monIds.length()`). Ids `[0, NUM_STARTERS)` (= 3) are *starter* mons. - -- `firstRoll(uint256 starterId)` — one-shot per player, onboarding in a single tx. Caller picks `starterId ∈ {0,1,2}`; the contract guarantees that mon at slot 0 of the result and rolls `INITIAL_ROLLS - 1` (= 3) more uniformly from `[NUM_STARTERS, numMons)`. Free. Also creates the player's first team (slot 0) from the first `MONS_PER_TEAM` rolled ids, via the same internal path `createTeam` uses (`_createTeamForUser`) — skipping the redundant ownership check since those ids were just written. The constructor reverts `MonsPerTeamExceedsInitialRolls` if `MONS_PER_TEAM > INITIAL_ROLLS`, since a deployment configured that way could never satisfy this. -- `roll(uint256 numRolls)` — paid (`ROLL_COST` per roll, default 16 points). Uniform across the entire pool. Reverts `NoMoreStock` once the caller owns every mon. -- Linear-probing dedup keeps draws inside their window so `firstRoll`'s 3 random picks never land on a starter. - -**Points / exp / facets storage.** All packed for gas: - -``` -playerData[address] (1 slot per player): - bit 255 bonusAwarded (first-game-ever bonus claimed) - bit 254 isWhitelistedAsOpponent (admin-set; replaces a separate mapping) - bit 253 (reserved; formerly isHardCpu) - bits 250-252 streakDay (1..STREAK_FLAT_BONUS_MAX; 0 = no streak yet) - bits 224-249 (reserved) - bits 192-223 lastQuestCompletedDay (uint32 calendar day) - bits 160-191 lastSeenTimestamp (uint32 seconds; last battle of ANY kind — drives streak grace/reset) - bits 128-159 lastFirstGameTimestamp (uint32 seconds; last streak-bonus game — gates the 24h cooldown) - bits 0-127 pointsBalance (uint128) - -packedExpForMon[player][monId / 16]: 16 mons × 16 bits each, capped at 65535. -facetData[player][monId / 16]: 16 mons × 16 bits each - (bits 0-11 unlockedBitmap, bits 12-15 assignedFacetId). -``` - -Streak is timestamp-driven (not calendar-day): a battle qualifies for the streak bonus -when ≥24h have passed since `lastFirstGameTimestamp` (the last *bonus-earning* game). On a -qualifying battle the ratchet-vs-reset decision is measured from `lastSeenTimestamp` (the -last battle of *any* kind, advanced every battle): a gap >36h (`STREAK_GRACE_WINDOW`) of -genuine inactivity resets `streakDay` to 1, otherwise it ratchets up toward the cap of -`STREAK_FLAT_BONUS_MAX` (= 5). Splitting the two anchors is deliberate — measuring the -reset from the bonus anchor instead would strand players who play slightly more often than -once per 24h (their sub-24h plays advance no anchor, so the next day reads a phantom ~46h -gap and resets the streak forever). - -Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws` walks the team in one pass and coalesces SSTOREs by bucket. - -**Battle rewards (`onBattleEnd`).** CPU side is short-circuited (no SSTOREs, no event). For each human side: - -- Base points: `POINTS_PER_WIN` (2) on win, `POINTS_PER_LOSS` (1) otherwise. -- Streak flat: `streakDay` (1..5) added inside the parenthetical of both the points and per-mon-exp formulas. Only granted when the battle qualifies as "first of day" (≥24h since the last grant). -- Points formula: `(basePts + streakFlat) × gachaMult + firstGameEverBonus`. `gachaMult` is `×QUEST_REWARD_MULT` (= 2) when the active daily quest completes (winner-only, one-shot per day), else 1. `firstGameEverBonus` is `+FIRST_GAME_EVER_BONUS` (= 16), one-shot ever, applied *after* the multiplier. -- Per-mon exp formula: `(baseExp + streakFlat) × expMult`, capped at 65535. `baseExp` is `EXP_PER_SURVIVING_MON` (2) for alive slots, `EXP_PER_KOD_MON` (1) for KO'd slots. -- `expMult` stack: a flat `GAME_EXP_MULT` (×2) on **every** battle regardless of game type (PvP, CPU, hard or not), times `QUEST_REWARD_MULT` (×2) on quest completion. Max stack = ×4. (There is no longer a PvP-vs-CPU or hard-CPU exp distinction — the old client-set "hard CPU" flag let any user self-grant the bonus, so it was removed.) -- Level-ups (12-tier curve, capped at level 12 to match `TOTAL_FACETS`) trigger one facet draw per level crossed. - -**Facets.** 12 systematically-derived stat tradeoffs across 4 stat groups (`HP`, `Atk`, `Def`, `Speed`). `_facetDef(facetId)` is pure — no constant table. Magnitudes are **boost-indexed**: the boost stat determines both the boost% and the cost% paid on the nerfed stat. HP/Atk/Def boosts are symmetric (+5% / -5%); speed boosts pay a heavier cost (+5% / -10%) so they can't cheaply break speed ties. The percentages live as `BOOST_PCT_*` / `COST_PCT_*` constants in `Facets.sol`. Unlocks are persistent per-mon; `assignFacets(monIds, facetIds)` is a free bulk re-assign that requires the caller to own every listed mon and the facet to be in the unlocked bitmap (`facetId == 0` clears). `GachaTeamRegistry.getTeams()` folds the active facet's delta into each mon's stats before returning, so by the time the Engine stores teams in `BattleConfig` they already reflect the boost/cost. `validateMon` no longer checks stat equality (the round-trip would always fail with facets applied) — moves and ability membership are still enforced. - -**CPU opponent facets.** When fighting a whitelisted opponent (CPU), the human caller picks the CPU's team *and* its facet config in one call: `setOpponentTeam(opponent, monIndices, facetIds)`. Per-user-per-CPU storage (`opponentTeamFacetsPacked[opponent][phantomKey]`) keyed by the same `uint16(uint160(msg.sender))` phantom slot as the team. No ownership/unlock checks — any facet 0..12 is allowed. `getTeamsWithDeltas` short-circuits to this slot-indexed config when a side is `isWhitelistedOpponent`, so per-user CPU facet configurations stay isolated even when many users fight the same CPU. - -**Quests.** Owner-managed `questPool`. One quest is active per UTC day, derived statelessly as `keccak256(day) % poolLength` (day = `block.timestamp / 1 days` + an owner-settable `_dayOffset`) — no rotation SSTORE and no race between concurrent battles. Each quest has up to `MAX_PREDICATES_PER_QUEST` (6) AND-composed predicates packed into one storage slot (41 bits each: `op` 5b, `cmp` 3b, `negate` 1b, `arg` 16b, `operand` 16b — total 246b + 3b count). Opcodes cover battle context (`TURNS`, `ALIVE_COUNT`, `ACTIVE_SLOT_INDEX`, `MON_KO_AT_SLOT`), team composition (`HAS_MON_ID`), per-mon progression (`MON_LEVEL`, `MON_FACET`), and live battle state (`MON_STATE` via `Engine.getMonStateForBattle`). - -**Events.** - -- `Roll(address indexed player, uint256[] monIds, uint256 pointsSpent)` — fires on both `firstRoll` (spend = 0) and paid `roll`. -- `GachaEvent(bytes32 indexed battleKey, uint256 p0Packed, uint256 p1Packed)` — one per battle, carrying both sides' packed payloads (CPU side is 0). Layout sized for `MONS_PER_TEAM` up to 8: points (bits 0-15), per-mon exp gain (bits 16-79, 8 lanes × 8b), per-mon facets unlocked this battle (bits 80-175, 8 lanes × 12-bit bitmap = 1 bit per facet id), `BONUS_*` flags (bits 176-183: `FIRST_ROLL` (bit 0) | `FIRST_GAME` (bit 1) | bit 2 reserved/formerly `HARD_CPU` | `QUEST` (bit 3)), combined exp multiplier (bits 184-191), outcome (bits 192-199: 0=loss, 1=win, 2=draw), `streakDay` (bits 200-202). Lanes saturate so a future tuning blow-up can't bleed into neighbouring fields. +`GachaTeamRegistry` (points, exp, facets, quests, rolling) is documented in `src/game-layer/CLAUDE.md`, which loads automatically when working under that directory. ### Storage Architecture @@ -501,165 +432,13 @@ When implementing new features or refactors, follow a test-first approach: 3. Implement the changes 4. Run to verify the tests pass -### Adding a New Mon - -1. Add mon stats to `drool/mons.csv` (HP, Attack, Defense, SpAtk, SpDef, Speed, Types) -2. Add 4 moves to `drool/moves.csv` (Name, Mon, Power, Stamina, Accuracy, Priority, Type, Class, etc.) -3. Add ability to `drool/abilities.csv` (Name, Mon, Effect description) -4. Create directory `src/mons//` (lowercase, e.g., `src/mons/aurox/`) -5. Implement 4 move contracts as `PascalCase.sol` files (see "Move Implementation Patterns" below) -6. Implement 1 ability contract as `PascalCase.sol` (see "Ability Patterns" below) -7. Run `python processing/validateMoves.py` to validate contracts match CSV data -8. Run `python processing/generateSolidity.py` to regenerate `SetupMons.s.sol` -9. Add tests in `test/mons/Test.sol` extending `BattleHelper` - -### Move Implementation Patterns - -Choose the simplest pattern that fits the move's behavior: - -**1. Pure `StandardAttack`** — constructor-only, no `move()` override. For straightforward damaging moves or simple effect-applying moves. Pass `EFFECT` + `EFFECT_ACCURACY` for probabilistic status application (e.g., 30% chance to burn): - -```solidity -contract Blow is StandardAttack { - constructor(IEngine _ENGINE, ITypeCalculator _TYPE_CALCULATOR) - StandardAttack(msg.sender, _ENGINE, _TYPE_CALCULATOR, ATTACK_PARAMS({ - BASE_POWER: 70, STAMINA_COST: 2, ACCURACY: DEFAULT_ACCURACY, - PRIORITY: DEFAULT_PRIORITY, MOVE_TYPE: Type.Air, - EFFECT_ACCURACY: 0, MOVE_CLASS: MoveClass.Physical, - CRIT_RATE: DEFAULT_CRIT_RATE, VOLATILITY: DEFAULT_VOL, - NAME: "Blow", EFFECT: IEffect(address(0)) - })) - {} -} -``` - -**2. `StandardAttack` + `move()` override** — for moves with side effects after damage (recoil, self-switch, self-status, multi-hit). Override the full `move(...)` signature and call `_move()`, which returns `(int32 damage, bytes32 eventType)`, then add custom logic: - -```solidity -function move( - IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex, - uint256 targetBits, uint256 activesPacked, uint16 extraData, uint256 rng -) public override { - (int32 damage,) = _move(engine, battleKey, attackerPlayerIndex, targetBits, rng); - if (damage > 0) { - engine.dealDamage(attackerPlayerIndex, attackerMonIndex, selfDamage); // recoil - } -} -``` - -A move's target domain is client-facing metadata in `moves.csv`'s `TargetSpec` column (→ TS `Move.targetSpec`), not a Solidity declaration; `validateMoves.py` checks each move's `targetBits` behavior against it. The player's chosen target arrives as `targetBits` (resolved against `activesPacked` via `TargetLib`); the old `targetSpec()` / `extraDataType()` / `ExtraDataType` selector layers have been removed. +### Adding a New Mon, Move, Ability, or Effect -**3. Custom `IMoveSet`** — for complex conditional moves (variable power, healing, stat manipulation, reading opponent state). Implement the `IMoveSet` functions directly (`move` + metadata getters + `getMeta`). Deal damage via `engine.dispatchStandardAttack` / `dispatchCustomAttack` (passing `targetBits`); `AttackCalculator._calculateDamage()` now just delegates to `dispatchCustomAttack`. Store dependencies as `immutable`. - -**4. `IMoveSet` + `BasicEffect` hybrid** — for moves that persist as effects across turns (traps, delayed damage, per-turn modifiers). Implement both interfaces in one contract. The `move()` function calls `ENGINE.addEffect(playerIndex, monIndex, IEffect(address(this)), ...)`. - -**Shared libraries** — when multiple moves in the same mon share logic, extract into a `library` contract in the same directory (e.g., `NineNineNineLib.sol`, `HeatBeaconLib.sol`). - -### Ability Patterns - -Each mon has exactly one ability, implemented in its own `PascalCase.sol` file within the mon directory. - -**Pure `IAbility`** — for one-time switch-in actions (deal damage, apply a stat boost). Implement `name()` and `activateOnSwitch()`. No persistent state. (e.g., `PreemptiveShock`, `SaviorComplex`) - -**`IAbility` + `BasicEffect`** (most common) — for abilities with ongoing lifecycle effects. `activateOnSwitch()` registers `address(this)` as an effect on the mon, then hooks into effect lifecycle via `getStepsBitmap()`. Must override `name()` with `override(IAbility, BasicEffect)`. Uses an idempotency guard to prevent duplicate registration: - -```solidity -function activateOnSwitch(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { - (EffectInstance[] memory effects,) = ENGINE.getEffects(battleKey, playerIndex, monIndex); - for (uint256 i; i < effects.length; i++) { - if (address(effects[i].effect) == address(this)) return; - } - ENGINE.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); -} -``` - -For once-per-battle abilities (e.g., `RiseFromTheGrave`), use a `globalKV` flag instead of the effect-list check. - -### Adding a New Effect - -Effects fall into several categories depending on scope: - -- **Status effects** (`src/effects/status/`): Extend the `StatusEffect` marker base. One-status-per-mon is **Engine-native**: each status folds a unique class id into `getStepsBitmap()` bits 10-13 (an internal `STATUS_CLASS` constant; deployable ids 1-14, 15 reserved for test mocks), and the Engine tracks it in a per-mon lane (`BattleConfig.monStatusLanes`, slot 3) — gating adds before any external call, setting the lane on apply, clearing it on removal. Readers use `engine.getMonStatusClass` (identity/existence) and `engine.clearMonStatus(player, mon, expectedClass)` (remove; 0 = any class); reader contracts cache the class id as a constructor `immutable` derived from the injected status's `getStepsBitmap()`. Escalating statuses (Burn) set `HAS_REAPPLY_BIT` (bit 14) and implement `IStatusEffect.onReapply`, which the Engine calls on a same-class re-apply; without the bit a re-apply is a zero-call no-op. Extra apply conditions beyond exclusivity (Sleep's single-sleeper rule) live in a `shouldApply` override (no `ALWAYS_APPLIES_BIT`). Shared across mons — deployed once, injected into moves via constructor parameters. (e.g., `BurnStatus`, `FrostbiteStatus`, `SleepStatus`) -- **Battlefield effects** (`src/effects/battlefield/`): Extend `BasicEffect`, use `targetIndex=2` for global scope. (e.g., `Overclock`) -- **Shared utility effects** (`src/effects/`): Deployed once, used by many contracts. (e.g., `StaminaRegen` for per-turn recovery). NOTE: stat modifiers are **not** an effect — they are inlined Engine functions (`addStatBoost`/`removeStatBoost`/…); see "Stat Boosts" above. -- **Mon-local effects** (`src/mons//`): Abilities or move-effect hybrids that only apply to one mon. These live in the mon's directory, not in `src/effects/`. - -To implement a new effect: -1. Extend `BasicEffect` (or `StatusEffect` for status conditions) -2. Override the relevant lifecycle hooks (`onRoundEnd`, `onAfterDamage`, `onPreDamage`, …) — each receives `activesPacked` for local slot resolution via `TargetLib` -3. Return a bitmap from `getStepsBitmap()` indicating which hooks to call. Before adding callback getters, opt into a supported fresh context by mirroring the applicable low lifecycle bit into bits 16-31 as described above. If the effect doesn't override `shouldApply()` (i.e. it relies on `BasicEffect`'s default, which always returns `true`), OR the low bitmap with `ALWAYS_APPLIES_BIT` (`src/Constants.sol`) so the Engine skips the external `shouldApply()` call on `addEffect`. Effects that override `shouldApply()` with real gating logic (e.g. `SleepStatus`'s single-sleeper rule) must NOT set this bit. Statuses additionally carry their class id in bits 10-13 and `HAS_REAPPLY_BIT` (bit 14) iff they implement `onReapply`. `python processing/validateEffectBitmaps.py` checks all of these invariants (bit pairing, class range 1-14, cross-src uniqueness) and is run as part of `deploy.py`. -4. Return `(updatedExtraData, removeAfterRun)` from hooks — use `extraData` (bytes32) to carry state between turns (counters, degrees, flags) -5. Shared effects are injected into moves/abilities via constructor parameters at deploy time — there is no runtime effect registry +The step-by-step workflow (CSV data, which move/ability contract pattern to choose, effect bitmap rules) is in the `add-game-content` skill (`.claude/skills/add-game-content/SKILL.md`) — invoke it when adding new game content. ## Build & Deploy Pipeline -### Processing Scripts (Python 3.11+) - -```bash -python processing/validateMoves.py # Validate contracts vs CSV -python processing/validateEffectBitmaps.py # Validate ALWAYS_APPLIES_BIT usage on IEffect contracts -python processing/generateSolidity.py # Generate SetupMons.s.sol -python processing/generateSetupCPU.py # Generate SetupCPU.s.sol from cpu-teams.json -python processing/deploy.py --testnet # Full deployment (forge scripts + codegen) -python processing/deploy.py --mainnet # Production deployment -``` - -Python dependencies: `numpy`, `pexpect`, `pillow` (managed via `uv`, see `pyproject.toml`) - -### Transpiler (Solidity to TypeScript / Rust) - -Converts Solidity contracts to TypeScript for local battle simulation: - -```bash -# Transpile all contracts -python3 -m transpiler src/ -o transpiler/ts-output -d src --emit-metadata - -# Run transpiler unit tests (the TS runtime/integration tests moved to munch with the codegen sync) -python3 transpiler/test_transpiler.py -``` - -**Rust target (sim performance).** The same front-end also drives a Rust -backend (`codegen_rs/`, native ints instead of bigint — see -`transpiler/codegen_rs/README.md`): - -```bash -python3 -m transpiler src/ --target rust # emit transpiler/rs-output (cargo workspace) -cd transpiler/rs-output && cargo build --release # engine + runtime + strategies (+ its bins) -CHOMP_ROOT=$(pwd)/../.. ./target/release/arena --games 6000 # whole-game batches, win-rate table -CHOMP_ROOT=$(pwd)/../.. ./target/release/trace --games 6000 --narrate 0 # trace analysis + greedy counterfactual -``` - -The full engine, all mons/effects, and the three CPU strategies (hard, -greedy, override) are transpiled/ported. The arena is now a **standalone, -pure-Rust binary** (`chomp-strategies`' `arena`/`trace` bins): it loads the -roster from `drool/*.csv` + `src/mons/*.json` at runtime and runs whole games -natively (~2,000 games/s at 8 threads, deterministic across thread counts). -The stacks are DECOUPLED — the Rust side was ported move-for-move against TS -and then cut loose; it may diverge freely (including its rng stream and CPU -decisions) as the prototyping substrate, and port-backs to the TS game carry -no bit-identicality requirement. Emission is allowlisted in -`transpiler-config-rust.json`. `rs-output/` is regenerated (gitignored); -hand-written crates live in `transpiler/{runtime-rs,strategies-rs}`. The -former bun↔Rust FFI seam (`chomp_run_games`, the `ffi` crate, and -`scripts/batch_benchmark.ts`) was removed once the pure-Rust arena replaced -it; the verification-era machinery (golden-vector suites, replay fixtures, -the drive-mode adapter, lockstep gates) lives in git history if parity ever -needs re-proving. The TS arena driver (`sims/src/arena/game.ts`) remains as -the port-back reference. - -**Known limitation — TODO when a second codebase needs it.** The transpiler hardcodes three TS namespace names (`Enums`, `Structs`, `Constants`) for type-only Solidity files. File-type detection is content-based (a file with only enums maps to the `Enums` namespace regardless of filename), but the *namespace name itself* is fixed, and only structs are tracked per source path. A codebase that splits types across multiple files (e.g. `PoolStructs.sol` + `OrderStructs.sol`, or `Errors.sol`) would produce colliding imports. To generalize: add `enum_paths` / `constant_paths` to `transpiler/type_system/registry.py` mirroring `struct_paths`, derive the namespace name from each source file's basename, and have `imports.py:_generate_module_imports` emit one `import * as ` per actually-referenced source module instead of three blanket imports. - -### Deployment Order - -1. `EngineAndPeriphery.s.sol` - Engine, type calculator, `SignedCommitManager`, `GachaTeamRegistry`, `SignedMatchmaker`, CPU host, `SimplePM`, shared effects -2. `SetupMons.s.sol` - All mon contracts (moves, abilities) -3. `SetupCPU.s.sol` - CPU players - -### CI/CD - -GitHub Actions runs on pull requests (`.github/workflows/main.yml`): -- `forge build` -- `forge test -vvv` +Processing scripts, the Solidity-to-TS/Rust transpiler, deployment order, and CI/CD are documented in the `deploy-chomp` skill (`.claude/skills/deploy-chomp/SKILL.md`) — invoke it for codegen, validation, transpiling, or deployment tasks. ## Key Data Files diff --git a/drool/imgs/attacks/attack_spritesheet.json b/drool/imgs/attacks/attack_spritesheet.json index e24c27ec..edbc4c44 100644 --- a/drool/imgs/attacks/attack_spritesheet.json +++ b/drool/imgs/attacks/attack_spritesheet.json @@ -3,120 +3,204 @@ "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[0, 0], [96, 0], [192, 0], [288, 0], [384, 0], [480, 0], [576, 0], [672, 0], [768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [1440, 0], [1536, 0], [1632, 0], [1728, 0], [1824, 0], [1920, 0], [0, 96], [96, 96], [192, 96]] + "frames": [[0, 0], [96, 0], [192, 0], [288, 0], [384, 0], [480, 0], [576, 0], [672, 0], [768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [1440, 0], [1536, 0], [1632, 0], [1728, 0], [1824, 0], [1920, 0], [2016, 0], [2112, 0], [2208, 0]] }, "bubble_bop": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[288, 96], [384, 96], [480, 96], [576, 96], [672, 96], [768, 96], [864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [1440, 96], [1536, 96], [1632, 96], [1728, 96], [1824, 96], [1920, 96], [0, 192], [96, 192], [192, 192], [288, 192], [384, 192], [480, 192]] + "frames": [[2304, 0], [2400, 0], [0, 96], [96, 96], [192, 96], [288, 96], [384, 96], [480, 96], [576, 96], [672, 96], [768, 96], [864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [1440, 96], [1536, 96], [1632, 96], [1728, 96], [1824, 96], [1920, 96], [2016, 96]] + }, + "chill_out": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[2112, 96], [2208, 96], [2304, 96], [2400, 96], [0, 192], [96, 192], [192, 192], [288, 192], [384, 192], [480, 192], [576, 192], [672, 192], [768, 192], [864, 192], [960, 192], [1056, 192], [1152, 192], [1248, 192]] }, "contagious_slumber": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[576, 192], [672, 192], [768, 192], [864, 192], [960, 192], [1056, 192], [1152, 192], [1248, 192], [1344, 192], [1440, 192], [1536, 192], [1632, 192], [1728, 192], [1824, 192], [1920, 192], [0, 288], [96, 288], [192, 288], [288, 288], [384, 288], [480, 288], [576, 288], [672, 288], [768, 288]] + "frames": [[1344, 192], [1440, 192], [1536, 192], [1632, 192], [1728, 192], [1824, 192], [1920, 192], [2016, 192], [2112, 192], [2208, 192], [2304, 192], [2400, 192], [0, 288], [96, 288], [192, 288], [288, 288], [384, 288], [480, 288], [576, 288], [672, 288], [768, 288], [864, 288], [960, 288], [1056, 288]] }, "deep_freeze": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[864, 288], [960, 288], [1056, 288], [1152, 288], [1248, 288], [1344, 288], [1440, 288], [1536, 288], [1632, 288], [1728, 288], [1824, 288], [1920, 288], [0, 384], [96, 384], [192, 384], [288, 384], [384, 384], [480, 384], [576, 384], [672, 384], [768, 384], [864, 384], [960, 384], [1056, 384]] + "frames": [[1152, 288], [1248, 288], [1344, 288], [1440, 288], [1536, 288], [1632, 288], [1728, 288], [1824, 288], [1920, 288], [2016, 288], [2112, 288], [2208, 288], [2304, 288], [2400, 288], [0, 384], [96, 384], [192, 384], [288, 384], [384, 384], [480, 384], [576, 384], [672, 384], [768, 384], [864, 384]] + }, + "dual_shock": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[960, 384], [1056, 384], [1152, 384], [1248, 384], [1344, 384], [1440, 384], [1536, 384], [1632, 384], [1728, 384], [1824, 384], [1920, 384], [2016, 384]] + }, + "electrocute": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[2112, 384], [2208, 384], [2304, 384], [2400, 384], [0, 480], [96, 480], [192, 480], [288, 480], [384, 480], [480, 480], [576, 480], [672, 480], [768, 480], [864, 480], [960, 480], [1056, 480]] + }, + "federal_investigation": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[1152, 480], [1248, 480], [1344, 480], [1440, 480], [1536, 480], [1632, 480], [1728, 480], [1824, 480], [1920, 480], [2016, 480], [2112, 480], [2208, 480], [2304, 480], [2400, 480], [0, 576], [96, 576], [192, 576], [288, 576]] }, "infernal_flame": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1152, 384], [1248, 384], [1344, 384], [1440, 384], [1536, 384], [1632, 384], [1728, 384], [1824, 384], [1920, 384], [0, 480], [96, 480], [192, 480], [288, 480], [384, 480], [480, 480], [576, 480], [672, 480], [768, 480], [864, 480], [960, 480], [1056, 480], [1152, 480], [1248, 480], [1344, 480], [1440, 480]] + "frames": [[384, 576], [480, 576], [576, 576], [672, 576], [768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576], [1344, 576], [1440, 576], [1536, 576], [1632, 576], [1728, 576], [1824, 576], [1920, 576], [2016, 576], [2112, 576], [2208, 576], [2304, 576], [2400, 576], [0, 672], [96, 672], [192, 672]] }, "infinite_love": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1536, 480], [1632, 480], [1728, 480], [1824, 480], [1920, 480], [0, 576], [96, 576], [192, 576], [288, 576], [384, 576], [480, 576], [576, 576], [672, 576], [768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576], [1344, 576], [1440, 576], [1536, 576], [1632, 576], [1728, 576], [1824, 576]] + "frames": [[288, 672], [384, 672], [480, 672], [576, 672], [672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672], [1440, 672], [1536, 672], [1632, 672], [1728, 672], [1824, 672], [1920, 672], [2016, 672], [2112, 672], [2208, 672], [2304, 672], [2400, 672], [0, 768], [96, 768]] }, "loop": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1920, 576], [0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672], [576, 672], [672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672], [1440, 672], [1536, 672]] + "frames": [[192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768], [768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [1440, 768], [1536, 768], [1632, 768], [1728, 768], [1824, 768]] }, "mega_star_blast": { "msPerFrame": 50, "width": 96, "height": 96, - "frames": [[1632, 672], [1728, 672], [1824, 672], [1920, 672], [0, 768], [96, 768], [192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768], [768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [1440, 768], [1536, 768], [1632, 768], [1728, 768], [1824, 768], [1920, 768], [0, 864], [96, 864], [192, 864], [288, 864], [384, 864], [480, 864], [576, 864], [672, 864], [768, 864], [864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864]] + "frames": [[1920, 768], [2016, 768], [2112, 768], [2208, 768], [2304, 768], [2400, 768], [0, 864], [96, 864], [192, 864], [288, 864], [384, 864], [480, 864], [576, 864], [672, 864], [768, 864], [864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864], [1440, 864], [1536, 864], [1632, 864], [1728, 864], [1824, 864], [1920, 864], [2016, 864], [2112, 864], [2208, 864], [2304, 864], [2400, 864], [0, 960], [96, 960], [192, 960], [288, 960], [384, 960], [480, 960], [576, 960], [672, 960]] + }, + "modal_bolt_fire": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[768, 960], [864, 960], [960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960], [1440, 960], [1536, 960], [1632, 960], [1728, 960], [1824, 960], [1920, 960], [2016, 960], [2112, 960], [2208, 960]] + }, + "modal_bolt_ice": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[2304, 960], [2400, 960], [0, 1056], [96, 1056], [192, 1056], [288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056], [768, 1056], [864, 1056], [960, 1056], [1056, 1056], [1152, 1056], [1248, 1056]] + }, + "modal_bolt_lightning": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[1344, 1056], [1440, 1056], [1536, 1056], [1632, 1056], [1728, 1056], [1824, 1056], [1920, 1056], [2016, 1056], [2112, 1056], [2208, 1056], [2304, 1056], [2400, 1056], [0, 1152], [96, 1152], [192, 1152], [288, 1152]] + }, + "negative_thoughts": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[384, 1152], [480, 1152], [576, 1152], [672, 1152], [768, 1152], [864, 1152], [960, 1152], [1056, 1152], [1152, 1152], [1248, 1152], [1344, 1152], [1440, 1152], [1536, 1152], [1632, 1152], [1728, 1152], [1824, 1152], [1920, 1152], [2016, 1152]] }, "pound_ground": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1440, 864], [1536, 864], [1632, 864], [1728, 864], [1824, 864], [1920, 864], [0, 960], [96, 960], [192, 960], [288, 960], [384, 960], [480, 960], [576, 960], [672, 960], [768, 960], [864, 960], [960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960]] + "frames": [[2112, 1152], [2208, 1152], [2304, 1152], [2400, 1152], [0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248], [480, 1248], [576, 1248], [672, 1248], [768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248], [1248, 1248], [1344, 1248], [1440, 1248], [1536, 1248]] }, "q5": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1440, 960], [1536, 960], [1632, 960], [1728, 960], [1824, 960], [1920, 960], [0, 1056], [96, 1056], [192, 1056], [288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056], [768, 1056], [864, 1056], [960, 1056], [1056, 1056], [1152, 1056], [1248, 1056], [1344, 1056], [1440, 1056], [1536, 1056], [1632, 1056]] + "frames": [[1632, 1248], [1728, 1248], [1824, 1248], [1920, 1248], [2016, 1248], [2112, 1248], [2208, 1248], [2304, 1248], [2400, 1248], [0, 1344], [96, 1344], [192, 1344], [288, 1344], [384, 1344], [480, 1344], [576, 1344], [672, 1344], [768, 1344], [864, 1344], [960, 1344], [1056, 1344], [1152, 1344], [1248, 1344], [1344, 1344]] + }, + "quickstorm": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[1440, 1344], [1536, 1344], [1632, 1344], [1728, 1344], [1824, 1344], [1920, 1344], [2016, 1344], [2112, 1344], [2208, 1344], [2304, 1344]] + }, + "round_trip": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[2400, 1344], [0, 1440], [96, 1440], [192, 1440], [288, 1440], [384, 1440], [480, 1440], [576, 1440], [672, 1440], [768, 1440], [864, 1440], [960, 1440], [1056, 1440], [1152, 1440]] + }, + "sanctify": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[1248, 1440], [1344, 1440], [1440, 1440], [1536, 1440], [1632, 1440], [1728, 1440], [1824, 1440], [1920, 1440], [2016, 1440], [2112, 1440], [2208, 1440], [2304, 1440], [2400, 1440], [0, 1536], [96, 1536], [192, 1536], [288, 1536], [384, 1536]] }, "scary_numbers": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1728, 1056], [1824, 1056], [1920, 1056], [0, 1152], [96, 1152], [192, 1152], [288, 1152], [384, 1152], [480, 1152], [576, 1152], [672, 1152], [768, 1152], [864, 1152], [960, 1152], [1056, 1152], [1152, 1152], [1248, 1152], [1344, 1152], [1440, 1152], [1536, 1152], [1632, 1152], [1728, 1152], [1824, 1152], [1920, 1152]] + "frames": [[480, 1536], [576, 1536], [672, 1536], [768, 1536], [864, 1536], [960, 1536], [1056, 1536], [1152, 1536], [1248, 1536], [1344, 1536], [1440, 1536], [1536, 1536], [1632, 1536], [1728, 1536], [1824, 1536], [1920, 1536], [2016, 1536], [2112, 1536], [2208, 1536], [2304, 1536], [2400, 1536], [0, 1632], [96, 1632], [192, 1632]] + }, + "set_ablaze": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[288, 1632], [384, 1632], [480, 1632], [576, 1632], [672, 1632], [768, 1632], [864, 1632], [960, 1632], [1056, 1632], [1152, 1632], [1248, 1632], [1344, 1632], [1440, 1632], [1536, 1632], [1632, 1632], [1728, 1632]] }, "sneak_attack": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248], [480, 1248], [576, 1248], [672, 1248], [768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248], [1248, 1248], [1344, 1248], [1440, 1248], [1536, 1248], [1632, 1248]] + "frames": [[1824, 1632], [1920, 1632], [2016, 1632], [2112, 1632], [2208, 1632], [2304, 1632], [2400, 1632], [0, 1728], [96, 1728], [192, 1728], [288, 1728], [384, 1728], [480, 1728], [576, 1728], [672, 1728], [768, 1728], [864, 1728], [960, 1728]] }, "stat_boost_enemy": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1728, 1248], [1824, 1248], [1920, 1248], [0, 1344], [96, 1344], [192, 1344], [288, 1344], [384, 1344], [480, 1344], [576, 1344], [672, 1344], [768, 1344], [864, 1344], [960, 1344]] + "frames": [[1056, 1728], [1152, 1728], [1248, 1728], [1344, 1728], [1440, 1728], [1536, 1728], [1632, 1728], [1728, 1728], [1824, 1728], [1920, 1728], [2016, 1728], [2112, 1728], [2208, 1728], [2304, 1728]] }, "stat_debuff_enemy": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1056, 1344], [1152, 1344], [1248, 1344], [1344, 1344], [1440, 1344], [1536, 1344], [1632, 1344], [1728, 1344], [1824, 1344], [1920, 1344], [0, 1440], [96, 1440], [192, 1440], [288, 1440]] + "frames": [[2400, 1728], [0, 1824], [96, 1824], [192, 1824], [288, 1824], [384, 1824], [480, 1824], [576, 1824], [672, 1824], [768, 1824], [864, 1824], [960, 1824], [1056, 1824], [1152, 1824]] + }, + "throw_pebble": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[1248, 1824], [1344, 1824], [1440, 1824], [1536, 1824], [1632, 1824], [1728, 1824], [1824, 1824], [1920, 1824], [2016, 1824], [2112, 1824], [2208, 1824], [2304, 1824], [2400, 1824], [0, 1920]] }, "triple_think": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[384, 1440], [480, 1440], [576, 1440], [672, 1440], [768, 1440], [864, 1440], [960, 1440], [1056, 1440], [1152, 1440], [1248, 1440]] + "frames": [[96, 1920], [192, 1920], [288, 1920], [384, 1920], [480, 1920], [576, 1920], [672, 1920], [768, 1920], [864, 1920], [960, 1920]] }, "unexpected_carrot": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1344, 1440], [1440, 1440], [1536, 1440], [1632, 1440], [1728, 1440], [1824, 1440], [1920, 1440], [0, 1536], [96, 1536], [192, 1536], [288, 1536], [384, 1536], [480, 1536], [576, 1536], [672, 1536], [768, 1536]] + "frames": [[1056, 1920], [1152, 1920], [1248, 1920], [1344, 1920], [1440, 1920], [1536, 1920], [1632, 1920], [1728, 1920], [1824, 1920], [1920, 1920], [2016, 1920], [2112, 1920], [2208, 1920], [2304, 1920], [2400, 1920], [0, 2016]] }, "vital_siphon": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[864, 1536], [960, 1536], [1056, 1536], [1152, 1536], [1248, 1536], [1344, 1536], [1440, 1536], [1536, 1536], [1632, 1536], [1728, 1536], [1824, 1536], [1920, 1536], [0, 1632], [96, 1632], [192, 1632], [288, 1632], [384, 1632], [480, 1632], [576, 1632], [672, 1632], [768, 1632], [864, 1632], [960, 1632], [1056, 1632]] + "frames": [[96, 2016], [192, 2016], [288, 2016], [384, 2016], [480, 2016], [576, 2016], [672, 2016], [768, 2016], [864, 2016], [960, 2016], [1056, 2016], [1152, 2016], [1248, 2016], [1344, 2016], [1440, 2016], [1536, 2016], [1632, 2016], [1728, 2016], [1824, 2016], [1920, 2016], [2016, 2016], [2112, 2016], [2208, 2016], [2304, 2016]] + }, + "wither_away": { + "msPerFrame": 100, + "width": 96, + "height": 96, + "frames": [[2400, 2016], [0, 2112], [96, 2112], [192, 2112], [288, 2112], [384, 2112], [480, 2112], [576, 2112], [672, 2112], [768, 2112], [864, 2112], [960, 2112], [1056, 2112], [1152, 2112], [1248, 2112], [1344, 2112]] }, "gachachacha_bunnies": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1152, 1632], [1248, 1632], [1344, 1632], [1344, 1632], [1440, 1632], [1536, 1632], [1152, 1632], [1152, 1632], [1152, 1632], [1632, 1632], [1728, 1632], [1824, 1632], [1920, 1632], [1632, 1632], [0, 1728], [288, 1728], [576, 1728], [864, 1728], [1152, 1728], [1440, 1728], [1728, 1728], [0, 1824], [288, 1824], [576, 1824], [864, 1824], [864, 1824], [864, 1824]] + "frames": [[1440, 2112], [1536, 2112], [1632, 2112], [1632, 2112], [1728, 2112], [1824, 2112], [1440, 2112], [1440, 2112], [1440, 2112], [1920, 2112], [2016, 2112], [2112, 2112], [2208, 2112], [1920, 2112], [2304, 2112], [96, 2208], [384, 2208], [672, 2208], [960, 2208], [1248, 2208], [1536, 2208], [1824, 2208], [2112, 2208], [2400, 2208], [192, 2304], [192, 2304], [192, 2304]] }, "gachachacha_carrots": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1152, 1632], [1248, 1632], [1344, 1632], [1344, 1632], [1440, 1632], [1536, 1632], [1152, 1632], [1152, 1632], [1152, 1632], [1632, 1632], [1728, 1632], [1824, 1632], [1920, 1632], [1632, 1632], [96, 1728], [384, 1728], [672, 1728], [960, 1728], [1248, 1728], [1536, 1728], [1824, 1728], [96, 1824], [384, 1824], [672, 1824], [864, 1824], [864, 1824], [864, 1824]] + "frames": [[1440, 2112], [1536, 2112], [1632, 2112], [1632, 2112], [1728, 2112], [1824, 2112], [1440, 2112], [1440, 2112], [1440, 2112], [1920, 2112], [2016, 2112], [2112, 2112], [2208, 2112], [1920, 2112], [2400, 2112], [192, 2208], [480, 2208], [768, 2208], [1056, 2208], [1344, 2208], [1632, 2208], [1920, 2208], [2208, 2208], [0, 2304], [192, 2304], [192, 2304], [192, 2304]] }, "gachachacha_skulls": { "msPerFrame": 100, "width": 96, "height": 96, - "frames": [[1152, 1632], [1248, 1632], [1344, 1632], [1344, 1632], [1440, 1632], [1536, 1632], [1152, 1632], [1152, 1632], [1152, 1632], [1632, 1632], [1728, 1632], [1824, 1632], [1920, 1632], [1632, 1632], [192, 1728], [480, 1728], [768, 1728], [1056, 1728], [1344, 1728], [1632, 1728], [1920, 1728], [192, 1824], [480, 1824], [768, 1824], [864, 1824], [864, 1824], [864, 1824]] + "frames": [[1440, 2112], [1536, 2112], [1632, 2112], [1632, 2112], [1728, 2112], [1824, 2112], [1440, 2112], [1440, 2112], [1440, 2112], [1920, 2112], [2016, 2112], [2112, 2112], [2208, 2112], [1920, 2112], [0, 2208], [288, 2208], [576, 2208], [864, 2208], [1152, 2208], [1440, 2208], [1728, 2208], [2016, 2208], [2304, 2208], [96, 2304], [192, 2304], [192, 2304], [192, 2304]] } } \ No newline at end of file diff --git a/drool/imgs/attacks/attack_spritesheet.png b/drool/imgs/attacks/attack_spritesheet.png index 0963d04460bc42188687199edc37e1ea122699ef..48c8eec990d99ca4cd5e212bfb454143c187c44f 100644 GIT binary patch literal 199115 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�<5Z&teU^S7=9#~VvNlS1A1T!2@pv-!^k-A?Ba zloqQ(h(*8D<$@mGM@ETN*c1X1P1QS5$@J_khX_CMUsnkNSl0WS`u2onoFwkxlvs{2 z@HBhem;RH3m(GkWjcraJ1bTx%ofV|}WE$Do^D*C3C;@VXx7lrjL5zucvme#{6N>(( zD+N0?>5cZ|0GfG?CJ%(Eh%#hB4`(rNAMBmnfqqJ+bD`i^q}UQ8rCHb~{itZT`&W?P zfsI%*8&2|R!sKKMk_MDgAiE^lZN&S|wy%E$2rb0DFh(g)M4h~G`SPR@>dB`&t^B6r z2b(Gp$uFqab8l&K8LT3evyF|2JGr?hgi@{qyj&AFMl61(SrK| zu-c&3gZDM~ogn5FgWz!^ytC3xmqq*{@nwH<{rY-W@kqHok;TFoj=5N*Ph{=e5|2l6OQ1Yz+Q!-D$$vVYJ>DM3Bq#F@#+lb#9c$|Uv(o?mbDbgdi|L~WqUue71$b^da(0ZPBN5$5 zF?RmGg8ufcMV+BCqHwSKZISAw;GiT}@wF#A2|abc9|93Pw>|S1Ixn)H@fdkSQOd;m zFWM&p@v#R7m6WG`{mj@1o*5be!{|=N8pQhe)l+*r)ba+wjT8m zsH>%aTi#c*~_`#tFMsS#9h!>&cnYEa7+uxOm2 z*u9+uD$mB4wgAh`^2f(q0pFkip(u7VgVE6Ok|C?2Gz2hg{@DUW}&LI!I!LVg=?xaVJcqnvE`W1rk`XKkTg;ua2l zX{l_Ic@N^Wd8$)bWYv1QXJNL~r*h<%I44~s4^?PxaH*CTXE%EFW0zorm;ltB9&Rm1 z@Hz$5PdQc81fL4Lk<$RchMPETFmf?0^*+^b?(k#aXkfT=>!E@FiKWeS$;Avmu00q4 z{P-)#dw+^?>Oo=yI5#ig-K-7BJ3L}d-g~kmC$u^6I9KJ=kBJ)7*TZ=amn6NLew%4j zigfl10(1qTKhf(!qjkIztBQcE*$kkjI!iK8XBa*r5?Zrft5Vw`Fmq$ZJi7uDeNV7n z)oVU|Vu67xCX@S4^gN54^H_pe=Jt@}{cBubL3F5ivp;}Z|NFHdU3uC3sdwOSPO*8| zY`wRpNG^yTUbs{F;Pcp9$?cpy8`#v4Iv}Ld)S$bI>C6-NAYJq6o!!S4JNt>-oob_w zm(PATueP7wVDm(BSAl`CL>+%&3C$u(BoZQsaR0w4YP;lZ*Li%I+$I=VCeC&xyUa0 z?l~4i0JOwXSS=!)g>8T;9@`JDiRGnKXK+-hN^EbDvrVG_3l?qLkCs-QWl*q6)$aWe)`CK ze^BHLQUA%)!rNDkpGlDEm(*Bpzy3W>a+rJMXMFxZ6yNZOgDPUuKk41!eK5$M<`RG< z=8#86TwX=Llto63duHPA&JkT$t$+26$*G|4Ud)P=L0(Y)klK`8+YsKLf(%$Hv1vIy zujeNTO*usCsng?l!++&^K!-9ghsK>iSkwDWz|>WuH8kF3 zYG#W3yJUB_Ue|Ss&DV5ix7dB(#ho9!zo6}FzOsH{+;bTEnn$r46TjB#AN8HlbU}v^ zu_w7t4;* z+Let<>lccwp?!4XbYdQ} z1EZ%El#u6kX1VB`F4!~k=t*`Cf7&qF@jxC|#{>s)(B($wDAidRP``YPMC?9X!>h@N zOEpH#<(O@xuz-0c_4({(4>S;h^M^bw$V5V*LDd zU=VoVoRJ&10QSi|HyY2MZe^2;FrYtR8q^BQC9^t@b8EpMQWC4nKqaX_zYDO{>|r7F9%+tp#Y z*JmjsEz|UYLV@v971ZKd2iXyLt>+tgJt}Y)K7ZXPQyTgGvB+wix4gB>M&E6g+k=e6 zK0~-hFX)?m)PBfwyp*J?z+hBaztMd5TIJ_%=yS-dHc2xdmlEO9jUiQagWi&vE`sgd z=7ox;KBiL;9Vet(J8om7>!d4Al#!14_3u&aOrZ2;IxevQ;d%8tIa@qcBHSXf5{})V z(<76)GNnMeV;&?GYY>Wd(j_hnm4W4%YFyCi+0LmqPtpjH@^4Mr6EZ{q{igOi(O4YM zVzcyp44aR$Qhoe^=WLQ=K3%tR6hD*SKSva=^Zk@-)r)&{tWqP7+_^;{ zHAkIot5W&cxwbud`P7Ub(5AA15Xt)eN@)-1s(zDnSwm>?pf7e(9(hZyPP$2pPJ9yf zu{5U-bl^mu^GspTb!8LGOvm2OmwPFVsA~T3PN(I50Xz5;rm4V{q#ex=yTioy>&ak8 zl{0+R%n<2~(M&!Gc?JQB}#xXY2^0;`0KidH^5!L=Lm z-Z)TeH|(kMaKX?)>)ZW=lc;O3vqQhTwyl`Talbb)6ZQ{x!{&?v-+M8VI6pSO4*PcS z^-0}^Xsu{{4UsB40_FP@+9`{=g{aq$V(`n_r+@3H!~8a1EIUj2!ndmhco)cmvM@|x zxjBbk!w@!xiH}RE+gZ8^drv3s=4nU$uzJ!IqQfn2_uB=J*JJ_?rkt>aZQOisULN)O zFnU7&r}$On))3Zffin6-Uhlig`dSl)8Fb(Gy=n|pXt=zc)7VJT{*`H5{^MBXxp`;W zrpkW-oESv#ik!`60VhMVIv8|u_7_gk%jvkz2#8+xe?`zstn!mRCc8Xvji@WvU=%6o5e&vbr zELs2ei!V_gp`PLEDkwTem3Z0SaGjzk{ml2ow+pN}0cTP?S78|kRBu)8h3D&22Dz@i zThY6*oXIN*>DadqGN$gNMc~O{F%w{r@HrflbA6Amgu)ivSnw^@SFgU_gbB|+q9_7w z`nM;y4j#>2TiFP#eeKe1e_;?jv|iI~SAx5{())3~oEqlWET9=$-kf~vQxNI$R1Q5B z(XWm_<@{9+DUDK9X=_XQ(l-c-3S|n6IS&o6vN?6%bpEdE$`&T=V_-pmAJ-#Xy$zfm zTF@bf@C36CHnv7?wrjtuXPZwp$#oaKRd;1uY)))@G8LdGw8B87)u+j6-m$uYNs+Vr zAS32Hr9k3TASL00M%$s1%=QXe|HF;QwdwCZlcxk2ziCvPJr}HwE9t%x@{4ZS^1Ip( zjcU^_rR&Vy^eoXk?jC(_*8ibM&Vk z-|!F~Id9uLaQ`Y=T7s05!D)%(Z|FcrR3gMXCQdIFmiBEsf`k#?T=#m3*Nm&R()!)e z@`!?aE6&q%Rqwk_-z4ktb>&Slsyz!T|J)l|ZwqI?2KSQhy?^alDL)Kzvh6rJ>_dWg z3*A&#RqB4W1xa=0iUWM|2CLfj%}+vlwUZ8|WTBKxz*I8yI!vj}r3%fW3vS zclxT*(9?yb1K8TcACqBeyO-VVT6T>#!_Ik&*60kM&FP9@%qryrfK5$stwqsh=yp9rbz#$saos34?Lys@&{OLRZu*TEdq15Vw90|ruzGcV~o*hvs zy=OcxVGRy@sj6P;yn=A93`2SoPQ7}Uota;=aP}GK-zNd!`@DlE2W~vk52_GsG1E{( z+YRnrpL!vQgeFXRMf@#K0jTVfi4;ROt*dbU zYw&-o=uED$1TUr)coA9t7vA=g1K;)NoDl7$03$|Snl&ka{kPAE@7Gh(hRJx+;UU^v z`~Ld1{~>kf%k>n#+-)%lyTtzm$8Ay@8oH;P0-LHs2;g-)CxpFz8SUEtZy&C@%z6OT z+@O#a!2R{wp{|~J0>t<8i>a7PuL8IdvAXhu6_U3O)ixp|W6SUT9 z`#-%9U>tj(zy=Nw!j|c%vVnvD=h9WUW~{pawxZOy*B3Al?6GxAtM&)Q>V%_9WS$G1i$mq1?h>ZP}RhB&00yQpp2EKj8h8r)Ij=PLEH*^#xG1#-lxnShkyg zX_%-&`P+ahe4MP<&WBGx73Zd5X_L=@P7$hKI=RYZQ*iMAQUv^FY|FUuzL*o6OE-D~ zo`5d!p>}{P(ig>3U1V1v*|j3tx5NGaNw+f@E!^81gM{51@BspdS=eQW2^*C*VebR zJ_Q1&RClq=D>TzD+ z-No$pXHziR4_WNQS6e`apyTO;rA(f-|1|zapvBMvtVPU^p9Ab^1tyRV3O4Hp=KaKg z;sF6}X*c!^FpS^?CU7~+Agdq!g5O*+rLqImK?z#PhgnL)H7)+AKd+9PHd6F(J@apw zOrVEr1Rjt+FnPG*HGhn5%;sw4M1R*qbK@J1T{`p#2eJZO|G^IH-I%@dybPMR z4~mw&cqEEWUs-?4PuO|P=+r+um?jQaOcX|3gb4rmrfLSj9UD@#^HAkS#beFlT#uuC zWZ|obrR9UF8TXSQpS6KRp$~UGb$B9-9v42-RZoJ)lgnzFN@iF+-Z5~w8 zbm2&A49m3XzTaCN;M8fagDx2L27{t0`HNNx4yqp=z<4M2*YCY{qvLEdKkS1jz6lWC zGp$pauuZ_deoCFvY zjFM?j;agRLixs*9>|Uj?V#A@+vQ%gxa-w$20&*h170KLi-Soh(OM>y8mA-e>Lv_R*Kz2D3AQ(zj-ZG=NipW$1#Rqfr6%rGKf`r zU0x^43_wLKK?d={V>QQ^ixa!#TzSYl!RhaCzDryyPU0&ByCePR+xhv+;3mOlxy zhZ%%3u5_7$%Z(2x8)4m$C&@HX|06|bNnOZ|p1sYxrW)8!!DB+aHtX}|dhQ((??wxl zs^)Y_8O03-iG4&(=vr-Xno2g9Z7K#hML_UMxqN1pDI1*Oz|K{L(G^ib*Vmq_%6wYi z`YhtRUE7P>VHpf~#eXGqiZbEP_WCa?mbUvWAN*P6U;U#`Bj?~2{h2PaBA_O=Ac-oS zl&>jfOJhCEpDp(j+d*eo zs*Q5>vlL#`<>1CKZE3=CgQr^u$*m)MZnxO9!v+(Go=;D@fj#Tg(x8ZH#N?T3E132d z3Ztp~Zf`pSkQ8#rFeb`S_gGskJ>rH8LeQo+p&qZRX8{OlVy##!=g0nKlJ{YP{OKphF)qD6R-NUsL>73!!=^QYZ9Bv)QoF z1dPPI*LjbgVP2<`bonBF!v>Jdr?nh|ZZmX2Uxb z&&3R0#~}e5x4UmW!w}58Yk#eNzJXnGt)IYO0WkmF8cO$m!l)sbP8dY&&S-7P6*@$4 zx*VK7TsiMT5bUB&RDz|V9p_9TInu4ZfxZJbRxoo?`U&7YO&c4d_}X?kv1VXJeK6z7 zEDov6l;brMH&WeYkvPV(LP-8ec3CUn?>G5Z!DFX>t6H$=*a3tYz&70UOZ?Keo`9~* zeGDUX-TM6VA#;4iyOs+=TIp~3x6s_jli+UJTSmN7wY+Wy&#;Y*;I0BHDZ=a2u+wVz zr@9Z8;H}oHdkwZGc6)lG6H7m|{GN%o$X7a}1^?A(*it&(b5k@0N_(>Kw5#H&v)K3> zr5hob*k~PB%Zj%x4#C&2YMdP(vWdfU66d#W^$*_oWCo^}Z9gN#w|V9a9pZ6`)HCeI z)k!W+e65!Sco%SXX%YACMJwhohy`nVNZi7cq`Hp@ehc2KwKOiUs4V?gCQi8ma5oWN zruBA;NLFm?=1x6A=$1J1YHy`aVEC}pwTzbA%5p(;Q!y?bd^rY(@qTTVRG2F{F#$lfv;S>|@2C$p>DOho_yk(||!RmNJ zHG!BGDeiWa;TY{a!hZ~nQ+|MuD?@IGNel516;tIw8V{QYrF2Y|c7kPe_i|ZmK5IT* z)m%RgQZnNY8+*>zn#b#UoEvTi4ls)oTSa!wDqqF#eLN%!qZU`2j5o>{pXLMu>{4c) z(azNSDV&@?W9u+~>r3fGKvh@cGt8jk$w0=BGU*v5Kf^~FV*>(uk_41MgcZ( ziXR$!1x1Sd{WC;1+z41r1Y3{^z>Zaf0sL6@btp&Aq)eH$YS-|#br32h}EoHI`pn z_6yIubZk0%ME4>!g?LNy9Q?*dmrBvLha5i)-g!@nt!hvWFKhwK$r85-r-rk%4^+ zHT$(z+Q=X*P+|9iR2K(gKnU*4+wWNdq);bzp+L1~&rjEH@)0+xn1IrLTu&TN6i7|c zxiI5)H~7ymAlQt=R2NSF;Id$cQcv)SgF%UB8BZlzS)Ec{He;KjRgFFYeAX69W6(L* z{_z5MzaZQwbh9KMgrP3f7xFnZAnj2;W%|I4C|OMkr?g5mpkyd2yePty=`51ykq%)6 zo0VCQpt5V@DGJAr#d1FP!%!Ri6TaD=4PJ-f9e9H_6j!WZlf-aonyCx7eDNgheG;8h zH_vsf0t?vi1Q@d_S2ji6;*(+ZiUy3npcC#QzXD`crle!pPES2r@mSXIz9<&CZDf3K zu#J{NrG7Jk-*^THsrbz^D(~O#EUXw^8Fu`+@n)^U!g;#fpeiAc^Q&%`_aCMhu4iGQ zzi-;LpQf7{rpvunxo{l-n31}|aoKg>g|Ff)1Hy?yXL~rIJ7~q{W)t<3`ZQGcf(hB4 zRc-gPJgQ@KeR7r0Nudw)b!AqDRE+z!m*2;~FfwBKHebMq2b(@|EhQ70`0f=_fkBrp zSBo?n8w$yjy4%0IdoZJfJ)o%mmI$1F^^1gP$zwopjhBaA+ST&lI0^L477@ythXj#Y zlW_8kv?CJ4-%0~Jb~8h-r&YeqEn63{L&$LYWMH?8cx$Qia;&KgqrXVlmV)rsNGh_W zQvco3%;&FnzGDoBZNww@b+@J}2{aVxLJ}?obK@OBY(t~q0}J>fFSeLf!RLYrt{`+2 z-w=uM7vuG6r5yr*-h@dr#=(QWdr3*1L$AUfV$>@kl4lEk5skN~bb~xtqvPJB8FEt* zs3MwnR&3l@`pwMKOsu|b;;b4KFL-Vs z2geSI1gfWqh=`bPKc!y{KdDJ>Z+X2+G!8tzUsRPE{j=L|{mK;w}=Id zD|n!Yn)WnIY|n|Mz+gZdWTibl-BdY)?y@`o0g?{5;bFA;ix@9^)mVSL`e_kHozeFo zJQcf?;8qSJ^BLyukzGg!Z?i1&=>TB9yc4&RJ{Q3~v@XF{vu?T_m)pCP#}nWk^&C0~P1(uZ)ZG6WYWL6D>do2!8qp3s z*o}H>(!1m6nIXf2!|HcZ^DxDo<{t;BnSxJToZX;Nwzz$x-;stQ@<_(=QFc~e*ZsRY z;C6ReQ7wk=$0>5i-E*PzfIEDKthepYkYFre{w|RAh$8zr`(#wfF%t8P@NavWYZ`Q=qDK$Ty{LC4B6li*OiSen9ODc(Hf(1}51y zQ?GP$_gagrHr4i&KVC$Cq>$kuFHJ9+!({=N`K9ExoT(1qRvRK|Y^Fwebi!RU<}69; z`N7G^wY1&mCc)P>cd0XcQ!`a1RW3g;7e7N@-k1%S%+Yv%U}VR42n3*RTzOJy#TQ@Q z%_9d?qlj1EI^Vn^2oJUbxmGv33E*P*+%iAyMB1TKHmH{D;O|}h%Cu_sZ?}NH4alZ@ zh!Voy=9@DkO64!E4FZ5RA4b{1=2cMXDQdAAyaz;tW-Et`hFpa;eq#C*+4s=V|y z3@h092SoO&IuCa4P<9za3b|0UB8;^#EOtu6Bo{SV!Cx#e0Z`?lsSE-O46k{V4qJ7t zT-Fb+E~}=Rcc=j5?Nj51ui5YPQ#P_b%}`x5Z-!eh7-jiFlCpCk$Fi0Qox7hECKu}w z$j|7IM4r}7hc#ZJf6Lv7`3$<(X!dZkKl-)g#U@|&M#kCu9P8xKs21Zw%({&9pfmUO zWVwql1A^3|6HHK$$z^EA^Al`4U-}x_oc<-%mbe$3Boe&KGDMzAG^SyEYFd~qZ54Z^ zS&=VEUOg7M$pd@znc9j>Oizv2@DZsfKUn15Yav?lrsO*axl^_NwN z4bsC}D^apv)OoRQH)ICIjP)fSM*Ptk0gSpBGa~8}L~Dr99UbD?%9yI&2I~4w{k{a~0+`khfA0H=Egkb=^9QjD zW792XKP&F;MY#FU)NKrasKNpc7?a)C)i4%{hoHlKo<|K@&W%w%;kxcL?@>~94JuQ# zqEzwYaI_^geUK&0tZD{l_JrnzD2yp95~#I;_tD^y-=TJ`Vr5G+2h+_n5m)vdhZ$R=KsH_}9FQv-W=ImJ`TmLa-b7ij+K${aZ z^J3#x^!ao%1=^0dH)?tMXTun7gnmC6!_Qg+7?u8#8t@`eD2qYY-4s$FOe$}v-(?2C zg{<5jD4-(aS4pcK8a5w8E(kjQtUk+#2xb#!^=o`0l4UH7x;%EE1+1v0!fr)7uFuv# zqVxRFS|-tStFfsT5Hh@3IgPZ7RxZC45*3eb3m!I{e`~W#B&@@XD3b zgud7@p{vHsMlz2up!8`NsjtX&WKp^AjS&ir$=RQYVWHkMhs9^|2%liJK$QYYH6_3_ zvh9A`N7sl|A?#Zgu)A5Ax?Fo0#5jD4=A5ToV4K>`_UE2l2KfLh^65s?%q5WumN(W3 zWX|=qTM_F2j(znxT#n(~4-pz@1fHMMnDOLxj^Bb;wF)^yv>*UJtGN8rN1jwJ zkBXTxi7tI^_S`ljd7TeCS0}#}WR(0w*T$3_aUt#o0qO|vqoh>AF2fTJRw$)JjS|}G z(-L{SDxFDtu4$O)+(GT-D!xB^1cUDUZ<+%JULd}l!+=ofvU(yRmA`mUbmV8e!^Wv` z*Xv7)*~@jltCjGKt|5zC*Rd0B&$M(MP5Wtb=!=?3In3NYMzM*6Z61Pl!V=)NP2Tbi z!)f#37EH$ehWm?~Bs!-~%<92aM`o|{N1EbKI0Ba#>dKEzJi9K8-QIO#K)CljOglnD zKWkB_%2>L%UXZeo&C2&JmG>#hh)$e!D=q&D%~lwy&b>oOpmTCc>66`dyy;~YeE5>I znrX2A0H}7W5}(ZBU5)q)Z5Bg{TL8Xzr=1@2fisH zRqw@W0S8$pSY^J!8LAEPp93sSbARAjAxJsYq5z^-qf$KP6+Z{D7j&G?>3ry<8p=?c z(YM{m!61azp$(*g)zfqb%fAD{elT_V^8P2kG>R8XYqt2>bCHYH^g!QY76A&#uKtL& zf^_jA(iNVAAl9>h2ZBPako7wiiWHwjz^F|Wggn86?bL+GsQ}l0yVSv0k1R48A;8qD zA0jvB9Dl3msY2_vO7hv4X$4+4uaGnO$}+n|9-lqO^e91PI+`YM6;HBb%A7#!_zVcO zPl0vyo?*&N(1n~>1_Z)D21WUueZR27x_^RRFKB&JYJ}&HW;|+PW8k{KERq)+cN?G2 zPT~Xr`USDadcZc@gn(+6&a$cv_B>|kT*)&a~xr6g(cmu2kx@#mOMHn86$x&+DC zp!lO>se0;hC!A*L|11K{XyiWR$)1H8O>Na5OWMymMHke2VeoR7Ewosp z_5)7%Vw@}VN_(%C-=_M0`KCUN0l#bQM_zzCN~i;Us*2k`S~KVt>eF76{^1*JtQ68& z7G=(hohi5=l;T`{3;;mfJDGIN)%{_&VT=Fsg?d3Xo~knQH`E&9VdW0&HF@OYD}Zpu zruy+o0UQhXaLGmhyCe9Y!NaZioQCq}bGn+Jr{@x~yB@t$qETeLK9m2FDQi>K+Iv-> zb?eJv3g+ONF~#9QH>i9P;(^{1g)kt}7%btF(kS!e8I$p@hqkSkJ>-szh$A;Kz`#@B z(GBmu-FsmV$tON>`^x}OR(BO+Hq$Q%-RoaAG*g+IY)<`L4qc^YZS2$^j-ER~QC0IA zEkYf4oj(Y?U!fGJ`6=tt4CRZ~1K5lliK z3l$q$Wj3^KI{WKP!iDkF0pMqmVEybNb_f1CATJjiUj7JjTWSr66wg znTZBtAG6756U&dP{lJ1*iLNJ1zFxZEwH&}fa~9z;H}y&ft>H--7FhrV$R`RP2z89Kt{cFF-x-*8xdnOvVkYlU=@R!W#YGcmm>jb1lIZE8k0Nsa;7@`l-Gq zytI{ZW$n#t$4BA$QGaL8DTd@6xSw#5UyJB-I;K!62LFRA7M#_NJ(jDnD4y@QY%2R-^Av8zJe7k9|x32a@$%&z>TqRQIAk(5@8)K2@?T%?ksR@S zpHhUC-StO&*7bt5j`f`YSMlG?V);+p>gb+h;{2!%$C74NEWy8JY;-tS{cpc$7f~K{ zHlR`#EQ7P)kJk3+GXVpM|KWk7%jR^5XGq&_dHDQerO`sKZ>m?5{)yU&V}E8<)`x5! zfGTPC@zW$@)35I*(-$4ZN)nFm?`UY3;PkBH`_(wV**8}?{;mLgCYOb_bAeB9iB&(`xO9(0&^Y5lKE%<{Ch$KE9^DvR z0Hd@{d(IQ{LcOSj6w0JA0YuGhq&oUh5tOFGb<82!AA;W_PH%M_zL{2yAKe{c&Hr;U zAQpxiJGyy{T0~H%tESqGA*b1STn8qgpqN!x_&pjmT3GPUzE8(;;7QzItHRB564uq~9dac(1aPl-Ss_lmF>=?j?;U5YrKsH)> zISx*lL{d`1IcSf=4v72iL?}TR#hFmKeaNVw7%m{ihUG!AVvol9+TIBGwwY>ZBlp`F zwVw}2B}#$}nT~n@qDmfUbRQ5S#|~YW8LDe3>`{Z$kx)tqoht^m*?S-?Mc}-#eTbiz zJ*7w0P=X}4bgjb}!A>)(;*zv5-w|;XE7;}^?X1MnHO$czSsF0;T0WqeO+9-HR!eSU)nkoa6gevvi-nCm z(?59B05VUNiDJI6ZzGuSC(!PRtZyMM-|02yQXuw3^%L(S-F zTl|Jk;j=9Vs(*0Rg8b`Wt)KHuw39C~tVV$nTDwg{k3)FH_7rF}le zIvs9w?T&*ouBLN)Rfa6JG~yyB(Ol{!YeujMrrLK;_Fh=Pc^r)(zQ)v3 zQ6sO6MY-AiF%2`*0j1CE4)BKj_RG5Dg_^1Vj`mrf&ol6z2YuJ}D^iK?H5GqX7t2Eq zaL{PvLW_~+ogUZ2ZOOY#;b$c39^r^&Qu!xk;q`lR@zhh$?I#}6@0|r|IDMQq9GAW}Q&N9eIUn{}9qz_R zlF$K?pEz+M`7gU8Bs!gli5`?Cif5Q+>M*W?to`ow_BKAn&8on!yR&{ca@KO=ztbwr z=A+{KiG_p%ebGcEX;Dwn{-76Xeppe-S(S^%-ZKpihd2@L`^8)6bC)9KD-JBflwrLG zd77SQbE{wb6QHHHf5bUK+3}P6-eNaDA*=q$?tniwuV0KCB|&;YpOiY!X!d^DQGt!T zIGSi963qXw&x<6+?fx6_?n}F!`!wXH#g@SkF-#NZlm9`}+IMlF(eCO&g&|c zl6J7=lV{BD>Nwbcp2L2xozvfTSp1KE11NM7K8D7|R9PmRzQhoefaCyE#vRi?i>Ms& zZsxj9L593Y_kM($NY5j7Q^=yuQVVo)UDG1h0gBTf)mWT0S7Khte)Na3?#drgw{s>AZJRX$X{Ia?N|1tj3;5u9EHZ*1ZiY%@W z&nau~O7EnG?69B7_G|q^I?`)gwNC%m(<6QM+lR4SSJ?}g{jlx+s?O|7mK7m+(fgg* zddVVt-t0#UQ1CJ()ms@>Jo2hJgDb@|;w(--(PR4%^<|h=>&S!%p0X|L1EpL7_uZYe zKVt5TtnoQ=Z$bP#v670v@<(Z+5*QmJlzwWUfaRRvUatAempH)3g%uw^CTUW-X@Xzb zD4DDE_*j;aMYS&IWzJN|q9LU75@g#Jv2UL6XMPCv-tdHgW^^MnO_?L2wCm@K5K1ko z7}vqRwnLin3P{AI-~yg5CCAj3c9icy5B2AF2houcoxLEF96E9ODcyPQujoA=PvW4^ z^de0L>Ls#j?zie{G7oH#J{|WFqZh~DMTW^&c;YW2o2OlIYEHExw1uggET~8V)tzIc ziPqE^j~Yj)G0a$KWG-&g(dlUVR^mPV(aVvl9$PcxKG#x&e$92e))WGD8y~z7rdL@? zcJ%gyuf8COYBHbyBO@ohzFC}j5E5}4+X*vD$0pySJeo;NuN|%NsdVj|pzNx$*ID#C+t4?2Ica`w=WYn7x<&)W#!nE8J6Q zHN-Gy(-~dx#m9V=HqmLTjEKK7L~FUKWu4L=epmJOjBDZNVXjQt&M;*crMXxiIM8)a zL1FJGq9Iu9L45exUWcb9hIvb9GHaR@^aoVvQDx`vs2u4UVzcBzfLh5Zw_UN=(qbv? zRwFzwewOP5JwDQTq;e3$0WP{%T8!spcgoS|d0LMc8D-M7e@xqjDu&dAD)GTq;{ajH zYAZ8dwN~5cnrvH!|n^1HStO7QPX}F^&f?YWc>kw$;_>TG3 z_F0a`ywto)mr4C&L-)nAbD0J$L;5Y34nVJv^S`Yvo%?K^jkqt``~1;wfB0(Fqh@j? z^ft85ILjL>G<-H`Ao&e?q~>TdjM`mSU(}IDgk6K*5TZXR4BDP6?8x>z(pBr4$VY^` z7U@T`?B$(^K6tt3W{};pKVaD(TY1l`wG!Pp9MM*1cL(mZ6Lv>_)g7L&?bs~oX@4GQ z1FXF$aO1?~(dLp+`QDO%` zrvR8NIu|~JOHw*WxZBIQorNSjPH`uq#pl7pm+p&Ki1sEs5idGWE+9*97Hv#E z@N@jWdie3GKlIXd5)C$0(Mt(=qK&f(b?@z%P*<-^Dm%EO?w)Kci4?Gv$Db%PB6 zU+wU7yr$g#hKc*PWuJFf&%OHLHlC6@ix|#`PryO9ODzu-aA<&CD$VM@6*3(SEtw*`hnRP=M|O|Xd`JAtW?t~tp z%md*Hw}|a3oy@*BQ$b-W*_^X9P_JblJpn z?t70*gKECEfA5U@F)?%(qJLyO*=@4=8J#xjzs#u6+F2l6kb%Ulg@cSoo1mrsMo`M> zkdXk#@A#u#*M}2)4cdqCgwr2sp5H!K=P9AbEj!=BE(3jMU$Uw})h@(X)X6OAAeg>? z|JHCQN@%5J8f$2yJGX*kAC@^U`F%nSIK%st+9C%t-}a?qZqUH8M*;P@l9Z)`Z6o}7 z*m-9DuI%gQ2O;Pu51Kw|kS5hG9Tpf_7!F*%_^dMN>-MYhp4~yY<^6Rx8tCD^>IsLS z$ipfvPg(k@<40R8XAPug3H-@@PgR-eKz#a`?waVvM+X9@&$4g8Jy@MDp}kbJpb(g?CSwDohs0PF=y20!F=>1+Wuh*zE&exXwl4m2I-SjmwX1$5IDE(!9ahk zsYr$AX2N(0olU2SfT=P)csb=keZxb5&ExspU+bgY;-lJF8w7?Rd-$#KJs;zk#m@{L zC#dAP?-V7UV+xU8r>xGqJ*nh5#ve8A9OrQe`*L19XO+le&qOzM2KkfaA(M=2EH&?4 zfxGn{EQi`tQF49{sor2UdZPPH08uzu7%~_#5!4F_G(DJ`{7tUKbnT<|KSh2!`Pzk{ zhKUv2#RTL!2TK^|;*GvBd_jp2qxGf^l2H%tTS(JgtGkqYGynha_1C-jj$F6$LDS(gG^Igiff5B1A+%X_1=Hdow^n>brXH z_rCY{Jiq6if0EsqJ-cVhnKNhRe2#SBwQovgBR>T^Q8pT;5+koO9Ud5201e(*W%VyD z;vEt*&J_P4h=SY0;V#!b$Bxwm6x6bB1Bt1HQ!6I9$zqMB^WfeyqgQeAw*zhs9~jo)sof zwUHOreTtc)PHGPdYjgJg3jIB4WZ+$blpH>2V+435g> zd};N%65Gh#XrEH7O+lqYC_8JX{LUBKSE)YVsmND?1_$N`cG$K_TP>9cXz1Am%5J5$ zx0JiHQ;R+5nCjts6S6|}#wgRDq#V4D8A^Gg*$Plki&Nb^h}$)Zh$d^x!C#(>B_XyS zqI(JA2w7y5YkE%=X)etySMjqgH4a&7*toYX!TR)}7!7Dk__-fcn|WBcTr68s0(Cu$ zhrIuHa17}^KOAcFv+uEOBSNg>yz2qHU$1I*6&tK%IodCaT(Il*Kj_7vy4-q6-d z`aEA$=?BTj3^P6GLT5(66uV0O=D6JlG$D1px@YPc><1;qec=`0^m|T`3%(LmBt~9t zOibRkej{_m)0*76Uns4DZ%nbZ*W$6J!i71k$Scxky&HTNuOj;9_Mojqg~c-$kMDO| zGBzCa`kG#Gf0f_4)Ep%drP@D*iSTai;`y}46YtP0oqBwU&310s-5xarBw8GB2DCICC){z$MGqa za@i``iMZo!+-BRWaJP!i*8mr8xYZe3+QXByi6h;p z=6+yJ&OYK0Gd9v&d|uS%{o8TN-}lZI`VDq#QXMef$);^rTwd_C*2do7WGBUFp1z&; zv2+%Yi^2MJve)XeyS+`8hAFx#?1oR-frag+Lw4O<4$19$H*>MN2yOlYnU`DxqG2`X zSV<4Gk{o7FMV4eouigey<`=G5H&&3}4xlaOnCQM&n3E z)OXH?Ls_}h9;|n5MVH#ddoi1LgIuptpT}w{vGov^>6rHQX9-Z2RzQwb3Klu9_a`nEt zMFk!A5$h!t$*x$syF7Lw`h?JjU`_702^#&9Xj~UX+)GvodwP0b+TG+$zeugR)J^xK z>wi$!)*EMIt5nv!NWuDgDh|Vyj^*m+qAX@CVg!9#Z}!QaO9k_k3W8;}hDO9k2DT zM$^Vb##9myPP}wK^_HjILw(-`7V%nC+-PM7QV5wksQmTl600XF-buo_B)sLU&5`NK zQ2)t7j*HqlaU&$z+;hb4bBx~hD*?0YmZ0E{3NS=8O}yR8YZX=GgKGG6lHqt zA8%DT_}n#7+QTK5Zp>mMw_*5@(1U4+Hl-mDSw2@Iw>4rAo^(sJYCgZK`30(wqKYvs zGVr&0VX(%BA7}~WvNbjC3yGMGzwfW>a*9!WTGx@dKi|dmdY@r5Q14kX=)(;^GyYMh zxXcP4q3Q|yVwK4Ab9nqcGXc~5ynU@jzhVBjoK0ytJMHNqL%ZtqPw7H%NC_~-`EH41UEf$ETD@XkN z)^O+L{JG#4-Y5I+1S~hk$)QCpJ4s0|PDbkwOIBUV-n=0m8-DmDPp|QmHS@iWv?gqC^LX`rT6tOrHF6#&Kj-a=`AMdTZu_|Vn}yx z+o&aKD2oqYOPpJ;ehN*CC$CHWa9Z-L7WnaixX zD-g?XUtU}Fx*9)w8}@39>KH}bacvat^eE>nh_VTEj7t299(yhD;d*CEcuV{#Xf1}y zB1Wmbl(@95csV|Ht5A~bWXn3`Re4DZBK673QYoLM%0w6GlPY_w6yocQR*GGHT0F}X zeXK_oKg4L4Ja#6EA%7LGI|e(36h1U{76QAc=@U^W?{k;6(LX=d8@*V36Kj3dhkp#- zEr1cUF}Zbg_gwGxvX-l1r+qGSrgbSwU1_NZ(#9s|a85=J@ZZgUJ=liveOa6h`mOJJ z*g`L^ue&9`*#IT4ir^3RedvU3OjI4L(o2_Yx#+OnMgKK9B46g3dqH_G2fn5L$DFOW zCQ2R*{qKqQxec|c)#%);EX9bmmra_(@pGt$@t!~Kzn0)v8>A{yT$2=4x6#d#$-?@E ziag&bEMh_T>#!EHOH9chfX6^l>06fRdBk;T04?#b?B_YbYXgmDc) zxj)w7S-js|7PY@9@76Z%Ns3#!Aug^QUEQZC2J z8JTj~wr$BC3hT|UwpB#0r}Dl!B^r-=aDnFYhFzC?@knqCS#;Q0M*69+Jb&lModyvj zv4T_Bd%2Ir)q3i{DM_%q0iyHkwPUEu7Rg)d)=Qcl$xcBUby87Xf}6MPNgDSP;d_5l zda3$ORK3_9^(MCIBh_$e8N~IJ#dCuFRcBr8b_y{`wuQ5moVQP?SHRO%We*--l^xI) z*F~fSyyu5vgAAoW2dwa~TA9ZsFlov6BTopdM0c%GwI?}I?2{Z<%MQI$wivyz=XL8_ z;U977H`qG)E=avK@-724k z9;7MZbN4FEJ>Aqk0_V$3QORTmr@maVIz6piZ-*Srh>ka!*|OhP*Z1Si>vx+Qi7vyt zg3j)_tDxt<5Vk`>FQ=U(9K79NtEQym%-Gy<)s_4%oYl_-YxqMzw9Gi_1zzIo{(u{b zsxgvhh+L(dAP2Q(+vm5M+*Lk%!TINyO62B8K;@nrpHVQ2@WndF>Y3?_!RSAN`zA`nffxpuFGErjpDX6T#SJDv*VnP`|^aAM4<2_Aci4jd6kK;pxk5dGGt`GF;BB?o*3en$RAx#i+@w*IXNchxZ^Em3Pjt z%50D@R#8m{%a&#_;<4ELB6$~iGe~^Z69kw|gM4gUF};lQNvnk&nn}DZWrE%@`FY6n z94cdpvRmAkeRlE0d7Gi2+KWe)mHhmdm+7lj5m8s(-CtIWsbR{5P1nEaWqkOfN}n%Ue)~>o2!7SU%y;fRg?4^rlf+5h>6kxN zYkfs8h7-W@U>nu@s^xlO5XpoN?_bwNU`+KZm6jg&qGViE)?uT>ebW}QN`B*#<%?{l zdynFnQO=v2UegleHCdt2K>T3Y#?)rLxi#7PR%Y_04{SZiUZIm>4;+s0OOIb1>f5>P z{-XD8@6UsuM$Q~aKiY)Z^YG~4eq#B-%7^{ryV5%r7qbZAE%T>yDSm&@h0_6ki~~Dm z7at2gNLU>FyjmdSaO{z}y8>2YfrCAFJp z%V|zB1Eis8g%Q_>jA+iHK6@=hoTN|y-NPike16(8{J;$ZG;|<3c6WeP<2o&EG5I zCX5B79C?-4PjUPG!stt8l)esN7cLJaH?|g{*t0S12d>nYjB`(GN5AcVrNM*z!|pSX z2ek<-!S0GmEx~xX2>GcDL8Xawx*wl@%Bf^$yycLTR2)M6J|e8i$7XNq)DRG5%^Z}`yZYf(1QAigF2sM3oleJz~1*wdpJk;{v_kZTYpdM`6rNpWFqaHM!Cf?K{gxSS8_!Y$TvP8hoi>__aMk7K*ZLernoNr0)n~$rd7(P!X01L>ARh;_)5ApihWgbY&X`EH zSXR|Af=dXpo#7sLKAw$aBYJb!obi#7J9HOJNIcq^ZDNrnQ{Kp;h(NJ}GCOzctAEOl zzyGzRKp)v7AV-4u+fX@2jjco6+F~W`t*=TbeG%z{tb_+#zRfL8x|Mv9KVD45bHFSq z#o@sE@T|>jLsGn1ZC>sq|-vyVdl<7rrWhP!lR4?I#xa1a7 z;u>>s&H|srP^O2qnUL6nZ=+ud|FY%fiFs!N9$dS4T_(j)y|K*aC^J7}@##eW{F(QQ z4Nm1dW;7pv>8*!|IYS9V6Jj@7E?5~zEBkTABu_c#JZ*(^pztzHw6ilsU}uFYec7GT zX|8ADkiM{?bM)Rvg^k)F(%;s2$B{d^T8_pUrRU&RT8ummvdwk-sj^FRY%oNX7$?0m z>>6d`>8YH36fOD5&@k@`uZ%4j0Vf87Y61^$N~Aq2e+I}wbZYh=^eYT}HM$xvTz}c4 z3_O#bI~WyOFPbUdvfNm%wIew=JE+1qqScj$GjMmIrNIQ+XV_l&WMDKp>e^hzKr}FV zy3^tGQRXZx2Am1SY`g?lCzMP?$^PQE3{6mi`%m8d%1uaJ*86yfH1}>ge(yoPh%8zX z2aJ?!O6y^pvbIYv>1~#W(;j_rX1y84Mc9Dh{0KhIp|)R%zGFF>YCH+NmUo2ST)i}VlJMq&l1{{K2(}Wn?^HoVwBuisg3{*QZ%>KB zDcfhgubh~TSPfab@)a}CA;R1O`;XSvKpV1*rr~~%J!Qnk-BbgWfkM~kztLRGQ$h5i zsgbYYbcaR6iylJY{MhuJt$ylBFHRhwoj*+d+K?79`1(v?n01*#9HVGY_~X*}i(t$Y z0lD-DxBBxlN|_bsuH4{jn$Y$VmRQ*wHkXmJ9@`i;ThTRu#aa>^ejnW!q3l^4;UXy| zl>Y*F5^W2FAUZCJ6J@HHcl+hQ+U{nbRN!(lXIwud_@+FfM+GRj`!4wN#UE>0e;!+1 z{ls}*x%`aV+t=DC>Y=_H1UwbO`}`bLH!F_6wN=O?}TeutRI6MtUG zg2Q{CYau>j#cPj&UuX61TpXCvsYYaxlZx}k2k-SL0d^v38#11Ut|p5r^#|D4SP6($ zIheirCFcJ{F3IDSO~=v%&V@471GR*Q=F( zW4~(1lPc*YpUoGRKCAGJoH%(?$@rpV{(>1QO|an@EsJ5 zW^iR4!zI-f^s&aCZ4V63?mID6wg0K};k($k9eg~i3Id~7N&>jxDibG$bHq`~@Ho-N ztEg&DV9zB!xYo49)|j%$xkR|7xYYrp-#RWydPjLU8;YElJC(%Q*5v3GV2D7WfPYwu$P7uKX+ z_({r-1x>`cOVOWA`Gi^T)}V&OWb&W7Z-20tS~Lbbt{Bo8ijZ7>f@D@nsD21rFT)I} z49uB%CX3-N`FfA-JDWa|&l4hI_PDUd1h|XZ~Pi$S13U>_JP>sV$hZ zp*?HBm8Dy9^4C4eyS;il;1LBXWn)UUn_x;^3mjnka3U}P*_0*R^|EX|U9=*~y9`#o z)bt_5Ft+v#MmJ{2?YCM8BB4Gh)pRe)lvj%_ z2(w_h`&ZpGpeoEW3OAY_EZhr7ZgUqjoFgeQcOj12H_j`6fm=ph(kHyBYdpRcw75`+ zBwh%V|Jv`$3aZmBd+i$B%Uqb&-fp~~o^&g1^?UsaDM*E=I$U%9F^? zd#uAhReQ5N6h=l&Vz<=-QC(8}{wj~LKa4QkPM4xc*uvl&oXDnaosBi5 zTYL-Fg4ZpoEjZM0@{D1~mK}17>hjY2*9JoC6mkK7EbF4XyvI`MTXj%_)b2Vvjfq%F z+x3NI5Z88p1gE+sW2pF%dShu*=4iaEf6Je!+9C+m;PG^4A~Og@VvDG z4FO6NjR|�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 UTMyLBmk@uhT(=@^+0LW?54c7xEdT%j diff --git a/drool/imgs/attacks/dual_shock.gif b/drool/imgs/attacks/dual_shock.gif new file mode 100644 index 0000000000000000000000000000000000000000..46b754db4c870e18c86edde0174a7403643d51df GIT binary patch literal 2353 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 XWrxzsNXSUmJ`iOR^*>`@V(j8vCv!L_#l;ZK5J0 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/modal_bolt_fire.gif b/drool/imgs/attacks/modal_bolt_fire.gif new file mode 100644 index 0000000000000000000000000000000000000000..5c067fa4dc62a0219ec61fff071f6113dd3af2a7 GIT binary patch literal 3742 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 IIe`QJ0pQdD-~a#s literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/modal_bolt_fire.png b/drool/imgs/attacks/modal_bolt_fire.png new file mode 100644 index 0000000000000000000000000000000000000000..8873e88d8f7d4ec4b411b18227a44622d70417c8 GIT binary patch literal 4606 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 AcK`qY literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/modal_bolt_ice.gif b/drool/imgs/attacks/modal_bolt_ice.gif new file mode 100644 index 0000000000000000000000000000000000000000..71217a66fd0a2ac275780c9c3bf1e9cf03b0e822 GIT binary patch literal 4056 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 E0_iN=UH||9 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/modal_bolt_ice.png b/drool/imgs/attacks/modal_bolt_ice.png new file mode 100644 index 0000000000000000000000000000000000000000..2cfeb348d2f2293f45c3ab9c0170f958c3b2d360 GIT binary patch literal 5159 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 BRnPzc literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/modal_bolt_lightning.png b/drool/imgs/attacks/modal_bolt_lightning.png new file mode 100644 index 0000000000000000000000000000000000000000..3677d304a1cf82f897073f0abaae08ccde1dfed9 GIT binary patch literal 5770 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/negative_thoughts.gif b/drool/imgs/attacks/negative_thoughts.gif new file mode 100644 index 0000000000000000000000000000000000000000..9e013d78db39204827a89bb4dd33b8ea400bf8ab GIT binary patch literal 6021 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 bC2%+WnrS^V{l)3wOdi+VWZtt3QT2ZSQ=VcO literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/negative_thoughts.png b/drool/imgs/attacks/negative_thoughts.png new file mode 100644 index 0000000000000000000000000000000000000000..442134d7696185b00babb5472c7fe00daadda875 GIT binary patch literal 8214 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/quickstorm.gif b/drool/imgs/attacks/quickstorm.gif new file mode 100644 index 0000000000000000000000000000000000000000..1ac9e187ee9760ea4a5ca91ad1894b7a6886e103 GIT binary patch literal 1890 zcmcJQeKga19LK-&kjXU&i8wMulMKudwsuN6i)}o$OsSt zjsqZ-N|*kKT)IL60HCBQG%_R5-IY$Zv9gwjEWi4spu8SP0YUaG+YkW&v=m5{4ZIsJ zcBVr$am=2Ea#1$Sl*;$MR?(HGYn$-2=UV0c)5h*>14`p1F&7(H%M>?O`EbB)37^s= z=s)j}Jo{AKRQ;gLC4D`T+FUbO>CMH3Nt%Vj)$~egDfN2oXx+}HgbB&@x<^+dL~Nwb zjru1|al^G?y*Dn8x3FGFN_|?cOx!y1VRoXo$$AO&O%3>W^JOg0AW-;%dsFN;WohFWu!zy=AS2Lo1(MhThu@Tx) zeb9mzqEm*-CsZw1XbKm#Bb6a8*yDK)5lJhwD2VktZ)7pm-kOX`yRh*XXF<97rT;~* z>{+cXr1-#UYMx|XU1;o`gZ=MD)d ze8w+H!t{;IaL@O~^53?l9V~%+Yp9LC*&3UrEe9 ztZL1TlL(B9aT8c=p{P2L7=-zJU>}2Lx*>Hm<$dJC409BRl)|zcY-aXmX;BU*hO})M zp1MAcxa@NKsCeD4J)17k@(-#RS=A37xOb1TJ%LQwi4SbqQML4y`rc}^mvv5V-l1eJ zqL`_zsR~_*Xef)Q`9m{h-y*&0~32Q87nY(H^IbY&R+t z!vKYCu2Q1a7Vy&ToVAjGp!cE5JrBOijr3$wG>uIq8U< za+0Ef(2HBkpM}@;)-gk}79Ag&>@0#vXbU)m<*r8SL$SW&wgM(^^V`q-5DszRN}9Yo zKlEqTSaY(p)KXTF*MF0Y|8FGgJ!c*Lze|ReU!{$)tQ_PI<$Q;qbY9km0R_+RW~(6G z_}+9AGFy$9&|Fgxc|TjvIaSM%6VrLd3@;${B<=CxVuK9}h45I>d7}M=c4aQ=LB6BX zlqybZ=0S#ssi7s##%%Z$g}7x}cQJy-4{{AEof$FfuP5E&XpJAN8z~RRSNP#sbHPn8 zvE3e(;C*8^jo(gLe&X%l)nN-6R&2L_8_}_2H9T4dZwQ>$$n)~M^WWf^%g$%ZoG0ie zRdlftm~YOLPj7T(8>GNM8bB+SY}30q-E-D5jGZC-v&2Q%y}#EBxmCXB@qy~wQs^^x zfY~W{yO1$Wt8^+eTYL;N?>l7>Kh*b_HQP~SUgpHi9W@W&Au*w3pX-xjW$0(Gd@UZu zg_TD<7c2-z2n_zQlzBzM$breW^u^!M%%aO-6@DVS?KyC~DAOCCoSAc^BDfm2IT@p0 z*eGeHY0xlV9iJx*kzE_jZt2*KJh@C9TNm1RF#6fiKzqZ4;+Xln@YO>=%7$PJ9M%k> z1*WXn#9~9;fPVp;dHAVagfV z!6yZG-qa+UdzO-pOWrs#;(SXXEIH_a?dt(RKWbH_ycsu??Xee8KSn~+g{p14M^gu? z9}Dpzo`*_;rUP#Hi`l>6b|yK2y_4OhXWhDc zTIQTP5*35gZZ9FGtJ>Ltt4wIVn+B;qL@vVOY;-w+w literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/quickstorm.png b/drool/imgs/attacks/quickstorm.png new file mode 100644 index 0000000000000000000000000000000000000000..11ce6a8495f6dc7bf4e6d481042e7aa45d4c4a51 GIT binary patch literal 2411 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/round_trip.png b/drool/imgs/attacks/round_trip.png new file mode 100644 index 0000000000000000000000000000000000000000..5329d77d4cd94b19b7260c6fe176994d1d773d90 GIT binary patch literal 1796 zcmcIk`#;lr82@~~>|&ZBM=fnABwZYw#AfD<$Xtf$a>OyINRs4Q)RJ^TmzO%OF$z(% zlVizpFY3r8$`+eOB34b7%ULdm^ADV#djIg;UeD|Oyq?$dem$2w+?}wPH5dQ@)_JFc z7XUD4DQ-X`mu&x@S3CfaZO#tAddKE|7Q2P@JFZ5uoY5YU{WmK=*YUN2uf7uLSv%lf zxgC>;^eFa%DSS;-7%OtU_U%~T><{(EYSwZaJC2^>g(TgrR*`IBvv!I1M?qLvR#ql| zf5BwR3V@H%qyt*E_e^z}I3>W=go7#Zc>||O#Y7wcc$CNB34TZZJ8J^W9Yy0`CKP} z81Wly?p^l+t-~dh%-~vI2MX6iTGiup-O^T%bx|80?lC`Q$cEDivM)>=6gx01k_Gx4 zG8oaYJL??j+dUcJbW566666T85EbqS01)a5{7{g=mMPTIWb~)ELPo+Lx37d|?br!) z3~a);qy!4hAuZN9GWy19A}!AAs(w)2Ypx+|+(6$y8=CCWD5($6+wW4~HSRx@u?86D z$#uR6T6yo zZ>nPe7k5l8%c^!h(uPV*VgPm;nP49AgwLQT0}z4XjCRUMmoWYS`+8VTYq z4>Ez9{qymPwoG-CyYT&L%m9>8^i+&(`u#y^t5;#qcT|+_T^E@e;mllhUnT(0eLsao zBRs?m_KIl;(XCnB!hhMhy(WJgHE|PQQV)^ps4Sj%)e-Hc5(N~$ERxTU(;vMS+BeuN zObR|uy#0q%Ja!gV;WQjL^=0_{pQakKP10{$ehx|QgLu{VEJa}aaitd`B$?yN+VqiUl(HfB-N!KL5(bVuQxxl9Gq4Io6o7$6eL2kXbbO{n6P+JVCDc6mgaDZ+P5rhqt<2 z3QrO@77>kTAMM)ffAJTi_!fb5JP#iJ=r#_4PY3I}Dkk1msULP7?J0}pI221DQYtpd z%%N8)XvMvRTb+u)BnHPoEf%btnY(|){Z@VM%D$fYzjTc~NGJ*$vS-TL<<#3euFsiN zN!ADq`^v@yGE-UOqx$*sg_lDp6l=er^6*{NKF9EyFBDIzaC?0Npy+flu=#$<#nN~A z8RrA2ixZQw#kqx&t-eiRLDh`;tY(u~j+M*N$pLNK)?B5(x#3mi;UCHcd&DQLk|yx9 zAoWDX3a8EYh?^0vU?(SAe=2ikE3z%Te~#Z3cPe4qG*Q&j^E|tZYNs#ug1+(gXIO}O z?Tg#4S8PhoJ|vd<1Fy8Sx!`+B->QvViC3Lkg&Nt7BRTnXd=V)jptkQqwm|B4hT+<% z)NpyIN-|2PEcm^id1IY-zd+`V?BPq}OrCVqy#bM{v*9bMS7;>`${oV5p|*?-5B$*i zQ-~dDG11+dHcMpntAEu;J1w3kCl9g6_%6#1H1XB1%%ITiyH$^V{Dh3op-o9RM1$7{{oWhtN;K2 literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/set_ablaze.gif b/drool/imgs/attacks/set_ablaze.gif new file mode 100644 index 0000000000000000000000000000000000000000..d596146cba3e19e4b8c6d75f4ebe499c5cf311a5 GIT binary patch literal 3687 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 MmQNQ=gbh~z---Pyr2qf` literal 0 HcmV?d00001 diff --git a/drool/imgs/attacks/set_ablaze.png b/drool/imgs/attacks/set_ablaze.png new file mode 100644 index 0000000000000000000000000000000000000000..103be62aa3498735247f1db7ecb259b3e81be02b GIT binary patch literal 5152 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 VX16Sh-}$G(?oX%lFRgDr_&;*3-75e9 literal 0 HcmV?d00001 diff --git a/drool/imgs/mon_switch.png b/drool/imgs/mon_switch.png index 947c6f60c1fb22a7b3175466842b94baa66f30f3..6262058fd63813a71f6e26235a1a595c7e77f17d 100644 GIT binary patch literal 47978 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

Ud*q5g@;?0-r(@zni#9*P*7A=fwSm4qT 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 A?EnA( literal 47935 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 LNkvXXu0mjfM4vYa diff --git a/processing/generateMonsTypeScript.py b/processing/generateMonsTypeScript.py index 01f528c4..be6c01fb 100644 --- a/processing/generateMonsTypeScript.py +++ b/processing/generateMonsTypeScript.py @@ -124,6 +124,13 @@ def build_sprite_config( "oppKO": "gachachacha_bunnies", "selfKO": "gachachacha_skulls", }, + # Modal Bolt picks by the chosen mode (extraData 0/1/2), not a runtime outcome. + # Art pending — the keys/wiring are live so the sprites drop in with no code change. + "Modal Bolt": { + "fire": "modal_bolt_fire", + "ice": "modal_bolt_ice", + "lightning": "modal_bolt_lightning", + }, } # Maps move name → attack-overlay placement. Absent means the default 'target' @@ -134,6 +141,7 @@ def build_sprite_config( "Gachachacha": "canvas-center", "Loop": "self", "Triple Think": "self", + "Sanctify": "self", } @@ -504,13 +512,13 @@ def generate_typescript_const(data: Dict[int, Dict[str, Any]], output_file: str) export const MonMetadata = {json_str} as const; -// Moves with runtime-branching outcomes (currently just Gachachacha) emit -// this shape on `sprite` instead of a single SpriteAnimationConfig; the -// renderer picks by branch. +// Moves that fan out to more than one attack animation emit this open map on +// `sprite` instead of a single SpriteAnimationConfig; the renderer picks the +// key by branch (the move's MoveBranch). Each variant move owns its own keys — +// Gachachacha uses runtime outcomes (normal/oppKO/selfKO), Modal Bolt uses the +// cast-time mode (fire/ice/lightning) — so nothing central enumerates them. export type MoveSpriteVariants = {{ - readonly normal: SpriteAnimationConfig; - readonly oppKO: SpriteAnimationConfig; - readonly selfKO: SpriteAnimationConfig; + readonly [variant: string]: SpriteAnimationConfig; }}; // Where a move's attack overlay renders. Default 'target' overlays the sprite diff --git a/src/game-layer/CLAUDE.md b/src/game-layer/CLAUDE.md new file mode 100644 index 00000000..b3434ac6 --- /dev/null +++ b/src/game-layer/CLAUDE.md @@ -0,0 +1,72 @@ +# Gacha & Progression System + +`GachaTeamRegistry` is the concrete leaf — it's an `IEngineHook` (subscribes to `OnBattleEnd`) and composes seven abstract bases that each own one slice of state and behavior: + +| Abstract | Owns | +|---|---| +| `MonOwnership` | `monsOwned` per-player set; ownership view + bulk-check helpers (`_isOwnerBatch`, `_validateOwnership`). Satisfies the `_isFacetMonOwned` hook on `Facets`. | +| `MonRegistry` | Owner-managed mon catalog (`monStats`, `monMoves`, `monAbilities`, `monMetadata`, sequential `monIds` set). Inherits `ITeamRegistry` so its `createMon`/`modifyMon`/batched-getters bind directly. Backs the `_getMonStatsForFacets` hook used by facet delta computation. | +| `PlayerProfile` | Packed `playerData[address]` (one uint256 per player) + the owner-managed `isAssigner` allowlist. Exposes `pointsBalance`, `isWhitelistedOpponent`, the bulk admin flag setters, and `assignPoints` (IGachaPointsAssigner). | +| `PackedTeamStore` | Bit-packed team CRUD: `teamGroupsPacked` (4 teams × 64 bits per slot), `teamOrderPacked` (16-slot live bitmap + display order). Constructor takes `MONS_PER_TEAM` / `MOVES_PER_MON` immutables. Subclass hooks: `_packedTeamValidateOwnership`, `_packedTeamIsCpuOpponent`, `_packedTeamGetMonData`. | +| `Facets` | 12-facet ±5% stat tradeoff system (see below). Pure helpers, packed per-mon facet data, `assignFacets`. | +| `MonExp` | Packed per-mon exp (`packedExpForMon`), level curve, level-up facet draws (`_processLevelUps`), public `getExp` / `getLevel` / `getExpAndLevelsFor*`, assigner-gated `assignExp`. Inherits `Facets` + `PackedTeamStore` because level-ups draw facets and team views need the lane helpers. Subclass hooks: `_assertExpAssigner`, `_monRegistrySize`. | +| `Quests` | Daily quest pool + packed predicate evaluator. Subclass hook: `_extract` (opcode dispatch implemented by the leaf since it sees Engine + all sub-systems). | + +The leaf adds: the `onBattleEnd` orchestration (streak / quest / points / exp loop, plus a `_applyExpAndFacetDraws` walk that mirrors `assignExp` but with KO bitmap + streak + event packing), gacha rolling (`firstRoll`/`roll`/`_rollInto`), the per-(user, opponent) CPU phantom facet config, the `getTeams` variant that folds facet deltas in, the `_extract` quest opcode dispatch, and the wiring overrides for every subclass hook. With this split the leaf is ~750 LOC of integration and event-shape concerns; each base is independently auditable. + +**Rolling.** Mon ids are sequential starting at 0 (`createMon` enforces `monId == monIds.length()`). Ids `[0, NUM_STARTERS)` (= 3) are *starter* mons. + +- `firstRoll(uint256 starterId)` — one-shot per player, onboarding in a single tx. Caller picks `starterId ∈ {0,1,2}`; the contract guarantees that mon at slot 0 of the result and rolls `INITIAL_ROLLS - 1` (= 3) more uniformly from `[NUM_STARTERS, numMons)`. Free. Also creates the player's first team (slot 0) from the first `MONS_PER_TEAM` rolled ids, via the same internal path `createTeam` uses (`_createTeamForUser`) — skipping the redundant ownership check since those ids were just written. The constructor reverts `MonsPerTeamExceedsInitialRolls` if `MONS_PER_TEAM > INITIAL_ROLLS`, since a deployment configured that way could never satisfy this. +- `roll(uint256 numRolls)` — paid (`ROLL_COST` per roll, default 16 points). Uniform across the entire pool. Reverts `NoMoreStock` once the caller owns every mon. +- Linear-probing dedup keeps draws inside their window so `firstRoll`'s 3 random picks never land on a starter. + +**Points / exp / facets storage.** All packed for gas: + +``` +playerData[address] (1 slot per player): + bit 255 bonusAwarded (first-game-ever bonus claimed) + bit 254 isWhitelistedAsOpponent (admin-set; replaces a separate mapping) + bit 253 (reserved; formerly isHardCpu) + bits 250-252 streakDay (1..STREAK_FLAT_BONUS_MAX; 0 = no streak yet) + bits 224-249 (reserved) + bits 192-223 lastQuestCompletedDay (uint32 calendar day) + bits 160-191 lastSeenTimestamp (uint32 seconds; last battle of ANY kind — drives streak grace/reset) + bits 128-159 lastFirstGameTimestamp (uint32 seconds; last streak-bonus game — gates the 24h cooldown) + bits 0-127 pointsBalance (uint128) + +packedExpForMon[player][monId / 16]: 16 mons × 16 bits each, capped at 65535. +facetData[player][monId / 16]: 16 mons × 16 bits each + (bits 0-11 unlockedBitmap, bits 12-15 assignedFacetId). +``` + +Streak is timestamp-driven (not calendar-day): a battle qualifies for the streak bonus +when ≥24h have passed since `lastFirstGameTimestamp` (the last *bonus-earning* game). On a +qualifying battle the ratchet-vs-reset decision is measured from `lastSeenTimestamp` (the +last battle of *any* kind, advanced every battle): a gap >36h (`STREAK_GRACE_WINDOW`) of +genuine inactivity resets `streakDay` to 1, otherwise it ratchets up toward the cap of +`STREAK_FLAT_BONUS_MAX` (= 5). Splitting the two anchors is deliberate — measuring the +reset from the bonus anchor instead would strand players who play slightly more often than +once per 24h (their sub-24h plays advance no anchor, so the next day reads a phantom ~46h +gap and resets the streak forever). + +Both per-mon mappings share the same 16-mon bucketing so `_applyExpAndFacetDraws` walks the team in one pass and coalesces SSTOREs by bucket. + +**Battle rewards (`onBattleEnd`).** CPU side is short-circuited (no SSTOREs, no event). For each human side: + +- Base points: `POINTS_PER_WIN` (2) on win, `POINTS_PER_LOSS` (1) otherwise. +- Streak flat: `streakDay` (1..5) added inside the parenthetical of both the points and per-mon-exp formulas. Only granted when the battle qualifies as "first of day" (≥24h since the last grant). +- Points formula: `(basePts + streakFlat) × gachaMult + firstGameEverBonus`. `gachaMult` is `×QUEST_REWARD_MULT` (= 2) when the active daily quest completes (winner-only, one-shot per day), else 1. `firstGameEverBonus` is `+FIRST_GAME_EVER_BONUS` (= 16), one-shot ever, applied *after* the multiplier. +- Per-mon exp formula: `(baseExp + streakFlat) × expMult`, capped at 65535. `baseExp` is `EXP_PER_SURVIVING_MON` (2) for alive slots, `EXP_PER_KOD_MON` (1) for KO'd slots. +- `expMult` stack: a flat `GAME_EXP_MULT` (×2) on **every** battle regardless of game type (PvP, CPU, hard or not), times `QUEST_REWARD_MULT` (×2) on quest completion. Max stack = ×4. (There is no longer a PvP-vs-CPU or hard-CPU exp distinction — the old client-set "hard CPU" flag let any user self-grant the bonus, so it was removed.) +- Level-ups (12-tier curve, capped at level 12 to match `TOTAL_FACETS`) trigger one facet draw per level crossed. + +**Facets.** 12 systematically-derived stat tradeoffs across 4 stat groups (`HP`, `Atk`, `Def`, `Speed`). `_facetDef(facetId)` is pure — no constant table. Magnitudes are **boost-indexed**: the boost stat determines both the boost% and the cost% paid on the nerfed stat. HP/Atk/Def boosts are symmetric (+5% / -5%); speed boosts pay a heavier cost (+5% / -10%) so they can't cheaply break speed ties. The percentages live as `BOOST_PCT_*` / `COST_PCT_*` constants in `Facets.sol`. Unlocks are persistent per-mon; `assignFacets(monIds, facetIds)` is a free bulk re-assign that requires the caller to own every listed mon and the facet to be in the unlocked bitmap (`facetId == 0` clears). `GachaTeamRegistry.getTeams()` folds the active facet's delta into each mon's stats before returning, so by the time the Engine stores teams in `BattleConfig` they already reflect the boost/cost. `validateMon` no longer checks stat equality (the round-trip would always fail with facets applied) — moves and ability membership are still enforced. + +**CPU opponent facets.** When fighting a whitelisted opponent (CPU), the human caller picks the CPU's team *and* its facet config in one call: `setOpponentTeam(opponent, monIndices, facetIds)`. Per-user-per-CPU storage (`opponentTeamFacetsPacked[opponent][phantomKey]`) keyed by the same `uint16(uint160(msg.sender))` phantom slot as the team. No ownership/unlock checks — any facet 0..12 is allowed. `getTeamsWithDeltas` short-circuits to this slot-indexed config when a side is `isWhitelistedOpponent`, so per-user CPU facet configurations stay isolated even when many users fight the same CPU. + +**Quests.** Owner-managed `questPool`. One quest is active per UTC day, derived statelessly as `keccak256(day) % poolLength` (day = `block.timestamp / 1 days` + an owner-settable `_dayOffset`) — no rotation SSTORE and no race between concurrent battles. Each quest has up to `MAX_PREDICATES_PER_QUEST` (6) AND-composed predicates packed into one storage slot (41 bits each: `op` 5b, `cmp` 3b, `negate` 1b, `arg` 16b, `operand` 16b — total 246b + 3b count). Opcodes cover battle context (`TURNS`, `ALIVE_COUNT`, `ACTIVE_SLOT_INDEX`, `MON_KO_AT_SLOT`), team composition (`HAS_MON_ID`), per-mon progression (`MON_LEVEL`, `MON_FACET`), and live battle state (`MON_STATE` via `Engine.getMonStateForBattle`). + +**Events.** + +- `Roll(address indexed player, uint256[] monIds, uint256 pointsSpent)` — fires on both `firstRoll` (spend = 0) and paid `roll`. +- `GachaEvent(bytes32 indexed battleKey, uint256 p0Packed, uint256 p1Packed)` — one per battle, carrying both sides' packed payloads (CPU side is 0). Layout sized for `MONS_PER_TEAM` up to 8: points (bits 0-15), per-mon exp gain (bits 16-79, 8 lanes × 8b), per-mon facets unlocked this battle (bits 80-175, 8 lanes × 12-bit bitmap = 1 bit per facet id), `BONUS_*` flags (bits 176-183: `FIRST_ROLL` (bit 0) | `FIRST_GAME` (bit 1) | bit 2 reserved/formerly `HARD_CPU` | `QUEST` (bit 3)), combined exp multiplier (bits 184-191), outcome (bits 192-199: 0=loss, 1=win, 2=draw), `streakDay` (bits 200-202). Lanes saturate so a future tuning blow-up can't bleed into neighbouring fields. From 0e78c3955d9fb4ceab8c3256253caed7627d6990 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:58:22 -0700 Subject: [PATCH 21/41] clean up --- script/SetupMons.s.sol | 2 +- src/Engine.sol | 25 +- src/IEngine.sol | 12 +- src/effects/IEffect.sol | 4 +- src/lib/MappingAllocator.sol | 8 - src/lib/SwitchTargetLib.sol | 7 +- src/mons/ghouliath/GraveAffliction.sol | 4 +- test/EngineTest.sol | 10 +- test/effects/PreDamageHookTest.sol | 58 ++- test/mons/GhouliathTest.sol | 24 +- transpiler/codegen/statement.py | 30 +- transpiler/codegen/yul.py | 104 ++++- transpiler/codegen_rs/definition.py | 14 + transpiler/codegen_rs/expression.py | 34 ++ transpiler/codegen_rs/statement.py | 113 ++++++ transpiler/codegen_rs/symbols.py | 30 +- transpiler/sol2rs.py | 3 + transpiler/strategies-rs/src/analysis.rs | 10 +- transpiler/strategies-rs/src/arena.rs | 163 +++++++- transpiler/strategies-rs/src/bin/analyze.rs | 7 +- transpiler/strategies-rs/src/bin/arena.rs | 14 +- transpiler/strategies-rs/src/bin/bench.rs | 100 +++++ transpiler/strategies-rs/src/bin/cert.rs | 8 +- transpiler/strategies-rs/src/bin/findpanic.rs | 7 +- transpiler/strategies-rs/src/bin/mock2.rs | 5 +- transpiler/strategies-rs/src/bin/statpeak.rs | 7 +- .../strategies-rs/src/bin/tracesearch.rs | 8 +- transpiler/strategies-rs/src/bot.rs | 130 ++++++ transpiler/strategies-rs/src/bots.rs | 369 ++++++++++++++++++ transpiler/strategies-rs/src/breadth.rs | 5 +- transpiler/strategies-rs/src/doubles.rs | 89 ++++- transpiler/strategies-rs/src/game.rs | 277 ++++++------- transpiler/strategies-rs/src/jsrng.rs | 22 ++ transpiler/strategies-rs/src/lib.rs | 2 + transpiler/strategies-rs/src/mock2.rs | 4 +- transpiler/strategies-rs/src/teams.rs | 8 +- transpiler/strategies-rs/src/yomi.rs | 5 +- transpiler/transpiler-config-rust.json | 4 + transpiler/type_system/registry.py | 5 + transpiler/type_system/slots.py | 309 +++++++++++++++ 40 files changed, 1759 insertions(+), 281 deletions(-) create mode 100644 transpiler/strategies-rs/src/bin/bench.rs create mode 100644 transpiler/strategies-rs/src/bot.rs create mode 100644 transpiler/strategies-rs/src/bots.rs create mode 100644 transpiler/type_system/slots.py diff --git a/script/SetupMons.s.sol b/script/SetupMons.s.sol index 1e9c0a1d..d4f7b3fc 100644 --- a/script/SetupMons.s.sol +++ b/script/SetupMons.s.sol @@ -170,7 +170,7 @@ contract SetupMons is Script { moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); moves[2] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); moves[3] = 0x5a00200000000000000000000000000000000000000000000000000000000000; - moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + moves[4] = 0x0030200000000000000000030000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); uint256[] memory abilities = new uint256[](1); abilities[0] = uint256(uint160(addrs[4])); bytes32[] memory keys = new bytes32[](0); diff --git a/src/Engine.sol b/src/Engine.sol index cc9b9c54..df32546c 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -2797,10 +2797,6 @@ contract Engine is IEngine, MappingAllocator, EIP712 { _dealDamageInternal(config, playerIndex, monIndex, damage, uint256(uint160(msg.sender))); } - function getPreDamage() external view returns (int32) { - return tempPreDamage; - } - function setPreDamage(int32 value) external { if (battleKeyForWrite == bytes32(0)) { revert NoWriteAllowed(); @@ -3956,23 +3952,18 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return (actedSlotsThisTurnMask >> absSlot) & 1 != 0; } - /// @notice The roster range a slot may switch within (Multi: the seat's quarter). - function getRosterBoundsForSlot(bytes32 battleKey, uint256 playerIndex, uint256 slotIndex) + /// @notice The roster range a slot may switch within (Multi: the seat's quarter) plus the + /// side's KO bitmap, so a switch-target search costs one call rather than one per + /// candidate. `koBitmap` is bit-per-roster-index and is kept in lockstep with each + /// mon's `isKnockedOut`. + function getSlotSwitchWindow(bytes32 battleKey, uint256 playerIndex, uint256 slotIndex) external view - returns (uint256 lo, uint256 hi) + returns (uint256 lo, uint256 hi, uint256 koBitmap) { BattleConfig storage config = battleConfig[_resolveStorageKey(battleKey)]; - return _slotRosterBounds(config, (playerIndex << 1) | (slotIndex & 1)); - } - - function computePriorityPlayerIndex(bytes32 battleKey, uint256 rng) public view returns (uint256) { - bytes32 storageKey = _resolveStorageKey(battleKey); - BattleConfig storage config = battleConfig[storageKey]; - BattleData storage battle = battleData[battleKey]; - return _computePriorityPlayerIndex( - config, battle, battleKey, rng, _getCurrentTurnMove(config, 0), _getCurrentTurnMove(config, 1) - ); + (lo, hi) = _slotRosterBounds(config, (playerIndex << 1) | (slotIndex & 1)); + koBitmap = _getKOBitmap(config, playerIndex); } /// @dev Internal priority computation that accepts already-resolved storage pointers and diff --git a/src/IEngine.sol b/src/IEngine.sol index de04616e..6fd84442 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -12,8 +12,8 @@ interface IEngine { function battleKeyForWrite() external view returns (bytes32); function tempRNG() external view returns (uint256); - // PreDamage threading: hooks read the running damage and call setPreDamage to mutate it. - function getPreDamage() external view returns (int32); + // PreDamage threading: hooks read the running damage from their packed hook context + // (TargetLib.hookPreDamage) and call setPreDamage to mutate it. function setPreDamage(int32 value) external; // State mutating effects @@ -141,15 +141,15 @@ interface IEngine { view returns (bytes32 battleKey, bytes32 partyHash); function getSeats(bytes32 battleKey) external view returns (address[4] memory seats); - function computePriorityPlayerIndex(bytes32 battleKey, uint256 rng) external view returns (uint256); // Per-slot "has acted (or is acting) this turn" — the mode-agnostic primitive status effects // use to decide between an immediate move-cancel and waiting for next RoundStart. function hasSlotActedThisTurn(uint256 absSlot) external view returns (bool); - // The roster range a slot may switch within (Multi partitions each side by seat quarter). - function getRosterBoundsForSlot(bytes32 battleKey, uint256 playerIndex, uint256 slotIndex) + // The roster range a slot may switch within (Multi partitions each side by seat quarter), + // bundled with the side's KO bitmap so a switch-target search is a single call. + function getSlotSwitchWindow(bytes32 battleKey, uint256 playerIndex, uint256 slotIndex) external view - returns (uint256 lo, uint256 hi); + returns (uint256 lo, uint256 hi, uint256 koBitmap); function getStorageKey(bytes32 battleKey) external view returns (bytes32); function getBattle(bytes32 battleKey) external view returns (BattleConfigView memory, BattleData memory); function getMonValueForBattle( diff --git a/src/effects/IEffect.sol b/src/effects/IEffect.sol index a55a94e7..c0ef313b 100644 --- a/src/effects/IEffect.sol +++ b/src/effects/IEffect.sol @@ -89,8 +89,8 @@ interface IEffect { // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) // Runs before damage is applied; effects can mutate the in-flight damage by calling - // `engine.setPreDamage(int32)`. Opted-in effects read the fresh current running damage via - // `TargetLib.hookPreDamage(activesPacked)`; legacy effects may use `engine.getPreDamage()`. + // `engine.setPreDamage(int32)`. Effects read the fresh current running damage via + // `TargetLib.hookPreDamage(activesPacked)`, which requires opting into PreDamage context. // Multiple subscribed effects compose sequentially in effect-array order, each observing // the post-mutation value from prior effects. function onPreDamage( diff --git a/src/lib/MappingAllocator.sol b/src/lib/MappingAllocator.sol index b5df937d..64ee2a61 100644 --- a/src/lib/MappingAllocator.sol +++ b/src/lib/MappingAllocator.sol @@ -63,12 +63,4 @@ abstract contract MappingAllocator { } delete battleKeyToStorageKey[battleKey]; } - - function getFreeStorageKeys() public view returns (bytes32[] memory keys) { - uint256 count = _freeKeyCount(); - keys = new bytes32[](count); - for (uint256 i; i < count; i++) { - keys[i] = freeStorageKeys[i]; - } - } } diff --git a/src/lib/SwitchTargetLib.sol b/src/lib/SwitchTargetLib.sol index 6c4846ab..a069147f 100644 --- a/src/lib/SwitchTargetLib.sol +++ b/src/lib/SwitchTargetLib.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.0; -import {MonStateIndexName} from "../Enums.sol"; import {IEngine} from "../IEngine.sol"; import {RNGLib} from "./RNGLib.sol"; @@ -22,14 +21,12 @@ library SwitchTargetLib { ) internal view returns (int32) { // Mix in target player index to break symmetry on mirror matchups uint256 offset = RNGLib.mixForAttacker(rng, playerIndex); - (uint256 lo, uint256 hi) = engine.getRosterBoundsForSlot(battleKey, playerIndex, slotIndex); + (uint256 lo, uint256 hi, uint256 koBitmap) = engine.getSlotSwitchWindow(battleKey, playerIndex, slotIndex); uint256 span = hi - lo; for (uint256 i; i < span; ++i) { uint256 candidate = lo + ((i + offset) % span); if (candidate != currentMonIndex && candidate != allyMonIndex) { - bool isKOed = - engine.getMonStateForBattle(battleKey, playerIndex, candidate, MonStateIndexName.IsKnockedOut) == 1; - if (!isKOed) { + if ((koBitmap >> candidate) & 1 == 0) { return int32(int256(candidate)); } } diff --git a/src/mons/ghouliath/GraveAffliction.sol b/src/mons/ghouliath/GraveAffliction.sol index 08e4a094..a36c6f13 100644 --- a/src/mons/ghouliath/GraveAffliction.sol +++ b/src/mons/ghouliath/GraveAffliction.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; +// @move-context: status-lanes + import "../../Constants.sol"; import "../../Enums.sol"; import {MoveMeta} from "../../Structs.sol"; @@ -36,7 +38,7 @@ contract GraveAffliction is IMoveSet { uint256 defenderMonIndex = TargetLib.activeAt(activesPacked, targetSlot); // Only fires if the opposing mon currently has a status condition (any class). - if (engine.getMonStatusClass(battleKey, defenderPlayerIndex, defenderMonIndex) == 0) { + if (TargetLib.hookStatusClass(activesPacked, defenderPlayerIndex, defenderMonIndex) == 0) { return; } diff --git a/test/EngineTest.sol b/test/EngineTest.sol index f6bcbb91..73d0b5df 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -2944,12 +2944,12 @@ contract EngineTest is Test, BattleHelper { team[0] = mon; defaultRegistry.setTeam(ALICE, team); defaultRegistry.setTeam(BOB, team); - // Assert there are no free battle config slots - assertEq(engine.getFreeStorageKeys().length, 0); bytes32 battleKey = _startBattle(engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); - vm.warp(block.timestamp + MAX_BATTLE_DURATION + 1); + bytes32 storageKey = engine.getStorageKey(battleKey); + vm.warp(vm.getBlockTimestamp() + MAX_BATTLE_DURATION + 1); engine.end(battleKey); - // Assert that there is now 1 free battle config slot - assertEq(engine.getFreeStorageKeys().length, 1); + // The timed-out battle's config slot returns to the pool, so the next battle recycles it. + bytes32 battleKey2 = _startBattle(engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); + assertEq(engine.getStorageKey(battleKey2), storageKey, "expected recycled storage key"); } } diff --git a/test/effects/PreDamageHookTest.sol b/test/effects/PreDamageHookTest.sol index 391de9a1..971be509 100644 --- a/test/effects/PreDamageHookTest.sol +++ b/test/effects/PreDamageHookTest.sol @@ -12,6 +12,7 @@ import {IEngine} from "../../src/IEngine.sol"; import {DefaultCommitManager} from "../../src/commit-manager/DefaultCommitManager.sol"; import {BasicEffect} from "../../src/effects/BasicEffect.sol"; import {IEffect} from "../../src/effects/IEffect.sol"; +import {TargetLib} from "../../src/lib/TargetLib.sol"; import {DefaultMatchmaker} from "../../src/matchmaker/DefaultMatchmaker.sol"; import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; @@ -26,15 +27,20 @@ import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; // PreDamage that halves the running damage. contract PreDamageHalveEffect is BasicEffect { function getStepsBitmap() external pure override returns (uint32) { - return 0x200; // PreDamage + return 0x02000200; // PreDamage + fresh PreDamage context } - function onPreDamage(IEngine engine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) - external - override - returns (bytes32, bool) - { - engine.setPreDamage(engine.getPreDamage() / 2); + function onPreDamage( + IEngine engine, + bytes32, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256 hookContext, + uint256 + ) external override returns (bytes32, bool) { + engine.setPreDamage(TargetLib.hookPreDamage(hookContext) / 2); return (extraData, false); } } @@ -58,15 +64,20 @@ contract PreDamageAbsorbEffect is BasicEffect { // PreDamage that doubles the running damage. contract PreDamageDoubleEffect is BasicEffect { function getStepsBitmap() external pure override returns (uint32) { - return 0x200; + return 0x02000200; } - function onPreDamage(IEngine engine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) - external - override - returns (bytes32, bool) - { - engine.setPreDamage(engine.getPreDamage() * 2); + function onPreDamage( + IEngine engine, + bytes32, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256 hookContext, + uint256 + ) external override returns (bytes32, bool) { + engine.setPreDamage(TargetLib.hookPreDamage(hookContext) * 2); return (extraData, false); } } @@ -81,17 +92,22 @@ contract SourceCaptureEffect is BasicEffect { uint256 public afterDamageCallCount; function getStepsBitmap() external pure override returns (uint32) { - return 0x240; // PreDamage | AfterDamage + return 0x02000240; // PreDamage | AfterDamage + fresh context } - function onPreDamage(IEngine engine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256 source) - external - override - returns (bytes32, bool) - { + function onPreDamage( + IEngine, + bytes32, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256 hookContext, + uint256 source + ) external override returns (bytes32, bool) { preDamageCallCount += 1; lastPreDamageSource = source; - lastPreDamageSeenDamage = engine.getPreDamage(); + lastPreDamageSeenDamage = TargetLib.hookPreDamage(hookContext); return (extraData, false); } diff --git a/test/mons/GhouliathTest.sol b/test/mons/GhouliathTest.sol index e43f0f37..b7d1038c 100644 --- a/test/mons/GhouliathTest.sol +++ b/test/mons/GhouliathTest.sol @@ -14,6 +14,7 @@ import {IEngine} from "../../src/IEngine.sol"; import {IEffect} from "../../src/effects/IEffect.sol"; import {IMoveSet} from "../../src/moves/IMoveSet.sol"; +import {MoveSlotLib} from "../../src/moves/MoveSlotLib.sol"; import {StandardAttackFactory} from "../../src/moves/StandardAttackFactory.sol"; import {ATTACK_PARAMS} from "../../src/moves/StandardAttackStructs.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; @@ -541,16 +542,22 @@ contract GhouliathTest is Test, BattleHelper { assertEq(spAttackDelta, -5, "Bob's mon's special attack should be debuffed"); } - // Build two single-mon (well, duplicated) teams and start a battle. Alice carries - // [burnApplier, graveAffliction]; Bob carries two fillers. Returns the battle key after both - // mons are switched in. - function _startGraveAfflictionBattle(IMoveSet aliceMove0, IMoveSet aliceMove1) + // Grave Affliction reads the defender's status class out of its move context, so its slot + // needs the capability bit the deployment generator derives from its @move-context tag. + function _graveAfflictionSlot() internal view returns (uint256) { + return MoveSlotLib.packDeployed(address(graveAffliction), 0, MOVE_META_DYNAMIC) | MOVE_CONTEXT_STATUS_LANES; + } + + // Build two single-mon (well, duplicated) teams and start a battle. Alice carries the two + // given packed move slots; Bob carries two fillers. Returns the battle key after both mons + // are switched in. + function _startGraveAfflictionBattle(uint256 aliceMove0, uint256 aliceMove1) internal returns (bytes32 battleKey) { uint256[] memory aliceMoves = new uint256[](2); - aliceMoves[0] = uint256(uint160(address(aliceMove0))); - aliceMoves[1] = uint256(uint160(address(aliceMove1))); + aliceMoves[0] = aliceMove0; + aliceMoves[1] = aliceMove1; uint256[] memory bobMoves = new uint256[](2); bobMoves[0] = uint256(uint160(address(osteoporosis))); @@ -623,7 +630,7 @@ contract GhouliathTest is Test, BattleHelper { }) ); - bytes32 battleKey = _startGraveAfflictionBattle(burnApplier, IMoveSet(address(graveAffliction))); + bytes32 battleKey = _startGraveAfflictionBattle(uint256(uint160(address(burnApplier))), _graveAfflictionSlot()); // Alice burns Bob (move 0); Bob no-ops. _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); @@ -650,8 +657,7 @@ contract GhouliathTest is Test, BattleHelper { // With no status on the opponent, Grave Affliction is a no-op for both mons. function testGraveAffliction_noOpWhenOpponentHealthy() public { - bytes32 battleKey = - _startGraveAfflictionBattle(IMoveSet(address(graveAffliction)), IMoveSet(address(graveAffliction))); + bytes32 battleKey = _startGraveAfflictionBattle(_graveAfflictionSlot(), _graveAfflictionSlot()); // Alice uses Grave Affliction (move 0) while Bob has no status; Bob no-ops. _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); diff --git a/transpiler/codegen/statement.py b/transpiler/codegen/statement.py index 2243e9f3..d5a8593b 100644 --- a/transpiler/codegen/statement.py +++ b/transpiler/codegen/statement.py @@ -519,10 +519,38 @@ def generate_revert_statement(self, stmt: RevertStatement) -> str: # ASSEMBLY # ========================================================================= + def _slot_layout_of(self, var_name: str): + """(slot-0 layout, member TypeNames) for a struct-typed local, or None. + + Assembly reaching a raw slot has to be reconciled with the modelled + fields; only the statement layer knows local variable types. + """ + from ..type_system.slots import slot_zero + + registry = getattr(self._ctx, '_registry', None) + if registry is None: + return None + type_name = self._ctx.var_types.get(var_name) + if type_name is None or not getattr(type_name, 'name', None): + return None + struct = registry.struct_defs.get(type_name.name) + if struct is None: + return None + layout = slot_zero( + struct, + enum_names=registry.enums, + struct_names=registry.structs, + structs=registry.struct_defs, + ) + if layout is None: + return None + member_types = {m.name: m.type_name for m in struct.members} + return layout, member_types + def generate_assembly_statement(self, stmt: AssemblyStatement) -> str: """Generate TypeScript code for an assembly block (transpiled from Yul).""" yul_code = stmt.block.code - ts_code = self._yul_transpiler.transpile(yul_code) + ts_code = self._yul_transpiler.transpile(yul_code, self._slot_layout_of) if self._yul_transpiler.unmodelable: self._ctx.current_function_unmodelable = True lines = [] diff --git a/transpiler/codegen/yul.py b/transpiler/codegen/yul.py index 24d4ea07..23c8b253 100644 --- a/transpiler/codegen/yul.py +++ b/transpiler/codegen/yul.py @@ -612,6 +612,9 @@ def __init__(self, known_constants: set = None): self._known_constants = known_constants or set() self._warnings: List[str] = [] self._unmodelable = False + # var name -> (SlotLayout, {member: TypeName}); set per block by the + # caller, which is the only layer that knows local variable types. + self._slot_resolver = None @property def warnings(self) -> List[str]: @@ -624,7 +627,95 @@ def unmodelable(self) -> bool: cannot be faithfully simulated — the caller should stub the function.""" return self._unmodelable - def transpile(self, yul_code: str) -> str: + # ===================================================================== + # Whole-slot access against the modelled fields + # ===================================================================== + + def _slot_target(self, expr, slot_vars: Dict[str, str]): + """(var name, layout, member types) for a slot expression, or None.""" + if self._slot_resolver is None: + return None + name = None + if isinstance(expr, YulSlotAccess): + name = expr.variable + elif isinstance(expr, YulIdentifier) and expr.name in slot_vars: + name = slot_vars[expr.name] + if name is None: + return None + resolved = self._slot_resolver(name) + if resolved is None: + return None + layout, member_types = resolved + return name, layout, member_types + + @staticmethod + def _member_pack(access: str, tn) -> Optional[str]: + """Widen a member to its slot bits as a bigint expression.""" + t = (tn.name or '').strip() if tn is not None else '' + if not t or (tn is not None and (tn.is_array or tn.is_mapping)): + return None + if t == 'bool': + return f'({access} ? 1n : 0n)' + if t.startswith('uint') or t.startswith('bytes') or t == 'address': + return f'BigInt({access})' + if t.startswith('int'): + bits = t[3:] or '256' + return f'BigInt.asUintN({bits}, BigInt({access}))' + # enums are numbers; every other user-defined name is a contract ref + return f'BigInt({access}?._contractAddress ?? {access} ?? 0n)' + + @staticmethod + def _member_unpack(word: str, tn, bits: int) -> Optional[str]: + """Narrow a slot-bit slice back to a member value.""" + t = (tn.name or '').strip() if tn is not None else '' + mask = f'((1n << {bits}n) - 1n)' + if not t or (tn is not None and (tn.is_array or tn.is_mapping)): + return None + if t == 'bool': + return f'((({word}) & {mask}) !== 0n)' + if t.startswith('uint'): + return f'((({word}) & {mask}))' + if t.startswith('int'): + return f'BigInt.asIntN({t[3:] or "256"}, ({word}))' + if t == 'address' or not (t.startswith('bytes')): + return f'Contract.at((({word}) & {mask}))' + return None + + def _emit_slot_read(self, target) -> Optional[str]: + name, layout, member_types = target + terms = [] + for f in layout.fields: + packed = self._member_pack(f'{name}.{f.name}', member_types.get(f.name)) + if packed is None: + return None + terms.append(packed if f.bit_offset == 0 else f'({packed} << {f.bit_offset}n)') + if layout.has_free_bits: + # Bits no member declares still need somewhere to live; the shadow + # slot store keeps carrying exactly those. + terms.append( + f'(this._storageRead(this._getStorageKey({name} as any)) ' + f'& {hex(layout.free_mask)}n)' + ) + return '(' + ' | '.join(terms) + ')' if terms else '0n' + + def _emit_slot_write(self, target, value: str, prefix: str) -> Optional[str]: + name, layout, member_types = target + tmp = '__slotWord' + lines = [f'{prefix}const {tmp} = BigInt({value});'] + for f in layout.fields: + src = tmp if f.bit_offset == 0 else f'({tmp} >> {f.bit_offset}n)' + val = self._member_unpack(src, member_types.get(f.name), f.bit_width) + if val is None: + return None + lines.append(f'{prefix}{name}.{f.name} = {val};') + if layout.has_free_bits: + lines.append( + f'{prefix}this._storageWrite(this._getStorageKey({name} as any), ' + f'{tmp} & {hex(layout.free_mask)}n);' + ) + return '\n'.join(lines) + + def transpile(self, yul_code: str, slot_resolver=None) -> str: """ Transpile a Yul assembly block to TypeScript. @@ -636,6 +727,7 @@ def transpile(self, yul_code: str) -> str: """ self._warnings = [] self._unmodelable = False + self._slot_resolver = slot_resolver slot_vars: Dict[str, str] = {} try: @@ -897,6 +989,11 @@ def _generate_call_statement( if func == 'sstore' and len(call.arguments) >= 2: slot_expr = call.arguments[0] value = self._generate_expression(call.arguments[1], slot_vars) + target = self._slot_target(slot_expr, slot_vars) + if target is not None: + emitted = self._emit_slot_write(target, value, prefix) + if emitted is not None: + return emitted if isinstance(slot_expr, YulIdentifier) and slot_expr.name in slot_vars: return f'{prefix}this._storageWrite({slot_vars[slot_expr.name]} as any, {value});' slot = self._generate_expression(slot_expr, slot_vars) @@ -997,6 +1094,11 @@ def _generate_function_call( if func == 'sload': if args: slot_expr = args[0] + target = self._slot_target(slot_expr, slot_vars) + if target is not None: + emitted = self._emit_slot_read(target) + if emitted is not None: + return emitted if isinstance(slot_expr, YulIdentifier) and slot_expr.name in slot_vars: return f'this._storageRead({slot_vars[slot_expr.name]} as any)' slot = self._generate_expression(slot_expr, slot_vars) diff --git a/transpiler/codegen_rs/definition.py b/transpiler/codegen_rs/definition.py index bfed6235..199910ef 100644 --- a/transpiler/codegen_rs/definition.py +++ b/transpiler/codegen_rs/definition.py @@ -16,6 +16,7 @@ from typing import List, Optional, TYPE_CHECKING from ..parser.ast_nodes import EnumDefinition, StateVariableDeclaration, StructDefinition +from ..type_system.slots import SLOT0_FREE_FIELD from .rust_types import rust_ident from .soltypes import SolType @@ -94,11 +95,22 @@ def generate_struct(self, struct: StructDefinition) -> str: if not has_mapping: derives.append('PartialEq') + layout = self._symbols.slot_layouts.get(struct.name) + overlay = layout if layout is not None and layout.has_free_bits else None + lines = [] lines.append(f'#[derive({", ".join(derives)})]') lines.append(f'pub struct {name} {{') for fname, ftype in fields: lines.append(f' pub {rust_ident(fname)}: {self._types.rust_type(ftype)},') + if overlay is not None: + lines.append( + ' /// Slot-0 bits no member declares. Yul reaches the raw slot, so this' + ) + lines.append( + ' /// carries what the typed fields cannot represent.' + ) + lines.append(f' pub {SLOT0_FREE_FIELD}: U256,') lines.append('}') lines.append('') lines.append(f'impl Default for {name} {{') @@ -106,6 +118,8 @@ def generate_struct(self, struct: StructDefinition) -> str: lines.append(' Self {') for fname, ftype in fields: lines.append(f' {rust_ident(fname)}: {self._types.default_value(ftype)},') + if overlay is not None: + lines.append(f' {SLOT0_FREE_FIELD}: U256::ZERO,') lines.append(' }') lines.append(' }') lines.append('}') diff --git a/transpiler/codegen_rs/expression.py b/transpiler/codegen_rs/expression.py index c695c180..6cc402db 100644 --- a/transpiler/codegen_rs/expression.py +++ b/transpiler/codegen_rs/expression.py @@ -43,6 +43,7 @@ ADDRESS, BOOL, BYTES, BYTES32, INTLIT, STRING, UINT256, UNKNOWN, SolType, TypeInferencer, common_type, parse_elementary, uint, ) +from ..type_system.slots import SLOT0_FREE_FIELD from .rust_types import RustTypeConverter, rust_ident if TYPE_CHECKING: @@ -753,6 +754,11 @@ def field_value(arg: Expression, ftype: SolType) -> str: else: for (fname, ftype), arg in zip(fields, call.arguments): parts.append(f'{rust_ident(fname)}: {field_value(arg, ftype)}') + # Structs whose raw slot is touched by Yul carry a generated + # undeclared-bit member, which no Solidity initializer names. + layout = self._symbols.slot_layouts.get(name) + if layout is not None and layout.has_free_bits: + parts.append(f'{SLOT0_FREE_FIELD}: U256::ZERO') return f'{rust_ident(name)} {{ {", ".join(parts)} }}', t def _value_of(self, arg: Expression, ptype: SolType) -> str: @@ -870,6 +876,13 @@ def _emit_direct_call(self, fn_path: str, sig, arguments: List[Expression]) -> s arg_names.append('unimplemented!("unlowerable storage param")') continue if low[0] == '!selector': + # An argument that IS a selector already gets forwarded; + # re-wrapping it would move the selector into the new + # closure and poison every later use in this function. + fwd = self._forwarded_selector(arg) + if fwd is not None: + arg_names.append(fwd) + continue place = self._selector_place_of(arg, lines) tmp = self._ctx.fresh_temp('a') lines.append( @@ -927,6 +940,27 @@ def _emit_call_with_shared_base(self, fn_path: str, sig, arguments, base_name: s self._ctx.storage_locals = saved return f'{{ let {tmp} = &mut {base_place}; {fn_path}({args}) }}' + def _forwarded_selector(self, arg: Expression) -> Optional[str]: + """Pass an existing selector straight through, or None to build one. + + A selector-lowered parameter is already a `&dyn Fn`; a deferred local + is a `Box` that only needs reborrowing. + """ + if not isinstance(arg, Identifier): + return None + info = self._ctx.storage_locals.get(arg.name) + if info is None: + return None + root = info.get('root') + if not root: + return None + name = rust_ident(arg.name) + if root[0] == '!selector': + return f'{name}_sel' + if root[0] == '!deferred_sel': + return f'&*{name}_sel' + return None + def _selector_place_of(self, arg: Expression, hoists: List[str]) -> str: """Place text for a selector-lowered storage argument, valid inside a `move |world: &mut World|` closure: the closure param shadows `world` diff --git a/transpiler/codegen_rs/statement.py b/transpiler/codegen_rs/statement.py index dc8a19a6..5a470f5d 100644 --- a/transpiler/codegen_rs/statement.py +++ b/transpiler/codegen_rs/statement.py @@ -42,6 +42,7 @@ VariableDeclarationStatement, WhileStatement, ) +from ..type_system.slots import SLOT0_FREE_FIELD from .soltypes import BOOL, SolType, UNKNOWN, UINT256 from .rust_types import rust_ident @@ -742,6 +743,103 @@ def _gen_revert(self, stmt: RevertStatement) -> str: # Inline assembly: known-block registry (see _KNOWN_YUL_NOTE) # ------------------------------------------------------------------ + # ------------------------------------------------------------------ + # Whole-slot access (`sload(x.slot)` / `sstore(x.slot, v)`) + # ------------------------------------------------------------------ + + @staticmethod + def _u256_literal(value: int) -> str: + limbs = [(value >> (64 * i)) & 0xFFFFFFFFFFFFFFFF for i in range(4)] + return f'U256::from_limbs([{", ".join(f"{l}u64" for l in limbs)}])' + + def _slot_place(self, ident: str): + """(place code, slot-0 layout, member SolTypes) for a `.slot`.""" + place, t = self._expr.emit_typed(Identifier(name=ident)) + if t is None or t.kind != 'struct' or not t.name: + return None + layout = self._symbols.slot_layouts.get(t.name) + if layout is None: + return None + return place, layout, dict(self._symbols.structs.get(t.name, [])) + + def _member_to_word(self, code: str, t: SolType) -> Optional[str]: + """Widen a member to its slot bits. None when the type isn't modeled.""" + if t is None: + return None + if t.kind in ('address', 'interface', 'contract'): + return f'rt::address_to_u256({code})' + if t.kind == 'bool': + return f'U256::from({code} as u8)' + if t.kind == 'enum': + return f'U256::from({code} as u8)' + if t.kind == 'uint': + return f'U256::from({code})' + if t.kind == 'bytes_fixed' and t.bytes_n == 32: + return f'rt::b256_to_u256({code})' + return None + + def _member_from_word(self, src: str, t: SolType, bits: int) -> Optional[str]: + """Narrow a slot-bit slice back to a member. None when unmodeled.""" + if t is None: + return None + if t.kind in ('address', 'interface', 'contract'): + return f'rt::address_from_u256({src})' + if t.kind == 'bool': + return f'rt::mask_bits({src}, {bits}) != U256::ZERO' + if t.kind == 'enum': + from .definition import enum_from_u8_fn + return ( + f'{rust_ident(t.name)}::{enum_from_u8_fn()}' + f'(rt::mask_bits({src}, {bits}).wrapping_to::())' + ) + if t.kind == 'uint': + return f'rt::mask_bits({src}, {bits}).wrapping_to::<{self._types.rust_type(t)}>()' + if t.kind == 'bytes_fixed' and t.bytes_n == 32: + return f'rt::u256_to_b256({src})' + return None + + def _slot_read(self, lhs_ident: str, place_ident: str) -> Optional[str]: + info = self._slot_place(place_ident) + if info is None: + return None + place, layout, ftypes = info + p = self._ctx.fresh_temp('sl') + terms = [] + for f in layout.fields: + conv = self._member_to_word(f'{p}.{rust_ident(f.name)}', ftypes.get(f.name)) + if conv is None: + return None + terms.append(conv if f.bit_offset == 0 else f'({conv} << {f.bit_offset}usize)') + if layout.has_free_bits: + terms.append(f'{p}.{SLOT0_FREE_FIELD}') + lhs, _ = self._expr.emit_typed(Identifier(name=lhs_ident)) + packed = ' | '.join(terms) if terms else 'U256::ZERO' + return f'{self.ind()}{lhs} = {{ let {p} = &{place}; {packed} }};' + + def _slot_write(self, place_ident: str, value_ident: str) -> Optional[str]: + info = self._slot_place(place_ident) + if info is None: + return None + place, layout, ftypes = info + value, _ = self._expr.emit_typed(Identifier(name=value_ident)) + v = self._ctx.fresh_temp('sv') + p = self._ctx.fresh_temp('sp') + assigns = [] + for f in layout.fields: + src = v if f.bit_offset == 0 else f'({v} >> {f.bit_offset}usize)' + conv = self._member_from_word(src, ftypes.get(f.name), f.bit_width) + if conv is None: + return None + assigns.append(f'{p}.{rust_ident(f.name)} = {conv};') + if layout.has_free_bits: + assigns.append( + f'{p}.{SLOT0_FREE_FIELD} = {v} & {self._u256_literal(layout.free_mask)};' + ) + return ( + f'{self.ind()}{{ let {v} = {value}; let {p} = &mut {place}; ' + f'{" ".join(assigns)} }}' + ) + def _gen_assembly(self, stmt: AssemblyStatement) -> str: import re # The lexer re-joins Yul with spaces around every token @@ -763,6 +861,21 @@ def _gen_assembly(self, stmt: AssemblyStatement) -> str: f'{{ *{p} = crate::world::cleared_mon_state(); }} }}' ) + # Shape 4: whole-slot read/write on a struct place. The model stores + # each member separately, so the slot word is packed from (or split + # back into) those members plus the generated undeclared-bit overlay. + flat = re.sub(r'\s+', '', code) + m = re.fullmatch(r'(\w+):=sload\((\w+)\.slot\)', flat) + if m: + emitted = self._slot_read(m.group(1), m.group(2)) + if emitted is not None: + return emitted + m = re.fullmatch(r'sstore\((\w+)\.slot,(\w+)\)', flat) + if m: + emitted = self._slot_write(m.group(1), m.group(2)) + if emitted is not None: + return emitted + # Shape 3: batch event-payload packer (_packBatchPayload). if 'calldataload' in code and 'shl(104' in code: return ( diff --git a/transpiler/codegen_rs/symbols.py b/transpiler/codegen_rs/symbols.py index cb3d1c4a..ab4ad799 100644 --- a/transpiler/codegen_rs/symbols.py +++ b/transpiler/codegen_rs/symbols.py @@ -82,6 +82,11 @@ def __init__(self): self.structs: Dict[str, List[Tuple[str, SolType]]] = {} # struct name -> ordered raw TypeName list (for emission-time detail) self.struct_type_names: Dict[str, list] = {} + # struct name -> raw AST definition (slot-layout computation) + self.struct_defs: Dict[str, object] = {} + # struct name -> slot-0 SlotLayout, for structs whose raw slot is read + # or written by Yul. + self.slot_layouts: Dict[str, object] = {} # Keyed by (container, name): the same constant name can exist at file # scope AND inside contracts with different types (live example: # Constants.sol's uint256 SWITCH_PRIORITY vs FairCPU's uint32 one) — @@ -211,6 +216,7 @@ def build(cls, asts: Dict[str, SourceUnit]) -> 'RustSymbols': if existing is None or len(sig.param_types) > len(existing.param_types): sym.functions[key] = sig sym.function_defs[key] = func + sym._discover_slot_overlays(asts) return sym def lookup_overload(self, container: Optional[str], name: str, nargs: int): @@ -659,13 +665,13 @@ def _compute_param_lowering(self) -> None: if sig.needs_world and getattr(p, 'storage_location', '') == 'storage': if pt.kind == 'struct' and pt.name in roots: entry = roots[pt.name] - elif pt.kind == 'mapping': - # No single key addresses a nested mapping; lower - # to a SELECTOR closure that re-derives the place - # from world per use (funnel: world::sel). - entry = ('!selector', pt, None) else: - entry = ('!unsupported', pt.name if pt.name else pt.kind, UNKNOWN) + # No single key addresses this place — a nested + # mapping, or a struct reached through one (an + # array inside a mapped struct). Lower to a + # SELECTOR closure that re-derives the place from + # world per use (funnel: world::sel). + entry = ('!selector', pt, None) lowered.append(entry) while len(lowered) < len(sig.param_types): lowered.append(None) @@ -694,6 +700,18 @@ def _record_struct(self, struct, resolver) -> None: tns.append((member.name, member.type_name)) self.structs[struct.name] = fields self.struct_type_names[struct.name] = tns + self.struct_defs[struct.name] = struct + + def _discover_slot_overlays(self, asts) -> None: + """Record slot-0 layouts for structs whose raw slot is touched by Yul.""" + from ..type_system.slots import discover_slot_layouts + + self.slot_layouts = discover_slot_layouts( + asts.values(), + enum_names=set(self.enums), + struct_names=set(self.structs), + struct_defs=self.struct_defs, + ) def _record_constant(self, var, resolver, container) -> None: self.constants[(container, var.name)] = ConstSig( diff --git a/transpiler/sol2rs.py b/transpiler/sol2rs.py index 48123d2e..49d3483d 100644 --- a/transpiler/sol2rs.py +++ b/transpiler/sol2rs.py @@ -389,6 +389,9 @@ def _write_world_rs(self, engine_src: Path, symbols) -> None: lines.append(f' {f}: crate::Constants::CLEARED_MON_STATE_SENTINEL,') lines.append(' isKnockedOut: false,') lines.append(' shouldSkipTurn: false,') + # Structs whose raw slot is touched by Yul carry a generated + # undeclared-bit member; the sentinel leaves it zeroed. + lines.append(' ..Default::default()') lines.append(' }') lines.append('}') lines.append('') diff --git a/transpiler/strategies-rs/src/analysis.rs b/transpiler/strategies-rs/src/analysis.rs index 0c361050..3811b286 100644 --- a/transpiler/strategies-rs/src/analysis.rs +++ b/transpiler/strategies-rs/src/analysis.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use crate::arena::{build_doubles_specs, build_specs, build_specs_full, build_specs_with, STRAT_PAIRS}; use crate::doubles::run_doubles_games_instrumented; -use crate::game::{run_games_instrumented, InstrRecord, StrategyKind}; +use crate::game::{run_games_instrumented, InstrRecord, BotName}; use crate::roster::{self, Roster}; /// Per-mon accumulators, split by the mon's own team result that game. @@ -77,7 +77,7 @@ pub fn run_mon_analysis_with( wseed: u32, seed_base: u32, threads: usize, - pairs: &[(StrategyKind, StrategyKind)], + pairs: &[(BotName, BotName)], ) -> MonAnalysis { let book = roster::address_book(); let (specs, _pair_of) = build_specs_with(roster, games, wseed, seed_base, pairs); @@ -93,12 +93,12 @@ pub fn run_mon_analysis_rotated( wseed: u32, seed_base: u32, threads: usize, - pairs: Option<&[(StrategyKind, StrategyKind)]>, + pairs: Option<&[(BotName, BotName)]>, ) -> MonAnalysis { let book = roster::address_book(); - let default_pairs: Vec<(StrategyKind, StrategyKind)> = STRAT_PAIRS + let default_pairs: Vec<(BotName, BotName)> = STRAT_PAIRS .iter() - .map(|&(p1, p0)| (StrategyKind::parse(p1).unwrap(), StrategyKind::parse(p0).unwrap())) + .map(|&(p1, p0)| (p1, p0)) .collect(); let pairs = pairs.unwrap_or(&default_pairs); let (specs, _) = build_specs_full(roster, games, wseed, seed_base, pairs, true); diff --git a/transpiler/strategies-rs/src/arena.rs b/transpiler/strategies-rs/src/arena.rs index 7a3639da..09c592d4 100644 --- a/transpiler/strategies-rs/src/arena.rs +++ b/transpiler/strategies-rs/src/arena.rs @@ -4,8 +4,9 @@ use chomp_engine::Structs::Mon; use crate::doubles::{run_doubles_games, Difficulty, DoublesEvalW, DoublesSpec}; +use crate::bot::InfoMode; use crate::evaluator::{Weights, DEFAULT_WEIGHTS}; -use crate::game::{run_games, GameSpec, StrategyKind}; +use crate::game::{run_games, GameSpec, BotName}; use crate::roster::{self, Roster, RosterMon}; const TEAM_SIZE: usize = 4; @@ -54,6 +55,26 @@ pub fn build_team_mon(m: &RosterMon) -> Mon { Mon { stats: m.stats.clone(), ability: m.ability, moves } } +/// Stamp the information mode across a batch of specs. +/// +/// Rotation counts occurrences of each matchup rather than raw spec index: +/// the pair cycle and a plain `i % 2` share a factor, which would pin every +/// matchup to one peek seat forever instead of alternating. +pub fn apply_info_mode(specs: &mut [GameSpec], pair_of: &[usize], mode: InfoMode) { + let mut seen: std::collections::HashMap = std::collections::HashMap::new(); + for (i, spec) in specs.iter_mut().enumerate() { + spec.peek_seat = match mode { + InfoMode::Blind => None, + InfoMode::RotatePeek => { + let pair = pair_of.get(i).copied().unwrap_or(0); + let n = seen.entry(pair).or_insert(0); + *n += 1; + Some(((*n - 1) % 2) as u8) + } + }; + } +} + #[derive(Clone)] pub struct PairStats { pub p1_strat: &'static str, @@ -74,9 +95,9 @@ impl PairStats { /// Build every game's spec (STRAT_PAIRS rotation, two random team draws each), returning the specs /// parallel to their pair index. Exposed so trace tooling can reconstruct the exact same games. pub fn build_specs(roster: &Roster, games: usize, wseed: u32, seed_base: u32) -> (Vec, Vec) { - let pairs: Vec<(StrategyKind, StrategyKind)> = STRAT_PAIRS + let pairs: Vec<(BotName, BotName)> = STRAT_PAIRS .iter() - .map(|&(p1, p0)| (StrategyKind::parse(p1).unwrap(), StrategyKind::parse(p0).unwrap())) + .map(|&(p1, p0)| (p1, p0)) .collect(); build_specs_with(roster, games, wseed, seed_base, &pairs) } @@ -110,7 +131,7 @@ pub fn build_specs_with( games: usize, wseed: u32, seed_base: u32, - pairs: &[(StrategyKind, StrategyKind)], + pairs: &[(BotName, BotName)], ) -> (Vec, Vec) { build_specs_full(roster, games, wseed, seed_base, pairs, false) } @@ -126,7 +147,7 @@ pub fn build_specs_full( games: usize, wseed: u32, seed_base: u32, - pairs: &[(StrategyKind, StrategyKind)], + pairs: &[(BotName, BotName)], rotate: bool, ) -> (Vec, Vec) { // Build each mon's loadout set once (13 mons), then clone per draft — avoids an O(n) find @@ -163,6 +184,7 @@ pub fn build_specs_full( p1_ids, p0_strategy: p0s, p1_strategy: p1s, + peek_seat: None, p0_weights: DEFAULT_WEIGHTS, p1_weights: DEFAULT_WEIGHTS, p0_search_depth: 0, @@ -178,9 +200,17 @@ pub fn build_specs_full( } /// Play `games` matchups (STRAT_PAIRS rotation, two random team draws each) and tally per pair. -pub fn run_arena(roster: &Roster, games: usize, wseed: u32, seed_base: u32, threads: usize) -> Vec { +pub fn run_arena( + roster: &Roster, + games: usize, + wseed: u32, + seed_base: u32, + threads: usize, + info: InfoMode, +) -> Vec { let book = roster::address_book(); - let (specs, pair_of) = build_specs(roster, games, wseed, seed_base); + let (mut specs, pair_of) = build_specs(roster, games, wseed, seed_base); + apply_info_mode(&mut specs, &pair_of, info); let outcomes = run_games(&specs, &book, threads, false); let mut stats: Vec = STRAT_PAIRS @@ -213,8 +243,8 @@ pub fn eval_weights_winrate( cand: &Weights, p1_search_depth: u32, p1_search_peek: bool, - p1_strat: StrategyKind, - p0_strat: StrategyKind, + p1_strat: BotName, + p0_strat: BotName, games: usize, wseed: u32, seed_base: u32, @@ -380,13 +410,15 @@ pub fn run_doubles_arena(roster: &Roster, games: usize, wseed: u32, seed_base: u /// One side's doubles driver config: pilot difficulty (used at depth 0), search depth, eval weights. #[derive(Clone, Copy)] pub struct DoublesSideCfg { + /// Registered doubles bot; empty falls back to `difficulty`/`depth`. + pub bot: &'static str, pub difficulty: Difficulty, pub depth: u32, pub eval: DoublesEvalW, } impl Default for DoublesSideCfg { fn default() -> Self { - Self { difficulty: Difficulty::Hard, depth: 0, eval: DoublesEvalW::default() } + Self { bot: "", difficulty: Difficulty::Hard, depth: 0, eval: DoublesEvalW::default() } } } @@ -418,6 +450,8 @@ fn doubles_spec( p1_ids, p0_difficulty: p0.difficulty, p1_difficulty: p1.difficulty, + p0_bot: p0.bot, + p1_bot: p1.bot, p0_search_depth: p0.depth, p1_search_depth: p1.depth, p0_eval: p0.eval, @@ -526,7 +560,7 @@ mod tests { #[test] fn rotation_preserves_drafts_and_varies_loadouts() { let roster = load_roster(&chomp_root()); - let pairs = [(StrategyKind::Greedy, StrategyKind::Greedy)]; + let pairs = [(crate::bots::GREEDY, crate::bots::GREEDY)]; let (plain, _) = build_specs_full(&roster, 300, 0xbeefcafe, 10_000, &pairs, false); let (rot, _) = build_specs_full(&roster, 300, 0xbeefcafe, 10_000, &pairs, true); @@ -550,3 +584,110 @@ mod tests { assert!(variants(&rot) > variants(&plain), "rotated field should equip more than one loadout per mon"); } } + + +// --------------------------------------------------------------------------- +// Bench: score one bot against a ladder of opponents +// --------------------------------------------------------------------------- + +/// One bot's record against one opponent, aggregated over BOTH seat orders. +/// +/// Seats are not perfectly symmetric — mirror matchups sit a point or two off +/// 50% — so every row plays each draft from both sides and sums. That cancels +/// the seat term instead of leaving it in the reported number. +pub struct BenchRow { + pub opponent: &'static str, + pub games: u32, + pub wins: u32, + pub losses: u32, + pub draws: u32, +} + +impl BenchRow { + /// Win share over decisive games. + pub fn win_rate(&self) -> f64 { + let decisive = self.wins + self.losses; + if decisive == 0 { + 0.0 + } else { + self.wins as f64 / decisive as f64 + } + } + + /// Half-width of the 95% interval on `win_rate` (normal approximation). + pub fn ci95(&self) -> f64 { + let decisive = (self.wins + self.losses) as f64; + if decisive == 0.0 { + return 0.0; + } + let p = self.win_rate(); + 1.96 * (p * (1.0 - p) / decisive).sqrt() + } +} + +/// Score `cand` against every opponent in `ladder` (singles). +pub fn bench_singles( + roster: &Roster, + cand: &'static str, + ladder: &[&'static str], + games: usize, + wseed: u32, + seed_base: u32, + threads: usize, + info: InfoMode, +) -> Vec { + let book = roster::address_book(); + ladder + .iter() + .map(|&opponent| { + // Alternating entries put the candidate in each seat for half the drafts. + let pairs = [(cand, opponent), (opponent, cand)]; + let (mut specs, pair_of) = build_specs_with(roster, games, wseed, seed_base, &pairs); + apply_info_mode(&mut specs, &pair_of, info); + let outcomes = run_games(&specs, &book, threads, false); + + let mut row = + BenchRow { opponent, games: 0, wins: 0, losses: 0, draws: 0 }; + for (i, outcome) in outcomes.iter().enumerate() { + // pair 0 seats the candidate at p1, pair 1 at p0. + let cand_seat = if pair_of[i] == 0 { 1u8 } else { 0u8 }; + row.games += 1; + // An engine panic surfaces as Err; count it with the draws + // rather than silently crediting either side. + match outcome.as_ref().ok().and_then(|o| o.winner_seat) { + Some(s) if s == cand_seat => row.wins += 1, + Some(_) => row.losses += 1, + None => row.draws += 1, + } + } + row + }) + .collect() +} + +/// Score `cand` against every opponent in `ladder` (doubles). +pub fn bench_doubles( + roster: &Roster, + cand: &'static str, + ladder: &[&'static str], + games: usize, + wseed: u32, + seed_base: u32, + threads: usize, +) -> Vec { + ladder + .iter() + .map(|&opponent| { + let c = DoublesSideCfg { bot: cand, ..Default::default() }; + let b = DoublesSideCfg { bot: opponent, ..Default::default() }; + let r = doubles_ab_winrate(roster, c, b, games, wseed, seed_base, threads); + BenchRow { + opponent, + games: r.cand_wins + r.base_wins + r.draws, + wins: r.cand_wins, + losses: r.base_wins, + draws: r.draws, + } + }) + .collect() +} diff --git a/transpiler/strategies-rs/src/bin/analyze.rs b/transpiler/strategies-rs/src/bin/analyze.rs index 377c239e..c80c557d 100644 --- a/transpiler/strategies-rs/src/bin/analyze.rs +++ b/transpiler/strategies-rs/src/bin/analyze.rs @@ -8,7 +8,8 @@ use chomp_strategies::analysis::{ run_doubles_mon_analysis, run_mon_analysis, run_mon_analysis_rotated, run_mon_analysis_with, }; -use chomp_strategies::game::StrategyKind; +use chomp_strategies::bots; +use chomp_strategies::game::BotName; use chomp_strategies::roster::load_roster; use std::path::PathBuf; @@ -67,8 +68,8 @@ fn main() { match (&p1, &p0) { (Some(p1s), Some(p0s)) => { let pair = ( - StrategyKind::parse(p1s).expect("bad --p1 strategy"), - StrategyKind::parse(p0s).expect("bad --p0 strategy"), + bots::parse(p1s).expect("bad --p1 strategy"), + bots::parse(p0s).expect("bad --p0 strategy"), ); eprintln!(" pilots: p1={p1s} vs p0={p0s}"); if rotate { diff --git a/transpiler/strategies-rs/src/bin/arena.rs b/transpiler/strategies-rs/src/bin/arena.rs index a32d0415..f1ec0251 100644 --- a/transpiler/strategies-rs/src/bin/arena.rs +++ b/transpiler/strategies-rs/src/bin/arena.rs @@ -10,7 +10,9 @@ use chomp_strategies::arena::{ }; use chomp_strategies::doubles::DoublesEvalW; use chomp_strategies::evaluator::{Weights, DEFAULT_WEIGHTS, N_FEATURES}; -use chomp_strategies::game::StrategyKind; +use chomp_strategies::bot::InfoMode; +use chomp_strategies::bots; +use chomp_strategies::game::BotName; use chomp_strategies::roster::load_roster; use std::path::PathBuf; @@ -46,13 +48,17 @@ fn main() { let threads = arg_u(&args, "--threads", default_threads as u64) as usize; let mode = arg(&args, "--mode").unwrap_or_else(|| "singles".to_string()); + // Blind is the default: simultaneous moves, symmetric seats. + let info = arg(&args, "--info") + .map(|v| InfoMode::parse(&v).expect("--info: blind | rotate")) + .unwrap_or(InfoMode::Blind); let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") }); let roster = load_roster(&chomp_root); - eprintln!("arena[{mode}]: {} mons · {} games · {} threads · seed {:#x}", roster.mons.len(), games, threads, seed); + eprintln!("arena[{mode}/{}]: {} mons · {} games · {} threads · seed {:#x}", info.label(), roster.mons.len(), games, threads, seed); // A/B a candidate linear weight vector vs the frozen baseline. `--search-depth N` (≥1) makes p1 // play no-peek maximin search at depth N over the weights; 0 = 1-ply greedy over them. @@ -106,7 +112,7 @@ fn main() { if let Some((label, cand)) = cand { let started = std::time::Instant::now(); let wr = eval_weights_winrate( - &roster, &cand, search_depth, peek, StrategyKind::Greedy, StrategyKind::Greedy, games, seed, seed_base, threads, + &roster, &cand, search_depth, peek, bots::GREEDY, bots::GREEDY, games, seed, seed_base, threads, ); let elapsed = started.elapsed().as_secs_f64(); let m = if search_depth >= 1 { format!("d{search_depth}-search") } else { "1-ply".to_string() }; @@ -125,7 +131,7 @@ fn main() { ); } } else { - for s in &run_arena(&roster, games, seed, seed_base, threads) { + for s in &run_arena(&roster, games, seed, seed_base, threads, info) { println!( "{:>9} {:>9} {:>6} {:>6.1}% {:>4}-{:>3}-{:<4}", s.p1_strat, s.p0_strat, s.games, s.p1_rate() * 100.0, s.p1_wins, s.p0_wins, s.draws diff --git a/transpiler/strategies-rs/src/bin/bench.rs b/transpiler/strategies-rs/src/bin/bench.rs new file mode 100644 index 00000000..aaa918ca --- /dev/null +++ b/transpiler/strategies-rs/src/bin/bench.rs @@ -0,0 +1,100 @@ +//! Score a bot against the baseline ladder. +//! +//! cargo run --release -p chomp-strategies --bin bench -- --bot greedy +//! cargo run --release -p chomp-strategies --bin bench -- --bot mybot --mode doubles +//! +//! Every row plays each draft from both seats and sums, because the seats are +//! not perfectly symmetric; and every row prints its own n and 95% interval, +//! because a deep bot's row is necessarily thinner than a shallow one's. +//! +//! Data (drool/*.csv, src/mons/*.json) is read relative to CHOMP_ROOT. + +use chomp_strategies::arena::{bench_doubles, bench_singles, BenchRow}; +use chomp_strategies::bot::InfoMode; +use chomp_strategies::bots; +use chomp_strategies::roster::load_roster; +use std::path::PathBuf; + +/// The published singles ladder, weakest first. +const SINGLES_LADDER: &[&str] = + &[bots::RANDOM, bots::GREEDY, bots::HEURISTIC, bots::OVERRIDE, bots::NOPEEK]; + +/// The published doubles ladder, weakest first. +const DOUBLES_LADDER: &[&str] = + &[bots::DOUBLES_EASY, bots::DOUBLES_MEDIUM, bots::DOUBLES_HARD]; + +fn arg(args: &[String], flag: &str) -> Option { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned() +} + +fn arg_u(args: &[String], flag: &str, def: u64) -> u64 { + match arg(args, flag) { + Some(v) if v.starts_with("0x") => u64::from_str_radix(&v[2..], 16).unwrap_or(def), + Some(v) => v.parse().unwrap_or(def), + None => def, + } +} + +fn print_rows(bot: &str, rows: &[BenchRow], elapsed: f64) { + println!("\n{:>14} {:>6} {:>8} {:>13} {:>12}", "opponent", "games", "win%", "95% CI", "w-l-draw"); + for r in rows { + println!( + "{:>14} {:>6} {:>7.1}% ±{:>11.1}% {:>4}-{:>3}-{:<4}", + r.opponent, + r.games, + r.win_rate() * 100.0, + r.ci95() * 100.0, + r.wins, + r.losses, + r.draws + ); + } + let beat: Vec<&str> = + rows.iter().filter(|r| r.win_rate() - r.ci95() > 0.5).map(|r| r.opponent).collect(); + println!(); + if beat.is_empty() { + println!("{bot} does not clear 50% against any ladder bot (beyond its interval)."); + } else { + println!("{bot} beats: {}", beat.join(", ")); + } + eprintln!("\n{elapsed:.2}s"); +} + +fn main() { + let args: Vec = std::env::args().collect(); + let bot = arg(&args, "--bot").unwrap_or_else(|| { + eprintln!("usage: bench --bot [--mode singles|doubles] [--games N] [--info blind|rotate]"); + eprintln!("registered singles bots: {}", bots::NAMES.join(", ")); + eprintln!("registered doubles bots: {}", bots::DOUBLES_NAMES.join(", ")); + std::process::exit(2); + }); + let bot = bots::parse(&bot).unwrap_or_else(|| { + eprintln!("unknown bot {bot:?}; registered: {}", bots::NAMES.join(", ")); + std::process::exit(2); + }); + + let mode = arg(&args, "--mode").unwrap_or_else(|| "singles".to_string()); + let games = arg_u(&args, "--games", 2000) as usize; + let seed = arg_u(&args, "--seed", 0xbeefcafe) as u32; + let seed_base = arg_u(&args, "--seed-base", 10_000) as u32; + let default_threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4); + let threads = arg_u(&args, "--threads", default_threads as u64) as usize; + let info = arg(&args, "--info") + .map(|v| InfoMode::parse(&v).expect("--info: blind | rotate")) + .unwrap_or(InfoMode::Blind); + + let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }); + let roster = load_roster(&chomp_root); + + let started = std::time::Instant::now(); + let rows = if mode == "doubles" { + eprintln!("bench[doubles]: {bot} · {games} games/opponent · seed {seed:#x}"); + bench_doubles(&roster, bot, DOUBLES_LADDER, games, seed, seed_base, threads) + } else { + eprintln!("bench[singles/{}]: {bot} · {games} games/opponent · seed {seed:#x}", info.label()); + bench_singles(&roster, bot, SINGLES_LADDER, games, seed, seed_base, threads, info) + }; + print_rows(bot, &rows, started.elapsed().as_secs_f64()); +} diff --git a/transpiler/strategies-rs/src/bin/cert.rs b/transpiler/strategies-rs/src/bin/cert.rs index 7fb0746e..dc594a47 100644 --- a/transpiler/strategies-rs/src/bin/cert.rs +++ b/transpiler/strategies-rs/src/bin/cert.rs @@ -7,7 +7,8 @@ use chomp_strategies::analysis::run_mon_analysis; use chomp_strategies::arena::build_team_mon; use chomp_strategies::evaluator::DEFAULT_WEIGHTS; -use chomp_strategies::game::{play_game, GameSpec, StrategyKind}; +use chomp_strategies::bots; +use chomp_strategies::game::{play_game, GameSpec, BotName}; use chomp_strategies::matrix::compute_static_matrix; use chomp_strategies::roster::{self, load_roster}; use std::path::PathBuf; @@ -49,8 +50,9 @@ fn main() { p1_team: vec![build_team_mon(&roster.mons[i])], p0_ids: vec![m.ids[j]], p1_ids: vec![m.ids[i]], - p0_strategy: StrategyKind::Greedy, - p1_strategy: StrategyKind::Greedy, + p0_strategy: bots::GREEDY, + p1_strategy: bots::GREEDY, + peek_seat: None, p0_weights: DEFAULT_WEIGHTS, p1_weights: DEFAULT_WEIGHTS, p0_search_depth: 0, diff --git a/transpiler/strategies-rs/src/bin/findpanic.rs b/transpiler/strategies-rs/src/bin/findpanic.rs index c46a2169..412eac53 100644 --- a/transpiler/strategies-rs/src/bin/findpanic.rs +++ b/transpiler/strategies-rs/src/bin/findpanic.rs @@ -2,7 +2,8 @@ //! cargo run --release -p chomp-strategies --bin findpanic -- --games 20000 use chomp_strategies::arena::{build_specs, build_specs_full, STRAT_PAIRS}; -use chomp_strategies::game::{run_games_instrumented, StrategyKind}; +use chomp_strategies::bots; +use chomp_strategies::game::{run_games_instrumented, BotName}; use chomp_strategies::roster::{self, load_roster}; use std::path::PathBuf; @@ -30,9 +31,9 @@ fn main() { let roster = load_roster(&chomp_root); let book = roster::address_book(); - let pairs: Vec<(StrategyKind, StrategyKind)> = STRAT_PAIRS + let pairs: Vec<(BotName, BotName)> = STRAT_PAIRS .iter() - .map(|&(p1, p0)| (StrategyKind::parse(p1).unwrap(), StrategyKind::parse(p0).unwrap())) + .map(|&(p1, p0)| (p1, p0)) .collect(); let (specs, _) = if rotate { build_specs_full(&roster, games, seed, seed_base, &pairs, true) diff --git a/transpiler/strategies-rs/src/bin/mock2.rs b/transpiler/strategies-rs/src/bin/mock2.rs index a9fccd74..6472981a 100644 --- a/transpiler/strategies-rs/src/bin/mock2.rs +++ b/transpiler/strategies-rs/src/bin/mock2.rs @@ -1,7 +1,8 @@ //! T2 mock-move A/B — loop-driven mock moves (conditional power from live state), no engine edits. //! cargo run --release -p chomp-strategies --bin mock2 -- --games 10000 -use chomp_strategies::game::StrategyKind; +use chomp_strategies::bots; +use chomp_strategies::game::BotName; use chomp_strategies::mock2::{batch1, run_mock_ab, run_mock_ab_with}; use chomp_strategies::roster::load_roster; use std::path::PathBuf; @@ -47,7 +48,7 @@ fn main() { } let replaced = roster.move_name(roster.mons.iter().find(|x| x.name == m.mon).unwrap().id, m.lane as u8); let (base, mocked) = if ceiling { - run_mock_ab_with(&roster, m, games, seed, seed_base, threads, &[(StrategyKind::NoPeekExpect, StrategyKind::NoPeekExpect)]) + run_mock_ab_with(&roster, m, games, seed, seed_base, threads, &[(bots::NOPEEK, bots::NOPEEK)]) } else { run_mock_ab(&roster, m, games, seed, seed_base, threads) }; diff --git a/transpiler/strategies-rs/src/bin/statpeak.rs b/transpiler/strategies-rs/src/bin/statpeak.rs index ad913cd0..51e348f2 100644 --- a/transpiler/strategies-rs/src/bin/statpeak.rs +++ b/transpiler/strategies-rs/src/bin/statpeak.rs @@ -3,7 +3,8 @@ //! cargo run --release -p chomp-strategies --bin statpeak -- --games 20000 use chomp_strategies::arena::{build_specs, build_specs_full, STRAT_PAIRS}; -use chomp_strategies::game::{run_games_instrumented, StrategyKind}; +use chomp_strategies::bots; +use chomp_strategies::game::{run_games_instrumented, BotName}; use chomp_strategies::roster::{self, load_roster}; use std::path::PathBuf; @@ -30,9 +31,9 @@ fn main() { }); let roster = load_roster(&chomp_root); let book = roster::address_book(); - let pairs: Vec<(StrategyKind, StrategyKind)> = STRAT_PAIRS + let pairs: Vec<(BotName, BotName)> = STRAT_PAIRS .iter() - .map(|&(p1, p0)| (StrategyKind::parse(p1).unwrap(), StrategyKind::parse(p0).unwrap())) + .map(|&(p1, p0)| (p1, p0)) .collect(); let (specs, _) = if rotate { build_specs_full(&roster, games, seed, seed_base, &pairs, true) diff --git a/transpiler/strategies-rs/src/bin/tracesearch.rs b/transpiler/strategies-rs/src/bin/tracesearch.rs index a8af0894..0de48d49 100644 --- a/transpiler/strategies-rs/src/bin/tracesearch.rs +++ b/transpiler/strategies-rs/src/bin/tracesearch.rs @@ -6,7 +6,8 @@ use chomp_strategies::arena::{build_team_mon, draw_team, Wrand}; use chomp_strategies::evaluator::{Weights, DEFAULT_WEIGHTS, N_FEATURES}; -use chomp_strategies::game::{narrate_game, run_games_traced, GameSpec, StrategyKind}; +use chomp_strategies::bots; +use chomp_strategies::game::{narrate_game, run_games_traced, GameSpec, BotName}; use chomp_strategies::roster::{self, load_roster}; use chomp_strategies::view::{NO_OP_INDEX, SWITCH_MOVE_INDEX}; use std::path::PathBuf; @@ -61,7 +62,7 @@ fn main() { let seed_base = arg_u(&args, "--seed-base", 10_000) as u32; let narrate_n = arg_u(&args, "--narrate", 0) as usize; let p0_name = arg(&args, "--p0").unwrap_or_else(|| "greedy".to_string()); - let p0_strat = StrategyKind::parse(&p0_name).expect("--p0: greedy / heuristic / override / ..."); + let p0_strat = bots::parse(&p0_name).expect("--p0: greedy / heuristic / override / ..."); let peek = args.iter().any(|a| a == "--peek"); let mixed = args.iter().any(|a| a == "--mixed"); let weights = weights_arg(&args); @@ -88,7 +89,8 @@ fn main() { p0_ids, p1_ids, p0_strategy: p0_strat, - p1_strategy: StrategyKind::Greedy, // ignored — p1_search_depth overrides to the search + p1_strategy: bots::GREEDY, // ignored — p1_search_depth overrides to the search + peek_seat: None, p0_weights: DEFAULT_WEIGHTS, p1_weights: weights, p0_search_depth: 0, diff --git a/transpiler/strategies-rs/src/bot.rs b/transpiler/strategies-rs/src/bot.rs new file mode 100644 index 00000000..931acf67 --- /dev/null +++ b/transpiler/strategies-rs/src/bot.rs @@ -0,0 +1,130 @@ +//! The bot interface — what a benchmark entrant implements. +//! +//! A bot is handed the live battle and decides one turn. It may fork the +//! engine through `ctx.sim` and play hypotheticals forward: the forward model +//! is the real transpiled engine, not an approximation of it. +//! +//! Two rules matter for comparable results: +//! +//! - **Draw only from `ctx.rng`.** It is this seat's private stream, so how +//! much a bot draws cannot shift the battle or its opponent. +//! - **Read the opponent's move only through `ctx.peek`.** It is `None` in +//! blind mode, so a bot cannot accidentally depend on seeing a reveal. + +use crate::evaluator::Weights; +use crate::jsrng::JsRng; +use crate::sim::Sim; +use crate::view::{BattleView, Mv, Seat}; + +/// How much a seat is told about the opponent's move. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InfoMode { + /// Genuinely simultaneous: neither seat sees the other's move. Matches + /// production play, and keeps the seats symmetric. + Blind, + /// One seat sees the reveal, alternating per game so that over a batch + /// each seat peeks equally often. + RotatePeek, +} + +impl InfoMode { + pub fn parse(name: &str) -> Option { + match name { + "blind" => Some(InfoMode::Blind), + "rotate" | "peek" => Some(InfoMode::RotatePeek), + _ => None, + } + } + + pub fn label(self) -> &'static str { + match self { + InfoMode::Blind => "blind", + InfoMode::RotatePeek => "rotate-peek", + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BattleMode { + Singles, + Doubles, +} + +/// One turn's submission: a single move, or one per active slot in doubles. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Action { + Single(Mv), + Slots([Mv; 2]), +} + +impl Action { + /// The singles move, taking slot 0 if a doubles bot answered. + pub fn single(self) -> Mv { + match self { + Action::Single(mv) => mv, + Action::Slots([mv, _]) => mv, + } + } +} + +/// Everything a bot may look at when deciding. +pub struct DecisionCtx<'a> { + /// The live battle. Fork it to evaluate hypotheticals. + pub sim: &'a mut Sim, + /// Which physical side this bot pilots. + pub seat: Seat, + /// Which mode this game is. + pub mode: BattleMode, + /// Singles position snapshot. `None` in doubles, where a single active pair + /// cannot describe the board — doubles bots read `sim` directly. + pub view: Option<&'a BattleView>, + /// The opponent's move this turn — `None` in blind mode. + pub peek: Option, + /// This seat's private stream. + pub rng: &'a mut JsRng, +} + +impl<'a> DecisionCtx<'a> { + /// The singles snapshot. Panics in doubles, where it does not exist. + pub fn singles_view(&self) -> &'a BattleView { + self.view.expect("singles_view() called in doubles; read ctx.sim instead") + } +} + +/// Tuning a built-in bot reads at construction. Entrants are free to ignore it. +#[derive(Clone, Copy, Debug)] +pub struct BotConfig { + pub weights: Weights, + /// Eval weights for the doubles search tier; ignored by every other bot. + pub doubles_eval: crate::doubles::DoublesEvalW, + pub search_depth: u32, + pub search_peek: bool, + pub search_mixed: bool, +} + +impl Default for BotConfig { + fn default() -> Self { + BotConfig { + weights: crate::evaluator::DEFAULT_WEIGHTS, + doubles_eval: crate::doubles::DoublesEvalW::default(), + search_depth: 0, + search_peek: false, + search_mixed: false, + } + } +} + +pub trait Bot: Send { + /// Registry name; also the label in result tables. + fn name(&self) -> &'static str; + + /// Modes this bot can pilot. Tables skip it for the rest. + fn modes(&self) -> &'static [BattleMode] { + &[BattleMode::Singles] + } + + /// Clear per-game state. Called once before each game. + fn reset(&mut self) {} + + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action; +} diff --git a/transpiler/strategies-rs/src/bots.rs b/transpiler/strategies-rs/src/bots.rs new file mode 100644 index 00000000..d7df71bc --- /dev/null +++ b/transpiler/strategies-rs/src/bots.rs @@ -0,0 +1,369 @@ +//! The built-in bots and the registry that names them. +//! +//! Each one wraps a decision module; the modules keep the logic, the bots own +//! the per-game state. Adding an entrant means adding a struct here and one +//! line to [`build`]. + +use crate::bot::{Action, BattleMode, Bot, BotConfig, DecisionCtx}; +use crate::evaluator::Weights; +use crate::greedy; +use crate::heuristic::{self, HeuristicState}; +use crate::nopeek; +use crate::override_cpu::{self, OverrideState}; +use crate::search; +use crate::doubles::{self, Difficulty, DoublesEvalW, SlotMove}; +use crate::view::{calculate_valid_moves, pick_uniform, Mv, NO_OP_INDEX}; + +pub const RANDOM: &str = "random"; +pub const GREEDY: &str = "greedy"; +pub const HEURISTIC: &str = "heuristic"; +pub const OVERRIDE: &str = "override"; +pub const NOPEEK: &str = "nopeek"; +pub const NOPEEK_WC: &str = "nopeek-wc"; +pub const SEARCH: &str = "search"; +pub const DOUBLES_EASY: &str = "doubles-easy"; +pub const DOUBLES_MEDIUM: &str = "doubles-medium"; +pub const DOUBLES_HARD: &str = "doubles-hard"; +pub const DOUBLES_SEARCH: &str = "doubles-search"; + +/// Every registered bot, in ladder order (weakest first). +pub const NAMES: &[&str] = &[RANDOM, GREEDY, HEURISTIC, OVERRIDE, NOPEEK, NOPEEK_WC, SEARCH]; + +/// Registered doubles bots, weakest first. +pub const DOUBLES_NAMES: &[&str] = + &[DOUBLES_EASY, DOUBLES_MEDIUM, DOUBLES_HARD, DOUBLES_SEARCH]; + +const DOUBLES_ONLY: &[BattleMode] = &[BattleMode::Doubles]; + +fn slot_to_mv(m: SlotMove) -> Mv { + Mv { move_index: m.move_index, extra_data: m.extra_data } +} + +/// Epsilon-greedy per slot; the difficulty is how often it takes the best option. +pub struct DoublesGreedyBot { + name: &'static str, + difficulty: Difficulty, +} + +impl Bot for DoublesGreedyBot { + fn name(&self) -> &'static str { + self.name + } + fn modes(&self) -> &'static [BattleMode] { + DOUBLES_ONLY + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let bk = ctx.sim.battle_key; + let (a, b) = doubles::pick_side_moves(ctx.sim, bk, ctx.seat.cpu, self.difficulty, ctx.rng); + Action::Slots([slot_to_mv(a), slot_to_mv(b)]) + } +} + +/// Joint maximin search over both of the side's slots. +pub struct DoublesSearchBot { + depth: u32, + eval: DoublesEvalW, +} + +impl Bot for DoublesSearchBot { + fn name(&self) -> &'static str { + DOUBLES_SEARCH + } + fn modes(&self) -> &'static [BattleMode] { + DOUBLES_ONLY + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let bk = ctx.sim.battle_key; + let (a, b) = doubles::search_side_moves(ctx.sim, bk, ctx.seat.cpu, self.depth, &self.eval); + Action::Slots([slot_to_mv(a), slot_to_mv(b)]) + } +} + +/// The move a bot with nothing legal to do submits. +fn no_op() -> Mv { + Mv { move_index: NO_OP_INDEX, extra_data: 0 } +} + +/// Uniform over the legal actions — the floor a real entrant must clear. +pub struct RandomBot; + +impl Bot for RandomBot { + fn name(&self) -> &'static str { + RANDOM + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let valid = calculate_valid_moves(ctx.sim, ctx.seat, ctx.singles_view().bk, ctx.rng); + let all: Vec = valid + .moves + .iter() + .chain(valid.switches.iter()) + .chain(valid.no_op.iter()) + .copied() + .collect(); + let pick = pick_uniform(all.len(), ctx.rng).map(|i| all[i]).unwrap_or_else(no_op); + Action::Single(pick) + } +} + +/// 1-ply best response on the forward model + linear evaluator. +pub struct GreedyBot { + weights: Weights, +} + +impl Bot for GreedyBot { + fn name(&self) -> &'static str { + GREEDY + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let pm = ctx.peek.unwrap_or_else(|| Mv { move_index: 0, extra_data: 0 }); + Action::Single(greedy::decide(ctx.sim, ctx.seat, ctx.singles_view(), pm, ctx.rng, &self.weights)) + } +} + +/// The on-chain heuristic ladder. +pub struct HeuristicBot { + weights: Weights, + state: HeuristicState, +} + +impl Bot for HeuristicBot { + fn name(&self) -> &'static str { + HEURISTIC + } + fn reset(&mut self) { + self.state = HeuristicState::default(); + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let pm = ctx.peek.unwrap_or_else(|| Mv { move_index: 0, extra_data: 0 }); + Action::Single(heuristic::decide( + ctx.sim, + ctx.seat, + ctx.singles_view(), + pm, + ctx.rng, + &mut self.state, + &self.weights, + )) + } +} + +/// Scripted per-mon overrides, falling back to the heuristic. +pub struct OverrideBot { + weights: Weights, + state: OverrideState, +} + +impl Bot for OverrideBot { + fn name(&self) -> &'static str { + OVERRIDE + } + fn reset(&mut self) { + self.state = OverrideState::default(); + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let pm = ctx.peek.unwrap_or_else(|| Mv { move_index: 0, extra_data: 0 }); + Action::Single(override_cpu::decide( + ctx.sim, + ctx.seat, + ctx.singles_view(), + pm, + ctx.rng, + &mut self.state, + &self.weights, + )) + } +} + +/// Greedy that never reads the reveal, aggregating over the opponent's replies. +pub struct NoPeekBot { + weights: Weights, + worst_case: bool, +} + +impl Bot for NoPeekBot { + fn name(&self) -> &'static str { + if self.worst_case { + NOPEEK_WC + } else { + NOPEEK + } + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let mv = if self.worst_case { + nopeek::decide_worst(ctx.sim, ctx.seat, ctx.singles_view(), ctx.rng, &self.weights) + } else { + nopeek::decide_expect(ctx.sim, ctx.seat, ctx.singles_view(), ctx.rng, &self.weights) + }; + Action::Single(mv) + } +} + +/// Simultaneous-move maximin search over the evaluator. +pub struct SearchBot { + weights: Weights, + depth: u32, + peek: bool, + mixed: bool, +} + +impl Bot for SearchBot { + fn name(&self) -> &'static str { + SEARCH + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let pm = ctx.peek.unwrap_or_else(|| Mv { move_index: 0, extra_data: 0 }); + Action::Single(search::decide( + ctx.sim, + ctx.seat, + ctx.singles_view(), + pm, + &self.weights, + self.depth, + self.peek, + self.mixed, + ctx.rng, + )) + } +} + +/// Build a registered bot, or None if the name is unknown. +/// +/// A configured search depth outranks the name: the seat searches over that +/// bot's evaluator instead of playing it. That is the pre-trait behaviour and +/// the arena's weight/depth sweeps depend on it. +pub fn build(name: &str, cfg: BotConfig) -> Option> { + if cfg.search_depth >= 1 && name != RANDOM && !DOUBLES_NAMES.contains(&name) { + return Some(Box::new(SearchBot { + weights: cfg.weights, + depth: cfg.search_depth, + peek: cfg.search_peek, + mixed: cfg.search_mixed, + })); + } + let w = cfg.weights; + match name { + RANDOM => Some(Box::new(RandomBot)), + GREEDY => Some(Box::new(GreedyBot { weights: w })), + HEURISTIC => Some(Box::new(HeuristicBot { weights: w, state: HeuristicState::default() })), + OVERRIDE => Some(Box::new(OverrideBot { weights: w, state: OverrideState::default() })), + NOPEEK => Some(Box::new(NoPeekBot { weights: w, worst_case: false })), + NOPEEK_WC => Some(Box::new(NoPeekBot { weights: w, worst_case: true })), + SEARCH => Some(Box::new(SearchBot { + weights: w, + depth: cfg.search_depth.max(1), + peek: cfg.search_peek, + mixed: cfg.search_mixed, + })), + DOUBLES_EASY => Some(Box::new(DoublesGreedyBot { name: DOUBLES_EASY, difficulty: Difficulty::Easy })), + DOUBLES_MEDIUM => { + Some(Box::new(DoublesGreedyBot { name: DOUBLES_MEDIUM, difficulty: Difficulty::Medium })) + } + DOUBLES_HARD => Some(Box::new(DoublesGreedyBot { name: DOUBLES_HARD, difficulty: Difficulty::Hard })), + DOUBLES_SEARCH => Some(Box::new(DoublesSearchBot { + depth: cfg.search_depth.max(1), + eval: cfg.doubles_eval, + })), + _ => None, + } +} + +/// Resolve a name (e.g. from a CLI flag) to its registry entry. +pub fn parse(name: &str) -> Option<&'static str> { + NAMES.iter().chain(DOUBLES_NAMES.iter()).copied().find(|&n| n == name) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::arena::build_team_mon; + use crate::game::{play_game, GameSpec}; + use crate::roster::{address_book, load_roster}; + use std::path::PathBuf; + + /// Every registered bot can pilot a full game. This is the smoke test an + /// entrant runs first: if a new bot is in `NAMES`, it plays here. + #[test] + fn every_registered_bot_plays() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join(".."); + let root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or(root); + let roster = load_roster(&root); + let book = address_book(); + let ids: Vec = roster.mons.iter().take(4).map(|m| m.id).collect(); + let team: Vec<_> = roster.mons.iter().take(4).map(build_team_mon).collect(); + + for &name in NAMES { + assert!(parse(name).is_some(), "{name} missing from the registry"); + let spec = GameSpec { + seed: 7, + max_turns: 60, + mons_per_team: 4, + p0_team: team.clone(), + p1_team: team.clone(), + p0_ids: ids.clone(), + p1_ids: ids.clone(), + p0_strategy: name, + p1_strategy: GREEDY, + peek_seat: None, + p0_weights: crate::evaluator::DEFAULT_WEIGHTS, + p1_weights: crate::evaluator::DEFAULT_WEIGHTS, + p0_search_depth: 0, + p1_search_depth: 0, + p0_search_peek: false, + p1_search_peek: false, + p0_search_mixed: false, + p1_search_mixed: false, + }; + let out = play_game(&spec, &book, false); + assert!(out.turns > 0, "{name} produced no turns"); + } + } + + /// Every registered doubles bot can pilot a full doubles game. + #[test] + fn every_registered_doubles_bot_plays() { + use crate::doubles::{play_doubles_game, DoublesEvalW, DoublesSpec, Difficulty}; + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join(".."); + let root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or(root); + let roster = load_roster(&root); + let book = address_book(); + let ids: Vec = roster.mons.iter().take(4).map(|m| m.id).collect(); + let team: Vec<_> = roster.mons.iter().take(4).map(build_team_mon).collect(); + + for &name in DOUBLES_NAMES { + assert!(parse(name).is_some(), "{name} missing from the registry"); + let spec = DoublesSpec { + seed: 11, + max_turns: 60, + mons_per_team: 4, + p0_team: team.clone(), + p1_team: team.clone(), + p0_ids: ids.clone(), + p1_ids: ids.clone(), + p0_difficulty: Difficulty::Hard, + p1_difficulty: Difficulty::Hard, + p0_bot: name, + p1_bot: DOUBLES_HARD, + p0_search_depth: 0, + p1_search_depth: 0, + p0_eval: DoublesEvalW::default(), + p1_eval: DoublesEvalW::default(), + }; + let out = play_doubles_game(&spec, &book); + assert!(out.turns > 0, "{name} produced no turns"); + } + } + + /// Salts depend on the seed alone, so swapping one seat's bot cannot change + /// the battle the other seat faces. This is what makes entrants comparable. + #[test] + fn seat_streams_are_independent() { + use crate::jsrng::{derive, random_salt, STREAM_SALT}; + for seed in [1u32, 42, 0xbeef] { + let mut a = derive(seed, STREAM_SALT); + let mut b = derive(seed, STREAM_SALT); + for _ in 0..8 { + assert_eq!(random_salt(&mut a), random_salt(&mut b)); + } + } + } +} diff --git a/transpiler/strategies-rs/src/breadth.rs b/transpiler/strategies-rs/src/breadth.rs index 14a4cd74..90c6f0d6 100644 --- a/transpiler/strategies-rs/src/breadth.rs +++ b/transpiler/strategies-rs/src/breadth.rs @@ -7,7 +7,8 @@ use std::collections::HashMap; use crate::arena::build_specs_with; -use crate::game::{run_games_breadth, StrategyKind}; +use crate::bots; +use crate::game::{run_games_breadth, BotName}; use crate::roster::{self, Roster}; /// Top two within this many eval-score units = "within noise". @@ -52,7 +53,7 @@ pub struct BreadthAnalysis { pub fn run_breadth_analysis(roster: &Roster, games: usize, wseed: u32, seed_base: u32, threads: usize) -> BreadthAnalysis { let book = roster::address_book(); - let pairs = [(StrategyKind::Greedy, StrategyKind::Greedy)]; + let pairs = [(bots::GREEDY, bots::GREEDY)]; let (specs, _) = build_specs_with(roster, games, wseed, seed_base, &pairs); let results = run_games_breadth(&specs, &book, threads); diff --git a/transpiler/strategies-rs/src/doubles.rs b/transpiler/strategies-rs/src/doubles.rs index c2bb8ac8..b052710e 100644 --- a/transpiler/strategies-rs/src/doubles.rs +++ b/transpiler/strategies-rs/src/doubles.rs @@ -20,7 +20,9 @@ use chomp_engine::Engine; use chomp_engine::Structs::{Mon, MoveMeta}; use chomp_rt::{Address, B256, U256}; -use crate::jsrng::{random_salt, JsRng}; +use crate::bot::{Action, BattleMode, Bot, BotConfig, DecisionCtx}; +use crate::bots; +use crate::jsrng::{derive, random_salt, JsRng, STREAM_P0, STREAM_P1, STREAM_SALT}; use crate::roster::{input_type_of, target_spec_of, InputType, TargetSpec}; use crate::shared::{build_damage_calc_context, estimate_damage_meta}; use crate::sim::{pack_side, Sim}; @@ -722,6 +724,10 @@ pub struct DoublesSpec { pub p1_ids: Vec, pub p0_difficulty: Difficulty, pub p1_difficulty: Difficulty, + /// Registered bot per side. Empty falls back to the difficulty/search + /// fields above, which is how the existing sweeps configure a side. + pub p0_bot: &'static str, + pub p1_bot: &'static str, /// Per-side search depth: 0 = epsilon-greedy at the difficulty; ≥1 = joint maximin search. pub p0_search_depth: u32, pub p1_search_depth: u32, @@ -736,10 +742,57 @@ pub struct DoublesOutcome { pub turns: u32, } +/// The registered bot a side plays, or the one its difficulty/search config names. +fn doubles_bot(spec: &DoublesSpec, side: usize) -> Box { + let (name, depth, diff, eval) = if side == 0 { + (spec.p0_bot, spec.p0_search_depth, spec.p0_difficulty, spec.p0_eval) + } else { + (spec.p1_bot, spec.p1_search_depth, spec.p1_difficulty, spec.p1_eval) + }; + let name = if !name.is_empty() { + name + } else if depth > 0 { + bots::DOUBLES_SEARCH + } else { + match diff { + Difficulty::Easy => bots::DOUBLES_EASY, + Difficulty::Medium => bots::DOUBLES_MEDIUM, + Difficulty::Hard => bots::DOUBLES_HARD, + } + }; + let cfg = BotConfig { search_depth: depth, doubles_eval: eval, ..BotConfig::default() }; + bots::build(name, cfg).unwrap_or_else(|| panic!("unknown doubles bot {name:?}")) +} + +/// One side's two slot moves for this turn. +fn decide_side(sim: &mut Sim, bot: &mut dyn Bot, side: u8, rng: &mut JsRng) -> [SlotMove; 2] { + let mut ctx = DecisionCtx { + sim, + seat: Seat { cpu: side }, + mode: BattleMode::Doubles, + view: None, + peek: None, + rng, + }; + match bot.decide(&mut ctx) { + Action::Slots([a, b]) => [ + SlotMove { move_index: a.move_index, extra_data: a.extra_data }, + SlotMove { move_index: b.move_index, extra_data: b.extra_data }, + ], + Action::Single(a) => { + [SlotMove { move_index: a.move_index, extra_data: a.extra_data }, NO_OP_MOVE] + } + } +} + /// Play one doubles game: each turn both sides pick their two slot moves (greedy CPU at the spec's /// difficulty), the moves are packed per side, and the turn executes via `execute_slot_turn`. pub fn play_doubles_game(spec: &DoublesSpec, book: &HashMap) -> DoublesOutcome { - let mut rng = JsRng::new(spec.seed); + let (mut rng0, mut rng1, mut salt_rng) = ( + derive(spec.seed, STREAM_P0), + derive(spec.seed, STREAM_P1), + derive(spec.seed, STREAM_SALT), + ); let mut sim = Sim::new_doubles( spec.mons_per_team, spec.p0_team.clone(), @@ -748,25 +801,17 @@ pub fn play_doubles_game(spec: &DoublesSpec, book: &HashMap) -> spec.p1_ids.clone(), book, ); + let mut bots_pair = [doubles_bot(spec, 0), doubles_bot(spec, 1)]; for t in 0..spec.max_turns { let w = sim.winner_index(); if w != 2 { return DoublesOutcome { winner_side: Some(w), turns: t }; } - let bk = sim.battle_key; - let (p0a, p0b) = if spec.p0_search_depth > 0 { - search_side_moves(&mut sim, bk, 0, spec.p0_search_depth, &spec.p0_eval) - } else { - pick_side_moves(&mut sim, bk, 0, spec.p0_difficulty, &mut rng) - }; - let (p1a, p1b) = if spec.p1_search_depth > 0 { - search_side_moves(&mut sim, bk, 1, spec.p1_search_depth, &spec.p1_eval) - } else { - pick_side_moves(&mut sim, bk, 1, spec.p1_difficulty, &mut rng) - }; - let salt0 = random_salt(&mut rng); - let salt1 = random_salt(&mut rng); + let [p0a, p0b] = decide_side(&mut sim, &mut *bots_pair[0], 0, &mut rng0); + let [p1a, p1b] = decide_side(&mut sim, &mut *bots_pair[1], 1, &mut rng1); + let salt0 = random_salt(&mut salt_rng); + let salt1 = random_salt(&mut salt_rng); let side0 = pack_side(p0a.move_index, p0a.extra_data, p0b.move_index, p0b.extra_data, salt0); let side1 = pack_side(p1a.move_index, p1a.extra_data, p1b.move_index, p1b.extra_data, salt1); sim.execute_slot_turn(side0, side1); @@ -889,7 +934,11 @@ pub fn play_doubles_game_instrumented( book: &HashMap, track: Option<(u8, usize)>, ) -> DoublesInstr { - let mut rng = JsRng::new(spec.seed); + let (mut rng0, mut rng1, mut salt_rng) = ( + derive(spec.seed, STREAM_P0), + derive(spec.seed, STREAM_P1), + derive(spec.seed, STREAM_SALT), + ); let mut sim = Sim::new_doubles( spec.mons_per_team, spec.p0_team.clone(), @@ -923,12 +972,12 @@ pub fn play_doubles_game_instrumented( let (p0a, p0b) = if spec.p0_search_depth > 0 { search_side_moves(&mut sim, bk, 0, spec.p0_search_depth, &spec.p0_eval) } else { - pick_side_moves(&mut sim, bk, 0, spec.p0_difficulty, &mut rng) + pick_side_moves(&mut sim, bk, 0, spec.p0_difficulty, &mut rng0) }; let (p1a, p1b) = if spec.p1_search_depth > 0 { search_side_moves(&mut sim, bk, 1, spec.p1_search_depth, &spec.p1_eval) } else { - pick_side_moves(&mut sim, bk, 1, spec.p1_difficulty, &mut rng) + pick_side_moves(&mut sim, bk, 1, spec.p1_difficulty, &mut rng1) }; // Slot occupancy + KO state before the turn — the victim's slot must be read pre-execute, @@ -973,8 +1022,8 @@ pub fn play_doubles_game_instrumented( }); } - let salt0 = random_salt(&mut rng); - let salt1 = random_salt(&mut rng); + let salt0 = random_salt(&mut salt_rng); + let salt1 = random_salt(&mut salt_rng); let side0 = pack_side(p0a.move_index, p0a.extra_data, p0b.move_index, p0b.extra_data, salt0); let side1 = pack_side(p1a.move_index, p1a.extra_data, p1b.move_index, p1b.extra_data, salt1); // Rollback point for the co-kill counterfactuals below — the real turn is about to advance diff --git a/transpiler/strategies-rs/src/game.rs b/transpiler/strategies-rs/src/game.rs index 0985c8a2..43154e3b 100644 --- a/transpiler/strategies-rs/src/game.rs +++ b/transpiler/strategies-rs/src/game.rs @@ -16,10 +16,12 @@ use chomp_rt::{Address, B256}; use crate::native::ForkCache; +use crate::bot::{BattleMode, Bot, BotConfig, DecisionCtx}; +use crate::bots; use crate::evaluator::{Weights, DEFAULT_WEIGHTS}; use crate::greedy; use crate::heuristic::{self, HeuristicState}; -use crate::jsrng::{random_salt, JsRng}; +use crate::jsrng::{derive, random_salt, JsRng, STREAM_P0, STREAM_P1, STREAM_SALT}; use crate::nopeek; use crate::override_cpu::{self, OverrideState}; use crate::search; @@ -29,32 +31,8 @@ use crate::view::{ mon_state, mon_value, Mv, Seat, NO_OP_INDEX, SWITCH_MOVE_INDEX, VCPU, VOPP, }; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum StrategyKind { - Heuristic, - Greedy, - Override, - /// No-peek greedy, expectation over the opponent's plausible replies. - NoPeekExpect, - /// No-peek greedy, worst-case (maximin) over the opponent's plausible replies. - NoPeekWorst, -} - -impl StrategyKind { - /// Names are a contract with the TS registry (`sims/src/cpu/registry.ts`) - /// and `workload.ts`'s STRAT_PAIRS — keep them in sync when adding a - /// strategy. - pub fn parse(name: &str) -> Option { - match name { - "heuristic" => Some(StrategyKind::Heuristic), - "greedy" => Some(StrategyKind::Greedy), - "override" => Some(StrategyKind::Override), - "nopeek" => Some(StrategyKind::NoPeekExpect), - "nopeek-wc" => Some(StrategyKind::NoPeekWorst), - _ => None, - } - } -} +/// A seat's bot, named by its registry key (see [`crate::bots::NAMES`]). +pub type BotName = &'static str; #[derive(Clone)] pub struct GameSpec { @@ -67,13 +45,18 @@ pub struct GameSpec { /// look up per-mon config by identity. Empty to disable (config stays inert). pub p0_ids: Vec, pub p1_ids: Vec, - pub p0_strategy: StrategyKind, - pub p1_strategy: StrategyKind, + pub p0_strategy: BotName, + pub p1_strategy: BotName, + /// Which seat sees the opponent's move this game. `None` is blind mode — + /// genuinely simultaneous, and what production play looks like. The seat + /// without the reveal always decides first, so the peeker sees a real move + /// rather than a stale one. + pub peek_seat: Option, /// Per-seat evaluator weights (default [`DEFAULT_WEIGHTS`]); lets a candidate /// weight vector face a baseline opponent in the same game. pub p0_weights: Weights, pub p1_weights: Weights, - /// Per-seat search depth: 0 = the seat plays its `StrategyKind`; ≥1 = maximin + /// Per-seat search depth: 0 = the seat plays its `BotName`; ≥1 = maximin /// search at that depth over the seat's evaluator (ignores the strategy). pub p0_search_depth: u32, pub p1_search_depth: u32, @@ -86,6 +69,36 @@ pub struct GameSpec { pub p1_search_mixed: bool, } +/// One game's generators: one per seat plus the engine's salt stream. +/// +/// Seats draw from their own stream so a bot's draw count cannot shift the +/// battle or its opponent — same seed means the same salts whoever pilots, +/// which is what makes two bots comparable on one seed. +#[derive(Clone, Copy)] +struct GameRngs { + p0: JsRng, + p1: JsRng, + salt: JsRng, +} + +impl GameRngs { + fn seat_mut(&mut self, i: usize) -> &mut JsRng { + if i == 0 { + &mut self.p0 + } else { + &mut self.p1 + } + } + + fn new(seed: u32) -> GameRngs { + GameRngs { + p0: derive(seed, STREAM_P0), + p1: derive(seed, STREAM_P1), + salt: derive(seed, STREAM_SALT), + } + } +} + /// One turn's submitted moves (physical p0/p1; None = side didn't act). #[derive(Clone, Copy, Debug)] pub struct TurnTrace { @@ -101,80 +114,93 @@ pub struct GameOutcome { pub trace: Vec, } -/// Per-seat mutable strategy state (`createState()` in the TS framework). -enum StratState { - Heuristic(HeuristicState), - Greedy, - Override(OverrideState), - NoPeekExpect, - NoPeekWorst, -} - -impl StratState { - fn new(kind: StrategyKind) -> StratState { - match kind { - StrategyKind::Heuristic => StratState::Heuristic(HeuristicState::default()), - StrategyKind::Greedy => StratState::Greedy, - StrategyKind::Override => StratState::Override(OverrideState::default()), - StrategyKind::NoPeekExpect => StratState::NoPeekExpect, - StrategyKind::NoPeekWorst => StratState::NoPeekWorst, - } - } -} - struct SeatState { seat: Seat, - state: StratState, + bot: Box, last_own_move: Mv, - weights: Weights, - search_depth: u32, - search_peek: bool, - search_mixed: bool, } -/// The two seats for a spec — the one place per-seat weights get wired in. +/// The two seats for a spec — the one place per-seat bot config gets wired in. fn init_seats(spec: &GameSpec) -> [SeatState; 2] { + let build = |name: BotName, cfg: BotConfig| { + bots::build(name, cfg).unwrap_or_else(|| panic!("unknown bot {name:?}")) + }; [ SeatState { seat: Seat { cpu: 0 }, - state: StratState::new(spec.p0_strategy), + bot: build( + spec.p0_strategy, + BotConfig { + doubles_eval: Default::default(), + weights: spec.p0_weights, + search_depth: spec.p0_search_depth, + search_peek: spec.p0_search_peek, + search_mixed: spec.p0_search_mixed, + }, + ), last_own_move: Mv { move_index: 0, extra_data: 0 }, - weights: spec.p0_weights, - search_depth: spec.p0_search_depth, - search_peek: spec.p0_search_peek, - search_mixed: spec.p0_search_mixed, }, SeatState { seat: Seat { cpu: 1 }, - state: StratState::new(spec.p1_strategy), + bot: build( + spec.p1_strategy, + BotConfig { + doubles_eval: Default::default(), + weights: spec.p1_weights, + search_depth: spec.p1_search_depth, + search_peek: spec.p1_search_peek, + search_mixed: spec.p1_search_mixed, + }, + ), last_own_move: Mv { move_index: 0, extra_data: 0 }, - weights: spec.p1_weights, - search_depth: spec.p1_search_depth, - search_peek: spec.p1_search_peek, - search_mixed: spec.p1_search_mixed, }, ] } -fn decide_one(sim: &mut Sim, s: &mut SeatState, pm: Mv, rng: &mut JsRng) -> Mv { +fn decide_one(sim: &mut Sim, s: &mut SeatState, peek: Option, rng: &mut JsRng) -> Mv { let view = capture_view(sim, s.seat, sim.battle_key); - let w = s.weights; - if s.search_depth >= 1 { - // Maximin search over the seat's evaluator (ignores the strategy). `pm` (the revealed - // opponent move) is used only when `search_peek` is set. - return search::decide(sim, s.seat, &view, pm, &w, s.search_depth, s.search_peek, s.search_mixed, rng); + let mut ctx = DecisionCtx { + sim, + seat: s.seat, + mode: BattleMode::Singles, + view: Some(&view), + peek, + rng, + }; + s.bot.decide(&mut ctx).single() +} + +/// Both seats decide one turn under the spec's information mode. +/// +/// The seat without the reveal moves first; in blind mode neither is given +/// one, so order carries no information and only fixes the rng draw sequence. +fn decide_turn( + sim: &mut Sim, + seats: &mut [SeatState; 2], + rngs: &mut GameRngs, + peek_seat: Option, + acts: [bool; 2], +) -> [Option; 2] { + let first = if peek_seat == Some(0) { 1usize } else { 0usize }; + let second = 1 - first; + let mut moves: [Option; 2] = [None, None]; + + if acts[first] { + let mv = decide_one(sim, &mut seats[first], None, rngs.seat_mut(first)); + seats[first].last_own_move = mv; + moves[first] = Some(mv); } - match &mut s.state { - StratState::Heuristic(st) => heuristic::decide(sim, s.seat, &view, pm, rng, st, &w), - StratState::Greedy => greedy::decide(sim, s.seat, &view, pm, rng, &w), - StratState::Override(st) => override_cpu::decide(sim, s.seat, &view, pm, rng, st, &w), - StratState::NoPeekExpect => nopeek::decide_expect(sim, s.seat, &view, rng, &w), - StratState::NoPeekWorst => nopeek::decide_worst(sim, s.seat, &view, rng, &w), + if acts[second] { + let peek = if peek_seat == Some(second as u8) { moves[first] } else { None }; + let mv = decide_one(sim, &mut seats[second], peek, rngs.seat_mut(second)); + seats[second].last_own_move = mv; + moves[second] = Some(mv); } + moves } pub fn play_game(spec: &GameSpec, book: &HashMap, trace: bool) -> GameOutcome { - let mut rng = JsRng::new(spec.seed); + let mut rngs = GameRngs::new(spec.seed); let mut sim = Sim::new( spec.mons_per_team, spec.p0_team.clone(), @@ -199,30 +225,16 @@ pub fn play_game(spec: &GameSpec, book: &HashMap, trace: bool) let p0_acts = flag != 1; let p1_acts = flag != 0; - // p0 seat decides first, peeking only the opponent's previous move. - let mut p0_move: Option = None; - if p0_acts { - let peek = seats[1].last_own_move; - let mv = decide_one(&mut sim, &mut seats[0], peek, &mut rng); - seats[0].last_own_move = mv; - p0_move = Some(mv); - } - // p1 seat replies with the true reveal (production semantics). - let mut p1_move: Option = None; - if p1_acts { - let peek = p0_move.unwrap_or(Mv { move_index: 0, extra_data: 0 }); - let mv = decide_one(&mut sim, &mut seats[1], peek, &mut rng); - seats[1].last_own_move = mv; - p1_move = Some(mv); - } + let [p0_move, p1_move] = + decide_turn(&mut sim, &mut seats, &mut rngs, spec.peek_seat, [p0_acts, p1_acts]); if trace { traces.push(TurnTrace { p0: p0_move, p1: p1_move }); } // Salt only for an acting side, p0 before p1. - let p0_salt = if p0_move.is_some() { random_salt(&mut rng) } else { 0 }; - let p1_salt = if p1_move.is_some() { random_salt(&mut rng) } else { 0 }; + let p0_salt = if p0_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; + let p1_salt = if p1_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; sim.execute_turn( p0_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p0_salt, @@ -250,7 +262,7 @@ pub fn narrate_game( name_mon: impl Fn(u32) -> String, name_move: impl Fn(u32, u8) -> String, ) -> GameOutcome { - let mut rng = JsRng::new(spec.seed); + let mut rngs = GameRngs::new(spec.seed); let mut sim = Sim::new( spec.mons_per_team, spec.p0_team.clone(), @@ -306,7 +318,7 @@ pub fn narrate_game( let mut p0_move: Option = None; if p0_acts { let peek = seats[1].last_own_move; - let mv = decide_one(&mut sim, &mut seats[0], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[0], Some(peek), &mut rngs.p0); seats[0].last_own_move = mv; p0_move = Some(mv); } @@ -318,14 +330,14 @@ pub fn narrate_game( // fork counter + greedy's own dispose_all leave the live decision fully unperturbed. // Run it for a search seat too (the search overrides the strategy), so `⟂ greedy→` // flags where the search diverges from the greedy baseline. - if seats[1].search_depth >= 1 || !matches!(seats[1].state, StratState::Greedy) { + if seats[1].bot.name() != bots::GREEDY { let cf_view = capture_view(&mut sim, seats[1].seat, bk); - let mut cf_rng = rng; // JsRng: Copy — a throwaway snapshot of the live stream + let mut cf_rng = rngs.p1; // JsRng: Copy — a throwaway snapshot of the live stream let saved_fc = sim.fork_counter(); - p1_greedy_cf = Some(greedy::decide(&mut sim, seats[1].seat, &cf_view, peek, &mut cf_rng, &seats[1].weights)); + p1_greedy_cf = Some(greedy::decide(&mut sim, seats[1].seat, &cf_view, peek, &mut cf_rng, &spec.p1_weights)); sim.set_fork_counter(saved_fc); } - let mv = decide_one(&mut sim, &mut seats[1], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[1], Some(peek), &mut rngs.p1); seats[1].last_own_move = mv; p1_move = Some(mv); } @@ -345,8 +357,8 @@ pub fn narrate_game( name_mon(spec.p0_ids[p0_active]), name_mon(spec.p1_ids[p1_active]), ); - let p0_salt = if p0_move.is_some() { random_salt(&mut rng) } else { 0 }; - let p1_salt = if p1_move.is_some() { random_salt(&mut rng) } else { 0 }; + let p0_salt = if p0_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; + let p1_salt = if p1_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; sim.execute_turn( p0_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p0_salt, p0_move.map(|m| m.extra_data).unwrap_or(0), p1_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p1_salt, p1_move.map(|m| m.extra_data).unwrap_or(0), @@ -447,7 +459,7 @@ pub struct TraceRecord { } pub fn play_game_traced(spec: &GameSpec, book: &HashMap) -> TraceRecord { - let mut rng = JsRng::new(spec.seed); + let mut rngs = GameRngs::new(spec.seed); let mut sim = Sim::new( spec.mons_per_team, spec.p0_team.clone(), @@ -475,14 +487,14 @@ pub fn play_game_traced(spec: &GameSpec, book: &HashMap) -> Tra let mut p0_move: Option = None; if p0_acts { let peek = seats[1].last_own_move; - let mv = decide_one(&mut sim, &mut seats[0], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[0], Some(peek), &mut rngs.p0); seats[0].last_own_move = mv; p0_move = Some(mv); } let mut p1_move: Option = None; if p1_acts { let peek = p0_move.unwrap_or(Mv { move_index: 0, extra_data: 0 }); - let mv = decide_one(&mut sim, &mut seats[1], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[1], Some(peek), &mut rngs.p1); seats[1].last_own_move = mv; p1_move = Some(mv); } @@ -492,8 +504,8 @@ pub fn play_game_traced(spec: &GameSpec, book: &HashMap) -> Tra let p0_hp_pre = mon_current_hp(&mut sim, obs, bk, VOPP, p0a); let p1_hp_pre = mon_current_hp(&mut sim, obs, bk, VCPU, p1a); - let p0_salt = if p0_move.is_some() { random_salt(&mut rng) } else { 0 }; - let p1_salt = if p1_move.is_some() { random_salt(&mut rng) } else { 0 }; + let p0_salt = if p0_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; + let p1_salt = if p1_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; sim.execute_turn( p0_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p0_salt, p0_move.map(|m| m.extra_data).unwrap_or(0), p1_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p1_salt, p1_move.map(|m| m.extra_data).unwrap_or(0), @@ -545,7 +557,7 @@ pub struct YomiSample { } pub fn play_game_yomi(spec: &GameSpec, book: &HashMap) -> Vec { - let mut rng = JsRng::new(spec.seed); + let mut rngs = GameRngs::new(spec.seed); let mut sim = Sim::new( spec.mons_per_team, spec.p0_team.clone(), @@ -576,7 +588,8 @@ pub fn play_game_yomi(spec: &GameSpec, book: &HashMap) -> Vec) -> Vec = None; if p0_acts { let peek = seats[1].last_own_move; - let mv = decide_one(&mut sim, &mut seats[0], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[0], Some(peek), &mut rngs.p0); seats[0].last_own_move = mv; p0_move = Some(mv); } let mut p1_move: Option = None; if p1_acts { let peek = p0_move.unwrap_or(Mv { move_index: 0, extra_data: 0 }); - let mv = decide_one(&mut sim, &mut seats[1], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[1], Some(peek), &mut rngs.p1); seats[1].last_own_move = mv; p1_move = Some(mv); } - let p0_salt = if p0_move.is_some() { random_salt(&mut rng) } else { 0 }; - let p1_salt = if p1_move.is_some() { random_salt(&mut rng) } else { 0 }; + let p0_salt = if p0_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; + let p1_salt = if p1_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; sim.execute_turn( p0_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p0_salt, p0_move.map(|m| m.extra_data).unwrap_or(0), p1_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p1_salt, p1_move.map(|m| m.extra_data).unwrap_or(0), @@ -654,7 +667,7 @@ fn summarize_scored(scored: &[(Mv, f64)]) -> ([f64; 4], f64, f64, f64) { } pub fn play_game_breadth(spec: &GameSpec, book: &HashMap) -> Vec { - let mut rng = JsRng::new(spec.seed); + let mut rngs = GameRngs::new(spec.seed); let mut sim = Sim::new( spec.mons_per_team, spec.p0_team.clone(), spec.p1_team.clone(), spec.p0_ids.clone(), spec.p1_ids.clone(), book, @@ -678,7 +691,7 @@ pub fn play_game_breadth(spec: &GameSpec, book: &HashMap) -> Ve let mut p0_move: Option = None; if p0_acts { let view = capture_view(&mut sim, seat0, bk); - let (chosen, scored) = greedy::decide_scored(&mut sim, seat0, &view, last1, &mut rng, &DEFAULT_WEIGHTS); + let (chosen, scored) = greedy::decide_scored(&mut sim, seat0, &view, last1, &mut rngs.p0, &DEFAULT_WEIGHTS); if !scored.is_empty() { let (lane, t1, t2, w) = summarize_scored(&scored); samples.push(BreadthSample { mon_id: spec.p0_ids[p0a], chosen_move: chosen.move_index as i16, lane_scores: lane, top1: t1, top2: t2, worst: w }); @@ -689,7 +702,7 @@ pub fn play_game_breadth(spec: &GameSpec, book: &HashMap) -> Ve if p1_acts { let peek = p0_move.unwrap_or(Mv { move_index: 0, extra_data: 0 }); let view = capture_view(&mut sim, seat1, bk); - let (chosen, scored) = greedy::decide_scored(&mut sim, seat1, &view, peek, &mut rng, &DEFAULT_WEIGHTS); + let (chosen, scored) = greedy::decide_scored(&mut sim, seat1, &view, peek, &mut rngs.p1, &DEFAULT_WEIGHTS); if !scored.is_empty() { let (lane, t1, t2, w) = summarize_scored(&scored); samples.push(BreadthSample { mon_id: spec.p1_ids[p1a], chosen_move: chosen.move_index as i16, lane_scores: lane, top1: t1, top2: t2, worst: w }); @@ -698,8 +711,8 @@ pub fn play_game_breadth(spec: &GameSpec, book: &HashMap) -> Ve p1_move = Some(chosen); } - let p0_salt = if p0_move.is_some() { random_salt(&mut rng) } else { 0 }; - let p1_salt = if p1_move.is_some() { random_salt(&mut rng) } else { 0 }; + let p0_salt = if p0_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; + let p1_salt = if p1_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; sim.execute_turn( p0_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p0_salt, p0_move.map(|m| m.extra_data).unwrap_or(0), p1_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p1_salt, p1_move.map(|m| m.extra_data).unwrap_or(0), @@ -751,7 +764,7 @@ pub struct InstrRecord { } pub fn play_game_instrumented(spec: &GameSpec, book: &HashMap) -> InstrRecord { - let mut rng = JsRng::new(spec.seed); + let mut rngs = GameRngs::new(spec.seed); let mut sim = Sim::new( spec.mons_per_team, spec.p0_team.clone(), @@ -813,14 +826,14 @@ pub fn play_game_instrumented(spec: &GameSpec, book: &HashMap) let mut p0_move: Option = None; if p0_acts { let peek = seats[1].last_own_move; - let mv = decide_one(&mut sim, &mut seats[0], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[0], Some(peek), &mut rngs.p0); seats[0].last_own_move = mv; p0_move = Some(mv); } let mut p1_move: Option = None; if p1_acts { let peek = p0_move.unwrap_or(Mv { move_index: 0, extra_data: 0 }); - let mv = decide_one(&mut sim, &mut seats[1], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[1], Some(peek), &mut rngs.p1); seats[1].last_own_move = mv; p1_move = Some(mv); } @@ -832,8 +845,8 @@ pub fn play_game_instrumented(spec: &GameSpec, book: &HashMap) let ko_before_p0 = ko_bitmap(&mut sim, obs, bk, VOPP); let ko_before_p1 = ko_bitmap(&mut sim, obs, bk, VCPU); - let p0_salt = if p0_move.is_some() { random_salt(&mut rng) } else { 0 }; - let p1_salt = if p1_move.is_some() { random_salt(&mut rng) } else { 0 }; + let p0_salt = if p0_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; + let p1_salt = if p1_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; sim.execute_turn( p0_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p0_salt, @@ -899,7 +912,7 @@ pub fn play_game_mock( lane: usize, mock: &crate::mock2::MockMove, ) -> InstrRecord { - let mut rng = JsRng::new(spec.seed); + let mut rngs = GameRngs::new(spec.seed); let mut sim = Sim::new( spec.mons_per_team, spec.p0_team.clone(), spec.p1_team.clone(), spec.p0_ids.clone(), spec.p1_ids.clone(), book, @@ -929,14 +942,14 @@ pub fn play_game_mock( let mut p0_move: Option = None; if p0_acts { let peek = seats[1].last_own_move; - let mv = decide_one(&mut sim, &mut seats[0], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[0], Some(peek), &mut rngs.p0); seats[0].last_own_move = mv; p0_move = Some(mv); } let mut p1_move: Option = None; if p1_acts { let peek = p0_move.unwrap_or(Mv { move_index: 0, extra_data: 0 }); - let mv = decide_one(&mut sim, &mut seats[1], peek, &mut rng); + let mv = decide_one(&mut sim, &mut seats[1], Some(peek), &mut rngs.p1); seats[1].last_own_move = mv; p1_move = Some(mv); } @@ -952,8 +965,8 @@ pub fn play_game_mock( let mhp0_before = if spec.p0_ids.get(p0a) == Some(&target_id) { mon_current_hp(&mut sim, obs, bk, VOPP, p0a) } else { i64::MIN }; let mhp1_before = if spec.p1_ids.get(p1a) == Some(&target_id) { mon_current_hp(&mut sim, obs, bk, VCPU, p1a) } else { i64::MIN }; - let p0_salt = if p0_move.is_some() { random_salt(&mut rng) } else { 0 }; - let p1_salt = if p1_move.is_some() { random_salt(&mut rng) } else { 0 }; + let p0_salt = if p0_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; + let p1_salt = if p1_move.is_some() { random_salt(&mut rngs.salt) } else { 0 }; sim.execute_turn( p0_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p0_salt, p0_move.map(|m| m.extra_data).unwrap_or(0), p1_move.map(|m| m.move_index).unwrap_or(NO_OP_INDEX), p1_salt, p1_move.map(|m| m.extra_data).unwrap_or(0), diff --git a/transpiler/strategies-rs/src/jsrng.rs b/transpiler/strategies-rs/src/jsrng.rs index 016aa3ad..11a4fa1b 100644 --- a/transpiler/strategies-rs/src/jsrng.rs +++ b/transpiler/strategies-rs/src/jsrng.rs @@ -24,6 +24,28 @@ impl JsRng { } } +/// Independent stream ids for one game's [`derive`]d generators. +pub const STREAM_P0: u32 = 0; +pub const STREAM_P1: u32 = 1; +pub const STREAM_SALT: u32 = 2; + +/// An independent stream for `(seed, stream)`. +/// +/// Each seat decides from its own generator and the engine draws salts from a +/// third, so a bot's draw count can't perturb the battle or the other seat: +/// same seed => same salts, whoever is piloting. That is what makes two bots +/// comparable on one seed, and it is why the mixing step matters — mulberry32 +/// seeded with `seed`, `seed+1`, `seed+2` would correlate across streams. +pub fn derive(seed: u32, stream: u32) -> JsRng { + let mut x = seed ^ stream.wrapping_mul(0x9e37_79b9); + x ^= x >> 16; + x = x.wrapping_mul(0x7feb_352d); + x ^= x >> 15; + x = x.wrapping_mul(0x846c_a68b); + x ^= x >> 16; + JsRng::new(x) +} + /// 26 hex nibbles -> uint104 (`randomSalt`). Each nibble is /// `Math.floor(rng() * 16)` — exact in f64 (rng() = x / 2^32, so the /// product is a dyadic rational well inside f64 precision). diff --git a/transpiler/strategies-rs/src/lib.rs b/transpiler/strategies-rs/src/lib.rs index 7d1545e4..2e54019b 100644 --- a/transpiler/strategies-rs/src/lib.rs +++ b/transpiler/strategies-rs/src/lib.rs @@ -19,6 +19,8 @@ #![allow(non_snake_case)] // engine call sites keep Solidity spelling +pub mod bot; +pub mod bots; pub mod roster; pub mod analysis; pub mod arena; diff --git a/transpiler/strategies-rs/src/mock2.rs b/transpiler/strategies-rs/src/mock2.rs index dd4ddf35..d9d28a0b 100644 --- a/transpiler/strategies-rs/src/mock2.rs +++ b/transpiler/strategies-rs/src/mock2.rs @@ -15,7 +15,7 @@ use chomp_rt::{B256, U256}; use crate::analysis::{fold, MonStat}; use crate::arena::{build_specs, build_specs_with}; -use crate::game::{run_games_instrumented, run_games_mock, GameSpec, StrategyKind}; +use crate::game::{run_games_instrumented, run_games_mock, GameSpec, BotName}; use crate::roster::{self, Roster}; use crate::sim::Sim; use crate::shared::{build_damage_calc_context, estimate_damage}; @@ -289,7 +289,7 @@ pub fn run_mock_ab(roster: &Roster, mock: &MockMove, games: usize, wseed: u32, s /// Same A/B, but under an explicit pilot rotation — e.g. the no-peek pilots as a "ceiling", which /// actually play the reads/setups the greedy basket ignores, so a conditional/yomi move shows its /// upside rather than just its floor. -pub fn run_mock_ab_with(roster: &Roster, mock: &MockMove, games: usize, wseed: u32, seed_base: u32, threads: usize, pairs: &[(StrategyKind, StrategyKind)]) -> (MonStat, MonStat) { +pub fn run_mock_ab_with(roster: &Roster, mock: &MockMove, games: usize, wseed: u32, seed_base: u32, threads: usize, pairs: &[(BotName, BotName)]) -> (MonStat, MonStat) { let (specs, _) = build_specs_with(roster, games, wseed, seed_base, pairs); run_mock_ab_on(roster, mock, specs, threads) } diff --git a/transpiler/strategies-rs/src/teams.rs b/transpiler/strategies-rs/src/teams.rs index 5bf99a87..bddcdd65 100644 --- a/transpiler/strategies-rs/src/teams.rs +++ b/transpiler/strategies-rs/src/teams.rs @@ -8,7 +8,8 @@ use std::collections::HashMap; use crate::arena::{build_team_mon, draw_team, Wrand}; use crate::evaluator::DEFAULT_WEIGHTS; -use crate::game::{run_games, GameSpec, StrategyKind}; +use crate::bots; +use crate::game::{run_games, GameSpec, BotName}; use crate::roster::{self, Roster}; /// All k-subsets of 0..n (lexicographic). @@ -74,8 +75,9 @@ pub fn run_team_search(roster: &Roster, games_per_team: usize, wseed: u32, seed_ p1_team: team_ids.iter().map(|&id| build_team_mon(mon(id))).collect(), p0_ids: opp, p1_ids: team_ids.clone(), - p0_strategy: StrategyKind::Greedy, - p1_strategy: StrategyKind::Greedy, + p0_strategy: bots::GREEDY, + p1_strategy: bots::GREEDY, + peek_seat: None, p0_weights: DEFAULT_WEIGHTS, p1_weights: DEFAULT_WEIGHTS, p0_search_depth: 0, diff --git a/transpiler/strategies-rs/src/yomi.rs b/transpiler/strategies-rs/src/yomi.rs index a4439282..7b80e570 100644 --- a/transpiler/strategies-rs/src/yomi.rs +++ b/transpiler/strategies-rs/src/yomi.rs @@ -5,7 +5,8 @@ use std::collections::HashMap; use crate::arena::build_specs_with; -use crate::game::{run_games_yomi, StrategyKind}; +use crate::bots; +use crate::game::{run_games_yomi, BotName}; use crate::roster::{self, Roster}; #[derive(Default, Clone)] @@ -41,7 +42,7 @@ pub fn run_yomi_analysis( let book = roster::address_book(); // Drive with greedy (fast); the grid itself is no-peek by construction, so the driver only // decides which positions get sampled, not the tension computed at them. - let pairs = [(StrategyKind::Greedy, StrategyKind::Greedy)]; + let pairs = [(bots::GREEDY, bots::GREEDY)]; let (specs, _) = build_specs_with(roster, games, wseed, seed_base, &pairs); let results = run_games_yomi(&specs, &book, threads); diff --git a/transpiler/transpiler-config-rust.json b/transpiler/transpiler-config-rust.json index a57a649e..9b6f93b2 100644 --- a/transpiler/transpiler-config-rust.json +++ b/transpiler/transpiler-config-rust.json @@ -17,6 +17,8 @@ "lib/TargetLib.sol", "lib/ValidatorLogic.sol", "moves/IMoveSet.sol", + "moves/IMoveResolver.sol", + "moves/MoveCommandLib.sol", "moves/MoveSlotLib.sol", "moves/AttackCalculator.sol", "moves/StandardAttack.sol", @@ -73,7 +75,9 @@ ], "dispatchInterfaces": [ "IMoveSet", + "IMoveResolver", "IEffect", + "IEffectResolver", "IStatusEffect", "IAbility" ], diff --git a/transpiler/type_system/registry.py b/transpiler/type_system/registry.py index d33f05fb..bc30b48e 100644 --- a/transpiler/type_system/registry.py +++ b/transpiler/type_system/registry.py @@ -24,6 +24,8 @@ class TypeRegistry: def __init__(self): self.structs: Set[str] = set() + # struct name -> raw AST definition (storage slot-layout computation) + self.struct_defs: Dict[str, 'StructDefinition'] = {} self.enums: Set[str] = set() self.constants: Set[str] = set() # Literal numeric values of constants (e.g. MOVE_LANES_PER_MON -> 4), so codegen can @@ -90,6 +92,7 @@ def discover_from_ast(self, ast: 'SourceUnit', rel_path: Optional[str] = None) - # Top-level structs for struct in ast.structs: self.structs.add(struct.name) + self.struct_defs[struct.name] = struct if rel_path and rel_path != 'Structs': self.struct_paths[struct.name] = rel_path self.struct_fields[struct.name] = {} @@ -150,6 +153,7 @@ def discover_from_ast(self, ast: 'SourceUnit', rel_path: Optional[str] = None) - contract_local_structs: Set[str] = set() for struct in contract.structs: self.structs.add(struct.name) + self.struct_defs[struct.name] = struct contract_local_structs.add(struct.name) # Also record struct fields (same as top-level structs) self.struct_fields[struct.name] = {} @@ -198,6 +202,7 @@ def discover_from_ast(self, ast: 'SourceUnit', rel_path: Optional[str] = None) - def merge(self, other: 'TypeRegistry') -> None: """Merge another registry into this one.""" self.structs.update(other.structs) + self.struct_defs.update(other.struct_defs) self.enums.update(other.enums) self.constants.update(other.constants) self.interfaces.update(other.interfaces) diff --git a/transpiler/type_system/slots.py b/transpiler/type_system/slots.py new file mode 100644 index 00000000..e072aeed --- /dev/null +++ b/transpiler/type_system/slots.py @@ -0,0 +1,309 @@ +"""Solidity storage slot packing for structs. + +Both backends model storage by field identity — a Rust struct field, a TS object +property — never by slot. Inline assembly that reads or writes a whole slot +(`sload(x.slot)`) therefore has to be reconciled against every field that slot +packs, and against the bits no field claims: hand-packed Solidity treats that +padding as free real estate, so those bits carry state the declared members +cannot represent. + +This computes the packing so the Yul lowerings in both backends can turn a slot +word into field assignments and back. +""" + +from dataclasses import dataclass +from typing import Dict, List, Optional, Sequence, Set, Tuple + +from ..parser.ast_nodes import StructDefinition, TypeName, VariableDeclaration + +WORD_BYTES = 32 +WORD_BITS = 256 + +#: Generated member holding a slot's undeclared bits. Double-underscored so it +#: cannot collide with a Solidity member name. +SLOT0_FREE_FIELD = '__slot0_free' + + +@dataclass(frozen=True) +class SlotField: + """One declared member's placement inside its slot.""" + + name: str + bit_offset: int + bit_width: int + + @property + def mask(self) -> int: + return ((1 << self.bit_width) - 1) << self.bit_offset + + +@dataclass(frozen=True) +class SlotLayout: + """One 32-byte slot: the members packed into it, plus the leftover bits.""" + + index: int + fields: Tuple[SlotField, ...] + + @property + def claimed_mask(self) -> int: + m = 0 + for f in self.fields: + m |= f.mask + return m + + @property + def free_mask(self) -> int: + return ((1 << WORD_BITS) - 1) & ~self.claimed_mask + + @property + def has_free_bits(self) -> bool: + return self.free_mask != 0 + + +def _int_bytes(name: str, prefix: str) -> Optional[int]: + """`uint96` -> 12. Bare `uint`/`int` is 256-bit.""" + suffix = name[len(prefix):] + if not suffix: + return 32 + if not suffix.isdigit(): + return None + bits = int(suffix) + if bits % 8 or not 8 <= bits <= 256: + return None + return bits // 8 + + +def value_byte_size( + type_name: TypeName, + *, + enum_names: Set[str], + struct_names: Set[str], +) -> Optional[int]: + """Packed byte size of a value type; None when the type takes whole slots.""" + if type_name is None: + return None + if type_name.is_mapping or type_name.is_array: + return None + + name = (type_name.name or '').strip() + if name in ('address', 'address payable'): + return 20 + if name == 'bool': + return 1 + if name.startswith('uint'): + return _int_bytes(name, 'uint') + if name.startswith('int'): + return _int_bytes(name, 'int') + if name in ('bytes', 'string'): + return None + if name.startswith('bytes'): + n = name[5:] + return int(n) if n.isdigit() and 1 <= int(n) <= 32 else None + if name in enum_names: + return 1 + if name in struct_names: + return None + # Remaining user-defined names are contracts/interfaces, i.e. addresses. + return 20 + + +def _whole_slot_span( + type_name: TypeName, + *, + enum_names: Set[str], + struct_names: Set[str], + structs: Dict[str, StructDefinition], +) -> Optional[int]: + """Slots occupied by a member that can't share a slot; None if unknown.""" + if type_name.is_mapping: + return 1 + if type_name.is_array: + # Only a single-dimension fixed array of a sized value type is exact. + if type_name.array_dimensions > 1 or type_name.array_size is None: + return 1 # dynamic arrays occupy exactly one slot (the length) + return None + name = (type_name.name or '').strip() + if name in ('bytes', 'string'): + return 1 + if name in struct_names: + nested = structs.get(name) + if nested is None: + return None + layouts = struct_slots( + nested, enum_names=enum_names, struct_names=struct_names, structs=structs + ) + return len(layouts) if layouts else None + return None + + +def struct_slots( + struct: StructDefinition, + *, + enum_names: Set[str], + struct_names: Set[str], + structs: Optional[Dict[str, StructDefinition]] = None, +) -> List[SlotLayout]: + """Slot layout for `struct`, in declaration order. + + Members pack left-to-right into the current slot and spill to the next when + they no longer fit — Solidity's rule. Computation stops at the first member + whose slot span can't be determined, so callers get exact layouts or none at + all for a given slot, never a guess. + """ + structs = structs or {} + layouts: List[SlotLayout] = [] + pending: List[SlotField] = [] + slot = 0 + used = 0 # bytes consumed in the current slot + + def flush() -> None: + nonlocal pending + if pending: + layouts.append(SlotLayout(index=slot, fields=tuple(pending))) + pending = [] + + members: Sequence[VariableDeclaration] = struct.members or [] + for member in members: + size = value_byte_size( + member.type_name, enum_names=enum_names, struct_names=struct_names + ) + if size is None: + flush() + if used: + slot += 1 + used = 0 + span = _whole_slot_span( + member.type_name, + enum_names=enum_names, + struct_names=struct_names, + structs=structs, + ) + if span is None: + return layouts # unknown span: everything after it is unknowable + slot += span + continue + + if used + size > WORD_BYTES: + flush() + slot += 1 + used = 0 + pending.append( + SlotField(name=member.name, bit_offset=used * 8, bit_width=size * 8) + ) + used += size + + flush() + return layouts + + +def _declared_type_of(ident: str, func) -> Optional[TypeName]: + """The declared TypeName of `ident` within `func`, or None.""" + import dataclasses + + for p in list(func.parameters or []) + list(func.return_parameters or []): + if p.name == ident: + return p.type_name + + found: List[TypeName] = [] + + def walk(node): + if node is None or found: + return + if isinstance(node, (list, tuple)): + for item in node: + walk(item) + return + if isinstance(node, dict): + for item in node.values(): + walk(item) + return + if not dataclasses.is_dataclass(node): + return + if type(node).__name__ == 'VariableDeclaration' and node.name == ident: + found.append(node.type_name) + return + for f in dataclasses.fields(node): + walk(getattr(node, f.name)) + + walk(func.body) + return found[0] if found else None + + +def discover_slot_layouts( + asts, + *, + enum_names: Set[str], + struct_names: Set[str], + struct_defs: Dict[str, StructDefinition], +) -> Dict[str, SlotLayout]: + """Slot-0 layouts for every struct whose raw slot is reached by Yul. + + `asts` is any iterable of parsed SourceUnits. A struct is included only + when its layout is exactly computable, so callers get a precise packing or + nothing — never an approximation. + """ + import dataclasses + import re + + slot_idents = re.compile(r'(\w+)\s*\.\s*slot\b') + touched: Set[str] = set() + + def walk(node, fn_scope): + if node is None: + return + if isinstance(node, (list, tuple)): + for item in node: + walk(item, fn_scope) + return + if isinstance(node, dict): + for item in node.values(): + walk(item, fn_scope) + return + if not dataclasses.is_dataclass(node): + return + cls = type(node).__name__ + if cls == 'FunctionDefinition': + fn_scope = node + elif cls == 'AssemblyStatement' and fn_scope is not None: + code = getattr(getattr(node, 'block', None), 'code', '') or '' + for ident in slot_idents.findall(code): + tn = _declared_type_of(ident, fn_scope) + if tn is not None and tn.name in struct_names: + touched.add(tn.name) + for f in dataclasses.fields(node): + walk(getattr(node, f.name), fn_scope) + + for ast in asts: + walk(ast, None) + + layouts: Dict[str, SlotLayout] = {} + for name in touched: + struct = struct_defs.get(name) + if struct is None: + continue + layout = slot_zero( + struct, + enum_names=enum_names, + struct_names=struct_names, + structs=struct_defs, + ) + if layout is not None: + layouts[name] = layout + return layouts + + +def slot_zero( + struct: StructDefinition, + *, + enum_names: Set[str], + struct_names: Set[str], + structs: Optional[Dict[str, StructDefinition]] = None, +) -> Optional[SlotLayout]: + """The base slot a `.slot` expression addresses, or None if unknown.""" + layouts = struct_slots( + struct, enum_names=enum_names, struct_names=struct_names, structs=structs + ) + for layout in layouts: + if layout.index == 0: + return layout + return None From f1129a222077674022b8f78e7e5fa50116473253 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:55:22 -0700 Subject: [PATCH 22/41] add benchmark flow --- BENCH.md | 165 ++++++++++++++++++++ README.md | 3 + bench.sh | 59 +++++++ transpiler/strategies-rs/src/arena.rs | 63 ++++++-- transpiler/strategies-rs/src/bin/bench.rs | 25 +-- transpiler/strategies-rs/src/bots.rs | 46 +++++- transpiler/strategies-rs/src/example_bot.rs | 97 ++++++++++++ transpiler/strategies-rs/src/lib.rs | 1 + 8 files changed, 431 insertions(+), 28 deletions(-) create mode 100644 BENCH.md create mode 100755 bench.sh create mode 100644 transpiler/strategies-rs/src/example_bot.rs diff --git a/BENCH.md b/BENCH.md new file mode 100644 index 00000000..c71a9e3e --- /dev/null +++ b/BENCH.md @@ -0,0 +1,165 @@ +# chomp bot benchmark + +Write a bot that plays Stomp. + +Score it against a ladder of built-in opponents! + +## Quick start + +```bash +./bench.sh setup # generate the engine + build (~35s on 8 cores) +./bench.sh run --bot greedy # score a built-in bot +./bench.sh run --bot example # the worked example you'll copy +``` + +Requires `python3` (3.11+) and a Rust toolchain. The engine is generated from the Solidity +source rather than committed, which is why there's a build step. + +## Writing a bot + +Copy [`transpiler/strategies-rs/src/example_bot.rs`](transpiler/strategies-rs/src/example_bot.rs). +It is a complete, registered, working bot in about 60 lines — one-ply search over the +forward model — and it beats `random` 96% and `heuristic` 54%. + +The interface is one method: + +```rust +pub trait Bot: Send { + fn name(&self) -> &'static str; + fn modes(&self) -> &'static [BattleMode] { &[BattleMode::Singles] } + fn reset(&mut self) {} + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action; +} +``` + +`ctx` gives you: + +| field | what it is | +|---|---| +| `ctx.sim` | the live battle — fork it and play hypotheticals forward | +| `ctx.seat` | which side you're piloting | +| `ctx.mode` | `Singles` or `Doubles` | +| `ctx.view` | position snapshot (singles only; `None` in doubles — read `ctx.sim`) | +| `ctx.peek` | the opponent's move, or `None` in blind mode | +| `ctx.rng` | your private random stream | + +To register: add `pub mod my_bot;` to `src/lib.rs`, then a name constant and one `build` arm +in `src/bots.rs`. Then `./bench.sh run --bot my-bot`. + +`./bench.sh test` runs a smoke test that plays one game with every registered bot. If your +bot is in the registry, it is covered. + +### Guidelines + +**Draw only from `ctx.rng`.** Salts are drawn from a +separate one derived from the seed alone, so however much you draw, the battle itself is +unchanged. + +**Read the opponent's move only through `ctx.peek`.** In blind mode it is `None`, so a bot +written against the interface cannot accidentally depend on seeing a reveal. + +Beyond that, do what you like. Search deep, cache, hand-tune tables, hard-code matchups. + +Every game is an **independent 4v4 draft**, redrawn from the mon roster. + +## Existing Ladder + + Your row from `./bench.sh run --bot yours` is directly comparable + +**Singles**, blind mode, seed `0xbeefcafe`. Intervals ±0.5% to ±3.0%. + +| bot | drafts | vs `random` | vs `greedy` | vs `heuristic` | vs `override` | vs `nopeek` | vs `search-d1` | +|---|---|---|---|---|---|---|---| +| `random` | 4000 | 50.7% *(self)* | 4.5% | 5.9% | 4.3% | 3.2% | 5.5% | +| `example` | 4000 | 95.4% | 46.7% | 55.3% | 50.6% | 44.9% | 37.5% | +| `greedy` | 4000 | 96.1% | 50.2% *(self)* | 57.3% | 54.4% | 45.9% | 39.3% | +| `heuristic` | 4000 | 94.1% | 40.4% | 49.4% *(self)* | 44.5% | 39.2% | 34.0% | +| `override` | 4000 | 96.2% | 44.5% | 55.0% | 50.8% *(self)* | 41.4% | 37.9% | +| `nopeek` | 4000 | 96.6% | 53.8% | 59.6% | 56.3% | 50.3% *(self)* | 33.1% | +| `nopeek-wc` | 4000 | 95.6% | 63.7% | 67.4% | 62.8% | 67.7% | 51.9% | +| `search-d1` | 4000 | 94.6% | 61.3% | 66.4% | 61.2% | 66.5% | 50.1% *(self)* | +| `search-d2` | 1000 | 97.0% | 72.2% | 73.8% | 71.1% | 74.6% | 62.3% | + +**Doubles**, seed `0xbeefcafe`. Intervals ±0.8% to ±6.8%. + +| bot | drafts | vs `doubles-easy` | vs `doubles-medium` | vs `doubles-hard` | vs `doubles-search-d1` | +|---|---|---|---|---|---| +| `doubles-easy` | 4000 | 49.9% *(self)* | 34.1% | 17.3% | 7.8% | +| `doubles-medium` | 4000 | 66.7% | 49.7% *(self)* | 30.6% | 15.6% | +| `doubles-hard` | 4000 | 83.5% | 69.5% | 49.3% *(self)* | 27.6% | +| `doubles-search-d1` | 2000 | 93.3% | 84.6% | 69.9% | 50.2% *(self)* | +| `doubles-search-d2` | 200 | 93.0% | 84.5% | 76.0% | 61.0% | + +Check the `drafts` column before comparing rows — `search-d2` costs ~25× `greedy` per +decision, so it gets fewer, and `doubles-search-d2` (200 drafts, ±6.8%) is indicative only. + +### Information modes + +- `--info blind` (default) — genuinely simultaneous. Neither side sees the other's move. + This is what production play looks like. +- `--info rotate` — one side sees the opponent's move, alternating each game so both seats + peek equally often. This is for beating up human players :> + +### Doubles + +```bash +./bench.sh run --bot doubles-hard --mode doubles +``` + +Two active mons per side; your bot returns `Action::Slots([move, move])` and declares +`modes()` as `Doubles`. Doubles is the mode the shipped CPU actually plays, so improvements +there transfer most directly. + +## Research directions + +Some ideas for you to try out: + +### Archetype pilots + +The bots above all evaluate positions the same way and differ only in search. None of them +plays a **strategy** in the way a human does. + +- **Stall.** Stamina is a real resource and resting is a legal action — win by exhausting + the opponent rather than out-damaging them. The search only learned to rest at all after a + bug fix, and it gained ~12 points when it did, so this axis is under-explored. +- **Setup sweep.** Stat boosts are multiplicative per source. Find the turn where a boost + pays for the tempo it costs, then commit. + +`override_cpu.rs` is the substrate — it already runs scripted per-mon rules (preferred move, +switch-in move, setup move) with a heuristic fallback. + +### Synergy-aware play + +`./bench.sh` aside, the tree has an exhaustive team analyser: all C(13,4) = 715 teams +evaluated against the random-draft field, with synergy read as the *interaction* beyond each +mon's main effect (`src/bin/teams.rs`). Overclock lifting slower attackers shows up there. + +No bot currently reads its own roster and plays to it. A bot that recognises it drafted a +speed-control core, or a specific two-mon combo, and sequences toward it, is open ground. + +### Wall / check exploitation + +`src/bin/matrix.rs` computes the static damage-to-KO matrix — who *checks* whom (2HKO and +out-damages) and who *walls* whom (needs 3+ and out-damaged). `src/bin/faceoff.rs` finds the +behavioural version, including walls that never show up as KOs at all: sleep-and-drain and +stamina denial. + +Both are offline analyses. A bot that consults that structure mid-game — pivoting to its +check instead of clicking damage — doesn't exist yet. + +### Bots humans enjoy losing to + +Strength and fun are different targets, and the current difficulty tiers conflate them: easy +and medium are just `hard` taking a random legal action some of the time, so they feel +*erratic* rather than *beatable*. A weak bot that plays a coherent but narrow plan is far +more satisfying to beat than one that flails. + +- **Unexploitable rather than strong.** Nash mixing over the per-turn matrix game already + exists (`--mixed`, regret matching). Measured against fixed bots it *loses* to plain + maximin — but that is the point: it cannot be pattern-read, which matters against a human + who plays fifty games and not one. +- **Personality bots.** A pilot that always leads the same mon, always sets up first, or + always preserves its last mon. Predictable in a way a human can discover and counter, + which is a better difficulty knob than injecting randomness. +- **Yomi-aware.** `src/bin/yomi.rs` scores how much a position's outcome hinges on guessing + right. Bluff where it matters, play straight where it doesn't. diff --git a/README.md b/README.md index b6017891..6e6a5d1f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ your own effects! your own hooks! +your own CPU! — see **[BENCH.md](BENCH.md)**: write a bot, score it against a ladder of +built-in opponents, with the real battle engine as your forward model. + general flow of the game is: - each turn, players simultaneously choose a move on their active mon. - moves can alter stats, do damage, or generally mutate game state in some way. diff --git a/bench.sh b/bench.sh new file mode 100755 index 00000000..9693c659 --- /dev/null +++ b/bench.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# chomp bot benchmark — setup and scoring. +# +# ./bench.sh setup generate the engine and build (first time, slow) +# ./bench.sh run --bot greedy score a bot against the ladder +# ./bench.sh test run the bot smoke tests +# +# See BENCH.md for how to write a bot. + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="$ROOT/transpiler/rs-output" +export CHOMP_ROOT="$ROOT" + +need() { + command -v "$1" >/dev/null 2>&1 || { echo "bench.sh: '$1' is required but not on PATH" >&2; exit 1; } +} + +setup() { + need python3 + need cargo + echo "==> generating the Rust engine from the Solidity source" + python3 -m transpiler src/ --target rust + echo "==> building (first run compiles the engine and its deps; ~35s on 8 cores, plus crate downloads)" + cargo build --release --manifest-path "$OUT/Cargo.toml" -p chomp-strategies + echo "==> ready. Try: ./bench.sh run --bot greedy" +} + +# The engine is generated, not committed, so a fresh checkout has to build once. +require_build() { + if [ ! -x "$OUT/target/release/bench" ]; then + echo "bench.sh: not built yet — running setup first." >&2 + setup + fi +} + +cmd="${1:-}" +shift || true + +case "$cmd" in + setup) setup ;; + run) + require_build + exec "$OUT/target/release/bench" "$@" + ;; + arena) + require_build + exec "$OUT/target/release/arena" "$@" + ;; + test) + need cargo + exec cargo test --release --manifest-path "$OUT/Cargo.toml" -p chomp-strategies "$@" + ;; + *) + sed -n '2,9p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 2 + ;; +esac diff --git a/transpiler/strategies-rs/src/arena.rs b/transpiler/strategies-rs/src/arena.rs index 09c592d4..f0230177 100644 --- a/transpiler/strategies-rs/src/arena.rs +++ b/transpiler/strategies-rs/src/arena.rs @@ -481,9 +481,13 @@ pub fn doubles_ab_winrate( ) -> DoublesAbResult { let book = roster::address_book(); let mut rng = Wrand::new(wseed); - // Both seats search in an A/B (depth 0 would compare the un-evaluated greedy pilot instead). - let cand = DoublesSideCfg { depth: cand.depth.max(1), ..cand }; - let base = DoublesSideCfg { depth: base.depth.max(1), ..base }; + // Weight A/Bs compare two SEARCH configs, so depth 0 is bumped to 1 — otherwise the + // comparison silently falls back to the un-evaluated greedy pilot. A named bot opts out: + // its name selects the pilot and this would misreport what actually played. + let bump = |c: DoublesSideCfg| { + if c.bot.is_empty() { DoublesSideCfg { depth: c.depth.max(1), ..c } } else { c } + }; + let (cand, base) = (bump(cand), bump(base)); let pairs = (games / 2).max(1); let mut specs = Vec::with_capacity(pairs * 2); for i in 0..pairs { @@ -598,6 +602,9 @@ mod tests { pub struct BenchRow { pub opponent: &'static str, pub games: u32, + /// Independent drafts behind `games`. Equal to `games` here; a matched-pair + /// construction would halve it, and the interval depends on THIS number. + pub drafts: u32, pub wins: u32, pub losses: u32, pub draws: u32, @@ -647,11 +654,12 @@ pub fn bench_singles( let outcomes = run_games(&specs, &book, threads, false); let mut row = - BenchRow { opponent, games: 0, wins: 0, losses: 0, draws: 0 }; + BenchRow { opponent, games: 0, drafts: 0, wins: 0, losses: 0, draws: 0 }; for (i, outcome) in outcomes.iter().enumerate() { // pair 0 seats the candidate at p1, pair 1 at p0. let cand_seat = if pair_of[i] == 0 { 1u8 } else { 0u8 }; row.games += 1; + row.drafts += 1; // An engine panic surfaces as Err; count it with the draws // rather than silently crediting either side. match outcome.as_ref().ok().and_then(|o| o.winner_seat) { @@ -666,6 +674,12 @@ pub fn bench_singles( } /// Score `cand` against every opponent in `ladder` (doubles). +/// +/// Independent drafts, one per game, with the candidate's seat alternating +/// across games. Deliberately NOT the matched-pair construction used by +/// `doubles_ab_winrate`: replaying each draft with the pilots swapped buys +/// precision by halving the number of distinct battles, which is the right +/// trade for "is this tweak better" and the wrong one for a ladder. pub fn bench_doubles( roster: &Roster, cand: &'static str, @@ -675,19 +689,42 @@ pub fn bench_doubles( seed_base: u32, threads: usize, ) -> Vec { + let book = roster::address_book(); ladder .iter() .map(|&opponent| { - let c = DoublesSideCfg { bot: cand, ..Default::default() }; - let b = DoublesSideCfg { bot: opponent, ..Default::default() }; - let r = doubles_ab_winrate(roster, c, b, games, wseed, seed_base, threads); - BenchRow { - opponent, - games: r.cand_wins + r.base_wins + r.draws, - wins: r.cand_wins, - losses: r.base_wins, - draws: r.draws, + let mut rng = Wrand::new(wseed); + let mut specs = Vec::with_capacity(games); + for i in 0..games { + let (p0_team, p0_ids) = draw_built_team(roster, &mut rng); + let (p1_team, p1_ids) = draw_built_team(roster, &mut rng); + // Alternate which seat the candidate pilots, one fresh draft each. + let (c0, c1) = if i % 2 == 0 { + (DoublesSideCfg { bot: opponent, ..Default::default() }, + DoublesSideCfg { bot: cand, ..Default::default() }) + } else { + (DoublesSideCfg { bot: cand, ..Default::default() }, + DoublesSideCfg { bot: opponent, ..Default::default() }) + }; + specs.push(doubles_spec( + seed_base.wrapping_add(i as u32), + p0_team, p0_ids, c0, + p1_team, p1_ids, c1, + )); } + let outcomes = run_doubles_games(&specs, &book, threads); + let mut row = BenchRow { opponent, games: 0, drafts: 0, wins: 0, losses: 0, draws: 0 }; + for (i, o) in outcomes.iter().enumerate() { + let cand_seat = if i % 2 == 0 { 1u8 } else { 0u8 }; + row.games += 1; + row.drafts += 1; + match o.winner_side { + Some(sd) if sd == cand_seat => row.wins += 1, + Some(_) => row.losses += 1, + None => row.draws += 1, + } + } + row }) .collect() } diff --git a/transpiler/strategies-rs/src/bin/bench.rs b/transpiler/strategies-rs/src/bin/bench.rs index aaa918ca..951ce2da 100644 --- a/transpiler/strategies-rs/src/bin/bench.rs +++ b/transpiler/strategies-rs/src/bin/bench.rs @@ -3,9 +3,10 @@ //! cargo run --release -p chomp-strategies --bin bench -- --bot greedy //! cargo run --release -p chomp-strategies --bin bench -- --bot mybot --mode doubles //! -//! Every row plays each draft from both seats and sums, because the seats are -//! not perfectly symmetric; and every row prints its own n and 95% interval, -//! because a deep bot's row is necessarily thinner than a shallow one's. +//! Every row draws independent drafts and alternates which seat the candidate +//! pilots, because the seats are not perfectly symmetric; and every row prints +//! its own draft count and 95% interval, because a deep bot's row is +//! necessarily thinner than a shallow one's. //! //! Data (drool/*.csv, src/mons/*.json) is read relative to CHOMP_ROOT. @@ -16,12 +17,18 @@ use chomp_strategies::roster::load_roster; use std::path::PathBuf; /// The published singles ladder, weakest first. -const SINGLES_LADDER: &[&str] = - &[bots::RANDOM, bots::GREEDY, bots::HEURISTIC, bots::OVERRIDE, bots::NOPEEK]; +const SINGLES_LADDER: &[&str] = &[ + bots::RANDOM, + bots::GREEDY, + bots::HEURISTIC, + bots::OVERRIDE, + bots::NOPEEK, + bots::SEARCH_D1, +]; /// The published doubles ladder, weakest first. const DOUBLES_LADDER: &[&str] = - &[bots::DOUBLES_EASY, bots::DOUBLES_MEDIUM, bots::DOUBLES_HARD]; + &[bots::DOUBLES_EASY, bots::DOUBLES_MEDIUM, bots::DOUBLES_HARD, bots::DOUBLES_SEARCH_D1]; fn arg(args: &[String], flag: &str) -> Option { args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned() @@ -36,12 +43,12 @@ fn arg_u(args: &[String], flag: &str, def: u64) -> u64 { } fn print_rows(bot: &str, rows: &[BenchRow], elapsed: f64) { - println!("\n{:>14} {:>6} {:>8} {:>13} {:>12}", "opponent", "games", "win%", "95% CI", "w-l-draw"); + println!("\n{:>18} {:>7} {:>8} {:>13} {:>12}", "opponent", "drafts", "win%", "95% CI", "w-l-draw"); for r in rows { println!( - "{:>14} {:>6} {:>7.1}% ±{:>11.1}% {:>4}-{:>3}-{:<4}", + "{:>18} {:>7} {:>7.1}% ±{:>11.1}% {:>4}-{:>3}-{:<4}", r.opponent, - r.games, + r.drafts, r.win_rate() * 100.0, r.ci95() * 100.0, r.wins, diff --git a/transpiler/strategies-rs/src/bots.rs b/transpiler/strategies-rs/src/bots.rs index d7df71bc..9e05bcb4 100644 --- a/transpiler/strategies-rs/src/bots.rs +++ b/transpiler/strategies-rs/src/bots.rs @@ -21,17 +21,31 @@ pub const OVERRIDE: &str = "override"; pub const NOPEEK: &str = "nopeek"; pub const NOPEEK_WC: &str = "nopeek-wc"; pub const SEARCH: &str = "search"; +pub const EXAMPLE: &str = "example"; +/// Search bots at a pinned depth. Depth is the single biggest lever a bot has, +/// so the ladder names it rather than hiding it in config. +pub const SEARCH_D1: &str = "search-d1"; +pub const SEARCH_D2: &str = "search-d2"; +pub const DOUBLES_SEARCH_D1: &str = "doubles-search-d1"; +pub const DOUBLES_SEARCH_D2: &str = "doubles-search-d2"; pub const DOUBLES_EASY: &str = "doubles-easy"; pub const DOUBLES_MEDIUM: &str = "doubles-medium"; pub const DOUBLES_HARD: &str = "doubles-hard"; pub const DOUBLES_SEARCH: &str = "doubles-search"; /// Every registered bot, in ladder order (weakest first). -pub const NAMES: &[&str] = &[RANDOM, GREEDY, HEURISTIC, OVERRIDE, NOPEEK, NOPEEK_WC, SEARCH]; +pub const NAMES: &[&str] = + &[RANDOM, EXAMPLE, GREEDY, HEURISTIC, OVERRIDE, NOPEEK, NOPEEK_WC, SEARCH, SEARCH_D1, SEARCH_D2]; /// Registered doubles bots, weakest first. -pub const DOUBLES_NAMES: &[&str] = - &[DOUBLES_EASY, DOUBLES_MEDIUM, DOUBLES_HARD, DOUBLES_SEARCH]; +pub const DOUBLES_NAMES: &[&str] = &[ + DOUBLES_EASY, + DOUBLES_MEDIUM, + DOUBLES_HARD, + DOUBLES_SEARCH, + DOUBLES_SEARCH_D1, + DOUBLES_SEARCH_D2, +]; const DOUBLES_ONLY: &[BattleMode] = &[BattleMode::Doubles]; @@ -67,7 +81,11 @@ pub struct DoublesSearchBot { impl Bot for DoublesSearchBot { fn name(&self) -> &'static str { - DOUBLES_SEARCH + match self.depth { + 1 => DOUBLES_SEARCH_D1, + 2 => DOUBLES_SEARCH_D2, + _ => DOUBLES_SEARCH, + } } fn modes(&self) -> &'static [BattleMode] { DOUBLES_ONLY @@ -208,7 +226,11 @@ pub struct SearchBot { impl Bot for SearchBot { fn name(&self) -> &'static str { - SEARCH + match self.depth { + 1 => SEARCH_D1, + 2 => SEARCH_D2, + _ => SEARCH, + } } fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { let pm = ctx.peek.unwrap_or_else(|| Mv { move_index: 0, extra_data: 0 }); @@ -232,7 +254,8 @@ impl Bot for SearchBot { /// bot's evaluator instead of playing it. That is the pre-trait behaviour and /// the arena's weight/depth sweeps depend on it. pub fn build(name: &str, cfg: BotConfig) -> Option> { - if cfg.search_depth >= 1 && name != RANDOM && !DOUBLES_NAMES.contains(&name) { + let pinned = matches!(name, SEARCH_D1 | SEARCH_D2 | DOUBLES_SEARCH_D1 | DOUBLES_SEARCH_D2); + if cfg.search_depth >= 1 && name != RANDOM && !pinned && !DOUBLES_NAMES.contains(&name) { return Some(Box::new(SearchBot { weights: cfg.weights, depth: cfg.search_depth, @@ -243,11 +266,22 @@ pub fn build(name: &str, cfg: BotConfig) -> Option> { let w = cfg.weights; match name { RANDOM => Some(Box::new(RandomBot)), + EXAMPLE => Some(Box::new(crate::example_bot::ExampleBot::new(w))), GREEDY => Some(Box::new(GreedyBot { weights: w })), HEURISTIC => Some(Box::new(HeuristicBot { weights: w, state: HeuristicState::default() })), OVERRIDE => Some(Box::new(OverrideBot { weights: w, state: OverrideState::default() })), NOPEEK => Some(Box::new(NoPeekBot { weights: w, worst_case: false })), NOPEEK_WC => Some(Box::new(NoPeekBot { weights: w, worst_case: true })), + SEARCH_D1 | SEARCH_D2 => Some(Box::new(SearchBot { + weights: w, + depth: if name == SEARCH_D1 { 1 } else { 2 }, + peek: cfg.search_peek, + mixed: cfg.search_mixed, + })), + DOUBLES_SEARCH_D1 | DOUBLES_SEARCH_D2 => Some(Box::new(DoublesSearchBot { + depth: if name == DOUBLES_SEARCH_D1 { 1 } else { 2 }, + eval: cfg.doubles_eval, + })), SEARCH => Some(Box::new(SearchBot { weights: w, depth: cfg.search_depth.max(1), diff --git a/transpiler/strategies-rs/src/example_bot.rs b/transpiler/strategies-rs/src/example_bot.rs new file mode 100644 index 00000000..ee783d88 --- /dev/null +++ b/transpiler/strategies-rs/src/example_bot.rs @@ -0,0 +1,97 @@ +//! A worked example — copy this file to start your own bot. +//! +//! It plays a one-ply search: try every legal action, simulate it on a fork of +//! the real engine, score the resulting position, keep the best. That is the +//! whole shape of a bot, and it is already enough to beat `random` badly and +//! `heuristic` narrowly. +//! +//! To enter your own: +//! 1. copy this file to `src/my_bot.rs` and rename the struct +//! 2. add `pub mod my_bot;` to `src/lib.rs` +//! 3. add a name constant and one `build` arm in `src/bots.rs` +//! 4. `./bench.sh run --bot my-bot` + +use crate::bot::{Action, Bot, DecisionCtx}; +use crate::evaluator::Weights; +use crate::native::ForkCache; +use crate::sim::HypoMove; +use crate::view::{calculate_valid_moves, Mv, NO_OP_INDEX}; + +pub struct ExampleBot { + /// Weights the position evaluator scores with. Yours can score however you + /// like — this is just what the built-in evaluator needs. + pub weights: Weights, +} + +impl ExampleBot { + pub fn new(weights: Weights) -> Self { + ExampleBot { weights } + } +} + +impl Bot for ExampleBot { + fn name(&self) -> &'static str { + "example" + } + + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let view = ctx.singles_view(); + + // Every legal action this turn: moves, switches, and resting. Draw from + // ctx.rng and nothing else — that is what keeps your games comparable + // to everyone else's on the same seed. + let valid = calculate_valid_moves(ctx.sim, ctx.seat, view.bk, ctx.rng); + let candidates: Vec = valid + .moves + .iter() + .chain(valid.switches.iter()) + .chain(valid.no_op.iter()) + .copied() + .collect(); + if candidates.is_empty() { + return Action::Single(Mv { move_index: NO_OP_INDEX, extra_data: 0 }); + } + + // In blind mode ctx.peek is None and this is a guess; in peek mode it is + // the opponent's real move. Reading it through ctx.peek is the only way + // to see it, so a blind-mode bot cannot accidentally cheat. + let assumed_reply = ctx.peek.unwrap_or(Mv { move_index: 0, extra_data: 0 }); + + // On a forced-switch turn the opponent does not act at all. + let opponent_acts = view.switch_flag != 1; + + let mut forks = ForkCache::new(self.weights); + let mut best = candidates[0]; + let mut best_score = f64::NEG_INFINITY; + + for candidate in &candidates { + let their_move = opponent_acts.then(|| HypoMove { + move_index: assumed_reply.move_index, + salt: 0, + extra_data: assumed_reply.extra_data, + }); + // Fork the live battle and play both moves onto the copy. This is + // the real engine, so anything that would happen in a game happens + // here: damage rolls, abilities, effects, KOs. + let child = forks.fork( + ctx.sim, + ctx.seat, + their_move, + HypoMove { + move_index: candidate.move_index, + salt: 0, + extra_data: candidate.extra_data, + }, + ); + let score = forks.score(ctx.sim, ctx.seat, child); + if score > best_score { + best_score = score; + best = *candidate; + } + } + // Always release forks — they stay resident in the world until disposed. + forks.dispose_all(ctx.sim); + + Action::Single(best) + } +} diff --git a/transpiler/strategies-rs/src/lib.rs b/transpiler/strategies-rs/src/lib.rs index 2e54019b..545779c7 100644 --- a/transpiler/strategies-rs/src/lib.rs +++ b/transpiler/strategies-rs/src/lib.rs @@ -20,6 +20,7 @@ #![allow(non_snake_case)] // engine call sites keep Solidity spelling pub mod bot; +pub mod example_bot; pub mod bots; pub mod roster; pub mod analysis; From 5d2e6ec628abd02ab56d5d57e9ebf707b742c0ef Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:26:31 -0700 Subject: [PATCH 23/41] fix migration/roll flow --- snapshots/DoublesGasBenchmark.json | 76 +++++++++---------- snapshots/EngineOptimizationTest.json | 4 +- snapshots/FullyOptimizedInlineGasTest.json | 42 +++++------ snapshots/MainnetReplayGasTest.json | 12 ++- snapshots/MatchmakerTest.json | 6 +- snapshots/ProdPvPGasBenchmark.json | 8 +- snapshots/StartBattleGasTest.json | 24 +++--- src/game-layer/GachaTeamRegistry.sol | 7 ++ test/GachaMigrationTest.sol | 16 ++++ test/GachaTest.sol | 86 +++++++++++++++++----- 10 files changed, 178 insertions(+), 103 deletions(-) diff --git a/snapshots/DoublesGasBenchmark.json b/snapshots/DoublesGasBenchmark.json index 1c42baf9..c4677533 100644 --- a/snapshots/DoublesGasBenchmark.json +++ b/snapshots/DoublesGasBenchmark.json @@ -1,64 +1,64 @@ { - "DoublesFloor_AllAttack8_rawGas": "926951", - "DoublesFloor_AllAttack_perTurn": "115868", - "DoublesFloor_AllRest8_rawGas": "389089", - "DoublesFloor_AllRest_perTurn": "48636", - "DoublesFloor_OneAttack8_rawGas": "695767", - "DoublesFloor_OneAttack_perTurn": "86970", + "DoublesFloor_AllAttack8_rawGas": "912581", + "DoublesFloor_AllAttack_perTurn": "114072", + "DoublesFloor_AllRest8_rawGas": "375887", + "DoublesFloor_AllRest_perTurn": "46985", + "DoublesFloor_OneAttack8_rawGas": "682389", + "DoublesFloor_OneAttack_perTurn": "85298", "Doubles_Batch12_afterDamageCalls": "0", "Doubles_Batch12_afterMoveCalls": "0", - "Doubles_Batch12_coldGas": "790395", - "Doubles_Batch12_coldSload": "35", - "Doubles_Batch12_effectDataReads": "2", - "Doubles_Batch12_effectDataWrites": "1", - "Doubles_Batch12_effectHeaderReads": "6", - "Doubles_Batch12_effectHeaderWrites": "3", + "Doubles_Batch12_coldGas": "747703", + "Doubles_Batch12_coldSload": "34", + "Doubles_Batch12_effectDataReads": "0", + "Doubles_Batch12_effectDataWrites": "0", + "Doubles_Batch12_effectHeaderReads": "5", + "Doubles_Batch12_effectHeaderWrites": "2", "Doubles_Batch12_extraAccounts": "6", "Doubles_Batch12_lifecycleCalls": "1", "Doubles_Batch12_moveDecisionCallbacks": "0", - "Doubles_Batch12_nonHookHeaderReadUpperBound": "5", + "Doubles_Batch12_nonHookHeaderReadUpperBound": "4", "Doubles_Batch12_noop": "23", - "Doubles_Batch12_nzToNz": "33", + "Doubles_Batch12_nzToNz": "32", "Doubles_Batch12_preDamageCalls": "0", - "Doubles_Batch12_prodStorageGas": "521900", + "Doubles_Batch12_prodStorageGas": "486500", "Doubles_Batch12_roundEndCalls": "1", "Doubles_Batch12_roundStartCalls": "0", - "Doubles_Batch12_totalSload": "1675", - "Doubles_Batch12_totalSstore": "67", + "Doubles_Batch12_totalSload": "1570", + "Doubles_Batch12_totalSstore": "65", "Doubles_Batch12_updateStateCalls": "0", - "Doubles_Batch12_zToNz": "8", - "Doubles_Drain6_coldGas": "506626", - "Doubles_Drain6_coldSload": "41", + "Doubles_Batch12_zToNz": "7", + "Doubles_Drain6_coldGas": "473844", + "Doubles_Drain6_coldSload": "40", "Doubles_Drain6_extraAccounts": "7", "Doubles_Drain6_noop": "11", - "Doubles_Drain6_nzToNz": "28", - "Doubles_Drain6_prodStorageGas": "442400", - "Doubles_Drain6_totalSload": "891", - "Doubles_Drain6_totalSstore": "50", - "Doubles_Drain6_zToNz": "8", + "Doubles_Drain6_nzToNz": "27", + "Doubles_Drain6_prodStorageGas": "411800", + "Doubles_Drain6_totalSload": "834", + "Doubles_Drain6_totalSstore": "48", + "Doubles_Drain6_zToNz": "7", "Doubles_KitBatch10_afterDamageCalls": "0", "Doubles_KitBatch10_afterMoveCalls": "0", - "Doubles_KitBatch10_coldGas": "1042457", - "Doubles_KitBatch10_coldSload": "27", - "Doubles_KitBatch10_effectDataReads": "9", - "Doubles_KitBatch10_effectDataWrites": "1", - "Doubles_KitBatch10_effectHeaderReads": "11", - "Doubles_KitBatch10_effectHeaderWrites": "2", + "Doubles_KitBatch10_coldGas": "960450", + "Doubles_KitBatch10_coldSload": "26", + "Doubles_KitBatch10_effectDataReads": "0", + "Doubles_KitBatch10_effectDataWrites": "0", + "Doubles_KitBatch10_effectHeaderReads": "9", + "Doubles_KitBatch10_effectHeaderWrites": "1", "Doubles_KitBatch10_extraAccounts": "3", "Doubles_KitBatch10_lifecycleCalls": "9", "Doubles_KitBatch10_moveDecisionCallbacks": "0", - "Doubles_KitBatch10_nonHookHeaderReadUpperBound": "2", + "Doubles_KitBatch10_nonHookHeaderReadUpperBound": "0", "Doubles_KitBatch10_noop": "21", - "Doubles_KitBatch10_nzToNz": "60", + "Doubles_KitBatch10_nzToNz": "59", "Doubles_KitBatch10_preDamageCalls": "0", - "Doubles_KitBatch10_prodStorageGas": "628100", + "Doubles_KitBatch10_prodStorageGas": "576200", "Doubles_KitBatch10_roundEndCalls": "9", "Doubles_KitBatch10_roundStartCalls": "0", - "Doubles_KitBatch10_totalSload": "2010", - "Doubles_KitBatch10_totalSstore": "91", + "Doubles_KitBatch10_totalSload": "1740", + "Doubles_KitBatch10_totalSstore": "89", "Doubles_KitBatch10_updateStateCalls": "0", - "Doubles_KitBatch10_zToNz": "9", - "Doubles_Stage6_coldGas": "174663", + "Doubles_KitBatch10_zToNz": "8", + "Doubles_Stage6_coldGas": "174531", "Doubles_Stage6_coldSload": "18", "Doubles_Stage6_extraAccounts": "6", "Doubles_Stage6_noop": "0", diff --git a/snapshots/EngineOptimizationTest.json b/snapshots/EngineOptimizationTest.json index fbda3132..16698b9d 100644 --- a/snapshots/EngineOptimizationTest.json +++ b/snapshots/EngineOptimizationTest.json @@ -1,4 +1,4 @@ { - "ExternalStaminaRegen": "340364", - "InlineStaminaRegen": "800695" + "ExternalStaminaRegen": "337784", + "InlineStaminaRegen": "801929" } \ No newline at end of file diff --git a/snapshots/FullyOptimizedInlineGasTest.json b/snapshots/FullyOptimizedInlineGasTest.json index a1299948..1e62e786 100644 --- a/snapshots/FullyOptimizedInlineGasTest.json +++ b/snapshots/FullyOptimizedInlineGasTest.json @@ -1,29 +1,29 @@ { - "Fast_Battle1_coldGas": "1102097", - "Fast_Battle1_coldSload": "197", + "Fast_Battle1_coldGas": "1077548", + "Fast_Battle1_coldSload": "199", "Fast_Battle1_extraAccounts": "52", "Fast_Battle1_noop": "23", - "Fast_Battle1_nzToNz": "69", - "Fast_Battle1_prodStorageGas": "1292800", - "Fast_Battle1_totalSload": "981", - "Fast_Battle1_totalSstore": "114", - "Fast_Battle1_zToNz": "22", - "Fast_Battle2_coldGas": "1015486", - "Fast_Battle2_coldSload": "205", + "Fast_Battle1_nzToNz": "68", + "Fast_Battle1_prodStorageGas": "1273500", + "Fast_Battle1_totalSload": "977", + "Fast_Battle1_totalSstore": "112", + "Fast_Battle1_zToNz": "21", + "Fast_Battle2_coldGas": "1008744", + "Fast_Battle2_coldSload": "209", "Fast_Battle2_extraAccounts": "52", "Fast_Battle2_noop": "25", - "Fast_Battle2_nzToNz": "90", - "Fast_Battle2_prodStorageGas": "1205000", - "Fast_Battle2_totalSload": "1061", - "Fast_Battle2_totalSstore": "129", + "Fast_Battle2_nzToNz": "88", + "Fast_Battle2_prodStorageGas": "1206300", + "Fast_Battle2_totalSload": "1052", + "Fast_Battle2_totalSstore": "127", "Fast_Battle2_zToNz": "13", - "Fast_Battle3_coldGas": "736630", - "Fast_Battle3_coldSload": "197", + "Fast_Battle3_coldGas": "751629", + "Fast_Battle3_coldSload": "199", "Fast_Battle3_extraAccounts": "52", - "Fast_Battle3_noop": "30", - "Fast_Battle3_nzToNz": "81", - "Fast_Battle3_prodStorageGas": "931200", - "Fast_Battle3_totalSload": "981", - "Fast_Battle3_totalSstore": "114", - "Fast_Battle3_zToNz": "2" + "Fast_Battle3_noop": "28", + "Fast_Battle3_nzToNz": "80", + "Fast_Battle3_prodStorageGas": "951700", + "Fast_Battle3_totalSload": "977", + "Fast_Battle3_totalSstore": "112", + "Fast_Battle3_zToNz": "3" } \ No newline at end of file diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json index 800141e4..d1688caf 100644 --- a/snapshots/MainnetReplayGasTest.json +++ b/snapshots/MainnetReplayGasTest.json @@ -16,7 +16,7 @@ "MainnetDoublesReplay1_Reused_engineStorageUniqueTouchedSlots": "55", "MainnetDoublesReplay1_Reused_engineStorageUniqueWrittenSlots": "28", "MainnetDoublesReplay1_Reused_engineStorageWrites": "161", - "MainnetDoublesReplay1_Reused_execGas": "1208482", + "MainnetDoublesReplay1_Reused_execGas": "1206172", "MainnetDoublesReplay1_Reused_externalStorageReads": "6", "MainnetDoublesReplay1_Reused_externalStorageWrites": "0", "MainnetDoublesReplay1_Reused_headerFrameCallbackPassthroughOps": "144", @@ -108,7 +108,7 @@ "MainnetReplay_Reused_engineStorageUniqueTouchedSlots": "84", "MainnetReplay_Reused_engineStorageUniqueWrittenSlots": "51", "MainnetReplay_Reused_engineStorageWrites": "411", - "MainnetReplay_Reused_execGas": "2107241", + "MainnetReplay_Reused_execGas": "2102467", "MainnetReplay_Reused_externalStorageReads": "18", "MainnetReplay_Reused_externalStorageWrites": "0", "MainnetReplay_Reused_headerFrameCallbackPassthroughOps": "535", @@ -182,5 +182,11 @@ "MainnetReplay_Reused_storage_staticCatalogUniqueTouched": "30", "MainnetReplay_Reused_storage_staticCatalogUniqueWritten": "0", "MainnetReplay_Reused_storage_staticCatalogWrites": "0", - "MainnetReplay_Reused_updateStateCalls": "0" + "MainnetReplay_Reused_updateStateCalls": "0", + "SinglesFloor_25Noops_rawGas": "529900", + "SinglesFloor_BatchFixed": "253", + "SinglesFloor_Noop_perTurn": "21196", + "SinglesFloor_OneNoop_rawGas": "21449", + "SinglesFloor_SendIn_rawGas": "164580", + "SinglesFloor_Whole27_rawGas": "668477" } \ No newline at end of file diff --git a/snapshots/MatchmakerTest.json b/snapshots/MatchmakerTest.json index f6c22c66..b90b54b9 100644 --- a/snapshots/MatchmakerTest.json +++ b/snapshots/MatchmakerTest.json @@ -1,5 +1,5 @@ { - "Accept1": "332599", - "Accept2": "33821", - "Propose1": "175443" + "Accept1": "334700", + "Accept2": "33755", + "Propose1": "175377" } \ No newline at end of file diff --git a/snapshots/ProdPvPGasBenchmark.json b/snapshots/ProdPvPGasBenchmark.json index e7a2abe7..68fb2ed2 100644 --- a/snapshots/ProdPvPGasBenchmark.json +++ b/snapshots/ProdPvPGasBenchmark.json @@ -1,5 +1,5 @@ { - "BuiltinBare_5Turns_coldGas": "291616", + "BuiltinBare_5Turns_coldGas": "291816", "BuiltinBare_5Turns_coldSload": "75", "BuiltinBare_5Turns_extraAccounts": "15", "BuiltinBare_5Turns_noop": "10", @@ -8,7 +8,7 @@ "BuiltinBare_5Turns_totalSload": "435", "BuiltinBare_5Turns_totalSstore": "45", "BuiltinBare_5Turns_zToNz": "2", - "BuiltinHooked_5Turns_coldGas": "292434", + "BuiltinHooked_5Turns_coldGas": "292634", "BuiltinHooked_5Turns_coldSload": "75", "BuiltinHooked_5Turns_extraAccounts": "15", "BuiltinHooked_5Turns_noop": "10", @@ -17,7 +17,7 @@ "BuiltinHooked_5Turns_totalSload": "435", "BuiltinHooked_5Turns_totalSstore": "45", "BuiltinHooked_5Turns_zToNz": "2", - "Builtin_Drain3_coldGas": "146209", + "Builtin_Drain3_coldGas": "146299", "Builtin_Drain3_coldSload": "21", "Builtin_Drain3_extraAccounts": "3", "Builtin_Drain3_noop": "7", @@ -26,7 +26,7 @@ "Builtin_Drain3_totalSload": "236", "Builtin_Drain3_totalSstore": "24", "Builtin_Drain3_zToNz": "2", - "Builtin_Stage3_coldGas": "90115", + "Builtin_Stage3_coldGas": "90049", "Builtin_Stage3_coldSload": "9", "Builtin_Stage3_extraAccounts": "3", "Builtin_Stage3_noop": "0", diff --git a/snapshots/StartBattleGasTest.json b/snapshots/StartBattleGasTest.json index 6a45c92d..24893086 100644 --- a/snapshots/StartBattleGasTest.json +++ b/snapshots/StartBattleGasTest.json @@ -1,29 +1,29 @@ { - "StartBattle_Cold_coldGas": "1282501", + "StartBattle_Cold_coldGas": "1282524", "StartBattle_Cold_coldSload": "119", "StartBattle_Cold_extraAccounts": "3", - "StartBattle_Cold_noop": "8", + "StartBattle_Cold_noop": "9", "StartBattle_Cold_nzToNz": "18", - "StartBattle_Cold_prodStorageGas": "1542600", + "StartBattle_Cold_prodStorageGas": "1544800", "StartBattle_Cold_totalSload": "156", - "StartBattle_Cold_totalSstore": "83", + "StartBattle_Cold_totalSstore": "84", "StartBattle_Cold_zToNz": "57", - "StartBattle_WarmSame_coldGas": "229495", + "StartBattle_WarmSame_coldGas": "229518", "StartBattle_WarmSame_coldSload": "129", "StartBattle_WarmSame_extraAccounts": "3", - "StartBattle_WarmSame_noop": "72", + "StartBattle_WarmSame_noop": "73", "StartBattle_WarmSame_nzToNz": "5", - "StartBattle_WarmSame_prodStorageGas": "476900", + "StartBattle_WarmSame_prodStorageGas": "479100", "StartBattle_WarmSame_totalSload": "162", - "StartBattle_WarmSame_totalSstore": "82", + "StartBattle_WarmSame_totalSstore": "83", "StartBattle_WarmSame_zToNz": "4", - "StartBattle_WarmSteady_coldGas": "229495", + "StartBattle_WarmSteady_coldGas": "229518", "StartBattle_WarmSteady_coldSload": "129", "StartBattle_WarmSteady_extraAccounts": "3", - "StartBattle_WarmSteady_noop": "40", + "StartBattle_WarmSteady_noop": "41", "StartBattle_WarmSteady_nzToNz": "37", - "StartBattle_WarmSteady_prodStorageGas": "566500", + "StartBattle_WarmSteady_prodStorageGas": "568700", "StartBattle_WarmSteady_totalSload": "162", - "StartBattle_WarmSteady_totalSstore": "82", + "StartBattle_WarmSteady_totalSstore": "83", "StartBattle_WarmSteady_zToNz": "4" } \ No newline at end of file diff --git a/src/game-layer/GachaTeamRegistry.sol b/src/game-layer/GachaTeamRegistry.sol index a8cd85f1..4780140d 100644 --- a/src/game-layer/GachaTeamRegistry.sol +++ b/src/game-layer/GachaTeamRegistry.sol @@ -98,6 +98,7 @@ contract GachaTeamRegistry is // ----- Errors ----- error NotWhitelistedOpponent(); error AlreadyFirstRolled(); + error NotFirstRolled(); error InvalidStarterId(); error NoMoreStock(); error NotEngine(); @@ -599,6 +600,12 @@ contract GachaTeamRegistry is } function roll(uint256 numRolls) external returns (uint256[] memory rolledIds) { + // Owning nothing means the account skipped onboarding entirely: rolling here would + // leave it with mons but no starter and no team, which nothing downstream repairs. + // Both entry points (`firstRoll`, `migrate`) seed ownership, so one check covers them. + if (monsOwned[msg.sender].length() == 0) { + revert NotFirstRolled(); + } if (monsOwned[msg.sender].length() == monIds.length()) { revert NoMoreStock(); } diff --git a/test/GachaMigrationTest.sol b/test/GachaMigrationTest.sol index 8fc3cbf2..8bc03827 100644 --- a/test/GachaMigrationTest.sol +++ b/test/GachaMigrationTest.sol @@ -191,6 +191,22 @@ contract GachaMigrationTest is Test { assertTrue(newReg.migrated(BOB), "bob now migrated"); } + // roll()'s precondition keys off owned mons, which an import satisfies — a returning + // player must not be pushed back through firstRoll on the new registry. + function test_migratedPlayerCanRoll() public { + vm.startPrank(ALICE); + newReg.migrate(); + uint256[] memory rolled = newReg.roll(1); + vm.stopPrank(); + assertEq(rolled.length, 1, "migrated player can roll"); + } + + function test_roll_revertsBeforeImport() public { + vm.expectRevert(GachaTeamRegistry.NotFirstRolled.selector); + vm.prank(ALICE); + newReg.roll(1); + } + // ----- helpers ----- function _seedCatalog(GachaTeamRegistry reg) internal { diff --git a/test/GachaTest.sol b/test/GachaTest.sol index 09b723c4..2415f003 100644 --- a/test/GachaTest.sol +++ b/test/GachaTest.sol @@ -108,6 +108,48 @@ contract GachaTest is Test, BattleHelper { gachaRegistry.firstRoll(invalidStarter); } + // Granted points must not let an account roll straight past onboarding: it would + // end up owning mons with no starter and no team, which nothing downstream repairs. + function test_roll_beforeFirstRoll_reverts() public { + GachaTeamRegistry gachaRegistry = new GachaTeamRegistry(0, 0, engine, mockRNG, GachaTeamRegistry(address(0))); + uint256 poolSize = gachaRegistry.NUM_STARTERS() + gachaRegistry.INITIAL_ROLLS() - 1; + for (uint256 i = 0; i < poolSize; i++) { + gachaRegistry.createMon( + i, + MonStats({ + hp: 10, + stamina: 2, + speed: 2, + attack: 1, + defense: 1, + specialAttack: 1, + specialDefense: 1, + type1: Type.Fire, + type2: Type.None + }), + new uint256[](0), + new uint256[](0), + new bytes32[](0), + new bytes32[](0) + ); + } + // Fund the roll so the guard is what reverts, not the points underflow. + address[] memory selfList = new address[](1); + selfList[0] = address(this); + gachaRegistry.setAssigners(selfList, new address[](0)); + gachaRegistry.assignPoints(ALICE, 10 * gachaRegistry.ROLL_COST()); + + vm.expectRevert(GachaTeamRegistry.NotFirstRolled.selector); + vm.prank(ALICE); + gachaRegistry.roll(1); + + // Same account clears the guard once onboarding has actually run. + vm.startPrank(ALICE); + gachaRegistry.firstRoll(0); + assertEq(gachaRegistry.roll(1).length, 1, "roll works after firstRoll"); + vm.stopPrank(); + } + function test_firstRoll_emitsRollWithZeroSpend() public { GachaTeamRegistry gachaRegistry = new GachaTeamRegistry(0, 0, engine, mockRNG, GachaTeamRegistry(address(0))); uint256 poolSize = gachaRegistry.NUM_STARTERS() + gachaRegistry.INITIAL_ROLLS() - 1; @@ -392,25 +434,28 @@ contract GachaTest is Test, BattleHelper { // bonus must only fire once, even though a roll happens in between. GachaTeamRegistry gachaRegistry = new GachaTeamRegistry(0, 0, engine, mockRNG, GachaTeamRegistry(address(0))); - // One mon in the registry is enough for a single regular roll. - gachaRegistry.createMon( - 0, - MonStats({ - hp: 10, - stamina: 2, - speed: 2, - attack: 1, - defense: 1, - specialAttack: 1, - specialDefense: 1, - type1: Type.Fire, - type2: Type.None - }), - new uint256[](0), - new uint256[](0), - new bytes32[](0), - new bytes32[](0) - ); + // roll() requires prior ownership, so the pool has to cover a firstRoll too. + uint256 poolSize = gachaRegistry.NUM_STARTERS() + gachaRegistry.INITIAL_ROLLS() - 1; + for (uint256 i = 0; i < poolSize; i++) { + gachaRegistry.createMon( + i, + MonStats({ + hp: 10, + stamina: 2, + speed: 2, + attack: 1, + defense: 1, + specialAttack: 1, + specialDefense: 1, + type1: Type.Fire, + type2: Type.None + }), + new uint256[](0), + new uint256[](0), + new bytes32[](0), + new bytes32[](0) + ); + } Mon[] memory team = new Mon[](1); team[0] = Mon({ @@ -450,8 +495,9 @@ contract GachaTest is Test, BattleHelper { alicePointsAfterFirstBattle, gachaRegistry.FIRST_GAME_EVER_BONUS() + gachaRegistry.POINTS_PER_WIN() + 1 ); - // ---- Roll ---- + // ---- Roll ---- (firstRoll is free, so it leaves the point accounting alone) vm.startPrank(ALICE); + gachaRegistry.firstRoll(0); gachaRegistry.roll(1); vm.stopPrank(); From b9cd027a8400bf68a920a81253b597fc31a89cd7 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:23:23 -0700 Subject: [PATCH 24/41] clean up to focus on rust flow here --- .claude/skills/deploy-chomp/SKILL.md | 5 +- sims/arena/bench-doubles.ts | 96 --- sims/arena/bench-search.ts | 106 --- sims/arena/mon-data.ts | 3 - sims/arena/probe-greedy-scores.ts | 49 -- sims/arena/probe-initialize.ts | 81 --- sims/arena/smoke-games.ts | 44 -- sims/arena/walk.ts | 3 - sims/arena/winrate-search.ts | 47 -- sims/src/arena/fast-status-key.ts | 34 - sims/src/arena/game.ts | 137 ---- sims/src/arena/mon-data-core.ts | 134 ---- sims/src/arena/mon-data-worker.ts | 14 - sims/src/arena/mon-data.ts | 150 ---- sims/src/arena/rng-oracle.ts | 45 -- sims/src/arena/rng.ts | 21 - sims/src/arena/team-builder.ts | 39 - sims/src/arena/team.ts | 153 ---- sims/src/arena/transpose.ts | 89 --- sims/src/arena/walk.ts | 191 ----- sims/src/cpu/battle-view.ts | 166 ----- sims/src/cpu/constants.ts | 4 - sims/src/cpu/doubles-search.ts | 389 ---------- sims/src/cpu/engine-view.ts | 317 -------- sims/src/cpu/evaluator-ttk.ts | 141 ---- sims/src/cpu/evaluator.ts | 101 --- sims/src/cpu/forward-model.ts | 333 --------- sims/src/cpu/heuristic-native.ts | 549 -------------- sims/src/cpu/heuristic-shared.ts | 718 ------------------- sims/src/cpu/hex.ts | 2 - sims/src/cpu/index.ts | 63 -- sims/src/cpu/mon-meta.ts | 42 -- sims/src/cpu/registry.ts | 34 - sims/src/cpu/strategies/greedy-eval.ts | 97 --- sims/src/cpu/strategies/hard-cpu.ts | 617 ---------------- sims/src/cpu/strategies/override-cpu.ts | 200 ------ sims/src/cpu/strategies/search-cpu.ts | 140 ---- sims/src/cpu/strategy.ts | 74 -- transpiler/codegen_rs/README.md | 4 +- transpiler/strategies-rs/src/evaluator.rs | 2 +- transpiler/strategies-rs/src/game.rs | 5 +- transpiler/strategies-rs/src/greedy.rs | 7 +- transpiler/strategies-rs/src/heuristic.rs | 9 +- transpiler/strategies-rs/src/jsrng.rs | 6 +- transpiler/strategies-rs/src/lib.rs | 17 +- transpiler/strategies-rs/src/native.rs | 9 +- transpiler/strategies-rs/src/override_cpu.rs | 2 +- transpiler/strategies-rs/src/roster.rs | 2 +- transpiler/strategies-rs/src/shared.rs | 13 +- transpiler/strategies-rs/src/view.rs | 5 +- 50 files changed, 41 insertions(+), 5468 deletions(-) delete mode 100644 sims/arena/bench-doubles.ts delete mode 100644 sims/arena/bench-search.ts delete mode 100644 sims/arena/mon-data.ts delete mode 100644 sims/arena/probe-greedy-scores.ts delete mode 100644 sims/arena/probe-initialize.ts delete mode 100644 sims/arena/smoke-games.ts delete mode 100644 sims/arena/walk.ts delete mode 100644 sims/arena/winrate-search.ts delete mode 100644 sims/src/arena/fast-status-key.ts delete mode 100644 sims/src/arena/game.ts delete mode 100644 sims/src/arena/mon-data-core.ts delete mode 100644 sims/src/arena/mon-data-worker.ts delete mode 100644 sims/src/arena/mon-data.ts delete mode 100644 sims/src/arena/rng-oracle.ts delete mode 100644 sims/src/arena/rng.ts delete mode 100644 sims/src/arena/team-builder.ts delete mode 100644 sims/src/arena/team.ts delete mode 100644 sims/src/arena/transpose.ts delete mode 100644 sims/src/arena/walk.ts delete mode 100644 sims/src/cpu/battle-view.ts delete mode 100644 sims/src/cpu/constants.ts delete mode 100644 sims/src/cpu/doubles-search.ts delete mode 100644 sims/src/cpu/engine-view.ts delete mode 100644 sims/src/cpu/evaluator-ttk.ts delete mode 100644 sims/src/cpu/evaluator.ts delete mode 100644 sims/src/cpu/forward-model.ts delete mode 100644 sims/src/cpu/heuristic-native.ts delete mode 100644 sims/src/cpu/heuristic-shared.ts delete mode 100644 sims/src/cpu/hex.ts delete mode 100644 sims/src/cpu/index.ts delete mode 100644 sims/src/cpu/mon-meta.ts delete mode 100644 sims/src/cpu/registry.ts delete mode 100644 sims/src/cpu/strategies/greedy-eval.ts delete mode 100644 sims/src/cpu/strategies/hard-cpu.ts delete mode 100644 sims/src/cpu/strategies/override-cpu.ts delete mode 100644 sims/src/cpu/strategies/search-cpu.ts delete mode 100644 sims/src/cpu/strategy.ts diff --git a/.claude/skills/deploy-chomp/SKILL.md b/.claude/skills/deploy-chomp/SKILL.md index ae4ff871..896eb48b 100644 --- a/.claude/skills/deploy-chomp/SKILL.md +++ b/.claude/skills/deploy-chomp/SKILL.md @@ -54,8 +54,9 @@ former bun↔Rust FFI seam (`chomp_run_games`, the `ffi` crate, and `scripts/batch_benchmark.ts`) was removed once the pure-Rust arena replaced it; the verification-era machinery (golden-vector suites, replay fixtures, the drive-mode adapter, lockstep gates) lives in git history if parity ever -needs re-proving. The TS arena driver (`sims/src/arena/game.ts`) remains as -the port-back reference. +needs re-proving. The TS arena was deleted once the pure-Rust one replaced +it; strategies that ship are hand-ported into munch and gated there against +`sim-tests/arena/cpu-reference.json`. **Known limitation — TODO when a second codebase needs it.** The transpiler hardcodes three TS namespace names (`Enums`, `Structs`, `Constants`) for type-only Solidity files. File-type detection is content-based (a file with only enums maps to the `Enums` namespace regardless of filename), but the *namespace name itself* is fixed, and only structs are tracked per source path. A codebase that splits types across multiple files (e.g. `PoolStructs.sol` + `OrderStructs.sol`, or `Errors.sol`) would produce colliding imports. To generalize: add `enum_paths` / `constant_paths` to `transpiler/type_system/registry.py` mirroring `struct_paths`, derive the namespace name from each source file's basename, and have `imports.py:_generate_module_imports` emit one `import * as ` per actually-referenced source module instead of three blanket imports. diff --git a/sims/arena/bench-doubles.ts b/sims/arena/bench-doubles.ts deleted file mode 100644 index 95a69dd3..00000000 --- a/sims/arena/bench-doubles.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Doubles TS benchmark + win-rate: the ported joint maximin search (side 1) vs the greedy "Hard" - * pilot (side 0). Rust reference: search d1 ≈ 57.5% vs Hard; d1 decision ≈ sub-ms native. - * - * bun arena/bench-doubles.ts [games] - */ -import { makeSimContext, startBattle, executeSlotTurn } from '../src/harness'; -import { loadRoster } from '../src/util/csv-load'; -import { buildTeamMon } from '../src/arena/team'; -import { MemoizedInlineRngOracle } from '../src/arena/rng-oracle'; -import { makeRng } from '../src/arena/rng'; -import { packSide } from '../src/cpu/forward-model'; -import { searchSideMoves, greedySideMoves, type SlotMove } from '../src/cpu/doubles-search'; - -const games = Number(process.argv[2] ?? 20); -const roster = loadRoster(); -const allIds = roster.mons.map((m) => m.id); - -type Ctx = ReturnType; - -function newDoublesBattle(seed: number): { ctx: Ctx; bk: `0x${string}` } { - const rng = makeRng(seed); - const pool = [...allIds]; - const draw = (): number => pool.splice(Math.floor(rng() * pool.length), 1)[0]; - const ctx = makeSimContext({ monsPerTeam: 4n }); - const p0 = [draw(), draw(), draw(), draw()].map((id) => buildTeamMon(ctx, roster, id)); - const p1 = [draw(), draw(), draw(), draw()].map((id) => buildTeamMon(ctx, roster, id)); - const { battleKey } = startBattle(ctx, p0, p1, 1); // BATTLE_MODE_DOUBLES - const engine = ctx.engine as any; - engine.battleConfig[engine._getStorageKey(battleKey)].rngOracle = new MemoizedInlineRngOracle(); - return { ctx, bk: battleKey }; -} - -type Pilot = (e: any, bk: `0x${string}`, side: 0 | 1) => [SlotMove, SlotMove]; - -function playDoubles(seed: number, p1Pilot: Pilot, p0Pilot: Pilot, maxTurns = 200): 0 | 1 | null { - const { ctx, bk } = newDoublesBattle(seed); - const engine = ctx.engine as any; - const salt = makeRng(seed ^ 0xabcdef); - for (let t = 0; t < maxTurns; t++) { - const w = Number(engine.battleData[bk].winnerIndex); - if (w !== 2) return w as 0 | 1; - const m0 = p0Pilot(engine, bk, 0); - const m1 = p1Pilot(engine, bk, 1); - const s0 = packSide(m0[0].moveIndex, m0[0].extraData, m0[1].moveIndex, m0[1].extraData, BigInt(Math.floor(salt() * 2 ** 30))); - const s1 = packSide(m1[0].moveIndex, m1[0].extraData, m1[1].moveIndex, m1[1].extraData, BigInt(Math.floor(salt() * 2 ** 30))); - executeSlotTurn(ctx, bk, s0, s1); - } - const w = Number(engine.battleData[bk].winnerIndex); - return w !== 2 ? (w as 0 | 1) : null; -} - -const greedy: Pilot = (e, bk, side) => greedySideMoves(e, bk, side); -const searchD1: Pilot = (e, bk, side) => searchSideMoves(e, bk, side, 1); - -// ── 1. Decision timing at a mid-game both-act position ────────────────────── -{ - const { ctx, bk } = newDoublesBattle(777); - const engine = ctx.engine as any; - const salt = makeRng(3); - for (let t = 0; t < 6; t++) { - if (Number(engine.battleData[bk].winnerIndex) !== 2) break; - const m0 = greedySideMoves(engine, bk, 0); - const m1 = greedySideMoves(engine, bk, 1); - executeSlotTurn( - ctx, bk, - packSide(m0[0].moveIndex, m0[0].extraData, m0[1].moveIndex, m0[1].extraData, BigInt(Math.floor(salt() * 2 ** 30))), - packSide(m1[0].moveIndex, m1[0].extraData, m1[1].moveIndex, m1[1].extraData, BigInt(Math.floor(salt() * 2 ** 30))), - ); - } - const flag = Number(engine.getBattleContext(bk).playerSwitchForTurnFlag); - console.log(`doubles mid-game ready (turn ${engine.getTurnIdForBattleState(bk)}, flag ${flag})`); - for (const [label, f, iters] of [ - ['greedy side-decision', () => greedySideMoves(engine, bk, 1), 50], - ['search d1 side-decision', () => searchSideMoves(engine, bk, 1, 1), 5], - ] as Array<[string, () => unknown, number]>) { - for (let i = 0; i < 2; i++) f(); - const t0 = performance.now(); - for (let i = 0; i < iters; i++) f(); - console.log(`${label.padEnd(28)} ${((performance.now() - t0) / iters).toFixed(1)} ms/op (${iters} iters)`); - } -} - -// ── 2. Win rate: search d1 (side 1) vs greedy Hard (side 0) ───────────────── -let wins = 0, losses = 0, draws = 0; -const t0 = performance.now(); -for (let g = 0; g < games; g++) { - const w = playDoubles(4000 + g, searchD1, greedy); - if (w === 1) wins++; - else if (w === 0) losses++; - else draws++; -} -const secs = (performance.now() - t0) / 1000; -console.log( - `\nsearch d1 vs greedy-hard: ${wins}W-${losses}L-${draws}D → ${((wins / Math.max(1, wins + losses)) * 100).toFixed(1)}% · ${games} games in ${secs.toFixed(1)}s (${((secs / games) * 1000).toFixed(0)} ms/game)`, -); diff --git a/sims/arena/bench-search.ts b/sims/arena/bench-search.ts deleted file mode 100644 index a408934a..00000000 --- a/sims/arena/bench-search.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Benchmark: what would the new search CPUs cost per decision in TypeScript? - * - * Measures the primitive the search is built from — a hypothetical fork (clone battle - * + run one silent turn + capture view) — plus the existing 1-ply strategies for - * context, on a real mid-game 4v4 position. Projects per-decision latency for the - * search configs from the Rust-measured forks-per-decision bands. - * - * bun arena/bench-search.ts - */ -import { makeSimContext, startBattle, executeTurn } from '../src/harness'; -import { loadRoster } from '../src/util/csv-load'; -import { buildTeamMon } from '../src/arena/team'; -import { MemoizedInlineRngOracle } from '../src/arena/rng-oracle'; -import { makeRng } from '../src/arena/rng'; -import { captureBattleView } from '../src/cpu/battle-view'; -import { applyHypotheticalMove } from '../src/cpu/forward-model'; -import { CPU_STRATEGIES } from '../src/cpu/registry'; -import * as Constants from '../../transpiler/ts-output/Constants'; - -const roster = loadRoster(); -const SWITCH = Number(Constants.SWITCH_MOVE_INDEX); - -// ── Stand up a 4v4 battle and walk it to a mid-game position ──────────────── -const ctx = makeSimContext({ monsPerTeam: 4n }); -const ids = roster.mons.map((m: any) => m.id); -const p0Team = ids.slice(0, 4).map((id: number) => buildTeamMon(ctx, roster, id)); -const p1Team = ids.slice(4, 8).map((id: number) => buildTeamMon(ctx, roster, id)); -const { battleKey } = startBattle(ctx, p0Team, p1Team); -const engine = ctx.engine as any; -engine.battleConfig[engine._getStorageKey(battleKey)].rngOracle = new MemoizedInlineRngOracle(); - -// Leads, then a few attack turns; resolve forced switches mid-walk (first legal bench) so the -// bench lands on a genuine both-act (flag == 2) position — the search's real per-turn workload. -executeTurn(ctx, battleKey, { p0MoveIndex: SWITCH, p1MoveIndex: SWITCH, p0ExtraData: 0n, p1ExtraData: 0n, p0Salt: 1n, p1Salt: 2n }); -const NO_OP = Number(Constants.NO_OP_MOVE_INDEX); -const firstBench = (side: 0 | 1): bigint => { - const ko = Number(engine.getKOBitmap(battleKey, BigInt(side))); - const ctx = engine.getBattleContext(battleKey); - const active = Number(side === 0 ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex); - for (let i = 0; i < 4; i++) if (i !== active && (ko & (1 << i)) === 0) return BigInt(i); - return 0n; -}; -for (let t = 0; t < 6; t++) { - const flag = Number(engine.getBattleContext(battleKey).playerSwitchForTurnFlag); - if (flag === 2) { - executeTurn(ctx, battleKey, { - p0MoveIndex: 0, p1MoveIndex: 0, p0ExtraData: 0n, p1ExtraData: 0n, - p0Salt: BigInt(100 + t), p1Salt: BigInt(200 + t), - }); - } else { - // Forced-switch turn: only the flagged side acts. - executeTurn(ctx, battleKey, { - p0MoveIndex: flag === 0 ? SWITCH : NO_OP, p1MoveIndex: flag === 1 ? SWITCH : NO_OP, - p0ExtraData: flag === 0 ? firstBench(0) : 0n, p1ExtraData: flag === 1 ? firstBench(1) : 0n, - p0Salt: BigInt(300 + t), p1Salt: BigInt(400 + t), - }); - } -} -const finalFlag = Number(engine.getBattleContext(battleKey).playerSwitchForTurnFlag); -console.log(`mid-game position ready (turn ${engine.getTurnIdForBattleState(battleKey)}, flag ${finalFlag})`); - -function bench(label: string, iters: number, warmup: number, f: () => void): number { - for (let i = 0; i < warmup; i++) f(); - const t0 = performance.now(); - for (let i = 0; i < iters; i++) f(); - const ms = (performance.now() - t0) / iters; - console.log(`${label.padEnd(34)} ${ms.toFixed(3)} ms/op (${iters} iters)`); - return ms; -} - -// ── 1. The search primitive: hypothetical fork (fork + silent turn + view) ── -const msFork = bench('applyHypotheticalMove (1 fork)', 200, 20, () => { - applyHypotheticalMove(engine, battleKey, { moveIndex: 0, salt: 0n, extraData: 0 }, { moveIndex: 0, salt: 0n, extraData: 0 }); -}); - -// ── 2. Existing 1-ply strategies for context ───────────────────────────────── -const rng = makeRng(42); -const view = () => captureBattleView(engine, battleKey); -const greedy = CPU_STRATEGIES.get('greedy')!; -const hard = CPU_STRATEGIES.get('hard')!; -const msGreedy = bench('greedy.decide (1-ply)', 50, 5, () => { - greedy.decide(view(), { moveIndex: 0, extraData: 0 }, rng, greedy.createState()); -}); -let msHard = Number.NaN; -try { - msHard = bench('hard.decide (heuristic)', 50, 5, () => { - hard.decide(view(), { moveIndex: 0, extraData: 0 }, rng, hard.createState()); - }); -} catch (e) { - console.log(`hard.decide: SKIPPED (stale vs current engine surface: ${(e as Error).message})`); -} - -// ── 3. REAL search decisions (the ported SearchCpu) ───────────────────────── -import { SearchCpu } from '../src/cpu/strategies/search-cpu'; -for (const [label, depth, peek, iters] of [ - ['search d1 no-peek', 1, false, 20], - ['search d2 PEEK', 2, true, 10], - ['search d2 no-peek', 2, false, 5], -] as Array<[string, number, boolean, number]>) { - const s = new SearchCpu(label, depth, peek); - bench(`${label}.decide`, iters, 2, () => { - s.decide(view(), { moveIndex: 0, extraData: 0 }, rng, s.createState()); - }); -} -console.log(`\nms/fork: TS=${msFork.toFixed(3)} (greedy decision=${msGreedy.toFixed(1)}ms, hard=${msHard.toFixed(1)}ms)`); diff --git a/sims/arena/mon-data.ts b/sims/arena/mon-data.ts deleted file mode 100644 index 8cf8e37c..00000000 --- a/sims/arena/mon-data.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Entry shim so `bun arena/mon-data.ts` (run from sims/) reaches the real script under src/arena. -// process.argv is global, so flags pass straight through. -import '../src/arena/mon-data.ts'; diff --git a/sims/arena/probe-greedy-scores.ts b/sims/arena/probe-greedy-scores.ts deleted file mode 100644 index 610402ea..00000000 --- a/sims/arena/probe-greedy-scores.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Probe: what does greedy's own forward model score for each candidate on a board where - * Inutia's Initialize lock is set? Prints the per-candidate scores so the selection bug's - * layer (fork vs evaluator vs tie-break) is measured instead of guessed. - * - * bun arena/probe-greedy-scores.ts - */ -import { makeSimContext, startBattle, executeTurn } from '../src/harness'; -import { loadRoster } from '../src/util/csv-load'; -import { buildTeamMon } from '../src/arena/team'; -import { MemoizedInlineRngOracle } from '../src/arena/rng-oracle'; -import { captureBattleView } from '../src/cpu/battle-view'; -import { applyHypotheticalMove, disposeFork } from '../src/cpu/forward-model'; -import { scoreStateWith, DEFAULT_EVAL_WEIGHTS } from '../src/cpu/evaluator'; -import * as Constants from '../../transpiler/ts-output/Constants'; - -const roster = loadRoster(); -const monByName = (n: string) => roster.mons.find((r: any) => r.name === n)!; - -const ctx = makeSimContext({ monsPerTeam: 1n }); -const p0Team = [buildTeamMon(ctx, roster, monByName('Gorillax').id)]; -const p1Team = [buildTeamMon(ctx, roster, monByName('Inutia').id)]; -const { battleKey } = startBattle(ctx, p0Team, p1Team); -const engine = ctx.engine as any; -engine.battleConfig[engine._getStorageKey(battleKey)].rngOracle = new MemoizedInlineRngOracle(); - -const SWITCH = Number(Constants.SWITCH_MOVE_INDEX); -const NO_OP = Number(Constants.NO_OP_MOVE_INDEX); - -// Leads, then t1: Inutia (p1) casts Initialize for real — lock now set, boost live. -executeTurn(ctx, battleKey, { p0MoveIndex: SWITCH, p1MoveIndex: SWITCH, p0ExtraData: 0n, p1ExtraData: 0n, p0Salt: 1n, p1Salt: 2n }); -executeTurn(ctx, battleKey, { p0MoveIndex: NO_OP, p1MoveIndex: 1, p0ExtraData: 0n, p1ExtraData: 0n, p0Salt: 3n, p1Salt: 4n }); - -// Score each p1 candidate against a revealed p0 rest, exactly as greedy does (salt 0). -const view = captureBattleView(engine, battleKey); -const candidates: Array<[string, number]> = [ - ['Chain Expansion (0)', 0], - ['Initialize LOCKED (1)', 1], - ['Big Bite (2)', 2], - ['Hit And Dip (3)', 3], - ['rest (126)', NO_OP], -]; -console.log('board: p1 Inutia, Initialize cast last turn (lock set, +50% live); p0 Gorillax rests'); -for (const [label, idx] of candidates) { - const fork = applyHypotheticalMove(engine, view.bk as any, { moveIndex: NO_OP, salt: 0n, extraData: 0 }, { moveIndex: idx, salt: 0n, extraData: 0 }); - const score = scoreStateWith(fork, DEFAULT_EVAL_WEIGHTS); - console.log(` ${label.padEnd(24)} -> ${score.toFixed(2)}`); - disposeFork(engine, fork.bk); -} diff --git a/sims/arena/probe-initialize.ts b/sims/arena/probe-initialize.ts deleted file mode 100644 index c456d034..00000000 --- a/sims/arena/probe-initialize.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Probe: how many of greedy's Inutia Initialize casts are silent no-ops (cast while the - * once-per-send-out lock is set)? Gap-list instrument for design-pass v4's Inutia section; - * the pre-registered prediction there is ">= 40% of casts are locked". - * - * Lock proxy: Inutia's ATK delta > 0 at selection time — Initialize is the only source of a - * positive ATK delta on Inutia, the boost is Temp (shed on switch-out), and the lock also - * clears on switch-out, so boost-live and lock-set coincide. - * - * bun arena/probe-initialize.ts [--games N] - */ -import { playGame } from '../src/arena/game'; -import { GreedyEvalCpu } from '../src/cpu/strategies/greedy-eval'; -import { activeMonIndices, monMaxHp } from '../src/cpu/engine-view'; -import { MonStateIndexName } from '../../transpiler/ts-output/Enums'; -import { SWITCH_MOVE_INDEX } from '../src/cpu/constants'; - -const args = process.argv.slice(2); -const gi = args.indexOf('--games'); -const GAMES = gi >= 0 ? Number(args[gi + 1]) : 300; - -const INUTIA_ID = 1; -const INUTIA_HP = 351; -const INITIALIZE_SLOT = 1; // default equip order: CE 0, Initialize 1, Big Bite 2, Hit And Dip 3 - -let moveTurns = 0; -let initCasts = 0; -let lockedCasts = 0; -let switches = 0; -let errors = 0; - -function mulberry(seed: number): () => number { - let a = seed >>> 0; - return () => { - a |= 0; a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -function draftTeam(rng: () => number, forceInutia: boolean): { id: number }[] { - const pool = Array.from({ length: 13 }, (_, i) => i); - const picked: number[] = forceInutia ? [INUTIA_ID] : []; - while (picked.length < 4) { - const c = pool[Math.floor(rng() * pool.length)]; - if (!picked.includes(c)) picked.push(c); - } - return picked.map((id) => ({ id })); -} - -for (let g = 0; g < GAMES; g++) { - const rng = mulberry(g + 1); - const teams: [{ id: number }[], { id: number }[]] = [draftTeam(rng, g % 2 === 0), draftTeam(rng, g % 2 === 1)]; - const outcome = playGame(new GreedyEvalCpu(), new GreedyEvalCpu(), teams as any, g + 1, 150, ({ engine, battleKey, p0Move, p1Move }) => { - const [p0Active, p1Active] = activeMonIndices(engine, battleKey); - const sides: Array<[bigint, number, { moveIndex: number } | null]> = [ - [0n, p0Active, p0Move], - [1n, p1Active, p1Move], - ]; - for (const [player, idx, mv] of sides) { - if (!mv) continue; - if (monMaxHp(engine, battleKey, player, idx) !== INUTIA_HP) continue; - if (mv.moveIndex === Number(SWITCH_MOVE_INDEX)) { switches++; continue; } - if (mv.moveIndex > 3) continue; // rest / other non-move actions - moveTurns++; - if (mv.moveIndex === INITIALIZE_SLOT) { - initCasts++; - const atkDelta = Number(engine.getMonStateForBattle(battleKey, player, BigInt(idx), MonStateIndexName.Attack)); - if (atkDelta > 0) lockedCasts++; - } - } - }); - if ('error' in outcome) errors++; -} - -const pct = (n: number, d: number) => (d === 0 ? '0' : ((100 * n) / d).toFixed(1)); -console.log(`games ${GAMES} (errors ${errors}) greedy-vs-greedy, Inutia forced onto one seat per game`); -console.log(`Inutia move-turns: ${moveTurns} switches: ${switches}`); -console.log(`Initialize casts: ${initCasts} (${pct(initCasts, moveTurns)}% of move-turns)`); -console.log(` cast while locked (ATK delta > 0): ${lockedCasts} (${pct(lockedCasts, initCasts)}% of casts)`); diff --git a/sims/arena/smoke-games.ts b/sims/arena/smoke-games.ts deleted file mode 100644 index 3f0c15f4..00000000 --- a/sims/arena/smoke-games.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Smoke: full TS games after the staleness fixes — greedy & hard on both seats - * (exercises the transpose proxy, forward model, and both strategies end-to-end). - * - * bun arena/smoke-games.ts - */ -import { playGame } from '../src/arena/game'; -import { CPU_STRATEGIES } from '../src/cpu/registry'; -import { loadRoster } from '../src/util/csv-load'; -import { monCatalog } from '../src/arena/team'; -import type { DraftedMon } from '../src/arena/team'; - -const roster = loadRoster(); -const draft = (ids: number[]): DraftedMon[] => - ids.map((id) => { - const mon = roster.mons.find((m) => m.id === id)!; - const n = Math.min(4, monCatalog(roster, mon).length); - return { id, equip: Array.from({ length: n }, (_, i) => i) }; - }); - -const greedy = CPU_STRATEGIES.get('greedy')!; -const hard = CPU_STRATEGIES.get('hard')!; -const pairs: Array<[string, any, any]> = [ - ['greedy vs greedy', greedy, greedy], - ['hard vs greedy', hard, greedy], - ['greedy vs hard ', greedy, hard], -]; - -let ok = 0, err = 0; -for (const [label, p1, p0] of pairs) { - for (let g = 0; g < 4; g++) { - const ids = [...roster.mons.map((m) => m.id)].sort(() => 0.5 - Math.abs(Math.sin(g * 13 + ok))); // deterministic-ish shuffle - const teams: [DraftedMon[], DraftedMon[]] = [draft(ids.slice(0, 4)), draft(ids.slice(4, 8))]; - const out = playGame(p1, p0, teams, 1000 + g, 200); - if ('error' in out) { - err++; - console.log(`${label} game ${g}: ERROR ${out.error}`); - } else { - ok++; - console.log(`${label} game ${g}: winner seat ${out.winnerSeat} in ${out.turns} turns`); - } - } -} -console.log(`\n${ok} games completed, ${err} errors`); diff --git a/sims/arena/walk.ts b/sims/arena/walk.ts deleted file mode 100644 index 54ebc8d1..00000000 --- a/sims/arena/walk.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Entry shim so `bun arena/walk.ts` (run from sims/) reaches the real script under src/arena. -// process.argv is global, so flags pass straight through. -import '../src/arena/walk.ts'; diff --git a/sims/arena/winrate-search.ts b/sims/arena/winrate-search.ts deleted file mode 100644 index d50c515d..00000000 --- a/sims/arena/winrate-search.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Win-rate sanity for the ported SearchCpu vs greedy — direction should match the Rust arena - * (d2 no-peek ≈ 65%, d2 peek ≈ 86% vs greedy over matched drafts). - * - * bun arena/winrate-search.ts [games-per-config] - */ -import { playGame } from '../src/arena/game'; -import { CPU_STRATEGIES } from '../src/cpu/registry'; -import { makeRng } from '../src/arena/rng'; -import { loadRoster } from '../src/util/csv-load'; -import { monCatalog } from '../src/arena/team'; -import type { DraftedMon } from '../src/arena/team'; - -const games = Number(process.argv[2] ?? 30); -const roster = loadRoster(); -const allIds = roster.mons.map((m) => m.id); - -function draftTeams(seed: number): [DraftedMon[], DraftedMon[]] { - const rng = makeRng(seed); - const pool = [...allIds]; - const draw = (): number => pool.splice(Math.floor(rng() * pool.length), 1)[0]; - const draft = (ids: number[]): DraftedMon[] => - ids.map((id) => { - const mon = roster.mons.find((m) => m.id === id)!; - const n = Math.min(4, monCatalog(roster, mon).length); - return { id, equip: Array.from({ length: n }, (_, i) => i) }; - }); - return [draft([draw(), draw(), draw(), draw()]), draft([draw(), draw(), draw(), draw()])]; -} - -const greedy = CPU_STRATEGIES.get('greedy')!; -for (const key of ['search-peek', 'search']) { - const strat = CPU_STRATEGIES.get(key)!; - let wins = 0, losses = 0, errs = 0; - const t0 = performance.now(); - for (let g = 0; g < games; g++) { - const out = playGame(strat, greedy, draftTeams(5000 + g), 9000 + g, 200); - if ('error' in out) errs++; - else if (out.winnerSeat === 1) wins++; - else if (out.winnerSeat === 0) losses++; - } - const secs = (performance.now() - t0) / 1000; - const wr = (wins / Math.max(1, wins + losses)) * 100; - console.log( - `${key.padEnd(12)} vs greedy: ${wins}W-${losses}L (${errs} err) → ${wr.toFixed(1)}% · ${games} games in ${secs.toFixed(1)}s (${(secs / games * 1000).toFixed(0)} ms/game)`, - ); -} diff --git a/sims/src/arena/fast-status-key.ts b/sims/src/arena/fast-status-key.ts deleted file mode 100644 index 984e2b9b..00000000 --- a/sims/src/arena/fast-status-key.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { StatusEffectLib } from '../../../transpiler/ts-output/effects/status/StatusEffectLib'; - -/** - * SIM-ONLY perf install (imported for its side effect). `StatusEffectLib.getKeyForMonIndex` derives a - * status-effect storage key as `keccak256(encodePacked(STATUS_EFFECT, playerIndex, monIndex)) & mask64`. - * It is PURE — the only varying inputs are `playerIndex ∈ {0,1}` and `monIndex ∈ {0..7}`, so there are - * 16 possible results — yet the engine re-derives it ~8x per turn and every forward-model fork replays - * those turns, making it the dominant keccak in arena runs (~9x the volume of the per-turn RNG hash). - * - * The effects call it through the module singleton `statusEffectLib`, which shares this prototype, so - * memoizing the prototype method covers every caller with a ≤16-entry cache. Values are byte identical; - * only the repeated hashing is skipped. (A general proxy-layer memo was measured and REJECTED: gating - * every external contract call cost more than the keccak it saved — a targeted memo on this one hot pure - * method is strictly cheaper.) Lives in `sims/` (never the deployed contracts) and survives transpiler - * regeneration since the method name is stable. - */ -const proto = StatusEffectLib.prototype as unknown as { - getKeyForMonIndex(playerIndex: bigint, monIndex: bigint): bigint; - __simMemoizedKey?: boolean; -}; -if (!proto.__simMemoizedKey) { - const orig = proto.getKeyForMonIndex; - const cache = new Map(); - proto.getKeyForMonIndex = function (playerIndex: bigint, monIndex: bigint): bigint { - const k = Number(playerIndex) * 256 + Number(monIndex); - let v = cache.get(k); - if (v === undefined) { - v = orig.call(this, playerIndex, monIndex); - cache.set(k, v); - } - return v; - }; - proto.__simMemoizedKey = true; -} diff --git a/sims/src/arena/game.ts b/sims/src/arena/game.ts deleted file mode 100644 index b0a4ded2..00000000 --- a/sims/src/arena/game.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Single CPU-vs-CPU game driver for the balance arena on the TS engine - * (`sims/src/harness.ts` over `transpiler/ts-output`). Bulk simulation now - * lives entirely on the native Rust stack (`transpiler/strategies-rs`, the - * `arena`/`trace` bins); this TS driver is the port-back reference — the two - * stacks are decoupled and need not make identical decisions. - * - * Seating (from munch): every strategy assumes it is p1, so the p0 seat reads through `transposeEngine`; - * the p1 seat gets the true reveal of p0's move (production peek), the p0 seat only the opponent's - * previous move. - * - * Bridge specifics vs the raw chomp harness: - * 1. After `startBattle`, the battle's rngOracle is patched to a MEMOIZED reproduction of the engine's - * inline zero-oracle rng (`keccak256(uint104 p0Salt, uint104 p1Salt)`) — same values as munch's - * ZERO-oracle harness, but forks with repeated salts skip the keccak (see `rng-oracle.ts`). - * 2. On a forced-switch turn only the acting side is decided AND only its salt is drawn (p0 before p1), - * exactly mirroring munch's `toDecision(null)` skip — the shared rng stream must stay in lockstep. - */ -import { CpuStrategy, PlayerMove, StrategyState } from '../cpu/strategy'; -import { captureBattleView } from '../cpu/battle-view'; -import { NO_OP_INDEX, TEAM_SIZE } from '../cpu/constants'; -import { makeSimContext, startBattle, executeTurn, P0_ADDR, P1_ADDR, type SimContext } from '../harness'; -import { loadRoster } from '../util/csv-load'; -import { buildTeamMon, type DraftedMon } from './team'; -import { transposeEngine } from './transpose'; -import { makeRng, randomSalt } from './rng'; -import { MemoizedInlineRngOracle } from './rng-oracle'; -import './fast-status-key'; // registers StatusEffectLib.getKeyForMonIndex as a pure (memoized) proxy method -const roster = loadRoster(); - -export interface Seat { - strategy: CpuStrategy; - state: StrategyState; - engine: any; // raw engine for the p1 seat, transposed proxy for the p0 seat - lastOwnMove: PlayerMove; // most recent move this seat actually played (the other seat's stale peek) -} - -export type GameOutcome = { winnerSeat: 0 | 1 | null; turns: number } | { error: string }; - -/** Called after both seats decided, before the turn executes. `engine`/`battleKey` are the RAW views. */ -export interface TurnHook { - (info: { - turn: number; - engine: any; - battleKey: `0x${string}`; - /** null when that seat does not act this turn (forced-switch flag). */ - p0Move: PlayerMove | null; - p1Move: PlayerMove | null; - seats: [Seat, Seat]; - }): void; -} - -// Reuse chomp's startBattle, then flip the rngOracle to ZERO so the keccak256(p0Salt,p1Salt) inline -// path runs (parity with munch's harness). -function startArenaBattle(ctx: SimContext, teams: [DraftedMon[], DraftedMon[]]): `0x${string}` { - const p0Team = teams[0].map((dm) => buildTeamMon(ctx, roster, dm.id, dm.equip)); - const p1Team = teams[1].map((dm) => buildTeamMon(ctx, roster, dm.id, dm.equip)); - const { battleKey } = startBattle(ctx, p0Team, p1Team); - const engine = ctx.engine as any; - const storageKey = engine._getStorageKey(battleKey); - engine.battleConfig[storageKey].rngOracle = new MemoizedInlineRngOracle(); - return battleKey; -} - -export function playGame( - stratP1: CpuStrategy, stratP0: CpuStrategy, - teams: [DraftedMon[], DraftedMon[]], seed: number, maxTurns: number, - onBeforeExecute?: TurnHook, -): GameOutcome { - const rng = makeRng(seed); - const ctx = makeSimContext({ monsPerTeam: BigInt(TEAM_SIZE) }); - const battleKey = startArenaBattle(ctx, teams); - return runGameLoop(ctx, battleKey, stratP1, stratP0, rng, maxTurns, onBeforeExecute); -} - -function runGameLoop( - ctx: SimContext, - battleKey: `0x${string}`, - stratP1: CpuStrategy, stratP0: CpuStrategy, - rng: () => number, maxTurns: number, - onBeforeExecute?: TurnHook, -): GameOutcome { - const engine = ctx.engine as any; - - const winnerNow = () => Number(engine.battleData[battleKey].winnerIndex); - - const seats: [Seat, Seat] = [ - { strategy: stratP0, state: stratP0.createState(), engine: transposeEngine(engine), lastOwnMove: { moveIndex: 0, extraData: 0 } }, - { strategy: stratP1, state: stratP1.createState(), engine, lastOwnMove: { moveIndex: 0, extraData: 0 } }, - ]; - - for (let t = 0; t < maxTurns; t++) { - const winner = winnerNow(); - if (winner !== 2) return { winnerSeat: winner as 0 | 1, turns: t }; - - const flag = Number(engine.getBattleContext(battleKey).playerSwitchForTurnFlag); - const p0Acts = flag !== 1; - const p1Acts = flag !== 0; - - try { - // p0 seat decides first, peeking only the opponent's previous move. - let p0Move: PlayerMove | null = null; - if (p0Acts) { - const view = captureBattleView(seats[0].engine, battleKey); - p0Move = seats[0].strategy.decide(view, seats[1].lastOwnMove, rng, seats[0].state); - seats[0].lastOwnMove = p0Move; - } - // p1 seat replies with the true reveal (production semantics). - let p1Move: PlayerMove | null = null; - if (p1Acts) { - const view = captureBattleView(seats[1].engine, battleKey); - p1Move = seats[1].strategy.decide(view, p0Move ?? { moveIndex: 0, extraData: 0 }, rng, seats[1].state); - seats[1].lastOwnMove = p1Move; - } - - onBeforeExecute?.({ turn: t, engine, battleKey, p0Move, p1Move, seats }); - - // Salt is drawn ONLY for an acting side, p0 before p1 — matching munch's `toDecision(null)` skip. - const p0Salt = p0Move ? randomSalt(rng) : 0n; - const p1Salt = p1Move ? randomSalt(rng) : 0n; - executeTurn(ctx, battleKey, { - p0MoveIndex: p0Move ? p0Move.moveIndex : NO_OP_INDEX, - p1MoveIndex: p1Move ? p1Move.moveIndex : NO_OP_INDEX, - p0Salt, - p1Salt, - p0ExtraData: BigInt(p0Move ? p0Move.extraData : 0), - p1ExtraData: BigInt(p1Move ? p1Move.extraData : 0), - }); - } catch (e) { - return { error: `turn ${t}: ${(e as Error).stack ?? (e as Error).message}` }; - } - } - - const final = winnerNow(); - if (final !== 2) return { winnerSeat: final as 0 | 1, turns: maxTurns }; - return { winnerSeat: null, turns: maxTurns }; // turn-cap draw (stalemate) -} diff --git a/sims/src/arena/mon-data-core.ts b/sims/src/arena/mon-data-core.ts deleted file mode 100644 index 5811f0e9..00000000 --- a/sims/src/arena/mon-data-core.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Reusable core for the per-mon win-rate + move-usage arena (`mon-data.ts`), factored out so the same - * tally logic runs inline (sequential) or inside a Worker (parallel shard). A work unit is one - * (strategy, seed) pair, which expands to the two seat-swapped games. Records merge by summation, so - * the aggregate is independent of how work units are sharded across workers — parallel output is - * byte-identical to sequential. - */ -import { buildRandomTeam } from './team-builder'; -import { getCpuStrategy } from '../cpu'; -import { MonMetadata } from '../cpu/mon-meta'; -import { activeMonIndices } from '../cpu/engine-view'; -import { makeRng } from './rng'; -import { playGame } from './game'; -import { draftMoveSelection, type DraftedMon } from './team'; - -export interface MonRec { - wins: number; - losses: number; - draws: number; - moveUsed: number[]; // per catalog lane: move-turns this mon chose that move - moveEquipped: number[]; // per catalog lane: drafts that fielded that move - moveTurns: number; // total move-turns for this mon (denominator for used%) - drafts: number; // team-slots this mon was drafted into (denominator for equip%) -} - -export interface ShardResult { - rec: Record; - /** Flat matchup tally: pairWins[a * PAIR_STRIDE + b] = decided games where a mon `a` stood on the - * winning side and a mon `b` on the losing side. Win rate of a vs b = wins[a,b]/(wins[a,b]+wins[b,a]). */ - pairWins: number[]; - totalGames: number; - errors: number; -} - -export interface WorkItem { - strat: string; - seed: number; -} - -export const MON_IDS: number[] = Object.keys(MonMetadata).map(Number); -export const PAIR_STRIDE = Math.max(...MON_IDS) + 1; - -// Catalog length per mon = the number of moves it could field (MonMetadata.moves is the full catalog). -const catalogLen = (id: number): number => (MonMetadata as any)[id].moves.length; - -export function newShardResult(): ShardResult { - const rec: Record = {}; - for (const id of MON_IDS) { - const n = catalogLen(id); - rec[id] = { wins: 0, losses: 0, draws: 0, moveUsed: new Array(n).fill(0), moveEquipped: new Array(n).fill(0), moveTurns: 0, drafts: 0 }; - } - return { rec, pairWins: new Array(PAIR_STRIDE * PAIR_STRIDE).fill(0), totalGames: 0, errors: 0 }; -} - -/** Run one (strategy, seed) unit — the two seat-swapped games — tallying into `res`. */ -export function runPair(stratKey: string, seed: number, maxTurns: number, res: ShardResult): void { - const s = getCpuStrategy(stratKey); - if (!s) throw new Error(`unknown strategy "${stratKey}"`); - const rec = res.rec; - const teamRng = makeRng(seed * 7919 + 17); - const baseIds: [number[], number[]] = [ - buildRandomTeam(teamRng).monIndices.map(Number), - buildRandomTeam(teamRng).monIndices.map(Number), - ]; - // Max-level loadouts come off a SEPARATE stream so the team-draw rng above stays byte-locked to munch. - const moveRng = makeRng(seed * 6151 + 23); - const draft = (ids: number[]): DraftedMon[] => ids.map((id) => ({ id, equip: draftMoveSelection(catalogLen(id), moveRng) })); - const baseTeams: [DraftedMon[], DraftedMon[]] = [draft(baseIds[0]), draft(baseIds[1])]; - // Equip is fixed per drafted slot (unchanged across the seat swap), so count equip stats once here. - for (const side of baseTeams) { - for (const dm of side) { - rec[dm.id].drafts++; - for (const lane of dm.equip) rec[dm.id].moveEquipped[lane]++; - } - } - for (const swap of [false, true]) { - const t: [DraftedMon[], DraftedMon[]] = swap ? [baseTeams[1], baseTeams[0]] : [baseTeams[0], baseTeams[1]]; - const hook = (info: any) => { - const [a0, a1] = activeMonIndices(info.engine, info.battleKey); - const tally = (dm: DraftedMon, moveIndex: number) => { - // Map the played battle slot back to its catalog lane via this draft's equip. - const lane = dm.equip[Math.min(moveIndex, dm.equip.length - 1)]; - rec[dm.id].moveUsed[lane]++; - rec[dm.id].moveTurns++; - }; - if (info.p0Move && info.p0Move.moveIndex < 4) tally(t[0][a0], info.p0Move.moveIndex); - if (info.p1Move && info.p1Move.moveIndex < 4) tally(t[1][a1], info.p1Move.moveIndex); - }; - const out = playGame(s, s, t, seed, maxTurns, hook); - res.totalGames++; - if ('error' in out) { res.errors++; continue; } - const drew = out.winnerSeat === null; - const tally = (id: number, seatWon: boolean) => { - if (drew) rec[id].draws++; - else if (seatWon) rec[id].wins++; - else rec[id].losses++; - }; - for (const id of new Set(t[0].map((d) => d.id))) tally(id, out.winnerSeat === 0); - for (const id of new Set(t[1].map((d) => d.id))) tally(id, out.winnerSeat === 1); - if (!drew) { - const winners = new Set(t[out.winnerSeat === 0 ? 0 : 1].map((d) => d.id)); - const losers = new Set(t[out.winnerSeat === 0 ? 1 : 0].map((d) => d.id)); - for (const w of winners) for (const l of losers) res.pairWins[w * PAIR_STRIDE + l]++; - } - } -} - -/** Run a list of work units into a fresh result (the unit of work a worker processes). */ -export function runItems(items: WorkItem[], maxTurns: number): ShardResult { - const res = newShardResult(); - for (const it of items) runPair(it.strat, it.seed, maxTurns, res); - return res; -} - -/** Fold `src` into `dst` (summation — commutative, so shard order is irrelevant). */ -export function mergeInto(dst: ShardResult, src: ShardResult): void { - dst.totalGames += src.totalGames; - dst.errors += src.errors; - if (src.pairWins) for (let i = 0; i < dst.pairWins.length; i++) dst.pairWins[i] += src.pairWins[i]; - for (const id of MON_IDS) { - const a = dst.rec[id]; - const b = src.rec[id]; - if (!b) continue; - a.wins += b.wins; - a.losses += b.losses; - a.draws += b.draws; - a.moveTurns += b.moveTurns; - a.drafts += b.drafts; - for (let k = 0; k < a.moveUsed.length; k++) { - a.moveUsed[k] += b.moveUsed[k]; - a.moveEquipped[k] += b.moveEquipped[k]; - } - } -} diff --git a/sims/src/arena/mon-data-worker.ts b/sims/src/arena/mon-data-worker.ts deleted file mode 100644 index 19e84cd3..00000000 --- a/sims/src/arena/mon-data-worker.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Worker entry for the parallel arena. Receives a shard of (strategy, seed) work units, runs them, and - * posts back the partial {@link ShardResult}. Each worker has its own module graph (own engine - * container, event stream, fork counter) so shards are fully isolated — no cross-worker shared state. - */ -import { runItems, type ShardResult, type WorkItem } from './mon-data-core'; - -declare var self: Worker; - -self.onmessage = (e: MessageEvent) => { - const { items, maxTurns } = e.data as { items: WorkItem[]; maxTurns: number }; - const res: ShardResult = runItems(items, maxTurns); - postMessage(res); -}; diff --git a/sims/src/arena/mon-data.ts b/sims/src/arena/mon-data.ts deleted file mode 100644 index cabaa2e6..00000000 --- a/sims/src/arena/mon-data.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Per-mon win rate + per-move usage over random 4v4s, seat-swapped to cancel the p1 peek, same strategy - * on both sides. - * - * bun arena/mon-data.ts --strategies hard,greedy --games 300 - * bun arena/mon-data.ts --strategies hard,greedy --games 1000 --workers 8 - * - * Win rate = wins / (wins + losses) for every game a mon was on the team (deduped per team so a - * repeated draft counts once). Move usage = share of that mon's move-turns spent on each catalog move; - * ✗ marks a move equipped but never chosen. Mons treated as max level, so mons with a >4-move catalog - * (the level-6 unlockers) field a random 4-of-N per draft — for those, `[equip N%]` shows how often each - * move made the loadout. - * - * Games are independent and deterministic per (strategy, seed), so the run is sharded across a Worker - * pool (default = CPU count; `--workers 1` forces sequential). Records merge by summation, so parallel - * output is byte-identical to sequential — only wall-time changes. - */ -import { createHash } from 'node:crypto'; -import { appendFileSync, readFileSync } from 'node:fs'; -import { MonMetadata } from '../cpu/mon-meta'; -import { getCpuStrategy } from '../cpu'; -import { newShardResult, mergeInto, runItems, PAIR_STRIDE, type ShardResult, type WorkItem } from './mon-data-core'; - -const args = process.argv.slice(2); -const argVal = (flag: string, def: string) => { - const i = args.indexOf(flag); - return i >= 0 && i + 1 < args.length ? args[i + 1] : def; -}; -const strategies = argVal('--strategies', 'hard,greedy').split(','); -const showMatrix = args.includes('--matrix'); -const games = Number(argVal('--games', '300')); -const maxTurns = Number(argVal('--turns', '150')); -const baseSeed = Number(argVal('--seed', '1')); -const defaultWorkers = Math.max(1, Number(navigator.hardwareConcurrency ?? 4)); -const workers = Math.max(1, Number(argVal('--workers', String(defaultWorkers)))); - -// Override runs measure a scripted hypothesis, so the hypothesis must exist before the run: write -// the prediction into the design doc, then pass it here. Plain screens stay ungated. -const prediction = argVal('--prediction', ''); -if (strategies.includes('override') && !prediction) { - console.error('error: override runs require --prediction "".'); - console.error('Register the prediction in the design doc first (prompting/design-pass-prompt.md).'); - process.exit(1); -} - -// Validate strategies up front so a bad key fails fast (not inside a worker). -for (const strat of strategies) { - if (!getCpuStrategy(strat)) throw new Error(`unknown strategy "${strat}"`); -} - -// Work units: one (strategy, seed) pair = the two seat-swapped games. Flatten across all strategies. -const items: WorkItem[] = []; -const pairs = Math.max(1, Math.floor(games / 2)); -for (const strat of strategies) { - for (let i = 0; i < pairs; i++) items.push({ strat, seed: baseSeed + i }); -} - -function shard(arr: T[], n: number): T[][] { - const out: T[][] = Array.from({ length: n }, () => []); - arr.forEach((v, i) => out[i % n].push(v)); - return out; -} - -async function runParallel(nw: number): Promise { - const shards = shard(items, nw).filter((s) => s.length > 0); - const partials = await Promise.all(shards.map((sd) => new Promise((resolve, reject) => { - const w = new Worker(new URL('./mon-data-worker.ts', import.meta.url).href, { type: 'module' }); - w.onmessage = (e: MessageEvent) => { resolve(e.data as ShardResult); w.terminate(); }; - w.onerror = (err) => { w.terminate(); reject(err); }; - w.postMessage({ items: sd, maxTurns }); - }))); - const agg = newShardResult(); - for (const p of partials) mergeInto(agg, p); - return agg; -} - -const nw = Math.min(workers, items.length); -const res = nw <= 1 ? runItems(items, maxTurns) : await runParallel(nw); -const { rec, totalGames, errors } = res; - -const ids: number[] = Object.keys(MonMetadata).map(Number); -const rows = ids.map((id) => { - const r = rec[id]; - const decided = r.wins + r.losses; - return { id, name: (MonMetadata as any)[id].name as string, wr: decided ? (100 * r.wins) / decided : 0, r }; -}).sort((a, b) => b.wr - a.wr); - -const parallelNote = nw > 1 ? ` workers: ${nw}` : ''; -console.log(`\nGames: ${totalGames} (errors ${errors}) strategies: ${strategies.join('+')} turns<=${maxTurns}${parallelNote}\n`); -console.log('WIN RATE wins/(wins+losses), seat-swapped:'); -for (const row of rows) { - const r = row.r; - console.log(` ${row.name.padEnd(10)} ${row.wr.toFixed(1).padStart(5)}% (W${r.wins}/L${r.losses}/D${r.draws})`); -} -console.log('\nMOVE USAGE used% = share of the mon\'s move-turns; ✗ = equipped but never chosen; [equip N%] = draft rate (rotating catalogs only):'); -for (const row of rows) { - const r = row.r; - const catalog = (MonMetadata as any)[row.id].moves; // full catalog names (may exceed 4 battle slots) - const rotating = catalog.length > 4; // only these vary their loadout per draft - const parts = catalog.map((m: any, lane: number) => { - const equipNote = rotating && r.drafts ? ` [equip ${((100 * r.moveEquipped[lane]) / r.drafts).toFixed(0)}%]` : ''; - if (r.moveUsed[lane] === 0) return `${m.name} ✗${equipNote}`; - return `${m.name} ${r.moveTurns ? ((100 * r.moveUsed[lane]) / r.moveTurns).toFixed(0) : '0'}%${equipNote}`; - }); - console.log(` ${row.name.padEnd(10)} | ${parts.join(' · ')}`); -} - -// Matchup matrix (--matrix): win rate of the row mon's side over decided games where the row and -// column mons stood on opposite sides. This is the instrument behind a design pass's "good at" / -// "checked by" profile claims — prey and walls read straight off the row. -if (showMatrix) { - const wrAB = (a: number, b: number) => { - const w = res.pairWins[a * PAIR_STRIDE + b]; - const l = res.pairWins[b * PAIR_STRIDE + a]; - return { pct: w + l > 0 ? (100 * w) / (w + l) : NaN, n: w + l }; - }; - const nSample = wrAB(rows[0].id, rows[rows.length - 1].id).n; - console.log(`\nMATCHUP MATRIX row mon's win% when row and column mons are on opposite sides (~n=${nSample} per cell; blank diagonal):`); - const header = ' ' + rows.map((r) => r.name.slice(0, 4).padStart(5)).join(''); - console.log(header); - for (const a of rows) { - const cells = rows.map((b) => { - if (a.id === b.id) return ' ·'; - const { pct } = wrAB(a.id, b.id); - return (Number.isNaN(pct) ? '?' : pct.toFixed(0)).padStart(5); - }); - console.log(` ${a.name.padEnd(10)}${cells.join('')}`); - } -} - -// Journal the run so every number a design doc cites traces to a recorded invocation, and so the -// prediction ledger can be rebuilt by joining predictions to results. The override-script hash makes -// a script edit between two runs visible in the record. -const overrideSrc = readFileSync(new URL('../cpu/strategies/override-cpu.ts', import.meta.url), 'utf8'); -const journalEntry = { - ts: new Date().toISOString(), - args, - strategies, - games: totalGames, - seed: baseSeed, - workers: nw, - prediction: prediction || null, - overrideScriptsSha256: createHash('sha256').update(overrideSrc).digest('hex').slice(0, 16), - results: Object.fromEntries( - rows.map((row) => [row.name, { wr: Number(row.wr.toFixed(1)), wins: row.r.wins, losses: row.r.losses, draws: row.r.draws }]), - ), - pairWins: res.pairWins, -}; -appendFileSync(new URL('../../runs.jsonl', import.meta.url), JSON.stringify(journalEntry) + '\n'); -console.log(`\nrun journaled -> sims/runs.jsonl (${journalEntry.ts})`); diff --git a/sims/src/arena/rng-oracle.ts b/sims/src/arena/rng-oracle.ts deleted file mode 100644 index 0bc950e3..00000000 --- a/sims/src/arena/rng-oracle.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { encodeAbiParameters, keccak256 } from 'viem'; - -/** - * Memoized drop-in for the engine's inline (zero-oracle) RNG. - * - * When `config.rngOracle` is the zero address the engine derives each turn's rng inline as - * `keccak256(abi.encode(uint104 p0Salt, uint104 p1Salt))` (Engine.ts). The forward model replays ~10 - * candidate turns per real turn, and in the arena those forks use CONSTANT salts (plain greedy passes - * `0n`), so the identical `(source0, source1)` pair recurs every fork — yet the inline path re-runs the - * full keccak + ABI-encode + hex round-trip each time (~15% of arena CPU per the profile). - * - * Installing this as a NON-zero `rngOracle` routes the engine through `getRNG` instead, which reproduces - * the inline formula EXACTLY (same uint104 masking, same keccak) but caches on the salt pair. Repeated - * fork salts become cache hits; real turns draw fresh 104-bit salts (misses) and compute the identical - * value — so battle outcomes are byte-identical to the inline path. - * - * Must be a CLASS instance, not a plain object: the fork's `cloneState` deep-copies plain objects but - * shares class instances by reference, so a plain-object oracle would be re-copied (and its cache lost) - * on every fork. The cache is module-level so it stays warm across every game in a worker. - */ - -// keccak256(abi.encode(uint104,uint104)) is a pure function of the salt pair, so one process-wide cache -// is valid for every battle/fork. -const rngCache = new Map(); -const MASK_104 = (1n << 104n) - 1n; - -// Any non-zero address works — it only has to differ from the zero address so the engine takes the -// oracle branch rather than the inline branch. -const ORACLE_ADDRESS = '0x0000000000000000000000000000000000005a17'; - -export class MemoizedInlineRngOracle { - _contractAddress = ORACLE_ADDRESS; - - getRNG(source0: string, source1: string): bigint { - const key = source0 + source1; - let v = rngCache.get(key); - if (v === undefined) { - const p0 = BigInt(source0) & MASK_104; - const p1 = BigInt(source1) & MASK_104; - v = BigInt(keccak256(encodeAbiParameters([{ type: 'uint104' }, { type: 'uint104' }], [p0, p1]))); - rngCache.set(key, v); - } - return v; - } -} diff --git a/sims/src/arena/rng.ts b/sims/src/arena/rng.ts deleted file mode 100644 index 1cd65cfe..00000000 --- a/sims/src/arena/rng.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Seeded RNG + salt — verbatim from munch's sim harness so the team draws, battle rng, and per-turn - * salt values reproduce munch's stream exactly (identical seed => identical salts => identical battle RNG). - */ -export function makeRng(seed: number): () => number { - let a = seed >>> 0; - return function () { - a = (a + 0x6d2b79f5) >>> 0; - let t = a; - t = Math.imul(t ^ (t >>> 15), t | 1); - t ^= t + Math.imul(t ^ (t >>> 7), t | 61); - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -export function randomSalt(rng: () => number): bigint { - // 26 hex nibbles -> uint104, matching SignedCommitLib / MonMoves packing. - let s = '0x'; - for (let i = 0; i < 26; i++) s += Math.floor(rng() * 16).toString(16); - return BigInt(s); -} diff --git a/sims/src/arena/team-builder.ts b/sims/src/arena/team-builder.ts deleted file mode 100644 index f8fa6a02..00000000 --- a/sims/src/arena/team-builder.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Self-contained port of munch's `cpu-team-builder.buildRandomTeam`. Reproduces the EXACT rng-draw - * sequence — including the four facet draws (their values are unused here, but each consumes one draw - * off the shared teamRng, so dropping them would desync the second team and break cross-repo parity). - */ -import { loadRoster } from '../util/csv-load'; -import { TEAM_SIZE } from '../cpu/constants'; - -type Rng = () => number; - -const TOTAL_FACETS = 12; -const RANDOM_REPEAT_BIAS = 0.04; - -// Mon ids [0..12] in id order — matches munch's `Object.values(MonMetadata).map(m => BigInt(m.id))`. -const ALL_MON_IDS: readonly bigint[] = loadRoster().mons - .slice() - .sort((a, b) => a.id - b.id) - .map((m) => BigInt(m.id)); - -function randomFacetId(rand: Rng): number { - return 1 + Math.floor(rand() * TOTAL_FACETS); -} - -export function buildRandomTeam(rand: Rng): { monIndices: bigint[] } { - const monIndices: bigint[] = []; - const remaining = ALL_MON_IDS.slice(); - for (let i = 0; i < TEAM_SIZE; i++) { - if (i > 0 && rand() < RANDOM_REPEAT_BIAS) { - monIndices.push(monIndices[Math.floor(rand() * i)]); - } else { - const idx = Math.floor(rand() * remaining.length); - monIndices.push(remaining[idx]); - remaining.splice(idx, 1); - } - } - // Facet draws — value discarded, but the draws must happen to keep the rng stream in lockstep. - for (let i = 0; i < TEAM_SIZE; i++) randomFacetId(rand); - return { monIndices }; -} diff --git a/sims/src/arena/team.ts b/sims/src/arena/team.ts deleted file mode 100644 index 8ea8f87f..00000000 --- a/sims/src/arena/team.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Builds a chomp `Structs.Mon` from a mon id, self-contained for the arena. - * - * Reuses only the SAFE chomp util helpers (`moveNameToContract`, `findInlineMoveJson`, the container), - * but does its OWN type mapping and inline packing so it handles every CSV type name including "Faith" - * (both `mon-builder.ts`'s TYPE_BY_NAME and `inline-pack.ts`'s TYPE_MAP predate the Mythic->Faith enum - * rename and would throw). `typeToEnum` reads the `Type` enum by name directly, so it survives renames. - * `monCatalog` is shared with `mon-meta.ts` so the usage-table labels index the same catalog the draft - * selects a battle loadout from. - */ -import { contracts as CONTRACT_REGISTRY } from '../../../transpiler/ts-output/factories'; -import { Type } from '../../../transpiler/ts-output/Enums'; -import { addressToUint } from '../../../transpiler/ts-output/runtime'; -import { registerMoveInputType, registerMoveTargetSpec } from '../cpu/engine-view'; -import * as Structs from '../../../transpiler/ts-output/Structs'; -import type { SimContext } from '../harness'; -import { moveNameToContract } from '../util/mon-builder'; -import { findInlineMoveJson, type InlineMoveJson } from '../util/inline-pack'; -import type { Roster, MonRow } from '../util/csv-load'; - -const DEFAULT_STAMINA = 5n; - -const CLASS_MAP: Record = { Physical: 0, Special: 1, Self: 2, Other: 3 }; - -function typeToEnum(name: string): number { - if (name === 'NA' || name === '') return Type.None; - const v = (Type as any)[name]; - if (v === undefined) throw new Error(`Unknown type "${name}"`); - return v as number; -} - -function isImplemented(contractName: string): boolean { - return contractName in CONTRACT_REGISTRY; -} - -type ResolvedSlot = - | { kind: 'contract'; contractName: string; name: string; inputType: string; targetSpec: string } - | { kind: 'inline'; json: InlineMoveJson; name: string }; - -/** - * The mon's full implemented move catalog in learnset (CSV) order — every move with a contract or - * inline JSON, uncapped and unpadded. Lanes [0,4) are the level-0 moves; a 5th lane, where present, is - * the mon's level-6 unlock. Treating mons as max level means the whole catalog is selectable; the draft - * then picks which lanes fill the 4 battle slots. Throws if the mon resolves to zero moves. - */ -export function monCatalog(roster: Roster, mon: MonRow): ResolvedSlot[] { - const csvMoves = roster.movesByMon.get(mon.name) ?? []; - const monDir = mon.name.toLowerCase(); - const slots: ResolvedSlot[] = []; - for (const mv of csvMoves) { - const contract = moveNameToContract(mv.name); - if (isImplemented(contract)) { - slots.push({ kind: 'contract', contractName: contract, name: mv.name, inputType: mv.inputType, targetSpec: mv.targetSpec }); - continue; - } - const inlineJson = findInlineMoveJson(monDir, contract); - if (inlineJson) { - slots.push({ kind: 'inline', json: inlineJson, name: mv.name }); - continue; - } - // Unimplemented move — skip (mirrors buildMonConfig). - } - if (slots.length === 0) throw new Error(`mon ${mon.name} has no resolvable moves`); - return slots; -} - -/** A mon drafted into a team slot: its id plus the catalog lanes it fields this draft (`equip`). */ -export interface DraftedMon { - id: number; - equip: number[]; -} - -/** - * Which catalog lanes fill a max-level mon's 4 battle slots. A mon with <=4 catalog moves fields all of - * them (no draw). A larger catalog — the level-6-unlock mons — drops one random lane per draft, so over a - * run every catalog move (including the level-6 move) gets played. Drops preserve ascending order, so - * slot 0 stays the mon's first move. The `rand` here MUST be a stream separate from the team-draw rng — - * that one is byte-locked to munch's for cross-repo team parity. - */ -export function draftMoveSelection(catalogLen: number, rand: () => number): number[] { - const pool = Array.from({ length: catalogLen }, (_, i) => i); - while (pool.length > 4) pool.splice(Math.floor(rand() * pool.length), 1); - return pool; -} - -// Inline-move packing — mirrors sims/src/util/inline-pack.ts:packMove but with a type-robust map. -// Layout: [basePower:8 | moveClass:2 | priority:2 | moveType:4 | stamina:4 | effectAccuracy:8 | ... | effect:160] -function packInlineMove(m: InlineMoveJson, effectAddress: bigint): bigint { - const movClass = CLASS_MAP[m.moveClass as string]; - const movType = typeToEnum(m.moveType as string); - const priority = m.priority ?? 0; - if (movClass === undefined) throw new Error(`Unknown moveClass "${m.moveClass}"`); - let packed = BigInt(m.basePower) << 248n; - packed |= BigInt(movClass) << 246n; - packed |= BigInt(priority) << 244n; - packed |= BigInt(movType) << 240n; - packed |= BigInt(m.staminaCost) << 236n; - packed |= BigInt(m.effectAccuracy) << 228n; - packed |= effectAddress; - return packed; -} - -function resolveContractAddress(ctx: SimContext, name: string): bigint { - const c = ctx.container.resolve(name); - return addressToUint(c._contractAddress); -} - -export function buildTeamMon(ctx: SimContext, roster: Roster, monId: number, equip?: number[]): Structs.Mon { - const mon = roster.mons.find((m) => m.id === monId); - if (!mon) throw new Error(`no mon with id ${monId}`); - const catalog = monCatalog(roster, mon); - // Default loadout = the first up-to-4 catalog lanes (the level-0 moves); callers pass `equip` to field - // a specific max-level 4-of-N selection. Engine fields exactly 4 lanes, so pad a short loadout by - // repeating the last (duplicate lanes are inert). - const lanes = equip ?? Array.from({ length: Math.min(4, catalog.length) }, (_, i) => i); - const slots = lanes.map((i) => catalog[i]); - while (slots.length < 4) slots.push(slots[slots.length - 1]); - - const moves = slots.map((s) => { - if (s.kind === 'contract') { - const addr = resolveContractAddress(ctx, s.contractName); - // Address → InputType / TargetSpec registries (the off-chain replacement for the removed - // on-chain ExtraDataType) — CPU enumeration consults them for payload/slot targeting. - registerMoveInputType(addr, s.inputType); - registerMoveTargetSpec(addr, s.targetSpec); - return addr; - } - const effectAddr = s.json.effect ? resolveContractAddress(ctx, s.json.effect) : 0n; - return packInlineMove(s.json, effectAddr); - }); - - const ability = roster.abilityByMon.get(mon.name); - const abilityContract = ability ? moveNameToContract(ability.name) : null; - const abilitySlot = abilityContract && isImplemented(abilityContract) - ? resolveContractAddress(ctx, abilityContract) - : 0n; - - return { - stats: { - hp: BigInt(mon.hp), - stamina: DEFAULT_STAMINA, - speed: BigInt(mon.speed), - attack: BigInt(mon.attack), - defense: BigInt(mon.defense), - specialAttack: BigInt(mon.specialAttack), - specialDefense: BigInt(mon.specialDefense), - type1: typeToEnum(mon.type1), - type2: typeToEnum(mon.type2), - }, - moves, - ability: abilitySlot, - }; -} diff --git a/sims/src/arena/transpose.ts b/sims/src/arena/transpose.ts deleted file mode 100644 index 59e9ee68..00000000 --- a/sims/src/arena/transpose.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Transposed engine view for the CPU arena. - * - * Every CPU strategy hardcodes the production convention "the CPU is p1, the - * opponent is p0". To let a strategy play the p0 seat of a real battle, we hand - * it a Proxy of the engine that flips the playerIndex argument (0n <-> 1n) on - * the strategy-facing read surface, so the seat-0 strategy sees itself as p1. - * - * Scope: this transposes exactly the methods the strategies + their helpers - * (engine-view / heuristic-shared / battle-view / forward-model) reach, plus - * the forward-model's fork submission hooks. Engine-internal self-calls are - * unaffected (wrapped methods are applied to the raw target). A known hole: - * an EXTERNAL move whose getMeta/basePower reads battle state through a - * captured engine singleton (rather than the engine argument it receives) - * would bypass the proxy — no current move does this; inline moves decode - * purely from the packed slot. - */ - -// Methods whose playerIndex argument(s) get flipped, by argument position. -const FLIP_ARG_POSITIONS: Record = { - getMonStatsForBattle: [1], - getMonStateForBattle: [1], - getMonValueForBattle: [1], - getMoveForMonForBattle: [1], - getKOBitmap: [1], - getMoveDecisionForSlot: [1], - validatePlayerMoveForBattle: [2], - getDamageCalcContext: [1, 3], // (bk, atkPlayer, atkMon, defPlayer, defMon) — player args at 1 & 3 - // forward-model fork submission: _setMoveInternal(config, playerIndex, ...) - _setMoveInternal: [1], -}; - -// Methods whose names swap with each other (forward-model turn transients). -const SWAP_NAMES: Record = { - __mutate_turnP0Packed: '__mutate_turnP1Packed', - __mutate_turnP1Packed: '__mutate_turnP0Packed', -}; - -function flipPlayerIndex(v: unknown): unknown { - if (v === 0n) return 1n; - if (v === 1n) return 0n; - if (v === 0) return 1; - if (v === 1) return 0; - return v; -} - -/** Wrap `engine` so playerIndex-sensitive reads are seen from the p0 seat as if it were p1. */ -export function transposeEngine(engine: any): any { - const wrapped = new Map(); - return new Proxy(engine, { - get(target, prop) { - if (wrapped.has(prop)) return wrapped.get(prop); - const name = String(prop); - const swapped = SWAP_NAMES[name]; - const val = target[swapped ?? name]; - if (typeof val !== 'function') return val; - - let out: any; - if (name in FLIP_ARG_POSITIONS) { - const positions = FLIP_ARG_POSITIONS[name]; - out = (...args: any[]) => { - for (const p of positions) args[p] = flipPlayerIndex(args[p]); - return val.apply(target, args); - }; - } else if (name === 'getActiveMonIndexForBattleState') { - out = (...args: any[]) => { - const r = val.apply(target, args); - return [r[1], r[0]]; - }; - } else if (name === 'getBattleContext') { - out = (...args: any[]) => { - const ctx = val.apply(target, args); - const flag = Number(ctx.playerSwitchForTurnFlag); - // Seat-swap the active indices; flip the singles switch flag (0/1). - return { - ...ctx, - p0ActiveMonIndex: ctx.p1ActiveMonIndex, - p1ActiveMonIndex: ctx.p0ActiveMonIndex, - playerSwitchForTurnFlag: flag === 0 || flag === 1 ? BigInt(1 - flag) : ctx.playerSwitchForTurnFlag, - }; - }; - } else { - out = val.bind(target); - } - wrapped.set(prop, out); - return out; - }, - }); -} diff --git a/sims/src/arena/walk.ts b/sims/src/arena/walk.ts deleted file mode 100644 index 960d89c8..00000000 --- a/sims/src/arena/walk.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Exact-1v1 walk runner — plays a scripted board deterministically so the consequence walks in the - * design docs can be machine-checked turn by turn. No CPU anywhere; both sides' actions come from - * the script, so there is no peek or prediction confound. - * - * bun arena/walk.ts --p0 gorillax --p1 malalien,sofabbi \ - * --script "p0:PoundGround p1:rest; p0:RockPull p1:switch:1" \ - * --expect "t2 p1.0.ko == 1; t2 p1.active == 1" - * - * Script: turns separated by ';', each turn gives both sides' actions. An action is a move name - * (case/space-insensitive, resolved against the active mon's four slots), a slot number 0-3, - * `switch:`, or `rest`. Targeted moves take extra data with `@`: `HitAndDip@1`. - * Expectations: "tN pS.. " or "tN pS.active == ", checked - * after turn N. Any failed expectation exits 3. Every run journals to sims/runs.jsonl. - */ -import { appendFileSync } from 'node:fs'; -import { makeSimContext, startBattle, executeTurn, type TurnInput, type TurnSnapshot } from '../harness'; -import { loadRoster } from '../util/csv-load'; -import { buildTeamMon } from './team'; -import { MemoizedInlineRngOracle } from './rng-oracle'; -import * as Constants from '../../../transpiler/ts-output/Constants'; - -const args = process.argv.slice(2); -const argVal = (flag: string, def: string) => { - const i = args.indexOf(flag); - return i >= 0 && i + 1 < args.length ? args[i + 1] : def; -}; - -const roster = loadRoster(); -const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); - -function monByName(name: string) { - const m = roster.mons.find((r: any) => norm(r.name) === norm(name)); - if (!m) throw new Error(`unknown mon "${name}" — roster: ${roster.mons.map((r: any) => r.name).join(', ')}`); - return m; -} - -const p0Names = argVal('--p0', ''); -const p1Names = argVal('--p1', ''); -const script = argVal('--script', ''); -const expectArg = argVal('--expect', ''); -const seed = Number(argVal('--seed', '1')); -if (!p0Names || !p1Names || !script) { - console.error('usage: bun arena/walk.ts --p0 --p1 --script "p0: p1:; ..." [--expect "..."] [--seed N]'); - process.exit(1); -} - -const p0Mons = p0Names.split(',').map(monByName); -const p1Mons = p1Names.split(',').map(monByName); -const ctx = makeSimContext({ monsPerTeam: BigInt(Math.max(p0Mons.length, p1Mons.length)) }); -const p0Team = p0Mons.map((m: any) => buildTeamMon(ctx, roster, m.id)); -const p1Team = p1Mons.map((m: any) => buildTeamMon(ctx, roster, m.id)); -const { battleKey } = startBattle(ctx, p0Team, p1Team); -// Same oracle flip as the arena, so the keccak(p0Salt, p1Salt) inline rng path runs. -const engine = ctx.engine as any; -engine.battleConfig[engine._getStorageKey(battleKey)].rngOracle = new MemoizedInlineRngOracle(); - -// The engine's first turn is lead selection (both sides must send a mon out), so the runner plays -// it implicitly — scripted turns then start on the first real move turn. Override with --leads. -const leads = argVal('--leads', '0,0').split(',').map(Number); -executeTurn(ctx, battleKey, { - p0MoveIndex: Number(Constants.SWITCH_MOVE_INDEX), - p1MoveIndex: Number(Constants.SWITCH_MOVE_INDEX), - p0ExtraData: BigInt(leads[0]), - p1ExtraData: BigInt(leads[1]), - p0Salt: 1n, - p1Salt: 2n, -}); -console.log(`leads: p0 ${p0Mons[leads[0]].name}, p1 ${p1Mons[leads[1]].name}`); - -interface Action { index: number; extra: bigint; label: string } - -function parseAction(token: string, sideMons: any[], activeIdx: number): Action { - const [head, extraStr] = token.split('@'); - const extra = extraStr !== undefined ? BigInt(extraStr) : 0n; - if (head === 'rest') return { index: Number(Constants.NO_OP_MOVE_INDEX), extra, label: 'rest' }; - if (head.startsWith('switch:')) { - const target = head.slice('switch:'.length); - return { index: Number(Constants.SWITCH_MOVE_INDEX), extra: BigInt(target), label: `switch:${target}` }; - } - if (/^[0-3]$/.test(head)) return { index: Number(head), extra, label: `slot ${head}` }; - const active = sideMons[activeIdx]; - const slots = (roster.movesByMon.get(active.name) ?? []).slice(0, 4); - const slot = slots.findIndex((mv: any) => norm(mv.name) === norm(head)); - if (slot < 0) { - throw new Error(`"${head}" is not a move of ${active.name} — slots: ${slots.map((mv: any) => mv.name).join(', ')}`); - } - return { index: slot, extra, label: `${slots[slot].name}` }; -} - -interface Expect { turn: number; side: 0 | 1; slot: string; field: string; op: string; value: number; raw: string } - -function parseExpects(text: string): Expect[] { - if (!text.trim()) return []; - return text.split(';').map((raw) => { - const m = raw.trim().match(/^t(\d+)\s+p([01])\.(\d+|active)(?:\.(hp|stamina|ko))?\s*(<=|>=|==|<|>)\s*(-?\d+)$/); - if (!m) throw new Error(`bad expectation "${raw.trim()}" — form: tN pS.. or tN pS.active == `); - return { turn: Number(m[1]), side: Number(m[2]) as 0 | 1, slot: m[3], field: m[4] ?? '', op: m[5], value: Number(m[6]), raw: raw.trim() }; - }); -} - -function evalExpect(e: Expect, snap: TurnSnapshot, teams: [any[], any[]]): { ok: boolean; actual: number } { - let actual: number; - if (e.slot === 'active') { - actual = e.side === 0 ? snap.p0Active : snap.p1Active; - } else { - const i = Number(e.slot); - const st = (e.side === 0 ? snap.p0States : snap.p1States)[i]; - const base = teams[e.side][i]; - if (e.field === 'hp') actual = Number(BigInt(base.stats.hp) + st.hpDelta); - else if (e.field === 'stamina') actual = Number(BigInt(base.stats.stamina) + st.staminaDelta); - else actual = st.isKnockedOut ? 1 : 0; - } - const ok = - (e.op === '<=' && actual <= e.value) || (e.op === '>=' && actual >= e.value) || - (e.op === '==' && actual === e.value) || (e.op === '<' && actual < e.value) || - (e.op === '>' && actual > e.value); - return { ok, actual }; -} - -function printState(turn: number, snap: TurnSnapshot) { - const line = (side: 0 | 1) => { - const mons = side === 0 ? p0Mons : p1Mons; - const states = side === 0 ? snap.p0States : snap.p1States; - const active = side === 0 ? snap.p0Active : snap.p1Active; - return states.map((st, i) => { - const base: any = (side === 0 ? p0Team : p1Team)[i]; - const hp = BigInt(base.stats.hp) + st.hpDelta; - const stam = BigInt(base.stats.stamina) + st.staminaDelta; - const mark = st.isKnockedOut ? ' KO' : i === active ? ' *' : ''; - return `${mons[i].name} ${hp}/${base.stats.hp} s${stam}${mark}`; - }).join(' | '); - }; - console.log(`t${turn} p0: ${line(0)}`); - console.log(` p1: ${line(1)}`); -} - -const expects = parseExpects(expectArg); -const turns = script.split(';').map((t) => t.trim()).filter(Boolean); -const failures: string[] = []; -const turnLog: any[] = []; -let p0Active = 0; -let p1Active = 0; - -for (let t = 1; t <= turns.length; t++) { - const parts = Object.fromEntries( - turns[t - 1].split(/\s+/).map((tok) => { - const m = tok.match(/^p([01]):(.+)$/); - if (!m) throw new Error(`bad script token "${tok}" in turn ${t} — expected p0: p1:`); - return [m[1], m[2]]; - }), - ); - if (!(('0' in parts) && ('1' in parts))) throw new Error(`turn ${t} needs both p0: and p1: actions`); - const a0 = parseAction(parts['0'], p0Mons, p0Active); - const a1 = parseAction(parts['1'], p1Mons, p1Active); - const input: TurnInput = { - p0MoveIndex: a0.index, - p1MoveIndex: a1.index, - p0ExtraData: a0.extra, - p1ExtraData: a1.extra, - p0Salt: BigInt(seed * 7919 + t * 2), - p1Salt: BigInt(seed * 104729 + t * 2 + 1), - }; - const flag = Number(engine.getBattleContext(battleKey).playerSwitchForTurnFlag); - const forced = flag === 0 ? ' (forced: only p0 acts)' : flag === 1 ? ' (forced: only p1 acts)' : ''; - console.log(`\n== turn ${t}: p0 ${a0.label} / p1 ${a1.label}${forced}`); - const snap = executeTurn(ctx, battleKey, input); - p0Active = snap.p0Active; - p1Active = snap.p1Active; - printState(t, snap); - if (snap.winnerIndex !== 2n) console.log(` winner: p${snap.winnerIndex}`); - turnLog.push({ t, p0: a0.label, p1: a1.label, p0Active, p1Active }); - for (const e of expects.filter((x) => x.turn === t)) { - const { ok, actual } = evalExpect(e, snap, [p0Team as any[], p1Team as any[]]); - console.log(` expect ${e.raw} -> ${ok ? 'PASS' : `FAIL (actual ${actual})`}`); - if (!ok) failures.push(`${e.raw} (actual ${actual})`); - } -} - -const journalEntry = { - ts: new Date().toISOString(), - kind: 'walk', - args, - script, - expects: expectArg || null, - turns: turnLog, - failures, -}; -appendFileSync(new URL('../../runs.jsonl', import.meta.url), JSON.stringify(journalEntry) + '\n'); -console.log(`\n${failures.length === 0 ? 'all expectations passed' : `${failures.length} expectation(s) FAILED`} — journaled -> sims/runs.jsonl`); -process.exit(failures.length === 0 ? 0 : 3); diff --git a/sims/src/cpu/battle-view.ts b/sims/src/cpu/battle-view.ts deleted file mode 100644 index e8147851..00000000 --- a/sims/src/cpu/battle-view.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Hex } from './hex'; -import { MonStateIndexName, Type } from '../../../transpiler/ts-output/Enums'; -import { - cpuActiveMonIndex, - oppActiveMonIndex, - cpuTeamSize, - oppTeamSize, - koBitmap, - monMaxHp, - monHpDelta, - monBaseStamina, - monStaminaDelta, - monTypes, -} from './engine-view'; - -/** - * SEARCH SUBSTRATE — layer 1: a read-once snapshot of a battle position. - * - * `captureBattleView` pulls the frequently-read position fields off the live (or forked) engine in one - * pass, reusing the faithful `engine-view.ts` readers (so HP / stamina / types / KO bitmaps match what - * the ported CPUs trust). It is a thin projection the forward model + evaluator consume without - * re-touching the engine. It deliberately does NOT enumerate valid moves: the ports call - * `calculateValidMoves` themselves with their own rng for stream parity, so a snapshot copy would be - * unused — they read the few scalars here off the view and enumerate candidates live. - * - * Convention (inherited from the CPU ports): the CPU is ALWAYS p1, the human opponent is p0. - * - `cpu*` fields read p1 (playerIndex 1). - * - `opp*` fields read p0 (playerIndex 0). - * - `mons.p0` / `mons.p1` carry the per-slot snapshot for each side. - */ - -const CPU_PLAYER_INDEX = 1n; -const OPP_PLAYER_INDEX = 0n; - -/** Per-slot snapshot. `hp` / `stamina` are CURRENT values (base + delta). */ -export interface MonView { - /** Current HP (base + delta). */ - hp: number; - /** Base (max) HP — lets consumers compute hp% without re-reading the engine. */ - maxHp: number; - /** Current stamina (base + delta). */ - stamina: number; - type1: Type; - type2: Type; - ko: boolean; - /** - * Net stat-stage position: Σ delta/base over the five combat stats (atk/def/spatk/spdef/speed). - * Setup boosts land positive; burn's attack divide / frostbite's spatk divide land negative. - */ - statDeltaScore: number; - /** ShouldSkipTurn flag (zap) — the mon loses its next action. */ - skipTurn: boolean; -} - -export interface BattleView { - engine: any; - bk: string; - /** playerSwitchForTurnFlag: 0 = p0-only, 1 = p1-only (CPU forced switch), 2 = both move. */ - switchFlag: number; - cpuActive: number; - oppActive: number; - /** KO bitmaps (bit i set => slot i is KO'd). */ - cpuKO: number; - oppKO: number; - // Team sizes are just `mons.p1.length` / `mons.p0.length` — read them off the arrays, not a field. - mons: { p0: MonView[]; p1: MonView[] }; -} - -// Σ delta/base over the five combat stats. The getters normalize the cleared sentinel to 0, so a -// switched-out mon's score reads 0 without special-casing. -function readStatDeltaScore(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - const stats = e.getMonStatsForBattle(bk, playerIndex, BigInt(monIndex)); - const mi = BigInt(monIndex); - let score = 0; - const add = (base: bigint, stat: MonStateIndexName) => { - const b = Number(base); - if (b <= 0) return; - score += Number(e.getMonStateForBattle(bk, playerIndex, mi, stat)) / b; - }; - add(stats.attack, MonStateIndexName.Attack); - add(stats.defense, MonStateIndexName.Defense); - add(stats.specialAttack, MonStateIndexName.SpecialAttack); - add(stats.specialDefense, MonStateIndexName.SpecialDefense); - add(stats.speed, MonStateIndexName.Speed); - return score; -} - -// Per-slot snapshot. `hp` / `maxHp` / `ko` are read eagerly (every consumer uses HP, and the fork that -// produced this view may be disposed after scoring). The four heavier fields — `stamina`, the type -// pair, `statDeltaScore` (5 stat reads + a stats load), `skipTurn` — are LAZY: computed on first -// access and cached. A position evaluator only reads them for the two ACTIVE mons, so the ~6 bench -// slots per view never pay for them. Exact: every consumer touches these before its fork is disposed -// (scoring precedes `disposeFork`), and fork/child views are only ever read for eager `.hp`. -class LazyMonView implements MonView { - readonly hp: number; - readonly maxHp: number; - readonly ko: boolean; - private _stamina?: number; - private _types?: [Type, Type]; - private _statDeltaScore?: number; - private _skipTurn?: boolean; - - constructor( - private readonly e: any, - private readonly bk: Hex, - private readonly pi: bigint, - private readonly mi: number, - ko: boolean, - ) { - this.ko = ko; - this.maxHp = monMaxHp(e, bk, pi, mi); - this.hp = this.maxHp + monHpDelta(e, bk, pi, mi); - } - - get stamina(): number { - if (this._stamina === undefined) { - this._stamina = monBaseStamina(this.e, this.bk, this.pi, this.mi) + monStaminaDelta(this.e, this.bk, this.pi, this.mi); - } - return this._stamina; - } - get type1(): Type { return this.typePair()[0]; } - get type2(): Type { return this.typePair()[1]; } - private typePair(): [Type, Type] { - if (this._types === undefined) this._types = monTypes(this.e, this.bk, this.pi, this.mi); - return this._types; - } - get statDeltaScore(): number { - if (this._statDeltaScore === undefined) this._statDeltaScore = readStatDeltaScore(this.e, this.bk, this.pi, this.mi); - return this._statDeltaScore; - } - get skipTurn(): boolean { - if (this._skipTurn === undefined) { - this._skipTurn = Number(this.e.getMonStateForBattle(this.bk, this.pi, BigInt(this.mi), MonStateIndexName.ShouldSkipTurn)) !== 0; - } - return this._skipTurn; - } -} - -function readSide(e: any, bk: Hex, playerIndex: bigint, size: number): MonView[] { - const ko = koBitmap(e, bk, playerIndex); - const out: MonView[] = []; - for (let i = 0; i < size; i++) { - out.push(new LazyMonView(e, bk, playerIndex, i, (ko & (1 << i)) !== 0)); - } - return out; -} - -/** Read-once snapshot of the position at `bk`, populated entirely from the engine-view readers. */ -export function captureBattleView(e: any, bk: string): BattleView { - const key = bk as Hex; - const p0Size = oppTeamSize(e, key); - const p1Size = cpuTeamSize(e, key); - return { - engine: e, - bk, - switchFlag: Number(e.getBattleContext(key).playerSwitchForTurnFlag), - cpuActive: cpuActiveMonIndex(e, key), - oppActive: oppActiveMonIndex(e, key), - cpuKO: koBitmap(e, key, CPU_PLAYER_INDEX), - oppKO: koBitmap(e, key, OPP_PLAYER_INDEX), - mons: { - p0: readSide(e, key, OPP_PLAYER_INDEX, p0Size), - p1: readSide(e, key, CPU_PLAYER_INDEX, p1Size), - }, - }; -} diff --git a/sims/src/cpu/constants.ts b/sims/src/cpu/constants.ts deleted file mode 100644 index e0b4c029..00000000 --- a/sims/src/cpu/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Subset of munch's src/app/types/constants.ts that the cpu stack + arena import. -export const NO_OP_INDEX = 126; -export const SWITCH_MOVE_INDEX = 125; -export const TEAM_SIZE = 4; diff --git a/sims/src/cpu/doubles-search.ts b/sims/src/cpu/doubles-search.ts deleted file mode 100644 index 61ec531a..00000000 --- a/sims/src/cpu/doubles-search.ts +++ /dev/null @@ -1,389 +0,0 @@ -/** - * DOUBLES SEARCH — TS port of the Rust doubles joint maximin (`strategies-rs/src/doubles.rs`, - * search half) plus the greedy pilot it benchmarks against. - * - * Doubles differs from singles in three ways handled here: two active mons per side at absolute - * slots 0-3 (side = absSlot >> 1), a move targets a SLOT via extraData's target nibble (bits - * 12-15), and `playerSwitchForTurnFlag` is a bitmask of which absolute slots must forced-switch - * (not the singles 0/1/2). - * - * Per-slot candidates: TOP-damage (move,target) options with per-target diversity (the best - * option against EACH live enemy slot is always kept), nibble-free status/setup moves - * (self-only / none TargetSpec), a pivot switch, and rest — bench/rest never truncated away. - * Joint actions are the slot-candidate cartesian product; forced half-turns resolve without - * consuming depth; terminal values are mate-distance discounted; the row prune is - * argmax-invariant (doubles enumeration draws no rng at all). - */ -import { Hex } from './hex'; -import { MoveClass, MonStateIndexName } from '../../../transpiler/ts-output/Enums'; -import { moveSlotLib } from '../../../transpiler/ts-output/moves/MoveSlotLib'; -import { buildDamageCalcContext, estimateDamageMeta } from './heuristic-shared'; -import { koBitmap, monCurrentHp, monCurrentStamina, moveSlot, moveInputTypeOf, moveTargetSpecOf, teamSize } from './engine-view'; -import { applyHypotheticalSlotMoveKeyed, disposeFork, packSide } from './forward-model'; - -const SWITCH = 125; -const NO_OP = 126; -const EMPTY_LANE = 0xff; -const MAX_DAMAGING = 3; -const MAX_STATUS = 2; -const WIN = 1e9; -const LOSS = -1e9; -// Linear 2-slot evaluator weights (mirrors the Rust doubles_eval). -const D_W_HP = 1.0; -const D_W_KO = 150.0; -const D_W_STAMINA = 2.0; - -export interface SlotMove { - moveIndex: number; - extraData: number; -} -const NO_OP_MOVE: SlotMove = { moveIndex: NO_OP, extraData: 0 }; -const sw = (i: number): SlotMove => ({ moveIndex: SWITCH, extraData: i }); - -/** The two absolute slots owned by a side (0 → [0,1], 1 → [2,3]). */ -const sideSlots = (side: 0 | 1): [number, number] => (side === 0 ? [0, 1] : [2, 3]); - -/** Target nibble: a bitmask with bit `absSlot` set, in extraData bits 12-15. */ -const targetBits = (absSlot: number): number => 1 << (12 + absSlot); - -function activeSlots(e: any, bk: Hex): number[] { - return (e.getActiveSlots(bk) as bigint[]).map((s) => Number(s)); -} - -function teamSizeOf(e: any, bk: Hex, side: 0 | 1): number { - return teamSize(e, bk, BigInt(side)); -} - -function monMaxHp(e: any, bk: Hex, side: 0 | 1, mon: number): number { - return Number(e.getMonValueForBattle(bk, BigInt(side), BigInt(mon), MonStateIndexName.Hp)); -} - -/** Every affordable damaging (move, target-slot) for `myMon` on `side`, with estimated damage. */ -function damagingOptions(e: any, bk: Hex, side: 0 | 1, myMon: number): Array<{ sm: SlotMove; dmg: number }> { - const oppSide = (1 - side) as 0 | 1; - const slots = activeSlots(e, bk); - const oppKo = koBitmap(e, bk, BigInt(oppSide)); - - const targets: Array<{ abs: number; mon: number }> = []; - for (const abs of sideSlots(oppSide)) { - const mon = slots[abs]; - if (mon !== EMPTY_LANE && (oppKo & (1 << mon)) === 0) targets.push({ abs, mon }); - } - if (targets.length === 0) return []; - - const stamina = monCurrentStamina(e, bk, BigInt(side), myMon); - const metas: Array<{ mi: number; meta: any }> = []; - for (let mi = 0; mi < 4; mi++) { - const slot = moveSlot(e, bk, BigInt(side), myMon, mi); - if (slot === undefined) break; - metas.push({ mi, meta: moveSlotLib.decodeMeta(slot, e, bk, BigInt(side), BigInt(myMon)) }); - } - - const options: Array<{ sm: SlotMove; dmg: number }> = []; - for (const t of targets) { - const ctx = buildDamageCalcContext(e, bk, BigInt(side), myMon, BigInt(oppSide), t.mon); - for (const { mi, meta } of metas) { - if (Number(meta.stamina) > stamina) continue; - const cls = Number(meta.moveClass); - if (cls !== Number(MoveClass.Physical) && cls !== Number(MoveClass.Special)) continue; - const dmg = estimateDamageMeta(ctx, meta); - if (dmg > 0) options.push({ sm: { moveIndex: mi, extraData: targetBits(t.abs) }, dmg }); - } - } - return options; -} - -/** All legal bench targets for a slot: non-KO roster mons not held by either of the side's slots. */ -function legalBenches(teamSize: number, ko: number, slots: number[], absSlot: number): number[] { - const side = absSlot >> 1; - const allyAbs = side * 2 + (1 - (absSlot & 1)); - const out: number[] = []; - for (let i = 0; i < teamSize; i++) { - if ((ko & (1 << i)) === 0 && i !== slots[allyAbs] && i !== slots[absSlot]) out.push(i); - } - return out; -} - -function firstLegalBench(teamSize: number, ko: number, slots: number[], absSlot: number): number { - const b = legalBenches(teamSize, ko, slots, absSlot); - return b.length ? b[0] : -1; -} - -/** Per-slot candidates on a normal turn (see module doc). Empty lane → rest only. */ -function slotCandidates(e: any, bk: Hex, side: 0 | 1, absSlot: number, slots: number[], teamSize: number): SlotMove[] { - const myMon = slots[absSlot]; - if (myMon === EMPTY_LANE) return [NO_OP_MOVE]; - - const dmg = damagingOptions(e, bk, side, myMon).sort((a, b) => b.dmg - a.dmg); - // Best option per distinct target first (≤2 live slots), then next-best overall. - const out: SlotMove[] = []; - const seenTargets = new Set(); - for (const o of dmg) { - if (!seenTargets.has(o.sm.extraData)) { - seenTargets.add(o.sm.extraData); - out.push(o.sm); - } - } - for (const o of dmg) { - if (out.length >= MAX_DAMAGING) break; - if (!out.some((s) => s.moveIndex === o.sm.moveIndex && s.extraData === o.sm.extraData)) out.push(o.sm); - } - out.length = Math.min(out.length, MAX_DAMAGING); - - // Status/setup moves (non-damaging class, affordable, nibble-free targeting): extraData 0. - const stamina = monCurrentStamina(e, bk, BigInt(side), myMon); - let nStatus = 0; - for (let mi = 0; mi < 4 && nStatus < MAX_STATUS; mi++) { - const slot = moveSlot(e, bk, BigInt(side), myMon, mi); - if (slot === undefined) break; - if (moveSlotLib.isInline(slot)) continue; // inline words are standard attacks - const meta = moveSlotLib.decodeMeta(slot, e, bk, BigInt(side), BigInt(myMon)); - const cls = Number(meta.moveClass); - if (cls === Number(MoveClass.Physical) || cls === Number(MoveClass.Special)) continue; - if (Number(meta.stamina) > stamina) continue; - const spec = moveTargetSpecOf(slot); - if ((spec === 'self-only' || spec === 'none') && moveInputTypeOf(slot) === 'none') { - out.push({ moveIndex: mi, extraData: 0 }); - nStatus++; - } - } - - const bench = firstLegalBench(teamSize, koBitmap(e, bk, BigInt(side)), slots, absSlot); - if (bench >= 0) out.push(sw(bench)); - out.push(NO_OP_MOVE); - return out; -} - -/** Joint (slot0, slot1) actions for a side on a normal turn. */ -function sideJoint(e: any, bk: Hex, side: 0 | 1): Array<[SlotMove, SlotMove]> { - const [a0, a1] = sideSlots(side); - const slots = activeSlots(e, bk); - const ts = teamSizeOf(e, bk, side); - const c0 = slotCandidates(e, bk, side, a0, slots, ts); - const c1 = slotCandidates(e, bk, side, a1, slots, ts); - const out: Array<[SlotMove, SlotMove]> = []; - for (const m0 of c0) for (const m1 of c1) out.push([m0, m1]); - return out; -} - -/** Fork one slot-turn from `bk`: `mine` = cpuSide's joint, `theirs` = the opponent's. */ -function forkJoint(e: any, bk: Hex, cpuSide: 0 | 1, mine: [SlotMove, SlotMove], theirs: [SlotMove, SlotMove]): Hex { - const myWord = packSide(mine[0].moveIndex, mine[0].extraData, mine[1].moveIndex, mine[1].extraData, 0n); - const thWord = packSide(theirs[0].moveIndex, theirs[0].extraData, theirs[1].moveIndex, theirs[1].extraData, 0n); - const [s0, s1] = cpuSide === 0 ? [myWord, thWord] : [thWord, myWord]; - return applyHypotheticalSlotMoveKeyed(e, bk, s0, s1); -} - -/** Σ hp% over a side's whole roster. */ -function sideRosterHp(e: any, bk: Hex, side: 0 | 1, teamSize: number): number { - let sum = 0; - for (let i = 0; i < teamSize; i++) { - const mhp = monMaxHp(e, bk, side, i); - if (mhp <= 0) continue; - sum = sum + (Math.max(0, monCurrentHp(e, bk, BigInt(side), i)) * 100) / mhp; - } - return sum; -} - -function sideActiveStamina(e: any, bk: Hex, side: 0 | 1, slots: number[]): number { - let sum = 0; - for (const abs of sideSlots(side)) { - const mon = slots[abs]; - if (mon !== EMPTY_LANE) sum += monCurrentStamina(e, bk, BigInt(side), mon); - } - return sum; -} - -const popcount = (b: number): number => { - let c = 0; - for (let x = b; x; x &= x - 1) c++; - return c; -}; - -/** Linear 2-slot position value, `cpuSide`-perspective (higher = better). */ -function doublesEval(e: any, bk: Hex, cpuSide: 0 | 1): number { - const oppSide = (1 - cpuSide) as 0 | 1; - const hp = sideRosterHp(e, bk, cpuSide, teamSizeOf(e, bk, cpuSide)) - sideRosterHp(e, bk, oppSide, teamSizeOf(e, bk, oppSide)); - const ko = popcount(koBitmap(e, bk, BigInt(oppSide))) - popcount(koBitmap(e, bk, BigInt(cpuSide))); - const slots = activeSlots(e, bk); - const stam = sideActiveStamina(e, bk, cpuSide, slots) - sideActiveStamina(e, bk, oppSide, slots); - return D_W_HP * hp + D_W_KO * ko + D_W_STAMINA * stam; -} - -/** Terminal value if a side is fully KO'd (mate-distance discounted), else null. */ -function terminal(e: any, bk: Hex, cpuSide: 0 | 1, depth: number): number | null { - const oppSide = (1 - cpuSide) as 0 | 1; - if (popcount(koBitmap(e, bk, BigInt(oppSide))) >= teamSizeOf(e, bk, oppSide)) return WIN + depth; - if (popcount(koBitmap(e, bk, BigInt(cpuSide))) >= teamSizeOf(e, bk, cpuSide)) return LOSS - depth; - return null; -} - -/** A side's deterministic forced-switch resolution (first-legal bench per masked slot). */ -function forcedJointModel(e: any, bk: Hex, side: 0 | 1, mask: number, slots: number[]): [SlotMove, SlotMove] { - const ts = teamSizeOf(e, bk, side); - const ko = koBitmap(e, bk, BigInt(side)); - const pick = (abs: number): SlotMove => { - if ((mask & (1 << abs)) === 0) return NO_OP_MOVE; - const b = firstLegalBench(ts, ko, slots, abs); - return b >= 0 ? sw(b) : NO_OP_MOVE; - }; - const [a0, a1] = sideSlots(side); - return [pick(a0), pick(a1)]; -} - -const isNoOpJoint = (j: [SlotMove, SlotMove]): boolean => - j[0].moveIndex === NO_OP && j[1].moveIndex === NO_OP; - -/** Recursive joint maximin value at `bk`, cpuSide-perspective. Forced half-turns don't consume depth. */ -function searchValue(e: any, bk: Hex, cpuSide: 0 | 1, depth: number): number { - const t = terminal(e, bk, cpuSide, depth); - if (t !== null) return t; - if (depth <= 0) return doublesEval(e, bk, cpuSide); - - const flag = Number(e.getBattleContext(bk).playerSwitchForTurnFlag); - if (flag !== 2) { - const mask = flag & 0x0f; - const slots = activeSlots(e, bk); - const mine = forcedJointModel(e, bk, cpuSide, mask, slots); - const theirs = forcedJointModel(e, bk, (1 - cpuSide) as 0 | 1, mask, slots); - if (isNoOpJoint(mine) && isNoOpJoint(theirs)) return doublesEval(e, bk, cpuSide); // no resolution — don't loop - const child = forkJoint(e, bk, cpuSide, mine, theirs); - const v = searchValue(e, child, cpuSide, depth); - disposeFork(e, child); - return v; - } - - const my = sideJoint(e, bk, cpuSide); - const opp = sideJoint(e, bk, (1 - cpuSide) as 0 | 1); - let best = -Infinity; - for (const mine of my) { - let worst = Infinity; - for (const theirs of opp) { - const child = forkJoint(e, bk, cpuSide, mine, theirs); - const v = searchValue(e, child, cpuSide, depth - 1); - disposeFork(e, child); - if (v < worst) worst = v; - if (worst <= best) break; // argmax-invariant row prune (enumeration is rng-free) - } - if (worst > best) best = worst; - } - return best; -} - -/** Pick `cpuSide`'s two slot moves by depth-`depth` joint maximin (turn 0 → searched leads; - * forced-switch → enumerated bench combos; normal → joint maximin). */ -export function searchSideMoves(e: any, bk: Hex, cpuSide: 0 | 1, depth: number): [SlotMove, SlotMove] { - const [a0, a1] = sideSlots(cpuSide); - - // Turn 0: SEARCH the lead pair — maximin over both sides' send-ins. - if (Number(e.getTurnIdForBattleState(bk)) === 0) { - const leadPairs = (n: number): Array<[SlotMove, SlotMove]> => { - const v: Array<[SlotMove, SlotMove]> = []; - for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) if (i !== j) v.push([sw(i), sw(j)]); - return v; - }; - const my = leadPairs(teamSizeOf(e, bk, cpuSide)); - const opp = leadPairs(teamSizeOf(e, bk, (1 - cpuSide) as 0 | 1)); - let best: [SlotMove, SlotMove] = [sw(0), sw(1)]; - let bestVal = -Infinity; - for (const mine of my) { - let worst = Infinity; - for (const theirs of opp) { - const child = forkJoint(e, bk, cpuSide, mine, theirs); - const v = searchValue(e, child, cpuSide, Math.max(0, depth - 1)); - disposeFork(e, child); - if (v < worst) worst = v; - if (worst <= bestVal) break; - } - if (worst > bestVal) { - bestVal = worst; - best = mine; - } - } - return best; - } - - const flag = Number(e.getBattleContext(bk).playerSwitchForTurnFlag); - if (flag !== 2) { - // Forced-switch turn: enumerate my legal bench combos; opponent modeled first-legal. - const mask = flag & 0x0f; - const slots = activeSlots(e, bk); - const ts = teamSizeOf(e, bk, cpuSide); - const ko = koBitmap(e, bk, BigInt(cpuSide)); - const m0 = (mask & (1 << a0)) !== 0; - const m1 = (mask & (1 << a1)) !== 0; - if (!m0 && !m1) return [NO_OP_MOVE, NO_OP_MOVE]; // only the opponent is forced - - const b0 = m0 ? legalBenches(ts, ko, slots, a0) : []; - const b1 = m1 ? legalBenches(ts, ko, slots, a1) : []; - const combos: Array<[SlotMove, SlotMove]> = []; - if (m0 && m1) { - for (const x of b0) for (const y of b1) if (x !== y) combos.push([sw(x), sw(y)]); - } else if (m0) { - for (const x of b0) combos.push([sw(x), NO_OP_MOVE]); - } else { - for (const y of b1) combos.push([NO_OP_MOVE, sw(y)]); - } - if (combos.length === 0) combos.push([NO_OP_MOVE, NO_OP_MOVE]); - - const theirs = forcedJointModel(e, bk, (1 - cpuSide) as 0 | 1, mask, slots); - let best = combos[0]; - let bestVal = -Infinity; - for (const mine of combos) { - const child = forkJoint(e, bk, cpuSide, mine, theirs); - const v = searchValue(e, child, cpuSide, depth); // forced turns don't consume depth - disposeFork(e, child); - if (v > bestVal) { - bestVal = v; - best = mine; - } - } - return best; - } - - // Normal turn: joint maximin with the row prune. - const my = sideJoint(e, bk, cpuSide); - const opp = sideJoint(e, bk, (1 - cpuSide) as 0 | 1); - let best: [SlotMove, SlotMove] = my[0] ?? [NO_OP_MOVE, NO_OP_MOVE]; - let bestVal = -Infinity; - for (const mine of my) { - let worst = Infinity; - for (const theirs of opp) { - const child = forkJoint(e, bk, cpuSide, mine, theirs); - const v = searchValue(e, child, cpuSide, Math.max(0, depth - 1)); - disposeFork(e, child); - if (v < worst) worst = v; - if (worst <= bestVal) break; - } - if (worst > bestVal) { - bestVal = worst; - best = mine; - } - } - return best; -} - -/** The epsilon-greedy "Hard" doubles pilot (opt_prob = 1: always the best damaging option) — - * the baseline the search benchmarks against, mirroring the Rust `pick_side_moves`. */ -export function greedySideMoves(e: any, bk: Hex, side: 0 | 1): [SlotMove, SlotMove] { - const [a0, a1] = sideSlots(side); - if (Number(e.getTurnIdForBattleState(bk)) === 0) return [sw(0), sw(1)]; - - const flag = Number(e.getBattleContext(bk).playerSwitchForTurnFlag); - const slots = activeSlots(e, bk); - const ts = teamSizeOf(e, bk, side); - if (flag !== 2) { - return forcedJointModel(e, bk, side, flag & 0x0f, slots); - } - const pick = (abs: number): SlotMove => { - const myMon = slots[abs]; - if (myMon === EMPTY_LANE) return NO_OP_MOVE; - const options = damagingOptions(e, bk, side, myMon); - if (options.length === 0) return NO_OP_MOVE; - let best = options[0]; - for (const o of options) if (o.dmg > best.dmg) best = o; - return best.sm; - }; - void ts; - return [pick(a0), pick(a1)]; -} diff --git a/sims/src/cpu/engine-view.ts b/sims/src/cpu/engine-view.ts deleted file mode 100644 index 27f3574b..00000000 --- a/sims/src/cpu/engine-view.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { Hex } from './hex'; -import { MoveClass, MonStateIndexName, Type } from '../../../transpiler/ts-output/Enums'; -import { moveSlotLib } from '../../../transpiler/ts-output/moves/MoveSlotLib'; -import { typeCalcLib } from '../../../transpiler/ts-output/types/TypeCalcLib'; -import { SWITCH_MOVE_INDEX, NO_OP_INDEX } from './constants'; - -/** - * Idiomatic TS port of the SHARED on-chain CPU base (`CPU.sol`): - * - `_calculateValidMoves` -> {@link calculateValidMoves} - * - `_sampleRNG` / `getRNG` -> {@link makeRng} (a simple injected PRNG; chain parity is NOT needed - * because whatever we submit is just a *legal* move; only the DISTRIBUTION of random draws must - * match — see the fidelity notes on each random pick). - * - the validation helpers (`_validateCPUMove*` / `ValidatorLogic`) -> the local engine's - * `validatePlayerMoveForBattle`, which runs the inline validator when `validator == 0` (the - * local case), so it is the faithful equivalent of `CPU._validateCPUMove`. - * - `_buildCPUContext` reads -> the small native readers below. - * - * The CPU is ALWAYS p1 (playerIndex 1n); the human opponent is p0 (0n). `e` is the LOCAL transpiled - * engine (LocalBattleService.getEngine). This module is heuristic-free — it provides the shared - * candidate-enumeration + readers that the OkayCPU / FairCPU / BetterCPU ports build their decisions - * on. (`MoveMeta` caching and the inline-vs-engine validation split are gas scaffolding and are - * intentionally dropped per the FIDELITY MANDATE.) - */ - -// The CPU's player index. CPU.sol hard-codes p1 everywhere (`_buildCPUContext`, `_validateCPUMove` -// pass playerIndex 1). -const CPU_PLAYER_INDEX = 1n; -const OPP_PLAYER_INDEX = 0n; - -// NUM_MOVES in CPU.sol is the engine's DEFAULT_MOVES_PER_MON (4). We read the active mon's actual -// move slots and stop at the first empty slot so <4-move mons are handled exactly like -// `_buildCPUContext` (which caps `len` at the mon's real `moves.length`). -const MAX_MOVES = 4; - -export type RevealedMove = { moveIndex: number; extraData: number }; - -// --------------------------------------------------------------------------------------------- -// RNG -// --------------------------------------------------------------------------------------------- - -/** - * Simple deterministic PRNG (mulberry32) returning a float in [0, 1). Whatever it returns is - * submitted as the CPU move, so there is NO chain-parity requirement — the only contract is that - * the *distribution* of draws reproduces the Solidity `_sampleRNG(...) % N` uniform picks. A - * Solidity `_getRNG % 6 == 5` becomes `rng() < 1/6`; a uniform pick among N candidates becomes - * `Math.floor(rng() * N)`. - */ -export function makeRng(seed: number = (Math.random() * 0x100000000) >>> 0): () => number { - let s = seed >>> 0; - return () => { - s = (s + 0x6d2b79f5) | 0; - let t = Math.imul(s ^ (s >>> 15), 1 | s); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -// --------------------------------------------------------------------------------------------- -// Small readers (the state CPU.sol's `_buildCPUContext` pulls off the engine) -// --------------------------------------------------------------------------------------------- - -/** Active mon indices [p0, p1], off getBattleContext (the transpose proxy seat-swaps them). */ -export function activeMonIndices(e: any, bk: Hex): [number, number] { - const ctx = e.getBattleContext(bk); - return [Number(ctx.p0ActiveMonIndex), Number(ctx.p1ActiveMonIndex)]; -} - -export function cpuActiveMonIndex(e: any, bk: Hex): number { - return activeMonIndices(e, bk)[1]; -} - -export function oppActiveMonIndex(e: any, bk: Hex): number { - return activeMonIndices(e, bk)[0]; -} - -export function teamSize(e: any, bk: Hex, playerIndex: bigint): number { - const packed = Number(e.battleConfig[e._getStorageKey(bk)].teamSizes); - return playerIndex === 0n ? packed & 0x0f : packed >> 4; -} - -export function cpuTeamSize(e: any, bk: Hex): number { - return teamSize(e, bk, CPU_PLAYER_INDEX); -} - -export function oppTeamSize(e: any, bk: Hex): number { - return teamSize(e, bk, OPP_PLAYER_INDEX); -} - -export function koBitmap(e: any, bk: Hex, playerIndex: bigint): number { - return Number(e.getKOBitmap(bk, playerIndex)); -} - -export function isMonKO(e: any, bk: Hex, playerIndex: bigint, monIndex: number): boolean { - return (koBitmap(e, bk, playerIndex) & (1 << monIndex)) !== 0; -} - -// Base stamina (max) of a mon — `cpuActiveMonBaseStamina` in CPUContext. -export function monBaseStamina(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - return Number(e.getMonValueForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Stamina)); -} - -// staminaDelta (the engine getter already normalizes the CLEARED sentinel to 0). -export function monStaminaDelta(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - return Number(e.getMonStateForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Stamina)); -} - -// Current stamina = base + delta (matches ValidatorLogic.validateSpecificMoveSelection's arithmetic). -export function monCurrentStamina(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - return monBaseStamina(e, bk, playerIndex, monIndex) + monStaminaDelta(e, bk, playerIndex, monIndex); -} - -export function monHpDelta(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - return Number(e.getMonStateForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Hp)); -} - -// Base (max) HP of a mon — the value getter, no delta. -export function monMaxHp(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - return Number(e.getMonValueForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Hp)); -} - -// Current HP = base + delta (the engine getter normalizes the CLEARED sentinel to 0). -export function monCurrentHp(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - return monMaxHp(e, bk, playerIndex, monIndex) + monHpDelta(e, bk, playerIndex, monIndex); -} - -// Current speed = base + delta (the speed-race sibling of monCurrentStamina). -export function monCurrentSpeed(e: any, bk: Hex, playerIndex: bigint, monIndex: number): number { - const base = Number(e.getMonValueForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Speed)); - const delta = Number(e.getMonStateForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Speed)); - return base + delta; -} - -// Mon types as [type1, type2]. type2 may be Type.None for single-type mons. -export function monTypes(e: any, bk: Hex, playerIndex: bigint, monIndex: number): [Type, Type] { - return [ - Number(e.getMonValueForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Type1)) as Type, - Number(e.getMonValueForBattle(bk, playerIndex, BigInt(monIndex), MonStateIndexName.Type2)) as Type, - ]; -} - -// Raw move slot (bigint) for a mon's move index, or undefined past the mon's real move count -// (the transpiled getter indexes `moves[i]`, returning undefined for <4-move mons). -export function moveSlot(e: any, bk: Hex, playerIndex: bigint, monIndex: number, moveIndex: number): bigint | undefined { - return e.getMoveForMonForBattle(bk, playerIndex, BigInt(monIndex), BigInt(moveIndex)); -} - -export function moveClassOf(e: any, bk: Hex, slot: bigint): MoveClass { - return moveSlotLib.moveClass(slot, e, bk) as MoveClass; -} - -export function moveTypeOf(e: any, bk: Hex, slot: bigint): Type { - return moveSlotLib.moveType(slot, e, bk) as Type; -} - -// Deployed-move address → its moves.csv InputType ('none' | 'self-mon' | 'opponent-mon') — the -// off-chain replacement for the removed on-chain ExtraDataType, mirroring the Rust port's -// INPUT_TYPE_BY_ADDR. Filled by `buildTeamMon` at team-resolve time (before any game runs). -const INPUT_TYPE_BY_ADDR = new Map(); - -export function registerMoveInputType(addr: bigint, inputType: string): void { - INPUT_TYPE_BY_ADDR.set(addr, inputType); -} - -/** InputType of an EXTERNAL move word (address = low 160 bits); 'none' for unknown moves. */ -export function moveInputTypeOf(slot: bigint): string { - return INPUT_TYPE_BY_ADDR.get(slot & ((1n << 160n) - 1n)) ?? 'none'; -} - -// Deployed-move address → its moves.csv TargetSpec (self-only / none / opponent-slot / …) — -// the client-facing target domain; nibble-free specs (self-only/none) need no target bits. -const TARGET_SPEC_BY_ADDR = new Map(); - -export function registerMoveTargetSpec(addr: bigint, targetSpec: string): void { - TARGET_SPEC_BY_ADDR.set(addr, targetSpec); -} - -/** TargetSpec of an EXTERNAL move word; 'any-other-slot' (the blank default) for unknown moves. */ -export function moveTargetSpecOf(slot: bigint): string { - return TARGET_SPEC_BY_ADDR.get(slot & ((1n << 160n) - 1n)) ?? 'any-other-slot'; -} - -// The opponent's (p0's) revealed move this turn — BetterCPU peeks at this. { moveIndex, extraData } -// with the on-chain packing decoded (low 7 bits = move index; bit 7 = isRealTurn). -export function opponentRevealedMove(e: any, bk: Hex): RevealedMove { - const d = e.getMoveDecisionForSlot(bk, OPP_PLAYER_INDEX, 0n); - return { moveIndex: Number(d.packedMoveIndex) & 0x7f, extraData: Number(d.extraData) }; -} - -// --------------------------------------------------------------------------------------------- -// Type effectiveness primitive — the SAME scaled value the Solidity CPUs compare against. -// -// This is the RAW single-pair primitive `TYPE_CALC.getTypeEffectiveness(attack, defType, scale)`. -// Subclasses MUST compose it exactly as their Solidity counterpart does — there is deliberately NO -// "combined dual-type" helper here, because the Solidity CPUs compose differently per use-site and -// baking one rule in would be an invented heuristic: -// - OkayCPU.sol:192-196 — move-vs-defender at scale 2: `eff = f(attack, defType1, 2)`, then if -// defType2 != None `eff *= f(attack, defType2, 2)` (a RAW PRODUCT, no division), comparing -// `eff > 2` (super-effective) and `eff == 0 || eff == 1` (immune/resist). -// - HeuristicCPUBase `_offensiveMatchupScore` (scale 10) — a SUM across the type pairs. -// With scale=2 the primitive returns 0 (immune) / 1 (0.5x) / 2 (1x) / 4 (2x); with scale=10 it -// returns 0 / 5 / 10 / 20. Use whatever scale + composition the specific Solidity CPU uses. -// --------------------------------------------------------------------------------------------- - -/** - * `TYPE_CALC.getTypeEffectiveness(attackType, defenderType, scale)` — TypeCalculator.sol delegates - * straight to TypeCalcLib, so calling the lib directly is the faithful equivalent. - */ -export function typeEffectiveness(attack: Type, defenderType: Type, scale: number): number { - return Number(typeCalcLib.getTypeEffectiveness(attack, defenderType, BigInt(scale))); -} - -// --------------------------------------------------------------------------------------------- -// calculateValidMoves — faithful port of CPU._calculateValidMoves -// --------------------------------------------------------------------------------------------- - -function validate(e: any, bk: Hex, moveIndex: number, extraData: number): boolean { - // Faithful equivalent of CPU._validateCPUMove: the local engine runs the inline validator - // (validator == 0) for p1. - return e.validatePlayerMoveForBattle(bk, BigInt(moveIndex), CPU_PLAYER_INDEX, BigInt(extraData)) as boolean; -} - -/** - * Port of `CPU._calculateValidMoves`. Returns the three candidate buckets the subclass heuristics - * consume. `rng` is only consulted to pick the extraData TARGET for Self/Opponent-index moves (the - * same place CPU.sol calls `_sampleRNG(...) % count`); pass one if you want deterministic targets. - * - * CPU.sol:58-169 - */ -export function calculateValidMoves( - e: any, - bk: Hex, - rng: () => number = makeRng(), -): { noOp: RevealedMove[]; moves: RevealedMove[]; switches: RevealedMove[] } { - const tId = Number(e.getTurnIdForBattleState(bk)); - const p1TeamSize = cpuTeamSize(e, bk); - - // CPU.sol:68-75 — turn 0: every team slot is an (unvalidated) switch-in choice. - if (tId === 0) { - const switches: RevealedMove[] = []; - for (let i = 0; i < p1TeamSize; i++) { - switches.push({ moveIndex: SWITCH_MOVE_INDEX, extraData: i }); - } - return { noOp: [], moves: [], switches }; - } - - const activeMonIndex = cpuActiveMonIndex(e, bk); - - // CPU.sol:80-91 — collect valid switch targets (i != active, validated). - const validSwitchIndices: number[] = []; - for (let i = 0; i < p1TeamSize; i++) { - if (i !== activeMonIndex && validate(e, bk, SWITCH_MOVE_INDEX, i)) { - validSwitchIndices.push(i); - } - } - - const validSwitchesArray: RevealedMove[] = validSwitchIndices.map((i) => ({ - moveIndex: SWITCH_MOVE_INDEX, - extraData: i, - })); - - // CPU.sol:93-103 — a p1 forced-switch turn (playerSwitchForTurnFlag === 1) returns ONLY valid switches. - if (Number(e.getBattleContext(bk).playerSwitchForTurnFlag) === 1) { - return { noOp: [], moves: [], switches: validSwitchesArray }; - } - - // CPU.sol:108-152 — enumerate valid moves. For each move slot pick a valid extraData target the - // way _calculateValidMoves does (random among the legal targets), then validate. - const moves: RevealedMove[] = []; - for (let i = 0; i < MAX_MOVES; i++) { - const slot = moveSlot(e, bk, CPU_PLAYER_INDEX, activeMonIndex, i); - if (slot === undefined) break; // <4-move mon: stop at the real move count (mirrors len cap) - - let extraDataToUse = 0; - - if (!moveSlotLib.isInline(slot)) { - // The engine dropped the on-chain ExtraDataType getter; consult the off-chain InputType - // (moves.csv) instead, a uniform pick like the old CPU._calculateValidMoves (Rust-port parity). - const it = moveInputTypeOf(slot); - - if (it === 'self-mon') { - // Needs a self switch target; skip the move if there are none. - if (validSwitchIndices.length === 0) continue; - const r = Math.floor(rng() * validSwitchIndices.length); - extraDataToUse = validSwitchIndices[r]; - } else if (it === 'opponent-mon') { - // Build non-KO opponent targets; skip the move if there are none. - const opponentTeamSize = oppTeamSize(e, bk); - const oppKO = koBitmap(e, bk, OPP_PLAYER_INDEX); - const validTargets: number[] = []; - for (let j = 0; j < opponentTeamSize; j++) { - if ((oppKO & (1 << j)) === 0) validTargets.push(j); - } - if (validTargets.length === 0) continue; - const r = Math.floor(rng() * validTargets.length); - extraDataToUse = validTargets[r]; - } - // 'none' / inline moves fall through with extraData 0. - } - - // CPU.sol:149-151 — validate the candidate (stamina + move-specific) and keep it if legal. - if (validate(e, bk, i, extraDataToUse)) { - moves.push({ moveIndex: i, extraData: extraDataToUse }); - } - } - - // CPU.sol:164-165 — a single no-op is always offered on a non-forced-switch turn. - const noOp: RevealedMove[] = [{ moveIndex: NO_OP_INDEX, extraData: 0 }]; - - return { noOp, moves, switches: validSwitchesArray }; -} - -// Uniform pick among a candidate list (the shape every subclass uses for "pick one of N"). Returns -// undefined for an empty list so callers can fall back the way the Solidity CPUs do. -export function pickUniform(arr: T[], rng: () => number): T | undefined { - if (arr.length === 0) return undefined; - return arr[Math.floor(rng() * arr.length)]; -} diff --git a/sims/src/cpu/evaluator-ttk.ts b/sims/src/cpu/evaluator-ttk.ts deleted file mode 100644 index f8f577ab..00000000 --- a/sims/src/cpu/evaluator-ttk.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Hex } from './hex'; -import { MoveClass } from '../../../transpiler/ts-output/Enums'; -import { MoveMeta } from '../../../transpiler/ts-output/Structs'; -import { BattleView } from './battle-view'; -import { monCurrentSpeed } from './engine-view'; -import { - buildDamageCalcContext, - estimateDamageMeta, - loadMonMetas, - popcount8, -} from './heuristic-shared'; - -/** - * RACE-MATRIX EVALUATOR — first-principles alternative to `scoreState`'s linear feature weights. - * - * The win condition is KOing the whole opposing roster, so a position's value is the state of every - * pairwise RACE: for each (my mon i, their mon j), the turns i needs to KO j (TTK) under the real - * damage formula — per-pair contexts include stat deltas, so boosts/burns reprice races natively. - * Stats, types, and HP carry no intrinsic weight; each matters exactly as far as it changes a race. - * - * Aggregation: each side's threat against an enemy mon is its best ANSWER speed — the min adjusted - * TTK over its living roster, where "adjusted" pays +1 turn for deploying from the bench and +1 for - * losing the head-to-head race (the answer can't simply stand and trade). Answer speeds sum as - * 1/TTK (a wall ⇒ 1/∞ ⇒ 0 — the anti-wall signal is intrinsic, and removing the one mon that - * answered a sweeper visibly rewrites a column). Banked KOs, the current active pair's race (tempo), - * and a small stamina nudge complete the score. Three scale knobs replace six feature weights. - * - * Costlier than the pure-view scoreState (it decodes metas and builds contexts per pair) — fine for - * root-level forks, noticeable but acceptable inside deep planner trees. - */ - -const CPU_PLAYER_INDEX = 1n; -const OPP_PLAYER_INDEX = 0n; - -const SCALE = 150; // one full answer-unit (a 1-turn answer) in scoreState-comparable units -const W_KO = 300; // a banked KO outranks the best living answer -const W_TEMPO = 75; // current-pair race edge — the one positional fact that matters -const W_STAMINA = 2; - -interface SideData { - metas: (MoveMeta[] | null)[]; - speed: number[]; -} - -function readSide(e: any, bk: Hex, playerIndex: bigint, mons: BattleView['mons']['p0']): SideData { - const metas: (MoveMeta[] | null)[] = []; - const speed: number[] = []; - for (let i = 0; i < mons.length; i++) { - metas.push(mons[i].ko ? null : loadMonMetas(e, bk, playerIndex, i)); - speed.push(mons[i].ko ? 0 : monCurrentSpeed(e, bk, playerIndex, i)); - } - return { metas, speed }; -} - -/** Best single-move damage from attacker (its decoded metas) into the prepared context. */ -function bestDamage(ctx: any, metas: MoveMeta[]): number { - let best = 0; - for (const meta of metas) { - if (meta.moveClass !== MoveClass.Physical && meta.moveClass !== MoveClass.Special) continue; - const d = estimateDamageMeta(ctx, meta); - if (d > best) best = d; - } - return best; -} - -export function ttkEval(view: BattleView): number { - const e = view.engine; - const bk = view.bk as Hex; - const mine = view.mons.p1; - const theirs = view.mons.p0; - - const my = readSide(e, bk, CPU_PLAYER_INDEX, mine); - const op = readSide(e, bk, OPP_PLAYER_INDEX, theirs); - - // Pairwise TTK in both directions (Infinity = walled). FRACTIONAL turns: the aggregation must be - // smooth in damage (ceil'd TTK is piecewise-constant in exactly what a root decision varies, which - // collapses most candidates into ties); race OUTCOMES compare ceil'd turns below, where turn - // boundaries are real. - const ttkMine: number[][] = mine.map(() => theirs.map(() => Infinity)); - const ttkTheirs: number[][] = theirs.map(() => mine.map(() => Infinity)); - for (let i = 0; i < mine.length; i++) { - if (mine[i].ko) continue; - for (let j = 0; j < theirs.length; j++) { - if (theirs[j].ko) continue; - // Floored at one turn: nothing dies faster, and without the floor overkill capacity against - // weakened prey outranks the KO credit for actually finishing it. - const ctxMine = buildDamageCalcContext(e, bk, CPU_PLAYER_INDEX, i, OPP_PLAYER_INDEX, j); - const dMine = bestDamage(ctxMine, my.metas[i]!); - if (dMine > 0) ttkMine[i][j] = Math.max(1, theirs[j].hp / dMine); - const ctxTheirs = buildDamageCalcContext(e, bk, OPP_PLAYER_INDEX, j, CPU_PLAYER_INDEX, i); - const dTheirs = bestDamage(ctxTheirs, op.metas[j]!); - if (dTheirs > 0) ttkTheirs[j][i] = Math.max(1, mine[i].hp / dTheirs); - } - } - - // Answer speeds: min adjusted TTK per enemy mon; +1 for bench deployment, +1 for losing the - // head-to-head race (equal TTK loses on slower-or-tied speed, matching weGoFirst's pessimism). - let offense = 0; - for (let j = 0; j < theirs.length; j++) { - if (theirs[j].ko) continue; - let bestAnswer = Infinity; - for (let i = 0; i < mine.length; i++) { - if (mine[i].ko || ttkMine[i][j] === Infinity) continue; - const tMine = Math.ceil(ttkMine[i][j]); - const tTheirs = Math.ceil(ttkTheirs[j][i]); - const losesRace = tTheirs < tMine || (tTheirs === tMine && op.speed[j] >= my.speed[i]); - const adj = ttkMine[i][j] + (i === view.cpuActive ? 0 : 1) + (losesRace ? 1 : 0); - if (adj < bestAnswer) bestAnswer = adj; - } - if (bestAnswer !== Infinity) offense += 1 / bestAnswer; - } - let defense = 0; - for (let i = 0; i < mine.length; i++) { - if (mine[i].ko) continue; - let bestAnswer = Infinity; - for (let j = 0; j < theirs.length; j++) { - if (theirs[j].ko || ttkTheirs[j][i] === Infinity) continue; - const tMine = Math.ceil(ttkMine[i][j]); - const tTheirs = Math.ceil(ttkTheirs[j][i]); - const losesRace = tMine < tTheirs || (tMine === tTheirs && my.speed[i] >= op.speed[j]); - const adj = ttkTheirs[j][i] + (j === view.oppActive ? 0 : 1) + (losesRace ? 1 : 0); - if (adj < bestAnswer) bestAnswer = adj; - } - if (bestAnswer !== Infinity) defense += 1 / bestAnswer; - } - - // Tempo: the race already in progress between the actives (a pending skip costs its side a turn). - const a = view.cpuActive; - const b = view.oppActive; - let tempo = 0; - let stamina = 0; - if (!mine[a]?.ko && !theirs[b]?.ko) { - const ta = ttkMine[a][b] + (mine[a].skipTurn ? 1 : 0); - const tb = ttkTheirs[b][a] + (theirs[b].skipTurn ? 1 : 0); - tempo = W_TEMPO * ((ta === Infinity ? 0 : 1 / ta) - (tb === Infinity ? 0 : 1 / tb)); - stamina = W_STAMINA * (mine[a].stamina - theirs[b].stamina); - } - - const ko = W_KO * (popcount8(view.oppKO) - popcount8(view.cpuKO)); - return ko + SCALE * (offense - defense) + tempo + stamina; -} diff --git a/sims/src/cpu/evaluator.ts b/sims/src/cpu/evaluator.ts deleted file mode 100644 index 81035152..00000000 --- a/sims/src/cpu/evaluator.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { offensiveMatchupScore, popcount8 } from './heuristic-shared'; -import { BattleView, MonView } from './battle-view'; - -/** - * SEARCH SUBSTRATE — layer 3: a single static evaluation of a position from the CPU's (p1's) point of - * view. HIGHER = better for the CPU. - * - * This is deliberately an AGGREGATOR, not a new combat model: every term is either a `BattleView` - * field or an existing `heuristic-shared` / `engine-view` primitive. There are NO invented combat - * heuristics, damage models, or threat tables here — a search routine layers those on top by calling - * `applyHypotheticalMove` and scoring the resulting views with this function. - * - * Terms (all from the CPU's perspective; p1 = CPU, p0 = opponent): - * 1. HP swing — Σ (cpu hp% − opp hp%) over active rosters, scaled to be the dominant term. - * 2. KO differential — (opp mons KO'd − cpu mons KO'd), heavily weighted (a KO is worth a lot). - * 3. Matchup edge — active-mon `offensiveMatchupScore` (CPU→opp) minus the opponent's (opp→CPU). - * 4. Stamina edge — small bonus for the CPU active mon having more stamina than the opp active mon. - * 5. Stat-stage edge — active-mon `statDeltaScore` diff: setup boosts score positive, burn's attack - * divide / frostbite's spatk divide negative. (Panic is a stamina drain — term 4 already sees it.) - * 6. Skip edge — the active mon's pending ShouldSkipTurn (zap) costs its next action. Sleep is - * NOT visible to a static snapshot (it NO_OPs the sleeper's move instead of flagging); a deeper - * search still prices it in as zero damage output across the simulated turns. - */ - -// Weights. HP/KO dominate; matchup and stamina are tie-breakers. Kept as named constants so the -// composition is legible — none of these are engine values, just the relative emphasis of the -// already-existing signals. -/** The relative emphasis of the six position signals. Tunable: `fit-eval.ts` fits these from logged - * game outcomes; strategies can carry their own weights for A/B without touching the default. */ -export interface EvalWeights { - hp: number; - ko: number; - matchup: number; - stamina: number; - statDelta: number; - skip: number; -} - -export const DEFAULT_EVAL_WEIGHTS: EvalWeights = { - hp: 1, // hp% diff is already 0..100 per mon; summed it is the primary signal. - ko: 150, // one KO swing ≈ a mon at full HP plus margin. - matchup: 0.5, // offensiveMatchupScore is in scale-10 units summed over type pairs. - stamina: 2, // tiny nudge per point of active-mon stamina advantage. - statDelta: 40, // a full base-stat of boost (Σ delta/base = 1.0) ≈ 40 hp%; burn's half-attack ≈ -20. - skip: 30, // losing the next action ≈ eating a strong hit. -}; - -/** hp% (0..100) for a slot: current hp / max hp — both already on the snapshot, so this is pure-view - * (no engine read), which matters when a search scores thousands of forked positions. */ -function hpPercent(mon: MonView): number { - if (mon.maxHp <= 0) return 0; - return (Math.max(0, mon.hp) * 100) / mon.maxHp; -} - -/** - * The six UNWEIGHTED terms, CPU-perspective (higher = better for the CPU). `scoreStateWith` is their - * weighted sum; the fitting script consumes them raw so features and scoring can't drift apart. - */ -export function evalFeatures(view: BattleView): [number, number, number, number, number, number] { - // 1. HP swing: Σ cpu hp% − Σ opp hp% over all roster slots. - let cpuHpPct = 0; - for (let i = 0; i < view.mons.p1.length; i++) { - cpuHpPct += hpPercent(view.mons.p1[i]); - } - let oppHpPct = 0; - for (let i = 0; i < view.mons.p0.length; i++) { - oppHpPct += hpPercent(view.mons.p0[i]); - } - const hp = cpuHpPct - oppHpPct; - - // 2. KO differential: opponent KOs are good for the CPU, CPU KOs are bad. - const ko = popcount8(view.oppKO) - popcount8(view.cpuKO); - - // 3-6. Active-mon terms: matchup edge, stamina edge, stat-stage edge (setup / burn / frostbite), - // pending turn-skips (zap). - const cpu = view.mons.p1[view.cpuActive]; - const opp = view.mons.p0[view.oppActive]; - let matchup = 0; - let stamina = 0; - let statDelta = 0; - let skip = 0; - if (cpu && opp) { - matchup = - offensiveMatchupScore(cpu.type1, cpu.type2, opp.type1, opp.type2) - - offensiveMatchupScore(opp.type1, opp.type2, cpu.type1, cpu.type2); - stamina = cpu.stamina - opp.stamina; - statDelta = cpu.statDeltaScore - opp.statDeltaScore; - skip = (opp.skipTurn ? 1 : 0) - (cpu.skipTurn ? 1 : 0); - } - - return [hp, ko, matchup, stamina, statDelta, skip]; -} - -export function scoreStateWith(view: BattleView, w: EvalWeights): number { - const [hp, ko, matchup, stamina, statDelta, skip] = evalFeatures(view); - return w.hp * hp + w.ko * ko + w.matchup * matchup + w.stamina * stamina + w.statDelta * statDelta + w.skip * skip; -} - -export function scoreState(view: BattleView): number { - return scoreStateWith(view, DEFAULT_EVAL_WEIGHTS); -} diff --git a/sims/src/cpu/forward-model.ts b/sims/src/cpu/forward-model.ts deleted file mode 100644 index 307180f1..00000000 --- a/sims/src/cpu/forward-model.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { Hex } from './hex'; -import { runNested } from '../../../transpiler/ts-output/runtime'; -import { SWITCH_MOVE_INDEX, MOVE_INDEX_OFFSET, IS_REAL_TURN_BIT } from '../../../transpiler/ts-output/Constants'; -import { - BattleConfig, - BattleData, - createDefaultBattleConfig, - createDefaultBattleData, -} from '../../../transpiler/ts-output/Structs'; -import { BattleView, captureBattleView } from './battle-view'; - -/** - * SEARCH SUBSTRATE — layer 2: a NON-DESTRUCTIVE forward model. - * - * Lets a strategy ask "what would the position look like if both players played X / Y this turn?" - * WITHOUT touching the live battle. The trick is a shallow-but-careful clone of the engine's per- - * battle storage (`battleData[bk]` + `battleConfig[storageKey]`) under a throwaway fork key, then a - * faithful replay of `Engine.executeWithMoves` against that fork — minus the moveManager authorization - * gate (which we are deliberately bypassing because the search code is not the registered moveManager). - * - * What is and isn't cloned: - * - Plain objects / arrays / bigints / strings / booleans -> DEEP-COPIED (mutating the fork can't - * leak back into the live battle's state tree). - * - Class instances (`v.constructor !== Object`) -> SHARED BY REFERENCE. Effects, the - * team registry, validator, rng oracle, engine-hook contracts and the packed move/ability - * Contract slots are global singletons; cloning them would (a) be wrong (they hold no per-battle - * mutable state we own) and (b) break identity comparisons the engine relies on. - * - * CPU is p1, human is p0 (inherited convention), but the fork machinery is side-agnostic. - */ - -// --------------------------------------------------------------------------------------------- -// cloneState — deep-copy plain data, share class instances by reference. -// --------------------------------------------------------------------------------------------- - -/** - * Deep-copy `v`, but pass anything that is NOT a plain Object/Array (i.e. a class instance, where - * `v.constructor !== Object`) straight through by reference. This is the single invariant that keeps - * the fork cheap AND correct: the engine's singletons (Contracts) are shared, the per-battle data - * tree (structs, packed Records, Mon arrays) is copied. - * - * Proxy-wrapped Records (the auto-vivifying `battleConfig.p0Team` etc.) report `constructor === Object` - * through their target, so they are treated as plain objects and copied via their OWN enumerable keys - * (the proxy only materializes a key on access, so `Object.keys` already lists every touched slot). - */ -export function cloneState(v: T, seen: WeakMap = new WeakMap()): T { - // Primitives (incl. bigint, string, boolean, number, null, undefined, symbol) — copied by value. - if (v === null || typeof v !== 'object') return v; - - // Class instances (Contracts, effects, registries, …) — shared by reference, NOT cloned. - // Arrays and plain/Proxy-wrapped objects have `constructor === Object`/`Array`; everything else - // (Engine, IEffect impls, ITeamRegistry, the stub `{_contractAddress}` is Object so it copies). - const ctor = (v as object).constructor; - if (ctor !== Object && ctor !== Array) return v; - - const cached = seen.get(v as object); - if (cached !== undefined) return cached; - - if (Array.isArray(v)) { - const out: any[] = []; - seen.set(v as object, out); - for (let i = 0; i < v.length; i++) out[i] = cloneState((v as any[])[i], seen); - return out as unknown as T; - } - - const out: Record = {}; - seen.set(v as object, out); - for (const k of Object.keys(v as object)) { - out[k] = cloneState((v as Record)[k], seen); - } - return out as unknown as T; -} - -// --------------------------------------------------------------------------------------------- -// forkBattle — clone the live battle's storage under a fresh fork key. -// --------------------------------------------------------------------------------------------- - -// The `BattleConfig` fields that are auto-vivifying Record Proxies (the engine writes NEW keys into -// these during execution — e.g. `config.p1Effects[slot]` when a move adds an effect — and relies on -// the default-factory `get` trap). A naive deep-clone turns them into plain objects and breaks that, -// so the fork must keep FRESH proxies (from `createDefaultBattleConfig`) and copy materialized -// entries into them. All OTHER config fields are scalars/structs and clone cleanly via `cloneState`. -const CONFIG_PROXY_MAP_FIELDS = [ - 'p0States', - 'p1States', - 'globalEffects', - 'p0Effects', - 'p1Effects', - 'engineHooks', -] as const; - -// Team arrays are written only at battle start (`Engine.startBattle`), never during a turn — the engine -// only READS `config.p{0,1}Team` while replaying a move (stat boosts mutate mon STATE deltas, not the -// stored team). So the fork shares the live team proxies by reference instead of deep-cloning ~8 -// nested StoredMon structs per fork. Exact: nothing on the replayed turn writes them. -const CONFIG_SHARED_FIELDS = new Set(['p0Team', 'p1Team']); - -/** - * Clone a live `BattleConfig` into a fresh one that PRESERVES the auto-vivifying proxies on its map - * fields. Scalar/struct fields are deep-copied (`cloneState`); each proxy-map field starts from the - * fresh default proxy and gets every materialized own-key copied in (deep-cloned), so the engine can - * keep adding new slots on the fork without `undefined`-slot crashes. The immutable-during-turn team - * proxies are shared by reference (see {@link CONFIG_SHARED_FIELDS}). - */ -function cloneBattleConfig(src: BattleConfig): BattleConfig { - const out = createDefaultBattleConfig(); - const proxyFields = new Set(CONFIG_PROXY_MAP_FIELDS as readonly string[]); - const s = src as unknown as Record; - const o = out as unknown as Record; - - for (const k of Object.keys(s)) { - if (CONFIG_SHARED_FIELDS.has(k)) { - o[k] = s[k]; // share the live team proxy by reference (read-only during a turn) - } else if (proxyFields.has(k)) { - // Copy each materialized entry into the fresh proxy (which retains its default factory). - const srcMap = s[k] as Record; - const dstMap = o[k] as Record; - for (const mk of Object.keys(srcMap)) { - dstMap[mk] = cloneState(srcMap[mk]); - } - } else { - o[k] = cloneState(s[k]); - } - } - return out; -} - -/** Clone live `BattleData` into a fresh struct (all scalar fields — plain deep-copy). */ -function cloneBattleData(src: BattleData): BattleData { - const out = createDefaultBattleData(); - const s = src as unknown as Record; - const o = out as unknown as Record; - for (const k of Object.keys(s)) o[k] = cloneState(s[k]); - return out; -} - -let _forkCounter = 0; - -/** Deterministic-ish unique fork battle key (32-byte hex). Marked with a high tag nibble so it can - * never collide with a real keccak battle key in practice. */ -function nextForkKey(): Hex { - _forkCounter = (_forkCounter + 1) >>> 0; - const tag = 'f0fc'; // "fork" - const body = _forkCounter.toString(16).padStart(8, '0'); - // 4 (tag) + 8 (counter) = 12 hex; pad the remaining 52 with the counter again for uniqueness. - return ('0x' + tag + body + body.padStart(52, '0')) as Hex; -} - -/** - * Clone `e.battleData[bk]` and `e.battleConfig[_getStorageKey(bk)]` into a fresh fork key and return - * that key. The fork's storageKey naturally EQUALS the fork key (no `battleKeyToStorageKey` redirect - * is wired), mirroring `LocalBattleService.createBattle`'s re-key block — so every engine reader and - * `_executeInternal` resolves the fork's config at `battleConfig[forkKey]`. - */ -export function forkBattle(e: any, bk: string): Hex { - const eng = e as any; - const forkKey = nextForkKey(); - - // Source data lives at battleData[bk]; source config at battleConfig[_getStorageKey(bk)]. - const srcStorageKey = eng._getStorageKey(bk); - const srcData = eng.battleData[bk]; - const srcConfig = eng.battleConfig[srcStorageKey]; - - // Deep-clone the per-battle data tree; singletons inside are shared by reference (cloneState). The - // config clone preserves the auto-vivifying proxies on its map fields so the engine can write new - // effect / state slots on the fork during execution. - eng.__mutateBattleData(forkKey, cloneBattleData(srcData)); - eng.__mutateBattleConfig(forkKey, cloneBattleConfig(srcConfig)); - - // globalKV + globalKVKeySlots live OUTSIDE battleData/battleConfig, keyed by storageKey — and the - // locks / once-per-game flags of ~18 contracts live there (Initialize/Loop locks, the one-status- - // per-mon flag, Snack Break's halving ladder, Savior Complex, Modal Bolt modes, ...). Copy the - // source battle's entries under the fork key, or every forked turn sees them all as zero and the - // search rewards re-casting locked moves. Values are immutable strings/bigints, so a shallow - // per-entry copy through the auto-vivifying proxies is exact. - const srcKV = eng.globalKV[srcStorageKey] as Record; - const dstKV = eng.globalKV[forkKey] as Record; - for (const k of Object.keys(srcKV)) dstKV[k] = srcKV[k]; - const srcSlots = eng.globalKVKeySlots[srcStorageKey] as Record; - const dstSlots = eng.globalKVKeySlots[forkKey] as Record; - for (const k of Object.keys(srcSlots)) dstSlots[k] = srcSlots[k]; - - // No redirect: leaving battleKeyToStorageKey[forkKey] == 0 makes _getStorageKey(forkKey) return - // forkKey itself, so battleConfig[forkKey] is the resolved slot for both reads and execution. - return forkKey; -} - -/** - * Reclaim a fork's cloned state — deletes the `battleData` / `battleConfig` entries `forkBattle` added - * (the fork's storageKey equals its key, so both live under `forkKey`). A 1-ply search can skip this, - * but a deep search forks O(branching^depth) times per turn and must dispose each fork once its subtree - * is scored, or the fork maps grow without bound across a session. - */ -export function disposeFork(e: any, forkKey: string): void { - const eng = e as any; - delete eng.battleData[forkKey]; - delete eng.battleConfig[forkKey]; - delete eng.globalKV[forkKey]; - delete eng.globalKVKeySlots[forkKey]; -} - -// --------------------------------------------------------------------------------------------- -// applyHypotheticalMove — replay one turn on a fork, return the resulting view. -// --------------------------------------------------------------------------------------------- - -export interface HypotheticalMove { - moveIndex: number; - salt: bigint; - extraData: number; -} - -/** - * Fork `bk`, submit `p0` / `p1` (either may be null on a forced-switch turn where that side doesn't - * act), run ONE turn on the fork, and return a `BattleView` of the result. The original `bk` is left - * completely untouched. - * - * The turn is run by replicating `Engine.executeWithMoves` WITHOUT the moveManager gate: - * 1. Write both moves onto the fork config via `_setMoveInternal` (so config.p{0,1}Move/Salt are set - * exactly as a real submit would leave them). - * 2. Set storageKeyForWrite + the `_turnP0Packed` / `_turnP1Packed` encoded transients the way - * executeWithMoves does (these take precedence inside `_getCurrentTurnMove`). - * 3. Call `_executeInternal(forkKey, forkKey, false)` — `emitEvents=false` to keep the simulation - * silent (no BattleComplete / EngineExecute events leaking into the live event stream). - * 4. `resetCallContext()` to clear the write-context transients. - * - * Boundary note: the runtime resets ALL transient storage at a transaction boundary (the depth - * 0→1 entry) — which would wipe the `_turnP*Packed` we just set. `executeWithMoves` avoids this - * because it sets the transients INSIDE its own already-entered frame. We reproduce that by running - * the whole sequence inside `runNested`, which keeps callDepth > 0 so the inner `_executeInternal` / - * `resetCallContext` calls are seen as nested (no boundary reset). - */ -export function applyHypotheticalMove( - e: any, - bk: string, - p0: HypotheticalMove | null, - p1: HypotheticalMove | null, -): BattleView { - const forkKey = applyHypotheticalMoveKeyed(e, bk, p0, p1); - return captureBattleView(e, forkKey); -} - -/** - * Like {@link applyHypotheticalMove} but returns the FORK KEY instead of a view — the depth-search - * primitive: the caller can read/evaluate the fork, fork AGAIN from it (child plies), and must - * `disposeFork` it when its subtree is scored. `bk` may itself be a fork key (forkBattle resolves - * any live key). - */ -export function applyHypotheticalMoveKeyed( - e: any, - bk: string, - p0: HypotheticalMove | null, - p1: HypotheticalMove | null, -): Hex { - const eng = e as any; - const forkKey = forkBattle(eng, bk); - const config = eng.battleConfig[forkKey]; - - // runNested keeps callDepth > 0 so the transaction-boundary transient reset does NOT - // fire between us setting the turn transients and `_executeInternal` reading them. - runNested(() => { - // Match executeWithMoves: BOTH write transients (the inlined stat-boost path gates external - // engine writes — ability/effect re-entry — on battleKeyForWrite being set). - eng.__mutateStorageKeyForWrite(forkKey); - eng.__mutateBattleKeyForWrite(forkKey); - - // (1) write both moves onto the fork config, and (2) set the encoded turn transients that - // executeWithMoves would set (these win in _getCurrentTurnMove / _getCurrentTurnSalt). - if (p0) { - eng._setMoveInternal(config, 0n, BigInt(p0.moveIndex), p0.salt, BigInt(p0.extraData)); - eng.__mutate_turnP0Packed(packTurn(p0)); - } - if (p1) { - eng._setMoveInternal(config, 1n, BigInt(p1.moveIndex), p1.salt, BigInt(p1.extraData)); - eng.__mutate_turnP1Packed(packTurn(p1)); - } - - try { - // (3) run the turn on the fork (silent; singles — slotPacked=false, explicit because the - // doubles-era engine's WrongBattleMode guard compares it against the battle's mode byte). - eng._executeInternal(forkKey, forkKey, false, false, false); - } finally { - // (4) clear write-context transients. - eng.resetCallContext(); - } - }); - - return forkKey; -} - -/** - * Build a side's raw slot-turn word for `executeWithSlotMoves` (doubles): slot-0 = (m0, e0) in - * bits 0-23, slot-1 = (m1, e1) in bits 24-47, salt in bits 48+. extraData carries the target - * nibble in bits 12-15. Mirrors the Rust `pack_side`. - */ -export function packSide(m0: number, e0: number, m1: number, e1: number, salt: bigint): bigint { - return BigInt(m0) | (BigInt(e0) << 8n) | (BigInt(m1) << 24n) | (BigInt(e1) << 32n) | (salt << 48n); -} - -/** - * DOUBLES analogue of {@link applyHypotheticalMoveKeyed}: fork `bk`, run ONE silent slot-turn - * from each side's packed word (see {@link packSide}), return the fork key. Mirrors - * `executeWithSlotMoves` (turns via `_packSideTurn` transients + `slotPacked=true`); `bk` may be - * the live key or a fork (depth search). Caller must `disposeFork`. - */ -export function applyHypotheticalSlotMoveKeyed(e: any, bk: string, side0Packed: bigint, side1Packed: bigint): Hex { - const eng = e as any; - const forkKey = forkBattle(eng, bk); - runNested(() => { - eng.__mutateStorageKeyForWrite(forkKey); - eng.__mutateBattleKeyForWrite(forkKey); - eng.__mutate_turnP0Packed(eng._packSideTurn(side0Packed)); - eng.__mutate_turnP1Packed(eng._packSideTurn(side1Packed)); - try { - eng._executeInternal(forkKey, forkKey, false, false, true); // slotPacked=true, silent - } finally { - eng.resetCallContext(); - } - }); - return forkKey; -} - -// Mirror Engine.executeWithMoves's transient packing: storedIndex = moveIndex (+offset if a real -// move slot), then `(storedIndex | IS_REAL_TURN_BIT) | (extraData << 8)` shifted into the high bits -// by `_packTurn` (salt in the low 104 bits). We read the engine's own constants off the instance to -// avoid duplicating magic numbers. -function packTurn(m: HypotheticalMove): bigint { - // _packTurn(encoded, salt) = salt | (encoded << 104). We replicate inline rather than call the - // protected helper through the proxy (which is fine, but inlining keeps the math visible/auditable). - const mi = BigInt(m.moveIndex); - const stored = mi < SWITCH_MOVE_INDEX ? mi + MOVE_INDEX_OFFSET : mi; - const encoded = (stored | IS_REAL_TURN_BIT) | (BigInt(m.extraData) << 8n); - return m.salt | (encoded << 104n); -} diff --git a/sims/src/cpu/heuristic-native.ts b/sims/src/cpu/heuristic-native.ts deleted file mode 100644 index 7291469a..00000000 --- a/sims/src/cpu/heuristic-native.ts +++ /dev/null @@ -1,549 +0,0 @@ -import { Hex } from './hex'; -import { MoveClass, MonStateIndexName, Type } from '../../../transpiler/ts-output/Enums'; -import { moveSlotLib } from '../../../transpiler/ts-output/moves/MoveSlotLib'; -import { DamageCalcContext, MoveMeta } from '../../../transpiler/ts-output/Structs'; -import { NO_OP_INDEX, SWITCH_MOVE_INDEX } from './constants'; -import { BattleView } from './battle-view'; -import { RevealedMove, monCurrentHp, monCurrentSpeed, monCurrentStamina } from './engine-view'; -import { applyHypotheticalMove, disposeFork, HypotheticalMove } from './forward-model'; -import { scoreState } from './evaluator'; -import { - SIMILAR_DAMAGE_THRESHOLD, - SWITCH_THRESHOLD, - buildDamageCalcContext, - estimateDamage, - estimateDamageMeta, - findBestDamageMove, - loadMonMetas, - offensiveMatchupScore, -} from './heuristic-shared'; - -/** - * TS-NATIVE shared strategy helpers — NOT Solidity ports. `heuristic-shared.ts` is the faithful - * HeuristicCPUBase.sol port and stays 1:1; helpers invented client-side that more than one strategy - * uses live here instead. Two families: - * - band/anti-wall picks (any strategy); - * - FAIR-INFO pool analysis: worst case over the opponent's AFFORDABLE move pool (no peeking; - * stamina is public info, so an unaffordable nuke is not a threat). Shared by Medium and Stall. - */ - -const CPU_PLAYER_INDEX = 1n; -const OPP_PLAYER_INDEX = 0n; - -// A matchup is "walled" if our best move does LESS than this % of the opponent active mon's CURRENT HP -// (a near-useless attack). Tuned low so the breaker fires only on a true wall, never on a slow-but-OK -// matchup — keeping switches rare and preventing oscillation. -export const WALL_DAMAGE_PCT = 5; - -/** - * Uniform pick among the moves inside the similar-damage band (within {@link SIMILAR_DAMAGE_THRESHOLD}% - * of the best damage). The deterministic `findBestDamageMove` always resolves the band to the - * cheapest-stamina member, which makes a strategy's default move scriptable game after game; sampling - * the band trades a little stamina efficiency for unpredictability at near-equal damage. Returns an - * index into `moves`, or -1 if no move deals damage. - */ -export function pickSimilarDamageMove(damages: number[], rng: () => number): number { - let bestDamage = 0; - for (let i = 0; i < damages.length; i++) { - if (damages[i] > bestDamage) bestDamage = damages[i]; - } - if (bestDamage === 0) return -1; - - const threshold = Math.floor((bestDamage * SIMILAR_DAMAGE_THRESHOLD) / 100); - const band: number[] = []; - for (let i = 0; i < damages.length; i++) { - if (damages[i] >= threshold) band.push(i); - } - return band[Math.floor(rng() * band.length)]; -} - -/** - * Anti-wall stalemate-breaker. Returns the index (into `switches`) of a bench mon to pivot to when the - * active mon is WALLED — its best move can't meaningfully damage the opponent's active mon — and a bench - * mon has a strictly better offensive type matchup; else -1. - * - * This fills the gap a threat-based defensive switch leaves: it only fires on a THREAT, so a passive - * opponent that we also can't hurt produces an endless flail at a wall. The breaker pivots to a mon that - * can actually do something. It only fires when we're NOT making progress and a STRICTLY better attacker - * exists, so a productive matchup is never abandoned and it can't oscillate (the better mon, by - * definition, is the new best — nothing beats it next turn). - */ -export function antiWallSwitch( - view: BattleView, - metas: MoveMeta[], - moves: RevealedMove[], - damages: number[], - switches: RevealedMove[], - forkPick?: { revealIdx: number; revealExtra: number; salt: bigint }, -): number { - if (switches.length === 0) return -1; - const e = view.engine; - const bk = view.bk as Hex; - const opponentMonIndex = view.oppActive; - const oppHp = monCurrentHp(e, bk, OPP_PLAYER_INDEX, opponentMonIndex); - if (oppHp <= 0) return -1; - - // Progress check: best move does >= WALL_DAMAGE_PCT% of the opp's current HP => productive, stay in. - const bestIdx = findBestDamageMove(metas, moves, damages); - const bestDmg = bestIdx >= 0 ? damages[bestIdx] : 0; - if (bestDmg * 100 >= oppHp * WALL_DAMAGE_PCT) return -1; - - // Walled: only bench mons whose offensive matchup STRICTLY beats the current one qualify — each - // pivot strictly improves the active matchup, so pivots can't oscillate. - const oppStats = e.getMonStatsForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex)); - const ourStats = e.getMonStatsForBattle(bk, CPU_PLAYER_INDEX, BigInt(view.cpuActive)); - const currentScore = offensiveMatchupScore( - ourStats.type1 as Type, ourStats.type2 as Type, oppStats.type1 as Type, oppStats.type2 as Type, - ); - const qualified: { idx: number; matchup: number }[] = []; - for (let i = 0; i < switches.length; i++) { - const candStats = e.getMonStatsForBattle(bk, CPU_PLAYER_INDEX, BigInt(switches[i].extraData)); - const score = offensiveMatchupScore( - candStats.type1 as Type, candStats.type2 as Type, oppStats.type1 as Type, oppStats.type2 as Type, - ); - if (score > currentScore) qualified.push({ idx: i, matchup: score }); - } - if (qualified.length === 0) return -1; - - // Among the qualified candidates, pick the pivot TARGET by fork score when a reveal is available - // (type matchup alone repeatedly picked the wrong mon); without one, keep the best-matchup pick. - if (forkPick && qualified.length > 1) { - let best = qualified[0].idx; - let bestScore = -Infinity; - for (const q of qualified) { - const s = forkScoreAction(e, bk, forkPick.revealIdx, forkPick.revealExtra, switches[q.idx], forkPick.salt); - if (s > bestScore) { - bestScore = s; - best = q.idx; - } - } - return best; - } - let best = qualified[0]; - for (const q of qualified) if (q.matchup > best.matchup) best = q; - return best.idx; -} - -// --------------------------------------------------------------------------------------------- -// Fair-info pool analysis (no peek): worst case over the opponent's AFFORDABLE move pool. -// --------------------------------------------------------------------------------------------- - -/** - * Max damage the opponent could deal to a defender given a prepared `defendCtx` (opp -> defender) and - * their decoded metas, over the Physical/Special moves they can afford this turn. - */ -export function maxPoolDamage(defendCtx: DamageCalcContext, oppMetas: MoveMeta[], oppStamina: number): number { - let maxDmg = 0; - for (let i = 0; i < oppMetas.length; i++) { - const meta = oppMetas[i]; - if (Number(meta.stamina) > oppStamina) continue; - // Only Physical/Special moves deal damage. - if (meta.moveClass === MoveClass.Physical || meta.moveClass === MoveClass.Special) { - const dmg = estimateDamageMeta(defendCtx, meta); - if (dmg > maxDmg) maxDmg = dmg; - } - } - return maxDmg; -} - -/** - * Max priority across the opponent's AFFORDABLE move pool. Default-zero metas contribute priority 0 — - * the correct pessimistic floor. - */ -export function maxPoolPriority(oppMetas: MoveMeta[], oppStamina: number): number { - let maxPrio = 0; - for (let i = 0; i < oppMetas.length; i++) { - if (Number(oppMetas[i].stamina) > oppStamina) continue; - const p = Number(oppMetas[i].priority); - if (p > maxPrio) maxPrio = p; - } - return maxPrio; -} - -/** - * Evaluate every switch candidate against the opponent's worst-case-over-affordable-pool damage — - * max(static model, sim-measured) per candidate, apples-to-apples with a measured - * `worstIncomingDmgToActive`. Switch when (a) staying in is lethal AND a candidate survives the - * worst case, OR (b) staying-in damage% exceeds the best candidate's by >= SWITCH_THRESHOLD. - * Candidate forks only run past the severity gate, so safe turns cost nothing. - */ -export function evaluateDefensiveSwitchFair( - e: any, - bk: Hex, - activeMonIndex: number, - opponentMonIndex: number, - oppMetas: MoveMeta[], - worstIncomingDmgToActive: number, - switches: RevealedMove[], - severeDamagePct: number, - oppStamina: number, - salt: bigint, -): { shouldSwitch: boolean; switchIdx: number } { - // Our active mon's max + current HP. - const ourMaxHp = Number(e.getMonValueForBattle(bk, CPU_PLAYER_INDEX, BigInt(activeMonIndex), MonStateIndexName.Hp)); - const ourCurrentHp = monCurrentHp(e, bk, CPU_PLAYER_INDEX, activeMonIndex); - - // Damage% taken and whether staying in is lethal. - const damagePctToUs = ourMaxHp > 0 ? Math.floor((worstIncomingDmgToActive * 100) / ourMaxHp) : 0; - const lethalToUs = worstIncomingDmgToActive >= ourCurrentHp; - - // Below the severe threshold and not lethal => never switch defensively. - if (damagePctToUs < severeDamagePct && !lethalToUs) { - return { shouldSwitch: false, switchIdx: 0 }; - } - - let bestDamagePct = Number.MAX_SAFE_INTEGER; - let bestSwitchIdx = 0; - let bestSurvives = false; - - for (let i = 0; i < switches.length; i++) { - const candidateMonIndex = switches[i].extraData; - // opp(active) -> candidate: static worst over the affordable pool, raised by the sim measurement. - const ctx = buildDamageCalcContext(e, bk, OPP_PLAYER_INDEX, opponentMonIndex, CPU_PLAYER_INDEX, candidateMonIndex); - const candWorst = Math.max( - maxPoolDamage(ctx, oppMetas, oppStamina), - forkMeasurePoolThreat(e, bk, opponentMonIndex, oppMetas, oppStamina, candidateMonIndex, salt), - ); - - const maxHp = Number(e.getMonValueForBattle(bk, CPU_PLAYER_INDEX, BigInt(candidateMonIndex), MonStateIndexName.Hp)); - const candCurrentHp = monCurrentHp(e, bk, CPU_PLAYER_INDEX, candidateMonIndex); - - // damage% (huge sentinel when maxHp is 0 so it never wins the min). - const dmgPct = maxHp > 0 ? Math.floor((candWorst * 100) / maxHp) : Number.MAX_SAFE_INTEGER; - const survives = candWorst < candCurrentHp; - - if (dmgPct < bestDamagePct) { - // Track the candidate taking the least damage%. - bestDamagePct = dmgPct; - bestSwitchIdx = i; - bestSurvives = survives; - } - } - - // Lethal staying in and a candidate survives => switch. - if (lethalToUs && bestSurvives) return { shouldSwitch: true, switchIdx: bestSwitchIdx }; - // Staying-in damage% clears best by >= SWITCH_THRESHOLD => switch. - if (damagePctToUs >= bestDamagePct + SWITCH_THRESHOLD) return { shouldSwitch: true, switchIdx: bestSwitchIdx }; - - return { shouldSwitch: false, switchIdx: 0 }; -} - -/** - * Forced/fallback switch under fair info: the best worst-case sponge — least max-affordable-pool - * damage taken across the opp's whole move pool. Static only: this runs on forced-switch turns, - * where the engine doesn't execute the opponent's move, so a fork has nothing to measure. - */ -export function selectBestSwitchFair(e: any, bk: Hex, opponentMonIndex: number, switches: RevealedMove[]): RevealedMove { - const oppMetas = loadMonMetas(e, bk, OPP_PLAYER_INDEX, opponentMonIndex); - const oppStamina = monCurrentStamina(e, bk, OPP_PLAYER_INDEX, opponentMonIndex); - let bestIdx = 0; - let leastWorst = Number.MAX_SAFE_INTEGER; - for (let i = 0; i < switches.length; i++) { - const candidateMonIndex = switches[i].extraData; - const ctx = buildDamageCalcContext(e, bk, OPP_PLAYER_INDEX, opponentMonIndex, CPU_PLAYER_INDEX, candidateMonIndex); - const candWorst = maxPoolDamage(ctx, oppMetas, oppStamina); - if (candWorst < leastWorst) { - leastWorst = candWorst; - bestIdx = i; - } - } - return switches[bestIdx]; -} - -/** - * Is our KO move GUARANTEED to resolve — i.e. the opponent's revealed move cannot KO us first? - * True when the reveal is a switch/rest/non-damaging move, when its damage leaves us standing, or - * when we win the priority/speed race outright (ties play it safe and return false). - */ -export function koIsGuaranteed( - e: any, - bk: Hex, - view: BattleView, - metas: MoveMeta[], - koMove: RevealedMove, - playerMoveIndex: number, -): boolean { - if (playerMoveIndex >= SWITCH_MOVE_INDEX) return true; - - let oppSlot: bigint; - let oppClass: MoveClass; - try { - oppSlot = e.getMoveForMonForBattle(bk, OPP_PLAYER_INDEX, BigInt(view.oppActive), BigInt(playerMoveIndex)); - oppClass = moveSlotLib.moveClass(oppSlot, e, bk) as MoveClass; - } catch { - return true; // unreadable reveal can't be a damage threat - } - if (oppClass !== MoveClass.Physical && oppClass !== MoveClass.Special) return true; - - const ctxToUs = e.getDamageCalcContext(bk, OPP_PLAYER_INDEX, BigInt(view.oppActive), CPU_PLAYER_INDEX, BigInt(view.cpuActive)); - const damageToUs = estimateDamage(e, bk, ctxToUs, oppSlot, oppClass); - if (damageToUs < monCurrentHp(e, bk, CPU_PLAYER_INDEX, view.cpuActive)) return true; - - // They could KO us back — only guaranteed if we act first. - const ourPriority = Number(metas[koMove.moveIndex].priority); - const oppPriority = Number(moveSlotLib.priority(oppSlot, e, bk, OPP_PLAYER_INDEX)); - if (ourPriority !== oppPriority) return ourPriority > oppPriority; - return monCurrentSpeed(e, bk, CPU_PLAYER_INDEX, view.cpuActive) > monCurrentSpeed(e, bk, OPP_PLAYER_INDEX, view.oppActive); -} - -/** - * Measure OUR moves' damage by actually stepping the local sim forward, instead of trusting the - * static damage model. Differential measurement: fork the turn with (reveal, rest) as the baseline, - * then (reveal, move_i) per candidate — the defender's HP gap is move_i's REAL damage, with the - * engine adjudicating everything the static estimator can't see (variable-power moves, ability - * procs like Adaptor, the post-switch defender, speed races). Effects that tick in both forks - * cancel out of the differential. - * - * `revealIdx`/`revealExtra` is the opponent's (possibly voided) revealed action; NO_OP measures - * against the current defender at rest. One fixed `salt` drives every fork, so a sub-100-accuracy - * roll can miss and read 0 — callers should take max(static, measured) for offense and never use - * this for threat assessment (threats need the always-hit worst case). - */ -export function forkMeasureMoveDamages( - e: any, - bk: Hex, - revealIdx: number, - revealExtra: number, - moves: RevealedMove[], - salt: bigint, -): { damages: number[]; scores: number[]; defenderMonIndex: number } { - const p0: HypotheticalMove = - revealIdx === NO_OP_INDEX - ? { moveIndex: NO_OP_INDEX, salt, extraData: 0 } - : { moveIndex: revealIdx, salt, extraData: revealExtra }; - - const base = applyHypotheticalMove(e, bk, p0, { moveIndex: NO_OP_INDEX, salt, extraData: 0 }); - const defenderMonIndex = base.oppActive; - const baseHp = base.mons.p0[defenderMonIndex].hp; - disposeFork(e, base.bk); - - // Each move's fork yields its damage AND the resulting position's score — the same forks feed both - // the damage table and the eval-veto, so the veto is nearly free. - const scores: number[] = new Array(moves.length); - const damages = moves.map((m, i) => { - const child = applyHypotheticalMove(e, bk, p0, { moveIndex: m.moveIndex, salt, extraData: m.extraData }); - const dealt = baseHp - child.mons.p0[defenderMonIndex].hp; - scores[i] = scoreState(child); - disposeFork(e, child.bk); - return dealt > 0 ? dealt : 0; - }); - return { damages, scores, defenderMonIndex }; -} - -// The tree's pick must be beaten by at least this much (scoreState units; > one KO swing) before the -// eval-veto overrides it. The margin keeps the decision tree as the policy — with its long-horizon -// heuristics and persona — and only strips egregious single-turn blunders. Lower margins measurably -// underperform: frequent 1-ply overrides break the tree's cross-turn coherence even though each -// override scores better in isolation. -export const EVAL_OVERRIDE_MARGIN = 200; - -/** - * Eval-veto: fork-score every legal alternative against the same reveal and return one that beats the - * tree's `chosen` by >= {@link EVAL_OVERRIDE_MARGIN}, else null. Move forks were already paid for by - * {@link forkMeasureMoveDamages} (pass their `scores`); only switches and rest fork here. - */ -export function pickEvalOverride( - e: any, - bk: Hex, - revealIdx: number, - revealExtra: number, - chosen: RevealedMove, - moves: RevealedMove[], - moveScores: number[], - switches: RevealedMove[], - noOp: RevealedMove[], - salt: bigint, -): RevealedMove | null { - const p0: HypotheticalMove = - revealIdx === NO_OP_INDEX - ? { moveIndex: NO_OP_INDEX, salt, extraData: 0 } - : { moveIndex: revealIdx, salt, extraData: revealExtra }; - - let chosenScore: number | undefined; - let best: RevealedMove | null = null; - let bestScore = -Infinity; - - const consider = (m: RevealedMove, score: number) => { - if (m.moveIndex === chosen.moveIndex && m.extraData === chosen.extraData) { - chosenScore = score; - return; - } - if (score > bestScore) { - bestScore = score; - best = m; - } - }; - - for (let i = 0; i < moves.length; i++) consider(moves[i], moveScores[i]); - for (const m of [...switches, ...noOp]) { - const child = applyHypotheticalMove(e, bk, p0, { moveIndex: m.moveIndex, salt, extraData: m.extraData }); - const s = scoreState(child); - disposeFork(e, child.bk); - consider(m, s); - } - - if (chosenScore === undefined) { - // Chosen wasn't among the enumerated candidates (rng-drawn extraData target) — score it directly. - const child = applyHypotheticalMove(e, bk, p0, { moveIndex: chosen.moveIndex, salt, extraData: chosen.extraData }); - chosenScore = scoreState(child); - disposeFork(e, child.bk); - } - - return best !== null && bestScore >= chosenScore + EVAL_OVERRIDE_MARGIN ? best : null; -} - -/** - * EV-scale damages by each move's accuracy: a 120-power 85%-accuracy move is worth 102 expected - * damage, and an "85% guaranteed KO" correctly stops reading as guaranteed. Inline attacks always - * run DEFAULT_ACCURACY (100); external moves expose `accuracy(battleKey)` (unreadable => 100). - */ -export function evScaleDamages( - e: any, - bk: Hex, - monIndex: number, - moves: RevealedMove[], - damages: number[], -): number[] { - return damages.map((d, i) => { - if (d === 0) return 0; - const slot = e.getMoveForMonForBattle(bk, CPU_PLAYER_INDEX, BigInt(monIndex), BigInt(moves[i].moveIndex)); - if (slot === undefined || slot === null || moveSlotLib.isInline(slot)) return d; - try { - const move = moveSlotLib.toIMoveSet(slot) as any; - if (typeof move.accuracy !== 'function') return d; - const acc = Number(move.accuracy(bk)); - return acc >= 100 || acc <= 0 ? d : Math.floor((d * acc) / 100); - } catch { - return d; - } - }); -} - -/** - * Measured worst-case incoming damage from the opponent's AFFORDABLE pool, by stepping the sim: - * for each affordable opponent move, fork (oppMove, ourAction) against the (rest, ourAction) - * baseline and read OUR mon's HP gap. `switchTarget` is null to measure the current active staying - * in, or a bench index to measure a switch-in's entry damage. Catches threats the static model - * can't price (variable-power moves, damaging Self/Other utility moves, ability interactions). - * - * Deliberately NOT EV-scaled and meant to be combined as max(static, measured): threats assume the - * hit lands, and a fork's fixed-salt miss must never understate one. Useless on a forced-switch - * turn (the engine doesn't execute the opponent's move there — nothing to measure). - */ -export function forkMeasurePoolThreat( - e: any, - bk: Hex, - oppMonIndex: number, - oppMetas: MoveMeta[], - oppStamina: number, - switchTarget: number | null, - salt: bigint, -): number { - const ourAction: HypotheticalMove = - switchTarget === null - ? { moveIndex: NO_OP_INDEX, salt, extraData: 0 } - : { moveIndex: SWITCH_MOVE_INDEX, salt, extraData: switchTarget }; - - const baseline = applyHypotheticalMove(e, bk, { moveIndex: NO_OP_INDEX, salt, extraData: 0 }, ourAction); - const ourMon = switchTarget ?? baseline.cpuActive; - const baseHp = baseline.mons.p1[ourMon].hp; - disposeFork(e, baseline.bk); - - let worst = 0; - for (let j = 0; j < oppMetas.length; j++) { - if (Number(oppMetas[j].stamina) > oppStamina) continue; - const slot = e.getMoveForMonForBattle(bk, OPP_PLAYER_INDEX, BigInt(oppMonIndex), BigInt(j)); - if (slot === undefined || slot === null) continue; // padded slot past the mon's real move count - const child = applyHypotheticalMove(e, bk, { moveIndex: j, salt, extraData: 0 }, ourAction); - const dealt = baseHp - child.mons.p1[ourMon].hp; - disposeFork(e, child.bk); - if (dealt > worst) worst = dealt; - } - return worst; -} - -/** - * Measure ONE opponent move's damage to our side by stepping the sim: differential between the - * (rest, ourAction) baseline and (oppMove, ourAction). `switchTarget` null measures the current - * active staying in; a bench index measures that candidate's entry damage. The peek-side single-move - * sibling of {@link forkMeasurePoolThreat}: it sees what the static model can't (variable-power - * reveals, our own ability mitigation, switch-in procs). A fixed-salt fork can roll a miss, so - * threat-side callers keep the static estimate as the floor/fallback. - */ -export function forkMeasureIncomingDamage( - e: any, - bk: Hex, - oppMoveIndex: number, - oppExtraData: number, - switchTarget: number | null, - salt: bigint, -): number { - if (oppMoveIndex >= SWITCH_MOVE_INDEX) return 0; // a switch or rest deals nothing - - const ourAction: HypotheticalMove = - switchTarget === null - ? { moveIndex: NO_OP_INDEX, salt, extraData: 0 } - : { moveIndex: SWITCH_MOVE_INDEX, salt, extraData: switchTarget }; - - const baseline = applyHypotheticalMove(e, bk, { moveIndex: NO_OP_INDEX, salt, extraData: 0 }, ourAction); - const ourMon = switchTarget ?? baseline.cpuActive; - const baseHp = baseline.mons.p1[ourMon].hp; - disposeFork(e, baseline.bk); - - const child = applyHypotheticalMove(e, bk, { moveIndex: oppMoveIndex, salt, extraData: oppExtraData }, ourAction); - const dealt = baseHp - child.mons.p1[ourMon].hp; - disposeFork(e, child.bk); - return dealt > 0 ? dealt : 0; -} - -/** Fork one candidate action against the reveal and return the resulting position's score. */ -export function forkScoreAction( - e: any, - bk: Hex, - revealIdx: number, - revealExtra: number, - action: RevealedMove, - salt: bigint, -): number { - const p0: HypotheticalMove = - revealIdx === NO_OP_INDEX - ? { moveIndex: NO_OP_INDEX, salt, extraData: 0 } - : { moveIndex: revealIdx, salt, extraData: revealExtra }; - const child = applyHypotheticalMove(e, bk, p0, { moveIndex: action.moveIndex, salt, extraData: action.extraData }); - const s = scoreState(child); - disposeFork(e, child.bk); - return s; -} - -// --------------------------------------------------------------------------------------------- -// Risk-sensitive sampling: every fork is ONE RNG realization (crits, accuracy, branch moves), so an -// eval-driven root can sample several salts per candidate and choose by expected score — and tilt: -// ahead, prefer certainty (lock the win in); behind, prefer variance (the gamble is free). -// --------------------------------------------------------------------------------------------- - -// Distinct salt streams per turn — offsets just need to be co-prime-ish so the engine's per-salt -// RNG derivations don't correlate. -export const RISK_SALT_OFFSETS = [1n, 1009n, 2017n] as const; - -// Posture flips outside ±RISK_THRESHOLD (≈ a KO swing); λ scales how hard the tilt leans on spread. -const RISK_THRESHOLD = 120; -const RISK_LAMBDA = 0.5; - -/** +1 = ahead (prefer low variance), -1 = behind (prefer high variance), 0 = play the mean. */ -export function riskPosture(currentScore: number): -1 | 0 | 1 { - if (currentScore >= RISK_THRESHOLD) return 1; - if (currentScore <= -RISK_THRESHOLD) return -1; - return 0; -} - -/** Mean of the sampled fork scores, tilted by ±λ·σ per the posture. */ -export function riskAdjustedScore(samples: number[], posture: -1 | 0 | 1): number { - let mean = 0; - for (const s of samples) mean += s; - mean /= samples.length; - if (posture === 0 || samples.length < 2) return mean; - let variance = 0; - for (const s of samples) variance += (s - mean) * (s - mean); - const sd = Math.sqrt(variance / samples.length); - return mean + (posture > 0 ? -RISK_LAMBDA * sd : RISK_LAMBDA * sd); -} diff --git a/sims/src/cpu/heuristic-shared.ts b/sims/src/cpu/heuristic-shared.ts deleted file mode 100644 index b30cda24..00000000 --- a/sims/src/cpu/heuristic-shared.ts +++ /dev/null @@ -1,718 +0,0 @@ -import { Hex } from './hex'; -import { MoveClass, MonStateIndexName, Type } from '../../../transpiler/ts-output/Enums'; -import { moveSlotLib } from '../../../transpiler/ts-output/moves/MoveSlotLib'; -import { attackCalculator } from '../../../transpiler/ts-output/moves/AttackCalculator'; -import { typeCalcLib } from '../../../transpiler/ts-output/types/TypeCalcLib'; -import { createDefaultDamageCalcContext, createDefaultMoveMeta, DamageCalcContext, MoveMeta } from '../../../transpiler/ts-output/Structs'; -import { SWITCH_MOVE_INDEX } from './constants'; -import { RevealedMove, typeEffectiveness } from './engine-view'; - -/** - * Idiomatic TS port of `HeuristicCPUBase.sol` — the shared scoring / decision machinery that - * FairCPU and BetterCPU both build on. Heuristic-free candidate enumeration + readers live in - * `engine-view.ts`; THIS module is where the actual strategy helpers live (damage estimation, KO / - * best-damage move finders, lead selection, switch selection, per-mon-strategy helpers, the adaptive - * Hell/Tartarus/Diyu state machine, and the random-pick / momentum utilities). - * - * The CPU is ALWAYS p1 (playerIndex 1n); the human opponent is p0 (0n). `e` is the LOCAL transpiled - * engine. Solidity storage-packing / context-struct / MoveMeta-caching scaffolding is dropped per the - * FIDELITY MANDATE — but EVERY decision branch, threshold, probability, ordering and formula is - * reproduced exactly, each cited to its `HeuristicCPUBase.sol` source line. - * - * State that the Solidity contract kept in storage mappings is, in idiomatic TS, passed in / returned - * out explicitly: - * - `playerState[p0]` (the adaptive mode ladder) -> a plain `number` threaded through {@link recordResult}. - * - `monConfig[monIndex][key]` -> a {@link MonConfig} lookup the subclass owns. - * - `cpuMoveUsedBitmap[battleKey]` -> a plain `number` threaded through the configured-move helpers. - */ - -const CPU_PLAYER_INDEX = 1n; -const OPP_PLAYER_INDEX = 0n; - -// HeuristicCPUBase.sol:28 — moves within 15% of the best damage are "similar" (85% threshold). -export const SIMILAR_DAMAGE_THRESHOLD = 85; -// HeuristicCPUBase.sol:29 — a candidate must beat the current mon by >= 30 (matchup-score units) -// to justify switching. -export const SWITCH_THRESHOLD = 30; - -// HeuristicCPUBase.sol:32-34 — per-mode severe-damage thresholds (Hell/Tartarus/Diyu ladder). -export const SEVERE_DAMAGE_PCT_HELL = 30; -export const SEVERE_DAMAGE_PCT_TARTARUS = 50; -export const SEVERE_DAMAGE_PCT_DIYU = 60; - -// HeuristicCPUBase.sol:37-39 — adaptive state-machine modes. -export const MODE_HELL = 0; -export const MODE_TARTARUS = 1; -export const MODE_DIYU = 2; - -// HeuristicCPUBase.sol:42-45 — per-mon strategy config keys (values stored as moveIndex+1; 0 = unset). -export const CONFIG_PREFERRED_MOVE = 0; -export const CONFIG_SWITCH_IN_MOVE = 1; -export const CONFIG_SETUP_MOVE = 2; - -// --------------------------------------------------------------------------------------------- -// Adaptive state machine (HeuristicCPUBase.sol:66-100) -// --------------------------------------------------------------------------------------------- -// -// In Solidity this is `playerState[p0]`, a packed uint256 (bits 8-9 = mode, bit 10 = diyuPriorLoss). -// Here it is a plain `number` the subclass persists per-opponent. `recordResult` is the faithful port -// of `_recordResult` (+ the `_afterTurn` guard that only records once the battle has a winner). - -/** Read the current mode out of a packed playerState value. HeuristicCPUBase.sol:72 */ -export function modeOf(state: number): number { - return (state >> 8) & 0x3; -} - -/** Read the diyuPriorLoss flag out of a packed playerState value. HeuristicCPUBase.sol:73 */ -export function diyuPriorLossOf(state: number): boolean { - return ((state >> 10) & 0x1) !== 0; -} - -/** - * Port of `_recordResult`: advance the Hell/Tartarus/Diyu ladder after a battle. DIYU is sticky for - * one loss then resets to HELL; a DIYU win demotes to TARTARUS. Returns the new packed state. - * HeuristicCPUBase.sol:70-95 - */ -export function recordResult(state: number, cpuWon: boolean): number { - const mode = modeOf(state); // HeuristicCPUBase.sol:72 - const diyuPriorLoss = diyuPriorLossOf(state); // HeuristicCPUBase.sol:73 - - let newMode: number; - let newDiyuPriorLoss = false; - - if (mode === MODE_HELL) { - // HeuristicCPUBase.sol:78-79 — HELL: a loss drops to TARTARUS; a win stays HELL. - newMode = cpuWon ? MODE_HELL : MODE_TARTARUS; - } else if (mode === MODE_TARTARUS) { - // HeuristicCPUBase.sol:80-81 — TARTARUS: a win promotes back to HELL; a loss drops to DIYU. - newMode = cpuWon ? MODE_HELL : MODE_DIYU; - } else if (cpuWon) { - // HeuristicCPUBase.sol:82-83 — DIYU win: demote to TARTARUS. - newMode = MODE_TARTARUS; - } else if (diyuPriorLoss) { - // HeuristicCPUBase.sol:84-85 — DIYU second loss: reset to HELL. - newMode = MODE_HELL; - } else { - // HeuristicCPUBase.sol:86-88 — DIYU first loss: stay DIYU, mark the prior-loss flag. - newMode = MODE_DIYU; - newDiyuPriorLoss = true; - } - - // HeuristicCPUBase.sol:91-93 — repack mode (bits 8-9) + diyuPriorLoss (bit 10). - return (state & ~0x700) | (newMode << 8) | (newDiyuPriorLoss ? 1 << 10 : 0); -} - -// --------------------------------------------------------------------------------------------- -// Damage estimation (HeuristicCPUBase.sol:102-182) -// --------------------------------------------------------------------------------------------- - -/** - * Port of `_buildDamageCalcContext`: assemble a DamageCalcContext for ANY attacker/defender pair - * (not just the active mons). The local engine's `getMonStateForBattle` already normalizes the - * CLEARED_MON_STATE_SENTINEL to 0, so the Solidity `delta == SENTINEL ? 0 : delta` guard is a no-op - * here — the reads come back already clean. HeuristicCPUBase.sol:105-143 - */ -export function buildDamageCalcContext( - e: any, - bk: Hex, - attackerIndex: bigint, - attackerMonIndex: number, - defenderIndex: bigint, - defenderMonIndex: number, -): DamageCalcContext { - const attackerStats = e.getMonStatsForBattle(bk, attackerIndex, BigInt(attackerMonIndex)); - const defenderStats = e.getMonStatsForBattle(bk, defenderIndex, BigInt(defenderMonIndex)); - - const ctx = createDefaultDamageCalcContext(); - ctx.attackerMonIndex = BigInt(attackerMonIndex); - ctx.defenderMonIndex = BigInt(defenderMonIndex); - - // HeuristicCPUBase.sol:118-127 — attacker offensive stats (base + delta, sentinel-normalized). - ctx.attackerAttack = attackerStats.attack; - ctx.attackerAttackDelta = e.getMonStateForBattle(bk, attackerIndex, BigInt(attackerMonIndex), MonStateIndexName.Attack); - ctx.attackerSpAtk = attackerStats.specialAttack; - ctx.attackerSpAtkDelta = e.getMonStateForBattle(bk, attackerIndex, BigInt(attackerMonIndex), MonStateIndexName.SpecialAttack); - - // HeuristicCPUBase.sol:129-138 — defender defensive stats. - ctx.defenderDef = defenderStats.defense; - ctx.defenderDefDelta = e.getMonStateForBattle(bk, defenderIndex, BigInt(defenderMonIndex), MonStateIndexName.Defense); - ctx.defenderSpDef = defenderStats.specialDefense; - ctx.defenderSpDefDelta = e.getMonStateForBattle(bk, defenderIndex, BigInt(defenderMonIndex), MonStateIndexName.SpecialDefense); - - // HeuristicCPUBase.sol:141-142 — defender types. - ctx.defenderType1 = defenderStats.type1; - ctx.defenderType2 = defenderStats.type2; - return ctx; -} - -/** - * Port of `_estimateDamage`: deterministic damage estimate for a raw move slot against a prepared - * context. Inline slots read basePower directly; external attack moves expose `basePower(battleKey)` - * (non-attack / non-IAttackMove moves estimate to 0). Damage uses fixed accuracy=100 (always hits), - * volatility=0 (no variance), rng=50, critRate=0 so the estimate is stable. HeuristicCPUBase.sol:146-168 - */ -export function estimateDamage( - e: any, - bk: Hex, - ctx: DamageCalcContext, - rawMoveSlot: bigint, - moveClass: MoveClass, -): number { - let basePower: bigint; - if (moveSlotLib.isInline(rawMoveSlot)) { - basePower = moveSlotLib.basePower(rawMoveSlot, bk); // HeuristicCPUBase.sol:152-153 - } else { - // HeuristicCPUBase.sol:154-160 — external move: try IAttackMove.basePower, fall back to 0. - basePower = externalBasePower(e, bk, rawMoveSlot); - } - if (basePower === 0n) return 0; // HeuristicCPUBase.sol:161 - - const moveType = moveSlotLib.moveType(rawMoveSlot, e, bk) as Type; // HeuristicCPUBase.sol:163 - // HeuristicCPUBase.sol:165-166 — accuracy=100, volatility=0, rng=50, critRate=0. - // `_calculateDamageFromContext` only invokes `.getTypeEffectiveness` on its first arg; TypeCalculator - // delegates straight to typeCalcLib, so passing the lib is the faithful TYPE_CALC. - const [damage] = attackCalculator._calculateDamageFromContext( - typeCalcLib as any, - ctx, - basePower, - 100n, - 0n, - moveType, - moveClass, - 50n, - 0n, - ); - return damage > 0n ? Number(damage) : 0; // HeuristicCPUBase.sol:167 -} - -/** - * Port of `_estimateDamageMeta`: same as {@link estimateDamage} but reading basePower / moveType / - * moveClass off a pre-decoded MoveMeta (no per-call metadata reads). HeuristicCPUBase.sol:172-182 - */ -export function estimateDamageMeta(ctx: DamageCalcContext, meta: MoveMeta): number { - if (meta.basePower === 0n) return 0; // HeuristicCPUBase.sol:177 - const [damage] = attackCalculator._calculateDamageFromContext( - typeCalcLib as any, - ctx, - meta.basePower, - 100n, - 0n, - meta.moveType, - meta.moveClass, - 50n, - 0n, - ); // HeuristicCPUBase.sol:178-180 - return damage > 0n ? Number(damage) : 0; // HeuristicCPUBase.sol:181 -} - -/** - * Port of the `_getMoveBasePower` external branch (and the `_estimateDamage` external try/catch): - * read `basePower(battleKey)` off an external attack move, returning 0 for moves that don't expose it. - * HeuristicCPUBase.sol:483-492 - */ -function externalBasePower(e: any, bk: Hex, rawMoveSlot: bigint): bigint { - try { - const move = moveSlotLib.toIMoveSet(rawMoveSlot) as any; - if (typeof move.basePower !== 'function') return 0n; - const bp = move.basePower(bk); - return bp === undefined || bp === null ? 0n : BigInt(bp); - } catch { - return 0n; // HeuristicCPUBase.sol:157-158 / 489-490 — catch => 0 - } -} - -/** - * Port of `_getMoveBasePower`: basePower of a raw move slot, 0 for non-attack moves. Used by Diyu D4 - * free-turn detection. HeuristicCPUBase.sol:483-492 - */ -export function getMoveBasePower(e: any, bk: Hex, rawMoveSlot: bigint): number { - if (moveSlotLib.isInline(rawMoveSlot)) { - return Number(moveSlotLib.basePower(rawMoveSlot, bk)); // HeuristicCPUBase.sol:484-485 - } - return Number(externalBasePower(e, bk, rawMoveSlot)); // HeuristicCPUBase.sol:487-490 -} - -// --------------------------------------------------------------------------------------------- -// Move selection (HeuristicCPUBase.sol:184-274) -// --------------------------------------------------------------------------------------------- - -/** - * Decode a mon's four move-slot metas (the `metas` array the damage/KO/best-move helpers index by - * moveIndex). A missing slot (<4-move mon) or a decode failure yields a default all-zero meta - * (basePower 0 => contributes nothing). Shared by FairCPU's loaders and the greedy easy CPU; BetterCPU - * keeps its own `buildMetas` for strict 1:1 fidelity with its (non-catching) decode path. - */ -export function loadMonMetas(e: any, bk: Hex, playerIndex: bigint, monIndex: number): MoveMeta[] { - const metas: MoveMeta[] = []; - for (let i = 0; i < 4; i++) { - const slot = e.getMoveForMonForBattle(bk, playerIndex, BigInt(monIndex), BigInt(i)); - if (slot === undefined || slot === null) { - metas.push(createDefaultMoveMeta()); - continue; - } - try { - metas.push(moveSlotLib.decodeMeta(slot, e, bk, playerIndex, BigInt(monIndex))); - } catch { - metas.push(createDefaultMoveMeta()); - } - } - return metas; -} - -/** - * Port of `_computeMoveDamages`: outgoing damage for every Physical/Special move in `moves`, indexed - * parallel to `moves` (non-damaging moves => 0). Built once per turn and threaded through the KO / - * best-damage finders. `metas[moveIndex]` is the decoded MoveMeta for each of the active mon's 4 move - * slots. HeuristicCPUBase.sol:188-203 - */ -export function computeMoveDamages(ctx: DamageCalcContext, metas: MoveMeta[], moves: RevealedMove[]): number[] { - const damages = new Array(moves.length).fill(0); - for (let i = 0; i < moves.length; i++) { - const meta = metas[moves[i].moveIndex]; - // HeuristicCPUBase.sol:196 — only Physical / Special moves deal damage. - if (meta.moveClass === MoveClass.Physical || meta.moveClass === MoveClass.Special) { - damages[i] = estimateDamageMeta(ctx, meta); - } - } - return damages; -} - -/** - * Port of `_findKOMove`: index INTO `moves` of the cheapest-stamina move that KOs the opponent's - * defender, or -1 if none (or the defender is already at <=0 HP). Damages are pre-computed by - * {@link computeMoveDamages}. HeuristicCPUBase.sol:207-235 - */ -export function findKOMove( - e: any, - bk: Hex, - defenderMonIndex: number, - metas: MoveMeta[], - moves: RevealedMove[], - damages: number[], -): number { - // HeuristicCPUBase.sol:214-216 — defender CURRENT hp = base + delta (opponent is p0). - const defenderBaseHp = Number(e.getMonValueForBattle(bk, OPP_PLAYER_INDEX, BigInt(defenderMonIndex), MonStateIndexName.Hp)); - const defenderHpDelta = Number(e.getMonStateForBattle(bk, OPP_PLAYER_INDEX, BigInt(defenderMonIndex), MonStateIndexName.Hp)); - const defenderCurrentHp = defenderBaseHp + defenderHpDelta; - if (defenderCurrentHp <= 0) return -1; // HeuristicCPUBase.sol:217 - - let bestMoveIndex = -1; - let bestStaminaCost = Number.MAX_SAFE_INTEGER; - - for (let i = 0; i < moves.length; i++) { - // HeuristicCPUBase.sol:223-228 — among KO-capable moves, keep the cheapest stamina. - if (damages[i] >= defenderCurrentHp) { - const stamina = Number(metas[moves[i].moveIndex].stamina); - if (stamina < bestStaminaCost) { - bestStaminaCost = stamina; - bestMoveIndex = i; - } - } - } - return bestMoveIndex; -} - -/** - * Port of `_findBestDamageMove`: index INTO `moves` of the best damaging move, then a cheapest-stamina - * tiebreak across all moves within 85% of that best damage. -1 if no move deals damage. Damages - * pre-computed. HeuristicCPUBase.sol:238-274 - */ -export function findBestDamageMove(metas: MoveMeta[], moves: RevealedMove[], damages: number[]): number { - let bestMoveIndex = -1; - let bestDamage = 0; - let bestStaminaCost = Number.MAX_SAFE_INTEGER; - - // HeuristicCPUBase.sol:247-256 — first pass: strictly-greatest damage. - for (let i = 0; i < moves.length; i++) { - if (damages[i] > bestDamage) { - bestDamage = damages[i]; - bestStaminaCost = Number(metas[moves[i].moveIndex].stamina); - bestMoveIndex = i; - } - } - - if (bestDamage === 0) return bestMoveIndex; // HeuristicCPUBase.sol:258 - - // HeuristicCPUBase.sol:261-271 — cheapest-stamina tiebreak among moves within 85% of best damage. - const threshold = Math.floor((bestDamage * SIMILAR_DAMAGE_THRESHOLD) / 100); - for (let i = 0; i < moves.length; i++) { - const stamina = Number(metas[moves[i].moveIndex].stamina); - if (damages[i] >= threshold && stamina < bestStaminaCost) { - bestStaminaCost = stamina; - bestMoveIndex = i; - } - } - return bestMoveIndex; -} - -// --------------------------------------------------------------------------------------------- -// Lead selection (HeuristicCPUBase.sol:276-356) -// --------------------------------------------------------------------------------------------- - -/** - * Port of `_offensiveMatchupScore`: SUM of type-effectiveness (scale 10) for every candidate-type × - * opponent-type pair (candidate offense vs opponent defense). Single-type mons (`Type.None` second - * type) contribute only their real type. HeuristicCPUBase.sol:280-294 - */ -export function offensiveMatchupScore(candType1: Type, candType2: Type, oppType1: Type, oppType2: Type): number { - let score = typeEffectiveness(candType1, oppType1, 10); // HeuristicCPUBase.sol:283 - if (oppType2 !== Type.None) { - score += typeEffectiveness(candType1, oppType2, 10); // HeuristicCPUBase.sol:285 - } - if (candType2 !== Type.None) { - score += typeEffectiveness(candType2, oppType1, 10); // HeuristicCPUBase.sol:288 - if (oppType2 !== Type.None) { - score += typeEffectiveness(candType2, oppType2, 10); // HeuristicCPUBase.sol:290 - } - } - return score; -} - -/** - * Defensive matchup: SUM of type-effectiveness (scale 10) for every opponent-type × candidate-type - * pair (opponent offense vs candidate defense). This is the inline `defensiveScore` block inside - * `_selectLead`. HeuristicCPUBase.sol:319-328 - */ -export function defensiveMatchupScore(oppType1: Type, oppType2: Type, candType1: Type, candType2: Type): number { - let score = typeEffectiveness(oppType1, candType1, 10); // HeuristicCPUBase.sol:319 - if (candType2 !== Type.None) { - score += typeEffectiveness(oppType1, candType2, 10); // HeuristicCPUBase.sol:321 - } - if (oppType2 !== Type.None) { - score += typeEffectiveness(oppType2, candType1, 10); // HeuristicCPUBase.sol:324 - if (candType2 !== Type.None) { - score += typeEffectiveness(oppType2, candType2, 10); // HeuristicCPUBase.sol:326 - } - } - return score; -} - -/** - * Port of `_selectLead`: dual-type-scored lead pick among the turn-0 `switches`. `aggressive` weights - * offense more heavily (`3*off - def` vs balanced `off - def`), producing offensive leads in - * TARTARUS/DIYU vs balanced/defensive in HELL. Returns the chosen switch RevealedMove. - * HeuristicCPUBase.sol:298-356 - */ -export function selectLead( - e: any, - bk: Hex, - opponentMonExtraData: number, - switches: RevealedMove[], - aggressive: boolean, -): RevealedMove { - // HeuristicCPUBase.sol:304-306 — opponent's chosen lead types. - const oppStats = e.getMonStatsForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonExtraData)); - const oppType1 = oppStats.type1 as Type; - const oppType2 = oppStats.type2 as Type; - - let bestScore = -Infinity; // HeuristicCPUBase.sol:308 — type(int256).min - let bestIndex = 0; - - for (let i = 0; i < switches.length; i++) { - const candStats = e.getMonStatsForBattle(bk, CPU_PLAYER_INDEX, BigInt(switches[i].extraData)); - const candType1 = candStats.type1 as Type; - const candType2 = candStats.type2 as Type; - - const defensiveScore = defensiveMatchupScore(oppType1, oppType2, candType1, candType2); // HeuristicCPUBase.sol:318-328 - const offensiveScore = offensiveMatchupScore(candType1, candType2, oppType1, oppType2); // HeuristicCPUBase.sol:330-340 - - // HeuristicCPUBase.sol:342-344 — aggressive triples offense weight. - const score = aggressive ? 3 * offensiveScore - defensiveScore : offensiveScore - defensiveScore; - - if (score > bestScore) { - // HeuristicCPUBase.sol:346-349 — strict >, so ties keep the first (lowest index). - bestScore = score; - bestIndex = i; - } - } - return switches[bestIndex]; // HeuristicCPUBase.sol:355 -} - -// --------------------------------------------------------------------------------------------- -// Switch selection (HeuristicCPUBase.sol:358-427) -// --------------------------------------------------------------------------------------------- - -/** - * Port of `_selectBestSwitch`. Non-aggressive (HELL): the safest sponge — least estimated damage from - * the opponent's revealed move (falls back to `switches[0]` if the opponent is switching, or the - * revealed move isn't a readable Physical/Special attack). Aggressive (TARTARUS/DIYU revenge): the - * best offensive matchup against the opponent's active mon. Returns the chosen switch RevealedMove. - * HeuristicCPUBase.sol:363-427 - */ -export function selectBestSwitch( - e: any, - bk: Hex, - opponentMonIndex: number, - opponentMoveIndex: number, - switches: RevealedMove[], - aggressive: boolean, -): RevealedMove { - if (aggressive) { - // HeuristicCPUBase.sol:370-388 — best offensive matchup vs the opponent's active mon. - const oppStats = e.getMonStatsForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex)); - const oppType1 = oppStats.type1 as Type; - const oppType2 = oppStats.type2 as Type; - - let bestScore = -Infinity; - let bestIdx = 0; - for (let i = 0; i < switches.length; i++) { - const candStats = e.getMonStatsForBattle(bk, CPU_PLAYER_INDEX, BigInt(switches[i].extraData)); - const score = offensiveMatchupScore(candStats.type1 as Type, candStats.type2 as Type, oppType1, oppType2); - if (score > bestScore) { - bestScore = score; - bestIdx = i; - } - } - return switches[bestIdx]; - } - - // HeuristicCPUBase.sol:391-393 — opponent is switching (no readable attack): default to switches[0]. - if (opponentMoveIndex >= SWITCH_MOVE_INDEX) { - return switches[0]; - } - - // HeuristicCPUBase.sol:395-404 — read the opponent's revealed move; only Physical/Special are estimable. - let oppMoveSlot: bigint; - let oppMoveClass: MoveClass; - let canEstimate = false; - try { - oppMoveSlot = e.getMoveForMonForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex), BigInt(opponentMoveIndex)); - oppMoveClass = moveSlotLib.moveClass(oppMoveSlot, e, bk) as MoveClass; - canEstimate = oppMoveClass === MoveClass.Physical || oppMoveClass === MoveClass.Special; - } catch { - canEstimate = false; - } - - if (!canEstimate) { - return switches[0]; // HeuristicCPUBase.sol:406-408 - } - - // HeuristicCPUBase.sol:410-426 — pick the candidate that takes the LEAST damage from that move. - let bestIdx = 0; - let leastDamage = Number.MAX_SAFE_INTEGER; - for (let i = 0; i < switches.length; i++) { - const candidateMonIndex = switches[i].extraData; - const ctx = buildDamageCalcContext(e, bk, OPP_PLAYER_INDEX, opponentMonIndex, CPU_PLAYER_INDEX, candidateMonIndex); - const dmg = estimateDamage(e, bk, ctx, oppMoveSlot!, oppMoveClass!); - if (dmg < leastDamage) { - leastDamage = dmg; - bestIdx = i; - } - } - return switches[bestIdx]; -} - -// --------------------------------------------------------------------------------------------- -// Per-mon strategy (HeuristicCPUBase.sol:429-600) -// --------------------------------------------------------------------------------------------- - -/** - * Per-mon strategy config — the TS stand-in for `monConfig[monIndex][configKey]`. Values follow the - * Solidity convention: stored as `moveIndex + 1`, with `0`/absent meaning unset. - */ -export type MonConfig = { [monIndex: number]: { [configKey: number]: number } }; - -function readConfig(config: MonConfig, monIndex: number, key: number): number { - return config[monIndex]?.[key] ?? 0; -} - -/** - * Port of `_tryConfiguredMove`: try to play a per-mon configured move (switch-in or setup), marking - * the corresponding lane bit in `usedBitmap`. Returns `{ index, usedBitmap }` where `index` is the - * position INTO `moves`, or -1 if unconfigured / already used this switch-in / absent from `moves`. - * The (possibly mutated) bitmap is returned so the caller can persist it (no in-place storage). - * HeuristicCPUBase.sol:434-458 - */ -export function tryConfiguredMove( - config: MonConfig, - usedBitmap: number, - activeMonIndex: number, - moves: RevealedMove[], - configKey: number, - laneBitOffset: number, -): { index: number; usedBitmap: number } { - const configValue = readConfig(config, activeMonIndex, configKey); // HeuristicCPUBase.sol:441 - if (configValue === 0) return { index: -1, usedBitmap }; // HeuristicCPUBase.sol:442 - const targetMoveIndex = configValue - 1; // HeuristicCPUBase.sol:443 - - const laneBit = 1 << (activeMonIndex + laneBitOffset); // HeuristicCPUBase.sol:445 - if ((usedBitmap & laneBit) !== 0) return { index: -1, usedBitmap }; // HeuristicCPUBase.sol:446 — already used - - for (let i = 0; i < moves.length; i++) { - if (moves[i].moveIndex === targetMoveIndex) { - // HeuristicCPUBase.sol:450-451 — mark the lane bit and return the position. - return { index: i, usedBitmap: usedBitmap | laneBit }; - } - } - return { index: -1, usedBitmap }; // HeuristicCPUBase.sol:457 -} - -/** - * Port of `_clearMoveUsedBitsOnSwitchIn`: clear move-used bits on mon re-entry. The switch-in lane - * (bit `monIdx`) clears unconditionally; the setup lane (bit `monIdx + 8`) clears only when current - * HP is strictly above 50% — a low-HP re-entry shouldn't waste a turn setting up. Returns the new - * bitmap. HeuristicCPUBase.sol:463-479 - */ -export function clearMoveUsedBitsOnSwitchIn(e: any, bk: Hex, usedBitmap: number, monIdx: number): number { - const setupBit = 1 << (monIdx + 8); // HeuristicCPUBase.sol:465 - let newBitmap = usedBitmap & ~(1 << monIdx); // HeuristicCPUBase.sol:466 — switch-in lane clears unconditionally - - // HeuristicCPUBase.sol:469-476 — only consult HP if the setup lane bit is actually set. - if ((usedBitmap & setupBit) !== 0) { - const maxHp = Number(e.getMonValueForBattle(bk, CPU_PLAYER_INDEX, BigInt(monIdx), MonStateIndexName.Hp)); - const hpDelta = Number(e.getMonStateForBattle(bk, CPU_PLAYER_INDEX, BigInt(monIdx), MonStateIndexName.Hp)); - const currentHp = maxHp + hpDelta; - // HeuristicCPUBase.sol:473-475 — strictly above 50% (currentHp*2 > maxHp) clears the setup lane. - if (currentHp * 2 > maxHp) { - newBitmap &= ~setupBit; - } - } - return newBitmap; -} - -/** - * Port of `_tryPreferredMove`: use the per-mon CONFIG_PREFERRED_MOVE if set AND within 85% of the best - * damaging move's damage. Returns the position INTO `moves`, or -1 if unconfigured / preferred not - * present / below the threshold. (If no move deals damage, the preferred move is used as-is.) - * HeuristicCPUBase.sol:555-600 - */ -export function tryPreferredMove( - config: MonConfig, - activeMonIndex: number, - ctx: DamageCalcContext, - metas: MoveMeta[], - moves: RevealedMove[], -): number { - const configValue = readConfig(config, activeMonIndex, CONFIG_PREFERRED_MOVE); // HeuristicCPUBase.sol:561 - if (configValue === 0) return -1; // HeuristicCPUBase.sol:563 - const targetMoveIndex = configValue - 1; // HeuristicCPUBase.sol:564 - - let preferredIdx = -1; - let preferredDamage = 0; - let bestDamage = 0; - - for (let i = 0; i < moves.length; i++) { - const meta = metas[moves[i].moveIndex]; - // HeuristicCPUBase.sol:573-578 — skip non-damaging classes. - if (meta.moveClass !== MoveClass.Physical && meta.moveClass !== MoveClass.Special) { - continue; - } - const dmg = estimateDamageMeta(ctx, meta); // HeuristicCPUBase.sol:580 - if (dmg > bestDamage) bestDamage = dmg; // HeuristicCPUBase.sol:581 - if (moves[i].moveIndex === targetMoveIndex) { - preferredIdx = i; // HeuristicCPUBase.sol:584-585 - preferredDamage = dmg; - } - } - - if (preferredIdx < 0) return -1; // HeuristicCPUBase.sol:592 - if (bestDamage === 0) return preferredIdx; // HeuristicCPUBase.sol:593 — no damage reference - - // HeuristicCPUBase.sol:596-598 — preferred is "good enough" if within 85% of best damage. - if (preferredDamage * 100 >= bestDamage * SIMILAR_DAMAGE_THRESHOLD) { - return preferredIdx; - } - return -1; -} - -/** - * Port of `_tryFreeTurnMatchupSwitch`: pick a switch candidate whose offensive matchup against the - * opponent's active mon beats the current mon's by >= SWITCH_THRESHOLD. Returns the position INTO - * `switches`, or -1 when no candidate clears the bar (caller falls through to the best-damage - * default). HeuristicCPUBase.sol:519-552 - */ -export function tryFreeTurnMatchupSwitch( - e: any, - bk: Hex, - activeMonIndex: number, - opponentMonIndex: number, - switches: RevealedMove[], -): number { - const oppStats = e.getMonStatsForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex)); - const oppType1 = oppStats.type1 as Type; - const oppType2 = oppStats.type2 as Type; - - // HeuristicCPUBase.sol:527-531 — current mon's offensive matchup is the bar to beat. - const ourStats = e.getMonStatsForBattle(bk, CPU_PLAYER_INDEX, BigInt(activeMonIndex)); - const currentScore = offensiveMatchupScore(ourStats.type1 as Type, ourStats.type2 as Type, oppType1, oppType2); - - let bestScore = currentScore; // HeuristicCPUBase.sol:533 - let bestIdx = -1; - for (let i = 0; i < switches.length; i++) { - const candStats = e.getMonStatsForBattle(bk, CPU_PLAYER_INDEX, BigInt(switches[i].extraData)); - const score = offensiveMatchupScore(candStats.type1 as Type, candStats.type2 as Type, oppType1, oppType2); - if (score > bestScore) { - bestScore = score; - bestIdx = i; - } - } - - // HeuristicCPUBase.sol:548-550 — require a >= SWITCH_THRESHOLD improvement over current. - if (bestIdx >= 0 && bestScore >= currentScore + SWITCH_THRESHOLD) { - return bestIdx; - } - return -1; -} - -// --------------------------------------------------------------------------------------------- -// Utility (HeuristicCPUBase.sol:494-625) -// --------------------------------------------------------------------------------------------- - -/** Port of `_popcount8`: count set bits in an 8-bit bitmap. HeuristicCPUBase.sol:494-499 */ -export function popcount8(bitmap: number): number { - let count = 0; - for (let i = 0; i < 8; i++) { - if (((bitmap >> i) & 1) === 1) count++; - } - return count; -} - -/** - * Port of `_hasMomentum` (Diyu D4 step 4): the CPU has momentum if it has MORE mons alive, or — on a - * tie — its active mon has at least as much stamina as the opponent's active mon. `cpuActiveCurrentStamina` - * is the CPU active mon's current (base + delta) stamina the caller already has on hand (e.g. off the - * shared BattleView). HeuristicCPUBase.sol:503-514 - */ -export function hasMomentum( - e: any, - bk: Hex, - p1TeamSize: number, - p1KOBitmap: number, - p0TeamSize: number, - p0KOBitmap: number, - p0ActiveMonIndex: number, - cpuActiveCurrentStamina: number, -): boolean { - const ourAlive = p1TeamSize - popcount8(p1KOBitmap); // HeuristicCPUBase.sol:504 - const theirAlive = p0TeamSize - popcount8(p0KOBitmap); // HeuristicCPUBase.sol:505 - if (ourAlive > theirAlive) return true; // HeuristicCPUBase.sol:506 - if (ourAlive < theirAlive) return false; // HeuristicCPUBase.sol:507 - - // HeuristicCPUBase.sol:509-513 — tie: compare active-mon stamina (>= ours wins the tie). - const ourStam = cpuActiveCurrentStamina; - const theirBase = Number(e.getMonValueForBattle(bk, OPP_PLAYER_INDEX, BigInt(p0ActiveMonIndex), MonStateIndexName.Stamina)); - const theirDelta = Number(e.getMonStateForBattle(bk, OPP_PLAYER_INDEX, BigInt(p0ActiveMonIndex), MonStateIndexName.Stamina)); - const theirStam = theirBase + theirDelta; - return ourStam >= theirStam; -} - -/** - * Port of `_pickRandomValidOption`: pick uniformly across `noOp ++ moves ++ switches` (the valid - * action set `_calculateValidMoves` already produced). The Solidity uses the high bits of one rng word - * for the index so a separate trigger roll and this index don't share entropy — replicated here as a - * single fresh uniform draw over the union. HeuristicCPUBase.sol:612-625 - */ -export function pickRandomValidOption( - rng: () => number, - noOp: RevealedMove[], - moves: RevealedMove[], - switches: RevealedMove[], -): RevealedMove { - const total = noOp.length + moves.length + switches.length; // HeuristicCPUBase.sol:618 - let idx = Math.floor(rng() * total); // HeuristicCPUBase.sol:619 — (rng >> 8) % total, uniform over [0,total) - if (idx < noOp.length) return noOp[idx]; // HeuristicCPUBase.sol:620 - idx -= noOp.length; // HeuristicCPUBase.sol:621 - if (idx < moves.length) return moves[idx]; // HeuristicCPUBase.sol:622 - idx -= moves.length; // HeuristicCPUBase.sol:623 - return switches[idx]; // HeuristicCPUBase.sol:624 -} diff --git a/sims/src/cpu/hex.ts b/sims/src/cpu/hex.ts deleted file mode 100644 index db742391..00000000 --- a/sims/src/cpu/hex.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Local Hex type — replaces the `viem` import so the cpu stack has no external deps. -export type Hex = `0x${string}`; diff --git a/sims/src/cpu/index.ts b/sims/src/cpu/index.ts deleted file mode 100644 index f5365132..00000000 --- a/sims/src/cpu/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Hex } from './hex'; -import { makeRng } from './engine-view'; -import { captureBattleView } from './battle-view'; -import { CPU_STRATEGIES } from './registry'; -import { CpuStrategy, StrategyState } from './strategy'; - -export { CPU_STRATEGIES, registerCpuStrategy, getCpuStrategy } from './registry'; -export type { CpuStrategy, CpuMove, PlayerMove, StrategyState } from './strategy'; -export { BaseCpuStrategy } from './strategy'; - -/** - * Difficulty dispatcher for the local CPU strategies. Routes through {@link CPU_STRATEGIES}: - * easy -> EasyCpu (greedy attacker), medium -> MediumCpu, hard -> one of the HARD_POOL profiles. - * Each strategy's bag is lazily created once and threaded back into every `decide` call (hard's setup - * lane bits and stall's status bits persist there). - * - * The signature is UNCHANGED so existing callers (LocalCpuGameService) keep compiling. - * `playerMoveIndex` / `playerExtraData` form the human's (p0's) revealed move the CPU responds to; - * both default to 0 when omitted. The CPU is always p1. - */ - -// The hard difficulty cycles across the strongest peek-seat profiles (a statistical tie in the -// arena, three very different characters) so repeat battles don't face one learnable policy. The -// pick is battleKey-derived: stable for the whole battle and across reloads, no state to persist. -const HARD_POOL = ['hard', 'planner', 'greedy-risk'] as const; - -function resolveStrategyKey(difficulty: 'easy' | 'medium' | 'hard' | 'mirror' | 'greedy', bk: Hex): string { - if (difficulty !== 'hard') return difficulty; - return HARD_POOL[Number(BigInt(bk) % BigInt(HARD_POOL.length))]; -} - -// Lazily-created state bags, keyed by the (shared singleton) strategy instance — one bag per strategy, -// NOT per opponent. A strategy needing per-(opponent, battle) state must key on that itself. -const strategyState = new WeakMap(); - -function getOrInitState(strat: CpuStrategy): StrategyState { - let s = strategyState.get(strat); - if (s === undefined) { - s = strat.createState(); - strategyState.set(strat, s); - } - return s; -} - -export function pickCpuMove( - difficulty: 'easy' | 'medium' | 'hard' | 'mirror' | 'greedy', - e: any, - bk: Hex, - playerMoveIndex: number = 0, - playerExtraData: number = 0, - rng: () => number = makeRng(), -): { moveIndex: number; extraData: number } { - const strat = CPU_STRATEGIES.get(resolveStrategyKey(difficulty, bk)); - if (strat === undefined) { - // Difficulty is a closed union, so this is unreachable in typed callers; guard for safety. - throw new Error(`No CPU strategy registered for difficulty "${difficulty}"`); - } - const state = getOrInitState(strat); - // Capture the shared read-once snapshot ONCE per turn; every strategy reads the same view (it also - // carries `.engine`/`.bk` for readers not projected onto it). captureBattleView consumes no strategy rng. - const view = captureBattleView(e, bk); - return strat.decide(view, { moveIndex: playerMoveIndex, extraData: playerExtraData }, rng, state); -} diff --git a/sims/src/cpu/mon-meta.ts b/sims/src/cpu/mon-meta.ts deleted file mode 100644 index e9d15e58..00000000 --- a/sims/src/cpu/mon-meta.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * MonMetadata-equivalent for the arena, built from chomp's drool CSVs. Provides the fields the arena - * analysis reads: `.id`, `.name`, `.stats` (base stats), and `.moves[i].name` — the mon's full move - * catalog (learnset order) via `monCatalog`. The arena maps each played battle slot back to a catalog - * entry through the draft's `equip`, so `.moves` labels every move a mon could field, not just its - * default four. - */ -import { loadRoster } from '../util/csv-load'; -import { monCatalog } from '../arena/team'; - -export interface MonMeta { - id: number; - name: string; - stats: { - hp: number; - attack: number; - defense: number; - specialAttack: number; - specialDefense: number; - speed: number; - }; - moves: { name: string }[]; -} - -const roster = loadRoster(); - -export const MonMetadata: Record = {}; -for (const mon of roster.mons) { - MonMetadata[mon.id] = { - id: mon.id, - name: mon.name, - stats: { - hp: mon.hp, - attack: mon.attack, - defense: mon.defense, - specialAttack: mon.specialAttack, - specialDefense: mon.specialDefense, - speed: mon.speed, - }, - moves: monCatalog(roster, mon).map((s) => ({ name: s.name })), - }; -} diff --git a/sims/src/cpu/registry.ts b/sims/src/cpu/registry.ts deleted file mode 100644 index eef4e1d0..00000000 --- a/sims/src/cpu/registry.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CpuStrategy } from './strategy'; -import { HardCpu } from './strategies/hard-cpu'; -import { GreedyEvalCpu } from './strategies/greedy-eval'; -import { OverrideCpu } from './strategies/override-cpu'; -import { SearchCpu } from './strategies/search-cpu'; - -/** - * CPU strategy registry — the strategies the balance arena exercises: - * - `hard` -> HardCpu (peek + best-respond, sim-measured damage, guarded setup punishment) - * - `greedy` -> GreedyEvalCpu (1-ply forward-model + evaluator best response) - * - `override` -> OverrideCpu (scripted per-mon plans, else delegates to hard) - * - `search` -> SearchCpu d2, no-peek (maximin matrix game — production reveal semantics) - * - `search-peek` -> SearchCpu d2, peek-at-root (best-respond to the reveal, maximin deeper) - * - * Strategy INSTANCES are shared singletons; per-battle MUTABLE state lives in the `StrategyState` bag - * the caller owns (`createState()` + thread it back into `decide`), so one instance is safe across battles. - */ -export const CPU_STRATEGIES = new Map([ - ['hard', new HardCpu()], - ['greedy', new GreedyEvalCpu()], - ['override', new OverrideCpu()], - ['search', new SearchCpu('search', 2, false)], - ['search-peek', new SearchCpu('search-peek', 2, true)], -]); - -/** Register (or replace) a strategy under `key`. Returns the registry for chaining. */ -export function registerCpuStrategy(key: string, strategy: CpuStrategy): Map { - return CPU_STRATEGIES.set(key, strategy); -} - -/** Look up a strategy by key, or `undefined` if none is registered. */ -export function getCpuStrategy(key: string): CpuStrategy | undefined { - return CPU_STRATEGIES.get(key); -} diff --git a/sims/src/cpu/strategies/greedy-eval.ts b/sims/src/cpu/strategies/greedy-eval.ts deleted file mode 100644 index 3068a8f5..00000000 --- a/sims/src/cpu/strategies/greedy-eval.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Hex } from '../hex'; -import { BaseCpuStrategy, CpuMove, PlayerMove, StrategyState } from '../strategy'; -import { BattleView } from '../battle-view'; -import { RevealedMove } from '../engine-view'; -import { applyHypotheticalMove, disposeFork, HypotheticalMove } from '../forward-model'; -import { DEFAULT_EVAL_WEIGHTS, EvalWeights, scoreStateWith } from '../evaluator'; -import { RISK_SALT_OFFSETS, riskAdjustedScore, riskPosture } from '../heuristic-native'; - -/** - * GREEDY (eval) — 1-ply best response on the search kit (forward-model + evaluator). NOT a faithful - * Solidity port; the only contract is that it returns a LEGAL move. - * - * Algorithm (from the CPU's (p1's) point of view): - * 1. Enumerate every legal CPU action via the shared `validMoves` buckets (moves + switches + noOp). - * 2. For each candidate, fork the live battle and replay ONE turn with the human's REVEALED move as - * p0 and the candidate as p1 (`applyHypotheticalMove`). On a forced-switch turn (p1-only) the - * human doesn't act, so p0 is passed as `null`. - * 3. Score the resulting position (higher = better for the CPU); keep every candidate tied for best - * and break ties uniformly with the injected rng. - * - * Options (the plain `greedy` key uses none): - * - `salts: k` — RISK-AWARE mode: sample each candidate with k salt streams and choose by - * risk-adjusted score (mean, tilted toward certainty when ahead / variance when behind). One fork - * is one RNG realization, so multi-salt sampling also de-noises crits and branchy moves. - * - `weights` — score with a custom {@link EvalWeights} (A/B harness for fitted weights). - * - `evalFn` — replace the position scorer entirely (e.g. the race-matrix `ttkEval`). - * - * The live battle is never mutated — every candidate runs on a throwaway fork. - */ -export class GreedyEvalCpu extends BaseCpuStrategy { - readonly name: string; - private readonly salts: number; - private readonly score: (view: BattleView) => number; - - constructor(opts: { salts?: number; weights?: EvalWeights; evalFn?: (view: BattleView) => number; tag?: string } = {}) { - super(); - this.salts = opts.salts ?? 1; - const weights = opts.weights ?? DEFAULT_EVAL_WEIGHTS; - this.score = opts.evalFn ?? (v => scoreStateWith(v, weights)); - const tags = [ - this.salts > 1 ? `risk k=${this.salts}` : '', - opts.weights ? 'tuned' : '', - opts.tag ?? '', - ].filter(Boolean); - this.name = `Greedy (eval${tags.length ? ', ' + tags.join(', ') : ''})`; - } - - override decide(view: BattleView, pm: PlayerMove, rng: () => number, _state: StrategyState): CpuMove { - const e = view.engine; - const bk = view.bk; - // Candidate CPU actions — pass rng so Self/Opponent-index extraData targets are drawn the same - // way the faithful ports draw them. - const { noOp, moves, switches } = this.validMoves(e, bk as Hex, rng); - const candidates: RevealedMove[] = [...moves, ...switches, ...noOp]; - if (candidates.length === 0) { - return { moveIndex: noOp.length > 0 ? noOp[0].moveIndex : 126, extraData: 0 }; - } - - // Salt streams: the single-sample default keeps the original fixed salt; risk mode draws k - // turn-seeded streams. Posture comes from the CURRENT position (pure-view, no fork). - const turnSalt = BigInt(Number(e.getTurnIdForBattleState(bk)) + 1); - const saltList: bigint[] = - this.salts <= 1 ? [0n] : RISK_SALT_OFFSETS.slice(0, this.salts).map(o => turnSalt * 4099n + o); - const posture = this.salts <= 1 ? 0 : riskPosture(this.score(view)); - - // The human (p0) move this turn. On a forced-switch turn (switchFlag === 1) p0 does not act. - const p0Acts = view.switchFlag !== 1; - - let best: RevealedMove[] = []; - let bestScore = -Infinity; - for (const cand of candidates) { - const samples: number[] = []; - for (const salt of saltList) { - const p0Move: HypotheticalMove | null = p0Acts - ? { moveIndex: pm.moveIndex, salt, extraData: pm.extraData } - : null; - const resultView = applyHypotheticalMove(e, bk as Hex, p0Move, { - moveIndex: cand.moveIndex, - salt, - extraData: cand.extraData, - }); - samples.push(this.score(resultView)); - disposeFork(e, resultView.bk); - } - const score = riskAdjustedScore(samples, posture); - if (score > bestScore) { - bestScore = score; - best = [cand]; - } else if (score === bestScore) { - best.push(cand); - } - } - - const chosen = this.pickUniform(best, rng) ?? best[0]; - return { moveIndex: chosen.moveIndex, extraData: chosen.extraData }; - } -} diff --git a/sims/src/cpu/strategies/hard-cpu.ts b/sims/src/cpu/strategies/hard-cpu.ts deleted file mode 100644 index 0fc52534..00000000 --- a/sims/src/cpu/strategies/hard-cpu.ts +++ /dev/null @@ -1,617 +0,0 @@ -import { Hex } from '../hex'; -import { MoveClass, MonStateIndexName } from '../../../../transpiler/ts-output/Enums'; -import { moveSlotLib } from '../../../../transpiler/ts-output/moves/MoveSlotLib'; -import { MoveMeta } from '../../../../transpiler/ts-output/Structs'; -import { SWITCH_MOVE_INDEX, NO_OP_INDEX } from '../constants'; -import { - RevealedMove, - moveSlot, - monCurrentHp as currentHp, - monCurrentSpeed, -} from '../engine-view'; -import { BattleView } from '../battle-view'; -import { BaseCpuStrategy, CpuMove, PlayerMove, StrategyState } from '../strategy'; -import { - SEVERE_DAMAGE_PCT_HELL, - SWITCH_THRESHOLD, - CONFIG_SWITCH_IN_MOVE, - CONFIG_SETUP_MOVE, - MonConfig, - buildDamageCalcContext, - estimateDamage, - computeMoveDamages, - findKOMove, - findBestDamageMove, - selectLead, - selectBestSwitch, - tryConfiguredMove, - clearMoveUsedBitsOnSwitchIn, - tryPreferredMove, - getMoveBasePower, - hasMomentum, -} from '../heuristic-shared'; -import { - antiWallSwitch, - evScaleDamages, - forkMeasureIncomingDamage, - forkMeasureMoveDamages, - forkScoreAction, - pickEvalOverride, - pickSimilarDamageMove, - WALL_DAMAGE_PCT, -} from '../heuristic-native'; - -/** - * HARD CPU — best-response with foreknowledge. - * - * A TS-native fork of the BetterCPU decision engine, frozen into ONE fixed policy. It PEEKS at the - * human's already-revealed move (the off-chain CPU replies after the commit) and best-responds: KO if - * we can and survive the race, defensive-switch on a real threat, else play the best damage. - * - * Relative to the BetterCPU HELL best-response baseline: - * - drops the Hell/Tartarus/Diyu mode ladder + the Tartarus chaos roll; - * - measures BOTH sides by STEPPING THE SIM instead of trusting the static model alone: its own - * damage (forkMeasureMoveDamages: max(static, measured), accuracy-EV-scaled) and the reveal's - * damage to it / to each switch candidate (forkMeasureIncomingDamage, static as the miss - * fallback) — variable-power moves, ability procs and switch-in effects all read true; - * - voids a zapped opponent's reveal (ShouldSkipTurn): the telegraphed action will never execute, - * so the whole tree plays the free turn against the mon that actually stays in; - * - folds in the >=90% near-KO bypass (stay in to finish a kill instead of switching away); - * - ADDS an anti-wall stalemate-breaker (P5.5): pivot out of a matchup our active mon can't damage, - * which the threat-based defensive switch misses; - * - runs the Diyu free-turn setup punishment (P4.5) always-on, made safe by dropping the matchup - * pivot and guarding setup behind momentum + a productive matchup (see freeTurnPick) — the - * unguarded original stalled whole games; - * - persists the configured-move lane bitmap across turns (StrategyState, keyed by battle) so setup - * fires once per switch-in like the on-chain cpuMoveUsedBitmap, not once per turn; - * - default damage picks sample the 85% similar-damage band instead of always cheapest-stamina, so - * the policy can't be scripted move-for-move across repeat games; - * - an eval-veto (pickEvalOverride) strips egregious single-turn blunders: the tree's pick stands - * unless a forked alternative beats it by more than a KO swing (EVAL_OVERRIDE_MARGIN). - * - * The CPU is ALWAYS p1; the opponent is p0. - */ - -const CPU_PLAYER_INDEX = 1n; -const OPP_PLAYER_INDEX = 0n; - -// SWITCH priority in the Engine's priority comparison. -const SWITCH_PRIORITY = 6; - -/** - * Per-mon setup-move config (the Diyu "free-turn" setup moves the free-turn + P3/P4/P6 logic uses). - * Values follow the on-chain convention: stored as `moveIndex + 1` (0 / absent = unset). Only - * CONFIG_SETUP_MOVE is configured; CONFIG_SWITCH_IN_MOVE / CONFIG_PREFERRED_MOVE are left unset (so - * `tryConfiguredMove(CONFIG_SWITCH_IN_MOVE)` and `tryPreferredMove` are inert unless a caller supplies - * more config). - */ -export const BETTER_CPU_MON_CONFIG: MonConfig = { - 1: { [CONFIG_SETUP_MOVE]: 2 }, // Inutia -> Initialize (slot 1) - 2: { [CONFIG_SETUP_MOVE]: 1 }, // Malalien -> Triple Think (slot 0) - 3: { [CONFIG_SETUP_MOVE]: 2 }, // Iblivion -> Loop (slot 1) - 6: { [CONFIG_SETUP_MOVE]: 2 }, // Pengym -> Deadlift (slot 1) - 7: { [CONFIG_SETUP_MOVE]: 3 }, // Embursa -> Heat Beacon (slot 2) - 9: { [CONFIG_SETUP_MOVE]: 3 }, // Aurox -> Iron Wall (slot 2) - 11: { [CONFIG_SETUP_MOVE]: 3 }, // Ekineki -> Nine Nine Nine (slot 2) - 12: { [CONFIG_SETUP_MOVE]: 1 }, // Nirvamma -> Hard Reset (slot 0) -}; - -const RET = (m: RevealedMove): CpuMove => ({ - moveIndex: m.moveIndex, - extraData: m.extraData, -}); - -// --------------------------------------------------------------------------------------------- -// metas — decode the active mon's four move-slot metas. -// --------------------------------------------------------------------------------------------- - -/** - * Decode the active mon's four move-slot metas exactly as `_calculateValidMoves` does. Reads the four - * slots directly off the engine (slots past the mon's real move count read as undefined → 0n, which - * decodes to a default all-zero meta — basePower 0, MoveClass.Physical(0) — harmless because those - * padded slots never enter the `moves` bucket). Always-length-4 fixed array. - */ -function buildMetas(e: any, bk: Hex, activeMonIndex: number): MoveMeta[] { - const metas: MoveMeta[] = new Array(4); - for (let i = 0; i < 4; i++) { - const slot = moveSlot(e, bk, CPU_PLAYER_INDEX, activeMonIndex, i) ?? 0n; - metas[i] = moveSlotLib.decodeMeta(slot, e, bk, CPU_PLAYER_INDEX, BigInt(activeMonIndex)); - } - return metas; -} - -// ============ SPEED / PRIORITY CHECK ============ - -/** - * `weGoFirst` — mirrors Engine.computePriorityPlayerIndex. Higher priority goes first; on a priority - * tie the faster mon goes first; a speed tie or being slower returns false (play safe). - */ -function weGoFirst( - e: any, - bk: Hex, - metas: MoveMeta[], - ourMonIndex: number, - opponentMonIndex: number, - ourMoveIndex: number, - opponentMoveIndex: number, -): boolean { - // Our priority: SWITCH_PRIORITY for a switch, else the decoded meta priority. - let ourPriority: number; - if (ourMoveIndex >= SWITCH_MOVE_INDEX) { - ourPriority = SWITCH_PRIORITY; - } else { - ourPriority = Number(metas[ourMoveIndex].priority); - } - - // Opponent priority: SWITCH_PRIORITY for a switch, else read their raw slot. - let oppPriority: number; - if (opponentMoveIndex >= SWITCH_MOVE_INDEX) { - oppPriority = SWITCH_PRIORITY; - } else { - const rawOppMove = e.getMoveForMonForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex), BigInt(opponentMoveIndex)); - oppPriority = Number(moveSlotLib.priority(rawOppMove, e, bk, OPP_PLAYER_INDEX)); - } - - if (ourPriority > oppPriority) return true; - if (ourPriority < oppPriority) return false; - - // Same priority: compare speeds (base + delta, sentinel already normalized). - const ourSpeed = monCurrentSpeed(e, bk, CPU_PLAYER_INDEX, ourMonIndex); - const oppSpeed = monCurrentSpeed(e, bk, OPP_PLAYER_INDEX, opponentMonIndex); - - if (ourSpeed > oppSpeed) return true; - return false; // speed tie or slower => play it safe -} - -/** - * `canOpponentKOUs`: does the opponent's specific chosen move KO our active mon? `damageToUs` is the - * pre-hoisted max(static, sim-measured) reveal damage — 0 for a switch/rest/harmless move, and real - * for damaging utility moves the static class gate can't price. - */ -function canOpponentKOUs( - e: any, - bk: Hex, - playerMonIndex: number, - opponentMoveIndex: number, - damageToUs: number, -): boolean { - if (opponentMoveIndex >= SWITCH_MOVE_INDEX) return false; - return damageToUs > 0 && damageToUs >= currentHp(e, bk, CPU_PLAYER_INDEX, playerMonIndex); -} - -// ============ DEFENSIVE SWITCH EVALUATION ============ - -/** - * `checkKOBypass`: true if our best damaging move would deal at least 90% of the opponent's CURRENT HP - * (±10% tolerance) AND we outspeed — in which case we stay in for the kill rather than swap out under - * heavy incoming damage. - */ -function checkKOBypass( - e: any, - bk: Hex, - metas: MoveMeta[], - activeMonIndex: number, - opponentMonIndex: number, - moves: RevealedMove[], - damages: number[], - playerMoveIndex: number, -): boolean { - const bestIdx = findBestDamageMove(metas, moves, damages); - if (bestIdx < 0) return false; - - const bestDmg = damages[bestIdx]; - if (bestDmg === 0) return false; - - const oppCurrentHp = currentHp(e, bk, OPP_PLAYER_INDEX, opponentMonIndex); - if (oppCurrentHp <= 0) return false; - - // ±10% tolerance: bestDmg >= 90% of opp current HP (bestDmg*10 >= oppHp*9). - if (bestDmg * 10 < oppCurrentHp * 9) return false; - - return weGoFirst(e, bk, metas, activeMonIndex, opponentMonIndex, moves[bestIdx].moveIndex, playerMoveIndex); -} - -/** - * `findBestSwitchCandidate`: among the switch candidates, find the one taking the LEAST damage % from - * the opponent's move, returning its index, damage %, and a survives flag. - */ -function findBestSwitchCandidate( - e: any, - bk: Hex, - opponentMonIndex: number, - opponentMoveIndex: number, - opponentExtraData: number, - oppMoveSlot: bigint | undefined, - oppMoveClass: MoveClass | undefined, - switches: RevealedMove[], - salt: bigint, -): { bestIdx: number; bestDamagePct: number; bestSurvives: boolean } { - let bestIdx = 0; - let bestDamagePct = Number.MAX_SAFE_INTEGER; - let bestSurvives = false; - - for (let i = 0; i < switches.length; i++) { - const candidateMonIndex = switches[i].extraData; - // opp (active) attacking the candidate: static estimate when readable, raised by the sim-measured - // entry damage (switch-in procs, variable-power moves the static model reads as 0). - const canEstimate = - oppMoveSlot !== undefined && (oppMoveClass === MoveClass.Physical || oppMoveClass === MoveClass.Special); - const ctx = buildDamageCalcContext(e, bk, OPP_PLAYER_INDEX, opponentMonIndex, CPU_PLAYER_INDEX, candidateMonIndex); - const dmg = Math.max( - canEstimate ? estimateDamage(e, bk, ctx, oppMoveSlot!, oppMoveClass!) : 0, - forkMeasureIncomingDamage(e, bk, opponentMoveIndex, opponentExtraData, candidateMonIndex, salt), - ); - - const maxHp = Number(e.getMonValueForBattle(bk, CPU_PLAYER_INDEX, BigInt(candidateMonIndex), MonStateIndexName.Hp)); - const curHp = currentHp(e, bk, CPU_PLAYER_INDEX, candidateMonIndex); - - // packed damagePct + survives bit; maxHp==0 => a huge sentinel pct. - const damagePct = maxHp > 0 ? Math.floor((dmg * 100) / maxHp) : Number.MAX_SAFE_INTEGER; - const survives = dmg < curHp; - - if (damagePct < bestDamagePct) { - bestDamagePct = damagePct; - bestIdx = i; - bestSurvives = survives; - } - } - return { bestIdx, bestDamagePct, bestSurvives }; -} - -/** - * `evaluateDefensiveSwitch`. `severeDamagePct` is the damage threshold below which incoming damage is - * ignored unless lethal. `koBypassFires` short-circuits (about to KO + outspeed => eat the hit). Takes - * the pre-hoisted opp-threat computation (oppMoveSlot / oppMoveClass / ctxToUs / damageToUs) so they - * aren't recomputed against P2. Returns `{ shouldSwitch, switchIdx }`. - */ -function evaluateDefensiveSwitch( - e: any, - bk: Hex, - activeMonIndex: number, - opponentMonIndex: number, - opponentMoveIndex: number, - opponentExtraData: number, - switches: RevealedMove[], - severeDamagePct: number, - koBypassFires: boolean, - oppMoveSlot: bigint | undefined, - oppMoveClass: MoveClass | undefined, - damageToUs: number, - salt: bigint, -): { shouldSwitch: boolean; switchIdx: number } { - if (koBypassFires) return { shouldSwitch: false, switchIdx: 0 }; - if (opponentMoveIndex >= SWITCH_MOVE_INDEX) return { shouldSwitch: false, switchIdx: 0 }; - - // No threat estimated OR measured => never switch defensively. - if (damageToUs <= 0) return { shouldSwitch: false, switchIdx: 0 }; - - // damage % to our active mon + lethality; bail if not severe and not lethal. - const ourMaxHp = Number(e.getMonValueForBattle(bk, CPU_PLAYER_INDEX, BigInt(activeMonIndex), MonStateIndexName.Hp)); - const ourCurHp = currentHp(e, bk, CPU_PLAYER_INDEX, activeMonIndex); - - const damagePctToUs = Math.floor((damageToUs * 100) / ourMaxHp); - const lethalToUs = damageToUs >= ourCurHp; - - if (damagePctToUs < severeDamagePct && !lethalToUs) return { shouldSwitch: false, switchIdx: 0 }; - - // Find the safest switch-in. - const { bestIdx, bestDamagePct, bestSurvives } = findBestSwitchCandidate( - e, bk, opponentMonIndex, opponentMoveIndex, opponentExtraData, oppMoveSlot, oppMoveClass, switches, salt, - ); - - // Materiality check. - if (lethalToUs && bestSurvives) return { shouldSwitch: true, switchIdx: bestIdx }; - if (damagePctToUs >= bestDamagePct + SWITCH_THRESHOLD) return { shouldSwitch: true, switchIdx: bestIdx }; - return { shouldSwitch: false, switchIdx: 0 }; -} - -// ============ FREE-TURN DETECTION ============ - -/** - * `isFreeTurnReveal`: opponent revealed a 0-power Self/Other move (setup, heal, hazard) — the - * free-turn punishment trigger gate. - */ -function isFreeTurnReveal(e: any, bk: Hex, opponentMonIndex: number, playerMoveIndex: number): boolean { - if (playerMoveIndex >= SWITCH_MOVE_INDEX) return false; - try { - const slot = e.getMoveForMonForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex), BigInt(playerMoveIndex)); - const oppClass = moveSlotLib.moveClass(slot, e, bk) as MoveClass; - // Only Other / Self moves qualify. - if (oppClass !== MoveClass.Other && oppClass !== MoveClass.Self) return false; - return getMoveBasePower(e, bk, slot) === 0; - } catch { - return false; - } -} - -/** - * `freeTurnPick` (free-turn punishment decision tree). Order: configured switch-in move -> 2HKO damage - * move -> guarded configured setup move. Returns `{ picked, move, moveUsedBitmap }`; when `picked` is - * false the caller falls through to P5. - * - * Two deliberate departures from the Diyu original keep this safe to run always-on (the old always-on - * version stalled whole games): the free-turn matchup PIVOT is dropped (trading tempo on every - * telegraphed free turn is what steered games into stalemates; a true wall is P5.5's job), and the - * setup branch only fires when the active mon can actually damage the opponent — setting up in front - * of something we can't hurt converts the free turn into a stall of our own. - */ -function freeTurnPick( - view: BattleView, - config: MonConfig, - moveUsedBitmap: number, - metas: MoveMeta[], - moves: RevealedMove[], - damages: number[], -): { picked: boolean; move?: RevealedMove; moveUsedBitmap: number } { - const e = view.engine; - const bk = view.bk as Hex; - const activeMonIndex = view.cpuActive; - const opponentMonIndex = view.oppActive; - - // Configured switch-in move on this safe turn. - let res = tryConfiguredMove(config, moveUsedBitmap, activeMonIndex, moves, CONFIG_SWITCH_IN_MOVE, 0); - moveUsedBitmap = res.usedBitmap; - if (res.index >= 0) return { picked: true, move: moves[res.index], moveUsedBitmap }; - - const bestIdx = findBestDamageMove(metas, moves, damages); - const bestDmg = bestIdx >= 0 ? damages[bestIdx] : 0; - const oppCurrentHp = currentHp(e, bk, OPP_PLAYER_INDEX, opponentMonIndex); - // 2HKO uses opp CURRENT HP (a damaged opp is easier to finish). - if (bestIdx >= 0 && oppCurrentHp > 0 && bestDmg * 2 >= oppCurrentHp) { - return { picked: true, move: moves[bestIdx], moveUsedBitmap }; - } - - // Setup only with momentum AND a matchup we can actually make progress in. - const productive = oppCurrentHp > 0 && bestDmg * 100 >= oppCurrentHp * WALL_DAMAGE_PCT; - if ( - productive && - hasMomentum(e, bk, view.mons.p1.length, view.cpuKO, view.mons.p0.length, view.oppKO, view.oppActive, view.mons.p1[view.cpuActive].stamina) - ) { - res = tryConfiguredMove(config, moveUsedBitmap, activeMonIndex, moves, CONFIG_SETUP_MOVE, 8); - moveUsedBitmap = res.usedBitmap; - if (res.index >= 0) return { picked: true, move: moves[res.index], moveUsedBitmap }; - } - - // No best-damage flail — fall through to P5/P6. - return { picked: false, moveUsedBitmap }; -} - -// --------------------------------------------------------------------------------------------- -// HardCpu — one fixed deterministic best-response + setup-punishment policy. -// --------------------------------------------------------------------------------------------- - -export class HardCpu extends BaseCpuStrategy { - readonly name = 'Hard (best-response + setup punishment)'; - - override decide(view: BattleView, pm: PlayerMove, rng: () => number, state: StrategyState): CpuMove { - const e = view.engine; - const bk = view.bk as Hex; - - // PEEK at the player's revealed move (we reply after the commit). - let playerMoveIndex = pm.moveIndex; - const playerExtraData = pm.extraData; - - // Configured-move lane bits persist across turns per battle (the on-chain cpuMoveUsedBitmap - // semantics) so a setup move fires once per switch-in, not once per free turn. Every return goes - // through DONE to write the bitmap back. - const bitmaps = (state['moveUsedBitmap'] ??= {}) as Record; - let moveUsedBitmap = bitmaps[bk] ?? 0; - const DONE = (m: RevealedMove, phase: string): CpuMove => { - bitmaps[bk] = moveUsedBitmap; - state['lastPhase'] = phase; // decision-phase trace (read by the arena analyzer) - return RET(m); - }; - // Single fixed policy: HELL severe-damage threshold, no mode branch. - const severeDamagePct = SEVERE_DAMAGE_PCT_HELL; - - // Enumerate valid options (the shared candidate buckets), and decode the active mon's metas. - const { noOp, moves, switches } = this.validMoves(e, bk, rng); - const metas = buildMetas(e, bk, view.cpuActive); - - // ── P0: Turn 0 — Lead Selection ── (a restarted/replayed battle reuses its key: reset the bitmap) - if (Number(e.getTurnIdForBattleState(bk)) === 0) { - moveUsedBitmap = 0; - const lead = selectLead(e, bk, playerExtraData, switches, false); - moveUsedBitmap = clearMoveUsedBitsOnSwitchIn(e, bk, moveUsedBitmap, lead.extraData); - return DONE(lead, 'P0-lead'); - } - - const activeMonIndex = view.cpuActive; - let opponentMonIndex = view.oppActive; - - // A zapped opponent (ShouldSkipTurn) loses its revealed action entirely — switch included. The - // reveal is VOID: treat it as a rest so the whole tree plays the free turn against the mon that - // actually stays in, instead of best-responding to a move that will never execute. - if (view.mons.p0[view.oppActive].skipTurn) { - playerMoveIndex = NO_OP_INDEX; - } - - // ── P1: KO'd / Swap-Out Effect — Forced Switch ── (with no valid target — sole survivor already - // active — the engine accepts a rest) - if (view.switchFlag === 1 || view.mons.p1[view.cpuActive].ko) { - if (switches.length === 0) return DONE({ moveIndex: NO_OP_INDEX, extraData: 0 }, 'P1-forced-switch'); - const sw = selectBestSwitch(e, bk, opponentMonIndex, playerMoveIndex, switches, false); - moveUsedBitmap = clearMoveUsedBitsOnSwitchIn(e, bk, moveUsedBitmap, sw.extraData); - return DONE(sw, 'P1-forced-switch'); - } - - // If the opponent is switching, target the incoming mon. - if (playerMoveIndex === SWITCH_MOVE_INDEX) { - opponentMonIndex = playerExtraData; - } - - // Outgoing damage = max(static model, sim-measured). The static estimate keeps the always-hit - // floor (a fork's fixed-salt accuracy roll can miss); the fork measurement sees everything the - // model can't — variable-power moves, ability procs, the actual post-switch defender. The static - // context getter always builds against CURRENT actives, so on a revealed switch rebuild against - // the INCOMING defender. - const attackCtx = - playerMoveIndex === SWITCH_MOVE_INDEX - ? buildDamageCalcContext(e, bk, CPU_PLAYER_INDEX, activeMonIndex, OPP_PLAYER_INDEX, opponentMonIndex) - : e.getDamageCalcContext(bk, CPU_PLAYER_INDEX, BigInt(activeMonIndex), OPP_PLAYER_INDEX, BigInt(view.oppActive)); - const staticDamages = computeMoveDamages(attackCtx, metas, moves); - const saltSeed = BigInt(Number(e.getTurnIdForBattleState(bk)) + 1); - const measured = forkMeasureMoveDamages(e, bk, playerMoveIndex, playerExtraData, moves, saltSeed); - const damages = evScaleDamages( - e, bk, activeMonIndex, moves, - staticDamages.map((s, i) => Math.max(s, measured.damages[i])), - ); - - // Eval-veto wrapper for every decision from here down: the tree's pick stands unless a forked - // alternative clearly beats it (EVAL_OVERRIDE_MARGIN). Move forks reuse the measurement scores; - // only switches/rest fork extra. Overrides surface as ">eval" in the decision trace. - const ARB = (m: RevealedMove, phase: string): CpuMove => { - const better = pickEvalOverride( - e, bk, playerMoveIndex, playerExtraData, m, moves, measured.scores, switches, noOp, saltSeed, - ); - if (better === null) return DONE(m, phase); - if (better.moveIndex === SWITCH_MOVE_INDEX) { - moveUsedBitmap = clearMoveUsedBitsOnSwitchIn(e, bk, moveUsedBitmap, better.extraData); - } - return DONE(better, `${phase}>eval`); - }; - - // Hoist the opp-threat computation ONCE: read the opp's revealed move slot + class, build the - // opp->us damage context, and estimate damage-to-our-active. P2 (canOpponentKOUs) and P5 - // (evaluateDefensiveSwitch) both consume these instead of recomputing the same numbers. - let oppMoveSlot: bigint | undefined; - let oppMoveClass: MoveClass | undefined; - let damageToUs = 0; - if (playerMoveIndex < SWITCH_MOVE_INDEX) { - try { - oppMoveSlot = e.getMoveForMonForBattle(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex), BigInt(playerMoveIndex)); - oppMoveClass = moveSlotLib.moveClass(oppMoveSlot!, e, bk) as MoveClass; - if (oppMoveClass === MoveClass.Physical || oppMoveClass === MoveClass.Special) { - const ctxToUs = e.getDamageCalcContext(bk, OPP_PLAYER_INDEX, BigInt(opponentMonIndex), CPU_PLAYER_INDEX, BigInt(activeMonIndex)); - damageToUs = estimateDamage(e, bk, ctxToUs, oppMoveSlot!, oppMoveClass); - } - } catch { - oppMoveSlot = undefined; - oppMoveClass = undefined; - } - // True reveal damage from the sim — prices what the static model can't (variable-power moves - // it reads as 0, our own ability mitigation it overestimates). The static estimate stays as - // the floor only when the fork measures nothing (a fixed-salt roll can miss). - const measuredToUs = forkMeasureIncomingDamage(e, bk, playerMoveIndex, playerExtraData, null, saltSeed); - if (measuredToUs > 0) damageToUs = measuredToUs; - } - - // ── P2: Can We KO the Opponent? ── - const koMoveIdx = findKOMove(e, bk, opponentMonIndex, metas, moves, damages); - if (koMoveIdx >= 0) { - const opponentCanKOUs = canOpponentKOUs(e, bk, activeMonIndex, playerMoveIndex, damageToUs); - if ( - !opponentCanKOUs || - weGoFirst(e, bk, metas, activeMonIndex, opponentMonIndex, moves[koMoveIdx].moveIndex, playerMoveIndex) - ) { - return ARB(moves[koMoveIdx], 'P2-ko'); - } - // else: opponent outspeeds us and can KO — fall through to P5. - } - - // ── P3: Opponent is Switching ── A telegraphed free turn: same guarded punish tree as P4.5 - // (configured switch-in -> 2HKO -> momentum+productive setup), then band damage, then rest. - if (playerMoveIndex === SWITCH_MOVE_INDEX) { - const fr = freeTurnPick(view, BETTER_CPU_MON_CONFIG, moveUsedBitmap, metas, moves, damages); - moveUsedBitmap = fr.moveUsedBitmap; - if (fr.picked) return ARB(fr.move!, 'P3-opp-switching'); - if (moves.length > 0) { - const bestMove = pickSimilarDamageMove(damages, rng); - if (bestMove >= 0) return ARB(moves[bestMove], 'P3-opp-switching'); - } - return ARB(noOp[0], 'P3-opp-switching'); // rest on free turn - } - - // ── P4: Opponent is Resting ── (same free-turn punish as P3) - if (playerMoveIndex === NO_OP_INDEX) { - if (moves.length === 0) return ARB(noOp[0], 'P4-opp-resting'); // both rest - const fr = freeTurnPick(view, BETTER_CPU_MON_CONFIG, moveUsedBitmap, metas, moves, damages); - moveUsedBitmap = fr.moveUsedBitmap; - if (fr.picked) return ARB(fr.move!, 'P4-opp-resting'); - const bestMove = pickSimilarDamageMove(damages, rng); - if (bestMove >= 0) return ARB(moves[bestMove], 'P4-opp-resting'); - return ARB(noOp[0], 'P4-opp-resting'); - } - - // ── P4.5: Free-turn setup-punishment — the opponent telegraphed a 0-power Self/Other move. - // Safe always-on via freeTurnPick's guards (no pivot branch; setup needs momentum + progress). - if (isFreeTurnReveal(e, bk, opponentMonIndex, playerMoveIndex)) { - const fr = freeTurnPick(view, BETTER_CPU_MON_CONFIG, moveUsedBitmap, metas, moves, damages); - if (fr.picked) { - moveUsedBitmap = fr.moveUsedBitmap; - return ARB(fr.move!, 'P4.5-free-turn'); - } - moveUsedBitmap = fr.moveUsedBitmap; - } - - // ── P5: Opponent Using a Move — Evaluate Defensive Switch ── - if (switches.length > 0) { - // KO-bypass (ALWAYS-ON): best move near-KOs (±10%) and we outspeed => stay in. - const koBypassFires = - moves.length > 0 && - checkKOBypass(e, bk, metas, activeMonIndex, opponentMonIndex, moves, damages, playerMoveIndex); - const { shouldSwitch, switchIdx } = evaluateDefensiveSwitch( - e, bk, activeMonIndex, opponentMonIndex, playerMoveIndex, playerExtraData, switches, severeDamagePct, - koBypassFires, oppMoveSlot, oppMoveClass, damageToUs, saltSeed, - ); - if (shouldSwitch) { - // The switch costs our turn, and the damage-% thresholds above never weighed our own offense. - // Cross-check: only switch if the fork says it leaves a better position than staying and - // hitting (the stay score was already paid for by the damage measurement). - const stayIdx = findBestDamageMove(metas, moves, damages); - const swScore = forkScoreAction(e, bk, playerMoveIndex, playerExtraData, switches[switchIdx], saltSeed); - if (stayIdx < 0 || swScore >= measured.scores[stayIdx]) { - moveUsedBitmap = clearMoveUsedBitsOnSwitchIn(e, bk, moveUsedBitmap, switches[switchIdx].extraData); - return ARB(switches[switchIdx], 'P5-defensive-switch'); - } - return ARB(moves[stayIdx], 'P5-stay-and-hit'); - } - } - - // ── P5.5: Anti-wall stalemate-breaker — pivot out of a dead matchup (we can't progress + a bench - // mon matches up strictly better). Catches the flail-at-a-wall the threat-based P5 misses. ── - if (switches.length > 0) { - const antiWallIdx = antiWallSwitch(view, metas, moves, damages, switches, { - revealIdx: playerMoveIndex, - revealExtra: playerExtraData, - salt: saltSeed, - }); - if (antiWallIdx >= 0) { - moveUsedBitmap = clearMoveUsedBitsOnSwitchIn(e, bk, moveUsedBitmap, switches[antiWallIdx].extraData); - return ARB(switches[antiWallIdx], 'P5.5-anti-wall'); - } - } - - // ── P6: Default — Best Damaging Move (sampled from the similar-damage band) ── - if (moves.length > 0) { - const r = tryConfiguredMove(BETTER_CPU_MON_CONFIG, moveUsedBitmap, activeMonIndex, moves, CONFIG_SWITCH_IN_MOVE, 0); - moveUsedBitmap = r.usedBitmap; - if (r.index >= 0) return ARB(moves[r.index], 'P6-default'); - - const preferredMove = tryPreferredMove(BETTER_CPU_MON_CONFIG, activeMonIndex, attackCtx, metas, moves); - if (preferredMove >= 0) return ARB(moves[preferredMove], 'P6-default'); - - const bestMove = pickSimilarDamageMove(damages, rng); - if (bestMove >= 0) return ARB(moves[bestMove], 'P6-default'); - } - - // Stuck fallback — no damaging option left. The tree has no opinion here, so take the best - // forked position outright: move scores are already measured; switches/rest fork now. - let fbBest: RevealedMove = noOp[0] ?? { moveIndex: NO_OP_INDEX, extraData: 0 }; - let fbScore = -Infinity; - const fbConsider = (m: RevealedMove, s: number) => { - if (s > fbScore) { - fbScore = s; - fbBest = m; - } - }; - for (let i = 0; i < moves.length; i++) fbConsider(moves[i], measured.scores[i]); - for (const m of [...switches, ...noOp]) { - fbConsider(m, forkScoreAction(e, bk, playerMoveIndex, playerExtraData, m, saltSeed)); - } - if (fbBest.moveIndex === SWITCH_MOVE_INDEX) { - moveUsedBitmap = clearMoveUsedBitsOnSwitchIn(e, bk, moveUsedBitmap, fbBest.extraData); - } - return DONE(fbBest, 'fallback'); - } -} diff --git a/sims/src/cpu/strategies/override-cpu.ts b/sims/src/cpu/strategies/override-cpu.ts deleted file mode 100644 index f4ef17cf..00000000 --- a/sims/src/cpu/strategies/override-cpu.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { Hex } from '../hex'; -import { SWITCH_MOVE_INDEX } from '../constants'; -import { - cpuActiveMonIndex, - oppActiveMonIndex, - monMaxHp, - monCurrentHp, - monCurrentStamina, - monCurrentSpeed, -} from '../engine-view'; -import { forkMeasureIncomingDamage } from '../heuristic-native'; -import { BattleView } from '../battle-view'; -import { BaseCpuStrategy, CpuMove, PlayerMove, StrategyState } from '../strategy'; -import { HardCpu } from './hard-cpu'; - -/** - * OVERRIDE CPU — a scripted-plan pilot for validating setup / resource / self-sacrifice lines the - * heuristic pilots refuse to play ("unpiloted is not weak"). It wraps {@link HardCpu}: for the active - * mon it consults a per-mon script (ordered rules with `when` / `once` / `maxUses` gates); the first - * rule whose move is affordable this turn and whose gate passes is played, otherwise it delegates the - * whole decision to hard. Reusable beyond the design pass — any "force this mon to play this line" - * experiment registers a script here. - * - * Mons are keyed by BASE HP, which is unique per mon (see drool/mons.csv), because the battle view - * exposes stats, not mon ids. - * - * getCpuStrategy('override') // then run via sim-tests/arena/*.ts --strategies override - */ - -const CPU = 1n; // the CPU is always p1 -const OPP = 0n; - -/** Read-only position facts a `when` predicate can gate on. */ -export interface OverrideCtx { - /** CPU active mon current HP / max HP, in [0, 1]. */ - hpFrac: number; - /** CPU active mon current stamina. */ - stamina: number; - /** CPU active mon outspeeds the opponent's active mon. */ - outspeeds: boolean; - /** The opponent's revealed move this turn would KO the CPU active mon if it stays in. */ - incomingLethal: boolean; - /** The opponent's revealed move index (SWITCH_MOVE_INDEX / NO_OP when it isn't attacking). */ - oppMoveIndex: number; - /** Turns elapsed this game. */ - turn: number; - /** How many times THIS rule has already fired this game. */ - uses: number; -} - -export interface OverrideRule { - /** Move slot 0-3 to play. */ - move: number; - /** Optional gate; the rule only fires when this returns true (or when omitted). */ - when?: (c: OverrideCtx) => boolean; - /** Fire at most once per game. */ - once?: boolean; - /** Fire at most N times per game. */ - maxUses?: number; - /** Force a specific extraData target; defaults to the engine-picked valid target. */ - extraData?: number; - /** Human label for logs. */ - label?: string; -} - -/** An ordered list of rules; first affordable + passing rule wins, else fall through to hard. */ -export type OverrideScript = OverrideRule[]; - -/** - * Scripts keyed by base HP. Add a mon by dropping its base HP (from mons.csv) and an ordered rule - * list. Start small — one signature line per mon is enough to test the hypothesis. - */ -export const OVERRIDE_SCRIPTS: Record = { - // Ghouliath: EG-on-lethal tested negative twice in the v4 pass (48.3 / 48.9 vs hard 51.7 at 600 - // games each, ungated and hpFrac<0.4-gated); script removed so later runs have a cleaner field. - - // Aurox (baseHp 400): the tank line — Iron Wall (slot 2) on a fresh, stamina-flush entry, then - // Bull Rush (slot 3) as the default so Up Only ramps behind the regen. - 400: [ - { move: 2, when: (c) => c.hpFrac > 0.9 && c.stamina >= 3, maxUses: 1, label: 'Iron Wall on entry' }, - { move: 3, label: 'Bull Rush' }, - ], - - // Iblivion (baseHp 277): the turn-1 Loop line (designer-requested recheck, v4 annotation) — - // Loop once on entry at the +15% Baselight-1 tier instead of spending two turns on the - // Renormalize battery; Baselight then accrues passively (3 by end of t2 -> empowered Unbounded - // Strike at t3), with Brightback sustain under half HP. The Renormalize-first battery tested - // flat four times (45.7/45.9/48.1 at 600, 45.6 at 1200) and 46.7 at the 2000-game anchor. - 277: [ - { move: 1, once: true, label: 'Loop on entry (+15%)' }, - { move: 2, when: (c) => c.hpFrac < 0.5, label: 'Brightback sustain' }, - { move: 0, label: 'Unbounded Strike' }, - ], - - // Xmon: script REMOVED in the v4 pass — Night Terrors x2 tested negative twice (43.3 pre-change - // and 41.3 post-change vs hard 46.6/45.4), and slot-keyed rules are corrupted for the four - // rotating-catalog mons (the slot-3 rule fired Invoke Taboo 30% of move-turns under rotation). - // Re-script only after the registry can key rules to move identity instead of battle slot. - - // Volthare (baseHp 310): prefer Mega Star Blast (slot 2) whenever affordable — testing whether - // greedy's 7-point edge over hard on this mon reduces to one preferred-move rule (the shape - // munch's MonConfig already supports). - 310: [{ move: 2, when: (c) => c.stamina >= 3, label: 'Mega Star Blast preference' }], - - // Embursa (baseHp 420): arm Q5 (slot 3) once on the first action — a 150-power undodgeable hit - // five round-starts later for 2 stamina — then fall through to hard. Tests whether the delayed - // nuke is unpiloted rather than weak (the fourth delayed-payoff dead slot of the pass). - 420: [{ move: 3, maxUses: 1, label: 'Arm Q5' }], - - // Pengym (baseHp 371): the committed Frostbite combo — Chill Out (slot 0) to tag, Deep Freeze - // (slot 2) whenever affordable, re-tag while saving stamina. Tests design.md's "somewhat slow" - // worry. OverrideCtx cannot see opponent status, so stamina gates approximate the sequence. - 371: [ - { move: 0, maxUses: 1, label: 'Chill Out tag' }, - { move: 2, when: (c) => c.stamina >= 3, maxUses: 2, label: 'Deep Freeze' }, - { move: 0, when: (c) => c.stamina < 3, maxUses: 2, label: 'Chill Out while saving' }, - ], - - // Inutia (baseHp 351): v4 isolation — arm Chain Expansion once, fall through to hard for - // everything else. (The earlier three-rule bundle incl. Initialize/Hit And Dip scored 51.6 vs - // hard 50.3 at 600; this isolates CE's share of that gain.) - 351: [{ move: 0, once: true, label: 'Arm Chain Expansion' }], - - // Ekineki: script REMOVED in the v4 pass — "NNN when stamina <= 2" tested sharply negative - // (50.8 vs fixed hard 56.7 at 2000 games): at <=2 stamina the following turn still can't afford - // Overflow, so the one-turn crit prime expired uncashed twice a game. The payable window is - // roughly "stamina exactly 3 with Overflow planned", which is too thin to script profitably. -}; - -export class OverrideCpu extends BaseCpuStrategy { - readonly name = 'override'; - private readonly fallback = new HardCpu(); - - createState(): StrategyState { - return { fallback: this.fallback.createState(), turn: 0, uses: {} }; - } - - decide(view: BattleView, playerMove: PlayerMove, rng: () => number, state: StrategyState): CpuMove { - const e = view.engine; - const bk = view.bk as Hex; - state.turn = (state.turn as number) + 1; - - const activeIdx = cpuActiveMonIndex(e, bk); - const script = OVERRIDE_SCRIPTS[monMaxHp(e, bk, CPU, activeIdx)]; - - if (script) { - const { moves } = this.validMoves(e, bk, rng); // affordable slots, each with a valid target - const uses = state.uses as Record; - let ctx: OverrideCtx | undefined; - - for (let i = 0; i < script.length; i++) { - const rule = script[i]; - const key = `${activeIdx}:${i}`; - const fired = uses[key] ?? 0; - if (rule.once && fired >= 1) continue; - if (rule.maxUses !== undefined && fired >= rule.maxUses) continue; - - const affordable = moves.find((m) => m.moveIndex === rule.move); - if (!affordable) continue; // not castable this turn (stamina / forced-switch / etc.) - - if (rule.when) { - if (!ctx) ctx = buildCtx(e, bk, activeIdx, playerMove, state.turn as number); - if (!rule.when({ ...ctx, uses: fired })) continue; - } - - uses[key] = fired + 1; - return { moveIndex: affordable.moveIndex, extraData: rule.extraData ?? affordable.extraData }; - } - } - - return this.fallback.decide(view, playerMove, rng, state.fallback as StrategyState); - } -} - -function buildCtx(e: any, bk: Hex, activeIdx: number, playerMove: PlayerMove, turn: number): OverrideCtx { - const curHp = monCurrentHp(e, bk, CPU, activeIdx); - const maxHp = monMaxHp(e, bk, CPU, activeIdx); - const oppIdx = oppActiveMonIndex(e, bk); - - // Incoming lethal: only meaningful when the opponent actually attacks this turn. - let incomingLethal = false; - if (playerMove.moveIndex < SWITCH_MOVE_INDEX) { - try { - incomingLethal = - forkMeasureIncomingDamage(e, bk, playerMove.moveIndex, playerMove.extraData, null, 0n) >= curHp; - } catch { - incomingLethal = false; // a fork that fails on an odd state just reads as non-lethal - } - } - - return { - hpFrac: maxHp > 0 ? curHp / maxHp : 0, - stamina: monCurrentStamina(e, bk, CPU, activeIdx), - outspeeds: monCurrentSpeed(e, bk, CPU, activeIdx) > monCurrentSpeed(e, bk, OPP, oppIdx), - incomingLethal, - oppMoveIndex: playerMove.moveIndex, - turn, - uses: 0, - }; -} diff --git a/sims/src/cpu/strategies/search-cpu.ts b/sims/src/cpu/strategies/search-cpu.ts deleted file mode 100644 index 4db312f0..00000000 --- a/sims/src/cpu/strategies/search-cpu.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * SEARCH CPU — TS port of the Rust depth-limited simultaneous-move maximin search - * (`transpiler/strategies-rs/src/search.rs`). - * - * At each ply both sides' candidate actions (moves + switches + rest — rest is ALWAYS a - * candidate, the rest-bug lesson) form a matrix game; the backup is maximin (worst-case - * opponent), leaves score through the linear evaluator. Two variants: - * - no-peek: full my×opp grid at the root (production commit-reveal semantics); - * - peek-at-root: best-respond to the revealed `playerMove` (single opponent column), - * maximin only deeper (arena/p1 reveal semantics). - * Terminal wins are mate-distance discounted (faster wins / later losses score better). - * - * Determinism: fixed salt 0 on every hypothetical; earliest-candidate tie-break. A throwing - * hypothetical (an unreachable/reverting line) is contained to the decision via try/catch → - * first legal fallback. Forks are disposed depth-first (only O(depth) live at once). - */ -import { Hex } from '../hex'; -import { captureBattleView, type BattleView } from '../battle-view'; -import { calculateValidMoves, makeRng, type RevealedMove } from '../engine-view'; -import { applyHypotheticalMoveKeyed, disposeFork, type HypotheticalMove } from '../forward-model'; -import { scoreState } from '../evaluator'; -import { transposeEngine } from '../../arena/transpose'; -import { NO_OP_INDEX } from '../constants'; -import { BaseCpuStrategy, type CpuMove, type PlayerMove, type StrategyState } from '../strategy'; - -const MAX_DEPTH = 3; -const MAX_ACTIONS = 8; // 4 moves + 3 switches + rest — never truncates in singles -const WIN = 1e9; -const LOSS = -1e9; -const SALT = 0n; - -function hypo(m: RevealedMove | PlayerMove): HypotheticalMove { - return { moveIndex: m.moveIndex, salt: SALT, extraData: m.extraData }; -} - -// Candidate enumeration draws payload targets from a NODE-LOCAL fixed-seed rng, so the tree is -// independent of visit order — that's what makes the row prune truly argmax-invariant (a shared -// stream would shift every later node's picks when a branch is skipped). -const ENUM_SEED = 0x5eed; - -/** All candidate actions for the engine-perspective CPU at `key`: moves + switches + rest. */ -function candidates(e: any, key: Hex): RevealedMove[] { - const v = calculateValidMoves(e, key, makeRng(ENUM_SEED)); - return [...v.moves, ...v.switches, ...v.noOp].slice(0, MAX_ACTIONS); -} - -/** Both sides' action lists given the (seat-relative) switch flag; a non-acting side is [null]. */ -function actionLists( - e: any, - key: Hex, - flag: number, -): { my: Array; opp: Array } { - const my: Array = flag !== 0 ? candidates(e, key) : [null]; - const opp: Array = flag !== 1 ? candidates(transposeEngine(e), key) : [null]; - return { my: my.length ? my : [null], opp: opp.length ? opp : [null] }; -} - -/** Recursive maximin value of the position at `key`, CPU-perspective. */ -function value(e: any, key: Hex, depth: number): number { - const view = captureBattleView(e, key); - const cpuAlive = view.mons.p1.filter((m) => !m.ko).length; - const oppAlive = view.mons.p0.filter((m) => !m.ko).length; - // Mate-distance discounting: more remaining depth = terminal reached sooner. - if (oppAlive <= 0) return WIN + depth; - if (cpuAlive <= 0) return LOSS - depth; - if (depth <= 0) return scoreState(view); - - const { my, opp } = actionLists(e, key, view.switchFlag); - let best = -Infinity; - for (const a of my) { - let worst = Infinity; - for (const o of opp) { - const child = applyHypotheticalMoveKeyed(e, key, o ? hypo(o) : null, a ? hypo(a) : null); - const v = value(e, child, depth - 1); - disposeFork(e, child); - if (v < worst) worst = v; - if (worst <= best) break; // row can no longer beat the best row — argmax-invariant prune - } - if (worst > best) best = worst; - } - return best; -} - -function fallback(e: any, bk: Hex, view: BattleView, rng: () => number): CpuMove { - if (view.switchFlag === 0) return { moveIndex: NO_OP_INDEX, extraData: 0 }; - const v = calculateValidMoves(e, bk, rng); - const first = v.moves[0] ?? v.switches[0] ?? v.noOp[0]; - return first ?? { moveIndex: NO_OP_INDEX, extraData: 0 }; -} - -/** Depth-`depth` maximin search; `peek` = best-respond to the revealed move at the root. */ -export class SearchCpu extends BaseCpuStrategy { - constructor( - readonly name: string, - private readonly depth: number, - private readonly peek: boolean, - ) { - super(); - } - - decide(view: BattleView, playerMove: PlayerMove, rng: () => number, _state: StrategyState): CpuMove { - try { - return this.decideInner(view, playerMove, rng); - } catch { - return fallback(view.engine, view.bk as Hex, view, rng); - } - } - - private decideInner(view: BattleView, playerMove: PlayerMove, _rng: () => number): CpuMove { - const depth = Math.max(1, Math.min(MAX_DEPTH, this.depth)); - const e = view.engine; - const bk = view.bk as Hex; - - // CPU passive (opp forced-switch) — nothing to choose. - if (view.switchFlag === 0) return { moveIndex: NO_OP_INDEX, extraData: 0 }; - - const { my, opp: oppFull } = actionLists(e, bk, view.switchFlag); - // Peek-at-root: the opponent's move IS revealed → a single opponent column. - const opp = this.peek && view.switchFlag !== 1 ? [playerMove as RevealedMove] : oppFull; - - let best: CpuMove = my[0] ?? { moveIndex: NO_OP_INDEX, extraData: 0 }; - let bestVal = -Infinity; - for (const a of my) { - let worst = Infinity; - for (const o of opp) { - const child = applyHypotheticalMoveKeyed(e, bk, o ? hypo(o) : null, a ? hypo(a) : null); - const v = value(e, child, depth - 1); - disposeFork(e, child); - if (v < worst) worst = v; - if (worst <= bestVal) break; // this action can no longer win the argmax — prune - } - // Strict >: ties keep the earliest candidate (moves before switches) — deterministic. - if (worst > bestVal && a) { - bestVal = worst; - best = a; - } - } - return best; - } -} diff --git a/sims/src/cpu/strategy.ts b/sims/src/cpu/strategy.ts deleted file mode 100644 index b709fc8d..00000000 --- a/sims/src/cpu/strategy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Hex } from './hex'; -import { BattleView } from './battle-view'; -import { RevealedMove, calculateValidMoves, pickUniform } from './engine-view'; - -/** - * CPU STRATEGY FRAMEWORK — the interface + minimal base class every strategy implements. - * - * Every strategy — the difficulty heuristics, personas, and the eval/search tier — implements - * {@link CpuStrategy}. Strategies read state through the free functions in `engine-view` / - * `heuristic-shared` / `heuristic-native` and the read-once {@link BattleView} snapshot they're - * handed — there is no fat wrapper surface; import the helper you need directly. - * - * The CPU is ALWAYS p1; the human opponent is p0. The `view` carries `.engine` / `.bk` for the readers - * not projected onto it. - */ - -/** The human (p0) move the CPU is responding to this turn. */ -export interface PlayerMove { - moveIndex: number; - extraData: number; -} - -/** What every strategy returns: the CPU's (p1) chosen move + its extraData target. */ -export type CpuMove = { moveIndex: number; extraData: number }; - -/** - * Per-(strategy) mutable state bag a strategy may persist across turns. The dispatcher owns it - * (`createState()` once per strategy, threaded back into every `decide`); hard keys its per-battle - * setup lane bits on it, stall its status-used bits — stateless strategies just ignore it. - */ -export interface StrategyState { - [k: string]: unknown; -} - -/** - * A pluggable CPU strategy. `decide` reads the position (via the `view` + free helpers), consults its - * own `state`, and returns the move. `playerMove` is p0's revealed move (BetterCPU peeks at it). - */ -export interface CpuStrategy { - readonly name: string; - /** Build the initial per-strategy state bag. */ - createState(): StrategyState; - /** Choose the CPU's move for the current turn. */ - decide(view: BattleView, playerMove: PlayerMove, rng: () => number, state: StrategyState): CpuMove; -} - -/** - * Minimal base: a `name`, a default empty `createState`, the abstract `decide`, and the two engine-view - * helpers strategies actually reach for via `this.` (candidate enumeration + uniform tie-break). Every - * other engine read is a direct free-function import from `engine-view` / `heuristic-shared`. - */ -export abstract class BaseCpuStrategy implements CpuStrategy { - abstract readonly name: string; - - createState(): StrategyState { - return {}; - } - - abstract decide(view: BattleView, playerMove: PlayerMove, rng: () => number, state: StrategyState): CpuMove; - - /** Port of `CPU._calculateValidMoves` — the candidate buckets a strategy chooses from. */ - protected validMoves( - e: any, - bk: string, - rng?: () => number, - ): { noOp: RevealedMove[]; moves: RevealedMove[]; switches: RevealedMove[] } { - return calculateValidMoves(e, bk as Hex, rng); - } - - /** Uniform pick from `arr` with the injected rng (tie-breaks). */ - protected pickUniform(arr: T[], rng: () => number): T | undefined { - return pickUniform(arr, rng); - } -} diff --git a/transpiler/codegen_rs/README.md b/transpiler/codegen_rs/README.md index 9ef5c6db..81ef76ff 100644 --- a/transpiler/codegen_rs/README.md +++ b/transpiler/codegen_rs/README.md @@ -119,5 +119,5 @@ substrate. What still runs: bins) that loads the roster from `drool/*.csv` + `src/mons/*.json` at runtime. The Rust side may diverge from TS freely (rng stream and CPU decisions included); port-backs to the game's CPU mode carry no - bit-identicality requirement. The TS arena driver - (`sims/src/arena/game.ts`) remains as the port-back reference. + bit-identicality requirement. Ports land in munch and are gated there + against `sim-tests/arena/cpu-reference.json`. diff --git a/transpiler/strategies-rs/src/evaluator.rs b/transpiler/strategies-rs/src/evaluator.rs index 42dd8a18..a30bf290 100644 --- a/transpiler/strategies-rs/src/evaluator.rs +++ b/transpiler/strategies-rs/src/evaluator.rs @@ -1,4 +1,4 @@ -//! Static position evaluation — port of `sims/src/cpu/evaluator.ts`. +//! Static position evaluation. //! The score is a linear function `φ(state)·w`: [`features`] reads the raw //! feature vector, [`dot`] weights it. [`DEFAULT_WEIGHTS`] reproduces the //! original hardcoded weights, so the default path is byte-identical. diff --git a/transpiler/strategies-rs/src/game.rs b/transpiler/strategies-rs/src/game.rs index 43154e3b..26072938 100644 --- a/transpiler/strategies-rs/src/game.rs +++ b/transpiler/strategies-rs/src/game.rs @@ -1,5 +1,6 @@ -//! Game loop + batch runner — port of `sims/src/arena/game.ts`'s -//! `runGameLoop` seating/peek/salt semantics, driving [`Sim`] natively. +//! Game loop + batch runner — the seating/peek/salt semantics munch's +//! arena also implements (`sim-tests/arena/game.ts`), driving [`Sim`] +//! natively. //! //! RNG stream discipline (must match TS turn-for-turn): the p0 seat //! decides first (its candidate enumeration + tie-breaks draw from the diff --git a/transpiler/strategies-rs/src/greedy.rs b/transpiler/strategies-rs/src/greedy.rs index 2180ae5b..e77513be 100644 --- a/transpiler/strategies-rs/src/greedy.rs +++ b/transpiler/strategies-rs/src/greedy.rs @@ -1,7 +1,6 @@ -//! GREEDY (eval) — port of `sims/src/cpu/strategies/greedy-eval.ts` in its -//! registry configuration (salts=1, default weights): 1-ply best response -//! on the forward model + evaluator. The risk-aware multi-salt mode is not -//! ported — the arena's `greedy` never enables it, and with one sample the +//! GREEDY (eval) — 1-ply best response on the forward model + evaluator +//! (salts=1, default weights). The risk-aware multi-salt mode is absent: +//! the arena's `greedy` never enables it, and with one sample the //! risk-adjusted score IS the sample (mean of one, exact in f64). use crate::evaluator::Weights; diff --git a/transpiler/strategies-rs/src/heuristic.rs b/transpiler/strategies-rs/src/heuristic.rs index 596c3f8b..803e79cd 100644 --- a/transpiler/strategies-rs/src/heuristic.rs +++ b/transpiler/strategies-rs/src/heuristic.rs @@ -1,8 +1,7 @@ -//! HEURISTIC CPU — port of `sims/src/cpu/strategies/hard-cpu.ts`: best-response -//! with foreknowledge (peeks the revealed move), sim-measured damage on -//! both sides, guarded free-turn setup punishment, defensive switching, -//! anti-wall pivot, and the eval-veto wrapper. Every phase, threshold and -//! rng draw mirrors the TS source order. +//! HEURISTIC CPU — best-response with foreknowledge (peeks the revealed +//! move), sim-measured damage on both sides, guarded free-turn setup +//! punishment, defensive switching, anti-wall pivot, and the eval-veto +//! wrapper. use chomp_engine::Constants; use chomp_engine::Enums::MoveClass; diff --git a/transpiler/strategies-rs/src/jsrng.rs b/transpiler/strategies-rs/src/jsrng.rs index 11a4fa1b..df863040 100644 --- a/transpiler/strategies-rs/src/jsrng.rs +++ b/transpiler/strategies-rs/src/jsrng.rs @@ -1,6 +1,6 @@ -//! Seeded RNG + salt — port of `sims/src/arena/rng.ts` (mulberry32, -//! verbatim from munch). Same seed => bit-identical float stream => the -//! same team draws, tie-breaks and per-turn salts as the TS arena. +//! Seeded RNG + salt — mulberry32, matching munch's `makeRng` +//! (`src/app/services/cpu/engine-view.ts`). Same seed => bit-identical +//! float stream => the same team draws, tie-breaks and per-turn salts. /// mulberry32. Mirrors the JS coercions exactly: all state math is u32 /// wrapping (JS `>>> 0` / `Math.imul`), the output is `u32 / 2^32` — both diff --git a/transpiler/strategies-rs/src/lib.rs b/transpiler/strategies-rs/src/lib.rs index 545779c7..c74759d5 100644 --- a/transpiler/strategies-rs/src/lib.rs +++ b/transpiler/strategies-rs/src/lib.rs @@ -1,15 +1,14 @@ //! Native CPU strategies + game loop over the transpiled engine. //! -//! Rust port of the arena decision stack (`sims/src/cpu/*` + the -//! `sims/src/arena/game.ts` loop). All three arena strategies — `hard`, -//! `greedy`, `override` — were ported 1:1 against the TS reference and -//! verified move-for-move by the (since retired) lockstep gates; the -//! JS-exact rng/float mirroring in here is frozen heritage from that -//! era, not an ongoing constraint. +//! This crate is the origin of the CPU stack. It began as a 1:1 port of a +//! since-deleted TS arena, verified move-for-move by the (retired) lockstep +//! gates; the JS-exact rng/float mirroring in here is frozen heritage from +//! that era, not an ongoing constraint. //! -//! The stacks are decoupled: Rust is the fast experimentation substrate -//! and may diverge freely; anything shipping to the game's CPU mode is -//! ported back to TS on its own terms (no bit-for-bit requirement). +//! Rust is the experimentation substrate and may diverge freely. Strategies +//! that ship to the game's CPU mode are hand-ported into munch +//! (`../munch/src/app/services/cpu/`) and gated on win-rate direction rather +//! than bit-identicality — see `sim-tests/arena/cpu-reference.json` there. //! //! Seat convention: strategies are written as if the CPU is p1 and the //! opponent p0 (inherited from the on-chain CPUs). The p0 seat plays diff --git a/transpiler/strategies-rs/src/native.rs b/transpiler/strategies-rs/src/native.rs index f5ed5ee2..cc22a25f 100644 --- a/transpiler/strategies-rs/src/native.rs +++ b/transpiler/strategies-rs/src/native.rs @@ -1,8 +1,7 @@ -//! Port of the TS-native shared helpers hard/greedy call -//! (`sims/src/cpu/heuristic-native.ts`): band picks, anti-wall pivot, and -//! the fork-based measurement/scoring kit. Fair-info pool analysis -//! (maxPoolDamage / evaluateDefensiveSwitchFair / …) is not ported — the -//! two shipped strategies never reach it. +//! Shared helpers hard/greedy call: band picks, anti-wall pivot, and the +//! fork-based measurement/scoring kit. Fair-info pool analysis +//! (maxPoolDamage / evaluateDefensiveSwitchFair / …) is deliberately +//! absent — the two shipped strategies never reach it. use chomp_engine::moves::MoveSlotLib; use chomp_engine::Structs::MoveMeta; diff --git a/transpiler/strategies-rs/src/override_cpu.rs b/transpiler/strategies-rs/src/override_cpu.rs index a17d6948..9dedb99b 100644 --- a/transpiler/strategies-rs/src/override_cpu.rs +++ b/transpiler/strategies-rs/src/override_cpu.rs @@ -1,4 +1,4 @@ -//! OVERRIDE CPU — port of `sims/src/cpu/strategies/override-cpu.ts`: a +//! OVERRIDE CPU — a //! scripted-plan pilot wrapping the hard CPU. For the active mon it //! consults a per-mon script (ordered rules with `when` / max-uses //! gates); the first rule whose move is affordable this turn and whose diff --git a/transpiler/strategies-rs/src/roster.rs b/transpiler/strategies-rs/src/roster.rs index 389b67cd..4320dad3 100644 --- a/transpiler/strategies-rs/src/roster.rs +++ b/transpiler/strategies-rs/src/roster.rs @@ -1,6 +1,6 @@ //! Standalone roster loader for the pure-Rust arena — replaces the TS team-builder the FFI arena //! fed in. Reads `drool/*.csv` + the per-move inline JSONs and resolves each mon's move catalog to -//! the packed engine words, a faithful port of `sims/src/arena/team.ts` + `mon-builder.ts` + +//! the packed engine words, mirroring `sims/src/util/mon-builder.ts` + //! `processing/packMoves.py`. Addresses are a deterministic name→address map (the arena only needs //! deploy_all and the move/ability words to agree; the specific values are irrelevant). diff --git a/transpiler/strategies-rs/src/shared.rs b/transpiler/strategies-rs/src/shared.rs index 39bebb5e..459ec9f6 100644 --- a/transpiler/strategies-rs/src/shared.rs +++ b/transpiler/strategies-rs/src/shared.rs @@ -1,10 +1,9 @@ -//! Port of the `HeuristicCPUBase.sol` helpers hard/greedy actually reach -//! (`sims/src/cpu/heuristic-shared.ts`). TS helpers neither strategy calls -//! (mode ladder, fair-info pool analysis, random-option pick, …) are -//! deliberately not ported. Every branch/threshold/formula matches the TS -//! source; integer math stays integral (i64), `Math.floor(int/int)` sites -//! reproduce the JS f64 division + floor (identical results at these -//! magnitudes, and bit-exact by construction). +//! Shared scoring primitives hard/greedy actually reach: damage estimation, +//! KO / best-damage move finders, lead and switch selection. Helpers neither +//! strategy calls (the old mode ladder, fair-info pool analysis, +//! random-option pick, …) are deliberately absent. Integer math stays +//! integral (i64); `Math.floor(int/int)` sites reproduce the JS f64 division +//! + floor (identical results at these magnitudes). use chomp_engine::moves::{AttackCalculator, MoveSlotLib}; use chomp_engine::Enums::{MonStateIndexName, MoveClass, Type}; diff --git a/transpiler/strategies-rs/src/view.rs b/transpiler/strategies-rs/src/view.rs index 2fd61c3b..7a9e036b 100644 --- a/transpiler/strategies-rs/src/view.rs +++ b/transpiler/strategies-rs/src/view.rs @@ -1,8 +1,7 @@ //! Seat translation + engine-view readers + candidate enumeration. //! -//! Port of `sims/src/cpu/engine-view.ts` and `sims/src/cpu/battle-view.ts`, -//! with the seat transposition of `sims/src/arena/transpose.ts` folded into -//! a [`Seat`] value instead of a JS Proxy. +//! The seat transposition is folded into a [`Seat`] value rather than a +//! proxy object wrapping the engine. //! //! Convention (inherited from the on-chain CPUs): strategy code sees the //! CPU as p1 and the opponent as p0 — VIRTUAL indices. `Seat` maps virtual From c888d91241ac03aafadf93d4834de81c61032699 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:49:55 -0700 Subject: [PATCH 25/41] transpiler: skip test-only PackedHeaderFrameLib prototype Its only importer is its own Foundry test, so transpiling it ships dead code into munch's production sim mirror. Same rationale as the other skipFiles entries. Co-Authored-By: Claude Opus 5 (1M context) --- transpiler/transpiler-config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/transpiler/transpiler-config.json b/transpiler/transpiler-config.json index 0468cfda..9c3d4cd0 100644 --- a/transpiler/transpiler-config.json +++ b/transpiler/transpiler-config.json @@ -132,7 +132,8 @@ "rng/ICPURNG.sol", "rng/IGachaRNG.sol", "game-layer/ReturnerGift.sol", - "game-layer/SignedAssigner.sol" + "game-layer/SignedAssigner.sol", + "kernel/PackedHeaderFrameLib.sol" ], "skipDirs": [ "cpu" From 1f625726ceb8fb580d8c2ef8b3c980a5435a0a90 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:55:53 -0700 Subject: [PATCH 26/41] transpiler: resolve library constants against the class, not the singleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library `internal constant`s emit as `static readonly`, but a qualified cross-file reference (Lib.CONST) went through the module singleton like library functions do — instance access to a static member, which TS rejects (TS2576). Track per-contract constants in the registry and emit class-qualified access, pulling the class into the import alongside the singleton. Surfaced by the new resolver/command libs (MoveCommandLib.OP_ATTACK, EffectCommandLib.OP_CLEAR_STATUS) and StatBoostLib.ACC_DISABLED_BIT. Co-Authored-By: Claude Opus 5 (1M context) --- transpiler/codegen/context.py | 6 ++++++ transpiler/codegen/expression.py | 6 +++++- transpiler/codegen/imports.py | 6 +++++- transpiler/type_system/registry.py | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/transpiler/codegen/context.py b/transpiler/codegen/context.py index 51aecdbd..f8d26ab9 100644 --- a/transpiler/codegen/context.py +++ b/transpiler/codegen/context.py @@ -65,6 +65,9 @@ class CodeGenerationContext: # Import tracking base_contracts_needed: Set[str] = field(default_factory=set) libraries_referenced: Set[str] = field(default_factory=set) + # Libraries referenced for a `static readonly` constant, which resolves against the + # class rather than the singleton — so the import needs the class name too. + library_classes_referenced: Set[str] = field(default_factory=set) contracts_referenced: Set[str] = field(default_factory=set) set_types_used: Set[str] = field(default_factory=set) external_structs_used: Dict[str, str] = field(default_factory=dict) @@ -91,6 +94,7 @@ class CodeGenerationContext: known_libraries: Set[str] = field(default_factory=set) known_contract_methods: Dict[str, Set[str]] = field(default_factory=dict) known_contract_vars: Dict[str, Set[str]] = field(default_factory=dict) + known_contract_constants: Dict[str, Set[str]] = field(default_factory=dict) known_public_state_vars: Set[str] = field(default_factory=set) known_public_mappings: Set[str] = field(default_factory=set) # Public mappings needing getter methods known_method_return_types: Dict[str, Dict[str, str]] = field(default_factory=dict) @@ -138,6 +142,7 @@ def reset_for_file(self) -> None: """Reset state for a new file.""" self.base_contracts_needed = set() self.libraries_referenced = set() + self.library_classes_referenced = set() self.contracts_referenced = set() self.set_types_used = set() self.external_structs_used = {} @@ -205,6 +210,7 @@ def from_registry( ctx.known_libraries = registry.libraries ctx.known_contract_methods = registry.contract_methods ctx.known_contract_vars = registry.contract_vars + ctx.known_contract_constants = registry.contract_constants ctx.known_public_state_vars = registry.known_public_state_vars ctx.known_public_mappings = registry.known_public_mappings ctx.known_method_return_types = registry.method_return_types diff --git a/transpiler/codegen/expression.py b/transpiler/codegen/expression.py index 49c2dacf..8bed47d1 100644 --- a/transpiler/codegen/expression.py +++ b/transpiler/codegen/expression.py @@ -643,8 +643,12 @@ def generate_member_access(self, access: MemberAccess) -> str: self._ctx.libraries_referenced.add(access.expression.name) elif access.expression.name in self._ctx.known_libraries: self._ctx.libraries_referenced.add(access.expression.name) - # Use the module-level singleton instance for library calls lib_name = access.expression.name + # Constants emit as `static readonly`, so they resolve against the class; + # everything else goes through the module-level singleton instance. + if member in self._ctx.known_contract_constants.get(lib_name, ()): + self._ctx.library_classes_referenced.add(lib_name) + return f'{lib_name}.{member}' singleton_name = lib_name[0].lower() + lib_name[1:] return f'{singleton_name}.{member}' diff --git a/transpiler/codegen/imports.py b/transpiler/codegen/imports.py index 604652ee..e99014b6 100644 --- a/transpiler/codegen/imports.py +++ b/transpiler/codegen/imports.py @@ -141,7 +141,11 @@ def _generate_library_imports(self) -> List[str]: continue import_path = self._get_relative_import_path(library) singleton_name = library[0].lower() + library[1:] - lines.append(f"import {{ {singleton_name} }} from '{import_path}';") + names = [singleton_name] + # A `static readonly` constant resolves against the class, so pull that in too. + if library in self._ctx.library_classes_referenced: + names.append(library) + lines.append(f"import {{ {', '.join(sorted(names))} }} from '{import_path}';") return lines def _generate_contract_type_imports(self, contract_name: str) -> List[str]: diff --git a/transpiler/type_system/registry.py b/transpiler/type_system/registry.py index c9c26f1b..f202c966 100644 --- a/transpiler/type_system/registry.py +++ b/transpiler/type_system/registry.py @@ -36,6 +36,10 @@ def __init__(self): self.interfaces: Set[str] = set() self.contracts: Set[str] = set() self.libraries: Set[str] = set() + # Per-contract constant members. Library constants emit as `static readonly`, so a + # qualified reference must resolve against the class, not the module singleton that + # library *functions* use. + self.contract_constants: Dict[str, Set[str]] = {} self.contract_methods: Dict[str, Set[str]] = {} self.contract_vars: Dict[str, Set[str]] = {} self.known_public_state_vars: Set[str] = set() @@ -186,10 +190,12 @@ def discover_from_ast(self, ast: 'SourceUnit', rel_path: Optional[str] = None) - # State variables state_vars = set() + const_vars = set() for var in contract.state_variables: state_vars.add(var.name) if var.mutability == 'constant': self.constants.add(var.name) + const_vars.add(var.name) self._record_constant_value(var) if var.visibility == 'public' and var.mutability not in ('constant', 'immutable'): self.known_public_state_vars.add(var.name) @@ -198,6 +204,8 @@ def discover_from_ast(self, ast: 'SourceUnit', rel_path: Optional[str] = None) - self.known_public_mappings.add(var.name) if state_vars: self.contract_vars[name] = state_vars + if const_vars: + self.contract_constants[name] = const_vars def merge(self, other: 'TypeRegistry') -> None: """Merge another registry into this one.""" @@ -221,6 +229,12 @@ def merge(self, other: 'TypeRegistry') -> None: else: self.contract_vars[name] = vars.copy() + for name, consts in other.contract_constants.items(): + if name in self.contract_constants: + self.contract_constants[name].update(consts) + else: + self.contract_constants[name] = consts.copy() + self.known_public_state_vars.update(other.known_public_state_vars) self.known_public_mappings.update(other.known_public_mappings) From 19602796a841e65425d7b9be59a15ae06bdec2fd Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:04:36 -0700 Subject: [PATCH 27/41] transpiler: fix `.slot` key helper colliding with contract methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yul `x.slot` emitted `this._getStorageKey(x)`, a helper that was never defined in the runtime. Engine happens to define its own unrelated `_getStorageKey(bytes32)`, so the call silently bound to that and passed a struct where a battleKey was expected — JSON.stringify then crashed on the struct's BigInt fields. Latent until now: the only prior `.slot` sites assigned to an unused variable. The branch's packed EffectInstance header reads and writes through it for real. Implement `_yulSlotKeyOf` in the runtime, keyed by an identity symbol so a struct keys the same whether or not a capture observer has proxied it. Co-Authored-By: Claude Opus 5 (1M context) --- transpiler/codegen/yul.py | 10 +++++----- transpiler/runtime/base.ts | 30 ++++++++++++++++++++++++++++++ transpiler/test_transpiler.py | 2 +- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/transpiler/codegen/yul.py b/transpiler/codegen/yul.py index 23c8b253..b32ed466 100644 --- a/transpiler/codegen/yul.py +++ b/transpiler/codegen/yul.py @@ -693,7 +693,7 @@ def _emit_slot_read(self, target) -> Optional[str]: # Bits no member declares still need somewhere to live; the shadow # slot store keeps carrying exactly those. terms.append( - f'(this._storageRead(this._getStorageKey({name} as any)) ' + f'(this._storageRead(this._yulSlotKeyOf({name} as any)) ' f'& {hex(layout.free_mask)}n)' ) return '(' + ' | '.join(terms) + ')' if terms else '0n' @@ -710,7 +710,7 @@ def _emit_slot_write(self, target, value: str, prefix: str) -> Optional[str]: lines.append(f'{prefix}{name}.{f.name} = {val};') if layout.has_free_bits: lines.append( - f'{prefix}this._storageWrite(this._getStorageKey({name} as any), ' + f'{prefix}this._storageWrite(this._yulSlotKeyOf({name} as any), ' f'{tmp} & {hex(layout.free_mask)}n);' ) return '\n'.join(lines) @@ -849,7 +849,7 @@ def _generate_let( if isinstance(stmt.value, YulSlotAccess): storage_var = stmt.value.variable slot_vars[stmt.name] = storage_var - return f'{prefix}const {stmt.name} = this._getStorageKey({storage_var} as any);' + return f'{prefix}const {stmt.name} = this._yulSlotKeyOf({storage_var} as any);' ts_expr = self._generate_expression(stmt.value, slot_vars) return f'{prefix}let {stmt.name} = {ts_expr};' @@ -867,7 +867,7 @@ def _generate_assignment( if isinstance(stmt.value, YulSlotAccess): storage_var = stmt.value.variable slot_vars[stmt.name] = storage_var - return f'{prefix}{stmt.name} = this._getStorageKey({storage_var} as any);' + return f'{prefix}{stmt.name} = this._yulSlotKeyOf({storage_var} as any);' ts_expr = self._generate_expression(stmt.value, slot_vars) return f'{prefix}{stmt.name} = {ts_expr};' @@ -1055,7 +1055,7 @@ def _generate_expression( elif isinstance(expr, YulFunctionCall): return self._generate_function_call(expr, slot_vars) elif isinstance(expr, YulSlotAccess): - return f'this._getStorageKey({expr.variable} as any)' + return f'this._yulSlotKeyOf({expr.variable} as any)' elif isinstance(expr, YulOffsetAccess): return '0n // .offset' return '0n' diff --git a/transpiler/runtime/base.ts b/transpiler/runtime/base.ts index 41988523..6ae8401d 100644 --- a/transpiler/runtime/base.ts +++ b/transpiler/runtime/base.ts @@ -862,4 +862,34 @@ export abstract class Contract { protected _storageWrite(key: any, value: bigint): void { this._storage.sstore(this._yulStorageKey(key), value); } + + /** + * Stable synthetic slot key for a storage struct (Solidity `x.slot`). + * + * Solidity packs several struct fields into one word, so a slot-level read or + * write also touches bits that no modelled property owns; those live in raw + * storage under this key. Identity is stamped on the object with a symbol, which + * the state-tracking proxy passes straight through to the raw target — so the + * same struct keys identically whether or not a capture observer is attached. + * + * Named `_yul*` rather than the more natural `_getStorageKey` because contracts + * share this class namespace: Engine defines its own `_getStorageKey(bytes32)`, + * and the emitted call bound to that instead of erroring. + */ + protected _yulSlotKeyOf(ref: any): string { + if (ref === null || ref === undefined || typeof ref !== 'object') { + return this._yulStorageKey(ref); + } + let id = (ref as any)[YUL_SLOT_ID]; + if (id === undefined) { + id = `yulslot#${_nextYulSlotId++}`; + Object.defineProperty(ref, YUL_SLOT_ID, { value: id, enumerable: false }); + } + return id; + } } + +/** Identity stamp for `_yulSlotKeyOf`. A symbol so it never collides with a + * Solidity field name and is skipped by the proxy's state-change recording. */ +const YUL_SLOT_ID = Symbol('yulSlotId'); +let _nextYulSlotId = 0; diff --git a/transpiler/test_transpiler.py b/transpiler/test_transpiler.py index 343c962b..493746cf 100644 --- a/transpiler/test_transpiler.py +++ b/transpiler/test_transpiler.py @@ -266,7 +266,7 @@ def test_simple_sload_sstore(self): } ''' result = self.transpiler.transpile(yul_code) - self.assertIn('_getStorageKey(myVar', result) + self.assertIn('_yulSlotKeyOf(myVar', result) self.assertIn('_storageRead(myVar', result) self.assertIn('_storageWrite(myVar', result) From 36172eb38f54d63ff5f2bc190fbc11230b0990fb Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:23:40 -0700 Subject: [PATCH 28/41] transpiler: tighten _yulSlotKeyOf comments Co-Authored-By: Claude Opus 5 (1M context) --- transpiler/runtime/base.ts | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/transpiler/runtime/base.ts b/transpiler/runtime/base.ts index 6ae8401d..9d582ecc 100644 --- a/transpiler/runtime/base.ts +++ b/transpiler/runtime/base.ts @@ -863,19 +863,10 @@ export abstract class Contract { this._storage.sstore(this._yulStorageKey(key), value); } - /** - * Stable synthetic slot key for a storage struct (Solidity `x.slot`). - * - * Solidity packs several struct fields into one word, so a slot-level read or - * write also touches bits that no modelled property owns; those live in raw - * storage under this key. Identity is stamped on the object with a symbol, which - * the state-tracking proxy passes straight through to the raw target — so the - * same struct keys identically whether or not a capture observer is attached. - * - * Named `_yul*` rather than the more natural `_getStorageKey` because contracts - * share this class namespace: Engine defines its own `_getStorageKey(bytes32)`, - * and the emitted call bound to that instead of erroring. - */ + /** Stable synthetic slot key for a storage struct (Solidity `x.slot`), holding the + * packed bits no modelled property owns. Keyed by an identity symbol so the + * state-tracking proxy and its raw target resolve alike. The `_yul` prefix keeps it + * out of the namespace contracts' own methods share. */ protected _yulSlotKeyOf(ref: any): string { if (ref === null || ref === undefined || typeof ref !== 'object') { return this._yulStorageKey(ref); @@ -889,7 +880,6 @@ export abstract class Contract { } } -/** Identity stamp for `_yulSlotKeyOf`. A symbol so it never collides with a - * Solidity field name and is skipped by the proxy's state-change recording. */ +/** Identity stamp for `_yulSlotKeyOf` — a symbol, so it can't collide with a field name. */ const YUL_SLOT_ID = Symbol('yulSlotId'); let _nextYulSlotId = 0; From 390f828efa5930e8845a259a795f85f54a222001 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:36:12 -0700 Subject: [PATCH 29/41] transpiler: simplify library-class import join, tighten comment Co-Authored-By: Claude Opus 5 (1M context) --- transpiler/codegen/imports.py | 7 ++++--- transpiler/runtime/base.ts | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/transpiler/codegen/imports.py b/transpiler/codegen/imports.py index e99014b6..ece82133 100644 --- a/transpiler/codegen/imports.py +++ b/transpiler/codegen/imports.py @@ -141,11 +141,12 @@ def _generate_library_imports(self) -> List[str]: continue import_path = self._get_relative_import_path(library) singleton_name = library[0].lower() + library[1:] - names = [singleton_name] # A `static readonly` constant resolves against the class, so pull that in too. if library in self._ctx.library_classes_referenced: - names.append(library) - lines.append(f"import {{ {', '.join(sorted(names))} }} from '{import_path}';") + imported = f'{library}, {singleton_name}' + else: + imported = singleton_name + lines.append(f"import {{ {imported} }} from '{import_path}';") return lines def _generate_contract_type_imports(self, contract_name: str) -> List[str]: diff --git a/transpiler/runtime/base.ts b/transpiler/runtime/base.ts index 9d582ecc..99a8bbf5 100644 --- a/transpiler/runtime/base.ts +++ b/transpiler/runtime/base.ts @@ -863,10 +863,9 @@ export abstract class Contract { this._storage.sstore(this._yulStorageKey(key), value); } - /** Stable synthetic slot key for a storage struct (Solidity `x.slot`), holding the - * packed bits no modelled property owns. Keyed by an identity symbol so the - * state-tracking proxy and its raw target resolve alike. The `_yul` prefix keeps it - * out of the namespace contracts' own methods share. */ + /** Stable synthetic slot key for a storage struct (Solidity `x.slot`), holding the packed + * bits no modelled property owns. Keyed by an identity symbol so a proxied struct and its + * raw target agree. `_yul`-prefixed to stay clear of contract method names. */ protected _yulSlotKeyOf(ref: any): string { if (ref === null || ref === undefined || typeof ref !== 'object') { return this._yulStorageKey(ref); From 115a38456c4d7382c515e1e536d33b81eb5be82f Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:03:40 -0700 Subject: [PATCH 30/41] clean up --- deployments.json | 156 ++--- docs/DOUBLES_KIT_RULINGS.md | 49 -- docs/mons.md | 25 + src/hooks/SimplePM.sol | 6 + test/SimplePMTest.sol | 52 +- test/mocks/MockSimplePMEngine.sol | 6 +- transpiler/runtime/base.ts | 14 + transpiler/strategies-rs/src/bin/replay.rs | 52 ++ .../strategies-rs/src/bin/sparsecost.rs | 99 +++ transpiler/strategies-rs/src/bots.rs | 47 +- transpiler/strategies-rs/src/doubles.rs | 270 +++++++- transpiler/strategies-rs/src/evaluator.rs | 45 +- transpiler/strategies-rs/src/heuristic.rs | 2 +- transpiler/strategies-rs/src/lib.rs | 1 + transpiler/strategies-rs/src/replay.rs | 587 ++++++++++++++++++ 15 files changed, 1251 insertions(+), 160 deletions(-) delete mode 100644 docs/DOUBLES_KIT_RULINGS.md create mode 100644 docs/mons.md create mode 100644 transpiler/strategies-rs/src/bin/replay.rs create mode 100644 transpiler/strategies-rs/src/bin/sparsecost.rs create mode 100644 transpiler/strategies-rs/src/replay.rs diff --git a/deployments.json b/deployments.json index 2a35808e..307f8b36 100644 --- a/deployments.json +++ b/deployments.json @@ -99,101 +99,101 @@ "ZAP_STATUS": "0xaafbb5b230d6e5f16d4c84183d09975f02c7ba9f" }, "TESTNET": { - "ACTUS_REUS": "0x0100000000000000000000002a2d203e3d457f4c8959d569fe126e8b6c35119b", - "ADAPTOR": "0x0100000000000000000000008d5c048af8a031452ad8007ecf2937203315bd0f", - "ANGERY": "0x01000000000000000000000007890c35e289ca758da4273f39cf820ea4370fd3", - "BASELIGHT": "0x045ed65d3cbb62c1c2509ae1725342eef1c8bf54", + "ACTUS_REUS": "0x0100000000000000000000003602ebd12f524603173bd1dceb7b0b1ccbe69d41", + "ADAPTOR": "0x010000000000000000000000a304a56d906d7995b611590aa60261507b79659d", + "ANGERY": "0x01000000000000000000000079787dbd9e974f5dfc95407a856602f50ea9447b", + "BASELIGHT": "0x52f204cceb27d2976abcada3a83863d1c40622e8", "BATTLE_HISTORY": "0xa4f90f3991f2f53ba77a417b9fc094a545fba8d3", - "BIG_BELLOW": "0x50e1799a568c7051b7581534980b2d44312fb756", + "BIG_BELLOW": "0xfa647f6b0741a2ff70825eb5149a81e00768d9db", "BIG_BITE": "0x5009200000000000000000000000000000000000000000000000000000000000", - "BLESSED_STATUS": "0xe342d837548c25fc8a617729e538491ce7e89781", + "BLESSED_STATUS": "0xe87ba41355b7e7e6884f9e72bcd5a67675175ddd", "BLOW": "0x460a200000000000000000000000000000000000000000000000000000000000", - "BRIGHTBACK": "0x529abcd5078040f898665c5b3af81d62328b7d54", - "BUBBLE_BOP": "0xe2cb7c29fa1f9d9b9546e26c1655fef2f6d2fbeb", - "BULL_RUSH": "0xae7a223b2ed2defe07f23b703063541377c241a6", - "BURN_STATUS": "0x188a2abca245ce1c7b29cd023d64dd8c7a56d302", - "CARROT_HARVEST": "0x010000000000000000000000d70f63f4e58d42335130eeac2fb408e17c392d9e", - "CHAIN_EXPANSION": "0xad7414dd15bac3da5e58382648f2179b9ed97763", - "CHILL_OUT": "0x00c606400000000000000000a6dc7cfdf53c1bd059c279ba9308459bb1149eeb", - "CHRONOFFENSE": "0x045a102d070e85aa6015326d5305135d23a84916", - "COMMIT_MANAGER": "0xe7f122a0bb1ab051f1b09cd7162dfb1e4cf76ef8", - "CONTAGIOUS_SLUMBER": "0xcec6beee3a908adbf1548c4e3eac19c85f37bb92", - "CPU": "0x7a29773cd412de704cafb4eef54848c6c808c0c7", - "DEADLIFT": "0x0fd1892a94e15ba1efcd430f81c2c8d34f3bf04b", - "DEEP_FREEZE": "0xa3b1b68e62114d2bf11402c37c8559ded95f4783", + "BRIGHTBACK": "0xa76e273e65e04beaca5c2cf20b7fb102fec0c310", + "BUBBLE_BOP": "0x08965095c4d0b3420f8166fba88caae9d7e49761", + "BULL_RUSH": "0x8b8c122ab3364ccd5471ebea5d94ec1ab4f609b4", + "BURN_STATUS": "0x58321098ce8d5b8ccab6ad123ac601cb5abb6385", + "CARROT_HARVEST": "0x0100000000000000000000008c631b4dd4ca711d59feaf26494a199d6ba3a82f", + "CHAIN_EXPANSION": "0x47abfc20669a5f7d766ee804f12c00715bd7f705", + "CHILL_OUT": "0x00c60640000000000000000002cad8f1e14f3b51557b96fab6c95166d6539cb3", + "CHRONOFFENSE": "0xc37db0bbca280f1b85e6b5c8a5a413bd8c5ba50e", + "COMMIT_MANAGER": "0xdd0fcf39c650ac05a755bb558c8191bfede3cadb", + "CONTAGIOUS_SLUMBER": "0x396b50afca9619b271a2be8447e0ae4f849252dd", + "CPU": "0x3d3cbc6bdf8ba435930002679451c422b2673e9f", + "DEADLIFT": "0xfcf4e328efeac7984945d123b0241a04b0995192", + "DEEP_FREEZE": "0x310d0dd5003c9aae5356ba7b8fa388af76338291", "DEFAULT_MATCHMAKER": "0xd89a64a438b913d4cc47a61576d27877bad7f6d6", "DEFAULT_MON_REGISTRY": "0xef5fa1a5e99184df3a379992f6c7e9eee0ef7a74", "DEFAULT_RANDOMNESS_ORACLE": "0x1c9d9769d40eba316009660f2d74b8427843cdf9", "DEFAULT_RULESET": "0xa5e678884033b94509287500a0b8e6da04a8102f", "DEFAULT_VALIDATOR": "0xaa122ac35a19eeded0cfea8e2ddeab674e0f2fe3", - "DREAMCATCHER": "0x01000000000000000000000027f5bd2cf0f70e5500d18c034e88f1f8105ba09c", - "DUAL_SHOCK": "0xfc21822c443ed7ba7198304d26b782aeb0506684", - "ELECTROCUTE": "0x5a4820a00000000000000000eeec04a361bbff7459c3e784ba143de0d1646e18", - "ENGINE": "0xb7a070924138138b4a3d91896b8a7b340c50d576", - "ETERNAL_GRUDGE": "0x7de0b52d87e026c755225c9e8744579c228bdcce", + "DREAMCATCHER": "0x010000000000000000000000f82e6dd22620dac253cdc3bbbfb2be7059c23a06", + "DUAL_SHOCK": "0x1d1665f2fc88f5990bfe29880afbc6637a55791f", + "ELECTROCUTE": "0x5a4820a000000000000000006918ee05b14836938811f1fb4a3415506f8db303", + "ENGINE": "0xe86fe8d45259391dda86030804c97580608f546c", + "ETERNAL_GRUDGE": "0x01794f7b27feeb3b49a2c8e5f5a064da46469f3c", "FAIR_CPU": "0xc02f904dd32d244d8e60e2a504429f3f4b091de4", "FEDERAL_INVESTIGATION": "0x644c300000000000000000000000000000000000000000000000000000000000", - "FOUL_LANGUAGE": "0xcd0c26782a02a3f4a53dd697974bca0b6a1c594f", - "FROSTBITE_STATUS": "0xa6dc7cfdf53c1bd059c279ba9308459bb1149eeb", - "GACHACHACHA": "0x84901ea4e408ac19a448511ab042608825577b79", - "GACHA_TEAM_REGISTRY": "0x430e9eea9f95bec5fc64c84a75c568dac2942b06", - "GILDED_RECOVERY": "0xcf3b2a96e4d0daa6f9ef8d7e8057cfd826607bfc", - "GRAVE_AFFLICTION": "0x5f2c89be105466613b24017895f2fa1f2dad6308", - "GUEST_FEATURE": "0x947e033440980cf53e5a4af3756dcc05c316b6de", - "HARD_RESET": "0x85320a95672eda36ff8e5297f69f47f9fae31740", - "HEAT_BEACON": "0x447eb72e7af844d0cf8d4e23177cbbd5adba5037", - "HIT_AND_DIP": "0x1cc10db18353cb23928226b8adad314072f5f805", - "HONEY_BRIBE": "0x74a33b4b0642031d19ad704ddfbfe302932fdd71", - "INFERNAL_FLAME": "0x62853b9df0e7ae8568eb27e701f86e545d66dde8", - "INFINITE_LOVE": "0x5a4d30a00000000000000000c24a15e9d4ca8537d8fb7c88f3f1554bab4625e2", - "INITIALIZE": "0xa127b097e492ad4816382ab551e60d32867eaff4", - "INTERWEAVING": "0x82f6fd492fa05cfdf4c9b573dd150cdde2a67ea1", - "INVOKE_TABOO": "0xf63bc397333284e23b4c12821b94a885d48dfb11", - "IRON_WALL": "0x0fcf5eb7640fa959aa54883c9d5975abce03ec27", - "LOOP": "0xc8d65c6d9e4a2f5562dff7e6e5abca85d3ba639d", - "MEGA_STAR_BLAST": "0xf21039330c97d5168e98733251f6de62450f20f7", - "MODAL_BOLT": "0x3344f9b1ca78d0205996da730ead93819c367340", - "NEGATIVE_THOUGHTS": "0x504b30a00000000000000000abe873b6daa3b7aed22d9d8bb26457bfd1d9bef1", + "FOUL_LANGUAGE": "0x0157b0ea79e2701b88b3db2345c54d054729128f", + "FROSTBITE_STATUS": "0x02cad8f1e14f3b51557b96fab6c95166d6539cb3", + "GACHACHACHA": "0x9ce4b1259a4fe74ceace8327ad3a44c8b0b9062f", + "GACHA_TEAM_REGISTRY": "0x7325848d05114861720b46065ae00607212a96dd", + "GILDED_RECOVERY": "0x21d234c2c4d600e60a23d3fb33fae569c9b99454", + "GRAVE_AFFLICTION": "0x30ed299574f60f142407a128b2784f2fd8c848ed", + "GUEST_FEATURE": "0x67760ca453f9ddb8c551f72b4cde01e18b2b2937", + "HARD_RESET": "0x0d25d29ae014b059900fd07c4257be0f70c1d339", + "HEAT_BEACON": "0x5beea17fcd5864e4da16e18fb7d23430520ff830", + "HIT_AND_DIP": "0x9f28661a4d2b10148dccba2df9c8ebfb7d679728", + "HONEY_BRIBE": "0x9d69d641af63f28585ea69fac472661545719749", + "INFERNAL_FLAME": "0xa0198c67b857d25d32703a3447fc2a5bc0b02c88", + "INFINITE_LOVE": "0x5a4d30a00000000000000000f875974407c361472cf6faa65a41109c9b1a7932", + "INITIALIZE": "0xf576c4364712cefc3748b3b2ea8c320ebe300465", + "INTERWEAVING": "0xa65a7a4ad29e28470e69ef63f143ed67b9a03a23", + "INVOKE_TABOO": "0x4688ccbdd762bfe96a7ad861c14149709e3550c7", + "IRON_WALL": "0x2bcbf07cd4b47026547bf0e6ffa5b89098e984c9", + "LOOP": "0x17f9c6a8e97279bd821c5501e62c21b876f60ddb", + "MEGA_STAR_BLAST": "0x3b54b488c4419de7fd365d1468bcc2a9cba5868e", + "MODAL_BOLT": "0xb9b249d98cf82a39111e2ceac16ac1f65cacd242", + "NEGATIVE_THOUGHTS": "0x504b30a00000000000000000117dc546461cb7cd6e675c8798c88d4b92680136", "NIGHT_TERRORS": "0xd9cc94ce09177091609222b02854f671deaa2579", - "NINE_NINE_NINE": "0x5a8cdd030409252962200f846ac43d62bfd809de", + "NINE_NINE_NINE": "0x3650a27e5c13516c55e5956a577457813ae9acce", "OKAY_CPU": "0xc14f1a8cdbf755055fee3576a6d06791f72232bf", - "OLD_VENGEANCE": "0x52ac2ec51a49d7f82a38ed3d98071c70dd1590d1", + "OLD_VENGEANCE": "0xacaf7b9794e84ce9d53390e0a0c676ad3d49bf7b", "OSTEOPOROSIS": "0x5a00200000000000000000000000000000000000000000000000000000000000", - "OVERCLOCK": "0x2392cf5a2e8b2aaeeb5e2b85d6ed140425c0f954", - "OVERFLOW": "0x225ff1b9a62a1ad02d8704640de06a07a4442cd5", - "PANIC_STATUS": "0xabe873b6daa3b7aed22d9d8bb26457bfd1d9bef1", - "PISTOL_SQUAT": "0x0227f275d2b554ccfab31fb414a9211e8d398d0b", - "POST_WORKOUT": "0x01000000000000000000000018a40f75856a92cb455604443994e71e122247a9", + "OVERCLOCK": "0x223ac7b0f7f8309e5a2496ff1835bb6d56c1b99a", + "OVERFLOW": "0x34e0126914e162fa929964030a5974a2d4752615", + "PANIC_STATUS": "0x117dc546461cb7cd6e675c8798c88d4b92680136", + "PISTOL_SQUAT": "0x49673a412eb0dbfd14497ff7e9f44e6422e00d88", + "POST_WORKOUT": "0x010000000000000000000000144c95356c72a3e4f4097406570d057d60643bdc", "POUND_GROUND": "0x5f02300000000000000000000000000000000000000000000000000000000000", - "PREEMPTIVE_SHOCK": "0x3bbd74d45dde74721a10cbceb50137a15598df5e", - "Q5": "0x741deae5c528bfaab410663eeb21580967c929ce", - "QUICKSTORM": "0xa6460c25579d9db5ab98ec6066a40da54a969828", - "RENORMALIZE": "0xd37f2202ce141564dd6f908b43eabd7e4bde2c21", - "RISE_FROM_THE_GRAVE": "0x5accf0e38946a03e1145df5e016936596da22637", - "ROCK_PULL": "0x79147160bffd1b5d9ee7b3c2dd62c56ec0380bca", - "ROUND_TRIP": "0x3fc12fb1da93b4089e4a3971f584cbb0b16b9db5", - "SANCTIFY": "0xbfdbea2a11ba12cea735b7e2203fcd6ef8640afe", - "SAVIOR_COMPLEX": "0x6f0e3b32d8ff3b1698334d89ab9a78b912417098", - "SCARY_NUMBERS": "0x500b31400000000000000000abe873b6daa3b7aed22d9d8bb26457bfd1d9bef1", - "SET_ABLAZE": "0x8bf53840474a64ff67f4c9c50fdee3be0bea6180", - "SIGNED_MATCHMAKER": "0xf95efaabed8b16703f29e8a48397de6480092024", - "SIMPLE_PM": "0x65b44cb3276deac4dfc5c9f76672743c5fbdcfc0", - "SLEEP_STATUS": "0xc24a15e9d4ca8537d8fb7c88f3f1554bab4625e2", - "SNACK_BREAK": "0x1e9311a3ebaa317980c23ebd23ae4f2374d2ead2", - "SNEAK_ATTACK": "0x4d2b2b6519d2a81cf8e22faf1fb1f8348859e344", - "SOMNIPHOBIA": "0x6fb2ac51e0aea5c003e50c900dd07ae035bcfb2f", + "PREEMPTIVE_SHOCK": "0xb5e2c7ff01142241f29c99a09f69236ee275df34", + "Q5": "0xea6ff6a8192ae96e45a684638ce4f06c26ede59f", + "QUICKSTORM": "0x249b5e1f7b6b0f56cbebeb2b8a514da53a011da5", + "RENORMALIZE": "0x39e25c283eebbe6ee38cd86bd9b66a1c0f8cc97e", + "RISE_FROM_THE_GRAVE": "0x5f3a9e5d89405f47395fde8f5595b938026a2126", + "ROCK_PULL": "0x9a0f40e664a5be89ac2b4dece6e073ad0eb97df3", + "ROUND_TRIP": "0x171eeb7dfe178e2fe0a1dae50560d6edd6822b0d", + "SANCTIFY": "0xe4770f5dc0c0cc9dc0ecaedc083d8c9d4d1a3fe5", + "SAVIOR_COMPLEX": "0xc70580dd51aebcfd849a0822a4ee47db280d6156", + "SCARY_NUMBERS": "0x500b31400000000000000000117dc546461cb7cd6e675c8798c88d4b92680136", + "SET_ABLAZE": "0x017da0197a98f518813527b7806b331945c12a41", + "SIGNED_MATCHMAKER": "0x02113d58473af8ea6a3ee8ac1c7226940fc9763a", + "SIMPLE_PM": "0x00cbb85ce12c955f197212d4c109b6a653629515", + "SLEEP_STATUS": "0xf875974407c361472cf6faa65a41109c9b1a7932", + "SNACK_BREAK": "0xea7402b23589ea218761ffe9b384781e68fc7249", + "SNEAK_ATTACK": "0x185af09e1d4c800d188b7697d8db1cd476c444bd", + "SOMNIPHOBIA": "0x93648083a1b991716ada1d3ea690baf79b0eff0b", "STAMINA_REGEN": "0xe9c82033011dedf20d2fe69920b5020403041d0f", "STAT_BOOSTS": "0x8c072f12345b3e95fa52fcdf74610c044c044940", "THROW_PEBBLE": "0x2802100000000000000000000000000000000000000000000000000000000000", - "TINDERCLAWS": "0x010000000000000000000000439538faceb6e57a32fe46664c6aa60cef2ef305", - "TRIPLE_THINK": "0xaa5c3974bb3f36bbb0145e05791479a73afaa4a5", - "TYPE_CALCULATOR": "0xd73c3e465e7a161847d17a88238248c46d31698f", - "UNBOUNDED_STRIKE": "0x01a1a99ba9619813bec739d3386dbce5d8b8c428", + "TINDERCLAWS": "0x010000000000000000000000a87843d449c2249efc18c69ee8b5e10c8ebbec7e", + "TRIPLE_THINK": "0x531768ec889323ce705432ce7c9200440a32bb7c", + "TYPE_CALCULATOR": "0x353b2b80a4921ae2f997a39054eb204492b02a7e", + "UNBOUNDED_STRIKE": "0x69ebae1865cd9698b0ce39707842d1080b23b191", "UNEXPECTED_CARROT": "0x7807400000000000000000000000000000000000000000000000000000000000", - "UP_ONLY": "0x0100000000000000000000004cb946f266461a016c1e723b1dbf64233335b8b1", - "VITAL_SIPHON": "0x4b0855c4e2db9c10429833b94de982cf5eefd151", - "VOLATILE_PUNCH": "0x44eca9fb22974c6a9d5fc085473e1e4945e477fe", - "WITHER_AWAY": "0x1db040db13cff8a40184290d226b636769779943", - "ZAP_STATUS": "0xeeec04a361bbff7459c3e784ba143de0d1646e18" + "UP_ONLY": "0x010000000000000000000000b3e89653a438dca5261747851095f3144af50de2", + "VITAL_SIPHON": "0x39df0d2fdad2d4de3af865cf54314734b17e7cf6", + "VOLATILE_PUNCH": "0x85b99ed2e044109815fb237d41fa968fb3c27811", + "WITHER_AWAY": "0x5581c5b6857b32befe865e62b52e031f957763fa", + "ZAP_STATUS": "0x6918ee05b14836938811f1fb4a3415506f8db303" } } diff --git a/docs/DOUBLES_KIT_RULINGS.md b/docs/DOUBLES_KIT_RULINGS.md deleted file mode 100644 index 5d914d95..00000000 --- a/docs/DOUBLES_KIT_RULINGS.md +++ /dev/null @@ -1,49 +0,0 @@ -# Doubles kit rulings (D23 audit) - -How every existing kit behaves in 2-slot battles, and the general rules the per-kit calls -derive from. Singles behavior is unchanged in every case (the rules all degenerate to "the -opposing active" when each side has one slot). - -## General rules - -- **Targeted moves follow their chosen slot.** A move's damage, applied status, and any - side-derived logic bind to `targetBits`' slot — including ally slots (D4). The defender's - *side* is derived from the slot, never hardcoded as "the opponent". -- **Mirror-slot rule** for effects/abilities that act on "the opposing active" with no target - context (switch-in chips, KO-triggered debuffs, per-turn punishments): the effect lands on - the slot directly opposite its owner's slot, falling back to the other opposing slot when the - mirror lane is vacant, and fizzling when the opposing side is empty. Occupancy only — a KO'd - occupant is still selected (the action then no-ops). -- **"Both actives" moves become caster + chosen target** (not all four slots): Contagious - Slumber, Honey Bribe, Grave Affliction keep their singles cost/benefit shape. -- **Move-decision reads are per-slot.** Effects observing a mon's committed move resolve the - mon's slot (`TargetLib.slotOfMon`) and read `getMoveDecisionForSlot`; a benched observer - treats the read as non-matching. -- **Pivot moves address their slot.** Self-switches vacate the caster's slot; forced switches - vacate the targeted slot (`switchActiveMonForSlot`). A random replacement pick that collides - with the ally slot's occupant no-ops (the pivot fizzles). -- **Side-scoped resources stay side-scoped** (D11, accepted stacking surface): one sleeper per - side, Chain Expansion's shared charges, Nine Nine Nine's crit prime, Somniphobia's tax. - -## Per-kit calls - -| Kit | Ruling | -|---|---| -| Eternal Grudge | Debuff follows the chosen target slot (may be the ally, D4). | -| Contagious Slumber / Honey Bribe / Grave Affliction | Caster + chosen target slot. | -| Q5 | Armed against the slot the cast aimed at; detonates on that slot's occupant at D-day (slot-bound, D3 — a switch redirects onto the replacement). Countdown ticks only on full turns (round effects don't run on switch-only turns, matching singles). | -| Heat Beacon / Deep Freeze / Vital Siphon / Volatile Punch / Mega Star Blast / Gachachacha (KO bands) / Invoke Taboo (brand) | Targeted-slot rule (mechanical Pattern A). | -| Interweaving | Swap-in and swap-out debuffs land on Inutia's mirror slot. | -| Preemptive Shock | Unchanged (dispatch resolves the implied singles bit in singles; in doubles it chips the target resolved by its dispatch call — slot 0 of the opposing side; acceptable until a kit pass gives it a chosen target). | -| Actus Reus | Arms when EITHER opposing occupant is KO'd after Malalien's move; the death-trigger speed halving lands on Malalien's mirror slot. | -| Old Vengeance | Each tick lands on Xmon's mirror slot (re-evaluated per round end). | -| Rock Pull | The punisher *stance* (priority bump) arms if either opposing slot committed a switch (priority() has no target context); the punish damage still requires the *targeted* slot to be switching, else the usual self-hit. | -| Hard Reset | Rest detection is per-slot (the Resting mon's own committed move); the forced swap vacates that mon's slot. | -| Tinderclaws | Rest detection reads Embursa's own slot. | -| Sleep / Zap | The move rewrite / skip check binds to the sleeper's own slot; a benched victim is untouched that round. "Has the victim acted" is the engine's per-slot acted mask, not a priority compare. Sleep's one-sleeper gate stays per-SIDE in all modes (in Multi one seat's sleeper blocks Sleep on the partner seat's mons — accepted). | -| Overclock / Dual Shock | Boost applies to every occupied lane of the summoner's side and is removed from both at expiry (side-scoped by design, D11). | -| Pivots (Hit And Dip, Round Trip, Pistol Squat, Hard Reset) | Slot-addressed switches per the pivot rule above. | -| Sneak Attack / Guest Feature / Gilded Recovery / Savior Complex | Roster-index domains unchanged in Doubles (own/opposing team is the side roster). Multi's per-seat quarters are a validation concern for the Multi phase. | -| Self-scoped kits (Baselight line, Iron Wall, Up Only, Dreamcatcher, Chronoffense, Rise From The Grave, Triple Think, Angery, Snack Break, Carrot Harvest...) | No ruling needed; ally hits feeding them (D4) work through the normal per-mon hooks. | -| Adaptor | Latches the first damage source as before — an ally's move can be the latched source (D4, accepted). | -| External StaminaRegen effect | Singles test scaffolding only; prod inline regen is slot-aware. | diff --git a/docs/mons.md b/docs/mons.md new file mode 100644 index 00000000..5de855ef --- /dev/null +++ b/docs/mons.md @@ -0,0 +1,25 @@ +# Glob +Liquid / Faith + +Physical attacker, low health, medium defense + +Ability: + +Holy Molt ++1 priority, 0 stamina, halves health, applies Bless on self +costs 1 stamina more per time used, up to max of 3 + +Water Claw +Deals damage + +Truth Claw + +??? + + +# Mulong +Air / Lightning + +Special attacker, high health, low speed + +Ability: Sky Ruler \ No newline at end of file diff --git a/src/hooks/SimplePM.sol b/src/hooks/SimplePM.sol index 3fd966fa..b9048aa1 100644 --- a/src/hooks/SimplePM.sol +++ b/src/hooks/SimplePM.sol @@ -17,6 +17,7 @@ struct PMBalance { contract SimplePM { error TooLate(uint256 turnId); + error GameOver(bytes32 battleKey); error GameNotOver(bytes32 battleKey); error InvalidBattle(bytes32 battleKey); @@ -49,6 +50,11 @@ contract SimplePM { if (ctx.startTimestamp == 0) { revert InvalidBattle(battleKey); } + // A settled battle's outcome is public, so a late bet on the winner would redeem the losing + // side's deposits risk-free. winnerIndex 2 = no winner; checked off the context we already hold. + if (ctx.winnerIndex != 2) { + revert GameOver(battleKey); + } uint256 turnId = uint256(ctx.turnId) + _bufferedTurnCount(battleKey); if (turnId > LAST_TURN_TO_JOIN) { revert TooLate(turnId); diff --git a/test/SimplePMTest.sol b/test/SimplePMTest.sol index dd43b81b..6b985224 100644 --- a/test/SimplePMTest.sol +++ b/test/SimplePMTest.sol @@ -315,6 +315,43 @@ contract SimplePMTest is Test { assertEq(p0Shares, 0.7 ether); } + function test_T2_7_RevertOnceWinnerIsSet() public { + mockEngine.setWinner(BATTLE_KEY, P0); + vm.prank(ALICE); + vm.expectRevert(abi.encodeWithSelector(SimplePM.GameOver.selector, BATTLE_KEY)); + pm.buyShares{value: 1 ether}(BATTLE_KEY, true); + } + + function test_T2_8_RevertOnceWinnerIsSetWhenP1Won() public { + // winnerIndex 1 — a guard written against index 0 alone would let this one through. + mockEngine.setWinner(BATTLE_KEY, P1); + vm.prank(ALICE); + vm.expectRevert(abi.encodeWithSelector(SimplePM.GameOver.selector, BATTLE_KEY)); + pm.buyShares{value: 1 ether}(BATTLE_KEY, false); + } + + // The exploit the settle guard closes: a battle that ends INSIDE the betting window leaves the + // turn cutoff satisfied, so betting the now-known winner would redeem the losers' pool risk-free. + function test_T2_9_NoRiskFreeBetAfterSettleInsideWindow() public { + mockEngine.setTurnId(BATTLE_KEY, 3); + vm.prank(ALICE); + pm.buyShares{value: 1 ether}(BATTLE_KEY, false); + + mockEngine.setWinner(BATTLE_KEY, P0); + + vm.prank(BOB); + vm.expectRevert(abi.encodeWithSelector(SimplePM.GameOver.selector, BATTLE_KEY)); + pm.buyShares{value: 10 ether}(BATTLE_KEY, true); + + (uint96 p0Shares, uint96 p1Shares, uint96 totalDeposits) = pm.marketForBattle(BATTLE_KEY); + assertEq(p0Shares, 0, "no winner-side shares minted after settle"); + assertEq(p1Shares, 0.94 ether, "turn-3 fee on the pre-settle bet only"); + assertEq(totalDeposits, 1 ether, "pool untouched by the post-settle bet"); + + (uint128 bobP0,) = pm.sharesPerUserForBattle(BATTLE_KEY, BOB); + assertEq(bobP0, 0); + } + // ========================================================================= // Group 3: claimShares — Happy Path // ========================================================================= @@ -663,21 +700,24 @@ contract SimplePMTest is Test { assertEq(payout, 2 ether); } - function test_T6_2_ReentrantBuySharesDuringClaim() public { + function test_T6_2_ReentrantBuySharesDuringClaimIsRejected() public { ReentrantBuyer attacker = new ReentrantBuyer(pm); vm.deal(address(attacker), 100 ether); - // Attacker buys shares attacker.buySharesOnBehalf{value: 1 ether}(BATTLE_KEY, true); mockEngine.setWinner(BATTLE_KEY, P0); - // Setup: on receiving ETH from claim, buy more shares + // The receive hook buys back in, which the settle guard now rejects — so the payout + // transfer fails and the claim takes the restore path rather than half-applying. attacker.setup(BATTLE_KEY, true); - - // This should not revert — the reentrant buy during claim is benign - // (though buying shares after game is over is a bit silly, it doesn't break accounting) + uint256 balanceBefore = address(attacker).balance; attacker.claim(); + + assertEq(address(attacker).balance, balanceBefore, "no payout to a reverting receive"); + (uint128 p0Balance,) = pm.sharesPerUserForBattle(BATTLE_KEY, address(attacker)); + assertEq(p0Balance, 1 ether, "position restored for a retry"); + assertEq(address(pm).balance, 1 ether, "pool intact"); } // ========================================================================= diff --git a/test/mocks/MockSimplePMEngine.sol b/test/mocks/MockSimplePMEngine.sol index 3d38f8d6..3c419765 100644 --- a/test/mocks/MockSimplePMEngine.sol +++ b/test/mocks/MockSimplePMEngine.sol @@ -51,7 +51,7 @@ contract MockSimplePMEngine { return playersList[battleKey]; } - /// @notice SimplePM reads startTimestamp + turnId + p0 via the batched BattleContext. + /// @notice SimplePM reads startTimestamp + turnId + p0 + winnerIndex via the batched BattleContext. function getBattleContext(bytes32 battleKey) external view returns (BattleContext memory ctx) { ctx.startTimestamp = uint96(startTimestamps[battleKey]); ctx.turnId = uint64(turnIds[battleKey]); @@ -60,6 +60,10 @@ contract MockSimplePMEngine { ctx.p0 = players[0]; ctx.p1 = players[1]; } + // Mirrors Engine: 2 = no winner, else the winner's player index. Derived from the same + // mapping getWinner serves, so the two can't disagree. + address winner = winners[battleKey]; + ctx.winnerIndex = winner == address(0) ? 2 : (winner == ctx.p0 ? 0 : 1); } /// @notice SimplePM reads the buffered-turn count as `packedTurns.length`; entries are unused here. diff --git a/transpiler/runtime/base.ts b/transpiler/runtime/base.ts index 99a8bbf5..afc0083b 100644 --- a/transpiler/runtime/base.ts +++ b/transpiler/runtime/base.ts @@ -877,6 +877,20 @@ export abstract class Contract { } return id; } + + /** + * Fork support: `dst` is a structural clone of `src`, but the identity stamp is + * non-enumerable so the clone can't inherit it — without this, a fork's struct reads a + * ZERO packed word (lost effect compact data / monState top bits). Give the clone its + * own key and copy the word: reads agree, writes stay isolated from the live battle. + */ + cloneYulSlotWord(src: any, dst: any): void { + if (src === null || typeof src !== 'object' || dst === null || typeof dst !== 'object') return; + const id = (src as any)[YUL_SLOT_ID]; + if (id === undefined) return; + const w = this._storage.sload(this._yulStorageKey(id)); + if (w !== 0n) this._storage.sstore(this._yulStorageKey(this._yulSlotKeyOf(dst)), w); + } } /** Identity stamp for `_yulSlotKeyOf` — a symbol, so it can't collide with a field name. */ diff --git a/transpiler/strategies-rs/src/bin/replay.rs b/transpiler/strategies-rs/src/bin/replay.rs new file mode 100644 index 00000000..c0e8c74d --- /dev/null +++ b/transpiler/strategies-rs/src/bin/replay.rs @@ -0,0 +1,52 @@ +//! Replay mainnet battle logs against the current Rust CPU policies. +//! +//! cargo run --release -p chomp-strategies --bin replay -- [--log logs.txt] [--battle N] + +use chomp_strategies::replay::{parse_logs, replay_doubles, replay_singles}; +use chomp_strategies::roster::{self, load_roster}; +use std::path::PathBuf; + +fn arg(args: &[String], flag: &str) -> Option { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned() +} + +fn main() { + let args: Vec = std::env::args().collect(); + let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }); + let log_path = arg(&args, "--log") + .map(PathBuf::from) + .unwrap_or_else(|| chomp_root.join("logs.txt")); + let only: Option = arg(&args, "--battle").and_then(|v| v.parse().ok()); + + let text = std::fs::read_to_string(&log_path) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", log_path.display())); + let battles = parse_logs(&text); + println!("{} battles parsed from {}", battles.len(), log_path.display()); + + let roster = load_roster(&chomp_root); + let book = roster::address_book(); + + for (i, b) in battles.iter().enumerate() { + if only.is_some_and(|n| n != i) { + continue; + } + println!( + "\n═══ battle {i}: {} ({}, {} — logged winner side {}) ═══", + b.key, + if b.doubles { "doubles" } else { "singles" }, + b.difficulty, + b.winner_side, + ); + let report = if b.doubles { + replay_doubles(b, &roster, &book) + } else { + replay_singles(b, &roster, &book) + }; + println!( + " fidelity: {}", + if report.fidelity_ok { "OK — exact replay" } else { "DESYNC(S) — see !! lines" } + ); + } +} diff --git a/transpiler/strategies-rs/src/bin/sparsecost.rs b/transpiler/strategies-rs/src/bin/sparsecost.rs new file mode 100644 index 00000000..f1a388c4 --- /dev/null +++ b/transpiler/strategies-rs/src/bin/sparsecost.rs @@ -0,0 +1,99 @@ +//! Per-decision cost of the doubles search variants — wall time + engine forks +//! (the fork count is the TS-port budget proxy: TS pays ~fixed cost per fork). +//! +//! cargo run --release -p chomp-strategies --bin sparsecost -- --games 30 + +use std::path::PathBuf; +use std::time::Instant; + +use chomp_strategies::arena::{build_team_mon, draw_team, Wrand}; +use chomp_strategies::doubles::{ + pick_side_moves, search_side_moves, search_side_moves_sparse, Difficulty, DoublesEvalW, + SPARSE_DEFAULT, SPARSE_LEAN, +}; +use chomp_strategies::jsrng::derive; +use chomp_strategies::roster::{self, load_roster}; +use chomp_strategies::sim::{pack_side, Sim}; + +fn arg_u(args: &[String], flag: &str, def: u64) -> u64 { + args.iter() + .position(|a| a == flag) + .and_then(|i| args.get(i + 1)) + .and_then(|v| v.parse().ok()) + .unwrap_or(def) +} + +fn main() { + let args: Vec = std::env::args().collect(); + let games = arg_u(&args, "--games", 30) as usize; + let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") + }); + let roster = load_roster(&chomp_root); + let book = roster::address_book(); + let eval = DoublesEvalW::default(); + + let variants: &[&str] = &["d1", "d2", "sparse", "sparse-lean"]; + for &variant in variants { + let mut times_ms: Vec = Vec::new(); + let mut forks: Vec = Vec::new(); + let (mut wins, mut losses) = (0u32, 0u32); + for g in 0..games { + let mut trng = Wrand::new(0xabcd_0000 + g as u32); + let ids0 = draw_team(&roster, &mut trng); + let ids1 = draw_team(&roster, &mut trng); + let mon = |id: u32| roster.mons.iter().find(|m| m.id == id).expect("mon id"); + let mut sim = Sim::new_doubles( + 4, + ids0.iter().map(|&id| build_team_mon(mon(id))).collect(), + ids1.iter().map(|&id| build_team_mon(mon(id))).collect(), + ids0.clone(), + ids1.clone(), + &book, + ); + let mut orng = derive(0x5eed + g as u32, 0); + let mut bitmap = 0u32; + for _ in 0..300 { + if sim.winner_index() != 2 { + break; + } + let bk = sim.battle_key; + let fc0 = sim.fork_counter(); + let t0 = Instant::now(); + let (b0, b1) = match variant { + "d1" => search_side_moves(&mut sim, bk, 1, 1, &eval, &mut bitmap), + "d2" => search_side_moves(&mut sim, bk, 1, 2, &eval, &mut bitmap), + "sparse" => search_side_moves_sparse(&mut sim, bk, 1, &eval, SPARSE_DEFAULT, &mut bitmap), + _ => search_side_moves_sparse(&mut sim, bk, 1, &eval, SPARSE_LEAN, &mut bitmap), + }; + times_ms.push(t0.elapsed().as_secs_f64() * 1e3); + forks.push(sim.fork_counter() - fc0); + let (a0, a1) = pick_side_moves(&mut sim, bk, 0, Difficulty::Medium, &mut orng); + sim.execute_slot_turn( + pack_side(a0.move_index, a0.extra_data, a1.move_index, a1.extra_data, 0), + pack_side(b0.move_index, b0.extra_data, b1.move_index, b1.extra_data, 0), + ); + } + match sim.winner_index() { + 1 => wins += 1, + 0 => losses += 1, + _ => {} + } + } + times_ms.sort_by(|a, b| a.total_cmp(b)); + forks.sort(); + let n = times_ms.len().max(1); + let q = |v: &[f64], p: f64| v[((p * v.len() as f64) as usize).min(v.len().saturating_sub(1))]; + let qf = |v: &[u64], p: f64| v[((p * v.len() as f64) as usize).min(v.len().saturating_sub(1))]; + println!( + "{variant:<12} {n:>5} decisions · ms mean {:>7.2} p50 {:>6.2} p95 {:>7.2} max {:>8.2} · forks p50 {:>5} p95 {:>6} max {:>6} · vs medium {wins}-{losses}", + times_ms.iter().sum::() / n as f64, + q(×_ms, 0.5), + q(×_ms, 0.95), + q(×_ms, 1.0), + qf(&forks, 0.5), + qf(&forks, 0.95), + qf(&forks, 1.0), + ); + } +} diff --git a/transpiler/strategies-rs/src/bots.rs b/transpiler/strategies-rs/src/bots.rs index 9e05bcb4..07ffd20a 100644 --- a/transpiler/strategies-rs/src/bots.rs +++ b/transpiler/strategies-rs/src/bots.rs @@ -32,6 +32,8 @@ pub const DOUBLES_EASY: &str = "doubles-easy"; pub const DOUBLES_MEDIUM: &str = "doubles-medium"; pub const DOUBLES_HARD: &str = "doubles-hard"; pub const DOUBLES_SEARCH: &str = "doubles-search"; +pub const DOUBLES_SPARSE: &str = "doubles-sparse"; +pub const DOUBLES_SPARSE_LEAN: &str = "doubles-sparse-lean"; /// Every registered bot, in ladder order (weakest first). pub const NAMES: &[&str] = @@ -45,6 +47,8 @@ pub const DOUBLES_NAMES: &[&str] = &[ DOUBLES_SEARCH, DOUBLES_SEARCH_D1, DOUBLES_SEARCH_D2, + DOUBLES_SPARSE, + DOUBLES_SPARSE_LEAN, ]; const DOUBLES_ONLY: &[BattleMode] = &[BattleMode::Doubles]; @@ -77,6 +81,34 @@ impl Bot for DoublesGreedyBot { pub struct DoublesSearchBot { depth: u32, eval: DoublesEvalW, + move_used_bitmap: u32, +} + +/// Sparse depth-2: d1 guidance matrix, then a top-K beam deepened to d2. +pub struct DoublesSparseBot { + lean: bool, + eval: DoublesEvalW, + move_used_bitmap: u32, +} + +impl Bot for DoublesSparseBot { + fn name(&self) -> &'static str { + if self.lean { DOUBLES_SPARSE_LEAN } else { DOUBLES_SPARSE } + } + fn modes(&self) -> &'static [BattleMode] { + DOUBLES_ONLY + } + fn reset(&mut self) { + self.move_used_bitmap = 0; + } + fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + let bk = ctx.sim.battle_key; + let cfg = if self.lean { doubles::SPARSE_LEAN } else { doubles::SPARSE_DEFAULT }; + let (a, b) = doubles::search_side_moves_sparse( + ctx.sim, bk, ctx.seat.cpu, &self.eval, cfg, &mut self.move_used_bitmap, + ); + Action::Slots([slot_to_mv(a), slot_to_mv(b)]) + } } impl Bot for DoublesSearchBot { @@ -90,9 +122,14 @@ impl Bot for DoublesSearchBot { fn modes(&self) -> &'static [BattleMode] { DOUBLES_ONLY } + fn reset(&mut self) { + self.move_used_bitmap = 0; + } fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { let bk = ctx.sim.battle_key; - let (a, b) = doubles::search_side_moves(ctx.sim, bk, ctx.seat.cpu, self.depth, &self.eval); + let (a, b) = doubles::search_side_moves( + ctx.sim, bk, ctx.seat.cpu, self.depth, &self.eval, &mut self.move_used_bitmap, + ); Action::Slots([slot_to_mv(a), slot_to_mv(b)]) } } @@ -281,6 +318,7 @@ pub fn build(name: &str, cfg: BotConfig) -> Option> { DOUBLES_SEARCH_D1 | DOUBLES_SEARCH_D2 => Some(Box::new(DoublesSearchBot { depth: if name == DOUBLES_SEARCH_D1 { 1 } else { 2 }, eval: cfg.doubles_eval, + move_used_bitmap: 0, })), SEARCH => Some(Box::new(SearchBot { weights: w, @@ -296,7 +334,14 @@ pub fn build(name: &str, cfg: BotConfig) -> Option> { DOUBLES_SEARCH => Some(Box::new(DoublesSearchBot { depth: cfg.search_depth.max(1), eval: cfg.doubles_eval, + move_used_bitmap: 0, })), + DOUBLES_SPARSE => { + Some(Box::new(DoublesSparseBot { lean: false, eval: cfg.doubles_eval, move_used_bitmap: 0 })) + } + DOUBLES_SPARSE_LEAN => { + Some(Box::new(DoublesSparseBot { lean: true, eval: cfg.doubles_eval, move_used_bitmap: 0 })) + } _ => None, } } diff --git a/transpiler/strategies-rs/src/doubles.rs b/transpiler/strategies-rs/src/doubles.rs index b052710e..a93c519a 100644 --- a/transpiler/strategies-rs/src/doubles.rs +++ b/transpiler/strategies-rs/src/doubles.rs @@ -23,8 +23,9 @@ use chomp_rt::{Address, B256, U256}; use crate::bot::{Action, BattleMode, Bot, BotConfig, DecisionCtx}; use crate::bots; use crate::jsrng::{derive, random_salt, JsRng, STREAM_P0, STREAM_P1, STREAM_SALT}; +use crate::heuristic::better_cpu_config; use crate::roster::{input_type_of, target_spec_of, InputType, TargetSpec}; -use crate::shared::{build_damage_calc_context, estimate_damage_meta}; +use crate::shared::{build_damage_calc_context, estimate_damage_meta, CONFIG_SETUP_MOVE}; use crate::sim::{pack_side, Sim}; use crate::view::{ decode_meta, mon_current_stamina, mon_max_hp, mon_skip_turn, mon_state, move_slot, stat_delta_score, turn_id, Seat, @@ -329,7 +330,16 @@ pub struct DoublesEvalW { } impl Default for DoublesEvalW { + /// Mirrors the shipped munch `slot-search` eval: boosts at 0.4/pct (≡ 40 per + /// delta/base unit here) and corpse-gated active terms. fn default() -> Self { + Self { w_boost: 40.0, gate_ko: true, ..Self::legacy() } + } +} + +impl DoublesEvalW { + /// The old hp/ko/stamina-only baseline (corpses counted). + pub fn legacy() -> Self { Self { w_hp: D_W_HP, w_ko: D_W_KO, @@ -431,16 +441,55 @@ fn legal_benches(team_size: usize, ko: u32, slots: &[u32; 4], abs_slot: usize) - .collect() } +/// TS `clearMoveUsedBitsOnSwitchIn` with the doubles `side` param: clear the incoming mon's +/// config lane bit; its setup lane bit only clears when it re-enters above half HP. +fn clear_used_bits_on_switch_in(sim: &mut Sim, bk: B256, side: u8, used_bitmap: u32, mon: usize) -> u32 { + let setup_bit = 1u32 << (mon + SETUP_LANE_BIT_OFFSET); + let mut b = used_bitmap & !(1u32 << mon); + if used_bitmap & setup_bit != 0 { + let mhp = mon_max_hp(sim, OBS, bk, side, mon); + let hp = mhp + mon_state(sim, OBS, bk, side, mon, MonStateIndexName::Hp); + if hp * 2 > mhp { + b &= !setup_bit; + } + } + b +} + +const SETUP_LANE_BIT_OFFSET: usize = 8; + /// Candidate actions for one active slot on a normal turn: the TOP-damage (move,target) options /// (with per-target diversity — the best option against EACH live enemy slot is always included, /// so focus-fire vs spread are both searchable), non-damaging status/setup moves whose targeting /// needs no nibble (self-only / none), a pivot switch, and rest. Bench and rest are always kept /// (never truncated behind damaging options — the singles rest-bug lesson). Empty lane → rest only. -fn slot_candidates(sim: &mut Sim, bk: B256, side: u8, abs_slot: usize, slots: &[u32; 4], team_size: usize) -> Vec { +/// `move_used_bitmap` excludes a one-shot setup whose lane bit is set (the engine blocks re-use +/// until the mon switches out, so it would search as a guaranteed no-op). +fn slot_candidates( + sim: &mut Sim, bk: B256, side: u8, abs_slot: usize, slots: &[u32; 4], team_size: usize, move_used_bitmap: u32, +) -> Vec { + slot_candidates_capped(sim, bk, side, abs_slot, slots, team_size, MAX_DAMAGING, MAX_STATUS, move_used_bitmap) +} + +/// `slot_candidates` with explicit caps — the sparse search's interior nodes run leaner. +#[allow(clippy::too_many_arguments)] +fn slot_candidates_capped( + sim: &mut Sim, + bk: B256, + side: u8, + abs_slot: usize, + slots: &[u32; 4], + team_size: usize, + max_dmg: usize, + max_status: usize, + move_used_bitmap: u32, +) -> Vec { let my_mon = slots[abs_slot]; if my_mon == EMPTY_LANE { return vec![NO_OP_MOVE]; } + let setup_locked = move_used_bitmap & (1u32 << (my_mon as usize + SETUP_LANE_BIT_OFFSET)) != 0; + let mon_id = sim.mon_id_phys(U256::from(side), my_mon as usize) as usize; let mut dmg = damaging_options(sim, bk, side, my_mon); dmg.sort_by_key(|&(_, d)| -d); // best damage first // Best option per distinct target first (≤2 live slots), then next-best overall. @@ -453,20 +502,20 @@ fn slot_candidates(sim: &mut Sim, bk: B256, side: u8, abs_slot: usize, slots: &[ } } for &(sm, _) in &dmg { - if out.len() >= MAX_DAMAGING { + if out.len() >= max_dmg { break; } if !out.contains(&sm) { out.push(sm); } } - out.truncate(MAX_DAMAGING); + out.truncate(max_dmg); // Status/setup moves (non-damaging class, affordable, nibble-free targeting): extraData 0. let stamina = mon_current_stamina(sim, OBS, bk, side, my_mon as usize); let mut n_status = 0usize; for mi in 0..4usize { - if n_status >= MAX_STATUS { + if n_status >= max_status { break; } let Some(slot_w) = move_slot(sim, OBS, bk, side, my_mon as usize, mi) else { break }; @@ -480,6 +529,9 @@ fn slot_candidates(sim: &mut Sim, bk: B256, side: u8, abs_slot: usize, slots: &[ if meta.stamina as i64 > stamina { continue; } + if setup_locked && better_cpu_config(mon_id, CONFIG_SETUP_MOVE) as usize == mi + 1 { + continue; + } let addr = MoveSlotLib::toIMoveSet(slot_w); let nibble_free = matches!(target_spec_of(addr), TargetSpec::SelfOnly | TargetSpec::NoTarget); if nibble_free && input_type_of(addr) == InputType::None { @@ -497,19 +549,27 @@ fn slot_candidates(sim: &mut Sim, bk: B256, side: u8, abs_slot: usize, slots: &[ } /// Joint (slot0, slot1) actions for a side on a normal turn, capped at MAX_JOINTS. -fn side_joint(sim: &mut Sim, bk: B256, side: u8) -> Vec<(SlotMove, SlotMove)> { +/// `move_used_bitmap` gates the side's OWN configured setups (0 for opponent/interior models). +fn side_joint(sim: &mut Sim, bk: B256, side: u8, move_used_bitmap: u32) -> Vec<(SlotMove, SlotMove)> { + side_joint_capped(sim, bk, side, MAX_DAMAGING, MAX_STATUS, move_used_bitmap) +} + +fn side_joint_capped( + sim: &mut Sim, bk: B256, side: u8, max_dmg: usize, max_status: usize, move_used_bitmap: u32, +) -> Vec<(SlotMove, SlotMove)> { let [a0, a1] = side_slots(side); let slots = active_slots(sim, bk); let ts = team_size_of(sim, bk, side); - let c0 = slot_candidates(sim, bk, side, a0, &slots, ts); - let c1 = slot_candidates(sim, bk, side, a1, &slots, ts); + let c0 = slot_candidates_capped(sim, bk, side, a0, &slots, ts, max_dmg, max_status, move_used_bitmap); + let c1 = slot_candidates_capped(sim, bk, side, a1, &slots, ts, max_dmg, max_status, move_used_bitmap); + let cap = (max_dmg + max_status + 2).pow(2); let mut out = Vec::with_capacity(c0.len() * c1.len()); for &m0 in &c0 { for &m1 in &c1 { out.push((m0, m1)); } } - out.truncate(MAX_JOINTS); + out.truncate(cap.min(MAX_JOINTS)); out } @@ -541,6 +601,18 @@ fn forced_joint_model(sim: &mut Sim, bk: B256, side: u8, mask: u32, slots: &[u32 /// deterministically and recursed WITHOUT consuming depth (they don't burn horizon; the chain is /// bounded by roster size / the terminal check). fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW) -> f64 { + search_value_capped(sim, bk, cpu_side, depth, w, MAX_DAMAGING, MAX_STATUS) +} + +fn search_value_capped( + sim: &mut Sim, + bk: B256, + cpu_side: u8, + depth: u32, + w: &DoublesEvalW, + max_dmg: usize, + max_status: usize, +) -> f64 { if let Some(v) = terminal(sim, bk, cpu_side, depth) { return v; } @@ -557,19 +629,19 @@ fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEv return doubles_eval(sim, bk, cpu_side, w); // no legal resolution — don't loop } let child = fork_joint(sim, bk, cpu_side, mine, theirs); - let v = search_value(sim, child, cpu_side, depth, w); + let v = search_value_capped(sim, child, cpu_side, depth, w, max_dmg, max_status); sim.dispose_fork(child); return v; } let opp_side = 1 - cpu_side; - let my = side_joint(sim, bk, cpu_side); - let opp = side_joint(sim, bk, opp_side); + let my = side_joint_capped(sim, bk, cpu_side, max_dmg, max_status, 0); + let opp = side_joint_capped(sim, bk, opp_side, max_dmg, max_status, 0); let mut best = f64::NEG_INFINITY; for &mine in &my { let mut worst = f64::INFINITY; for &theirs in &opp { let child = fork_joint(sim, bk, cpu_side, mine, theirs); - let v = search_value(sim, child, cpu_side, depth - 1, w); + let v = search_value_capped(sim, child, cpu_side, depth - 1, w, max_dmg, max_status); sim.dispose_fork(child); if v < worst { worst = v; @@ -588,10 +660,16 @@ fn search_value(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEv /// Pick `cpu_side`'s two slot moves by depth-`depth` joint maximin (turn 0 → leads; forced-switch → /// first-legal bench; normal turn → search). A reverting hypothetical mid-search is contained to /// this decision (fall back to resting both slots) rather than aborting the game. -pub fn search_side_moves(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW) -> (SlotMove, SlotMove) { +pub fn search_side_moves( + sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW, bitmap: &mut u32, +) -> (SlotMove, SlotMove) { let saved_fc = sim.fork_counter(); - match catch_unwind(AssertUnwindSafe(|| search_side_moves_inner(sim, bk, cpu_side, depth, w))) { - Ok(m) => m, + let bm = *bitmap; + match catch_unwind(AssertUnwindSafe(|| search_side_moves_inner(sim, bk, cpu_side, depth, w, bm))) { + Ok(m) => { + *bitmap = update_used_bitmap(sim, bk, cpu_side, bm, m); + m + } Err(_) => { sim.set_fork_counter(saved_fc); (NO_OP_MOVE, NO_OP_MOVE) @@ -599,7 +677,34 @@ pub fn search_side_moves(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: & } } -fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW) -> (SlotMove, SlotMove) { +/// TS `finishPick`/`decideSlots` bitmap bookkeeping: a switch clears the incoming mon's +/// bits; playing the configured setup sets its lane bit. +fn update_used_bitmap(sim: &mut Sim, bk: B256, cpu_side: u8, mut bitmap: u32, picked: (SlotMove, SlotMove)) -> u32 { + if turn_id(sim, bk) == 0 { + bitmap = 0; + } + let slots = active_slots(sim, bk); + let [a0, a1] = side_slots(cpu_side); + for (abs, m) in [(a0, picked.0), (a1, picked.1)] { + if m.move_index == SWITCH { + bitmap = clear_used_bits_on_switch_in(sim, bk, cpu_side, bitmap, m.extra_data as usize); + } else if m.move_index < 4 { + let mon = slots[abs]; + if mon == EMPTY_LANE { + continue; + } + let mon_id = sim.mon_id_phys(U256::from(cpu_side), mon as usize) as usize; + if better_cpu_config(mon_id, CONFIG_SETUP_MOVE) as usize == m.move_index as usize + 1 { + bitmap |= 1u32 << (mon as usize + SETUP_LANE_BIT_OFFSET); + } + } + } + bitmap +} + +fn search_side_moves_inner( + sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: &DoublesEvalW, move_used_bitmap: u32, +) -> (SlotMove, SlotMove) { let [a0, a1] = side_slots(cpu_side); let sw = |i: u16| SlotMove { move_index: SWITCH, extra_data: i }; @@ -687,8 +792,8 @@ fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: // Normal turn: depth-`depth` joint maximin (with the argmax-invariant row prune). let depth = depth.max(1); let opp_side = 1 - cpu_side; - let my = side_joint(sim, bk, cpu_side); - let opp = side_joint(sim, bk, opp_side); + let my = side_joint(sim, bk, cpu_side, move_used_bitmap); + let opp = side_joint(sim, bk, opp_side, 0); let mut best = my.first().copied().unwrap_or((NO_OP_MOVE, NO_OP_MOVE)); let mut best_val = f64::NEG_INFINITY; for &mine in &my { @@ -712,6 +817,128 @@ fn search_side_moves_inner(sim: &mut Sim, bk: B256, cpu_side: u8, depth: u32, w: best } +// ── Sparse depth-2 search (guided beam over the joint matrix) ──────────────── +// +// Full d2 squares the joint matrix and is unaffordable in the TS client. The sparse variant +// spends d1's budget on a guidance matrix, then deepens only the top `k_my` joints against +// each one's `k_opp` most threatening d1 replies, with leaner candidate caps inside interior +// nodes. Ordering worst-reply-first keeps the argmax row prune effective. + +/// Beam widths + interior-node candidate caps for [`search_side_moves_sparse`]. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct SparseCfg { + pub k_my: usize, + pub k_opp: usize, + pub int_dmg: usize, + pub int_status: usize, +} + +/// Full-width interiors — maximum fidelity to true d2 within the beam. +pub const SPARSE_DEFAULT: SparseCfg = + SparseCfg { k_my: 6, k_opp: 6, int_dmg: MAX_DAMAGING, int_status: MAX_STATUS }; +/// Lean interiors + trimmed reply set — the client-budget shape (the TS twin's config). +pub const SPARSE_LEAN: SparseCfg = SparseCfg { k_my: 6, k_opp: 4, int_dmg: 2, int_status: 0 }; + +pub fn search_side_moves_sparse( + sim: &mut Sim, + bk: B256, + cpu_side: u8, + w: &DoublesEvalW, + cfg: SparseCfg, + bitmap: &mut u32, +) -> (SlotMove, SlotMove) { + let saved_fc = sim.fork_counter(); + let bm = *bitmap; + match catch_unwind(AssertUnwindSafe(|| sparse_inner(sim, bk, cpu_side, w, cfg, bm))) { + Ok(m) => { + *bitmap = update_used_bitmap(sim, bk, cpu_side, bm, m); + m + } + Err(_) => { + sim.set_fork_counter(saved_fc); + (NO_OP_MOVE, NO_OP_MOVE) + } + } +} + +fn sparse_inner( + sim: &mut Sim, bk: B256, cpu_side: u8, w: &DoublesEvalW, cfg: SparseCfg, move_used_bitmap: u32, +) -> (SlotMove, SlotMove) { + // Leads and forced switches keep the shipped depth-1 paths — d2's edge is in normal turns. + let flag = Engine::getBattleContext(&mut sim.world, bk).playerSwitchForTurnFlag as u32; + if turn_id(sim, bk) == 0 || flag != 2 { + return search_side_moves_inner(sim, bk, cpu_side, 1, w, move_used_bitmap); + } + + // Phase 1: the d1 guidance matrix, with two beam prunes that keep selection EXACT: a row + // whose running minimum drops to/below the k-th best completed row value can never enter + // the beam, and columns scan in the first row's threat order so minima are hit early. + let opp_side = 1 - cpu_side; + let my = side_joint(sim, bk, cpu_side, move_used_bitmap); + let opp = side_joint(sim, bk, opp_side, 0); + let mut cells = vec![vec![f64::INFINITY; opp.len()]; my.len()]; + let mut row_val = vec![f64::INFINITY; my.len()]; + let mut full = vec![false; my.len()]; + let mut col_order: Vec = (0..opp.len()).collect(); + let mut top_vals: Vec = Vec::new(); + for (i, &mine) in my.iter().enumerate() { + let bound = if top_vals.len() >= cfg.k_my { top_vals[cfg.k_my - 1] } else { f64::NEG_INFINITY }; + let mut abandoned = false; + for &j in &col_order { + let child = fork_joint(sim, bk, cpu_side, mine, opp[j]); + let v = search_value(sim, child, cpu_side, 0, w); + sim.dispose_fork(child); + cells[i][j] = v; + if v < row_val[i] { + row_val[i] = v; + } + if row_val[i] <= bound { + abandoned = true; + break; + } + } + if abandoned { + continue; + } + full[i] = true; + top_vals.push(row_val[i]); + top_vals.sort_by(|a, b| b.total_cmp(a)); + top_vals.truncate(cfg.k_my); + if i == 0 { + col_order.sort_by(|&a, &b| cells[0][a].total_cmp(&cells[0][b])); + } + } + + // Phase 2: deepen the top rows, each against its own worst d1 replies first. + let mut order: Vec = (0..my.len()).filter(|&i| full[i]).collect(); + order.sort_by(|&a, &b| row_val[b].total_cmp(&row_val[a])); + order.truncate(cfg.k_my.max(1)); + let mut best = my[order[0]]; // the d1 argmax survives as the floor + let mut best_val = f64::NEG_INFINITY; + for &i in &order { + let mut cols: Vec = (0..opp.len()).collect(); + cols.sort_by(|&a, &b| cells[i][a].total_cmp(&cells[i][b])); + cols.truncate(cfg.k_opp.max(1)); + let mut worst = f64::INFINITY; + for &j in &cols { + let child = fork_joint(sim, bk, cpu_side, my[i], opp[j]); + let v = search_value_capped(sim, child, cpu_side, 1, w, cfg.int_dmg, cfg.int_status); + sim.dispose_fork(child); + if v < worst { + worst = v; + } + if worst <= best_val { + break; // argmax-invariant row prune + } + } + if worst > best_val { + best_val = worst; + best = my[i]; + } + } + best +} + // ── Doubles game driver + arena ────────────────────────────────────────────── pub struct DoublesSpec { @@ -961,6 +1188,7 @@ pub fn play_doubles_game_instrumented( tracked_active_turns: 0, }; + let (mut bm0, mut bm1) = (0u32, 0u32); for t in 0..spec.max_turns { let w = sim.winner_index(); if w != 2 { @@ -970,12 +1198,12 @@ pub fn play_doubles_game_instrumented( } let bk = sim.battle_key; let (p0a, p0b) = if spec.p0_search_depth > 0 { - search_side_moves(&mut sim, bk, 0, spec.p0_search_depth, &spec.p0_eval) + search_side_moves(&mut sim, bk, 0, spec.p0_search_depth, &spec.p0_eval, &mut bm0) } else { pick_side_moves(&mut sim, bk, 0, spec.p0_difficulty, &mut rng0) }; let (p1a, p1b) = if spec.p1_search_depth > 0 { - search_side_moves(&mut sim, bk, 1, spec.p1_search_depth, &spec.p1_eval) + search_side_moves(&mut sim, bk, 1, spec.p1_search_depth, &spec.p1_eval, &mut bm1) } else { pick_side_moves(&mut sim, bk, 1, spec.p1_difficulty, &mut rng1) }; diff --git a/transpiler/strategies-rs/src/evaluator.rs b/transpiler/strategies-rs/src/evaluator.rs index a30bf290..85608024 100644 --- a/transpiler/strategies-rs/src/evaluator.rs +++ b/transpiler/strategies-rs/src/evaluator.rs @@ -5,6 +5,9 @@ //! Float ops mirror the TS source order exactly: f64 products summed //! left-to-right, no reassociation, no FMA. +use chomp_engine::Engine; +use chomp_rt::U256; + use crate::shared::{offensive_matchup_score, popcount8}; use crate::sim::Sim; use crate::view::{ @@ -21,12 +24,18 @@ pub const F_MATCHUP: usize = 2; pub const F_STAMINA: usize = 3; pub const F_STAT_DELTA: usize = 4; pub const F_SKIP: usize = 5; -pub const N_FEATURES: usize = 6; +/// Active-mon status classes, opp minus cpu (a status hurts its carrier). +pub const F_STATUS: usize = 6; +/// Stamina starvation (≤1: most moves unaffordable), opp minus cpu — the +/// Vital-Siphon-lock signal the flat stamina term is too weak to see. +pub const F_STARVED: usize = 7; +pub const N_FEATURES: usize = 8; /// A weight per feature lane. pub type Weights = [f64; N_FEATURES]; -/// The original hardcoded baseline weights — the byte-identical default. +/// The original hardcoded baseline weights — the byte-identical default +/// (the extension lanes are zero-weighted, and their reads are skipped). pub const DEFAULT_WEIGHTS: Weights = [ 1.0, // F_HP 150.0, // F_KO @@ -34,8 +43,13 @@ pub const DEFAULT_WEIGHTS: Weights = [ 2.0, // F_STAMINA 40.0, // F_STAT_DELTA 30.0, // F_SKIP + 0.0, // F_STATUS + 0.0, // F_STARVED ]; +/// Hell-tier candidate: the baseline with the extension lanes turned on. +pub const HELL_WEIGHTS: Weights = [1.0, 150.0, 0.5, 2.0, 40.0, 30.0, 25.0, 40.0]; + /// hp% (0..100) for a slot — pure-view. fn hp_percent(mon: &MonSnap) -> f64 { if mon.max_hp <= 0 { @@ -91,6 +105,31 @@ pub fn features(sim: &mut Sim, seat: Seat, view: &BattleView) -> [f64; N_FEATURE f } +/// [`features`] plus the weight-gated extension lanes: each extra engine read +/// runs only when its weight is nonzero, so the default path costs what it +/// always did. +pub fn features_ext(sim: &mut Sim, seat: Seat, view: &BattleView, w: &Weights) -> [f64; N_FEATURES] { + let mut f = features(sim, seat, view); + if view.cpu_active < view.p1.len() && view.opp_active < view.p0.len() { + let bk = view.bk; + if w[F_STATUS] != 0.0 { + let opp = Engine::getMonStatusClass( + &mut sim.world, bk, seat.phys(VOPP), U256::from(view.opp_active as u64), + ) != U256::ZERO; + let cpu = Engine::getMonStatusClass( + &mut sim.world, bk, seat.phys(VCPU), U256::from(view.cpu_active as u64), + ) != U256::ZERO; + f[F_STATUS] = (opp as i64 - cpu as i64) as f64; + } + if w[F_STARVED] != 0.0 { + let cpu_starved = mon_current_stamina(sim, seat, bk, VCPU, view.cpu_active) <= 1; + let opp_starved = mon_current_stamina(sim, seat, bk, VOPP, view.opp_active) <= 1; + f[F_STARVED] = (opp_starved as i64 - cpu_starved as i64) as f64; + } + } + f +} + /// φ·w, summed left-to-right (accumulator seeded with the first term, not /// 0.0) so the default weights reproduce the original expression bit-for-bit. pub fn dot(f: &[f64; N_FEATURES], w: &Weights) -> f64 { @@ -105,5 +144,5 @@ pub fn dot(f: &[f64; N_FEATURES], w: &Weights) -> f64 { /// Weighted position score, CPU-perspective (higher = better). pub fn score_state(sim: &mut Sim, seat: Seat, view: &BattleView, w: &Weights) -> f64 { - dot(&features(sim, seat, view), w) + dot(&features_ext(sim, seat, view, w), w) } diff --git a/transpiler/strategies-rs/src/heuristic.rs b/transpiler/strategies-rs/src/heuristic.rs index 803e79cd..0634abe0 100644 --- a/transpiler/strategies-rs/src/heuristic.rs +++ b/transpiler/strategies-rs/src/heuristic.rs @@ -35,7 +35,7 @@ const SWITCH_PRIORITY: i64 = Constants::SWITCH_PRIORITY.as_limbs()[0] as i64; /// Per-mon setup-move config (`BETTER_CPU_MON_CONFIG`): values stored as /// moveIndex+1, 0 = unset. Only CONFIG_SETUP_MOVE is populated — the /// switch-in / preferred lanes stay inert, like the TS table. -fn better_cpu_config(mon_index: usize, config_key: usize) -> u16 { +pub(crate) fn better_cpu_config(mon_index: usize, config_key: usize) -> u16 { if config_key != CONFIG_SETUP_MOVE { return 0; } diff --git a/transpiler/strategies-rs/src/lib.rs b/transpiler/strategies-rs/src/lib.rs index c74759d5..866548b3 100644 --- a/transpiler/strategies-rs/src/lib.rs +++ b/transpiler/strategies-rs/src/lib.rs @@ -38,6 +38,7 @@ pub mod mock2; pub mod native; pub mod nopeek; pub mod override_cpu; +pub mod replay; pub mod search; pub mod shared; pub mod sim; diff --git a/transpiler/strategies-rs/src/replay.rs b/transpiler/strategies-rs/src/replay.rs new file mode 100644 index 00000000..e229b390 --- /dev/null +++ b/transpiler/strategies-rs/src/replay.rs @@ -0,0 +1,587 @@ +//! Mainnet battle-log replay — drive the engine with the LOGGED actions and +//! salts (the CPU's salt is always 0 on-chain, so complete player salts +//! reproduce the keccak rng bit-exactly), and at every CPU decision point ask +//! the current Rust policies what they would play instead. +//! +//! Fidelity is self-checking: a truncated salt or wrong stat model desyncs the +//! forced-switch pattern / legality of logged moves within a turn or two, and +//! the replay reports the mismatch instead of silently diverging. + +use std::collections::HashMap; + +use chomp_engine::Engine; +use chomp_engine::Structs::{Mon, MonStats}; +use chomp_rt::{Address, U256}; + +use crate::arena::build_team_mon; +use crate::bot::{BattleMode, Bot, BotConfig, DecisionCtx}; +use crate::bots; +use crate::doubles::{self, Difficulty, DoublesEvalW, SlotMove}; +use crate::evaluator::{Weights, DEFAULT_WEIGHTS, N_FEATURES}; +use crate::jsrng::derive; +use crate::roster::Roster; +use crate::sim::{pack_side, Sim}; +use crate::view::{ + capture_view, mon_current_hp, mon_current_stamina, mon_max_hp, mon_skip_turn, Mv, Seat, + NO_OP_INDEX, SWITCH_MOVE_INDEX, VCPU, VOPP, +}; + +// ── Parsed log model ──────────────────────────────────────────────────────── + +#[derive(Clone, Copy, PartialEq)] +pub struct LoggedAction { + pub move_index: u8, // lane 0-3, SWITCH_MOVE_INDEX, NO_OP_INDEX + pub extra_data: u16, +} + +pub struct LoggedTurn { + /// One action per side in singles, two (lane 0, lane 1) in doubles. + pub a: Vec, + pub b: Vec, + pub a_salt: u128, + pub b_salt: u128, +} + +pub struct LoggedMon { + pub name: String, + pub facet: u8, // facetId; 0 = none + pub moves: Vec, +} + +pub struct LoggedBattle { + pub key: String, + pub difficulty: String, + pub doubles: bool, + /// 0 = side A is the CPU, 1 = side B. + pub cpu_side: u8, + /// 0 = A won, 1 = B won (by address match). + pub winner_side: u8, + pub logged_turns_field: u32, + pub team_a: Vec, + pub team_b: Vec, + pub turns: Vec, +} + +fn paren_addr(s: &str) -> &str { + let open = s.rfind('(').map(|i| i + 1).unwrap_or(0); + let close = s.rfind(')').unwrap_or(s.len()); + &s[open..close] +} + +fn parse_action(s: &str) -> Option<(LoggedAction, u128)> { + let mut toks = s.split_whitespace(); + let head = toks.next()?; + let act = if let Some(t) = head.strip_prefix("SWITCH→") { + LoggedAction { move_index: SWITCH_MOVE_INDEX, extra_data: t.parse().ok()? } + } else if head == "NO_OP" { + LoggedAction { move_index: NO_OP_INDEX, extra_data: 0 } + } else if let Some(t) = head.strip_prefix("move") { + let mi: u8 = t.parse().ok()?; + let x = toks.next()?.strip_prefix('x')?.parse().ok()?; + LoggedAction { move_index: mi, extra_data: x } + } else { + return None; + }; + let salt = toks.next()?.strip_prefix("salt")?.parse().ok()?; + Some((act, salt)) +} + +/// A side's turn text: `mon0 ` (singles) or ` | ` (doubles). +fn parse_side(s: &str) -> Option<(Vec, u128)> { + let mut acts = Vec::new(); + let mut salt = 0u128; + for lane in s.split(" | ") { + let lane = lane.trim().strip_prefix("mon0 ").unwrap_or(lane.trim()); + let (a, sl) = parse_action(lane)?; + acts.push(a); + salt = sl; // lanes repeat the side's single salt + } + Some((acts, salt)) +} + +fn parse_mon_line(s: &str) -> Option { + let name = s.split_whitespace().next()?.to_string(); + let facet: u8 = s + .split_whitespace() + .find_map(|t| t.strip_prefix("facet")) + .and_then(|t| t.parse().ok())?; + let inner = &s[s.find("moves[")? + 6..s.rfind(']')?]; + let moves = inner.split(", ").map(|m| m.trim().to_string()).collect(); + Some(LoggedMon { name, facet, moves }) +} + +pub fn parse_logs(text: &str) -> Vec { + let mut out: Vec = Vec::new(); + let mut lines = text.lines().peekable(); + while let Some(line) = lines.next() { + let Some(rest) = line.strip_prefix("Battle 0x") else { continue }; + let key = format!("0x{}", rest.split_whitespace().next().unwrap_or("")); + + let vs = lines.next().unwrap_or(""); + let (a_part, b_part) = vs.split_once(" vs ").unwrap_or((vs, "")); + let cpu_side = if a_part.contains("CPU") { 0 } else { 1 }; + let (a_addr, b_addr) = (paren_addr(a_part).to_string(), paren_addr(b_part).to_string()); + + let win_line = lines.next().unwrap_or(""); + let win_addr = paren_addr(win_line.split("Turns:").next().unwrap_or("")).to_string(); + let winner_side = if win_addr == a_addr { 0 } else { 1 }; + debug_assert!(win_addr == a_addr || win_addr == b_addr); + let logged_turns_field: u32 = win_line + .split("Turns:") + .nth(1) + .and_then(|t| t.trim().parse().ok()) + .unwrap_or(0); + + let difficulty = lines + .next() + .and_then(|l| l.strip_prefix("CPU difficulty:")) + .map(|s| s.trim().to_string()) + .unwrap_or_default(); + + let mut team_a = Vec::new(); + let mut team_b = Vec::new(); + let mut turns = Vec::new(); + let mut doubles = false; + let mut current: Option<&mut Vec> = None; + loop { + let Some(&peeked) = lines.peek() else { break }; + if peeked.starts_with("Battle 0x") { + break; + } + let l = lines.next().unwrap(); + if l.starts_with("Team A:") { + current = Some(&mut team_a); + } else if l.starts_with("Team B:") { + current = Some(&mut team_b); + } else if l.starts_with('T') && l[1..2].chars().all(|c| c.is_ascii_digit()) { + let body = l.splitn(2, char::is_whitespace).nth(1).unwrap_or("").trim(); + let body = body.strip_prefix("A ").unwrap_or(body); + let (a_txt, b_txt) = body.split_once(" B ").unwrap_or((body, "")); + let (Some((a, a_salt)), Some((b, b_salt))) = (parse_side(a_txt), parse_side(b_txt)) + else { + eprintln!(" !! unparseable turn line: {l}"); + continue; + }; + doubles = doubles || a.len() == 2; + turns.push(LoggedTurn { a, b, a_salt, b_salt }); + } else if let Some(m) = parse_mon_line(l) { + if let Some(team) = current.as_deref_mut() { + team.push(m); + } + } + } + out.push(LoggedBattle { + key, + difficulty, + doubles, + cpu_side, + winner_side, + logged_turns_field, + team_a, + team_b, + turns, + }); + } + out +} + +// ── Fixture building ──────────────────────────────────────────────────────── + +/// Facets.sol: facetId 1-12, boost = (id-1)/3, nerf skips the boost group; +/// +5% always, cost 5% (10% when the boost is Speed), floor of BASE stats. +pub fn apply_facet(stats: &mut MonStats, facet_id: u8) { + if facet_id == 0 || facet_id > 12 { + return; + } + let idx = (facet_id - 1) as u32; + let boost = idx / 3; + let off = idx % 3; + let nerf = if off < boost { off } else { off + 1 }; + let cost_pct = if boost == 3 { 10 } else { 5 }; + let base = stats.clone(); + let mut apply = |group: u32, pct: u32, up: bool| { + let bump = |s: &mut u32, b: u32| { + let d = b * pct / 100; + *s = if up { *s + d } else { *s - d }; + }; + match group { + 0 => bump(&mut stats.hp, base.hp), + 1 => { + bump(&mut stats.attack, base.attack); + bump(&mut stats.specialAttack, base.specialAttack); + } + 2 => { + bump(&mut stats.defense, base.defense); + bump(&mut stats.specialDefense, base.specialDefense); + } + _ => bump(&mut stats.speed, base.speed), + } + }; + apply(boost, 5, true); + apply(nerf, cost_pct, false); +} + +/// Logged team → engine Mons + roster ids; verifies logged move names against +/// the default lanes so a lane-order mismatch fails loudly, not as skew. +fn build_side(roster: &Roster, team: &[LoggedMon], side: &str) -> (Vec, Vec) { + let mut mons = Vec::new(); + let mut ids = Vec::new(); + for lm in team { + let rm = roster + .mons + .iter() + .find(|m| m.name == lm.name) + .unwrap_or_else(|| panic!("unknown mon in log: {}", lm.name)); + for (lane, want) in lm.moves.iter().enumerate() { + let have = roster.move_name(rm.id, lane as u8); + assert!( + have == *want, + "{side} {}: lane {lane} is {have:?} in the catalog but {want:?} in the log — \ + non-default loadout, teach build_side to equip lanes first", + lm.name + ); + } + let mut mon = build_team_mon(rm); + apply_facet(&mut mon.stats, lm.facet); + mons.push(mon); + ids.push(rm.id); + } + (mons, ids) +} + +// ── Replay drivers ────────────────────────────────────────────────────────── + +fn mv(a: LoggedAction) -> Mv { + Mv { move_index: a.move_index, extra_data: a.extra_data } +} + +/// Display label for a singles/doubles action of `side`, given its active roster index. +fn label(roster: &Roster, ids: &[u32], active: usize, m: Mv) -> String { + if m.move_index == SWITCH_MOVE_INDEX { + let name = ids + .get(m.extra_data as usize) + .map(|&id| roster.mon_name(id)) + .unwrap_or_else(|| format!("#{}", m.extra_data)); + format!("switch→{name}") + } else if m.move_index == NO_OP_INDEX { + "rest".into() + } else { + let base = ids + .get(active) + .map(|&id| roster.move_name(id, m.move_index)) + .unwrap_or_else(|| format!("move{}", m.move_index)); + if m.extra_data != 0 { + format!("{base} x{}", m.extra_data) + } else { + base + } + } +} + +fn hp_pct(cur: i64, max: i64) -> i64 { + if max <= 0 { 0 } else { (cur.max(0) * 100) / max } +} + +pub struct ReplayReport { + /// (policy label, CPU acting turns compared, turns where it agreed with the log). + pub agreement: Vec<(String, u32, u32)>, + pub fidelity_ok: bool, +} + +/// Replay a SINGLES battle. Logged side A plays engine p0, B plays p1. +pub fn replay_singles( + battle: &LoggedBattle, + roster: &Roster, + book: &HashMap, +) -> ReplayReport { + assert_eq!(battle.cpu_side, 1, "replay assumes the CPU is side B (p1)"); + let (p0_team, p0_ids) = build_side(roster, &battle.team_a, "A"); + let (p1_team, p1_ids) = build_side(roster, &battle.team_b, "B"); + let mut sim = Sim::new(4, p0_team, p1_team, p0_ids.clone(), p1_ids.clone(), book); + let cpu_seat = Seat { cpu: 1 }; + + // Candidate policies, each on its own rng stream. The heuristic's + // move-used bitmap follows ITS OWN picks, not the logged line — a small + // infidelity that only affects configured-setup availability. + let mut policies: Vec<(&str, Box)> = vec![ + ("heuristic", bots::build(bots::HEURISTIC, singles_cfg(0, false)).unwrap()), + ("search-d1", bots::build(bots::SEARCH_D1, singles_cfg(1, true)).unwrap()), + ("search-d2", bots::build(bots::SEARCH_D2, singles_cfg(2, true)).unwrap()), + ]; + let mut rngs: Vec<_> = (0..policies.len() as u32).map(|i| derive(0xbeef, 100 + i)).collect(); + let mut compared = vec![(0u32, 0u32); policies.len()]; + let mut fidelity_ok = true; + + for (t, turn) in battle.turns.iter().enumerate() { + if sim.winner_index() != 2 { + eprintln!(" !! battle ended at engine turn {t} but the log has more lines — DESYNC"); + fidelity_ok = false; + break; + } + let bk = sim.battle_key; + let ctx0 = Engine::getBattleContext(&mut sim.world, bk); + let flag = ctx0.playerSwitchForTurnFlag; + let (p0_acts, p1_acts) = (flag != 1, flag != 0); + let (a0, a1) = (ctx0.p0ActiveMonIndex as usize, ctx0.p1ActiveMonIndex as usize); + + // Fidelity: a non-acting side must be logged as a salt-0 NO_OP. + let a_log = turn.a[0]; + let b_log = turn.b[0]; + if !p0_acts && (a_log.move_index != NO_OP_INDEX || turn.a_salt != 0) { + eprintln!(" !! T{}: engine says p0 doesn't act, log disagrees — DESYNC", t + 1); + fidelity_ok = false; + } + if !p1_acts && b_log.move_index != NO_OP_INDEX { + eprintln!(" !! T{}: engine says p1 doesn't act, log disagrees — DESYNC", t + 1); + fidelity_ok = false; + } + + // Ask each policy for its counterfactual pick (prod peeks the reveal). + let mut notes = String::new(); + if p1_acts && t > 0 { + let peek = if p0_acts { Some(mv(a_log)) } else { None }; + for (pi, (name, bot)) in policies.iter_mut().enumerate() { + let saved_fc = sim.fork_counter(); + let view = capture_view(&mut sim, cpu_seat, bk); + let pick = { + let mut ctx = DecisionCtx { + sim: &mut sim, + seat: cpu_seat, + mode: BattleMode::Singles, + view: Some(&view), + peek, + rng: &mut rngs[pi], + }; + bot.decide(&mut ctx).single() + }; + sim.set_fork_counter(saved_fc); + compared[pi].0 += 1; + if (pick.move_index, pick.extra_data) == (b_log.move_index, b_log.extra_data) { + compared[pi].1 += 1; + } else { + notes.push_str(&format!(" {name}→{}", label(roster, &p1_ids, a1, pick))); + } + } + } + + let p0hp = hp_pct( + mon_current_hp(&mut sim, cpu_seat, bk, VOPP, a0), + mon_max_hp(&mut sim, cpu_seat, bk, VOPP, a0), + ); + let p1hp = hp_pct( + mon_current_hp(&mut sim, cpu_seat, bk, VCPU, a1), + mon_max_hp(&mut sim, cpu_seat, bk, VCPU, a1), + ); + let dump = if std::env::var("REPLAY_DUMP").is_ok() { + let s0 = mon_current_stamina(&mut sim, cpu_seat, bk, VOPP, a0); + let s1 = mon_current_stamina(&mut sim, cpu_seat, bk, VCPU, a1); + let k0 = mon_skip_turn(&mut sim, cpu_seat, bk, VOPP, a0); + let k1 = mon_skip_turn(&mut sim, cpu_seat, bk, VCPU, a1); + let h0 = mon_current_hp(&mut sim, cpu_seat, bk, VOPP, a0); + let h1 = mon_current_hp(&mut sim, cpu_seat, bk, VCPU, a1); + format!(" [flag {flag} | p0 hp{h0} st{s0} skip{k0} | p1 hp{h1} st{s1} skip{k1}]") + } else { + String::new() + }; + println!( + "T{:<3} {:>9}({p0hp:>3}%) {:<24}| CPU {:>9}({p1hp:>3}%) {:<24}{notes}{dump}", + t + 1, + roster.mon_name(p0_ids[a0]), + if p0_acts { label(roster, &p0_ids, a0, mv(a_log)) } else { "—".into() }, + roster.mon_name(p1_ids[a1]), + if p1_acts { label(roster, &p1_ids, a1, mv(b_log)) } else { "—".into() }, + ); + + for (side, act, phys) in [("p0", a_log, p0_acts), ("p1", b_log, p1_acts)] { + if phys + && !sim.validate_move( + bk, + if side == "p0" { U256::ZERO } else { U256::from(1u64) }, + act.move_index, + act.extra_data, + ) + { + eprintln!(" !! T{}: logged {side} move invalid in replay state — DESYNC", t + 1); + fidelity_ok = false; + } + } + + sim.execute_turn( + if p0_acts { a_log.move_index } else { NO_OP_INDEX }, + if p0_acts { turn.a_salt } else { 0 }, + if p0_acts { a_log.extra_data } else { 0 }, + if p1_acts { b_log.move_index } else { NO_OP_INDEX }, + 0, // the on-chain CPU relayer always submits salt 0 + if p1_acts { b_log.extra_data } else { 0 }, + ); + } + + finish(&mut sim, battle, compared, &policies.iter().map(|(n, _)| n.to_string()).collect::>(), fidelity_ok) +} + +fn singles_cfg(depth: u32, peek: bool) -> BotConfig { + BotConfig { + doubles_eval: DoublesEvalW::default(), + weights: env_weights().unwrap_or(DEFAULT_WEIGHTS), + search_depth: depth, + search_peek: peek, + search_mixed: false, + } +} + +/// REPLAY_WEIGHTS="1,150,..." swaps candidate eval weights into every queried +/// policy — the counterfactual side of the loss-replay regression gate. +fn env_weights() -> Option { + let s = std::env::var("REPLAY_WEIGHTS").ok()?; + let vals: Vec = s.split(',').filter_map(|x| x.trim().parse().ok()).collect(); + if vals.len() != N_FEATURES { + eprintln!(" !! REPLAY_WEIGHTS ignored: expected {N_FEATURES} comma-separated floats"); + return None; + } + let mut w = [0.0f64; N_FEATURES]; + w.copy_from_slice(&vals); + Some(w) +} + +/// Replay a DOUBLES battle (CPU = side B = p1, lanes at absolute slots 2/3). +pub fn replay_doubles( + battle: &LoggedBattle, + roster: &Roster, + book: &HashMap, +) -> ReplayReport { + assert_eq!(battle.cpu_side, 1, "replay assumes the CPU is side B (p1)"); + let (p0_team, p0_ids) = build_side(roster, &battle.team_a, "A"); + let (p1_team, p1_ids) = build_side(roster, &battle.team_b, "B"); + let mut sim = Sim::new_doubles(4, p0_team, p1_team, p0_ids.clone(), p1_ids.clone(), book); + let cpu_seat = Seat { cpu: 1 }; + + let names = ["slot-greedy", "search-d1", "search-d2"]; + let mut grng = derive(0xbeef, 200); + let eval = DoublesEvalW::default(); + let mut compared = vec![(0u32, 0u32); names.len()]; + let (mut bm_d1, mut bm_d2) = (0u32, 0u32); + let mut fidelity_ok = true; + + for (t, turn) in battle.turns.iter().enumerate() { + if sim.winner_index() != 2 { + eprintln!(" !! battle ended at engine turn {t} but the log has more lines — DESYNC"); + fidelity_ok = false; + break; + } + let bk = sim.battle_key; + let flag = Engine::getBattleContext(&mut sim.world, bk).playerSwitchForTurnFlag as u32; + let cpu_acts = flag == 2 || (flag & 0b1100) != 0; + + let raw = Engine::getActiveSlots(&mut sim.world, bk); + let lane_mon = |abs: usize| u64::try_from(raw[abs]).unwrap_or(0xFF) as usize; + let lane_label = |ids: &[u32], abs: usize, a: LoggedAction| { + let active = lane_mon(abs); + if active == 0xFF && a.move_index == NO_OP_INDEX { + "—".to_string() + } else { + label(roster, ids, active.min(3), mv(a)) + } + }; + + let logged: [SlotMove; 2] = [ + SlotMove { move_index: turn.b[0].move_index, extra_data: turn.b[0].extra_data }, + SlotMove { move_index: turn.b[1].move_index, extra_data: turn.b[1].extra_data }, + ]; + let mut notes = String::new(); + if cpu_acts && t > 0 { + let picks: [(usize, (SlotMove, SlotMove)); 3] = [ + (0, doubles::pick_side_moves(&mut sim, bk, 1, Difficulty::Hard, &mut grng)), + (1, doubles::search_side_moves(&mut sim, bk, 1, 1, &eval, &mut bm_d1)), + (2, doubles::search_side_moves(&mut sim, bk, 1, 2, &eval, &mut bm_d2)), + ]; + for (pi, (s0, s1)) in picks { + compared[pi].0 += 1; + let agree = (s0.move_index, s0.extra_data, s1.move_index, s1.extra_data) + == (logged[0].move_index, logged[0].extra_data, logged[1].move_index, logged[1].extra_data); + if agree { + compared[pi].1 += 1; + } else { + let m0 = LoggedAction { move_index: s0.move_index, extra_data: s0.extra_data }; + let m1 = LoggedAction { move_index: s1.move_index, extra_data: s1.extra_data }; + notes.push_str(&format!( + " {}→[{}, {}]", + names[pi], + lane_label(&p1_ids, 2, m0), + lane_label(&p1_ids, 3, m1) + )); + } + } + } + + let side_hp = |sim: &mut Sim, vp: u8, ids: &[u32]| -> String { + (0..4) + .map(|i| { + let pct = hp_pct( + mon_current_hp(sim, cpu_seat, bk, vp, i), + mon_max_hp(sim, cpu_seat, bk, vp, i), + ); + format!("{}:{pct}%", roster.mon_name(ids[i])) + }) + .collect::>() + .join(" ") + }; + println!( + "T{:<3} A[{}, {}] B[{}, {}]{notes}\n A {} B {}", + t + 1, + lane_label(&p0_ids, 0, turn.a[0]), + lane_label(&p0_ids, 1, turn.a[1]), + lane_label(&p1_ids, 2, turn.b[0]), + lane_label(&p1_ids, 3, turn.b[1]), + side_hp(&mut sim, VOPP, &p0_ids), + side_hp(&mut sim, VCPU, &p1_ids), + ); + + sim.execute_slot_turn( + pack_side( + turn.a[0].move_index, + turn.a[0].extra_data, + turn.a[1].move_index, + turn.a[1].extra_data, + turn.a_salt, + ), + pack_side( + turn.b[0].move_index, + turn.b[0].extra_data, + turn.b[1].move_index, + turn.b[1].extra_data, + 0, + ), + ); + } + + finish(&mut sim, battle, compared, &names.iter().map(|s| s.to_string()).collect::>(), fidelity_ok) +} + +fn finish( + sim: &mut Sim, + battle: &LoggedBattle, + compared: Vec<(u32, u32)>, + names: &[String], + mut fidelity_ok: bool, +) -> ReplayReport { + let w = sim.winner_index(); + let expect = battle.winner_side; + if w == 2 { + println!("== replay: battle still running after all logged turns (log says side {expect} won) — DESYNC"); + fidelity_ok = false; + } else if w != expect { + println!("== replay winner p{w} ≠ logged winner side {expect} — DESYNC"); + fidelity_ok = false; + } else { + println!("== replay winner p{w} matches the log ({} logged turns) ==", battle.turns.len()); + } + let agreement = names + .iter() + .zip(compared) + .map(|(n, (total, agree))| (n.clone(), total, agree)) + .collect::>(); + for (n, total, agree) in &agreement { + println!(" {n:<12} agreed {agree}/{total} CPU turns"); + } + ReplayReport { agreement, fidelity_ok } +} From a81a2ea20094078808aa6454b86a545f9162924e Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:34:52 -0700 Subject: [PATCH 31/41] wip --- .../fixtures/mainnet-2026-07.txt | 135 +++++++++++++ transpiler/strategies-rs/src/arena.rs | 34 ++-- transpiler/strategies-rs/src/bin/arena.rs | 19 +- transpiler/strategies-rs/src/bin/cert.rs | 2 + transpiler/strategies-rs/src/bin/replay.rs | 10 +- .../strategies-rs/src/bin/tracesearch.rs | 2 + transpiler/strategies-rs/src/bot.rs | 3 + transpiler/strategies-rs/src/bots.rs | 35 +++- transpiler/strategies-rs/src/evaluator.rs | 2 - transpiler/strategies-rs/src/game.rs | 5 + transpiler/strategies-rs/src/replay.rs | 23 +++ transpiler/strategies-rs/src/search.rs | 183 ++++++++++++++++-- transpiler/strategies-rs/src/teams.rs | 2 + 13 files changed, 415 insertions(+), 40 deletions(-) create mode 100644 transpiler/strategies-rs/fixtures/mainnet-2026-07.txt diff --git a/transpiler/strategies-rs/fixtures/mainnet-2026-07.txt b/transpiler/strategies-rs/fixtures/mainnet-2026-07.txt new file mode 100644 index 00000000..938025c2 --- /dev/null +++ b/transpiler/strategies-rs/fixtures/mainnet-2026-07.txt @@ -0,0 +1,135 @@ +Battle 0x3d975a5c8e58df60c746a2321537c0635a4c484a6c96122dedec671de5788a15 (mainnet) +A 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) +Winner: 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) Turns: 26 +CPU difficulty: hard + +Team A: + Malalien L8 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] + Xmon L8 facet0 moves[Contagious Slumber, Vital Siphon, Somniphobia, Old Vengeance] + Volthare L8 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] + Ekineki L8 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] +Team B: + Inutia L0 facet0 moves[Chain Expansion, Initialize, Big Bite, Hit And Dip] + Ekineki L0 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] + Pengym L0 facet0 moves[Chill Out, Deadlift, Deep Freeze, Pistol Squat] + Embursa L0 facet0 moves[Honey Bribe, Set Ablaze, Heat Beacon, Q5] + +T1 A mon0 SWITCH→2 salt7642991204039085190839734102788 B mon0 SWITCH→3 salt0 +T2 A mon0 move2 x0 salt6523886991944769092997260894615 B mon0 move0 x0 salt0 +T3 A mon0 move2 x0 salt7173571679591124929013698586035 B mon0 SWITCH→2 salt0 +T4 A mon0 move3 x0 salt715973405151049693142183856130 B mon0 move1 x0 salt0 +T5 A mon0 SWITCH→3 salt10434400288200938430387780785640 B mon0 move3 x0 salt0 +T6 A mon0 move1 x0 salt16590288657525008761715849387279 B mon0 SWITCH→0 salt0 +T7 A mon0 move1 x0 salt6648290556624459692187665162988 B mon0 SWITCH→1 salt0 +T8 A mon0 SWITCH→3 salt11274310919296775172131196580187 B mon0 move0 x0 salt0 +T9 A mon0 SWITCH→2 salt16134457634591674276270373928776 B mon0 NO_OP salt0 +T10 A mon0 NO_OP salt2683191092725922261265135640441 B mon0 move3 x0 salt0 +T11 A mon0 move2 x0 salt11154374962366060505624650320091 B mon0 SWITCH→2 salt0 +T12 A mon0 NO_OP salt0 B mon0 SWITCH→3 salt0 +T13 A mon0 NO_OP salt2442172819432642288986362144799 B mon0 move1 x0 salt0 +T14 A mon0 SWITCH→0 salt13755720069283378538249966050958 B mon0 NO_OP salt0 +T15 A mon0 NO_OP salt6756780855722838315842010211924 B mon0 move0 x0 salt0 +T16 A mon0 move1 x0 salt7134362144532818851786463667953 B mon0 NO_OP salt0 +T17 A mon0 NO_OP salt18284396846231976760979538969039 B mon0 move0 x0 salt0 +T18 A mon0 move1 x0 salt1618181630032726973372722526491 B mon0 move1 x0 salt0 +T19 A mon0 SWITCH→1 salt4487693600846520293556172147971 B mon0 SWITCH→1 salt0 +T20 A mon0 move1 x0 salt9839922508345259735150333678320 B mon0 move1 x1 salt0 +T21 A mon0 move1 x0 salt3477810539310681962522516256065 B mon0 SWITCH→0 salt0 +T22 A mon0 move1 x0 salt10450567441627425515527389264062 B mon0 SWITCH→1 salt0 +T23 A mon0 move1 x0 salt3676355149806238644881809734477 B mon0 move1 x1 salt0 +T24 A mon0 NO_OP salt0 B mon0 SWITCH→0 salt0 +T25 A mon0 NO_OP salt882665223311632533854195206303 B mon0 move0 x0 salt0 +T26 A mon0 move1 x0 salt6717266306622880485132858893329 B mon0 move0 x0 salt0 +T27 A mon0 move1 x0 salt1998170498903120033041120388285 B mon0 move0 x0 salt0 + +Battle 0x7529114a519de772c47a08d62c910cd1abcf13ed23b28f6d46fdc662d70a58cd (mainnet) +A 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) +Winner: 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) Turns: 24 +CPU difficulty: hard + +Team A: + Malalien L8 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] + Xmon L8 facet0 moves[Contagious Slumber, Vital Siphon, Somniphobia, Old Vengeance] + Volthare L7 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] + Ekineki L8 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] +Team B: + Inutia L0 facet0 moves[Chain Expansion, Initialize, Big Bite, Hit And Dip] + Ekineki L0 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] + Pengym L0 facet0 moves[Chill Out, Deadlift, Deep Freeze, Pistol Squat] + Embursa L0 facet0 moves[Honey Bribe, Set Ablaze, Heat Beacon, Q5] + +T1 A mon0 SWITCH→2 salt16278078627131487114132547704398 B mon0 SWITCH→3 salt0 +T2 A mon0 move2 x0 salt7783107712856898203443960860761 B mon0 move0 x0 salt0 +T3 A mon0 move2 x0 salt19887999801398167433714186522490 B mon0 move1 x0 salt0 +T4 A mon0 move1 x3 salt5027765037949021299786506045172 B mon0 SWITCH→1 salt0 +T5 A mon0 move1 x3 salt16885550753857402497828728004069 B mon0 move0 x0 salt0 +T6 A mon0 SWITCH→2 salt12823241624889775396764909896588 B mon0 NO_OP salt0 +T7 A mon0 move1 x1 salt11769922866231276628642544586802 B mon0 move1 x2 salt0 +T8 A mon0 SWITCH→0 salt13890169115124444928492980561828 B mon0 NO_OP salt0 +T9 A mon0 move0 x0 salt1967333149517071111268237089957 B mon0 move0 x0 salt0 +T10 A mon0 move1 x0 salt1087772717206985673591137063737 B mon0 SWITCH→2 salt0 +T11 A mon0 SWITCH→1 salt6064891294010572925388263446039 B mon0 move2 x0 salt0 +T12 A mon0 move1 x0 salt13970724489817994243868990999906 B mon0 SWITCH→1 salt0 +T13 A mon0 move1 x0 salt3664146897130979578418152456832 B mon0 move2 x0 salt0 +T14 A mon0 move1 x0 salt18892824784579841314989597992588 B mon0 move0 x0 salt0 +T15 A mon0 move1 x0 salt1376118833330709076722273176502 B mon0 move0 x0 salt0 +T16 A mon0 move1 x0 salt8244822615319294248991548387402 B mon0 SWITCH→0 salt0 +T17 A mon0 move1 x0 salt18344034292549560831899680922034 B mon0 move1 x0 salt0 +T18 A mon0 move1 x0 salt9096455331192103053941663375814 B mon0 move0 x0 salt0 +T19 A mon0 SWITCH→0 salt7853893379365566137156318043105 B mon0 NO_OP salt0 +T20 A mon0 SWITCH→3 salt3920529028209073602227420726182 B mon0 NO_OP salt0 +T21 A mon0 move1 x2 salt5944833039053403453796308587341 B mon0 move2 x0 salt0 +T22 A mon0 move0 x0 salt3969701329630262960307396414223 B mon0 move3 x1 salt0 +T23 A mon0 NO_OP salt0 B mon0 SWITCH→1 salt0 +T24 A mon0 SWITCH→1 salt10391795889289817909269995339622 B mon0 NO_OP salt0 +T25 A mon0 move1 x0 salt5256755908273271588468609386902 B mon0 NO_OP salt0 + +Battle 0xc996696b718feaa03805730a31d25de9d995439a7e6a8082c73b91a43e9d0d08 (mainnet) +A @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) +Winner: @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) Turns: 7 +CPU difficulty: hard + +Team A: + Gorillax L12 facet6 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] + Malalien L12 facet6 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] + Sofabbi L12 facet6 moves[Gachachacha, Guest Feature, Unexpected Carrot, Snack Break] + Ekineki L12 facet6 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] +Team B: + Malalien L0 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] + Aurox L0 facet0 moves[Volatile Punch, Gilded Recovery, Iron Wall, Bull Rush] + Gorillax L0 facet0 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] + Volthare L0 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] + +T1 A SWITCH→3 salt1145376790288196797197575 | SWITCH→0 salt1145376790288196797197575 B SWITCH→0 salt0 | SWITCH→1 salt0 +T2 A move0 x16384 salt202495186900453106763512 | move2 x16384 salt202495186900453106763512 B move2 x8192 salt0 | move3 x8192 salt0 +T3 A NO_OP salt845014879727075913631049 | SWITCH→1 salt845014879727075913631049 B SWITCH→3 salt0 | NO_OP salt0 +T4 A move0 x16384 salt303380633387863941492980 | move1 x16384 salt303380633387863941492980 B move2 x8192 salt0 | move0 x4096 salt0 +T5 A NO_OP salt0 | NO_OP salt0 B SWITCH→2 salt0 | NO_OP salt0 +T6 A NO_OP salt830580391542377377074416 | move1 x16384 salt830580391542377377074416 B move1 x8192 salt0 | NO_OP salt0 +T7 A move0 x16384 salt342698529802560752655528 | NO_OP salt342698529802560752655528 B move1 x8192 salt0 | move3 x4096 salt0 +T8 A NO_OP salt982712523374567969471956 | move1 x32768 salt982712523374567969471956 B move1 x8192 salt0 | move3 x4096 salt0 + +Battle 0x6a87950b92eb969ce04a68109fcac3038d26f420234a68132c7bdc7f3e2ac0fc (mainnet) +A @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) +Winner: @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) Turns: 7 +CPU difficulty: hard + +Team A: + Gorillax L12 facet6 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] + Malalien L12 facet6 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] + Sofabbi L12 facet6 moves[Gachachacha, Guest Feature, Unexpected Carrot, Snack Break] + Ekineki L12 facet6 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] +Team B: + Malalien L0 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] + Aurox L0 facet0 moves[Volatile Punch, Gilded Recovery, Iron Wall, Bull Rush] + Gorillax L0 facet0 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] + Volthare L0 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] + +T1 A SWITCH→0 salt706840469641094748672528 | SWITCH→3 salt706840469641094748672528 B SWITCH→0 salt0 | SWITCH→1 salt0 +T2 A move1 x16384 salt384554543562985196003319 | move0 x16384 salt384554543562985196003319 B move2 x4096 salt0 | move3 x4096 salt0 +T3 A SWITCH→1 salt1018655568522462328983640 | NO_OP salt1018655568522462328983640 B SWITCH→3 salt0 | NO_OP salt0 +T4 A move1 x16384 salt1152575408630275207959332 | move0 x16384 salt1152575408630275207959332 B move2 x4096 salt0 | move0 x8192 salt0 +T5 A NO_OP salt0 | NO_OP salt0 B SWITCH→2 salt0 | NO_OP salt0 +T6 A move1 x32768 salt542740429310367792741919 | NO_OP salt542740429310367792741919 B move2 x4096 salt0 | move3 x4096 salt0 +T7 A SWITCH→2 salt44291697099022681926235 | NO_OP salt44291697099022681926235 B NO_OP salt0 | NO_OP salt0 +T8 A move2 x16384 salt715639655745582546424049 | move0 x16384 salt715639655745582546424049 B move2 x4096 salt0 | move3 x8192 salt0 diff --git a/transpiler/strategies-rs/src/arena.rs b/transpiler/strategies-rs/src/arena.rs index f0230177..664c32ed 100644 --- a/transpiler/strategies-rs/src/arena.rs +++ b/transpiler/strategies-rs/src/arena.rs @@ -193,6 +193,8 @@ pub fn build_specs_full( p1_search_peek: false, p0_search_mixed: false, p1_search_mixed: false, + p0_search_opts: Default::default(), + p1_search_opts: Default::default(), }); pair_of.push(pi); } @@ -237,7 +239,6 @@ pub fn run_arena( /// [`DEFAULT_WEIGHTS`]) over `games` matched drafts. Draws excluded from the denominator. Passing /// `DEFAULT_WEIGHTS` at depth 0 reproduces the corresponding STRAT_PAIRS row exactly. #[allow(clippy::too_many_arguments)] -#[allow(clippy::too_many_arguments)] pub fn eval_weights_winrate( roster: &Roster, cand: &Weights, @@ -245,6 +246,8 @@ pub fn eval_weights_winrate( p1_search_peek: bool, p1_strat: BotName, p0_strat: BotName, + p0_search_depth: u32, + cand_opts: crate::search::SearchOpts, games: usize, wseed: u32, seed_base: u32, @@ -256,22 +259,31 @@ pub fn eval_weights_winrate( s.p1_weights = *cand; // candidate under test s.p1_search_depth = p1_search_depth; // 0 = 1-ply greedy; ≥1 = maximin search s.p1_search_peek = p1_search_peek; // peek-at-root best-response + s.p1_search_opts = cand_opts; s.p0_weights = DEFAULT_WEIGHTS; // frozen baseline + s.p0_search_depth = p0_search_depth; // 0 = the named p0 strategy; ≥1 = same-depth baseline search } - let outcomes = run_games(&specs, &book, threads, false); - + // Chunked so long runs stream a running tally to stderr (check-in friendly). + let chunk = (specs.len() / 8).max(50); let (mut p1_wins, mut decisive) = (0u32, 0u32); - for o in &outcomes { - if let Ok(g) = o { - match g.winner_seat { - Some(1) => { - p1_wins += 1; - decisive += 1; + let mut done = 0usize; + for block in specs.chunks(chunk) { + let outcomes = run_games(block, &book, threads, false); + for o in &outcomes { + if let Ok(g) = o { + match g.winner_seat { + Some(1) => { + p1_wins += 1; + decisive += 1; + } + Some(0) => decisive += 1, + _ => {} } - Some(0) => decisive += 1, - _ => {} } } + done += block.len(); + let running = if decisive == 0 { 0.0 } else { p1_wins as f64 * 100.0 / decisive as f64 }; + eprintln!("[{done}/{}] win {running:.1}%", specs.len()); } if decisive == 0 { 0.0 } else { p1_wins as f64 / decisive as f64 } } diff --git a/transpiler/strategies-rs/src/bin/arena.rs b/transpiler/strategies-rs/src/bin/arena.rs index f1ec0251..50290bc6 100644 --- a/transpiler/strategies-rs/src/bin/arena.rs +++ b/transpiler/strategies-rs/src/bin/arena.rs @@ -110,13 +110,28 @@ fn main() { None }; if let Some((label, cand)) = cand { + let base_depth = arg_u(&args, "--base-depth", 0) as u32; + let cand_opts = chomp_strategies::search::SearchOpts { + vetoes: if args.iter().any(|a| a == "--veto-rest") { + chomp_strategies::search::VETO_DRAIN_LOCK_REST + } else { + 0 + }, + settle_rounds: arg_u(&args, "--settle", 0) as u32, + opp_streak: 0, + beam_k: arg_u(&args, "--beam", 0) as usize, + int_actions: arg_u(&args, "--int-actions", 0) as usize, + int_actions_deep: arg_u(&args, "--int-deep", 0) as usize, + }; let started = std::time::Instant::now(); let wr = eval_weights_winrate( - &roster, &cand, search_depth, peek, bots::GREEDY, bots::GREEDY, games, seed, seed_base, threads, + &roster, &cand, search_depth, peek, bots::GREEDY, bots::GREEDY, base_depth, cand_opts, games, seed, + seed_base, threads, ); let elapsed = started.elapsed().as_secs_f64(); let m = if search_depth >= 1 { format!("d{search_depth}-search") } else { "1-ply".to_string() }; - println!("{label} ({m}) p1 vs greedy(default) p0 · {games} games · win {:.1}%", wr * 100.0); + let b = if base_depth >= 1 { format!("d{base_depth}-search(default)") } else { "greedy(default)".to_string() }; + println!("{label} ({m}) p1 vs {b} p0 · {games} games · win {:.1}%", wr * 100.0); eprintln!("{games} games in {elapsed:.2}s ({:.0} games/s)", games as f64 / elapsed); return; } diff --git a/transpiler/strategies-rs/src/bin/cert.rs b/transpiler/strategies-rs/src/bin/cert.rs index dc594a47..4517c9c5 100644 --- a/transpiler/strategies-rs/src/bin/cert.rs +++ b/transpiler/strategies-rs/src/bin/cert.rs @@ -61,6 +61,8 @@ fn main() { p1_search_peek: false, p0_search_mixed: false, p1_search_mixed: false, + p0_search_opts: Default::default(), + p1_search_opts: Default::default(), }; let o = play_game(&spec, &book, false); checked += 1; diff --git a/transpiler/strategies-rs/src/bin/replay.rs b/transpiler/strategies-rs/src/bin/replay.rs index c0e8c74d..4c2585b4 100644 --- a/transpiler/strategies-rs/src/bin/replay.rs +++ b/transpiler/strategies-rs/src/bin/replay.rs @@ -1,6 +1,8 @@ //! Replay mainnet battle logs against the current Rust CPU policies. //! -//! cargo run --release -p chomp-strategies --bin replay -- [--log logs.txt] [--battle N] +//! cargo run --release -p chomp-strategies --bin replay -- [--log ] [--battle N] +//! +//! Defaults to the curated fixture corpus in `strategies-rs/fixtures/`. use chomp_strategies::replay::{parse_logs, replay_doubles, replay_singles}; use chomp_strategies::roster::{self, load_roster}; @@ -15,9 +17,9 @@ fn main() { let chomp_root = std::env::var("CHOMP_ROOT").map(PathBuf::from).unwrap_or_else(|_| { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..").join("..") }); - let log_path = arg(&args, "--log") - .map(PathBuf::from) - .unwrap_or_else(|| chomp_root.join("logs.txt")); + let log_path = arg(&args, "--log").map(PathBuf::from).unwrap_or_else(|| { + chomp_root.join("transpiler/strategies-rs/fixtures/mainnet-2026-07.txt") + }); let only: Option = arg(&args, "--battle").and_then(|v| v.parse().ok()); let text = std::fs::read_to_string(&log_path) diff --git a/transpiler/strategies-rs/src/bin/tracesearch.rs b/transpiler/strategies-rs/src/bin/tracesearch.rs index 0de48d49..5e413970 100644 --- a/transpiler/strategies-rs/src/bin/tracesearch.rs +++ b/transpiler/strategies-rs/src/bin/tracesearch.rs @@ -99,6 +99,8 @@ fn main() { p1_search_peek: peek, p0_search_mixed: false, p1_search_mixed: mixed, + p0_search_opts: Default::default(), + p1_search_opts: Default::default(), }); } diff --git a/transpiler/strategies-rs/src/bot.rs b/transpiler/strategies-rs/src/bot.rs index 931acf67..bc633ac1 100644 --- a/transpiler/strategies-rs/src/bot.rs +++ b/transpiler/strategies-rs/src/bot.rs @@ -100,6 +100,8 @@ pub struct BotConfig { pub search_depth: u32, pub search_peek: bool, pub search_mixed: bool, + /// Root vetoes + leaf settlement for the singles search tier; ignored elsewhere. + pub search_opts: crate::search::SearchOpts, } impl Default for BotConfig { @@ -110,6 +112,7 @@ impl Default for BotConfig { search_depth: 0, search_peek: false, search_mixed: false, + search_opts: crate::search::SearchOpts::default(), } } } diff --git a/transpiler/strategies-rs/src/bots.rs b/transpiler/strategies-rs/src/bots.rs index 07ffd20a..09e5f8a6 100644 --- a/transpiler/strategies-rs/src/bots.rs +++ b/transpiler/strategies-rs/src/bots.rs @@ -259,6 +259,9 @@ pub struct SearchBot { depth: u32, peek: bool, mixed: bool, + opts: crate::search::SearchOpts, + window: [Option; 6], + window_at: usize, } impl Bot for SearchBot { @@ -269,9 +272,27 @@ impl Bot for SearchBot { _ => SEARCH, } } + fn reset(&mut self) { + self.window = [None; 6]; + self.window_at = 0; + } fn decide(&mut self, ctx: &mut DecisionCtx<'_>) -> Action { + if let Some(pm) = ctx.peek { + self.window[self.window_at % 6] = Some(pm); + self.window_at += 1; + } + // Modal count of the revealed move over the last 6 turns — repetition with + // interruptions (pivot resets, Sneak Attack recycling) still registers. + let streak = self + .window + .iter() + .flatten() + .map(|m| self.window.iter().flatten().filter(|x| x == &m).count()) + .max() + .unwrap_or(0) as u32; let pm = ctx.peek.unwrap_or_else(|| Mv { move_index: 0, extra_data: 0 }); - Action::Single(search::decide( + let opts = crate::search::SearchOpts { opp_streak: streak, ..self.opts }; + Action::Single(search::decide_with( ctx.sim, ctx.seat, ctx.singles_view(), @@ -281,6 +302,7 @@ impl Bot for SearchBot { self.peek, self.mixed, ctx.rng, + &opts, )) } } @@ -298,6 +320,9 @@ pub fn build(name: &str, cfg: BotConfig) -> Option> { depth: cfg.search_depth, peek: cfg.search_peek, mixed: cfg.search_mixed, + opts: cfg.search_opts, + window: [None; 6], + window_at: 0, })); } let w = cfg.weights; @@ -314,6 +339,9 @@ pub fn build(name: &str, cfg: BotConfig) -> Option> { depth: if name == SEARCH_D1 { 1 } else { 2 }, peek: cfg.search_peek, mixed: cfg.search_mixed, + opts: cfg.search_opts, + window: [None; 6], + window_at: 0, })), DOUBLES_SEARCH_D1 | DOUBLES_SEARCH_D2 => Some(Box::new(DoublesSearchBot { depth: if name == DOUBLES_SEARCH_D1 { 1 } else { 2 }, @@ -325,6 +353,9 @@ pub fn build(name: &str, cfg: BotConfig) -> Option> { depth: cfg.search_depth.max(1), peek: cfg.search_peek, mixed: cfg.search_mixed, + opts: cfg.search_opts, + window: [None; 6], + window_at: 0, })), DOUBLES_EASY => Some(Box::new(DoublesGreedyBot { name: DOUBLES_EASY, difficulty: Difficulty::Easy })), DOUBLES_MEDIUM => { @@ -391,6 +422,8 @@ mod tests { p1_search_peek: false, p0_search_mixed: false, p1_search_mixed: false, + p0_search_opts: Default::default(), + p1_search_opts: Default::default(), }; let out = play_game(&spec, &book, false); assert!(out.turns > 0, "{name} produced no turns"); diff --git a/transpiler/strategies-rs/src/evaluator.rs b/transpiler/strategies-rs/src/evaluator.rs index 85608024..ca9259fe 100644 --- a/transpiler/strategies-rs/src/evaluator.rs +++ b/transpiler/strategies-rs/src/evaluator.rs @@ -47,8 +47,6 @@ pub const DEFAULT_WEIGHTS: Weights = [ 0.0, // F_STARVED ]; -/// Hell-tier candidate: the baseline with the extension lanes turned on. -pub const HELL_WEIGHTS: Weights = [1.0, 150.0, 0.5, 2.0, 40.0, 30.0, 25.0, 40.0]; /// hp% (0..100) for a slot — pure-view. fn hp_percent(mon: &MonSnap) -> f64 { diff --git a/transpiler/strategies-rs/src/game.rs b/transpiler/strategies-rs/src/game.rs index 26072938..46170005 100644 --- a/transpiler/strategies-rs/src/game.rs +++ b/transpiler/strategies-rs/src/game.rs @@ -68,6 +68,9 @@ pub struct GameSpec { /// pure maximin — mind-games at yomi points, seeded (deterministic per game seed). pub p0_search_mixed: bool, pub p1_search_mixed: bool, + /// Per-seat singles-search shaping (vetoes + settlement); default = shipped search. + pub p0_search_opts: crate::search::SearchOpts, + pub p1_search_opts: crate::search::SearchOpts, } /// One game's generators: one per seat plus the engine's salt stream. @@ -137,6 +140,7 @@ fn init_seats(spec: &GameSpec) -> [SeatState; 2] { search_depth: spec.p0_search_depth, search_peek: spec.p0_search_peek, search_mixed: spec.p0_search_mixed, + search_opts: spec.p0_search_opts, }, ), last_own_move: Mv { move_index: 0, extra_data: 0 }, @@ -151,6 +155,7 @@ fn init_seats(spec: &GameSpec) -> [SeatState; 2] { search_depth: spec.p1_search_depth, search_peek: spec.p1_search_peek, search_mixed: spec.p1_search_mixed, + search_opts: spec.p1_search_opts, }, ), last_own_move: Mv { move_index: 0, extra_data: 0 }, diff --git a/transpiler/strategies-rs/src/replay.rs b/transpiler/strategies-rs/src/replay.rs index e229b390..b2e196a2 100644 --- a/transpiler/strategies-rs/src/replay.rs +++ b/transpiler/strategies-rs/src/replay.rs @@ -307,6 +307,7 @@ pub fn replay_singles( ("heuristic", bots::build(bots::HEURISTIC, singles_cfg(0, false)).unwrap()), ("search-d1", bots::build(bots::SEARCH_D1, singles_cfg(1, true)).unwrap()), ("search-d2", bots::build(bots::SEARCH_D2, singles_cfg(2, true)).unwrap()), + ("search-d3", bots::build(bots::SEARCH, singles_cfg(3, true)).unwrap()), ]; let mut rngs: Vec<_> = (0..policies.len() as u32).map(|i| derive(0xbeef, 100 + i)).collect(); let mut compared = vec![(0u32, 0u32); policies.len()]; @@ -426,9 +427,31 @@ fn singles_cfg(depth: u32, peek: bool) -> BotConfig { search_depth: depth, search_peek: peek, search_mixed: false, + search_opts: env_opts(), } } +/// REPLAY_OPTS="veto-rest,settle=N" applies search shaping to every queried search policy. +fn env_opts() -> crate::search::SearchOpts { + let mut opts = crate::search::SearchOpts::default(); + let Ok(s) = std::env::var("REPLAY_OPTS") else { return opts }; + for tok in s.split(',') { + let tok = tok.trim(); + if tok == "veto-rest" { + opts.vetoes |= crate::search::VETO_DRAIN_LOCK_REST; + } else if let Some(n) = tok.strip_prefix("settle=") { + opts.settle_rounds = n.parse().unwrap_or(0); + } else if let Some(n) = tok.strip_prefix("beam=") { + opts.beam_k = n.parse().unwrap_or(0); + } else if let Some(n) = tok.strip_prefix("int=") { + opts.int_actions = n.parse().unwrap_or(0); + } else if let Some(n) = tok.strip_prefix("intdeep=") { + opts.int_actions_deep = n.parse().unwrap_or(0); + } + } + opts +} + /// REPLAY_WEIGHTS="1,150,..." swaps candidate eval weights into every queried /// policy — the counterfactual side of the loss-replay regression gate. fn env_weights() -> Option { diff --git a/transpiler/strategies-rs/src/search.rs b/transpiler/strategies-rs/src/search.rs index 744ba05a..bd0299c2 100644 --- a/transpiler/strategies-rs/src/search.rs +++ b/transpiler/strategies-rs/src/search.rs @@ -19,7 +19,35 @@ use chomp_rt::B256; use crate::evaluator::{score_state, Weights}; use crate::jsrng::JsRng; use crate::sim::{HypoMove, Sim}; -use crate::view::{apply_hypothetical_from, calculate_valid_moves, capture_view, BattleView, Mv, Seat, NO_OP_INDEX}; +use crate::view::{ + apply_hypothetical_from, calculate_valid_moves, capture_view, mon_current_stamina, switch_flag, + BattleView, Mv, Seat, NO_OP_INDEX, VCPU, +}; + +/// Veto: drop rest while stamina-starved under a repeated revealed opponent move — +/// cross-turn opponent-model information no single-position search can derive. +pub const VETO_DRAIN_LOCK_REST: u32 = 1; + +/// Root-only search shaping: candidate vetoes + leaf settlement. Zero-valued = the +/// shipped search, byte-identical. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct SearchOpts { + pub vetoes: u32, + /// Rest-vs-rest rounds applied at each leaf before scoring, so queued/periodic + /// effects (Q5, burn ticks) materialize inside the eval. With a beam, settlement + /// runs only in the deepened phase (guidance stays at today's cost). + pub settle_rounds: u32, + /// Consecutive turns the opponent has revealed the same move (bot-tracked). + pub opp_streak: u32, + /// Sparse deepening: value every root candidate at depth-1 (the shipped cost), then + /// deepen only the top `beam_k` at full depth. 0 = full-width. + pub beam_k: usize, + /// Interior-node candidate cap for the deepened phase (rest always kept). 0 = full. + pub int_actions: usize, + /// Cap for the DEEPEST interior ply (remaining depth 1). 0 = use `int_actions` — + /// root-adjacent plies keep the wider list where pivot lines carry the value. + pub int_actions_deep: usize, +} /// Hard cap on search depth (ruled 2026-07-12). pub const MAX_DEPTH: u32 = 3; @@ -35,17 +63,20 @@ const SALT: u128 = 0; /// (a shared stream would shift every later node's picks when a branch is skipped). const ENUM_SEED: u32 = 0x5EED; -/// Candidate actions for `seat` at `key`: all moves + all switches + rest, -/// capped at [`MAX_ACTIONS`]. Rest is ALWAYS a candidate — banking stamina is -/// a real line (and the opponent model needs it too: greedy/heuristic rest). -fn candidates(sim: &mut Sim, seat: Seat, key: B256) -> Vec { +/// Candidate actions for `seat` at `key`: all moves + all switches + rest, capped at +/// `cap` (0 = [`MAX_ACTIONS`]). Rest is ALWAYS kept — banking stamina is a real line +/// (and the opponent model needs it too: greedy/heuristic rest); the cap trims +/// trailing switches, never moves or rest. +fn candidates(sim: &mut Sim, seat: Seat, key: B256, cap: usize) -> Vec { + let cap = if cap == 0 { MAX_ACTIONS } else { cap.min(MAX_ACTIONS) }; let mut local = JsRng::new(ENUM_SEED); let v = calculate_valid_moves(sim, seat, key, &mut local); - let mut out: Vec = Vec::with_capacity(v.moves.len() + v.switches.len() + v.no_op.len()); + let mut out: Vec = Vec::with_capacity(cap); out.extend(v.moves.iter().copied()); out.extend(v.switches.iter().copied()); + out.truncate(cap.saturating_sub(v.no_op.len()).max(1)); out.extend(v.no_op.iter().copied()); - out.truncate(MAX_ACTIONS); + out.truncate(cap); out } @@ -61,15 +92,15 @@ fn step(sim: &mut Sim, seat: Seat, key: B256, my: Option, opp: Option) - /// The two sides' action lists at `key` given the (virtual) switch flag: /// 0 = opp-only acts, 1 = CPU-only acts, 2 = both. A non-acting side is `[None]`. -fn action_lists(sim: &mut Sim, seat: Seat, key: B256, flag: u8) -> (Vec>, Vec>) { +fn action_lists(sim: &mut Sim, seat: Seat, key: B256, flag: u8, cap: usize) -> (Vec>, Vec>) { let my = if flag != 0 { - candidates(sim, seat, key).into_iter().map(Some).collect::>() + candidates(sim, seat, key, cap).into_iter().map(Some).collect::>() } else { vec![None] }; let opp = if flag != 1 { let opp_seat = Seat { cpu: 1 - seat.cpu }; - candidates(sim, opp_seat, key).into_iter().map(Some).collect::>() + candidates(sim, opp_seat, key, cap).into_iter().map(Some).collect::>() } else { vec![None] }; @@ -78,8 +109,42 @@ fn action_lists(sim: &mut Sim, seat: Seat, key: B256, flag: u8) -> (Vec f64 { +/// Score a leaf, optionally after `settle_rounds` rest-vs-rest turns (stops early on a +/// forced-switch flag or terminal — the settled score's HP/KO terms then carry the payoff). +fn leaf_score(sim: &mut Sim, seat: Seat, key: B256, w: &Weights, settle_rounds: u32) -> f64 { + let rest = Mv { move_index: NO_OP_INDEX, extra_data: 0 }; + let mut at = key; + let mut owned: Vec = Vec::new(); + for _ in 0..settle_rounds { + if switch_flag(sim, seat, at) != 2 { + break; + } + let child = step(sim, seat, at, Some(rest), Some(rest)); + owned.push(child); + at = child; + let v = capture_view(sim, seat, at); + let cpu_dead = v.p1.len() as i64 - (v.cpu_ko & 0xff).count_ones() as i64 <= 0; + let opp_dead = v.p0.len() as i64 - (v.opp_ko & 0xff).count_ones() as i64 <= 0; + if cpu_dead || opp_dead { + break; + } + } + let score = { + let view = capture_view(sim, seat, at); + score_state(sim, seat, &view, w) + }; + for f in owned.into_iter().rev() { + sim.dispose_fork(f); + } + score +} + +/// Recursive maximin value of the position at `key`, CPU-perspective. `int_cap` bounds +/// interior candidate lists (0 = full width). +fn value( + sim: &mut Sim, seat: Seat, key: B256, w: &Weights, depth: u32, settle_rounds: u32, int_cap: usize, + int_cap_deep: usize, +) -> f64 { let view = capture_view(sim, seat, key); let cpu_alive = view.p1.len() as i64 - (view.cpu_ko & 0xff).count_ones() as i64; let opp_alive = view.p0.len() as i64 - (view.opp_ko & 0xff).count_ones() as i64; @@ -92,16 +157,17 @@ fn value(sim: &mut Sim, seat: Seat, key: B256, w: &Weights, depth: u32) -> f64 { return LOSS - depth as f64; } if depth == 0 { - return score_state(sim, seat, &view, w); + return leaf_score(sim, seat, key, w, settle_rounds); } - let (my, opp) = action_lists(sim, seat, key, view.switch_flag); + let cap = if depth >= 2 || int_cap_deep == 0 { int_cap } else { int_cap_deep }; + let (my, opp) = action_lists(sim, seat, key, view.switch_flag, cap); let mut best = f64::NEG_INFINITY; for a in &my { let mut worst = f64::INFINITY; for o in &opp { let child = step(sim, seat, key, *a, *o); - let v = value(sim, seat, child, w, depth - 1); + let v = value(sim, seat, child, w, depth - 1, settle_rounds, int_cap, int_cap_deep); sim.dispose_fork(child); if v < worst { worst = v; @@ -117,6 +183,24 @@ fn value(sim: &mut Sim, seat: Seat, key: B256, w: &Weights, depth: u32) -> f64 { best } +/// Root-only candidate vetoes. Never empties the list — if every candidate would be +/// dropped, the original list stands. +fn apply_vetoes(sim: &mut Sim, seat: Seat, view: &BattleView, my: Vec>, opts: &SearchOpts) -> Vec> { + if opts.vetoes & VETO_DRAIN_LOCK_REST == 0 || opts.opp_streak < 3 { + return my; + } + let starved = mon_current_stamina(sim, seat, view.bk, VCPU, view.cpu_active) < 3; + if !starved { + return my; + } + let kept: Vec> = my + .iter() + .copied() + .filter(|m| !matches!(m, Some(m) if m.move_index == NO_OP_INDEX)) + .collect(); + if kept.is_empty() { my } else { kept } +} + /// A safe legal move for the fallback path (first move → switch → rest). fn fallback(sim: &mut Sim, seat: Seat, view: &BattleView, rng: &mut JsRng) -> Mv { if view.switch_flag == 0 { @@ -175,8 +259,16 @@ fn nash_mix(g: &[Vec], iters: u32) -> Vec { /// one decision and fall back to a legal move. Leaked forks are reclaimed when the world drops. #[allow(clippy::too_many_arguments)] pub fn decide(sim: &mut Sim, seat: Seat, view: &BattleView, pm: Mv, w: &Weights, depth: u32, peek: bool, mixed: bool, rng: &mut JsRng) -> Mv { + decide_with(sim, seat, view, pm, w, depth, peek, mixed, rng, &SearchOpts::default()) +} + +#[allow(clippy::too_many_arguments)] +pub fn decide_with( + sim: &mut Sim, seat: Seat, view: &BattleView, pm: Mv, w: &Weights, depth: u32, peek: bool, mixed: bool, + rng: &mut JsRng, opts: &SearchOpts, +) -> Mv { let saved_fc = sim.fork_counter(); - match catch_unwind(AssertUnwindSafe(|| decide_inner(sim, seat, view, pm, w, depth, peek, mixed, rng))) { + match catch_unwind(AssertUnwindSafe(|| decide_inner(sim, seat, view, pm, w, depth, peek, mixed, rng, opts))) { Ok(mv) => mv, Err(_) => { sim.set_fork_counter(saved_fc); @@ -186,7 +278,10 @@ pub fn decide(sim: &mut Sim, seat: Seat, view: &BattleView, pm: Mv, w: &Weights, } #[allow(clippy::too_many_arguments)] -fn decide_inner(sim: &mut Sim, seat: Seat, view: &BattleView, pm: Mv, w: &Weights, depth: u32, peek: bool, mixed: bool, rng: &mut JsRng) -> Mv { +fn decide_inner( + sim: &mut Sim, seat: Seat, view: &BattleView, pm: Mv, w: &Weights, depth: u32, peek: bool, mixed: bool, + rng: &mut JsRng, opts: &SearchOpts, +) -> Mv { let depth = depth.clamp(1, MAX_DEPTH); let saved_fc = sim.fork_counter(); let key = view.bk; @@ -197,18 +292,66 @@ fn decide_inner(sim: &mut Sim, seat: Seat, view: &BattleView, pm: Mv, w: &Weight return Mv { move_index: NO_OP_INDEX, extra_data: 0 }; } - let (my, opp_full) = action_lists(sim, seat, key, view.switch_flag); + let (my, opp_full) = action_lists(sim, seat, key, view.switch_flag, 0); + let my = apply_vetoes(sim, seat, view, my, opts); // Peek-at-root: the opponent's move IS revealed this turn (`pm`) → best-respond to it (a single // opponent action), maximin only deeper. Without peek, the full no-peek grid at the root too. let opp = if peek && view.switch_flag != 1 { vec![Some(pm)] } else { opp_full }; + // Sparse deepening: guidance-value every root row at depth-1 (full-width interiors, no + // settlement — the shipped search's exact cost), then re-value only the top `beam_k` + // rows at full depth with lean interiors + settlement. The guidance argmax is always + // in the beam, and the final pick is the argmax among deepened rows only. + if opts.beam_k > 0 && depth >= 2 && !mixed { + let mut guided: Vec<(usize, f64)> = Vec::with_capacity(my.len()); + for (i, a) in my.iter().enumerate() { + let mut worst = f64::INFINITY; + for o in &opp { + let child = step(sim, seat, key, *a, *o); + let v = value(sim, seat, child, w, depth - 2, 0, 0, 0); + sim.dispose_fork(child); + if v < worst { + worst = v; + } + } + guided.push((i, worst)); + } + guided.sort_by(|a, b| b.1.total_cmp(&a.1)); + guided.truncate(opts.beam_k.max(1)); + + let mut best = my[guided[0].0].unwrap_or(Mv { move_index: NO_OP_INDEX, extra_data: 0 }); + let mut best_val = f64::NEG_INFINITY; + for &(i, _) in &guided { + let mut worst = f64::INFINITY; + for o in &opp { + let child = step(sim, seat, key, my[i], *o); + let v = value(sim, seat, child, w, depth - 1, opts.settle_rounds, opts.int_actions, opts.int_actions_deep); + sim.dispose_fork(child); + if v < worst { + worst = v; + } + if worst <= best_val { + break; // argmax-invariant row prune + } + } + if worst > best_val { + best_val = worst; + if let Some(m) = my[i] { + best = m; + } + } + } + sim.set_fork_counter(saved_fc); + return best; + } + // Mixed play needs the FULL root grid (Nash weighs every cell) — no pruning here. if mixed && my.len() > 1 && opp.len() > 1 { let mut grid = vec![vec![0.0f64; opp.len()]; my.len()]; for (i, a) in my.iter().enumerate() { for (j, o) in opp.iter().enumerate() { let child = step(sim, seat, key, *a, *o); - grid[i][j] = value(sim, seat, child, w, depth - 1); + grid[i][j] = value(sim, seat, child, w, depth - 1, opts.settle_rounds, 0, 0); sim.dispose_fork(child); } } @@ -233,7 +376,7 @@ fn decide_inner(sim: &mut Sim, seat: Seat, view: &BattleView, pm: Mv, w: &Weight let mut worst = f64::INFINITY; for o in &opp { let child = step(sim, seat, key, *a, *o); - let v = value(sim, seat, child, w, depth - 1); + let v = value(sim, seat, child, w, depth - 1, opts.settle_rounds, 0, 0); sim.dispose_fork(child); if v < worst { worst = v; diff --git a/transpiler/strategies-rs/src/teams.rs b/transpiler/strategies-rs/src/teams.rs index bddcdd65..55bda34d 100644 --- a/transpiler/strategies-rs/src/teams.rs +++ b/transpiler/strategies-rs/src/teams.rs @@ -86,6 +86,8 @@ pub fn run_team_search(roster: &Roster, games_per_team: usize, wseed: u32, seed_ p1_search_peek: false, p0_search_mixed: false, p1_search_mixed: false, + p0_search_opts: Default::default(), + p1_search_opts: Default::default(), }); team_of.push(ti); counter = counter.wrapping_add(1); From dfbb6becff9100102f4b204d5a22cdf21d8a3d47 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:35:30 -0700 Subject: [PATCH 32/41] wip ts-specific optimizations --- docs/mons.md | 31 +++- src/mons/glob/ChosenOne.sol | 39 +++++ transpiler/codegen/contract.py | 4 +- transpiler/codegen/generator.py | 7 +- transpiler/codegen/imports.py | 21 ++- transpiler/codegen/type_converter.py | 21 ++- transpiler/codegen/yul.py | 67 ++++++--- transpiler/runtime/base.ts | 133 ++++++++++++++--- transpiler/runtime/index.ts | 8 +- .../fixtures/mainnet-2026-07.txt | 135 ------------------ transpiler/test_transpiler.py | 6 +- 11 files changed, 266 insertions(+), 206 deletions(-) create mode 100644 src/mons/glob/ChosenOne.sol delete mode 100644 transpiler/strategies-rs/fixtures/mainnet-2026-07.txt diff --git a/docs/mons.md b/docs/mons.md index 5de855ef..9d8c4dee 100644 --- a/docs/mons.md +++ b/docs/mons.md @@ -1,25 +1,44 @@ # Glob Liquid / Faith -Physical attacker, low health, medium defense +Physical attacker, low health, medium defense, low speed -Ability: +Ability: Chosen One +Enters with Blessed first time on swap in. Holy Molt +1 priority, 0 stamina, halves health, applies Bless on self costs 1 stamina more per time used, up to max of 3 Water Claw +80, liquid Deals damage -Truth Claw +Holy Water Claw +80, faith +Deals damage -??? +Get Slippery +Boosts atk/speed 50% # Mulong Air / Lightning -Special attacker, high health, low speed +High speed, low physical defense, low sp def, med hp, high sp atk + +Ability: Imposing Aura +For 3 turns (starting from swap in), Mulong takes half damage and deals half damage + +Summon Storm: +Summons Storm, a battlefield effect that lasts for 7 turns and damages all mons at end of turn for Electric damage + +King's Respite: +The next time Mulong rests, deal massive Air damage to the target slot. + +Thunderclap: +Deals damage, % to Zap -Ability: Sky Ruler \ No newline at end of file +Sky Strike: +Deals Air damage, % to deal Frostbite. +If Storm is active 50% more damage, high % to inflict Frosbite \ No newline at end of file diff --git a/src/mons/glob/ChosenOne.sol b/src/mons/glob/ChosenOne.sol new file mode 100644 index 00000000..b4df5646 --- /dev/null +++ b/src/mons/glob/ChosenOne.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +// @inline-ability: singleton-local + +import {IEngine} from "../../IEngine.sol"; +import {EffectInstance, StatBoostToApply} from "../../Structs.sol"; +import {IAbility} from "../../abilities/IAbility.sol"; +import {BasicEffect} from "../../effects/BasicEffect.sol"; +import {IEffect} from "../../effects/IEffect.sol"; + +contract ChosenOne is IAbility { + + uint64 constant MAX_MON_INDEX = 8; + + IEffect immutable BLESSED_STATUS; + + constructor(IEffect _BLESSED_STATUS) { + BLESSED_STATUS = _BLESSED_STATUS; + } + + function name() public pure override returns (string memory) { + return "Chosen One"; + } + + function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { + // Check if global KV has already been set + if (engine.getGlobalKV(battleKey, _getChosenOneKey(playerIndex, monIndex)) == 1) { + return; + } + engine.addEffect(playerIndex, monIndex, BLESSED_STATUS, bytes32(0)); + engine.setGlobalKV(_getChosenOneKey(playerIndex, monIndex), 1); + } + + function _getChosenOneKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64 scaledKey) { + uint64 scaledKey = (uint64(playerIndex) * MAX_MON_INDEX) + monIndex; + } +} \ No newline at end of file diff --git a/transpiler/codegen/contract.py b/transpiler/codegen/contract.py index c0470cd2..2287888e 100644 --- a/transpiler/codegen/contract.py +++ b/transpiler/codegen/contract.py @@ -578,7 +578,7 @@ def _generate_public_mapping_getter( key_ts = self._type_converter.solidity_type_to_ts(key_tn) param = f'key{i + 1}' if len(keys) > 1 else 'key' params.append(f'{param}: {key_ts}') - indexer = f'String({param})' if key_ts == 'bigint' else param + indexer = f'__ik({param})' if key_ts == 'bigint' else param access += f'[{indexer}]' if i == 0 else f'?.[{indexer}]' default = self._type_converter.default_value(value_ts, value_tn) @@ -640,7 +640,7 @@ def _generate_mapping_mutator( key_params.append(f'{key_name}: {key_ts_type}') # Convert non-string keys to string for Record indexing - key_access = key_name if key_ts_type == 'string' else f'String({key_name})' + key_access = key_name if key_ts_type == 'string' else f'__ik({key_name})' if current_type.value_type.is_mapping: null_coalesce_lines.append( diff --git a/transpiler/codegen/generator.py b/transpiler/codegen/generator.py index 33d2f497..975fd681 100644 --- a/transpiler/codegen/generator.py +++ b/transpiler/codegen/generator.py @@ -139,10 +139,13 @@ def generate(self, ast: SourceUnit) -> str: for contract in ast.contracts: output.append(self._contract_generator.generate_contract(contract)) - # Scan generated content to determine which viem imports are actually used + # Scan generated content to determine which viem imports are actually used. + # '__ik' rides this scan too: it's not a viem name, but the RUNTIME_WRAPPED_VIEM + # routing in imports.py already moves scanned names onto the runtime import line. content = '\n'.join(output) for viem_fn in ('keccak256', 'encodePacked', 'encodeAbiParameters', - 'decodeAbiParameters', 'parseAbiParameters', 'stringToHex'): + 'decodeAbiParameters', 'parseAbiParameters', 'stringToHex', + '__ik'): if viem_fn in content: self._ctx.viem_imports_used.add(viem_fn) diff --git a/transpiler/codegen/imports.py b/transpiler/codegen/imports.py index ece82133..5b57b3b5 100644 --- a/transpiler/codegen/imports.py +++ b/transpiler/codegen/imports.py @@ -11,6 +11,11 @@ if TYPE_CHECKING: from .context import CodeGenerationContext +# viem names the runtime re-exports with memoized / fast-path wrappers (byte-identical +# outputs); generated files import these from the runtime, not from viem. '__ik' (the +# interned mapping-key helper) is runtime-only but routed through the same mechanism. +RUNTIME_WRAPPED_VIEM = frozenset({'keccak256', 'encodePacked', 'encodeAbiParameters', '__ik'}) + class ImportGenerator: """ @@ -47,12 +52,15 @@ def generate(self, contract_name: str = '') -> str: lines = [] - # viem imports (only what's actually used) + # viem imports (only what's actually used). Names in RUNTIME_WRAPPED_VIEM are + # emitted from the runtime instead — it wraps them with memoized / fast-path + # equivalents (byte-identical outputs). if self._ctx.viem_imports_used: - viem_imports = sorted(self._ctx.viem_imports_used) - lines.append( - f"import {{ {', '.join(viem_imports)} }} from 'viem';" - ) + viem_imports = sorted(self._ctx.viem_imports_used - RUNTIME_WRAPPED_VIEM) + if viem_imports: + lines.append( + f"import {{ {', '.join(viem_imports)} }} from 'viem';" + ) # Runtime imports runtime_imports = self._build_runtime_imports() @@ -93,6 +101,9 @@ def _build_runtime_imports(self) -> List[str]: 'ecrecover', 'selfdestruct', ] + # Wrapped viem names route through the runtime (memoized keccak256, fast ABI encoders) + imports.extend(sorted(self._ctx.viem_imports_used & RUNTIME_WRAPPED_VIEM)) + # Add set types if used if self._ctx.set_types_used: imports.extend(sorted(self._ctx.set_types_used)) diff --git a/transpiler/codegen/type_converter.py b/transpiler/codegen/type_converter.py index d79a20de..71707231 100644 --- a/transpiler/codegen/type_converter.py +++ b/transpiler/codegen/type_converter.py @@ -361,7 +361,7 @@ def generate_type_cast( # Solidity truncates on cast; BigInt does not, so we must mask explicitly. if type_name.startswith('uint') or type_name.startswith('int'): expr = generate_expression_fn(inner_expr) - bigint_expr = self._ensure_bigint(expr) + bigint_expr = self._ensure_bigint(expr, inner_expr) # Extract bit width (e.g., 'uint160' -> 160, 'int32' -> 32) width_str = type_name[4:] if type_name.startswith('uint') else type_name[3:] if width_str.isdigit(): @@ -379,8 +379,7 @@ def generate_type_cast( # Default: generate the inner expression return generate_expression_fn(inner_expr) - @staticmethod - def _ensure_bigint(expr: str) -> str: + def _ensure_bigint(self, expr: str, node: 'Expression | None' = None) -> str: """Wrap expression in BigInt() only if it's not already a bigint expression. Avoids redundant BigInt(BigInt(x)) patterns in generated code. @@ -394,6 +393,16 @@ def _ensure_bigint(expr: str) -> str: # Already a bigint expression (mask, shift, or other bitwise op) if expr.startswith('(') and ('n)' in expr or '1n' in expr): return expr + # Statically bigint by type: a uint/int identifier, or a member access that + # resolves to a numeric field (both transpile to bigint). + if node is not None: + if isinstance(node, Identifier) and self.is_bigint_typed_identifier(node): + return expr + if isinstance(node, MemberAccess): + resolved = self.resolve_access_type(node) + if (resolved and not resolved.is_array + and (resolved.name.startswith('uint') or resolved.name.startswith('int'))): + return expr return f'BigInt({expr})' def _is_already_address_type(self, expr: Expression) -> bool: @@ -710,10 +719,10 @@ def convert_index( """Convert an index expression to match the container key type. Mappings transpile to ``Record`` and preserve bigint - precision via ``String(idx)``. Arrays use ``Number(idx)`` because TS - array indices are numeric. + precision via ``__ik(idx)`` (interned small-index String). Arrays use + ``Number(idx)`` because TS array indices are numeric. """ - wrap = 'String' if mapping_access else 'Number' + wrap = '__ik' if mapping_access else 'Number' if index.startswith('BigInt('): inner = index[7:-1] diff --git a/transpiler/codegen/yul.py b/transpiler/codegen/yul.py index b32ed466..8bedff92 100644 --- a/transpiler/codegen/yul.py +++ b/transpiler/codegen/yul.py @@ -8,9 +8,13 @@ of nested constructs (if blocks, for loops, switch/case, nested function calls). """ +import re from typing import Dict, List, Optional, Tuple from dataclasses import dataclass, field +# Bigint literals (decimal or hex) — statically bigint, never need a BigInt() wrap. +_BIGINT_LITERAL_RE = re.compile(r'(?:0x[0-9a-fA-F]+|\d+)n') + # ============================================================================= # YUL AST NODES @@ -1081,6 +1085,23 @@ def _generate_identifier(self, ident: YulIdentifier, slot_vars: Dict[str, str]) return f'Constants.{name}' return name + def _bi(self, expr: str) -> str: + """Wrap `expr` in BigInt() unless it is statically bigint-typed. + + Provably-bigint shapes from this generator: bigint literals, the parenthesized + outputs of the yul op emitters, BigInt(...) conversions, and _storageRead loads. + Bare identifiers and Constants.* stay wrapped — a constant may be a string. + """ + if _BIGINT_LITERAL_RE.fullmatch(expr) is not None: + return expr + if expr.startswith('BigInt'): + return expr + if expr.startswith('this._storageRead('): + return expr + if expr.startswith('(') and expr.endswith(')') and ' as any' not in expr: + return expr + return f'BigInt({expr})' + def _generate_function_call( self, call: YulFunctionCall, @@ -1107,50 +1128,50 @@ def _generate_function_call( # Binary: (BigInt(left) op BigInt(right)) if func in _BINARY_OPS and len(args) == 2: - left = self._generate_expression(args[0], slot_vars) - right = self._generate_expression(args[1], slot_vars) - return f'(BigInt({left}) {_BINARY_OPS[func]} BigInt({right}))' + left = self._bi(self._generate_expression(args[0], slot_vars)) + right = self._bi(self._generate_expression(args[1], slot_vars)) + return f'({left} {_BINARY_OPS[func]} {right})' # Ternary mod: ((BigInt(a) op BigInt(b)) % BigInt(m)) if func in _TERNARY_MOD_OPS and len(args) == 3: - a = self._generate_expression(args[0], slot_vars) - b = self._generate_expression(args[1], slot_vars) - m = self._generate_expression(args[2], slot_vars) - return f'((BigInt({a}) {_TERNARY_MOD_OPS[func]} BigInt({b})) % BigInt({m}))' + a = self._bi(self._generate_expression(args[0], slot_vars)) + b = self._bi(self._generate_expression(args[1], slot_vars)) + m = self._bi(self._generate_expression(args[2], slot_vars)) + return f'(({a} {_TERNARY_MOD_OPS[func]} {b}) % {m})' # Unary not if func == 'not' and len(args) >= 1: - operand = self._generate_expression(args[0], slot_vars) - return f'(~BigInt({operand}))' + operand = self._bi(self._generate_expression(args[0], slot_vars)) + return f'(~{operand})' # Shift: args are (shift_amount, value) if func in _SHIFT_OPS and len(args) == 2: - shift = self._generate_expression(args[0], slot_vars) - val = self._generate_expression(args[1], slot_vars) - return f'(BigInt({val}) {_SHIFT_OPS[func]} BigInt({shift}))' + shift = self._bi(self._generate_expression(args[0], slot_vars)) + val = self._bi(self._generate_expression(args[1], slot_vars)) + return f'({val} {_SHIFT_OPS[func]} {shift})' # byte extraction if func == 'byte' and len(args) == 2: - pos = self._generate_expression(args[0], slot_vars) - val = self._generate_expression(args[1], slot_vars) - return f'((BigInt({val}) >> (BigInt(248) - BigInt({pos}) * 8n)) & 0xFFn)' + pos = self._bi(self._generate_expression(args[0], slot_vars)) + val = self._bi(self._generate_expression(args[1], slot_vars)) + return f'(({val} >> (248n - {pos} * 8n)) & 0xFFn)' # signextend if func == 'signextend' and len(args) == 2: - b = self._generate_expression(args[0], slot_vars) - val = self._generate_expression(args[1], slot_vars) - return f'BigInt.asIntN(Number(BigInt({b}) + 1n) * 8, BigInt({val}))' + b = self._bi(self._generate_expression(args[0], slot_vars)) + val = self._bi(self._generate_expression(args[1], slot_vars)) + return f'BigInt.asIntN(Number({b} + 1n) * 8, {val})' # Comparison: (BigInt(left) op BigInt(right) ? 1n : 0n) if func in _COMPARISON_OPS and len(args) == 2: - left = self._generate_expression(args[0], slot_vars) - right = self._generate_expression(args[1], slot_vars) - return f'(BigInt({left}) {_COMPARISON_OPS[func]} BigInt({right}) ? 1n : 0n)' + left = self._bi(self._generate_expression(args[0], slot_vars)) + right = self._bi(self._generate_expression(args[1], slot_vars)) + return f'({left} {_COMPARISON_OPS[func]} {right} ? 1n : 0n)' # iszero if func == 'iszero' and len(args) >= 1: - operand = self._generate_expression(args[0], slot_vars) - return f'(BigInt({operand}) === 0n ? 1n : 0n)' + operand = self._bi(self._generate_expression(args[0], slot_vars)) + return f'({operand} === 0n ? 1n : 0n)' # Memory/calldata zero placeholders if func in _ZERO_OPS: diff --git a/transpiler/runtime/base.ts b/transpiler/runtime/base.ts index afc0083b..913917c7 100644 --- a/transpiler/runtime/base.ts +++ b/transpiler/runtime/base.ts @@ -8,7 +8,95 @@ * index.ts re-exports it; it does NOT define a second Contract class. */ -import { keccak256, encodePacked, toHex, hexToBigInt } from 'viem'; +import { + keccak256 as viemKeccak256, + encodePacked as viemEncodePacked, + encodeAbiParameters as viemEncodeAbiParameters, + toHex, + hexToBigInt, +} from 'viem'; + +/** Memoized keccak256 for hex inputs — sim keys (effect ids, mapping slots) repeat massively. */ +const KECCAK_CACHE = new Map(); +export function keccak256(value: `0x${string}` | Uint8Array): `0x${string}` { + if (typeof value !== 'string') return viemKeccak256(value); + let h = KECCAK_CACHE.get(value); + if (h === undefined) { + if (KECCAK_CACHE.size > 50_000) KECCAK_CACHE.clear(); + h = viemKeccak256(value); + KECCAK_CACHE.set(value, h); + } + return h; +} + +// ============================================================================= +// INTERNED MAPPING KEYS +// ============================================================================= +// Mapping keys transpile to `String(bigintIdx)` — an allocation per access. Indices are +// tiny (mon/slot/effect ids), so a 256-entry table returns the same string alloc-free. + +const IK_TABLE: string[] = Array.from({ length: 256 }, (_, i) => String(i)); + +export function __ik(v: bigint | number): string { + const n = Number(v); + return n >= 0 && n < 256 ? IK_TABLE[n] : String(v); +} + +// ============================================================================= +// FAST ABI ENCODERS +// ============================================================================= +// Byte-identical fast paths for the static shapes the generated code hot loops emit +// (uintN / intN / bool / already-lowercase bytes32 & address). Anything else — dynamic +// types, mixed-case hex, unexpected value kinds — falls back to viem wholesale. + +const UINT_RE = /^u?int(\d*)$/; +const HEX32_RE = /^0x[0-9a-f]{64}$/; +const ADDR_RE = /^0x[0-9a-f]{40}$/; + +export function encodePacked(types: readonly string[], values: readonly unknown[]): `0x${string}` { + let out = '0x'; + for (let i = 0; i < types.length; i++) { + const t = types[i]; + const v = values[i]; + const m = UINT_RE.exec(t); + if (m !== null && (typeof v === 'bigint' || typeof v === 'number')) { + const bits = m[1] === '' ? 256 : Number(m[1]); + out += BigInt.asUintN(bits, BigInt(v)).toString(16).padStart(bits >> 2, '0'); + } else if (t === 'bytes32' && typeof v === 'string' && HEX32_RE.test(v)) { + out += v.slice(2); + } else if (t === 'address' && typeof v === 'string' && ADDR_RE.test(v)) { + out += v.slice(2); + } else if (t === 'bool') { + out += v ? '01' : '00'; + } else { + return viemEncodePacked(types as never, values as never); + } + } + return out as `0x${string}`; +} + +export function encodeAbiParameters(params: unknown, values: readonly unknown[]): `0x${string}` { + if (Array.isArray(params)) { + let out = '0x'; + for (let i = 0; i < params.length; i++) { + const t = (params[i] as { type?: string })?.type ?? ''; + const v = values[i]; + if (UINT_RE.test(t) && (typeof v === 'bigint' || typeof v === 'number')) { + out += BigInt.asUintN(256, BigInt(v)).toString(16).padStart(64, '0'); + } else if (t === 'bytes32' && typeof v === 'string' && HEX32_RE.test(v)) { + out += v.slice(2); + } else if (t === 'address' && typeof v === 'string' && ADDR_RE.test(v)) { + out += v.slice(2).padStart(64, '0'); + } else if (t === 'bool') { + out += (v ? '1' : '0').padStart(64, '0'); + } else { + return viemEncodeAbiParameters(params as never, values as never) as `0x${string}`; + } + } + return out as `0x${string}`; + } + return viemEncodeAbiParameters(params as never, values as never) as `0x${string}`; +} // ============================================================================= // CONSTANTS @@ -856,43 +944,44 @@ export abstract class Contract { } protected _storageRead(key: any): bigint { + if (key !== null && typeof key === 'object') return (key as any)[YUL_SLOT_WORD] ?? 0n; return this._storage.sload(this._yulStorageKey(key)); } protected _storageWrite(key: any, value: bigint): void { + if (key !== null && typeof key === 'object') { + (key as any)[YUL_SLOT_WORD] = value; + return; + } this._storage.sstore(this._yulStorageKey(key), value); } - /** Stable synthetic slot key for a storage struct (Solidity `x.slot`), holding the packed - * bits no modelled property owns. Keyed by an identity symbol so a proxied struct and its - * raw target agree. `_yul`-prefixed to stay clear of contract method names. */ - protected _yulSlotKeyOf(ref: any): string { + /** Synthetic slot for a storage struct (Solidity `x.slot`), holding the packed bits no + * modelled property owns. The word lives ON the struct under a symbol (both proxy layers + * pass symbols through to the target, so a proxied struct and its raw target agree), and + * dies with it — search forks can't accumulate orphaned words in `_storage`. */ + protected _yulSlotKeyOf(ref: any): any { if (ref === null || ref === undefined || typeof ref !== 'object') { return this._yulStorageKey(ref); } - let id = (ref as any)[YUL_SLOT_ID]; - if (id === undefined) { - id = `yulslot#${_nextYulSlotId++}`; - Object.defineProperty(ref, YUL_SLOT_ID, { value: id, enumerable: false }); - } - return id; + return ref; } /** - * Fork support: `dst` is a structural clone of `src`, but the identity stamp is - * non-enumerable so the clone can't inherit it — without this, a fork's struct reads a - * ZERO packed word (lost effect compact data / monState top bits). Give the clone its - * own key and copy the word: reads agree, writes stay isolated from the live battle. + * Fork support: `dst` is a structural clone of `src`, but the word is symbol-keyed so + * `Object.keys` cloning drops it — without this, a fork's struct reads a ZERO packed word + * (lost effect compact data / monState top bits). Copy it onto the clone: reads agree, + * writes stay isolated from the live battle. */ cloneYulSlotWord(src: any, dst: any): void { if (src === null || typeof src !== 'object' || dst === null || typeof dst !== 'object') return; - const id = (src as any)[YUL_SLOT_ID]; - if (id === undefined) return; - const w = this._storage.sload(this._yulStorageKey(id)); - if (w !== 0n) this._storage.sstore(this._yulStorageKey(this._yulSlotKeyOf(dst)), w); + const w = (src as any)[YUL_SLOT_WORD]; + if (w !== undefined && w !== 0n) (dst as any)[YUL_SLOT_WORD] = w; + // A pooled/reused dst may carry a stale word from its previous life — reads treat 0n as + // absent, so zeroing (not deleting) keeps the clone faithful without a shape change. + else if ((dst as any)[YUL_SLOT_WORD] !== undefined) (dst as any)[YUL_SLOT_WORD] = 0n; } } -/** Identity stamp for `_yulSlotKeyOf` — a symbol, so it can't collide with a field name. */ -const YUL_SLOT_ID = Symbol('yulSlotId'); -let _nextYulSlotId = 0; +/** Key for the yul slot word — a symbol, so it can't collide with a field name. */ +const YUL_SLOT_WORD = Symbol('yulSlotWord'); diff --git a/transpiler/runtime/index.ts b/transpiler/runtime/index.ts index 015f6dd1..821731cf 100644 --- a/transpiler/runtime/index.ts +++ b/transpiler/runtime/index.ts @@ -5,7 +5,8 @@ * including storage simulation, bit manipulation, and type utilities. */ -import { keccak256, encodePacked, encodeAbiParameters, parseAbiParameters, toHex, hexToBigInt, sha256 as viemSha256 } from 'viem'; +import { parseAbiParameters, toHex, hexToBigInt, sha256 as viemSha256 } from 'viem'; +import { keccak256, encodePacked, encodeAbiParameters } from './base'; // Note: Contract, Storage, EventStream, and globalEventStream are defined in ./base // and re-exported here. Runtime replacement modules (Ownable, etc.) import @@ -248,7 +249,7 @@ export function uintToBytes32(value: bigint): string { // HASH FUNCTIONS // ============================================================================= -export { keccak256 } from 'viem'; +export { keccak256 } from './base'; // sha256 is defined at the top of the file with Node.js crypto @@ -256,7 +257,8 @@ export { keccak256 } from 'viem'; // ABI ENCODING // ============================================================================= -export { encodePacked, encodeAbiParameters, parseAbiParameters } from 'viem'; +export { encodePacked, encodeAbiParameters, __ik } from './base'; +export { parseAbiParameters } from 'viem'; /** * Simple ABI encode for common types diff --git a/transpiler/strategies-rs/fixtures/mainnet-2026-07.txt b/transpiler/strategies-rs/fixtures/mainnet-2026-07.txt deleted file mode 100644 index 938025c2..00000000 --- a/transpiler/strategies-rs/fixtures/mainnet-2026-07.txt +++ /dev/null @@ -1,135 +0,0 @@ -Battle 0x3d975a5c8e58df60c746a2321537c0635a4c484a6c96122dedec671de5788a15 (mainnet) -A 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) -Winner: 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) Turns: 26 -CPU difficulty: hard - -Team A: - Malalien L8 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] - Xmon L8 facet0 moves[Contagious Slumber, Vital Siphon, Somniphobia, Old Vengeance] - Volthare L8 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] - Ekineki L8 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] -Team B: - Inutia L0 facet0 moves[Chain Expansion, Initialize, Big Bite, Hit And Dip] - Ekineki L0 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] - Pengym L0 facet0 moves[Chill Out, Deadlift, Deep Freeze, Pistol Squat] - Embursa L0 facet0 moves[Honey Bribe, Set Ablaze, Heat Beacon, Q5] - -T1 A mon0 SWITCH→2 salt7642991204039085190839734102788 B mon0 SWITCH→3 salt0 -T2 A mon0 move2 x0 salt6523886991944769092997260894615 B mon0 move0 x0 salt0 -T3 A mon0 move2 x0 salt7173571679591124929013698586035 B mon0 SWITCH→2 salt0 -T4 A mon0 move3 x0 salt715973405151049693142183856130 B mon0 move1 x0 salt0 -T5 A mon0 SWITCH→3 salt10434400288200938430387780785640 B mon0 move3 x0 salt0 -T6 A mon0 move1 x0 salt16590288657525008761715849387279 B mon0 SWITCH→0 salt0 -T7 A mon0 move1 x0 salt6648290556624459692187665162988 B mon0 SWITCH→1 salt0 -T8 A mon0 SWITCH→3 salt11274310919296775172131196580187 B mon0 move0 x0 salt0 -T9 A mon0 SWITCH→2 salt16134457634591674276270373928776 B mon0 NO_OP salt0 -T10 A mon0 NO_OP salt2683191092725922261265135640441 B mon0 move3 x0 salt0 -T11 A mon0 move2 x0 salt11154374962366060505624650320091 B mon0 SWITCH→2 salt0 -T12 A mon0 NO_OP salt0 B mon0 SWITCH→3 salt0 -T13 A mon0 NO_OP salt2442172819432642288986362144799 B mon0 move1 x0 salt0 -T14 A mon0 SWITCH→0 salt13755720069283378538249966050958 B mon0 NO_OP salt0 -T15 A mon0 NO_OP salt6756780855722838315842010211924 B mon0 move0 x0 salt0 -T16 A mon0 move1 x0 salt7134362144532818851786463667953 B mon0 NO_OP salt0 -T17 A mon0 NO_OP salt18284396846231976760979538969039 B mon0 move0 x0 salt0 -T18 A mon0 move1 x0 salt1618181630032726973372722526491 B mon0 move1 x0 salt0 -T19 A mon0 SWITCH→1 salt4487693600846520293556172147971 B mon0 SWITCH→1 salt0 -T20 A mon0 move1 x0 salt9839922508345259735150333678320 B mon0 move1 x1 salt0 -T21 A mon0 move1 x0 salt3477810539310681962522516256065 B mon0 SWITCH→0 salt0 -T22 A mon0 move1 x0 salt10450567441627425515527389264062 B mon0 SWITCH→1 salt0 -T23 A mon0 move1 x0 salt3676355149806238644881809734477 B mon0 move1 x1 salt0 -T24 A mon0 NO_OP salt0 B mon0 SWITCH→0 salt0 -T25 A mon0 NO_OP salt882665223311632533854195206303 B mon0 move0 x0 salt0 -T26 A mon0 move1 x0 salt6717266306622880485132858893329 B mon0 move0 x0 salt0 -T27 A mon0 move1 x0 salt1998170498903120033041120388285 B mon0 move0 x0 salt0 - -Battle 0x7529114a519de772c47a08d62c910cd1abcf13ed23b28f6d46fdc662d70a58cd (mainnet) -A 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) -Winner: 0xd749…5185 (0xd749a326d3a5ef91fe5fce4a4c37ba263e455185) Turns: 24 -CPU difficulty: hard - -Team A: - Malalien L8 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] - Xmon L8 facet0 moves[Contagious Slumber, Vital Siphon, Somniphobia, Old Vengeance] - Volthare L7 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] - Ekineki L8 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] -Team B: - Inutia L0 facet0 moves[Chain Expansion, Initialize, Big Bite, Hit And Dip] - Ekineki L0 facet0 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] - Pengym L0 facet0 moves[Chill Out, Deadlift, Deep Freeze, Pistol Squat] - Embursa L0 facet0 moves[Honey Bribe, Set Ablaze, Heat Beacon, Q5] - -T1 A mon0 SWITCH→2 salt16278078627131487114132547704398 B mon0 SWITCH→3 salt0 -T2 A mon0 move2 x0 salt7783107712856898203443960860761 B mon0 move0 x0 salt0 -T3 A mon0 move2 x0 salt19887999801398167433714186522490 B mon0 move1 x0 salt0 -T4 A mon0 move1 x3 salt5027765037949021299786506045172 B mon0 SWITCH→1 salt0 -T5 A mon0 move1 x3 salt16885550753857402497828728004069 B mon0 move0 x0 salt0 -T6 A mon0 SWITCH→2 salt12823241624889775396764909896588 B mon0 NO_OP salt0 -T7 A mon0 move1 x1 salt11769922866231276628642544586802 B mon0 move1 x2 salt0 -T8 A mon0 SWITCH→0 salt13890169115124444928492980561828 B mon0 NO_OP salt0 -T9 A mon0 move0 x0 salt1967333149517071111268237089957 B mon0 move0 x0 salt0 -T10 A mon0 move1 x0 salt1087772717206985673591137063737 B mon0 SWITCH→2 salt0 -T11 A mon0 SWITCH→1 salt6064891294010572925388263446039 B mon0 move2 x0 salt0 -T12 A mon0 move1 x0 salt13970724489817994243868990999906 B mon0 SWITCH→1 salt0 -T13 A mon0 move1 x0 salt3664146897130979578418152456832 B mon0 move2 x0 salt0 -T14 A mon0 move1 x0 salt18892824784579841314989597992588 B mon0 move0 x0 salt0 -T15 A mon0 move1 x0 salt1376118833330709076722273176502 B mon0 move0 x0 salt0 -T16 A mon0 move1 x0 salt8244822615319294248991548387402 B mon0 SWITCH→0 salt0 -T17 A mon0 move1 x0 salt18344034292549560831899680922034 B mon0 move1 x0 salt0 -T18 A mon0 move1 x0 salt9096455331192103053941663375814 B mon0 move0 x0 salt0 -T19 A mon0 SWITCH→0 salt7853893379365566137156318043105 B mon0 NO_OP salt0 -T20 A mon0 SWITCH→3 salt3920529028209073602227420726182 B mon0 NO_OP salt0 -T21 A mon0 move1 x2 salt5944833039053403453796308587341 B mon0 move2 x0 salt0 -T22 A mon0 move0 x0 salt3969701329630262960307396414223 B mon0 move3 x1 salt0 -T23 A mon0 NO_OP salt0 B mon0 SWITCH→1 salt0 -T24 A mon0 SWITCH→1 salt10391795889289817909269995339622 B mon0 NO_OP salt0 -T25 A mon0 move1 x0 salt5256755908273271588468609386902 B mon0 NO_OP salt0 - -Battle 0xc996696b718feaa03805730a31d25de9d995439a7e6a8082c73b91a43e9d0d08 (mainnet) -A @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) -Winner: @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) Turns: 7 -CPU difficulty: hard - -Team A: - Gorillax L12 facet6 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] - Malalien L12 facet6 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] - Sofabbi L12 facet6 moves[Gachachacha, Guest Feature, Unexpected Carrot, Snack Break] - Ekineki L12 facet6 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] -Team B: - Malalien L0 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] - Aurox L0 facet0 moves[Volatile Punch, Gilded Recovery, Iron Wall, Bull Rush] - Gorillax L0 facet0 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] - Volthare L0 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] - -T1 A SWITCH→3 salt1145376790288196797197575 | SWITCH→0 salt1145376790288196797197575 B SWITCH→0 salt0 | SWITCH→1 salt0 -T2 A move0 x16384 salt202495186900453106763512 | move2 x16384 salt202495186900453106763512 B move2 x8192 salt0 | move3 x8192 salt0 -T3 A NO_OP salt845014879727075913631049 | SWITCH→1 salt845014879727075913631049 B SWITCH→3 salt0 | NO_OP salt0 -T4 A move0 x16384 salt303380633387863941492980 | move1 x16384 salt303380633387863941492980 B move2 x8192 salt0 | move0 x4096 salt0 -T5 A NO_OP salt0 | NO_OP salt0 B SWITCH→2 salt0 | NO_OP salt0 -T6 A NO_OP salt830580391542377377074416 | move1 x16384 salt830580391542377377074416 B move1 x8192 salt0 | NO_OP salt0 -T7 A move0 x16384 salt342698529802560752655528 | NO_OP salt342698529802560752655528 B move1 x8192 salt0 | move3 x4096 salt0 -T8 A NO_OP salt982712523374567969471956 | move1 x32768 salt982712523374567969471956 B move1 x8192 salt0 | move3 x4096 salt0 - -Battle 0x6a87950b92eb969ce04a68109fcac3038d26f420234a68132c7bdc7f3e2ac0fc (mainnet) -A @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) vs B Hard CPU (0x1724f0b19685037be1dbcc499e37cf13a23620a1) -Winner: @tenkoghost1 (0x2c9623a2ac7336b09c91923183318099ac82b7e3) Turns: 7 -CPU difficulty: hard - -Team A: - Gorillax L12 facet6 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] - Malalien L12 facet6 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] - Sofabbi L12 facet6 moves[Gachachacha, Guest Feature, Unexpected Carrot, Snack Break] - Ekineki L12 facet6 moves[Bubble Bop, Sneak Attack, Nine Nine Nine, Overflow] -Team B: - Malalien L0 facet0 moves[Triple Think, Federal Investigation, Negative Thoughts, Infinite Love] - Aurox L0 facet0 moves[Volatile Punch, Gilded Recovery, Iron Wall, Bull Rush] - Gorillax L0 facet0 moves[Rock Pull, Pound Ground, Blow, Throw Pebble] - Volthare L0 facet0 moves[Electrocute, Round Trip, Mega Star Blast, Dual Shock] - -T1 A SWITCH→0 salt706840469641094748672528 | SWITCH→3 salt706840469641094748672528 B SWITCH→0 salt0 | SWITCH→1 salt0 -T2 A move1 x16384 salt384554543562985196003319 | move0 x16384 salt384554543562985196003319 B move2 x4096 salt0 | move3 x4096 salt0 -T3 A SWITCH→1 salt1018655568522462328983640 | NO_OP salt1018655568522462328983640 B SWITCH→3 salt0 | NO_OP salt0 -T4 A move1 x16384 salt1152575408630275207959332 | move0 x16384 salt1152575408630275207959332 B move2 x4096 salt0 | move0 x8192 salt0 -T5 A NO_OP salt0 | NO_OP salt0 B SWITCH→2 salt0 | NO_OP salt0 -T6 A move1 x32768 salt542740429310367792741919 | NO_OP salt542740429310367792741919 B move2 x4096 salt0 | move3 x4096 salt0 -T7 A SWITCH→2 salt44291697099022681926235 | NO_OP salt44291697099022681926235 B NO_OP salt0 | NO_OP salt0 -T8 A move2 x16384 salt715639655745582546424049 | move0 x16384 salt715639655745582546424049 B move2 x4096 salt0 | move3 x8192 salt0 diff --git a/transpiler/test_transpiler.py b/transpiler/test_transpiler.py index 493746cf..2e3619f2 100644 --- a/transpiler/test_transpiler.py +++ b/transpiler/test_transpiler.py @@ -1078,8 +1078,10 @@ def test_uint256_cast(self): generator = TypeScriptCodeGenerator() output = generator.generate(ast) - # Should have BigInt wrapping for numeric type casts - self.assertIn('BigInt', output) + # A statically-bigint operand needs no BigInt() wrap — the cast elides to the + # operand itself (int256 params are already bigint in TS). + self.assertIn('return x;', output) + self.assertNotIn('BigInt(x)', output) def test_address_cast(self): """Test address type cast.""" From 4e3c28927a6f56e7d84febd2909e84ae6a123f43 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:56:47 -0700 Subject: [PATCH 33/41] clean up --- src/mons/glob/ChosenOne.sol | 2 +- transpiler/codegen/generator.py | 5 +- transpiler/codegen/imports.py | 16 +++---- transpiler/codegen/type_converter.py | 15 +++--- transpiler/codegen/yul.py | 12 ++--- transpiler/runtime/base.ts | 72 +++++++++++++++------------- transpiler/test_transpiler.py | 8 ++-- 7 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/mons/glob/ChosenOne.sol b/src/mons/glob/ChosenOne.sol index b4df5646..cce41829 100644 --- a/src/mons/glob/ChosenOne.sol +++ b/src/mons/glob/ChosenOne.sol @@ -34,6 +34,6 @@ contract ChosenOne is IAbility { } function _getChosenOneKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64 scaledKey) { - uint64 scaledKey = (uint64(playerIndex) * MAX_MON_INDEX) + monIndex; + scaledKey = uint64(playerIndex * MAX_MON_INDEX + monIndex); } } \ No newline at end of file diff --git a/transpiler/codegen/generator.py b/transpiler/codegen/generator.py index 975fd681..07ef5622 100644 --- a/transpiler/codegen/generator.py +++ b/transpiler/codegen/generator.py @@ -139,9 +139,8 @@ def generate(self, ast: SourceUnit) -> str: for contract in ast.contracts: output.append(self._contract_generator.generate_contract(contract)) - # Scan generated content to determine which viem imports are actually used. - # '__ik' rides this scan too: it's not a viem name, but the RUNTIME_WRAPPED_VIEM - # routing in imports.py already moves scanned names onto the runtime import line. + # Scan generated content for used viem / runtime-provided names (imports.py + # routes RUNTIME_PROVIDED members onto the runtime import line). content = '\n'.join(output) for viem_fn in ('keccak256', 'encodePacked', 'encodeAbiParameters', 'decodeAbiParameters', 'parseAbiParameters', 'stringToHex', diff --git a/transpiler/codegen/imports.py b/transpiler/codegen/imports.py index 5b57b3b5..e4242c24 100644 --- a/transpiler/codegen/imports.py +++ b/transpiler/codegen/imports.py @@ -11,10 +11,9 @@ if TYPE_CHECKING: from .context import CodeGenerationContext -# viem names the runtime re-exports with memoized / fast-path wrappers (byte-identical -# outputs); generated files import these from the runtime, not from viem. '__ik' (the -# interned mapping-key helper) is runtime-only but routed through the same mechanism. -RUNTIME_WRAPPED_VIEM = frozenset({'keccak256', 'encodePacked', 'encodeAbiParameters', '__ik'}) +# Scanned names generated files import from the runtime instead of viem: wrapped viem +# functions (memoized / fast-path, byte-identical) plus runtime-only helpers like '__ik'. +RUNTIME_PROVIDED = frozenset({'keccak256', 'encodePacked', 'encodeAbiParameters', '__ik'}) class ImportGenerator: @@ -52,11 +51,9 @@ def generate(self, contract_name: str = '') -> str: lines = [] - # viem imports (only what's actually used). Names in RUNTIME_WRAPPED_VIEM are - # emitted from the runtime instead — it wraps them with memoized / fast-path - # equivalents (byte-identical outputs). + # viem imports (only what's actually used); RUNTIME_PROVIDED names come from the runtime. if self._ctx.viem_imports_used: - viem_imports = sorted(self._ctx.viem_imports_used - RUNTIME_WRAPPED_VIEM) + viem_imports = sorted(self._ctx.viem_imports_used - RUNTIME_PROVIDED) if viem_imports: lines.append( f"import {{ {', '.join(viem_imports)} }} from 'viem';" @@ -101,8 +98,7 @@ def _build_runtime_imports(self) -> List[str]: 'ecrecover', 'selfdestruct', ] - # Wrapped viem names route through the runtime (memoized keccak256, fast ABI encoders) - imports.extend(sorted(self._ctx.viem_imports_used & RUNTIME_WRAPPED_VIEM)) + imports.extend(sorted(self._ctx.viem_imports_used & RUNTIME_PROVIDED)) # Add set types if used if self._ctx.set_types_used: diff --git a/transpiler/codegen/type_converter.py b/transpiler/codegen/type_converter.py index 71707231..1de07410 100644 --- a/transpiler/codegen/type_converter.py +++ b/transpiler/codegen/type_converter.py @@ -379,7 +379,7 @@ def generate_type_cast( # Default: generate the inner expression return generate_expression_fn(inner_expr) - def _ensure_bigint(self, expr: str, node: 'Expression | None' = None) -> str: + def _ensure_bigint(self, expr: str, node: 'Expression') -> str: """Wrap expression in BigInt() only if it's not already a bigint expression. Avoids redundant BigInt(BigInt(x)) patterns in generated code. @@ -395,14 +395,13 @@ def _ensure_bigint(self, expr: str, node: 'Expression | None' = None) -> str: return expr # Statically bigint by type: a uint/int identifier, or a member access that # resolves to a numeric field (both transpile to bigint). - if node is not None: - if isinstance(node, Identifier) and self.is_bigint_typed_identifier(node): + if isinstance(node, Identifier) and self.is_bigint_typed_identifier(node): + return expr + if isinstance(node, MemberAccess): + resolved = self.resolve_access_type(node) + if (resolved and not resolved.is_array + and (resolved.name.startswith('uint') or resolved.name.startswith('int'))): return expr - if isinstance(node, MemberAccess): - resolved = self.resolve_access_type(node) - if (resolved and not resolved.is_array - and (resolved.name.startswith('uint') or resolved.name.startswith('int'))): - return expr return f'BigInt({expr})' def _is_already_address_type(self, expr: Expression) -> bool: diff --git a/transpiler/codegen/yul.py b/transpiler/codegen/yul.py index 8bedff92..8e0a6f94 100644 --- a/transpiler/codegen/yul.py +++ b/transpiler/codegen/yul.py @@ -697,7 +697,7 @@ def _emit_slot_read(self, target) -> Optional[str]: # Bits no member declares still need somewhere to live; the shadow # slot store keeps carrying exactly those. terms.append( - f'(this._storageRead(this._yulSlotKeyOf({name} as any)) ' + f'(this._storageRead({name} as any) ' f'& {hex(layout.free_mask)}n)' ) return '(' + ' | '.join(terms) + ')' if terms else '0n' @@ -714,7 +714,7 @@ def _emit_slot_write(self, target, value: str, prefix: str) -> Optional[str]: lines.append(f'{prefix}{name}.{f.name} = {val};') if layout.has_free_bits: lines.append( - f'{prefix}this._storageWrite(this._yulSlotKeyOf({name} as any), ' + f'{prefix}this._storageWrite({name} as any, ' f'{tmp} & {hex(layout.free_mask)}n);' ) return '\n'.join(lines) @@ -853,7 +853,7 @@ def _generate_let( if isinstance(stmt.value, YulSlotAccess): storage_var = stmt.value.variable slot_vars[stmt.name] = storage_var - return f'{prefix}const {stmt.name} = this._yulSlotKeyOf({storage_var} as any);' + return f'{prefix}const {stmt.name} = {storage_var} as any;' ts_expr = self._generate_expression(stmt.value, slot_vars) return f'{prefix}let {stmt.name} = {ts_expr};' @@ -871,7 +871,7 @@ def _generate_assignment( if isinstance(stmt.value, YulSlotAccess): storage_var = stmt.value.variable slot_vars[stmt.name] = storage_var - return f'{prefix}{stmt.name} = this._yulSlotKeyOf({storage_var} as any);' + return f'{prefix}{stmt.name} = {storage_var} as any;' ts_expr = self._generate_expression(stmt.value, slot_vars) return f'{prefix}{stmt.name} = {ts_expr};' @@ -889,7 +889,7 @@ def _generate_if( # Yul `if` runs the body when the condition is NONZERO. Make the comparison explicit # instead of relying on bigint truthiness — semantically identical, and it avoids # tsc's TS2872 (always-truthy) diagnostic on translated boolean-ternary conditions. - lines = [f'{prefix}if (BigInt({cond}) !== 0n) {{'] + lines = [f'{prefix}if ({self._bi(cond)} !== 0n) {{'] if body and body != '// Assembly: no-op': lines.append(body) lines.append(f'{prefix}}}') @@ -1059,7 +1059,7 @@ def _generate_expression( elif isinstance(expr, YulFunctionCall): return self._generate_function_call(expr, slot_vars) elif isinstance(expr, YulSlotAccess): - return f'this._yulSlotKeyOf({expr.variable} as any)' + return f'({expr.variable} as any)' elif isinstance(expr, YulOffsetAccess): return '0n // .offset' return '0n' diff --git a/transpiler/runtime/base.ts b/transpiler/runtime/base.ts index 913917c7..f785f86a 100644 --- a/transpiler/runtime/base.ts +++ b/transpiler/runtime/base.ts @@ -22,7 +22,7 @@ export function keccak256(value: `0x${string}` | Uint8Array): `0x${string}` { if (typeof value !== 'string') return viemKeccak256(value); let h = KECCAK_CACHE.get(value); if (h === undefined) { - if (KECCAK_CACHE.size > 50_000) KECCAK_CACHE.clear(); + if (KECCAK_CACHE.size > 10_000) KECCAK_CACHE.clear(); h = viemKeccak256(value); KECCAK_CACHE.set(value, h); } @@ -53,15 +53,32 @@ const UINT_RE = /^u?int(\d*)$/; const HEX32_RE = /^0x[0-9a-f]{64}$/; const ADDR_RE = /^0x[0-9a-f]{40}$/; +// Type string → uint/int bit width, or -1 for non-numeric types. The type vocabulary the +// transpiler emits is a tiny fixed set, so this replaces a regex exec per element. +const UINT_WIDTH = new Map(); +function uintWidth(t: string): number { + let w = UINT_WIDTH.get(t); + if (w === undefined) { + const m = UINT_RE.exec(t); + w = m === null ? -1 : m[1] === '' ? 256 : Number(m[1]); + UINT_WIDTH.set(t, w); + } + return w; +} + +/** `v` as a two's-complement `bits`-wide unpadded hex string. */ +function padUint(v: bigint | number, bits: number): string { + return BigInt.asUintN(bits, BigInt(v)).toString(16).padStart(bits >> 2, '0'); +} + export function encodePacked(types: readonly string[], values: readonly unknown[]): `0x${string}` { let out = '0x'; for (let i = 0; i < types.length; i++) { const t = types[i]; const v = values[i]; - const m = UINT_RE.exec(t); - if (m !== null && (typeof v === 'bigint' || typeof v === 'number')) { - const bits = m[1] === '' ? 256 : Number(m[1]); - out += BigInt.asUintN(bits, BigInt(v)).toString(16).padStart(bits >> 2, '0'); + const bits = uintWidth(t); + if (bits > 0 && (typeof v === 'bigint' || typeof v === 'number')) { + out += padUint(v, bits); } else if (t === 'bytes32' && typeof v === 'string' && HEX32_RE.test(v)) { out += v.slice(2); } else if (t === 'address' && typeof v === 'string' && ADDR_RE.test(v)) { @@ -76,26 +93,24 @@ export function encodePacked(types: readonly string[], values: readonly unknown[ } export function encodeAbiParameters(params: unknown, values: readonly unknown[]): `0x${string}` { - if (Array.isArray(params)) { - let out = '0x'; - for (let i = 0; i < params.length; i++) { - const t = (params[i] as { type?: string })?.type ?? ''; - const v = values[i]; - if (UINT_RE.test(t) && (typeof v === 'bigint' || typeof v === 'number')) { - out += BigInt.asUintN(256, BigInt(v)).toString(16).padStart(64, '0'); - } else if (t === 'bytes32' && typeof v === 'string' && HEX32_RE.test(v)) { - out += v.slice(2); - } else if (t === 'address' && typeof v === 'string' && ADDR_RE.test(v)) { - out += v.slice(2).padStart(64, '0'); - } else if (t === 'bool') { - out += (v ? '1' : '0').padStart(64, '0'); - } else { - return viemEncodeAbiParameters(params as never, values as never) as `0x${string}`; - } + if (!Array.isArray(params)) return viemEncodeAbiParameters(params as never, values as never) as `0x${string}`; + let out = '0x'; + for (let i = 0; i < params.length; i++) { + const t = (params[i] as { type?: string })?.type ?? ''; + const v = values[i]; + if (uintWidth(t) > 0 && (typeof v === 'bigint' || typeof v === 'number')) { + out += padUint(v, 256); + } else if (t === 'bytes32' && typeof v === 'string' && HEX32_RE.test(v)) { + out += v.slice(2); + } else if (t === 'address' && typeof v === 'string' && ADDR_RE.test(v)) { + out += v.slice(2).padStart(64, '0'); + } else if (t === 'bool') { + out += (v ? '1' : '0').padStart(64, '0'); + } else { + return viemEncodeAbiParameters(params as never, values as never) as `0x${string}`; } - return out as `0x${string}`; } - return viemEncodeAbiParameters(params as never, values as never) as `0x${string}`; + return out as `0x${string}`; } // ============================================================================= @@ -956,17 +971,6 @@ export abstract class Contract { this._storage.sstore(this._yulStorageKey(key), value); } - /** Synthetic slot for a storage struct (Solidity `x.slot`), holding the packed bits no - * modelled property owns. The word lives ON the struct under a symbol (both proxy layers - * pass symbols through to the target, so a proxied struct and its raw target agree), and - * dies with it — search forks can't accumulate orphaned words in `_storage`. */ - protected _yulSlotKeyOf(ref: any): any { - if (ref === null || ref === undefined || typeof ref !== 'object') { - return this._yulStorageKey(ref); - } - return ref; - } - /** * Fork support: `dst` is a structural clone of `src`, but the word is symbol-keyed so * `Object.keys` cloning drops it — without this, a fork's struct reads a ZERO packed word diff --git a/transpiler/test_transpiler.py b/transpiler/test_transpiler.py index 2e3619f2..81ebebf8 100644 --- a/transpiler/test_transpiler.py +++ b/transpiler/test_transpiler.py @@ -266,9 +266,11 @@ def test_simple_sload_sstore(self): } ''' result = self.transpiler.transpile(yul_code) - self.assertIn('_yulSlotKeyOf(myVar', result) - self.assertIn('_storageRead(myVar', result) - self.assertIn('_storageWrite(myVar', result) + # `.slot` of a storage struct passes the struct ref straight through — the runtime + # accessors key object refs by their symbol-stored word. + self.assertIn('const slot = myVar as any;', result) + self.assertIn('_storageRead(myVar as any)', result) + self.assertIn('_storageWrite(myVar as any', result) def test_unmodelable_calldata_assembly_flagged(self): """Raw calldata reads / calldata .offset have no faithful simulation, so From 7a84d96b4ed82898d62d3f834fd2395a311b1541 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:54:08 -0700 Subject: [PATCH 34/41] clean up and wip for new mons --- docs/mons.md | 25 +- drool/abilities.csv | 4 +- drool/mons.csv | 2 + drool/moves.csv | 8 + script/EngineAndPeriphery.s.sol | 4 + script/SetupMons.s.sol | 116 ++++++- snapshots/DoublesGasBenchmark.json | 26 +- snapshots/FullyOptimizedInlineGasTest.json | 6 +- snapshots/MainnetReplayGasTest.json | 136 ++++---- snapshots/ProdPvPGasBenchmark.json | 8 +- src/Engine.sol | 109 +++++- src/IEngine.sol | 13 + src/abilities/IAbility.sol | 1 - src/effects/BasicEffect.sol | 4 - src/effects/IEffect.sol | 2 - src/effects/StaminaRegen.sol | 4 - src/effects/battlefield/Overclock.sol | 4 - src/effects/battlefield/Storm.sol | 84 +++++ src/effects/status/BlessedStatus.sol | 4 - src/effects/status/BurnStatus.sol | 4 - src/effects/status/FrostbiteStatus.sol | 4 - src/effects/status/PanicStatus.sol | 4 - src/effects/status/SleepStatus.sol | 8 +- src/effects/status/ZapStatus.sol | 4 - src/mons/aurox/BigBellow.sol | 4 - src/mons/aurox/GildedRecovery.sol | 4 - src/mons/aurox/IronWall.sol | 4 - src/mons/aurox/UpOnly.sol | 4 - src/mons/ekineki/NineNineNine.sol | 4 - src/mons/ekineki/NineNineNineLib.sol | 6 +- src/mons/ekineki/SaviorComplex.sol | 8 +- src/mons/ekineki/SneakAttack.sol | 4 - src/mons/embursa/HeatBeacon.sol | 4 - src/mons/embursa/HeatBeaconLib.sol | 6 +- src/mons/embursa/HoneyBribe.sol | 8 +- src/mons/embursa/Q5.sol | 8 +- src/mons/embursa/Tinderclaws.sol | 4 - src/mons/ghouliath/EternalGrudge.sol | 4 - src/mons/ghouliath/GraveAffliction.sol | 4 - src/mons/ghouliath/RiseFromTheGrave.sol | 6 +- src/mons/glob/ChosenOne.sol | 22 +- src/mons/glob/GetSlippery.sol | 65 ++++ src/mons/glob/HolyMolt.sol | 87 +++++ src/mons/glob/HolyWaterClaw.json | 9 + src/mons/glob/WaterClaw.json | 9 + src/mons/gorillax/Angery.sol | 4 - src/mons/gorillax/RockPull.sol | 4 - src/mons/iblivion/Baselight.sol | 4 - src/mons/iblivion/Brightback.sol | 4 - src/mons/iblivion/Loop.sol | 4 - src/mons/iblivion/Renormalize.sol | 4 - src/mons/iblivion/UnboundedStrike.sol | 4 - src/mons/inutia/ChainExpansion.sol | 8 +- src/mons/inutia/Initialize.sol | 8 +- src/mons/inutia/Interweaving.sol | 4 - src/mons/inutia/Sanctify.sol | 4 - src/mons/malalien/ActusReus.sol | 4 - src/mons/malalien/TripleThink.sol | 4 - src/mons/mulong/CloudStrike.sol | 96 ++++++ src/mons/mulong/KingsLeisure.sol | 87 +++++ src/mons/mulong/KingsRespite.sol | 121 +++++++ src/mons/mulong/SummonStorm.sol | 62 ++++ src/mons/mulong/Thunderclap.json | 9 + src/mons/nirvamma/Adaptor.sol | 4 - src/mons/nirvamma/Chronoffense.sol | 8 +- src/mons/nirvamma/HardReset.sol | 4 - src/mons/nirvamma/ModalBolt.sol | 8 +- src/mons/pengym/Deadlift.sol | 4 - src/mons/pengym/DeepFreeze.sol | 4 - src/mons/pengym/PostWorkout.sol | 4 - src/mons/sofabbi/CarrotHarvest.sol | 4 - src/mons/sofabbi/Gachachacha.sol | 4 - src/mons/sofabbi/GuestFeature.sol | 4 - src/mons/sofabbi/SnackBreak.sol | 8 +- src/mons/volthare/MegaStarBlast.sol | 4 - src/mons/volthare/PreemptiveShock.sol | 12 +- src/mons/volthare/Quickstorm.sol | 10 +- src/mons/volthare/QuickstormLib.sol | 15 + src/mons/xmon/ContagiousSlumber.sol | 4 - src/mons/xmon/Dreamcatcher.sol | 4 - src/mons/xmon/InvokeTaboo.sol | 4 - src/mons/xmon/OldVengeance.sol | 4 - src/mons/xmon/Somniphobia.sol | 4 - src/moves/IMoveSet.sol | 1 - test/DoublesDispatchStatusTest.sol | 8 - test/DoublesEngineTest.sol | 4 - test/EngineOptimizationTest.sol | 4 - test/MultiTargetDispatchTest.sol | 319 +++++++++++++++++ test/effects/EffectTest.sol | 9 +- test/mocks/AlwaysRejectsEffect.sol | 4 - test/mocks/BoostBenchMove.sol | 4 - test/mocks/CustomAttack.sol | 4 - test/mocks/DirectStatWriteMove.sol | 4 - test/mocks/DummyStatus.sol | 4 - test/mocks/EditEffectAttack.sol | 4 - test/mocks/EffectAbility.sol | 4 - test/mocks/EffectAttack.sol | 4 - test/mocks/ForceSwitchMove.sol | 4 - test/mocks/FreshMoveContextAbility.sol | 8 - test/mocks/GlobalEffectAttack.sol | 4 - test/mocks/InstantDeathEffect.sol | 4 - test/mocks/InstantDeathOnSwitchInEffect.sol | 4 - test/mocks/MockEffectRemover.sol | 4 - test/mocks/MockKVWriterMove.sol | 4 - test/mocks/MockSingletonAbility.sol | 4 - test/mocks/NeverAppliesEffect.sol | 4 - test/mocks/OneTurnStatBoost.sol | 4 - test/mocks/OrderRecorderAbility.sol | 4 - test/mocks/RandomForceOutMove.sol | 4 - test/mocks/RawDamageMove.sol | 4 - test/mocks/ReduceSpAtkMove.sol | 4 - test/mocks/SelfSwitchAndDamageMove.sol | 4 - test/mocks/SetEffectDataAttack.sol | 4 - test/mocks/SingleInstanceEffect.sol | 4 - test/mocks/SkipTurnMove.sol | 4 - test/mocks/SpAtkDebuffEffect.sol | 4 - test/mocks/SpreadAttack.sol | 100 ++++++ test/mocks/StaminaGainKOEffect.sol | 4 - test/mocks/StatBoostsMove.sol | 4 - test/mocks/TempStatBoostEffect.sol | 4 - test/mocks/TestMoveFactory.sol | 4 - test/mocks/WideDataEffect.sol | 4 - test/mons/EkinekiTest.sol | 5 +- test/mons/EmbursaTest.sol | 5 +- test/mons/GhouliathTest.sol | 4 +- test/mons/GlobTest.sol | 255 ++++++++++++++ test/mons/InutiaTest.sol | 4 +- test/mons/MulongTest.sol | 361 ++++++++++++++++++++ test/mons/VolthareTest.sol | 13 +- transpiler/codegen/abi.py | 5 - 130 files changed, 2099 insertions(+), 540 deletions(-) create mode 100644 src/effects/battlefield/Storm.sol create mode 100644 src/mons/glob/GetSlippery.sol create mode 100644 src/mons/glob/HolyMolt.sol create mode 100644 src/mons/glob/HolyWaterClaw.json create mode 100644 src/mons/glob/WaterClaw.json create mode 100644 src/mons/mulong/CloudStrike.sol create mode 100644 src/mons/mulong/KingsLeisure.sol create mode 100644 src/mons/mulong/KingsRespite.sol create mode 100644 src/mons/mulong/SummonStorm.sol create mode 100644 src/mons/mulong/Thunderclap.json create mode 100644 src/mons/volthare/QuickstormLib.sol create mode 100644 test/MultiTargetDispatchTest.sol create mode 100644 test/mocks/SpreadAttack.sol create mode 100644 test/mons/GlobTest.sol create mode 100644 test/mons/MulongTest.sol diff --git a/docs/mons.md b/docs/mons.md index 9d8c4dee..2ea18150 100644 --- a/docs/mons.md +++ b/docs/mons.md @@ -11,14 +11,15 @@ Holy Molt costs 1 stamina more per time used, up to max of 3 Water Claw -80, liquid +80 power, liquid, physical, 2 stamina, normal priority Deals damage Holy Water Claw -80, faith +90 power, faith liquid, physical, 3 stamina, normal priority Deals damage Get Slippery +2 stamina, normal priority Boosts atk/speed 50% @@ -27,18 +28,26 @@ Air / Lightning High speed, low physical defense, low sp def, med hp, high sp atk -Ability: Imposing Aura +Ability: King's Leisure For 3 turns (starting from swap in), Mulong takes half damage and deals half damage Summon Storm: +0 stamina, normal priority Summons Storm, a battlefield effect that lasts for 7 turns and damages all mons at end of turn for Electric damage +(assume base power of 60, and then scaled by Lightning type chart) +(Storm is not affected by King's Leisure) King's Respite: The next time Mulong rests, deal massive Air damage to the target slot. +Rest deals base power 120, 2 stamina, normal priority +(is affected by King's Leisure) Thunderclap: -Deals damage, % to Zap - -Sky Strike: -Deals Air damage, % to deal Frostbite. -If Storm is active 50% more damage, high % to inflict Frosbite \ No newline at end of file +120 power, 3 stamina, normal priority, Lightning +Deals damage, 20% to Zap +(is affected by King's Leisure) + +Cloud Strike: +Ice, 70 power, 2 stamina, 10% to deal Frostbite. +If Storm is active 100 power, 30% to inflict Frosbite +(is affected by King's Leisure) \ No newline at end of file diff --git a/drool/abilities.csv b/drool/abilities.csv index 8950a531..ad3cb855 100644 --- a/drool/abilities.csv +++ b/drool/abilities.csv @@ -11,4 +11,6 @@ Interweaving,Inutia,"When Inutia swaps in, the opposing mon's ATK decreases 15%. Up Only,Aurox,"Whenever Aurox takes damage, they gain a persistent 10% ATK boost." Dreamcatcher,Xmon,"Whenever Xmon gains stamina, heal 1/16 (6.25%) of max HP." Savior Complex,Ekineki,"On switch-in, gain a temporary 15/25/30% SpATK boost based on KO'd mons (1/2/3+). Triggers once per game." -Adaptor,Nirvamma,"When Nirvamma takes damage for the first time each game, they adapt to that source of damage. Nirvamma take 50% less damage from that move or effect." \ No newline at end of file +Adaptor,Nirvamma,"When Nirvamma takes damage for the first time each game, they adapt to that source of damage. Nirvamma take 50% less damage from that move or effect." +Chosen One,Glob,"The first time Glob switches in, it enters Blessed." +King's Leisure,Mulong,"For 3 active turns after its first switch-in, Mulong takes half damage and deals half damage." \ No newline at end of file diff --git a/drool/mons.csv b/drool/mons.csv index 6e452a8c..5af854ea 100644 --- a/drool/mons.csv +++ b/drool/mons.csv @@ -12,3 +12,5 @@ Id,Name,HP,Attack,Defense,SpecialAttack,SpecialDefense,Speed,Type1,Type2,Flavor 10,Xmon,311,123,179,222,185,285,Cosmic,NA,"bGJoIHBuYSBmcnIgdmcgdmEgbGJoZSBxZXJuemY=" 11,Ekineki,299,130,180,280,175,266,Liquid,NA,"Born from a single drop of water." 12,Nirvamma,373,202,168,140,202,177,Math,NA,"Supposedly watches the entire universe." +13,Glob,280,300,205,115,185,140,Liquid,Faith,"PLACEHOLDER" +14,Mulong,320,115,135,290,145,300,Air,Lightning,"PLACEHOLDER" diff --git a/drool/moves.csv b/drool/moves.csv index 3f69901a..336ff4db 100644 --- a/drool/moves.csv +++ b/drool/moves.csv @@ -57,3 +57,11 @@ Hard Reset,Nirvamma,0,2,100,0,Math,Other,"The next time your team rests, the res Scary Numbers,Nirvamma,80,3,100,0,Math,Physical,"Deals damage, {EFFECT_ACCURACY} chance to inflict Panic.",,,none,0,EFFECT_ACCURACY=20,opponent-slot Chronoffense,Nirvamma,?,2,100,0,Math,Physical,"First use boosts defenses. Second use deals damage based on how much time has passed, and then resets.",,,none,0,,opponent-slot Modal Bolt,Nirvamma,90,3,100,0,Math,Physical,"Choose between Fire, Ice, or Lightning each use. Each mode is usable once, and applies its corresponding status (Burn / Frostbite / Zap) at {EFFECT_ACCURACY} chance.",,,mode-select,0,EFFECT_ACCURACY=50,opponent-slot +Holy Molt,Glob,0,?,100,1,Faith,Self,"Loses half of current HP and applies Blessed to self. Free on the first cast; each later cast costs 1 more stamina, up to {MAX_STAMINA_COST}.",,,none,0,HP_DENOM=2;MAX_STAMINA_COST=3,self-only +Water Claw,Glob,80,2,100,0,Liquid,Physical,Deals damage.,,,none,0,,opponent-slot +Holy Water Claw,Glob,90,3,100,0,Faith,Physical,Deals damage.,,,none,0,,opponent-slot +Get Slippery,Glob,0,2,100,0,Liquid,Self,Boosts ATK and Speed by {ATTACK_BUFF_PERCENT}.,,,none,0,ATTACK_BUFF_PERCENT=50;SPEED_BUFF_PERCENT=50,self-only +Summon Storm,Mulong,0,0,100,0,Lightning,Other,"Summons a Storm for 7 rounds that damages every active mon at end of round with Lightning damage scaled off the summoner's stats. Recasting re-latches the Storm to the new caster and resets its timer.",,,none,0,,none +King's Respite,Mulong,120,2,100,0,Air,Special,"Marks the chosen slot. The next time Mulong rests, it deals heavy Air damage to that slot; the rest still restores stamina as normal.",,,none,0,BASE_POWER=120,opponent-slot +Thunderclap,Mulong,120,3,100,0,Lightning,Special,"Deals damage, 20% chance to inflict Zap.",,,none,0,,opponent-slot +Cloud Strike,Mulong,?,2,100,0,Ice,Special,"Deals damage, {EFFECT_ACCURACY} chance to inflict Frostbite. While a Storm is active, power rises to {STORM_BASE_POWER} and the chance to {STORM_EFFECT_ACCURACY}.",,,none,0,BASE_POWER=70;STORM_BASE_POWER=100;EFFECT_ACCURACY=10;STORM_EFFECT_ACCURACY=30,opponent-slot diff --git a/script/EngineAndPeriphery.s.sol b/script/EngineAndPeriphery.s.sol index c069964a..39342e66 100644 --- a/script/EngineAndPeriphery.s.sol +++ b/script/EngineAndPeriphery.s.sol @@ -16,6 +16,7 @@ import {TypeCalculator} from "../src/types/TypeCalculator.sol"; // Shared effects import {Overclock} from "../src/effects/battlefield/Overclock.sol"; +import {Storm} from "../src/effects/battlefield/Storm.sol"; import {BlessedStatus} from "../src/effects/status/BlessedStatus.sol"; import {BurnStatus} from "../src/effects/status/BurnStatus.sol"; import {FrostbiteStatus} from "../src/effects/status/FrostbiteStatus.sol"; @@ -88,6 +89,9 @@ contract EngineAndPeriphery is Script { Overclock overclock = new Overclock(); deployedContracts.push(DeployData({name: "OVERCLOCK", contractAddress: address(overclock)})); + Storm storm = new Storm(); + deployedContracts.push(DeployData({name: "STORM", contractAddress: address(storm)})); + SleepStatus sleepStatus = new SleepStatus(); deployedContracts.push(DeployData({name: "SLEEP STATUS", contractAddress: address(sleepStatus)})); diff --git a/script/SetupMons.s.sol b/script/SetupMons.s.sol index d4f7b3fc..aa3730a9 100644 --- a/script/SetupMons.s.sol +++ b/script/SetupMons.s.sol @@ -9,6 +9,7 @@ import {Type} from "../src/Enums.sol"; import {IEffect} from "../src/effects/IEffect.sol"; import {Overclock} from "../src/effects/battlefield/Overclock.sol"; +import {Storm} from "../src/effects/battlefield/Storm.sol"; import {BigBellow} from "../src/mons/aurox/BigBellow.sol"; import {BullRush} from "../src/mons/aurox/BullRush.sol"; import {GildedRecovery} from "../src/mons/aurox/GildedRecovery.sol"; @@ -30,6 +31,9 @@ import {GraveAffliction} from "../src/mons/ghouliath/GraveAffliction.sol"; import {InfernalFlame} from "../src/mons/ghouliath/InfernalFlame.sol"; import {RiseFromTheGrave} from "../src/mons/ghouliath/RiseFromTheGrave.sol"; import {WitherAway} from "../src/mons/ghouliath/WitherAway.sol"; +import {ChosenOne} from "../src/mons/glob/ChosenOne.sol"; +import {GetSlippery} from "../src/mons/glob/GetSlippery.sol"; +import {HolyMolt} from "../src/mons/glob/HolyMolt.sol"; import {Angery} from "../src/mons/gorillax/Angery.sol"; import {RockPull} from "../src/mons/gorillax/RockPull.sol"; import {Baselight} from "../src/mons/iblivion/Baselight.sol"; @@ -45,6 +49,10 @@ import {Sanctify} from "../src/mons/inutia/Sanctify.sol"; import {ActusReus} from "../src/mons/malalien/ActusReus.sol"; import {FoulLanguage} from "../src/mons/malalien/FoulLanguage.sol"; import {TripleThink} from "../src/mons/malalien/TripleThink.sol"; +import {CloudStrike} from "../src/mons/mulong/CloudStrike.sol"; +import {KingsLeisure} from "../src/mons/mulong/KingsLeisure.sol"; +import {KingsRespite} from "../src/mons/mulong/KingsRespite.sol"; +import {SummonStorm} from "../src/mons/mulong/SummonStorm.sol"; import {Adaptor} from "../src/mons/nirvamma/Adaptor.sol"; import {Chronoffense} from "../src/mons/nirvamma/Chronoffense.sol"; import {HardReset} from "../src/mons/nirvamma/HardReset.sol"; @@ -82,7 +90,7 @@ contract SetupMons is Script { GachaTeamRegistry registry = GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY")); // Deploy all mons and collect deployment data - DeployData[][] memory allDeployData = new DeployData[][](13); + DeployData[][] memory allDeployData = new DeployData[][](15); allDeployData[0] = deployGhouliath(registry); allDeployData[1] = deployInutia(registry); @@ -97,6 +105,8 @@ contract SetupMons is Script { allDeployData[10] = deployXmon(registry); allDeployData[11] = deployEkineki(registry); allDeployData[12] = deployNirvamma(registry); + allDeployData[13] = deployGlob(registry); + allDeployData[14] = deployMulong(registry); // Calculate total length for flattened array uint256 totalLength = 0; @@ -849,4 +859,108 @@ contract SetupMons is Script { registry.createMon(12, stats, moves, abilities, keys, values); } + function deployGlob(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](3); + + // Cache commonly used addresses + address blessedstatus = vm.envAddress("BLESSED_STATUS"); + + address[3] memory addrs; + + { + addrs[0] = address(new HolyMolt(IEffect(blessedstatus))); + deployedContracts[0] = DeployData({name: "Holy Molt", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new GetSlippery()); + deployedContracts[1] = DeployData({name: "Get Slippery", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new ChosenOne(IEffect(blessedstatus))); + deployedContracts[2] = DeployData({name: "Chosen One", contractAddress: addrs[2]}); + } + + _registerGlob(registry, addrs); + + return deployedContracts; + } + + function _registerGlob(GachaTeamRegistry registry, address[3] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 280, + stamina: 5, + speed: 140, + attack: 300, + defense: 205, + specialAttack: 115, + specialDefense: 185, + type1: Type.Liquid, + type2: Type.Faith + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0040f00000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x5003200000000000000000000000000000000000000000000000000000000000; + moves[2] = 0x5a09300000000000000000000000000000000000000000000000000000000000; + moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[2])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(13, stats, moves, abilities, keys, values); + } + + function deployMulong(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](4); + + // Cache commonly used addresses + address storm = vm.envAddress("STORM"); + + address[4] memory addrs; + + { + addrs[0] = address(new SummonStorm(Storm(storm))); + deployedContracts[0] = DeployData({name: "Summon Storm", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new KingsRespite(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); + deployedContracts[1] = DeployData({name: "King's Respite", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new CloudStrike(Storm(storm), IEffect(vm.envAddress("FROSTBITE_STATUS")))); + deployedContracts[2] = DeployData({name: "Cloud Strike", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new KingsLeisure()); + deployedContracts[3] = DeployData({name: "King's Leisure", contractAddress: addrs[3]}); + } + + _registerMulong(registry, addrs); + + return deployedContracts; + } + + function _registerMulong(GachaTeamRegistry registry, address[4] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 320, + stamina: 5, + speed: 300, + attack: 115, + defense: 135, + specialAttack: 290, + specialDefense: 145, + type1: Type.Air, + type2: Type.Lightning + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0030000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x7848314000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("ZAP_STATUS"))); + moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[3])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(14, stats, moves, abilities, keys, values); + } + } \ No newline at end of file diff --git a/snapshots/DoublesGasBenchmark.json b/snapshots/DoublesGasBenchmark.json index c4677533..6d8d518e 100644 --- a/snapshots/DoublesGasBenchmark.json +++ b/snapshots/DoublesGasBenchmark.json @@ -1,13 +1,13 @@ { - "DoublesFloor_AllAttack8_rawGas": "912581", - "DoublesFloor_AllAttack_perTurn": "114072", - "DoublesFloor_AllRest8_rawGas": "375887", - "DoublesFloor_AllRest_perTurn": "46985", - "DoublesFloor_OneAttack8_rawGas": "682389", - "DoublesFloor_OneAttack_perTurn": "85298", + "DoublesFloor_AllAttack8_rawGas": "909477", + "DoublesFloor_AllAttack_perTurn": "113684", + "DoublesFloor_AllRest8_rawGas": "375215", + "DoublesFloor_AllRest_perTurn": "46901", + "DoublesFloor_OneAttack8_rawGas": "681251", + "DoublesFloor_OneAttack_perTurn": "85156", "Doubles_Batch12_afterDamageCalls": "0", "Doubles_Batch12_afterMoveCalls": "0", - "Doubles_Batch12_coldGas": "747703", + "Doubles_Batch12_coldGas": "741594", "Doubles_Batch12_coldSload": "34", "Doubles_Batch12_effectDataReads": "0", "Doubles_Batch12_effectDataWrites": "0", @@ -20,25 +20,25 @@ "Doubles_Batch12_noop": "23", "Doubles_Batch12_nzToNz": "32", "Doubles_Batch12_preDamageCalls": "0", - "Doubles_Batch12_prodStorageGas": "486500", + "Doubles_Batch12_prodStorageGas": "485200", "Doubles_Batch12_roundEndCalls": "1", "Doubles_Batch12_roundStartCalls": "0", - "Doubles_Batch12_totalSload": "1570", + "Doubles_Batch12_totalSload": "1557", "Doubles_Batch12_totalSstore": "65", "Doubles_Batch12_updateStateCalls": "0", "Doubles_Batch12_zToNz": "7", - "Doubles_Drain6_coldGas": "473844", + "Doubles_Drain6_coldGas": "468239", "Doubles_Drain6_coldSload": "40", "Doubles_Drain6_extraAccounts": "7", "Doubles_Drain6_noop": "11", "Doubles_Drain6_nzToNz": "27", - "Doubles_Drain6_prodStorageGas": "411800", - "Doubles_Drain6_totalSload": "834", + "Doubles_Drain6_prodStorageGas": "410500", + "Doubles_Drain6_totalSload": "821", "Doubles_Drain6_totalSstore": "48", "Doubles_Drain6_zToNz": "7", "Doubles_KitBatch10_afterDamageCalls": "0", "Doubles_KitBatch10_afterMoveCalls": "0", - "Doubles_KitBatch10_coldGas": "960450", + "Doubles_KitBatch10_coldGas": "960316", "Doubles_KitBatch10_coldSload": "26", "Doubles_KitBatch10_effectDataReads": "0", "Doubles_KitBatch10_effectDataWrites": "0", diff --git a/snapshots/FullyOptimizedInlineGasTest.json b/snapshots/FullyOptimizedInlineGasTest.json index 1e62e786..cca6e916 100644 --- a/snapshots/FullyOptimizedInlineGasTest.json +++ b/snapshots/FullyOptimizedInlineGasTest.json @@ -1,5 +1,5 @@ { - "Fast_Battle1_coldGas": "1077548", + "Fast_Battle1_coldGas": "1075929", "Fast_Battle1_coldSload": "199", "Fast_Battle1_extraAccounts": "52", "Fast_Battle1_noop": "23", @@ -8,7 +8,7 @@ "Fast_Battle1_totalSload": "977", "Fast_Battle1_totalSstore": "112", "Fast_Battle1_zToNz": "21", - "Fast_Battle2_coldGas": "1008744", + "Fast_Battle2_coldGas": "1007012", "Fast_Battle2_coldSload": "209", "Fast_Battle2_extraAccounts": "52", "Fast_Battle2_noop": "25", @@ -17,7 +17,7 @@ "Fast_Battle2_totalSload": "1052", "Fast_Battle2_totalSstore": "127", "Fast_Battle2_zToNz": "13", - "Fast_Battle3_coldGas": "751629", + "Fast_Battle3_coldGas": "750010", "Fast_Battle3_coldSload": "199", "Fast_Battle3_extraAccounts": "52", "Fast_Battle3_noop": "28", diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json index d1688caf..d0ca1658 100644 --- a/snapshots/MainnetReplayGasTest.json +++ b/snapshots/MainnetReplayGasTest.json @@ -6,27 +6,27 @@ "MainnetDoublesReplay1_Reused_effectHeaderReads": "45", "MainnetDoublesReplay1_Reused_effectHeaderWrites": "14", "MainnetDoublesReplay1_Reused_effectResolverCalls": "0", - "MainnetDoublesReplay1_Reused_engineCallbackStorageReads": "251", - "MainnetDoublesReplay1_Reused_engineCallbackStorageWrites": "43", - "MainnetDoublesReplay1_Reused_engineRootStorageReads": "1819", - "MainnetDoublesReplay1_Reused_engineRootStorageWrites": "118", + "MainnetDoublesReplay1_Reused_engineCallbackStorageReads": "261", + "MainnetDoublesReplay1_Reused_engineCallbackStorageWrites": "44", + "MainnetDoublesReplay1_Reused_engineRootStorageReads": "1796", + "MainnetDoublesReplay1_Reused_engineRootStorageWrites": "117", "MainnetDoublesReplay1_Reused_engineStorageLoadCommitFloorOps": "83", - "MainnetDoublesReplay1_Reused_engineStorageReads": "2070", - "MainnetDoublesReplay1_Reused_engineStorageRemovableOps": "2148", + "MainnetDoublesReplay1_Reused_engineStorageReads": "2057", + "MainnetDoublesReplay1_Reused_engineStorageRemovableOps": "2135", "MainnetDoublesReplay1_Reused_engineStorageUniqueTouchedSlots": "55", "MainnetDoublesReplay1_Reused_engineStorageUniqueWrittenSlots": "28", "MainnetDoublesReplay1_Reused_engineStorageWrites": "161", - "MainnetDoublesReplay1_Reused_execGas": "1206172", + "MainnetDoublesReplay1_Reused_execGas": "1199563", "MainnetDoublesReplay1_Reused_externalStorageReads": "6", "MainnetDoublesReplay1_Reused_externalStorageWrites": "0", - "MainnetDoublesReplay1_Reused_headerFrameCallbackPassthroughOps": "144", + "MainnetDoublesReplay1_Reused_headerFrameCallbackPassthroughOps": "152", "MainnetDoublesReplay1_Reused_headerFrameCleanInvalidationBoundaries": "31", - "MainnetDoublesReplay1_Reused_headerFrameCommitsWithLegacy": "27", + "MainnetDoublesReplay1_Reused_headerFrameCommitsWithLegacy": "26", "MainnetDoublesReplay1_Reused_headerFrameDirtyFlushBoundaries": "13", - "MainnetDoublesReplay1_Reused_headerFrameLoadsWithLegacy": "191", - "MainnetDoublesReplay1_Reused_headerFrameReloadedWords": "184", - "MainnetDoublesReplay1_Reused_headerFrameRemovableStorageOpsWithLegacy": "909", - "MainnetDoublesReplay1_Reused_headerFrameStorageOpsWithLegacy": "362", + "MainnetDoublesReplay1_Reused_headerFrameLoadsWithLegacy": "198", + "MainnetDoublesReplay1_Reused_headerFrameReloadedWords": "191", + "MainnetDoublesReplay1_Reused_headerFrameRemovableStorageOpsWithLegacy": "887", + "MainnetDoublesReplay1_Reused_headerFrameStorageOpsWithLegacy": "376", "MainnetDoublesReplay1_Reused_legacyAbilityBoundaries": "2", "MainnetDoublesReplay1_Reused_legacyBoundaryUpperBound": "44", "MainnetDoublesReplay1_Reused_legacyEffectBoundaries": "35", @@ -34,14 +34,14 @@ "MainnetDoublesReplay1_Reused_lifecycleCalls": "34", "MainnetDoublesReplay1_Reused_monStateCleanInvalidationBoundaries": "14", "MainnetDoublesReplay1_Reused_monStateDirtyFlushBoundaries": "30", - "MainnetDoublesReplay1_Reused_monStateFrameCommitsWithLegacy": "55", - "MainnetDoublesReplay1_Reused_monStateFrameLoadsWithLegacy": "145", - "MainnetDoublesReplay1_Reused_monStateFrameReloadedLanes": "137", + "MainnetDoublesReplay1_Reused_monStateFrameCommitsWithLegacy": "56", + "MainnetDoublesReplay1_Reused_monStateFrameLoadsWithLegacy": "144", + "MainnetDoublesReplay1_Reused_monStateFrameReloadedLanes": "136", "MainnetDoublesReplay1_Reused_monStateFrameStorageOps": "16", "MainnetDoublesReplay1_Reused_monStateFrameStorageOpsWithLegacy": "200", - "MainnetDoublesReplay1_Reused_monStateReads": "611", - "MainnetDoublesReplay1_Reused_monStateRemovableStorageOps": "668", - "MainnetDoublesReplay1_Reused_monStateRemovableStorageOpsWithLegacy": "484", + "MainnetDoublesReplay1_Reused_monStateReads": "606", + "MainnetDoublesReplay1_Reused_monStateRemovableStorageOps": "663", + "MainnetDoublesReplay1_Reused_monStateRemovableStorageOpsWithLegacy": "479", "MainnetDoublesReplay1_Reused_monStateUniqueTouchedLanes": "8", "MainnetDoublesReplay1_Reused_monStateUniqueWrittenLanes": "8", "MainnetDoublesReplay1_Reused_monStateWrites": "73", @@ -53,7 +53,7 @@ "MainnetDoublesReplay1_Reused_roundEndCalls": "19", "MainnetDoublesReplay1_Reused_roundStartCalls": "0", "MainnetDoublesReplay1_Reused_statusClassCallbacks": "0", - "MainnetDoublesReplay1_Reused_storage_battleDataReads": "788", + "MainnetDoublesReplay1_Reused_storage_battleDataReads": "782", "MainnetDoublesReplay1_Reused_storage_battleDataUniqueTouched": "2", "MainnetDoublesReplay1_Reused_storage_battleDataUniqueWritten": "2", "MainnetDoublesReplay1_Reused_storage_battleDataWrites": "38", @@ -61,7 +61,7 @@ "MainnetDoublesReplay1_Reused_storage_boostsUniqueTouched": "2", "MainnetDoublesReplay1_Reused_storage_boostsUniqueWritten": "2", "MainnetDoublesReplay1_Reused_storage_boostsWrites": "2", - "MainnetDoublesReplay1_Reused_storage_configHeaderReads": "415", + "MainnetDoublesReplay1_Reused_storage_configHeaderReads": "413", "MainnetDoublesReplay1_Reused_storage_configHeaderUniqueTouched": "5", "MainnetDoublesReplay1_Reused_storage_configHeaderUniqueWritten": "5", "MainnetDoublesReplay1_Reused_storage_configHeaderWrites": "30", @@ -74,7 +74,7 @@ "MainnetDoublesReplay1_Reused_storage_globalKVUniqueTouched": "3", "MainnetDoublesReplay1_Reused_storage_globalKVUniqueWritten": "3", "MainnetDoublesReplay1_Reused_storage_globalKVWrites": "4", - "MainnetDoublesReplay1_Reused_storage_monStateReads": "611", + "MainnetDoublesReplay1_Reused_storage_monStateReads": "606", "MainnetDoublesReplay1_Reused_storage_monStateUniqueTouched": "8", "MainnetDoublesReplay1_Reused_storage_monStateUniqueWritten": "8", "MainnetDoublesReplay1_Reused_storage_monStateWrites": "73", @@ -92,84 +92,84 @@ "MainnetDoublesReplay1_Reused_storage_staticCatalogWrites": "0", "MainnetDoublesReplay1_Reused_updateStateCalls": "2", "MainnetReplay_Reused_afterDamageCalls": "9", - "MainnetReplay_Reused_afterMoveCalls": "19", + "MainnetReplay_Reused_afterMoveCalls": "18", "MainnetReplay_Reused_effectDataReads": "0", "MainnetReplay_Reused_effectDataWrites": "0", - "MainnetReplay_Reused_effectHeaderReads": "191", - "MainnetReplay_Reused_effectHeaderWrites": "39", - "MainnetReplay_Reused_effectResolverCalls": "19", - "MainnetReplay_Reused_engineCallbackStorageReads": "1052", - "MainnetReplay_Reused_engineCallbackStorageWrites": "199", - "MainnetReplay_Reused_engineRootStorageReads": "1956", - "MainnetReplay_Reused_engineRootStorageWrites": "212", + "MainnetReplay_Reused_effectHeaderReads": "181", + "MainnetReplay_Reused_effectHeaderWrites": "38", + "MainnetReplay_Reused_effectResolverCalls": "18", + "MainnetReplay_Reused_engineCallbackStorageReads": "1045", + "MainnetReplay_Reused_engineCallbackStorageWrites": "195", + "MainnetReplay_Reused_engineRootStorageReads": "1916", + "MainnetReplay_Reused_engineRootStorageWrites": "208", "MainnetReplay_Reused_engineStorageLoadCommitFloorOps": "135", - "MainnetReplay_Reused_engineStorageReads": "3008", - "MainnetReplay_Reused_engineStorageRemovableOps": "3284", + "MainnetReplay_Reused_engineStorageReads": "2961", + "MainnetReplay_Reused_engineStorageRemovableOps": "3229", "MainnetReplay_Reused_engineStorageUniqueTouchedSlots": "84", "MainnetReplay_Reused_engineStorageUniqueWrittenSlots": "51", - "MainnetReplay_Reused_engineStorageWrites": "411", - "MainnetReplay_Reused_execGas": "2102467", + "MainnetReplay_Reused_engineStorageWrites": "403", + "MainnetReplay_Reused_execGas": "2069335", "MainnetReplay_Reused_externalStorageReads": "18", "MainnetReplay_Reused_externalStorageWrites": "0", - "MainnetReplay_Reused_headerFrameCallbackPassthroughOps": "535", + "MainnetReplay_Reused_headerFrameCallbackPassthroughOps": "532", "MainnetReplay_Reused_headerFrameCleanInvalidationBoundaries": "68", - "MainnetReplay_Reused_headerFrameCommitsWithLegacy": "60", - "MainnetReplay_Reused_headerFrameDirtyFlushBoundaries": "29", - "MainnetReplay_Reused_headerFrameLoadsWithLegacy": "318", - "MainnetReplay_Reused_headerFrameReloadedWords": "310", - "MainnetReplay_Reused_headerFrameRemovableStorageOpsWithLegacy": "975", - "MainnetReplay_Reused_headerFrameStorageOpsWithLegacy": "913", + "MainnetReplay_Reused_headerFrameCommitsWithLegacy": "57", + "MainnetReplay_Reused_headerFrameDirtyFlushBoundaries": "28", + "MainnetReplay_Reused_headerFrameLoadsWithLegacy": "305", + "MainnetReplay_Reused_headerFrameReloadedWords": "297", + "MainnetReplay_Reused_headerFrameRemovableStorageOpsWithLegacy": "972", + "MainnetReplay_Reused_headerFrameStorageOpsWithLegacy": "894", "MainnetReplay_Reused_legacyAbilityBoundaries": "2", - "MainnetReplay_Reused_legacyBoundaryUpperBound": "97", - "MainnetReplay_Reused_legacyEffectBoundaries": "78", + "MainnetReplay_Reused_legacyBoundaryUpperBound": "96", + "MainnetReplay_Reused_legacyEffectBoundaries": "77", "MainnetReplay_Reused_legacyMoveBoundaries": "17", - "MainnetReplay_Reused_lifecycleCalls": "84", - "MainnetReplay_Reused_monStateCleanInvalidationBoundaries": "28", + "MainnetReplay_Reused_lifecycleCalls": "83", + "MainnetReplay_Reused_monStateCleanInvalidationBoundaries": "27", "MainnetReplay_Reused_monStateDirtyFlushBoundaries": "69", "MainnetReplay_Reused_monStateFrameCommitsWithLegacy": "98", - "MainnetReplay_Reused_monStateFrameLoadsWithLegacy": "158", - "MainnetReplay_Reused_monStateFrameReloadedLanes": "150", + "MainnetReplay_Reused_monStateFrameLoadsWithLegacy": "154", + "MainnetReplay_Reused_monStateFrameReloadedLanes": "146", "MainnetReplay_Reused_monStateFrameStorageOps": "16", - "MainnetReplay_Reused_monStateFrameStorageOpsWithLegacy": "256", - "MainnetReplay_Reused_monStateReads": "779", - "MainnetReplay_Reused_monStateRemovableStorageOps": "911", - "MainnetReplay_Reused_monStateRemovableStorageOpsWithLegacy": "671", + "MainnetReplay_Reused_monStateFrameStorageOpsWithLegacy": "252", + "MainnetReplay_Reused_monStateReads": "768", + "MainnetReplay_Reused_monStateRemovableStorageOps": "899", + "MainnetReplay_Reused_monStateRemovableStorageOpsWithLegacy": "663", "MainnetReplay_Reused_monStateUniqueTouchedLanes": "8", "MainnetReplay_Reused_monStateUniqueWrittenLanes": "8", - "MainnetReplay_Reused_monStateWrites": "148", + "MainnetReplay_Reused_monStateWrites": "147", "MainnetReplay_Reused_moveDecisionCallbacks": "0", "MainnetReplay_Reused_moveResolverCalls": "4", "MainnetReplay_Reused_moveResolverContinuations": "2", - "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "107", + "MainnetReplay_Reused_nonHookHeaderReadUpperBound": "98", "MainnetReplay_Reused_preDamageCalls": "0", "MainnetReplay_Reused_roundEndCalls": "56", "MainnetReplay_Reused_roundStartCalls": "0", "MainnetReplay_Reused_statusClassCallbacks": "0", - "MainnetReplay_Reused_storage_battleDataReads": "814", + "MainnetReplay_Reused_storage_battleDataReads": "809", "MainnetReplay_Reused_storage_battleDataUniqueTouched": "2", "MainnetReplay_Reused_storage_battleDataUniqueWritten": "1", "MainnetReplay_Reused_storage_battleDataWrites": "92", - "MainnetReplay_Reused_storage_boostsReads": "46", + "MainnetReplay_Reused_storage_boostsReads": "43", "MainnetReplay_Reused_storage_boostsUniqueTouched": "18", "MainnetReplay_Reused_storage_boostsUniqueWritten": "18", - "MainnetReplay_Reused_storage_boostsWrites": "39", - "MainnetReplay_Reused_storage_configHeaderReads": "899", + "MainnetReplay_Reused_storage_boostsWrites": "37", + "MainnetReplay_Reused_storage_configHeaderReads": "886", "MainnetReplay_Reused_storage_configHeaderUniqueTouched": "6", "MainnetReplay_Reused_storage_configHeaderUniqueWritten": "5", - "MainnetReplay_Reused_storage_configHeaderWrites": "83", - "MainnetReplay_Reused_storage_effectsReads": "191", + "MainnetReplay_Reused_storage_configHeaderWrites": "79", + "MainnetReplay_Reused_storage_effectsReads": "181", "MainnetReplay_Reused_storage_effectsUniqueTouched": "14", "MainnetReplay_Reused_storage_effectsUniqueWritten": "14", - "MainnetReplay_Reused_storage_effectsWrites": "39", + "MainnetReplay_Reused_storage_effectsWrites": "38", "MainnetReplay_Reused_storage_firstOtherSlot": "0", "MainnetReplay_Reused_storage_globalKVReads": "27", "MainnetReplay_Reused_storage_globalKVUniqueTouched": "3", "MainnetReplay_Reused_storage_globalKVUniqueWritten": "2", "MainnetReplay_Reused_storage_globalKVWrites": "7", - "MainnetReplay_Reused_storage_monStateReads": "779", + "MainnetReplay_Reused_storage_monStateReads": "768", "MainnetReplay_Reused_storage_monStateUniqueTouched": "8", "MainnetReplay_Reused_storage_monStateUniqueWritten": "8", - "MainnetReplay_Reused_storage_monStateWrites": "148", + "MainnetReplay_Reused_storage_monStateWrites": "147", "MainnetReplay_Reused_storage_otherReads": "0", "MainnetReplay_Reused_storage_otherUniqueTouched": "0", "MainnetReplay_Reused_storage_otherUniqueWritten": "0", @@ -178,15 +178,15 @@ "MainnetReplay_Reused_storage_shellUniqueTouched": "3", "MainnetReplay_Reused_storage_shellUniqueWritten": "3", "MainnetReplay_Reused_storage_shellWrites": "3", - "MainnetReplay_Reused_storage_staticCatalogReads": "250", + "MainnetReplay_Reused_storage_staticCatalogReads": "245", "MainnetReplay_Reused_storage_staticCatalogUniqueTouched": "30", "MainnetReplay_Reused_storage_staticCatalogUniqueWritten": "0", "MainnetReplay_Reused_storage_staticCatalogWrites": "0", "MainnetReplay_Reused_updateStateCalls": "0", - "SinglesFloor_25Noops_rawGas": "529900", + "SinglesFloor_25Noops_rawGas": "528300", "SinglesFloor_BatchFixed": "253", - "SinglesFloor_Noop_perTurn": "21196", - "SinglesFloor_OneNoop_rawGas": "21449", - "SinglesFloor_SendIn_rawGas": "164580", - "SinglesFloor_Whole27_rawGas": "668477" + "SinglesFloor_Noop_perTurn": "21132", + "SinglesFloor_OneNoop_rawGas": "21385", + "SinglesFloor_SendIn_rawGas": "164452", + "SinglesFloor_Whole27_rawGas": "666685" } \ No newline at end of file diff --git a/snapshots/ProdPvPGasBenchmark.json b/snapshots/ProdPvPGasBenchmark.json index 68fb2ed2..01ffd650 100644 --- a/snapshots/ProdPvPGasBenchmark.json +++ b/snapshots/ProdPvPGasBenchmark.json @@ -1,5 +1,5 @@ { - "BuiltinBare_5Turns_coldGas": "291816", + "BuiltinBare_5Turns_coldGas": "290846", "BuiltinBare_5Turns_coldSload": "75", "BuiltinBare_5Turns_extraAccounts": "15", "BuiltinBare_5Turns_noop": "10", @@ -8,7 +8,7 @@ "BuiltinBare_5Turns_totalSload": "435", "BuiltinBare_5Turns_totalSstore": "45", "BuiltinBare_5Turns_zToNz": "2", - "BuiltinHooked_5Turns_coldGas": "292634", + "BuiltinHooked_5Turns_coldGas": "291664", "BuiltinHooked_5Turns_coldSload": "75", "BuiltinHooked_5Turns_extraAccounts": "15", "BuiltinHooked_5Turns_noop": "10", @@ -17,7 +17,7 @@ "BuiltinHooked_5Turns_totalSload": "435", "BuiltinHooked_5Turns_totalSstore": "45", "BuiltinHooked_5Turns_zToNz": "2", - "Builtin_Drain3_coldGas": "146299", + "Builtin_Drain3_coldGas": "145871", "Builtin_Drain3_coldSload": "21", "Builtin_Drain3_extraAccounts": "3", "Builtin_Drain3_noop": "7", @@ -26,7 +26,7 @@ "Builtin_Drain3_totalSload": "236", "Builtin_Drain3_totalSstore": "24", "Builtin_Drain3_zToNz": "2", - "Builtin_Stage3_coldGas": "90049", + "Builtin_Stage3_coldGas": "90115", "Builtin_Stage3_coldSload": "9", "Builtin_Stage3_extraAccounts": "3", "Builtin_Stage3_noop": "0", diff --git a/src/Engine.sol b/src/Engine.sol index df32546c..0f0e15ba 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -3091,6 +3091,81 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } } + /// @notice Spread variant: honors `targetBits` as a full slot mask instead of collapsing it to the + /// lowest set bit, resolving the attacker's stats once for the whole sweep. + /// @dev Purely additive — dispatchCustomAttack is untouched, so single-target callers are byte + /// identical. Every target rolls its own accuracy and damage variance. Empty lanes and KO'd + /// mons are skipped, and the sweep stops the moment a KO ends the battle. Returns summed + /// damage only: a single eventType is meaningless across several targets. + function dispatchCustomAttackMulti( + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint32 basePower, + uint32 accuracy, + uint256 volatility, + Type moveType, + MoveClass moveClass, + uint256 rng, + uint256 critRate + ) external returns (int32 totalDamage) { + if (battleKeyForWrite == bytes32(0)) { + revert NoWriteAllowed(); + } + BattleConfig storage config = battleConfig[storageKeyForWrite]; + BattleData storage battle = battleData[battleKeyForWrite]; + + uint256 baseHash = AttackCalculator.mixRngForAttacker(rng, attackerPlayerIndex); + uint256 source = uint256(uint160(msg.sender)); + DamageCalcContext memory ctx; + bool ctxSeeded; + + for (uint256 s; s < 4;) { + if (targetBits & (1 << s) != 0) { + uint256 defenderPlayerIndex = s >> 1; + uint256 defenderMonIndex = _slotActive(battle, s); + if ( + defenderMonIndex != EMPTY_ACTIVE_LANE + && !_getMonState(config, defenderPlayerIndex, defenderMonIndex).isKnockedOut + ) { + // Per-slot hash, not a per-slot slice: _calculateDamageCore already consumes bits + // 64+ for volatility and crit, so slices would collide. + uint256 h = uint256(keccak256(abi.encode(baseHash, s))); + if ((uint64(h) % 100) < accuracy) { + if (ctxSeeded) { + _fillDefenderContext(ctx, config, defenderPlayerIndex, defenderMonIndex); + } else { + ctx = _getDamageCalcContextInternal( + config, attackerPlayerIndex, attackerMonIndex, defenderPlayerIndex, defenderMonIndex + ); + ctxSeeded = true; + } + + uint32 scaledBasePower = + TypeCalcLib.getTypeEffectiveness(moveType, ctx.defenderType1, basePower); + if (ctx.defenderType2 != Type.None) { + scaledBasePower = + TypeCalcLib.getTypeEffectiveness(moveType, ctx.defenderType2, scaledBasePower); + } + (int32 damage,) = AttackCalculator._calculateDamageCore( + ctx, scaledBasePower, moveClass, volatility, h, critRate + ); + if (damage != 0) { + _dealDamageInternal(config, defenderPlayerIndex, defenderMonIndex, damage, source); + totalDamage += damage; + if (battle.winnerIndex != 2) { + return totalDamage; + } + } + } + } + } + unchecked { + ++s; + } + } + } + /// @notice Slot-addressed pivot for 2-slot battles (self-switch and force-out moves resolve /// their slot via TargetLib.slotOfMon). Applies the same silent-no-op legality gates /// as the scheduler's switch action; the end-of-turn flag recompute covers the rest. @@ -4025,18 +4100,22 @@ contract Engine is IEngine, MappingAllocator, EIP712 { uint256 moveIndex, uint256 activeMonIndex ) private view returns (uint256) { - if (moveIndex == SWITCH_MOVE_INDEX || moveIndex == NO_OP_MOVE_INDEX) { + if (moveIndex == SWITCH_MOVE_INDEX) { return SWITCH_PRIORITY; } + // Resting orders on speed like any ordinary move. + if (moveIndex == NO_OP_MOVE_INDEX) { + return DEFAULT_PRIORITY; + } // Out-of-lane moveIndex / zero lane = "no move here"; treat as the same priority as a // no-op so _handleMove can silently skip it later. StoredMon storage attackerMon = _getTeamMon(config, playerIndex, activeMonIndex); if (moveIndex >= MOVE_LANES_PER_MON) { - return SWITCH_PRIORITY; + return DEFAULT_PRIORITY; } uint256 raw = attackerMon.moves[moveIndex]; if (raw == 0) { - return SWITCH_PRIORITY; + return DEFAULT_PRIORITY; } if (raw & MOVE_META_TAG != 0) { // Deployed move with packed metadata: absolute priority, 0xF = dynamic (call live). @@ -5900,6 +5979,30 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ctx.defenderType2 = defenderMon.stats.type2; } + /// @dev Re-points an existing ctx's defender half at another slot, so a spread sweep resolves the + /// attacker's stats once. Deliberately duplicates the block above rather than both calling a + /// shared helper: factoring it out costs every single-target caller ~18-33 gas per turn + /// (measured; the helper does not fully inline and the memory struct gets passed by pointer). + function _fillDefenderContext( + DamageCalcContext memory ctx, + BattleConfig storage config, + uint256 defenderPlayerIndex, + uint256 defenderMonIndex + ) private view { + StoredMon storage defenderMon = _getTeamMon(config, defenderPlayerIndex, defenderMonIndex); + MonState storage defenderState = _getMonState(config, defenderPlayerIndex, defenderMonIndex); + ctx.defenderMonIndex = uint8(defenderMonIndex); + ctx.defenderDef = defenderMon.stats.defense; + ctx.defenderDefDelta = + defenderState.defenceDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : defenderState.defenceDelta; + ctx.defenderSpDef = defenderMon.stats.specialDefense; + ctx.defenderSpDefDelta = defenderState.specialDefenceDelta == CLEARED_MON_STATE_SENTINEL + ? int32(0) + : defenderState.specialDefenceDelta; + ctx.defenderType1 = defenderMon.stats.type1; + ctx.defenderType2 = defenderMon.stats.type2; + } + function getDamageCalcContext( bytes32 battleKey, uint256 attackerPlayerIndex, diff --git a/src/IEngine.sol b/src/IEngine.sol index 6fd84442..1aa493a8 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -62,6 +62,19 @@ interface IEngine { uint256 rng, uint256 critRate ) external returns (int32 damage, bytes32 eventType); + /// @notice Spread variant of dispatchCustomAttack: targetBits is a full slot mask. + function dispatchCustomAttackMulti( + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint32 basePower, + uint32 accuracy, + uint256 volatility, + Type moveType, + MoveClass moveClass, + uint256 rng, + uint256 critRate + ) external returns (int32 totalDamage); function switchActiveMon(uint256 playerIndex, uint256 monToSwitchIndex) external; function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, uint104 salt, uint16 extraData) external; function execute(bytes32 battleKey) external returns (address winner); diff --git a/src/abilities/IAbility.sol b/src/abilities/IAbility.sol index 5718f36f..f63f94d0 100644 --- a/src/abilities/IAbility.sol +++ b/src/abilities/IAbility.sol @@ -4,6 +4,5 @@ pragma solidity ^0.8.0; import "../IEngine.sol"; interface IAbility { - function name() external view returns (string memory); function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external; } diff --git a/src/effects/BasicEffect.sol b/src/effects/BasicEffect.sol index 39332ed6..418c1248 100644 --- a/src/effects/BasicEffect.sol +++ b/src/effects/BasicEffect.sol @@ -10,10 +10,6 @@ abstract contract BasicEffect is IEffect { // fresh-context step requests in the high uint16 (see IEffect). function getStepsBitmap() external pure virtual returns (uint32); - function name() external virtual returns (string memory) { - return ""; - } - // Whether or not to add the effect if the step condition is met function shouldApply(IEngine, bytes32, bytes32, uint256, uint256) external virtual returns (bool) { return true; diff --git a/src/effects/IEffect.sol b/src/effects/IEffect.sol index 4ed9d951..7c25b177 100644 --- a/src/effects/IEffect.sol +++ b/src/effects/IEffect.sol @@ -6,8 +6,6 @@ import "../IEngine.sol"; import "../Structs.sol"; interface IEffect { - function name() external returns (string memory); - // Returns static metadata: lifecycle steps in bits 0-15 and requested fresh-context steps in // bits 16-31. Storage collapses any nonzero context half to one capability bit; opted-in effects // receive all context types supported for the hook currently running. Lifecycle layout: diff --git a/src/effects/StaminaRegen.sol b/src/effects/StaminaRegen.sol index dfec7b51..b239d0bd 100644 --- a/src/effects/StaminaRegen.sol +++ b/src/effects/StaminaRegen.sol @@ -9,10 +9,6 @@ import {BasicEffect} from "./BasicEffect.sol"; /// @dev Singles test scaffolding: prod uses the engine-inline regen (which is slot-aware); /// this external effect only regens each side's slot-0 lane in 2-slot battles. contract StaminaRegen is BasicEffect { - function name() external pure override returns (string memory) { - return "Stamina Regen"; - } - // Steps: RoundEnd, AfterMove function getStepsBitmap() external pure override returns (uint32) { return 0x8084; diff --git a/src/effects/battlefield/Overclock.sol b/src/effects/battlefield/Overclock.sol index 4de2f21e..8273962f 100644 --- a/src/effects/battlefield/Overclock.sol +++ b/src/effects/battlefield/Overclock.sol @@ -16,10 +16,6 @@ contract Overclock is BasicEffect { uint8 public constant SPEED_PERCENT = 25; uint8 public constant SP_DEF_PERCENT = 25; - function name() public pure override returns (string memory) { - return "Overclock"; - } - // Steps: OnApply, RoundEnd, OnRemove, OnMonSwitchIn function getStepsBitmap() external pure override returns (uint32) { return 0x801D; diff --git a/src/effects/battlefield/Storm.sol b/src/effects/battlefield/Storm.sol new file mode 100644 index 00000000..304737c1 --- /dev/null +++ b/src/effects/battlefield/Storm.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../../Enums.sol"; +import "../../Structs.sol"; + +import {DEFAULT_ACCURACY, DEFAULT_VOL} from "../../Constants.sol"; +import {IEngine} from "../../IEngine.sol"; +import {BasicEffect} from "../BasicEffect.sol"; +import {IEffect} from "../IEffect.sol"; + +/// @notice Battlefield storm: damages every active at end of round for a fixed number of rounds. +/// Damage runs the normal formula off the summoner's stats, so it tracks their buffs and +/// debuffs and keeps resolving after they leave the field. +contract Storm is BasicEffect { + uint256 public constant DEFAULT_DURATION = 7; + uint32 public constant BASE_POWER = 50; + + // Every absolute slot; the engine skips empty lanes and KO'd mons. + uint256 private constant ALL_SLOTS = 0xF; + + // extraData: [duration 0-7 | casterPlayerIndex 8-15 | casterMonIndex 16-23] + uint256 private constant CASTER_SIDE_SHIFT = 8; + uint256 private constant CASTER_MON_SHIFT = 16; + + // Steps: RoundEnd. No OnApply — applyStorm packs the initial extraData itself, and addEffect + // stores it verbatim when the bit is clear. + function getStepsBitmap() external pure override returns (uint32) { + return 0x8004; + } + + function _pack(uint256 duration, uint256 casterSide, uint256 casterMon) internal pure returns (bytes32) { + return bytes32(duration | (casterSide << CASTER_SIDE_SHIFT) | (casterMon << CASTER_MON_SHIFT)); + } + + function _durationOf(bytes32 data) internal pure returns (uint256) { + return uint256(data) & 0xFF; + } + + function _casterSideOf(bytes32 data) internal pure returns (uint256) { + return (uint256(data) >> CASTER_SIDE_SHIFT) & 0xFF; + } + + function _casterMonOf(bytes32 data) internal pure returns (uint256) { + return (uint256(data) >> CASTER_MON_SHIFT) & 0xFF; + } + + /// @notice Summons the storm, or re-latches it to this caster and resets the clock if one is + /// already up. Exactly one storm and one latched caster exist at a time. + function applyStorm(IEngine engine, bytes32 battleKey, uint256 casterSide, uint256 casterMon) external { + bytes32 packed = _pack(DEFAULT_DURATION, casterSide, casterMon); + (bool exists, uint256 effectIndex,) = engine.getEffectData(battleKey, 2, 2, address(this)); + if (exists) { + engine.editEffect(2, effectIndex, packed); + return; + } + engine.addEffect(2, 0, IEffect(address(this)), packed); + } + + function onRoundEnd(IEngine engine, bytes32, uint256 rng, bytes32 extraData, uint256, uint256, uint256) + external + override + returns (bytes32, bool) + { + engine.dispatchCustomAttackMulti( + _casterSideOf(extraData), + _casterMonOf(extraData), + ALL_SLOTS, + BASE_POWER, + DEFAULT_ACCURACY, + DEFAULT_VOL, + Type.Lightning, + MoveClass.Special, + rng, + 0 + ); + + uint256 duration = _durationOf(extraData); + if (duration <= 1) { + return (extraData, true); + } + return (_pack(duration - 1, _casterSideOf(extraData), _casterMonOf(extraData)), false); + } +} diff --git a/src/effects/status/BlessedStatus.sol b/src/effects/status/BlessedStatus.sol index 4e19647a..b71ad37e 100644 --- a/src/effects/status/BlessedStatus.sol +++ b/src/effects/status/BlessedStatus.sol @@ -14,10 +14,6 @@ contract BlessedStatus is StatusEffect { // Healed on removal, as a fraction of max HP. int32 public constant HEAL_DENOM = 16; - function name() public pure override returns (string memory) { - return "Blessed"; - } - // Steps: OnApply (0x01), OnRemove (0x08), PreDamage (0x200); fresh PreDamage context (0x0200_0000) function getStepsBitmap() external pure override returns (uint32) { return uint32(0x02000209) | uint32(uint16(STATUS_CLASS << STATUS_CLASS_SHIFT)) | uint32(ALWAYS_APPLIES_BIT); diff --git a/src/effects/status/BurnStatus.sol b/src/effects/status/BurnStatus.sol index 3dea880c..b92bc7a5 100644 --- a/src/effects/status/BurnStatus.sol +++ b/src/effects/status/BurnStatus.sol @@ -21,10 +21,6 @@ contract BurnStatus is StatusEffect, IStatusEffect { int32 public constant DEG2_DAMAGE_DENOM = 8; int32 public constant DEG3_DAMAGE_DENOM = 4; - function name() public pure override returns (string memory) { - return "Burn"; - } - // Steps: OnApply, RoundEnd, OnRemove (no RoundStart behavior — the bit would only buy a // no-op external call per burned turn). HAS_REAPPLY routes same-class re-applies to the // degree escalation in onReapply. diff --git a/src/effects/status/FrostbiteStatus.sol b/src/effects/status/FrostbiteStatus.sol index 5b73376d..99e7fc6c 100644 --- a/src/effects/status/FrostbiteStatus.sol +++ b/src/effects/status/FrostbiteStatus.sol @@ -15,10 +15,6 @@ contract FrostbiteStatus is StatusEffect { int32 constant DAMAGE_DENOM = 16; uint8 constant SP_ATTACK_PERCENT = 50; - function name() public pure override returns (string memory) { - return "Frostbite"; - } - // Steps: OnApply, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint32) { return 0x00010000 | 0x0D | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; diff --git a/src/effects/status/PanicStatus.sol b/src/effects/status/PanicStatus.sol index 014eba47..fde43595 100644 --- a/src/effects/status/PanicStatus.sol +++ b/src/effects/status/PanicStatus.sol @@ -12,10 +12,6 @@ contract PanicStatus is StatusEffect { uint256 constant DURATION = 3; - function name() public pure override returns (string memory) { - return "Panic"; - } - // Steps: OnApply, RoundStart, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint32) { return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; diff --git a/src/effects/status/SleepStatus.sol b/src/effects/status/SleepStatus.sol index e6e2b1fc..e07822ab 100644 --- a/src/effects/status/SleepStatus.sol +++ b/src/effects/status/SleepStatus.sol @@ -14,17 +14,13 @@ contract SleepStatus is StatusEffect { uint256 constant DURATION = 3; - function name() public pure override returns (string memory) { - return "Sleep"; - } - // Steps: OnApply, RoundStart, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint32) { return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT); } - function _globalSleepKey(uint256 targetIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encodePacked(name(), targetIndex)))); + function _globalSleepKey(uint256 targetIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encodePacked(address(this), targetIndex)))); } // Extra apply condition beyond the Engine's lane gate (which already guarantees the mon is diff --git a/src/effects/status/ZapStatus.sol b/src/effects/status/ZapStatus.sol index 5a52537f..03698c81 100644 --- a/src/effects/status/ZapStatus.sol +++ b/src/effects/status/ZapStatus.sol @@ -16,10 +16,6 @@ contract ZapStatus is StatusEffect { uint8 private constant ALREADY_SKIPPED = 1; - function name() public pure override returns (string memory) { - return "Zap"; - } - // Steps: OnApply, RoundStart, RoundEnd, OnRemove function getStepsBitmap() external pure override returns (uint32) { return 0x0F | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; diff --git a/src/mons/aurox/BigBellow.sol b/src/mons/aurox/BigBellow.sol index 20ee0ccb..e83736fe 100644 --- a/src/mons/aurox/BigBellow.sol +++ b/src/mons/aurox/BigBellow.sol @@ -14,10 +14,6 @@ contract BigBellow is IMoveSet { uint8 public constant SP_ATTACK_DEBUFF_PERCENT = 50; uint8 public constant SPEED_DEBUFF_PERCENT = 50; - function name() public pure override returns (string memory) { - return "Big Bellow"; - } - function move( IEngine engine, bytes32, diff --git a/src/mons/aurox/GildedRecovery.sol b/src/mons/aurox/GildedRecovery.sol index 412e61c9..8c8e1c7b 100644 --- a/src/mons/aurox/GildedRecovery.sol +++ b/src/mons/aurox/GildedRecovery.sol @@ -13,10 +13,6 @@ contract GildedRecovery is IMoveSet { int32 public constant HEAL_PERCENT = 50; int32 public constant STAMINA_BONUS = 1; - function name() public pure override returns (string memory) { - return "Gilded Recovery"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/aurox/IronWall.sol b/src/mons/aurox/IronWall.sol index a40acdfa..cec8743a 100644 --- a/src/mons/aurox/IronWall.sol +++ b/src/mons/aurox/IronWall.sol @@ -15,10 +15,6 @@ contract IronWall is IMoveSet, BasicEffect { int32 public constant HEAL_PERCENT = 50; int32 public constant INITIAL_HEAL_PERCENT = 20; - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Iron Wall"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/aurox/UpOnly.sol b/src/mons/aurox/UpOnly.sol index 2ce731f2..b01b46d8 100644 --- a/src/mons/aurox/UpOnly.sol +++ b/src/mons/aurox/UpOnly.sol @@ -15,10 +15,6 @@ contract UpOnly is IAbility, BasicEffect { uint8 public constant ATTACK_BOOST_PERCENT = 10; // 10% attack boost per hit // IAbility implementation - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Up Only"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Check if the effect has already been set for this mon (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); diff --git a/src/mons/ekineki/NineNineNine.sol b/src/mons/ekineki/NineNineNine.sol index 27bc92bd..4ab51493 100644 --- a/src/mons/ekineki/NineNineNine.sol +++ b/src/mons/ekineki/NineNineNine.sol @@ -13,10 +13,6 @@ import {NineNineNineLib} from "./NineNineNineLib.sol"; contract NineNineNine is IMoveSet { constructor() {} - function name() external pure returns (string memory) { - return "Nine Nine Nine"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/ekineki/NineNineNineLib.sol b/src/mons/ekineki/NineNineNineLib.sol index 8e272bd1..fd739b48 100644 --- a/src/mons/ekineki/NineNineNineLib.sol +++ b/src/mons/ekineki/NineNineNineLib.sol @@ -9,8 +9,12 @@ import {IEngine} from "../../IEngine.sol"; library NineNineNineLib { uint32 constant NINE_NINE_NINE_CRIT_RATE = 90; + // Explicit tag rather than address(this): these are internal functions, so address(this) would + // resolve to whichever Ekineki move inlined them and each would address a different slot. + bytes32 private constant KEY_TAG = "NINE_NINE_NINE"; + function _getKey(uint256 playerIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, "NINE_NINE_NINE")))); + return uint64(uint256(keccak256(abi.encode(playerIndex, KEY_TAG)))); } function _getEffectiveCritRate(IEngine engine, bytes32 battleKey, uint256 playerIndex) diff --git a/src/mons/ekineki/SaviorComplex.sol b/src/mons/ekineki/SaviorComplex.sol index 3381a891..1e632f66 100644 --- a/src/mons/ekineki/SaviorComplex.sol +++ b/src/mons/ekineki/SaviorComplex.sol @@ -12,12 +12,8 @@ contract SaviorComplex is IAbility { uint8 public constant STAGE_2_BOOST = 25; // 2 KO'd uint8 public constant STAGE_3_BOOST = 30; // 3+ KO'd - function name() external pure returns (string memory) { - return "Savior Complex"; - } - - function _getSaviorComplexKey(uint256 playerIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, "SAVIOR_COMPLEX")))); + function _getSaviorComplexKey(uint256 playerIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, address(this))))); } function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { diff --git a/src/mons/ekineki/SneakAttack.sol b/src/mons/ekineki/SneakAttack.sol index 83932644..0db5a8cd 100644 --- a/src/mons/ekineki/SneakAttack.sol +++ b/src/mons/ekineki/SneakAttack.sol @@ -25,10 +25,6 @@ contract SneakAttack is IMoveSet, BasicEffect { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Sneak Attack"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/embursa/HeatBeacon.sol b/src/mons/embursa/HeatBeacon.sol index 69ee1b3c..3ecb15d5 100644 --- a/src/mons/embursa/HeatBeacon.sol +++ b/src/mons/embursa/HeatBeacon.sol @@ -23,10 +23,6 @@ contract HeatBeacon is IMoveSet { BURN_CLASS = (uint256(_BURN_STATUS.getStepsBitmap()) >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; } - function name() public pure override returns (string memory) { - return "Heat Beacon"; - } - function move( IEngine engine, bytes32, diff --git a/src/mons/embursa/HeatBeaconLib.sol b/src/mons/embursa/HeatBeaconLib.sol index f4165bfe..691c89a3 100644 --- a/src/mons/embursa/HeatBeaconLib.sol +++ b/src/mons/embursa/HeatBeaconLib.sol @@ -8,8 +8,12 @@ import "../../Enums.sol"; import {IEngine} from "../../IEngine.sol"; library HeatBeaconLib { + // Explicit tag rather than address(this): these are internal functions, so address(this) would + // resolve to whichever Embursa move inlined them and each would address a different slot. + bytes32 private constant KEY_TAG = "HEAT_BEACON"; + function _getKey(uint256 playerIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, "HEAT_BEACON")))); + return uint64(uint256(keccak256(abi.encode(playerIndex, KEY_TAG)))); } function _getPriorityBoost(IEngine engine, bytes32 battleKey, uint256 playerIndex) internal view returns (uint32) { diff --git a/src/mons/embursa/HoneyBribe.sol b/src/mons/embursa/HoneyBribe.sol index fb857bea..b9a543c4 100644 --- a/src/mons/embursa/HoneyBribe.sol +++ b/src/mons/embursa/HoneyBribe.sol @@ -16,12 +16,8 @@ contract HoneyBribe is IMoveSet { uint256 public constant MAX_DIVISOR = 3; uint8 public constant SP_DEF_PERCENT = 50; - function name() public pure override returns (string memory) { - return "Honey Bribe"; - } - - function _bribeKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, name())))); + function _bribeKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, address(this))))); } function _getBribeLevel(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) diff --git a/src/mons/embursa/Q5.sol b/src/mons/embursa/Q5.sol index 9e77bff2..d804ed29 100644 --- a/src/mons/embursa/Q5.sol +++ b/src/mons/embursa/Q5.sol @@ -24,10 +24,6 @@ contract Q5 is IMoveSet, BasicEffect { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Q5"; - } - function _packExtraData( uint256 turnCount, uint256 attackerPlayerIndex, @@ -49,8 +45,8 @@ contract Q5 is IMoveSet, BasicEffect { } // Per-side (no monIndex) so only one Q5 can be queued per side at a time, unique across mons. - function _q5GuardKey(uint256 attackerPlayerIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(attackerPlayerIndex, "Q5_ACTIVE")))); + function _q5GuardKey(uint256 attackerPlayerIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(attackerPlayerIndex, address(this))))); } function move( diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index 136fe663..d554edd2 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -33,10 +33,6 @@ contract Tinderclaws is IAbility, BasicEffect, IEffectResolver { BURN_CLASS = (uint256(_BURN_STATUS.getStepsBitmap()) >> STATUS_CLASS_SHIFT) & STATUS_CLASS_MASK; } - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Tinderclaws"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Check if the effect has already been set for this mon (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); diff --git a/src/mons/ghouliath/EternalGrudge.sol b/src/mons/ghouliath/EternalGrudge.sol index d93c220c..cea9aa82 100644 --- a/src/mons/ghouliath/EternalGrudge.sol +++ b/src/mons/ghouliath/EternalGrudge.sol @@ -14,10 +14,6 @@ contract EternalGrudge is IMoveSet { uint8 public constant ATTACK_DEBUFF_PERCENT = 50; uint8 public constant SP_ATTACK_DEBUFF_PERCENT = 50; - function name() public pure override returns (string memory) { - return "Eternal Grudge"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/ghouliath/GraveAffliction.sol b/src/mons/ghouliath/GraveAffliction.sol index a36c6f13..4a68958a 100644 --- a/src/mons/ghouliath/GraveAffliction.sol +++ b/src/mons/ghouliath/GraveAffliction.sol @@ -16,10 +16,6 @@ contract GraveAffliction is IMoveSet { // Both mons lose 1/FRACTION_DENOM of their current HP. int32 public constant FRACTION_DENOM = 2; - function name() public pure override returns (string memory) { - return "Grave Affliction"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/ghouliath/RiseFromTheGrave.sol b/src/mons/ghouliath/RiseFromTheGrave.sol index 688b8c14..ed1ed22a 100644 --- a/src/mons/ghouliath/RiseFromTheGrave.sol +++ b/src/mons/ghouliath/RiseFromTheGrave.sol @@ -14,13 +14,9 @@ contract RiseFromTheGrave is IAbility, BasicEffect { uint64 constant MON_EFFECT_IDENTIFIER = 17; // IAbility implementation - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Rise From The Grave"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Check if the effect has already been set for this mon (can only be set once) - uint64 monEffectId = uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, name())))); + uint64 monEffectId = uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, address(this))))); if (engine.getGlobalKV(battleKey, monEffectId) != 0) { return; } diff --git a/src/mons/glob/ChosenOne.sol b/src/mons/glob/ChosenOne.sol index cce41829..4616e0b1 100644 --- a/src/mons/glob/ChosenOne.sol +++ b/src/mons/glob/ChosenOne.sol @@ -2,38 +2,30 @@ pragma solidity ^0.8.0; -// @inline-ability: singleton-local +// Deliberately NOT @inline-ability: that path registers the ability itself as the mon's effect, +// but this one applies a different effect behind a once-per-battle guard. import {IEngine} from "../../IEngine.sol"; -import {EffectInstance, StatBoostToApply} from "../../Structs.sol"; import {IAbility} from "../../abilities/IAbility.sol"; -import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; contract ChosenOne is IAbility { - - uint64 constant MAX_MON_INDEX = 8; - IEffect immutable BLESSED_STATUS; constructor(IEffect _BLESSED_STATUS) { BLESSED_STATUS = _BLESSED_STATUS; } - function name() public pure override returns (string memory) { - return "Chosen One"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { - // Check if global KV has already been set - if (engine.getGlobalKV(battleKey, _getChosenOneKey(playerIndex, monIndex)) == 1) { + uint64 blessKey = _getChosenOneKey(playerIndex, monIndex); + if (engine.getGlobalKV(battleKey, blessKey) != 0) { return; } engine.addEffect(playerIndex, monIndex, BLESSED_STATUS, bytes32(0)); - engine.setGlobalKV(_getChosenOneKey(playerIndex, monIndex), 1); + engine.setGlobalKV(blessKey, 1); } - function _getChosenOneKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64 scaledKey) { - scaledKey = uint64(playerIndex * MAX_MON_INDEX + monIndex); + function _getChosenOneKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, address(this))))); } } \ No newline at end of file diff --git a/src/mons/glob/GetSlippery.sol b/src/mons/glob/GetSlippery.sol new file mode 100644 index 00000000..ba9f55cf --- /dev/null +++ b/src/mons/glob/GetSlippery.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../Constants.sol"; +import "../../Enums.sol"; + +import {IEngine} from "../../IEngine.sol"; +import {MoveMeta, StatBoostToApply} from "../../Structs.sol"; +import {IMoveSet} from "../../moves/IMoveSet.sol"; + +contract GetSlippery is IMoveSet { + uint8 public constant ATTACK_BUFF_PERCENT = 50; + uint8 public constant SPEED_BUFF_PERCENT = 50; + + function move( + IEngine engine, + bytes32, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint256, + uint16, + uint256 + ) external { + StatBoostToApply[] memory statBoosts = new StatBoostToApply[](2); + statBoosts[0] = StatBoostToApply({ + stat: MonStateIndexName.Attack, boostPercent: ATTACK_BUFF_PERCENT, boostType: StatBoostType.Multiply + }); + statBoosts[1] = StatBoostToApply({ + stat: MonStateIndexName.Speed, boostPercent: SPEED_BUFF_PERCENT, boostType: StatBoostType.Multiply + }); + engine.addStatBoost(attackerPlayerIndex, attackerMonIndex, statBoosts, StatBoostFlag.Temp); + } + + function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { + return 2; + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return DEFAULT_PRIORITY; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Liquid; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Self; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + pure + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: 0 + }); + } +} diff --git a/src/mons/glob/HolyMolt.sol b/src/mons/glob/HolyMolt.sol new file mode 100644 index 00000000..b666de54 --- /dev/null +++ b/src/mons/glob/HolyMolt.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../Constants.sol"; +import "../../Enums.sol"; + +import {IEngine} from "../../IEngine.sol"; +import {MoveMeta} from "../../Structs.sol"; +import {IEffect} from "../../effects/IEffect.sol"; +import {IMoveSet} from "../../moves/IMoveSet.sol"; + +/// @notice Sheds half of Glob's current HP to bless itself. Free the first time, then a stamina +/// costlier on each subsequent cast until it caps. +contract HolyMolt is IMoveSet { + int32 public constant HP_DENOM = 2; + uint32 public constant MAX_STAMINA_COST = 3; + + IEffect immutable BLESSED_STATUS; + + constructor(IEffect _BLESSED_STATUS) { + BLESSED_STATUS = _BLESSED_STATUS; + } + + function _moltKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, address(this))))); + } + + function move( + IEngine engine, + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint256, + uint16, + uint256 + ) external { + // HP loss, not damage: routing through dealDamage would let the blessing this very move + // applies absorb its own cost. + int32 currentHp = + engine.getMonCurrentValue(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); + engine.updateMonState(attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp, -(currentHp / HP_DENOM)); + engine.addEffect(attackerPlayerIndex, attackerMonIndex, BLESSED_STATUS, bytes32(0)); + + uint64 moltKey = _moltKey(attackerPlayerIndex, attackerMonIndex); + uint256 casts = uint256(engine.getGlobalKV(battleKey, moltKey)); + if (casts < MAX_STAMINA_COST) { + engine.setGlobalKV(moltKey, uint192(casts + 1)); + } + } + + /// @dev Read before the engine deducts it, so it reflects the count from prior casts only. + function stamina(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) + public + view + returns (uint32) + { + return uint32(uint256(engine.getGlobalKV(battleKey, _moltKey(playerIndex, monIndex)))); + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return DEFAULT_PRIORITY + 1; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Faith; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Self; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + view + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: 0 + }); + } +} diff --git a/src/mons/glob/HolyWaterClaw.json b/src/mons/glob/HolyWaterClaw.json new file mode 100644 index 00000000..29fe264f --- /dev/null +++ b/src/mons/glob/HolyWaterClaw.json @@ -0,0 +1,9 @@ +{ + "name": "Holy Water Claw", + "basePower": 90, + "staminaCost": 3, + "moveType": "Faith", + "moveClass": "Physical", + "effectAccuracy": 0, + "effect": null +} diff --git a/src/mons/glob/WaterClaw.json b/src/mons/glob/WaterClaw.json new file mode 100644 index 00000000..6e8e976c --- /dev/null +++ b/src/mons/glob/WaterClaw.json @@ -0,0 +1,9 @@ +{ + "name": "Water Claw", + "basePower": 80, + "staminaCost": 2, + "moveType": "Liquid", + "moveClass": "Physical", + "effectAccuracy": 0, + "effect": null +} diff --git a/src/mons/gorillax/Angery.sol b/src/mons/gorillax/Angery.sol index 530ac196..080f2c93 100644 --- a/src/mons/gorillax/Angery.sol +++ b/src/mons/gorillax/Angery.sol @@ -17,10 +17,6 @@ contract Angery is IAbility, BasicEffect { int32 public constant MAX_HP_DENOM = 6; // Heal for 1/6 of HP // IAbility implementation - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Angery"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Check if the effect has already been set for this mon (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); diff --git a/src/mons/gorillax/RockPull.sol b/src/mons/gorillax/RockPull.sol index 8506ece9..86fe0453 100644 --- a/src/mons/gorillax/RockPull.sol +++ b/src/mons/gorillax/RockPull.sol @@ -31,10 +31,6 @@ contract RockPull is IMoveSet { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override returns (string memory) { - return "Rock Pull"; - } - function _didTargetChooseSwitch(IEngine engine, bytes32 battleKey, uint256 targetSlot) internal view diff --git a/src/mons/iblivion/Baselight.sol b/src/mons/iblivion/Baselight.sol index 41e8b88b..b3c50656 100644 --- a/src/mons/iblivion/Baselight.sol +++ b/src/mons/iblivion/Baselight.sol @@ -18,10 +18,6 @@ contract Baselight is IAbility, BasicEffect { uint256 public constant MAX_BASELIGHT_LEVEL = 3; uint256 public constant INITIAL_BASELIGHT_LEVEL = 1; - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Baselight"; - } - /// @dev Finds the Baselight effect on a mon and returns its state /// @return exists Whether the effect exists on this mon /// @return effectIndex The index to use with editEffect (only valid if exists) diff --git a/src/mons/iblivion/Brightback.sol b/src/mons/iblivion/Brightback.sol index 01eb9f06..e8c6559b 100644 --- a/src/mons/iblivion/Brightback.sol +++ b/src/mons/iblivion/Brightback.sol @@ -26,10 +26,6 @@ contract Brightback is IMoveSet { BASELIGHT = _BASELIGHT; } - function name() public pure override returns (string memory) { - return "Brightback"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/iblivion/Loop.sol b/src/mons/iblivion/Loop.sol index 79ba592e..cd6755c1 100644 --- a/src/mons/iblivion/Loop.sol +++ b/src/mons/iblivion/Loop.sol @@ -31,10 +31,6 @@ contract Loop is IMoveSet, BasicEffect { BASELIGHT = _BASELIGHT; } - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Loop"; - } - // The marker effect is planted once (first Loop of the battle) and never removed; its extraData // is the armed/used flag (1 = used this switch-in, 0 = armed). onMonSwitchOut resets it to 0, // so the slot count stays at 1 for the whole battle instead of churning tombstones every cycle. diff --git a/src/mons/iblivion/Renormalize.sol b/src/mons/iblivion/Renormalize.sol index 099d30ce..60d5e083 100644 --- a/src/mons/iblivion/Renormalize.sol +++ b/src/mons/iblivion/Renormalize.sol @@ -22,10 +22,6 @@ contract Renormalize is IMoveSet { LOOP = _LOOP; } - function name() public pure override returns (string memory) { - return "Renormalize"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/iblivion/UnboundedStrike.sol b/src/mons/iblivion/UnboundedStrike.sol index 6e363fe1..1d2beb9a 100644 --- a/src/mons/iblivion/UnboundedStrike.sol +++ b/src/mons/iblivion/UnboundedStrike.sol @@ -35,10 +35,6 @@ contract UnboundedStrike is IMoveSet { BASELIGHT = _BASELIGHT; } - function name() public pure override returns (string memory) { - return "Unbounded Strike"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index d6404bee..0ef60bee 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -25,12 +25,8 @@ contract ChainExpansion is IMoveSet, BasicEffect { TYPE_CALC = _TYPE_CALCULATOR; } - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Chain Expansion"; - } - - function _key(uint256 playerIndex, uint256 monIndex) internal pure returns (bytes32) { - return keccak256(abi.encode(playerIndex, monIndex, name())); + function _key(uint256 playerIndex, uint256 monIndex) internal view returns (bytes32) { + return keccak256(abi.encode(playerIndex, monIndex, address(this))); } function move( diff --git a/src/mons/inutia/Initialize.sol b/src/mons/inutia/Initialize.sol index 9ddc4cd8..565b6437 100644 --- a/src/mons/inutia/Initialize.sol +++ b/src/mons/inutia/Initialize.sol @@ -14,12 +14,8 @@ contract Initialize is IMoveSet, BasicEffect { uint8 public constant ATTACK_BUFF_PERCENT = 50; uint8 public constant SP_ATTACK_BUFF_PERCENT = 50; - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Initialize"; - } - - function _initializeKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, name())))); + function _initializeKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, address(this))))); } function move( diff --git a/src/mons/inutia/Interweaving.sol b/src/mons/inutia/Interweaving.sol index c8673749..cad0ed19 100644 --- a/src/mons/inutia/Interweaving.sol +++ b/src/mons/inutia/Interweaving.sol @@ -14,10 +14,6 @@ import {TargetLib} from "../../lib/TargetLib.sol"; contract Interweaving is IAbility, BasicEffect { uint8 public constant DECREASE_PERCENTAGE = 15; - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Interweaving"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Lower opposing mon Attack stat (mirror-slot ruling; slot 0 in singles) uint256[4] memory lanes = engine.getActiveSlots(battleKey); diff --git a/src/mons/inutia/Sanctify.sol b/src/mons/inutia/Sanctify.sol index 47e4ea94..5e2a0228 100644 --- a/src/mons/inutia/Sanctify.sol +++ b/src/mons/inutia/Sanctify.sol @@ -18,10 +18,6 @@ contract Sanctify is IMoveSet { BLESSED_STATUS = _BLESSED_STATUS; } - function name() public pure override returns (string memory) { - return "Sanctify"; - } - function move( IEngine engine, bytes32, diff --git a/src/mons/malalien/ActusReus.sol b/src/mons/malalien/ActusReus.sol index e1a2e67d..519b705b 100644 --- a/src/mons/malalien/ActusReus.sol +++ b/src/mons/malalien/ActusReus.sol @@ -17,10 +17,6 @@ contract ActusReus is IAbility, BasicEffect { uint8 public constant SPEED_DEBUFF_PERCENT = 50; bytes32 public constant INDICTMENT = bytes32("INDICTMENT"); - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Actus Reus"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Check if the effect has already been set for this mon (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); diff --git a/src/mons/malalien/TripleThink.sol b/src/mons/malalien/TripleThink.sol index 9c25ad11..85229e91 100644 --- a/src/mons/malalien/TripleThink.sol +++ b/src/mons/malalien/TripleThink.sol @@ -12,10 +12,6 @@ import {IMoveSet} from "../../moves/IMoveSet.sol"; contract TripleThink is IMoveSet { uint8 public constant SP_ATTACK_BUFF_PERCENT = 75; - function name() public pure override returns (string memory) { - return "Triple Think"; - } - function move( IEngine engine, bytes32, diff --git a/src/mons/mulong/CloudStrike.sol b/src/mons/mulong/CloudStrike.sol new file mode 100644 index 00000000..0c2e4a5d --- /dev/null +++ b/src/mons/mulong/CloudStrike.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../Constants.sol"; +import "../../Enums.sol"; + +import {IEngine} from "../../IEngine.sol"; +import {MoveMeta} from "../../Structs.sol"; +import {Storm} from "../../effects/battlefield/Storm.sol"; +import {IEffect} from "../../effects/IEffect.sol"; +import {IMoveSet} from "../../moves/IMoveSet.sol"; + +/// @notice Hits harder and chills more reliably while a Storm is on the field. +contract CloudStrike is IMoveSet { + uint32 public constant BASE_POWER = 70; + uint32 public constant STORM_BASE_POWER = 100; + uint8 public constant EFFECT_ACCURACY = 10; + uint8 public constant STORM_EFFECT_ACCURACY = 30; + + Storm immutable STORM; + IEffect immutable FROSTBITE_STATUS; + + constructor(Storm _STORM, IEffect _FROSTBITE_STATUS) { + STORM = _STORM; + FROSTBITE_STATUS = _FROSTBITE_STATUS; + } + + /// @dev Storm is battle-unique, so the address-keyed lookup is unambiguous — no scan of the + /// global list needed. + function _stormIsUp(IEngine engine, bytes32 battleKey) internal view returns (bool) { + (bool exists,,) = engine.getEffectData(battleKey, 2, 2, address(STORM)); + return exists; + } + + function move( + IEngine engine, + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint256, + uint16, + uint256 rng + ) external { + bool stormIsUp = _stormIsUp(engine, battleKey); + engine.dispatchStandardAttack( + attackerPlayerIndex, + attackerMonIndex, + targetBits, + stormIsUp ? STORM_BASE_POWER : BASE_POWER, + DEFAULT_ACCURACY, + DEFAULT_VOL, + Type.Ice, + MoveClass.Special, + DEFAULT_CRIT_RATE, + stormIsUp ? STORM_EFFECT_ACCURACY : EFFECT_ACCURACY, + FROSTBITE_STATUS, + rng + ); + } + + function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { + return 2; + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return DEFAULT_PRIORITY; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Ice; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Special; + } + + function basePower(IEngine engine, bytes32 battleKey) public view returns (uint32) { + return _stormIsUp(engine, battleKey) ? STORM_BASE_POWER : BASE_POWER; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + view + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: basePower(engine, battleKey) + }); + } +} diff --git a/src/mons/mulong/KingsLeisure.sol b/src/mons/mulong/KingsLeisure.sol new file mode 100644 index 00000000..5db6b438 --- /dev/null +++ b/src/mons/mulong/KingsLeisure.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../Enums.sol"; +import "../../Structs.sol"; + +import {ALWAYS_APPLIES_BIT} from "../../Constants.sol"; +import {IEngine} from "../../IEngine.sol"; +import {IAbility} from "../../abilities/IAbility.sol"; +import {BasicEffect} from "../../effects/BasicEffect.sol"; +import {IEffect} from "../../effects/IEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; + +/// @notice For a fixed number of active turns after its first entry, Mulong halves the damage it +/// takes and the damage it deals. Arms once per battle; the countdown only advances on +/// rounds Mulong is on the field, so bench time and forced-switch turns don't burn it. +contract KingsLeisure is IAbility, BasicEffect { + uint256 public constant DURATION = 3; + uint8 public constant ATTACK_DEBUFF_PERCENT = 50; + int32 public constant DAMAGE_DENOM = 2; + + function _armedKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, address(this))))); + } + + function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { + uint64 armedKey = _armedKey(playerIndex, monIndex); + if (engine.getGlobalKV(battleKey, armedKey) != 0) { + return; + } + engine.setGlobalKV(armedKey, 1); + + // Perm, not Temp: the debuff has to survive bench cycles, so it is applied once here and + // cleared once in onRemove. + StatBoostToApply[] memory statBoosts = new StatBoostToApply[](2); + statBoosts[0] = StatBoostToApply({ + stat: MonStateIndexName.Attack, boostPercent: ATTACK_DEBUFF_PERCENT, boostType: StatBoostType.Divide + }); + statBoosts[1] = StatBoostToApply({ + stat: MonStateIndexName.SpecialAttack, boostPercent: ATTACK_DEBUFF_PERCENT, boostType: StatBoostType.Divide + }); + engine.addStatBoost(playerIndex, monIndex, statBoosts, StatBoostFlag.Perm); + + engine.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(DURATION)); + } + + // Steps: RoundEnd, OnRemove, PreDamage, ALWAYS_APPLIES; fresh PreDamage context. + // No OnApply — activateOnSwitch seeds the countdown and applies the debuff itself. + function getStepsBitmap() external pure override returns (uint32) { + return 0x0200820C; + } + + function onPreDamage( + IEngine engine, + bytes32, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256 hookContext, + uint256 + ) external override returns (bytes32, bool) { + engine.setPreDamage(TargetLib.hookPreDamage(hookContext) / DAMAGE_DENOM); + return (extraData, false); + } + + function onRoundEnd(IEngine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256) + external + pure + override + returns (bytes32, bool) + { + uint256 turnsLeft = uint256(extraData); + if (turnsLeft <= 1) { + return (extraData, true); + } + return (bytes32(turnsLeft - 1), false); + } + + function onRemove(IEngine engine, bytes32, bytes32, uint256 targetIndex, uint256 monIndex, uint256) + external + override + { + engine.removeStatBoost(targetIndex, monIndex, StatBoostFlag.Perm); + } +} diff --git a/src/mons/mulong/KingsRespite.sol b/src/mons/mulong/KingsRespite.sol new file mode 100644 index 00000000..12e78e69 --- /dev/null +++ b/src/mons/mulong/KingsRespite.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../Constants.sol"; +import "../../Enums.sol"; + +import {IEngine} from "../../IEngine.sol"; +import {MoveMeta} from "../../Structs.sol"; +import {BasicEffect} from "../../effects/BasicEffect.sol"; +import {IEffect} from "../../effects/IEffect.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; +import {AttackCalculator} from "../../moves/AttackCalculator.sol"; +import {IMoveSet} from "../../moves/IMoveSet.sol"; +import {ITypeCalculator} from "../../types/ITypeCalculator.sol"; + +/// @notice Arms a delayed strike at the chosen slot that fires the next time Mulong rests. The +/// rest itself is untouched, so it still regenerates stamina as normal. +contract KingsRespite is IMoveSet, BasicEffect { + uint32 public constant BASE_POWER = 120; + + ITypeCalculator immutable TYPE_CALCULATOR; + + constructor(ITypeCalculator _TYPE_CALCULATOR) { + TYPE_CALCULATOR = _TYPE_CALCULATOR; + } + + function move( + IEngine engine, + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint256, + uint16, + uint256 + ) external { + // Slot-bound like Q5: a later switch redirects the strike onto whoever holds the slot. + uint256 targetSlot = TargetLib.lowestSlot(targetBits); + if (targetSlot == NO_SLOT) { + return; + } + (bool exists, uint256 effectIndex,) = + engine.getEffectData(battleKey, attackerPlayerIndex, attackerMonIndex, address(this)); + if (exists) { + engine.editEffect(attackerPlayerIndex, effectIndex, bytes32(targetSlot)); + return; + } + engine.addEffect(attackerPlayerIndex, attackerMonIndex, IEffect(address(this)), bytes32(targetSlot)); + } + + function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { + return 2; + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return DEFAULT_PRIORITY; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Air; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Special; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + pure + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: BASE_POWER + }); + } + + // Steps: AfterMove, ALWAYS_APPLIES; fresh AfterMove context for rest detection. + function getStepsBitmap() external pure override returns (uint32) { + return 0x00808080; + } + + function onAfterMove( + IEngine engine, + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 hookContext + ) external override returns (bytes32, bool) { + uint256 restSlot = TargetLib.slotOfMon(hookContext, targetIndex, monIndex); + if (restSlot == NO_SLOT) { + return (extraData, false); + } + if (uint8(TargetLib.hookMoveWordAt(hookContext, restSlot)) & MOVE_INDEX_MASK != NO_OP_MOVE_INDEX) { + return (extraData, false); + } + + AttackCalculator._calculateDamage( + engine, + TYPE_CALCULATOR, + battleKey, + targetIndex, + monIndex, + uint256(1) << uint256(extraData), + BASE_POWER, + DEFAULT_ACCURACY, + DEFAULT_VOL, + Type.Air, + MoveClass.Special, + rng, + DEFAULT_CRIT_RATE + ); + return (extraData, true); + } +} diff --git a/src/mons/mulong/SummonStorm.sol b/src/mons/mulong/SummonStorm.sol new file mode 100644 index 00000000..60808cd3 --- /dev/null +++ b/src/mons/mulong/SummonStorm.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import "../../Constants.sol"; +import "../../Enums.sol"; + +import {IEngine} from "../../IEngine.sol"; +import {MoveMeta} from "../../Structs.sol"; +import {Storm} from "../../effects/battlefield/Storm.sol"; +import {IMoveSet} from "../../moves/IMoveSet.sol"; + +contract SummonStorm is IMoveSet { + Storm immutable STORM; + + constructor(Storm _STORM) { + STORM = _STORM; + } + + function move( + IEngine engine, + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint256, + uint16, + uint256 + ) external { + STORM.applyStorm(engine, battleKey, attackerPlayerIndex, attackerMonIndex); + } + + function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { + return 0; + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return DEFAULT_PRIORITY; + } + + function moveType(IEngine, bytes32) public pure returns (Type) { + return Type.Lightning; + } + + function moveClass(IEngine, bytes32) public pure returns (MoveClass) { + return MoveClass.Other; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + pure + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: 0 + }); + } +} diff --git a/src/mons/mulong/Thunderclap.json b/src/mons/mulong/Thunderclap.json new file mode 100644 index 00000000..31f91e7e --- /dev/null +++ b/src/mons/mulong/Thunderclap.json @@ -0,0 +1,9 @@ +{ + "name": "Thunderclap", + "basePower": 120, + "staminaCost": 3, + "moveType": "Lightning", + "moveClass": "Special", + "effectAccuracy": 20, + "effect": "ZapStatus" +} diff --git a/src/mons/nirvamma/Adaptor.sol b/src/mons/nirvamma/Adaptor.sol index 30536786..b751573b 100644 --- a/src/mons/nirvamma/Adaptor.sol +++ b/src/mons/nirvamma/Adaptor.sol @@ -20,10 +20,6 @@ import {TargetLib} from "../../lib/TargetLib.sol"; contract Adaptor is IAbility, BasicEffect { int32 public constant DAMAGE_DENOM = 2; - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Adaptor"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); for (uint256 i = 0; i < effects.length; i++) { diff --git a/src/mons/nirvamma/Chronoffense.sol b/src/mons/nirvamma/Chronoffense.sol index 01152018..fdb9ac7a 100644 --- a/src/mons/nirvamma/Chronoffense.sol +++ b/src/mons/nirvamma/Chronoffense.sol @@ -15,12 +15,8 @@ contract Chronoffense is IMoveSet { uint32 public constant BP_CAP = 999; uint8 public constant BOOST_PERCENT = 25; - function name() public pure returns (string memory) { - return "Chronoffense"; - } - - function _anchorKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode("Chronoffense", playerIndex, monIndex)))); + function _anchorKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(address(this), playerIndex, monIndex)))); } function move( diff --git a/src/mons/nirvamma/HardReset.sol b/src/mons/nirvamma/HardReset.sol index 967abdde..c85f7d26 100644 --- a/src/mons/nirvamma/HardReset.sol +++ b/src/mons/nirvamma/HardReset.sol @@ -24,10 +24,6 @@ contract HardReset is IMoveSet, BasicEffect { uint256 private constant OWN_FIRED_BIT = 0x2; uint256 private constant OPP_FIRED_BIT = 0x4; - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Hard Reset"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/nirvamma/ModalBolt.sol b/src/mons/nirvamma/ModalBolt.sol index f8fd24b9..d4018e03 100644 --- a/src/mons/nirvamma/ModalBolt.sol +++ b/src/mons/nirvamma/ModalBolt.sol @@ -28,12 +28,8 @@ contract ModalBolt is IMoveSet { ZAP_STATUS = _ZAP_STATUS; } - function name() public pure returns (string memory) { - return "Modal Bolt"; - } - - function _modalKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode("ModalBolt", playerIndex, monIndex)))); + function _modalKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(address(this), playerIndex, monIndex)))); } function getUsedModes(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) diff --git a/src/mons/pengym/Deadlift.sol b/src/mons/pengym/Deadlift.sol index ac5a77c5..629dbcaa 100644 --- a/src/mons/pengym/Deadlift.sol +++ b/src/mons/pengym/Deadlift.sol @@ -13,10 +13,6 @@ contract Deadlift is IMoveSet { uint8 public constant ATTACK_BUFF_PERCENT = 50; uint8 public constant DEF_BUFF_PERCENT = 50; - function name() public pure override returns (string memory) { - return "Deadlift"; - } - function move( IEngine engine, bytes32, diff --git a/src/mons/pengym/DeepFreeze.sol b/src/mons/pengym/DeepFreeze.sol index 2d6086c0..abebfdba 100644 --- a/src/mons/pengym/DeepFreeze.sol +++ b/src/mons/pengym/DeepFreeze.sol @@ -26,10 +26,6 @@ contract DeepFreeze is IMoveSet { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override returns (string memory) { - return "Deep Freeze"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/pengym/PostWorkout.sol b/src/mons/pengym/PostWorkout.sol index 7618de61..2bafab8f 100644 --- a/src/mons/pengym/PostWorkout.sol +++ b/src/mons/pengym/PostWorkout.sol @@ -12,10 +12,6 @@ import {BasicEffect} from "../../effects/BasicEffect.sol"; import {IEffect} from "../../effects/IEffect.sol"; contract PostWorkout is IAbility, BasicEffect { - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Post-Workout"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Check if the effect has already been set for this mon (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); diff --git a/src/mons/sofabbi/CarrotHarvest.sol b/src/mons/sofabbi/CarrotHarvest.sol index a38fc602..f74561fd 100644 --- a/src/mons/sofabbi/CarrotHarvest.sol +++ b/src/mons/sofabbi/CarrotHarvest.sol @@ -16,10 +16,6 @@ contract CarrotHarvest is IAbility, BasicEffect { uint256 constant CHANCE = 2; // IAbility implementation - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Carrot Harvest"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external override diff --git a/src/mons/sofabbi/Gachachacha.sol b/src/mons/sofabbi/Gachachacha.sol index 84b94fd3..fafc29fc 100644 --- a/src/mons/sofabbi/Gachachacha.sol +++ b/src/mons/sofabbi/Gachachacha.sol @@ -33,10 +33,6 @@ contract Gachachacha is IMoveSet { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override returns (string memory) { - return "Gachachacha"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/sofabbi/GuestFeature.sol b/src/mons/sofabbi/GuestFeature.sol index 7681c453..f524deba 100644 --- a/src/mons/sofabbi/GuestFeature.sol +++ b/src/mons/sofabbi/GuestFeature.sol @@ -20,10 +20,6 @@ contract GuestFeature is IMoveSet { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override returns (string memory) { - return "Guest Feature"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/sofabbi/SnackBreak.sol b/src/mons/sofabbi/SnackBreak.sol index a18a36a4..811a2fb8 100644 --- a/src/mons/sofabbi/SnackBreak.sol +++ b/src/mons/sofabbi/SnackBreak.sol @@ -13,12 +13,8 @@ contract SnackBreak is IMoveSet { uint256 public constant DEFAULT_HEAL_DENOM = 2; uint256 public constant MAX_DIVISOR = 3; - function name() public pure override returns (string memory) { - return "Snack Break"; - } - - function _snackKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, name())))); + function _snackKey(uint256 playerIndex, uint256 monIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, address(this))))); } function _getSnackLevel(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) diff --git a/src/mons/volthare/MegaStarBlast.sol b/src/mons/volthare/MegaStarBlast.sol index 1cb75e57..c9099933 100644 --- a/src/mons/volthare/MegaStarBlast.sol +++ b/src/mons/volthare/MegaStarBlast.sol @@ -29,10 +29,6 @@ contract MegaStarBlast is IMoveSet { OVERCLOCK = _OVERCLOCK; } - function name() public pure override returns (string memory) { - return "Mega Star Blast"; - } - function _checkForOverclock(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex) internal view diff --git a/src/mons/volthare/PreemptiveShock.sol b/src/mons/volthare/PreemptiveShock.sol index 2d73c23a..32e8311d 100644 --- a/src/mons/volthare/PreemptiveShock.sol +++ b/src/mons/volthare/PreemptiveShock.sol @@ -8,6 +8,7 @@ import {IAbility} from "../../abilities/IAbility.sol"; import {TargetLib} from "../../lib/TargetLib.sol"; import {AttackCalculator} from "../../moves/AttackCalculator.sol"; import {ITypeCalculator} from "../../types/ITypeCalculator.sol"; +import {QuickstormLib} from "./QuickstormLib.sol"; contract PreemptiveShock is IAbility { ITypeCalculator immutable TYPE_CALCULATOR; @@ -19,10 +20,6 @@ contract PreemptiveShock is IAbility { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override returns (string memory) { - return "Preemptive Shock"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external override @@ -50,11 +47,6 @@ contract PreemptiveShock is IAbility { // to switches), so a mon that enters on turn T first *acts* on turn T+1 for every entry path // (lead or mid-battle switch). Store firstActTurn+1 = T+2 so 0 stays "unset". uint256 t = engine.getTurnIdForBattleState(battleKey); - engine.setGlobalKV(_windowKey(playerIndex, monIndex), uint192(t + 2)); - } - - // Shared with Quickstorm: keyed per (side, mon) so it re-arms cleanly on every switch-in. - function _windowKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, "QUICKSTORM")))); + engine.setGlobalKV(QuickstormLib._windowKey(playerIndex, monIndex), uint192(t + 2)); } } diff --git a/src/mons/volthare/Quickstorm.sol b/src/mons/volthare/Quickstorm.sol index e28c228c..7a8be590 100644 --- a/src/mons/volthare/Quickstorm.sol +++ b/src/mons/volthare/Quickstorm.sol @@ -12,6 +12,7 @@ import {TargetLib} from "../../lib/TargetLib.sol"; import {StandardAttack} from "../../moves/StandardAttack.sol"; import {ATTACK_PARAMS} from "../../moves/StandardAttackStructs.sol"; import {ITypeCalculator} from "../../types/ITypeCalculator.sol"; +import {QuickstormLib} from "./QuickstormLib.sol"; contract Quickstorm is StandardAttack { IEffect immutable ZAP_STATUS; @@ -38,12 +39,6 @@ contract Quickstorm is StandardAttack { ZAP_STATUS = _ZAP_STATUS; } - // Shared with PreemptiveShock's switch-in anchor: the turn Volthare first gets to act after - // entering play. Only that turn matches, so Quickstorm is a strict first-action-only move. - function _windowKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, "QUICKSTORM")))); - } - function move( IEngine engine, bytes32 battleKey, @@ -55,7 +50,8 @@ contract Quickstorm is StandardAttack { uint256 rng ) public override { // Anchor stores firstActTurn+1 (0 = unset); usable only on that turn. - uint192 anchor = engine.getGlobalKV(battleKey, _windowKey(attackerPlayerIndex, attackerMonIndex)); + uint192 anchor = + engine.getGlobalKV(battleKey, QuickstormLib._windowKey(attackerPlayerIndex, attackerMonIndex)); if (anchor == 0 || uint256(anchor - 1) != engine.getTurnIdForBattleState(battleKey)) { return; } diff --git a/src/mons/volthare/QuickstormLib.sol b/src/mons/volthare/QuickstormLib.sol new file mode 100644 index 00000000..92515c68 --- /dev/null +++ b/src/mons/volthare/QuickstormLib.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +/// @notice The switch-in anchor PreemptiveShock arms and Quickstorm reads. Lives here so the two +/// contracts share one derivation rather than two matching literals. +library QuickstormLib { + // Explicit tag rather than address(this): the key is read from a different contract than the + // one that writes it, so it cannot be scoped to either. + bytes32 private constant KEY_TAG = "QUICKSTORM"; + + function _windowKey(uint256 playerIndex, uint256 monIndex) internal pure returns (uint64) { + return uint64(uint256(keccak256(abi.encode(playerIndex, monIndex, KEY_TAG)))); + } +} diff --git a/src/mons/xmon/ContagiousSlumber.sol b/src/mons/xmon/ContagiousSlumber.sol index 2ecbb60b..88dab9d9 100644 --- a/src/mons/xmon/ContagiousSlumber.sol +++ b/src/mons/xmon/ContagiousSlumber.sol @@ -18,10 +18,6 @@ contract ContagiousSlumber is IMoveSet { SLEEP_STATUS = _SLEEP_STATUS; } - function name() public pure override returns (string memory) { - return "Contagious Slumber"; - } - function move( IEngine engine, bytes32, diff --git a/src/mons/xmon/Dreamcatcher.sol b/src/mons/xmon/Dreamcatcher.sol index d0087fdf..8c3ea4a6 100644 --- a/src/mons/xmon/Dreamcatcher.sol +++ b/src/mons/xmon/Dreamcatcher.sol @@ -16,10 +16,6 @@ import {TargetLib} from "../../lib/TargetLib.sol"; contract Dreamcatcher is IAbility, BasicEffect { int32 public constant HEAL_DENOM = 16; - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Dreamcatcher"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { // Check if the effect has already been set for this mon (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); diff --git a/src/mons/xmon/InvokeTaboo.sol b/src/mons/xmon/InvokeTaboo.sol index 9f68dd5b..57c6afeb 100644 --- a/src/mons/xmon/InvokeTaboo.sol +++ b/src/mons/xmon/InvokeTaboo.sol @@ -19,10 +19,6 @@ contract InvokeTaboo is IMoveSet, BasicEffect { SLEEP_STATUS = _SLEEP_STATUS; } - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Invoke Taboo"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/xmon/OldVengeance.sol b/src/mons/xmon/OldVengeance.sol index 64794dba..fed1ced4 100644 --- a/src/mons/xmon/OldVengeance.sol +++ b/src/mons/xmon/OldVengeance.sol @@ -33,10 +33,6 @@ contract OldVengeance is IMoveSet, BasicEffect { TYPE_CALCULATOR = _TYPE_CALCULATOR; } - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Old Vengeance"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/mons/xmon/Somniphobia.sol b/src/mons/xmon/Somniphobia.sol index c6f981c4..6a6c1824 100644 --- a/src/mons/xmon/Somniphobia.sol +++ b/src/mons/xmon/Somniphobia.sol @@ -24,10 +24,6 @@ contract Somniphobia is IMoveSet, BasicEffect { return (uint256(data) >> (casterSide << 4)) & 0xFFFF; } - function name() public pure override(IMoveSet, BasicEffect) returns (string memory) { - return "Somniphobia"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/src/moves/IMoveSet.sol b/src/moves/IMoveSet.sol index 34ac4f22..8c3baf61 100644 --- a/src/moves/IMoveSet.sol +++ b/src/moves/IMoveSet.sol @@ -6,7 +6,6 @@ import "../IEngine.sol"; import "../Structs.sol"; interface IMoveSet { - function name() external view returns (string memory); /// @param targetBits Absolute-slot bitmask chosen by the player (top nibble of the wire /// extraData). In singles the engine passes the implied target (the opposing slot). /// @param activesPacked Every slot's active roster index (one 8-bit lane per absolute slot, diff --git a/test/DoublesDispatchStatusTest.sol b/test/DoublesDispatchStatusTest.sol index eb16dd0a..a735eb84 100644 --- a/test/DoublesDispatchStatusTest.sol +++ b/test/DoublesDispatchStatusTest.sol @@ -37,10 +37,6 @@ contract OverclockCastMove is IMoveSet { OC = oc; } - function name() external pure returns (string memory) { - return "Overclock Cast"; - } - function move( IEngine engine, bytes32 battleKey, @@ -88,10 +84,6 @@ contract OverclockCastMove is IMoveSet { /// @dev Metadata getters revert: a battle only executes with this move if the engine reads /// the word's packed stamina/priority instead of staticcalling. contract RevertingMetaAttack is IMoveSet { - function name() external pure returns (string memory) { - return "Reverting Meta"; - } - function move( IEngine engine, bytes32, diff --git a/test/DoublesEngineTest.sol b/test/DoublesEngineTest.sol index 6401671f..a1b76b37 100644 --- a/test/DoublesEngineTest.sol +++ b/test/DoublesEngineTest.sol @@ -26,10 +26,6 @@ import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; /// @dev Priority +2 ally-speed boost (+127% Temp on own side's mon from the payload low 2 bits): /// resolves before default-priority attacks so the dynamic scheduler's re-pick is visible. contract FastAllySpeedBoost is IMoveSet { - function name() external pure returns (string memory) { - return "Fast Ally Speed Boost"; - } - function move( IEngine engine, bytes32, diff --git a/test/EngineOptimizationTest.sol b/test/EngineOptimizationTest.sol index c0790b25..6fdfda5a 100644 --- a/test/EngineOptimizationTest.sol +++ b/test/EngineOptimizationTest.sol @@ -32,10 +32,6 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; contract KOOpponentOnSwitchAbility is IAbility { - function name() external pure returns (string memory) { - return "KO Opponent On Switch"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256) external { uint256 opponentIndex = 1 - playerIndex; BattleContext memory ctx = engine.getBattleContext(battleKey); diff --git a/test/MultiTargetDispatchTest.sol b/test/MultiTargetDispatchTest.sol new file mode 100644 index 00000000..b157f202 --- /dev/null +++ b/test/MultiTargetDispatchTest.sol @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "../src/Constants.sol"; +import "../src/Enums.sol"; +import "../src/Structs.sol"; + +import {Engine} from "../src/Engine.sol"; +import {IEngine} from "../src/IEngine.sol"; +import {IMatchmaker} from "../src/matchmaker/IMatchmaker.sol"; +import {IMoveSet} from "../src/moves/IMoveSet.sol"; + +import {defaultBattle, sideWord, targetBits} from "./abstract/SlotWire.sol"; +import {SpreadAttack} from "./mocks/SpreadAttack.sol"; +import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; +import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; + +/// @notice Engine.dispatchCustomAttackMulti — the spread variant that honors `targetBits` as a full +/// slot mask. Single-target dispatch is untouched by that addition; these pin the new path's +/// target resolution, skip rules, per-target rolls and game-over bail. +contract MultiTargetDispatchTest is Test { + address constant ALICE = address(0x1); + address constant BOB = address(0x2); + + // Absolute slots + uint256 constant A0 = 0; + uint256 constant A1 = 1; + uint256 constant B0 = 2; + uint256 constant B1 = 3; + + // Atk 10 into Def 10 at BP 10 with volatility 0 => 10 damage, absent a crit. + int32 constant BASE_HIT = 10; + + // critRate 0 still crits when critRoll % 100 == 0 (~1%), so the rng-neutral comparisons below + // need a salt where neither dispatch path crits. Found offline by probing the engine's stream; + // hardcoded so the test carries no search loop. + uint104 constant NO_CRIT_SALT = 1; + + Engine engine; + TestTeamRegistry registry; + TestTypeCalculator typeCalc; + IMoveSet spread; // BP 10, acc 100, vol 0, crit 0 — multi + IMoveSet single; // identical, single-target + IMoveSet spreadVolatile; // BP 10, acc 100, vol 10 — multi + IMoveSet spreadCoinflip; // BP 10, acc 50, vol 0 — multi + IMoveSet spreadLethal; // BP 500, acc 100, vol 0 — multi + bytes32 battleKey; + + function setUp() public { + engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); + registry = new TestTeamRegistry(); + typeCalc = new TestTypeCalculator(); + + spread = _mk(10, 100, 0, true); + single = _mk(10, 100, 0, false); + spreadVolatile = _mk(10, 100, DEFAULT_VOL, true); + spreadCoinflip = _mk(10, 50, 0, true); + spreadLethal = _mk(500, 100, 0, true); + + address[] memory toAdd = new address[](1); + toAdd[0] = address(this); + vm.prank(ALICE); + engine.updateMatchmakers(toAdd, new address[](0)); + vm.prank(BOB); + engine.updateMatchmakers(toAdd, new address[](0)); + } + + function _mk(uint32 basePower, uint32 accuracy, uint256 volatility, bool useMulti) internal returns (IMoveSet) { + return IMoveSet( + address( + new SpreadAttack( + SpreadAttack.Args({ + TYPE: Type.Air, + MOVE_CLASS: MoveClass.Physical, + BASE_POWER: basePower, + ACCURACY: accuracy, + VOLATILITY: volatility, + CRIT_RATE: 0, + STAMINA_COST: 1, + USE_MULTI: useMulti + }) + ) + ) + ); + } + + function _mkMon(uint32 hp, uint32 speed, uint32 attack, IMoveSet move0) internal pure returns (Mon memory mon) { + uint256[] memory moves = new uint256[](1); + moves[0] = uint256(uint160(address(move0))); + mon = Mon({ + stats: MonStats({ + hp: hp, + stamina: 5, + speed: speed, + attack: attack, + defense: 10, + specialAttack: 10, + specialDefense: 10, + type1: Type.Air, + type2: Type.None + }), + ability: 0, + moves: moves + }); + } + + function _startDoubles(Mon[] memory aTeam, Mon[] memory bTeam) internal { + registry.setTeam(ALICE, aTeam); + registry.setTeam(BOB, bTeam); + Battle memory battle = defaultBattle(ALICE, BOB, registry, address(this), IMatchmaker(address(this))); + (battleKey,) = engine.computeBattleKey(ALICE, BOB); + engine.startBattleWithMode(battle, BATTLE_MODE_DOUBLES); + vm.warp(vm.getBlockTimestamp() + 1); + } + + function _side(uint8 m0, uint16 e0, uint8 m1, uint16 e1) internal pure returns (uint256) { + return sideWord(m0, e0, m1, e1, NO_CRIT_SALT); + } + + function _rest() internal pure returns (uint256) { + return sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, NO_CRIT_SALT); + } + + function _turn0Leads() internal { + engine.executeWithSlotMoves( + battleKey, + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, NO_CRIT_SALT), + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, NO_CRIT_SALT) + ); + } + + function _hp(uint256 side, uint256 mon) internal view returns (int32) { + return engine.getMonStateForBattle(battleKey, side, mon, MonStateIndexName.Hp); + } + + /// Four 1000 HP mons, A0 carrying `aLead`, everyone else inert. + function _standardField(IMoveSet aLead) internal { + Mon[] memory aTeam = new Mon[](2); + aTeam[0] = _mkMon(1000, 40, 10, aLead); + aTeam[1] = _mkMon(1000, 30, 10, single); + Mon[] memory bTeam = new Mon[](2); + bTeam[0] = _mkMon(1000, 20, 10, single); + bTeam[1] = _mkMon(1000, 10, 10, single); + _startDoubles(aTeam, bTeam); + _turn0Leads(); + } + + // --------------------------------------------------------------------- + // Equivalence with the single-target path + // --------------------------------------------------------------------- + + /// @dev The guardrail. Multi with a one-bit mask must resolve the same target and land the same + /// damage as dispatchCustomAttack with that bit. The two paths derive their combat hash + /// differently by design (multi folds the slot in so spread targets roll independently), so + /// this runs rng-neutral — volatility 0, accuracy 100, non-critting salt — to isolate target + /// resolution, context building, type scaling and damage application. + function test_multi_singleBitMask_matchesSingleTargetDispatch() public { + _standardField(spread); + engine.executeWithSlotMoves(battleKey, _side(0, targetBits(B0), NO_OP_MOVE_INDEX, 0), _rest()); + int32 multiDamage = _hp(1, 0); + assertEq(multiDamage, -BASE_HIT, "spread path, one target"); + assertEq(_hp(1, 1), 0, "no bleed onto the unmasked slot"); + + _standardField(single); + engine.executeWithSlotMoves(battleKey, _side(0, targetBits(B0), NO_OP_MOVE_INDEX, 0), _rest()); + assertEq(_hp(1, 0), multiDamage, "single-target path lands identical damage"); + assertEq(_hp(1, 1), 0, "no bleed onto the unmasked slot"); + } + + // --------------------------------------------------------------------- + // Mask handling + // --------------------------------------------------------------------- + + /// @dev The whole point: every set bit takes a hit, including the attacker's own side. + function test_multi_hitsEverySetSlot() public { + _standardField(spread); + uint16 all = targetBits(A0) | targetBits(A1) | targetBits(B0) | targetBits(B1); + engine.executeWithSlotMoves(battleKey, _side(0, all, NO_OP_MOVE_INDEX, 0), _rest()); + + assertEq(_hp(0, 0), -BASE_HIT, "attacker hits itself when masked"); + assertEq(_hp(0, 1), -BASE_HIT, "ally hit"); + assertEq(_hp(1, 0), -BASE_HIT, "opposing slot 0 hit"); + assertEq(_hp(1, 1), -BASE_HIT, "opposing slot 1 hit"); + } + + /// @dev A partial mask must leave unmasked slots untouched. + function test_multi_partialMaskSkipsUnmaskedSlots() public { + _standardField(spread); + engine.executeWithSlotMoves( + battleKey, _side(0, targetBits(B0) | targetBits(B1), NO_OP_MOVE_INDEX, 0), _rest() + ); + + assertEq(_hp(1, 0), -BASE_HIT, "masked"); + assertEq(_hp(1, 1), -BASE_HIT, "masked"); + assertEq(_hp(0, 0), 0, "unmasked attacker untouched"); + assertEq(_hp(0, 1), 0, "unmasked ally untouched"); + } + + /// @dev An empty mask is a no-op, not a revert. + function test_multi_emptyMaskIsNoOp() public { + _standardField(spread); + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + + assertEq(_hp(0, 0), 0, "nothing hit"); + assertEq(_hp(0, 1), 0, "nothing hit"); + assertEq(_hp(1, 0), 0, "nothing hit"); + assertEq(_hp(1, 1), 0, "nothing hit"); + } + + /// @dev A side fielding one mon leaves its slot-1 lane empty; the sweep must skip that lane and + /// still hit the rest of the mask. + function test_multi_skipsEmptyLane() public { + Mon[] memory aTeam = new Mon[](2); + aTeam[0] = _mkMon(1000, 40, 10, spread); + aTeam[1] = _mkMon(1000, 30, 10, single); + Mon[] memory bTeam = new Mon[](1); + bTeam[0] = _mkMon(1000, 20, 10, single); + _startDoubles(aTeam, bTeam); + engine.executeWithSlotMoves( + battleKey, + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, NO_CRIT_SALT), + sideWord(SWITCH_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, NO_CRIT_SALT) + ); + assertEq(engine.getActiveSlots(battleKey)[B1], EMPTY_ACTIVE_LANE, "B1 lane is empty"); + + uint16 all = targetBits(A0) | targetBits(A1) | targetBits(B0) | targetBits(B1); + engine.executeWithSlotMoves( + battleKey, _side(0, all, NO_OP_MOVE_INDEX, 0), sideWord(NO_OP_MOVE_INDEX, 0, 0, 0, NO_CRIT_SALT) + ); + + assertEq(_hp(0, 0), -BASE_HIT, "occupied lanes still hit"); + assertEq(_hp(0, 1), -BASE_HIT, "occupied lanes still hit"); + assertEq(_hp(1, 0), -BASE_HIT, "occupied lanes still hit"); + } + + // --------------------------------------------------------------------- + // Per-target rolls + // --------------------------------------------------------------------- + + /// @dev Targets must not share one roll. With volatility the four hits land on at least two + /// distinct damage values; a shared hash would make them uniform. + function test_multi_targetsRollIndependently() public { + _standardField(spreadVolatile); + uint16 all = targetBits(A0) | targetBits(A1) | targetBits(B0) | targetBits(B1); + engine.executeWithSlotMoves(battleKey, _side(0, all, NO_OP_MOVE_INDEX, 0), _rest()); + + int32 d0 = _hp(0, 0); + int32 d1 = _hp(0, 1); + int32 d2 = _hp(1, 0); + int32 d3 = _hp(1, 1); + assertTrue(d0 < 0 && d1 < 0 && d2 < 0 && d3 < 0, "all four took damage"); + assertTrue(d0 != d1 || d0 != d2 || d0 != d3, "volatility rolls are not shared across slots"); + } + + /// @dev Accuracy is rolled per target, so a coinflip spread must be able to split hit and miss + /// within one cast. + function test_multi_accuracyRolledPerTarget() public { + _standardField(spreadCoinflip); + uint16 all = targetBits(A0) | targetBits(A1) | targetBits(B0) | targetBits(B1); + engine.executeWithSlotMoves(battleKey, _side(0, all, NO_OP_MOVE_INDEX, 0), _rest()); + + uint256 hits; + if (_hp(0, 0) < 0) hits++; + if (_hp(0, 1) < 0) hits++; + if (_hp(1, 0) < 0) hits++; + if (_hp(1, 1) < 0) hits++; + assertTrue(hits > 0 && hits < 4, "a 50% spread split hit and miss across slots"); + } + + // --------------------------------------------------------------------- + // KO and game-over handling + // --------------------------------------------------------------------- + + /// @dev A mon already KO'd when the sweep runs must be skipped rather than hit again. + function test_multi_skipsAlreadyKOdTarget() public { + Mon[] memory aTeam = new Mon[](2); + aTeam[0] = _mkMon(1000, 40, 10, spread); // slowest of side 0's actors + aTeam[1] = _mkMon(1000, 50, 10, spreadLethal); // acts first, kills B1 + Mon[] memory bTeam = new Mon[](3); + bTeam[0] = _mkMon(1000, 20, 10, single); + bTeam[1] = _mkMon(100, 10, 10, single); // dies to A1 + bTeam[2] = _mkMon(1000, 5, 10, single); + _startDoubles(aTeam, bTeam); + _turn0Leads(); + + uint16 all = targetBits(B0) | targetBits(B1); + engine.executeWithSlotMoves( + battleKey, _side(0, all, 0, targetBits(B1)), sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, NO_CRIT_SALT) + ); + + assertTrue( + engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.IsKnockedOut) != 0, "B1 was KO'd this turn" + ); + // The lethal hit alone; a sweep that failed to skip the corpse would read -510. + assertEq(_hp(1, 1), -500, "KO'd mon took no further damage from the later sweep"); + assertEq(_hp(1, 0), -BASE_HIT, "the live target still took its hit"); + } + + /// @dev When a sweep hit ends the battle, the remaining targets must not be processed. + function test_multi_stopsOnceBattleIsOver() public { + Mon[] memory aTeam = new Mon[](2); + aTeam[0] = _mkMon(1000, 40, 10, spreadLethal); + aTeam[1] = _mkMon(1000, 30, 10, single); + Mon[] memory bTeam = new Mon[](2); + bTeam[0] = _mkMon(100, 20, 10, single); + bTeam[1] = _mkMon(100, 10, 10, single); + _startDoubles(aTeam, bTeam); + _turn0Leads(); + + uint16 all = targetBits(B0) | targetBits(B1); + engine.executeWithSlotMoves( + battleKey, _side(0, all, NO_OP_MOVE_INDEX, 0), sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, NO_CRIT_SALT) + ); + + assertEq(engine.getWinner(battleKey), ALICE, "sweep ended the battle"); + } +} diff --git a/test/effects/EffectTest.sol b/test/effects/EffectTest.sol index e709b6b5..b8f58779 100644 --- a/test/effects/EffectTest.sol +++ b/test/effects/EffectTest.sol @@ -108,9 +108,6 @@ contract EffectTest is Test, BattleHelper { }) ); - // Verify the name matches - assertEq(frostbiteAttack.name(), "FrostbiteHit"); - uint256[] memory moves = new uint256[](1); moves[0] = uint256(uint160(address(frostbiteAttack))); Mon memory mon = Mon({ @@ -877,9 +874,9 @@ contract EffectTest is Test, BattleHelper { // One-sleeper-per-player gate (SleepStatus global sleep key) // ------------------------------------------------------------------ - /// @dev Mirrors SleepStatus._globalSleepKey. - function _sleepGlobalKey(uint256 targetIndex) internal pure returns (uint64) { - return uint64(uint256(keccak256(abi.encodePacked("Sleep", targetIndex)))); + /// @dev Mirrors SleepStatus._globalSleepKey, which scopes the gate to the status contract. + function _sleepGlobalKey(uint256 targetIndex) internal view returns (uint64) { + return uint64(uint256(keccak256(abi.encodePacked(address(sleepStatus), targetIndex)))); } /// @dev Finds an oracle rng value whose SleepStatus.onRoundStart wake roll for diff --git a/test/mocks/AlwaysRejectsEffect.sol b/test/mocks/AlwaysRejectsEffect.sol index 44a78bd4..83ae4f22 100644 --- a/test/mocks/AlwaysRejectsEffect.sol +++ b/test/mocks/AlwaysRejectsEffect.sol @@ -11,10 +11,6 @@ import {BasicEffect} from "../../src/effects/BasicEffect.sol"; /// @notice Effect whose shouldApply() returns false, but has ALWAYS_APPLIES_BIT set. /// Used to test that the Engine bypasses shouldApply() when the bit is set. contract AlwaysRejectsEffect is BasicEffect { - function name() external pure override returns (string memory) { - return "AlwaysRejects"; - } - // ALWAYS_APPLIES_BIT (0x8000) | RoundEnd (0x04) function getStepsBitmap() external pure override returns (uint32) { return 0x8004; diff --git a/test/mocks/BoostBenchMove.sol b/test/mocks/BoostBenchMove.sol index 599e834c..81738524 100644 --- a/test/mocks/BoostBenchMove.sol +++ b/test/mocks/BoostBenchMove.sol @@ -41,10 +41,6 @@ contract BoostBenchMove is IMoveSet { } } - function name() external pure returns (string memory) { - return "Boost Bench"; - } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint256, uint16 extraData, uint256) external { diff --git a/test/mocks/CustomAttack.sol b/test/mocks/CustomAttack.sol index f12065d1..47cc0120 100644 --- a/test/mocks/CustomAttack.sol +++ b/test/mocks/CustomAttack.sol @@ -45,10 +45,6 @@ contract CustomAttack is IMoveSet { ); } - function name() external pure returns (string memory) { - return "CustomAttack"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/test/mocks/DirectStatWriteMove.sol b/test/mocks/DirectStatWriteMove.sol index 32e42c60..beba7098 100644 --- a/test/mocks/DirectStatWriteMove.sol +++ b/test/mocks/DirectStatWriteMove.sol @@ -13,10 +13,6 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; * Engine rejects direct stat writes (stats are owned by the inlined stat-boost system). */ contract DirectStatWriteMove is IMoveSet { - function name() external pure returns (string memory) { - return "Direct Stat Write"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/DummyStatus.sol b/test/mocks/DummyStatus.sol index ee618b70..4798b5a2 100644 --- a/test/mocks/DummyStatus.sol +++ b/test/mocks/DummyStatus.sol @@ -6,10 +6,6 @@ import "../../src/Enums.sol"; import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract DummyStatus is BasicEffect { - function name() external pure override returns (string memory) { - return "Dummy"; - } - // Steps: None function getStepsBitmap() external pure override returns (uint32) { return 0x00; diff --git a/test/mocks/EditEffectAttack.sol b/test/mocks/EditEffectAttack.sol index 6145ec56..7f118e44 100644 --- a/test/mocks/EditEffectAttack.sol +++ b/test/mocks/EditEffectAttack.sol @@ -8,10 +8,6 @@ import {MoveMeta} from "../../src/Structs.sol"; import {IMoveSet} from "../../src/moves/IMoveSet.sol"; contract EditEffectAttack is IMoveSet { - function name() external pure returns (string memory) { - return "Edit Effect Attack"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/EffectAbility.sol b/test/mocks/EffectAbility.sol index 876c841c..b237b097 100644 --- a/test/mocks/EffectAbility.sol +++ b/test/mocks/EffectAbility.sol @@ -13,10 +13,6 @@ contract EffectAbility is IAbility { EFFECT = _EFFECT; } - function name() external pure returns (string memory) { - return ""; - } - function activateOnSwitch(IEngine engine, bytes32, uint256 playerIndex, uint256 monIndex) external { engine.addEffect(playerIndex, monIndex, EFFECT, ""); } diff --git a/test/mocks/EffectAttack.sol b/test/mocks/EffectAttack.sol index f4a24ebb..d89b4fe0 100644 --- a/test/mocks/EffectAttack.sol +++ b/test/mocks/EffectAttack.sol @@ -30,10 +30,6 @@ contract EffectAttack is IMoveSet { PRIORITY = args.PRIORITY; } - function name() external pure returns (string memory) { - return "Effect Attack"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/ForceSwitchMove.sol b/test/mocks/ForceSwitchMove.sol index 3281bd27..a9af12c5 100644 --- a/test/mocks/ForceSwitchMove.sol +++ b/test/mocks/ForceSwitchMove.sol @@ -26,10 +26,6 @@ contract ForceSwitchMove is IMoveSet { PRIORITY = args.PRIORITY; } - function name() external pure returns (string memory) { - return "Force Switch"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/FreshMoveContextAbility.sol b/test/mocks/FreshMoveContextAbility.sol index 4891c45e..31428fe5 100644 --- a/test/mocks/FreshMoveContextAbility.sol +++ b/test/mocks/FreshMoveContextAbility.sol @@ -40,10 +40,6 @@ contract FreshMoveContextAbility is IAbility, BasicEffect { REWRITER = IEffect(address(new RewriteMoveAfterMove())); } - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Fresh Move Context"; - } - function activateOnSwitch(IEngine engine, bytes32, uint256 playerIndex, uint256 monIndex) external override { // Stable effect-array order: the rewriter runs before this observer. engine.addEffect(playerIndex, monIndex, REWRITER, bytes32(0)); @@ -101,10 +97,6 @@ contract FreshStatusContextAbility is IAbility, BasicEffect { WRITER = IEffect(address(new AddStatusAtRoundEnd(status))); } - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Fresh Status Context"; - } - function activateOnSwitch(IEngine engine, bytes32, uint256 playerIndex, uint256 monIndex) external override { engine.addEffect(playerIndex, monIndex, WRITER, bytes32(0)); engine.addEffect(playerIndex, monIndex, IEffect(address(this)), bytes32(0)); diff --git a/test/mocks/GlobalEffectAttack.sol b/test/mocks/GlobalEffectAttack.sol index 268c1409..bd208b07 100644 --- a/test/mocks/GlobalEffectAttack.sol +++ b/test/mocks/GlobalEffectAttack.sol @@ -29,10 +29,6 @@ contract GlobalEffectAttack is IMoveSet { PRIORITY = args.PRIORITY; } - function name() external pure returns (string memory) { - return "Effect Attack"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/InstantDeathEffect.sol b/test/mocks/InstantDeathEffect.sol index 07fa8d52..4ccf97f6 100644 --- a/test/mocks/InstantDeathEffect.sol +++ b/test/mocks/InstantDeathEffect.sol @@ -9,10 +9,6 @@ import {IEngine} from "../../src/IEngine.sol"; import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract InstantDeathEffect is BasicEffect { - function name() external pure override returns (string memory) { - return "Instant Death"; - } - // Steps: RoundEnd function getStepsBitmap() external pure override returns (uint32) { return 0x04; diff --git a/test/mocks/InstantDeathOnSwitchInEffect.sol b/test/mocks/InstantDeathOnSwitchInEffect.sol index dedf8830..d03188b3 100644 --- a/test/mocks/InstantDeathOnSwitchInEffect.sol +++ b/test/mocks/InstantDeathOnSwitchInEffect.sol @@ -9,10 +9,6 @@ import {IEngine} from "../../src/IEngine.sol"; import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract InstantDeathOnSwitchInEffect is BasicEffect { - function name() external pure override returns (string memory) { - return "Instant Death On Switch"; - } - // Steps: OnMonSwitchIn function getStepsBitmap() external pure override returns (uint32) { return 0x10; diff --git a/test/mocks/MockEffectRemover.sol b/test/mocks/MockEffectRemover.sol index 7207dd58..d03abba9 100644 --- a/test/mocks/MockEffectRemover.sol +++ b/test/mocks/MockEffectRemover.sol @@ -14,10 +14,6 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; * The effect's slot index is passed as extraData. Targets the opponent's active mon. */ contract MockEffectRemover is IMoveSet { - function name() public pure override returns (string memory) { - return "Mock Effect Remover"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/test/mocks/MockKVWriterMove.sol b/test/mocks/MockKVWriterMove.sol index ee7b782c..78514fdf 100644 --- a/test/mocks/MockKVWriterMove.sol +++ b/test/mocks/MockKVWriterMove.sol @@ -14,10 +14,6 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; /// without encoding a non-zero value every time. Tests that need wider key/value ranges /// should use a different mock. contract MockKVWriterMove is IMoveSet { - function name() external pure returns (string memory) { - return "MockKVWriter"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/MockSingletonAbility.sol b/test/mocks/MockSingletonAbility.sol index 730ac840..7fb34256 100644 --- a/test/mocks/MockSingletonAbility.sol +++ b/test/mocks/MockSingletonAbility.sol @@ -11,10 +11,6 @@ import {IEffect} from "../../src/effects/IEffect.sol"; /// Registers itself as an effect on switch-in with idempotency check. /// Tracks call count in extraData for testing. contract MockSingletonAbility is IAbility, BasicEffect { - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "MockSingleton"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { (EffectInstance[] memory effects,) = engine.getEffects(battleKey, playerIndex, monIndex); for (uint256 i; i < effects.length; i++) { diff --git a/test/mocks/NeverAppliesEffect.sol b/test/mocks/NeverAppliesEffect.sol index 8b5d286d..2eef8d1a 100644 --- a/test/mocks/NeverAppliesEffect.sol +++ b/test/mocks/NeverAppliesEffect.sol @@ -10,10 +10,6 @@ import {BasicEffect} from "../../src/effects/BasicEffect.sol"; /// @notice Effect whose shouldApply() returns false and does NOT have ALWAYS_APPLIES_BIT. /// Used to verify that normal shouldApply() rejection still works. contract NeverAppliesEffect is BasicEffect { - function name() external pure override returns (string memory) { - return "NeverApplies"; - } - // RoundEnd (0x04), NO ALWAYS_APPLIES_BIT function getStepsBitmap() external pure override returns (uint32) { return 0x04; diff --git a/test/mocks/OneTurnStatBoost.sol b/test/mocks/OneTurnStatBoost.sol index f9c446de..da1063c3 100644 --- a/test/mocks/OneTurnStatBoost.sol +++ b/test/mocks/OneTurnStatBoost.sol @@ -9,10 +9,6 @@ import {IEngine} from "../../src/IEngine.sol"; import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract OneTurnStatBoost is BasicEffect { - function name() external pure override returns (string memory) { - return ""; - } - // Steps: OnApply, RoundEnd function getStepsBitmap() external pure override returns (uint32) { return 0x05; diff --git a/test/mocks/OrderRecorderAbility.sol b/test/mocks/OrderRecorderAbility.sol index 64a38a70..528041d5 100644 --- a/test/mocks/OrderRecorderAbility.sol +++ b/test/mocks/OrderRecorderAbility.sol @@ -16,10 +16,6 @@ contract OrderRecorderAbility is IAbility, BasicEffect { // entry = (uint8(step) << 16) | (side << 8) | monIndex uint256[] public entries; - function name() public pure override(IAbility, BasicEffect) returns (string memory) { - return "Order Recorder"; - } - function activateOnSwitch(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) external { (bool exists,,) = engine.getEffectData(battleKey, playerIndex, monIndex, address(this)); if (!exists) { diff --git a/test/mocks/RandomForceOutMove.sol b/test/mocks/RandomForceOutMove.sol index df4af588..1e92daf5 100644 --- a/test/mocks/RandomForceOutMove.sol +++ b/test/mocks/RandomForceOutMove.sol @@ -14,10 +14,6 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; /// @dev Forces the targeted slot to swap to a random legal replacement (the PistolSquat / /// HardReset force-out shape, minus the damage). contract RandomForceOutMove is IMoveSet { - function name() external pure returns (string memory) { - return "Random Force Out"; - } - function move( IEngine engine, bytes32 battleKey, diff --git a/test/mocks/RawDamageMove.sol b/test/mocks/RawDamageMove.sol index eb17f0b6..aa9f3c34 100644 --- a/test/mocks/RawDamageMove.sol +++ b/test/mocks/RawDamageMove.sol @@ -19,10 +19,6 @@ contract RawDamageMove is IMoveSet { DAMAGE = damage; } - function name() external pure returns (string memory) { - return "Raw Damage Move"; - } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 targetBits, uint256 activesPacked, uint16, uint256) external { diff --git a/test/mocks/ReduceSpAtkMove.sol b/test/mocks/ReduceSpAtkMove.sol index 969a02c9..69b52e13 100644 --- a/test/mocks/ReduceSpAtkMove.sol +++ b/test/mocks/ReduceSpAtkMove.sol @@ -16,10 +16,6 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; * @dev Used to test the OnUpdateMonState lifecycle hook */ contract ReduceSpAtkMove is IMoveSet { - function name() external pure returns (string memory) { - return "Reduce SpAtk"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/SelfSwitchAndDamageMove.sol b/test/mocks/SelfSwitchAndDamageMove.sol index ea1b387c..48bcdb92 100644 --- a/test/mocks/SelfSwitchAndDamageMove.sol +++ b/test/mocks/SelfSwitchAndDamageMove.sol @@ -17,10 +17,6 @@ contract SelfSwitchAndDamageMove is IMoveSet { DAMAGE = power; } - function name() external pure returns (string memory) { - return "Self Switch And Damage Move"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/SetEffectDataAttack.sol b/test/mocks/SetEffectDataAttack.sol index cb212376..ac1353dc 100644 --- a/test/mocks/SetEffectDataAttack.sol +++ b/test/mocks/SetEffectDataAttack.sol @@ -13,10 +13,6 @@ contract SetEffectDataAttack is IMoveSet { DATA = data; } - function name() external pure returns (string memory) { - return "Set Effect Data"; - } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint256, uint16 extraData, uint256) external { uint256 targetIndex = uint256(extraData) & 0x3; uint256 effectIndex = uint256(extraData) >> 2; diff --git a/test/mocks/SingleInstanceEffect.sol b/test/mocks/SingleInstanceEffect.sol index 8798c7f3..c6c79d1a 100644 --- a/test/mocks/SingleInstanceEffect.sol +++ b/test/mocks/SingleInstanceEffect.sol @@ -9,10 +9,6 @@ import {IEngine} from "../../src/IEngine.sol"; import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract SingleInstanceEffect is BasicEffect { - function name() external pure override returns (string memory) { - return "Instant Death"; - } - // Steps: OnApply function getStepsBitmap() external pure override returns (uint32) { return 0x01; diff --git a/test/mocks/SkipTurnMove.sol b/test/mocks/SkipTurnMove.sol index c0f14054..c7d37f40 100644 --- a/test/mocks/SkipTurnMove.sol +++ b/test/mocks/SkipTurnMove.sol @@ -27,10 +27,6 @@ contract SkipTurnMove is IMoveSet { PRIORITY = args.PRIORITY; } - function name() external pure returns (string memory) { - return "Skip Turn"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/SpAtkDebuffEffect.sol b/test/mocks/SpAtkDebuffEffect.sol index bf731c05..f19f846a 100644 --- a/test/mocks/SpAtkDebuffEffect.sol +++ b/test/mocks/SpAtkDebuffEffect.sol @@ -14,10 +14,6 @@ contract SpAtkDebuffEffect is StatusEffect { uint8 constant SP_ATTACK_PERCENT = 50; - function name() public pure override returns (string memory) { - return "SpAtk Debuff"; - } - // Steps: OnApply, OnRemove function getStepsBitmap() external pure override returns (uint32) { return 0x09 | uint16(STATUS_CLASS << STATUS_CLASS_SHIFT) | ALWAYS_APPLIES_BIT; diff --git a/test/mocks/SpreadAttack.sol b/test/mocks/SpreadAttack.sol new file mode 100644 index 00000000..3a1883d3 --- /dev/null +++ b/test/mocks/SpreadAttack.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../../src/Constants.sol"; +import "../../src/Enums.sol"; +import "../../src/Structs.sol"; + +import {IEngine} from "../../src/IEngine.sol"; +import {IMoveSet} from "../../src/moves/IMoveSet.sol"; + +/// @dev Routes the target nibble straight to the engine, through either the single-target or the +/// spread dispatch, so both paths can be driven with identical inputs. +contract SpreadAttack is IMoveSet { + struct Args { + Type TYPE; + MoveClass MOVE_CLASS; + uint32 BASE_POWER; + uint32 ACCURACY; + uint256 VOLATILITY; + uint256 CRIT_RATE; + uint32 STAMINA_COST; + bool USE_MULTI; + } + + Args private _a; + + constructor(Args memory args) { + _a = args; + } + + function move( + IEngine engine, + bytes32, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 targetBits, + uint256, + uint16, + uint256 rng + ) external { + Args memory a = _a; + if (a.USE_MULTI) { + engine.dispatchCustomAttackMulti( + attackerPlayerIndex, + attackerMonIndex, + targetBits, + a.BASE_POWER, + a.ACCURACY, + a.VOLATILITY, + a.TYPE, + a.MOVE_CLASS, + rng, + a.CRIT_RATE + ); + } else { + engine.dispatchCustomAttack( + attackerPlayerIndex, + attackerMonIndex, + targetBits, + a.BASE_POWER, + a.ACCURACY, + a.VOLATILITY, + a.TYPE, + a.MOVE_CLASS, + rng, + a.CRIT_RATE + ); + } + } + + function priority(IEngine, bytes32, uint256) public pure returns (uint32) { + return DEFAULT_PRIORITY; + } + + function stamina(IEngine, bytes32, uint256, uint256) public view returns (uint32) { + return _a.STAMINA_COST; + } + + function moveType(IEngine, bytes32) public view returns (Type) { + return _a.TYPE; + } + + function moveClass(IEngine, bytes32) public view returns (MoveClass) { + return _a.MOVE_CLASS; + } + + function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) + external + view + returns (MoveMeta memory) + { + return MoveMeta({ + moveType: moveType(engine, battleKey), + moveClass: moveClass(engine, battleKey), + priority: priority(engine, battleKey, attackerPlayerIndex), + stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), + basePower: 0 + }); + } +} diff --git a/test/mocks/StaminaGainKOEffect.sol b/test/mocks/StaminaGainKOEffect.sol index 93227664..003195c7 100644 --- a/test/mocks/StaminaGainKOEffect.sol +++ b/test/mocks/StaminaGainKOEffect.sol @@ -12,10 +12,6 @@ import {BasicEffect} from "../../src/effects/BasicEffect.sol"; /// mon-agnostic stand-in for effects whose OnUpdateMonState hook reaches dealDamage and can therefore /// KO a mon off an inline stamina-regen tick. contract StaminaGainKOEffect is BasicEffect { - function name() external pure override returns (string memory) { - return "Stamina Gain KO"; - } - // Steps: OnUpdateMonState function getStepsBitmap() external pure override returns (uint32) { return 0x100; diff --git a/test/mocks/StatBoostsMove.sol b/test/mocks/StatBoostsMove.sol index abe36392..d3d60843 100644 --- a/test/mocks/StatBoostsMove.sol +++ b/test/mocks/StatBoostsMove.sol @@ -10,10 +10,6 @@ import {IEngine} from "../../src/IEngine.sol"; import {IMoveSet} from "../../src/moves/IMoveSet.sol"; contract StatBoostsMove is IMoveSet { - function name() external pure returns (string memory) { - return ""; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/TempStatBoostEffect.sol b/test/mocks/TempStatBoostEffect.sol index 31fe6968..d84d46a3 100644 --- a/test/mocks/TempStatBoostEffect.sol +++ b/test/mocks/TempStatBoostEffect.sol @@ -9,10 +9,6 @@ import {IEngine} from "../../src/IEngine.sol"; import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract TempStatBoostEffect is BasicEffect { - function name() external pure override returns (string memory) { - return ""; - } - // Steps: OnApply function getStepsBitmap() external pure override returns (uint32) { return 0x01; diff --git a/test/mocks/TestMoveFactory.sol b/test/mocks/TestMoveFactory.sol index 90e4a0bf..8e706fc2 100644 --- a/test/mocks/TestMoveFactory.sol +++ b/test/mocks/TestMoveFactory.sol @@ -20,10 +20,6 @@ contract TestMove is IMoveSet { _damage = damage; } - function name() external pure returns (string memory) { - return "Test Move"; - } - function move( IEngine engine, bytes32, diff --git a/test/mocks/WideDataEffect.sol b/test/mocks/WideDataEffect.sol index a09188e9..58315cb8 100644 --- a/test/mocks/WideDataEffect.sol +++ b/test/mocks/WideDataEffect.sol @@ -8,10 +8,6 @@ import {BasicEffect} from "../../src/effects/BasicEffect.sol"; contract WideDataEffect is BasicEffect { bytes32 public constant INITIAL_DATA = bytes32((uint256(1) << 200) | 123); - function name() external pure override returns (string memory) { - return "Wide Data"; - } - function getStepsBitmap() external pure override returns (uint32) { return uint16(1) | ALWAYS_APPLIES_BIT; } diff --git a/test/mons/EkinekiTest.sol b/test/mons/EkinekiTest.sol index 1684c501..497539fb 100644 --- a/test/mons/EkinekiTest.sol +++ b/test/mons/EkinekiTest.sol @@ -26,6 +26,7 @@ import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; import {BubbleBop} from "../../src/mons/ekineki/BubbleBop.sol"; import {NineNineNine} from "../../src/mons/ekineki/NineNineNine.sol"; import {Overflow} from "../../src/mons/ekineki/Overflow.sol"; +import {NineNineNineLib} from "../../src/mons/ekineki/NineNineNineLib.sol"; import {SaviorComplex} from "../../src/mons/ekineki/SaviorComplex.sol"; import {SneakAttack} from "../../src/mons/ekineki/SneakAttack.sol"; @@ -334,7 +335,7 @@ contract EkinekiTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); // Verify the 999 KV is set for the next turn - uint64 nineKey = uint64(uint256(keccak256(abi.encode(uint256(0), "NINE_NINE_NINE")))); + uint64 nineKey = NineNineNineLib._getKey(0); uint192 storedTurn = engine.getGlobalKV(battleKey, nineKey); uint256 currentTurn = engine.getTurnIdForBattleState(battleKey); assertEq( @@ -627,7 +628,7 @@ contract EkinekiTest is Test, BattleHelper { // Mon 1 has no ability, so no savior complex trigger // But the savior complex on mon 0 should NOT have been consumed (it didn't trigger) // Verify by checking global KV is still 0 - uint64 scKey = uint64(uint256(keccak256(abi.encode(uint256(0), "SAVIOR_COMPLEX")))); + uint64 scKey = uint64(uint256(keccak256(abi.encode(uint256(0), address(saviorComplex))))); uint192 scTriggered = engine.getGlobalKV(battleKey, scKey); assertEq(scTriggered, 0, "Savior Complex should not have been consumed with 0 KOs"); } diff --git a/test/mons/EmbursaTest.sol b/test/mons/EmbursaTest.sol index 9a5e1de6..096a2e6a 100644 --- a/test/mons/EmbursaTest.sol +++ b/test/mons/EmbursaTest.sol @@ -585,8 +585,9 @@ contract EmbursaTest is Test, BattleHelper { engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); - // Bob uses burn attack on Alice (Bob is faster) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, 0); + // Bob (faster) burns Alice. Alice attacks rather than rests so the burn survives the turn — + // a rest here would resolve after the burn and cleanse it on the spot. + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); // Verify Alice is burned and has SpATK boost (EffectInstance[] memory effects,) = engine.getEffects(battleKey, 0, 0); diff --git a/test/mons/GhouliathTest.sol b/test/mons/GhouliathTest.sol index b7d1038c..dd8fc7ef 100644 --- a/test/mons/GhouliathTest.sol +++ b/test/mons/GhouliathTest.sol @@ -384,14 +384,14 @@ contract GhouliathTest is Test, BattleHelper { bool bobHasPanic = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (keccak256(abi.encodePacked(aliceEffects[i].effect.name())) == keccak256(abi.encodePacked("Panic"))) { + if (address(aliceEffects[i].effect) == address(panicStatus)) { aliceHasPanic = true; break; } } for (uint256 i = 0; i < bobEffects.length; i++) { - if (keccak256(abi.encodePacked(bobEffects[i].effect.name())) == keccak256(abi.encodePacked("Panic"))) { + if (address(bobEffects[i].effect) == address(panicStatus)) { bobHasPanic = true; break; } diff --git a/test/mons/GlobTest.sol b/test/mons/GlobTest.sol new file mode 100644 index 00000000..5a7b9bd7 --- /dev/null +++ b/test/mons/GlobTest.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +import "../../src/Constants.sol"; +import "../../src/Enums.sol"; +import "../../src/Structs.sol"; + +import {Engine} from "../../src/Engine.sol"; +import {IEngine} from "../../src/IEngine.sol"; +import {BlessedStatus} from "../../src/effects/status/BlessedStatus.sol"; +import {IMatchmaker} from "../../src/matchmaker/IMatchmaker.sol"; +import {IMoveSet} from "../../src/moves/IMoveSet.sol"; + +import {ChosenOne} from "../../src/mons/glob/ChosenOne.sol"; +import {GetSlippery} from "../../src/mons/glob/GetSlippery.sol"; +import {HolyMolt} from "../../src/mons/glob/HolyMolt.sol"; + +import {defaultBattle, sideWord, targetBits} from "../abstract/SlotWire.sol"; +import {CustomAttack} from "../mocks/CustomAttack.sol"; +import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; +import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; + +contract GlobTest is Test { + address constant ALICE = address(0x1); + address constant BOB = address(0x2); + + uint256 constant A0 = 0; + uint256 constant B0 = 2; + + Engine engine; + TestTeamRegistry registry; + TestTypeCalculator typeCalc; + BlessedStatus blessed; + HolyMolt holyMolt; + GetSlippery getSlippery; + ChosenOne chosenOne; + IMoveSet filler; + bytes32 battleKey; + + function setUp() public { + engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); + registry = new TestTeamRegistry(); + typeCalc = new TestTypeCalculator(); + blessed = new BlessedStatus(); + holyMolt = new HolyMolt(blessed); + getSlippery = new GetSlippery(); + chosenOne = new ChosenOne(blessed); + filler = new CustomAttack( + typeCalc, + CustomAttack.Args({ + TYPE: Type.Fire, BASE_POWER: 10, ACCURACY: 100, STAMINA_COST: 1, PRIORITY: DEFAULT_PRIORITY + }) + ); + + address[] memory toAdd = new address[](1); + toAdd[0] = address(this); + vm.prank(ALICE); + engine.updateMatchmakers(toAdd, new address[](0)); + vm.prank(BOB); + engine.updateMatchmakers(toAdd, new address[](0)); + } + + function _mkMon(uint32 hp, uint32 speed, IMoveSet move0, address ability) internal pure returns (Mon memory mon) { + uint256[] memory moves = new uint256[](2); + moves[0] = uint256(uint160(address(move0))); + moves[1] = 0; + mon = Mon({ + stats: MonStats({ + hp: hp, + stamina: 5, + speed: speed, + attack: 100, + defense: 100, + specialAttack: 100, + specialDefense: 100, + type1: Type.Liquid, + type2: Type.Faith + }), + ability: uint256(uint160(ability)), + moves: moves + }); + } + + function _mkMonWithSecond(uint32 hp, uint32 speed, IMoveSet move0, IMoveSet move1, address ability) + internal + pure + returns (Mon memory mon) + { + mon = _mkMon(hp, speed, move0, ability); + mon.moves[1] = uint256(uint160(address(move1))); + } + + function _start(Mon[] memory aTeam, Mon[] memory bTeam) internal { + registry.setTeam(ALICE, aTeam); + registry.setTeam(BOB, bTeam); + Battle memory battle = defaultBattle(ALICE, BOB, registry, address(this), IMatchmaker(address(this))); + (battleKey,) = engine.computeBattleKey(ALICE, BOB); + engine.startBattleWithMode(battle, BATTLE_MODE_DOUBLES); + vm.warp(vm.getBlockTimestamp() + 1); + } + + /// Glob acts from slot 0; its ally idles in slot 1. + function _side(uint8 m, uint16 e) internal pure returns (uint256) { + return sideWord(m, e, NO_OP_MOVE_INDEX, 0, uint104(1)); + } + + function _turn0Leads() internal { + engine.executeWithSlotMoves( + battleKey, + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, uint104(1)), + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, uint104(1)) + ); + } + + function _hp(uint256 side, uint256 mon) internal view returns (int32) { + return engine.getMonStateForBattle(battleKey, side, mon, MonStateIndexName.Hp); + } + + function _stamina(uint256 side, uint256 mon) internal view returns (int32) { + return engine.getMonStateForBattle(battleKey, side, mon, MonStateIndexName.Stamina); + } + + /// Glob leads slot 0 with `globMove`; Bob is inert. Mon 2 is the bench for switch tests. + function _standardField(IMoveSet globMove) internal { + Mon[] memory aTeam = new Mon[](3); + aTeam[0] = _mkMon(1000, 100, globMove, address(chosenOne)); + aTeam[1] = _mkMon(1000, 50, filler, address(0)); + aTeam[2] = _mkMon(1000, 40, filler, address(0)); + Mon[] memory bTeam = new Mon[](3); + bTeam[0] = _mkMon(1000, 10, filler, address(0)); + bTeam[1] = _mkMon(1000, 8, filler, address(0)); + bTeam[2] = _mkMon(1000, 5, filler, address(0)); + _start(aTeam, bTeam); + _turn0Leads(); + } + + // --------------------------------------------------------------------- + // Chosen One + // --------------------------------------------------------------------- + + /// @dev Blessed on the first entry. The ability is not @inline-ability tagged — it applies a + /// different effect than itself, so the inline path could not express it. + function test_chosenOne_blessesOnFirstSwitchIn() public { + _standardField(filler); + (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(blessed)); + assertTrue(exists, "Glob entered Blessed"); + } + + /// @dev Once per battle: a second entry must not re-bless. + function test_chosenOne_doesNotRearmOnLaterSwitchIn() public { + _standardField(filler); + + // Spend the blessing on an incoming hit, then cycle Glob out and back. + engine.executeWithSlotMoves(battleKey, _side(NO_OP_MOVE_INDEX, 0), _side(0, targetBits(A0))); + (bool spent,,) = engine.getEffectData(battleKey, 0, 0, address(blessed)); + assertFalse(spent, "blessing absorbed the hit and cleared"); + + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 2), _side(NO_OP_MOVE_INDEX, 0)); + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 0), _side(NO_OP_MOVE_INDEX, 0)); + + (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(blessed)); + assertFalse(exists, "Chosen One is once per battle"); + } + + // --------------------------------------------------------------------- + // Holy Molt + // --------------------------------------------------------------------- + + /// @dev Free on the first cast, then +1 stamina per cast, capped. + function test_holyMolt_staminaCostEscalatesToCap() public { + _standardField(IMoveSet(address(holyMolt))); + + assertEq(holyMolt.stamina(IEngine(address(engine)), battleKey, 0, 0), 0, "first cast is free"); + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + assertEq(holyMolt.stamina(IEngine(address(engine)), battleKey, 0, 0), 1, "second cast costs 1"); + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + assertEq(holyMolt.stamina(IEngine(address(engine)), battleKey, 0, 0), 2, "third cast costs 2"); + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + assertEq(holyMolt.stamina(IEngine(address(engine)), battleKey, 0, 0), 3, "fourth cast costs 3"); + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + assertEq( + holyMolt.stamina(IEngine(address(engine)), battleKey, 0, 0), 3, "cost caps at MAX_STAMINA_COST" + ); + } + + /// @dev The counter is battle-scoped, not switch-in-scoped. + function test_holyMolt_costPersistsAcrossSwitchOut() public { + _standardField(IMoveSet(address(holyMolt))); + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + assertEq(holyMolt.stamina(IEngine(address(engine)), battleKey, 0, 0), 1, "one cast spent"); + + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 2), _side(NO_OP_MOVE_INDEX, 0)); + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 0), _side(NO_OP_MOVE_INDEX, 0)); + + assertEq(holyMolt.stamina(IEngine(address(engine)), battleKey, 0, 0), 1, "cost survives a bench cycle"); + } + + /// @dev Halves current HP, so repeated casts can never self-KO. + function test_holyMolt_halvesCurrentHpAndCannotSelfKO() public { + _standardField(IMoveSet(address(holyMolt))); + + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + assertEq(_hp(0, 0), -500, "first molt halves 1000"); + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + assertEq(_hp(0, 0), -750, "second molt halves the remaining 500"); + + // Drive it down until integer division would floor to zero. + for (uint256 i; i < 12; i++) { + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + } + assertEq( + engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut), 0, "Holy Molt cannot self-KO" + ); + assertTrue(engine.getMonCurrentValue(battleKey, 0, 0, MonStateIndexName.Hp) > 0, "always leaves at least 1 HP"); + } + + /// @dev Re-blessing while already Blessed is an engine-level no-op (status lane gate), but the + /// cast still costs its stamina and still sheds HP. + function test_holyMolt_recastWhileBlessedStillCosts() public { + _standardField(IMoveSet(address(holyMolt))); + (bool blessedAtStart,,) = engine.getEffectData(battleKey, 0, 0, address(blessed)); + assertTrue(blessedAtStart, "Chosen One blessing is up"); + + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + (bool stillBlessed,,) = engine.getEffectData(battleKey, 0, 0, address(blessed)); + assertTrue(stillBlessed, "still exactly one blessing"); + assertEq(_hp(0, 0), -500, "HP cost still paid"); + } + + // --------------------------------------------------------------------- + // Get Slippery + // --------------------------------------------------------------------- + + function test_getSlippery_boostsAttackAndSpeed() public { + Mon[] memory aTeam = new Mon[](3); + aTeam[0] = _mkMonWithSecond(1000, 100, IMoveSet(address(getSlippery)), filler, address(chosenOne)); + aTeam[1] = _mkMon(1000, 50, filler, address(0)); + aTeam[2] = _mkMon(1000, 40, filler, address(0)); + Mon[] memory bTeam = new Mon[](3); + bTeam[0] = _mkMon(1000, 10, filler, address(0)); + bTeam[1] = _mkMon(1000, 8, filler, address(0)); + bTeam[2] = _mkMon(1000, 5, filler, address(0)); + _start(aTeam, bTeam); + _turn0Leads(); + + engine.executeWithSlotMoves(battleKey, _side(0, 0), _side(NO_OP_MOVE_INDEX, 0)); + + assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack), 50, "+50% of Atk 100"); + assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Speed), 50, "+50% of Speed 100"); + assertEq(_stamina(0, 0), -2, "costs 2 stamina"); + } +} diff --git a/test/mons/InutiaTest.sol b/test/mons/InutiaTest.sol index ccf81cb7..2a062ba1 100644 --- a/test/mons/InutiaTest.sol +++ b/test/mons/InutiaTest.sol @@ -145,7 +145,7 @@ contract InutiaTest is Test, BattleHelper { (EffectInstance[] memory aliceEffects,) = engine.getEffects(battleKey, 0, 0); bool hasBlessed = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (keccak256(bytes(aliceEffects[i].effect.name())) == keccak256(bytes("Blessed"))) { + if (address(aliceEffects[i].effect) == address(blessedStatus)) { hasBlessed = true; } } @@ -163,7 +163,7 @@ contract InutiaTest is Test, BattleHelper { (EffectInstance[] memory aliceEffectsAfter,) = engine.getEffects(battleKey, 0, 0); bool stillBlessed = false; for (uint256 i = 0; i < aliceEffectsAfter.length; i++) { - if (keccak256(bytes(aliceEffectsAfter[i].effect.name())) == keccak256(bytes("Blessed"))) { + if (address(aliceEffectsAfter[i].effect) == address(blessedStatus)) { stillBlessed = true; } } diff --git a/test/mons/MulongTest.sol b/test/mons/MulongTest.sol new file mode 100644 index 00000000..4a7e4def --- /dev/null +++ b/test/mons/MulongTest.sol @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: AGPL-3.0 + +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +import "../../src/Constants.sol"; +import "../../src/Enums.sol"; +import "../../src/Structs.sol"; + +import {Engine} from "../../src/Engine.sol"; +import {IEngine} from "../../src/IEngine.sol"; +import {Storm} from "../../src/effects/battlefield/Storm.sol"; +import {FrostbiteStatus} from "../../src/effects/status/FrostbiteStatus.sol"; +import {IMatchmaker} from "../../src/matchmaker/IMatchmaker.sol"; +import {IMoveSet} from "../../src/moves/IMoveSet.sol"; + +import {CloudStrike} from "../../src/mons/mulong/CloudStrike.sol"; +import {KingsLeisure} from "../../src/mons/mulong/KingsLeisure.sol"; +import {KingsRespite} from "../../src/mons/mulong/KingsRespite.sol"; +import {SummonStorm} from "../../src/mons/mulong/SummonStorm.sol"; + +import {defaultBattle, sideWord, targetBits} from "../abstract/SlotWire.sol"; +import {CustomAttack} from "../mocks/CustomAttack.sol"; +import {TestTeamRegistry} from "../mocks/TestTeamRegistry.sol"; +import {TestTypeCalculator} from "../mocks/TestTypeCalculator.sol"; + +contract MulongTest is Test { + address constant ALICE = address(0x1); + address constant BOB = address(0x2); + + uint256 constant A0 = 0; + uint256 constant A1 = 1; + uint256 constant B0 = 2; + uint256 constant B1 = 3; + + Engine engine; + TestTeamRegistry registry; + TestTypeCalculator typeCalc; + Storm storm; + FrostbiteStatus frostbite; + SummonStorm summonStorm; + KingsRespite kingsRespite; + CloudStrike cloudStrike; + KingsLeisure kingsLeisure; + IMoveSet filler; // BP 40 Fire physical + bytes32 battleKey; + + function setUp() public { + engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); + registry = new TestTeamRegistry(); + typeCalc = new TestTypeCalculator(); + storm = new Storm(); + frostbite = new FrostbiteStatus(); + summonStorm = new SummonStorm(storm); + kingsRespite = new KingsRespite(typeCalc); + cloudStrike = new CloudStrike(storm, frostbite); + kingsLeisure = new KingsLeisure(); + filler = new CustomAttack( + typeCalc, + CustomAttack.Args({ + TYPE: Type.Fire, BASE_POWER: 40, ACCURACY: 100, STAMINA_COST: 1, PRIORITY: DEFAULT_PRIORITY + }) + ); + + address[] memory toAdd = new address[](1); + toAdd[0] = address(this); + vm.prank(ALICE); + engine.updateMatchmakers(toAdd, new address[](0)); + vm.prank(BOB); + engine.updateMatchmakers(toAdd, new address[](0)); + } + + function _mkMon(uint32 hp, uint32 speed, IMoveSet move0, address ability) internal pure returns (Mon memory mon) { + uint256[] memory moves = new uint256[](2); + moves[0] = uint256(uint160(address(move0))); + mon = Mon({ + stats: MonStats({ + hp: hp, + stamina: 5, + speed: speed, + attack: 100, + defense: 100, + specialAttack: 100, + specialDefense: 100, + type1: Type.Air, + type2: Type.None + }), + ability: uint256(uint160(ability)), + moves: moves + }); + } + + function _withSecond(Mon memory mon, IMoveSet move1) internal pure returns (Mon memory) { + mon.moves[1] = uint256(uint160(address(move1))); + return mon; + } + + function _start(Mon[] memory aTeam, Mon[] memory bTeam) internal { + registry.setTeam(ALICE, aTeam); + registry.setTeam(BOB, bTeam); + Battle memory battle = defaultBattle(ALICE, BOB, registry, address(this), IMatchmaker(address(this))); + (battleKey,) = engine.computeBattleKey(ALICE, BOB); + engine.startBattleWithMode(battle, BATTLE_MODE_DOUBLES); + vm.warp(vm.getBlockTimestamp() + 1); + } + + function _side(uint8 m0, uint16 e0, uint8 m1, uint16 e1) internal pure returns (uint256) { + return sideWord(m0, e0, m1, e1, uint104(1)); + } + + function _rest() internal pure returns (uint256) { + return _side(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0); + } + + function _turn0Leads() internal { + engine.executeWithSlotMoves( + battleKey, + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, uint104(1)), + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, 1, uint104(1)) + ); + } + + function _hp(uint256 side, uint256 mon) internal view returns (int32) { + return engine.getMonStateForBattle(battleKey, side, mon, MonStateIndexName.Hp); + } + + function _stamina(uint256 side, uint256 mon) internal view returns (int32) { + return engine.getMonStateForBattle(battleKey, side, mon, MonStateIndexName.Stamina); + } + + function _stormIsUp() internal view returns (bool up) { + (up,,) = engine.getEffectData(battleKey, 2, 2, address(storm)); + } + + /// Mulong leads side 0 slot 0 with `mulongMove` and the given ability; everyone else is inert. + function _field(IMoveSet mulongMove, address ability) internal { + Mon[] memory aTeam = new Mon[](3); + aTeam[0] = _mkMon(2000, 100, mulongMove, ability); + aTeam[1] = _mkMon(2000, 50, filler, address(0)); + aTeam[2] = _mkMon(2000, 40, filler, address(0)); + Mon[] memory bTeam = new Mon[](3); + bTeam[0] = _mkMon(2000, 10, filler, address(0)); + bTeam[1] = _mkMon(2000, 8, filler, address(0)); + bTeam[2] = _mkMon(2000, 5, filler, address(0)); + _start(aTeam, bTeam); + _turn0Leads(); + } + + // --------------------------------------------------------------------- + // King's Leisure + // --------------------------------------------------------------------- + + /// @dev The outgoing half is a flat ATK/SpATK debuff, applied once on the first entry. + function test_kingsLeisure_appliesAttackDebuffOnFirstEntry() public { + _field(filler, address(kingsLeisure)); + assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack), -50, "ATK halved"); + assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.SpecialAttack), -50, "SpATK halved"); + } + + /// @dev The incoming half is a PreDamage hook. B0 hits Mulong for BP 40 into Def 100 = 40 raw, + /// halved to 20 while the window is up. + function test_kingsLeisure_halvesIncomingDamage() public { + _field(filler, address(kingsLeisure)); + engine.executeWithSlotMoves(battleKey, _rest(), _side(0, targetBits(A0), NO_OP_MOVE_INDEX, 0)); + assertEq(_hp(0, 0), -20, "incoming damage halved"); + } + + /// @dev Three active rounds, then both halves lapse together. + function test_kingsLeisure_expiresAfterThreeActiveRounds() public { + _field(filler, address(kingsLeisure)); + + for (uint256 i; i < 3; i++) { + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + } + assertEq( + engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack), 0, "ATK debuff cleared at expiry" + ); + (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(kingsLeisure)); + assertFalse(exists, "window closed"); + + engine.executeWithSlotMoves(battleKey, _rest(), _side(0, targetBits(A0), NO_OP_MOVE_INDEX, 0)); + assertEq(_hp(0, 0), -40, "full damage once the window lapses"); + } + + /// @dev The countdown only advances on rounds Mulong is active, and the Perm debuff rides + /// through the bench rather than being dropped and re-applied. Turn 0 consumes the first + /// of the three, so leaving immediately after it banks the other two. + function test_kingsLeisure_pausesWhileBenched() public { + _field(filler, address(kingsLeisure)); + + // Mulong is benched at every RoundEnd below, so none of these rounds tick the clock. + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 2, NO_OP_MOVE_INDEX, 0), _rest()); + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + assertEq( + engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack), -50, "Perm debuff survives benching" + ); + + // Back in with 2 rounds still banked: one more active round must not close the window. + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0), _rest()); + (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(kingsLeisure)); + assertTrue(exists, "clock paused on the bench, not burned"); + assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack), -50, "debuff still in force"); + } + + /// @dev Once spent, a later entry must not re-arm the window. + function test_kingsLeisure_doesNotRearmAfterExpiry() public { + _field(filler, address(kingsLeisure)); + for (uint256 i; i < 3; i++) { + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + } + + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 2, NO_OP_MOVE_INDEX, 0), _rest()); + engine.executeWithSlotMoves(battleKey, _side(SWITCH_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0), _rest()); + + (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(kingsLeisure)); + assertFalse(exists, "once per battle"); + assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Attack), 0, "no second debuff"); + } + + // --------------------------------------------------------------------- + // Summon Storm + // --------------------------------------------------------------------- + + /// @dev Ticks every active at end of round, the caster's own side included. + function test_storm_damagesEveryActiveAtRoundEnd() public { + _field(IMoveSet(address(summonStorm)), address(0)); + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + + assertTrue(_stormIsUp(), "storm summoned"); + assertTrue(_hp(0, 0) < 0, "caster takes its own storm"); + assertTrue(_hp(0, 1) < 0, "ally hit"); + assertTrue(_hp(1, 0) < 0, "opposing slot 0 hit"); + assertTrue(_hp(1, 1) < 0, "opposing slot 1 hit"); + } + + /// @dev Runs its full duration and then lapses. + function test_storm_expiresAfterDuration() public { + _field(IMoveSet(address(summonStorm)), address(0)); + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + + for (uint256 i; i < storm.DEFAULT_DURATION() - 1; i++) { + assertTrue(_stormIsUp(), "storm still running"); + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + } + assertFalse(_stormIsUp(), "storm expired"); + + int32 settled = _hp(1, 0); + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + assertEq(_hp(1, 0), settled, "no ticks after expiry"); + } + + /// @dev A recast refreshes the clock in place rather than stacking a second storm. + function test_storm_recastRefreshesRatherThanStacking() public { + _field(IMoveSet(address(summonStorm)), address(0)); + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + int32 afterOne = _hp(1, 0); + + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + int32 afterTwo = _hp(1, 0); + assertEq(afterTwo - afterOne, afterOne, "one tick per round, not two"); + + // Refreshed to full duration on the recast: still up a full duration later. + for (uint256 i; i < storm.DEFAULT_DURATION() - 1; i++) { + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + } + assertFalse(_stormIsUp(), "expires a full duration after the recast"); + } + + /// @dev Damage scales off the latched caster's stats, so King's Leisure's debuff drags it down. + function test_storm_scalesWithCastersDebuff() public { + _field(IMoveSet(address(summonStorm)), address(0)); + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + int32 undebuffedTick = _hp(1, 0); + + _field(IMoveSet(address(summonStorm)), address(kingsLeisure)); + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + int32 debuffedTick = _hp(1, 0); + + assertTrue(debuffedTick > undebuffedTick, "halved SpATK weakens the storm"); + } + + // --------------------------------------------------------------------- + // King's Respite + // --------------------------------------------------------------------- + + /// @dev Arms silently, then detonates on the next rest — and the rest still regenerates stamina. + function test_kingsRespite_firesOnNextRest() public { + _field(IMoveSet(address(kingsRespite)), address(0)); + + engine.executeWithSlotMoves(battleKey, _side(0, targetBits(B0), NO_OP_MOVE_INDEX, 0), _rest()); + assertEq(_hp(1, 0), 0, "arming does no damage"); + assertEq(_stamina(0, 0), -2, "arming costs 2 stamina"); + + engine.executeWithSlotMoves(battleKey, _rest(), _rest()); + assertTrue(_hp(1, 0) < 0, "the rest detonated the strike"); + assertEq(_stamina(0, 0), -2, "the strike rides the rest for free"); + + (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(kingsRespite)); + assertFalse(exists, "one-shot"); + } + + /// @dev Slot-bound: a replacement occupying the marked slot eats the strike. B0 outspeeds Mulong + /// so its switch resolves first — rest and switch share SWITCH_PRIORITY, so speed decides. + function test_kingsRespite_followsTheLatchedSlot() public { + Mon[] memory aTeam = new Mon[](3); + aTeam[0] = _mkMon(2000, 100, IMoveSet(address(kingsRespite)), address(0)); + aTeam[1] = _mkMon(2000, 50, filler, address(0)); + aTeam[2] = _mkMon(2000, 40, filler, address(0)); + Mon[] memory bTeam = new Mon[](3); + bTeam[0] = _mkMon(2000, 200, filler, address(0)); + bTeam[1] = _mkMon(2000, 8, filler, address(0)); + bTeam[2] = _mkMon(2000, 5, filler, address(0)); + _start(aTeam, bTeam); + _turn0Leads(); + + engine.executeWithSlotMoves(battleKey, _side(0, targetBits(B0), NO_OP_MOVE_INDEX, 0), _rest()); + engine.executeWithSlotMoves(battleKey, _rest(), _side(SWITCH_MOVE_INDEX, 2, NO_OP_MOVE_INDEX, 0)); + + assertEq(_hp(1, 0), 0, "the mon that left is untouched"); + assertTrue(_hp(1, 2) < 0, "the replacement in the marked slot takes it"); + } + + // --------------------------------------------------------------------- + // Cloud Strike + // --------------------------------------------------------------------- + + /// @dev Storm raises Cloud Strike's power. + function test_cloudStrike_strongerUnderStorm() public { + _field(IMoveSet(address(cloudStrike)), address(0)); + engine.executeWithSlotMoves(battleKey, _side(0, targetBits(B0), NO_OP_MOVE_INDEX, 0), _rest()); + int32 plain = _hp(1, 0); + assertTrue(plain < 0, "hit landed"); + + // Same field, but with a storm up before the strike. + Mon[] memory aTeam = new Mon[](3); + aTeam[0] = _withSecond(_mkMon(2000, 100, IMoveSet(address(summonStorm)), address(0)), cloudStrike); + aTeam[1] = _mkMon(2000, 50, filler, address(0)); + aTeam[2] = _mkMon(2000, 40, filler, address(0)); + Mon[] memory bTeam = new Mon[](3); + bTeam[0] = _mkMon(2000, 10, filler, address(0)); + bTeam[1] = _mkMon(2000, 8, filler, address(0)); + bTeam[2] = _mkMon(2000, 5, filler, address(0)); + _start(aTeam, bTeam); + _turn0Leads(); + + engine.executeWithSlotMoves(battleKey, _side(0, 0, NO_OP_MOVE_INDEX, 0), _rest()); + int32 beforeStrike = _hp(1, 0); + engine.executeWithSlotMoves(battleKey, _side(1, targetBits(B0), NO_OP_MOVE_INDEX, 0), _rest()); + int32 stormedStrikePlusTick = beforeStrike - _hp(1, 0); + + assertTrue(_stormIsUp(), "storm active for the strike"); + assertTrue(stormedStrikePlusTick > -plain, "storm-boosted Cloud Strike hits harder"); + } + + function test_cloudStrike_basePowerReflectsStorm() public { + _field(IMoveSet(address(cloudStrike)), address(0)); + assertEq(cloudStrike.basePower(IEngine(address(engine)), battleKey), cloudStrike.BASE_POWER(), "no storm"); + } +} diff --git a/test/mons/VolthareTest.sol b/test/mons/VolthareTest.sol index f4926b69..02c47bdd 100644 --- a/test/mons/VolthareTest.sol +++ b/test/mons/VolthareTest.sol @@ -473,13 +473,20 @@ contract VolthareTest is Test, BattleHelper { return battleKey; } - // Quickstorm lands on Volthare's first acting turn: damages and Zaps the opponent. + // Quickstorm lands on Volthare's first acting turn: damages and Zaps the opponent. Volthare + // (speed 100) outspeeds Bob (50), so the Zap arrives before Bob acts and eats this turn's + // action — his committed attack never lands. function test_quickstormLandsOnFirstTurn() public { bytes32 battleKey = _quickstormBattle(); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, 0, 0); assertLt(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp), 0, "Quickstorm should damage Bob"); + assertEq( + engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp), + 0, + "Zap consumed Bob's action on the turn it landed" + ); (bool bobZapped,,) = engine.getEffectData(battleKey, 1, 0, address(qsZap)); - assertTrue(bobZapped, "Quickstorm should Zap Bob on the first turn"); + assertFalse(bobZapped, "a same-turn Zap is spent immediately, not carried to the next turn"); } // Once Volthare acts with anything else, its first-turn window closes and Quickstorm fizzles. diff --git a/transpiler/codegen/abi.py b/transpiler/codegen/abi.py index c3c14502..eceeed72 100644 --- a/transpiler/codegen/abi.py +++ b/transpiler/codegen/abi.py @@ -280,8 +280,6 @@ def _infer_single_packed_type(self, arg: Expression) -> str: return 'bytes32' if func_name == 'keccak256': return 'bytes32' - if func_name == 'name': - return 'string' # Type-cast calls (e.g. uint8(x), uint104(x), bytes32(x)) carry their # target type — mirror the non-packed inference (_infer_function_call_type) # so encodePacked emits the real width. Defaulting to uint256 here both @@ -291,9 +289,6 @@ def _infer_single_packed_type(self, arg: Expression) -> str: return 'address' if func_name.startswith(('uint', 'int')) or func_name.startswith('bytes'): return func_name - elif isinstance(arg.function, MemberAccess): - if arg.function.member == 'name': - return 'string' if isinstance(arg, TypeCast): if arg.type_name and arg.type_name.name: return self._get_packed_type(arg.type_name.name) From 10a0bfdc588297761ee55a96374aab3834744966 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:25:18 -0700 Subject: [PATCH 35/41] add mulong / glob --- CLAUDE.md | 1 + drool/imgs/.DS_Store | Bin 10244 -> 10244 bytes drool/imgs/deprecated/Llama Back.gif | Bin 2472 -> 0 bytes drool/imgs/deprecated/Llama Front.gif | Bin 5225 -> 0 bytes drool/imgs/deprecated/Llama Mini.gif | Bin 642 -> 0 bytes drool/imgs/glob_back.gif | Bin 0 -> 8506 bytes drool/imgs/glob_front.gif | Bin 0 -> 7114 bytes drool/imgs/glob_front_damage.gif | Bin 0 -> 7116 bytes drool/imgs/glob_mini.gif | Bin 0 -> 2263 bytes drool/imgs/icons/_src_aurox_mini.png | Bin 1120 -> 0 bytes drool/imgs/icons/_src_sofabbi_mini.png | Bin 1287 -> 0 bytes drool/imgs/icons/aurox_icon.preview.png | Bin 802 -> 0 bytes drool/imgs/icons/ekineki_icon.preview.png | Bin 706 -> 0 bytes drool/imgs/icons/embursa_icon.preview.png | Bin 894 -> 0 bytes drool/imgs/icons/ghouliath_icon.preview.png | Bin 844 -> 0 bytes drool/imgs/icons/gorillax_icon.preview.png | Bin 837 -> 0 bytes drool/imgs/icons/iblivion_icon.preview.png | Bin 872 -> 0 bytes drool/imgs/icons/inutia_icon.preview.png | Bin 915 -> 0 bytes drool/imgs/icons/malalien_icon.preview.png | Bin 740 -> 0 bytes drool/imgs/icons/nirvamma_icon.preview.png | Bin 805 -> 0 bytes drool/imgs/icons/pengym_icon.preview.png | Bin 866 -> 0 bytes drool/imgs/icons/sofabbi_icon.preview.png | Bin 951 -> 0 bytes drool/imgs/icons/volthare_icon.preview.png | Bin 834 -> 0 bytes drool/imgs/icons/xmon_icon.preview.png | Bin 936 -> 0 bytes drool/imgs/mulong_back.gif | Bin 0 -> 8966 bytes drool/imgs/mulong_front.gif | Bin 0 -> 18384 bytes drool/imgs/mulong_front_damage.gif | Bin 0 -> 9177 bytes drool/imgs/mulong_mini.gif | Bin 0 -> 2271 bytes processing/buildMonGifs.py | 196 ++++++++++++++++++++ 29 files changed, 197 insertions(+) delete mode 100644 drool/imgs/deprecated/Llama Back.gif delete mode 100644 drool/imgs/deprecated/Llama Front.gif delete mode 100644 drool/imgs/deprecated/Llama Mini.gif create mode 100644 drool/imgs/glob_back.gif create mode 100644 drool/imgs/glob_front.gif create mode 100644 drool/imgs/glob_front_damage.gif create mode 100644 drool/imgs/glob_mini.gif delete mode 100644 drool/imgs/icons/_src_aurox_mini.png delete mode 100644 drool/imgs/icons/_src_sofabbi_mini.png delete mode 100644 drool/imgs/icons/aurox_icon.preview.png delete mode 100644 drool/imgs/icons/ekineki_icon.preview.png delete mode 100644 drool/imgs/icons/embursa_icon.preview.png delete mode 100644 drool/imgs/icons/ghouliath_icon.preview.png delete mode 100644 drool/imgs/icons/gorillax_icon.preview.png delete mode 100644 drool/imgs/icons/iblivion_icon.preview.png delete mode 100644 drool/imgs/icons/inutia_icon.preview.png delete mode 100644 drool/imgs/icons/malalien_icon.preview.png delete mode 100644 drool/imgs/icons/nirvamma_icon.preview.png delete mode 100644 drool/imgs/icons/pengym_icon.preview.png delete mode 100644 drool/imgs/icons/sofabbi_icon.preview.png delete mode 100644 drool/imgs/icons/volthare_icon.preview.png delete mode 100644 drool/imgs/icons/xmon_icon.preview.png create mode 100644 drool/imgs/mulong_back.gif create mode 100644 drool/imgs/mulong_front.gif create mode 100644 drool/imgs/mulong_front_damage.gif create mode 100644 drool/imgs/mulong_mini.gif create mode 100644 processing/buildMonGifs.py diff --git a/CLAUDE.md b/CLAUDE.md index fdaaf82d..1d4c9465 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,6 +127,7 @@ chomp/ │ ├── deploy_addresses.py # Inline ability/move address packing for the deploy pipeline │ ├── buildTypeChart.py # Build type effectiveness chart │ ├── buildDamageGifs.py # Render damage-preview GIFs +│ ├── buildMonGifs.py # Slice horizontal sprite sheets back into GIFs │ ├── createAddressAndABIs.py # Extract deployed addresses + ABIs │ ├── generateMonsTypeScript.py # Generate TypeScript mon data │ ├── generateEventLayouts.py # Generate generated/eventLayouts.ts (GachaEvent bit layout) diff --git a/drool/imgs/.DS_Store b/drool/imgs/.DS_Store index d31df2f19cea1b33c2b36aaafc018d8c32505b6b..08240208c1e0f34c5d424436cb07052a61aff4c8 100644 GIT binary patch delta 72 zcmZn(XbG6$&&a(oU^hP__hue}dF+#yOWaaUDNfEw%FoYX00LHqOon8Je1<%R;l&Fl(`K^md}Ex8r6 delta 222 zcmZn(XbG6$&nU1lU^hRbz-AtSdF-NG3@Hq$3*%*q!>T|&AKMDxPqKM??ySOCfvOFmMF)?th$oG3jix4rpqhdV!{Frn S+=7jTzgRc3D=fzCU{?T2h&ucL diff --git a/drool/imgs/deprecated/Llama Back.gif b/drool/imgs/deprecated/Llama Back.gif deleted file mode 100644 index cf3a8a54a2effe594e459f3a163fc3d63821f2e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2472 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 OQma=9iclcn_J08xmc4!e diff --git a/drool/imgs/deprecated/Llama Mini.gif b/drool/imgs/deprecated/Llama Mini.gif deleted file mode 100644 index d90ff9917112153818f4cb12b0ece3b649980e78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 642 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 ZTvim-Agy5#@> literal 0 HcmV?d00001 diff --git a/drool/imgs/glob_front_damage.gif b/drool/imgs/glob_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..66801be934d1a9709b95dbaf4b2d2b413443f065 GIT binary patch literal 7116 zcmeI$S5(v4w+HZFXrTxMlqMae7qQU9LJyrt38DAUaS#X{LJds>A)$p3#85@4f+$Qt zsmcgaB=k-|no1E+xifd&|GG2J_wA1RWuJ%hvcGGc{aNck4fIsiT$}-C00aC_DX?+3O5p!t0-~oZrn;6E+L|!kD+-FVpg%_x zJ)&b`{VfE-RZnneWv^B%X%M( zXr~DnG(7Ilk+P_^?{6Tz%~Nz6z#BA{4;HBgZA|w!R=h9KNMPQVCQ^qoXdqHtB16*M7-`vs1a0=#^J&?h+#*LRZB8^BVQT%+yT z7ahOe>=dK0f+NQQ+s_4pm=dj>ESG1XP7xEu%fv{|dlmLcbKN00KSm#}k_ed{UOpnAqQp;R^+kv0@%8gB8{oq0Ib(2T$aAF2=`ms1;>Hm8oS9xV6;8 zv0po}!U?j`q2f`dKGJ-74gr;}`Ot>(8Pr@sxI}~=0V{l8KP{I(gb|@%hPNw+lEP@| z3)Z0X*_D>55;QXBZRh$!1xso8(#*4v~ zq@wDy)f(Tpq56SPSngGvL?7QQQi9$0pxTopj+ROi*xJK z!f7)reXIH1q~|Bn4FI1z@PzXAvcz{GaI4S{w=u$qWWk z;(m%ox#s=BI~e>-?@It(*-y&_aQ~}s0VjpB=O_&8Pu=g$59`7n1q@6qt!m#fu0xB+ z^hUwn-xgllRN!~2yf?lR^Lg*q5oKnZ;C*`eF71_diP;`?GsP5*1hXIL9q9XxqfJm| z(|1k(p60IaCxY`sp|`(^>xIx?Bhv8nkr}S<#z8WObdvg=%z76^{`X}rs4!6NyFHyI7Z zDoUdKyosWH`Ko+vIvQ7jcOZ=*3BT4xakG*OaS{mTrm7K;VybNu;@r9($%y@$Lp9Nt zh8_krEH*sw>H~q<>QTecO_^qX?(&7`qZ6Beq;OJz+9tlPS&xbwWioAQ9EHWLWB z&bUZtjag4)qP`2{^iqcUvbEmx?ZuUcwbBs{aRZ#g$b&X`AV5>+qTdqmED_uFyY0 z<6hlTJ2_a+JJPx&5-Wc+o;ec)b}o*#*{<)*gV?60hC0Fb7D9)7r}x+9yv_`u5Yc|a zdE=g-*5^b℘o938V6F?0h2yA>=ULLMY5FYhhK?EQu)vg2ZFj|h~Qz0bfkz_rAIFodNt1FMwy*D-B2ZhM2P%jp$9sA+tXk{)ky=ay#{ zfsX4XpW8@B^=61X$51T*;@yx<;3cdPJgCf;PvDZ()Pm}3Oxyu^!#&)kHL~!?fRv3G z@On)@%KL6-qR)A|R?uYkQr-9M!%K0Y(I|Nv$_YhR{X$lZD5W!A7x zX!E{iez8OIN5y@{CuTP6mCs=uB&jVB;H7&w%lGpndB;2woQW$Sv~#J>B!rGnPGclJ zoRNNfv#O@4&){N9IXefVOV?*H;j8!Duw(EV>G|wr5SEbJ|zO5KcS2(s# zQmN+Is2V}2`&xE1pSR|2aU%ZnJb0y?TktX| z@r$GPY>m!hztMiA)N>i zrwgJHG@b6d?D9H|@x0@veuH~YI>VWaYyDRq_2n|?xXP&!kw!^0mSSi=^rCU5(pkn@ zgC;7F!x~aPP04YGm4Z{4tu&P4;>!ecY?~@aHAe2!vI#&6H8v3qx{a&T1GpM%q7q9V zmyt4P_`Yz?hTlsp*)CP*ec{a_5wdlP@L`Yr+mDatUbBD^q-vsq%jus$o1Knq?ihqj3yR@(lVaa!=+RRE!2+pCa zvo|%FF(!DFe^THY+qeVyo-a^k!EnlWt6koAspnMU%*$86Oxui^wy6m^>S$u`zP557 zVHflQ_d?Q1bB_`#Y`x}yfkc7|u{J;@hdaWtM34#(JK+kR@}8t8+9APF9uwyF?MP9x zjatl9>~@kA9F%;goRU)4j}kNE^O52WWxFnQ;qFQj+{Lq(tEs)u1z!QMH%e>u^ZR<)UQJ2!WebZu*ud2o0V^j zJ@iYv6U2h+!f5u7FZW#Jk?>BR& z(FVE)7P)HuQ7W_9W0NYD+9p<5Gz)DEBr$&^Gl1F<#btYbo7QvzMqMo&GEipApSdZX9tBJPCqA{#;I z5s>%;TfCu?_{e>;QkuSMVhub+_%<`i5Jxu8G(B5+b9Ya}`neNVTCEIr)3VW8lnqt{ zY|plscG5JfE3G;=FzlKN{D$6%>y4sK&^2t`U3g4)4(k5BRU@t^>gzzQ@pbfvcaP2@ zS$FvwCV@J)UgfZYCWn#Kt0iM^^Pep>gRT@M82|_5$>(nwcse2%=AJS~-M$4AQU01+ zdDYz1F(mu|(_RWa4c$BnqM_8?0W-cVR{vwM&4Ooht0>BMV~wRtD82D%B;Lro9O6c= z4|NDHDODMY!FG>2Ccu?coWn=rR(bt~`VX9~kb&S4^Dl##xo{AobsPkeLW?GxhA=K* zMALZBr#$N~7fU|{wa#FKxhwK~qYIKX{8mVeMGh|6JxdR@Y&`Hm54yQM0PWI`1Yj}F zod;zsv*M7^URuc+k@OkLjpOyP;X=Lcv_u5f+mI$x<+fWyt-Q)Bk>+a6l%*V+0`5ge zrZDA|V!PEU0H)6YcKyw7pnNV1%yE4&x0RACh~wiVOBR8;QfEWs%zYqn^bFz4g#iK5 zMJ0ZZP9+2^0+l7HRYWd?C%z;m1(ElTw02Pw>KG!cxz{XFrplxJr)yL!3TDxIg3ME@4qG!Yuie(Y2H@M8j`}~dL zzJ1ondg4~_nCQ9jF4j~%65dW4z&^*K_HE;JIvH$pl@?I&G+TObdG|vFUbI^v*4@*U z*HD1kH_S@5b(DODy#l4mSJb~P0`Fd?F@{sD$#3I{n^Ay<2+v?L1*D#sB)G}<-g zfSoZ*u9XR$t*Vk;x`A9!hPVG6X`YUUa~<^ulb>j16dK`;BVhaSR%^L~sQu`lkwBqJcVkR9zpCACwPpt~ zxQhFX)E-ASN-UmB;=zyRQEbX@(WBw)?cWx8OgfI%Ft>|sdtUF~)KM#QY~TBOAvY#O zf{+sX^O$9hx}mLfQ2gvVEgc?I@7XeRe=Qc!(YF)eeMWy}C@xyMgr09goe`%X0;vBN Dcw)6T literal 0 HcmV?d00001 diff --git a/drool/imgs/glob_mini.gif b/drool/imgs/glob_mini.gif new file mode 100644 index 0000000000000000000000000000000000000000..4ac2c424666c1b0d11a41b61d2152212b0689079 GIT binary patch literal 2263 zcmZ?wbhEHbRA5kGXlH-|JzgO@k;oclgXv3tZZJ2#v-AJ!@Ssmm{sRF_$tXP<0>dx_ z6#sMkxrPKgI|jHK=@~FH0==mClZ8_RNb7*S3(6)89RC>@Ib=LGEI8QAA*>a1B4A-d zJFhaANCU&c!wf>~JOv#G99(<2O|@=hd{A^{W@A!d$P8F~dLk>c&6fzl)P~vY>?$4$ zgr7OgG~wm=kkPuwPOq>+F>$LUtSzwv& z%ypWpwN35Zv{>dhJPRHyyRpJFc$)8mpxoQDf^wroSA5n!wIPzZ+I8k6or6m_cWv`s z*|6f_dh@Swy?3q`#T|3&y(hAE=7$r^WyZ#Ab8oIVc1&mECzsmI2NL=lrlV9 zt^8O`^=47Y`|bViNk?7@X9nGl>{R~xa??_`bw2za?x&0w9$Ra*Ip3|`My{gl?ZonF z=_g`#pZ;=7Rqgr;>xN5AAD_mnN+vik{`vXR{etfV&8_oX7;hXCiDGxVGT)EK>_$LP z`bG5xrB`EDEOcUIL=IXe200*vmO!KB4=AmTf{_;j@S?@Bkx57{=0w2)1{Mw>whWJn z51g3=)Wc*94kR4v29_unPE1(b)W^)Mas*h|oM2GlESghkw3J~QTi&)FO=HIlcA;3V=C!9+ z8RM9L@GM}+xxFzZ)k>5zbV)=9OU$kSus3r5js;FLX;flZ3L5&Y4^sV;$ z(-mqI-_sEtx9yfhTH@IVf4K(Bptq@8!z{B}W`2F1HG?(a+g#rUhii<6^~YnoE-8Gt zx4(D(oEf1RH<#|*DkeWeM%(?d%x)`NgSo%XPja}(zu9ba^x2*C zP0!;tzB$=vFjH(neEhP!9lt+bk9XX%@_Af6g3 z^n+5x9vhDN_HymdwYHV}pI&9@c3Xn+h#<%dF=fRJ(Vuhg*RR=|wE0ubzeleeQ~MXL z{V10&uWSx?T(@b)Q>)wW)<&0RZ@QcQVzy=Q4N2eUC;cs%==n_RgR0Y}`Mu z)vrIeUXFhd```Y(>HQ=Y0e1$FbvNg7&xl$sZteM-bIrn=?H&c^=da1$ca#It-tO9lU3*?L?3q>n=nMCa;ng4p5pdurla%zufHvvF}L`xgW%(n&DB%S@2~lO@n1(a^F>*3Xl5_|$6~tw z&)=PwrQW}^273ST^t>Z;SO56>*kmpx@;tX{b>DZB2? zNA~T%O0&z>J~n1|SPhBwuXPN&^%E+8v)tQtZ{6Ws-tSp|4piQ|ma*Rc0n_vUy1y7N z$U)q^NB+RI6F2Ot`?r73%dUF!S#G`hhS&4#`C&;U`|m%7ZE4k}MY$Vf&S=Wz-CY}# zUj6!4#Ow0BcKZ+i`5wk3!kpdvo%zfM$)@Y})muV;Cgt-b9G%A@BRF{@aOl}&ni|vS26amLIlUn``Q&*bLD($TVZ>&M``*YzDxBB0f$aA_XH?sY!)+TdT|9!)(0Ev&&g?s^(v5o4E@^z8V ze{kCMU-x&kXSk#Y${9P}JZCWW*!6DzoBNhfuR`)}#}9VHX{mkltd2>81Uy~+T-G@y GGywoGX9o}f diff --git a/drool/imgs/icons/_src_sofabbi_mini.png b/drool/imgs/icons/_src_sofabbi_mini.png deleted file mode 100644 index eaa6873b9f9fbdfdb872934503916882ef635d64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1287 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU^(aM;uumf=k46R*`k30N6Y7L zTNDwwBG6G);ZsxF4UHRKi+X$O8$ypLC5Q_riRrL#8`d}X_Wm$fx*$~Gq?S{_MS-ni zsu_tBx9*#q+Mf7s-|mx^``>-KSGPeX?cCp4v%^ED3dU5ejBi-d%EZvl#E`!JJKNE{ zs?$4X%$f-bH|ELq4?UHJ6m4<`t-Sffo;X7obbxFLo+U`=f`m0 z`a8LO?fK$bc{hdv4KNDfe{l9^+?sg{bNd4{Ebq;}yf?y@QJKL)1dQJ8Y}hu@a*MuA z#}|&udF+!%dIL?^j=lPa%Pc~2Z-oG_Z|3{shzGd!bRb&Na zw=5dV-z_TXdmH`y`&JqKkL$`4U>2(HV!vQ>@X?{iI#2DIZ%lFPFZlB%6GdFs2H(JOkN9{FS?b3ORP#EqpsiAMSqluM9@d`}OAV)y2>L zFR^9%@qD+l4>unF6udrCr}Vrtze5{4D+3=ZL*HJz1Km>>SXv4G+q38C>*?NmJMyD4svQ=?GFO>T{QQ@j z{b$=dlXL5{Pnb5Detz8h`{UZuFKfI zc75;XpSBMJRqp-$8}+wk8$8I4+lnj9d}(`Pmd(M#2bapmDz6ABs`KStvgGwYQwhcc z3mHJD@H}hA>sRS&@uv6utD3fDe~T#Ivt-%)`I;A)C%l7N`;B4KeeLU8{zm4+8E@ZK zq>wRRe$VNodFBhAerYT^HeuWJfW>L+6PMc{((=Xg+&99)3m#3^e4hPWXxrZM^>3pN z#Lm`+rw(8YcqkNCnU#iaIm-I!e^cW6iJO&<3;sIwi*4t>#;=SXL69u7sG8yA%+2x5 z-`c;c8=aSQUitg$Qlsq($N%j%%Tt8=RQ*>>!`3>7>*s{@rp&Uv^*igikXKtQzr!I8 zP?7*5zY_ig)zba@bAzrkUV%G8eJ6XzTh65Gf?Fcc5;;iW4t6wqAKaQP_VR+~qX#h@O}8A4I$1my zN-)oic~t+Qwsre+>0dVUzrKC?>+!<}wZHGhhI&p?fuSXv(^f{+e9kCHFSfO*`2N7) zb^4u~G5qKM|GWJC!;gnADi+osKfQE6%t*xQTG@ovS9zZQJU{>Ff4%(e%OC!@STSqs ze1)g6|BEgDeSJK)pr%adz50o}>nEJnW|+fxKoW_FdHb4SW#zB#@Q)^YukW5%`tRg} z@M|AeukF4X`An4I9K!=1Bx2pS(uOO4KW?6!`RM;w@lEIU|4uZ0{Uh>yxrN;?$@|kc z*h~DXVbIk>cAYW9jz{O&J#5q08}FEAdfxwTxq;^!agVp`8QWji+Bx3c!-V1-pzCy} z*6}?!y?y;=yZ`53WUl?rdZ+%AvP}GLA$xa&rC;h8*TX{#7H-Q8*?FJ;e0*>3swH{( zmf7+3>yBNm*zff6J6{z>Xhmz<^84?duK)D*^a{Dy@5OxAg?=->ic3H76svEa6o294 zJNz;2agoKp5AVLNdSZ3pZ1l&Uz^J&QxkumOsRU9`0^R<%-!kJ-EyKoh-`p<#W#~P! z@81i@1n)lhjw9nw_>gTe~DWM4f>X~j_ diff --git a/drool/imgs/icons/ekineki_icon.preview.png b/drool/imgs/icons/ekineki_icon.preview.png deleted file mode 100644 index c08b2a731f643fdc33fd5bc4221ccfba9158ad58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 706 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVCwXAaSW-L^Y*S`mPnvX!^fEd z+}#bX+XO;5x&^j*WS!rzC@#T2A+@Og?Ii9OtY$AHowN$MV+90LPU~J`;corNa&G^R z&lQt@?u)lRleg*f#|L|Bjc=PwQt_Om(zz&?ZQbi#>)!u(`YZqaPy1{4gWucRR?f5f z|Mq3GWBsw}t^XOWrZN<;F{HzZ@Y=qrk5B!>i{$J}P9I(K%Cvx`!I;5<8Hvb#|J%m? z*S>vUtPYv{{CDx^((l3+Oa~+x<}e}=Ta)KMPyhbaJ!`#j%5_c|#s@qM=NOQPZ=1hA zpI`su8qdw;{nuUN|Npp|c(JtSMMX*Nt!-~()iG`5zRtL9b?im^1z*1ZogKgXicOvH zocQQ(zc8(Ctv>bg{730sZ)%zQ;QoU7@L^rU#`B-e4}SmqdAh#a-=CP;wF@V{dnbNkov_RK-{`Q7IwAl7 diff --git a/drool/imgs/icons/embursa_icon.preview.png b/drool/imgs/icons/embursa_icon.preview.png deleted file mode 100644 index 1e11f59e5870a4614527282008062b43d699f905..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 894 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV0QI%aSW-L^Y-q3@326bhL6b} z!2*dh`F6G?EN~Ljk93;M`JGp8akuw(r~l6984B1K(vgXWr47gX45Ja@kN z%a$*9I-c*lZPVk~Z_CR5oH=vzi~V}x3bqDg1`B2+;`Lfv{=0j#)5~f~YLra;kf8{X)Vf8-w1y4R-&tF*ityTSJ z<-ep?Eq3w0f192)W|+fxKoW_F`I>!k>2nsh``i(`^c8mH?+^a@W9fIPbN8b@?PIu_ zjN$x*oRl5XJMYQwohLW`d* z4Sv)#yr>pjY{QJ=0-%GwUfFl={;Zr-`~5fb+MoUOea-!jJ>!>U(>z9$#03oUL%$gp zUEl0}+5dtaSW-L^Y*THTaI@4EhH)|$q6K$2k&oQR3O^yK%Ct9@@je4N_Z{PC;%18=?{za>8}NLmp)p|Ij`#MIddn5T~mMUef9rmoWm_U zyZrO7-yiz9bne2S{m0fZ6tFR*BNMIlatdb;ujW7W^MuEry5A4pcJF-mS^fK6k6AOd zkvt3bcFfE3j9FX%71&KM`XBvU?%Z^-@WVg9ovO0%pQq+~Rut9QJPhl;JZE_Ow*BzW zD_49h?CaO*&#Cvg{d~JcID-XqgE11Z!)Ok($)=ih^$CjqY#hQiZ~pA5UCfH21gJy% zk6c;rt@9sb6wYoH4mYx`zdrF?!M@L5qnEwc-Ls?KT=VmLu3Z=bu~oEo|NoT4{F=st z@iV`Dj67>I>r6>~y_nEu=H!13wY5BC_&c>n(2 z+P!sCGw1J{vgy44ZvTh3Uv4a@dv>F-z|QQ-KKG0r#oBkChiThBeEeE^ z_QRT*KUK?W|K*(e%ed{D*x{ci&)Is4)yX;RG(rjqus3BCeDm}3EBp%nTzJ3a%rmZk k|LUbM!VH*9fvBTi+pqETart<8 diff --git a/drool/imgs/icons/gorillax_icon.preview.png b/drool/imgs/icons/gorillax_icon.preview.png deleted file mode 100644 index a7d1f2ac0766bd5a4098e09baaf359c2fca4229e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 837 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVCM03aSW-L^Y-q>ED=YBhKHRQ z*@6?S*Izm2q|AQIt@-+#R*siJEbltR!t5IPC#Y}m5`4-ewzB!?O^vL{9PP737!Mv& zO6T~&^<4T)$6*8R|Nplpe>?Qk*kJSK*#at_lTDpW>HaoL_&hMnrc1uLZ)g_Le>I_0Mk?me{mx$oL)S#h1=t!Q5bs zMC@34>Ghwh($~B0t(WgLU-Y$+0Q9k&z=8U&AxTMG^RsV*R8kuzQ^HJ-M=?q zbf(m-OPp5u_ttxHEDHDWPuLZIFY=w(-#_C1@5ExCKiAsp4-7t(;AUe8|Fg4U$`vd7 zQa$^xcMkrXfB)jgo0<1^{=f44xn1~&FZbrYsJMJ)^S^ZqjZs}K$q@6kw&9AEt;)5F zD@(sQzWUW#57e~$+Ry(7J~RBPM?@)@b@e-A(e;BXe}D8f*8lZ$XVH1%Cq)>cyc+0c zBdL6sb zi$PKOq5_z1tp0tvnRfNRGG^Sr`puxbL7~1K)3kcV?V^+TPUI$mG75vItDnm{r-UW| DD9L5{ diff --git a/drool/imgs/icons/iblivion_icon.preview.png b/drool/imgs/icons/iblivion_icon.preview.png deleted file mode 100644 index 276ff1dd44d265e6417c7860538b84b835e3b4a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 872 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVAl6^aSW-L^Y-rkY!OEphmXEJ z3KCldxV10OQCPweBOKsj;B;WgHRl$QZEUxjC7L*{Ynz2RbbN(MJXF9y8df)fV<6ryECMTrjm%Dcye_2Cx znH!85Ea1eBTk8v_i(5Uks(<-$`G+3|-`!qwzW(n!cZ+*_er@{p+~5%pLpnnN8xmn2 z^!@eEw?~~_=ic4*+gK+4ZC+?ypYl0WML<2tq4kYFZ^n1l7j9Xx+1mX3;g47IChp2# zc&U~t9mxS;r|dZOo$*?=`=O5~H-G#%m%YAw&VAt$qkj1^Ok=0MXMS=jcKLSUA(MRzW#IN>xcKtAC-RkxBc|PA5W$(fB0kbRN;p|j{N+wulde%hHt;) z4}X;YJp18~bNAjC)GWKn_1{kVcDC!e&+M2+@)fPM%$~o%&``8*zGdY)jo_@gcRXtL zUt5~}@W-w0iusFvXY5(==_l99`@$B?2P7HhFd`9GAI>d%Xm{9WbLf?uFOMBBsL9-R zC%9(6`H8Chg~q5#B^hF#p6B$5?U(;n`tR1ahXs4C{t@mwzqW?+`2G5dsar1sy@?i; z;XmyhPQ80>9{FALiQIkvJOAyk>j%C5Qc&~!*U>8Kgfks$Ufr`)=Jm z6?J_1wTGHJ&+B{C*1anKom$L>lCXgB3RL}o;q?Q@b6@{$%1yCbzds8zrnbf{zUHX> yG4AM<-z@KJGq3-Wu|^LBk4Y+?lT;l4Fdw#>EpNnO7 zwA7*%{~!EmlV7e~yMO-I74Mbe`R)|Iw_Km8;yFphQ|M^GTmBhyEx#`}v9l}N^W*pF z)#rn*TNqk}{rPk6-GX2L6YQ-od}Cb^%f^t-P=Fu~=iYx_Zv8&!`$yx=>9eQ!ZC-z! z^W6Q)z&-oSyTgO7Kj%SJ&c-l3j{U~%pVv~ZF<$!qvGLyn@6Ge?R{!5wF@OKxqTR2a zTQeV!WSGN!Tk*ANExLy0r4j{r5I?GpgbjT)yjLd%izrj!l%z z_gaR~RAf)FF^I2YcKQ8y@$;wce)8J?{<0seNZDj5@#jcQ;THKl{sKQYmhHFr_wqwd zmTbbSPay@YJ3lkeVSK>DaE<|qxRm}N_58=(yEaYRb^iZ_KRSK(`)fD9`pNkC;7|66 zFFvb_&tLM*S7-092$^yW$9vD`zi?YtotS$;$+`v zGKbdeeej!Q)28e4vGFB!&POCsJ;B4U?$>v=wC|!<_MLynI$wTm@Rr)Yk>^l@3z#IX zUT3^jcTawAxV>DD`)|HQr41SPop1k^!VH$S?DzlsOLomZ`u*1PyjWo9-hBOeWiiX| zgS8bUwHNobpSW|V>ea96)jPinVru?(^e2PnMXLjgr!GHw|Nf0zntkWzSl;~hUm?nU vMO7`PvvTh~ zOcavR7b^W&e?hA^tgCaGS=PSB)>fwjB8;4j+}5XKvP(UEWH}dw3AwZ^G0^HtPn_Pc zz)Yy)2kSk-V~^9mSJu5X*#59WV#djPd0vxLJSVAiCTVQFvvcm#G!AHW)KlFe4GG z|H&wPm)SefeC`L!CiZ$=k=XjO*A*7`USInwb3i5>Rk1O{j#uXy*>bIZ?{4t3`1Rsp z#j2SYp1)IY`M&gz2Vb7=i~E1KebaZLYgj!0tGJ=}=+qDUetp(#ea^C8?a_1gKNWv9 zW#aFi+QYvf`ulH&BCKB1sO?KQHHSIo=F6j>%dL9<{jKlAa6ruB?;Gmx&W*hN`gt|$ zdtD#VyXyn2F@56pKJow2hswtf6*s*OzWMym)?d~|=)p4ql!`o?elY*$$XKoNCd?C< PdKo-j{an^LB{Ts5!4X4Y diff --git a/drool/imgs/icons/nirvamma_icon.preview.png b/drool/imgs/icons/nirvamma_icon.preview.png deleted file mode 100644 index 3c3ca554d91d543c48b1f26d8daee6ea56c38613..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 805 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV0z=};uumf=j~m??8%NY4IjI2 zh}%1e3-<7pp1AhnR@nvqM+$vMSMHYWc772d^IP}I!IZ0FGBR%#aC(TVt5GAVf&g;&3^FE}u{ycj?C~^n`1N`iFW|!Zu7Jp`s zs{Z%v?t+xL%2DnWm46B&KYtg)^yRGg>>ba3?moQx=IiC3PltT|6?F0MpNzQ+zWrqG zLybI;(bpLlRTh-^C6x5VXJ1a3yYSn-{~cJ|TiS4`_W#Ad|4!^En45U@`AaSW-L^Y-q3?<< zbiO6vmwVImbRzG?X9>a1&KLN3vsN5XT*Sc>BO4>t!ndTOZGjY*kyA!NMs@+mriFcC z8`rHBD_m)Ixc%n=#JD5=z+MXxu*Er+WYQ@ zzIKP-e)#M5bn!>m<2&N_zuSNR*EO$ewM^v<7R(LCNW>1eyYmm58;j4sJh`~(`t?I! zSM3&F-`}75x2F1G?9}4-_TS%^e<(42UH^s;)hBEW(`)4wO5gwebF8!A@5MX41$!%> zX6(4P@6Vzaa~90Cc>cVu@8PXy(^vUnhQiJ7jQjrGJ^f2&|Fh#ie*T$q@nwbIgtgM= zIrr`Fs{eoGyXc==H%|Wg!#tlmKn%^fZ+z1Ei@q5~|N0yGSgKierWgd5bH5YrNFZd#(sA`&iBJ_JLBW4se&S$=2n;sGKWhcW| d1GMn??|oqES#^tz^1!^q;OXk;vd$@?2>>Eaik<)f diff --git a/drool/imgs/icons/sofabbi_icon.preview.png b/drool/imgs/icons/sofabbi_icon.preview.png deleted file mode 100644 index 35bdadda7afdbf0b5deadf6281c4c19ac6f69f10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 951 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV6O9YaSW-L^Y-q3@2P<@4Ii^x zXY&eOnc(4aE^ft2vG%41%Qgo`J9;%X3sg+q<>%^bFO$=s=G>H(!F6lh=81}CcN~~9 zI}ha5J3hCrsjS}m{O6kX-$~osnV;`@zw^C=^2*}(k@lZ|e!KL>fq6~v=kMhzpA%*4 z*cj3o3J}EcO|P%C9-Vnb{=%$zT5NX>lDhq`O6!hDqWXx3LGKQ`Mg7_uyS>&Eci)${wB9j)*L}IU|LrgT zj@b9_LF<*De>3D2t;_?z{uY?Sh!hs!PB$^Y@Fx z7QXwhm+HyepBKoOUv+!MWMkAAkYtFdSzlSYsbYWC>-5UGwVy8K71k8L2z+d?f4V`? z*S~*suz0Sr;l-a{-4Awt+j*V8dwTr;*tb9bzPNMX`Gd;lH`T0*zl&Yl>ko7vN*ZBf z5P#3ivgrG*t>Nn3kLNxq{yFi(pSs1jp0h5hZ2?9LrjPy|-TUH^)%M*xgJ17#=zXxa z>Eds;MWuCLD(ABYtVN^*Fzc25Zn;1I9R9l%pu)@P& zg7S<*UM@$Xg!Uh-?@FBMd++zw$-8fJ_czX+eb?7>k_r$7mSo90y#MoZZ^oauOS2!o z_Sdo3{>5j$ZVsoteR^r`?qB~m99ejaeZejshI0%L5X7-dcIFYYc+NkTU-O4E{{N#p zB^Gt*pPj8v%wa@Q1y;XmJ@cCU8vc6!|5nC4=RfZj{unM{FKPbo<(2IPHo41~&q|^i z&BL(nmz9IxyYGLlpYIplTW`L(&miiiFZ)bmhB=G}B$0@i*Vh@v()&dA)+OedeHPE} z`X;<4e@QK-fq84*e&@`&wRrjd!yj+|Jv{T{kAEzGf1Y2vuYUoip^u-hh(5gXgH7f0 z)B8h$&5L6l{9b;%9;aTw)?mzF!Hh&?*VrGpd0O7ucb)JanLoV8>SOyifoz0Ue%-TCuxZ?E4Qub{sD+{G_-jOnNb8#C;9^_oF*X@P7)w#C&K z`~KuT{ruA%jdPRxb?7=!;k83 zXLs-aHmhb`!ppCJ7t8NJ1- zSGT?`r~!s{G0W+dp65{A0d&^dc=i)-XaBdX3f*#l!StQ}55Ig>e>huzS^L9^db_D| v^@}l+$E||-2f|*bUgPw7e;G@7-KqcXT+b1{M(Y?bZ!mbe`njxgN@xNAi??OO diff --git a/drool/imgs/icons/xmon_icon.preview.png b/drool/imgs/icons/xmon_icon.preview.png deleted file mode 100644 index ce0ce25dce57765334bfbd5c3579be1cad050c22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 936 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV9xh+aSW-L^Y-q>?BGC=hL6*w zWR~d3cFEqE^Gd1Y-Gze(6(>9I?&4^QjcVz86fr?;3G2ays~rnY0%Z8Rs-&uD*zxJ7GTR;2rXW#ZNo2cSBNySrSZDD)_6wO= zUEkiiZ_CRU?_XaZo1JB~JNO>|#V^%2w@aj6tJ=eKK$2k&BNDN7b8+34_x1Ifcf{jE zO`hk6#;JE)Gi$OgE-TI0;OnV;IHF*E2t$)SbQv5FN=RfANtD{$bk6*t>{=xC* ztCs&ae(wJN$X|wCGmveOWQei;v)A6j@!R)T8TV^SB`?@_CBzy{N8l`^J2^2Q5)hFtm;*M|C_w;+(V3DTle+dzr;e>gtuGv z@C8Jd^_{okxMsL$|LhKz*BleyST%<;STHvjBN02UJv^L$w6uZ$_*2UtUmt&8`Il+y z^AB%+f1muDaZ_>4mzX0jeph~b?fP*Crq&NPzZP@P4NT)Zf9~Ygxva$%-*&cM{BQs3 z-=g0(}pE|M%?IyN^E3zkK-7$EkT={>{BI zXYVzY_v<%=|J!T7=~Lmo=d3T1kW(Nqu7Sa?ZLsR+p2m#(zu#RsedXu*$-iYcedq6( zcUq#h{-RYMrd#)K9>j diff --git a/drool/imgs/mulong_back.gif b/drool/imgs/mulong_back.gif new file mode 100644 index 0000000000000000000000000000000000000000..00103a168c3cecac8ecb451ec84a201cea016a64 GIT binary patch literal 8966 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 GD*pvoV1c&) literal 0 HcmV?d00001 diff --git a/drool/imgs/mulong_mini.gif b/drool/imgs/mulong_mini.gif new file mode 100644 index 0000000000000000000000000000000000000000..b21e7a171705ba478c784476390a62fc92b943e6 GIT binary patch literal 2271 zcmZ?wbhEHbRA5kGXlH-|d7W4zGbhXXD@BJt)VIxBVc4+g_|4~Ee#7KP>Cq4vo*|(4 zpWDwhB-q(8z|~04fSD2KOU0iooFYJ42jpW=HeulS&%nqb1NVM5?N8G?B2)7QXu1b>B&hJG1Fx!I|Uj&dkrm*9g$#Y zJlku*c|s?^;eg9Tu6(^e5eXTAi|r-~>C9w!6vX5rt|t<3CGE-*A$7MSv$kplF49uv zOUT%;{CsCRH-qhM<3|nigSPIvIxAG0aUmnmkCM)iw9sDP)022}5?=aFvS00$r)7NL z{sC{rN3A{)>%*q>neJQ5bzs%C?E?HN77P)YPqw?Os%LPSzf0Y_!F`xgDOs7lTd;6-le0!YIy31!y#I5QZAKP@z*ZSRi z``}}ouXuk))q>yGH}poI{a>n8|IbwS>--Rvt6%On{uF#6pYvVu#5L#LrZ*HSZERoM z%lx|W%CxYhFPA=S=U`<)4qzr=@XEo;q0v$Zl;TFg$O{2z0TYnWz`!c36_a7$z{JAA z&Nbu2hDC>2B!m^_bQCUX=n!R(JJa#;kt3t3vyjZpkB?j?Glei&R7_ZMx?eTEDWpPx zk!iLp=L}#0({O&ScKx}e@#<<9Y*~0XffYQ}<;_H*HdQ8zc)y~ZK z=}YC7oW^OX%o?&EgkPSSC%~Vy)YBn@f!WS8OXTH-4@Z|LAN^-~V9Qd+rBcfFrjiTZ z-=D=dwJBw#q4wEH^OeibT5mRv>)_;D@+~_!&uv?!t9|Q>Z4FP?Y>1bi${qO8abaET z>|+uYEAJfQTo<-ZN95DP?g@M4`rqvQ=sas%YyZ2KmfQ2%Ydg6-MCV+u4*#TPs-^z2 zbXn!y>(;ZwtU{Mm-f(~V`B;C6ecg>asmJ8~|NTvW(7^JkT%jS$@X7tm9LF2;Tun?x S9>$8Fa!F{8)D#h5um%8I^Gy=~ literal 0 HcmV?d00001 diff --git a/processing/buildMonGifs.py b/processing/buildMonGifs.py new file mode 100644 index 00000000..a9169998 --- /dev/null +++ b/processing/buildMonGifs.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Slice horizontal sprite sheets back into animated GIFs. + +The reverse of createMonSpritesheets.py, for art delivered as a single-row +strip. Input files follow the existing sprite naming ({mon}_{category}.png); +the frame size is the sheet height, so the frame count is width // height. +Frame duration comes from the category's existing convention. + +Usage: + buildMonGifs.py # every recognized sheet in drool/imgs + buildMonGifs.py sheets/ # a directory of sheets + buildMonGifs.py aurox_front.png ... # explicit files + buildMonGifs.py --out-dir drool/imgs --force + buildMonGifs.py --ms 150 # override the category duration +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +# Frame duration per category, measured from the existing GIFs in drool/imgs. +CATEGORY_MS = { + "front": 100, + "back": 200, + "front_damage": 100, + "mini": 200, +} + +# Input suffixes that normalize onto a canonical category. +CATEGORY_ALIASES = { + "hurt": "front_damage", + "damage": "front_damage", + "damage_front": "front_damage", +} + +# Packed outputs of the forward pipeline — never inputs. +PACKED_SHEETS = { + "mon_spritesheet.png", + "mon_switch.png", + "mon_mini.png", + "mon_micro.png", + "mon_shadow.png", +} + +ALPHA_THRESHOLD = 128 # GIF alpha is 1-bit; anything fainter drops out +LOOP = 0 +DISPOSAL = 2 + + +def classify(png: Path) -> tuple[str, str] | None: + """Split {mon}_{category}.png into (mon, canonical category), or None if the + suffix is not a known sprite category.""" + if "_" not in png.stem: + return None + mon, suffix = png.stem.split("_", 1) + category = CATEGORY_ALIASES.get(suffix, suffix) + return (mon, category) if category in CATEGORY_MS else None + + +def slice_sheet(sheet: Image.Image) -> list[np.ndarray]: + """Cut a single-row sheet into square frames of side == sheet height.""" + width, size = sheet.size + if width % size: + raise ValueError(f"width {width} is not a multiple of height {size}") + rgba = np.asarray(sheet.convert("RGBA")) + return [rgba[:, i * size : (i + 1) * size] for i in range(width // size)] + + +def pack_rgb(rgb: np.ndarray) -> np.ndarray: + """(N, 3) uint8 -> (N,) int32 sort key, so colours dedupe in one pass.""" + channels = rgb.astype(np.int32) + return (channels[:, 0] << 16) | (channels[:, 1] << 8) | channels[:, 2] + + +def save_gif(frames: list[np.ndarray], out_path: Path, duration: int) -> None: + """Write an indexed GIF with a shared palette + transparent index 0.""" + masks = [f[..., 3] >= ALPHA_THRESHOLD for f in frames] + opaque = [pack_rgb(f[m][:, :3]) for f, m in zip(frames, masks)] + colors = np.unique(np.concatenate(opaque)) if any(len(o) for o in opaque) else np.empty(0, np.int32) + if len(colors) > 255: + raise ValueError(f"too many colours for a GIF palette: {len(colors)}") + + flat = [0, 0, 0] # transparent placeholder + for key in colors.tolist(): + flat.extend(((key >> 16) & 0xFF, (key >> 8) & 0xFF, key & 0xFF)) + flat.extend([0] * (768 - len(flat))) + + pal_frames = [] + for frame, mask, keys in zip(frames, masks, opaque): + idx = np.zeros(frame.shape[:2], dtype=np.uint8) + idx[mask] = np.searchsorted(colors, keys) + 1 + img = Image.fromarray(idx, mode="P") + img.putpalette(flat) + pal_frames.append(img) + + pal_frames[0].save( + out_path, + save_all=True, + append_images=pal_frames[1:], + duration=duration, + loop=LOOP, + disposal=DISPOSAL, + transparency=0, + optimize=False, + ) + + +def convert(png: Path, out_dir: Path, ms: int | None, force: bool, dry_run: bool) -> bool: + classified = classify(png) + if classified is None: + print(f" ⚠ Skipping {png.name}: unrecognized category (expected one of {', '.join(sorted(CATEGORY_MS))})") + return False + mon, category = classified + + gif_path = out_dir / f"{mon}_{category}.gif" + if gif_path.exists() and not force: + print(f" ⚠ Skipping {png.name}: {gif_path.name} exists (pass --force to overwrite)") + return False + + with Image.open(png) as sheet: + frames = slice_sheet(sheet) + duration = ms if ms is not None else CATEGORY_MS[category] + + partial = sum(int(np.count_nonzero((f[..., 3] > 0) & (f[..., 3] < ALPHA_THRESHOLD))) for f in frames) + if partial: + print(f" ⚠ {png.name}: dropped {partial} px below alpha {ALPHA_THRESHOLD}") + + size = frames[0].shape[0] + if dry_run: + print(f" · {png.name} -> {gif_path.name} ({len(frames)} x {size}px @ {duration}ms)") + return True + + save_gif(frames, gif_path, duration) + print(f" ✓ {png.name} -> {gif_path.name} ({len(frames)} x {size}px @ {duration}ms)") + return True + + +def collect(inputs: list[str], default_dir: Path) -> list[Path]: + if not inputs: + return [p for p in sorted(default_dir.glob("*.png")) if p.name not in PACKED_SHEETS] + paths = [] + for raw in inputs: + path = Path(raw) + if path.is_dir(): + paths.extend(p for p in sorted(path.glob("*.png")) if p.name not in PACKED_SHEETS) + else: + paths.append(path) + return paths + + +def main() -> None: + base = Path(__file__).parent.parent + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("inputs", nargs="*", help="sheet PNGs or a directory (default: drool/imgs)") + parser.add_argument("--out-dir", help="where to write the GIFs (default: alongside each sheet)") + parser.add_argument("--ms", type=int, help="override the category frame duration") + parser.add_argument("--force", action="store_true", help="overwrite existing GIFs") + parser.add_argument("--dry-run", action="store_true", help="report what would be written") + args = parser.parse_args() + + default_dir = base / "drool" / "imgs" + sheets = collect(args.inputs, default_dir) + if not sheets: + print("No PNG sheets found") + sys.exit(1) + + missing = [p for p in sheets if not p.is_file()] + if missing: + print(f"Error: no such file: {missing[0]}") + sys.exit(1) + + out_dir = Path(args.out_dir) if args.out_dir else None + if out_dir: + out_dir.mkdir(parents=True, exist_ok=True) + + print(f"Converting {len(sheets)} sheet(s)") + converted = 0 + for png in sheets: + try: + converted += convert(png, out_dir or png.parent, args.ms, args.force, args.dry_run) + except ValueError as e: + print(f" ✗ {png.name}: {e}") + + if not converted: + print("\nNothing converted") + sys.exit(1) + print(f"\n✅ Done — {converted} GIF(s)") + + +if __name__ == "__main__": + main() From 16d723dd44eecd8b04ec3b89997f7976eef7d56e Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:13:01 -0700 Subject: [PATCH 36/41] clean up --- .claude/skills/deploy-chomp/SKILL.md | 8 +- .gitignore | 12 +- .python-version | 1 - BENCH.md | 43 +- CLAUDE.md | 34 +- drool/abilities.csv | 4 +- drool/index.html | 138 --- drool/mon-analysis.js | 738 -------------- drool/mon-full.js | 318 ------ drool/mon-moves.js | 690 ------------- drool/mon-stats.js | 683 ------------- drool/mons.csv | 4 +- drool/moves.csv | 12 +- drool/style.css | 943 ------------------ drool/type-data.js | 107 -- drool/type-metadata.csv | 15 + drool/utils.js | 200 ---- processing/addressPacking.py | 7 +- processing/csvTable.py | 95 ++ processing/dataEditor.html | 640 ++++++++++++ processing/deploy.py | 24 +- processing/editData.py | 327 +++++++ processing/generateMonsTypeScript.py | 16 +- processing/generateSolidity.py | 405 ++++---- processing/packMoves.py | 114 ++- processing/validateMoves.py | 186 ++-- script/DeployData.sol | 8 + script/SetupMons.s.sol | 966 ------------------- script/mons/SetupAurox.s.sol | 90 ++ script/mons/SetupEkineki.s.sol | 83 ++ script/mons/SetupEmbursa.s.sol | 85 ++ script/mons/SetupGhouliath.s.sol | 85 ++ script/mons/SetupGlob.s.sol | 73 ++ script/mons/SetupGorillax.s.sol | 65 ++ script/mons/SetupIblivion.s.sol | 83 ++ script/mons/SetupInutia.s.sol | 85 ++ script/mons/SetupMalalien.s.sol | 71 ++ script/mons/SetupMulong.s.sol | 80 ++ script/mons/SetupNirvamma.s.sol | 75 ++ script/mons/SetupPengym.s.sol | 79 ++ script/mons/SetupSofabbi.s.sol | 78 ++ script/mons/SetupVolthare.s.sol | 87 ++ script/mons/SetupXmon.s.sol | 91 ++ sims/src/harness.ts | 6 +- sims/src/util/csv-load.ts | 16 +- sims/src/util/inline-pack.ts | 32 +- sims/src/util/mon-builder.ts | 9 +- src/mons/ghouliath/Osteoporosis.json | 6 - src/mons/glob/HolyWaterClaw.json | 6 - src/mons/glob/WaterClaw.json | 6 - src/mons/gorillax/Blow.json | 6 - src/mons/gorillax/PoundGround.json | 6 - src/mons/gorillax/ThrowPebble.json | 6 - src/mons/inutia/BigBite.json | 6 - src/mons/malalien/FederalInvestigation.json | 6 - src/mons/malalien/InfiniteLove.json | 6 - src/mons/malalien/NegativeThoughts.json | 6 - src/mons/mulong/CloudStrike.sol | 6 +- src/mons/mulong/KingsRespite.sol | 3 +- src/mons/mulong/Thunderclap.json | 6 - src/mons/nirvamma/ScaryNumbers.json | 6 - src/mons/pengym/ChillOut.json | 6 - src/mons/sofabbi/UnexpectedCarrot.json | 6 - src/mons/volthare/Electrocute.json | 6 - src/moves/MoveSlotLib.sol | 15 +- test/MainnetReplayGasTest.t.sol | 28 +- test/RealMonReplayGasTest.t.sol | 30 +- test/mons/MulongTest.sol | 3 +- transpiler/codegen_rs/README.md | 11 +- transpiler/sol2rs.py | 39 +- transpiler/strategies-rs/src/analysis.rs | 59 +- transpiler/strategies-rs/src/bin/analyze.rs | 26 +- transpiler/strategies-rs/src/bin/cert.rs | 2 +- transpiler/strategies-rs/src/bin/montrace.rs | 62 ++ transpiler/strategies-rs/src/bin/teams.rs | 20 +- transpiler/strategies-rs/src/heuristic.rs | 6 +- transpiler/strategies-rs/src/matrix.rs | 2 +- transpiler/strategies-rs/src/mock2.rs | 5 +- transpiler/strategies-rs/src/roster.rs | 113 ++- transpiler/strategies-rs/src/shared.rs | 38 +- transpiler/strategies-rs/src/teams.rs | 57 +- transpiler/strategies-rs/src/view.rs | 7 - transpiler/transpiler-config-rust.json | 1 - 83 files changed, 3199 insertions(+), 5435 deletions(-) delete mode 100644 .python-version delete mode 100644 drool/index.html delete mode 100644 drool/mon-analysis.js delete mode 100644 drool/mon-full.js delete mode 100644 drool/mon-moves.js delete mode 100644 drool/mon-stats.js delete mode 100644 drool/style.css delete mode 100644 drool/type-data.js create mode 100644 drool/type-metadata.csv delete mode 100644 drool/utils.js create mode 100644 processing/csvTable.py create mode 100644 processing/dataEditor.html create mode 100644 processing/editData.py create mode 100644 script/DeployData.sol delete mode 100644 script/SetupMons.s.sol create mode 100644 script/mons/SetupAurox.s.sol create mode 100644 script/mons/SetupEkineki.s.sol create mode 100644 script/mons/SetupEmbursa.s.sol create mode 100644 script/mons/SetupGhouliath.s.sol create mode 100644 script/mons/SetupGlob.s.sol create mode 100644 script/mons/SetupGorillax.s.sol create mode 100644 script/mons/SetupIblivion.s.sol create mode 100644 script/mons/SetupInutia.s.sol create mode 100644 script/mons/SetupMalalien.s.sol create mode 100644 script/mons/SetupMulong.s.sol create mode 100644 script/mons/SetupNirvamma.s.sol create mode 100644 script/mons/SetupPengym.s.sol create mode 100644 script/mons/SetupSofabbi.s.sol create mode 100644 script/mons/SetupVolthare.s.sol create mode 100644 script/mons/SetupXmon.s.sol diff --git a/.claude/skills/deploy-chomp/SKILL.md b/.claude/skills/deploy-chomp/SKILL.md index 896eb48b..02db3dcb 100644 --- a/.claude/skills/deploy-chomp/SKILL.md +++ b/.claude/skills/deploy-chomp/SKILL.md @@ -8,8 +8,9 @@ description: Build and deploy pipeline for chomp — processing scripts, the Sol ```bash python processing/validateMoves.py # Validate contracts vs CSV python processing/validateEffectBitmaps.py # Validate ALWAYS_APPLIES_BIT usage on IEffect contracts -python processing/generateSolidity.py # Generate SetupMons.s.sol +python processing/generateSolidity.py # Generate script/mons/Setup.s.sol (one per mon) python processing/generateSetupCPU.py # Generate SetupCPU.s.sol from cpu-teams.json +python processing/editData.py # Visual editor: mon stats / move rows / CPU teams (saves + reruns the above) python processing/deploy.py --testnet # Full deployment (forge scripts + codegen) python processing/deploy.py --mainnet # Production deployment ``` @@ -63,7 +64,10 @@ it; strategies that ship are hand-ported into munch and gated there against ### Deployment Order 1. `EngineAndPeriphery.s.sol` - Engine, type calculator, `SignedCommitManager`, `GachaTeamRegistry`, `SignedMatchmaker`, CPU host, `SimplePM`, shared effects -2. `SetupMons.s.sol` - All mon contracts (moves, abilities) +2. `script/mons/Setup.s.sol` - one script per mon (moves, abilities, `createMon`), broadcast in + ascending mon-id order because `createMon` rejects non-sequential ids. `deploy.py` derives the + list from `mons.csv`, so a failed deploy resumes by rerunning from the mon that failed rather + than redeploying the whole roster. 3. `SetupCPU.s.sol` - CPU players ### CI/CD diff --git a/.gitignore b/.gitignore index f57cea18..a0951102 100644 --- a/.gitignore +++ b/.gitignore @@ -34,20 +34,10 @@ drool/* # Keep main data !drool/abilities.csv -!drool/index.html -!drool/mon-analysis.js -!drool/mon-full.js -!drool/mon-moves.js -!drool/mon-stats.js !drool/mons.csv !drool/moves.csv -!drool/style.css !drool/types.csv -!drool/type-data.js -!drool/utils.js -!drool/combine.py -!drool/switch-animation.py -!drool/docs +!drool/type-metadata.csv # Transpiler output ts-output/ diff --git a/.python-version b/.python-version deleted file mode 100644 index 2c073331..00000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11 diff --git a/BENCH.md b/BENCH.md index c71a9e3e..966c2bfe 100644 --- a/BENCH.md +++ b/BENCH.md @@ -110,6 +110,40 @@ Two active mons per side; your bot returns `Action::Slots([move, move])` and dec `modes()` as `Doubles`. Doubles is the mode the shipped CPU actually plays, so improvements there transfer most directly. +## Measuring mons, not just bots + +The same harness answers balance questions. These read `drool/*.csv` and `src/mons/*.json` at +run time from `$CHOMP_ROOT`, so a stat or move edit takes effect on the next run with no rebuild +— point `CHOMP_ROOT` at a scratch copy of the repo to run ablations without touching your tree. + +```bash +cd transpiler/rs-output +CHOMP_ROOT=../.. ./target/release/analyze --games 6000 [--mode doubles] [--p0 greedy --p1 greedy] +CHOMP_ROOT=../.. ./target/release/montrace --mon Mulong --games 800 +CHOMP_ROOT=../.. ./target/release/teams --games-per-team 150 +``` + +- **`analyze`** — per-mon win%, active turns, KOs dealt/taken, and the mon-vs-mon KO matrix. + `--p0`/`--p1` pin the pilots; results shift a lot with bot strength, so state which you used. +- **`montrace`** — one mon's doubles diagnostic: move usage, survival, and KOs per 100 active + turns for its *teammates* and *opponents* against a no-target control. That last one separates + "this mon kills things" from "this mon makes its partner's kills happen". +- **`teams`** — the exhaustive team search and main effect described above. + +### Watching a long run + +`analyze` and `teams` accept `--progress`, which streams the ranked table to **stderr** every 5% +while stdout stays a single clean result table: + +``` +[ 10% 300/3000] Mulong 61.9 · Volthare 56.2 · Malalien 55.4 · Ekineki 54.7 · … +``` + +Slicing only moves scheduling boundaries — every game is independently seeded, so output is +byte-identical with and without the flag. `teams` walks a strided permutation when streaming, +because its specs are team-major and consecutive slices would otherwise give a badly biased +partial main effect rather than a merely noisy one. + ## Research directions Some ideas for you to try out: @@ -130,10 +164,17 @@ switch-in move, setup move) with a heuristic fallback. ### Synergy-aware play -`./bench.sh` aside, the tree has an exhaustive team analyser: all C(13,4) = 715 teams +`./bench.sh` aside, the tree has an exhaustive team analyser: all C(15,4) = 1365 teams evaluated against the random-draft field, with synergy read as the *interaction* beyond each mon's main effect (`src/bin/teams.rs`). Overclock lifting slower attackers shows up there. +Read the main effect with its harness in mind: `teams.rs` pilots **both sides with `greedy` at +depth 0**, so it measures how much a mon lifts a team *against unsophisticated play*, which is +not the same question as `analyze`'s win rate. A mon can be ordinary in one and dominant in the +other — Glob's free once-per-battle damage absorb is worth +25 main-effect points against +`greedy` and ~3 win-rate points against `heuristic`, because only the smarter bot plays around +it. + No bot currently reads its own roster and plays to it. A bot that recognises it drafted a speed-control core, or a specific two-mon combo, and sequences toward it, is open ground. diff --git a/CLAUDE.md b/CLAUDE.md index 1d4c9465..a43b3b67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,12 +115,17 @@ chomp/ │ └── *.sol # Other test files ├── script/ # Foundry deployment scripts │ ├── EngineAndPeriphery.s.sol # Deploy engine + periphery contracts -│ ├── SetupMons.s.sol # Deploy all mons (auto-generated by processing/) +│ ├── mons/ # One deploy script per mon (auto-generated by processing/) +│ │ └── Setup.s.sol # Deploy abstract recipe + Setup broadcast runner +│ ├── DeployData.sol # Shared {name, address} return shape parsed by deploy.py │ ├── SetupCPU.s.sol # Deploy CPU players +│ ├── cpu-teams.json # CPU team source of truth (edit via processing/editData.py) │ └── Surgery.s.sol # Maintenance/upgrade script ├── processing/ # Python build scripts -│ ├── generateSolidity.py # Generate SetupMons.s.sol from CSV data +│ ├── generateSolidity.py # Generate script/mons/Setup.s.sol from CSV data │ ├── generateSetupCPU.py # Generate SetupCPU.s.sol team config from cpu-teams.json +│ ├── editData.py # Local visual editor: mon stats, move rows, CPU teams (writes + resyncs) +│ ├── csvTable.py # Quote-preserving CSV read/write for drool/*.csv │ ├── validateMoves.py # Validate move contracts match CSV data │ ├── validateEffectBitmaps.py # Validate IEffect contracts set ALWAYS_APPLIES_BIT correctly │ ├── deploy.py # Full deployment pipeline orchestrator @@ -149,13 +154,14 @@ chomp/ │ ├── runtime-rs/, strategies-rs/ # Hand-written Rust crates (rsynced into rs-output/) │ ├── dependency_resolver/ # Dependency resolution │ └── test_transpiler.py # Transpiler unit tests (the TS integration tests live in munch) -├── drool/ # Game data (CSV) and frontend assets +├── drool/ # Game data (CSV) and sprite assets — data only; the Tauri viewer it +│ │ # used to host was replaced by processing/editData.py │ ├── mons.csv # Mon stats (HP, attack, speed, types, etc.) │ ├── moves.csv # Move definitions (power, stamina, accuracy, etc.) │ ├── abilities.csv # Ability definitions │ ├── types.csv # Type chart data -│ ├── imgs/ # Sprites (front/back/mini GIFs, spritesheets) -│ └── *.js, *.css, index.html # Data viewer/analysis web app +│ ├── type-metadata.csv # Per-type display fields (kanji, colours, abbreviations) +│ └── imgs/ # Sprites (front/back/mini GIFs, spritesheets) ├── docs/ # Design docs and notes ├── snapshots/ # Foundry gas snapshots (JSON) ├── lib/ # Git submodules (forge-std) @@ -276,10 +282,20 @@ ATTACK_PARAMS({ ``` These are packed into inline move words (see `MoveSlotLib`) rather than deployed as separate -contracts, and mirrored to JSON for the client. `StandardAttack`'s base `move()` resolves the defender +contracts. `StandardAttack`'s base `move()` resolves the defender from `targetBits`; a move's client-facing target domain lives in `moves.csv`'s `TargetSpec` column, not in Solidity. +**Inline moves.** A move with a `src/mons//.json` file is packed inline instead of +deployed. The file holds only `{"effect": "" | null}` — every packed field +(`basePower`, `staminaCost`, `moveType`, `moveClass`, priority offset, and `EFFECT_ACCURACY` from +the `Constants` column) is read from the move's `moves.csv` row by +`packMoves.inline_params_from_csv`, so the CSV is the single source of truth and can't drift from +the deployed word. `validateMoves.py` checks each inline row is *representable* (accuracy 100, +priority offset 0-3, power ≤ 255, stamina ≤ 15, no constants besides `EFFECT_ACCURACY`) rather than +comparing it against a second copy. The same CSV-sourced packing is mirrored in +`sims/src/util/inline-pack.ts` and `transpiler/strategies-rs/src/roster.rs`. + Custom moves implement `IMoveSet` directly for complex behavior. The current interface is `move(engine, battleKey, attackerPlayerIndex, attackerMonIndex, targetBits, activesPacked, extraData, rng)` plus the metadata getters (`priority`/`stamina`/`moveType`/`moveClass`) and the @@ -288,6 +304,12 @@ Moves resolve targets from `targetBits` + `activesPacked` via `TargetLib` (they defender index), and deal damage through `engine.dispatchStandardAttack` / `dispatchCustomAttack` (both take `targetBits`). +`getMeta().basePower` means **damage on the turn the move is cast**, and it is the only basePower +source — there is no separate getter to probe. A move whose damage is deferred (Q5, King's Respite) +or conditional must report `0` even when it holds a `BASE_POWER` constant for the eventual hit: the +off-chain CPU treats a nonzero quote as this-turn damage and a zero quote as "simulate it", so +advertising nominal power makes the CPU throw the move away on turns it does nothing. + ### Effect System Effects implement `IEffect` with a bitmap (`getStepsBitmap()`) indicating which lifecycle steps they diff --git a/drool/abilities.csv b/drool/abilities.csv index ad3cb855..22ec1291 100644 --- a/drool/abilities.csv +++ b/drool/abilities.csv @@ -12,5 +12,5 @@ Up Only,Aurox,"Whenever Aurox takes damage, they gain a persistent 10% ATK boost Dreamcatcher,Xmon,"Whenever Xmon gains stamina, heal 1/16 (6.25%) of max HP." Savior Complex,Ekineki,"On switch-in, gain a temporary 15/25/30% SpATK boost based on KO'd mons (1/2/3+). Triggers once per game." Adaptor,Nirvamma,"When Nirvamma takes damage for the first time each game, they adapt to that source of damage. Nirvamma take 50% less damage from that move or effect." -Chosen One,Glob,"The first time Glob switches in, it enters Blessed." -King's Leisure,Mulong,"For 3 active turns after its first switch-in, Mulong takes half damage and deals half damage." \ No newline at end of file +Chosen One,Glob,"Glob starts each game Blessed when it switches in." +King's Leisure,Mulong,"Mulong takes half damage and deals half damage for the first 3 turns it arrives." \ No newline at end of file diff --git a/drool/index.html b/drool/index.html deleted file mode 100644 index 176b6168..00000000 --- a/drool/index.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - wiki - - - - - - - - -

- - diff --git a/drool/mon-analysis.js b/drool/mon-analysis.js deleted file mode 100644 index c498ecd2..00000000 --- a/drool/mon-analysis.js +++ /dev/null @@ -1,738 +0,0 @@ -import { typeData } from "./type-data.js"; -import { loadMovesFromCsv, loadTypeData } from "./utils.js"; - -document.addEventListener("DOMContentLoaded", async function () { - // Elements - const maxDamageTable = document.getElementById("max-damage-table"); - const transposeTableCheckbox = document.getElementById("transpose-table"); - const monsterSelect = document.getElementById("monster-select"); - - // Variables for damage table - let data = []; - let columns = []; - let isTransposed = false; // for max damage table view - - // Variables for max damage table sorting - let maxDamageColumnToSort = undefined; // index of the column to sort by - let maxDamageDirection = false; // false for ascending, true for descending - - // Variables for move damage table sorting - let moveDamageColumnToSort = undefined; // column name to sort by - let moveDamageDirection = false; // false for ascending, true for descending - - // Variables for move damage calculation - let movesData = await loadMovesFromCsv(); - let typeEffectivenessData = await loadTypeData(); - - // Event listeners - transposeTableCheckbox.addEventListener("change", function() { - isTransposed = this.checked; - document.getElementById("view-perspective").textContent = - isTransposed ? "(Defender View)" : "(Attacker View)"; - renderMaxDamageTable(); - }); - monsterSelect.addEventListener("change", (e) => { - if (e.target.value !== "") { - calculateMonsterAnalysis(parseInt(e.target.value)); - } else { - document.getElementById("analysis-results").innerHTML = ""; - } - }); - - // Listen for data updates from mon-stats.js - document.addEventListener("monster-data-updated", function(event) { - data = event.detail.data; - columns = event.detail.columns; - updateMonsterSelect(); - renderMaxDamageTable(); - }); - - // Listen for moves data updates from mon-moves.js - document.addEventListener("moves-data-updated", async function(event) { - // Update moves data - movesData = event.detail.data; - // Re-render the damage table with the updated moves - renderMaxDamageTable(); - - // If a monster is currently selected, recalculate its analysis - const selectedMonsterIndex = monsterSelect.value; - if (selectedMonsterIndex !== "") { - calculateMonsterAnalysis(parseInt(selectedMonsterIndex)); - } - }); - - function updateMonsterSelect() { - const select = document.getElementById("monster-select"); - select.innerHTML = ''; - - data.forEach((monster, index) => { - const option = document.createElement("option"); - option.value = index; - option.textContent = monster.Name || "Unknown"; - select.appendChild(option); - }); - - // Set the first monster as the default if there are any monsters - if (data.length > 0) { - select.value = 0; // Select the first monster - calculateMonsterAnalysis(0); // Calculate analysis for the first monster - } - } - - function calculateMonsterAnalysis(monsterIndex) { - const defender = data[monsterIndex]; - const results = { - moveDamage: [] - }; - - // Calculate all damage values - data.forEach((attacker) => { - // Calculate damage for each of the attacker's moves against the defender - const attackerMoves = getMovesForMonster(attacker.Name); - if (attackerMoves.length > 0) { - attackerMoves.forEach(move => { - const moveDamage = calculateMoveDamage(move, attacker, defender); - if (moveDamage) { - results.moveDamage.push({ - ...moveDamage, - attacker: attacker.Name - }); - } - }); - } - }); - - const stats = { - moveDamage: results.moveDamage - }; - - displayAnalysisResults(stats, defender.Name); - } - - // Function to get type effectiveness multiplier - function getTypeEffectiveness(attackerType, defenderType1, defenderType2) { - let type1Multiplier = typeEffectivenessData[attackerType][defenderType1]; - if (type1Multiplier == 5) { - type1Multiplier = 0.5; - } - let type2Multiplier = defenderType2 === 'NA' ? 1 : typeEffectivenessData[attackerType][defenderType2]; - if (type2Multiplier == 5) { - type2Multiplier = 0.5; - } - return type1Multiplier * type2Multiplier; - } - - // Function to calculate damage for a move - function calculateMoveDamage(move, attacker, defender) { - // Skip if move has no power or power is '?' - if (!move.Power || move.Power === '?' || move.Power === 0) { - return null; - } - - // Determine if physical or special move - const isPhysical = move.Class === 'Physical'; - const isSpecial = move.Class === 'Special'; - - // Skip if not a damaging move - if (!isPhysical && !isSpecial) { - return null; - } - - // Get the relevant attack and defense stats - const attackStat = isPhysical ? attacker.Attack : attacker.SpecialAttack; - const defenseStat = isPhysical ? defender.Defense : defender.SpecialDefense; - const attackStatName = isPhysical ? 'Attack' : 'Sp.Atk'; - const defenseStatName = isPhysical ? 'Defense' : 'Sp.Def'; - - // Calculate base damage - let baseDamage = (move.Power * attackStat) / defenseStat; - - // Apply type effectiveness - const typeMultiplier = getTypeEffectiveness(move.Type, defender.Type1, defender.Type2); - const damage = baseDamage * typeMultiplier; - - return { - damage, - baseDamage, - moveName: move.Name, - moveType: move.Type, - moveClass: move.Class, - typeMultiplier, - power: move.Power, - percentHp: (damage / defender.HP) * 100, - attackStat, - defenseStat, - attackStatName, - defenseStatName - }; - } - - // Function to get all moves for a monster - function getMovesForMonster(monsterName) { - return movesData.filter(move => move.Mon === monsterName); - } - - // Function to render the move damage table rows - function renderMoveDamageRows(moves, tableElement) { - const tbody = tableElement.querySelector('tbody'); - tbody.innerHTML = ''; // Clear existing rows - - // Get min/max values for color scaling - const allDamageValues = moves.map(move => move.percentHp); - const minDamage = Math.min(...allDamageValues); - const maxDamage = Math.max(...allDamageValues); - - moves.forEach(move => { - const row = document.createElement('tr'); - - // Highlight rows with percentHp > 90% - if (move.percentHp > 90) { - row.classList.add('high-damage-row'); - } else { - // Add color scaling for normal damage - const getColorIntensity = (value) => { - const normalized = (value - minDamage) / (maxDamage - minDamage); - return Math.floor(normalized * 40); - }; - row.style.backgroundColor = `rgba(255, 99, 71, ${getColorIntensity(move.percentHp)}%)`; - } - - // Get type color - const typeInfo = typeData[move.moveType] || { bgColor: '#333', textColor: '#fff', emoji: '' }; - - // Create calculation formula - const calculationHtml = ` - - (${move.power} Power × ${move.attackStat} ${move.attackStatName}) ÷ ${move.defenseStat} ${move.defenseStatName} × ${move.typeMultiplier} - -
- = ${move.damage.toFixed(1)} -
- `; - - row.innerHTML = ` - ${move.moveName} - - ${typeInfo.emoji} ${move.moveType} - - -
- ${move.attacker} - ${move.attacker} -
- - ${calculationHtml} - ${move.damage.toFixed(1)} - ${move.percentHp.toFixed(1)}% - `; - - tbody.appendChild(row); - }); - } - - function displayAnalysisResults(stats, monsterName) { - const resultsDiv = document.getElementById("analysis-results"); - const monsterNameLower = monsterName.toLowerCase(); - - resultsDiv.innerHTML = ` -
- ${monsterName} -

Analysis for ${monsterName}

-
-
-
- `; - - // Add move damage section if we have move data - if (stats.moveDamage && stats.moveDamage.length > 0) { - const moveDamageSection = document.createElement('div'); - moveDamageSection.className = 'analysis-item move-damage-section'; - moveDamageSection.innerHTML = `

Move Damage Analysis

`; - - // Create a table for move damage - const moveTable = document.createElement('table'); - moveTable.className = 'move-damage-table'; - moveTable.innerHTML = ` - - - Move - Type - From - Calculation - Damage - % HP - - - - `; - - // Add event listeners to sortable headers - moveTable.querySelectorAll('th.sortable').forEach(header => { - header.style.cursor = 'pointer'; - header.addEventListener('click', () => { - const sortKey = header.dataset.sort; - sortMoveDamageTable(stats.moveDamage, sortKey, moveTable); - }); - }); - - // Sort moves by percentHp (highest first) by default - moveDamageColumnToSort = 'percentHp'; - moveDamageDirection = true; - sortMoveDamageTable(stats.moveDamage, moveDamageColumnToSort, moveTable, true); - - // Initial render of the move damage table - renderMoveDamageRows(stats.moveDamage, moveTable); - - moveDamageSection.appendChild(moveTable); - resultsDiv.querySelector('.analysis-grid').appendChild(moveDamageSection); - } - } - - // Function to calculate the greatest %HP damage each monster can deal to every other monster - function calculateMaxDamage() { - const maxDamageData = []; - - // For each attacker - data.forEach((attacker) => { - const attackerData = { - name: attacker.Name, - damages: [] - }; - - // For each defender - data.forEach((defender) => { - // Calculate move damage if available - let maxMoveDamage = 0; - let maxMoveName = ""; - let maxMoveClass = ""; - let maxMoveType = ""; - let maxTypeMultiplier = 0; - const attackerMoves = getMovesForMonster(attacker.Name); - - if (attackerMoves.length > 0) { - attackerMoves.forEach(move => { - const moveDamage = calculateMoveDamage(move, attacker, defender); - if (moveDamage && moveDamage.percentHp > maxMoveDamage) { - maxMoveDamage = moveDamage.percentHp; - maxMoveName = move.Name; - maxMoveClass = move.Class; - maxMoveType = move.Type; - maxTypeMultiplier = moveDamage.typeMultiplier; - } - }); - } - - let greatestDamage = maxMoveDamage; - let damageSource = `${maxMoveName} (${maxMoveClass})`; - - attackerData.damages.push({ - defenderName: defender.Name, - percentHp: greatestDamage, - source: damageSource, - moveType: maxMoveType, - moveClass: maxMoveClass, - typeMultiplier: maxTypeMultiplier - }); - }); - - maxDamageData.push(attackerData); - }); - - return maxDamageData; - } - - // Function to sort the max damage table - function sortMaxDamageTable(columnIndex) { - // Toggle direction if clicking the same column - if (maxDamageColumnToSort === columnIndex) { - maxDamageDirection = !maxDamageDirection; - } else { - maxDamageColumnToSort = columnIndex; - maxDamageDirection = true; // Default to descending (highest damage first) - } - - // Re-render the table with the new sorting - renderMaxDamageTable(); - } - - // Function to sort the move damage table - function sortMoveDamageTable(moves, columnName, tableElement, skipDirectionToggle = false) { - // Toggle direction if clicking the same column - if (moveDamageColumnToSort === columnName && !skipDirectionToggle) { - moveDamageDirection = !moveDamageDirection; - } else if (!skipDirectionToggle) { - moveDamageColumnToSort = columnName; - moveDamageDirection = true; // Default to descending (highest first) - } - - // Update sort indicators in the table headers - tableElement.querySelectorAll('th.sortable').forEach(header => { - const indicator = header.querySelector('.sort-indicator'); - if (header.dataset.sort === moveDamageColumnToSort) { - indicator.textContent = moveDamageDirection ? ' ▼' : ' ▲'; - } else { - indicator.textContent = ''; - } - }); - - // Sort the moves array based on the selected column - moves.sort((a, b) => { - let valueA, valueB; - - // Handle different column types - switch (columnName) { - case 'moveType': - valueA = a.moveType || ''; - valueB = b.moveType || ''; - return moveDamageDirection - ? valueB.localeCompare(valueA) - : valueA.localeCompare(valueB); - - case 'attacker': - valueA = a.attacker || ''; - valueB = b.attacker || ''; - return moveDamageDirection - ? valueB.localeCompare(valueA) - : valueA.localeCompare(valueB); - - case 'damage': - valueA = a.damage || 0; - valueB = b.damage || 0; - return moveDamageDirection - ? valueB - valueA - : valueA - valueB; - - case 'percentHp': - valueA = a.percentHp || 0; - valueB = b.percentHp || 0; - return moveDamageDirection - ? valueB - valueA - : valueA - valueB; - - default: - return 0; - } - }); - - // Re-render the table with the sorted data - renderMoveDamageRows(moves, tableElement); - } - - // Function to render the max damage table - function renderMaxDamageTable() { - if (!data || data.length === 0) return; - - maxDamageTable.innerHTML = ""; - - // Calculate max damage data - const maxDamageData = calculateMaxDamage(); - - // Create header row - const headerRow = document.createElement("tr"); - const cornerCell = document.createElement("th"); - cornerCell.textContent = isTransposed ? "Def ↓ / Atk →" : "Atk ↓ / Def →"; - headerRow.appendChild(cornerCell); - - // Create headers for each monster (columns) - const columnMonsters = isTransposed ? maxDamageData : data; - columnMonsters.forEach((monster, colIndex) => { - const th = document.createElement("th"); - th.style.cursor = "pointer"; - - // Add click handler for sorting - th.addEventListener("click", () => { - sortMaxDamageTable(colIndex); - }); - - const headerContent = document.createElement("div"); - headerContent.style.display = "flex"; - headerContent.style.flexDirection = "column"; - headerContent.style.alignItems = "center"; - headerContent.style.gap = "4px"; - - const img = document.createElement("img"); - const monsterName = isTransposed ? monster.name.toLowerCase() : monster.Name.toLowerCase(); - img.src = `imgs/${monsterName}_mini.gif`; - img.alt = isTransposed ? monster.name : monster.Name; - img.onerror = () => (img.style.display = "none"); - - const nameSpan = document.createElement("span"); - let nameText = isTransposed - ? `⚔️ ${monster.name}` - : `🛡️ ${monster.Name}`; - - // Add sort indicator if this is the sorted column - if (maxDamageColumnToSort === colIndex) { - nameText += maxDamageDirection ? " \u25BC" : " \u25B2"; - } - - nameSpan.textContent = nameText; - - headerContent.appendChild(img); - headerContent.appendChild(nameSpan); - th.appendChild(headerContent); - headerRow.appendChild(th); - }); - - maxDamageTable.appendChild(headerRow); - - // Get min/max values for color scaling - const allDamageValues = []; - maxDamageData.forEach(attacker => { - attacker.damages.forEach(damage => { - allDamageValues.push(damage.percentHp); - }); - }); - - const minDamage = Math.min(...allDamageValues); - const maxDamage = Math.max(...allDamageValues); - - // Create data rows - let rowMonsters = isTransposed ? data : maxDamageData; - - // Sort the rows if a column is selected for sorting - if (maxDamageColumnToSort !== undefined) { - rowMonsters = [...rowMonsters].sort((a, b) => { - let valueA, valueB; - - if (isTransposed) { - // When transposed, we need to find the damage from the specific attacker - const attackerName = maxDamageData[maxDamageColumnToSort].name; - const damageToA = maxDamageData.find(d => d.name === attackerName) - .damages.find(d => d.defenderName === a.Name); - const damageToB = maxDamageData.find(d => d.name === attackerName) - .damages.find(d => d.defenderName === b.Name); - - valueA = damageToA ? damageToA.percentHp : 0; - valueB = damageToB ? damageToB.percentHp : 0; - } else { - // Normal view - just get the damage to the specific defender - valueA = a.damages[maxDamageColumnToSort].percentHp; - valueB = b.damages[maxDamageColumnToSort].percentHp; - } - - return maxDamageDirection ? valueB - valueA : valueA - valueB; - }); - } - - rowMonsters.forEach((rowMonster, rowIndex) => { - const tr = document.createElement("tr"); - - // Row header with image and name - const rowHeader = document.createElement("th"); - const headerContent = document.createElement("div"); - headerContent.style.display = "flex"; - headerContent.style.alignItems = "center"; - headerContent.style.gap = "8px"; - - const img = document.createElement("img"); - const monsterName = isTransposed - ? rowMonster.Name.toLowerCase() - : rowMonster.name.toLowerCase(); - img.src = `imgs/${monsterName}_mini.gif`; - img.alt = isTransposed ? rowMonster.Name : rowMonster.name; - img.onerror = () => (img.style.display = "none"); - - const nameSpan = document.createElement("span"); - nameSpan.textContent = isTransposed - ? `🛡️ ${rowMonster.Name}` - : `⚔️ ${rowMonster.name}`; - - headerContent.appendChild(img); - headerContent.appendChild(nameSpan); - rowHeader.appendChild(headerContent); - tr.appendChild(rowHeader); - - // Create cells for each column - if (isTransposed) { - // Transposed view: rows are defenders, columns are attackers - maxDamageData.forEach((attacker) => { - const damageInfo = attacker.damages.find(d => d.defenderName === rowMonster.Name); - if (damageInfo) { - const td = document.createElement("td"); - // Create a structured display for the damage info - const damageValue = document.createElement("div"); - damageValue.style.fontWeight = "bold"; - damageValue.textContent = `${damageInfo.percentHp.toFixed(1)}%`; - - // Create type badge and multiplier display - const badgeContainer = document.createElement("div"); - badgeContainer.className = "damage-badge-container"; - - // Extract move name from source for better display - const moveName = damageInfo.source.split(' (')[0]; - const moveClass = damageInfo.moveClass || ''; - - // Create move name element with enhanced styling - const moveNameElement = document.createElement("div"); - moveNameElement.className = "move-name-display"; - moveNameElement.textContent = moveName; - - // Add type badge with multiplier if we have move type info - if (damageInfo.moveType) { - const typeInfo = typeData[damageInfo.moveType] || { bgColor: '#333', textColor: '#fff', emoji: '' }; - const typeBadge = document.createElement("span"); - typeBadge.className = "damage-type-badge"; - typeBadge.style.backgroundColor = typeInfo.bgColor; - typeBadge.style.color = typeInfo.textColor; - - // Add emoji and type multiplier together - let badgeContent = typeInfo.emoji; - - // Add multiplier right next to the type - if (damageInfo.typeMultiplier) { - const multiplierSpan = document.createElement("span"); - multiplierSpan.className = "damage-type-multiplier"; - multiplierSpan.textContent = `×${damageInfo.typeMultiplier}`; - typeBadge.appendChild(document.createTextNode(badgeContent)); - typeBadge.appendChild(multiplierSpan); - } else { - typeBadge.textContent = badgeContent; - } - - badgeContainer.appendChild(typeBadge); - } - - // Create source info with class emoji paired with the text - const sourceInfo = document.createElement("div"); - sourceInfo.style.fontSize = "0.85em"; - sourceInfo.style.color = "#aaa"; - - // Add move class with emoji - if (moveClass) { - const classEmoji = moveClass === 'Physical' ? '👊' : - moveClass === 'Special' ? '🌀' : '✨'; - sourceInfo.innerHTML = `${classEmoji} ${moveClass}`; - } else { - sourceInfo.textContent = damageInfo.source.split('(')[1]?.replace(')', '') || ''; - } - - td.appendChild(damageValue); - td.appendChild(moveNameElement); - td.appendChild(badgeContainer); - td.appendChild(sourceInfo); - - // Check if damage is above 90% for special styling - if (damageInfo.percentHp > 90) { - td.classList.add('high-damage-cell'); - } else { - // Add color scaling for normal damage - const getColorIntensity = (value) => { - const normalized = (value - minDamage) / (maxDamage - minDamage); - return Math.floor(normalized * 40); - }; - td.style.backgroundColor = `rgba(255, 99, 71, ${getColorIntensity(damageInfo.percentHp)}%)`; - } - tr.appendChild(td); - } else { - const td = document.createElement("td"); - td.textContent = "N/A"; - tr.appendChild(td); - } - }); - } else { - // Normal view: rows are attackers, columns are defenders - rowMonster.damages.forEach((damageInfo) => { - const td = document.createElement("td"); - // Create a structured display for the damage info - const damageValue = document.createElement("div"); - damageValue.style.fontWeight = "bold"; - damageValue.textContent = `${damageInfo.percentHp.toFixed(1)}%`; - - // Create type badge and multiplier display - const badgeContainer = document.createElement("div"); - badgeContainer.className = "damage-badge-container"; - - // Extract move name from source for better display - const moveName = damageInfo.source.split(' (')[0]; - const moveClass = damageInfo.moveClass || ''; - - // Create move name element with enhanced styling - const moveNameElement = document.createElement("div"); - moveNameElement.className = "move-name-display"; - moveNameElement.textContent = moveName; - - // Add type badge with multiplier if we have move type info - if (damageInfo.moveType) { - const typeInfo = typeData[damageInfo.moveType] || { bgColor: '#333', textColor: '#fff', emoji: '' }; - const typeBadge = document.createElement("span"); - typeBadge.className = "damage-type-badge"; - typeBadge.style.backgroundColor = typeInfo.bgColor; - typeBadge.style.color = typeInfo.textColor; - - // Add emoji and type multiplier together - let badgeContent = typeInfo.emoji; - - // Add multiplier right next to the type - if (damageInfo.typeMultiplier) { - const multiplierSpan = document.createElement("span"); - multiplierSpan.className = "damage-type-multiplier"; - multiplierSpan.textContent = `×${damageInfo.typeMultiplier}`; - typeBadge.appendChild(document.createTextNode(badgeContent)); - typeBadge.appendChild(multiplierSpan); - } else { - typeBadge.textContent = badgeContent; - } - - badgeContainer.appendChild(typeBadge); - } - - // Create source info with class emoji paired with the text - const sourceInfo = document.createElement("div"); - sourceInfo.style.fontSize = "0.85em"; - sourceInfo.style.color = "#aaa"; - - // Add move class with emoji - if (moveClass) { - const classEmoji = moveClass === 'Physical' ? '👊' : - moveClass === 'Special' ? '🌀' : '✨'; - sourceInfo.innerHTML = `${classEmoji} ${moveClass}`; - } else { - sourceInfo.textContent = damageInfo.source.split('(')[1]?.replace(')', '') || ''; - } - - td.appendChild(damageValue); - td.appendChild(moveNameElement); - td.appendChild(badgeContainer); - td.appendChild(sourceInfo); - - // Check if damage is above 90% for special styling - if (damageInfo.percentHp > 90) { - td.classList.add('high-damage-cell'); - } else { - // Add color scaling for normal damage - const getColorIntensity = (value) => { - const normalized = (value - minDamage) / (maxDamage - minDamage); - return Math.floor(normalized * 40); - }; - td.style.backgroundColor = `rgba(255, 99, 71, ${getColorIntensity(damageInfo.percentHp)}%)`; - } - tr.appendChild(td); - }); - } - - maxDamageTable.appendChild(tr); - }); - - // Add some styling to the table - maxDamageTable.style.borderCollapse = "collapse"; - const cells = maxDamageTable.getElementsByTagName("td"); - for (let cell of cells) { - cell.style.padding = "8px 10px"; - cell.style.border = "1px solid #ddd"; - cell.style.textAlign = "center"; - cell.style.minWidth = "80px"; - } - } - - // Check if we need to initialize with existing data - // This is for when the page loads and mon-stats.js has already loaded data - if (window.monsterData && window.monsterData.data && window.monsterData.columns) { - data = window.monsterData.data; - columns = window.monsterData.columns; - updateMonsterSelect(); - renderMaxDamageTable(); - } -}); diff --git a/drool/mon-full.js b/drool/mon-full.js deleted file mode 100644 index 7a3c20c6..00000000 --- a/drool/mon-full.js +++ /dev/null @@ -1,318 +0,0 @@ - -import { loadFullMonsFromCsv, loadMovesFromCsv, loadAbilitiesFromCsv } from "./utils.js"; -import { typeData } from "./type-data.js"; - -document.addEventListener("DOMContentLoaded", async function () { - // Load mon data from CSV - const monsFromCsv = await loadFullMonsFromCsv(); - const movesData = await loadMovesFromCsv(); - const abilitiesData = await loadAbilitiesFromCsv(); - - // Calculate max values for each stat - const maxStats = { - HP: Math.max(...monsFromCsv.map(mon => mon.HP)), - Attack: Math.max(...monsFromCsv.map(mon => mon.Attack)), - Defense: Math.max(...monsFromCsv.map(mon => mon.Defense)), - SpecialAttack: Math.max(...monsFromCsv.map(mon => mon.SpecialAttack)), - SpecialDefense: Math.max(...monsFromCsv.map(mon => mon.SpecialDefense)), - Speed: Math.max(...monsFromCsv.map(mon => mon.Speed)) - }; - - let monIndex = 0; - - // Create all monster elements but hide them initially - createAllMonElements(monsFromCsv, maxStats, movesData); - - // Function to navigate to previous monster - function goToPrevMon() { - monIndex = (monIndex - 1 + monsFromCsv.length) % monsFromCsv.length; - showActiveMon(); - updateUrlHash(); - } - - // Function to navigate to next monster - function goToNextMon() { - monIndex = (monIndex + 1 + monsFromCsv.length) % monsFromCsv.length; - showActiveMon(); - updateUrlHash(); - } - - // Function to navigate to a specific mon by index - function goToMon(index) { - if (index >= 0 && index < monsFromCsv.length) { - monIndex = index; - showActiveMon(); - updateUrlHash(); - } - } - - // Function to update URL hash with current mon index - function updateUrlHash() { - // Only update hash if we're on the mons tab - const monsTabActive = document.querySelector(".tab[data-tab='mons']").classList.contains("active"); - if (monsTabActive) { - window.history.replaceState(null, null, `#mons${monIndex}`); - } - } - - // Bind to prev/next buttons to update index - document.querySelector("#prev-mon-btn").addEventListener("click", goToPrevMon); - document.querySelector("#next-mon-btn").addEventListener("click", goToNextMon); - - // Add keyboard navigation with arrow keys when Guide tab is active - document.addEventListener("keydown", (event) => { - // Only handle arrow keys when the Guide tab is active - const monsTabActive = document.querySelector(".tab[data-tab='mons']").classList.contains("active"); - if (monsTabActive) { - if (event.key === "ArrowLeft") { - goToPrevMon(); - event.preventDefault(); // Prevent scrolling - } else if (event.key === "ArrowRight") { - goToNextMon(); - event.preventDefault(); // Prevent scrolling - } - } - }); - - // Listen for navigate-to-mon events from URL hash changes - document.addEventListener("navigate-to-mon", (event) => { - goToMon(event.detail.index); - }); - - // Check if there's a hash on initial load - const initialHash = window.location.hash.substring(1); - const monsMatch = initialHash.match(/^mons(\d+)$/); - if (monsMatch) { - const initialIndex = parseInt(monsMatch[1], 10); - goToMon(initialIndex); - } else if (!initialHash || initialHash === 'mons') { - // Only show first mon and update hash if no hash or just #mons - showActiveMon(); - updateUrlHash(); - } else { - // For other hashes (like #stats), just show the first mon without updating hash - showActiveMon(); - } - - // Create all monster elements but hide them - function createAllMonElements(mons, maxStats, movesData) { - const nameContainer = document.querySelector("#mon-container-name"); - const statsContainer = document.querySelector("#mon-container-stats"); - const abilitiesMovesContainer = document.querySelector("#mon-container-abilities-moves"); - - // Clear containers - nameContainer.innerHTML = ''; - statsContainer.innerHTML = ''; - abilitiesMovesContainer.innerHTML = ''; - - mons.forEach((mon, idx) => { - const monNameLower = mon.Name.toLowerCase(); - - // Create name element - const nameElement = document.createElement('div'); - nameElement.className = 'mon-name-element'; - nameElement.dataset.index = idx; - nameElement.style.display = 'none'; // Hide initially - nameElement.innerHTML = ` - ${mon.Name} -
${mon.Name}
- `; - - // Create stats element - const statsElement = document.createElement('div'); - statsElement.className = 'mon-stats-element'; - statsElement.dataset.index = idx; - statsElement.style.display = 'none'; // Hide initially - - // Create abilities and moves element - const abilitiesMovesElement = document.createElement('div'); - abilitiesMovesElement.className = 'mon-abilities-moves-element'; - abilitiesMovesElement.dataset.index = idx; - abilitiesMovesElement.style.display = 'none'; // Hide initially - - // Add front and back images - const monImagesHTML = ` -
- ${mon.Name} front - ${mon.Name} back -
- `; - - // Create stats grid - const statsHTML = ` -
-
- HP -
-
-
-
- ${mon.HP} -
-
-
- ATK -
-
-
-
- ${mon.Attack} -
-
-
- 𝞕ATK -
-
-
-
- ${mon.SpecialAttack} -
-
-
- DEF -
-
-
-
- ${mon.Defense} -
-
-
- 𝞕DEF -
-
-
-
- ${mon.SpecialDefense} -
-
-
- SPD -
-
-
-
- ${mon.Speed} -
-
-
- `; - - // Filter abilities for this monster - const monAbilities = abilitiesData ? abilitiesData.filter(ability => ability.Mon === mon.Name) : []; - - // Create abilities section - let abilitiesHTML = ''; - if (monAbilities && monAbilities.length > 0) { - abilitiesHTML = ` -
- ${monAbilities.map(ability => ` -
-
${ability.Name || 'Unknown'}
-
${ability.Effect || 'No effect'}
-
- `).join('')} -
- `; - } - - // Filter moves for this monster - const monMoves = movesData ? movesData.filter(move => move.Mon === mon.Name) : []; - - // Create moves section - let movesHTML = ''; - if (monMoves && monMoves.length > 0) { - movesHTML = ` -
-
- ${monMoves.map(move => { - const moveType = move.Type || ''; - const moveClass = move.Class || ''; - const typeInfo = typeData[moveType] || { bgColor: '#333', textColor: '#fff', emoji: '' }; - - // Determine class styling and emoji based on mon-moves.js - let classStyle = ''; - let classEmoji = ''; - - if (moveClass === 'Physical') { - classStyle = 'background-color: #C92112; color: white;'; - classEmoji = '👊'; // Physical emoji - } else if (moveClass === 'Special') { - classStyle = 'background-color: #4F5870; color: white;'; - classEmoji = '🌀'; // Special emoji - } else if (moveClass === 'Other') { - classStyle = 'background-color: #8C888C; color: white;'; - classEmoji = '✨'; // Other emoji - } else if (moveClass === 'Self') { - classStyle = 'background-color: #8C888C; color: white;'; - classEmoji = '🔄'; // Self emoji - } - - return ` -
-
- ${move.Name || 'Unknown'} - - ${typeInfo.emoji} ${moveType} - -
-
-
- ${classEmoji} ${moveClass} -
-
- ⚔️ ${move.Power || '-'} -
-
- 🔋 ${move.Stamina || '-'} -
-
- 🎯 ${move.Accuracy || '-'}% -
-
- ${move.DevDescription ? `
${move.DevDescription}
` : ''} -
- `; - }).join('')} -
-
- `; - } - - // Set HTML content for each container - statsElement.innerHTML = monImagesHTML + statsHTML; - abilitiesMovesElement.innerHTML = abilitiesHTML + movesHTML; - - // Add elements to containers - nameContainer.appendChild(nameElement); - statsContainer.appendChild(statsElement); - abilitiesMovesContainer.appendChild(abilitiesMovesElement); - }); - } - - // Show only the active monster - function showActiveMon() { - // Hide all elements - document.querySelectorAll('.mon-name-element, .mon-stats-element, .mon-abilities-moves-element').forEach(el => { - el.style.display = 'none'; - }); - - // Show only the active elements - const activeNameEl = document.querySelector(`.mon-name-element[data-index="${monIndex}"]`); - const activeStatsEl = document.querySelector(`.mon-stats-element[data-index="${monIndex}"]`); - const activeAbilitiesMovesEl = document.querySelector(`.mon-abilities-moves-element[data-index="${monIndex}"]`); - - if (activeNameEl) activeNameEl.style.display = 'flex'; - if (activeStatsEl) activeStatsEl.style.display = 'block'; - if (activeAbilitiesMovesEl) activeAbilitiesMovesEl.style.display = 'block'; - } -}); diff --git a/drool/mon-moves.js b/drool/mon-moves.js deleted file mode 100644 index ddbfb3f3..00000000 --- a/drool/mon-moves.js +++ /dev/null @@ -1,690 +0,0 @@ -import { getFS } from "./utils.js"; -import { typeData } from "./type-data.js"; - -document.addEventListener("DOMContentLoaded", function () { - // Init filesystem APIs (if available) - getFS().then(async (fs) => { - // Elements - const movesTable = document.getElementById("moves-table"); - const movesFileInput = document.getElementById("moves-file-input"); - const exportMovesBtn = document.getElementById("export-moves-btn"); - const addMoveBtn = document.getElementById("add-move-btn"); - - // Add event listener for keyboard shortcuts - document.addEventListener("keydown", function(event) { - // Check for Ctrl+S (or Cmd+S on Mac) - if ((event.ctrlKey || event.metaKey) && event.key === 's') { - event.preventDefault(); // Prevent browser's save dialog - - // Check which tab is active - const activeTab = document.querySelector(".tab.active"); - if (activeTab && activeTab.dataset.tab === "moves") { - exportToCsv(); // Save moves data as CSV instead of JS - } - } - }); - - // Add save indicator - const saveIndicator = document.createElement("span"); - saveIndicator.id = "moves-save-indicator"; - saveIndicator.textContent = "●"; - saveIndicator.style.color = "#4CAF50"; - saveIndicator.style.marginLeft = "8px"; - saveIndicator.style.opacity = "0"; - saveIndicator.title = "All changes saved"; - - // Add the indicator next to export buttons - exportMovesBtn.parentElement.appendChild(saveIndicator); - - let hasUnsavedChanges = false; - - function markUnsavedChanges() { - hasUnsavedChanges = true; - saveIndicator.style.opacity = "1"; - saveIndicator.style.color = "#ff9800"; - saveIndicator.title = "Unsaved changes"; - } - - function markChangesSaved() { - hasUnsavedChanges = false; - saveIndicator.style.opacity = "1"; - saveIndicator.style.color = "#4CAF50"; - saveIndicator.title = "All changes saved"; - // Fade out after 2 seconds - setTimeout(() => { - saveIndicator.style.opacity = "0"; - }, 2000); - } - - // Function to notify other components when moves data changes - function notifyMovesDataUpdated() { - // Dispatch custom event - document.dispatchEvent( - new CustomEvent("moves-data-updated", { - detail: { - data: data, - columns: columns - } - }) - ); - } - - // Initialize with empty data - let columns = []; - let data = []; - let monsFromCsv = []; - - // Load mons from csv - await loadMonsFromCsv(); - - // Try to load from moves.csv first using fetch - try { - const response = await fetch('moves.csv'); - if (response.ok) { - const csvContent = await response.text(); - parseCSV(csvContent); - } else { - throw new Error('Failed to fetch CSV'); - } - } catch (error) { - console.log("Could not load moves.csv, using default data", error); - } - - async function loadMonsFromCsv() { - try { - const response = await fetch('mons.csv'); - if (response.ok) { - const csvContent = await response.text(); - const lines = csvContent.split(/\r\n|\n/); - - if (lines.length < 2) return []; - - // Skip header, parse data rows - monsFromCsv = []; - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; - - const values = parseCsvLine(line); - if (values[0]) { // If it has a name - monsFromCsv.push({ Name: values[1] }); - } - } - return monsFromCsv; - } - } catch (error) { - console.error("Error loading mons.csv:", error); - } - } - - // Function to parse CSV content - function parseCSV(csvContent) { - const lines = csvContent.split(/\r\n|\n/); - - // Extract headers - const headers = lines[0].split(",").map(header => header.trim()); - - // Set columns - columns = headers - .map(header => ({ - name: header, - type: ["Power", "Accuracy", "Stamina", "Priority"].includes(header) ? "number" : "text", - editable: true - })); - - // Parse data - data = []; - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; - - const values = parseCsvLine(line); - const rowData = {}; - - // Process all columns - headers.forEach((header, index) => { - const value = values[index]?.trim() || ""; - - // Store all data - rowData[header] = value; - - // Convert number types for UI display, but preserve special values like "?" - if (["Power", "Accuracy", "Stamina", "Priority"].includes(header)) { - if (value === "?" || value === "") { - rowData[header] = value; - } else { - const numValue = parseFloat(value); - rowData[header] = isNaN(numValue) ? 0 : numValue; - } - } - }); - - data.push(rowData); - } - - renderMovesTable(); - } - - // Helper function to properly parse CSV lines with quoted values - function parseCsvLine(line) { - const result = []; - let current = ""; - let inQuotes = false; - - for (let i = 0; i < line.length; i++) { - const char = line[i]; - - if (char === '"') { - if (inQuotes && i + 1 < line.length && line[i + 1] === '"') { - // Double quotes inside quotes - add a single quote - current += '"'; - i++; - } else { - // Toggle quote mode - inQuotes = !inQuotes; - } - } else if (char === ',' && !inQuotes) { - // End of field - result.push(current); - current = ""; - } else { - current += char; - } - } - - // Add the last field - result.push(current); - return result; - } - - // Add sorting variables - let columnToSort = undefined; - let columnDirection = false; // false for ascending, true for descending - - // Event listeners - movesFileInput.addEventListener("change", handleFileImport); - exportMovesBtn.addEventListener("click", exportToCsv); - addMoveBtn.addEventListener("click", addRow); - - // Initialize table - renderMovesTable(); - - // Add header click events for sorting - addHeaderSortEvents(); - - function addHeaderSortEvents() { - const headers = movesTable.querySelectorAll("thead th"); - headers.forEach((header, index) => { - header.style.cursor = "pointer"; - header.onclick = function() { - const columnName = columns[index].name; - // Toggle sort direction if clicking the same column - if (columnToSort === columnName) { - columnDirection = !columnDirection; - } else { - columnToSort = columnName; - columnDirection = false; // default to ascending - } - - // Sort the data - sortData(columnName, !columnDirection); - }; - }); - } - - function sortData(columnName, ascending = true) { - data.sort((a, b) => { - let aValue = a[columnName]; - let bValue = b[columnName]; - - // Handle null/undefined values - if (aValue === undefined || aValue === null) aValue = ""; - if (bValue === undefined || bValue === null) bValue = ""; - - // Check if we're sorting a numeric column - const column = columns.find(col => col.name === columnName); - if (column && column.type === "number") { - // Convert to numbers for numeric comparison - aValue = parseFloat(aValue) || 0; - bValue = parseFloat(bValue) || 0; - return ascending ? aValue - bValue : bValue - aValue; - } else { - // String comparison for text columns - return ascending - ? String(aValue).localeCompare(String(bValue)) - : String(bValue).localeCompare(String(aValue)); - } - }); - - // Re-render the table with sorted data - renderMovesTable(); - markUnsavedChanges(); - } - - function renderMovesTable() { - // Render table headers dynamically - const movesTableHead = movesTable.querySelector("thead tr"); - movesTableHead.innerHTML = ""; - - columns.forEach((column) => { - const th = document.createElement("th"); - th.textContent = column.name; - movesTableHead.appendChild(th); - }); - - const movesTableBody = movesTable.querySelector("tbody"); - movesTableBody.innerHTML = ""; - - // Map to store monster name to hue mapping - const monsterHues = new Map(); - const hueMinDistance = 35; // Minimum distance between hues - - // Create a function to generate distinct colors from monster names - const getMonsterColor = (monsterName) => { - if (!monsterName) return ""; - - // If we've already assigned a hue to this monster, use it - if (monsterHues.has(monsterName)) { - const hue = monsterHues.get(monsterName); - return `hsla(${hue}, 30%, 50%, 0.5)`; - } - - // Generate a new hue for this monster - let hash = 0; - for (let i = 0; i < monsterName.length; i++) { - hash = monsterName.charCodeAt(i) * 23 + ((hash << 17) - hash); - } - hash = Math.abs(hash); - - // Generate initial hue (0-360) - let hue = hash % 360; - - // Check if this hue is too close to existing ones - let attempts = 0; - const usedHues = Array.from(monsterHues.values()); - - // Limit attempts to avoid infinite loops - while (attempts < 5) { - // Check distance to all used hues - const tooClose = usedHues.some(usedHue => { - const distance = Math.min( - Math.abs(hue - usedHue), - 360 - Math.abs(hue - usedHue) - ); - return distance < hueMinDistance; - }); - - if (!tooClose || usedHues.length === 0) { - // This hue is distinct enough or it's the first one - break; - } - - // Try a new hue by rehashing - hash = (hash * 17) + 23; - hue = hash % 360; - attempts++; - } - - // Store the hue for this monster - monsterHues.set(monsterName, hue); - - // Return a subtle HSL color - return `hsla(${hue}, 30%, 50%, 0.5)`; - }; - - data.forEach((row, rowIndex) => { - const tr = document.createElement("tr"); - - // Apply subtle background color based on monster - const monsterName = row["Mon"]; - if (monsterName) { - tr.style.backgroundColor = getMonsterColor(monsterName); - } - - columns.forEach((column) => { - const td = document.createElement("td"); - td.dataset.row = rowIndex; - td.dataset.column = column.name; - - if (column.name === "Mon") { - // Create select dropdown - const select = document.createElement("select"); - select.style.width = "100%"; - select.style.padding = "4px"; - select.style.backgroundColor = "transparent"; - select.style.border = "none"; - select.style.cursor = "pointer"; - select.style.color = "inherit"; - - // Add empty option - const emptyOption = document.createElement("option"); - emptyOption.value = ""; - emptyOption.textContent = "Select monster..."; - select.appendChild(emptyOption); - - // Use the cached monsFromCsv instead of defaultMonsterData - monsFromCsv.forEach(monster => { - if (monster.Name) { - const option = document.createElement("option"); - option.value = monster.Name; - option.textContent = monster.Name; - if (monster.Name === row[column.name]) { - option.selected = true; - } - select.appendChild(option); - } - }); - - // Handle change event - select.addEventListener("change", (e) => { - data[rowIndex][column.name] = e.target.value; - markUnsavedChanges(); - notifyMovesDataUpdated(); - }); - - // Add monster icon next to the select - const container = document.createElement("div"); - container.style.display = "flex"; - container.style.alignItems = "center"; - container.style.gap = "8px"; - - const img = document.createElement("img"); - const monsterName = (row[column.name] || "").toLowerCase(); - img.src = `imgs/${monsterName}_mini.gif`; - img.alt = row[column.name] || ""; - img.style.width = "32x"; - img.style.imageRendering = "pixelated"; - img.onerror = () => img.style.display = "none"; - - // Update image when selection changes - select.addEventListener("change", (e) => { - const newMonsterName = e.target.value.toLowerCase(); - img.src = `imgs/${newMonsterName}_mini.gif`; - img.alt = e.target.value; - img.style.display = ""; // Reset display in case it was hidden - }); - - container.appendChild(img); - container.appendChild(select); - td.appendChild(container); - } else if (column.name === "Type") { - // Create type dropdown - const select = document.createElement("select"); - select.style.width = "100%"; - select.style.padding = "4px"; - select.style.backgroundColor = "transparent"; - select.style.border = "none"; - select.style.cursor = "pointer"; - select.style.color = "inherit"; // Add this line to inherit the td's text color - - // Add empty option - const emptyOption = document.createElement("option"); - emptyOption.value = ""; - emptyOption.textContent = "Select type..."; - select.appendChild(emptyOption); - - // Add type options - Object.entries(typeData).forEach(([type, info]) => { - const option = document.createElement("option"); - option.value = type; - option.textContent = `${info.emoji} ${type}`; - option.style.backgroundColor = info.bgColor; - option.style.color = info.textColor; - if (type === row[column.name]) { - option.selected = true; - td.style.backgroundColor = info.bgColor; - td.style.color = info.textColor; - // Add these lines to set the select's colors to match the selected type - select.style.backgroundColor = info.bgColor; - select.style.color = info.textColor; - } - select.appendChild(option); - }); - - // Handle change event - select.addEventListener("change", (e) => { - const selectedType = e.target.value; - const typeInfo = typeData[selectedType]; - - // Update cell styling - td.style.backgroundColor = selectedType ? typeInfo.bgColor : ""; - td.style.color = selectedType ? typeInfo.textColor : ""; - - // Update data - data[rowIndex][column.name] = selectedType; - markUnsavedChanges(); - notifyMovesDataUpdated(); - }); - - td.appendChild(select); - } else if (column.name === "Class") { - // Create class dropdown - const select = document.createElement("select"); - select.style.width = "100%"; - select.style.padding = "4px"; - select.style.backgroundColor = "transparent"; - select.style.border = "none"; - select.style.cursor = "pointer"; - select.style.color = "inherit"; - - // Add empty option - const emptyOption = document.createElement("option"); - emptyOption.value = ""; - emptyOption.textContent = "Select class..."; - select.appendChild(emptyOption); - - // Add class options with emojis - const classOptions = [ - { value: "Physical", emoji: "👊", bgColor: "#222", textColor: "#eee" }, - { value: "Special", emoji: "🌀", bgColor: "#222", textColor: "#eee" }, - { value: "Other", emoji: "✨", bgColor: "#222", textColor: "#eee" }, - { value: "Self", emoji: "🔄", bgColor: "#222", textColor: "#eee" } - ]; - - classOptions.forEach(option => { - const optElement = document.createElement("option"); - optElement.value = option.value; - optElement.textContent = `${option.emoji} ${option.value}`; - optElement.style.backgroundColor = option.bgColor; - optElement.style.color = option.textColor; - - if (option.value === row[column.name]) { - optElement.selected = true; - td.style.backgroundColor = option.bgColor; - td.style.color = option.textColor; - select.style.backgroundColor = option.bgColor; - select.style.color = option.textColor; - } - - select.appendChild(optElement); - }); - - // Handle change event - select.addEventListener("change", (e) => { - const selectedClass = e.target.value; - const classInfo = classOptions.find(opt => opt.value === selectedClass); - - // Update cell styling - td.style.backgroundColor = selectedClass ? classInfo.bgColor : ""; - td.style.color = selectedClass ? classInfo.textColor : ""; - - // Update data - data[rowIndex][column.name] = selectedClass; - markUnsavedChanges(); - notifyMovesDataUpdated(); - }); - - td.appendChild(select); - } else if (column.name === "DevDescription" || column.name === "UserDescription") { - // Use textarea for description fields to support multi-line editing - const textarea = document.createElement("textarea"); - textarea.style.width = "100%"; - textarea.style.minWidth = "200px"; - textarea.style.minHeight = "60px"; - textarea.style.padding = "4px"; - textarea.style.backgroundColor = "transparent"; - textarea.style.border = "1px solid #444"; - textarea.style.color = "inherit"; - textarea.style.fontFamily = "inherit"; - textarea.style.fontSize = "inherit"; - textarea.style.resize = "vertical"; - textarea.value = row[column.name] || ""; - - textarea.addEventListener("blur", (e) => { - data[rowIndex][column.name] = e.target.value; - markUnsavedChanges(); - notifyMovesDataUpdated(); - }); - - td.appendChild(textarea); - } else { - if (column.editable) { - td.contentEditable = true; - td.className = "editable"; - - if (column.type === "number") { - // Handle special values like "?" for numeric fields - const value = row[column.name]; - if (value === "?" || value === "") { - td.textContent = value; - } else { - td.textContent = value || 0; - } - } else { - td.textContent = row[column.name] || ""; - } - - td.addEventListener("blur", updateData); - } else { - td.textContent = row[column.name] || ""; - } - } - - tr.appendChild(td); - }); - - // Remove the delete button column - // No actionsTd or deleteBtn added here - - movesTableBody.appendChild(tr); - }); - } - - function updateData(event) { - const td = event.target; - const rowIndex = parseInt(td.dataset.row); - const column = td.dataset.column; - const value = td.textContent.trim(); - - if (columns.find(col => col.name === column)?.type === "number") { - // Handle special non-numeric values like "?" for numeric fields - if (value === "?" || value === "") { - data[rowIndex][column] = value; - } else { - const numValue = parseFloat(value); - data[rowIndex][column] = isNaN(numValue) ? 0 : numValue; - } - } else { - data[rowIndex][column] = value; - } - markUnsavedChanges(); - notifyMovesDataUpdated(); - } - - function addRow() { - const newRow = {}; - - columns.forEach((column) => { - if (column.type === "number") { - newRow[column.name] = 0; - } else { - newRow[column.name] = ""; - } - }); - - data.push(newRow); - renderMovesTable(); - markUnsavedChanges(); - notifyMovesDataUpdated(); - } - - function handleFileImport(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function(e) { - const text = e.target.result; - const lines = text.split("\n"); - - if (lines.length < 2) return; - - // Extract headers - const headers = lines[0].split(",").map(header => header.trim()); - - // Reset columns - columns = headers.map(header => ({ - name: header, - type: "text", - editable: true - })); - - // Parse data - data = []; - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; - - const values = line.split(","); - const rowData = {}; - headers.forEach((header, index) => { - const value = values[index]?.trim() || ""; - rowData[header] = columns[index].type === "number" ? parseFloat(value) || 0 : value; - }); - data.push(rowData); - } - - renderMovesTable(); - movesFileInput.value = ""; - notifyMovesDataUpdated(); - }; - reader.readAsText(file); - } - - async function exportToCsv() { - // Get all headers from the first data row - const originalHeaders = Object.keys(data[0] || {}); - const csvHeaders = originalHeaders.join(","); - - const rows = data.map(row => { - return originalHeaders.map(header => { - let value = row[header] !== undefined ? row[header] : ""; - - if (typeof value === "string" && (value.includes(",") || value.includes('"') || value.includes("\n"))) { - value = `"${value.replace(/"/g, '""')}"`; - } - return value; - }).join(","); - }); - - const csvContent = [csvHeaders, ...rows].join("\n"); - - try { - await fs.writeTextFile("moves.csv", csvContent, { - dir: fs.BaseDirectory.Runtime, - }); - } catch (error) { - // Web fallback - const blob = new Blob([csvContent], { type: "text/csv" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "moves-export.csv"; - a.click(); - window.URL.revokeObjectURL(url); - } - markChangesSaved(); - notifyMovesDataUpdated(); - } - }); -}); diff --git a/drool/mon-stats.js b/drool/mon-stats.js deleted file mode 100644 index f35ec810..00000000 --- a/drool/mon-stats.js +++ /dev/null @@ -1,683 +0,0 @@ -import { typeData } from "./type-data.js"; -import { getFS } from "./utils.js"; - -// Create a global variable to store monster data for other modules to access -window.monsterData = { - data: [], - columns: [] -}; - -document.addEventListener("DOMContentLoaded", function () { - // Init filesystem APIs (if available) - getFS().then(async (fs) => { - // Elements - const dataTable = document.getElementById("data-table"); - const importBtn = document.getElementById("import-btn"); - const fileInput = document.getElementById("file-input"); - const addRowBtn = document.getElementById("add-row-btn"); - const exportBtn = document.getElementById("export-btn"); - const exportJsBtn = document.getElementById("export-js-btn"); - const tabs = document.querySelectorAll(".tab"); - const tabContents = document.querySelectorAll(".tab-content"); - const mobileTabsSelect = document.getElementById("mobile-tabs"); - - // Add this near other element declarations - const saveIndicator = document.createElement("span"); - saveIndicator.id = "save-indicator"; - saveIndicator.textContent = "●"; - saveIndicator.style.color = "#4CAF50"; - saveIndicator.style.marginLeft = "8px"; - saveIndicator.style.opacity = "0"; - saveIndicator.title = "All changes saved"; - - // Add the indicator next to export buttons - exportBtn.parentElement.appendChild(saveIndicator); - - let hasUnsavedChanges = false; - - function markUnsavedChanges() { - hasUnsavedChanges = true; - saveIndicator.style.opacity = "1"; - saveIndicator.style.color = "#ff9800"; - saveIndicator.title = "Unsaved changes"; - } - - function markChangesSaved() { - hasUnsavedChanges = false; - saveIndicator.style.opacity = "1"; - saveIndicator.style.color = "#4CAF50"; - saveIndicator.title = "All changes saved"; - // Fade out after 2 seconds - setTimeout(() => { - saveIndicator.style.opacity = "0"; - }, 2000); - } - - // Function to notify other components when monster data changes - function notifyDataUpdated() { - // Update global variable - window.monsterData = { - data: data, - columns: columns - }; - - // Dispatch custom event - document.dispatchEvent( - new CustomEvent("monster-data-updated", { - detail: { - data: data, - columns: columns - } - }) - ); - } - - let columns = []; - let data = []; - - // Try to load from mons.csv first using fetch - try { - const response = await fetch('mons.csv'); - if (response.ok) { - const csvContent = await response.text(); - parseCSV(csvContent); - } else { - throw new Error('Failed to fetch CSV'); - } - } catch (error) { - console.log("Could not load mons.csv, using default data", error); - // Still notify with empty data - notifyDataUpdated(); - } - - // Sort vars - let columnToSort = undefined; - let columnDirection = false; - - // Event listeners - importBtn.addEventListener("click", () => fileInput.click()); - fileInput.addEventListener("change", handleFileImport); - addRowBtn.addEventListener("click", addRow); - exportBtn.addEventListener("click", exportToCsv); - exportJsBtn.addEventListener("click", exportToJs); - - // Tab navigation - tabs.forEach((tab) => { - tab.addEventListener("click", () => { - const tabId = tab.dataset.tab; - activateTab(tabId); - let hashId = tabId; - // Update URL hash without triggering a page reload - window.history.pushState(null, null, `#${hashId}`); - - // Update mobile dropdown to match selected tab - if (mobileTabsSelect) { - mobileTabsSelect.value = tabId; - } - }); - }); - - // Mobile tabs dropdown navigation - if (mobileTabsSelect) { - mobileTabsSelect.addEventListener("change", () => { - const tabId = mobileTabsSelect.value; - activateTab(tabId); - - // Special case for data tab - use #stats in URL - let hashId = tabId; - - // Update URL hash without triggering a page reload - window.history.pushState(null, null, `#${hashId}`); - }); - } - - // Function to activate a specific tab - function activateTab(tabId) { - // Remove active class from all tabs and contents - tabs.forEach((t) => t.classList.remove("active")); - tabContents.forEach((c) => c.classList.remove("active")); - - // Add active class to selected tab and content - const selectedTab = document.querySelector(`.tab[data-tab="${tabId}"]`); - - // Handle special case for data tab which now has id="stats" - let contentId = tabId; - - const selectedContent = document.getElementById(contentId); - - if (selectedTab && selectedContent) { - selectedTab.classList.add("active"); - selectedContent.classList.add("active"); - } - } - - // Check URL hash on page load - function checkUrlHash() { - const hash = window.location.hash.substring(1); // Remove the # symbol - - // Handle special case for stats hash which should activate data tab - if (hash === "stats") { - activateTab("stats"); - // Update mobile dropdown to match - if (mobileTabsSelect) { - mobileTabsSelect.value = "stats"; - } - return; - } - - // Handle mons with index pattern (e.g., #mons0, #mons1) - const monsMatch = hash.match(/^mons(\d+)$/); - if (monsMatch) { - const monIndex = parseInt(monsMatch[1], 10); - activateTab("mons"); - // Update mobile dropdown to match - if (mobileTabsSelect) { - mobileTabsSelect.value = "mons"; - } - // Dispatch event to navigate to specific mon - document.dispatchEvent(new CustomEvent("navigate-to-mon", { detail: { index: monIndex } })); - return; - } - - if (hash && document.getElementById(hash)) { - activateTab(hash); - // Update mobile dropdown to match - if (mobileTabsSelect) { - mobileTabsSelect.value = hash; - } - } - } - - // Listen for hash changes (browser back/forward buttons) - window.addEventListener("hashchange", checkUrlHash); - - // Initialize tables first, then check hash - renderDataTable(); - notifyDataUpdated(); // Notify other components about initial data - - // Check hash when page loads (after rendering) - checkUrlHash(); - - // Add event listener for keyboard shortcuts - document.addEventListener("keydown", function(event) { - // Check for Ctrl+S (or Cmd+S on Mac) - if ((event.ctrlKey || event.metaKey) && event.key === 's') { - event.preventDefault(); // Prevent browser's save dialog - - // Check which tab is active - const activeTab = document.querySelector(".tab.active"); - - if (activeTab && activeTab.dataset.tab === "stats") { - exportToCsv(); // Save monster data - } - } - }); - - // Functions - function renderDataTable() { - const tbody = dataTable.querySelector("tbody"); - tbody.innerHTML = ""; - - // Update headers first - updateTableHeaders(); - - // Render data rows - data.forEach((row, rowIndex) => { - const tr = document.createElement("tr"); - - // Add image cell before other columns - const imgTd = document.createElement("td"); - const img = document.createElement("img"); - const monsterName = (row.Name || "").toLowerCase(); - img.src = `imgs/${monsterName}_mini.gif`; - img.alt = row.Name || ""; - img.onerror = () => (img.style.display = "none"); // Hide if image not found - imgTd.appendChild(img); - tr.appendChild(imgTd); - - columns.forEach((column) => { - const td = document.createElement("td"); - - // Always add data attributes regardless of editability - td.dataset.row = rowIndex; - td.dataset.column = column.name; - - if (column.name === "Type1" || column.name === "Type2") { - // Create type dropdown - const select = document.createElement("select"); - select.style.width = "100%"; - select.style.padding = "4px"; - select.style.backgroundColor = "transparent"; - select.style.border = "none"; - select.style.cursor = "pointer"; - select.style.color = "inherit"; - - // Add empty option - const emptyOption = document.createElement("option"); - emptyOption.value = ""; - emptyOption.textContent = "Select type..."; - select.appendChild(emptyOption); - - // Add type options - Object.entries(typeData).forEach(([type, info]) => { - const option = document.createElement("option"); - option.value = type; - option.textContent = `${info.emoji} ${type}`; - option.style.backgroundColor = info.bgColor; - option.style.color = info.textColor; - if (type === row[column.name]) { - option.selected = true; - td.style.backgroundColor = info.bgColor; - td.style.color = info.textColor; - select.style.backgroundColor = info.bgColor; - select.style.color = info.textColor; - } - select.appendChild(option); - }); - - // Handle change event - select.addEventListener("change", (e) => { - const selectedType = e.target.value; - const typeInfo = typeData[selectedType]; - - // Update cell styling - td.style.backgroundColor = selectedType ? typeInfo.bgColor : ""; - td.style.color = selectedType ? typeInfo.textColor : ""; - select.style.backgroundColor = selectedType ? typeInfo.bgColor : "transparent"; - select.style.color = selectedType ? typeInfo.textColor : "inherit"; - - // Update data - data[rowIndex][column.name] = selectedType; - }); - - td.appendChild(select); - } else if (column.name === "Flavor") { - // Special handling for Flavor field - make it a textarea for multi-line text - const textarea = document.createElement("textarea"); - textarea.style.width = "100%"; - textarea.style.minHeight = "60px"; - textarea.style.padding = "4px"; - textarea.style.backgroundColor = "transparent"; - textarea.style.border = "1px solid #444"; - textarea.style.color = "inherit"; - textarea.style.fontFamily = "inherit"; - textarea.style.fontSize = "inherit"; - textarea.style.resize = "vertical"; - textarea.value = row[column.name] || ""; - - textarea.addEventListener("blur", (e) => { - data[rowIndex][column.name] = e.target.value; - markUnsavedChanges(); - notifyDataUpdated(); - }); - - td.appendChild(textarea); - } else if (column.editable) { - td.contentEditable = true; - td.className = "editable"; - - if (column.type === "number") { - td.textContent = row[column.name] || 0; - td.addEventListener("input", validateNumberInput); - } else { - td.textContent = row[column.name] || ""; - } - - td.addEventListener("blur", updateData); - } else { - td.textContent = row[column.name] || ""; - } - - tr.appendChild(td); - }); - - // Add action buttons - const actionsTd = document.createElement("td"); - const deleteBtn = document.createElement("button"); - deleteBtn.textContent = "❌"; - deleteBtn.style.backgroundColor = "#111"; - deleteBtn.addEventListener("click", () => { - deleteRow(rowIndex); - }); - - actionsTd.appendChild(deleteBtn); - tr.appendChild(actionsTd); - - tbody.appendChild(tr); - }); - - updateStatsHeatmap(); - } - - function updateTableHeaders() { - const headerRow = dataTable.querySelector("thead tr"); - headerRow.innerHTML = ""; - - // Add image column header - const imgHeader = document.createElement("th"); - imgHeader.textContent = "Icon"; - headerRow.appendChild(imgHeader); - - columns.forEach((column) => { - const th = document.createElement("th"); - th.className = "column-header"; - - const headerContent = document.createElement("div"); - headerContent.style.display = "flex"; - headerContent.style.alignItems = "center"; - headerContent.style.gap = "5px"; - - const headerText = document.createElement("span"); - headerText.textContent = column.name; - headerContent.appendChild(headerText); - - if (column.type === "number") { - const sortIndicator = document.createElement("span"); - sortIndicator.className = "sort-indicator"; - sortIndicator.innerHTML = ""; - - headerContent.addEventListener("click", () => { - if (columnToSort === column.name && columnDirection === true) { - columnToSort = column.name; - columnDirection = false; - sortData(columnToSort, false); - } else { - columnToSort = column.name; - columnDirection = true; - sortData(columnToSort, true); - } - }); - - headerContent.appendChild(sortIndicator); - } - - th.appendChild(headerContent); - headerRow.appendChild(th); - }); - - const actionsHeader = document.createElement("th"); - actionsHeader.textContent = ""; - headerRow.appendChild(actionsHeader); - } - - function validateNumberInput(event) { - const value = event.target.textContent; - - if (value !== "" && isNaN(parseFloat(value))) { - event.target.classList.add("error"); - } else { - event.target.classList.remove("error"); - } - } - - function updateData(event) { - const cell = event.target; - const rowIndex = parseInt(cell.dataset.row); - const columnName = cell.dataset.column; - const value = cell.textContent.trim(); - - if (!isNaN(rowIndex) && columnName) { - const column = columns.find((col) => col.name === columnName); - - if (column.type === "number") { - if (value === "" || isNaN(parseFloat(value))) { - data[rowIndex][columnName] = 0; - cell.textContent = "0"; - } else { - data[rowIndex][columnName] = parseFloat(value); - } - } else { - data[rowIndex][columnName] = value; - } - markUnsavedChanges(); - notifyDataUpdated(); - } - } - - function addRow() { - const newRow = {}; - - columns.forEach((column) => { - if (column.type === "number") { - newRow[column.name] = 0; - } else { - newRow[column.name] = ""; - } - }); - - data.push(newRow); - renderDataTable(); - notifyDataUpdated(); - } - - function deleteRow(rowIndex) { - data.splice(rowIndex, 1); - renderDataTable(); - notifyDataUpdated(); - } - - function handleFileImport(event) { - const file = event.target.files[0]; - - if (file) { - const reader = new FileReader(); - - reader.onload = function (e) { - const content = e.target.result; - parseCSV(content); - }; - - reader.readAsText(file); - } - } - - function parseCSV(csvContent) { - const lines = csvContent.split(/\r\n|\n/); - - if (lines.length < 2) { - return; - } - - // Parse CSV with proper handling of quoted fields - function parseCSVLine(line) { - const result = []; - let current = ''; - let inQuotes = false; - - for (let i = 0; i < line.length; i++) { - const char = line[i]; - - if (char === '"') { - if (inQuotes && line[i + 1] === '"') { - // Escaped quote - current += '"'; - i++; // Skip next quote - } else { - // Toggle quote state - inQuotes = !inQuotes; - } - } else if (char === ',' && !inQuotes) { - // Field separator - result.push(current.trim()); - current = ''; - } else { - current += char; - } - } - - // Add the last field - result.push(current.trim()); - return result; - } - - // Extract headers - const headers = parseCSVLine(lines[0]); - - // Reset columns - columns = []; - headers.forEach((header) => { - // Try to determine if it's likely a number column - const isLikelyNumber = [ - "hp", - "attack", - "defense", - "specialattack", - "specialdefense", - "speed", - "id" - ].includes(header.toLowerCase()); - - columns.push({ - name: header, - type: isLikelyNumber ? "number" : "text", - editable: true, - }); - }); - - // Parse data rows - data = []; - for (let i = 1; i < lines.length; i++) { - if (lines[i].trim() === "") continue; - - const values = parseCSVLine(lines[i]); - const rowData = {}; - - headers.forEach((header, index) => { - const value = values[index] || ""; - - if (columns[index].type === "number") { - rowData[header] = value !== "" ? parseFloat(value) : 0; - } else { - rowData[header] = value; - } - }); - - data.push(rowData); - } - - renderDataTable(); - notifyDataUpdated(); - - fileInput.value = ""; - } - - async function exportToCsv() { - const headers = columns.map((col) => col.name).join(","); - const rows = data.map((row) => { - return columns - .map((col) => { - let value = row[col.name] !== undefined ? row[col.name] : ""; - // Always quote the Flavor field or if the value contains special characters - if ( - col.name === "Flavor" || - (typeof value === "string" && - (value.includes(",") || - value.includes('"') || - value.includes("\n") || - value.includes("\r"))) - ) { - value = `"${String(value).replace(/"/g, '""')}"`; - } - return value; - }) - .join(","); - }); - - const csvContent = [headers, ...rows].join("\n"); - - try { - await fs.writeTextFile("mons.csv", csvContent, { - dir: fs.BaseDirectory.Runtime, - }); - } catch (error) { - // Web fallback - const blob = new Blob([csvContent], { type: "text/csv" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "mons.csv"; - a.click(); - window.URL.revokeObjectURL(url); - } - markChangesSaved(); - notifyDataUpdated(); - } - - async function exportToJs() { - if (data.length === 0) return; - - const jsContent = `export const defaultMonsterData = { - columns: ${JSON.stringify(columns, null, 2)}, - data: ${JSON.stringify(data, null, 2)} - };`; - - try { - await fs.writeTextFile("default-data.js", jsContent, { - dir: fs.BaseDirectory.Runtime, - }); - } catch (error) { - // Web fallback - const blob = new Blob([jsContent], { type: "application/javascript" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "default-data.js"; - a.click(); - window.URL.revokeObjectURL(url); - } - markChangesSaved(); - notifyDataUpdated(); - } - - function applyCellHeatmap(cell, value, minValue, maxValue) { - const normalized = (value - minValue) / (maxValue - minValue); - const intensity = Math.floor(normalized * 40); // Using 40 for very light colors - cell.style.backgroundColor = `rgba(65, 105, 225, ${intensity}%)`; - } - - function updateStatsHeatmap() { - // Get all numeric columns - const numericColumns = columns.filter( - (col) => col.type === "number" - ); - - // Calculate min/max for each column - numericColumns.forEach((column) => { - const values = data.map((row) => parseFloat(row[column.name]) || 0); - const minValue = Math.min(...values); - const maxValue = Math.max(...values); - - // Apply heatmap to each cell in the column - data.forEach((_, rowIndex) => { - const cell = document.querySelector( - `td[data-row="${rowIndex}"][data-column="${column.name}"]` - ); - if (cell) { - const value = parseFloat(cell.textContent) || 0; - applyCellHeatmap(cell, value, minValue, maxValue); - } - }); - }); - } - - function sortData(columnName, ascending = true) { - data.sort((a, b) => { - const aValue = a[columnName]; - const bValue = b[columnName]; - - if (columns.find((col) => col.name === columnName).type === "number") { - return ascending - ? (parseFloat(aValue) || 0) - (parseFloat(bValue) || 0) - : (parseFloat(bValue) || 0) - (parseFloat(aValue) || 0); - } else { - return ascending - ? String(aValue).localeCompare(String(bValue)) - : String(bValue).localeCompare(String(aValue)); - } - }); - - renderDataTable(); - updateStatsHeatmap(); - } - }); -}); diff --git a/drool/mons.csv b/drool/mons.csv index 5af854ea..a4c7eba4 100644 --- a/drool/mons.csv +++ b/drool/mons.csv @@ -12,5 +12,5 @@ Id,Name,HP,Attack,Defense,SpecialAttack,SpecialDefense,Speed,Type1,Type2,Flavor 10,Xmon,311,123,179,222,185,285,Cosmic,NA,"bGJoIHBuYSBmcnIgdmcgdmEgbGJoZSBxZXJuemY=" 11,Ekineki,299,130,180,280,175,266,Liquid,NA,"Born from a single drop of water." 12,Nirvamma,373,202,168,140,202,177,Math,NA,"Supposedly watches the entire universe." -13,Glob,280,300,205,115,185,140,Liquid,Faith,"PLACEHOLDER" -14,Mulong,320,115,135,290,145,300,Air,Lightning,"PLACEHOLDER" +13,Glob,301,180,201,115,191,151,Liquid,Faith,"A very hardy creature. Found everywhere." +14,Mulong,388,115,188,288,138,212,Air,Lightning,"History's Strongest." diff --git a/drool/moves.csv b/drool/moves.csv index 336ff4db..d9d78933 100644 --- a/drool/moves.csv +++ b/drool/moves.csv @@ -22,7 +22,7 @@ Federal Investigation,Malalien,100,3,100,0,Cyber,Special,Deals damage.,,,none,0, Negative Thoughts,Malalien,80,3,100,0,Math,Special,"Deals damage, {EFFECT_ACCURACY} chance to cause Panic.",,,none,0,EFFECT_ACCURACY=10,opponent-slot Infinite Love,Malalien,90,3,100,0,Cosmic,Special,"Deals damage, {EFFECT_ACCURACY} chance to cause Sleep.",,,none,0,EFFECT_ACCURACY=10,opponent-slot Foul Language,Malalien,60,2,100,0,Cyber,Special,Deals damage. Half of the damage done is taken as recoil.,,,none,6,,opponent-slot -Chill Out,Pengym,0,0,100,0,Ice,Other,Inflicts Frostbite on the opponent.,,,none,0,,opponent-slot +Chill Out,Pengym,0,0,100,0,Ice,Other,Inflicts Frostbite on the opponent.,,,none,0,EFFECT_ACCURACY=100,opponent-slot Deadlift,Pengym,0,2,100,0,Metal,Self,Increases ATK and DEF by {ATTACK_BUFF_PERCENT}.,,,none,0,ATTACK_BUFF_PERCENT=50;DEF_BUFF_PERCENT=50,self-only Deep Freeze,Pengym,90,3,100,0,Ice,Physical,"If the target has Frostbite, consumes Frostbite and does double damage (the target's SpATK returns to normal).",,,none,0,,opponent-slot Pistol Squat,Pengym,80,2,100,-1,Metal,Physical,"Deals damage, then forces the enemy mon to switch to a random living teammate.",,,none,0,,opponent-slot @@ -57,11 +57,11 @@ Hard Reset,Nirvamma,0,2,100,0,Math,Other,"The next time your team rests, the res Scary Numbers,Nirvamma,80,3,100,0,Math,Physical,"Deals damage, {EFFECT_ACCURACY} chance to inflict Panic.",,,none,0,EFFECT_ACCURACY=20,opponent-slot Chronoffense,Nirvamma,?,2,100,0,Math,Physical,"First use boosts defenses. Second use deals damage based on how much time has passed, and then resets.",,,none,0,,opponent-slot Modal Bolt,Nirvamma,90,3,100,0,Math,Physical,"Choose between Fire, Ice, or Lightning each use. Each mode is usable once, and applies its corresponding status (Burn / Frostbite / Zap) at {EFFECT_ACCURACY} chance.",,,mode-select,0,EFFECT_ACCURACY=50,opponent-slot -Holy Molt,Glob,0,?,100,1,Faith,Self,"Loses half of current HP and applies Blessed to self. Free on the first cast; each later cast costs 1 more stamina, up to {MAX_STAMINA_COST}.",,,none,0,HP_DENOM=2;MAX_STAMINA_COST=3,self-only +Holy Molt,Glob,0,?,100,1,Faith,Self,"Spend half of current health to apply Bless to self. Stamina cost goes up per use, to a max of {MAX_STAMINA_COST}.",,,none,0,HP_DENOM=2;MAX_STAMINA_COST=3,self-only Water Claw,Glob,80,2,100,0,Liquid,Physical,Deals damage.,,,none,0,,opponent-slot Holy Water Claw,Glob,90,3,100,0,Faith,Physical,Deals damage.,,,none,0,,opponent-slot Get Slippery,Glob,0,2,100,0,Liquid,Self,Boosts ATK and Speed by {ATTACK_BUFF_PERCENT}.,,,none,0,ATTACK_BUFF_PERCENT=50;SPEED_BUFF_PERCENT=50,self-only -Summon Storm,Mulong,0,0,100,0,Lightning,Other,"Summons a Storm for 7 rounds that damages every active mon at end of round with Lightning damage scaled off the summoner's stats. Recasting re-latches the Storm to the new caster and resets its timer.",,,none,0,,none -King's Respite,Mulong,120,2,100,0,Air,Special,"Marks the chosen slot. The next time Mulong rests, it deals heavy Air damage to that slot; the rest still restores stamina as normal.",,,none,0,BASE_POWER=120,opponent-slot -Thunderclap,Mulong,120,3,100,0,Lightning,Special,"Deals damage, 20% chance to inflict Zap.",,,none,0,,opponent-slot -Cloud Strike,Mulong,?,2,100,0,Ice,Special,"Deals damage, {EFFECT_ACCURACY} chance to inflict Frostbite. While a Storm is active, power rises to {STORM_BASE_POWER} and the chance to {STORM_EFFECT_ACCURACY}.",,,none,0,BASE_POWER=70;STORM_BASE_POWER=100;EFFECT_ACCURACY=10;STORM_EFFECT_ACCURACY=30,opponent-slot +Summon Storm,Mulong,0,0,100,0,Lightning,Other,"Summons a Storm for 7 turns that damages all active mons.",,,none,0,,none +King's Respite,Mulong,120,2,100,0,Air,Special,"The next time Mulong rests, it deals damage to the target.",,,none,0,BASE_POWER=120,opponent-slot +Thunderclap,Mulong,120,3,100,0,Lightning,Special,"Deals damage, {EFFECT_ACCURACY} chance to inflict Zap.",,,none,0,EFFECT_ACCURACY=20,opponent-slot +Cloud Strike,Mulong,?,2,100,0,Ice,Special,"Deals damage, {EFFECT_ACCURACY} chance to inflict Frostbite. While Storm is active, power rises to {STORM_BASE_POWER} and the chance to {STORM_EFFECT_ACCURACY}.",,,none,0,BASE_POWER=70;STORM_BASE_POWER=100;EFFECT_ACCURACY=10;STORM_EFFECT_ACCURACY=30,opponent-slot diff --git a/drool/style.css b/drool/style.css deleted file mode 100644 index e3d5487f..00000000 --- a/drool/style.css +++ /dev/null @@ -1,943 +0,0 @@ -:root { - /* Referenced by Lightning's bg in type-metadata.csv */ - --text-warm: #FFD700; -} -body { - font-family: Arial, sans-serif; - padding: 10px; - margin: 0 auto; - background-color: #1a1a1a; - color: #e0e0e0; -} -.container { - display: flex; - flex-direction: column; - gap: 20px; -} -.controls { - display: flex; - gap: 10px; - margin-bottom: 20px; - flex-wrap: wrap; -} -button { - padding: 8px 12px; - background-color: #2c5a2e; - color: #e0e0e0; - border: none; - border-radius: 4px; - cursor: pointer; -} -button:hover { - background-color: #367238; -} -.file-input { - display: none; -} -table { - border-collapse: collapse; - width: 100%; - margin-bottom: 20px; - overflow-x: auto; -} -th, -td { - border: 1px solid #404040; - padding: 4px 6px; - text-align: left; -} -th { - background-color: #2d2d2d; - position: sticky; - top: 0; -} -tr:nth-child(even) { - background-color: #252525; -} -.editable:hover { - background-color: #2a3744; -} -.editable:focus { - background-color: #1c2834; - outline: none; -} -.tabs { - display: flex; - margin-bottom: 10px; -} - -/* Mobile tabs dropdown styling */ -.mobile-tabs-container { - display: none; - margin-bottom: 15px; -} - -.mobile-tabs-select { - width: 100%; - padding: 10px; - background-color: #2d2d2d; - color: #e0e0e0; - border: 1px solid #404040; - border-radius: 4px; - font-size: 1rem; - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: right 10px center; -} -.tab { - padding: 8px 16px; - background-color: #2d2d2d; - border: 1px solid #404040; - border-bottom: none; - cursor: pointer; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - margin-right: 4px; -} -.tab.active { - background-color: #1a1a1a; - border-bottom: 1px solid #1a1a1a; - margin-bottom: -1px; - z-index: 1; -} -.tab-content { - display: none; - border: 1px solid #404040; - padding: 15px; - overflow-x: auto; - position: relative; -} -.tab-content.active { - display: block; -} -.results-container { - overflow-x: auto; - margin-bottom: 20px; -} -.highlight { - background-color: #3d3c2f !important; -} -.column-header { - cursor: default; - user-select: none; - position: relative; -} - -.column-header span { - cursor: pointer; -} - -.column-header span:hover { - opacity: 0.7; -} -#message { - padding: 10px; - margin-bottom: 10px; - display: none; -} -#data-table img { - width: 32px; - vertical-align: middle; - image-rendering: pixelated; -} - - - -.damage-controls { - margin-bottom: 10px; -} - -#view-perspective { - font-size: 0.9em; - color: #aaa; - font-style: italic; -} - -.damage-controls label { - margin-right: 15px; - cursor: pointer; -} - -.damage-controls input[type="checkbox"] { - margin-right: 5px; -} - -.column-hidden { - display: none; -} - -.analysis-section { - margin-bottom: 20px; - padding: 15px; - background: #2d2d2d; - border-radius: 5px; - border: 1px solid #404040; -} - -#monster-select { - width: 200px; - padding: 8px; - margin-bottom: 15px; - background-color: #1a1a1a; - color: #e0e0e0; - border: 1px solid #404040; - border-radius: 4px; -} - -#monster-select option { - background-color: #1a1a1a; - color: #e0e0e0; -} - -.analysis-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - margin-top: 15px; -} - -.analysis-item { - background: #252525; - padding: 15px; - border-radius: 5px; - border: 1px solid #404040; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); -} - -.analysis-item h4 { - margin-top: 0; - color: #e0e0e0; - border-bottom: 2px solid #404040; - padding-bottom: 5px; - margin-bottom: 10px; -} - -.analysis-item p { - margin: 5px 0; - color: #b0b0b0; -} - -.monster-select-option { - display: flex; - align-items: center; - gap: 8px; -} - -.monster-select-option img { - width: 32px; - height: 32px; - image-rendering: pixelated; -} - -.analysis-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 15px; -} - -.analysis-header img { - width: 32px; - height: 32px; - image-rendering: pixelated; -} - -#moves-table select { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - font-family: inherit; - font-size: inherit; -} - -#moves-table select:focus { - outline: none; -} - -#moves-table select option { - padding: 4px; -} - -#moves-table td { - transition: background-color 0.3s, color 0.3s; -} - -#data-table select { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - font-family: inherit; - font-size: inherit; -} - -#data-table select:focus { - outline: none; -} - -#data-table select option { - padding: 4px; -} - -#data-table td { - transition: background-color 0.3s, color 0.3s; -} - -.main-btn { - background-color: #365d72; -} - -.main-btn:hover { - background-color: #40738c; -} -#save-indicator, -#moves-save-indicator, -#types-save-indicator { - transition: all 0.3s ease; - font-size: 16px; - vertical-align: middle; - user-select: none; -} - -/* Specific column widths */ -#moves-table th:nth-child(1), -#moves-table td:nth-child(1) { /* Name column */ - min-width: 150px; -} - -#moves-table th:nth-child(2), -#moves-table td:nth-child(2) { /* Mon column */ - min-width: 150px; -} - -#moves-table th:nth-child(3), -#moves-table td:nth-child(3) { /* Power column */ - min-width: 50px; -} - -#moves-table th:nth-child(4), -#moves-table td:nth-child(4) { /* Stamina column */ - min-width: 50px; -} - -#moves-table th:nth-child(5), -#moves-table td:nth-child(5) { /* Accuracy column */ - min-width: 50px; -} - -#moves-table th:nth-child(6), -#moves-table td:nth-child(6) { /* Priority column */ - min-width: 50px; -} - -#moves-table th:nth-child(7), -#moves-table td:nth-child(7) { /* Type column */ - min-width: 120px; -} - -#moves-table th:nth-child(8), -#moves-table td:nth-child(8) { /* Class column */ - min-width: 100px; -} - -#moves-table th:nth-child(9), -#moves-table td:nth-child(9) { /* Description column */ - min-width: 300px; -} - -.sticky-header { - position: sticky; - top: 0; - z-index: 100; - background-color: #1a1a1a; - padding: 10px 0; - margin-bottom: 10px; - border-bottom: 1px solid #404040; -} - -#add-move-btn { - margin-bottom: 1rem; -} - -.table-container { - overflow-y: visible; - max-height: none; -} - -.mon-nav-holder { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.keyboard-hint { - color: #888; - font-size: 0.85rem; - margin-left: 0.5rem; -} - -#mon-container-name { - font-size: 1.25rem; - font-weight: bold; - margin-top: 1rem; -} - -.mon-name-element { - display: flex; - align-items: center; - gap: 0.2rem; -} - -.stats-grid { - max-width: 20rem; - font-size: 1rem; - margin-top: 1rem; -} - -.hp-bar { - background-color: #458b45; -} - -.attack-bar { - background-color: #8b4545; -} - -.special-attack-bar { - background-color: #6e458b; -} - -.defense-bar { - background-color: #45728b; -} - -.special-defense-bar { - background-color: #577b5c; -} - -.speed-bar { - background-color: #8b8245; -} - -.stat-value { - text-align: right; - font-family: monospace; -} - -.stat-bar-container { - display: flex; - align-items: center; - gap: 0.5rem; - flex: 1; -} - -.stat-bar-background { - flex: 1; - height: 0.4rem; - background-color: #303633; - border-radius: 0.2rem; -} - -.stat-bar-fill { - height: 100%; - border-radius: 0.2rem; - transition: width 0.3s ease; -} - -.stat-label { - font-weight: bold; -} - -.mon-sprite { - image-rendering: pixelated; - width: 192px; -} - -/* Moves section styling */ -.mon-moves-section { - margin-top: 1.5rem; - margin-bottom: 1.5rem; - border-top: 1px solid #404040; - padding-top: 1rem; -} - -.moves-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; - margin-top: 0.5rem; -} - -.move-card { - background-color: #2a2a2a; - border-radius: 8px; - padding: 0.75rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - display: flex; - flex-direction: column; - border: 1px solid #3a3a3a; -} - -.move-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #3a3a3a; -} - -.mon-move-header { - font-size: 1.5rem; -} - -.move-name { - font-weight: bold; - font-size: 1.1rem; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); -} - -.move-type { - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.9rem; - display: inline-flex; - align-items: center; - gap: 0.25rem; - white-space: nowrap; -} - -.move-details { - display: grid; - grid-template-columns: 2fr 1fr 1fr 1fr; - gap: 0.5rem; - margin-bottom: 0.75rem; -} - -.move-stat { - background-color: #333; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.9rem; - display: flex; - align-items: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.move-description { - font-size: 0.9rem; - color: #ccc; - margin-top: 0.5rem; - line-height: 1.4; - border-top: 1px solid #3a3a3a; - padding-top: 0.5rem; - flex-grow: 1; - overflow-wrap: break-word; -} - -.stat-icon { - margin-right: 0.25rem; - font-size: 0.9rem; -} - -.mon-abilities-section { - margin-top: 1.5rem; - margin-bottom: 1.5rem; - border-top: 1px solid #404040; - padding-top: 1rem; -} - -.mon-container-columns { - display: flex; - flex-direction: column; - gap: 1.5rem; - margin-top: 1rem; -} - -/* Media queries for responsive design */ -@media (max-width: 767px) { - /* Hide desktop tabs and show mobile dropdown on small screens */ - .desktop-tabs { - display: none; - } - - .mobile-tabs-container { - display: block; - } -} - -/* On desktop, use two columns and show tabs */ -@media (min-width: 768px) { - .mon-container-columns { - flex-direction: row; - } - - .mon-column { - flex: 1; - } - - #mon-container-stats { - max-width: 50%; - } - - /* Remove border above abilities on desktop */ - #mon-container-abilities-moves .mon-abilities-section { - border-top: none; - margin-top: 0; - padding-top: 0; - } - - /* Hide mobile dropdown on desktop */ - .mobile-tabs-container { - display: none; - } -} - -.ability-card { - background-color: #2a2a2a; - border-radius: 8px; - padding: 0.75rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - display: flex; - flex-direction: column; - border: 1px solid #3a3a3a; -} - -.ability-name { - font-weight: bold; - font-size: 1.1rem; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); -} - -.ability-effect { - font-size: 0.9rem; - color: #ccc; - margin-top: 0.5rem; - line-height: 1.4; - border-top: 1px solid #3a3a3a; - padding-top: 0.5rem; - flex-grow: 1; - overflow-wrap: break-word; -} - -/* Move damage table styling */ -.move-damage-table { - width: 100%; - border-collapse: collapse; - margin-top: 10px; -} - -.move-damage-table th, -.move-damage-table td { - padding: 6px; - text-align: left; - border: 1px solid #404040; -} - -.move-damage-table th { - background-color: #2d2d2d; - font-weight: bold; -} - -.move-damage-table tr:nth-child(even) { - background-color: #252525; -} - -.move-damage-table tr:hover { - background-color: #333333; -} - -.move-damage-section { - margin-top: 20px; - grid-column: span 3; -} - -.calculation-formula { - display: block; - font-family: monospace; - white-space: nowrap; - color: #b0b0b0; - padding: 4px; - background-color: #222; - border-radius: 4px; - margin-bottom: 6px; -} - -.calculation-steps { - display: block; - font-family: monospace; - color: #8a8a8a; - font-size: 0.9em; - padding-left: 8px; - border-left: 2px solid #444; - line-height: 1.4; -} - -/* Max damage table styling */ -.damage-badge-container { - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - margin-top: 4px; - margin-bottom: 4px; -} - -.damage-type-badge { - padding: 2px 4px; - border-radius: 4px; - font-size: 0.75em; - font-weight: bold; - display: flex; - align-items: center; -} - -.damage-type-multiplier { - font-size: 0.85em; - font-weight: bold; - margin-left: 2px; -} - -.multiplier-super { - color: #4CAF50; /* Green for super effective */ -} - -.multiplier-not-effective { - color: #F44336; /* Red for not very effective */ -} - -.move-class-indicator { - font-size: 0.9em; -} - -.move-name-display { - font-weight: bold; - font-size: 1.05em; - color: #ffffff; -} - -/* High damage cell styling */ -.high-damage-cell { - background-color: rgba(27, 116, 172, 0.5) !important; -} - -/* High damage row styling for Move Damage Analysis */ -.high-damage-row { - background-color: rgba(27, 116, 172, 0.3) !important; -} - -.high-damage-row:hover { - background-color: rgba(27, 116, 172, 0.4) !important; -} - -/* Sortable header styling */ -th.sortable { - cursor: pointer; -} - -th.sortable:hover { - background-color: #3d3d3d; -} - -.sort-indicator { - display: inline-block; - margin-left: 4px; -} - -/* ===== Type chart (Types tab) ===== */ -.type-subtabs { - display: flex; - gap: 6px; - margin-bottom: 14px; - flex-wrap: wrap; -} -.type-subtab { - background-color: #2d2d2d; - border: 1px solid #404040; -} -.type-subtab.active { - background-color: #365d72; -} -.type-subview { - display: none; -} -.type-subview.active { - display: block; -} - -.type-chip { - display: inline-flex; - align-items: center; - gap: 3px; - padding: 2px 6px; - border-radius: 4px; - font-size: 0.78rem; - font-weight: bold; - white-space: nowrap; -} - -table.type-grid { - border-collapse: collapse; - width: auto; - margin-bottom: 10px; -} -.type-grid th, -.type-grid td { - border: 1px solid #404040; - text-align: center; - padding: 2px; -} -.type-grid thead th { - position: sticky; - top: 0; - background-color: #1a1a1a; - z-index: 2; -} -.type-grid tbody th { - position: static; - background-color: #1a1a1a; - text-align: left; - padding: 2px 4px; -} -.type-grid .tc-corner { - color: #888; - font-size: 0.7rem; - font-weight: normal; - text-align: right; - white-space: nowrap; -} -.tc-cell { - width: 40px; - height: 30px; - cursor: pointer; - user-select: none; - font-size: 0.78rem; - font-weight: bold; - transition: background-color 0.15s; -} -.tc-cell:hover { - outline: 2px solid #999; - outline-offset: -2px; -} -.tc-neutral { - background-color: transparent; - color: #555; -} -.tc-super { - background-color: rgba(76, 175, 80, 0.28); - color: #bdf5bd; -} -.tc-half { - background-color: rgba(244, 67, 54, 0.24); - color: #ffb3ac; -} -.tc-immune { - background-color: #0d0d0d; - color: #777; -} -.tc-diag { - box-shadow: inset 0 0 0 1px #666; -} - -.tc-legend { - display: flex; - gap: 14px; - margin-bottom: 12px; - font-size: 0.8rem; - color: #bbb; - flex-wrap: wrap; - align-items: center; -} -.tc-legend .swatch { - display: inline-block; - width: 13px; - height: 13px; - border-radius: 3px; - vertical-align: middle; - margin-right: 5px; - border: 1px solid #404040; -} - -.tc-hint { - color: #aaa; - font-size: 0.85rem; - margin: 0 0 8px; -} -.tc-textarea { - width: 100%; - min-height: 340px; - background-color: #111; - color: #e0e0e0; - border: 1px solid #404040; - border-radius: 4px; - font-family: monospace; - font-size: 0.85rem; - line-height: 1.4; - padding: 10px; - white-space: pre; - overflow-wrap: normal; -} -#tc-msg { - margin-left: 8px; - font-family: monospace; - font-size: 0.85rem; -} - -.balance-rank { - width: auto; -} -.balance-rank th { - cursor: pointer; - white-space: nowrap; -} -.balance-rank td, -.balance-rank th { - text-align: center; -} -.balance-rank td:first-child, -.balance-rank th:first-child { - text-align: left; -} -.balance-cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 14px; - margin-top: 18px; -} -.balance-card { - background-color: #252525; - border: 1px solid #404040; - border-radius: 6px; - padding: 12px; -} -.balance-card .bc-head { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 8px; - font-size: 1rem; -} -.balance-card .bc-row { - display: flex; - gap: 6px; - flex-wrap: wrap; - align-items: baseline; - margin: 5px 0; -} -.balance-card .bc-label { - color: #999; - font-size: 0.72rem; - min-width: 70px; - text-transform: uppercase; - letter-spacing: 0.04em; -} -.balance-card .type-chip { - font-size: 0.72rem; -} -.bc-empty { - color: #666; - font-size: 0.78rem; -} - diff --git a/drool/type-data.js b/drool/type-data.js deleted file mode 100644 index b3458c39..00000000 --- a/drool/type-data.js +++ /dev/null @@ -1,107 +0,0 @@ -export const typeData = { - 'Yin': { - bgColor: '#ffffff', - textColor: '#000000', - abbreviation: 'YANG', - emoji: '⚪', - name: 'Yang' - }, - 'Yang': { - bgColor: '#000000', - textColor: '#FFFFFF', - abbreviation: 'YIN', - emoji: '🌚', - name: 'Yin' - }, - 'Earth': { - bgColor: '#8B4513', - textColor: '#FFFFFF', - abbreviation: 'ETH', - emoji: '🪨', - name: 'Earth' - }, - 'Liquid': { - bgColor: '#1E90FF', - textColor: '#000000', - abbreviation: 'LIQ', - emoji: '🌊', - name: 'Liquid' - }, - 'Fire': { - bgColor: '#d2400a', - textColor: '#FFFFFF', - abbreviation: 'FIR', - emoji: '🔥', - name: 'Fire' - }, - 'Metal': { - bgColor: '#71797E', - textColor: '#FFFFFF', - abbreviation: 'MTL', - emoji: '🔩', - name: 'Metal' - }, - 'Ice': { - bgColor: '#4682B4', - textColor: '#FFFFFF', - abbreviation: 'ICE', - emoji: '❄️', - name: 'Ice' - }, - 'Nature': { - bgColor: '#228B22', - textColor: '#FFFFFF', - abbreviation: 'NTR', - emoji: '🌳', - name: 'Nature' - }, - 'Lightning': { - bgColor: '#FFD700', - textColor: '#000000', - abbreviation: 'LTG', - emoji: '🌩️', - name: 'Lightning' - }, - 'Mythic': { - bgColor: '#8A2BE2', - textColor: '#FFFFFF', - abbreviation: 'MYT', - emoji: '🧿', - name: 'Mythic' - }, - 'Air': { - bgColor: '#46B4AF', - textColor: '#FFFFFF', - abbreviation: 'AIR', - emoji: '🌪️', - name: 'Air' - }, - 'Math': { - bgColor: '#FF1493', - textColor: '#FFFFFF', - abbreviation: 'MTH', - emoji: '🧠', - name: 'Math' - }, - 'Cyber': { - bgColor: '#00CED1', - textColor: '#000000', - abbreviation: 'CYB', - emoji: '💻', - name: 'Cyber' - }, - 'Wild': { - bgColor: '#9a7247', - textColor: '#FFF', - abbreviation: 'WLD', - emoji: '🦙', - name: 'Wild' - }, - 'Cosmic': { - bgColor: '#4B0082', - textColor: '#FFFFFF', - abbreviation: 'CMC', - emoji: '🌠', - name: 'Cosmic' - } -}; \ No newline at end of file diff --git a/drool/type-metadata.csv b/drool/type-metadata.csv new file mode 100644 index 00000000..6733e0ca --- /dev/null +++ b/drool/type-metadata.csv @@ -0,0 +1,15 @@ +Type,Name,Character,BgColor,TextColor,Abbreviation +Yin,Yin,陰,#111,#eee,YIN +Yang,Yang,陽,#eee,#111,YAN +Earth,Earth,土,#8B4513,#eee,ETH +Liquid,Liquid,水,#1E90FF,#111,LIQ +Fire,Fire,火,#d2400a,#eee,FIR +Metal,Metal,金,#60676b,#eee,MTL +Ice,Ice,氷,#4682B4,#eee,ICE +Nature,Nature,木,#228B22,#eee,NTR +Lightning,Lightning,雷,var(--text-warm),#111,LTG +Faith,Faith,信,#e39eab,#111,FAI +Air,Air,氣,#7278b5,#eee,AIR +Math,Math,數,#aa2e70ff,#eee,MTH +Cyber,Cyber,量,#00CED1,#111,CYB +Cosmic,Cosmic,混,#4B0082,#eee,CMC diff --git a/drool/utils.js b/drool/utils.js deleted file mode 100644 index 27b5ed99..00000000 --- a/drool/utils.js +++ /dev/null @@ -1,200 +0,0 @@ -export async function getFS() { - try { - return await import("@tauri-apps/plugin-fs"); - } catch (error) { - // Web fallback implementation - return { - readTextFile: async (path) => { - console.warn("Tauri FS not available in web environment"); - throw new Error("File system API not available in browser"); - }, - // Add other method fallbacks as needed - writeTextFile: async (path, content) => { - console.warn("Tauri FS not available in web environment"); - throw new Error("File system API not available in browser"); - }, - }; - } -} - -// Function to load full monster data from CSV -export async function loadFullMonsFromCsv() { - try { - const response = await fetch("mons.csv"); - if (response.ok) { - const csvContent = await response.text(); - const lines = csvContent.split(/\r\n|\n/); - - if (lines.length < 2) return []; - - // Extract headers - const headers = lines[0].split(",").map((header) => header.trim()); - - // Parse data rows - let monsData = []; - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; - - const values = parseCsvLine(line); - if (values[0]) { - // If it has a name - const monData = {}; - headers.forEach((header, index) => { - // Convert numeric values to numbers - if ( - [ - "HP", - "Attack", - "Defense", - "SpecialAttack", - "SpecialDefense", - "Speed", - ].includes(header) - ) { - monData[header] = parseInt(values[index]) || 0; - } else { - monData[header] = values[index] || ""; - } - }); - monsData.push(monData); - } - } - return monsData; - } - } catch (error) { - console.error("Error loading mons.csv:", error); - } - return []; -} - -export async function loadMovesFromCsv() { - try { - const response = await fetch("moves.csv"); - if (response.ok) { - const csvContent = await response.text(); - const lines = csvContent.split(/\r\n|\n/); - - if (lines.length < 2) return []; - - // Extract headers - const headers = lines[0].split(",").map((header) => header.trim()); - - // Parse data rows - let movesFromCsv = []; - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; - const values = parseCsvLine(line); - if (values[0]) { - // If it has a name - const moveData = {}; - headers.forEach((header, index) => { - // Convert numeric values to numbers - if (["Power", "Stamina", "Accuracy"].includes(header)) { - moveData[header] = values[index] === "?" ? values[index] : parseInt(values[index]) || 0; - } else { - moveData[header] = values[index] || ""; - } - }); - movesFromCsv.push(moveData); - } - } - return movesFromCsv; - } - } catch (error) { - console.error("Error loading moves.csv:", error); - } - return []; -} - -export async function loadAbilitiesFromCsv() { - try { - const response = await fetch("abilities.csv"); - if (response.ok) { - const csvContent = await response.text(); - const lines = csvContent.split(/\r\n|\n/); - - if (lines.length < 2) return []; - - // Extract headers - const headers = lines[0].split(",").map((header) => header.trim()); - const abilitiesFromCsv = []; - - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; - const values = parseCsvLine(line); - if (values[0]) { - const abilityData = {}; - headers.forEach((header, index) => { - abilityData[header] = values[index] || ""; - }); - abilitiesFromCsv.push(abilityData); - } - } - return abilitiesFromCsv; - } - } catch (error) { - console.error("Error loading abilities.csv:", error); - } - return []; -} - -function parseCsvLine(line) { - const result = []; - let current = ""; - let inQuotes = false; - - for (let i = 0; i < line.length; i++) { - const char = line[i]; - - if (char === '"') { - if (inQuotes && i + 1 < line.length && line[i + 1] === '"') { - // Double quotes inside quotes - add a single quote - current += '"'; - i++; - } else { - // Toggle quote mode - inQuotes = !inQuotes; - } - } else if (char === "," && !inQuotes) { - // End of field - result.push(current); - current = ""; - } else { - current += char; - } - } - - // Add the last field - result.push(current); - return result; -} - -export async function loadTypeData() { - try { - const response = await fetch('types.csv'); - if (response.ok) { - const csvContent = await response.text(); - const lines = csvContent.split(/\r\n|\n/); - if (lines.length < 2) return []; - const typeData = {}; - for (let i = 1; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) continue; - const values = parseCsvLine(line); - const typeValue = typeData[values[0]]; - if (!typeValue) { - typeData[values[0]] = {}; - } - typeData[values[0]][values[1]] = values[2]; - } - return typeData; - } - } - catch (error) { - console.error("Error loading types.csv:", error); - } - return {}; -} \ No newline at end of file diff --git a/processing/addressPacking.py b/processing/addressPacking.py index 0dd14e0a..35c3c932 100644 --- a/processing/addressPacking.py +++ b/processing/addressPacking.py @@ -11,7 +11,7 @@ import re from pathlib import Path -from packMoves import detect_inline_ability, pack_ability, pack_move +from packMoves import detect_inline_ability, inline_params_from_csv, pack_ability, pack_move from generateSolidity import contract_name_from_move_or_ability, get_mon_directory_name @@ -22,7 +22,8 @@ def collect_inline_move_addresses( """Find all JSON inline moves, pack them, and return as (move_display_name, hex_value) tuples. Uses CSV move names (e.g. "Pound Ground") so the Address keys match what - generateMonsTypeScript.py expects. + generateMonsTypeScript.py expects. Packed values come from the CSV row; the .json + supplies only the effect name. Resolves effect contract addresses from deployed_addresses so the packed values match on-chain (where SetupMons.s.sol OR's the effect address in). @@ -58,7 +59,7 @@ def collect_inline_move_addresses( if addr_str: effect_address = int(addr_str, 16) - packed = pack_move(move_data, effect_address=effect_address) + packed = pack_move(inline_params_from_csv(row), effect_address=effect_address) results.append((move_name, f"0x{packed:064x}")) return results diff --git a/processing/csvTable.py b/processing/csvTable.py new file mode 100644 index 00000000..8cd5af2c --- /dev/null +++ b/processing/csvTable.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Fidelity-preserving CSV read/write for the drool data files. + +csv.DictWriter re-quotes by minimal rules, which strips the quotes drool/*.csv puts around +every description and flavour field — rewriting all 66 move rows on any save. This keeps each +cell's original quoting, so an untouched row round-trips byte-identically and a diff shows only +the cells that actually changed. +""" + +from pathlib import Path + + +def split_csv_line(line: str) -> tuple[list[str], list[bool]]: + """Split one CSV line into values plus whether each value was quoted in the source.""" + values: list[str] = [] + quoted: list[bool] = [] + i, n = 0, len(line) + while True: + if i < n and line[i] == '"': + i += 1 + buf: list[str] = [] + while i < n: + if line[i] == '"': + if i + 1 < n and line[i + 1] == '"': + buf.append('"') + i += 2 + continue + i += 1 + break + buf.append(line[i]) + i += 1 + values.append("".join(buf)) + quoted.append(True) + else: + j = line.find(",", i) + if j == -1: + j = n + values.append(line[i:j]) + quoted.append(False) + i = j + if i < n and line[i] == ",": + i += 1 + if i == n: # trailing comma closes one more empty field + values.append("") + quoted.append(False) + break + else: + break + return values, quoted + + +def join_csv_line(values: list[str], quoted: list[bool]) -> str: + """Re-emit a row, keeping each cell's original quoting and adding it where now required.""" + out = [] + for value, was_quoted in zip(values, quoted): + needs_quotes = was_quoted or any(c in value for c in ',"\n') + out.append('"' + value.replace('"', '""') + '"' if needs_quotes else value) + return ",".join(out) + + +class Table: + """A CSV file as ordered dict rows, remembering per-cell quoting for a clean rewrite.""" + + def __init__(self, path: Path | str): + self.path = Path(path) + raw = self.path.read_text(encoding="utf-8") + self.trailing_newline = raw.endswith("\n") + lines = raw.split("\n") + if self.trailing_newline: + lines = lines[:-1] + if not lines: + raise ValueError(f"{self.path} is empty") + + self.header = lines[0] + self.fields = split_csv_line(self.header)[0] + self.rows: list[dict[str, str]] = [] + self.quoted: list[list[bool]] = [] + for number, line in enumerate(lines[1:], start=2): + if not line.strip(): + raise ValueError(f"{self.path}:{number} is blank; this reader expects one row per line") + values, quoted = split_csv_line(line) + pad = len(self.fields) - len(values) + values += [""] * pad + quoted += [False] * pad + self.rows.append(dict(zip(self.fields, values))) + self.quoted.append(quoted) + + def render(self) -> str: + out = [self.header] + for row, quoted in zip(self.rows, self.quoted): + out.append(join_csv_line([row[f] for f in self.fields], quoted)) + return "\n".join(out) + ("\n" if self.trailing_newline else "") + + def write(self) -> None: + self.path.write_text(self.render(), encoding="utf-8") diff --git a/processing/dataEditor.html b/processing/dataEditor.html new file mode 100644 index 00000000..8ea0b407 --- /dev/null +++ b/processing/dataEditor.html @@ -0,0 +1,640 @@ + + + + + + chomp data editor + + + +
+ + + +
+ no changes + + +
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+ + + + + + +
+
+
+ +
+
+
+ +

Roster

+
+
+
+ + + + diff --git a/processing/deploy.py b/processing/deploy.py index 7e85bf9f..3eb5040f 100755 --- a/processing/deploy.py +++ b/processing/deploy.py @@ -2,7 +2,7 @@ """ Meta-script to orchestrate the full deployment pipeline: 1. Validate move contracts against CSV data -2. Generate Solidity deploy scripts (SetupMons.s.sol, SetupCPU.s.sol) +2. Generate Solidity deploy scripts (script/mons/Setup.s.sol, SetupCPU.s.sol) 3. Run the forge scripts -> parse output -> update .env (raw addresses) 4. Run the TypeScript generators (addresses/ABIs -> deployments.json + munch/belch address.ts; mon data, event layouts, EIP-712 meta) @@ -36,12 +36,18 @@ DEFAULT_RPC_MAINNET = "https://mainnet.megaeth.com/rpc" DEFAULT_RPC_TESTNET = "https://carrot.megaeth.com/rpc" -# Script paths relative to chomp directory -SCRIPTS = [ - "script/EngineAndPeriphery.s.sol", - "script/SetupMons.s.sol", - "script/SetupCPU.s.sol", -] +def build_script_list(chomp_dir: Path) -> list[str]: + """forge targets to broadcast, in order: periphery, then one script per mon, then CPU teams. + + Mon order comes from mons.csv (ascending id) because createMon rejects non-sequential ids. + """ + from generateSolidity import mon_script_targets + + return [ + "script/EngineAndPeriphery.s.sol", + *mon_script_targets(str(chomp_dir)), + "script/SetupCPU.s.sol", + ] def get_chomp_dir() -> Path: @@ -343,7 +349,7 @@ def run_deploy(args, network, rpc_url, password, chomp_dir, env_path): if not run_validate_effect_bitmaps(): raise RuntimeError("Effect bitmap validation failed. Fix issues before deploying.") - print_banner("Generating Solidity deployment script (SetupMons.s.sol)") + print_banner("Generating per-mon Solidity deployment scripts (script/mons/)") from generateSolidity import run as run_solidity if not run_solidity(): raise RuntimeError("Solidity generation failed.") @@ -363,7 +369,7 @@ def run_deploy(args, network, rpc_url, password, chomp_dir, env_path): set_env_network(env_path, network) # Run forge scripts and update .env after each. - for script_path in SCRIPTS: + for script_path in build_script_list(chomp_dir): # EngineAndPeriphery deploys a new GachaTeamRegistry; point it at the prior # one (from the canonical deployments record for this network) so players can migrate. extra_env = None diff --git a/processing/editData.py b/processing/editData.py new file mode 100644 index 00000000..82da9fb9 --- /dev/null +++ b/processing/editData.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +"""Visual editor for chomp's game data: mon stats, move rows, and CPU teams. + +Serves a local page with three tabs: + Mons — drool/mons.csv, with live stat rankings, defensive typing and threat/KO tables + Moves — drool/moves.csv, badged by whether the row drives the deployed move or mirrors a .sol + CPU teams — script/cpu-teams.json + +Saving writes the file and reruns the generators it feeds, so the deploy scripts and munch's +generated data stay in step. Contract-backed move rows are reported rather than auto-patched: +their .sol is the deployed artifact, so syncing it is an explicit `validateMoves.py` run. + +Usage: + python processing/editData.py [--port 8777] [--no-open] +""" + +import argparse +import contextlib +import csv +import io +import json +import re +import subprocess +import sys +import webbrowser +from functools import partial +from http.server import HTTPServer, SimpleHTTPRequestHandler +from pathlib import Path +from typing import Any + +# Add processing directory to path for local imports +sys.path.insert(0, str(Path(__file__).parent)) + +from csvTable import Table +from generateSetupCPU import load_cpu_teams, run as run_setup_cpu +from generateSolidity import contract_name_from_move_or_ability, get_mon_directory_name, run as run_solidity + + +EDITOR_HTML = Path(__file__).parent / "dataEditor.html" +STAT_FIELDS = ("HP", "Attack", "Defense", "SpecialAttack", "SpecialDefense", "Speed") +MOVE_NUMERIC_FIELDS = ("Power", "Stamina", "Accuracy", "Priority", "UnlockLevel") + + +def chomp_dir() -> Path: + return Path(__file__).parent.parent + + +def load_team_size(base: Path) -> int: + """GAME_MONS_PER_TEAM from Constants.sol — the per-seat roster length the Engine enforces.""" + match = re.search(r"GAME_MONS_PER_TEAM\s*=\s*(\d+)", (base / "src" / "Constants.sol").read_text()) + if not match: + raise RuntimeError("GAME_MONS_PER_TEAM not found in src/Constants.sol") + return int(match.group(1)) + + +def move_source(base: Path, mon: str, move: str) -> tuple[str, str]: + """Classify a move row by what the CSV actually drives, and name the file it pairs with. + + inline — no .sol; the row is packed straight into the move word, so the CSV is authoritative + contract — mirrored in a .sol that has to be re-synced when the row changes + complex — power is '?', computed by the engine at runtime; the CSV can't drive it + """ + contract = contract_name_from_move_or_ability(move) + mon_dir = base / "src" / "mons" / get_mon_directory_name(mon) + if (mon_dir / f"{contract}.json").exists(): + return "inline", f"{contract}.json" + if (mon_dir / f"{contract}.sol").exists(): + return "contract", f"{contract}.sol" + return "missing", f"{contract}.sol" + + +def load_state(base: Path) -> dict[str, Any]: + mons_table = Table(base / "drool" / "mons.csv") + moves_table = Table(base / "drool" / "moves.csv") + + mons = [] + for row in mons_table.rows: + mons.append({ + "id": int(row["Id"]), + "name": row["Name"], + "t1": row["Type1"], + "t2": "" if row["Type2"] in ("NA", "") else row["Type2"], + "sprite": f"/drool/imgs/{row['Name'].lower()}_mini.gif", + "stats": {f: int(row[f]) for f in STAT_FIELDS}, + "flavor": row["Flavor"], + }) + + moves = [] + for index, row in enumerate(moves_table.rows): + source, filename = move_source(base, row["Mon"].strip(), row["Name"].strip()) + if source == "contract" and row["Power"].strip() == "?": + source = "complex" + moves.append({ + "index": index, + "src": source, + "file": filename, + "sprite": f"/drool/imgs/{row['Mon'].strip().lower()}_mini.gif", + **{field: row[field] for field in moves_table.fields}, + }) + + types = {} + with open(base / "drool" / "type-metadata.csv", newline="", encoding="utf-8") as f: + for row in csv.DictReader(f): + types[row["Type"]] = {"bg": row["BgColor"], "fg": row["TextColor"], "ab": row["Abbreviation"]} + + # Attacker -> defender -> raw code (0 immune, 1 neutral, 2 super, 5 half), as types.csv stores it. + chart: dict[str, dict[str, int]] = {} + with open(base / "drool" / "types.csv", newline="", encoding="utf-8") as f: + for row in csv.DictReader(f): + attacker = (row["Attacker"] or "").strip() + defender = (row["Defender"] or "").strip() + if attacker and defender: + chart.setdefault(attacker, {})[defender] = int(row["Multiplier"]) + + cpu = load_cpu_teams(base / "script" / "cpu-teams.json") + return { + "mons": mons, + "moves": moves, + "moveFields": moves_table.fields, + "types": types, + "chart": chart, + "cpuTeams": cpu["teams"], + "cpuPlayers": cpu["cpuPlayers"], + "teamSize": load_team_size(base), + } + + +def render_cpu_json(payload: dict[str, Any]) -> str: + """One team per line, matching the hand-authored layout so diffs stay readable.""" + players = ", ".join(json.dumps(p) for p in payload["cpuPlayers"]) + teams = ",\n".join(" [" + ", ".join(str(int(m)) for m in team) + "]" for team in payload["teams"]) + return f'{{\n "cpuPlayers": [{players}],\n "teams": [\n{teams}\n ]\n}}\n' + + +def regenerate_deploy_scripts(base: Path) -> list[str]: + """Mon stats and inline move words are baked into script/mons/*.s.sol, so re-emit them.""" + log = io.StringIO() + with contextlib.redirect_stdout(log): + ok = run_solidity(base_path=str(base)) + lines = [line for line in log.getvalue().splitlines() if line.startswith(("Generated", "Removed"))] + if not ok: + lines.append("generateSolidity FAILED — see the server log") + print(log.getvalue(), end="") + return lines + + +def validate_moves_report(base: Path) -> list[str]: + """Run validateMoves read-only ('n' declines its contract-patching prompt) and summarise.""" + result = subprocess.run( + [sys.executable, "processing/validateMoves.py"], + cwd=base, input="n\n", capture_output=True, text=True, + ) + output = result.stdout + result.stderr + if "All move validations passed" in output: + return ["validateMoves: all rows match their contracts"] + + mismatches = [line.strip().lstrip("• ") for line in output.splitlines() if "mismatch" in line.lower()] + summary = [f"validateMoves: {len(mismatches)} mismatch(es) against the .sol contracts"] + summary += [f" • {m}" for m in mismatches[:12]] + if len(mismatches) > 12: + summary.append(f" … and {len(mismatches) - 12} more") + summary.append("Run `python processing/validateMoves.py` and answer y to patch the contracts.") + return summary + + +class EditorHandler(SimpleHTTPRequestHandler): + """Editor routes over a static file server rooted at chomp/ (for drool/imgs sprites).""" + + def do_GET(self): + if self.path in ("/", "/index.html"): + self._send(EDITOR_HTML.read_bytes(), "text/html; charset=utf-8") + elif self.path == "/api/state": + self._send_json(load_state(chomp_dir())) + else: + super().do_GET() + + def do_POST(self): + base = chomp_dir() + try: + payload = json.loads(self.rfile.read(int(self.headers.get("Content-Length") or 0)) or b"{}") + except json.JSONDecodeError as e: + self._send_json({"ok": False, "errors": [f"Malformed request: {e}"]}, status=400) + return + + handlers = { + "/api/save/mons": self._save_mons, + "/api/save/moves": self._save_moves, + "/api/save/cpu-teams": self._save_cpu_teams, + } + handler = handlers.get(self.path) + if handler is None: + self.send_error(404) + return + + try: + errors, log = handler(base, payload) + except Exception as e: # a bad payload shouldn't take the server down mid-session + self._send_json({"ok": False, "errors": [f"{type(e).__name__}: {e}"]}, status=400) + return + + self._send_json({"ok": not errors, "errors": errors, "log": log}, + status=400 if errors else 200) + + def _save_mons(self, base: Path, payload: dict[str, Any]) -> tuple[list[str], list[str]]: + table = Table(base / "drool" / "mons.csv") + by_id = {int(row["Id"]): row for row in table.rows} + errors = [] + for entry in payload.get("mons", []): + row = by_id.get(int(entry["id"])) + if row is None: + errors.append(f"Unknown mon id {entry['id']}") + continue + for field, value in entry.get("stats", {}).items(): + if field not in STAT_FIELDS: + errors.append(f"{row['Name']}: {field} is not an editable stat") + continue + if not (isinstance(value, int) and value > 0): + errors.append(f"{row['Name']}: {field} must be a positive integer, got {value!r}") + continue + row[field] = str(value) + if errors: + return errors, [] + + table.write() + return [], ["Wrote drool/mons.csv"] + regenerate_deploy_scripts(base) + + def _save_moves(self, base: Path, payload: dict[str, Any]) -> tuple[list[str], list[str]]: + table = Table(base / "drool" / "moves.csv") + errors = [] + for entry in payload.get("moves", []): + index = int(entry["index"]) + if not 0 <= index < len(table.rows): + errors.append(f"Move row {index} is out of range") + continue + row = table.rows[index] + for field, value in entry.get("cells", {}).items(): + if field not in table.fields: + errors.append(f"{row['Name']}: {field} is not a moves.csv column") + continue + value = str(value) + if field in MOVE_NUMERIC_FIELDS and value != "?" and not re.fullmatch(r"-?\d+", value): + errors.append(f"{row['Name']}: {field} must be a number (or '?'), got {value!r}") + continue + row[field] = value + if errors: + return errors, [] + + table.write() + log = ["Wrote drool/moves.csv"] + regenerate_deploy_scripts(base) + return [], log + validate_moves_report(base) + + def _save_cpu_teams(self, base: Path, payload: dict[str, Any]) -> tuple[list[str], list[str]]: + team_size = load_team_size(base) + mon_ids = {int(row["Id"]) for row in Table(base / "drool" / "mons.csv").rows} + errors = [] + + players = payload.get("cpuPlayers") + if not isinstance(players, list) or not players or not all(isinstance(p, str) and p for p in players): + errors.append("cpuPlayers must be a non-empty list of env var names.") + + teams = payload.get("teams") + if not isinstance(teams, list) or not teams: + return errors + ["Need at least one team."], [] + + for index, team in enumerate(teams): + label = f"Team {index + 1}" + if not isinstance(team, list) or not all(isinstance(m, int) for m in team): + errors.append(f"{label} is not a list of mon ids.") + continue + if len(team) != team_size: + errors.append(f"{label} has {len(team)} mons; the Engine requires exactly {team_size}.") + unknown = sorted(set(team) - mon_ids) + if unknown: + errors.append(f"{label} references unknown mon id(s) {unknown}.") + if errors: + return errors, [] + + (base / "script" / "cpu-teams.json").write_text(render_cpu_json(payload), encoding="utf-8") + log = io.StringIO() + with contextlib.redirect_stdout(log): + synced = run_setup_cpu() + print(log.getvalue(), end="") + if not synced: + return ["generateSetupCPU failed; see the server log."], [] + return [], ["Wrote script/cpu-teams.json"] + log.getvalue().strip().splitlines() + + def log_message(self, fmt, *args): + # Static sprite fetches are noise; only report the API calls. + if self.path.startswith("/api/"): + super().log_message(fmt, *args) + + def _send(self, body: bytes, content_type: str, status: int = 200): + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + self.wfile.write(body) + + def _send_json(self, payload: dict[str, Any], status: int = 200): + self._send(json.dumps(payload).encode("utf-8"), "application/json", status) + + +def main(): + parser = argparse.ArgumentParser(description="Visual editor for chomp's mon, move and CPU-team data") + parser.add_argument("--port", type=int, default=8777, help="Port to serve on (default: 8777)") + parser.add_argument("--no-open", action="store_true", help="Do not open a browser window") + args = parser.parse_args() + + url = f"http://127.0.0.1:{args.port}" + server = HTTPServer(("127.0.0.1", args.port), partial(EditorHandler, directory=str(chomp_dir()))) + + print(f"chomp data editor: {url}") + print("Saving rewrites the CSV / JSON and reruns the generators it feeds. Ctrl-C to stop.") + if not args.no_open: + webbrowser.open(url) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nStopped.") + finally: + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/processing/generateMonsTypeScript.py b/processing/generateMonsTypeScript.py index 9a72c947..6fa01711 100644 --- a/processing/generateMonsTypeScript.py +++ b/processing/generateMonsTypeScript.py @@ -11,7 +11,7 @@ from typing import Dict, List, Any from types_enum import REAL_TYPE_NAMES -from packMoves import pack_move, find_json_moves +from packMoves import find_json_moves, inline_params_from_csv, pack_move, parse_constants def to_address_key(name: str) -> str: @@ -24,18 +24,6 @@ def to_spritesheet_key(name: str) -> str: return re.sub(r"_+", "_", re.sub(r"[^a-zA-Z0-9]", "_", name)).strip("_").lower() -def parse_constants(raw: str) -> Dict[str, int]: - """Parse the Constants column ('NAME=VAL;NAME=VAL') into a {NAME: int} dict.""" - consts: Dict[str, int] = {} - for part in (raw or "").split(";"): - part = part.strip() - if not part: - continue - name, _, val = part.partition("=") - consts[name.strip()] = int(val.strip()) - return consts - - def _format_percent(value: float) -> str: """Render a percentage with at most 1 decimal place (trailing .0 dropped).""" rounded = round(value, 1) @@ -377,7 +365,7 @@ def parse_int_or_unknown(val: str) -> int | str: contract_name = re.sub(r"\W+", "", move_name.replace(" ", "")) inline_json = json_moves.get(mon_name.lower(), {}).get(contract_name) if inline_json is not None: - move_data["inlineKey"] = hex(pack_move(inline_json, effect_address=0) >> 160) + move_data["inlineKey"] = hex(pack_move(inline_params_from_csv(row), effect_address=0) >> 160) placement = MOVE_OVERLAY_PLACEMENT.get(move_name) if placement: diff --git a/processing/generateSolidity.py b/processing/generateSolidity.py index 26484482..070a540c 100644 --- a/processing/generateSolidity.py +++ b/processing/generateSolidity.py @@ -7,7 +7,7 @@ import argparse from typing import Dict, List, Tuple, Optional -from packMoves import pack_move, detect_inline_ability +from packMoves import pack_move, detect_inline_ability, inline_params_from_csv class MonData: @@ -28,6 +28,7 @@ def __init__(self, mon_id: int, name: str, hp: int, stamina: int, speed: int, self.type2 = type2 self.moves: List[str] = [] self.move_unlock_levels: Dict[str, int] = {} # move name -> unlock level (lane gating) + self.move_rows: Dict[str, dict] = {} # move name -> moves.csv row (inline packing source) self.abilities: List[str] = [] self.sprite_data: List[int] = [] # uint256 values for compressed sprite data self.palette_data: List[int] = [] # uint256 values for compressed palette data @@ -98,6 +99,7 @@ def read_moves_csv(file_path: str, mons: Dict[str, MonData]) -> None: if move_name and mon_name and mon_name in mons: mons[mon_name].moves.append(move_name) mons[mon_name].move_unlock_levels[move_name] = int((row.get('UnlockLevel') or '0').strip() or '0') + mons[mon_name].move_rows[move_name] = row # Stable-sort each mon's moves by unlock level so lanes line up with the on-chain curve. for mon in mons.values(): @@ -277,14 +279,14 @@ def is_json_move(mon_name: str, move_name: str, base_path: str) -> bool: return os.path.exists(json_path) -def load_json_move(mon_name: str, move_name: str, base_path: str) -> Optional[dict]: - """Load a JSON move definition if it exists.""" +def json_move_effect(mon_name: str, move_name: str, base_path: str) -> Optional[str]: + """Effect contract name an inline move applies, or None. Only meaningful for JSON moves.""" mon_dir = get_mon_directory_name(mon_name) contract_name = contract_name_from_move_or_ability(move_name) json_path = os.path.join(base_path, "src", "mons", mon_dir, f"{contract_name}.json") if os.path.exists(json_path): with open(json_path, "r", encoding="utf-8") as f: - return json.load(f) + return json.load(f).get("effect") return None @@ -420,6 +422,7 @@ def collect_env_addresses_for_mon(mon_contracts: Dict[str, ContractInfo], intra_ def generate_deploy_function_for_mon(mon: MonData, base_path: str, include_color: bool = False) -> List[str]: """Generate the deploy function for a specific mon""" function_name = f"deploy{mon.name.replace(' ', '')}" + helper_name = f"_register{mon.name.replace(' ', '')}" lines = [] lines.append(f" function {function_name}(GachaTeamRegistry registry) internal returns (DeployData[] memory) {{") @@ -500,243 +503,205 @@ def generate_deploy_function_for_mon(mon: MonData, base_path: str, include_color # Call helper function to register the mon (reduces stack pressure) lines.append("") - helper_name = f"_register{mon.name.replace(' ', '')}" lines.append(f" {helper_name}(registry, addrs);") + else: + lines.append("") + lines.append(f" {helper_name}(registry);") lines.append("") lines.append(" return deployedContracts;") lines.append(" }") lines.append("") - # Generate the helper function for registering the mon + # Generate the helper function for registering the mon. A mon whose moves are all + # inline JSON deploys no contracts but still needs its createMon call. if mon_contracts: deployment_order, intra_dependencies = analyze_intra_mon_dependencies(mon_contracts) deployed_indices = {name: idx for idx, name in enumerate(deployment_order)} + addrs_param = f", address[{num_contracts}] memory addrs" + else: + deployed_indices = {} + addrs_param = "" + + lines.append(f" function {helper_name}(GachaTeamRegistry registry{addrs_param}) internal {{") + + # Generate MonStats + type1 = convert_type_to_solidity(mon.type1) + type2 = convert_type_to_solidity(mon.type2) + lines.extend([ + " MonStats memory stats = MonStats({", + f" hp: {mon.hp},", + f" stamina: {mon.stamina},", + f" speed: {mon.speed},", + f" attack: {mon.attack},", + f" defense: {mon.defense},", + f" specialAttack: {mon.special_attack},", + f" specialDefense: {mon.special_defense},", + f" type1: {type1},", + f" type2: {type2}", + " });" + ]) - helper_name = f"_register{mon.name.replace(' ', '')}" - lines.append(f" function {helper_name}(GachaTeamRegistry registry, address[{num_contracts}] memory addrs) internal {{") - - # Generate MonStats - type1 = convert_type_to_solidity(mon.type1) - type2 = convert_type_to_solidity(mon.type2) - lines.extend([ - " MonStats memory stats = MonStats({", - f" hp: {mon.hp},", - f" stamina: {mon.stamina},", - f" speed: {mon.speed},", - f" attack: {mon.attack},", - f" defense: {mon.defense},", - f" specialAttack: {mon.special_attack},", - f" specialDefense: {mon.special_defense},", - f" type1: {type1},", - f" type2: {type2}", - " });" - ]) - - # Generate moves array using addrs indices or packed inline values - if mon.moves: - lines.append(f" uint256[] memory moves = new uint256[]({len(mon.moves)});") - for i, move_name in enumerate(mon.moves): - json_data = load_json_move(mon.name, move_name, base_path) - if json_data is not None: - # Inline move: pack params into uint256 - packed = pack_move(json_data, effect_address=0) - effect_name = json_data.get("effect") - if effect_name: - # Effect address is resolved at deploy time via env var - env_var = effect_name_to_env_var(effect_name) - lines.append(f" moves[{i}] = 0x{packed:064x} | uint256(uint160(vm.envAddress(\"{env_var}\")));") - else: - lines.append(f" moves[{i}] = 0x{packed:064x};") + # Generate moves array using addrs indices or packed inline values + if mon.moves: + lines.append(f" uint256[] memory moves = new uint256[]({len(mon.moves)});") + for i, move_name in enumerate(mon.moves): + if is_json_move(mon.name, move_name, base_path): + # Inline move: pack the CSV row's params into uint256 + packed = pack_move(inline_params_from_csv(mon.move_rows[move_name]), effect_address=0) + effect_name = json_move_effect(mon.name, move_name, base_path) + if effect_name: + # Effect address is resolved at deploy time via env var + env_var = effect_name_to_env_var(effect_name) + lines.append(f" moves[{i}] = 0x{packed:064x} | uint256(uint160(vm.envAddress(\"{env_var}\")));") else: - # External move contract: deployed address + packed static metadata - # (stamina/priority; 0xF nibble = state-dependent, engine calls live). - contract_name = contract_name_from_move_or_ability(move_name) - if contract_name in deployed_indices: - idx = deployed_indices[contract_name] - meta = deployed_move_meta(mon.name, move_name, base_path) - lines.append(f" moves[{i}] = 0x{meta:064x} | uint256(uint160(addrs[{idx}]));") - else: - lines.append(" uint256[] memory moves = new uint256[](0);") - - # Generate abilities array using addrs indices (packed for inline abilities) - if mon.abilities: - lines.append(f" uint256[] memory abilities = new uint256[]({len(mon.abilities)});") - for i, ability_name in enumerate(mon.abilities): - contract_name = contract_name_from_move_or_ability(ability_name) + lines.append(f" moves[{i}] = 0x{packed:064x};") + else: + # External move contract: deployed address + packed static metadata + # (stamina/priority; 0xF nibble = state-dependent, engine calls live). + contract_name = contract_name_from_move_or_ability(move_name) if contract_name in deployed_indices: idx = deployed_indices[contract_name] - # Check if this ability has @inline-ability magic comment - mon_dir = get_mon_directory_name(mon.name) - sol_path = os.path.join(base_path, "src", "mons", mon_dir, f"{contract_name}.sol") - ability_type_id = detect_inline_ability(sol_path) - if ability_type_id is not None: - # Pack: (typeId << 248) | address - lines.append(f" abilities[{i}] = (uint256({ability_type_id}) << 248) | uint256(uint160(addrs[{idx}]));") - else: - # External: store raw address as uint256 - lines.append(f" abilities[{i}] = uint256(uint160(addrs[{idx}]));") - else: - lines.append(" uint256[] memory abilities = new uint256[](0);") + meta = deployed_move_meta(mon.name, move_name, base_path) + lines.append(f" moves[{i}] = 0x{meta:064x} | uint256(uint160(addrs[{idx}]));") + else: + lines.append(" uint256[] memory moves = new uint256[](0);") + + # Generate abilities array using addrs indices (packed for inline abilities) + if mon.abilities: + lines.append(f" uint256[] memory abilities = new uint256[]({len(mon.abilities)});") + for i, ability_name in enumerate(mon.abilities): + contract_name = contract_name_from_move_or_ability(ability_name) + if contract_name in deployed_indices: + idx = deployed_indices[contract_name] + # Check if this ability has @inline-ability magic comment + mon_dir = get_mon_directory_name(mon.name) + sol_path = os.path.join(base_path, "src", "mons", mon_dir, f"{contract_name}.sol") + ability_type_id = detect_inline_ability(sol_path) + if ability_type_id is not None: + # Pack: (typeId << 248) | address + lines.append(f" abilities[{i}] = (uint256({ability_type_id}) << 248) | uint256(uint160(addrs[{idx}]));") + else: + # External: store raw address as uint256 + lines.append(f" abilities[{i}] = uint256(uint160(addrs[{idx}]));") + else: + lines.append(" uint256[] memory abilities = new uint256[](0);") - # Generate metadata arrays with sprite and palette data (if color flag is enabled) - if include_color: - total_metadata_count = len(mon.sprite_data) + len(mon.palette_data) - - if total_metadata_count > 0: - lines.extend([ - f" bytes32[] memory keys = new bytes32[]({total_metadata_count});", - f" bytes32[] memory values = new bytes32[]({total_metadata_count});" - ]) - - metadata_index = 0 - - # Add sprite data as IMG_0, IMG_1, IMG_2, etc. - for i, uint256_value in enumerate(mon.sprite_data): - lines.append(f" keys[{metadata_index}] = bytes32(\"IMG_{i}\");") - lines.append(f" values[{metadata_index}] = bytes32(uint256({uint256_value}));") - metadata_index += 1 - - # Add palette data as PAL_0, PAL_1, PAL_2, etc. - for i, uint256_value in enumerate(mon.palette_data): - lines.append(f" keys[{metadata_index}] = bytes32(\"PAL_{i}\");") - lines.append(f" values[{metadata_index}] = bytes32(uint256({uint256_value}));") - metadata_index += 1 - else: - lines.extend([ - " bytes32[] memory keys = new bytes32[](0);", - " bytes32[] memory values = new bytes32[](0);" - ]) + # Generate metadata arrays with sprite and palette data (if color flag is enabled) + if include_color: + total_metadata_count = len(mon.sprite_data) + len(mon.palette_data) + + if total_metadata_count > 0: + lines.extend([ + f" bytes32[] memory keys = new bytes32[]({total_metadata_count});", + f" bytes32[] memory values = new bytes32[]({total_metadata_count});" + ]) + + metadata_index = 0 + + # Add sprite data as IMG_0, IMG_1, IMG_2, etc. + for i, uint256_value in enumerate(mon.sprite_data): + lines.append(f" keys[{metadata_index}] = bytes32(\"IMG_{i}\");") + lines.append(f" values[{metadata_index}] = bytes32(uint256({uint256_value}));") + metadata_index += 1 + + # Add palette data as PAL_0, PAL_1, PAL_2, etc. + for i, uint256_value in enumerate(mon.palette_data): + lines.append(f" keys[{metadata_index}] = bytes32(\"PAL_{i}\");") + lines.append(f" values[{metadata_index}] = bytes32(uint256({uint256_value}));") + metadata_index += 1 else: - # No color data - empty metadata arrays lines.extend([ " bytes32[] memory keys = new bytes32[](0);", " bytes32[] memory values = new bytes32[](0);" ]) + else: + # No color data - empty metadata arrays + lines.extend([ + " bytes32[] memory keys = new bytes32[](0);", + " bytes32[] memory values = new bytes32[](0);" + ]) - # Generate createMon call - lines.append(f" registry.createMon({mon.mon_id}, stats, moves, abilities, keys, values);") - lines.append(" }") - lines.append("") + # Generate createMon call + lines.append(f" registry.createMon({mon.mon_id}, stats, moves, abilities, keys, values);") + lines.append(" }") return lines -def generate_solidity_script(mons: Dict[str, MonData], contracts: Dict[str, ContractInfo], base_path: str, include_color: bool = False) -> str: - """Generate the complete Solidity deployment script""" +def mon_contract_name(mon: MonData) -> str: + """Solidity-safe mon name (spaces dropped).""" + return mon.name.replace(" ", "") - # Generate imports - imports = [ - "// SPDX-License-Identifier: AGPL-3.0", - "// Created by mon_stats_to_sol.py", - "pragma solidity ^0.8.0;", - "", - "import {Script} from \"forge-std/Script.sol\";", - "import {GachaTeamRegistry} from \"../src/game-layer/GachaTeamRegistry.sol\";", - "import {MonStats} from \"../src/Structs.sol\";", - "import {Type} from \"../src/Enums.sol\";", - "" - ] - # Collect all import paths and deduplicate - all_import_paths = set() +def mon_script_rel_path(mon: MonData) -> str: + """Repo-relative path of a mon's generated deploy script.""" + return f"script/mons/Setup{mon_contract_name(mon)}.s.sol" - # Add contract imports for main contracts (moves/abilities) - for contract in contracts.values(): - mon_dir = None - for mon in mons.values(): - if contract.name in mon.moves or contract.name in mon.abilities: - mon_dir = get_mon_directory_name(mon.name) - break - - if mon_dir: - contract_name = contract_name_from_move_or_ability(contract.name) - import_path = f"../src/mons/{mon_dir}/{contract_name}.sol" - all_import_paths.add((contract_name, import_path)) - - # Add imports for dependencies of this contract - for dep_path in contract.import_paths: - # Convert absolute path to relative import path - rel_path = os.path.relpath(dep_path, base_path) - rel_path = "../" + rel_path.replace("\\", "/") # Convert to relative and use forward slashes - - # Extract contract name from file path - dep_contract_name = os.path.splitext(os.path.basename(dep_path))[0] - all_import_paths.add((dep_contract_name, rel_path)) - - # Sort imports for consistent output - sorted_imports = sorted(all_import_paths, key=lambda x: x[1]) - for contract_name, import_path in sorted_imports: - imports.append(f"import {{{contract_name}}} from \"{import_path}\";") - - imports.append("") - - # Generate contract header and main run function - contract_lines = [ - "struct DeployData {", - " string name;", - " address contractAddress;", - "}", - "contract SetupMons is Script {", - " function run() external returns (DeployData[] memory deployedContracts) {", - " vm.startBroadcast();", - "", - " // Get the GachaTeamRegistry address", - " GachaTeamRegistry registry = GachaTeamRegistry(vm.envAddress(\"GACHA_TEAM_REGISTRY\"));", - "" + +def mon_script_targets(base_path: str = ".") -> List[str]: + """forge `path:contract` targets for every mon, in mon-id order. + + createMon rejects non-sequential ids, so id order is deploy order. The contract is named + explicitly because each file also holds the abstract deploy base the gas tests inherit. + """ + mons = read_mons_csv(os.path.join(base_path, "drool", "mons.csv")) + return [ + f"{mon_script_rel_path(mon)}:Setup{mon_contract_name(mon)}" + for mon in sorted(mons.values(), key=lambda m: m.mon_id) ] - # Add calls to individual deploy functions and collect deployment data - sorted_mons = sorted(mons.values(), key=lambda m: m.mon_id) - num_mons = len(sorted_mons) - contract_lines.extend([ - " // Deploy all mons and collect deployment data", - f" DeployData[][] memory allDeployData = new DeployData[][]({num_mons});", - "" - ]) +def generate_mon_script(mon: MonData, base_path: str, include_color: bool = False) -> str: + """Generate one mon's standalone deploy script. - # Generate calls to collect deployment data from each function - for i, mon in enumerate(sorted_mons): - function_name = f"deploy{mon.name.replace(' ', '')}" - contract_lines.append(f" allDeployData[{i}] = {function_name}(registry);") + The recipe sits in an abstract base so tests can inherit several mons at once; only the + concrete Setup carries run() and the broadcast. + """ + name = mon_contract_name(mon) + mon_contracts = get_contracts_for_mon(mon, base_path) + mon_dir = get_mon_directory_name(mon.name) - contract_lines.extend([ - "", - " // Calculate total length for flattened array", - " uint256 totalLength = 0;", - " for (uint256 i = 0; i < allDeployData.length; i++) {", - " totalLength += allDeployData[i].length;", - " }", - "", - " // Create flattened array and copy all entries", - " deployedContracts = new DeployData[](totalLength);", - " uint256 currentIndex = 0;", - "", - " // Copy all deployment data using nested loops", - " for (uint256 i = 0; i < allDeployData.length; i++) {", - " for (uint256 j = 0; j < allDeployData[i].length; j++) {", - " deployedContracts[currentIndex] = allDeployData[i][j];", - " currentIndex++;", - " }", - " }", + lines = [ + "// SPDX-License-Identifier: AGPL-3.0", + "// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand.", + "pragma solidity ^0.8.0;", "", - " vm.stopBroadcast();", - " }", - "" - ]) + "import {Script} from \"forge-std/Script.sol\";", + "import {DeployData} from \"../DeployData.sol\";", + "import {Type} from \"../../src/Enums.sol\";", + "import {MonStats} from \"../../src/Structs.sol\";", + "import {GachaTeamRegistry} from \"../../src/game-layer/GachaTeamRegistry.sol\";", + ] - # Generate individual deploy functions for each mon - deploy_functions = [] - for mon in sorted(mons.values(), key=lambda m: m.mon_id): - deploy_functions.extend(generate_deploy_function_for_mon(mon, base_path, include_color)) + # This mon's move/ability contracts, plus the contract types their constructors take. + import_paths = set() + for contract in mon_contracts.values(): + contract_name = contract_name_from_move_or_ability(contract.name) + import_paths.add((contract_name, f"../../src/mons/{mon_dir}/{contract_name}.sol")) + for dep_path in contract.import_paths: + rel_path = os.path.relpath(dep_path, base_path).replace("\\", "/") + dep_contract_name = os.path.splitext(os.path.basename(dep_path))[0] + import_paths.add((dep_contract_name, f"../../{rel_path}")) - # Generate contract footer - contract_footer = ["}"] + for contract_name, import_path in sorted(import_paths, key=lambda x: x[1]): + lines.append(f"import {{{contract_name}}} from \"{import_path}\";") - # Combine all parts - all_lines = imports + contract_lines + deploy_functions + contract_footer - return "\n".join(all_lines) + lines.append("") + lines.append(f"abstract contract {name}Deploy is Script {{") + lines.extend(generate_deploy_function_for_mon(mon, base_path, include_color)) + lines.append("}") + lines.append("") + lines.append(f"contract Setup{name} is {name}Deploy {{") + lines.append(" function run() external returns (DeployData[] memory deployedContracts) {") + lines.append(" vm.startBroadcast();") + lines.append(f" deployedContracts = deploy{name}(GachaTeamRegistry(vm.envAddress(\"GACHA_TEAM_REGISTRY\")));") + lines.append(" vm.stopBroadcast();") + lines.append(" }") + lines.append("}") + lines.append("") + + return "\n".join(lines) def run(include_color: bool = False, base_path: str = ".") -> bool: @@ -758,20 +723,30 @@ def run(include_color: bool = False, base_path: str = ".") -> bool: contracts = collect_all_contracts(mons, base_path) print(f"Found {len(contracts)} unique contracts to deploy") - # Generate Solidity script - solidity_code = generate_solidity_script(mons, contracts, base_path, include_color) + # Generate one deploy script per mon + script_dir = os.path.join(base_path, "script", "mons") + os.makedirs(script_dir, exist_ok=True) - # Write to output file - output_path = os.path.join(base_path, "script", "SetupMons.s.sol") - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - with open(output_path, 'w', encoding='utf-8') as f: - f.write(solidity_code) - - print(f"Generated deployment script: {output_path}") + written = set() + for mon in sorted(mons.values(), key=lambda m: m.mon_id): + output_path = os.path.join(base_path, mon_script_rel_path(mon)) + with open(output_path, 'w', encoding='utf-8') as f: + f.write(generate_mon_script(mon, base_path, include_color)) + written.add(os.path.basename(output_path)) + + print(f"Generated {len(written)} deployment scripts in {script_dir}") + + # Drop scripts for renamed/removed mons, plus the pre-split monolith. + for stale in sorted(f for f in os.listdir(script_dir) if f.endswith(".s.sol") and f not in written): + os.remove(os.path.join(script_dir, stale)) + print(f"Removed stale script/mons/{stale}") + legacy_path = os.path.join(base_path, "script", "SetupMons.s.sol") + if os.path.exists(legacy_path): + os.remove(legacy_path) + print("Removed legacy script/SetupMons.s.sol") if include_color: - print("Color data included in deployment script") + print("Color data included in deployment scripts") else: print("Color data not included (use --color flag to include)") diff --git a/processing/packMoves.py b/processing/packMoves.py index 33fd4a8c..e76740f1 100644 --- a/processing/packMoves.py +++ b/processing/packMoves.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 """ -Pack JSON move definitions into uint256 inline slot values. +Pack inline move definitions into uint256 inline slot values. + +Every packed value is sourced from the move's drool/moves.csv row; the per-move .json file +only marks the move as inline and names its effect contract. Packed format (256 bits total): [basePower:8 | moveClass:2 | priority:2 | moveType:4 | stamina:4 | effectAccuracy:8 | unused:68 | effect:160] @@ -10,9 +13,11 @@ accuracy=100, critRate=5, volatility=10 """ +import csv import json import os -from typing import Dict, Optional, Tuple +import re +from typing import Dict, Optional from types_enum import TYPE_INDEX as TYPE_MAP # name -> enum index, from src/Enums.sol @@ -20,23 +25,58 @@ CLASS_MAP = {"Physical": 0, "Special": 1, "Self": 2, "Other": 3} -def pack_move(move_json: dict, effect_address: int = 0) -> int: - """Pack a JSON move definition into a uint256 value. +def parse_constants(raw: str) -> Dict[str, int]: + """Parse the Constants column ('NAME=VAL;NAME=VAL') into a {NAME: int} dict.""" + consts: Dict[str, int] = {} + for part in (raw or "").split(";"): + part = part.strip() + if not part: + continue + name, _, val = part.partition("=") + consts[name.strip()] = int(val.strip()) + return consts + + +def contract_name_from_move(move_name: str) -> str: + """Move display name -> its contract/file base name ('Pound Ground' -> 'PoundGround').""" + return re.sub(r"\W+", "", move_name.replace(" ", "")) + + +def inline_params_from_csv(row: dict) -> dict: + """Build an inline move's packing params from its moves.csv row. + + moves.csv is the source of truth for every packed field; the .json file supplies only + `effect`. CSV Priority is already the offset from DEFAULT_PRIORITY that the inline word + stores, so it maps across unchanged (unlike deployed words, which store absolute priority). + """ + consts = parse_constants(row.get("Constants", "")) + return { + "basePower": int(row["Power"]), + "staminaCost": int(row["Stamina"]), + "moveType": row["Type"].strip(), + "moveClass": row["Class"].strip(), + "priority": int((row.get("Priority") or "0").strip() or "0"), + "effectAccuracy": consts.get("EFFECT_ACCURACY", 0), + } + + +def pack_move(move_params: dict, effect_address: int = 0) -> int: + """Pack an inline move definition into a uint256 value. Args: - move_json: Parsed JSON move data with keys: basePower, staminaCost, moveType, - moveClass, effectAccuracy, and optionally priority (offset from default). + move_params: Params from inline_params_from_csv: basePower, staminaCost, moveType, + moveClass, effectAccuracy, and priority (offset from default). effect_address: Deployed address of the IEffect contract (0 for no effect). Returns: Packed uint256 value with inline move data. """ - base_power = move_json["basePower"] - move_class = CLASS_MAP[move_json["moveClass"]] - priority_offset = move_json.get("priority", 0) # offset from DEFAULT_PRIORITY - move_type = TYPE_MAP[move_json["moveType"]] - stamina = move_json["staminaCost"] - effect_accuracy = move_json["effectAccuracy"] + base_power = move_params["basePower"] + move_class = CLASS_MAP[move_params["moveClass"]] + priority_offset = move_params.get("priority", 0) # offset from DEFAULT_PRIORITY + move_type = TYPE_MAP[move_params["moveType"]] + stamina = move_params["staminaCost"] + effect_accuracy = move_params["effectAccuracy"] # Validate ranges assert 0 <= base_power <= 255, f"basePower {base_power} out of range [0, 255]" @@ -60,7 +100,7 @@ def pack_move(move_json: dict, effect_address: int = 0) -> int: def find_json_moves(src_path: str) -> Dict[str, Dict[str, dict]]: - """Find all JSON move files under src/mons/*/. + """Find all JSON move files under src/mons/*/. A file's presence marks its move as inline. Returns: Dict of {mon_dir_name: {move_name: parsed_json}}. @@ -94,40 +134,10 @@ def find_json_moves(src_path: str) -> Dict[str, Dict[str, dict]]: return result -def pack_all_moves(src_path: str, effect_addresses: Optional[Dict[str, int]] = None) -> Dict[str, Dict[str, Tuple[int, dict]]]: - """Find and pack all JSON moves. - - Args: - src_path: Path to the src/ directory. - effect_addresses: Optional mapping of effect name -> deployed address. - e.g. {"ZapStatus": 0x1234...} - - Returns: - Dict of {mon_dir_name: {move_name: (packed_uint256, json_data)}}. - """ - if effect_addresses is None: - effect_addresses = {} - - json_moves = find_json_moves(src_path) - result = {} - - for mon_dir, moves in json_moves.items(): - result[mon_dir] = {} - for move_name, move_data in moves.items(): - effect_name = move_data.get("effect") - effect_addr = 0 - if effect_name is not None: - if effect_name not in effect_addresses: - raise ValueError( - f"Effect '{effect_name}' required by {mon_dir}/{move_name} " - f"not found in effect_addresses. Available: {list(effect_addresses.keys())}" - ) - effect_addr = effect_addresses[effect_name] - - packed = pack_move(move_data, effect_addr) - result[mon_dir][move_name] = (packed, move_data) - - return result +def load_moves_csv(moves_csv_path: str) -> Dict[str, dict]: + """Load moves.csv keyed by contract name ('PoundGround' -> row).""" + with open(moves_csv_path, "r", newline="", encoding="utf-8") as f: + return {contract_name_from_move(row["Name"].strip()): row for row in csv.DictReader(f)} ABILITY_TYPE_MAP = { @@ -178,21 +188,21 @@ def pack_ability(ability_type_id: int, effect_address: int) -> int: def main(): - """CLI: pack all JSON moves and print results.""" + """CLI: pack all inline moves and print results.""" import sys - src_path = "src" - if len(sys.argv) > 1: - src_path = sys.argv[1] + src_path = sys.argv[1] if len(sys.argv) > 1 else "src" + moves_csv_path = sys.argv[2] if len(sys.argv) > 2 else os.path.join("drool", "moves.csv") json_moves = find_json_moves(src_path) + csv_rows = load_moves_csv(moves_csv_path) print(f"Found JSON moves in {len(json_moves)} mon directories:\n") for mon_dir, moves in sorted(json_moves.items()): for move_name, move_data in sorted(moves.items()): # Pack with effect_address=0 for display (real addresses come at deploy time) - packed = pack_move(move_data, effect_address=0) + packed = pack_move(inline_params_from_csv(csv_rows[move_name]), effect_address=0) effect_info = f" (effect: {move_data['effect']})" if move_data.get("effect") else "" print(f" {mon_dir}/{move_name}: 0x{packed:064x}{effect_info}") # Verify detection: upper 96 bits must be non-zero diff --git a/processing/validateMoves.py b/processing/validateMoves.py index 01bb2548..f0e629f6 100644 --- a/processing/validateMoves.py +++ b/processing/validateMoves.py @@ -5,13 +5,14 @@ """ import csv -import json import os import re from pathlib import Path from typing import Dict, List, Optional, Tuple, Any, Union from dataclasses import dataclass, field +from packMoves import CLASS_MAP, TYPE_MAP, parse_constants + @dataclass class MoveData: """Data structure for move information from CSV""" @@ -28,8 +29,8 @@ class MoveData: # CSV TargetSpec (kebab-case): the move's slot-target domain, checked behaviorally against the # .sol (does move() resolve a defender from targetBits?). Blank in CSV = any-other-slot. target_spec: str = 'any-other-slot' - # Named %/denominator constants the move's .sol must match: [(NAME, VALUE), ...] - constants: List[Tuple[str, int]] = field(default_factory=list) + # Named %/denominator constants the move's .sol must match: {NAME: VALUE} + constants: Dict[str, int] = field(default_factory=dict) @dataclass class ContractData: @@ -76,11 +77,21 @@ class MoveValidator: VALID_TARGET_SPECS = {'any-other-slot', 'none', 'self-only', 'opponent-slot', 'ally-slot', 'any-subset'} SLOT_TARGETING_SPECS = {'any-other-slot', 'opponent-slot', 'ally-slot', 'any-subset'} + # Field widths of the inline move word (see MoveSlotLib.sol / packMoves.pack_move). Inline + # moves are packed straight from moves.csv, so a row that overflows one has no representation. + INLINE_MAX_POWER = 255 + INLINE_MAX_STAMINA = 15 + INLINE_MAX_PRIORITY = 3 + INLINE_MAX_EFFECT_ACCURACY = 255 + INLINE_ACCURACY = 100 + def __init__(self, csv_path: str, src_path: str): self.csv_path = csv_path self.src_path = src_path self.moves_data: Dict[str, MoveData] = {} self.validation_results: List[Dict[str, Any]] = [] + self.inline_count = 0 + self.inline_errors: List[Tuple[str, str]] = [] # (move name, error) # Initialize mon-specific parsing rules self.mon_specific_rules = self._init_mon_specific_rules() @@ -120,17 +131,6 @@ def _parse_int_or_question(self, value: str) -> Union[int, str]: return '?' return int(value) - def _parse_constants(self, raw: str) -> List[Tuple[str, int]]: - """Parse the Constants column: 'NAME=VAL;NAME=VAL' -> [(NAME, VAL), ...]""" - pairs: List[Tuple[str, int]] = [] - for part in (raw or '').split(';'): - part = part.strip() - if not part: - continue - name, _, val = part.partition('=') - pairs.append((name.strip(), int(val.strip()))) - return pairs - def _extract_all_constants(self, content: str) -> Dict[str, int]: """Collect every plain-integer `constant NAME = ;` declaration (hex/expr forms ignored)""" return {m.group(1): int(m.group(2)) @@ -156,7 +156,7 @@ def load_csv_data(self) -> None: description=row['DevDescription'], # Change to UserDescription later unlock_level=int((row.get('UnlockLevel') or '0').strip() or '0'), target_spec=raw_target, - constants=self._parse_constants(row.get('Constants', '')), + constants=parse_constants(row.get('Constants', '')), ) normalized_name = self.normalize_move_name(move_data.name) self.moves_data[normalized_name] = move_data @@ -176,31 +176,50 @@ def find_move_file(self, move_name: str) -> Optional[str]: return None - def parse_json_move(self, file_path: str) -> ContractData: - """Parse a JSON inline move definition""" - contract_data = ContractData(file_path=file_path) - contract_data.is_standard_attack = True # JSON moves are inline StandardAttacks + def check_inline_move(self, move_data: MoveData) -> List[str]: + """Check that a moves.csv row can be expressed as an inline move word. + + Inline (JSON) moves have no contract to compare against — the CSV row is packed + directly by packMoves — so the only failure mode is a row the word can't represent. + """ + errors: List[str] = [] + + if '?' in (move_data.power, move_data.stamina, move_data.priority): + errors.append("dynamic '?' Power/Stamina/Priority needs a .sol move, not an inline word") + else: + if not 0 <= move_data.power <= self.INLINE_MAX_POWER: + errors.append(f"Power {move_data.power} outside inline range [0, {self.INLINE_MAX_POWER}]") + if not 0 <= move_data.stamina <= self.INLINE_MAX_STAMINA: + errors.append(f"Stamina {move_data.stamina} outside inline range [0, {self.INLINE_MAX_STAMINA}]") + if not 0 <= move_data.priority <= self.INLINE_MAX_PRIORITY: + errors.append( + f"Priority {move_data.priority} outside inline range [0, {self.INLINE_MAX_PRIORITY}] " + f"(the packed offset from DEFAULT_PRIORITY is 2 bits, unsigned)") - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) + if move_data.accuracy != self.INLINE_ACCURACY: + errors.append( + f"Accuracy {move_data.accuracy}: inline words have no accuracy field " + f"(always {self.INLINE_ACCURACY}) — use a .sol move") - contract_data.power = data.get('basePower') - contract_data.stamina = data.get('staminaCost') - contract_data.accuracy = 100 # JSON moves always use DEFAULT_ACCURACY - contract_data.move_type = data.get('moveType') - contract_data.move_class = data.get('moveClass') - contract_data.consumes_target = True # inline StandardAttacks dispatch damage to targetBits + if move_data.move_type not in TYPE_MAP: + errors.append(f"Unknown Type {move_data.move_type}") + if move_data.move_class not in CLASS_MAP: + errors.append(f"Unknown Class {move_data.move_class}") - # Priority: JSON stores offset from DEFAULT_PRIORITY, convert to absolute - # (JSON has no priority field = offset 0 = DEFAULT_PRIORITY) - contract_data.priority = self.DEFAULT_PRIORITY + if move_data.target_spec not in self.SLOT_TARGETING_SPECS: + errors.append(f"TargetSpec {move_data.target_spec}: inline moves always dispatch to targetBits") - # Effect trigger chance lives in the effectAccuracy field; expose under EFFECT_ACCURACY. - effect_accuracy = data.get('effectAccuracy') - if effect_accuracy is not None: - contract_data.constants['EFFECT_ACCURACY'] = effect_accuracy + for cname, cval in move_data.constants.items(): + if cname != 'EFFECT_ACCURACY': + errors.append(f"Constant {cname} is not expressible inline (only EFFECT_ACCURACY is packed)") + elif not 0 <= cval <= self.INLINE_MAX_EFFECT_ACCURACY: + errors.append(f"EFFECT_ACCURACY {cval} outside inline range [0, {self.INLINE_MAX_EFFECT_ACCURACY}]") - return contract_data + for token in re.findall(r'\{(\w+)(?::\w+)?\}', move_data.description or ''): + if token not in move_data.constants: + errors.append(f"Description token {{{token}}} has no matching Constants entry") + + return errors def parse_contract_file(self, file_path: str, mon_name: str = None) -> ContractData: """Parse a Solidity contract file to extract move data""" @@ -510,7 +529,7 @@ def validate_move(self, move_name: str, move_data: MoveData, contract_data: Cont f"{move_data.target_spec} expects it to {want} the target nibble") # Validate declared %/denominator constants against the contract source - for cname, cval in move_data.constants: + for cname, cval in move_data.constants.items(): actual = contract_data.constants.get(cname) if actual is None: result['errors'].append(f"Constant {cname} not found in contract (expected: {cval})") @@ -518,9 +537,8 @@ def validate_move(self, move_name: str, move_data: MoveData, contract_data: Cont result['errors'].append(f"Constant {cname} mismatch: contract={actual}, csv={cval}") # Every {NAME} token in the description must resolve to a declared constant - declared = {name for name, _ in move_data.constants} for token in re.findall(r'\{(\w+)(?::\w+)?\}', move_data.description or ''): - if token not in declared: + if token not in move_data.constants: result['errors'].append(f"Description token {{{token}}} has no matching Constants entry") return result @@ -588,11 +606,14 @@ def run_validation(self) -> tuple[bool, bool]: found_contracts += 1 - # Parse and validate the move (JSON or Solidity) + # Inline moves are packed from the CSV row itself, so there is nothing to compare + # against — only whether the row fits the inline word. if move_file.endswith('.json'): - contract_data = self.parse_json_move(move_file) - else: - contract_data = self.parse_contract_file(move_file, move_data.mon) + self.inline_count += 1 + self.inline_errors.extend((move_data.name, e) for e in self.check_inline_move(move_data)) + continue + + contract_data = self.parse_contract_file(move_file, move_data.mon) validation_result = self.validate_move(move_name, move_data, contract_data) self.validation_results.append(validation_result) @@ -605,6 +626,16 @@ def run_validation(self) -> tuple[bool, bool]: if missing_contracts: self.print_missing_contracts(missing_contracts) + # Inline moves are packed straight from the CSV, so a bad row is an authoring mistake the + # contract updater can't fix — fail fast, like the unlock curve. + if self.inline_errors: + print("\n" + "=" * 80) + print("INLINE MOVE ERRORS") + print("=" * 80) + for name, err in self.inline_errors: + print(f" • {name}: {err}") + return (False, False) + # Prompt user to update contracts if there are errors moves_with_errors = sum(1 for result in self.validation_results if result['errors']) if moves_with_errors > 0: @@ -632,16 +663,19 @@ def print_summary(self) -> None: standard_attack_count = sum(1 for result in self.validation_results if result['is_standard_attack']) custom_implementation_count = sum(1 for result in self.validation_results if result['is_custom_implementation']) - print(f"Total moves validated: {total_moves}") - print(f"Moves with errors: {moves_with_errors}") + inline_moves_with_errors = len({name for name, _ in self.inline_errors}) + + print(f"Total moves validated: {total_moves + self.inline_count}") + print(f"Moves with errors: {moves_with_errors + inline_moves_with_errors}") print(f"Moves with warnings: {moves_with_warnings}") print(f"StandardAttack implementations: {standard_attack_count}") print(f"Custom IMoveSet implementations: {custom_implementation_count}") + print(f"Inline moves (packed from CSV): {self.inline_count}") - if moves_with_errors == 0: + if moves_with_errors == 0 and not self.inline_errors: print("\n✅ All validations passed!") else: - print(f"\n❌ {moves_with_errors} moves have validation errors") + print(f"\n❌ {moves_with_errors + inline_moves_with_errors} moves have validation errors") def print_detailed_errors(self) -> None: """Print detailed error information""" @@ -698,10 +732,6 @@ def update_contract_file(self, result: Dict[str, Any]) -> bool: move_name = result['normalized_name'] move_data = self.moves_data[move_name] - # JSON inline moves use a different update path - if file_path.endswith('.json'): - return self._update_json_move(file_path, move_data) - with open(file_path, 'r', encoding='utf-8') as file: content = file.read() @@ -742,7 +772,7 @@ def update_contract_file(self, result: Dict[str, Any]) -> bool: modified = True # Update EFFECT_ACCURACY (effect trigger chance) if declared in the Constants column - consts = dict(move_data.constants) + consts = move_data.constants if 'EFFECT_ACCURACY' in consts: ea_pattern = r'(EFFECT_ACCURACY:\s*)(\d+)' if re.search(ea_pattern, updated_params): @@ -859,7 +889,7 @@ def update_contract_file(self, result: Dict[str, Any]) -> bool: # Fix declared constant drift (name already present, value wrong). A missing constant # can't be auto-added — it's reported and left for manual placement. - for cname, cval in move_data.constants: + for cname, cval in move_data.constants.items(): pattern = rf'(constant\s+{re.escape(cname)}\s*=\s*)\d+' new_content, n = re.subn(pattern, rf'\g<1>{cval}', content) if n and new_content != content: @@ -874,51 +904,6 @@ def update_contract_file(self, result: Dict[str, Any]) -> bool: return False - def _update_json_move(self, file_path: str, move_data: MoveData) -> bool: - """Update a JSON inline move definition with CSV values. Returns True if file was modified.""" - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - modified = False - - if move_data.power != '?' and isinstance(move_data.power, int): - if data.get('basePower') != move_data.power: - data['basePower'] = move_data.power - modified = True - - if move_data.stamina != '?' and isinstance(move_data.stamina, int): - if data.get('staminaCost') != move_data.stamina: - data['staminaCost'] = move_data.stamina - modified = True - - if move_data.move_type and data.get('moveType') != move_data.move_type: - data['moveType'] = move_data.move_type - modified = True - - if move_data.move_class and data.get('moveClass') != move_data.move_class: - data['moveClass'] = move_data.move_class - modified = True - - # Update effectAccuracy (effect trigger chance) if declared in the Constants column - consts = dict(move_data.constants) - if 'EFFECT_ACCURACY' in consts and data.get('effectAccuracy') != consts['EFFECT_ACCURACY']: - data['effectAccuracy'] = consts['EFFECT_ACCURACY'] - modified = True - - # JSON inline moves can't represent non-default accuracy or priority - if move_data.accuracy != '?' and isinstance(move_data.accuracy, int) and move_data.accuracy != 100: - print(f" ⚠️ JSON inline moves only support DEFAULT_ACCURACY (100); CSV wants {move_data.accuracy}. Convert to .sol to express this.") - if move_data.priority != '?' and isinstance(move_data.priority, int) and move_data.priority != 0: - print(f" ⚠️ JSON inline moves only support DEFAULT_PRIORITY; CSV priority offset is {move_data.priority}. Convert to .sol to express this.") - - if modified: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2) - f.write('\n') - return True - - return False - def update_all_contracts(self) -> None: """Update all contract files with CSV values where there are mismatches""" print("\n" + "="*80) @@ -930,12 +915,7 @@ def update_all_contracts(self) -> None: if not result['errors']: continue - if result['contract_file'].endswith('.json'): - impl_type = "JSON inline" - elif result['is_standard_attack']: - impl_type = "StandardAttack" - else: - impl_type = "Custom IMoveSet" + impl_type = "StandardAttack" if result['is_standard_attack'] else "Custom IMoveSet" print(f"\n Updating ({impl_type}): {result['contract_file']}") if self.update_contract_file(result): diff --git a/script/DeployData.sol b/script/DeployData.sol new file mode 100644 index 00000000..19efd1c7 --- /dev/null +++ b/script/DeployData.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +/// @dev Return shape of every deploy script; processing/deploy.py parses it out of forge's output. +struct DeployData { + string name; + address contractAddress; +} diff --git a/script/SetupMons.s.sol b/script/SetupMons.s.sol deleted file mode 100644 index aa3730a9..00000000 --- a/script/SetupMons.s.sol +++ /dev/null @@ -1,966 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -// Created by mon_stats_to_sol.py -pragma solidity ^0.8.0; - -import {Script} from "forge-std/Script.sol"; -import {GachaTeamRegistry} from "../src/game-layer/GachaTeamRegistry.sol"; -import {MonStats} from "../src/Structs.sol"; -import {Type} from "../src/Enums.sol"; - -import {IEffect} from "../src/effects/IEffect.sol"; -import {Overclock} from "../src/effects/battlefield/Overclock.sol"; -import {Storm} from "../src/effects/battlefield/Storm.sol"; -import {BigBellow} from "../src/mons/aurox/BigBellow.sol"; -import {BullRush} from "../src/mons/aurox/BullRush.sol"; -import {GildedRecovery} from "../src/mons/aurox/GildedRecovery.sol"; -import {IronWall} from "../src/mons/aurox/IronWall.sol"; -import {UpOnly} from "../src/mons/aurox/UpOnly.sol"; -import {VolatilePunch} from "../src/mons/aurox/VolatilePunch.sol"; -import {BubbleBop} from "../src/mons/ekineki/BubbleBop.sol"; -import {NineNineNine} from "../src/mons/ekineki/NineNineNine.sol"; -import {Overflow} from "../src/mons/ekineki/Overflow.sol"; -import {SaviorComplex} from "../src/mons/ekineki/SaviorComplex.sol"; -import {SneakAttack} from "../src/mons/ekineki/SneakAttack.sol"; -import {HeatBeacon} from "../src/mons/embursa/HeatBeacon.sol"; -import {HoneyBribe} from "../src/mons/embursa/HoneyBribe.sol"; -import {Q5} from "../src/mons/embursa/Q5.sol"; -import {SetAblaze} from "../src/mons/embursa/SetAblaze.sol"; -import {Tinderclaws} from "../src/mons/embursa/Tinderclaws.sol"; -import {EternalGrudge} from "../src/mons/ghouliath/EternalGrudge.sol"; -import {GraveAffliction} from "../src/mons/ghouliath/GraveAffliction.sol"; -import {InfernalFlame} from "../src/mons/ghouliath/InfernalFlame.sol"; -import {RiseFromTheGrave} from "../src/mons/ghouliath/RiseFromTheGrave.sol"; -import {WitherAway} from "../src/mons/ghouliath/WitherAway.sol"; -import {ChosenOne} from "../src/mons/glob/ChosenOne.sol"; -import {GetSlippery} from "../src/mons/glob/GetSlippery.sol"; -import {HolyMolt} from "../src/mons/glob/HolyMolt.sol"; -import {Angery} from "../src/mons/gorillax/Angery.sol"; -import {RockPull} from "../src/mons/gorillax/RockPull.sol"; -import {Baselight} from "../src/mons/iblivion/Baselight.sol"; -import {Brightback} from "../src/mons/iblivion/Brightback.sol"; -import {Loop} from "../src/mons/iblivion/Loop.sol"; -import {Renormalize} from "../src/mons/iblivion/Renormalize.sol"; -import {UnboundedStrike} from "../src/mons/iblivion/UnboundedStrike.sol"; -import {ChainExpansion} from "../src/mons/inutia/ChainExpansion.sol"; -import {HitAndDip} from "../src/mons/inutia/HitAndDip.sol"; -import {Initialize} from "../src/mons/inutia/Initialize.sol"; -import {Interweaving} from "../src/mons/inutia/Interweaving.sol"; -import {Sanctify} from "../src/mons/inutia/Sanctify.sol"; -import {ActusReus} from "../src/mons/malalien/ActusReus.sol"; -import {FoulLanguage} from "../src/mons/malalien/FoulLanguage.sol"; -import {TripleThink} from "../src/mons/malalien/TripleThink.sol"; -import {CloudStrike} from "../src/mons/mulong/CloudStrike.sol"; -import {KingsLeisure} from "../src/mons/mulong/KingsLeisure.sol"; -import {KingsRespite} from "../src/mons/mulong/KingsRespite.sol"; -import {SummonStorm} from "../src/mons/mulong/SummonStorm.sol"; -import {Adaptor} from "../src/mons/nirvamma/Adaptor.sol"; -import {Chronoffense} from "../src/mons/nirvamma/Chronoffense.sol"; -import {HardReset} from "../src/mons/nirvamma/HardReset.sol"; -import {ModalBolt} from "../src/mons/nirvamma/ModalBolt.sol"; -import {Deadlift} from "../src/mons/pengym/Deadlift.sol"; -import {DeepFreeze} from "../src/mons/pengym/DeepFreeze.sol"; -import {PistolSquat} from "../src/mons/pengym/PistolSquat.sol"; -import {PostWorkout} from "../src/mons/pengym/PostWorkout.sol"; -import {CarrotHarvest} from "../src/mons/sofabbi/CarrotHarvest.sol"; -import {Gachachacha} from "../src/mons/sofabbi/Gachachacha.sol"; -import {GuestFeature} from "../src/mons/sofabbi/GuestFeature.sol"; -import {SnackBreak} from "../src/mons/sofabbi/SnackBreak.sol"; -import {DualShock} from "../src/mons/volthare/DualShock.sol"; -import {MegaStarBlast} from "../src/mons/volthare/MegaStarBlast.sol"; -import {PreemptiveShock} from "../src/mons/volthare/PreemptiveShock.sol"; -import {Quickstorm} from "../src/mons/volthare/Quickstorm.sol"; -import {RoundTrip} from "../src/mons/volthare/RoundTrip.sol"; -import {ContagiousSlumber} from "../src/mons/xmon/ContagiousSlumber.sol"; -import {Dreamcatcher} from "../src/mons/xmon/Dreamcatcher.sol"; -import {InvokeTaboo} from "../src/mons/xmon/InvokeTaboo.sol"; -import {OldVengeance} from "../src/mons/xmon/OldVengeance.sol"; -import {Somniphobia} from "../src/mons/xmon/Somniphobia.sol"; -import {VitalSiphon} from "../src/mons/xmon/VitalSiphon.sol"; -import {ITypeCalculator} from "../src/types/ITypeCalculator.sol"; - -struct DeployData { - string name; - address contractAddress; -} -contract SetupMons is Script { - function run() external returns (DeployData[] memory deployedContracts) { - vm.startBroadcast(); - - // Get the GachaTeamRegistry address - GachaTeamRegistry registry = GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY")); - - // Deploy all mons and collect deployment data - DeployData[][] memory allDeployData = new DeployData[][](15); - - allDeployData[0] = deployGhouliath(registry); - allDeployData[1] = deployInutia(registry); - allDeployData[2] = deployMalalien(registry); - allDeployData[3] = deployIblivion(registry); - allDeployData[4] = deployGorillax(registry); - allDeployData[5] = deploySofabbi(registry); - allDeployData[6] = deployPengym(registry); - allDeployData[7] = deployEmbursa(registry); - allDeployData[8] = deployVolthare(registry); - allDeployData[9] = deployAurox(registry); - allDeployData[10] = deployXmon(registry); - allDeployData[11] = deployEkineki(registry); - allDeployData[12] = deployNirvamma(registry); - allDeployData[13] = deployGlob(registry); - allDeployData[14] = deployMulong(registry); - - // Calculate total length for flattened array - uint256 totalLength = 0; - for (uint256 i = 0; i < allDeployData.length; i++) { - totalLength += allDeployData[i].length; - } - - // Create flattened array and copy all entries - deployedContracts = new DeployData[](totalLength); - uint256 currentIndex = 0; - - // Copy all deployment data using nested loops - for (uint256 i = 0; i < allDeployData.length; i++) { - for (uint256 j = 0; j < allDeployData[i].length; j++) { - deployedContracts[currentIndex] = allDeployData[i][j]; - currentIndex++; - } - } - - vm.stopBroadcast(); - } - - function deployGhouliath(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](5); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[5] memory addrs; - - { - addrs[0] = address(new EternalGrudge()); - deployedContracts[0] = DeployData({name: "Eternal Grudge", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new InfernalFlame(ITypeCalculator(typecalculator), IEffect(vm.envAddress("BURN_STATUS")))); - deployedContracts[1] = DeployData({name: "Infernal Flame", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new WitherAway(ITypeCalculator(typecalculator), IEffect(vm.envAddress("PANIC_STATUS")))); - deployedContracts[2] = DeployData({name: "Wither Away", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new GraveAffliction()); - deployedContracts[3] = DeployData({name: "Grave Affliction", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new RiseFromTheGrave()); - deployedContracts[4] = DeployData({name: "Rise From The Grave", contractAddress: addrs[4]}); - } - - _registerGhouliath(registry, addrs); - - return deployedContracts; - } - - function _registerGhouliath(GachaTeamRegistry registry, address[5] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 303, - stamina: 5, - speed: 181, - attack: 157, - defense: 202, - specialAttack: 151, - specialDefense: 202, - type1: Type.Yin, - type2: Type.Fire - }); - uint256[] memory moves = new uint256[](5); - moves[0] = 0x0040200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[3] = 0x5a00200000000000000000000000000000000000000000000000000000000000; - moves[4] = 0x0030200000000000000000030000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = uint256(uint160(addrs[4])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(0, stats, moves, abilities, keys, values); - } - - function deployInutia(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](5); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[5] memory addrs; - - { - addrs[0] = address(new ChainExpansion(ITypeCalculator(typecalculator))); - deployedContracts[0] = DeployData({name: "Chain Expansion", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new Initialize()); - deployedContracts[1] = DeployData({name: "Initialize", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new HitAndDip(ITypeCalculator(typecalculator))); - deployedContracts[2] = DeployData({name: "Hit And Dip", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new Sanctify(IEffect(vm.envAddress("BLESSED_STATUS")))); - deployedContracts[3] = DeployData({name: "Sanctify", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new Interweaving()); - deployedContracts[4] = DeployData({name: "Interweaving", contractAddress: addrs[4]}); - } - - _registerInutia(registry, addrs); - - return deployedContracts; - } - - function _registerInutia(GachaTeamRegistry registry, address[5] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 351, - stamina: 5, - speed: 229, - attack: 171, - defense: 189, - specialAttack: 175, - specialDefense: 192, - type1: Type.Faith, - type2: Type.None - }); - uint256[] memory moves = new uint256[](5); - moves[0] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x5009200000000000000000000000000000000000000000000000000000000000; - moves[3] = 0x0030200000000000000000050000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = uint256(uint160(addrs[4])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(1, stats, moves, abilities, keys, values); - } - - function deployMalalien(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](3); - - address[3] memory addrs; - - { - addrs[0] = address(new TripleThink()); - deployedContracts[0] = DeployData({name: "Triple Think", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new FoulLanguage(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); - deployedContracts[1] = DeployData({name: "Foul Language", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new ActusReus()); - deployedContracts[2] = DeployData({name: "Actus Reus", contractAddress: addrs[2]}); - } - - _registerMalalien(registry, addrs); - - return deployedContracts; - } - - function _registerMalalien(GachaTeamRegistry registry, address[3] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 258, - stamina: 5, - speed: 308, - attack: 121, - defense: 125, - specialAttack: 322, - specialDefense: 151, - type1: Type.Cyber, - type2: Type.None - }); - uint256[] memory moves = new uint256[](5); - moves[0] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x644c300000000000000000000000000000000000000000000000000000000000; - moves[2] = 0x504b30a000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("PANIC_STATUS"))); - moves[3] = 0x5a4d30a000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("SLEEP_STATUS"))); - moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[2])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(2, stats, moves, abilities, keys, values); - } - - function deployIblivion(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](5); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[5] memory addrs; - - { - addrs[0] = address(new Baselight()); - deployedContracts[0] = DeployData({name: "Baselight", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new UnboundedStrike(ITypeCalculator(typecalculator), Baselight(addrs[0]))); - deployedContracts[1] = DeployData({name: "Unbounded Strike", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new Loop(Baselight(addrs[0]))); - deployedContracts[2] = DeployData({name: "Loop", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new Brightback(ITypeCalculator(typecalculator), Baselight(addrs[0]))); - deployedContracts[3] = DeployData({name: "Brightback", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new Renormalize(Baselight(addrs[0]), Loop(addrs[2]))); - deployedContracts[4] = DeployData({name: "Renormalize", contractAddress: addrs[4]}); - } - - _registerIblivion(registry, addrs); - - return deployedContracts; - } - - function _registerIblivion(GachaTeamRegistry registry, address[5] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 277, - stamina: 5, - speed: 256, - attack: 199, - defense: 164, - specialAttack: 180, - specialDefense: 168, - type1: Type.Yang, - type2: Type.Air - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x0030f00000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[1] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[2] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - moves[3] = 0x0020000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[4])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = uint256(uint160(addrs[0])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(3, stats, moves, abilities, keys, values); - } - - function deployGorillax(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](2); - - address[2] memory addrs; - - { - addrs[0] = address(new RockPull(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); - deployedContracts[0] = DeployData({name: "Rock Pull", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new Angery()); - deployedContracts[1] = DeployData({name: "Angery", contractAddress: addrs[1]}); - } - - _registerGorillax(registry, addrs); - - return deployedContracts; - } - - function _registerGorillax(GachaTeamRegistry registry, address[2] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 407, - stamina: 5, - speed: 129, - attack: 302, - defense: 175, - specialAttack: 112, - specialDefense: 176, - type1: Type.Earth, - type2: Type.None - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x00f0300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x5f02300000000000000000000000000000000000000000000000000000000000; - moves[2] = 0x460a200000000000000000000000000000000000000000000000000000000000; - moves[3] = 0x2802100000000000000000000000000000000000000000000000000000000000; - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[1])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(4, stats, moves, abilities, keys, values); - } - - function deploySofabbi(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](4); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[4] memory addrs; - - { - addrs[0] = address(new Gachachacha(ITypeCalculator(typecalculator))); - deployedContracts[0] = DeployData({name: "Gachachacha", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new GuestFeature(ITypeCalculator(typecalculator))); - deployedContracts[1] = DeployData({name: "Guest Feature", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new SnackBreak()); - deployedContracts[2] = DeployData({name: "Snack Break", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new CarrotHarvest()); - deployedContracts[3] = DeployData({name: "Carrot Harvest", contractAddress: addrs[3]}); - } - - _registerSofabbi(registry, addrs); - - return deployedContracts; - } - - function _registerSofabbi(GachaTeamRegistry registry, address[4] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 333, - stamina: 5, - speed: 175, - attack: 180, - defense: 201, - specialAttack: 120, - specialDefense: 269, - type1: Type.Nature, - type2: Type.None - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x7807400000000000000000000000000000000000000000000000000000000000; - moves[3] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[3])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(5, stats, moves, abilities, keys, values); - } - - function deployPengym(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](4); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[4] memory addrs; - - { - addrs[0] = address(new Deadlift()); - deployedContracts[0] = DeployData({name: "Deadlift", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new DeepFreeze(ITypeCalculator(typecalculator), IEffect(vm.envAddress("FROSTBITE_STATUS")))); - deployedContracts[1] = DeployData({name: "Deep Freeze", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new PistolSquat(ITypeCalculator(typecalculator))); - deployedContracts[2] = DeployData({name: "Pistol Squat", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new PostWorkout()); - deployedContracts[3] = DeployData({name: "Post-Workout", contractAddress: addrs[3]}); - } - - _registerPengym(registry, addrs); - - return deployedContracts; - } - - function _registerPengym(GachaTeamRegistry registry, address[4] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 371, - stamina: 5, - speed: 149, - attack: 212, - defense: 191, - specialAttack: 233, - specialDefense: 172, - type1: Type.Ice, - type2: Type.None - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x00c6064000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("FROSTBITE_STATUS"))); - moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[2] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[3] = 0x0020200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[3])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(6, stats, moves, abilities, keys, values); - } - - function deployEmbursa(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](5); - - // Cache commonly used addresses - address burnstatus = vm.envAddress("BURN_STATUS"); - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[5] memory addrs; - - { - addrs[0] = address(new HoneyBribe()); - deployedContracts[0] = DeployData({name: "Honey Bribe", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new SetAblaze(ITypeCalculator(typecalculator), IEffect(burnstatus))); - deployedContracts[1] = DeployData({name: "Set Ablaze", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new HeatBeacon(IEffect(burnstatus))); - deployedContracts[2] = DeployData({name: "Heat Beacon", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new Q5(ITypeCalculator(typecalculator))); - deployedContracts[3] = DeployData({name: "Q5", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new Tinderclaws(IEffect(burnstatus))); - deployedContracts[4] = DeployData({name: "Tinderclaws", contractAddress: addrs[4]}); - } - - _registerEmbursa(registry, addrs); - - return deployedContracts; - } - - function _registerEmbursa(GachaTeamRegistry registry, address[5] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 420, - stamina: 5, - speed: 111, - attack: 141, - defense: 220, - specialAttack: 190, - specialDefense: 161, - type1: Type.Fire, - type2: Type.None - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x00f0200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x00f0300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x00f0000000000000000000030000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[3] = 0x00f0200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[4])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(7, stats, moves, abilities, keys, values); - } - - function deployVolthare(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](5); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - address zapstatus = vm.envAddress("ZAP_STATUS"); - - address[5] memory addrs; - - { - addrs[0] = address(new RoundTrip(ITypeCalculator(typecalculator))); - deployedContracts[0] = DeployData({name: "Round Trip", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new MegaStarBlast(ITypeCalculator(typecalculator), IEffect(zapstatus), IEffect(vm.envAddress("OVERCLOCK")))); - deployedContracts[1] = DeployData({name: "Mega Star Blast", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new DualShock(ITypeCalculator(typecalculator), IEffect(zapstatus), Overclock(vm.envAddress("OVERCLOCK")))); - deployedContracts[2] = DeployData({name: "Dual Shock", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new Quickstorm(ITypeCalculator(typecalculator), IEffect(zapstatus))); - deployedContracts[3] = DeployData({name: "Quickstorm", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new PreemptiveShock(ITypeCalculator(typecalculator))); - deployedContracts[4] = DeployData({name: "Preemptive Shock", contractAddress: addrs[4]}); - } - - _registerVolthare(registry, addrs); - - return deployedContracts; - } - - function _registerVolthare(GachaTeamRegistry registry, address[5] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 310, - stamina: 5, - speed: 311, - attack: 120, - defense: 184, - specialAttack: 255, - specialDefense: 176, - type1: Type.Lightning, - type2: Type.Cyber - }); - uint256[] memory moves = new uint256[](5); - moves[0] = 0x5a4820a000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("ZAP_STATUS"))); - moves[1] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[2] = 0x0050300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[3] = 0x0030000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = uint256(uint160(addrs[4])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(8, stats, moves, abilities, keys, values); - } - - function deployAurox(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](6); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[6] memory addrs; - - { - addrs[0] = address(new VolatilePunch(ITypeCalculator(typecalculator), IEffect(vm.envAddress("BURN_STATUS")), IEffect(vm.envAddress("FROSTBITE_STATUS")))); - deployedContracts[0] = DeployData({name: "Volatile Punch", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new GildedRecovery()); - deployedContracts[1] = DeployData({name: "Gilded Recovery", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new IronWall()); - deployedContracts[2] = DeployData({name: "Iron Wall", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new BullRush(ITypeCalculator(typecalculator))); - deployedContracts[3] = DeployData({name: "Bull Rush", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new BigBellow()); - deployedContracts[4] = DeployData({name: "Big Bellow", contractAddress: addrs[4]}); - } - { - addrs[5] = address(new UpOnly()); - deployedContracts[5] = DeployData({name: "Up Only", contractAddress: addrs[5]}); - } - - _registerAurox(registry, addrs); - - return deployedContracts; - } - - function _registerAurox(GachaTeamRegistry registry, address[6] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 400, - stamina: 5, - speed: 100, - attack: 150, - defense: 230, - specialAttack: 100, - specialDefense: 220, - type1: Type.Metal, - type2: Type.None - }); - uint256[] memory moves = new uint256[](5); - moves[0] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - moves[4] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[4])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[5])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(9, stats, moves, abilities, keys, values); - } - - function deployXmon(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](6); - - // Cache commonly used addresses - address sleepstatus = vm.envAddress("SLEEP_STATUS"); - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[6] memory addrs; - - { - addrs[0] = address(new ContagiousSlumber(IEffect(sleepstatus))); - deployedContracts[0] = DeployData({name: "Contagious Slumber", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new VitalSiphon(ITypeCalculator(typecalculator))); - deployedContracts[1] = DeployData({name: "Vital Siphon", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new Somniphobia()); - deployedContracts[2] = DeployData({name: "Somniphobia", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new OldVengeance(ITypeCalculator(typecalculator))); - deployedContracts[3] = DeployData({name: "Old Vengeance", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new InvokeTaboo(IEffect(sleepstatus))); - deployedContracts[4] = DeployData({name: "Invoke Taboo", contractAddress: addrs[4]}); - } - { - addrs[5] = address(new Dreamcatcher()); - deployedContracts[5] = DeployData({name: "Dreamcatcher", contractAddress: addrs[5]}); - } - - _registerXmon(registry, addrs); - - return deployedContracts; - } - - function _registerXmon(GachaTeamRegistry registry, address[6] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 311, - stamina: 5, - speed: 285, - attack: 123, - defense: 179, - specialAttack: 222, - specialDefense: 185, - type1: Type.Cosmic, - type2: Type.None - }); - uint256[] memory moves = new uint256[](5); - moves[0] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[3] = 0x0040400000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - moves[4] = 0x0020100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[4])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[5])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(10, stats, moves, abilities, keys, values); - } - - function deployEkineki(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](5); - - // Cache commonly used addresses - address typecalculator = vm.envAddress("TYPE_CALCULATOR"); - - address[5] memory addrs; - - { - addrs[0] = address(new BubbleBop(ITypeCalculator(typecalculator))); - deployedContracts[0] = DeployData({name: "Bubble Bop", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new SneakAttack(ITypeCalculator(typecalculator))); - deployedContracts[1] = DeployData({name: "Sneak Attack", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new NineNineNine()); - deployedContracts[2] = DeployData({name: "Nine Nine Nine", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new Overflow(ITypeCalculator(typecalculator))); - deployedContracts[3] = DeployData({name: "Overflow", contractAddress: addrs[3]}); - } - { - addrs[4] = address(new SaviorComplex()); - deployedContracts[4] = DeployData({name: "Savior Complex", contractAddress: addrs[4]}); - } - - _registerEkineki(registry, addrs); - - return deployedContracts; - } - - function _registerEkineki(GachaTeamRegistry registry, address[5] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 299, - stamina: 5, - speed: 266, - attack: 130, - defense: 180, - specialAttack: 280, - specialDefense: 175, - type1: Type.Liquid, - type2: Type.None - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x0040f00000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - moves[3] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = uint256(uint160(addrs[4])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(11, stats, moves, abilities, keys, values); - } - - function deployNirvamma(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](4); - - address[4] memory addrs; - - { - addrs[0] = address(new HardReset()); - deployedContracts[0] = DeployData({name: "Hard Reset", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new Chronoffense()); - deployedContracts[1] = DeployData({name: "Chronoffense", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new ModalBolt(IEffect(vm.envAddress("BURN_STATUS")), IEffect(vm.envAddress("FROSTBITE_STATUS")), IEffect(vm.envAddress("ZAP_STATUS")))); - deployedContracts[2] = DeployData({name: "Modal Bolt", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new Adaptor()); - deployedContracts[3] = DeployData({name: "Adaptor", contractAddress: addrs[3]}); - } - - _registerNirvamma(registry, addrs); - - return deployedContracts; - } - - function _registerNirvamma(GachaTeamRegistry registry, address[4] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 373, - stamina: 5, - speed: 177, - attack: 202, - defense: 168, - specialAttack: 140, - specialDefense: 202, - type1: Type.Math, - type2: Type.None - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x500b314000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("PANIC_STATUS"))); - moves[2] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[3] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[3])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(12, stats, moves, abilities, keys, values); - } - - function deployGlob(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](3); - - // Cache commonly used addresses - address blessedstatus = vm.envAddress("BLESSED_STATUS"); - - address[3] memory addrs; - - { - addrs[0] = address(new HolyMolt(IEffect(blessedstatus))); - deployedContracts[0] = DeployData({name: "Holy Molt", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new GetSlippery()); - deployedContracts[1] = DeployData({name: "Get Slippery", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new ChosenOne(IEffect(blessedstatus))); - deployedContracts[2] = DeployData({name: "Chosen One", contractAddress: addrs[2]}); - } - - _registerGlob(registry, addrs); - - return deployedContracts; - } - - function _registerGlob(GachaTeamRegistry registry, address[3] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 280, - stamina: 5, - speed: 140, - attack: 300, - defense: 205, - specialAttack: 115, - specialDefense: 185, - type1: Type.Liquid, - type2: Type.Faith - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x0040f00000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x5003200000000000000000000000000000000000000000000000000000000000; - moves[2] = 0x5a09300000000000000000000000000000000000000000000000000000000000; - moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = uint256(uint160(addrs[2])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(13, stats, moves, abilities, keys, values); - } - - function deployMulong(GachaTeamRegistry registry) internal returns (DeployData[] memory) { - DeployData[] memory deployedContracts = new DeployData[](4); - - // Cache commonly used addresses - address storm = vm.envAddress("STORM"); - - address[4] memory addrs; - - { - addrs[0] = address(new SummonStorm(Storm(storm))); - deployedContracts[0] = DeployData({name: "Summon Storm", contractAddress: addrs[0]}); - } - { - addrs[1] = address(new KingsRespite(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); - deployedContracts[1] = DeployData({name: "King's Respite", contractAddress: addrs[1]}); - } - { - addrs[2] = address(new CloudStrike(Storm(storm), IEffect(vm.envAddress("FROSTBITE_STATUS")))); - deployedContracts[2] = DeployData({name: "Cloud Strike", contractAddress: addrs[2]}); - } - { - addrs[3] = address(new KingsLeisure()); - deployedContracts[3] = DeployData({name: "King's Leisure", contractAddress: addrs[3]}); - } - - _registerMulong(registry, addrs); - - return deployedContracts; - } - - function _registerMulong(GachaTeamRegistry registry, address[4] memory addrs) internal { - MonStats memory stats = MonStats({ - hp: 320, - stamina: 5, - speed: 300, - attack: 115, - defense: 135, - specialAttack: 290, - specialDefense: 145, - type1: Type.Air, - type2: Type.Lightning - }); - uint256[] memory moves = new uint256[](4); - moves[0] = 0x0030000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); - moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); - moves[2] = 0x7848314000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("ZAP_STATUS"))); - moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); - uint256[] memory abilities = new uint256[](1); - abilities[0] = uint256(uint160(addrs[3])); - bytes32[] memory keys = new bytes32[](0); - bytes32[] memory values = new bytes32[](0); - registry.createMon(14, stats, moves, abilities, keys, values); - } - -} \ No newline at end of file diff --git a/script/mons/SetupAurox.s.sol b/script/mons/SetupAurox.s.sol new file mode 100644 index 00000000..0c4922e4 --- /dev/null +++ b/script/mons/SetupAurox.s.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {BigBellow} from "../../src/mons/aurox/BigBellow.sol"; +import {BullRush} from "../../src/mons/aurox/BullRush.sol"; +import {GildedRecovery} from "../../src/mons/aurox/GildedRecovery.sol"; +import {IronWall} from "../../src/mons/aurox/IronWall.sol"; +import {UpOnly} from "../../src/mons/aurox/UpOnly.sol"; +import {VolatilePunch} from "../../src/mons/aurox/VolatilePunch.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract AuroxDeploy is Script { + function deployAurox(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](6); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[6] memory addrs; + + { + addrs[0] = address(new VolatilePunch(ITypeCalculator(typecalculator), IEffect(vm.envAddress("BURN_STATUS")), IEffect(vm.envAddress("FROSTBITE_STATUS")))); + deployedContracts[0] = DeployData({name: "Volatile Punch", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new GildedRecovery()); + deployedContracts[1] = DeployData({name: "Gilded Recovery", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new IronWall()); + deployedContracts[2] = DeployData({name: "Iron Wall", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new BullRush(ITypeCalculator(typecalculator))); + deployedContracts[3] = DeployData({name: "Bull Rush", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new BigBellow()); + deployedContracts[4] = DeployData({name: "Big Bellow", contractAddress: addrs[4]}); + } + { + addrs[5] = address(new UpOnly()); + deployedContracts[5] = DeployData({name: "Up Only", contractAddress: addrs[5]}); + } + + _registerAurox(registry, addrs); + + return deployedContracts; + } + + function _registerAurox(GachaTeamRegistry registry, address[6] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 400, + stamina: 5, + speed: 100, + attack: 150, + defense: 230, + specialAttack: 100, + specialDefense: 220, + type1: Type.Metal, + type2: Type.None + }); + uint256[] memory moves = new uint256[](5); + moves[0] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + moves[4] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[4])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[5])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(9, stats, moves, abilities, keys, values); + } +} + +contract SetupAurox is AuroxDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployAurox(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupEkineki.s.sol b/script/mons/SetupEkineki.s.sol new file mode 100644 index 00000000..0ce88aea --- /dev/null +++ b/script/mons/SetupEkineki.s.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {BubbleBop} from "../../src/mons/ekineki/BubbleBop.sol"; +import {NineNineNine} from "../../src/mons/ekineki/NineNineNine.sol"; +import {Overflow} from "../../src/mons/ekineki/Overflow.sol"; +import {SaviorComplex} from "../../src/mons/ekineki/SaviorComplex.sol"; +import {SneakAttack} from "../../src/mons/ekineki/SneakAttack.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract EkinekiDeploy is Script { + function deployEkineki(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](5); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[5] memory addrs; + + { + addrs[0] = address(new BubbleBop(ITypeCalculator(typecalculator))); + deployedContracts[0] = DeployData({name: "Bubble Bop", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new SneakAttack(ITypeCalculator(typecalculator))); + deployedContracts[1] = DeployData({name: "Sneak Attack", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new NineNineNine()); + deployedContracts[2] = DeployData({name: "Nine Nine Nine", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new Overflow(ITypeCalculator(typecalculator))); + deployedContracts[3] = DeployData({name: "Overflow", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new SaviorComplex()); + deployedContracts[4] = DeployData({name: "Savior Complex", contractAddress: addrs[4]}); + } + + _registerEkineki(registry, addrs); + + return deployedContracts; + } + + function _registerEkineki(GachaTeamRegistry registry, address[5] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 299, + stamina: 5, + speed: 266, + attack: 130, + defense: 180, + specialAttack: 280, + specialDefense: 175, + type1: Type.Liquid, + type2: Type.None + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0040f00000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[3] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[4])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(11, stats, moves, abilities, keys, values); + } +} + +contract SetupEkineki is EkinekiDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployEkineki(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupEmbursa.s.sol b/script/mons/SetupEmbursa.s.sol new file mode 100644 index 00000000..15cd742a --- /dev/null +++ b/script/mons/SetupEmbursa.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {HeatBeacon} from "../../src/mons/embursa/HeatBeacon.sol"; +import {HoneyBribe} from "../../src/mons/embursa/HoneyBribe.sol"; +import {Q5} from "../../src/mons/embursa/Q5.sol"; +import {SetAblaze} from "../../src/mons/embursa/SetAblaze.sol"; +import {Tinderclaws} from "../../src/mons/embursa/Tinderclaws.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract EmbursaDeploy is Script { + function deployEmbursa(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](5); + + // Cache commonly used addresses + address burnstatus = vm.envAddress("BURN_STATUS"); + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[5] memory addrs; + + { + addrs[0] = address(new HoneyBribe()); + deployedContracts[0] = DeployData({name: "Honey Bribe", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new SetAblaze(ITypeCalculator(typecalculator), IEffect(burnstatus))); + deployedContracts[1] = DeployData({name: "Set Ablaze", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new HeatBeacon(IEffect(burnstatus))); + deployedContracts[2] = DeployData({name: "Heat Beacon", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new Q5(ITypeCalculator(typecalculator))); + deployedContracts[3] = DeployData({name: "Q5", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new Tinderclaws(IEffect(burnstatus))); + deployedContracts[4] = DeployData({name: "Tinderclaws", contractAddress: addrs[4]}); + } + + _registerEmbursa(registry, addrs); + + return deployedContracts; + } + + function _registerEmbursa(GachaTeamRegistry registry, address[5] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 420, + stamina: 5, + speed: 111, + attack: 141, + defense: 220, + specialAttack: 190, + specialDefense: 161, + type1: Type.Fire, + type2: Type.None + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x00f0200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x00f0300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x00f0000000000000000000030000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[3] = 0x00f0200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[4])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(7, stats, moves, abilities, keys, values); + } +} + +contract SetupEmbursa is EmbursaDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployEmbursa(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupGhouliath.s.sol b/script/mons/SetupGhouliath.s.sol new file mode 100644 index 00000000..84e33eb1 --- /dev/null +++ b/script/mons/SetupGhouliath.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {EternalGrudge} from "../../src/mons/ghouliath/EternalGrudge.sol"; +import {GraveAffliction} from "../../src/mons/ghouliath/GraveAffliction.sol"; +import {InfernalFlame} from "../../src/mons/ghouliath/InfernalFlame.sol"; +import {RiseFromTheGrave} from "../../src/mons/ghouliath/RiseFromTheGrave.sol"; +import {WitherAway} from "../../src/mons/ghouliath/WitherAway.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract GhouliathDeploy is Script { + function deployGhouliath(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](5); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[5] memory addrs; + + { + addrs[0] = address(new EternalGrudge()); + deployedContracts[0] = DeployData({name: "Eternal Grudge", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new InfernalFlame(ITypeCalculator(typecalculator), IEffect(vm.envAddress("BURN_STATUS")))); + deployedContracts[1] = DeployData({name: "Infernal Flame", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new WitherAway(ITypeCalculator(typecalculator), IEffect(vm.envAddress("PANIC_STATUS")))); + deployedContracts[2] = DeployData({name: "Wither Away", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new GraveAffliction()); + deployedContracts[3] = DeployData({name: "Grave Affliction", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new RiseFromTheGrave()); + deployedContracts[4] = DeployData({name: "Rise From The Grave", contractAddress: addrs[4]}); + } + + _registerGhouliath(registry, addrs); + + return deployedContracts; + } + + function _registerGhouliath(GachaTeamRegistry registry, address[5] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 303, + stamina: 5, + speed: 181, + attack: 157, + defense: 202, + specialAttack: 151, + specialDefense: 202, + type1: Type.Yin, + type2: Type.Fire + }); + uint256[] memory moves = new uint256[](5); + moves[0] = 0x0040200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[3] = 0x5a00200000000000000000000000000000000000000000000000000000000000; + moves[4] = 0x0030200000000000000000030000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[4])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(0, stats, moves, abilities, keys, values); + } +} + +contract SetupGhouliath is GhouliathDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployGhouliath(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupGlob.s.sol b/script/mons/SetupGlob.s.sol new file mode 100644 index 00000000..48437e73 --- /dev/null +++ b/script/mons/SetupGlob.s.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {ChosenOne} from "../../src/mons/glob/ChosenOne.sol"; +import {GetSlippery} from "../../src/mons/glob/GetSlippery.sol"; +import {HolyMolt} from "../../src/mons/glob/HolyMolt.sol"; + +abstract contract GlobDeploy is Script { + function deployGlob(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](3); + + // Cache commonly used addresses + address blessedstatus = vm.envAddress("BLESSED_STATUS"); + + address[3] memory addrs; + + { + addrs[0] = address(new HolyMolt(IEffect(blessedstatus))); + deployedContracts[0] = DeployData({name: "Holy Molt", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new GetSlippery()); + deployedContracts[1] = DeployData({name: "Get Slippery", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new ChosenOne(IEffect(blessedstatus))); + deployedContracts[2] = DeployData({name: "Chosen One", contractAddress: addrs[2]}); + } + + _registerGlob(registry, addrs); + + return deployedContracts; + } + + function _registerGlob(GachaTeamRegistry registry, address[3] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 301, + stamina: 5, + speed: 151, + attack: 200, + defense: 201, + specialAttack: 115, + specialDefense: 191, + type1: Type.Liquid, + type2: Type.Faith + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0040f00000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x5003200000000000000000000000000000000000000000000000000000000000; + moves[2] = 0x5a09300000000000000000000000000000000000000000000000000000000000; + moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[2])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(13, stats, moves, abilities, keys, values); + } +} + +contract SetupGlob is GlobDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployGlob(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupGorillax.s.sol b/script/mons/SetupGorillax.s.sol new file mode 100644 index 00000000..0bc45d3b --- /dev/null +++ b/script/mons/SetupGorillax.s.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {Angery} from "../../src/mons/gorillax/Angery.sol"; +import {RockPull} from "../../src/mons/gorillax/RockPull.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract GorillaxDeploy is Script { + function deployGorillax(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](2); + + address[2] memory addrs; + + { + addrs[0] = address(new RockPull(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); + deployedContracts[0] = DeployData({name: "Rock Pull", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new Angery()); + deployedContracts[1] = DeployData({name: "Angery", contractAddress: addrs[1]}); + } + + _registerGorillax(registry, addrs); + + return deployedContracts; + } + + function _registerGorillax(GachaTeamRegistry registry, address[2] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 407, + stamina: 5, + speed: 129, + attack: 302, + defense: 175, + specialAttack: 112, + specialDefense: 176, + type1: Type.Earth, + type2: Type.None + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x00f0300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x5f02300000000000000000000000000000000000000000000000000000000000; + moves[2] = 0x460a200000000000000000000000000000000000000000000000000000000000; + moves[3] = 0x2802100000000000000000000000000000000000000000000000000000000000; + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[1])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(4, stats, moves, abilities, keys, values); + } +} + +contract SetupGorillax is GorillaxDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployGorillax(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupIblivion.s.sol b/script/mons/SetupIblivion.s.sol new file mode 100644 index 00000000..044ba1ba --- /dev/null +++ b/script/mons/SetupIblivion.s.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {Baselight} from "../../src/mons/iblivion/Baselight.sol"; +import {Brightback} from "../../src/mons/iblivion/Brightback.sol"; +import {Loop} from "../../src/mons/iblivion/Loop.sol"; +import {Renormalize} from "../../src/mons/iblivion/Renormalize.sol"; +import {UnboundedStrike} from "../../src/mons/iblivion/UnboundedStrike.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract IblivionDeploy is Script { + function deployIblivion(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](5); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[5] memory addrs; + + { + addrs[0] = address(new Baselight()); + deployedContracts[0] = DeployData({name: "Baselight", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new UnboundedStrike(ITypeCalculator(typecalculator), Baselight(addrs[0]))); + deployedContracts[1] = DeployData({name: "Unbounded Strike", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new Loop(Baselight(addrs[0]))); + deployedContracts[2] = DeployData({name: "Loop", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new Brightback(ITypeCalculator(typecalculator), Baselight(addrs[0]))); + deployedContracts[3] = DeployData({name: "Brightback", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new Renormalize(Baselight(addrs[0]), Loop(addrs[2]))); + deployedContracts[4] = DeployData({name: "Renormalize", contractAddress: addrs[4]}); + } + + _registerIblivion(registry, addrs); + + return deployedContracts; + } + + function _registerIblivion(GachaTeamRegistry registry, address[5] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 277, + stamina: 5, + speed: 256, + attack: 199, + defense: 164, + specialAttack: 180, + specialDefense: 168, + type1: Type.Yang, + type2: Type.Air + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0030f00000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[1] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[2] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + moves[3] = 0x0020000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[4])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[0])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(3, stats, moves, abilities, keys, values); + } +} + +contract SetupIblivion is IblivionDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployIblivion(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupInutia.s.sol b/script/mons/SetupInutia.s.sol new file mode 100644 index 00000000..967261e0 --- /dev/null +++ b/script/mons/SetupInutia.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {ChainExpansion} from "../../src/mons/inutia/ChainExpansion.sol"; +import {HitAndDip} from "../../src/mons/inutia/HitAndDip.sol"; +import {Initialize} from "../../src/mons/inutia/Initialize.sol"; +import {Interweaving} from "../../src/mons/inutia/Interweaving.sol"; +import {Sanctify} from "../../src/mons/inutia/Sanctify.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract InutiaDeploy is Script { + function deployInutia(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](5); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[5] memory addrs; + + { + addrs[0] = address(new ChainExpansion(ITypeCalculator(typecalculator))); + deployedContracts[0] = DeployData({name: "Chain Expansion", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new Initialize()); + deployedContracts[1] = DeployData({name: "Initialize", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new HitAndDip(ITypeCalculator(typecalculator))); + deployedContracts[2] = DeployData({name: "Hit And Dip", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new Sanctify(IEffect(vm.envAddress("BLESSED_STATUS")))); + deployedContracts[3] = DeployData({name: "Sanctify", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new Interweaving()); + deployedContracts[4] = DeployData({name: "Interweaving", contractAddress: addrs[4]}); + } + + _registerInutia(registry, addrs); + + return deployedContracts; + } + + function _registerInutia(GachaTeamRegistry registry, address[5] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 351, + stamina: 5, + speed: 229, + attack: 171, + defense: 189, + specialAttack: 175, + specialDefense: 192, + type1: Type.Faith, + type2: Type.None + }); + uint256[] memory moves = new uint256[](5); + moves[0] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x5009200000000000000000000000000000000000000000000000000000000000; + moves[3] = 0x0030200000000000000000050000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[4])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(1, stats, moves, abilities, keys, values); + } +} + +contract SetupInutia is InutiaDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployInutia(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupMalalien.s.sol b/script/mons/SetupMalalien.s.sol new file mode 100644 index 00000000..1b46c938 --- /dev/null +++ b/script/mons/SetupMalalien.s.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {ActusReus} from "../../src/mons/malalien/ActusReus.sol"; +import {FoulLanguage} from "../../src/mons/malalien/FoulLanguage.sol"; +import {TripleThink} from "../../src/mons/malalien/TripleThink.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract MalalienDeploy is Script { + function deployMalalien(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](3); + + address[3] memory addrs; + + { + addrs[0] = address(new TripleThink()); + deployedContracts[0] = DeployData({name: "Triple Think", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new FoulLanguage(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); + deployedContracts[1] = DeployData({name: "Foul Language", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new ActusReus()); + deployedContracts[2] = DeployData({name: "Actus Reus", contractAddress: addrs[2]}); + } + + _registerMalalien(registry, addrs); + + return deployedContracts; + } + + function _registerMalalien(GachaTeamRegistry registry, address[3] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 258, + stamina: 5, + speed: 308, + attack: 121, + defense: 125, + specialAttack: 322, + specialDefense: 151, + type1: Type.Cyber, + type2: Type.None + }); + uint256[] memory moves = new uint256[](5); + moves[0] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x644c300000000000000000000000000000000000000000000000000000000000; + moves[2] = 0x504b30a000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("PANIC_STATUS"))); + moves[3] = 0x5a4d30a000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("SLEEP_STATUS"))); + moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[2])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(2, stats, moves, abilities, keys, values); + } +} + +contract SetupMalalien is MalalienDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployMalalien(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupMulong.s.sol b/script/mons/SetupMulong.s.sol new file mode 100644 index 00000000..2002a323 --- /dev/null +++ b/script/mons/SetupMulong.s.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {Storm} from "../../src/effects/battlefield/Storm.sol"; +import {CloudStrike} from "../../src/mons/mulong/CloudStrike.sol"; +import {KingsLeisure} from "../../src/mons/mulong/KingsLeisure.sol"; +import {KingsRespite} from "../../src/mons/mulong/KingsRespite.sol"; +import {SummonStorm} from "../../src/mons/mulong/SummonStorm.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract MulongDeploy is Script { + function deployMulong(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](4); + + // Cache commonly used addresses + address storm = vm.envAddress("STORM"); + + address[4] memory addrs; + + { + addrs[0] = address(new SummonStorm(Storm(storm))); + deployedContracts[0] = DeployData({name: "Summon Storm", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new KingsRespite(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); + deployedContracts[1] = DeployData({name: "King's Respite", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new CloudStrike(Storm(storm), IEffect(vm.envAddress("FROSTBITE_STATUS")))); + deployedContracts[2] = DeployData({name: "Cloud Strike", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new KingsLeisure()); + deployedContracts[3] = DeployData({name: "King's Leisure", contractAddress: addrs[3]}); + } + + _registerMulong(registry, addrs); + + return deployedContracts; + } + + function _registerMulong(GachaTeamRegistry registry, address[4] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 368, + stamina: 5, + speed: 290, + attack: 115, + defense: 188, + specialAttack: 288, + specialDefense: 138, + type1: Type.Air, + type2: Type.Lightning + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0030000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x7848314000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("ZAP_STATUS"))); + moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[3])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(14, stats, moves, abilities, keys, values); + } +} + +contract SetupMulong is MulongDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployMulong(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupNirvamma.s.sol b/script/mons/SetupNirvamma.s.sol new file mode 100644 index 00000000..f83a553d --- /dev/null +++ b/script/mons/SetupNirvamma.s.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {Adaptor} from "../../src/mons/nirvamma/Adaptor.sol"; +import {Chronoffense} from "../../src/mons/nirvamma/Chronoffense.sol"; +import {HardReset} from "../../src/mons/nirvamma/HardReset.sol"; +import {ModalBolt} from "../../src/mons/nirvamma/ModalBolt.sol"; + +abstract contract NirvammaDeploy is Script { + function deployNirvamma(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](4); + + address[4] memory addrs; + + { + addrs[0] = address(new HardReset()); + deployedContracts[0] = DeployData({name: "Hard Reset", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new Chronoffense()); + deployedContracts[1] = DeployData({name: "Chronoffense", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new ModalBolt(IEffect(vm.envAddress("BURN_STATUS")), IEffect(vm.envAddress("FROSTBITE_STATUS")), IEffect(vm.envAddress("ZAP_STATUS")))); + deployedContracts[2] = DeployData({name: "Modal Bolt", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new Adaptor()); + deployedContracts[3] = DeployData({name: "Adaptor", contractAddress: addrs[3]}); + } + + _registerNirvamma(registry, addrs); + + return deployedContracts; + } + + function _registerNirvamma(GachaTeamRegistry registry, address[4] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 373, + stamina: 5, + speed: 177, + attack: 202, + defense: 168, + specialAttack: 140, + specialDefense: 202, + type1: Type.Math, + type2: Type.None + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x500b314000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("PANIC_STATUS"))); + moves[2] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[3] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[3])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(12, stats, moves, abilities, keys, values); + } +} + +contract SetupNirvamma is NirvammaDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployNirvamma(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupPengym.s.sol b/script/mons/SetupPengym.s.sol new file mode 100644 index 00000000..35a24d08 --- /dev/null +++ b/script/mons/SetupPengym.s.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {Deadlift} from "../../src/mons/pengym/Deadlift.sol"; +import {DeepFreeze} from "../../src/mons/pengym/DeepFreeze.sol"; +import {PistolSquat} from "../../src/mons/pengym/PistolSquat.sol"; +import {PostWorkout} from "../../src/mons/pengym/PostWorkout.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract PengymDeploy is Script { + function deployPengym(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](4); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[4] memory addrs; + + { + addrs[0] = address(new Deadlift()); + deployedContracts[0] = DeployData({name: "Deadlift", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new DeepFreeze(ITypeCalculator(typecalculator), IEffect(vm.envAddress("FROSTBITE_STATUS")))); + deployedContracts[1] = DeployData({name: "Deep Freeze", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new PistolSquat(ITypeCalculator(typecalculator))); + deployedContracts[2] = DeployData({name: "Pistol Squat", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new PostWorkout()); + deployedContracts[3] = DeployData({name: "Post-Workout", contractAddress: addrs[3]}); + } + + _registerPengym(registry, addrs); + + return deployedContracts; + } + + function _registerPengym(GachaTeamRegistry registry, address[4] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 371, + stamina: 5, + speed: 149, + attack: 212, + defense: 191, + specialAttack: 233, + specialDefense: 172, + type1: Type.Ice, + type2: Type.None + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x00c6064000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("FROSTBITE_STATUS"))); + moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[2] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[3] = 0x0020200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[3])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(6, stats, moves, abilities, keys, values); + } +} + +contract SetupPengym is PengymDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployPengym(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupSofabbi.s.sol b/script/mons/SetupSofabbi.s.sol new file mode 100644 index 00000000..9c5fa148 --- /dev/null +++ b/script/mons/SetupSofabbi.s.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {CarrotHarvest} from "../../src/mons/sofabbi/CarrotHarvest.sol"; +import {Gachachacha} from "../../src/mons/sofabbi/Gachachacha.sol"; +import {GuestFeature} from "../../src/mons/sofabbi/GuestFeature.sol"; +import {SnackBreak} from "../../src/mons/sofabbi/SnackBreak.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract SofabbiDeploy is Script { + function deploySofabbi(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](4); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[4] memory addrs; + + { + addrs[0] = address(new Gachachacha(ITypeCalculator(typecalculator))); + deployedContracts[0] = DeployData({name: "Gachachacha", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new GuestFeature(ITypeCalculator(typecalculator))); + deployedContracts[1] = DeployData({name: "Guest Feature", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new SnackBreak()); + deployedContracts[2] = DeployData({name: "Snack Break", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new CarrotHarvest()); + deployedContracts[3] = DeployData({name: "Carrot Harvest", contractAddress: addrs[3]}); + } + + _registerSofabbi(registry, addrs); + + return deployedContracts; + } + + function _registerSofabbi(GachaTeamRegistry registry, address[4] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 333, + stamina: 5, + speed: 175, + attack: 180, + defense: 201, + specialAttack: 120, + specialDefense: 269, + type1: Type.Nature, + type2: Type.None + }); + uint256[] memory moves = new uint256[](4); + moves[0] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0030300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x7807400000000000000000000000000000000000000000000000000000000000; + moves[3] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[3])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(5, stats, moves, abilities, keys, values); + } +} + +contract SetupSofabbi is SofabbiDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deploySofabbi(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupVolthare.s.sol b/script/mons/SetupVolthare.s.sol new file mode 100644 index 00000000..b49996f1 --- /dev/null +++ b/script/mons/SetupVolthare.s.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {Overclock} from "../../src/effects/battlefield/Overclock.sol"; +import {DualShock} from "../../src/mons/volthare/DualShock.sol"; +import {MegaStarBlast} from "../../src/mons/volthare/MegaStarBlast.sol"; +import {PreemptiveShock} from "../../src/mons/volthare/PreemptiveShock.sol"; +import {Quickstorm} from "../../src/mons/volthare/Quickstorm.sol"; +import {RoundTrip} from "../../src/mons/volthare/RoundTrip.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract VolthareDeploy is Script { + function deployVolthare(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](5); + + // Cache commonly used addresses + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + address zapstatus = vm.envAddress("ZAP_STATUS"); + + address[5] memory addrs; + + { + addrs[0] = address(new RoundTrip(ITypeCalculator(typecalculator))); + deployedContracts[0] = DeployData({name: "Round Trip", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new MegaStarBlast(ITypeCalculator(typecalculator), IEffect(zapstatus), IEffect(vm.envAddress("OVERCLOCK")))); + deployedContracts[1] = DeployData({name: "Mega Star Blast", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new DualShock(ITypeCalculator(typecalculator), IEffect(zapstatus), Overclock(vm.envAddress("OVERCLOCK")))); + deployedContracts[2] = DeployData({name: "Dual Shock", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new Quickstorm(ITypeCalculator(typecalculator), IEffect(zapstatus))); + deployedContracts[3] = DeployData({name: "Quickstorm", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new PreemptiveShock(ITypeCalculator(typecalculator))); + deployedContracts[4] = DeployData({name: "Preemptive Shock", contractAddress: addrs[4]}); + } + + _registerVolthare(registry, addrs); + + return deployedContracts; + } + + function _registerVolthare(GachaTeamRegistry registry, address[5] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 310, + stamina: 5, + speed: 311, + attack: 120, + defense: 184, + specialAttack: 255, + specialDefense: 176, + type1: Type.Lightning, + type2: Type.Cyber + }); + uint256[] memory moves = new uint256[](5); + moves[0] = 0x5a4820a000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("ZAP_STATUS"))); + moves[1] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[2] = 0x0050300000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[3] = 0x0030000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[4] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = uint256(uint160(addrs[4])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(8, stats, moves, abilities, keys, values); + } +} + +contract SetupVolthare is VolthareDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployVolthare(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/script/mons/SetupXmon.s.sol b/script/mons/SetupXmon.s.sol new file mode 100644 index 00000000..9ca52294 --- /dev/null +++ b/script/mons/SetupXmon.s.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: AGPL-3.0 +// AUTO-GENERATED by processing/generateSolidity.py. Do not edit by hand. +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {DeployData} from "../DeployData.sol"; +import {Type} from "../../src/Enums.sol"; +import {MonStats} from "../../src/Structs.sol"; +import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; +import {IEffect} from "../../src/effects/IEffect.sol"; +import {ContagiousSlumber} from "../../src/mons/xmon/ContagiousSlumber.sol"; +import {Dreamcatcher} from "../../src/mons/xmon/Dreamcatcher.sol"; +import {InvokeTaboo} from "../../src/mons/xmon/InvokeTaboo.sol"; +import {OldVengeance} from "../../src/mons/xmon/OldVengeance.sol"; +import {Somniphobia} from "../../src/mons/xmon/Somniphobia.sol"; +import {VitalSiphon} from "../../src/mons/xmon/VitalSiphon.sol"; +import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; + +abstract contract XmonDeploy is Script { + function deployXmon(GachaTeamRegistry registry) internal returns (DeployData[] memory) { + DeployData[] memory deployedContracts = new DeployData[](6); + + // Cache commonly used addresses + address sleepstatus = vm.envAddress("SLEEP_STATUS"); + address typecalculator = vm.envAddress("TYPE_CALCULATOR"); + + address[6] memory addrs; + + { + addrs[0] = address(new ContagiousSlumber(IEffect(sleepstatus))); + deployedContracts[0] = DeployData({name: "Contagious Slumber", contractAddress: addrs[0]}); + } + { + addrs[1] = address(new VitalSiphon(ITypeCalculator(typecalculator))); + deployedContracts[1] = DeployData({name: "Vital Siphon", contractAddress: addrs[1]}); + } + { + addrs[2] = address(new Somniphobia()); + deployedContracts[2] = DeployData({name: "Somniphobia", contractAddress: addrs[2]}); + } + { + addrs[3] = address(new OldVengeance(ITypeCalculator(typecalculator))); + deployedContracts[3] = DeployData({name: "Old Vengeance", contractAddress: addrs[3]}); + } + { + addrs[4] = address(new InvokeTaboo(IEffect(sleepstatus))); + deployedContracts[4] = DeployData({name: "Invoke Taboo", contractAddress: addrs[4]}); + } + { + addrs[5] = address(new Dreamcatcher()); + deployedContracts[5] = DeployData({name: "Dreamcatcher", contractAddress: addrs[5]}); + } + + _registerXmon(registry, addrs); + + return deployedContracts; + } + + function _registerXmon(GachaTeamRegistry registry, address[6] memory addrs) internal { + MonStats memory stats = MonStats({ + hp: 311, + stamina: 5, + speed: 285, + attack: 123, + defense: 179, + specialAttack: 222, + specialDefense: 185, + type1: Type.Cosmic, + type2: Type.None + }); + uint256[] memory moves = new uint256[](5); + moves[0] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); + moves[2] = 0x0030100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); + moves[3] = 0x0040400000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[3])); + moves[4] = 0x0020100000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[4])); + uint256[] memory abilities = new uint256[](1); + abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[5])); + bytes32[] memory keys = new bytes32[](0); + bytes32[] memory values = new bytes32[](0); + registry.createMon(10, stats, moves, abilities, keys, values); + } +} + +contract SetupXmon is XmonDeploy { + function run() external returns (DeployData[] memory deployedContracts) { + vm.startBroadcast(); + deployedContracts = deployXmon(GachaTeamRegistry(vm.envAddress("GACHA_TEAM_REGISTRY"))); + vm.stopBroadcast(); + } +} diff --git a/sims/src/harness.ts b/sims/src/harness.ts index 60d3e3fa..0b502d85 100644 --- a/sims/src/harness.ts +++ b/sims/src/harness.ts @@ -4,11 +4,11 @@ import { setupContainer } from '../../transpiler/ts-output/factories'; import { Engine } from '../../transpiler/ts-output/Engine'; import * as Structs from '../../transpiler/ts-output/Structs'; import * as Constants from '../../transpiler/ts-output/Constants'; -import { packMove, type InlineMoveJson } from './util/inline-pack'; +import { packMove, type InlineMoveParams } from './util/inline-pack'; export type MoveSlotSource = | { kind: 'contract'; contractName: string } - | { kind: 'inline'; json: InlineMoveJson }; + | { kind: 'inline'; params: InlineMoveParams; effect: string | null }; const HARNESS_MOVE_MANAGER = '0x000000000000000000000000000000000000beef'; const HARNESS_TEAM_REGISTRY_ADDR = '0x000000000000000000000000000000000000a55e'; @@ -139,7 +139,7 @@ export function buildMon(ctx: SimContext, m: HarnessMonConfig): Structs.Mon { const c = ctx.container.resolve(src.contractName); return addressToUint(c._contractAddress); } - return packMove(src.json, resolveEffectAddress(ctx, src.json.effect)); + return packMove(src.params, resolveEffectAddress(ctx, src.effect)); }); let ability = 0n; if (m.ability) { diff --git a/sims/src/util/csv-load.ts b/sims/src/util/csv-load.ts index d69e479d..1ed72d35 100644 --- a/sims/src/util/csv-load.ts +++ b/sims/src/util/csv-load.ts @@ -33,6 +33,17 @@ export interface MoveRow { inputType: string; /** Client-facing target domain (kebab-case; blank = any-other-slot per the data contract). */ targetSpec: string; + /** Named constants from the Constants column; EFFECT_ACCURACY feeds the inline move word. */ + constants: Record; +} + +function parseConstants(raw: string): Record { + const out: Record = {}; + for (const part of (raw ?? '').split(';')) { + const [name, val] = part.split('='); + if (name?.trim() && val !== undefined) out[name.trim()] = Number(val.trim()); + } + return out; } function parseNumOrNull(s: string): number | null { @@ -120,8 +131,9 @@ export function loadMoves(): MoveRow[] { type: r[6], cls: r[7] as MoveClass, description: r[8], - inputType: r[10] ?? 'none', - targetSpec: r[13] || 'any-other-slot', + inputType: r[11] ?? 'none', + constants: parseConstants(r[13]), + targetSpec: r[14] || 'any-other-slot', })); } diff --git a/sims/src/util/inline-pack.ts b/sims/src/util/inline-pack.ts index 52b5026c..55aea389 100644 --- a/sims/src/util/inline-pack.ts +++ b/sims/src/util/inline-pack.ts @@ -1,17 +1,20 @@ /** - * Port of chomp/processing/packMoves.py — pack a JSON move definition into the - * uint256 slot value the Engine consumes when `(rawMoveSlot >> 160) != 0`. + * Port of chomp/processing/packMoves.py — pack an inline move into the uint256 + * slot value the Engine consumes when `(rawMoveSlot >> 160) != 0`. * * Layout (256 bits): [basePower:8 | moveClass:2 | priority:2 | moveType:4 | * stamina:4 | effectAccuracy:8 | unused:68 | effect:160] * - * Inline moves always run with DEFAULT_ACCURACY=100, DEFAULT_VOL=10 in the - * engine — fields not in this format aren't customizable per-move. + * Every packed value comes from the move's moves.csv row; the per-move .json + * only marks the move inline and names its effect. Inline moves always run with + * DEFAULT_ACCURACY=100, DEFAULT_VOL=10 in the engine — fields not in this + * format aren't customizable per-move. */ import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { Type } from '../../../transpiler/ts-output/Enums'; +import type { MoveRow } from './csv-load'; const SRC_MONS_DIR = join(import.meta.dir, '..', '..', '..', 'src', 'mons'); @@ -27,18 +30,33 @@ const CLASS_MAP: Record = { Physical: 0, Special: 1, Self: 2, Other: 3, }; +/** Contents of a `src/mons//.json` marker file. */ export interface InlineMoveJson { - name: string; + effect: string | null; +} + +export interface InlineMoveParams { basePower: number; staminaCost: number; moveType: keyof typeof TYPE_MAP; moveClass: keyof typeof CLASS_MAP; effectAccuracy: number; - effect: string | null; priority?: number; } -export function packMove(m: InlineMoveJson, effectAddress: bigint = 0n): bigint { +/** moves.csv is the source of truth for every packed field. */ +export function inlineParamsFromCsv(move: MoveRow): InlineMoveParams { + return { + basePower: move.power ?? 0, + staminaCost: move.stamina ?? 0, + moveType: move.type, + moveClass: move.cls, + effectAccuracy: move.constants.EFFECT_ACCURACY ?? 0, + priority: move.priority, + }; +} + +export function packMove(m: InlineMoveParams, effectAddress: bigint = 0n): bigint { const movClass = CLASS_MAP[m.moveClass]; const movType = TYPE_MAP[m.moveType]; const priority = m.priority ?? 0; diff --git a/sims/src/util/mon-builder.ts b/sims/src/util/mon-builder.ts index 4ef4a62f..3f45d6c1 100644 --- a/sims/src/util/mon-builder.ts +++ b/sims/src/util/mon-builder.ts @@ -2,7 +2,7 @@ import { contracts as CONTRACT_REGISTRY } from '../../../transpiler/ts-output/fa import { Type } from '../../../transpiler/ts-output/Enums'; import type { MonRow, MoveRow, Roster } from './csv-load'; import type { HarnessMonConfig, MoveSlotSource } from '../harness'; -import { findInlineMoveJson, type InlineMoveJson } from './inline-pack'; +import { findInlineMoveJson, inlineParamsFromCsv } from './inline-pack'; const TYPE_BY_NAME: Record = { Yin: Type.Yin, @@ -33,6 +33,7 @@ export function moveNameToContract(name: string): string { return name .split(/[\s\-]+/) .filter(Boolean) + .map((w) => w.replace(/[^\p{L}\p{N}]/gu, '')) .map((w) => w[0].toUpperCase() + w.slice(1)) .join(''); } @@ -67,7 +68,11 @@ export function buildMonConfig(roster: Roster, mon: MonRow, defaultStamina = 5n) } const inlineJson = findInlineMoveJson(monDir, contract); if (inlineJson) { - resolvedMoves.push({ move, index: resolvedMoves.length, source: { kind: 'inline', json: inlineJson } }); + resolvedMoves.push({ + move, + index: resolvedMoves.length, + source: { kind: 'inline', params: inlineParamsFromCsv(move), effect: inlineJson.effect }, + }); continue; } missingMoves.push(move.name); diff --git a/src/mons/ghouliath/Osteoporosis.json b/src/mons/ghouliath/Osteoporosis.json index d72e2579..bf638c9f 100644 --- a/src/mons/ghouliath/Osteoporosis.json +++ b/src/mons/ghouliath/Osteoporosis.json @@ -1,9 +1,3 @@ { - "name": "Osteoporosis", - "basePower": 90, - "staminaCost": 2, - "moveType": "Yin", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/glob/HolyWaterClaw.json b/src/mons/glob/HolyWaterClaw.json index 29fe264f..bf638c9f 100644 --- a/src/mons/glob/HolyWaterClaw.json +++ b/src/mons/glob/HolyWaterClaw.json @@ -1,9 +1,3 @@ { - "name": "Holy Water Claw", - "basePower": 90, - "staminaCost": 3, - "moveType": "Faith", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/glob/WaterClaw.json b/src/mons/glob/WaterClaw.json index 6e8e976c..bf638c9f 100644 --- a/src/mons/glob/WaterClaw.json +++ b/src/mons/glob/WaterClaw.json @@ -1,9 +1,3 @@ { - "name": "Water Claw", - "basePower": 80, - "staminaCost": 2, - "moveType": "Liquid", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/gorillax/Blow.json b/src/mons/gorillax/Blow.json index e8f90b48..bf638c9f 100644 --- a/src/mons/gorillax/Blow.json +++ b/src/mons/gorillax/Blow.json @@ -1,9 +1,3 @@ { - "name": "Blow", - "basePower": 70, - "staminaCost": 2, - "moveType": "Air", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/gorillax/PoundGround.json b/src/mons/gorillax/PoundGround.json index e15297ff..bf638c9f 100644 --- a/src/mons/gorillax/PoundGround.json +++ b/src/mons/gorillax/PoundGround.json @@ -1,9 +1,3 @@ { - "name": "Pound Ground", - "basePower": 95, - "staminaCost": 3, - "moveType": "Earth", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/gorillax/ThrowPebble.json b/src/mons/gorillax/ThrowPebble.json index 0d38f953..bf638c9f 100644 --- a/src/mons/gorillax/ThrowPebble.json +++ b/src/mons/gorillax/ThrowPebble.json @@ -1,9 +1,3 @@ { - "name": "Throw Pebble", - "basePower": 40, - "staminaCost": 1, - "moveType": "Earth", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/inutia/BigBite.json b/src/mons/inutia/BigBite.json index 4c48e573..bf638c9f 100644 --- a/src/mons/inutia/BigBite.json +++ b/src/mons/inutia/BigBite.json @@ -1,9 +1,3 @@ { - "name": "Big Bite", - "basePower": 80, - "staminaCost": 2, - "moveType": "Faith", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/malalien/FederalInvestigation.json b/src/mons/malalien/FederalInvestigation.json index 3c209cdf..bf638c9f 100644 --- a/src/mons/malalien/FederalInvestigation.json +++ b/src/mons/malalien/FederalInvestigation.json @@ -1,9 +1,3 @@ { - "name": "Federal Investigation", - "basePower": 100, - "staminaCost": 3, - "moveType": "Cyber", - "moveClass": "Special", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/malalien/InfiniteLove.json b/src/mons/malalien/InfiniteLove.json index a2c83571..f47d27b9 100644 --- a/src/mons/malalien/InfiniteLove.json +++ b/src/mons/malalien/InfiniteLove.json @@ -1,9 +1,3 @@ { - "name": "Infinite Love", - "basePower": 90, - "staminaCost": 3, - "moveType": "Cosmic", - "moveClass": "Special", - "effectAccuracy": 10, "effect": "SleepStatus" } diff --git a/src/mons/malalien/NegativeThoughts.json b/src/mons/malalien/NegativeThoughts.json index 3ded43e7..bcdc3db4 100644 --- a/src/mons/malalien/NegativeThoughts.json +++ b/src/mons/malalien/NegativeThoughts.json @@ -1,9 +1,3 @@ { - "name": "Negative Thoughts", - "basePower": 80, - "staminaCost": 3, - "moveType": "Math", - "moveClass": "Special", - "effectAccuracy": 10, "effect": "PanicStatus" } diff --git a/src/mons/mulong/CloudStrike.sol b/src/mons/mulong/CloudStrike.sol index 0c2e4a5d..5e1a21ac 100644 --- a/src/mons/mulong/CloudStrike.sol +++ b/src/mons/mulong/CloudStrike.sol @@ -76,10 +76,6 @@ contract CloudStrike is IMoveSet { return MoveClass.Special; } - function basePower(IEngine engine, bytes32 battleKey) public view returns (uint32) { - return _stormIsUp(engine, battleKey) ? STORM_BASE_POWER : BASE_POWER; - } - function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex) external view @@ -90,7 +86,7 @@ contract CloudStrike is IMoveSet { moveClass: moveClass(engine, battleKey), priority: priority(engine, battleKey, attackerPlayerIndex), stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), - basePower: basePower(engine, battleKey) + basePower: _stormIsUp(engine, battleKey) ? STORM_BASE_POWER : BASE_POWER }); } } diff --git a/src/mons/mulong/KingsRespite.sol b/src/mons/mulong/KingsRespite.sol index 12e78e69..1321a146 100644 --- a/src/mons/mulong/KingsRespite.sol +++ b/src/mons/mulong/KingsRespite.sol @@ -75,7 +75,8 @@ contract KingsRespite is IMoveSet, BasicEffect { moveClass: moveClass(engine, battleKey), priority: priority(engine, battleKey, attackerPlayerIndex), stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex), - basePower: BASE_POWER + // The strike is deferred to the next rest, so this turn's damage is 0. + basePower: 0 }); } diff --git a/src/mons/mulong/Thunderclap.json b/src/mons/mulong/Thunderclap.json index 31f91e7e..1cda1c6f 100644 --- a/src/mons/mulong/Thunderclap.json +++ b/src/mons/mulong/Thunderclap.json @@ -1,9 +1,3 @@ { - "name": "Thunderclap", - "basePower": 120, - "staminaCost": 3, - "moveType": "Lightning", - "moveClass": "Special", - "effectAccuracy": 20, "effect": "ZapStatus" } diff --git a/src/mons/nirvamma/ScaryNumbers.json b/src/mons/nirvamma/ScaryNumbers.json index 724194d3..bcdc3db4 100644 --- a/src/mons/nirvamma/ScaryNumbers.json +++ b/src/mons/nirvamma/ScaryNumbers.json @@ -1,9 +1,3 @@ { - "name": "Scary Numbers", - "basePower": 80, - "staminaCost": 3, - "moveType": "Math", - "moveClass": "Physical", - "effectAccuracy": 20, "effect": "PanicStatus" } diff --git a/src/mons/pengym/ChillOut.json b/src/mons/pengym/ChillOut.json index e427c437..d4f659f0 100644 --- a/src/mons/pengym/ChillOut.json +++ b/src/mons/pengym/ChillOut.json @@ -1,9 +1,3 @@ { - "name": "Chill Out", - "basePower": 0, - "staminaCost": 0, - "moveType": "Ice", - "moveClass": "Other", - "effectAccuracy": 100, "effect": "FrostbiteStatus" } diff --git a/src/mons/sofabbi/UnexpectedCarrot.json b/src/mons/sofabbi/UnexpectedCarrot.json index 9a624004..bf638c9f 100644 --- a/src/mons/sofabbi/UnexpectedCarrot.json +++ b/src/mons/sofabbi/UnexpectedCarrot.json @@ -1,9 +1,3 @@ { - "name": "Unexpected Carrot", - "basePower": 120, - "staminaCost": 4, - "moveType": "Nature", - "moveClass": "Physical", - "effectAccuracy": 0, "effect": null } diff --git a/src/mons/volthare/Electrocute.json b/src/mons/volthare/Electrocute.json index 33a08423..1cda1c6f 100644 --- a/src/mons/volthare/Electrocute.json +++ b/src/mons/volthare/Electrocute.json @@ -1,9 +1,3 @@ { - "name": "Electrocute", - "basePower": 90, - "staminaCost": 2, - "moveType": "Lightning", - "moveClass": "Special", - "effectAccuracy": 10, "effect": "ZapStatus" } diff --git a/src/moves/MoveSlotLib.sol b/src/moves/MoveSlotLib.sol index d895fa95..9d24b003 100644 --- a/src/moves/MoveSlotLib.sol +++ b/src/moves/MoveSlotLib.sol @@ -8,9 +8,6 @@ import {MoveMeta} from "../Structs.sol"; import {IMoveSet} from "./IMoveSet.sol"; /// @notice Abstracts reading move properties from a raw uint256 move slot. -/// Three forms: MOVE_META_TAG set = a deployed IMoveSet address with packed static -/// stamina/priority (0xF nibble = dynamic, staticcall live); untagged with upper bits set = -/// packed inline data; bare address = deployed move without metadata. library MoveSlotLib { function isInline(uint256 raw) internal pure returns (bool) { return raw & MOVE_META_TAG == 0 && raw >> 160 != 0; @@ -36,9 +33,8 @@ library MoveSlotLib { if (isInline(raw)) { return uint32((raw >> 248) & 0xFF); } - // External: try IAttackMove.basePower — not all external moves expose this - // Callers should handle the external case themselves if they need try/catch - revert("MoveSlotLib: use try/catch for external basePower"); + // External slots carry no packed power — read it from decodeMeta's getMeta instead. + revert("MoveSlotLib: use decodeMeta for external basePower"); } function moveClass(uint256 raw, IEngine engine, bytes32 battleKey) internal view returns (MoveClass) { @@ -95,10 +91,9 @@ library MoveSlotLib { /// come from bit-unpacking the slot itself — pure memory ops. For external slots, /// it's a single IMoveSet.getMeta staticcall instead of the 5-call fan-out /// (moveType / moveClass / priority / stamina). - /// @dev `basePower` is 0 for non-attack slots — callers that care should still try the - /// IAttackMove(addr).basePower(battleKey) shim for legacy custom attacks that haven't - /// adopted MoveMeta. For inline slots and StandardAttack-based moves, basePower is - /// authoritative here. + /// @dev `basePower` is 0 for non-attack slots, and deferred-damage moves (Q5, King's Respite) + /// report 0 because they deal nothing on the turn they're cast — 0 means "no damage now", + /// not "unknown" function decodeMeta(uint256 raw, IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) internal view diff --git a/test/MainnetReplayGasTest.t.sol b/test/MainnetReplayGasTest.t.sol index 1cc1881b..0e175b74 100644 --- a/test/MainnetReplayGasTest.t.sol +++ b/test/MainnetReplayGasTest.t.sol @@ -8,7 +8,17 @@ import {Test} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {console} from "forge-std/console.sol"; -import {SetupMons} from "../script/SetupMons.s.sol"; +import {AuroxDeploy} from "../script/mons/SetupAurox.s.sol"; +import {EmbursaDeploy} from "../script/mons/SetupEmbursa.s.sol"; +import {GhouliathDeploy} from "../script/mons/SetupGhouliath.s.sol"; +import {GorillaxDeploy} from "../script/mons/SetupGorillax.s.sol"; +import {IblivionDeploy} from "../script/mons/SetupIblivion.s.sol"; +import {InutiaDeploy} from "../script/mons/SetupInutia.s.sol"; +import {MalalienDeploy} from "../script/mons/SetupMalalien.s.sol"; +import {PengymDeploy} from "../script/mons/SetupPengym.s.sol"; +import {SofabbiDeploy} from "../script/mons/SetupSofabbi.s.sol"; +import {VolthareDeploy} from "../script/mons/SetupVolthare.s.sol"; +import {XmonDeploy} from "../script/mons/SetupXmon.s.sol"; import {Engine} from "../src/Engine.sol"; import {IEngine} from "../src/IEngine.sol"; import {IAbility} from "../src/abilities/IAbility.sol"; @@ -48,7 +58,21 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// p1Salt) with NO battleKey dependency, so replaying the same salts reproduces the exact /// battle here. Measured through engine.executeBatchedTurns (the same path the tx took) on /// a VIRGIN storageKey and again on a REUSED (steady-state) one. -contract MainnetReplayGasTest is Test, SetupMons, GasMeasure { +contract MainnetReplayGasTest is + Test, + GhouliathDeploy, + InutiaDeploy, + MalalienDeploy, + IblivionDeploy, + GorillaxDeploy, + SofabbiDeploy, + PengymDeploy, + EmbursaDeploy, + VolthareDeploy, + AuroxDeploy, + XmonDeploy, + GasMeasure +{ uint256 constant P0_PK = 0xA11CE; uint256 constant P1_PK = 0xB0B; uint256 constant TX_BASE = 21000; diff --git a/test/RealMonReplayGasTest.t.sol b/test/RealMonReplayGasTest.t.sol index cff9980a..74e04f0f 100644 --- a/test/RealMonReplayGasTest.t.sol +++ b/test/RealMonReplayGasTest.t.sol @@ -7,7 +7,17 @@ import "../src/Structs.sol"; import {Test} from "forge-std/Test.sol"; import {console} from "forge-std/console.sol"; -import {SetupMons} from "../script/SetupMons.s.sol"; +import {AuroxDeploy} from "../script/mons/SetupAurox.s.sol"; +import {EmbursaDeploy} from "../script/mons/SetupEmbursa.s.sol"; +import {GhouliathDeploy} from "../script/mons/SetupGhouliath.s.sol"; +import {GorillaxDeploy} from "../script/mons/SetupGorillax.s.sol"; +import {IblivionDeploy} from "../script/mons/SetupIblivion.s.sol"; +import {InutiaDeploy} from "../script/mons/SetupInutia.s.sol"; +import {MalalienDeploy} from "../script/mons/SetupMalalien.s.sol"; +import {PengymDeploy} from "../script/mons/SetupPengym.s.sol"; +import {SofabbiDeploy} from "../script/mons/SetupSofabbi.s.sol"; +import {VolthareDeploy} from "../script/mons/SetupVolthare.s.sol"; +import {XmonDeploy} from "../script/mons/SetupXmon.s.sol"; import {Engine} from "../src/Engine.sol"; import {IEngine} from "../src/IEngine.sol"; import {GachaTeamRegistry} from "../src/game-layer/GachaTeamRegistry.sol"; @@ -33,7 +43,7 @@ import {BatchHelper} from "./abstract/BatchHelper.sol"; import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// @notice Faithful replay of a REAL prod battle (26 turns, switch/no-op heavy): real mon loadouts -/// via SetupMons' canonical deployX() recipes + the log's per-turn moveIndex/salt/extraData, +/// via the generated per-mon deployX() recipes + the log's per-turn moveIndex/salt/extraData, /// run through LEGACY (per-turn execute), BUILT-IN (turn-by-turn submits + drain), and ONE-TX /// (CPU batch). Asserts byte-equal end state (equivalence) across all three. /// Gas accounting is production-faithful per tx: TX_BASE (21000 intrinsic) + the EIP-2028 @@ -41,7 +51,21 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// measured bracket and the call is dispatched low-level (see _measuredCall) because in prod the /// relayer/wallet encodes off-chain — measuring it would over-count paths with larger structs. /// Every path is measured on COLD slots (fresh storageKey) and again on REUSED slots. -contract RealMonReplayGasTest is Test, SetupMons, BatchHelper { +contract RealMonReplayGasTest is + Test, + GhouliathDeploy, + InutiaDeploy, + MalalienDeploy, + IblivionDeploy, + GorillaxDeploy, + SofabbiDeploy, + PengymDeploy, + EmbursaDeploy, + VolthareDeploy, + AuroxDeploy, + XmonDeploy, + BatchHelper +{ uint256 constant P0_PK = 0xA11CE; uint256 constant P1_PK = 0xB0B; uint256 constant TX_BASE = 21000; diff --git a/test/mons/MulongTest.sol b/test/mons/MulongTest.sol index 4a7e4def..06ca94e1 100644 --- a/test/mons/MulongTest.sol +++ b/test/mons/MulongTest.sol @@ -356,6 +356,7 @@ contract MulongTest is Test { function test_cloudStrike_basePowerReflectsStorm() public { _field(IMoveSet(address(cloudStrike)), address(0)); - assertEq(cloudStrike.basePower(IEngine(address(engine)), battleKey), cloudStrike.BASE_POWER(), "no storm"); + MoveMeta memory meta = cloudStrike.getMeta(IEngine(address(engine)), battleKey, 0, 0); + assertEq(meta.basePower, cloudStrike.BASE_POWER(), "no storm"); } } diff --git a/transpiler/codegen_rs/README.md b/transpiler/codegen_rs/README.md index 81ef76ff..745ea355 100644 --- a/transpiler/codegen_rs/README.md +++ b/transpiler/codegen_rs/README.md @@ -28,7 +28,9 @@ transpiler/ transpiler-config-rust.json includeFiles allowlist + world/dispatch config runtime-rs/ hand-written chomp-rt crate (source of truth; synced to rs-output/runtime) strategies-rs/ native CPU strategies + game loop + the standalone arena - (bins: `arena` win-rate table, `trace` behavioural analysis + counterfactual) + (bins: `arena` win-rate table, `trace` behavioural analysis + counterfactual, + `analyze` per-mon win%/KO matrix, `montrace` one mon's doubles diagnostic, + `teams` exhaustive team search + main effect; see BENCH.md) rs-output/ GENERATED cargo workspace (gitignored, like ts-output) ``` @@ -61,6 +63,13 @@ substrate. What still runs: - `cargo run --release -p chomp-strategies --bin arena -- --games 6000` — whole-game batches on the native stack, win-rate table + games/s. +- The roster loads from `$CHOMP_ROOT`'s `drool/*.csv` + `src/mons/*.json` at + run time, so data edits need no rebuild. A move or ability whose name + doesn't resolve to a contract is reported on stderr rather than silently + dropped — a dropped lane shortens the catalog and `build_team_mon` pads it + by repeating the last move, quietly changing the mon being measured. +- `analyze` / `teams` take `--progress` to stream partial standings to stderr; + results are byte-identical with and without it. - `chomp-rt` / `chomp-strategies` unit tests pin ABI/keccak/shift/pow and the mulberry32 golden stream standalone (`cargo test`). - `python3 -m unittest transpiler.test_transpiler` — TS target unaffected. diff --git a/transpiler/sol2rs.py b/transpiler/sol2rs.py index 49d3483d..f8a63918 100644 --- a/transpiler/sol2rs.py +++ b/transpiler/sol2rs.py @@ -25,7 +25,7 @@ import json import shutil from pathlib import Path -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, Tuple from .lexer import Lexer from .parser import Parser, SourceUnit @@ -221,7 +221,7 @@ def run(self) -> bool: print(f'Written: {out_path}') self._write_world_rs(engine_src, symbols) - self._write_dispatch_rs(engine_src, symbols) + all_notes.extend(self._write_dispatch_rs(engine_src, symbols)) self._write_lib_rs(engine_src, emitted) self._write_workspace_files() self._sync_crates() @@ -480,10 +480,13 @@ def _deploy_all_lines(self, symbols) -> List[str]: out.append('') return out - def _write_dispatch_rs(self, engine_src: Path, symbols) -> None: + def _write_dispatch_rs(self, engine_src: Path, symbols) -> List[Tuple[str, str]]: """Generated dispatch tables: one fn per dispatch-interface method, matching the target address's ContractId to the implementor's - module (with the msg.sender frame push around the call).""" + module (with the msg.sender frame push around the call). + + Returns the diagnostics raised while building the tables.""" + dispatch_notes: List[Tuple[str, str]] = [] from .codegen_rs.rust_types import RustTypeConverter, rust_ident from .codegen_rs.context import RustCodeGenerationContext @@ -569,6 +572,33 @@ def _write_dispatch_rs(self, engine_src: Path, symbols) -> None: impls = [(c, s) for c, s in impls if s is not None] if not impls: continue + + # A duck-typed probe resolves by selector, so an implementor whose signature differs + # from the probe's is simply not reachable through it. Emit the majority signature and + # drop the rest to None, mirroring the on-chain selector miss. + def sig_key(s): + return ( + tuple(types.rust_type(t) for t in s.param_types), + tuple(types.rust_type(t) for t in s.return_types), + ) + + def fmt_sig(key): + return f'{method}({", ".join(key[0])}) -> {", ".join(key[1]) or "()"}' + + by_sig: dict = {} + for c, s in impls: + by_sig.setdefault(sig_key(s), []).append((c, s)) + probe_key, impls = max(by_sig.items(), key=lambda kv: (len(kv[1]), kv[0])) + for other_key, others in sorted(by_sig.items()): + if other_key == probe_key: + continue + names = ', '.join(c for c, _ in others) + dispatch_notes.append(( + 'warning', + f'try_{method}: {names} {"declares" if len(others) == 1 else "declare"} ' + f'{fmt_sig(other_key)}, not the probe\'s {fmt_sig(probe_key)} — unreachable ' + f'by selector, dispatching to None', + )) sig = impls[0][1] params = ['world: &mut World', 'target: Address'] arg_names = [] @@ -602,6 +632,7 @@ def _write_dispatch_rs(self, engine_src: Path, symbols) -> None: (engine_src / 'dispatch.rs').write_text('\n'.join(lines)) print(f'Written: {engine_src / "dispatch.rs"}') + return dispatch_notes def _write_lib_rs(self, engine_src: Path, emitted: List[str]) -> None: # Build the nested module tree from emitted file paths. diff --git a/transpiler/strategies-rs/src/analysis.rs b/transpiler/strategies-rs/src/analysis.rs index 3811b286..21dfacab 100644 --- a/transpiler/strategies-rs/src/analysis.rs +++ b/transpiler/strategies-rs/src/analysis.rs @@ -10,10 +10,39 @@ use std::collections::HashMap; use crate::arena::{build_doubles_specs, build_specs, build_specs_full, build_specs_with, STRAT_PAIRS}; -use crate::doubles::run_doubles_games_instrumented; +use crate::doubles::{run_doubles_games_instrumented, DoublesInstr}; use crate::game::{run_games_instrumented, InstrRecord, BotName}; use crate::roster::{self, Roster}; +/// Work-in-progress reporter: (aggregate so far, games run, games total). +pub type MonProgress<'a> = &'a dyn Fn(&MonAnalysis, usize, usize); + +/// How many slices a progress-reporting run is cut into. +const PROGRESS_CHUNKS: usize = 20; + +/// Run in slices and re-fold after each so a long batch can show a partial table. Slicing only +/// moves scheduling boundaries — every game is independently seeded, so the final fold is the +/// same as running the batch whole. +fn run_folded( + specs: &[S], + progress: Option, + run: impl Fn(&[S]) -> Vec, + fold_all: impl Fn(&[R]) -> MonAnalysis, +) -> MonAnalysis { + let Some(report) = progress else { + return fold_all(&run(specs)); + }; + let size = specs.len().div_ceil(PROGRESS_CHUNKS).max(1); + let mut records: Vec = Vec::with_capacity(specs.len()); + let mut agg = fold_all(&records); + for chunk in specs.chunks(size) { + records.extend(run(chunk)); + agg = fold_all(&records); + report(&agg, records.len(), specs.len()); + } + agg +} + /// Per-mon accumulators, split by the mon's own team result that game. #[derive(Default, Clone)] pub struct MonStat { @@ -63,11 +92,17 @@ pub struct MonAnalysis { } /// Draw the same 4v4 field as the plain arena, run it instrumented, fold per-mon. -pub fn run_mon_analysis(roster: &Roster, games: usize, wseed: u32, seed_base: u32, threads: usize) -> MonAnalysis { +pub fn run_mon_analysis( + roster: &Roster, + games: usize, + wseed: u32, + seed_base: u32, + threads: usize, + progress: Option, +) -> MonAnalysis { let book = roster::address_book(); let (specs, _pair_of) = build_specs(roster, games, wseed, seed_base); - let records = run_games_instrumented(&specs, &book, threads); - fold(&records) + run_folded(&specs, progress, |c| run_games_instrumented(c, &book, threads), fold) } /// Same, with an explicit [p1, p0] pilot rotation (e.g. a no-peek-vs-peek matched-draft split). @@ -78,11 +113,11 @@ pub fn run_mon_analysis_with( seed_base: u32, threads: usize, pairs: &[(BotName, BotName)], + progress: Option, ) -> MonAnalysis { let book = roster::address_book(); let (specs, _pair_of) = build_specs_with(roster, games, wseed, seed_base, pairs); - let records = run_games_instrumented(&specs, &book, threads); - fold(&records) + run_folded(&specs, progress, |c| run_games_instrumented(c, &book, threads), fold) } /// Singles analysis over the standard pilot basket with loadout rotation — each drafted slot equips @@ -94,6 +129,7 @@ pub fn run_mon_analysis_rotated( seed_base: u32, threads: usize, pairs: Option<&[(BotName, BotName)]>, + progress: Option, ) -> MonAnalysis { let book = roster::address_book(); let default_pairs: Vec<(BotName, BotName)> = STRAT_PAIRS @@ -102,8 +138,7 @@ pub fn run_mon_analysis_rotated( .collect(); let pairs = pairs.unwrap_or(&default_pairs); let (specs, _) = build_specs_full(roster, games, wseed, seed_base, pairs, true); - let records = run_games_instrumented(&specs, &book, threads); - fold(&records) + run_folded(&specs, progress, |c| run_games_instrumented(c, &book, threads), fold) } /// Doubles per-mon attribution over the DIFF_PAIRS field. KOs are credited from the killer side's @@ -117,17 +152,21 @@ pub fn run_doubles_mon_analysis( threads: usize, rotate: bool, search_depth: u32, + progress: Option, ) -> MonAnalysis { let book = roster::address_book(); let (specs, _) = build_doubles_specs(roster, games, wseed, seed_base, rotate, search_depth); - let records = run_doubles_games_instrumented(&specs, &book, threads); + run_folded(&specs, progress, |c| run_doubles_games_instrumented(c, &book, threads), fold_doubles) +} +/// Fold instrumented doubles games into the same per-mon shape `fold` produces for singles. +pub fn fold_doubles(records: &[DoublesInstr]) -> MonAnalysis { let mut per_mon: HashMap = HashMap::new(); let mut ko_matrix: HashMap<(u32, u32), u32> = HashMap::new(); let (mut n_games, mut decided, mut draws) = (0u32, 0u32, 0u32); let (mut shared, mut incidental) = (0u32, 0u32); - for rec in &records { + for rec in records { n_games += 1; let (p0_res, p1_res): (i8, i8) = match rec.winner_side { Some(0) => { decided += 1; (1, -1) } diff --git a/transpiler/strategies-rs/src/bin/analyze.rs b/transpiler/strategies-rs/src/bin/analyze.rs index c80c557d..a1bcb486 100644 --- a/transpiler/strategies-rs/src/bin/analyze.rs +++ b/transpiler/strategies-rs/src/bin/analyze.rs @@ -7,6 +7,7 @@ use chomp_strategies::analysis::{ run_doubles_mon_analysis, run_mon_analysis, run_mon_analysis_rotated, run_mon_analysis_with, + MonAnalysis, }; use chomp_strategies::bots; use chomp_strategies::game::BotName; @@ -61,9 +62,24 @@ fn main() { eprintln!(" loadouts: rotated (uniform 4-subset per drafted slot)"); } + // --progress streams the ranked table to stderr as slices land, so a long run is watchable + // without waiting for the final fold. stdout stays the single clean result table. + let stream = args.iter().any(|a| a == "--progress"); + let reporter = |a: &MonAnalysis, done: usize, total: usize| { + let mut ids: Vec = a.per_mon.keys().copied().collect(); + ids.sort_by(|&x, &y| a.per_mon[&y].win_rate().partial_cmp(&a.per_mon[&x].win_rate()).unwrap()); + let line = ids + .iter() + .map(|id| format!("{} {:.1}", roster.mon_name(*id), a.per_mon[id].win_rate() * 100.0)) + .collect::>() + .join(" · "); + eprintln!("[{:>3}% {}/{}] {}", done * 100 / total.max(1), done, total, line); + }; + let progress: Option<&dyn Fn(&MonAnalysis, usize, usize)> = if stream { Some(&reporter) } else { None }; + let started = std::time::Instant::now(); let a = if mode == "doubles" { - run_doubles_mon_analysis(&roster, games, seed, seed_base, threads, rotate, search_depth) + run_doubles_mon_analysis(&roster, games, seed, seed_base, threads, rotate, search_depth, progress) } else { match (&p1, &p0) { (Some(p1s), Some(p0s)) => { @@ -73,13 +89,13 @@ fn main() { ); eprintln!(" pilots: p1={p1s} vs p0={p0s}"); if rotate { - run_mon_analysis_rotated(&roster, games, seed, seed_base, threads, Some(&[pair])) + run_mon_analysis_rotated(&roster, games, seed, seed_base, threads, Some(&[pair]), progress) } else { - run_mon_analysis_with(&roster, games, seed, seed_base, threads, &[pair]) + run_mon_analysis_with(&roster, games, seed, seed_base, threads, &[pair], progress) } } - _ if rotate => run_mon_analysis_rotated(&roster, games, seed, seed_base, threads, None), - _ => run_mon_analysis(&roster, games, seed, seed_base, threads), + _ if rotate => run_mon_analysis_rotated(&roster, games, seed, seed_base, threads, None, progress), + _ => run_mon_analysis(&roster, games, seed, seed_base, threads, progress), } }; let elapsed = started.elapsed().as_secs_f64(); diff --git a/transpiler/strategies-rs/src/bin/cert.rs b/transpiler/strategies-rs/src/bin/cert.rs index 4517c9c5..b9c4e589 100644 --- a/transpiler/strategies-rs/src/bin/cert.rs +++ b/transpiler/strategies-rs/src/bin/cert.rs @@ -24,7 +24,7 @@ fn main() { // Check 1 — globalKV-class regression: fixed-model pilots draw ~never and never error. The // globalKV forward-model bug manifested as stall-loop draws (50–167 per 2000 pre-fix). - let a = run_mon_analysis(&roster, 4000, 0xbeefcafe, 10_000, default_threads); + let a = run_mon_analysis(&roster, 4000, 0xbeefcafe, 10_000, default_threads, None); let draw_rate = a.draws as f64 / a.games.max(1) as f64; let ok1 = draw_rate < 0.01 && a.errors == 0; println!("[1] draw rate {:.3}% (<1%), errors {} (0): {}", draw_rate * 100.0, a.errors, if ok1 { "PASS" } else { "FAIL" }); diff --git a/transpiler/strategies-rs/src/bin/montrace.rs b/transpiler/strategies-rs/src/bin/montrace.rs index fe28bb0b..3868fee1 100644 --- a/transpiler/strategies-rs/src/bin/montrace.rs +++ b/transpiler/strategies-rs/src/bin/montrace.rs @@ -83,6 +83,68 @@ fn main() { println!(" min HP reached {:.1}% (mean over appearances)", sum_min_hp / nf); println!(" setup ever landed {:.1}% of appearances (Attack delta > 0 at any point)", ever_boosted as f64 / nf * 100.0); + // Teammate / opponent uplift: does the target's presence change what everyone ELSE does? + // Every bucket excludes the target itself and normalises by active turns, so it isolates the + // effect on other mons rather than re-counting the target's own output. + let (mut mate_kos, mut mate_turns) = (0u64, 0u64); + let (mut opp_kos, mut opp_turns) = (0u64, 0u64); + let (mut ctl_kos, mut ctl_turns) = (0u64, 0u64); + let (mut with_len, mut with_n, mut ctl_len, mut ctl_n) = (0u64, 0u64, 0u64, 0u64); + for spec in &specs { + let tr = play_doubles_game_instrumented(spec, &book, None); + let sides = [(0u8, &tr.p0_ids, &tr.active_turns_p0), (1u8, &tr.p1_ids, &tr.active_turns_p1)]; + let target_side = sides.iter().find(|(_, ids, _)| ids.contains(&tid)).map(|(s, _, _)| *s); + if target_side.is_some() { + with_len += tr.turns as u64; + with_n += 1; + } else { + ctl_len += tr.turns as u64; + ctl_n += 1; + } + for (s, ids, turns) in sides.iter() { + let kos = tr.kos.iter().filter(|k| k.killer_seat == *s && k.killer_id != tid).count() as u64; + let act: u64 = ids + .iter() + .zip(turns.iter()) + .filter(|(id, _)| **id != tid) + .map(|(_, t)| *t as u64) + .sum(); + match target_side { + Some(ts) if ts == *s => { + mate_kos += kos; + mate_turns += act; + } + Some(_) => { + opp_kos += kos; + opp_turns += act; + } + None => { + ctl_kos += kos; + ctl_turns += act; + } + } + } + } + let per100 = |k: u64, t: u64| k as f64 / t.max(1) as f64 * 100.0; + let base = per100(ctl_kos, ctl_turns); + println!("\n KOs per 100 active turns (excluding {target} itself):"); + println!( + " {target}'s teammates {:>6.2} ({:+.1}% vs control)", + per100(mate_kos, mate_turns), + (per100(mate_kos, mate_turns) / base - 1.0) * 100.0 + ); + println!( + " {target}'s opponents {:>6.2} ({:+.1}% vs control)", + per100(opp_kos, opp_turns), + (per100(opp_kos, opp_turns) / base - 1.0) * 100.0 + ); + println!(" control (no {target}) {:>6.2}", base); + println!( + " game length {:.2} turns with {target} vs {:.2} without", + with_len as f64 / with_n.max(1) as f64, + ctl_len as f64 / ctl_n.max(1) as f64 + ); + let total: u64 = usage.iter().sum(); println!("\n move usage over {total} acting turns:"); for (mi, &c) in usage.iter().enumerate() { diff --git a/transpiler/strategies-rs/src/bin/teams.rs b/transpiler/strategies-rs/src/bin/teams.rs index 50aa72fb..d65157f7 100644 --- a/transpiler/strategies-rs/src/bin/teams.rs +++ b/transpiler/strategies-rs/src/bin/teams.rs @@ -2,7 +2,7 @@ //! cargo run --release -p chomp-strategies --bin teams -- --games-per-team 300 use chomp_strategies::roster::load_roster; -use chomp_strategies::teams::{run_team_search, synergy}; +use chomp_strategies::teams::{run_team_search, synergy, TeamResult}; use std::path::PathBuf; fn arg(args: &[String], flag: &str) -> Option { @@ -30,8 +30,24 @@ fn main() { let roster = load_roster(&chomp_root); let team_str = |t: &[u32]| t.iter().map(|&id| roster.mon_name(id)).collect::>().join(", "); + // --progress streams the per-mon main effect to stderr as slices land; it's the number that + // matters here and it stabilises long before the full search finishes. + let stream = args.iter().any(|a| a == "--progress"); + let reporter = |partial: &[TeamResult], done: usize, total: usize| { + let syn = synergy(partial); + let mut m: Vec<(u32, f64)> = syn.main.iter().map(|(&k, &v)| (k, v)).collect(); + m.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + let line = m + .iter() + .map(|(id, v)| format!("{} {:+.1}", roster.mon_name(*id), v * 100.0)) + .collect::>() + .join(" · "); + eprintln!("[{:>3}% {}/{}] {}", done * 100 / total.max(1), done, total, line); + }; + let progress: Option<&dyn Fn(&[TeamResult], usize, usize)> = if stream { Some(&reporter) } else { None }; + let started = std::time::Instant::now(); - let mut results = run_team_search(&roster, gpt, seed, seed_base, threads); + let mut results = run_team_search(&roster, gpt, seed, seed_base, threads, progress); let syn = synergy(&results); let elapsed = started.elapsed().as_secs_f64(); diff --git a/transpiler/strategies-rs/src/heuristic.rs b/transpiler/strategies-rs/src/heuristic.rs index 0634abe0..bc7c7184 100644 --- a/transpiler/strategies-rs/src/heuristic.rs +++ b/transpiler/strategies-rs/src/heuristic.rs @@ -184,7 +184,7 @@ fn find_best_switch_candidate( sim, seat, bk, VOPP, opponent_mon_index, VCPU, candidate_mon_index, ); let static_dmg = if can_estimate { - estimate_damage(sim, bk, &mut ctx, opp_move_slot.unwrap(), opp_move_class.unwrap()) + estimate_damage(sim, bk, &mut ctx, VOPP, opponent_mon_index, opp_move_slot.unwrap()) } else { 0 }; @@ -279,7 +279,7 @@ fn is_free_turn_reveal( if opp_class != MoveClass::Other && opp_class != MoveClass::Self_ { return false; } - get_move_base_power(sim, bk, slot) == 0 + get_move_base_power(sim, bk, VOPP, opponent_mon_index, slot) == 0 } /// `freeTurnPick`: configured switch-in -> 2HKO -> momentum-guarded setup. @@ -490,7 +490,7 @@ fn decide_inner( opp_move_class = Some(mc); if mc == MoveClass::Physical || mc == MoveClass::Special { let mut ctx_to_us = damage_calc_context(sim, seat, bk, VOPP, VCPU); - damage_to_us = estimate_damage(sim, bk, &mut ctx_to_us, slot, mc); + damage_to_us = estimate_damage(sim, bk, &mut ctx_to_us, VOPP, opponent_mon_index, slot); } } // True reveal damage from the sim; the static estimate stays as the diff --git a/transpiler/strategies-rs/src/matrix.rs b/transpiler/strategies-rs/src/matrix.rs index ce19e47b..b6114f6f 100644 --- a/transpiler/strategies-rs/src/matrix.rs +++ b/transpiler/strategies-rs/src/matrix.rs @@ -85,7 +85,7 @@ pub fn compute_static_matrix(roster: &Roster) -> StaticMatrix { if let Some(slot) = move_slot(&mut sim, obs, bk, VOPP, 0, mi) { let mc = slot_move_class(&mut sim, bk, slot); if mc == MoveClass::Physical || mc == MoveClass::Special { - let d = estimate_damage(&mut sim, bk, &mut ctx, slot, mc); + let d = estimate_damage(&mut sim, bk, &mut ctx, VOPP, 0, slot); if d > best { best = d; } diff --git a/transpiler/strategies-rs/src/mock2.rs b/transpiler/strategies-rs/src/mock2.rs index d9d28a0b..aedcd8a3 100644 --- a/transpiler/strategies-rs/src/mock2.rs +++ b/transpiler/strategies-rs/src/mock2.rs @@ -21,7 +21,7 @@ use crate::sim::Sim; use crate::shared::{build_damage_calc_context, estimate_damage}; use crate::view::{ active_mon_indices, ko_bitmap, mon_max_hp, mon_skip_turn, mon_state, mon_value, move_slot, - slot_move_class, slot_move_type, Mv, Seat, NO_OP_INDEX, SWITCH_MOVE_INDEX, VCPU, VOPP, + slot_move_type, Mv, Seat, NO_OP_INDEX, SWITCH_MOVE_INDEX, VCPU, VOPP, }; /// Pack an inline move word from its fields. @@ -208,8 +208,7 @@ fn opp_action_power(sim: &mut Sim, bk: B256, obs: Seat, rule: PowerRule, opp_mov Some(m) if m.move_index < 4 => match move_slot(sim, obs, bk, opp_vp, opp_active, m.move_index as usize) { Some(word) => { let mut ctx = build_damage_calc_context(sim, obs, bk, opp_vp, opp_active, self_vp, self_active); - let mc = slot_move_class(sim, bk, word); - estimate_damage(sim, bk, &mut ctx, word, mc) + estimate_damage(sim, bk, &mut ctx, opp_vp, opp_active, word) } None => 0, }, diff --git a/transpiler/strategies-rs/src/roster.rs b/transpiler/strategies-rs/src/roster.rs index 4320dad3..0ada2dfe 100644 --- a/transpiler/strategies-rs/src/roster.rs +++ b/transpiler/strategies-rs/src/roster.rs @@ -28,12 +28,14 @@ pub fn address_book() -> std::collections::HashMap { CONTRACT_NAMES.iter().map(|&n| (n.to_string(), addr_of(n))).collect() } -/// "Bull Rush" / "Hit-And-Dip" → "BullRush" / "HitAndDip": split on space/hyphen, capitalize each -/// word's first char (rest untouched), join. Mirrors mon-builder.ts:moveNameToContract. +/// "Bull Rush" / "Hit-And-Dip" / "King's Respite" → "BullRush" / "HitAndDip" / "KingsRespite": +/// split on space/hyphen, capitalize each word's first char, drop remaining punctuation, join. +/// Mirrors mon-builder.ts:moveNameToContract and the deploy generator's `\W+` strip. fn move_name_to_contract(name: &str) -> String { let mut out = String::new(); for word in name.split(|c: char| c.is_whitespace() || c == '-').filter(|w| !w.is_empty()) { - let mut chars = word.chars(); + let cleaned: String = word.chars().filter(|c| c.is_alphanumeric()).collect(); + let mut chars = cleaned.chars(); if let Some(first) = chars.next() { out.extend(first.to_uppercase()); out.push_str(chars.as_str()); @@ -69,20 +71,26 @@ fn class_from_name(name: &str) -> u64 { match name { "Physical" => 0, "Special" => 1, "Self" => 2, "Other" => 3, o => panic!("class {o:?}") } } +/// An inline move's .json holds only its effect; every packed value comes from moves.csv. #[derive(Deserialize)] struct InlineMoveJson { - #[serde(rename = "basePower")] base_power: u64, - #[serde(rename = "staminaCost")] stamina_cost: u64, - #[serde(rename = "moveType")] move_type: String, - #[serde(rename = "moveClass")] move_class: String, - #[serde(rename = "effectAccuracy")] effect_accuracy: u64, effect: Option, - #[serde(default)] priority: u64, +} + +/// Inline-move packing params, read off the move's moves.csv row. +#[derive(Clone)] +struct InlineParams { + base_power: u64, + stamina_cost: u64, + move_type: String, + move_class: String, + priority: u64, + effect_accuracy: u64, } /// packMoves.py layout: [basePower:8|moveClass:2|priority:2|moveType:4|stamina:4|effectAccuracy:8|_:68|effect:160]. -fn pack_inline_move(m: &InlineMoveJson) -> U256 { - let effect_addr = match &m.effect { +fn pack_inline_move(p: &InlineParams, effect: Option<&str>) -> U256 { + let effect_addr = match effect { Some(e) if !e.is_empty() => U256::from_be_bytes::<32>({ let mut b = [0u8; 32]; b[12..].copy_from_slice(addr_of(&move_name_to_contract(e)).as_slice()); @@ -90,15 +98,23 @@ fn pack_inline_move(m: &InlineMoveJson) -> U256 { }), _ => U256::ZERO, }; - (U256::from(m.base_power) << 248) - | (U256::from(class_from_name(&m.move_class)) << 246) - | (U256::from(m.priority) << 244) - | (U256::from(type_from_name(&m.move_type) as u64) << 240) - | (U256::from(m.stamina_cost) << 236) - | (U256::from(m.effect_accuracy) << 228) + (U256::from(p.base_power) << 248) + | (U256::from(class_from_name(&p.move_class)) << 246) + | (U256::from(p.priority) << 244) + | (U256::from(type_from_name(&p.move_type) as u64) << 240) + | (U256::from(p.stamina_cost) << 236) + | (U256::from(p.effect_accuracy) << 228) | effect_addr } +/// Value of a `NAME=VAL;…` entry in the moves.csv Constants column. +fn csv_constant(raw: &str, name: &str) -> Option { + raw.split(';') + .filter_map(|p| p.trim().split_once('=')) + .find(|(k, _)| k.trim() == name) + .and_then(|(_, v)| v.trim().parse().ok()) +} + pub fn addr_to_word(a: Address) -> U256 { let mut b = [0u8; 32]; b[12..].copy_from_slice(a.as_slice()); @@ -241,12 +257,16 @@ pub fn load_roster(chomp_root: &Path) -> Roster { let drool = chomp_root.join("drool"); let src_mons = chomp_root.join("src").join("mons"); - // moves.csv → per-mon ordered (name, unlock) list, plus the deployed-move → InputType map. + // moves.csv → per-mon ordered (name, unlock, inline params) list, plus the deployed-move → + // InputType map. The inline params are unused for deployed moves (their row may carry '?'). let (mh, mrows) = read_csv(&drool.join("moves.csv")); let (m_name, m_mon, m_unlock) = (col(&mh, "Name"), col(&mh, "Mon"), col(&mh, "UnlockLevel")); let m_input = col(&mh, "InputType"); let m_tspec = col(&mh, "TargetSpec"); - let mut moves_by_mon: Vec<(String, Vec<(String, u8)>)> = Vec::new(); + let (m_power, m_stamina) = (col(&mh, "Power"), col(&mh, "Stamina")); + let (m_type, m_class, m_prio) = (col(&mh, "Type"), col(&mh, "Class"), col(&mh, "Priority")); + let m_consts = col(&mh, "Constants"); + let mut moves_by_mon: Vec<(String, Vec<(String, u8, InlineParams)>)> = Vec::new(); let mut input_by_addr: std::collections::HashMap = std::collections::HashMap::new(); let mut tspec_by_addr: std::collections::HashMap = std::collections::HashMap::new(); for r in &mrows { @@ -255,7 +275,18 @@ pub fn load_roster(chomp_root: &Path) -> Roster { Some(e) => e, None => { moves_by_mon.push((mon.clone(), Vec::new())); moves_by_mon.last_mut().unwrap() } }; - entry.1.push((r[m_name].clone(), r[m_unlock].parse().unwrap_or(0))); + entry.1.push(( + r[m_name].clone(), + r[m_unlock].parse().unwrap_or(0), + InlineParams { + base_power: r[m_power].parse().unwrap_or(0), + stamina_cost: r[m_stamina].parse().unwrap_or(0), + move_type: r[m_type].clone(), + move_class: r[m_class].clone(), + priority: r[m_prio].parse().unwrap_or(0), + effect_accuracy: csv_constant(&r[m_consts], "EFFECT_ACCURACY").unwrap_or(0), + }, + )); let contract = move_name_to_contract(&r[m_name]); if is_deployed(&contract) { @@ -280,20 +311,30 @@ pub fn load_roster(chomp_root: &Path) -> Roster { let (c_t1, c_t2) = (col(&h, "Type1"), col(&h, "Type2")); let mut mons = Vec::new(); + // A dropped move silently shortens a mon's catalog (build_team_mon then pads by repeating the + // last lane), which quietly changes the mon being measured — report them rather than hide them. + let mut dropped: Vec = Vec::new(); for r in &rows { let name = r[c_name].clone(); let mon_dir = name.to_lowercase(); let mut catalog = Vec::new(); - for (mv_name, unlock) in moves_for(&name) { + for (mv_name, unlock, params) in moves_for(&name) { let contract = move_name_to_contract(&mv_name); let word = if is_deployed(&contract) { addr_to_word(addr_of(&contract)) } else { let json_path = src_mons.join(&mon_dir).join(format!("{contract}.json")); match fs::read_to_string(&json_path) { - Ok(text) => pack_inline_move(&serde_json::from_str(&text).unwrap()), - Err(_) => continue, // unimplemented move — skip (mirrors monCatalog) + Ok(text) => { + let j: InlineMoveJson = serde_json::from_str(&text).unwrap(); + pack_inline_move(¶ms, j.effect.as_deref()) + } + Err(_) => { + // unimplemented move — skip (mirrors monCatalog) + dropped.push(format!("{name}/{mv_name}")); + continue; + } } }; catalog.push(CatalogMove { word, name: mv_name, unlock_level: unlock }); @@ -302,11 +343,20 @@ pub fn load_roster(chomp_root: &Path) -> Roster { panic!("mon {name} has no resolvable moves"); } - let ability = ability_for(&name) - .map(|a| move_name_to_contract(&a)) - .filter(|c| is_deployed(c)) - .map(|c| addr_to_word(addr_of(&c))) - .unwrap_or(U256::ZERO); + let ability = match ability_for(&name) { + Some(a) => { + let contract = move_name_to_contract(&a); + if is_deployed(&contract) { + addr_to_word(addr_of(&contract)) + } else { + // Silently losing an ability changes the mon being measured just as a dropped + // move does. + dropped.push(format!("{name}/{a} (ability)")); + U256::ZERO + } + } + None => U256::ZERO, + }; mons.push(RosterMon { id: r[c_id].parse().unwrap(), @@ -326,6 +376,13 @@ pub fn load_roster(chomp_root: &Path) -> Roster { catalog, }); } + if !dropped.is_empty() { + eprintln!( + "roster: {} move(s) had no contract or inline json and were dropped: {}", + dropped.len(), + dropped.join(", ") + ); + } mons.sort_by_key(|m| m.id); Roster { mons } } diff --git a/transpiler/strategies-rs/src/shared.rs b/transpiler/strategies-rs/src/shared.rs index 459ec9f6..25fe9a28 100644 --- a/transpiler/strategies-rs/src/shared.rs +++ b/transpiler/strategies-rs/src/shared.rs @@ -5,15 +5,15 @@ //! integral (i64); `Math.floor(int/int)` sites reproduce the JS f64 division //! + floor (identical results at these magnitudes). -use chomp_engine::moves::{AttackCalculator, MoveSlotLib}; +use chomp_engine::moves::AttackCalculator; use chomp_engine::Enums::{MonStateIndexName, MoveClass, Type}; use chomp_engine::Structs::{DamageCalcContext, MoveMeta}; use chomp_rt::{Address, B256, U256}; use crate::sim::Sim; use crate::view::{ - mon_current_hp, mon_max_hp, mon_state, mon_stats, mon_value, move_slot, slot_external_base_power, - slot_move_class, slot_move_type, type_effectiveness, Mv, Seat, SWITCH_MOVE_INDEX, VCPU, VOPP, + decode_meta, mon_current_hp, mon_max_hp, mon_state, mon_stats, mon_value, move_slot, + slot_move_class, type_effectiveness, Mv, Seat, SWITCH_MOVE_INDEX, VCPU, VOPP, }; /// JS Number.MAX_SAFE_INTEGER — the TS sentinel for "least-of" scans. @@ -109,19 +109,17 @@ pub fn estimate_damage( sim: &mut Sim, bk: B256, ctx: &mut DamageCalcContext, + vp: u8, + mon: usize, raw_move_slot: U256, - move_class: MoveClass, ) -> i64 { - let base_power = if MoveSlotLib::isInline(raw_move_slot) { - MoveSlotLib::basePower(raw_move_slot, bk) - } else { - slot_external_base_power(sim, bk, raw_move_slot) - }; - if base_power == 0 { + // getMeta is the interface every move implements, so it reads dynamic power the legacy + // basePower(battleKey) probe couldn't see (it missed any move not declaring that exact shape). + let meta = decode_meta(sim, bk, vp, mon, raw_move_slot); + if meta.basePower == 0 { return 0; } - let move_type = slot_move_type(sim, bk, raw_move_slot); - deterministic_damage(ctx, base_power, move_type, move_class) + deterministic_damage(ctx, meta.basePower, meta.moveType, meta.moveClass) } /// Port of `_estimateDamageMeta`: like estimate_damage off a pre-decoded meta. @@ -133,12 +131,8 @@ pub fn estimate_damage_meta(ctx: &mut DamageCalcContext, meta: &MoveMeta) -> i64 } /// Port of `_getMoveBasePower`: basePower of a raw slot, 0 for non-attacks. -pub fn get_move_base_power(sim: &mut Sim, bk: B256, raw_move_slot: U256) -> i64 { - if MoveSlotLib::isInline(raw_move_slot) { - MoveSlotLib::basePower(raw_move_slot, bk) as i64 - } else { - slot_external_base_power(sim, bk, raw_move_slot) as i64 - } +pub fn get_move_base_power(sim: &mut Sim, bk: B256, vp: u8, mon: usize, raw_move_slot: U256) -> i64 { + decode_meta(sim, bk, vp, mon, raw_move_slot).basePower as i64 } // --------------------------------------------------------------------------- @@ -308,12 +302,12 @@ pub fn select_best_switch( // Read the revealed move; only Physical/Special are estimable. let slot = move_slot(sim, seat, bk, VOPP, opponent_mon_index, opponent_move_index as usize); - let (opp_move_slot, opp_move_class, can_estimate) = match slot { + let (opp_move_slot, can_estimate) = match slot { Some(s) => { let mc = slot_move_class(sim, bk, s); - (s, mc, mc == MoveClass::Physical || mc == MoveClass::Special) + (s, mc == MoveClass::Physical || mc == MoveClass::Special) } - None => (U256::ZERO, MoveClass::Physical, false), + None => (U256::ZERO, false), }; if !can_estimate { return switches[0]; @@ -327,7 +321,7 @@ pub fn select_best_switch( let mut ctx = build_damage_calc_context( sim, seat, bk, VOPP, opponent_mon_index, VCPU, candidate_mon_index, ); - let dmg = estimate_damage(sim, bk, &mut ctx, opp_move_slot, opp_move_class); + let dmg = estimate_damage(sim, bk, &mut ctx, VOPP, opponent_mon_index, opp_move_slot); if dmg < least_damage { least_damage = dmg; best_idx = i; diff --git a/transpiler/strategies-rs/src/teams.rs b/transpiler/strategies-rs/src/teams.rs index 55bda34d..82910402 100644 --- a/transpiler/strategies-rs/src/teams.rs +++ b/transpiler/strategies-rs/src/teams.rs @@ -52,7 +52,31 @@ impl TeamResult { } /// Every 4-mon team vs `games_per_team` random-field opponents (greedy both sides). -pub fn run_team_search(roster: &Roster, games_per_team: usize, wseed: u32, seed_base: u32, threads: usize) -> Vec { +/// Work-in-progress reporter: (team results so far, games run, games total). +pub type TeamProgress<'a> = &'a dyn Fn(&[TeamResult], usize, usize); + +fn tally(r: &mut TeamResult, o: &Result) { + r.games += 1; + if let Ok(g) = o { + match g.winner_seat { + Some(1) => r.wins += 1, // p1 = the searched team + Some(0) => r.losses += 1, + _ => {} + } + } +} + +/// How many slices a progress-reporting run is cut into. +const PROGRESS_CHUNKS: usize = 20; + +pub fn run_team_search( + roster: &Roster, + games_per_team: usize, + wseed: u32, + seed_base: u32, + threads: usize, + progress: Option, +) -> Vec { let book = roster::address_book(); let ids: Vec = roster.mons.iter().map(|m| m.id).collect(); let n = ids.len(); @@ -94,21 +118,32 @@ pub fn run_team_search(roster: &Roster, games_per_team: usize, wseed: u32, seed_ } } - let outcomes = run_games(&specs, &book, threads, false); let mut results: Vec = teams .iter() .map(|c| TeamResult { team: c.iter().map(|&i| ids[i]).collect(), games: 0, wins: 0, losses: 0 }) .collect(); - for (i, o) in outcomes.iter().enumerate() { - let r = &mut results[team_of[i]]; - r.games += 1; - if let Ok(g) = o { - match g.winner_seat { - Some(1) => r.wins += 1, // p1 = the searched team - Some(0) => r.losses += 1, - _ => {} - } + + let Some(report) = progress else { + for (i, o) in run_games(&specs, &book, threads, false).iter().enumerate() { + tally(&mut results[team_of[i]], o); + } + return results; + }; + + // Specs are team-major, so consecutive slices would cover only the first teams and make the + // partial main effect wildly biased. Walk a strided permutation instead: same games, same + // seeds, but every slice samples across the whole team space. + let stride = PROGRESS_CHUNKS.min(specs.len().max(1)); + let order: Vec = (0..stride).flat_map(|off| (off..specs.len()).step_by(stride)).collect(); + let chunk_len = order.len().div_ceil(PROGRESS_CHUNKS).max(1); + let mut done = 0usize; + for idx_chunk in order.chunks(chunk_len) { + let batch: Vec = idx_chunk.iter().map(|&i| specs[i].clone()).collect(); + for (j, o) in run_games(&batch, &book, threads, false).iter().enumerate() { + tally(&mut results[team_of[idx_chunk[j]]], o); } + done += idx_chunk.len(); + report(&results, done, specs.len()); } results } diff --git a/transpiler/strategies-rs/src/view.rs b/transpiler/strategies-rs/src/view.rs index 7a9e036b..fb7fd8ec 100644 --- a/transpiler/strategies-rs/src/view.rs +++ b/transpiler/strategies-rs/src/view.rs @@ -210,13 +210,6 @@ pub fn slot_priority(sim: &mut Sim, bk: B256, vp: u8, slot: U256) -> u32 { MoveSlotLib::priority(&mut sim.world, slot, sim.engine_addr, bk, U256::from(vp as u64)) } -/// External move's `basePower(battleKey)` — duck-typed probe, 0 for moves -/// that don't expose it (the TS try/`typeof`-guard path). -pub fn slot_external_base_power(sim: &mut Sim, bk: B256, slot: U256) -> u32 { - let target = MoveSlotLib::toIMoveSet(slot); - chomp_engine::dispatch::try_basePower(&mut sim.world, target, bk).unwrap_or(0) -} - /// External move's `accuracy(battleKey)` — duck-typed probe, None when absent. pub fn slot_external_accuracy(sim: &mut Sim, bk: B256, slot: U256) -> Option { let target = MoveSlotLib::toIMoveSet(slot); diff --git a/transpiler/transpiler-config-rust.json b/transpiler/transpiler-config-rust.json index 9b6f93b2..ea44efb2 100644 --- a/transpiler/transpiler-config-rust.json +++ b/transpiler/transpiler-config-rust.json @@ -82,7 +82,6 @@ "IAbility" ], "duckDispatchMethods": [ - "basePower", "accuracy" ] } \ No newline at end of file From eef902906e8e37803dfe20ffab760be3c714b765 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:49:00 -0700 Subject: [PATCH 37/41] clean up --- README.md | 13 +++++++++++-- script/SetupCPU.s.sol | 22 +++++++++++----------- script/cpu-teams.json | 6 +++--- script/mons/SetupGlob.s.sol | 2 +- script/mons/SetupMulong.s.sol | 4 ++-- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6e6a5d1f..e91b278e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ mechanical differences are: - far greater support for state-based moves / mechanics - stamina-based resource system instead of PP for balancing moves -## Getting Started +## getting started This repo uses [foundry](https://book.getfoundry.sh/getting-started/installation). @@ -40,7 +40,16 @@ To get started: `forge test` -## Main Components +## game data + +mon stats, move rows, and CPU teams live in `drool/mons.csv`, `drool/moves.csv`, and +`script/cpu-teams.json`. + +There's a local visual editor at: + +`python processing/editData.py` + +## solidity components ### Engine.sol Main entry point for creating/advancing Battles. diff --git a/script/SetupCPU.s.sol b/script/SetupCPU.s.sol index fb9a98bf..9c679d38 100644 --- a/script/SetupCPU.s.sol +++ b/script/SetupCPU.s.sol @@ -24,26 +24,26 @@ contract SetupCPU is Script { uint256[] memory monIndices = new uint256[](4); uint8[] memory facetIds = new uint8[](4); - monIndices[0] = 0; // Ghouliath - monIndices[1] = 3; // Iblivion - monIndices[2] = 10; // Xmon - monIndices[3] = 5; // Sofabbi + monIndices[0] = 14; // Mulong + monIndices[1] = 12; // Nirvamma + monIndices[2] = 11; // Ekineki + monIndices[3] = 1; // Inutia for (uint256 i; i < cpuPlayers.length; i++) { gachaTeamRegistry.setTeamForUser(vm.envAddress(cpuPlayers[i]), 0, monIndices, facetIds); } - monIndices[0] = 1; // Inutia - monIndices[1] = 11; // Ekineki - monIndices[2] = 6; // Pengym - monIndices[3] = 7; // Embursa + monIndices[0] = 0; // Ghouliath + monIndices[1] = 5; // Sofabbi + monIndices[2] = 8; // Volthare + monIndices[3] = 9; // Aurox for (uint256 i; i < cpuPlayers.length; i++) { gachaTeamRegistry.setTeamForUser(vm.envAddress(cpuPlayers[i]), 1, monIndices, facetIds); } monIndices[0] = 2; // Malalien - monIndices[1] = 9; // Aurox - monIndices[2] = 4; // Gorillax - monIndices[3] = 8; // Volthare + monIndices[1] = 6; // Pengym + monIndices[2] = 10; // Xmon + monIndices[3] = 13; // Glob for (uint256 i; i < cpuPlayers.length; i++) { gachaTeamRegistry.setTeamForUser(vm.envAddress(cpuPlayers[i]), 2, monIndices, facetIds); } diff --git a/script/cpu-teams.json b/script/cpu-teams.json index f51d6401..6bf72589 100644 --- a/script/cpu-teams.json +++ b/script/cpu-teams.json @@ -1,8 +1,8 @@ { "cpuPlayers": ["CPU"], "teams": [ - [0, 3, 10, 5], - [1, 11, 6, 7], - [2, 9, 4, 8] + [14, 12, 11, 1], + [0, 5, 8, 9], + [2, 6, 10, 13] ] } diff --git a/script/mons/SetupGlob.s.sol b/script/mons/SetupGlob.s.sol index 48437e73..f8977fc6 100644 --- a/script/mons/SetupGlob.s.sol +++ b/script/mons/SetupGlob.s.sol @@ -44,7 +44,7 @@ abstract contract GlobDeploy is Script { hp: 301, stamina: 5, speed: 151, - attack: 200, + attack: 180, defense: 201, specialAttack: 115, specialDefense: 191, diff --git a/script/mons/SetupMulong.s.sol b/script/mons/SetupMulong.s.sol index 2002a323..87eebc25 100644 --- a/script/mons/SetupMulong.s.sol +++ b/script/mons/SetupMulong.s.sol @@ -48,9 +48,9 @@ abstract contract MulongDeploy is Script { function _registerMulong(GachaTeamRegistry registry, address[4] memory addrs) internal { MonStats memory stats = MonStats({ - hp: 368, + hp: 388, stamina: 5, - speed: 290, + speed: 212, attack: 115, defense: 188, specialAttack: 288, From ca0da2678a33fc8acf7261f90956a7aa18d6c0fa Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:08:17 -0700 Subject: [PATCH 38/41] rename --- CLAUDE.md | 4 +- deployments.json | 167 ++++++----- drool/imgs/icons/glob_icon.gif | Bin 0 -> 1871 bytes drool/imgs/icons/mulong_icon.gif | Bin 0 -> 1894 bytes drool/imgs/mon_micro.png | Bin 2532 -> 3006 bytes drool/imgs/mon_mini.png | Bin 5203 -> 6085 bytes drool/imgs/mon_shadow.json | 32 +- drool/imgs/mon_shadow.png | Bin 1078 -> 1329 bytes drool/imgs/mon_spritesheet.png | Bin 64738 -> 83651 bytes drool/imgs/mon_switch.png | Bin 47978 -> 56230 bytes drool/imgs/spritesheet.json | 278 ++++++++++++------ drool/moves.csv | 2 +- foundry.toml | 2 +- processing/createMonShadows.py | 11 + processing/deploy.py | 2 +- processing/generateMonsTypeScript.py | 10 +- snapshots/DoublesGasBenchmark.json | 20 +- snapshots/EngineOptimizationTest.json | 4 +- snapshots/FullyOptimizedInlineGasTest.json | 6 +- snapshots/MainnetReplayGasTest.json | 16 +- snapshots/MatchmakerTest.json | 6 +- snapshots/ProdPvPGasBenchmark.json | 8 +- snapshots/StartBattleGasTest.json | 6 +- src/Engine.sol | 108 +++---- .../{KingsRespite.sol => GrandRespite.sol} | 2 +- 25 files changed, 393 insertions(+), 291 deletions(-) create mode 100644 drool/imgs/icons/glob_icon.gif create mode 100644 drool/imgs/icons/mulong_icon.gif rename src/mons/mulong/{KingsRespite.sol => GrandRespite.sol} (98%) diff --git a/CLAUDE.md b/CLAUDE.md index a43b3b67..bdb37105 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -421,7 +421,7 @@ How it works: - AGPL-3.0 license header on all files - Pragma: `^0.8.0` - Imports: Use named imports (`import {Foo} from "path"`) - `sort_imports = true` in formatter -- Optimizer: max runs (4294967295) with via-IR enabled +- Optimizer: 10000 runs with via-IR enabled (bounded to cap deployed bytecode size; runs above ~1e6 saturate) - Constants: `SCREAMING_SNAKE_CASE` (though lint excludes this check) - Move indices: 0-3 for regular moves (stored +1 to avoid zero ambiguity), 125 = switch, 126 = no-op - State sentinel: `CLEARED_MON_STATE_SENTINEL = type(int32).max - 1` @@ -496,4 +496,4 @@ CSV-to-code mapping notes: - Effect step bitmaps avoid calling effects at steps they don't use - `MappingAllocator` for efficient storage slot management - Transient storage for per-call state to avoid unnecessary SLOADs/SSTOREs -- Optimizer runs set to max (4294967295) with via-IR for aggressive optimization +- Optimizer runs set to 10000 with via-IR; deploy gas, not EIP-170, is the binding size constraint diff --git a/deployments.json b/deployments.json index 307f8b36..d796fb79 100644 --- a/deployments.json +++ b/deployments.json @@ -99,101 +99,112 @@ "ZAP_STATUS": "0xaafbb5b230d6e5f16d4c84183d09975f02c7ba9f" }, "TESTNET": { - "ACTUS_REUS": "0x0100000000000000000000003602ebd12f524603173bd1dceb7b0b1ccbe69d41", - "ADAPTOR": "0x010000000000000000000000a304a56d906d7995b611590aa60261507b79659d", - "ANGERY": "0x01000000000000000000000079787dbd9e974f5dfc95407a856602f50ea9447b", - "BASELIGHT": "0x52f204cceb27d2976abcada3a83863d1c40622e8", + "ACTUS_REUS": "0x0100000000000000000000009f3b7b3cf7f2eec198902e0568a78f6172b3580d", + "ADAPTOR": "0x0100000000000000000000000c303798a3b08d0f0745aad740a4a64a7cc51dcd", + "ANGERY": "0x0100000000000000000000003508eebfe3a0f3996ef049154327de5efa00e2d5", + "BASELIGHT": "0xd707e3404453bcf3c587d7f8b8d8273a5f7307f9", "BATTLE_HISTORY": "0xa4f90f3991f2f53ba77a417b9fc094a545fba8d3", - "BIG_BELLOW": "0xfa647f6b0741a2ff70825eb5149a81e00768d9db", + "BIG_BELLOW": "0x996cd1abdaba80763a543f8ab3e883ec60a33d1d", "BIG_BITE": "0x5009200000000000000000000000000000000000000000000000000000000000", - "BLESSED_STATUS": "0xe87ba41355b7e7e6884f9e72bcd5a67675175ddd", + "BLESSED_STATUS": "0x0b8bc685a915dc6eb21447b400f41b5677b6fca8", "BLOW": "0x460a200000000000000000000000000000000000000000000000000000000000", - "BRIGHTBACK": "0xa76e273e65e04beaca5c2cf20b7fb102fec0c310", - "BUBBLE_BOP": "0x08965095c4d0b3420f8166fba88caae9d7e49761", - "BULL_RUSH": "0x8b8c122ab3364ccd5471ebea5d94ec1ab4f609b4", - "BURN_STATUS": "0x58321098ce8d5b8ccab6ad123ac601cb5abb6385", - "CARROT_HARVEST": "0x0100000000000000000000008c631b4dd4ca711d59feaf26494a199d6ba3a82f", - "CHAIN_EXPANSION": "0x47abfc20669a5f7d766ee804f12c00715bd7f705", - "CHILL_OUT": "0x00c60640000000000000000002cad8f1e14f3b51557b96fab6c95166d6539cb3", - "CHRONOFFENSE": "0xc37db0bbca280f1b85e6b5c8a5a413bd8c5ba50e", - "COMMIT_MANAGER": "0xdd0fcf39c650ac05a755bb558c8191bfede3cadb", - "CONTAGIOUS_SLUMBER": "0x396b50afca9619b271a2be8447e0ae4f849252dd", - "CPU": "0x3d3cbc6bdf8ba435930002679451c422b2673e9f", - "DEADLIFT": "0xfcf4e328efeac7984945d123b0241a04b0995192", - "DEEP_FREEZE": "0x310d0dd5003c9aae5356ba7b8fa388af76338291", + "BRIGHTBACK": "0xdbcdcaf7a7a27178ba150c6566f831db80c3a1cb", + "BUBBLE_BOP": "0x3eb075b2c19224f76135c055df0c7bbe4a137364", + "BULL_RUSH": "0xd739f019683eb9bf21399fff056f10a42ac49994", + "BURN_STATUS": "0x79bcb8a8028dff6716d51bbc7e64f1896d153d36", + "CARROT_HARVEST": "0x01000000000000000000000096c6bf4778b6c41c78e2d1241daf0317ffe4ab61", + "CHAIN_EXPANSION": "0x170dd7c8f6e92823d0618fcb8b7f81b9a032eaad", + "CHILL_OUT": "0x00c606400000000000000000a57bdd735d684296110115843631385fb70fafe1", + "CHOSEN_ONE": "0x4d947f8c0dee3fead9823c8cfdb199e5d08e80d5", + "CHRONOFFENSE": "0x6b7c9fee59bfa15761804e6c87104e0bb238c48b", + "CLOUD_STRIKE": "0x7850e57c2a48b436b6b6bd10363280b8bd5f2d10", + "COMMIT_MANAGER": "0xcd86b934733d04ce591d2d85bd883c59a4242da7", + "CONTAGIOUS_SLUMBER": "0x2141b85fdc7da9e8b4e15947e97085c357c7ec20", + "CPU": "0x52d21794ce64d27a2755d8fc3ae32648d20ed353", + "DEADLIFT": "0xdfbe23c718f8196f3842873d5f6705e148679e39", + "DEEP_FREEZE": "0x47ea50eef09cf9bae1f7010ea310103697b12f71", "DEFAULT_MATCHMAKER": "0xd89a64a438b913d4cc47a61576d27877bad7f6d6", "DEFAULT_MON_REGISTRY": "0xef5fa1a5e99184df3a379992f6c7e9eee0ef7a74", "DEFAULT_RANDOMNESS_ORACLE": "0x1c9d9769d40eba316009660f2d74b8427843cdf9", "DEFAULT_RULESET": "0xa5e678884033b94509287500a0b8e6da04a8102f", "DEFAULT_VALIDATOR": "0xaa122ac35a19eeded0cfea8e2ddeab674e0f2fe3", - "DREAMCATCHER": "0x010000000000000000000000f82e6dd22620dac253cdc3bbbfb2be7059c23a06", - "DUAL_SHOCK": "0x1d1665f2fc88f5990bfe29880afbc6637a55791f", - "ELECTROCUTE": "0x5a4820a000000000000000006918ee05b14836938811f1fb4a3415506f8db303", - "ENGINE": "0xe86fe8d45259391dda86030804c97580608f546c", - "ETERNAL_GRUDGE": "0x01794f7b27feeb3b49a2c8e5f5a064da46469f3c", + "DREAMCATCHER": "0x010000000000000000000000be73911dc2908517d8eb5bc148c9b51136b5ce9d", + "DUAL_SHOCK": "0x57c36957dab3725063f1385df0725933c869c537", + "ELECTROCUTE": "0x5a4820a00000000000000000afb8129a264878b65d0d4e842771d0a57bca9cbf", + "ENGINE": "0x658182ab52de83adb4b16ab75886bc7822432503", + "ETERNAL_GRUDGE": "0xfe61e4984bbbc28f35b42090fa67aeb7ec1bb625", "FAIR_CPU": "0xc02f904dd32d244d8e60e2a504429f3f4b091de4", "FEDERAL_INVESTIGATION": "0x644c300000000000000000000000000000000000000000000000000000000000", - "FOUL_LANGUAGE": "0x0157b0ea79e2701b88b3db2345c54d054729128f", - "FROSTBITE_STATUS": "0x02cad8f1e14f3b51557b96fab6c95166d6539cb3", - "GACHACHACHA": "0x9ce4b1259a4fe74ceace8327ad3a44c8b0b9062f", - "GACHA_TEAM_REGISTRY": "0x7325848d05114861720b46065ae00607212a96dd", - "GILDED_RECOVERY": "0x21d234c2c4d600e60a23d3fb33fae569c9b99454", - "GRAVE_AFFLICTION": "0x30ed299574f60f142407a128b2784f2fd8c848ed", - "GUEST_FEATURE": "0x67760ca453f9ddb8c551f72b4cde01e18b2b2937", - "HARD_RESET": "0x0d25d29ae014b059900fd07c4257be0f70c1d339", - "HEAT_BEACON": "0x5beea17fcd5864e4da16e18fb7d23430520ff830", - "HIT_AND_DIP": "0x9f28661a4d2b10148dccba2df9c8ebfb7d679728", - "HONEY_BRIBE": "0x9d69d641af63f28585ea69fac472661545719749", - "INFERNAL_FLAME": "0xa0198c67b857d25d32703a3447fc2a5bc0b02c88", - "INFINITE_LOVE": "0x5a4d30a00000000000000000f875974407c361472cf6faa65a41109c9b1a7932", - "INITIALIZE": "0xf576c4364712cefc3748b3b2ea8c320ebe300465", - "INTERWEAVING": "0xa65a7a4ad29e28470e69ef63f143ed67b9a03a23", - "INVOKE_TABOO": "0x4688ccbdd762bfe96a7ad861c14149709e3550c7", - "IRON_WALL": "0x2bcbf07cd4b47026547bf0e6ffa5b89098e984c9", - "LOOP": "0x17f9c6a8e97279bd821c5501e62c21b876f60ddb", - "MEGA_STAR_BLAST": "0x3b54b488c4419de7fd365d1468bcc2a9cba5868e", - "MODAL_BOLT": "0xb9b249d98cf82a39111e2ceac16ac1f65cacd242", - "NEGATIVE_THOUGHTS": "0x504b30a00000000000000000117dc546461cb7cd6e675c8798c88d4b92680136", + "FOUL_LANGUAGE": "0xd1aa3fd61ac93931b291fe9bf535f1b5b4e0fbd1", + "FROSTBITE_STATUS": "0xa57bdd735d684296110115843631385fb70fafe1", + "GACHACHACHA": "0x82f9eda4c713d007d879cf4c9db92bb70dcfd0d9", + "GACHA_TEAM_REGISTRY": "0xd3357dbb90451d94cf689ec88b69b64188fafb3c", + "GET_SLIPPERY": "0xfcb758d590978ce1a240149a07bcc27728bfb84b", + "GILDED_RECOVERY": "0x39591a30ea60593a4a47eb31a9f249ba10d7e955", + "GRAVE_AFFLICTION": "0x62db7597af221b607dd0ea582967a77b1a220609", + "GUEST_FEATURE": "0x5b08b1b2e2b55bec525626d5db7455b51a6cb3ef", + "HARD_RESET": "0x6549498c1e9cf90aedfb014c5aba1361b61648a3", + "HEAT_BEACON": "0x93d5c076b77a63487e903e234f434924e65dc047", + "HIT_AND_DIP": "0x1286ae46c229cf9f720304c7a1531b7cf7908cf2", + "HOLY_MOLT": "0x8e4c9053d7bdcdb6ccf3b34476fd888362c6a79e", + "HOLY_WATER_CLAW": "0x5a09300000000000000000000000000000000000000000000000000000000000", + "HONEY_BRIBE": "0x13ad61482a3c6d0ec1d2e44858d7fa6b090c73fe", + "INFERNAL_FLAME": "0xf1cf792ec713a40d8dc49c1437198a3c4cf6b955", + "INFINITE_LOVE": "0x5a4d30a00000000000000000c8a998cf6c0ccb4f726696c95ab1862eab251f7a", + "INITIALIZE": "0x39f703dac1c2d2900c5df9c08f7f34f95725d233", + "INTERWEAVING": "0xdcf13b32db15011c5a3d7f38e3daff492225763e", + "INVOKE_TABOO": "0x24b64e5043768bf63e5f20b0d6976ab53d86fb97", + "IRON_WALL": "0xd6a57132eaa52656f4a69d9087c0014a0a64e8b7", + "KINGS_LEISURE": "0xc351a294db9b4248cdcd2f1443f75eb746987c21", + "KINGS_RESPITE": "0xe56793639eb661cf2df0334462e12c0f7c0d321d", + "LOOP": "0xd5e30372ef1710577d33c4402ce910b239c7f83c", + "MEGA_STAR_BLAST": "0x272bdd9f7a0531c462293a452833ec8cc477bb4f", + "MODAL_BOLT": "0x4cf59390f6013e2e880e32a2b4defaade82d57f0", + "NEGATIVE_THOUGHTS": "0x504b30a000000000000000002cad69c1b73c736fdf5642d66020d3110bc5f54b", "NIGHT_TERRORS": "0xd9cc94ce09177091609222b02854f671deaa2579", - "NINE_NINE_NINE": "0x3650a27e5c13516c55e5956a577457813ae9acce", + "NINE_NINE_NINE": "0x04001f82534b5462e96b3636ecc6562607fd4b7b", "OKAY_CPU": "0xc14f1a8cdbf755055fee3576a6d06791f72232bf", - "OLD_VENGEANCE": "0xacaf7b9794e84ce9d53390e0a0c676ad3d49bf7b", + "OLD_VENGEANCE": "0x2b6b3e6618c8edb67850df61f066c2ab9b67a566", "OSTEOPOROSIS": "0x5a00200000000000000000000000000000000000000000000000000000000000", - "OVERCLOCK": "0x223ac7b0f7f8309e5a2496ff1835bb6d56c1b99a", - "OVERFLOW": "0x34e0126914e162fa929964030a5974a2d4752615", - "PANIC_STATUS": "0x117dc546461cb7cd6e675c8798c88d4b92680136", - "PISTOL_SQUAT": "0x49673a412eb0dbfd14497ff7e9f44e6422e00d88", - "POST_WORKOUT": "0x010000000000000000000000144c95356c72a3e4f4097406570d057d60643bdc", + "OVERCLOCK": "0xc0ea2de762fa2a07f8c89736d33a8c5fd8972dd3", + "OVERFLOW": "0x2e38634bc198561ccc95fa892ee2fd9936d1349d", + "PANIC_STATUS": "0x2cad69c1b73c736fdf5642d66020d3110bc5f54b", + "PISTOL_SQUAT": "0x7ccd5b86109946b3488b20d06777d191e3abe7af", + "POST_WORKOUT": "0x01000000000000000000000058a3415ec3cf85211be9fde7ba82aeed9b8de721", "POUND_GROUND": "0x5f02300000000000000000000000000000000000000000000000000000000000", - "PREEMPTIVE_SHOCK": "0xb5e2c7ff01142241f29c99a09f69236ee275df34", - "Q5": "0xea6ff6a8192ae96e45a684638ce4f06c26ede59f", - "QUICKSTORM": "0x249b5e1f7b6b0f56cbebeb2b8a514da53a011da5", - "RENORMALIZE": "0x39e25c283eebbe6ee38cd86bd9b66a1c0f8cc97e", - "RISE_FROM_THE_GRAVE": "0x5f3a9e5d89405f47395fde8f5595b938026a2126", - "ROCK_PULL": "0x9a0f40e664a5be89ac2b4dece6e073ad0eb97df3", - "ROUND_TRIP": "0x171eeb7dfe178e2fe0a1dae50560d6edd6822b0d", - "SANCTIFY": "0xe4770f5dc0c0cc9dc0ecaedc083d8c9d4d1a3fe5", - "SAVIOR_COMPLEX": "0xc70580dd51aebcfd849a0822a4ee47db280d6156", - "SCARY_NUMBERS": "0x500b31400000000000000000117dc546461cb7cd6e675c8798c88d4b92680136", - "SET_ABLAZE": "0x017da0197a98f518813527b7806b331945c12a41", - "SIGNED_MATCHMAKER": "0x02113d58473af8ea6a3ee8ac1c7226940fc9763a", - "SIMPLE_PM": "0x00cbb85ce12c955f197212d4c109b6a653629515", - "SLEEP_STATUS": "0xf875974407c361472cf6faa65a41109c9b1a7932", - "SNACK_BREAK": "0xea7402b23589ea218761ffe9b384781e68fc7249", - "SNEAK_ATTACK": "0x185af09e1d4c800d188b7697d8db1cd476c444bd", - "SOMNIPHOBIA": "0x93648083a1b991716ada1d3ea690baf79b0eff0b", + "PREEMPTIVE_SHOCK": "0x1e17a129f325553ccc884779e3ccbae26283dbcf", + "Q5": "0x82572ab5783b5a117b87ecbfe8978c3d6d0cba09", + "QUICKSTORM": "0xfb4d7a254967406ed1c714bef0c06a734b27d4ed", + "RENORMALIZE": "0x2b7d4e68f71523036e40cb1f8e39d8a73278d846", + "RISE_FROM_THE_GRAVE": "0x4d7255f78ba6b9bae695e979173d840c561e94f6", + "ROCK_PULL": "0x739762872302827919e0c9575eb01318dd30a47a", + "ROUND_TRIP": "0xd4893d6d59d8c14ea4b8b4a07a618a96aeb73d73", + "SANCTIFY": "0x5e5a2e7791267cfb34f62f5f4f9a2a479efde004", + "SAVIOR_COMPLEX": "0x90ff72bd13256d57f04c51e83e48b5c23bae2387", + "SCARY_NUMBERS": "0x500b314000000000000000002cad69c1b73c736fdf5642d66020d3110bc5f54b", + "SET_ABLAZE": "0x3675d2ccee3e3796974ac01745fe4e661bf53d93", + "SIGNED_MATCHMAKER": "0xf20ab3832c67bd52cd3e05dc9af01479afd2a8a0", + "SIMPLE_PM": "0xd0b08465b4a259e528de2eac38cab2a2a94dca3c", + "SLEEP_STATUS": "0xc8a998cf6c0ccb4f726696c95ab1862eab251f7a", + "SNACK_BREAK": "0x69ba80b9520d023c46ff1940a7e01c062b55cc59", + "SNEAK_ATTACK": "0xdc79d473a52c9ce0af2680286c981ba8ca0ae343", + "SOMNIPHOBIA": "0x2f703f4d0e173ed3b0fa738fb3e360ec1f0ae15f", "STAMINA_REGEN": "0xe9c82033011dedf20d2fe69920b5020403041d0f", "STAT_BOOSTS": "0x8c072f12345b3e95fa52fcdf74610c044c044940", + "STORM": "0x9a6e108254e205c09030a6dd10408065f3983328", + "SUMMON_STORM": "0x083dea143a38a20b5067cdf49c5d1924f172fa56", "THROW_PEBBLE": "0x2802100000000000000000000000000000000000000000000000000000000000", - "TINDERCLAWS": "0x010000000000000000000000a87843d449c2249efc18c69ee8b5e10c8ebbec7e", - "TRIPLE_THINK": "0x531768ec889323ce705432ce7c9200440a32bb7c", - "TYPE_CALCULATOR": "0x353b2b80a4921ae2f997a39054eb204492b02a7e", - "UNBOUNDED_STRIKE": "0x69ebae1865cd9698b0ce39707842d1080b23b191", + "THUNDERCLAP": "0x784831400000000000000000afb8129a264878b65d0d4e842771d0a57bca9cbf", + "TINDERCLAWS": "0x01000000000000000000000041713d25dffaf7d512c3ce7e1e02c4e3514144b0", + "TRIPLE_THINK": "0x04650339cfca6c1f2a170b94eed45abc92ee9084", + "TYPE_CALCULATOR": "0x065da72dd0d9855ba991c20b03c7514cfb7e3abc", + "UNBOUNDED_STRIKE": "0x00faeafa8dc75baaa6d60a0aa27005f33f23fa19", "UNEXPECTED_CARROT": "0x7807400000000000000000000000000000000000000000000000000000000000", - "UP_ONLY": "0x010000000000000000000000b3e89653a438dca5261747851095f3144af50de2", - "VITAL_SIPHON": "0x39df0d2fdad2d4de3af865cf54314734b17e7cf6", - "VOLATILE_PUNCH": "0x85b99ed2e044109815fb237d41fa968fb3c27811", - "WITHER_AWAY": "0x5581c5b6857b32befe865e62b52e031f957763fa", - "ZAP_STATUS": "0x6918ee05b14836938811f1fb4a3415506f8db303" + "UP_ONLY": "0x0100000000000000000000009c42f663defcbdb0046f703a271636afaab77ae6", + "VITAL_SIPHON": "0xc573b67221c957ec2d4a3bfb4571c51f96e72605", + "VOLATILE_PUNCH": "0x564dfb46ed2924bc5c6fc18e20cfaeeeb72633cf", + "WATER_CLAW": "0x5003200000000000000000000000000000000000000000000000000000000000", + "WITHER_AWAY": "0x379ef31cb4dd355cefc3c596f085e9d81e7e7834", + "ZAP_STATUS": "0xafb8129a264878b65d0d4e842771d0a57bca9cbf" } } diff --git a/drool/imgs/icons/glob_icon.gif b/drool/imgs/icons/glob_icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..42046fa918d97cf3b2b676aff8df4658d540fe40 GIT binary patch literal 1871 zcmZ?wbhEHb6krfwXlDR{8f60@xxw66k5|Y}By#$apRdD%K0Wz=XXk&g{3tOR0>d^0 z6#sMkxrPKgI|jHK=@~FH0zInulZ8_RNb7(+4az1A9L)@j95Nmo794D5Wa6-}Xi#8e zVrJ73iCECk$i&3SB2nP*=l~;!kVt^UgNBnW%(_xLJQgfI*(b~>;j!c6(_@T`905K7 zpB+vz^2=+nBy418oXE+^!C?@z{0tM5%QBfvg=emdM8ef3%$lTdqMK77!G?oj1p{-; zNv;hurs>>TowEEGF9))_7#aA15R`pKGcPEXM!|>-0ggs+z6?lUU}6#3p4h!J(mnf!#??W(I>}6O#gai;aPTazh7Wz`8va4Gc~UY+NhmR9;x- z+sP*zXCvXjz_?giS&ZSq#S8{!^J1YH8U{+o8JVXGnKU#gGp*$`7t)x}u;gDa5^4;vU% z*emv&+_2D}KLp=+L;7)X>PPyiO$Z z!4d{WCjCiwG$%2*v@HNrh0bOW*R+p-JYzzsa0aZ1Gh$C?z9E~0|kZM literal 0 HcmV?d00001 diff --git a/drool/imgs/mon_micro.png b/drool/imgs/mon_micro.png index 732fdb79bbf4bdc7100d0091d99ce6da632cb89f..ffef704403aaee7299c3648c2c0bdab78e3e012f 100644 GIT binary patch delta 3000 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 literal 5203 zcmX9?dpOgN_kZtgo7s%H%-l_Gxs&<0e3Z?d!l;m3R!T}PNri^@W|NOf3ZE!tg(3-E zL}@09id-U!GM7+%GPlfiU%%(~$GJVvIj{4a^PJ~7&pH24yodq^57vR8`gz@Jz9ijfFDrEg#r0}^567L6 z$HP_zUHf^3qUBD?$&RGME=gyOKe%7y!G1k;-?Al1VXS)E?Od5?7^ujk#T9?nRV7rT z=P6r=V7#x_UH~|o(_sw&vL)_Llzj;y?fL!D<7Sf1Im0EHkp(v%uZeiqNcH;?X*e2Y zr-ky5Un8moaoG*)(@7iQ#yd7m!4ExP?Wfw!4D#q|Ex@ZtS+(L20AOwYGFo$;Z=gzV z0?}_#0jJ=LG7f!_@auy;7;*}5#W2frh3Qcx6^bX3W%sMh=I z;cuaGW$@!3lO?ik(hu;TlU)Pw0z_rw-WR)HA%4n+b^)4i6IEKHvScKwaZVCpzB!W?RD0y=2=i24go;}~+kYUg?o z6$6dt@M7zOJkmn42~R<%`*ida#xLX3!kCS7(=>Lpz#lWCl;i)ym2s#IJ%E`=V)T@x z3G+25h%WrY+61~+XU$i>?L?45FL){469jC;h_~mLn6(@d3ZANZ?yz1DkItoG6~jop zM`0ao=BsvnqKHj<-Ux8t@?>8E_e8X`Q~jmVBIX^CYBRh#^v zO7OhM+=f8fVEg4;7UP(!2Aras zlrFuJ`Ik&xh??O1U|~8y0%*Ol0Jmj4yv&3;q?T>iM?X?EpRRK`zs;CFBYLL;F*b?r zC?zjnWkcSsWA>s(DK6)1TOJDb6B0i1*IAtZRU$pDv+obThnwrbT4iee2!Q&unaKojDNHddG~u zAblj3_m&-dD9j8_h}Lx;kZhOV`)#%C8KrVl)X#NQK9;;K@y*H1;DK{H-m|R;xAi6m z(rsSlr>gNAwaJUy+Hv#O$W@|pK;{eUwKV*~V)e=@T+rw^v~;H=jFaGSn)6Lx|LzUR@l{C z@)|6qBD;hVf&<-1%o*07^~=$^pCWda>UAv+m@Ur8N@NH7Hxxe0-PVqgf(#qETn|Qm zK{l=n38qWmny|W|$5Pnl4LXhcX&iGmN%Epfapbkyyxh=xZs?gE!X+PuLl+=(`fpeH zslqsy`o6M0{m4>ENbFd#jw-R;d%J?bkVFY9^J z_IE<(x8>iEP=LZ~z zDce8N8aQ1-4l!eaw)5x9S+Kd!k}?SNxpaHTB$KojHiUGx-d zJ%kDDgug#g{(e%TAb_UcdRJqqED}|6w|l}F*8lXWbE#H+bJMr*CEf! zfp!HWZonm1@ZjZLDYD`Yf}GQ@i3;&|bwvm%q&5d#R9y66q z_7g0c3638WhcOf-0`=WcW4Ei0jRtidQ$St+&ug(tf&!TKx}b+NW9{W3TBslsG&GO zjwr%a`tRlX)ewPBw@J$~2Ze@+{$fq|?8Hk2xro>t7?4o&Yhs6qG0%!GC zQ8-)vkI0^PtBf_}|DH*8^Hv+|G-M#)4iWBqj z*tE&(V0Cj|^!xW5WTU)U`264d2yMv^hf5frbId8!KgY=knft!rrnX@cY`_O~{jOeZ z8X2{)oAGXn>jIrKomibWkxbeo<8qUgv*iH<=o2}pAbw$A?>QBsG#G|r&r}(8pE+m> zM{|)AD{{my+X$(tFk(A;v4AhY_0lIL8QJJVW$F$ zwGcHV{7yZxMxqTG^r2z*{ZcY3%jOi8K_FYg`#`BY*v#5)MK|>c$!POsiU$*bR z5Bn$ZSuMz8r#ut74rxh~L$a;o9b9`UoZS*M%V?_*0dvkIMkV!;JqUlmgOt774kVN$ zv22m@;12adTc26fRjXZ!zbQrkeS+?{M6u+O2&*msy73mpkGyiFJDNap!um~ss@-Ef zRZCIbocvE7G}XlleKR(_Q$tdf_}<|&!Os&_Gr;Q0hl-ham@JN*R*i0UMpK+aG@*;c z4wHXpgkXwj?PL3nds8(jUkBusDCud%qjdRF_?L|-fcN{^N)(jJ-%k|FH2{`WW+Au- zkF)7`L{q30TQ9B}vFW-oKR2(|mPqv(X||Vd@`c^xO>bs&2a7(9#M@p1yAYt5QP&r7 z4;~*cWlPU`&TKVw0RzHn*wuDmEQ=$~GW~rICYS+r$cHGm?Qqb8;K)>oar#I@<#I|w zs|-^e$K`H1i+FVB?F;8puO4xsiGzT*OiM{%3Oooky4WTA6~J1LmOm$hi5GQVl)&~#z%p>1#|fx( z7@@d>4G5O?P(^?r`H%jJ)0hO*Im={5%Akn$ude{zHV)SPz$$q=;MfdxBFQ!ITHs0= z+TINMd`^E>Q)1|enc6JO)@jRWBxbrFoP3tgM08}R@@E$ME@g`CtI%K~@)Vd>-g;hb$-S4va2n zz{W1ESYMVt4LAvam_%R-I7n_kDi7Od9Dqo8y0R%{<{d-&gaj&12gcd(rd1)55SzqP z;PhfSC^HTXyS=8ZAB&+SN#;)$^dw?{uYr}%`m|9BFL(;L-EZ8OZp=Hdmi*b%Furm8 z+q~~ll0O#K6;XDddyZ}V7!>y-KLmPFP#c1_EYG*=l+0dQyl`(dKvuC=+z&Ww;CSX%d-T{i3#He)^K#BXwtm=JugL%Y%H^KjO- z=wMlPBY!uh2(8UzNtY}TRuEq?h`wWckp0WeZ2T)Q8C z@K1h2C@l7``C9uehu7PR{)*#`{0S6m0D__xSY!Ju+bO0oDp|#HP;|&LN z#j7>>@pZb5Uy;DKor=+d`b!M_Q}8aJ3KWHehT3Oz$(=;JSakh$i9B`;d-LaLzU!*` z4bAh+8WNHkp7O%gzZ#FyM`&@(l>(a6HGH@#VxhQRfH8;i5@NRQgMkk4+i8a2AWU&h z-F0TId!}9sRchk?W(^k<8p|H|hkOjtdqvpSh2rlw16o~pgncdBrkg+~@l@B?4#fbX zV#-nued0Oj-|*V3?UGA`kWR@cd&Al@iKzu8`^zEA0esM$XmwTjPzh!fDM)&yOiA?o zl`~v+)HmZOGI$|%mken=oUPh~-af`Q%yRn5G} zOMJWLPKTxb%5>wS{mABuuH3F1Jg3m&DRcg-QEa1JP$8^%Gx`x1l{f0?bo(WSHK+_FytDRLHSj$_7M2h zU?EoA2|r`LY=CMY$Ox7kqyM-9-f+E^1)Ea&d*}4W3>yvWz!uHopbO|So2~It5OiRCH$Pyy%~a|uLEy$h(RU2`(AN)>sww3m3=)pXsvF%PRLK6C--&zbLH&Xx7l`KYFyp7$GJSmI}cmM=vddpd&b>8nShzGcp@g_Q6*{BfUFHfWE8$UG-6zRkSx3v z63c}@WJDoxF1jW~*avX;+pzOQ(iBhf%pDZ!jb!X$JGyi`fxFYqp0mCemxglVieEFO zCztX6a`~D^4978=n*QajyJJ%EJ$^-hANew#A|EQYfIcH(W+7j4kIIx5S(#x1;oCXy z%YS$q!8N!@Fd206&2jZM*MjSFk-iPP8NH6Va{J%VRnx{J`OS^oe}2W;^P=#+u~L}_ z5wZ=tdBoguz(E$@zH3G`At4Pi*ZXyNh5kDOr7>X+HZD||BC2+?sQZ?l)M?TU$W_y9 ztL}yU!^!SJJYIg1I&_l>9fz z?c#cbEM2kJu^&|gS^IHC+MVSnpRRCXw7TXD^OU;Sq-lelkSGBS?j)(wbu{0!QJWK0U=(A$@%Adq!a z=(w2FvMl3F47cUzrbF4gV0$8Bx3gM!E$2MBk1u`xglVh1CyNiPGY{Frs49Kp7qwEq zI&4PF$fuVOsQ^Q9(m0IrNnmA$ef?U**myrd=wlvDQ|Uk;f)hfy%{b-+Ur>r%rtrVS-x4}K-<&i`9zJMt_B*j@9fnzd7b@Qtl%`-4MbSEc z3i82KR4?~omy`yxlg~S@vrIdAv|O!lhn)^vtr&v70!DB#B*YKNCfu^4nQ~WfIn{y& z0)fA6LynyjEdprct30c1MyzYLp2e#)<+rz?fE?#ujd5U8%3*)gX1-*=B&878u8mvec_kles(J~UOvT4_1=+~= zBfj%kEf~634C}9oGjVtzW5_l&2wi0>q@Q@zze8uU2-y64XgNP}$QD%Ct!sYvto0WC zMUs}PV4wtw6%4`<0OlCf?Sc9Hk6j=1nTpbli)jE9m5)%!o5Uxqd3@JDW%Ex0>9Nvv?>2|-O z!t>LXU600Z4tA(r(9c#GQ<+Uu>SW40>+|{dm)SEUf;07{qoD?WW6_jK@iG{P-<697 z^T(c_nchcjJ+YF0#ir+C^SPpf!Gi!ikjXHcb%!==Dn$!6fKlyXb_biG|Cb_=BIQJ@9 z&R^;~*As>>oRm!CK+BFTw5L;KLdEM)TAzueapz3vd9N;}mMvKWmc&ZX>sn_4a&aVf z9y<$$nu}rLs-I1q1&|T7$|K08{>;Wq=mC-wKOj(2M(j3^-sWv^^N&t0L!@vN2y{ef ziatBU?C;C5;8C;^{pn!yEM7SQ5@nGEtStIyirHo#L4nu!BmA>p|?_> z7i4}4%+|4#-BKV|JGZB3eeI058gMSnce`AOuumc$Gr{?nph*W;{JWy#F>%7!lRy0f D(*L9R delta 1061 zcmYjQeK6Yx7*4s-*_GXDP1gn|>~6PpTc}KxEYroMZiF(^5Qg)t=!uRq@Bd7pbN<7b<^^=W^t#rg}0$pGG@Wt?o(i&)i)BY3)q!&(#13D@-t({Sk zu)mV{k}j-*9Y?Fj@OHE3B?LuA9L>!%kDuzl$Ih4Jb{ZyN9+5Jk{wo|Q&e-4U0T|l; ztcPP zjSo~1ti6mf)*@h`oczP|d~MDgFvKV`ZXj6mH5y3<(iSi@C=*rJ#`juX)y6_gyVw@A zUT}9v7q)giw#fJcc%bUP)?Br{yv0IK-WC(4im$bBH$QvQ+9@x~3#JL6NyDo56P~8} zLyv0`9%6Ui>N<~ZK``GaXBh~tSwu1&aP(-Se9(?fD&pk$wgnjH0;cm2(5%dbp~kc% z6`;|nG|t8e#W)#|V^&MSPj1MCqZq0@ z@CMzGJWR-cPfE_Xs?3;b2zq<$cg5;U4Xa*V1 zWhM)md^}bz84HP6xIs*y#n#VTQJ15a!h=D9s~?J}q%DUIOOb5Voo};xc;k7c zFy4j7)K6G)?oFyZb{U7`O!N>@XSsTN&kJO(x=^05w3xV3kT%oDGK8c{zGw+l?HJW& zC0!@Un47<}ltP_uIPK5iZLCeP2c`6anf;dmQR|@l4%*os#RUh%AaNO_&4Q&{q(hQ> Y4ywWHpMvRLIR|BU7!K2YA?5pj0CZ>~Qvd(} diff --git a/drool/imgs/mon_spritesheet.png b/drool/imgs/mon_spritesheet.png index 150cc98f509679696aa3191ff9cbef5ccffd2102..1a34136c0f58f8819f4df9b3c1dfd4fbba23be6e 100644 GIT binary patch literal 83651 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
A>2M~V*niO1dKTmRl;6eNy&fBjYl zcvkK6GsaoX+bo3=ERpvz7IT1o`*@$-{Sg%d9bo6;$Ec(o9i1puRAc~aNMntOuThwh zTdt#V%OC3j@z=w!7otj3mN+nJA_q8G2Oi#j>Smy*71j6?dnYbJTIDU=`Z1GOX+8JV zmd=2uQSezcH!PeYQ0@^cduqm0unSQjgNm`@yD0qo3Njn!siymZ7PMJGAoB#QCZi)p zc>+uGR8N^mSeCu+eh2Zfihai%LWz+@`6FLP!B^C@qGV7x>ujTY)&WUT5$uG~n3=!2 z3g`=;lu;}ZkyQ-~!yC;yB3GwjHU zc-A!e>60Cx@gp8B_UOQw^ND0m&sS%aJTTelV&M1-rzX+e_vBb`$fqq1=;qFSa zzj*vz@D1Gdf}g|)59kJahSegOGf5nGjB7AFHF6_n3ZT}emLg-rIA7C~8PUCV#avhw z4Ac)l^igu(SlDZ=2k&h^g)Fi8+c~Hb@3X3;Uw?WO7$?>yY1^JH{}=>o5n|DXNP|P2 zj++qU-vRBdu0hWs36>uuY-ImoM8jRQ9u_u{!Fl2Uz5X#OSd+#Z?jL@nkJZcpq+li- zhbL6rZ~Zf7S<9%UvYK&zS^U1x#+Sw|Ja*`Y71L7)tt4~ra<#0+!kB3uuwMvYmDdfo zAhvTF(`AxR9Sp7xf*PHU(hR7whf-nj0u=Qul^`&q=3UX@hdN$}yG$xy*5Zk|f0{o; z-FHzxk3lydxE{6IjyQXsQ9Sxl2Lp&FRsvsvF3FZ9-{e6)SKFB0V}qeH&_(F$VeZ}k ziL^OVT;(5IeD>$rf%G=tj}LXz#_ToQ(i( zo7g>CC4{n=ydPuiLP!#*fj+;Qqr>to#Svg;mGf@d@_5#(C=!lXw<_3u5v?k|NfEah z>EsvP!AE@fq;WI@(|WS=uK^8r6O)~cL@i- z*CUgQ3vV<|QG}1bVK$<5Y3wW<`}W~~c3P=-FhZhg5~G$7fx&;D4?|s?O;!2mQJ$VA zb`kw|oPJ7;8<12{{|TKw{sZ$@-Yi-T9-fFNW_8%uiyP6Ew7MMT-D--7&r1T{ z``*sUamqXqxoDLI^=q8{gv7l>E)| zh%AD$$E8Ck%baQq{5`e>9Sism5ACPtVzQ#{DJazYe zy$$-hb?(gWx0|z%dt1^yKwZ;+>g@tP(yqvBDTeY69bEACJgz5i-bx}&FChb&B>@m# z^92#9a^{GCT8Q19GPVM@T^G9ZRBIP!@1mQUw*_Y;Xv8D%&^k<>dFK}_bb+93L0}E( zKpSE8TCh(x=)fBQm>hibS=-FUfDpj(@9M()`onHy!N&UqpB>d#8u>3XwwlD>ZF&{d zyQD8S|BixMsO%CcK^Q!sg*5S(2?FPkgVzZk0)?r%=u@#-0RpzRG}K1ev_B5=)LF*r zMhLj!s2+{Y!`{I!{^UI8g=gkPW{$E8PwS%j=)sh&88W?~h5>Vw-bz0DwFB4qPuf^M zJ8Hb>=h5P2lnMA*@}<#F<3A5OlTBIpc!Q$$>?Xk7Htd~0&!9CVfNgZpVJmZz{m`4Y z;2^L0uZaRl_FwVSUTME%e7{nT=zr^_E|AxbeSB^4ieG@o>IA9#n_gP2J{++bFjv$& zD)WE%mBx|u`E`!3Qe?vwcI9}t)RzuvfN8Kh^}=y4!7iNZLVUX@Yd$qvYLD;e8s?q_ zZ8dCrM!#`lW;Q;D=X%7vnY!_D-+KJes3n&kL*_O2fZo{o&pDx>+k!tuf3pQTCd_OFoTvO=7dgUC-UhW^>{^XC z`f6maa??VU_(}v^d3bmNri7h5v?JVn3{T-b#hH94b({~@J^SGk`qp3*#BV$xwwbH1 z(uE?C8{=NE;PGw(+6!ZCTFPP3lER0Rx4 z(56GxAxu1Am+*jKN4bWKs?<@CNg7>OSYQixIRW07RJ~5!f}FoYJ$kmJi59gf@PGM$ zJp@?!FsbsGSjy*eOOdC1{|xPQTK3X^nfv}8czJ^?wo9jDCJMmV)JleJ(6$OLv8=#@>4z=ln&< zb#2>`kEj(3HPrgtS|}n08=W9^Id-94%~f7CcPVW%p9x_e^gmT{^YfEs3u;&COI} zZ`yA!5&NRR4))-+(m5M-J*QPwOErW;;X1FGoYWGQt1&N>V^?$kN`4Bv_>JNqP6kx! zUL<$Kuw})mwT77!9PwDTEJLNhvwXG`05<|j+csa=H^)_?Ld|brd{>N}ZAOelljC;LMgXgPTJVm-h)kA@W?hW9Ycs-(;7=&4>Cw zKAlyenzM?anSH~t@&(~kaW?>%#4dUhd#}35$iH-}wS*pA?7#36dSoFnvjA8U6UE?B$`3zt!kUi3}FJl?#Df>BUCa~@O?p5?r7H|2X z?D2Ar36aLx+ba|nbWqQFMVd3%i?PF~psO4$OJ8$-PI6{J)Vy%;K>QJ5soUjo;oWpG zE~RZ|HzRMlwyBlDUcB1UB*~1G(02kYf&@@%dvUt|tn2Av!}T%=;<{Xwi9z8&C*`ZrKSLU4NzKe!cGn6ZU3v^q4KJ zryG30VXsykP-KE?k-7eI7|@V2%ylNkfdjiv@&GLWSG33tIl{T))ivy*y+rKdp6+4l zX{>#7^p(l(ouxjeCm2`-`XBwDr5e)}^4Vz9XO(oiIvj+T;N;@f&J?Vo>I;lPJPuAvv_3&Yi%b zo~!y(j$CZ4Zh7y#{ahf2v!r-9npKnuVPv{u#Z0V;$UXq$RPe*c7yok4-Da$oIt zz$lq}7m(C32ILR_^m>Vk$Aoy+fa2kyb=UMcyAxz~@2iy;cX_^hO zH>u#IZAxR=09^FhGI!7bs4}dCHJ9b&l+|rc088~P>Q0DGyZn|g z)ncV~)Z?>3j?_QyE(-v9JAX-SWHTusqr3ePoEgVCa`1qc_$m6O z8=rSDn==P&^{R%- zuJjY~L0!Qoo&X0{_>sW!ma9`h$^|SlO2;B}_#TFX0P7(!>fA_8;S1QWiqx2Jhh?mh zPn{9={=$vYU!~Vn5msxo)0Hp}IMM3iwmNfG*8B-m=C&V&_*1-ayD={oA`XFXI~=w} z?tXdFEiQ~w&&I`w0JKSG!?iFw9EU(^ut_$FPkD?f0SzyZKmhgD50Ax`xH-Gtvb4`$ zY8^XSxfc*zCe3?K>RATW-i>5E+x((r@nu)WHO~f5N7vtaLK)Dyr&~ zr-{Vk?iW*3T6i3Nb{WfIvn;~=MFm61slP<^d^aJ^r0EP?0{Pb;KWdoMx3{r#WA(vd z%AO&AmwpY}M!!2L1P*_Yxw|->PkjZ@y><+!W?RKJl}tp05p$Ly;rryG2%&$gD1s%6si0WUu^za3BQg>{s>xq^xgFrL z_y8;l{9c6jUJ%f&2oNSh4Jjs5yWve-2yOC5kTdzCFNoPT+PNeA5VF)=X=ybi9GTyj z!t`K7a6lRe*4ADC2ks;6R_15D^9Ms2dCAD)a=Ez_AeD zD=2-pP_;sk)g4I_%DJe_f5uyjHkNAWM(tXcXv_3e?G2b<2l~>b-1Y8CX5uG;BVYBN zAa$Jo{m5-m)Z(rZ8)9yZJ^R3^zN)bW<^Tv40@&(vbJd|(vo5F_E2?{+L?z6`hwt(x zS2s(q`~12WvsctMpj@Z%W$IiUR?n!c>Ji>48FZV~3{R@x z+djqDHnJMkf|sCt!96fK`jv8CE%9pdN&QIIXX3|^AFAMPMT7UR&D95ib{7u2wuFjI zDDUh?bw6>(lQ~#oW7WX90bx`HA3&+fo5#u3qk!HV1?MxR$B*6Hdp0#c&PQI=MnM)E zSeCPnSOBYBxl7$Qt*)J`ecdg#Bt)1& zKCa9mdIQ*MY;n0)mlJz%z>|lzFTbRji*JA1vGJ(g9b^^;b5=O16X$li$)V~wQAxlM zK&~0CSOM5E*kIE4Imj3ND|@y zl-x?G%vPn6ETYLVyb+J-Ec~X#&48@Kj8PW*V78vypev^aq(FPvL9A-d>yKsrHrt_O zKWMJy&cgktfR`Glm7GxeIazyzqO89mwfAC0P|BkGHaex#z%9Zj7j_EY7l`wCFYCVR-!L<|STi>9O?7l}j~pkUygwX1_``Eac2dUZBuB zq`Fq4A3sxssBr}qmlb;9NT+Z`-h%4CIN=*}_4)$&bUKC?y9B0M8*t-=@o z+^l8E_AW239p^rn2UxjJKpr5l-E7_4&Giw+~h1?MF32Ev75JTd33@13K*ov?rihXq)LXMlJ^|F z>EUN5|Mr@?^msK5SNe8Mu!Nu)>(&HH_-nbO8~%=&>7f6l9&N@n+raov;s06bbV( zuq`9sGgMp=pOlH@PcpMZNo4_A-VcH-z(G}i$m*_h{Sp3Pa8E2Ky7W2Ed9U8e?;fve z+cFTb#M3z!S_8o-Q`DI{eq!yVcE69MV77qnRxu3cr8xX*ge(KHa4J*wbvCc03k{z4 zC}GV`FZgo=?D)N9R#|KyfsZ<0wl#p`FW9ceYE_jM4#Xi>>{n{Ijb$CU=NrZX!!ySZ9-V%)R>^72YX4>8G%4H}PIrA4A2>Ne<@=ER7#8 zr{xJ*;XS?C*MK`86@Zn=VOyHpBkpzmWWWv(vyz5uhqz=89FON#M;#W1**owQ?D+Ab zrvJU+N&+Ygqn=s~%)L@KLT&wBusN={A681ZaPG5hcrN~CSZVXOd9K#$B(CV$D6_Ng zGVfQCJ(Oy^X(16G(T%@TI>tTHP(R_Zvb<}6ok6HS@^}N&XqS*1Pz?;t1SEkzHfg!` z-kgzy7ukcvbY&nV?Dd~>J}$&IWXfkDiG7Ji7RTq;--5Zr!{j_jC_I=q_c^NrCf@&x zEa*~REMYrmZX5TN;6BAE^fzT+n>lL|oT7)uM!0UVV|gvW0}CfP2>lYs+WO)Ooe2`i zN;Guc`r#elC}Hc;e)A=$6IMBYwDB@m@!i}^R=|gF!P+{0)L-YTO5Dhs_yO&;jRhZM z?$M2709F#BwaN5b7iyC_q-(Ga#hWOGPCiQSKK50>Mp;7RK>lR$Rjq+vY;nfr^iZS# zJQlJ98rbz?Csvf(7=OMA2qjx@t~x(;dR!R5k^KHb-Xm5m3FqGnSLHrh20L#v-T?fs zi#E~#&s+z9%OT6mNyCKG7!kA!nak#B^ug6$z{&i{F_O)#F=?tD zUtglC`KP$Lj>81{=6;i(0>{5%48s(J4pRa;l3(8hYfc8URc*IbhO~w*y#{oqcUiX_ z1%Z6nqD1hQ7xoK)@a=F8m^S(j;?xV=+yW+yEep{jnfamy=4;?7)~cpRRi9yQ*=_kdm~1WIA^)X=%&3trd%U=DvDu~4HN z1~IpR!y-d04^H-HxZA1%=Jx`?!xS%BRHF93S*X1F&EF(06im$vF)Z*r=z{XFFp53e zvO}3$^WnUP6l-}9@N&Q@(?pm6xBV$~imw`6RB=CDkyhr14v)Zy(4WqMx?VHFs8$F$ z{C&;em(-a>i{{G+{~o9@&JhKlfJOJ!T|0*c8KAF9OHhD6m1#)Lm1@KN&4#1-Q9skY zP(t*~Vj7;u|Amm=?#LfrU$g*0_<=vYO7fl!v9?;y+_`^JS;R(!xLS7QRO#{E@Z-B^ z#@)x`2Z5~l+u@gwlT!o0l_-2BZycakmm{lVPBWXPkWGth{3a&~6OGP1I3}2@0+tSg zRvyE~-)koXqo+k=xWt7KLjDnx2cQsr#ZC3oP3L`nAISht0c1a6Ms#;S*4kR$4L&=Dgf*_BZHc~p6}U!qG^{#W$X}eooE09`|78ClJ9P{SxI5Qn-r-|F z5bRv%VLq^5FT^TP4netuNH8iJ`Z7~H;Z5;4d5#|&s`HA;st=YCPoCmOT?DlnJFYA@ z`w;>w=C#+GR1DpUQ^%cdPlI4=5Evn=ppl3x=Xew>{;{&od1A*N2+hLw`y@e z!LK^ana|AjkvZlvF?POmy0!A{su&g}EJvC@FQ?ViWkZ0~x%X8hxklhSd?p*StPi}; zlO0O&*@eBX{TtYsu`=H8P3O8wtPH~6N`Qoppv&}Mj)f?y-9pFDpU5>?G`M1 z?tR>h2=sn4`Zm>oIY@+a{q7H1t_;4VObChydhQ@sDD{68vau8=teTnEPW_xEi-lT6laC&L={2s# z^69YJyb$5zyteXTo#DO3s43BUE~W_l5A_JIeKn_e%rei7h=8q?oN_LN=KD385N>i--}#*Ctdr^ zElrg%{keGd?sCE9?YD`d1<|Z75IHxN`j)GLOogm4_yvv_5z(;oWtci>x{z&ZV)&7a zy)-hS?DK(gSRie~8~I+RF`ya7zoV9@Y7)M0qnR=x6)Alh6mU{nyulP5H>B6w+B7A! zZN0WmXS?otA~z#kni?e1uKYCtCY=&ZD4n z0~9CV+*`bwiC#qf9Oazy<2+qI-;LfxdQ*mW4Dw6_-Fuz=ku$T0T-)7jL|?m3(a65y zQKiL{x!Xh^Dy4B%IDbPQcgz``s=xt&Dg__ zmh~+8I3*NZbe8MY=ZELc)mp0|#>;Vgi0hw*#%!Pu=*%^9Zf-Zc=GWn+oT@Jdc~TC{ z1!j~*lN=iAs?{peC|li$;+)gp&#o>ar@zR<+1YiKDWO5XigQu|2mc#uL!tZscYKVl=ve~Qo8V`;5oyVbO>XW;6hK8R8FJB~M)3)Ba{T@mB=O`9G zJEtX7eiGY$TkM5gda1hbabASQm4}qeuA96z3I8!o{E+(_TTIF{6||?w{~$CS+dgWb zA&d>)?Q-hT5RL=L>{xa3UX)757O?u zPKKH{A2nija88G@uVFBMdy@XR_CwyRgtF(5LpxP00gL$FHD~fQl)83GZ^}iPNwywTswa=qa2oIH~oEi7I0SnWS`(nqP*u zXqDJ@784TjH$bwfl<|J}8==Y0Bl@q~98&pc%j2o=I>@sD33*XpoxuKM(?||FCe$!M$ls{8g@l+d!0*_adVZKC1Oa%dp zI}8X|sK1PO(%n{cT1(vq_9eI*6QH?u6?_&fVk zcw8J3;hTNb+9mb{1#fZ(Opd2VbTJH7QzCPWD{H-St*c#HDl)x)xF+9qR%L|CqavF- z$gv3;k@xA|TOAAzRHIsZj~bG}_OqF+-qDC4rUK`gl6uXVv|a;HF=QZlfSg*n5x10b z8j?h&0hiduXL&^5OWbvGnBEzAw+U%Qpa9h81e$DkAm%a1oO-~Cg(PW?^PBXjv=oQr zp$Jk~FVKr@5SZWv*ykAiQJ03D&%7#&-Ic)N1ERLUvKqQrh*e^)QRA&&EicJ@Ce%V& zgdm2lA&B~v7~l}~Mm=LQP)SJ83JWGUeVTly5hBbR-5)&kC1l5s@t`yJ65fk=1Ca#- z;x665;JXz|1t?m7j&8y$cvOa}hV1<~M4M#52z1Cs^rY%RggzdG9o*ezbqf(GRI^?>Huht?7&2_g2>mSg;i6G%l@Wk41SDajQO zkMI@m2}{HdL0IR|-T=L%nfS6b-Ct`QAHFwTRlTG?0CDcwzWJBSs`xCE%BroM+p-<( zBy~60CRJ@bu7^Okeh`*p{`6Fitvi8evHjR zCcb)Q+3Cx_-`JvHf)5osThwZL`FrxGFU6QGu`98Z2{B`>?Q=ZA%U2vTttkpsp`DnWskuP;c=)K0{Ob65|f#j?8=rOJu*oZG; z-U8ze33K@`D_=Y(1$u(9S=6OxKOS-u29Q8sk~&sr3ZeVdob3&cHgrk&raoa=Ku&ID zu!_ipqJMGysY33yBFNR}Ez9^)tdIC!6Qt_zAA`mp0aX0P@VWOmQ{-Pq+fgAs?8fBY zK?r&n`AdlE_Ou|iQs%Q?Tc!~4^~5{LJ9Bp)4*m;J^cG` zSL-nnM%ZO$6P7olF|At6u)Xh?tiM^#UyT1kcIZbkjj!pS)BnPD0nXgpju9(jH$YZp zkNvb;Ko0J?9qj+{$-j%=SFdfU-1$Q4ZB~4_sAkmU64s0amekcka({H5qqLm@56`oG zUSlnp%X7d4|e$f{iJ^kkkAkQ#$?VRfPp3_(04m1*jxJE;kv1Rxaqb&w*HfbxnE!a-z{>YN#|Bdi zAj-ApX{S4(qD3voym(i)I>j0aez38fqM|L9{=`w+K9YqGTZ0d@~Wt>a3H1k0rf)Ax30xX#b4A zjzbBHzM(t<^u8^p4fq&fQwDWkPkdxjp%TBzKUi3*`;4i*vw3N7Y2nWIxH~|rlts=B zwINl}hIb^xQw89)uh@eNQ!odo5Fct=xAkv1Y^$}QUH(&WE^|2(`3xmiU;~LY@Vle; zD0gJw4gj0dcTQkPauaF*dn>FsuO;I-{;n`RbJTbuvS+I-Q8i*z@2f%0^9)(Atk>X@ z697AH?Iy0EZ3P7CNTGZT09=CfW`D`qlEj{1Gu& z|H$*nZa%B|pKPAE1TIQk?tv_b2=Q*(Gxp zM0GIm`V$Lpkqmfa0c)&SGuXk2n?g*t{9X0zKuo;4aDQp;Et7a8c3T(IO4Ah^$c9)g zHn>*v+~6p`iGL)n(ECfc@sqxEirn1BCH(UKnPQXVZr$I=y2ix-Mgk_~a~~JAx$+gO z)r`#B=(G)KDJp`hW=F7WAo&eKiV>DNQAV1!bzB=&A0`{4n#ydyW43|ci@}irj~{yu z#*kd9(`vXZTqltgzlU4nx#NE@QExc!(55!6a@VJ?YL!#Yj4 zDVTT|Njv)3VhCSh5EWGskhx)snJsOrEMgvfV@>=Nu{58+P8A}2w9R5$hEN*=7b<`H8dN&jvX7{!0%K3r za_x^sSy?~r^TK;s++yz#Gp7OWHUMq#wMwZ_QH3{r;=8iX7G?VVk_{lIE8Y{N-tVpo zFEP#bUeU!WW0nsne5I40SH5synYnzL{761qRlE9v%WBCnWGiZUtQ-4yxnKW@rtf7| z>{&l+LNgD;N;)Bg-UXfGikvbq>P0SC^tUu=B!H(k_)@;Q!pDBMbY2yZ>`O`cHu})` zpD`~!8+_E*S9rm!H~t*~jOoJG%%-%o_9NMNlETKWL&l}{6ZZ-ySW6jiQWRL1J@ z+_-jDY@w3;Er6d|P%F$%cuSVY=(Xxz+N&J>6ne>P6!=CJG<-no(WDCB(A|cxiqfh| zmKMfKSl;r=)u~JkRL*W+n$fnvzYo-~3Vi_1Z%l1vJ52FwXV<|Hj@Ws5!F;(L0O-N% z>x+FTbSp5wQHD3kx-Q#C5Sn25<#@0il)nQs(noW zyQQwzq5DWiTG`z@`)Y2uMHams4>icS^T9K9oghWjZm}WcW}Bh8jvm;bs#NM;Hg(Re zHcL37M_0_`HeoK9yCN~e$8ir-a`{D>M5vkBwEb3Z?AhQrNfRI+74!sOJ75OJoI-mr zXlVnno8;f)g*uF}mh#5pC+Bz1ttw)msDGV3az|H=i>EpPq7`o8f*?j8E0nIz8h8xr zI2PQp6(3f3y2&?xq(_c{d=4Pe4s!aTSwHUpV?|zvou1NY2E>Ni=;8wIZM-i~&PCu9 z)o07qv8;%w$*4y1lxm!?8JM}+cM`@td?B!ydSPa3DvSbrLjvCDbK4hPxC7l$kj=&P zKJhjrCnn?NUxz}6P;TZaL(!xCMj~OxfR{qM=R(tD0MHT^wN|#AKJ%Y3aN7_VHcpsL zfRSz51G=D@fDo|u`p2JZk;~KizGvXBK@RDV%cv|6FQP>8Bs_WIlwYAxe%KGX(Vt^u zkQrVE$~XO$1~yQ|&PctoHC;diXC;fFa#{orZ-pViQR2`f%2=dh01KH%)#LEs?1XO! z0J|>pqEtECH0m0a1YBAsa|RxDBjMwRPpmd*1HZ41JEI%)%#=Ck@_=GF$l*2$6m+=C z<)4Da!SBL)4CHLeBOb8oq9Wd`%=#FmQ7)#AjE*=6I_a=BcfgFcMmoM&`*4uCRsWuB zqw}hg{A*0EbD$W_{$s7-gk!>8kIgerxVAOJjdGMkX79e96zonb=H!Mk2cf#g=v-9) z*-Ufdy<;W*V;!)riVZ?H2M*ey!ql#BcU~ReXV==zyH0!%HX5%alvEf1a$c&1IL(yh ziucwwr$`YF<%Wk^&DE!Q6nKS>y?Ds*#qKsn0ZC{plx!Fu&>h09krPGc3{wFZ3g$l9 zT?iAvlZ+B>?;%44XWN+{J)CB&bO+-XzJy&tfeM~L92?Hih32>Qp0dUBl&Z~^mHxU+ z2?kl@aQMQu@{0npNGpFCK5KA$(P4rdety+N{Ejhy*#_s%I|oPMyu|Gz7MhMRY3Jny z2bVXCm-5YrnTAu+O{s4NsF6|QtBLr`mdZ0sLfgB1?`5N;0EV|oH!Q<7?*H0bhLdld zwN-)L-`ZMcB7c?_f-SIO7ThWWYc5qfO?kW{XyQrj4r0e7<24^k_qETOe7T>OQ-BcD zDxcFw-%J+`H=&bNHS$>DWK9rZ_>y5oe z)Tv)b=vcE*NV_~noF`Q+6c7=C!_aR26*f#UV$Tvb2DAc0j*f(A`hd#dvsSIjgeDHy zRh07njC0svbrNl!$`;kHLAi)}%%QzV2XW%>H-7xgDlMgV!z^R^)3wvi7ZxabH?Z(e z;mi5E;qIx94oRnbd~jhxyrBY^{O*9hThwM<5bRXX%)+4|-s#BDdn&MlL1?~H#9areu_|WDa0&DLy`B7A=Cny4|98Bp8rSHU&l4| z|L+6v5=M`17!67&APfaILP3#I5S0=^LTV^V4Hh&=SfCQa0u(7hnFXV}1d(QRNsbx} zxcC12zK_R!-}`gtoa|@er-ue>OPUpBWZ-)*FS!+ z?+%On#8iA-)6RO;`0Yv#X=#M&nsu)Dbkw2LSC9Y*8;!Zw z&udyx#9J=m4;`H%U3^9nVU(aHPLWo+Hwapd9nufjW=kY63NxID^fV}jR>W|(uoH+` z&Y(Pus*~mldddr#$iGOV3zqF1oskZ-x4sLyDXltiFtl?V$aaE-IqUm8H{Ch8PAn$* zH8}Mm$;;9~Sv%5#dPP9=R2&9?;Laf3Gi-wIfL2ZL^(|_)5x7&t#0=d*0In&fNJcbT zaUy5g>{841Xi73ZJ z0>n8zp3`=Jb8i}%$t75pmbgjgPk$fcJEM?TFJe3>mB0hoFams+?!n?hsjGbI)oz?o zMV2fl?Qa{~XNVm&F#_`-uhlpmzyW1OB`*op9XP*DbD%tLhUS+$!LE8#IAqdhUG&I? zuEhU+yUz+M3GC9in@&Nv<9)ZOAKvns=GT7-KebE_sA)y-T>u27W2;dNe_7?rR3tTg z^3_T5<4&xcC#Iwt(cMnVH6z?t!#q4Bl6E+ixXCW$0WTIrL;#8CxiOcvX#yBKJF(}3 z{&AWr1lRT&$bgodxzo<6?(BD+dh=>V7r09Q1jcZ$dHlbct?}@TZj#^XvpH#G2NZ~6 zLfa4WFV~$?qglr94=H2B(w6pWl`2s!zYSmV2wBaCTpeXgnI^Q0DrRrDi2C1@gM2VL zFSSt&tCGQFs8zixkt1Jz?Od76JVmU`8>*(9HU@w;UKPQlPe*4`o z`xzbCnzEGE-g0RZc;Tu82x<9wQRd_6=-k>H7kl1+ZRwyoP{g3=IsUh*Rux!~h=Hv_ z=bt>_0+vXju$eO9fYZcI<;2|0MAli`@O6cQG{sTrHeyzNZ93$}pY z`4I#CH6sC(*aT(^kb5vcngckIFjiidtuZ z?c*{j`fud8`IOP=`ilN=u*A!1X-tt~3GD=>IIVHse*DB29yn{q{6RfX}_C-?3f0+evR|l~qQ9V=7APH+@|(Dp;!;+GEn?I<_zEdBdm>F)~*2h`P0v?-s67 zd*)ZXBmb)xu1T0aiWZLYTSQhcCb!!@o5XHaW++=bWY44PJAE*5(|dBX-&mbomU@0s zGrff7lSJ8*dwO2K5oKi1TH~HD0!nGze4NR){EyFvYu6x`p(8+zW&Bl5uhOf3pr(=_ zY4>bg+q$;`ShBJFE0TX9j=qxhHJLYnI=EKu5r;-Tybd#NwYKQ)CLMv7-wHb!pz>05 zzGCu_Yae1mO-T^bhsZ3gQ5BG{RVHsLM&&q$akVXzBS58xrbZ~yzB7nUR?zGrwl6HG zq^ z6^iHQtzCZ@rnMB$)o^<|2tm9gD=|6ILd69_#e}*wf!EEV}I|t4s&(0T9xnJ#s0M&1c{hGLPB)VtQ@z)CT z0e!lfbAIoml4`w$_K#bo5@nj9K-ceRS?B`|!BO4yuaBsH?#q&*0m<>zYcjv$!IsL- z!ZZ%x>gn3%X{{gnbFK@4<{glm86@_7h51rP(ogEyQ%+cM379%H<tfUT_M!$D&N znU2+DCu*4ZLNhn5(_Kep8d<_?PviKuUP7IXF_hb2wak{7E1*joplUzD7SZDHnhWMp z(+LE*gy7+?BT~xq%JNjri(5&Up9$QhSBNN*Zp;dW>x?$Q=x5wSS>ACHEfdDgrTdF3 zK=~&?ctz;2NzIPm>5Plqx!yCHC(5atjM9R=G?vg|>c(Owzf*R>oP_`j&=SNMyTK9F zWK;*;$8mH`ZB>j=6Q&}%0v`qT*+h*PvWS$tL#e?W`6XWcOz0*a{pd!aD1W;AC4z_j zi=`QJ7XvYCt%0M)2;|jr=^nHzQa>Gc+XrV2K|5eyP;=)dw!t{a#c%*FD)yfA+gGjV z+I2oINl(yC4rpe*1BlQjWd0UWa+1vlnn@AmlHun_WzB29bN{tB7k!Kt^LzKj1Pd(! zDFw~apIM{u^h~{(Io_dlM{#8rg#>^@1_K3UD*Y~8Qz$q4(dw{@AWC@=210u5N-V3 zazPdO?j+=I@R@hQtMFo(n$U0Ae4Il`bpO*%HW8nG%3L`T>dBNGea@J;e?(;C_A>qX zcjN{~zvG&Bl;c-8TR{4Vc7v!^#B08PSK2qFd+X(U_?IV>0_~#Q!Ho(;FN+#`qn2)} zAmgt_deuLKdJ6T*cjw}5z z=DRz2uS)mqe7;auD(|4=dpLa7K@-~f7h>>T#w_YbsG}849-eG^6wDE-3J|=ej0hQwi z(U*SWk4-pcR*GeR*(M$~ZGECX!#yQb#5#05zQ`Mg{}NsM>h>=qr|%@bHXV`A(( zCiKxHtM`0WZxHuvMkTqGyexg9p8cj2``1DR`pdm5NG*Wvtr8w#L89xt?3Ph+M5gqdSLhHDnB4`Oe;vNXH;|>lmtB=p4?^hsm^DA1 z_$_p=_T}~L@#~=4=kpWdI8AJ>4Le)OD+jVznTr|{c+|K&Z6NMFm-n7(V=%i=1<71{ zG7{^y?OzkWl`5J_Pcf-?LkURe&&KPLPOjRJfLWokZx4!yd-(kATJ zxw=2unaQTUX^TN8->Yw~-t|ciMC=*@%$d_iu3Dh_=W`RSfpI9EKWwRLk^u+)okzA) zT8#9`gV#&IXUtEwxildQplsc6!xOF&y@95K&!Q#-VJ-^8-qIuR&rhtf;dIT$RrLI?)kJ zQ^9cqSt)2tcFAc+^5LeT%jM>?=-z3w>Tzr$br3VKU+XdMPI6SccB0l^6j&V3&R4rg ze>oJNM8DX%F~#faxs<+}CZ#cXJi5OJR}f&lLzKV;EAP|_zE>iS7^;!e!oHVOe3|Hz zgX|(c*JpKax5Qr@FD+~w^cugdaByLwmq!%%m;fR2uRkYhO>Fi7iXMi$$>ae${nT72 z$Fe3t&fsOGdqiv61*Eq`zG~G0LWSn z6|B1Tg$<$$p;~Ws*eObuGod%$JNBa{XziRg{yZmeawtD}>CB=*J?_{K-Lw{Yci%o2 zD$M2Z_x%q|J)E8wUC?lm;bE->l3Nmb%n~`S4^{~0g1V8OWH$UQ57;aPmh<9PWRl^>4hBZ9bs zV={w>BeaZTE^^8t)ra6}o-eR27_2w@hRKPe+Pv#M;1~CxmFf68^9z`_duKEd(m0{R z*wP10x?$$E=KwGBv-unnR()fEv8cV??nx$*Bj~%ckN@DQ-Al} zHXGC8>Jv^vs#MSZyvI#!@p>w41Ah+UoTL%KX{k@K(`Ib@s+L4BEa5Uxo)xI(0Y_{q z>=CI{6boQqeXfG+0aTuh z2DZe(nmypZp>Zk{k!s{c4%hlMVo{gPCfj&g;5-d#d>a?0Toy;oabJ2kRez-tEIbf+ z>9&^r1FC_+v=6XA|Juj0xVO$^6|xEvKlN0ByFkdCn5XrK+O*MlQH)-rPWY$=C$Zqk zfQiO=j(%GyJWZxl>SH^*Y|X{Qqm9zCy3Zd8)|UO-u_#q4CRQXLmY+pTe#u-=zu^P> zoz!1*)6~pS1*3Z;`dB(c>qBt1-K<{aE4_Y4SM47aG zLKTS;B#|}@VBE8b_ghrR&s30plB1Q?W887($#L}u&%{%kd>^S$;MK~%yM&*wUxj~} zQPOw9ug%h+`+as&gR3$lJBxV^nKX~&4oJ=U+M&p*D~DYg8xWhvcSAnBnarnR^QmR7 z@!^#Eq3!d3joW;+y$3Ld*lM%^w)TDY8+9^pPo1fwGO&rizagIKJ}&v}d-fYncJ=+v zo8N?)Jz)~Lkg5@8HAe-U*da#V&Q7kuKnaa9?d(NO&Z%VC73>BxQ2gr}_jLm%Xv!?m zY{L5iIWwkz_7hB<&ZRcvZVyIHS<~5DuMK-6RSKBP9hgFp6Q6vkhfxm{18?x1;lkIL ztuV{?aibOWpt>-g1RB=@Omxq4Z2e+PFrIS8H2>@ZRmD-jr)JsJVF60D&TS610q5B5 zF5W4i=2I8AugqfwO{H2#-!hlK)B5Sp&br#wTL)2j>UL>#(8jC^^cmN)^TEx#rBZkG z6RF*TPQy_Z@?;MBwHxAeWQzS&%vB7X`-yR;Lr|;_=Y1Z%7aE3r^}^4!FQ4Y?rX6f> z)@)f{>#nDNk||Z3|N7hjB#_N%UHD!x+WzgH8<9ySvH(R0F#AuYGBmbVgu!zmFnaGz zhl;Djy|(`^h<=R#ytw=cF4u5F=1UbfP_L!%7Q**E&&?L3ETL*)&j zdns1;mW4h~=~eRI@HPRz^hVCQzn29%w^hwERLI|0L+a(qq?aGM?@*gI8@&waZ*qwm zFHT*3)JS8v=y%UUk~W+Ev8P|1$#YUvc?>l*nRropE?I8e5gIDSs1~HBm1PyV_a<~qkUX*#1jSvifqHxf1Xdm0bebD zu20FZ=a*n>Gu0GY*o_eUW54jDD322e`2YWCwmX7aZdUx1!~ctQTuqI3ntLBoZ*ZKN zT;c@zEL9X4fbL`}ITRcG!0?tHNglp<-t}2bGwqL|Uf$eaom~GW*s`&>}lcc-iNhf1@gajCaSYcO{on z6Yos;rKYfYv1Q(tuo&=a^O%xefiJKC_E!r+v+@_W&hdmjZHWVxoIr6^U7z?A>f!+G zYQAx%zw5@zzZ32>?wc&;7qFH5-;R9IW;m?DkB-?! z*AzSF|E$T!{>Zzh^6|pv@WQ8KDkusJnOXdLM~utqxHorQWB<9Xn2prJ-qD8M>?g|5 z!|+ph-=DT#-;xa8!;EtRupf7xpQu*BEZL}i=GrW7Nh|;K{79)+cwIbiz%cU1u~KWT z**PwWzHYEVA!(j;_Cxz;#@q^Tk6NY%*x^t78+TP&abJp-_xQR#>Af?d$e-Lea${7t zRQuetI#m}Pd7%5*)6sUDpD8kU>7Qc_{o)o6;MIb3%~3-b|L{E>Djv1#$j6m^?savs zV*36Z8z{iQ;)kCCi_;AUphi;_*W8V^6std#O)iu`{LO^&@B2B`J zAv}jr28;RddzV_^y(1y&>Q769Kubxv!e85LsKg^6`sv~HUsCoxuoW?xWg*PB;c>Mf zYYTxE@ke=L?i{9QH?ZpInY^I}Kg!F%_tjnX9%ka#?_KCNlMm$RMX?|}7AJ-?*EWkI zkNuF9FWA&ti^(uJ)6Zi--pl{|Wc&*kV#8ZJ0i@{8$!}R*pPS3FX6-L4r4D=sF7*`- zI9tqS6|aFY=NGZBMWst7P;|U>j_7Or*P~E;D7ts)y@5vaA=YZ&G`Tz}pkrMHF8c%k zY3rUois3~!@20up-aO@a82U(e4DH?!b{h92f{Ml8sivEnEJ&D~PO(EC{JItvbEY(D z{%&x_-vv`ca+GAAOHwGB0KmiY5~ukMf8p=YfVoX38aCexd9wfKc;eCIvNd+c?z1_g zEgdAseI&N`w1!kb+O~)#-4C2wzL{{S3JV7zF z+YPnmn5+7^oh;u8#o)d>VNT<(qY|Tp7J!HK1ElP)kF#6_#WCY~Ndy#|0e?UYZ~1QV zfEYNtfzTJ9Wp@{rGtP+E@rI)i)yVUY%u&{P?;IT(XeAzdx!>^&CseYfHZ{ zZwp@R53TSceTs9slzi~D>a{SelT`{=UdM+y6q>g&?a;4;<6A%Tf4?J!rm63oFhb0C2;V*@zSUb3cO;LY2nq&_ z7sOeSa?E|KC(n5$q&^dD z4vQ4*I#8Mf!r96y*q%?tum1nwny|`Kf)~d3I%E9TzEPp5U#?HI{!7V6l51S3r3?pL zcVe}VV=jw7@()u~E(d}afj-|aLi)Y`L3l^&Bh|jFaONsfU(pC`v}O*L-eKDWBmTX; z6oRLj0BUz>Xszb>HB|2H3iZLiT35!DR^)1cp^F0+2WtPj7eqygPMtZg3h(H^+Ww0#>V1-DD_Bb?%xBaS;DD~^EN$Ku0%FovokXGv8q&F%htO^^{E^q3m@x-C5`OiqS*| zb1*D-Lf|invwaGx^l-e&2+FETix4-KHMny=SI%X+{ajrnQ7&9Iv4hj<-gvOQiRgY9 zNHXAx1$=b+I%ZibWTyO_DuSpyGkwJfT#0L=GZHM{!P!|<=f-+=t;%vl-E_@qUVrB{ zH3YSLsEW)O6F=(STefD=6I_N@8D#-@v%l1;8~0xAK5U<&i{&U8Y9Nz)Ex*avA9{a# zRNa?W)PDl0<%cUwpe7(n=I#^t^p}|{T_|_{#+A2hpu1v!O!$SYp0>zD&hYGqH#!{& z601|Xi)>6W2_JPg|7XWU_)9IM;MG^1h4acn*^LWCOA`^k*!P2ID&{K zk#@g&y$@S2)&i@6_MNQpz3t)sRb z+7!;`K>LQ)%iXosL!}zsg#iXNyq867i48Ni$99G7Ygr7py+n`Mjdai4fCz6EBv>n9 z#&(c`OVRAl6d_jD)N9jHm2MaM(p9$RGi8=`B1$ z4XEok;YM}{tS78ZiKrMljyjL6KBRfqFF|Fy;Q4af)^eIeD&8F4K=Q$6;@2&p#i6-* z**Ig&?XSgGR=UAA^*kt|wGW~VH6sUE{*Bx^`Omo4Xgit%Vx~E|(Dl`MI9@oL?r-!} zdZr?iHk3F@C}o4ypnJY+E3hR60-EOc5I17XY&HN(u65kre?el|FDZ*RPykgmf3W9eG8?oC@DJ^#36-DWBHDvX-3Sz zKjALXgVPj$x`R?@4YZdJn#DMJ)979z{X9CW-%!uj)>#?O!~|=;Y(ZroH21`M6Dyhc zPc&^0j`%nZ5Xe;}y8-eX!bK}jFbcUq@g7fhINQ@T5}kYWdJaX~dKJmnpscR_|v`Ky;-HB)4D}<3?&!)nT1vU~960XTGdFPPJF`N;qCY$*kJpo6b zZXLa2yKQKV7i;nYSz4zzbW^hW-bTmz{4OmHujlsO4%IWkl#5R5c%x&hp1}gPQbRw; z@2^QuC2(ktM!PsIe7!Hc6jf#wGZmt(?U&pk{-*vlBACT=?{eNF>;I#d2h{UCWV5c( zM9i40=u!B)WuwCz_H)*+5OXxhDMLZ;aPZkWjr!%@xrl8R+q^{kEy zv4x)xcium_{W1lED^9*;HQpMBAI-YP08hvZncGG&606`k zFVG#VtW8qE;nfx;qKI~*Yt#rkruY2YT&f(-26AtgS7LG%Bfih?)~+E8mKpyNH3GX<_|y0{Nq-nO`6# zNBL?Do4Qa;Cmwd(leh4j34D5D{SyvGI^0d9p5@Ih+2KB%mX_N*N|dbIVq5Cogv7Wi z+SGZ4MwxRIjh$*=Dbn>QY#XkM`-E$cc1`e$i@+ru%;ToIT&jX3VQ<3{9N*c0I1zi| znh=7_TW@?m?-Du1Ro>bdVI4ki)^+_LER((UheWf>8c1(i;dQ-eV%g}wpZeKy=t0l< zx}W~`5N#MW9mwZ#pVt6-O9}GVR^r6b;M*u0^oGTu=jVk^I09CL|BclyWYg|6BjnTL ztRGn)f$&&uQ{A11kNq#NJX|@8xpjp^?cQg5MqHy;Q;dncYWSE`NcECUeY$dNR3n8 z3(DS~FQ@sFy`rqRYfb$*eWY+Bv2PA1>rqMpOt(!5%{N^Y6HIp%$a|Sv&n0*W*4L5 znJE|t-D30`ez@O$_QPlEW#Bg3(I*mRZx+-DX(~-ytC2OU3H)4*pL)acYjJYvLC`a6 zNSwRnUe#ncJbU*a$!D{Ir$VVda-8W0W=#=kUgqyej`WnjwaQ?~F#zsIQ_=9q%e3A+L=WWn8TxTrs0674O4;+N&2b)}Pqn=_ zGkCWpDz0SCQTYdS>xa|KaBX$U+=PkwT6+}#gNz3S+e)DNuw7YQ-pGjHbP}2U$*(&j z|88Ka6iO5Om;BdR;vwR6MlODdF!iM02Kd?@dl)z3UAC+C zXrG@j>1iGC^uYFI^yoW#cIiY{Ly4wIhsj{TKE?0yQD zQfyE6M((;FlHgAKW!!O*`ZJ=?AUZ_CFloZm^JMsaoM(l!JoEXPj|X%%*LKPN#9S4} zK_?`hi0n3tCZa2#DcA!ZfK-!`I}i{xVE7@3@g)6z$`UD6AgLz(wRN9k7?B<1UjZ0Y z7tZlZ7ul+wwZ-L^)2piq!x>yN`fd1mBAfpr_OtwF27RX~BZ3Xc|4;C?70B3e zMUCSBqcBp_Dipr%}V#qi>Up)`EwpwI+`LMA0W3FE8lYnf}X#l_bZ5 zN~~d10@7S!8i*b~Be@y}f|yiu0TzfpqKj->tj+8$e#nQrrwaaO84B_*g^pghI((o* z^3jc`et&rG*lEmNP`g}erZwq$VAUf<`@QI0)gA(5-xpL2uX+SY`Z|2y{c8;VW#mr% zGJ*9dOfhdYgm~4!sXy>g%ZTwJjqP%t0`WpM+b}cY=R){VP=J6&q2}6=n?Ox5*2#^? z0`h(ux@M%x`e0P2Tvk~qal1fp3eEmT`V!UDIOEm;J1^gi z04bF!wtSZD;9vZ_VUqUka0;1ZGM~_^&ihKb0VIpu;eyFV^MF7E5bA24qIZ4$o`43R z<#^+d!N&{LCe2I6-EGZ+ip)_ZotqSA_1Ry62QMGeu#(CRfl)$>mkGH~_flML@R9`V zfI%Kk@1FAok-z_#)UI`rF(zhD_3=miIpHsJ>a;a(uYTiTUkT`Q$ZN^NbM&oe;s*S7 zQo;Xmj>hC1f8}Hlc2gKY_1gD;B=@V&CKPggGU&XAc@H|zzcPVx9$K0+NJntlxXaOmMgU`Ww1gfd5T8xemew z8MfkZ*Bw}m9O({zA<cKQPb>cp8S=kiBxUhX;!wKG2s?oDgRMbzkc8o`U-M3UViuEEnQgv9 z*CMZnwk>fo_GgCJvm3Q1OfTEi848i+hp?;47U(6`9JbZQcUFs26GGmRhN;%}QLm*| z&29`Ce%gerK$D_Oum|H+ho+Eb-n#bNG6rKumjbScbz54oK%N8!mh(RBJ%yV7dtdP9~Pn&w;#+2Po=K=ZS1A7j><|I&z03U*@ zty|k&19U4)%8k^3v(XUy!K5gU<{-h zT+g6&B>tWr9M@L=V-cKO^|=~SrH(0Lj^pfg{9MT|%nxwo(Ny;9UYZa=tAj*INvJ^Y$>nhx;N+Vo63*~R#Wz6rR$)$!b-93A)?ziTR9piq>Y67w z{BB--8ss<%{@{f-IFPU&fo2PzGk;XWTU#|v?Emi?n~2b)3v!BEMn&5?$o7agdo!ir zV^g#esB>qH^x!}=J5dKN469)!=FLB9*b|qz8E#_$l1koB7J-ica}fqD3CCq$S&9ycTtBBJ8`xWw>ehwgR+j7e z;U;pypOD zHxPcH_t?T${|n^yiglUCl(V#cU@3uiUF6hSH@ET#?u4=2Y2L6wox*FSCN_-gnFa~; zEF3Fd1s8>;AN9e-5)(Jhd>H|~&Vg=HUhb;77@wsC4gqKPyU%;eE4ESj@+_qpEzxK6 zd3eb_xQFQ0qo=&gYkE(bKN!(H7&?Bq@5O4iR{?I@COv}7?lYfqE#%t7$UB2OolFOK zlqb5)U{G~F+q<}oD*(qd{f3P{3nP^iR$v!<6Gah%O|7r7qWoyOuLu1T?J$W$V3Gs`KcLP3yJrV_FL78hF~T@JGKK#V=x?&a^Z5=u-c)kObkR`x>6=nE+ctO6*UG?MrE zw|ycqKM*QG(6Xq&4EzPNEqq4A^2Rd{ANx-9hh6;>I<@7`#ZHc6gQikmCccxut!0vv zjxXW3@`xuCy+Ny)>U?)cu+#ERiE$!Dx~C?t7Hme%u7V<&I}^d#7OJ2sF-ZNqJ-RE| zCh6i;+g-JYHy@Q{*IBnp-PS3*l(G~$qvp-MfO@q=8uH6gp zcR&{}8W+rE{}FG#60A#&Tl%bUeA#-RuPYJ&C+PnVaKw1TK#3bXx`H*dt}{>;{xp99 zYbTE;pF)$=%|{X`;ImPnrPWaXOChwCd3wbv$Z1>NvaD6r@sbanzyI$k051hhEOFbX z=mq-z)tPO`v62>{2Jipy%)r5#0t2^8W6*lAiZU=8TB zcCNubDKhaV1y9=Y)J~f5cw@eEy#;d|*!pBngFtOo?usJnDj3m$TrTOdP&3x8?+(W+ zV@JCn?#nkG+#ODesm~mMd1X)?U4XKe)Y5lPU&dl-!u>Cab;Y7n53em0l*D4fw)+pR zec>fu0*H|j=h?Y=7QPq^A59z#Xmf#Bt3-q9rL~C!U!&1YO@7-b!p}6fy6LR#PVjuJ z$rL*v{;2;!N-5}dc6iBj0nYMS9ta=xlZleb<9&3vOn7?%#I2AgXamXnVjiDgUMHyd zh5^DhS)TWDI^!r^Z_3ocy;|h^Ti12VRn0DGqc5>~z|C_Ci$NL{q zK!wT2q1{r})cu?Pptp=2%T88So&Y<%O4#H86y50MX-=&Qr&1k3&DLU`(N5nn7k}n- z&oi4&;0HUg6czrzT9V{k%`xTFO1g^oD3{iIzGu=sdr*~|L0B|x&6P(YnG$|mOisC^ zo+7Bgba3ZU=In@6y(yMIv;Rxo`;7svEslt88hnb&gnco_v2@;ST(K_Y0gd;kZN6QM zeBK)U=P>4g9W5s;{~puKYFC3hgDKVnd~%8VXn;ANs9b|La1Ys?oj7{*hdGLO1)0hl zAWe-ck)tj=ZzkhW5@#^W+l;*Km~W8+sf@|XFTT{3`&Tyn@JubTX3>6VbOr1n)x$XW zYmM>06GN*Pe?J@T-5P(-^qDI)sEOsMX>Re6J*9Sf`PW1mZ1Ra2d9NTav%PUcapN`B zZ&ot^Vl}9;Z3hsBuYLrTglw1C140}pq`x722q$^G>4yd2kSS&SRd~=SRLYC~%C2ftrkKe{ zgiY=M0+8J=w3-0TkKg`zLcS^TY;(g!w|mcG%BM(>LI=DTJl*1^15{e2AnG6`i)r0F zXAy1SR}shI)Zvc>8(hb{n0MgqHZRU_*;NbKRbqmi$TKF*8+279Vnf|0wJBUVtlVJt z6j7jlpy8KUZzo`OM7T`Y+Z|H_6q~$%kq1oAs!W-C!QTu$O*1K^uH&UJ$3V&Ok<`*C z(VLG4hIq&ahpUMc-6si~EI!ip3xub29|-OfPU<3n!k5US;U)`;K18HBc!JTFtTawG zxY3;kspUUsk2JTH6{iDJI(f=-r~jYBc^|E0EgN6#CqK%7$@=qRPbMe_+KgMPOrVTY zILul;;Kh5i<%bC$RsAJ9G6CG6%h(awi!7l)3j^%m}9h%HX}z_3!st z8#tWpfJ!G`@-_)lxJ2@xFtXVUL+J0-4Kq7->SvHxjkL!cD^DcwAiAcMSqS0TjV5p!fJtE3N`6VUe{ zUmENLUbP;GkhL;{gzsF2faVi#nN%=sAs#k`vV>DTc0d8t-j5X(BkwJM0T3e_23<8J z0g!>5ma9@)rU!k$1WZj?GI6(HL-WtY+YqE|RyEktqmC>#@uVhzzqZ$un|Hnai`K2gds?9@+w&a48AP7085Yh27OdQ0sHn-NgwbxM>u zZ~a$G|Gl>Th!TCk#u)S*pzRX{6YvH;ID`U!uSxe6;laPDAxr*>TU_<5x9SaNWQi#Y zJU;+~0U^}_xOQ{(G<_5MG>i;h)v!ejg@#UxqgSejPhyK?#&o$$dz`xl)rR@EIgfwA z!I{r^>X`WcQE=!9^c~!3{MLb2l(s9sPq^IZw&#%nc6sC)duB4sF!rrFfGcD~E7$4V zsT`+VVe(ohMGdRv&=QslgdteFij2;%T#5W)V<2?(>_MnerP7kL2sj~jQlfca*GVI$-6+QCH3RgS@i*sm6-~|L**uVY zA|Irrr25v{3%@+53u-BlB8}TVk@gaPxxO~V6xn~-4YRj7g6L!XWCxd{pf9J%OEx>*2B!*F} ztK}hsK>*(%qnFG8PmvU0Sztf+Db|tcaF&=BPN%LTL0bJagfafY{&Rqyl;+AY7b@xa z^G~1C1LTkI(*u97vHoF|&3$Re+={j7Q8iDd$;q}fmQi$OvsOPq5)bU!h_eP{tf~gr zSJkrS+%wc(*}L=-np-`#%bE;6@&GHN@_gr5ZS#P36W!ZDypaCFKKKCF-mOD*y4W66 zO)n()5>5A*-9r`vuILO0gGD-WYDC`w+YX<-L5Q}b&e`Y$U2d$=KjJWSs>hbu7Ac2jV`I}gY0*Z2j4Io}cQ88$9FFe+{x0Knltw&Q zt!(@X-g|fQ$!D+P%#%kqJgck9oI8;$d-x0i(GZCvG*vS*vx6zd2VCN5-DUQx89_1G zV{x)n1|$J~#;*myee(2cP`j%=kcGYZb)YN*PR;nMKvAns;nz3L`XS_u!~8n>S$ghM z2w=iRcb(&1uzBw5oVrB{-%+}ifsYpp=2sV=dO~A6u+!N^wlXYA8d_M|vCEr%>!|GE z+Zol5g@gXetX6Ot3Be2f!ZzeOkWJZe2=3!v>V8<$2N?2JF1}f9;C9NnOL2h*!;#T3 zee*jg7y@%3P96%F;_Lh1C4_+#ijgzHuWEjZ^eCga(-Uw3Q9ahXP9P^26JCsVFbTHG zx6Ce&IRoX~t^dae!JQbX3!B@-PqT0bgzC`p)B8LR$H|^JlL|^qT!{gkdj)$@DlL=QpaK%!#TTPYqzo0<{HbPx^!r2UbU0c}-lJ{0B- zoV&}n3f3K8wVC=oPdc^^0UYg)sy+$4k!nmuymjI6Zr$XQv7|+?rJHTH(hcZCb0_CU zfS>X1*}v5h?`4Kq+1no5J+)osa}?;0KgsUEBN2PRstoKBp@8Fc$E6#NjaA}*Xj2IN z56sv7zq>SVb1SGvex=0rv1Ok=iIIFlxJn18Z2iip`z6)$X0~z0Wb;c`>#olqKqKuy z1jPu*e;|Yr;I_G0aK>Y=p23RnNb{dmFE=u}v$q~h4OQH(fRPU@O4;Y*qf^1iCmm-% z5~bz$b-Np~B6E4Ib}4G#x<-HT3!ekcf^<27lgxM)H*XssFkAq^m!o(VAW@@Q_weq~ zSH8*?%M3}gf6VJQz%)zC_U7fu>Ssi@38gkn+Oa$RyFNTn6^NJ@N5C-f+f5%2FY%41-J|A!sCjMe18ZsRl5 zH9V_2<~y&k0DCZ|)hBNm9(uT{W5)80~v?A@?(& z*^BpB`t-Yn$U&D!DWV-xC-;R2S7-8%7cR4)!+8~yoxo^|OyPRRd6a1wUJ7F0-SM}j zBMn^T-vc}2#}W5V`j=9kHt6nRPg@uJVw3bLpSVy%y$qgzh>!eR;{)>jWrwpr{e;V| zzf^~kTRXH?q$v;H6F{Cw}eeWN4+4KGG7Q? zcvrhy08&bb8*r4nzI8p?6S}cZsX;b;)_APu5RdzGv%$z9Z0_R7T z65#(KHVXOPA1z%Yy()eDT$u(;g`=3^+?@%b(33s)KQ-7E}FoD1mU6Cx*-uV z3tg~1d65}27e;?|m%%VRhvLp=aD2~UY2=F~THJBgPsz);d(pG=V~O!JD2jKM`_)j^ z`j{UmgucHoiwT;zYBfvOqL#8y{s3!d&m(Tm+}DOGp#2#3 zbT`Bxn(9&iswmM!4+DWYnpa(ZYj2co-BWR0hd`?iqap`oxRLLzL<6QZpGH2USk6)! zC;hmbqOl+&JYS~pbkZra%z0;f_r`Kbk(c(~`;pwKc3=c;*M1k32`J zNe}-BEZAzDUs>{$Q1j&`2jyOPDw{28DU~fqsZF~QcC^KXFLI>ZJe`BC>|O&iZ$_51 zY05?89}U*ZG*vLrnZ=6DFT%sD0u;6OLg&lJS zK3{+BZx~NEpgr%D<%yqVBEz!fD=iLUHjkbY+-{vQO&Qoqg&kc=wRYo_; zyB)Pt1(4pQJh4Tz0$^Su8X1)b$}IodJ@38Z)@yhk6zo)uJ^ny}!dAXNvrga@HC0u4 zRhS@^@W0ZD=43{)5Kc&UGv0-SiDuVT6(i4z=DDzHBiVbx z5vnK|{P4U6l-$S@&60M&CdJ)RwHQlz@Vlpk17{pd@jw3maP{3`O*~P%n;v@TEf9(b zNS79RXriEiq9R3_1r&iu5rjYrRX{*duuy`iAV{+jKuKt#C{0n68j2K^-V*AKzwf*E zkDEVcpFDf^ly@eR-Pt+sfuBy3?d^Re*QH#n`+{qz4oqWipNsbo; zX~ny2zLB}h?c7sa3{)VRyLRkNuO>XZuuIhmaB5l9nPFbFSjHcX`@Q8f0csK!0P6WU zcOSu9hSlx}gG`jtG463f#}%6w;y>{tFn;vkzoW8Kgg4E$wq9d6W?6Uq63MLvev*Z8(YFsz=MS2ow9a-#SIGdq?w2;V0|wkOM10 zJ$)FzH{04o`=`{s9@6!ZX_xu;mXVvOuuoAdf(Z>~q)x6OOhJ_~6TkO9U&w18{Q}c5 zKCC5k_ic!Z+J33@VsHq&ndM&muHOB=+L}Q0;hpUvr~7HEk*2qc&T=dO8J~OaMBUh3 zH0mmBR-LiX^C{S1-)Xqoli%nETGGu1hZuGo!q!VCKCA|3%o@NCsu3C{<|RTIb4KA( zc}k75?xZgyN}R0vb!k*Dq7dfYKm3-C>C@i!^ymwDlWbsr_A!0p7Vq4$tg`|BBvm|d zm8nHW8oUpwwlASBSm^$bIr(d!7;X)3U7J_LZj-_`vhjv!)|7X2piHXrg&3j(2d>Yx zkPDXqd&q&!tvlFBGn})S(yMD0?|Ls`23~ltrB-N+al#7xFRVHNl?_O2QMCR(;<$sX z^u&&@i#vZ7d+5)5%MF$Y|F!K$jFlXr#FbjJ(66hsW2~!SWx((>fDj+Q$)TnhJc8e4 z+Wm>=^Mv7EOWSxACI!4jATKAAi%bqZeCNaLHw)*8iCy`x-XNe@0F`Yv4jm3e(r z(XyhqM)ert17ZGJ_e$09x%8Ow^INs6x9jUHhGpp>e@pQ@kPW`&&S{|NC3A@Nw9gKp zJe$i^X3F58vMTTPd@Cxp;(5OmiScUojvnz%Qdj`p1*ZK@?|yJDKIih_A5wD#)7}k+ z-oH!}`OxB;pbaFQ>BKyfVrb%Cp94;A9hA2!X;PgZJx}}%qI{gyl5$4a(80}a2NPoc zYMp(!w({DRS7ibBJGst=I08=ECe?S6Dp9oEStrcS*ts8f-b2otM>b5m{mvn;q#ol` z42XtoMA800xv-aC>>_^c_2!x%$-5oja|u7SFq#S~8>KHTf~p{vurmi!wz4no5Q5m& z6;CecGlh1p^GH0RLK&BFxiTvKkB>UJX?AQW-t={m63Ak!FHA9R%C$Y#X3FkXt^}R0 znRiDH(F+XiztiyraQ~^@W0Gh2P)Dy@2uLp;Kyis&Cw_TeSXF^9wXO5~=l8yVaqVEo z)P!PZ)wZl|q@gJFhlIhCS>S&6z`Tyqf%m#fqqp&y4(aCXq-DdD2*_{pVE|a(eSE|sv7%ztJ&%&MI<4Vzq^GmUr2dglez;4G&K3I=P+s@% zd1d=aL0Sl!_K`=Nf9!eol^e$xO(#h74ieAD$vHi2*v>Nq^7_sb;XCisaE}2$*X%g% z?`)H`jor|q-On&}WPR`B7&{!UbXW(dJP7>hi$n~oQ$lWEb%6%2-2P2^xp<`2?TmB2 zQ@=q~%l%m=Gn_W&w`&w%%JaUBz3vgoNi4F(S?}}yNFNnO zxc{K(g;l9{`QayDHs)uoU8sP@J^C*jw#k-&p$Zi8LqZ$VW3*LHF)yQhdc-VkzQxSh zlFs3NYQ}ZWni3zND0j>}Su$M3a%RQf)A0`7o7dwrwEgKlftkEx8y3+bG<8-5*Zg}a zbk8|y&3#yvWdT6-02G?M{0nq7L%UvXH)c3<+5Zm#($ou>G}Wk1L5%&KkfOBhac;CY zTK&h!?=S1?UJiKUy$@iDUv{Q+YP(=U98T#?;<(9lax6aHJ^#^ZKaY$5yo{MJ+>f0% zJc#C+FF$2Cw0TXxX%0(PKO!%Oy{FZF&KB*0+WZse|53oi7tSXJOtrruv4NE<@0c?Z z;zLl9^0Ud>G_K0hYo#B-_Nj#HFNbF+5}39(Jo?KbE1=<*A43Gb7hcJ{+H|bilsrqJ z2ydNIKdwj()wAH5y*DbU9Ns!c@sOO85ch<$Yo7UrPkJzv?vgU5lzR&`vEq5waa)6U zdfcI=8wtI=>|6^p9H|dwJl`WFLNd+EAU8fiI7329QW>V?!&jVc=1hj z%)F5GzjFG1C|*W@wZU1QFi69n?<4Xh=nqE%?QW!L-acV1F&*o0RUVn?y=~TX_u78i zkR|hJ(O1Hy2ad-`kURi^1v&mZDl<68ui;+cyo(@yJox`R@qYz`ng1gQ8G50A6UBsL z8T!NH-?8ui>G15;=sS^m6&U{~Qn`5gQaRsIG&9F;C6FU_YUJ{z&L^J8ZpFM^st0!+ zw(%@qFAbj2bCJUb;r7nQIwVdUXnrD`kmp6ZIA6s*^m(wvw5X*4I2n^TI#F^-%4J#D zAIwdp;ITiRIb(WeRq(Yp!yVq@-#z7Lf|}2g4h-$7js`b|D?4dZMxfLIwe7}`S(*Yl z#DtO0u_OiRWIg8(ea17xNzD^vsu14xC+%x~mVxhuf)q4yx*Pp4$7Q2EL8-S~|6Dxl zDP{doSCsV;5^hd?Z}j!vsYyBS;sewP6~-^uzP{RrH&?XVF6}<@4Wj^p;?&ulK%hT1 z4gr71g@dUPRPj8TNeVGrsl&*FHoN4&6Snc9{% zR-PoW(Y+(1`($)^9~pv5*SQOQA1&$VwI6<9ixFN_0US9A;j@z&v?msaM@m(B&$MXw zK~9!+Gx#K#%Z-*;iO8?Qn)l`8T44cGyJd`@Cgzqs;)uhq&A~-!WS)AxjaLrauaZ{(eBvfXHFs`J z$i!bs$xFV6#+5+0N3*n5_e(bFg5>mZx0b{iZd(a>%753(!!&W(TjdbDT3&n=PgdM@ zw=s(~Qhdl@vnb#y>O9eV#bZKOAb|_ta*%#~G^qwm{o(wQZ}&Wcb`on7l;EI@Ovi~+ z?{B%n=kJ2PRE4yNq>45ov0&{R7F<_hj7yx3muml;r_-hLau5|Db#ZhbvqZuu&s{S8R zu^&ENOWrROydK0i^cWgo+$00ah>lrCZnW^god#;blLYEtL4q-2v;Y#!$ef~?9eofAX zh%(2;kG3Q?dKcJ}LPxbS`Kg;5-H`r`Pm1yn<5G)>O3ce~&XRi|^^RCoLQgJ`Rhq?0 z%6|X%^BI50@BAeeN^%_4e+8_(ny_~D3s~e#2H^PM4k(w~ZZ(j(8Fzi3wAPtt0>eAc zz$#kb?#FxwPVVhqXF6kS_BFl}!QHEVwR6-p>J&4*JKkV6@;xn`TN(U2%_s+T73%r6 zIHP<{kQD$jE@~e-kKWzS1bT*ChHITUEP;qSy`-4$P>wRT(})$8Z*&z@X5ss-0AD*@ z{+cshEFZ7cgyqT4nBqWr6YOrI%3~zyDQ|o#zNZB3eBSeo4=5OY0JNc|%!DL--KgNf z1wcBEL;h1Z*jQwq-m_R?j+2|wME#6vJ|I``)2so}G$$QxXw2tR0YYyxGXn>q*s5-K z?01j|WrQpZ*JoZa!ru~baD~YL30lE)49JSdIUyJRR~oPLAEM&l1dwj0M&+^$aB@h_ zYowDg&BJHqZoM`ZP`|)35_JYd+FygP!U`*c0F&t?NG;W(*b7|J**?p#1rp!9aYj5= z9CY)h(`~H+^nETdHsGGzoL*mHw5_$s-^vdZ#`mBOi^`pBN4mqEn-a=-(LV7Zho|zf zqR!s$+nS9K%mJe>zP-~XizUdOSB-i;-oz7}8#stKM1+5dV%E3sQ8A7+zRTVnfV}!s zAJ1sx!wR5u_rliyGjY{XsE4ZT%zUd=EQjZy;e@oZb%TKAVgPNbjd;I9i$iPpgeR2%}L9NQ0 z?AA+*o7?z)xFT@Nn?kx;jEJGIpF3aofCBoSn84K?Glvv=*rPpj1=8;HJpVQ6BV=u$ zeSkyHzYU-Fs9faF%R{6b+BzAdaYOlRnbfjVq-A>1(~2WOlW$^KmRB~!@k!UB&D7E3 z@eR%elu!r|fNeBuQCdFP5oWx`pEYH-6)y$MN8xO#R?fx*cG%O7vko>i0pL7bW!c46 znknabF~d>u_1f4;2vXQEdVR#&&lO3w%Cglnp*lI=XWhwMZ6LS25ax3>{Ro?K*nSb^ zy+8Q)_{5bx{hoI_AxQGJolsVoo+?V?(+iNwUGD&H>~1EJGO24kWW!%5tcj?y0Q$E6 zN{{Q{x&egyW^5pLxHc7)61c{HooN`eC z0aY&CT8*>9dQJpriU;00<~5D{_hIgt3~w6YKF#(&w<=5`;@E)90wowumP_;6rBq}+Z*PAncfjTXEiASDON@Hgj8_?&p? zz=^U1*I(c?A&e6T4o?moJ3BC%tR@Cqgio8=ImoR1@-{7JNs$ zr2;Zwh?C*@U{3n7=4Cx|7$@HoBNqH;a4ijM8fpwY0KLlB`ZKy8A0K#9FvHR1u6Qvw zRbhyak)IOiBh}jeo9j0i@|#mw*df$Y@hkR5zxbI+7V6v?W&Eu_dP79M@Q}T&>K@YV z&s0eoD&Pe5Z?``@K*Eyp04^DnsxYP`Tnlpa z`;~YH`52Ibzsk>)Zk7Z^2F5OtlyHqer2Lo$faM{HDYG+JO^FOr2A3nUT0*&PJ!+PuF^^aYBcF6>FcftvlKj3L%KXR z36Qj#tOWl9H`61Bm$>w@a+CX;kX(1!Ez^{zAl*YeBmx{v;)j%IaejPD@`nk$y+Q%s z_Kq`ulf;<&yuR2qsW2{JSH5<}b>F`XDoc0?Kgs8;jK@C->TzT10(lx#cR7)5BQ+wY zKC~qVzr~lrG}*B-VXOlwAZEpA<{~63(gc^>U&?{)^mwsqPd!m|gJ+hPeRIuAg@qkCgYKg{E#QtsSX=JnJ>`hz(;Z?bJ1a1SAU0-na< zi!KIQ!{vjvnXr>Qh|#P;Ai#>YutJo72OnZ8oMK<(%~D=ECg>0C8NWs{HY}i@Bn`H; z2k>x4R&uUS1M52J@vZJJN3(2)H-m~7+xeX(xUG(XPC-`H@u38UQ*>(Q{T;?nNSXA& zC}d|N=V}kIHr-N-U?XP`nrPlU_y^&wtXk8_&YdLSdM!dPUC8wv!|%uYWE2OL{2h!J zWp3gU9H|97X;CJeWB$MJ@62lY<~Yn3>xS{JM*ErdzJRe)|WCt z#@PbQS9rxnH?R%ZQ(mD*!xQ}zG?@<$XBFO<;YlZdUFt>6jO+?%HLWk%2ser{A5X`@ zx^4mD)%7nSuYkyweZ6RBGu#fNsBwkw3}^o^G<=@3FQyxdUuTE-I@6-dWNvYPh}7|ixqLw4 zAN+$H`18aL-)~ALE0t+b>r;N`YXr${?#O`O$?eSvRhyjTGGspF!`ZOoj*?aGOr^}6 zhh~NOB1#pdpp~KIHdA ztVuPJCV3)FaKj(#nkslV!D$D~3lxHR4f$4ecQ76l!Jvtsf(M})C-&!Z>vwGYmHy-z z_d&vW;JLOo4#LQPxXY~g*DJElB3HkyzkHPP@`8uyE37ew0h=CD3fFXrplaYIPYkC7 zTe2^Lvv^`~XGb=TZe{YQm$B{{Rt^Be!<@p8t6KMty*0zNHcj5TBH&3;zQd&=IlUo} z1)m{^aC$<;y?o!hevlBcI?2?OqZz@JJvM&0T@qq5$*H1if)@lI1wwrVN*~_XP^Wl< zep#Pf>J^!p-n4iDEe_hwv${+W6O2EhLFC8MKkPgHCLZ>OV20bzc%u9}7U)}?mcvC@ zqYl4$r$)Y4qJV0}YGW8TLVk3N$nn4Zc{-NM+8o2h_W;<(i3n`u3PikQ+a$t4nyn(3 zb2l=-nZePb4v>WJ!5R3vTi9g&M+gaqCA2Udi~&se(`(43OR{4(rI$xsKaP+@e$W zrWn7bE`k(5AIg`;qJWKB@W5Sq*q8&gTglhwD`CP|58U7!UO_MggnN~gqgNjWqP8r9(8nOj^ky%Xj>Q7E zv+|5L5+#^_FwaI!+>K|F&OhV2CkUkeUb5Wg%!ZK2C*XAWd>QLj?fidJn1egZYN`x4 zU{GP+mZ1FQUwq=1r4lZ0Uc2fU^;g`{N?N(}MhH<#PExcPmRDl^S#aDNv6^Pl0xlk0 zy5)L!uZ|S#LsE@SeZrna@Uz!RzdQI;NKDvbiXiZA-`=tLpC#ntYqQnN^@utgt>AJ# z_0aAQNJ0nOYrIX-PnOXgNG>=_`FZCRr>HSOdx!mzbeSnP6rt=qZ1_x*m=(fPs;R)r z2I@yaD0_Ao-khWs!SnF$In5JF3$Z-t5;IlL38?;9yBPINPr1!x&4@MQ4~e5tlH2aY z7vu|~G4&MjGugV)j9P#A@~`Owt$Lt(y9BzNHv}=_-uf{#=`aK_R3Kh?jUo$VMKqdb z3sMTT;L*Wc7gcPQaPJY@+gEV<_+)-jRelJ2!L{{pob_}bmTFR_f}v` zYyc<;pJ>&dv^KKb^IzZ(MI$)e^p1fBZkerod5W(V7?-iQ1cL74yzrx^dN2*@9XZ0c z_}3aH_Nk{1yGRao_OI+%#&6n;RzFPbzE`2&Q_)`ypy4OL0)_km-<1;>b`9A?%crTxV?)W(@?q`+#wbuE+6a z_PsLe3F@!TvaJDoB1k#Asa^0+peXU&xxDd*{8{(LL7uW$M5(MD)aYBG*cu4Ule@iW zwslRTk&CH)FxdlZ3Q;CY^Mh-njpzUT+(l}78CZj_b&`YMB_$pniw0h;y1F=~h#>8H zNezD&mD_0d6mmO8yC7x4uaXPlYfAX|zoYoWgXlu<_S0W)=`v=850kwv zo{gbCtni(ED18=4g{K*-+pE9AE5}1ZwY`MzYb)9)TEHsrvtIpYN^>^^zLtS|^p=DOt zaQ}xEeb$s2Mg)I|is414)*|Y^N#zun1GdDwEt{<Mrf^)*F3m6iEu-Ux8#E z#sbO%hH+009et%dGe5lJdbTqk1Yo$Y>FF!+P{G!I9VsMy<%nj=vV;%d60rFBt$!m@Y2?G zm`_FVAn;u?a$-m_xdoc{jJtFT&)h~Y&-5~Go*D&)=7>!A=CRKny`6pxH$9To8Pg?) ze(POo<9;+S*J23Q{Kdqaz5=v2ej};5$-cdy!&Gm>yuxpQQCq_J_uDxk2XHSMfl6@c zRupqFK!a4yf{#hi^za*payz=7wUKn0hpAg9Xuo@T9bt#7ydiy(c)z2-w8NjN+(tKE zEYJ2;9ei3F_z2UW`&iF9P_|aAuJSt)>?wt*tBlv-8p^xF+W&g|;Xg&1w?Ge`axrck zCW??aa=^*&3%s=Q!;;yIo9aW0huaL(K>iFEH^6WuvJgW&kmCY=b2c0gq;zR zb4HgruLYae z{MSnV*J}K0=gmV8Mq4wX8GQ%6yYKf(fR*w4USI$HMsIf<#i={UNr0@qaN6C`2kOW? zJeBk2ti~l{vLKfHwf2_?&Ax#voxz2E=GGvc_{kL~%1>?}Su|WsAJpcx?mhnrMhhID ze5u`#^r(GIBmCQ;)SFLz!5#eN^qHU{P~1I|tPQrg;}HIe@7*jl1pz(J>yvt*&;H!} zovRNUb20_R6qW%E=BPY%4S zGis5xW-2LqNEe9K-*CP5{ZgFRa&2;^X2eF4T`(2v&SaD}%qpWK{I)W44 zWKD7R52wvnJ=|emm1vtM*Ojj9d|R!JD?gk!8mjc`(3V@V3z-`{I$9_Y*ETQiEV0oN z{`9@|n3*>x1Ds29H)$LSwMi$!>b+*Iln1baXSH50gKj?$9$6@vn{{kCLL~Bwc?RRX zd<~KS7>|SbEK}gIoKL&$t;&H{ zCK}%aF6@+S>Ivgix+qWo;InOoyTx~|g z;Lb693f~hIxm?zc9ZmmTT`wQCgA{g(wDrnQkjNrgb2207LPSshs`J|=dFgs)DV%AnXr4LYz}Q~O6V-lWQd$K(L;ciL&g{|-IW*-zOUwTP~0Tv z8LfhU7lJ>yp)}|4Bc~z3e}8Ob<5CZyaC_%K9&$q)e0irBik}56xzoQ0eHdl*hI@V^ z$Z81S=TsRrNV~ZwaIYrcs3dJkn#=z~2!-qVd9v%2^2yc9N7H9Da&)emq6PITU0cCp zFW?zhb+kpGa7`85BaCQh3eL#Ezm7z+H^eUvAD`D9ReAL0RJe77jofRbXXSLP9v&Ha z04pKz3_vFC-e%9({mllJU-`IM2xrtVhOKBt5yH)CiWg4XU}B4iBMkWa#7tZG#RH4p zOCMpZ#iOt{MY_X`!P=)w+CcI-rBNjQfgtS)b8=Ms3fh$SUDrsWESx zu7d24cO}kbAn!zs5S>WpCEi_y5PQ&wdU8meIYW(`SdbfvB|qI$`l_j7iXgp zLnzVpLs%})-(rJeqX$a6cJVF0;Uf*3iF+e20lPpnD5PO5ABfmMgg3hnz3r0eD>KRlXs z6bz(sC%Y&DDOnTuS1gkDK0ma~c&W{sxJT^gsrGM6^}0au@vgc5a8>Ae7Irqvj`{k= zZJ!}-^@xG4@$2J#qpXi*micK{tw@jih*ud<&vfb%9BwSi_uLlBKE#MU2r3r8iJyl@ zfkyaP{Gx5spo1huFVm4zgD#26jKfeYcT$3byN{W?Ps3t5t%_hGgNoky(~sc)CfGGw zyQQ&i3ET?lvYbZ@q2UjTN4>g=LleS;{Zju~DZ0z|+zzTS5-Fs;o`HW0@&ZB_Ei1)M zEK@oPRH=B*lv$B!52F1LQuHI=tnaBpPc(XYTNgt_{5t44puzdmQZYNbL{y$)XT@` z0>XIlUr_~eql^4-@1+G}2uACn_~_FH&ZJz~Clg#_LetP#gw(Mr-e z9=i`0Z@UT;PJK&WOR44xAh&Vh6V6j^hG21Y@xs?SO)wn(x#WCOg@MXDal8!R%GQo9 z`eMmQJbqjs?rbK99*ZO1sQv!S6oZ=n^U^%%sd;o7U!~g{;`v&xx#i)LRbdiVm>Lb7rJFq;#w8(VwNNhYpJ2j?onwQiQY>=8O6+YHB~WthWyY^ui9Y}BC zu4#0-SFG*+QM+rnqlooDB|&d}*oTa0T;2-X6pQi1nbZ>32U*JdWy-}-X!jy!0*B-5 zhLFWE{-{C*A46Q;n_|7HZ0sUN31OBnWhW?#MG+pSbC|q@lp+=ELjSj?!9Q2qh@0*{ zeqqYe{u9bZQBD-+X*IeyWUDSCKY%I@DD+xC+Eo2HHEp$Qw&J%iQPm;P8xZQ|z`1mP5P5 zZWhp4On9rO%I6hGepkGix~$u{cm$;YYW1!_5bkay(_wGSGdsQgyI52bbB^9Roaq1t z)#~g%X4->Q?;Q7`BBipjwsj?YmTKZ<^IV;-+*UqIz61(-a7zRom*N`^b1jilLE%!I zSO={rNHVnc=Gr%$RWMK4f??g5{jT2HUWdMRl=c<%`Ptk1js&{S8IbTH2W1ul0( zX44~Abj_Ynlc2W;zTA&&4K|1}ievHb{HZMvvH=^LGgjU(Ry05%q-iWk?Ycjoj6Y9;M9nIK8<~r9^tU2Wt$07e z4!C?@9tL&p(Ar{&%|o@Go*L$78Z$kInO@2ysA07f<(gykn?ymcw_l@gCCW)M0k6#z zk`^t0kbacy{ccNu%Q3xML;HLDKNxh5Id})utC3*-eK3EfB<2S=Z2O`|)Aeec?^1ks zS5~$^Qf5zAIz<5r&Vxnma=UurW9xF_#!u{oHOQdyB#5>2YQ_Z|S^r-^oCF=%d){eY zKFJy7CVis0?@|0zDKkBlxsOkrpoKj`I6UF0Ogm_fgr6bhdReZ(T$4H?V$`U=3$0^L z8en)bIFDXrnO0$rs;ZxK@Swhg(9qdac)bY$ofNJ8a^>nYCYP3eCx`1(C}ReeytJtn zK!A&X^&G#(yZ+SUwbVD<3Z;De!*nrNtDg+UxcS(2LU%~CZEyT4^=$+ATsS9PcSm-u zOE_;=Ok$&4FfZc8PDIyl?#h^*qItEuEHy|~4Q2O;#VP#Y!jm`f)C-!co!6V%ZZ0GPQxx{|q=Q*pSmpuQ#kYWJQY9|EQ}i(DW&xaMzEKgoavFcp3J*u}Bkyd*WSu^a zFZzYJ+Vpx`Pkv^}74CHo_HLr^G`{A(g7{|eB#-7B-HvuxhRrMwx{Ah70rmm5ct~}; zc0M#WJ0KOwp%n~CUvb2DPIS374S$0r^IWN5$-bUORPT<_<9K_f|5QkEt*W8+dqM8J^E!Ml_Y=b4Z zA9ZMo_l%T3f9j+ye(x@{LYL(UFVuVU{;K|2jWQx@(toVuz1-$ERL=y@vCL6n&-l={ zuzsC@&05M9L8SF4Wo5Fvb$yrZ0$5z8$j(eBGOKgsX^p3YDAu5*zuIEw$v7iPSazr)Rduwcb)bI-1kvkvvnkhEp?@;;r%dw}H_%hte z9L0oN==R>7YWwdCNRuf2R&hYHN9=meGkIa~tf`FO!{d0C-dVpC#p(^Xtelp4^lk}P z_^JkkRiSoKg<8M!lEnuz-?nK&QYMEyygKC_LQtXjz!WXOvl!(4m;z6-)*jD)>3Ia+ z)Z4DCfi-ly^RNZp$N~wM;*Wb}_Sw=pP)>2t%Ko1o+_lH<@-4xnZjv+(VSTaBWiuxY zTLkZE>adW1)oBmUqQouYz~3k)UYhk&82+WPL0EFmBsv(Wnqrr`j$W2l*xtWwR9{C` ztS)EH>+TobMF;*k)>sw0=-WiyAIy^ zx^uhw;QCL_)81l5(05XhQ!B|K3f$l#?KtR!=co~L-AKZc}EF<7~hl_H#NV16Q(T)gFH_H%Gr=$QGB*N2n(hy0*=8L{F86| zLvHz78>E-93M3vRGnh*mf)&=Bv_h{S?SNYIF9SQ0JCmKHa&zV+(`qt7k;KEP6f5-U z@2n^We~!LoKt}}?BAMTlLr(3lBfZ?&|B|N$K!ZH}0Od{~bRi^%^JVBO6hF3Q18nqz z^7Ke)DS)O0Nv)Uh(_D~Ik#_Qr4!#ZG{)ynYlydmb;ipFAaysY^l9|&EEBOrs5AQ0Q zgr5Wh0QmT4({+JT;(nCi8(@JsE(H9)PgYPvq1K8nb1#?rLq7)H?kmCVBp-1(9CAt% z$}5n4aA!ottg3$V#!-RvPHP|hUC>%;`Ve6q6@_VL7;W9oi(qT|>%U!J;URWn{1e^H z;*k;C zPy56+#uVJMBx`jc7BxEe@``HrrEa|_$6l=a{%cTF&ubwsIxDF4mABo{C8^u#l6|2qb#?onJzU5F62-)`N-4L4|T624J;+t4A!bb_U z&WUrTn$_l`jO`+3fnP(~*rZzCGPfzl>C2DrUi?B+0Po{=L%D!OS#nqmJ*7EpODe=N zL9Fb4dwbLZo*cp6Kb5(Ir^ogWCH+1jWE=3$MUhN+sseRFic8`EL6~B$oVyzL5pEr1 zU-53P$6xB%GhoJo@fbvA1tupt|qm=GLo_VdmMm_KEQATbbPIQA%;}?NM{0I`-mj zn2A)dux&sU$CcIGhOqV^CB&l{o)6E3=W)GIXy3EQG1A!NJK=NJm@Kf@Y|5!CqO(HI z7hBN$QREFiM=sb9ng^g}P3 zi|=^|?>AOD!QupjMZMa#?M4XQRv_=1Lp!Mc$$_Su4dCwfg$feNm{;#My^$J0$7bA* z8LZ0R6vay0ZMKP4BRr=gB_4(6vM!R97jp;bJCf(?9RLZ+Ln7=ur6H*w&RyT+p*<2H z_1@dF=UUTtSAW%)s=1$4$E+%7C##pX(`$L2FBY@RD{F}+DskCpYVF^<%!}USAmaMO zrxHD6SyKn@P&{H$T>RAzL0sRbYV@p?CeCE&`t-PBWwDPqtFmzKE4TqmrcdogWNb~8 zW>;LxWfmbF@GDh}P#|dg+2XZ`NTBFawLw-dQ)l}EEXeS4-3ihg?>ffzQQ4UT@V7>Y zvSZ=q{9}+?i~X!?WYC`X;w1~7`BAYU8QO%GveNDs;7Q7?tIuHJjQEgPD{kyH*=MNv z{i3V*E62x|&9)-Md?w<8PLjeN`XZ|7?~3m1e=kUAid-UkG9O^R=vo4xB~_D6ir>pLZO-)GzkRG5uCq#JZt}ZWJc;KFzxhSax5J z%?5AX(^wliq-!{qQcu1Z#?cI(hT4H3)2jakM^rKhFr+@z-~)RvGI5+y zK=G%?#>z)<_>J87n@4wrC5CREX*m_&BRb_WU{KqHY z(xT6<6-UqD&i#0$ldC?+;AIZPqT9Y$s(9w28B&Yyv9##}`U`5rBS_iuJiocL$jF47 z2K0-1=!op4xXsdt1Yjtl@g=J_LOjaATR@_oI1T?hD&|>VYxE;ThVn2PR>@Dz3pTw& zseV^lEl(BPegdly9y?C7`cMG|7OwBNZ?IxWm&CEW#BmeJOItjqg;j|3;cjH322DDU zZ{vXKpU$WrKaN)uKk*+K(%fG#g78b)B3HGEv6g`l5Wb=NXMj@^glYtOYid8MMc?>b z(>Mp6h++3mZSYXo7Z{wZWYVJIy4F_R+r2$IS8oLU$dN{dIY{Fv+%^0B=_!RuZu*3z+BIvJes<8KR+88r=@e+_~5yO3D z=>kb&W`q5LSa_*;9mAoNAxtQrxHZQA_)rMj?WceQiSuh}a)`@_)Z#;IzMhp!&`^w| zM{+69NZTXfNCHG(~0@LEgf?0yrB{n*oMSh=ZF$i9f zQ>`8!tlG<-J~U1jfvT7h-VqsJ9WPi|Zntb*-Vip~=>GFl&wIV_+}vKWQHBC7^WhnV z{F2&aW^fYo-&pUSZI8KxkH7*89?A(pEsYBnak}~S+P5vv^o+C}C+wt{1c}a@!sK&F zL6(r-&60bqwZ{uzsG2j~%@NHZyk+bc!kzxa%j6uvBwUdBy#=*6!GzDw;NN30BaMF% ztVUky)W%$4zsm~j&{nSG)Sue0$Kj^S(>v?8FsT)j@2_dY4EMOzS>aq-7jRMVW%GJR z=h3YznjCi1l9e?Og>MX-wV(A=lJJF_4g)`kvAikQSVMd$Ftu`LLHGAgK%)k><7zcCSappjc$YJn z?JRcLpZO`!B7Ky~b}6*5dcz!acDh@Xa_ zNehac?g2XJeii&}YublbI9;x2r~;K;ndxLL$xSV=+|pr9#Q~Ww3PauR2-0hatZacR zdd28&)%Ux{nyzMSY(&;SNoAVLUi+}O7A*Jthbyx#%GfDz?-kr?*c))(z2CJUK0|2S zxBF@2=L9wb;`<76Fzk`^l~*A0sWOK6%m>TSqL70>0{c$6K37;P`s^)mtTgc__`Q^b zZl(=r!TQM41{bmb&y2S>-pDlg#pHVPM-?Qz#h%cyXqK%umAMkAJ$jPwdlcCmp#ps` zWRjRGF?Fu_?sk*Z7tEr-+dm8R(S{xknRCWD5_1+`h(#V}Ad#(WyQ|81nThQayC2z$ z*Xg75xSavrb9=gZAumJ7fvhEx;?*DY`0XVa!)^NyLhpB0D;lgAyPbAa`JjnS?NR3r z`T@G>Cs8)IYo)GFiX8IF`$C*b2VBKHL21U;e7(x!F)OBycBo@RyMJ`=T-A&}Z3=2X`<+>iC=!hlM8GEkJ?lrB1rsG z#z%`=F!kued%JhG#z`w}M(t~z`eL|j<<^DcZ%+v~L}z6PRJV!& z=){HZ5pA6*WT&xKWmWo%yWD`eO9ShMPpM;^K8OGHQ(hOHu;;DLkJXgHH->Tcj&zYFN%Nn>0NQ&(c!;i zwc>}_H4Im9jQJw<8;fynPiEUgd2e}#SR-4Aq?u&e<^{Krqzh3x7{}(AsI=g^J57n! zUJm-?u4w`2%yq&f#O+T6#W!a%&*au|i0vuwA%sy|GNWbtIUOZI(9pJhD>*{!z|3@C z*s>78XPO;+d>O>?m0}JWwe2QsSwk~kdn5zVXMLl_F0PcoOY>1ZCp0%NT)Hc1mA(j$ zstWkTQu-1O&pHm?;~#y+;^;)0cD3&|u)chL{hl7ZhX7L`ny1<3p#-b#+Ycyv^N(A2 ztbii?9f*s=sj(II4(R{T^yTqTe^LLlF=L&vWoPXBTA^ZQ>{*JaR78uSjnRTMX2y~| zWLGpJiZ+zW7BjMjEKw4fL5w9knPHaSe4pp}yVe(wF;d*1J;gQ}p+ z8OBco14LTg!x^8M5lP5^rFHjyX0B{qhKWql4HSgWQ&^6ZeTX8uvabTZGhwjgu@CJO zJZ~2ax{rG`!i=}lp-1wHZ%5jyw%rHV2^z_r$@$zEsLEkyNHM2@!l(^$D`omN6^S?{Q#+_8FuI=w=7B`a1};Q~scxEmB(6#z9S0NT-ef9*bo)C&g`5$l z@j8bxbD*J(uaW^BXU6O5S%TqF6zcnAH`jB=WEr}gfRbu-4XwXiiWw^E`D@~f^RZQz zowtz&_M_8>=eWji3ykF)r6IHL`e*h%Igm)b%bmKPih~(`pGu|Dd$MfLt)AVK1!{h; zr78wU zuQn-PLq~YPM4(nff~3!Mn(=-6$zqug~?Vo0QKaRqQl-V=a;5aBQ-O`qbnFHb5P&Bn&JOu=r)nxC&HYbwTs?2*j)@B@H^FAhQ$3C-4fGMcm84wlq18rhQ|77&E<_u0x&4QQDkv^PK$>)If_q;j=vrTrByWea0*A?9 zZ%NHLPfnT`_{qP0`ujrIX1xZn_a4ikYTX%pJ1wBS)ud16Ey~EFcd|lMS9pBt zT1@-fg8?F<_y#JQGp(mLxWKAa|6h5U<4uR?9pFuz^<}VJk!<8?OB-snUKZTa!YUt= zl<1GT#N6X9o(jYpV3NOaMOW{Em*Y-VpObqA>T+_|Hyv3h8hn&F>rEdMM9UOhyF2TB z`rPVUYR_&Wl@DIDk{@w!&F(wbkHX^sBS?x^KB8H1Ms0&Wi1y=%j?(GRfU@ff)BM*N zTaJPtQz`jXYbljyoYXN`qy`{;^;*oc@gHiY%;#+*pok5ZwG9#X-)QDKM^L@q*e#d9 z;ci2V0^9hwKQRPz6_BM!;g@n)b~K zMrKD7Kq zo0wkW(S29N?u$&PvKO~06qSt5c;dAqNsQ?u>ezb@CHI;4MSwX+kcEvDP>Nrt7@!k#ocTl%SMtCpYEYu%3nSl6;CO`n~yI~Q^j zmFoQj_9}Jgv~8?u|Clrpzo9Bj%V}}A@K&y79xCT>6E?V%%eV+sUzn`1Q9Bu~ z&s?@-iP&ri1$pkL{3cnDepwyAt{>b8P`GcJrZN}#cjhDfHiU$^c!a*>M0*`SH2*IWy_SL+jFP&(Fq0{ zrFZem%+1@(I8LVvZ;(z&qj_TgC01F6CUr-w;ah-#T>DQy5IeR9eds47K8^q|xVE}b zfzKmg9LkbgnN-X>OlaHFEK{)-u+B`0V17|uKT%(gRsb9_#z+c^VNpoH)lWI9;!MeN zGa74->|)ih%UsQmy(?>C9vSfWtNptflUxdgZn4}Z_ErLptSM^GI(qt8_(Q)pldlQu zW20}!i5S5H)M;;KIA$;5KxgQ`AU*b^_Ex8ZbQS=hU@CJMb#+P(rO1*nrX4M?LhG^> znJe?G+^aX7TIPYbm`*E9QDf<+0zXe0N)flTe$A-qCE25xioEY|Oo#K~tFzhG?V)R( z%w}}HusTEski!WwRJ~4DtVa)B50`$Q31 zV)oFk+Ly6!p8lg%_VbP>1~U~3wZa(;h#tra6$tDyCa_s*LP=W)hv4ROdyBbAvj+(T zIa(`N8MgvxwOnsv`Em*wgjM$TRyJPybY4B`fBX9h**(*aw;!MK9gC?0{j<0%SCe!h z`oJ%dDIZiBM(jYm314_PU-&UTrq$tUSDKV&1gvAV*Z}d(RvAYWM};#rn2pSp?bZ#w z#*_T+kFxh1?6weAxM6%6zrRIu8?R9tRcc$pe!BaubCaD5HyVhSZ-ZhCkTbj7eP07i z>W(A|W$r$8nUDdIRL=`rO`#96@WjrwJNOpMos3bg>N=m!U^VA#g+~J7i~4mcv zIe|;^C<7d1T2*Eai7I4zVa{^vHcC%ob3+{^w8g0HD=Q9StH)7CH&_X*Ms}w=FBg4X z_L1fcg%l!~Lb+@q@9Nn2^ZiW3DECs`WUkRUR+5kFXzT z7Ju`F8rY^lXu#=L@dzoQp)H3x!P;O7ay>dYc<=C92DrM~hrhHw# za>IU+?JO~rk#-n_{P39r&~5Ik1vjkAq%!qY?XBeIJ513uffhRmuaG792VEwCVBfwj}kf=39lCg@z2%EzmW3DWE zV;B9wm*R&WRK?xn?sz@DKfqhuh?|l^6LUbt?aq~NxpVA{#cm6sn>fJXbLJ|#O!_^h z4>8_A7E}~tXREW5l!?WXEVLXV9p?GumI3}vv*TY5nw)P zCe;5F*=4yFy3BLlGEtKea#9;RvcCQF*(-mdBU4lr^A=TMY}ins z_0aSU_Tycigjb|Q>=2@%=D2@j#}QZ_{ULu)-DNisxf5(&4Znh4CJuvmRxtnVkAn1- z?Ydjv9o`X6er!1$kU;Bw75+HreS4QQrZCY=dpiJ%kZUcr&V0eNC+mk#tkdQl-_nZ==ePUu-xJL z_>BDzuRyBfaL$_N+Em6Nf@|4+yc#DGi{gv9D|d0=K(aLJheFu>FR$)T-IqS*9sYfV zdpwO(rmm@H=f@zKl1>1F(riU;W+aw9MvS_ae0taTRD`xA^7B^k%K&#?4lCG&Dt~c6 zEh+;W5E|Y4_||G8zlbh?>%9n1o=;wWzlj`e)ggp=DH zU6=B0oIu}0U9#!0qGS7r*GEHY;!V{DEX`S;2>)>1)@f#Qc3GY2rvrQee+AoqW+aew z&?q{|h7|Cx?^Ju3oakrcqCUrOeltglg>6R2)xYzik}*+~g*Q*>lL+TiWwt-r0X3CQ zrkNfW!}Hytou;C#{2#Vbk4n9bCC336C)Y*%rD8UB2I&NtuCuq1l-7c<)&6TATU-z}t}kXPbV38LbB zTEHpeOYZ`mgp7OkKZ6vakEO?GQrbMorCQ}g3mKx^y?!fFVZ14?pt{W49Dm*nE#)k1 ztX)F)zijU=^eA#NR-;=+*d1dxVnE$>b1Fx!6cRj7eM>e0az2~4Tw252!H5S{t3N?% zA13q+82#?g|7>WUNh(AKpPP3RmS55$kCgjkwCh~7trS2+{YSwT7uq1OO^tn-{JkQ+ z01dM{=>=mk$xV>Woqa7OFg_qyw#dKl1f3w`$D z!Go=O$j@wl(zJk&xZG|8R>*TlN-9Ut@qn)0%aZsqz{{B2c{8`9?cVKJ+|HZzxH?VA zytAR_dopaG>i77Zare??m1H6&c!7X-^gw6RGlH@|)X2oy?V*ob1zOT^w5^l)+dVqX zDwv(D@S7f?F4nc|pBs`D_ZCjWr=5*o@1z}z^7TPAp2&vJs>&#p6NH1GYkr(P{h687 zud2L}%;SCfmghkE9a4*U;)eelDO6ovy**6suWxy>_|pGW;Lj-Es%u-i{UaiFq^gy2 z&ezlc{O!?4K`zCF`67=`RctEYk_&HV>EHOz(;05$Cy7m)tg-xScw~L`T$7XBK9z{y zA8P!I57Hn;OXYhIlttXD%gHE8bO5U`3fYhq4*YH2j_!e%^k@xSQOO%m3$4~cKXwdL zj(}x%-0MFk5^`sO&*xRC+8xt-C?8(qP()jXDjYrKWzn`=YfcPt)4sjINPMEOzc<3%-^>Gwbtya+Yw& za=n10yWT=?X~RCBk|$a`9FOiq4sW`37LSia$$8wSu(vE)iljp|7StAxxOfyG5TWYK>#M-Nc$jjzGLyLTJqI{xuFc@ zP{92dQq@iK_R@0xn(&m|%&MRgy9t=MBpj7;p=i%5w8iRL5DOSPFKBm&De|QDs|iWz zR6+H*rq^1atRfEk7NZL65PavEvY11`+Pj4Jl}5B#1?k+PNE@)UAilXvPjPy1NQ#*H zu;nDm?$B6jiZVHzs|08}N4)SZl)l)%4%o*Gr=2K{}4`I+;aVzmOt!ie|YiA4dzK9+l}(R#*^`7D2P9!Z8qgLq90*1hKmBU zTeasf_qMwrf&fJ-WG>N70+O<(3|L=HZ0dp{g0bSVAry&)Lh-*i_jbP_qSs$|!Xj4`DzqBk z@!dI)?^*G9OdRsw1IfXs}yauus67g49CbRcF5z z#T?_>F-^b)yYZ0xc7MqgaV~P-wJ{t(?H5%QO(5Y+AFaO6lV)S~3g~}zfaTb5ZZRe3 z;cLadr+5dZ1)AT!GcHrXGWyAVS;9gSt}%#U*#MZ}=Qxy`INap0SvrK`#N601J^ag~7=&djUQt0uwB z1bJ9*uc)`(;e+@}@PSE$XByn#Xko9NwKCyCn?|0q zxm1_9XCr`o$+ag-`?U5UB#x~H#V~hw8Khgro>n$*v_Ryl4-U+|?sX)lH=~LUX8!2K zC|&+k_eqVDPBGLSbM3mQ_?Uep>v?t_&s7w!#e6q_w z$Gccq!Vq1#!0noL~+XZqA zHh>CGMkLRlCL9_99j(i%sp9D^?w&w{!wS~Cavb7^Lp==zLf$j_0)5*@kV^Zk&f994fi+Ja1HZ^VSE*NPkd;-YK=j zcYP~c<$VDs>_&q2B9_udN;I$9eRjGZszX0AynaL~3soLt4_K?1Pmav=fYwFJ--zD} z+NTAj3PrfO9Pl&K%1fjalj4ZvL=ta04(OcDS_E!C&D&%bQr2~HPT*e=} ze)9^1^7%tO>Oo%$*?EV_jMRh@4$+-}wr~@|4l+6M)B1=3Piy7xor|3hO&5sWN)=K- zF}%OB2s zN{msNyoS!{hh5YNzFcE>Uv*hHJr5x>@bcr#7bA(p??QBg?$*A-&C1VDV&PT3Ey*-Q zQ$xkc5~Mx(2Ec>dP9uflvY-RqF=NO5fZ>ZD`XGJ4mXBNTPtCUvI>+fXahf4as={CcVj_9&@lpVUKWh;2AIre&t zh17W6D$Ny>q!OEI_F-~JwS<3^-Cu7e5Tk|N+nlydI>`z+IfBj?*pAqAIFzw9_Rv*u*?uI(lqAsZZ(bmX;mG0lsxnXdOU8~h%*2%I*mFFljQIkE}FT^?? zG~97I9L_1tZlg0DJThCDS%&8|U3NFpZ31LTLvod2Q9-nluBV?#A2(`QcIuNoR=&lqf|S3Bt$RO@K!f7Vjlm>lf6nS4l%^QlNPJ@sE2SM!~Y z{M^u$hW84Yqea<5eKOzn9)ZT*qr4Ib5;@*tTjf`_eh2sS{2y)BLr9yMOL{P19wyE3O^ z_=CsEtUXsvgUM3z|9XQU7bOR_N}iUp+q&3ke~~siv5JC4pke*4Jr%QYXSWu=It^y* z+hng5c_gm_kRsBK>o;ph}=N{-==-LH*^uF*dO&)VDJeKhqtyM zeVi`(aj7^5T?5nI5j!})?g=KF1;{4PgP=!|5~D|Q?m0e{h4-IN?#U|1roVr28ar|b zNVyUhEB=WM>rJ%z>H?^|^y!?(W%=a&Hf>=ADmW#TSj8-Q-TYO7QP6iB;H7BG^JF#G zOmoLTO|#7*dz@l`T9P0;Xk&WgVey5=Q)-NpJbE|IEajr?A+7~)cMrucUh;)hS1_1q z1iWRdREd=TBA%IMn6jHR+5P-^zNQOqtNxK@n0vWs_icK1Y~`-hsBLnRze=C^WwByf ze5;AM>)^#re%}$nkBLMiRNTVB+^Iiwn_@YDeaM3&Znv^qLlFp3%{#DNojK6n-MbF) zv<>^MbL$k&rRKBo;W+{y2i2_b8@+r!k{QaSE3-*i#25LshhN%DyQ|hnT|i#)9EkD2 zH%@w#LA5T-J>!n5)oE~#VpkO*i2;|7%Rk4AwYP~Mj>?>=j%S|VkJt{s`Zd%j>BoQR zcPL*d_;-1(|i`T^ePq~(`B35sqV zTR3FZ_XANuWZ0su1MMjQ+3Ywrxq&lED)C1w@u#O!fj?Pm&3dRJgX>?!q0sZR3JsP<;~tS1To*x3^#d*)}nl=p_l0oSayz>h*ERu7II zPIna);K;15j-4Q=+W<3vP>ZG5;dwDA8Om#qCSn>*o?9Quq9KBBwYXGYe2&x7Hal`c zP{GGbio5SP>?x2}&%OqXHoe}cNj~tGN5}2`aqpVVj~UQ9YC7n9t2~YJFRo{*R0xvN z#!-5CcYU*A}eW1Y3=80H16(_}z5kYwnF zeTWPqyskB#_D0}GDyQ{=5a=etGb!8SrsPX#YQNZDs83-itWRdU;K%Nj3i@0%?CWEs z@OLD-Fh?k4YwTKfS>`dTrg5R$ih*cmB0Z zk zVd#6@p{Z9b*VaDJQPjpI7eU7_cPWi4XIAZp!LiY~nhqh_uCeyc>qj-2jMgjW2W|V5 z<#V3Ws3Y9r+f9HzH;2>UA#3khOT91w(njO5p^bW7NetaE=A)#NsDx2>M59y4e#0&% zZ$iV6x$L{)EArHgH^>i=3r^|tt{cTqjb@CeI)jtgFJ=D~9R39-gqOsD zCA#6=q3I37-+CVcKW(4Z$ujdIpv#Mq`HI6WWFO9G@y^hU)>Nv$VI9Q7MC9>MDf3=K z!`va!ZcKDBT{Ol*cD&8Vp{wiK8_QUp#~D(r5q{`r9Rp5{LlgfSw5fbMo{cJVNAZ50 zSAbV+f`KJby6cBlzti6_P5tmEvq*tdcwa;q4{?4^@>2_#@y`bo@k23sao46iKMLL~ z5f~#+WKZ_P1kZ|yYdxLwXTl_j8|&j6Jv;Ft@97=hEu*fc0>50b6-u^>nz49IbwY}( z!pcUcBKz$J$1lh5zqo}IOXku{OE>A$}@H=WqRcCUoTy=ogt31^*l!2^(W zKjU>ib4%jSY6_OyW5vUUlT9yus(X7}=3akbZka~h#AbPQk8sNMoTT$&qMr;K8{J6< zc%?(79zZMt>woUZNJ5U0r(4Mu&^~ZYe3=;YXbrl0O#sLVj`=tPpKfLO92$RYvw1s2 zcz13=EkVJT{%Ya8Ad0cra%rO-yHmXf3i!77v(S>rRg}4(_A&LlnM22pdR3ebaL{aD zi-I}yOh*cvY6ez zA@Iu&184C5qz%0KkSvKvd949fKEV%46*RvkpIq8{=5E9y^r7-UxP-3cG5_3szYfS8 zbdg?kx_e}CJ)gv=e|<&$c`I3%@SOzJd)j2H1+qD}xAj^xZ1_!h59j$vMu+59G)E=T zh`)$`{4e3>KxehMVy6?1{#f|^2jY{jlwXs^!hKp4nOBLwA(U1T(htxP5pniVi$ENJ z5P8aX9bA$yw2SSAKeH%^*VWO>OPk!v<)-B4hrH61=R$t}(}AR@$X%oFe=8pOHGfr$ zxzD{D8y#wR?5@p41OBdM&@Oww{+@FHva5W#_Wx+DC#v+KA(~;<;l&R1Q3WC(5YKHR z21?miC)yl#>ak@EI16qcpla1;D5++3$fXS;Y{C=~R>9lrxD#SSc5j`GJUivu+qq-7 zo{drdL0wRQLNo)W=bt}%W93Uuu#;(Sc973jy-f(*+a#dM`~kXQOfFxC1KWR!M$Fl! zq}D97@M+pp4Vxo6==$qRt!LgN|9s`1=i_hROVq8+6w-H8a~Uj^-q$%f8ae_g*NKTD z#4d|Q=doS@j0Vc1AMD>`JPsU+zdMn(3%W10AOrQM$atdX)o&H#1ye0oZwTzLApj7Q zgEiqHGIu^55S9M;)Z-pH9E?KCu zSMrNl1BsW99J=!%ISd|MBYNT>^UA1L%m7Zqz>zw^Z)e5){E8M4};iCySF%^La^6c1kUD>MNa zJx>&Q{gl4L&)|=kCBX&GkDlKr*vDj8{_+(B4&ZD8xh|#64x_|JN|U|QsRf=;e9~k8-crtr~I8?p4B`k{z}Rf`}3dc-xL?R z5BL4nJZ?CsFA@C#HMH;&S;r-4@k6^L(Ag|M$dqfQy(`z6IAgnoqsz8(Od!f*?*z{D zTPjvC##kd4eV#ZQ$nS4-JGXLI?BH|+T|+!Y^>Gzi47qw&`%H9J_p9GrHE~Nd<8t=p z+jcth4%p=~sV(mDY~pjU$k@}J01(m3U;Ro zixX2P*?rOHtn8ACbPuJ=bGV&Csp?W`Q`u>%)6Gb9|>72aI`iJwQwoAIs&>C!|lk_jotSp?}lWx zkm#D;M5^2k)REJu88V|1E@dO%%?zBB1NT9tWri6ipozsvaeLZmIy{|#2EhErvN&d4 z3ls3`++|@MUOar3U5y<10}Pg2br!t=ve!Cl)mFYBuL{5djq_GKW-o*qo*PjG4D=AE zFW8^GR>`q^@`#t60b}fzsviKcvd&@AwHqh$;qjkpDSMCYjv_Owbj zx{&0(r&BxZ?^+x<1>C7`>z3G=Me*Qz;CCzun_5e*(E5V`)5W;XMA7jj2sSwTDkJcl z_PLrc)PX-2v?pRx8=ci|?xL-9GE`xnGSaH_6zIpzpO4X2gtIz-2sa5{UUBTy(lP&1 zKq0e*J*F0L6kc*Uc5w$OYx#nW&2i#vPG72za?-hAJY{_N|coLseF~qm@^?tHMyPIHNCKJ zLetsvt03oZr-((VFUlVIqw3VTu_A+KE69(;TL2JXf2=xSo}5*9DiwZ)&0=YtG+F=r zp3UO86pQ5M=to5Xl0WUIwB3u>rY5q*u4ciDddDD=<4^Pn8j0Y_O=9?C{NXNXFLS0RL-=2e0tO2LSB<#r0);B_S#$Colq*pm~|W> z-FPVUw1Q8X9Rmh@mynQ2KY+L&XV*b;H!N6dEunuQF@EenSu^_JR1-%w@x z?P7FIHi9DJOXv6jP63zOAue4^5!?goZXeo+R^Qh&b>5ZB=9YELJ>l z?i|w^jm3c<4KHNT9|M+y%&$IJ(>p>8?@f9N@{zyo6YEFEV#4Uhk=l67z;t3{c(Vs& z@$bdE1)1n3G|S78bu8|WfUp@iO)_X|5J!Uoal}WrIobmT@?sIcyc<`5<=t2ABO2`Y z{8lCaFiU8FRIQ9Ekz6X?#-3;OI^1cPIgdw8sD3h(ZpeGVYKHHDQ7zxQY*@zRJX$i_ zyN{2V(zgZe8i@oeUH3aVHy`JJ_l7E^fHAu}$xHNlw7hC+$#=N_yAxWOT{(R9s<5*? z@!DTEYKBy1hW;_&ha%E`nIas{pK=r@Up;@WAwss;IHK!>y>t(J<)=Z`!%@8nnw+2Y zv>qy6ga;%)#7~ThVYud7u z)kQ+k!|~ZCGPHfLhoUQ;=<9?CvTZqY0Dx zr!%fYlEf|=&=1B~1MI0$0GfUX9I{0*Sqh$Y>gDDNgiDb%{k`RQGfH`JUf$lZ%Abh-2qW`$aLHMsT>%5R1E(&zg&lYH@HAN9oL zW$Kmq(i(xqsF>MZq)@CO=z_9sXHij1=~daMZcxAQe1z2?7pHnW4CC>DeDO>lkDjtz z8Jh|8IYalVro4tn(Kln$uN(k74+xkOMaNwL@>W6gSlFYa0gywmIaaVwzHUUqm7a+z zV~BWyBYs^Qr&`mCQghY8IhKN_u?g@+K8xtu-)Qd@4`5$C7)qXsu{uKDiUEvrDNAD> z9Bvq$zv7Q9yj8AD<#54>u9&1cLj?aMvC(FmmOo0E9P1n#3y=u16&ok~TmJ8k2W*<@ z|Hdg7Ne`KD=+CtKFxFBLMmX`LwJPdSQlo9{sq5=kkRU>J^7+XC*k)^WdwLV#(~aw* za{_H{8ke&3`scR+r?PLE+7ii9E1>kzIgl>)BXJoAT)RL*Rnug;elNv)+v-ewP{Hhx z#cZf&YLO2GU_SkXZ#q76=f>?QPh{PS@Uy}U)JFh=B7l+J^w)sj&j`S^8@)S&|LkFZ zY>vTNiLJZJ6U+T20ATFVMiHwCONqWed_Cb9T`UI`;-Ct%Mt|y89NG2_fhTdfCUPuu zE7Ef~xVE935AU=Ch8^5BrW+a)K|vyO6T%}a(@vW))C~?we0=JepKNS^Q!)G(dhN!Y zcXL=G!1hhxS)E0~I;&_$dv85JKU1D8tTfSSV)~?W*NAI)XTq2M*-QHlOGRhjFZ4ya zReH{a+oo6|S=-I{>$gteJ2WXh^bOW6#?}&pTaZDqT)lTnSn6owncW(aQ{Q3JET*Bp_o90g5a5;I9KNRZ`yy4ZM+v zMy}$jGB&D8v%Xpun!tOiH*9wqRj1Yt_~c1qK*);rZ@+WjcyERg|Mr(_v~`U85#L1t z|6BoJy=p=4n=u7j-(iZ#eYC}C_F@w5xnKWtYv!c^7L}tCx=gzO$pl0819erdq@%<$ zIjka)79-3}V8s+>P9XD&3RIf!vD-w<`W}u~P{``T-}>aqe>jBg7>yD%cb7C5O>a}# zz9Df8CMzAFFZFu5;ppK1_(3&JO_n+$LZ1QQCc%LHb6}?~F7$%?#PfbO`-Sh}VOe$! zuG_B_OFF|JhdrN+Cc;qCcM5eb;Y`bph9Ny=~9o43bHaf zV_JdjyLA#{N-@l=PotfQy01vIV3@=C0S^$1{gP_rmdQF~|6(-4c>9gn<_ocfXLFP$h0nY_V-a0_>xKH{f+#g`OHcEKv*&Mlt zo`-`bZtIPQ#HeuA+dtkZve;_5-zw0a*qNhUqV~nzTRYZcWg1Y-W8XknE2DLN`Um(N zPeX#SBe{qE#fhdkHpN6luZC6tckF0(Kdpsd67nj@`qQ;iO5-|9LnS6PMi0e`CKZtg zP2FXC_lw^OzZD2fN$msyQ_yH}rMrcXMZ@o(VBEmmJzek1$MZ&-K2ogPk(4YDMrIC7 z^>+v#`0{Of5#X%l?m^!Ke_CVx{PLd5?OXM?kdw{cE9gknZi+Yv4estGgTDRQF^vU#L6U%2btwVZo+cUOY=J}4!-r3fuAsy{d0SU@PR3W`F|<>W=Ky@c zTh7GAA-J0gJ?YZx3=s7F7{E5(i=%ol_Fr+wHivw7x>tNlmHJ^v8@=T~RmFHB$YCkl zyJsN^7mJ(_WGjEKhPH`lIWl)~=ue5CccCINpMo$xoQ4vF$P(+ERiS-=&vfX5M!eTd z#=qhSg@}8^+c`h{qo*O;ckz-)Fj5!f;C^@ZG(m{TIR*uYMJLcI@EhqwTvD0hBZ9qE>=|0EhrEip02fp*etY z7wsf`p(czm8_NsQYKxEHGkq$6#^kdh*HkKqV}Bz-wrk^K35gZtaoou8Z^(fyvH z)s}!%agy}~0W>qK7RbMRu?t-L3iw2mxb6FAKc-jl7_wN)boF;e_VQPtySk4CBKM(E_ynXs-g*^wr zybOo|I3M@-jgI$>V%lez?Od|4xidFLjpim?hcwT`Lj;ISiM^sDH0~S=3Ye0O2+AI_ zl8hUi|6uV!9|MT6wyh)|F(x~{iB7uC0Ezv9fo^^~&(Tdg;(MHx>ST**3pr$DV*wk! zHl(HFc4H_j;qRl(paXfl!B4a4VN>T&Dim}-Aw7FN zI_KW6DXH%w-z!q_*>Xk!=Ch_yh77-)7a3dQ68f%W#K3BtoZr7Ga#(a<)lMPwuDH-s zdG-Bx1yOXg2~GGg_0Gv7>yf6oI`koyZ|Ie5l$NmBpSB(6AIfB_Eght10UWxRQI&*~ zbvd<}NzyvS#+?0L5F!xBnm#heo1Bh)%$Q#~!kJ}3Ei2Na?qp`oOz0)K+;mwSJpe|+ z_QyNGBwI<%g`onP&)gim^D?$Or*~s_pTbh4HCaamWHs!4W8;NEjMA3fX^Nme5>G9E zc$hQrrq1NiTihFxJx-7kfPMGro%C(Y78|U1oaKG?7O3vF@P$V|=G_pt))du~F2lZR z)=zoRdTT*DuT9IMk|yo>E^DUW{(=Zg8^sPA=Bs|J2O|a!S0k~gJrp#VHVnXJ^pA#+ z694VF`1F()AOSunMn4qm_JpvYwDDw9sUKZuyET55@(wa=JlEh*tn}PshVKp=vYLVnUW?hN_l8ffC9;PtGkXnsD&VRm^|JirJQl{Jqtu`_3R4h)_Zf46zhYPtiXD@~Jtb#c(0ntaMu zw}a#A6(QkQW_H|gfG8BU8Q7d)r820h|2ir)gY~Ocw&1DY=@xp!UN3QivMhFL2#tsO5+k#*4y!XBPyog}>Gq*0e^Y&$@N)gvtT%r&?sHUVmKiv}MghOp?7SJ_ z$DC3WX$u(?eS0o;01!)LrnbfWmr3dLr>+m~`jy6BbQ0~9K+L_s9eLKVvJBs!H4CIX zdNXkm)X;B9+{@%z5Wn}oj#}e@<@bHC5-0+QFm*P%S*~s z^gBcTBxWqY&z`lj&0rm`Jx1?2tFn5ekG{PG?p>%A=auF8$2}hw7Jt^Z{^tqRoTQmV z>hIj0Hr{)=>AD)OUgyln{fk!ap7@jI#eUM=v-}ZtrxeeC#K`ST*oBa8IC= zKqo&XX{f+1bU;cX6XCL0NW3g&x2CB(X>7W|cf^a7d9`eM`}P#bhSaZ7g-b4-8(N+VFE+th6?xuRZ{MtPM|=vRG_i-mnodM@2s5WtQ7LPIkhznldk{+^~shnK;GeM@m)I%DfFQ_SWhX z!iK(6B{Nx4(P1q4qn#_t|BBno|e8MdR6_2r?N}VityA) ziJGm}9DvQH(EqyND8aMkt-@z_Ii-QKmVRU_QbVQKu@)9bEqx}9d-^A1jBU}pmLW7t z3NLG}3=_Q?p)5_)t6MzK%WPTmm7S1<_){6v0z11yS76Gke@`5RxbG)o&ZD-Z>Btui z7s}BJm9ZjOm3vsY$FvShu2~EFqIkRpE&!o=LhMogh=C1}`!>>p90^R!kX_x46EV)( zv28sD`h>EDq-=0N{|3xU!Z*u{Vs3Y>7qj)p` zS!v64C#rt@|M40aRrSL*xE)A50ga-~j;!(9 zpU3gcVS5vr0$YvpuiRt{(v@w=0!k=JbZ_HUi5H@8 zf5uK?&_mbCwGXXK&&NzA%w8_w7rJw*B~)RmHGO?^db8~~n@abMrHfEy_2-H_cqVf@ zMyOuV&E6rbj9^P5#*v9@*-ouzoVe3=y`*G#+^xb%ef%^NQ2|pG)16~bO6ZAQj0?CcJn8M%)ojH?XMe`ITA?5ZfUvQrlarJ_8|Hk z6PvDk3V*gml&NF-H_2aRNf~QLuvYE?UiJ=-X;aKwvhO3144OKVY{`uAjED0@4g6W` z?PPwUH@ouEe7$tfdQJ&BmL(*eRsg6FmScIF1`QoLvDg>NRzOGYnTJC{HoB`XSaJcU zu>X&&_l|1niMoc<3pI3*l7tRYl@e+~7gUOXh*T>e0ldBvTp92yLYeTR=3VgRG9ZD@1`?b za@!AJOTr}aSC^sS?F9Gel0iPjrgn8U3Uv_0iQ6^xPR)$>e%aLC7&jHMhja6&tury+ zwvtu~)$&jkzD$zr>q=I$ z)MX2mXgIM#jqyUgHG0|qcbXzz!8?m&u`O0g-UTjzswSzQ?L1i23s<6#Y;4mtY-8Yr z;hZ-NR~RN6MKO%T5O7K4T}Z*#D_Rv_;vFvEHZ{FAkFrSGEqY`U3#kuBmerfbbX zyI7}vahYJhpJI3Q`@Kchb{;Q~@^{O*pYFrB3)dXeVGnj;7Q7n5u5M{Ue_kN$vd>~W zNHZSUA7gFIp8Y|Z)$OeSzgjYO3XpOy@yjXe3N!)VAyTsr34l-Yyvb*(+TGoBy+kWhM8M#yc zK>CnsR25kD}dMd(|$uzMbZqrPj$FG-wWAov6=0Yj!<;y&ee}JFi?yR?59=iWPRBy-f!Yp{c+>(1pelKyO>-|ZT70~hSxk_pBh=$Y$?{!zG zi+5yi_VR)fls`_y;Y53XMdEq$TKT}(=1^*0LJJmal>;I%G2I3n3O4f2b9 zDLcz5$t9+bSS(L=Bo~5nmwvQzzUp6aYRS}50{w!u__JL)n7xtz`Ke4a5FI`>nqfqk zfbFf?>imd^C_gu~VQKYWF9%O_=O3YIjU8L58Eh`}(%Z1&9_VirKm3i~85>@vN|d>a zA{sDa4O&D)C>`QAjtUfF-k?C>sXKV+C7c0Bbn)r^@tqv?)&)Q4P-Gv3_z`uRS?sZB zzM4CIQ~C8pUShhe)Q`WjHA->)o+Mi@QQXPi19iR7CqvU{i$y1Ekf_J|_Qi~Xon!!0qrG0emvBDO%e`n>1`%}FJxxV3&(A}ZxZ2xVGL}DHk zJb8ta#&FndLCW4bP;vjo%U#>MIB(Uvs|u!2o^9BQJcVEi?7whTZ~1n$Kj@9oJQ;X% zBA$_d3x7+a`-|nJNJBr+YG_HHg{Ap|u7t=gsJ!462XFc*jxaP>Z1BkQ#?Erg&a zCpY@Pl<#GhS+z90UwZH%?|)&>b+ua`mfy4tguTr@1WMK~IpgsdQM!F77ShpF9@<%9}^Ez3oa@CRFVLE1n{OdeyIpM=PHlh-VeOVxpNbq`~BAf`5U{cNt-RO ziIf{j9z)N}WyW3FP}r*3`T*7SI<_s9V=`IX_-ws6=C(kNC?B-mA3pX`qDm6sM)aFt zMz0n`&BV7#Z$xSFP3M@%E3}V=4m|R3x~bc3E7dvq1T%-ykEgN5ugtgqeM& z`(sTi`4mIM)T3yZZvH@l%1?EuS5@+_oe1a|lG9~Bb#gZyDVR=>x3l!QZFB7MItUkw z2$mE%KkSHTi!5>dtOu{~+Lq)P2(BnL_vTScE`K(og1B#EC{^40C$Rrm8~@f`9>m@E z1_#fI_`qpU_GJLB;vd$2Z4Qt@c#q$#WW9x6Xq4#jYjUAxpGk9kEN!}E^(|}hYxks^ zSP@3aHoE)*K2x?Wpyjf2il!`8`GG5vQ0nTpp%B??zh(?+i23UKdy}L1V@qL>qs5)l zB5JNSn7=^9Q@%9o@|TldnH)G{cxLHc97GqgYx7tbOO-hg^rsuh)E$~jxYQIQc2j>Z zAo7Kzc(T4P3a$9HO?K9U(~9toY;o5WvD`PNuI_R55Qn9BimEJ~Z#=mc8bc0{a#p|p zr6xh{fdtB=Q_ZW`K?o1kg}nM7_zA;=NoX)L_+-;Bqp##c#;Hs7&SuL`y&PJk1S3ul zq`t~VACe5t_HW79MLav{blm0Yp|32HyNmk?TMPWqJ#{BGIY-ZCU_6d2miuQs0k=4> z{H?5oGL|ED60rI6JzKM!<=jXey5z_MoNa5DrMy((!xG5qgbY;WF~(k7=A!PBw#O9| zyIeaKh;#P|(8%s*KY>-qq>#Kg+uEq29i0rX>i>q~vO8}!RI)|aSy_ntiT-bvgJybm z7o2*Bo<74x;gIJyv3@EZE%M2u@_@&OSZ%mcy@RVk3Fo*gSD&qjvI9>!*Hf$dEoZlC zN}(7OHCr>P$y@D8+>3~Ik{oD;}@X5$blktU#`4Y8VOJJ@z( zrSoaFPA0~?(PfbM(LGuyrJ4W7L51^+8l>$R7tXH9T)wo|Gg@ZHLC#NqnlUL`}04VXlnL|F3EXFo<-Fb5N2_ZeU?7Xi~H~u_H>%mp86ojMw;}Nphg^w$% z>XB0V`}+dNn@UexIf@WHt}+{_Qpi7VGn@Yfm?9dU)?I%W>5rWOI4}o$U}L|{G;QqG z423pJY`qTKP8CTUxfOc#oyjGCV#G*^<+RFPXBqki%GvS})|jEuj0u^Mdf@V8{h^t1 z=E4#uUpc++#tt9n8_J5NGs9Aw{{qZu=Tl^d|8s!=AF;hH$lX{OvINP3qPG69wkDKe zs#ysdrx<469$dzKcTXQzOSNBaP2hSersn%vjIaK)I8xN`>;Tv(XInNkb5^m3y}I!x zpx(95Mp5qlg&6Ygw3+0u?k@vq{(y<50>maD@rDJ(5%Y;I7+~S-`|oE_^#WN^=C6Go z622fCbZ*`5<*rDtT<&4Fu3X3?T8w9?9U)v@8VnEBBqTBRj8g0rRn%dHL!j3eJa{cEPar+gR<986if3~-#~6~Wk^3W275JR z#WD3A*QS&y$_=cng;`YAQvS1Nu3dTSbu8HvNZe_10N8DiDkCm>DI*f7`=#TDP)1u9 zsj0+hUr_h{h(1+MLUSL(m!(lZaxx0>8U&>{KzE3D*~(dKJ-epsGRGH|XHGq(U1Kc` z&LyesJtOCbTk6T3_VUO>HAL-DSq3;jM>3MXl@mJL)O~;q+wc3m zxA_>i1*r;;mH57F+@C9#0B}ly-EoVd5giYS)Kr=}5GWgg$rzVE40)OiJ$e!M{` z+S#gpf0N*Kq#Va$XrYG!zBkPF8HRIX9;y#hW&k`<)0CI?1+41?kByXp%KZW7FGvI2&8$C^gJ8`*M?~_=4&KKQ%$?G0!`U#qkVZ*Pum=tjn#IPP^k5@vk%v2s{}7v2 z%F(7uod#y$qL<%jm{@Pg^;7d}Jt;Qjux=w2Q6~Bt!vFQyXiKZ~GIKd-|Rl zPx2l}>I<0pcGeP*rELNQWbtF!@z+A#FzT17Q4^+`!nuN-P#AZ+IEeiEO{`>CTsO{F z5bgXfONr@CTYD7rR~%ZRgu9D580YpASPq|hshO@A%}~5R!eQ+}?i9ivNlHQpYZv}T z4E1(eHSwByOZzmi!HHm%tPp%EmrX$_<_1_&IC?vXl zupHRoBg-*Z+Z;>gp5(HFGKecflPXs(()#4RRWTALD34xAQC6;PqHx_Z!J5BWU*i;L z@AB0Eb1zA%I5TSLC4`%vr6Xu_UHb$_E$)nrL&5M|x}MF^UD9hPqkrJ}rBcnsXHu*3 z<af&DWrUz}|&~ zU=OrYK8dxOI6;uXf#9}FeVUB9rqThaY}5J=`ynnD2V}HWOyB(jbAzT(o8M?}Hw6#3 z<=9$?w+J&G?)x`!aqg0&ogNyZHs~x}iG2`qR`0nOD4BraY@QaeZqnJ?YBTX%}br zg`}pwD_q(~@O|`fNeY;;l{vGvS*c8IhMg@YuFE(39dASf`b4~y3GTQAm^yi@G-m79 zK&1uR3bRM;5F<4fsxmyq&At!(M+L*tH~z1&_br?$Q0 zGl@kGu?_V02@R1Z78u`4-b@2MH)LEyI3PHj*w4$m_YU$|;ezMIdE>#M*ihDoPhU6b zCLw29`b<(UU7b?f5RmLnrGSYwfkr)HbHl58nv;VR^(?0j4YTjS4dM9V@hO*p%YCWCz;8>LwJvL3K8@xIx~*Lu-`tyfgWMkSDNB5m;~jKThYW2uZT+BlVR$0J zzm#a>@w>(!AGU{3M|hW)Io7*YiAw+59C(e-ZlpK1S;pOhSaCStXiLaOI}*;j;P!`h zWD)u0dD{-1VJB(^J}obfV}d-@u#Jy_NmuT2)={lRl3_ia?&L`9(OkPtPSw1MG&)} z-an!QQRS5W?<)cPG^p(E`uMlz#Fy&J$)St^w^ZNe^OB%XYaBXiD1D-E<5Sq7bzqI7 z=*q?(;;CCRh(B9*$TP5P<=dRMbRs4D57A!sNj|sFWP&!G&L!?Yd7G=Yn?P{3M@^X* z2&?x~9TQ8yx2 zL1??P$)=o8!sXA;#;6Y_D3{Mk)7M44k-n)(HZ?^T`%Yn~#$sDJ_L45)^C6<2=G!xW z*k>aMPhE;`%}(lbX#fe1*oYWDgiF(;bb@eBxMnW}T$8y!MOl>VAyyJRH|g4U1k4-( z7|B(<(LKVO7Ja{p;K~AS!ii2zZ@P;{%CM2}T!7IEJXlVc&K&$KVAf3t=TH*>AzB0G z(*PCDTMwssQOKWE4T=qfcx)ud(|cCO7BB7q%ms8KxgN*&WK5Uo=?;dnMh00x9XlwF zVl%w-#mMyn>0+vVFP*QZr+yni>Q=#i=bZ-{Z{k*Cf8yH~$T_rcl~BJbg!y^&-Nla<&C4`$Xxna`kC z5}vEITQ=;M>4u+A!T`pnovRf7mFNEhC{4%DT6@dwRf5vv?_k9$r z>w1CA!SWFi*qPpNCk3FxZ$XwkAn?F7W8XC<^GOS9u; zB`72iF%vVUj0`^JblQ>zL4e+ErX>{{UnHba#Lu*N+y)rU-&e)c^8`QQH4zjNhKS3+ zC);X7>Jm`x1L&54WOoP=sC-nH$jgV3v%cK(l4ky@&~X5)?4}g=saS*C5If-ca_wkl z@bZV>=mYFb+%BR%AeIp%cGMop<)>=^RnfQ$FqQQ&z9vK*+%A%yMCSGrSCALu8kykwX=LfGZ7ingG=|@)tlu5ALzwkg3Juj=)!-Ml^jr`O3g5Av&$Z zSJ;Ue@v9u}a}`D7cOk4|M~jjIKzAmoyx$70B38}k=C8?!Be*z95|x`5-O!QJ5c#o3 z@4VAE=UnMHKg$E4K-@og$4dl8z+nE7PmPw6i*cb%7PfTA69z zrv3G-D>Di~%o6i(r{iG`jB9IqYyC2PUITRQ#51A~{}&mI%HCiAV^Q23tx#L%fIGl& zpDXat;f4;O!2ieHfn-3LZ&d#0@I~=6^-Y9BWm&pUCBAH=xPo$Td4ZUMVU+U_0!4Yn zzW*EQwli+|!@IpPK+m7-%WSkHJ_ZcJfiNDJ47}&k1`79WMMC*#lX%Vr+80UOXU~Rr z0&vZwP@#&fhj3PM26ce54&Qi_Vt1S6ard{OatVNGZG{7R0=gS}KBZ;?A=J|i)}S&g zWv-vr>f(=}sdSs1h6V2xW#WUe;IGol?m+lRQd$z>0{3N$;kgO%hE;fO9%l$6joF#s zgzn*I*AG*i8t6UdXvejrHaKTkmidJO%cb{N5Z_=>SQVhIkK8Wb&Qcz4xJHJ1X+Ne4 z6i9jxT5EKjOvDWyjD=!g$h$Q!DF zpRkC_se-wc_|&*z_T?Xy?dzXXw}{YG6rkN714vJ*Oy=DSGn=W$W^Zd{uI-lf%ix_F z^edeOmT(MRFWKvHw|o-Y-e1))RirF*lt13{)Nu~@EKt?;`mp*53RGf`@}p1Z9$^_e zc0Mwjc@ZV;U%c*}8!|LFw$#(qDd<{ir^yf7W{d$RGG;f=_}#VQV7%XhiEi@W?Lcyd zD%9%RLMgi?Bi_OGtTzQgsOzMxJ3kO<6xGuiI%M zQVE}irI)W~(GMjq$gk(4?p6}+FD>z)kpMWSY5sUdTy9X`s4_6PMGy9wK4hunE$bl0 zNS@X@W0&josph)n`fqsfGwRrwr0B&$R0tf4!@M0ETee9g1UVd=msw$X!S~$!QI1dm z1iJwge$a~>0;M2lD1S=@xQC1(N0ru^W4rrdL~a|JISy5!F(_lg)?*pJos_ic!o z&NcEQl2!1N8CQV03b~LXi&@$)h zYigK_6zKQ7+j)KR!i8k5VE!6=NxTKzCgTk55EAQPRCB#FC}2Eksr(Xzs4Q?7X>42O zRi$R${j#BQtTTXJ`s10n0`e!A!=hU7?<3U#SHCmfgKp|`*(EC~zSW-r;tkmJU zZpLI}twVBkB9(FWYCYx+R;s&7c24F_d8ykQgpukP@7BjI?o&TIk~dMouTvz3Xs1*! z+G#vUvh+p~QS&v(zQvOV{Ci|5yGrx&gm`{eB9jgo-C7r=0=y@RcM{v=zc0gr?Mpwd z@L#I-h-!w7M_)IVHZ}{hdWSBgwaal|T>Bupg&Nb5Ux(qAVliYc21R=gy>sxP{)i$W zFNX%0j{?U2@!C94TU|5!Aorm$k*h;R8#TaQTQ>=njRdxiCQb_!O#Bh{&U|LTZ#XNCmI{mVKMJ%;zU# zhX$A1WKw#NYvRJCbIEZ?a_XL8j+Dc{$%JI>F`vog`o9Bibqlmxa;esvnI5m>2#%0R zg@Xwp6j3!Pe=^C_hEly7y8L1_#_wCGJ2ewjV&q?uoa|&z z5uS3L@**Fu5-6ebezv<2y2vJ%|9)C1Dl-9a4-ShE?`x=Ibq5Ky#3w!MkyrZiy$Ko9 z+T2iViK?!_HwA77|FOI~62i?EPjFe6heQ&|yO!{H{DIyiuCskp>1-xe5}bVQL12Tb z)R-U#F0pZ1jOc9d+7dN+RUK9`x^HAz<9jO*uJ{Kyq72jp2Ja!awzW;RiR!N^oqFm= zE^s(Bx^LVw>qzf>SI}8ysfihy?zORPsvh?(6cnXcsZM=f5%=@q9pc!KU*(KOugloY zolP|;LRyW<`Q&8b3gGLF>j&$1P95Ou!DtqAQKAlAtJD@l3AwK(<${Sk5B`P5*`qZ^ zfm}8Jwi`8{p*UuO&t*V2x_7@uMa+?sjpa^N>CxupG3Z3zk(}4QI#KUXB~l0CVTXGf z$_>qNq>z;vkhAR%x-dZ7^314(M5PX^=G!>_>&Xi$cax*3djUoW?=bG8pG`@u=71&F z^bhdgopGuW?UisZLD}Q%Deup(mwi+O{@Pl5s>r#7GR42L= zP)e!x&2+(qsExY)P>!n)xoXdUeZpoh>U<(0uK}oIjD6L&bacs{cR*72amb7To`=%8 z%J`MVxL=k(n&kTY==9;yPw26-GzY9GN__k$oke=;3d%}SM~nVZ9Z?Gyd5zw?TKYSU z?cV7%ng9KH5~_VAXga2-U<%?;*7{4!}$mGOK{DBM?k7+3pU-m~F0Dg{_NOu5-X{SPq^GpVBa zRgm?p}$vs}5L*NEZjU7pzNhz3M z;dgiT96Dfrx9WR1=GMbjUs2R(6k&hwUExOHB~BdcLPj)%930Rs`fZhHlk9{9>Utyq z*No#6UkqA^r3?8dZO-(zJ3I$=@A&Z17<=Bpn?d+jm)WM?3B35_u-iAXi@xpNww-sJ zW{+_}+eDV5ZS=pgL+D!A z*@o5Ccf-ynL3be)vJy-BguM?zTS+4msk%x0ln{pHy2?1xnQFmlQ+)qTDpj7~Me zzB`y;DY6eB(Hl3q_RuJTh%)Ma@?^1GyF@vPR8}C8$Sp=PrqV8U!)<{&8qCXa5EPbV z8MYv)-zoXDQ62RIWbNDOWN&nY;8Q7?4sd{przeliF1u5WdH`)!AfgB%Pt=XW{70GH zHYe^=-j2I%>_yX|ux0GPSkR-MNtib8>ElcASJ3kZfH&Vz7>eFMDlfMmB+lM?P_7*3{U<#T7+vjww?f$~1{L`vU60j3LWSqz^Lq4jGUR z$hXHUjz6TH6D7*r24?f+RaGF@VX}gaaIo)mG-2iVbnZMg>|;^5DIx3Un2fl&H*5;aMmmP3e0oL4{Pt<`bI z!eoN!VfyQ4K=<*qv=hnSdgK_f;32Jn2sG}2r!-xhaNO#JAJ{z9YK5O_%}V0?Q4K`vECasPeK*F{bs?+-d7$RuW2+wES(&fv_5hI zbblAe`uUye8Nru7W}A( zu6>t)?UBp@Q=Hxd8SMemGnhy##`hO^N^$#32b6Fiz4jfF<65s5iU;DKJQvk$>7<-W ze>gaIBZYe+PvhO5YB}PQ{1xHJkj$t%7o<&>MkrxY8ax?VdbSef2iCRG(=dv9n3l7I zb307-`D&YlvKcB-IFx^+3I0@TKmcM5AOVW$9(t6d)Vph&LUcC%qbx$#Zt7k=4pB*0 zhD8&iWk#0?=^`i)VJ5kP4Va)V)PT0ykx1~SJdD>vSzSXx|khNci}CYYPpku z9yuXuh}!Z66v>9_OPM}CwKO(ZDM=%M7b!rON&IICV!C1MOM4#Z#8T_5F}*RQa=-|@ zM`gtN=cQTDA(_#4xxj1~JY#k&(@KREMJZHGwM$cC$T^l%*U5iX4gWj>8+fc*kx1v; zcM!5*BQ54{1)ri|jf|r)uXzm)Teu6BRtpNU7FkE#F&hg&7r0rZT;AzWvZK8Mf}07~iTTwO*6!}q@t{q9!h2=Ah7E&?s8g*Ms!*ZDtTgpj zSCLeDj+ayW`a>jMLPAaP_~fXus|@UR`()WbR5)YFzm2@V#8YFe%x2Tn<|81Xz`A^+I^)b=ut;v&zHFX>VkuOq zLBTuJ;V;kUKjZaJi{wJBRSov=TF8-G5hKn8=j9j2VxZU=osAQU^8pgZg2GYGs3fTI zLUYK?I3;eD<&Tg`wrT3735$S|uy>Grig~3ssUW)tPe%Xg9~e`1d;j8yKlgiS6YP%b zB`9srx^!JK`2ewNKjEM0nVj*<&x~-9%zE&PZOS9Abh7F8iN*%*J(Km6&WW?6HX=@t zmU`}(vbob3O;9*_?n!sVnV_ZRbA7XBsKAJXT{|&G`Sbt97-%cn9t#VSRI$ym@y6$E zxi3xd2cQ`vH=hl3^7b|Qo2l3v_STakOH*|xs|k!ONfIe{!?nEec9QNR`kU{ApgVg@ zJgMiQD+88OlNFdl+B-3spr!}t4V}hG-U;Z0L)WDMdbVu(4CtFwaP1-L+Rb3p1=5&& z{7Q;kL#-AV)$OG?WuBgT(4TlbtCw*x^%(U$e0rg$;$fCpJ~eX91*vhUn|gtx8VZr!!s$p$S+eBsdh537}* zg>~3|VfW&Z4HRKYJb z&I9%OyJ!)tc#jm}p`^fScTb8MqLq4o|JQltvC6b$5$2KYSc*gvnYYE@%UQGT?Y5wr z^)(&n!K5D+a;lKNWbsSxPBx^3pZ54}bL^XC`NUl4W({!TAA0h_0jT~7&ww;XL(*Q< zPr5f@>O7Vh@%vpITJg%@%J9|S`z|rZw(y`C_JwY9{$9wFqhDLdKL8;-E6bjg zO*V1pG>4|5_R7oQW3hP2q!rOh$3O~mDyjZ4E@W2Vk`ik zZbeHm@NbDXMKz^x#~P}8z|&(r$Ft2fzWrb=`Q$(n+uASsHFRrA(yO(#he~h4tdO>@v8+ul0>kW!8||?kow@G9BwX%>v!+d+mi3dFd8f$Bu&A1&IR_Zu1_`BpuGG&mE# zgXw?wA!uZJQ=(+@GN%7v$|1e{!xgCe{mXEQrn`AT9q@Q}&87f+Nb>dO7@gbCa{Aw! z1xHx9`xP^(x|H|zhxZ<*6dBp2t+k6OjUT*9JSui(4;zu*eCf^l`%CZm2%XEM!N{AZ z|5~uVUefCO&(NEfu=0ECA;+WcI0tk+&MgjaKNI8sDw^6+^AN3q=Je86zQGT0wn*^CTTz!`wcz`)4Csqcw%(Aw-$Ktph>NDcn`!q zOK(Io654Ab8Ck#GIbR9r?Z=}lPGBLr2!SWTbhq63m|NE;l)XiCR8M9!8$4kF{BDEK zOHc7O2h$1P&3i5A7dU{5>#}g><^b^hxu==G$wyzjU^@JqZrs`6&Tw#e4$lEwi?8qn z-|$J9?v##XZKplg8sv>yr#LX(%cN_!?<$`>0gN~>6`Kd-@CBBsv`#va{U04Bk_=-M-2OcFp(B2VRj3?B8o!>-0nHw zzVm7Xl~z#bw(UyTeOp60{6vN;_m;wWwcZ1arL7)LScE3=l;Eor>J~(rsI#^J_G9lC z`Uova{;u!15AEHm`1G+VCKG<1$f7*&im)bFkBAf9z%x2@&^$%F`W0nx3n?rgUYfkJ zV9d>(4xpL-Rl`k&6H-puyKZL&24aiY?wG*itzE!2pob!OO6UoSxJpV~^Whxr+TueH z0OXoKrD<+2?4)H^2dH~t=JduVAUg%n8zu)zZYY0Jj)AyHF98ya{%y=g^>92@00u#orb8!#J%`aXs zwx4TZAUziF5{W6u`Gzn;@}xlS<+!ks`(;xuF_C#$p1FA=<7$Qj9cET=J9)RYMOTN& z)Eu`gfc>$?0dt;xhH|!7)Nk49_m)UtaqAX8rYsK#PZ-<3rHCaUikec?yw#sx%@sz) zF^AaH0WYDo^NR?~OByI1pbsw^Suf<$IBOrW_Mh)Ff)>r8J%AFc=Z8=e$2* zqNjm*0Un9W4*){*C1xrCePSw=lMgqdxZ{K3|2C2{(+lI8;NdbvYO)m2QgX^d@=3zJ z6auIlEpaHY;RFRUeq4E2Aez@OfNa>Fl9wZVS0;I1!ZiXN@){yRnSu3_0t49-ZE1j}mY|cPT7-fMu(Tu+6AkN2 zdBBoDiFKEe$agNZ#|zVnzmd=~o(;b;+ACs|6Uw!%T6zW+SpYLx$~{PAZCt{!ng(hj zcYPrx=**lekczLa{s>R2B@7cOYDCku0H6!y?OGz>PJH-D`w4-69%VL(tyS^!_ik88 zvdn#oR4hnI9sr|$1%3xJj>vkio&vXYz~qTLcajFLf)JYN>4lQP*7u0k1OW@ULxixo zISY77O1586V7m|!0XKQ4@8s3)U5O>HyX&4z*X#{d+w<{jju`JT3A2g^cA$uM%8K!o zFW@C=ghxq9GS#;zg~Y3Hf**dwlAs4rovLIJd<6XDw@2;En~q4b%|B&HamRc1BLIWK zd4K|r;)r}%+7MpsiFRKV6&PP>zBb*Zq2H(MvDn7V07bYI*#O8=$~@D4q%^((N% zvRh~L7an;(?Z8S`#~lymlPC2u-uHmw4=EzdT=74-Q(=MZ%UNP2EMYQyW^E4bbC9Gr zeq=ukXtQWxu0>|>=7J2^)WrmX|8&w=Yy*E_2lvmw8xs64g!S-DabFpYvptH*lc($SWf0a9|UX5+a)QNmNY*P;CH-K(=jyvq`|E zFKF_-j#_`^26!@UW9P2(LwNlapfd>>`=tz}tLU;i32*_Q8aB~eqB5^TgE1YizSM{> zs^XFWMqd&Dpr+8xX5Z~VN0l!^fZty?(!uPD^PJg1UK4#B@^%weiTuRUOx@#{xFYJk z>IKvvxoEjBhaxKos~#nH8GVq^QY>nSQ9jg>fw;NMD*rj8?NC01A_0GMYtKyF zQ#9S{YZ>uYU{3G3Z6mJH&AqVPu3Obh*Su?Y!YB9cdS4$#X_zHL`~)#xxa!~Hg~Xty z@?d_C7gtkdB4G%_KQWL~rJ8X>*E;4@jH9Uc^fmJ&B&+P0W!&>LE$TIFZrAZ$`;#W= zoR)^Or@PHt{-mA*UuIQM)L3%N$u&r$Hcf)Ov8vZFg15xm+XSv_sp}I*^I;feRj|g7w~)c~`Pe zGEQYbk1z3RU&neQN!jqxXvqbWnwyXl zuUU7VT({a1@ILacy2--xz++jLJOmcwbp7a;NlGiSGl@j8vyI;l{M^Qp5N4B{)^)0z zZp!Jbs^2$9j2hj`%`Mu8m0c6FuJ>Ov|7iTK(QUB+F-ram-mc<}NHxZIAN_rrFXM~VMes@XLXP+ra?bID+kh1#dZb|CzWVH#n8j0l5l4g@r30kaAtv%W!|4nw}#<6+D z*fnJXKB|PPle(@L3*Rajl{;&H z;Zj_yj`fE;(8J{D8{Bgwd%P9-goCD7gysEu%Fq|t)F8bdA^~^= zqjez;h-EB#Rk461#CeL>o^>jx#M3k7U9+v4BjzAi`5dxaY{W*h0JR#c{=#8>q!G2d zb^Y~n*ame~I4l5ovS}&?4u+KV4X&Z6P>4=Y*Otqvt07 z*5P0KyC7OJsZ7c@?z`626KL;OcTp@VS(;=`D%*L`AAU<&NC#h6e)7nQ!0C{@-|PfH z*hyk?me=O2Nh4ZXrHgS$#mlZvzyH(+X~Gic#?Ff;-k~3xJIena({iqZ?KoVGeDL>A zs^VlPYr+k9yE~8kVLcY6|elrCn;^_D4&$^aKu`qw0Ii)?b7Re z2eD)DhDuBVRT?`1ABMoAB@FDhLs*T`X$`@|08Jyt)7SfIDZhCmQ>X6S8f(+Szh)h| zH!Ies9``qH=s_Qs1LK|571HVTS+0NNi~X(Q+&3_M)#QV2Te$eJ9|g$ZdMwt;IXc|> zhVPNvKZ91Say9e$Z|s-S5^Sj_E)tteQBIO#SYho^ij^2 zI`8aV2fTTq713+={mdvcJ?bV2~lj8!lxk5Ct>O*^-C3>x@@PMZWrC-hABNH za$QE(J#r%4`SlU7gi4!(?OfK#R?=z~v1bAJ0@$!M%&FYIwI-S-xbUZv9$6i1wQ_6` zaHw9c{h~fbkJ*j)z&JGL_mCJB526JK{9#t$8lokUAIfk{yj%sUpIZx~uu`3H`8#n) zk?_@nMExJ4o+yFkE9_rh`63pqU3eMo>UW;@Q5y$CeN2|}+Qk1}0BZ~6!@*;-+nZaV z+wEk><0RqKmiBP}$f|MHc>-;lN9*`KX-o!K&F&vvm1f9JH8$+n&z#LFw~h$rZ#|pO z;T--^qtGvi-@e~BFMENC?{>L#2lSb&qkO47kZ_g6KKf7=P|$Q){AG^u(mApMs`&*y zh%Juylou>4NbWpN($R8XytuO~-~EPbmlBpB@I?ZEXF(w@3P}Q!$*$!HKy9Prj$n|8 zHoqbZD_-^}mY1IZd%;nc_@TaaI-&V~aaECrUX{-Q4`)cWMI&&|^m_{4vz27D!(?F% zuzHMJlbQcefj=fVBdA`jc5Hn!diLo{V>Q4DDv(CrMcp5F6HpoXw^?iCqY^`X!-49# zZc~5Heu)cWR{;!vg{?z1Aj$Ep7zAk>o8NQDIgD@dAkPuAv;(`?SB;JgV*JHE=g#~r`f zjX}z6DGA@qWDV{^9jooh}Zhf0ZFP#de)XvIFr<3j<<2bH{LU) zZGVGAs2};ks&GS~j4qbj-3qU+f}Qdu=DY;9YOrp6N+-t)df|5|)7XtNk|>ci(iyg- zJcBNjlj46(kKcYjELt(}@Nac^^=H`GgK%cn)IGHE3$$((7U`Yhw|x=donAnWNh~S3 zS$o88BxU>g#dqZm<($(n@IIXohq|e@Z^ppRt?#I)H=@(kGl_!l+h4YF&NQ;z53R8( z+CDxaRXddE(n=}APCimnS zFsbW+EK%KfAy=m3bRivl`v05M1IvU@o5MMltY?$TD|RmVeyt?I(N-su19ceVU)_2s T&rs#5mLOqIS3j3^P6`EasS`gWaqQxGv zWJ#8ky^!qdFlK(fzyJS#&i{SR`@ENP@9SCabDw+fbDx`JYi-JjI*tMWfYaQ}$PNIY zr2pLr2mk<8VeLKu0D5a{b;0=Plf`1G-?&c!cyB!$j`T3GFw`|~ys9jJYP)%|w3uoU z-|{|HIn3}RecY5kJ6!36BrU|)_qyljV)wFS_a6;vMp9z>e!U}2`F%fT6&>wG`32e7 z5MwmFk;*0QyEeuIWe&&iOJ)+m-mbHMSS$kF-}>y(E^DD!Z45d%Y(KZZ?=OW@iVVGe z!`DqgL1^JdaEbA-s=Qb%Is1?`JCXRcp|Mm*^p0lmP5K}R9g1vkoAX^ zKP@RKDU22S^kwXB&#wqe@%qLCw1z5cY9&s&$4V8O>QJ~b`^K{jza5Jux+liQhnaDa2CZok#VC2C;PyIYptb>^<94=UuI?fnDCED-r!QP%3BGTLpb$P;@|TS zSrXiVb}UTulqU6lY?75>YBeqlvi5|7>k(x7wxFIM!uXz%i(^>rFQP|(TN49mXYotk zZSS-bd6~X*F#WDk0q1~htVj=D8G8GhkwBv8b9-(`n|0s1VgD^HjxXN@vvY8Xxd3Dy zo+o^0b93w*53{(jpK%tZ&ygyai|1@3YNEatP-PH_2S`SH)?IuhZ?=on{`;ro1?oAD zm?{mAC?>D1#f*%`9pdo<#bW{P+Jx=CF;u_H=PNE_(CjylpaT(xToRkoul4x-_~6Z& zb9t-MOe0CsjlyURa&mH;tK%&l)PfSClSUDvf5ZDO3pBqz8KhXHJINw4q{Zu(itWLU z=SM@OI?f*&R-L%i6H_JEQh*2DX#5&);L)1T?$@{5h-D?2u@JG^5SWYO5&cUVovP{Q zFf3(c=W`k|eXxaC-E7IdF8}|6iWK4^tK`z2mdoMhQ<&4xAFnBwFjzN1sMq=aIoHaH za`{?s^5JHN-I2 zqw&lDonMvEHYD$LmjV2R@jSN3msFEOO*_KKkyti;!)?zfu%c*w!-j3*Oyhc;k?aPV zGb2|4Jk&ZUPmWkh?yD3Pq1TdK7mJ%S&R7w8qmT6VT#MMOB;K8qk(SEzJ)4Y1j)1T@uu-JPVa z%jL(|fD<5~T&*77WpH_*uj9!y=MNqXQLiI8@*roR&S&&9(BK6D@bw0Tz;L3uRKJ{; z?N&Sb_Gdy9y>E9C%Tg)ut_u9s`U0qV=maees|BYnCsUFKS{XXFuImU)i@u%)_&NK~ z=IW;XpT-unwS6IZ&cBbyBV5rG&lX7Bv{#KDJ-qIyF+y76668}=cnFg^T2YR4!8OjE$(8aTp^>g zWu70v^$)|orwD6ctA|y-4JECg|MB%E>8&Wqzt3dzZt5y0*#w@7i~*;wyURk;fdqI( z0k)LxxsnFHWp~lhC_4IK=okDMf&hD^|8kez4~OVRkHI2&*woKr97Lc)g4y zWuG>E*UZ!*1T+93#Hk0cq16O7YaImj7u9n6>pDn*QP*THu=b>Ko!jU6;dLtcdzDB_ zTZ};~i2q+gfn<8{hrxfoX?P)O<|u=`1~mAjN{?JH-C>Vw+HY}*e%{*O6ol$}9``SH zLQ}6P9@}(YK+v5y&)jn~Qj-rL`LHWU4OzRGb%cdZU8D2)=RZ3mr>wUw;4?J~v(3Z0 zky4A#jcTZ-u!{tL5|>tkTkhq2RO!qZPl)m1UvvO&21B0qsmLwg0^D9?-k8*Qrl|k@ zIj(>6UcUUs6+u_s-00Ft>49{%oVgy1$m5H+4#l!bDx{o==tG8o{2FzZqpcnP^Tuh@ zVlxg+7i6Q}V2510qpMfMg00O^4)>35Hj{(~5;ueKq3yaJS=BAjN&oYx%>`*qPpD}a z{~EOU*yM^0e05^|FMUOz4|R@KrQCm9Rw}8mxXj*P-*m(?d{SK4mWmr;2F>4} z;$0`mb_VK=q!eny5>0LmB?>qH>T$_q@wy2kbd>Ayt!aTwWX<=sJZam`LBBt5WI&IL zB(*uQv!3!Zh_W;;tZS= z;*NY5hFio^_|9piWSi-LJ=f?*3pO*U;g46ZmA?QAasO`K(1PJE!}p5I?oAB0`gcmJ z%4b{pbl|y-1~KIkQN+e79Vwb`vr@X4#wrKsq1HpxF=eB`)u@eYpu9fzMhgBNo_Qke z34H}of=h;fbV5mO8IJC1t8RVsPrLtfkNMkCyl@jbw|N)zOBj(L#?dUq2JYN}aQuS^ z5{@gj7XA@t7i7FVTwjgw@@{Yj%Tjr(_-v;YPh!8~Cq{~|bJ)iOkt@5&U} z77G0S#CcLF%VvFkr=xLJZSw>PIzPOB>BAe#H)sn&<8RmnT%@~k&aeL}9%c5APO{Ds z_F>l8^FNtfhf*myWUZKPDQS%qT&D!bdnKi?xOaQf^n`#NQwG0cgD1|#Rf`yA zd|}u8&M8Ebg}5AUz~QxXRgyXKg}AB{2LCy?weJsG_oZp-^64<(`p1+}-pg8=BMU=d zadW|l^-6XGJ_lchzeIHJ({@#vJjFCS@Dbj2eLF*cX0j3)ZdeI3va%|XqO@^c<;8pB z;-nrvYY|G67q{nb^J}Jq?LvP1lzBMeb-C=ZuuXHgUb6y=D=LgmjMizrQWn|pF6JJm z2Psa-@u_}?E^ySLYGA3g|>{|=xFB}9&C?!l~K3h88 z6An71g=BED#PkvP`z*7y6ecA=@GIZt=xS5krS0okQ1+KF!ra!$_ry+WlwW8+XyyaY zjk}gB>}HG+-{&NnB1!7RgK4;F;MhPY=O5)dRY85PHECA$`WPa{;mm2GSpDYMwD2KU z<05(rQ6f?I;Jgy7=Af9i_$>&*jlKPGp&B{;;%rp7!5bTo65?_28TzhU>?$!`+-J`_ zf9l_Y%%jm#i({|m&N4j7Q|tj7LYabnj?trA9aFwd;cNs-Acgewhn`D8POD(#%LQp( z9-dsj^ogWV{L`NKUG^aS4QP!<B}K#X6XBMtusfis+;hZ^wvoM90^8n z326pF=cThuBWOHvEwe%%a{n<>ael-xy470NVRJYqP-b&@vngQkZUX5gav5dzeLYPK zH_b^b{sBQfoZpZH438B|F}yNI%l3iU9uD{Qv9;MzapDxGo}8X#%<4A$5gz#2QqP(B zswj{CMexz`k=(i5-Qd`|0T#|YM0iq=J z&EDU%y-hgjQolOpyPxMzv=BkuFwB5;b(JG*ZipdXXscenoDyZc2{@fkEn5BvBZf%h}OCc|Mv8S`wI{|N-`6I$It@Cj-d~U02u%$O1$T~#kFlb zaC?4dXUQgdw=ueCW|e*@bciNA>CV#aN)r>GT`gpWm2v#8MzY8KT3jOP5c)1Pxe-YH zP1Btp?x;veFt&2YOY257E6QIgWL0>jy^=KM64P^1l~9L8+bZ<)Bj|&PPnDY>iIds` z96~e2Zl%PJ_}}wQhb13ad>8%ZvA3&`Na#ujB;=pn@JC0}?SU7B7`9`L;aj+Ws{j3G0Wp~(O+A-;qcSxW;?Ou*3~E51h#|4Rn9>1Cf$E1dCMPFDdBmc z@>iRH?~D@=N#;|=J17w`$zHOWzp>Rb##PawgV~Ha=JOEAr`7bCyGuM82A%&dvj^RF zYG3ZNc$tMS1lesj`}R$G`gIp1U92imXIt3XWfu|Su(epUql~#iuGG@M)Zw%GfiHh& zZ-38-?Is22?+5?YhNfcJWYAV`vEFq|>6ij-S^uH* z@45Y|{2NLCU(w%^V6G`p-D=SBdW;iZPEmpepucIF-2%5yH8NreQ+&!2=gH< znQh6M3WRR)82r15jY%K?L?wbIn|J&f@xED63Li4=U}SBz(=z4<*a!jf+N^|?{OenV zn|(r`#|4t@_DAkDPDA4`_w*k9($TD)NPHg+VuX6!`pN80D%ZXXzOY~W-3IYkeh1M| ze1#0&n+9HDk3rdOh3eSwfEY*mXO3Mr&h&HcB#Sx5WJC>`s}2Vqnv#yQ`fkt@HcmZN z$Q=7O%*r{`y5J!QZBe373?_mdn}l;XLa$5e{#favD5`Kp|LKAQfRT{}7H4gK_sHf( zE75{DX~DH$kf3omr6vVJ;l5W{<<#vAocDFY9*K8WkYHqo#bxdT)k?ZQUEB$)pU(Zp ze(ruBrR(Af)C?aeTjA9W=n&;i`)nxj{{F#@@)H8HS6WOh?zzEdl;tngWj3Xqk9M@# z`Fe5WyddhSGmbqzH6h?PR;E4rW3}f*n^@-^l~)zDd8a5nSzpZ*QY($uNz;;~zLSfA zE|_ukR51~bnir!3FGx{i0U5)G&UW6djUh@z`r%R{NJQN{aVciBJ^KgYV5#cTI$ZB? z=jC+5JR%pf_X~U9jmX*+;7Dom%nGTKq1^02)=HE}2{+6Xe?H~&Y?>nm(~c)kvYPjP z??y)p<$`pM(Bxudm@oO7N7O;RrxclDdgYgz{)@%Mt@kBO9&?Hz?KY3&8(EgF3GX0j z=LV;faWXj63{47xpZ_mcwx3Zk6%1NT?WaesWxd;$YJdl@7)-?s)4IDLkcloh|=AgH6!}Wvu2kZIey`@+;La3K2DkH_oE9X%fD5M(|+Ln zMt5RvX2YJ#8O$%~C%m&4GT4T=E5-eTM>nvf(5){?Pp6Pqvht^DaSMrSS*qd>)EZNS z&QOsHwFFE$4S^<(qf2-s{$AG3^}O`78Iy};(3v?p>PG1To|ogI9TfaB<)ZX>Fx!FS zBmaS_arRyBD!#?cVD$;f;uZ7vh_vVx(688tWuMgRg=B8mxp&g?EAe;?h;V^eK&9&_BGYeWrnu1>59HB(NiQ#~l=aEPb&dm<9SWm9Y> zf_5T%{P?x(IZ9?owKXrYVwR_JeVk|bvm-l+F#_I-_~Hp0Rue*e3l`^aXcw_yC71J(D! zswH89YNdz3)#7!qeH$B_((=5(?uIAiB6Emcfq>pmJSTRxhFm{U+$RG*;L>03F88Nw&6;?Cv<&qie!zpLNA9tRezThaXy1}Y0?e%3zG|6|Mb>xWKS2Eeq5P1E~tQe z3#42nF&Xur)`ea1hOuZh!4B)GL6S!w1TYqR6-ASn6by`e3Pc8|lWs20dD^FTLOZ&{`eFd1KCxU_hRHdlaqjjv&0dGRxa~i| ze>->8FA#tdQ~aFlpUv~S;OW6TF(2{^Dn%bCzw4PT1VD=9y30=uk!{<#Vzv*wt0ICO zWI)y1rtoZeOt>hs6#Tr+F4nZFWfHt{g%n3&awx@7=*aGbWH7wUh8 zOSUhPWWo@nlLV*Acu0lFMZEdMqyr~2Q%&Y~i^3JbHCus6| zQmrzi|02lx0S>46(skq)p1@$tdiTpl09QrjFS0{HZRyb~$gh37MON_+X^lR!`>_I8 zDbfTt!dL^;iN(?c&XNiy7U&%6!{pIpe%u7NktNesUai^j{9J|hei~aoDg9V89D>kh zMUeia{3QU|=#Z+j*vfYY`#_sTLv)O`*yLJq!(TG8pFm*RGBtqDhkES6J`I-A7$B2b z8wgTX2Z@P7`yKglYJ%Y>3Dho|wopJ?6h#m;*rPY#oz5t!&8_fU{nzWw?V|#|wYel+ z$Fp&#*s4ATZTqnsY~~nniu{*|H8iJH2v}E`5bn@_!aQDqt9`O7pteDFZ*nSohBVcE zK-PMGz8FjbhDTa%@+f?Kg)|ogN~S}b9fj!a$d$GzKGf!XOF=IjA$Z|EOpEO?aD`p< z7|86)5;n^RVn(>HhO@h?pzs~AOXS5`8W#D*CNS>fIgYpte7$*>WaeMo2cZqCW9Dv! zoivx2)M$Q^>qb3On^HdpNl(9LoGD5;?r;C?rT}eJUj@AQQ?Md2+nLn={RG8Vfl^kDfGj&p)p``a%@+0Z=!-rWNo zZrrc`K7RmGw579QNdkm-$@s3m6IIz*;VqzsKgLgRN0=U)G1)Nt6H)W-+7yyL|77a9 zvuT6MRV_B!4nbj{sw` z(Lw>?Cro^$M*_n|gX2RL>##lhyncR(rUebd2}f8 z<<+s!Pl?YJ#@N)Riu^L3pN^xh0=(f8mWC%j`jJS_HbHYFU#%;J5Zc2iEba3(wL-LQ z!`^#NUvklP18G82WKp>#gx1By+xs3#adXC1q$LxgDu7b@GdA$TgRP(^Wk;6&v&M1A zh)#HUFQC<- zI!L~}OkV^&U;9q4U)Nlcu?1${gAKandr57EFV&k)_m_0keLKRx@an&mzOJ!#k~4Nem94>;??C$hB&$be5A*#>vl@msunp#Zp(Jwb#3;UGP)*)yj)U5B)7 zz0LoFk}k8A0j9l6OV(vYoo;-vv5yTZ*5|A{bL=FBNUS7)Jt)BXM1BtYEFvcH zPnNFpzZZ7I3VpNovI%}Ia4F-W>M&4KWVLLu7DHc#xl6qRO2hSoKE36@Jc3cq-D*GK zb!GFLsNQT9q|KY3<{XP(uV4D^i&fjNR7+d5IP`dWjTg2)!%6=1G-Lu~RATnQ*j5E-f49gXenSl{*L+Ad82{BuN zoSi8_$6Sh@fcQkcMd$~If*d!R1zP&> z+_)1VS@>>8EKaxBq$rAh4J0}B=$|>_^fTcmkQoVu1D$}ye}I>)AnDx@iVC6i9(C*j zDDId_7mAF$ZygH);Y!02~d$t{4$X8Y{ve(@X+RXlpxBvS!HvHa6~UV3PZRtu?Z19 zg>9v=4V?WQ4jAdy0u88(4J8T-N6rilT3FlH2D<)O;d z%m3&Q_6D{20BB^JKQKt0p1R@t2cpXX@)-rx(iZQM(zzX@ud}~&Ddx%@bng#Z!I4T* zXa)}iQJe-pEAC;ar77s7#I!qVsBAXNMcJT9k-%{dl2$whvmp&`Yl!+R zhi_|qfaY`HHruiVhkhc`pdk^5?Sj_8gZ8Zu7F4Pri3EVnJ#&iJ)my?&-wj^i5V+ab zlLVlEYFVQV=_nc0_J%ieP}~+F+|Bv7iB52@2!M3r!Jl>Keg8aL4Ana z6l25*nefX{6kVa83nvI%M|Jc*(#t>RN3vtT7(EWDU*CB>DL@zn$61p?FjpsoUrBL# zB}83}Lx4luOtWqrfdkUO&~8KAKCt=gYLRqWx1$beBFYP6P>QfMC4y{`$K9ewo}RO> zyYfi2Xe8ZAeH7g508q*Yf<7MdBHV!zMG>vre{o&9M{$S3mnp|&^l^%=oL&C|W?7N0 zfxcX}6nRh?p@?V4nA0KYK?|sY42aXp!ytR@6jdZiU zX6*;|SrFEL2U`C)u_qyRf6opgjPRqMKLPH4hpH(sT}d~Ua7!(U`kWYLLlX$p;%M&d z1J$srrsy^?ABmP(2Z9EJ)Bbig95f+>bCz@e=mW0;GozT0C4na?HwjQtCwz?BpS0Uk zQ8>(&CO?kGk*;X#LGFn)m2)e}5S#eo#x6OP@Dbn85AspLTTk;xg9$Y&H7H4{uKA8u z7oy+y!%32RVwtU|SwD|~&V48H3Hko*Qnr*PwSX_6#b6D>se?e$h39%@>6-t1{>VA3 z!@*DtiT|CM`eXX}1=)UPwx3E7oZLuA_>mHKf~+)bTuvcEq)n%PKxByYR)aQ=ZO)+-7>g5 z4t?)b;GHyBzH z%Iu>ye&iG>c)$aeh0`u-PH~X@#P)bL$?CB{ldYKbw3%n;9HK3@;a&`0dUDzsBMw|a zsy>9~Hhw&TZk%H3;if$SFGMr-ahfCFf_dnXf<;bv6q0c>GUGu1T1Q=LDh6zz_HFH} zh!dS&3J%vJoW~ty*cdSK(n4X8bA5~7t~3fYvQFk>^@igpD!Br@Bjdf4G`eRg@N?{F zcy|X+v)mc=^mD(TP94lnvd=K`rV>Z)+CCtqquKFS%sku6WSg25t01(OU$D_y?p~}o zhC1=xdWd|=+S!U$VopW1QlAyK z62-hR>(O<{`9x{n7z`JJj#Gu&+I$SOXR>zq0*e937>^7AERF@Awdp&%r%BZM7kk5R z?S5XMf1S}w5ZB#0*IJqI+4(vt{E58mBU|IC_r?59<4kA8u1`ia2fGs;aLk-fD#Pal z5iv5bWci;iSut+b)dwBOF5_0s9r+34_t#wGYxf8@1;2csO9qPuE%PN* zN0kM?0Ey`!U>Iwd>rc$(T(!a^I$T? zoxBiR&$vO&7|8;Xg;87h#Rdl?l-z>YeFk5D$e-TN6nEu?GJyA3rKitl2RPqyT@K6} zzY!Nx#TT(5^E4x>@X#1y+m4>yhF$zS+C z_r(Yq|2uc9W-oyd;@s(v={~gYhvtNWWp921aOpm1HTK2qbyJmY#lO=0nD^oZu&uqH zd~xV9DX3%|^uiXzBs)aMr+8y&KMCgL+Aq#RX~x7)4~`jh^8NH z!wqA(!GGI>_PlYJbQqZ1VE>Dd4=NSwK+ok_)Dpc#DW&jh0&Jn5;usPppduTYet z&CG(|`s~oTU+A|G=y`q&!w}}By|39NU|`Eq!f)Ln#FD}+vTtiuPDKcosO6$T(>DiB zXGAj$%;udJuwrc~1O{nU45x}X2{o#q(R0dXLErU~Kr(r&h-Dl;5yOqyZM`L}ZFe0k zPM?QPKX3gckNZo_B*mqZ{K;50eOg1A`W1b`Z*(NY|F%B* z?%aosf1S!dHvAHD5K^XJ_IMEe=%;bV>5U1+|NRG^diknw^A-de2gZot`;`foE+H`( z4%-pfT}ZZ+_{RN{L=rbmo>T#{ApWIt-~N0OY0HAe&F^y9J{;0IR|x&`L)^5Ax&XNi z&)&At>+Jq4O(0?;mLV44>=y=Bwf`ZJBU_a%JUA=t-WSJy_vIEIP#2hCV)aeU1h;Ma zjfXZ8)y{^)DB<{NcKd8HAZ{sf6Wq(wI;K^uW6;rcUYuXcQyEOqwioHO=xm=ZHyfXK zV7H}@Aq9bJ`syh5m1jMQ#J1lE>5^Mm5>J=+MO(Tt&i{H_oe8Lk`QZ?dk~S15H&x$3 zuho?AqwB|?bP0z({NFf7+TAplx1GvI!m&Yvq;cSYXU=Dg*1!Zw!rX~!Q2%sPJ-npC z%u1M4YL6QORLcQ}j6=9)jL@Aba9b-H;im@#&S~@3M73Ec#%qu$Y?QBz^_-z{h1Y`X zM3)So$+|nQ>Rcur-a!}IzJO3upq7Ns8E&YkG@b_SlM=3Eg%A$RuXbOM$mc6J-gk1+ zPNoxQc+6^yH|N=GeP7X$>Aeu%DM`#IfrD3Kyz6Yy2fM(A&z|5E0ooquTEV={aEtWw zHMZXu6Q3yDx&v_r?T^}L>wuHq`W$T)-B+1jr++5KXt&;IDm1q%MGc*-g5mhgK!a-bxDTH<*eVAspVo9#Lw%H+e#YM# zKFWx3Y~UZQvx|X({m4;{N>lvcq1+&maPZ=BBhjjBYhb853NFH&|Mmgt2Q!1&sq27W zDxxmk^H+Ja;knauz?so8&VUU>r@I8a>`|G5V!^{t015pEh zrl0%iadU`snI1PT!|8<_iD)0*u2r$50raXvT3@7l03!;6KW|`4@($y6-O5_1AG6Ie zZgF1{f8ng-`tyE=QmRQpsKNQ9Wy!%88(_18CXNf=PoedTPj=gw?)HD!hO9-~vt@rYSn3R+=L0o;nprr^eMT2h>GhN-F_WZ}ac~d3L3~TfM|A$~E(X?q+oQlhB%TCm zv766<)HSBqIm@AV#mxW_y2wYb${PTETsZoQ9{bn-;u7i}HKi9O3?ihHFMiF~pF1hI z;2HCKEuZsG;3De%+W2WA zuMYaIVtYLH85U;6CcYlkj9xJ$FiVbtQmqOG7ng$$UGQ%a$mgPro5NsqqdgoYLxnnxVy zya%6X4Y`_RetfUTTd})(|CFGSuAztvB3MD7%5a4YJuNX7%-c~=vB$aoN3y$+lq-<_ zY(X_H%JPXzAUEdD7aYz8Y`_>Jkcw=3)^eF36#K%ov|=K?Gn2zNnbd|pQ(cNy(ySWC zz^KrHR!8K*12-WTBfs(T0&$Na`mM`X%d5OGZdUFh)9{BSD3+gaD4qQ}mxHRFbJF&YSxs0yEeH^?~Oq z+R6V|hQpoCcPc@9V#OFBLa8?gTrajVH@sx%Z1=PfLteGuB>O3*AZ@2?UaUBk)iEt3;=yshHSd1>4O>Z5-JL4>PO3;)UU4l_)WI}2VTWu_$>Z-J!tOEH+r;Hv{(kS8!`-boPB zb)iv8_uh4?G%iyH!*gLeQS_rg^X=7V*^K)RO6M&+0fCnv!??dGBI2U~bPzw3vr_dv z|7Zj?_9ax(`ue73&Ur+M)ob@DM-?Of2NQ&=F`3R9xRuE@MSrND;r8^wr0j*PrMUu(B2qG386yeXf)`7vfT_16^;rxs={NO^KaqD9ZeFE)mBC$~ zHUT@OdeRf>TG^H_0vCs&>BMmS1z&!!sYJ;!}f_p3fd_ z*kJiESDm~xUr%@@lZ*n7RDaiQuu1ljpn+!7(Bx;^bw^jjGwdEnv8jcal;3Tp^)D@~ zjx(>ujMl_{P%DZs0VUz;+M#U+;rHWxC+-dXO@Lk4zd5Vom1k7&r&%7i;f$C$8}6#I zpxVaNgmJU}sk0ESgi8c7Q+Rc>2~?7;MR+oc6mr$)$y8Ol+H2Iw1Y6E3hoBLq7CHJH zJeI6rlG7u0iE06Nm$h1~{f05;@&+x>BLz>;w$i37_d#>B8kqMiR5T+@WI{sjVYV3|(Vwk| zC%?nBm&f)F>HLByZs9`IPHb}D%?qYE7Y10xQEhUqd5k~MT1}#`khn;b;_y|!9#S2A zUcnaLqn0}?bOvP>3JMC*qpa;}oD!#-7MW$z_VTm@`?h8&A#@r}&`dv|BHJFoBN47e zMDBfgjmm=A?^-k=ck!f^YoRge2kOooyrVvof|W58u-&@xz&TViDxY&8N@MN1Gy-MSc+{n3dGF4J9t9x&clU-w7jEd>POAoZ*1=NrgMOA>=IBH z-$GyX<_Kkm6_$@vlX>Ru4w3c=fZEUe9$4Mg?u3O2V6l`bG1`ph3$r08;<29ujC<3ay6>t7bFor zUKDXtPmQ+IcW(38w`ih{Tnx-;@?1yB1Ip<=j@kXNxid#eS4B#^ z`@L8nHJFibEQGy9f0Uz*AvI0LlKS?_dHYK{3|04fS8uDrrnS!44#f@-%0+>(68Okx zMN8H93wPoD)z8GtAeq1k8Qj>-5<*TyQ5H!&!=m4aDoMi+;PG3_=x*+hE`hV)8)$du z1o1t8ecHSg8_L-Ogja43yx+&*JM>hx%~O<;zeoDP^E^~7)s@@l89Yfo>L<To$u; z_h4Z&PtW|zx6|wB6+-AJOmYw2a*CTL42UQq5ew4h`_LLQi_@?w)`|7rsODd}F+ON_ z_!ZS7%qvlAf@^1ICMB4}v?$bco_7CK7O7NorWX4{=xO(rU{Dc0KW z9$u2Eae_+D6I-ooJ< zp5|w#it8;$)6@3iUq=t-xe(NdK7Z+5$LcV5>d9BkyhYK=DH+Xr zed3L&V;O6Cqz>3tlFc9xV%XOju-4YdDJUa9hH(Rd>DN0-*-m-;Xwk83}^|C`b;rx$a)0pVH-D;sA_< zi)sMH#Ha{c8E;Dal-!+PYvb37C`va!mV9uN>s@SU&c{!m=3TU|@Wx3NaDD-xS22pynN$eHaN;6zj;DVw!om-l_4MC77kUIY z3yIG@VMM_nSkvo~Xv2?vBle$Kf{0u9^b793p|U2qn@v8Rolw#n51E%FnbF>8l3p*? z+C@GwUX%kJmj)U6xZ*?|&X95~TR`v35 z-{{Wc-i?`eT)xNk9j&yL9Fh3AQYxF>>S{##!Vo3Sw`wd(vwhqW2p6t1Kg$GYq=1ou zM-->|Tk4_nS)~Ig@z;xgtzX=zaqTv#QkHo*JpAJpsoe%l8Ga!l7+M5sIs3?WN{JvJ zHwQud%w0IX0W#M7kp22fL^3@9=P8Fl&r;@b1eB^N#OF@pBXG;|^zvK6k}iAtgyFLx zn{}}V(Xq7Qv%&BapoIl5qbDZ$=Y9^v+BoSOopCvOu~@1l@@UQ3@Yd-N+JNWPobB`1 zP6v@{fH+4*By^*;m0+YKdNsT`TjTq&kZc*kXYkxX8SgIYod}^1tchR!a?S(P=>qHl zq8=ot#FPM_rVaROVD#kQR>cbKm|%uQWo>vUJbp`ua1?ge`mw&AcDAtcDj)>;lgy@~ zWjm`+i6ew>pZg`T1{hsL{7M56qH=c=g?KzmrqHMN-{1)T$F_BV)O<8k-2dnx&SDfK zNdMzwTYI7bZf+H}Wot~qNnA`w#LM>^kkVbfBaO>pKCg%6*oRIMZoVXlYRpxF5eEH* ztnWVwrrGeX^Yh;Uv(fYflgKuT@~-c7Z9a7Po|WHmiVZ^c3OHSuIRl*6@vp40vMF9A z*>1-CHH|Bk$BG`WQV_4 znhr*1{4b_3V=H(q4{@`;eCd>o0-wQk+sn_*jUNaIb(!4;3*4U5?NGlX7^OLNboO?I zDid>m>-HZR=ATicr1EDH?+x6d-?+P%p`_@j)At)| zk8}2A`|DJR_{@d)bp7>0DOQ#la$#H}+LN_)ZX9N;8scA>zsu1UD0<~&ogXpdY_sTJ zXm=m$Gdk=}AlwM$QP-m|yEB9C!EWru=bqa>l`g2<_R1(BAzko(!AyLmFhT(%Piy19 zsftmc@qWFA(UPUz6R_o=7r!u7Ki@6fao2Zsa&q_oQkX8D3L!R-dQLrQYPCNp=!KYG z;t`w=7PJXD-i&TCOdU&NiLlM^Fdw%n*7&_*9b;Y*>#;B@f*eXPcon0p0wc&<#&)_V zxoi(gxj?6zzHp}WgFr#M_Mg6ncu6zG-lKO<-MqQFd$8T>AlNLo5>pg&C)UkRCCMmx={YcRWP<0UrlhUtcE=O*ORl;?6tUS?`QkGE@W4iL5cpJg$?ozoRFo# zB0i*%{uWgzOc1>FYVX?Y)c=Y~91LwnOHv9rgBs^wwZySDs8@8kemXNe#u@toU_1%= zYQq?_f;c+SeBte>-Ffr{oGT~}6bLH+jupUEw_!H9h2G`h>~xyoC@8|`FZB>6>GSm% z_-}49e&POUBjSQ@R|@xz&-*HKtG-CXsQKd*`dmRW8xC}Z;jzgfEW9v{v~qufkCXbS z9f#fZNziAKJoozKiG^344}(DQL@y`tvnUVV>~SIdtXwL9+B#bG{^1+^eb1S9{2n$& zG`Ibra5)y|Q>Xf7FfBzGu*_2*Ypu$CJrg{Irt1Tkq>b`ZZ1*M7989kjCl4^wX*57qa_kKa3+vCfQrH@2v(A#3K^qKIjw#Trp$iG(b75Tk5G zQCS8hl@N-ujU|LEEtZI}gv!2-{WtH=_xpW3e*et5_uSX(`C867_c1f)^?IFJcar8z z?BGt;J1pPsooy47>i0_o@U3J&Tv6iNKaQ%D*uj zYv20gmSQa_=VU)_gntuMC`RUT8=ReQx!AEZ==Ht^9!uQY2nr1_^xEsh~g?kmti z-&z&g?5b)Yfh^vbAaKLYN(PI^ExbjHCbHgADF2l$An;iZAADJ+YEpt+ziH`-1#_=|odvP=?Qg5l0uUBAtw4PQ|?40$4|d z_#BeKs-viEOkc{9!2OsNz7$c!i+k|C6xb_ z%E(qG3OR^qNXxxYx-VeF2<^b>WnVUI?wg?Xv6Y$V#qfEIv56p>kfG05tFJIg*h7Z) zDw(koXyntTb}y#jsm7h{?$}$CC9;acIPJUZ#?6BGjxM3(OU4PK;}fAZCiNRI%wMa5 z&A@Re<{9d<7i2W=c#snXlOy-YVTA%FR^sYHfVxT52l_+0;>H z%UwO&#>+_aZ8cnXu6U>RZ)_iP-}o$5hhzy2tb+PPzesV;Fa-np9u$_5;OB+?H#3b@ zZ~8zxXw*Zf`i4H<_jFhgzePFWq^}w`{IleI$A^)VA5o)`wOQd$%vpC_M(sJ=$|!>=e(uZ? zHNpLF#SJSrE}7VIm4x+f?3LbX{+Q6HCfuuRG;CCc;hs5zy1?^s^wHBL7xC>@_e13( zwu0C)4UXa`-S8r>xD-}o+Npc$|)}J z{CVzCwWO!>LimP^gP8=G=b1*`XYmOg=3RIsGt%80|Dwg#Xn|tFbk@$=Wuofy`8ZSr zlItT6(|M|+NV%kI#0%5?Lfxi_ukVQ3H43yPeD?%_?1`O!kd(~=HI5JXN*}<=9GB#M z%y&CdxWSxQ-cYsO8bWjCC(Sf}lS-39N%PAc;*@y>zBO4J6RR~6>CY~iJGEm}-G$sM zh&c5mKh;F#dH(kE00LRrt@-rBGvWwQ$vW6qz|_?BNT@io^a9GI1ux4K2Gy6;71lPa z(wwP#rwVr2KUPDd1 z*b5gVAJA1e;I9HoH=pp$gbGzB63y_`7-IN7$0)TJskO0xHah&EB8u=1A#%b+u{FxQ z7U`qe`Zr18qvbnO?vs+c`{kcsSXTJM%Dnp*uln1WrooQ=tnl<5SJMx?!Cj*>LRb&% zSnRSNo;1_cmpzX7d(LA+c_?XpX%@4(2wU$}`%S0ZpE=hfRW;3l?~1tb>fPi_i|jXa zF(1v~^65!k{2N531=PRozBqZxRIq97t~6C>4~9dx^G;LW8zZrwHVD;Ei;^ldT>cpe`C2w1~sD zw%{`R<;#uRg+jMQtFwKyE#Ry*hW_tfJ>y$1n+UF00sINr!%B|G=D2u|l3s+6`EW8J zKnDCIJc{uB;Ke1K67$9L^PRaE;XM1fPZj8isaWe7-s9IdB3o1&w>AQ!ii*}4vzh)? zVF690mhZ|;Pxq5>;E$`X_DfAw1fgB?JCQ=iv@%U0)@sCQ&eTM#oVR>bMX)ldX1<3F z8*%4;@`iz7xZ$n}v5D)r;rXQmIK$n%RpqQlBf@3x`;+{m5N4#|HJsn()=4C?mlA;| z>q&-;SuqQ5^YZo@<{DRPmv)m=Ds*>uSLHwZS_dSn|7oA{GCY_wbLu8*Cqde>2q z<3iQ8)x?t^+WzkIDANNp5lWC5ItRx*k8jTu`grB+?ZxZhD)VowA!+4u8ht8M_CJlj zm+z7E-3!O{u%;&+_-Z2hHwF0hAG-C}tx0txot#LKx{STddAs;s^sk1YhPzl@i~Ihl zl>4CLPBNEtU$xDmmFM<^{BIr)f$r1jm2*5 z!krsW{A+c&9b8&`qKl`-Rk0CAo)b%@OBFG}JDWHqo>y`KY^iOeGLGql?SW4JiS|Qf zH57OyI;UswMP*dL*LNCLQ{xh3?h)E0$6R^I<$sH?>pxykuM3f$+lxwgZ1%NQg17=Go^ zI=y=qMSWyFDb9k4rzzr#+*rF@f!e!Ci#J1UQuaySSJaETddcL_zWJB(N7bY6VM?Xz zaR;asOH+{%Nup%$Y&hvtFf4j}Dk2Z_EYv?M(g&jRUSCXiyw?({MC^&=^2I7|6!2qQ zBab(TU2!j?-*2ggjMRepU&VT#eYwaK!Q77AswnOTKIhU>&q8Ri_?l*1io6!RSq=Blj2uW zwpD1m$hnO!Z;6by){ce3kKCx^Jc7ppLYous-N<^Cr@2YmhOz@JJ;x#!Z!E>hJgwO0#l&Xfh8KAd;}`wxJQ2A%cE1j!S~k1Xnj+XWA1_+Z#i^+90-Wa zglf?b_HMuYalEAW2qmSvW&JZtH@#=YJs1GJ4M^kf!KbA?6@ayj;Ddb^cFkz)LjV<; z-w~icJYJHT2E1@+4xa=A-k{~UyV=9a{MXwO2Fd(zO2-1XU5RPb*x~joE&c~bom=mI zGGym4yx|+++W&x%-ZQEgccbI^Z)Kiokx>n-<&}KlIBHieH!gV`7Q*5(wlrdSuQL~P zqfj>(htb{_1CiboM1>xHtn0nf77yBcIO?0AAX|uih4MCztlAM8MqVx+S9qXHnOCVt zruqwCV<1K;Pu;u|zR96ae=ay13xpW@z6KRkEY^7Kc6PP8Ep<4FlY1zrL;Ywk-i-5S z_N|_U+K2n}Bp_{a$X;caFUfX*qQzZT{Br4%7j6mve_sKTl8x zes7pO8tjmSNs5fpTrI21!ltwv9LJ*(nk+2&|6zS~f5AafFZdM^5RKe@X5qRmSF_p> ztNJ;ivAD!;lX{PYNY=@@61AOkL-DD@XHx*a^7-SjN1kgo`!Uz$|A8{r7!{n1F{tQs zyZ(jvpd=S9`C=LKCj1YXfToUwb+h06>RtXu zX`(_TSgOl{E@QV~e(a7^(6zdRI5z5^Jd$ap#8|B#5nkab&9LMr9*?7{?+Rx$lzrXz zb20BWbVmwVpq$KfI&YcC;kwp}chtN+{O_P|7`zYk5chw7F`c-p2$jC_z%KIAM=5hi z(@?aFkn{v0FJkuWzsGJB>^MB%#>`DPG!wD#!bkk_Pt}(QS6TT~(gkG}<6T7_+Iy(o z1o$$)`fgQ__GVy&&EvrRDxrin{DhGh)fOniX^IlJC?3ZBs7Cgtrri%5O?rSN@fd4< z@g-M1tM%jhI?F*`3a_$vfy(Zl15ZU@5o7_6iyVjz70=aC9Vi?9aq0!v2Z^~Zqp5kd zc&#=YDO%ruumL#5{C~UjIel?`E8@PMH=#v49RXf2a+f)ARxC6{_=EYkLp<-%sjaZ( zU-VUNJ`<07E9^wxpTa7&iNwg>!)|wk3#_iNZo!%y4w@K)vRu^PCNDvPg(JtwBYizj=Xs4 z=k#Ft&FIRv?v+VI*Q{Wl&?e{ra8JNKhQi9F>&wUV0{Qc?8+d$RV(Lfpj^Fm z$XA!EY?^F{qWL{Ahc~vm*mzKyyhQsmJ}L z%!kx?OvOEOkaBxNTrSw_!`rnXDdPm#;K!YCQA5_kGw3i37LaNW3iC9nnnuHoh+k~f zSzFx+hz7cx^A1I)qI9x7rjHPJB)MWOB=6uV&BvSVz2h6X>i0d#Xa^diO;<2q_bpCZ z;9+sJ95jVYTUN8WCBk;mQnT-r8skPd0^j|Q4Z(jbkpm@x&Yc&#k4QaUolR%`@q8dT z^(l@JlG613aH9H-DAFQ#*AxYvx*pnm3+81!?AGmS)IMbS1TsKYUMk$UnetiT?E`IX z4kCSZa}i<`M;rww%2q*QNbRw0iAVdJFW?-&U{l(3BdmSZrCsMJXj{i7)H7iA?g_W_5Rq}7I*;q6@Y3$_OZD`+{C7;Q5pt#aH-i1X0w3L%0YZ zQLx`TQO5-U7THfp#l?G|3S7PSg86OQ99&Vj?H)h=o+sK_9`K>b{MNi!kR$3$JBsk3 znM|B5Zaro<(JFG|&+}5*r;;A~BBMGQWJ*4*r*Q5`DVfkWZ+?1+(v;;T)fm6f81h>t zOT5Tb8{fqTaQyzl7xHK~Y>ruf2I_gtN$nn48*e7KqEk4nW2GecOjm6lr)-b!XoPd< zsM6TkGV*tBpSFdXvUnq&aE!1a!QaPrK8f6SaCliYuNsb!#y#0RjCq@^(vsHmi{Pb>d7-gEYynRZ z)kB)5xBO~OC!Cri*PmA`lzb#xNNIUJ&z5n#pR>~rUsUmnG&KbWIY2kn*bzU_XB3uC zpvsw-O3Qz8dMFd}+5L~Jne6eAXYp`}(^}{U;w7KTy{+v)NP({@{ypMJ&qb`qqNkN| zyXS+b_Y#%kiVcSKZCMV-poIp+UmxIV{mjMnoDUbGrFRqeryEc!rE0o( zX0cnS+vS%aq5yfBls3rHtMIGQg_3_8NBB&g|1Ej~;tS61l_!@ir)jD*x2nY>o67xI zkAdDp!$%>52E);i^5G-AO)=7i=oZey#&P%gXF_T(M=I8xnyeb!)s;FxIuXwevZCgn z=2Hyz?XF0|cZyYrR_KeCjiLRnzqIa^*p!%)G*PTjxXy$j=wl?GAlFd~S;cfhgt*zZ z6(6T@=dCY0X{f2WOd20&Lj&Yykej|2+StY{;?&F><6aWlM!~S} zVHXs3(V9=3y}z9f;AsI9(#Q$jT+m)yYT zksd!21t=Gdi(1+=@g3Ys(rm8P(!)NJK36qW^zJudIm3NZPZqG<9^J3VpZ?tsb#3sW zDQ&1h!SV$dXlV5z?_yKtPW6+W_k`xvNIa@Mn=ygW7=Mkqy=Ed&v@F$0GE&ye?`ujyNOO(^e)w3OI9^wML<*0%y^dHXi*KXB~Y;}~Cw2vdUoro(o7{?M5 z52r8MD>na>&baH_1k$Khdv^9cQazjJ^%~ymm^nqqMnAwln)-wZ;-$C5A@Fx(57 z@~zwL1-~Qv9er;^)m&qI$0Gn z-pq*I@hMT1I+tcnfW#Jv8tuXBJa5Gu-@r+Vt91s?zv~>Ut3YZFBygb+qKikH-`Z<$ zwS;J!yRuu{!UH@Pbc`!B&4@##5v8)_@4s!bXJM@P&RN6nL)5Fx!Y9WYrx$oxq}P{C zxWjcI$c(#i7h!>qUHV|ydVL!VwqBEO z$x^J)VEB657sS_Zj0MHtoPi}YjKxR$cTcy+El34yOE%P2Lh>ww3&YC@U+v+i5Ika; z=Up=>15$dYY^PG-s|{_O-7L|aRSb7S!*h1kMB07Jm)FB}B#5+9jKXB|G>7(I4mGslR;wSK$#nDD|?Uzlnxz4u0927QIEvhmNe-b9Wa z2*#l}nh@`{Bp4HM+%G?kO~-8hffI;`ggk9UcmX|D?TRt<#kT#3k3;5EY>fJy7pWan$vi%S{XL6|uZ8xGldkk!c-4)&&0bVp&(B}Smc9_0 zFy#?WM=C9(B#aZOVXRlm?-SD>ZaqCZj!l%A-g@-@YCi$F1^zIK?DryCDgG|5Fkxi` zzkk}9oF!XY6g6=0Osqj|W$!x)Q*r~D_r6~Fyo7C1$iX0??k?ZDV)du4t+MoK`o9tIUmW*%+#I6DlRnwW)8^}@Y-Z? zDH5n^VhPEDFC_fftW_|0;gl$~B;y*3unyvppVrT_Ek*A^FW*Jtx67g!cSX7W&1YIv zH!jMDLcKAw=dwMQLq_7zZ^a^JH(%d!KV~O+fpbPpKXQj|(%60Z?LO4rOiRSP6m9vs zaMo<{(KE*EglVDX@x1l9-TTymoZ5;w6H5d_v@a{ywv_LP>%+p>CHUveSEmDt@&DH0U3n!(fD&#>u(W_zj2%c!PMezHrfTrThNyG|MZ z6r1&L*~qqWrCHfsg%0Z*6}!C%A&Vs9S4!?N+Qd3gM&03aqoK{GK}S%cU1?#YF%8Xs zp`BZaEtIRKJ%-M^bGlQNfZtcCp#O~U@XQU=L;rNd?uM0VK)Oq8K8T*d);KS!Zfjf- zHtufZi9G17W*B?2QrzJOlY9GZ*2djG9X9KkiIeRJjyIobEFUa%E?Ud?0(X)kBLaUH z_eAyb6K0Z+w%$mVt<*82wjL5Mz2xg5N^ar_87^lk94YvASVp@*2c%$?cuV`)=g_PH)-dy2FfqX2-3C*A+4J1}V&|EY45K&e@sF zA8jFb3tn+)iSv(^M8l2!oJSPpxge29MTruGVRXSFDX^3H?x8R>a+z%tbD`su_O#{p ziMchNOvM{Vxv)f$ z4&B3&JI-Ts4GwN=Bh-(2q1a_3!IyIyIEn9-I6va#4*QOOB-jxN2Ca;FOm$B(TMnKm z)AT1@nYVU`h>fT^iyiyy{&3GhQcujNf$9VmZT*x=S7t|TCz7&YTP3?Llf0F#rYE!O zfgQt{42#typhoxN0h#K(rZ6fJRM@VEFLIt!;e0=SGUn)hA@`!X*Rgfty(Vhg3tn}I zCRys14L^2wS0kN^CRF(8nICFBRy;y?=XssJR=XSY-Vq!lEbhD*U;P8obtBvl!52#i zsT(`eUwlRcmzT`+xu&YV-Tz|?KcWF=ClUN?2y12(*c3UdK;Pa?9-Bu#`7z5`;UYYL z7&);eK$NUsljcF$?S)33P3AldRp0JHtUWS z3kRWtCt)W~gb_1}{oMT$`iI@GNvJwRISNdYqP`|jfZ1$ZiAzuldXn<+bsce}7+Ay> z{U4N1g=1lY=~CS#*~r_q|1$%b5G;?NDD1qgwfi)vqP91eI&TXHMW3$gjzgYgCoOt5 zZKHR$6d?nd>g;qeK5RsK-fdGs$u&zN}aIB6LWy)GL^ec6uTJ(0tfjp zNAepk)V!1uHkk|`BJ^la{HTPgzxuF)yc}dvj$~aiYQNM!Hco^bKg|H8G>sIY_HE9l zp)I^VP;J)c@DPo4Jypet9%5OFRyz#k1Zq+8a^Cz{_eRBy9!9&*_T`vR<T#}{QG4_KQ`n2W8wFUs5==fau=jESiX3hie&$9aU{_CHp8h)4pfJ;Aq z*fH1YijLcjc7T~(E1xOU7S&r*SMk_nrEujhPu8!L;=%v-@B@Vz8e08$O8BHTfGRi| z@npKIUi7Sdq3_@SdacKsr;-Kz_Sv}vNhACDD>CG!8$R&u_xj0rIOrht)g z*gG`;Whj39A3wIPpv&O!qVnc8xal-MbDtFuOx$+(kbJ6 z5D&;L`U~|(;Fkg%W2twMH)>b{dReNKxw?-RtD1#11;+ACq#J(z#~?_=r2Rgh&E0L3 zWf+RcqqFPe?D$OLB$Tg;S8Ky`VNYcQcCOwoic=#4n4mo6xr2Qz*4npn9CixsRfw7( zTGRfp$0HONu4{GoNtBH!NuFVG=9Bi;^j3Ulr|B2Cb zj(zfX~z#Q)_p zTqph6Dth98#H+c|KeVJ`9%!*-o^G(FE6+FzLVmS>zUHGtFy(ZKWNlMo?MB?VvRVIW zda6vJ4_&L~le5+Qt3LvO(bBP}Q|vb$n61>=IPvR%+I11ZfPI}`pjvM3L=hV~?Ft4u z@IO5z$q%k=7I0Tx3qeCP!!NlMCAf_*oi`9s6tXwSG%;y8Cjn1b&V2|q1J3`ezuXB( zqL%D?!2PCnzu=390HPlb*t)hk4%RO{2QOZlz@EvVXE6AD{kPIE$y;}#LfOwCJ_juhRt}7OW&lh*j`7iWK?$tdk1Sk+jj0Du`0o|eAENg z4^xQVrO*zqK@=pQ0)dmk9}8JSrI>JaJIzt>?wtO_Tf2ijId zgaPUiGPJtqK(w^hx-U-J!9(P@AQ0pe&qcvQ;-gl?L^U_NEP@PX=)s2K+`UI$WlRce z*LSN0IgJv(lnCIFd>1a(7qF^ zEoxLCByoY|xB)1YfWZ7yemtTSu1R$AWg9_!LBl9(`q{a?%Y2k@6$4U(02w3`0HfID zoBx)Fs3iN3U|S=>lQ5b(x_h0Q=;?0~)2KXfWs#*o&#go9WCFao?XawgywqBT!0zn$ z$Ti^%FdNIeEbrew-Zzz5E4lrrMwR{hfriku)kKU_N}%^=YWL8C>q0)itv~e(}($ zF6DuPV{FLC)jn<@5a}VjB-4Hfc(+lrTeJ`6Jbxrwx)3xD-&+M-!j48OM)X?F<%mMH z*^k8Y&3F3^5U9xKSXDLa5%|-I%HNN;(gx&*0sJN~2FTg??HZ(Z=Yb4_7?JV_$f?`= zt$=(CySMxnW~0REeAah9I9y~rm`XDE0hE>zCa2N{j0b7_ z+F{QI^W>^xolaS<D(<&82p5ICQb&*)lFb#^y0ka9k*RyBj@);WBL@ymSmg~F$kTW9_JplPHB zFC6>kwUug@z|fEThN1%Of@;_v;c^NNv$4>xhmEy37kCYXzs1 z|TZ`^Y zc0hH1Cn_N$!)=Qz_L6SoZ*CiQw;l;=m0CIu*@Dx!o41O`b&5v<%b= zVwG!|d`~P3PO4Vg)l?R&+W&C+hxePlEGOSYCn($FU} z>N8cYaoHxLQf$WAq!W7Zd&ukARcGX7Upy_YR;qU57K4i*!Pj~gZ4FSq2(4LtcWa4A zJNLd0iQUXgYls5Rs(Oq4@hK*uSBfu*YLS0wM|?HS-V z<$G6}PN)muN$Ti{*_=!TS$diI zo*yxw;kn?DW*K^{7lUXff&s}V-`}A^{JrqLXBQ{lhQlSktru%udG^Eu_QO@q@8K_t z%U;(Vn3b$N9?3dX!2IIK;OV8?c)IX3AhT@BOpXoC8lhzE^ovXZf43UXDL#T4Dn|g` z*Gg^_l!YVlPUparygg0)$L&IK@3#FAqZc)X;TvC+i~Q?=k&RLD@Jsp@pg$bTaD?lzs-uD{aO#x|@i0BuW=rjHFjC3%t4H1TGJ^l+wrIQ#VO_6g#`XLciHI9oCK2pd_@BjJD&S2A(@I(s>*BC2Q-yrv7nzQee2}3~-}q{Aexd%XlC79mSI_ z1MxlBCmJ0@h;|C^bfc65pAQ;v@|zbaazF-<5PE-xI4Gh0;%x6b^yb2EZHggx^uBD) zpPYsFp?@67Q=dMG0y_;D4dJz#I=zE z6k^V4*WV2PL|=(Ooio*g??Uv_A`N zeIAY1Prh0IvFHfga8UI8kx@ZrM#j=>Tqv*OSsHHM%@II(*cg4QxDR>FFJ|N#vY_)Z z)>R_EY8$GKR*P{ID3KB0G2UV}uq5p~!gOTsOu95TWDrMjbE6Q!5U)VJkBE17E)&PF zQcc!;Q;w(q1C0>JUZ=!MaiQ$clJ#A9Vm#0(ocAS7A^r9EXI*XH?zKfv@B#hD;;mIV zTG$nlcHp`CvyB9*8BHF0#wS#Xoi^)Khm)78W8&*A!nW1tA0Wblw6}PPC9;l9I81{K zQx+?VYVtb3i0_%|;)Y)ZO*JEa8ior%*BP&vRpI#D%b&9^nK+;J2`M!Zg%4v351#XK zb2@k}!Xf_i@bw{Xyj^I^#|63RXk75ao6bfgaRkkKO&)?R?vn(uoRbvh{k3wK0@LTo5T4V$C%%yawhr?7U~Rt3K} z_HKEAk;*{*5+{=1N8s{da7YU{@*c?Xva*C9xpX5wB`w`yj+t1s-L81P?`0*U4H2w* z;9vX$`#bk$4nwZ-!C%>6JpFgQDz^l?H4+?tnz1d*^YEiOB%I5k|# zcTOTVzHQeq40gW(pE>Cn+oV~W)!hZl1A6S;CWvw)S+KmR?x~j!&~3uR z+wL#8?b?Dy+Yblf^O9VbOQW*}vKOH<^tk%lcZ`u}{7E@UeHzaaN{vDbz7h$S?fQl% zD^Ka~YYmPQ4;o^XbCjyPk%Z~t%-W)&@oYlysccCi#>Eo#%BNW{i+R`MY!~LS~|)9}i~OoWF<5Wy>9Z6=~=r9;UPvhe*EBwr@El`&%|A z^l{tJcLzj{PLa5y4Z@p()<$nYths+ae(N)stQ~n_zALPi!^0k_eB<(yjUL2=Sn{%C z@oE~kZ;xLXW#T%Ya@iKJVY?F)9MtAdm`Q+Q6xr8%DL!ele zKhVbMbd7*Zx&}DG- zt;`mm`zqw54H-H)%U4Ebf8DQiZ4B|-gVUkd56w$E>@(}7SvCj|7E2K$hH|pQp+T}P zM_aNhCkgb~qsJo*F@8Yf|9sO=9W!nDVTj1xww5!dCF}!b_tta--V`}Ts zGYv~`&IAqSTxF`U!@`xw#W2S1m3gikvTPK^zdh91LOWLz&$Y{+g&16hXNvc7*gU2= zRP^1f(!W}SAGa`lwr6Y5Bmb#3ApGC+ElKler`;-;$$;_R% zQh2$Y+~BA7D?wJYHv3+PzVC?av5dUUs`9QYRKU>5Smh&3;p*QHf@6^lC zZdu|7Sj;8;-U|Asx|}3?w=6AALT%w~K&7&)I%?)$l^GQGdRlV*A zxl*PT@3~9qgJVWB);p)z!cJCuCJLeZv`TxlP3va<4y$t*P&?6iuIHY-e$Bi@oD)(MbrSIbq3-K4B>$F1rH(}yo#bs6AtQ`e%1 z{?uf#6$ts(*mnks%-_%AmKAl$*A_Pg<-eI$-$qKcRs@-sw^&A=p33)ONgqI-%Qr?H z)_YuwMCl5jYrL?+N%lXYKiDF>KWw`BTrkZ$mG%* zrjdFnK?t?pRrvw!>h%Vr|5w(oDfN>}5aq_2!k(ImuTip$1yq|}KhJ!j>VO||^7aN7 zD=3q0a74kU^in4l-FqckNRk+$Q9Vxll79dGM;eE#%jzNg;8 zZl?2 zTSWPWMC}>KVQg2{O33xf?_3nz9kv^Jgu{ZRSksVI!@MFm9ZnbV{4&kaV_csn&!|=a z;=rDey_>-G@K{xW^JnWM5WCyf#s*9b%_%p)^pk0jiIdUQYYck3?|% zsTA==i655Ks=IJeZE+Tu%3S}VP)Qt)kcXCUG41J-0?NvP1w=)^JpwaQ^Wx3k3n1(Y zCofOc`))C$&h5pY0{ns=<5IX@KK(=gYk>e>B6+Z7<>oUFgk1KO59?3;1QUl80Nv{l z@bmy&gqVJ+zin+l`~Pg~7P!_8_TC0I3ey1AxrchM<^_QAXLT2~I>yks_W@`YXnA9g z%44?C3E`2;KjA2YnN`rpcbWLqYws-Z<=1EuSoRKV7t%QgvB$NUC*PA;m!O^bR7mkZ z&l;g}a7Fjpej=v(r){`IV(zM0J_@VLKy4evkGBI=08{|-ECFOBl&q`H594R*IA|9@ zKJ#=-nz6~z)`$P-191I#O*`@efW5xk?N}amja9$>Gu{HA<-;0W@0`^NuhR5DkHkXv zU7cc_&P19>4~K8o8tE<%-?Ob^u)v!>L3s2!j1tOrC?F6GdqdP;=_iIw->kza7_<4B-eBcaG2=CaWN1SakPLpJ{k`W54-%+_ZS37xr*+u7zIo+Jg~`-uSAl76BjuIF^a z%!;LUx6G6`$zW!F&WNYd3cl~83_Xihkn4XvU&lp$4(~T-a_N3ToVu=;Y%S4`q_Pby zjDvEIOKya!$?u-&P&@&~h7x?=gYU}&t^PVlZY3OteuU3PNBE3S>e1EI^%mUhsA&L-iqgD_i z;`i|b#yH3kH*F~Z#oVOTZ)jik+6QPO6Tf)Ld`b@lek%0)c+XiB3o;PbtjwyHFTjA& z01LWw?>Ib(Y}OY-S&2F?umN9{fIF75-9w-zqnk1gx2|+aL25bJmC#PovwjlKU$+tT zy({gpe34At3!lv=5C|B*zIs# zd!~6JlTIOC60kIo-X%C9gyu#0Zg9Q=Pdle1k@5PJQB|Nn-g|g2qT&^~DU)CKN31zx z9?}+y!)h3qEV$`Ypkp)ib!8cG3DmdeSkh4&)ZmG&bxW}RM=;L16!4ZyH|=8Fjg|NM zT{mZFd{0D2bG%^ai=W-~1@zy2gr7Hky-o|ykgVrFI~I^_cLBs>tm~gj{jt4&?lgjV zS_dI%GXwvMt@!9z6Koi^5qW_~N*Fm74K4%t6p<@Yp6udl*~H6H|i3vr0M^ zuej-&hvtnUfd;{DApE&@hX!7_Q!LuosjoC(=f;}9>p)0Q@F(qpD%w{Un%08$YAR-^ z+|JT2{56GKbNyX9d;N#)9`0q7=~9h^pORs;(>j%^B*eb_O6zF+b25UG6LnJ@|GcE{ zQ>!G!IJzRi2eKu_dPj9^AW_dCH1VM6yf#H$!)kC49NNVp@Ln`umOX{YQ?Xhbn%vx? zJ6CxBcy7_+ZPck_s3Y;Y;+hA;P`(=- zl!A3%ycK99;t@h;p>DA?Rg5*5mt%AG10$bhB%_c0rD4P4iq1Q}ONfQ#OB{lVljlp% zt%mKvtn=@v_+FB(Zt*$OH)r3&p}Ed5C>D*$urpmmO97u-(#0=0h5YI8gMGknkF>=)=d@LF49a2~cz(!T^;(7{*Q1eufhIWoR3uJo-Jb5~2xSz2dP z&aJ*X!%IN-yi5d5gMk_}DiJ*b%JfFX;9rR?erB{AMb94Z1np23OkNR}oUYl_o- zA%dn68>{+3aqyRvxbtK1Wp)as$L_2C;%Dv0qyDFFjn|=|eM~uugBx9qeL@-a1b8-a z8bXvglRQaFt@|i;8Wy`SS2JK5XC$DX7pPI}sD%YHLC}9OLf`s15F?e3tEgxDJeSOt z`iPp_Nfg8nxfRY_^p4fV|E+ri_p!TWN+IOUk*E(rQ}L=yAaTyxNe4j*K}oG|grpZm zbsY!mE5*M6)^Np<`_$^IU;=MbhZQs6a;PuMok&d@+7F#GmX)j^M0w>#E-UMFp`W?9 z5`kMr1dI3F&5&KZk2H+fm;fgx0WENQtZ6kYifbbVH3K6fL(LIrb_Gpil)Ym659m2y zK*nSJ_Iy=m^sk@tSyrh>Hqv2DYT*t;4T~?G zD@{U*$4(_^-_R?85+x0%4*4O!s&O)4weucq9<6=_l;R#?hEaypA1IebYV&}yAh_X! zqq$*F*-=QOL~0=F<^@W33{i#{feo{e(%~gBI)j8l|T+ zYv1_xtivVZJ!bf#Qz0Q&BY^V3anA$yZ+VyZb|VuCbKLU?cyGB3tIS2#40VdKmm-Mv zi|W+8S9gMR+WIHA%QnUcK-VA}$Zj1qM2k$+LeKyge5}3rVGB_jU-0S2Cg#;T3P0L* z9w*_EaS_@swr{|_Rf12#d=K%pFBf8)J*dm~9XoLb)~zFT)>KmT<HfQJ25<|_^>>|GQ$+ltiwwsNfQ1smpYxJt1C`z8~9rxSkMlASo90VE=|48Nq0E8y$R92UGn#RoS8 z?KcmamQy2k`gq45pfDkK1u!m}NE7i#B7C>&!zvE-IgSMKW@x&+w?pXg#{TohCz8(? zZ<;~{B28sjhy>tIqQ-V)2#Afgl|8=@b2kds^2+xdD2ZvS7k+W;#;<;L^N5N6d2V7I z-U?ZOl-9<SGkG#2%lwzNkMhBsjU(p*94{f+uF9W7Qux zceR3znUTiZ;h$n0Xfb9+N6f`V3$>`^7UcVFLrFbO0pw!Sww<&lZhl2P@!Qe5bvW?D zpsbLtOy7+=YuxLLaj7ov@6#koL$=ZD+0g=^cJ7+NX!?NU4)0?%mS*q+c8lrO_`c|a z7hU5H-ioH*X?KKZ$fj%u#0^*oT*T-4)9-*Ni&&+ETkoVK8mS)S=p#Ob|LUJ~YP^PA zqd^qg?ro3u%*9ggLm^=3F`WbWiteD6@l6&6Oehm5gQCC2(8@$yA4V2;)}0WGomY%O5oD2DOLqRIYEl}F4)`$#l=iQ`{psx)XC7S%F(qjXUYr6tP1bp zP6y@{JZo@LFq>I9DZQA0U?R6x8>hq{~cG?DZ!~0bXJ(>;- zoHHFb`KA}IpPv`s$n#V5WI(m=lS-VAZ+}5JiI4qjn;WGH4dw3OnI;qhr`;7`N$O8$3j<+}thaAuWtuB-L8nGJizRT=N};6pm!B zNy;W3>SQ%g_VLLt%V$KA!{DmJXelRJ-Q6??-CSwKxb>%(b*x>dsKqnt6mSm_r$zTA z@{&lqm;9wFEY67X2@=+?DMTR93Nuctl-uY%qou<(G;jW58U6|&!8{Shem)}m>_yo= z=3#{Mz1aagcgQZM2>puHAbm2-z1qp^3SA=+A@$U-9{t^^UG+MC^ct%hz6TlY5u8i; z9CIC4DLRwqoB}RLRV?KO1==RjDR+%<*`Vtdjqon(W_Spm9}&F`tp=m$F0|ed?b*TT zdM8yIEE|;sBa9~U!``uWv+)3o;`L^TA<8EHgY?~?HI%y!m{(jq0kc3rKnK0IH{pQ@ z2TQcwa_^H1v4RS$Bh!RhwuJ&=Y(@YUbYySO;~=SsPEIhHP>tz=iKnhuQyzAw*)ykoV6vus zVGanRV1RqLbv;nWUJR~dXk@GCifW)LHR=_=5-!>$KSn7mf;a!6VIZaHl;7Cp6-hu7 zs5gK|uU`((U?zlJ@8%o)>v)fR2s1{vT`lGx(bAAeY-TiZ>jk_1nY62T6uwn!BhC-n za}RdPl<~8l2*d*TZ!Jsl$*;w}^dA)4J86rs>!%;jtQTzkCo#Mn>uZ;ItFQxo6`c9u zk;FInH%fUEO+&4HOi|tX37{GI54C>r4z3KPx;x5G3jhEJDxjY>lXy`4F;YF-0$=AX zcUgX0BEdCfoGNLkwPFx{Pg7ReCfL37a`T50K%G$i%l@BKmjDhpRPKGI0w^SRnLiSP zv1~r_GKjzucIQKK&&)ANU|b=&Q7N4l0z)uEa!;Lo!^0DS2JBA1AAvBrnKD^e2KEmY zCMP(7?%!#t3MeNUVVq-~UXtF&>0NT=^1Og_4Bdt`04GjQoRmwEeavl-N|bQSav8h z7zvTBGLoqNaD33dI|aq=Jh`anNd9=i;-#!h;!I6Dp331@xrKWV`o**@>jVvX11*~e z($Ss@qtR*Tw)Sc%rHS+-U09|jg(l(iO|qfX)I2MsYOl?V+u09_6o4Z^FsHdnO&5J&!upF~{O^IgztE`Kvd_ozHak$>?iD%pSUBc=L1B;13(oS-H{kT%*?}zO{zOG`+p4xVmCeUI=)*(Yl->RLE|APfwLeC z7!g|WWQh@P1R0Zjn4-n!0RJe%(NNJy!Wby?fJ;*oQ;pwQQ??JR~+LaA?}Ri-}# ztfO8D@J}g*l-9C?#4&F7um^n{6KvfC9~r4zt6^k_zW*0nX5xiyT>zurGn)-4Z zENX`xa{K(A^ZJq2 zz$|2#ZOJeKkWoIkw1Ay&f{;4^3q!JiHaNrHb3OUMAs}Q{7Df?>gc6X%)-gN48W3gY zA7jpk-`Gp!3^fz3fG`0byB+kTM6$SuzM9Vh@2spV2#Q^UcEm!{h!sR2+5vVzlPHXP z_2$`K_EmnDkIt04F_s8bo?LGk zi-Aj#IKZC!K(JVBr~-yfoL{2t9X1l(e$h#D@-o1;oumH3vW;4QoxVcJ_Ki=&H6Sje z%#3@e-2kHL&NVKK_>(P4&T~E3@I%MQUYka1SaaMjBeL|St|EZ=ttnGU83swY&*9U( z>w}QZ#}maHVNGzvhE>5Wle|H$V#u+Qj{0avKrw@Gl~kDF(to9?Yxxj+Ms!`-pxvbIgyD!Gozxg2(V%E+HGjyh=^DQWNvt> z-VDe{10k)+3FuM<%p3_q0d|-gSmwf3$lpc5gC+G7aK^d}#V5PbM+R?j2%m z-}#|-w7kqKx42mR-)Qlz%y*odrp!TU9A&X+FuA?Xd-KoA)>Z`RajVrd)*Nu#4OkDwC<0}V25)D=*~`e$cR)Vs|N5MP+c;bbCPN-i8s_S1n7u&CTDpyP%9gU{d5vx}PBQY|8&C@JZ6)HQfW6zD`W@QMRQ zzpcMgJ~)S6Jrtz&4u2bVkAqc}P)5QMwHgC4VP1h4Owe}_%o&FjlT6Jp#0H_7TF^??0EA>|g%~K`Y(o6h4`p5IWYl z!UyFHp-?vd{Fq}@YQhY#W9=Y-_g_5xAv3c_9co6Z+in^~P%lvG893m^r3N7tg| zGtG+xLwFJEpr;6`o#49Y(0O^Lw>t05TI)RqW8~-rcUkL@1~4aZZ@AhY|1FX81CWHD zPg4K4VEgORKf50X5JsWUYGx1lkFdRsKeRP1<^a%-EhI{m;xYMiVAp$JYxSrjI(!Z zx`T|*7$XD&vgX#fKN9Hmnoij?2iTu> z4>R3J2rfVPIbBvt81DLMEoNnB^=7IxA3-@S($N@u0t(&(xoDk?i(9&FT>VVA8Gmq^ z)YGYK+(ER#S=dQ+{8pj7#z)!|Zh+`-8T_ParIXZyv+w@Rgj~W8?-37iS^z4vj21lmqc1$)c zKCQP+1x|g$`)l5sOzN75G3cZ2g~{t%a2`r_xT=i+#71%KRBU)zd4#T))+t1gvJ)`P z0&+#^QrAHIc*)Mn<8+?#Qz4y&*$%(O-?_sl#r|hrEgC3W_c-IQyN}c(QeMO-)akIU z*_uNZNl`ENRbd)SS)B#MXK$flpX%inmII@_#oz+q3)(?vz zXi*mB5AW0L3OaqI?VcHwdRr8h>qvq@qaMH#l2R$hEEmc$sY5`lq$tWt>F*!R>Y+Fm zVD0SG16u$GR~9C=P<8=099S&>TeFm*^ zAG}ABK%4Bb)F-i{rT~megEC6sU9W)gL!kJ2iZvS3g;t>`C-$TdYiB8$p=VRW1qw=Y zbp*+bBVaQL*(?p+1iCuGaq}D#pOk_@C+^S@wW%j7O&J{>bq0+IfZDK-_=|3_Jb4W6`Z8(mWG~SGwjKLR^RapedgC!g9-z{ zdb|ouQuGbq6E&W5;Wp*ikDt-XHzo1_^x#&QDG4m}LKgM|co%pV!clO=0WQsSU$(yl zwFvY~=2wHVmU!~IfJ!28h-|$W{;}lawU315dwM8@9vnTG{ad`lJ+Tz!JU3DF47dh^ zTu1B=X$|_(C9adI>6wuNaXCQpqOj&UzPfM6^8>BHdw7$o!G?~C7WLK@*MMt8YgOg} zYGb}7xe-R35+t5q<+aU_SHXm-5RmeYRkKN=RGullt_v9Wav5s; z4CH!UjZ8IpInp2Wt^IujSKrwrypxBvgA^D$AGp?yau!__&;53D2OLTpIv)_) zBYUde@&CcF$1B*G6^@cRm@awaSz>AQ7Ul-(J0J`QQN*CE0MqE?9l*7p15eJ){hGVR zotwl7BG)2cEy00}1YL*Bi~l~J+i$n(${=`t zd|dUO^QEsCiRgD>J9czxRPHn}71Yy&7J13~MKr!b+=BHoV8=}!DD*pW54<(W?k|jTi z^4WH}s&7zleWN~vkwzMQ|Nlh4Z#PfLIANSKRc!WnugZ~$$DWV7q<&&)L;yXo6$N`e z827Ec?QR#p#eWd#)}lK}2kAA{ZJP z;z#r{33+Zs-*RYl))}*dRY!&0?Mg6s>DtO`)r;uDS6Ir}_0RUEd%pvlXRSetp;6);0+{pP!0UXUxWNDvsH69X_?= zugdm1jc~W2;~K%;62Z4ehxrHWgYFJLL`d%7>>z$Qv@?IKgG7_fjV5LwbyX}l@WDp@ z6R%Nd&)Z=7Dw~QxOw?_!Df=j2z)|zhoSV^Go{$W5lz)ITj;sIFZ2Aufhu`rT} zW*&X`AT-tFw0d8$WU_XTPNsRXI1Ph7PonH%2CPj*B)rq349?LfxhNqdp z%KE?%85>DTTSRL)za3cQM${{Tux%r<4kuw0PT&rTilp6h#HhL-V$`3mK>2+ZDkVsZ zHfRs;!q?+1Cez_%Gk*f0t#G@^NFqgqNtnTY%&k0dGi(Q{Q0>p}Y~c23mWk-8Ng(0Q zP0IW@5Ip6;-J&qP)`_V&g`b%wz8=uqwZO%F9A{YQVA1nnAIv z2tmP1_|nqe@rBWkW5*;8Fe#tRHw-Tajcz+(o}|Mjxcw5i&;_It|hLF-=ZT1TF%DbA%jm&3~3@A&<4yqfRUIu>?wTGe;W z9KRhA{Al3R?|afiy9-|d2Er+~`}r7*#ErarV__;bvK@gU7Tw^r6-ig@isT3uBU6kd zYN{}+M}Fv;{?hE+7J$kV8{H4SMZV)tV4J;Iig;<{t=KcYd*|qlwz z&fLaPrH}BJwKB={(s|*_ zizxjYo2~HzFP80ri~`yVg3R1t?@gxzxXLHrWKv(h{~U6%w8G;;sd~IX##X!KfYLq0 zdz!!kBCD`3fcKU*8P^G^W5Zo3B_X253m|`Y)#q2&vFABL13k|J0}D-HDt2@~w3{Y} zFJ~WC1qBr zvTwGaCSm^En=He%Ga1K)DFRO7LPAZGSQ;6a4bWl`yti;{*|L0l@B&Adc)sSDv2I$w z|6R6IOb~Ytxvtat`JU4^bm>Yo1QZNZM5i{7pw1qXu>Htif(P*jq`N3~13 zr=$-L{krh3cm8($?PG(<=Q{25WPdG`)5M;YHdi=#{H)crUyf(yz~PRCi) zm_J(FbO~A5cXkYStJ{_H+;8hXxo`#dOgk8N+I5LLtAh7P!*yxwW7)uvR3gishEr}J z$T^!5Hg~~m7!#|e;soUlh~gUb{O|Dit`~brrrfmpwa$4zoOQz?bmCsW^Ix{n?mtxm z8@CYa&9j3pMVUmC9cIPe>OPUXZ|DNJ3W1@S-G^6h0-78-Y9+7-QzL>$yJPf3@5pyVy0R&ts{`!1Xo%yOG&A6;en&Z<@EC?XBAj%l^+ zDA%gGiG1|%-PGE10(U_*vaRU4eyXQm{`8V!#sRM385X27nZ$+EK4X1CdK)&8btU`{dW-}BQglQphs>LKJsiG)ILMpyzV0`{qwS`mPjO>1sk2WL;D#`zsnVNjStSPMMJS^=XS+ zqq?)XmsC{`$8g6|7+_4j(7Rpo6s(%;H3L6%3@u8C4!Npxo4Py;%Z^be2tDGaA+{SjUY>FJn0x}m&nA@rmkyn5|bn)b- z$nKUmn#WA(cX=-2AL$aa7)6=>A^ESAb1Y-xu+%8pGNAPLR$2If5E)8TxGPK8tdld3 zi5&I1CAUxAU-HwY8r1iE`BXVUV%r&%{zs~AqFuuafJY@y{Sulya5+Y?J=g zO@6=z_~5F-Y?wd?j)(6OycCF1SZIErJjYgkuF^WlOmGnjk`3f6ztF4RnIxdvMDH+4 zbZyX#B=%&NNL7#AAF6!$lyemvIQs!L&?QhiUt>JqG%H@qUYo)`B+alN2I zf#6az0+Zv~R?R?rI9yoP+CqiKJa1ov=3>mkrNX+Uv>?Nhs*R*8l#c+FvK!nZxporF>{Y2N8bEBCQsY$`WJ^CJzQUYvTg)>O2c^B10=tIyWF-+?#!90 zJ*H%yn?~=`lJyZyt#khseboA0p>Rr%#JAzXj=ZqLcW44$QjJ^7;7;X+FJ;V%ao|X59`lsxL z4R;dC>lqEdYp{jU?FqvvK?jm~Z(q>&aPG`7-jWxv`BvnE7ud2JcRA|4nEC$9COJrk z_x4pYR|igM9wy_zmqYZce^lyX!BD($-|+LngJyzFDTx^ia4KtOgYd3qaS$jpvrb~5 ziy4fSfbc70^W>7O1%`Ay;nI#lX~HZIW?^5Qd1e@{WVmHOsxWp<-n1hN?x}Xn5!X z*8*FAII`7kt9MvrC3zv`;U{6)8JPbk*hkqq;0LtMjETE^rwm`L!Y-%J9XSGRqaYIY3WDvm zVXNF$H!bh|yidYN(xDL?>XG4EQV>gtEj5@e;oMg!#&g}#wU7gFu%m3p3k2Xxj^kM} z8AX&X-TGe}3+R|%?h0K0tcutR-9;tw7}|HZJFb_f1Wl~Se z{}Cyg9|4@N6XF#2+8nz{d(wDyluz3rQ89Bm5s)IGnqzz0nj8?tnRIS{dqHt6LE=nf zx-dCYzm_S(85jT4@hKhjuGp%FXbPN@o)5Pj6%(i;IrG&Fr<$@_!r5YeNgH=@zuTV& zD!Zz*UaUMBxqp$BaMmx=ICyh>Vv*VGCs8Y`hYY04nZJx)97slek^V-Pup#KvD zWt*rbq^Ps%e>(5JCbV$qik+c)lmFK8wvcU$M{JMaNJPwU_isM)PV@F6w2@zCgn!5< z4+$V-KJb?YXxv(C65GH_9p^r-d*n|$t7|324m_Wy`BIby5WjJ*@}GAi)tuS5HxIh@ z>z`~P$A$=7xfhofa(U~J2&j23bU%Ux9Ne5A|yB$+dAp!$pDL)gNLL$7auj zHMdWgFMLkjn|EL8-9+hcl;U_kH99>WD4#VFu5q_YP*as2ECv8uCu7Y%yjnhe$gN!og{s`qzW33mgpxjA_O}dL69*4vr-&DCqiZq%k|?ub#|x z(I^!}kFE&k`&-kRxM$i|AFip!-{T(>KI{{*0qex7D=bGCI@jw|nR-|L7rV7RM=0%_ z5}v=>+IxY0<$8wL5X$`dFQ7QqiFTUx3!B$3{6xQb=xWYSWnn~q@k^L6rZ24>OeSF4 z(gx}_a_ZgC#V?4f{A%8ai>QQJHd&Ci|1g{0VL3PUg13!g1K4#PmY;=JX?kBnTEn<` zuLUVp4B_ID6F-Z#j0lS~rBNv5h@{C(YkLG>hs{;kN z>BZd|Mw~-8y_WgWQ*#lN$+=#7CPUE95MCOr-dWwYbqIMZtp>LgxU~EHe5FrJ^%JL6 z^H9OiUPVlESEwp$M)H@&ms*6J@}&LrWH`I^L{=(}}*nYIbeuZ{%tLofor->3w?X0212ImzjR@+R^Agk_p5UJ$>F3 z?7LOR6A^8lE&pU^G(qJ@23W6-(%k9|H%KF6BL(!;rWI5)a<@6nuRqD6xSh1d9b*XQ zDs7Uv(U)XSTwx$KM6`7N$MTH&I_j9krD*n-9v$w*mhS~tlKe#{A!lMoqZwlNz_SRK z>D2d*7ynroZ=A#Srk=%(S9?zgdFv&_2YJio)q*fT-R2c1+`84NAUc1c z5fo4-MISU1|MS!Q`GI=Ag%sRJ&e2G?V0sBvSN+U(SSbQD2pPW6vwuU3LcFxD%Ub2m z53*Vr?Rw)6wevje>t_@p3x-6z&YrWcq@>EH z0foAOpX(SyPdv}n_>X)8_oxf3NR|;w8`VH(u^&e(zhyNYbU6}Oq>0#M_ECQkb=%@5J>QpgxU4PZ;E z_Eh)u=0VSj3;Ykqzi+Qb`ev;;v7HJJ@6dDj<}zbx-4DDypS)QZ{VZq35bgXa{lU%< ztq{W=3n(viY+)rhYpuGUu=^1G`tCUOl{G`>6WawyUBdoS?}_^}Me#O;@NO?36`3x{p0$r zfpy9ITj+?)pf5YI8%x>ka;H}eZt;EeYU|as%H4gn4m|VC=9H<}=tU@(?i8KW7&GEp zCXL(LUa1WJ4S3*}6jnuH)(qhs@rG&|?dh4=`76NSYCnC+Tq5kQrdBn;zxqDTk`kCY zaQQu7Rp-F->7!qwL^JxKT4UYNws46*8*$dq9xg=Toh>O;ialMbs&uuIQN zQ4A>+0G~(=bxqHlSQw&s3fD?_@z!8o1O$5`U2DU7ak?9j;i{J!9HOg==T=a+_wV#y zelXKTFgx|I;c2>_^Ne_4=_OVaE3WQggU4mjOFFT;-HU{rVTkVp+os(z_j_%%qWtZ2YfMW#~$o8YN)NAdIB&c&QphlWuOXx=XRdnq0*0m$E{B=~6) zQdqhP+OP$a0pO*Qs`<9p=V{jSy$`QMswE13jI0JJx3{D-lfcR6 zc2@jz$VV&OHk8BdyN_x$8#h4VZtgMn(btf(AYKZV7hdH}xiw9h;ThxkCAifztcDE^ zatYHgt98xm>DV^h727`=R9-z)G5KJoWzO(SIF{%^e6}Be!d>0##5A<10bs2ruV8VowL=}#x@3E2s@o*_rP@mH|C1NE@F(s{@-EhVBV1`L;W2+^G;X@T;G z%^?JU!Al|XBCCkpgW=N;`cXC$_cVSqwau7m#W0O&%aT?0!za%~nL4yBm4}sA9}5|5 zyrPzwv(RfW%5A&wL3MrGW6II{OkhA>K?)CwidjTaMwiF=*j|=w(qh{TBF|N*{yaUW zf3F?V7LFi$R3P%kx%a=A@aC4v#|o==Br45VWR*}80_YnqsL$-`ma{;dx`Uz@Wj(fy zL&D1d60vrGQizK>5qq9}wKSEM-p7?BYJ8)YOdK3(0>y7~xJf3F>tiG~>cU+!Dft3` zKPA`dgZ+H(o}bS-g0VJ=P4?^0FgP%nwHph){*pv}TVbT@vxhy+vrhB%^3 z4gd!weh#edwQsUilz!!4HHLHc5%I9qlwUgL0oKNOL_SnbT;;-gSvxHZxnV`~3jqs1 zDEnHS{?Km|K9JgY&-E5$wCA+!=1l5E%-z8inB*By=COgDr`h4Fbk%WVd6uB6TJChp zIISGlq3bV(;B@BJ+JNFw=gpUz#^8oMHl0IaFUr6d3pDUNdtLUOk%PAxr2EyVyp6)A zyq`-K{*rZ}kd_Ztr?jV}QcMO9Ks|^lVBNO1Z|;NVsO9B%>m%tON&QE9`z7jWzZg>T z3MgmHK-$YYI&VaY!K;$?-Jxz`WfvpUHMuc8qDe^p8q_04Y^6)u9TFV8O?XG@<2^4= zT)+qQMt{Y(3+Z~U&}7WrSam2v<H@*W`Q3RUDlTlIkk&FMI{;#f^(`~ z`j8+Clj zNyzgliY=toJx?cB2R8F8*GX(Yq0eZVNDQd9K4`z*`Qu@2AK*lny8tD6tvV!ClYS*$ zdOZDOdm34N<0d)grEB@>DkUN`_LcQSd4zzf7@^?7YG7Pf!ZbIK*zADwBwUGX&j@2* zv3;YtGJQg)E^qEcz6opn7X}oI#VhcVZbkEZzw7#-$ zO~4$;mhdy1Ue!(g7^!Qrtiu$LQDk-f;sHr*4agN;AX579taU!N0y17!oV9lIeydx3 zSV+0NG_o&pCDs1wX!{mGeNIj6grfBgP~3@f7vKhN60?-tux@rJr&}Mz9}^vj*C@A+ zF)u8xLD&L|aoBmH=NgtT-^r4c`151D+7UWV2>O4z3~hc>PMwlo6vykywnl{&f%8Ay z52xs{fPp4UzoW>tPSH8_7lg6qpI!nL)4HA@!MwhHX>m3xetxgG_BLvS75ADT{tAky zfWLfO>k3_tc--xHkk$VB^*Cq0Lk5rABfEmmLK{lKtl!{`pqK{_;BpkaLC$oPp8jEq*e#4lLMirdF&;0c*>?UBLbq8db; z#cAN@->Rp+0K&~3%}r=Q%h%2~E$8~?@2$60j17(^~2?Q0CgFY zz{#K2{oQwo#U6G~U`f8lX=5vOv~FETU5zMIo)c@*qdS)9XY@1r`Msc)H|12gHu(bt zBC3GZwe*46&pJy98@zJ=yE9bpA+HHE?CRV~XfBv;n5UEoKYjFyxAnERi-i#=OCD zD0yqZj&~!3Tn^{7Z4QgLdtA{$Y&ov;594&t*LPsi^2*^LV-XDM#-=V5#BL3v%!ukI6y7!W(h}nJ?2evDc7WL{!(aKI$vQF~pFBiwbSWYwF~dAAQH z<;7}CH|mItn3QTdQ*Oc9yz_p;E^o6tGWGs3W34MC?Sc$dq9-%x9taWH@#+Lo%Hc)_ zHc@t>l7C1;f&eWlP&u$r!Iw*LBJfSeTa{Sh;Yq05-sOG+hSIG{TIU?v6K@rQI2DPs zk$~QT=8MZdt05VZ9Rk4}$*nZt>wS?!*j)P`lEzvH$BI)@tj(GG_dD3DcXq% z$Dg5raF|Q?xijynL$Z@d_t|FsoxxRaCAQ@?k{iXdcmCqWCs`%7y6mczU1!IhZFfgd zy4OmYOnQff+o4HSz<2f98=MN?xO-{GCll}cmB>T|vQV!>!c*g5Bd7JtG(;rVmieLd{5zeDcyGu$oQH!8Ld)6GBLM8)r zvJx}S())5j2o|9tv-WT$^$$dD<^QNseJM^HK=+(_ zu6~Skl*?EGEFyu?5j1lK4KF*3ZY6xTLTpbZ`<>sA`ge;$#iObXsk;d z2dO;ZXFIOK7`WFX^%ZHacD4%nu3@Y04!^mY6)&SW;3MkenlF8t!MH-6s;x)=cNBFu ziE*;4jxyhdhJPpmcHgww9I%ifjK)ALG5gT^nt;FdZ@iwVSeHkKi$)?2saTr0G}iG2 z+fMx2@EN!!@I(?1u~XmRo@2i%Vt&MgXH2fbh@;^}3?YvPND)84w;FwF#(^<|TNH$8 z(%JN72mNI)hAc(f=<@Q&CQIXU?9_L-wjC=eREr4Q_<`UF>_0RS|3On1lIQ_dr$XZb zifd>*0wXii)#KzX_$&v9=(w0YhG2 zG`HgQ2?&myiFfySXZqXG<>piPQKY}8Gi?s*ps{PgxZ3^A*Dlr$f z^+cV92Sk2D|Im|QeJm~s=YK~V>!_LWEO)VPU%J(tfFYh_OU(Mn0%mWQ4wdvT1D|En z_TcZl8$kZ@nA6-2`wCA(7Xp}IfYJKTKsazkWT47#-Ol=8Z5Vxn#&dwEP$3D6z3J(- zw7MThbh0?pyjHS*JLiSO044!}xc?tnT$kJf>|!5QtSV zwmX#e0+(Q)Wx)fU=%nJcG(_&O51~UWw zR+s2N1){>oo!VfrXryPv-o-Br1_2;d{}Qub@4%aT1a!~*fIu}Hy1#}o**0}$Tk@tP; zZ?d^$GMQxyab(lJcQMyWro_(T~Ef5NYy2^5iM3U#8R*ju!( zvx2bppM@Yt$*Z*kk~gzbad$EQr>rlJ zhwA(Pzjt=)j3qlWmh4%wR+_oClomoL%2tt-sr1f1cPyDU6h$RQ(WWe=s4!zGDqE#Q zVr-2_)-uDG`Awh4_rKr$?>x?Zt@rh~_uO+{ujk8P<<8p-p0PtO9=-rhy2pM!hi^gx z3>9E<=!)8pH0RsFyU{qQ;qM+;>t29VP81h1^xQJLmy6dyZ2)>$8*FBhns@E|h&4@H zvLgImIq#c@YFTO(J3^By%Y;QBFvuHx2&{QMU9V21&i}TkMW%vmZFnQxoV(uu4{>J3dH_$+ z?;gP+t{A!DES+>-e{;Z^NG=Mor18^Bb!*wDs9{~KQn8`Mx{Ba>q7$jf4lF@spR_>CHAs-w z!l-UgSA#Wt6A<4Aq$6I00iyv0qiKKnxi1N>XRY;x^LqP_bD<^8bdr{x?K2 zG89Jx`lwsPqbn3kIaw{{#M#rw|8FipdRROc`oGbUES#GHoTBc_0>%e`{|SZJgvy2! zO>HoxODGc8SLKE+#g~6QnP`$Q6usq}@4?T~WxRZmhW)}JZyBl^lSwgep zNA}tJv`R5`B}lIZ)uuO4M(3~IA~4T0-Vx=QoU?1pNpuc)0~ni(6&sPs_g??)b8Gog zefVAVJl&yCE)$u8ok(`vC6OaGDr42?(&6W4_{FFvr4hm+P|Z_D9od3&LpCCG1(JQt_Ys3T~Jxvw&g;PE0M=Sske8n!6^XkJ5jDkwXRvXWB>YGhIOc z9IMl8p9fb`c~)w-cJ{!ZQa@#G{2pQHh!(tcJO5OWwFdU7U3Z-IN`w=UqsE{mRNQNe*aOe{dhmqZ)JodlF{2bj4Q`#Q>&oJ=Y{raWr8i zupbgP!#ER}T8fWZI*H)JY3vLWK{1GkHm|EUNv~b3m9no!MbI)_5sk=d+?k(T0b%GU znD!(EzMXVfFiam+&sE?K)#pQBHX2+NW^ku%YR(XvegSQ4>|o6^Pz9cZA|!LUqJTA| z-1aSs(^en>98i@}!p9`Qv3e*daJ1;$+7I7G>L>ro;#7d#c*Oyv1CX5(osm8YbB_M< zn@>oBgIzw56`*YPt)bvK=u?KnG6H0rW29kJGyfWoPUlYFB14ISVbGrx3y%PC$?O~y zu6t|^!~a;3s<^g-hZ%3-v)Bndmq(Y7->=Sgn&E^0W(;>I)yYlo`8KVYW>HF5i{AgCE1`abAW!HijTkWzh2rcY4o7 zN=Skm;^4e6EsJ6rxSxSSi7PaXZS+q9gpqA<@}BJ^q1WQM3+PXp?1If*GA0EGGLykm|_)m^B0gma}4Dr5-mmBH)NQ{>WQQB)tii=EiaDXsZgO-VD$ z38@9&fTE1`3esy}cB(ijQ;I}z1bVC+5`>l5RjVmHK5zw7X47q);ZR5LMC7dlBa4z< z9q@z!!`N;{Yo&)&O%X#igRlcA(HfebZGHJGL-#zV*peFIsnHkw46|=EJ3|?9al|Vc zdG>Vb(Y5gN-tgYth{wo-H;@Mv!KRrbhe_gtmRJn)7p(LwC*u@df&2EKKpA}86JwQ}j8w@DzpfqgiF6suJVP)r{Qa`Ij2(!7d zZ&y|x6P#FJ%8^>%qwu^Jx@`=gXPgE86uZ-6;n2Z?BMk3YokF1P(9F4`vZ(oQcK+P1 zBJx-b5o*jeV{dtQ->5x!~oK<@C6mVS$*iULnN8Hz{kj-9rzvYcY z`SsqJ=`SkwEvIDMSNJ&@4uNlHmI9pOgp29vA9*BPZ|E^#?CbZiCWYc!RJ5twX_yx) zy>~8yw{|fwtkyP-(m%If6K4QyrS44J?H`yo4W|<7@Y7rm;(YcxPeN!txfu01bnT!Q z?P~dkLFk(%>9S(R_T-W41mN}$Jl=ByfbX7A*14F~iv@H>GQvRw;YbxD;9Ay&UVm~1 zxSVtptC7Y@ilESbXJ=pyB^UX8zD$v)@l6YmTE+4jk$Kf>^dTha?0d;*6vSzSv1Sc` z83p`=nCOZ)Oxj|8fP{i$pPZE@wd4UZ-{!t65!iSA$pvyJ_S+(5oo30^MHE_FGjgn= zcL%d;N69Uk5Y>+4>-TXaxTHfh4;V@<*3&sJCIK?FAIyXV# z;&CJ{{PB0e&o*E#`6&ATRQ-{iVfLZ}v2(E2b2c1vQ|-;#;eE5VAH-prm?f?W5I8!3 zfifg^iW1r>Rzi7Hx-A>#pp2886O&e>f#F9_uNGyBH-eGXpsZ6jd3GUMhA@P1bzh4c ztCynHZFfpE(`7haDD6O9`wLLnY3?;K z0!OmAYTd(6TSv_z^XI31?=3$QE{tVlRUobR^W7+#qqj1WwoW~fp#qGh;#GlNXTIOA z>nEmf$wIrCHy_#ffBgB(|6LvZd0f&y<#!MNizaag9TqoFXOXFEbevojNG)T|a0=%i zznJ%^7I=R_H&YBNXsGJ+VkA z_Rwpoeqs`5@>Pj8ctTm{yOspaXY`dMfmLa{TM%x~J>7@1$2BgCIc0U_Z{mKdcg6OU zZqboLeS73udzL!HKXKdTTk4+&x-MTflmrw|mf3q4`S~#6ZHA}ra8_iZABaF4dhdp1 zo;iZlNDCInpeQ4}T~k(j&PV`6E;F{Eded0r)^lD}BGYRB z_@?i1snNS0#7+738gw6MC+01$6ipHv8*Axxh=!OFZp2GaWOLP>cN3 z`SE7tWu;_LV}4JX|3c&QLY6Evrz1JGo3EW!hzA)dM4-e>W5njlrbiy-N$T!e?nI|X z#$WNm_$^^Fw8cg1FwHDUWm`Ivz~E8oyd?g2SyIc7@y+HarQ~+=e%em$lSF&y4_8DT zMbODPQGKe>i1N^qXl!%wns zI>|xFepshEb3t)ByXoG-6doW~vVV5b7B0S+($bhW)bu*HB*CcOds?_s#d=6xKQ$9G za=AdNr37EBq}ATuh(=$Izuo*1bzE6aj&U6a$h{{Z_KaNi0CK-v%F_48YFYzLP3H4g z)AQ+2eUHFS98$R4g|O2va~OwIjMcQ~On&b-%{m#`q#e{@C->qT|JW+wr?crcLOxF! z<<#n>9PxFcGKT(bJ4zDD_Y7MLGls4XeWtfHNNktDRV}594!q^2x51F$LOX5c;)Vk$ z%=QiUP}zB^xS(s^3cIX`eJN|x{)D}lm0(8TCp!_q`q9{*b~-WiQE<`{{+W;y)2%QO zAh6j6{WKWb;Zai#K12|E=A$E&CrM15Q&h! z?4KWs>deyRKPg9V9>srDL9jOAv`s4tA4X}v(atQYf*AT$-+&Y`3p@-6{Fjo>OD^9L|gmc zQfgb?TdPxaN!eh%xi+4z(Qz{$izaTFqaDm`Li6sR0_&UmS50oHG7h51e2|2r5m+}w zO7`D@`EOnclP;g{0{W#l*bEqb4VKK>}?_y-0mad#&-$}^-+Kiy6Mh( zQe7!(d1{6w9PB(}?1XD~ae(bl3-O+8ZLR0Avu-tL_pDJZNv$gcQi_sn%aN9_Nw0FP z*}J%Irxuk@mwsAl;AoR#AVDOc9rp!<@(0Bp$rb$vkbvPY^vX~3!@zP`dyw!skg4G0 zI}#=#=h4BECCJX{MdoIh&`7$QWm&$y9F6yB|2f?Kojp#&*`F1@ z?BeE)C{Le|H{ixajJpHI$wV}L1P0O;rXa>8WobNuHorFJYW@tUp__KfWDZ3Y2&RBx zg2&5OknN#6Et@oH2R|2$8y(lt80mY!^_clH>Ks+rx#IoTP?v0J`N_%}C?5=&UZbNO z;(2#w2`J7H{rxXaNvys=jqqjD_2XK-+8od2(Z?6Iw`C*(ZW6;^#8!-S-K5nVuy45l zfOfy$$TRDppC@>qZxn?l4fA4mY+e|0H@vWD3luMxIEQh%Lo`u_`?9A9^ocY7_}e#> zA%+FUgs1px69f9m*;%08mQDO|n>6awoE)yD$WFAZdMZP&Eok(V4U`jg|Od)(Zw4?i5UVOvWQ86uTdiFjd z@ZN59{BTb!`4;B>Z&Z{@KZqTXMJPssI{ub-Z?6t7yjpegpDOVF%Qaw~jIDl$z5n3DD*h5f|jdGV{>#N6WX zJ0hK+TcFi-_d;Ab;CYn8>G}j*iVphGk(hL;*vW9K3d<}=F7py#Mr@J`sGPbNun#Bq zkNl^yN)TTcS94CgGc&yTwZqi8(s~OYYAo~ z$TB$jSL|*XUbjT8Jv8sxIIqcwItofo8zgkckb6NjwVf1W|&FpPVQZ>DcQdJ<*&UIzCTjOqWqNu25CBd8%W)%jqz&x@8fFOMgcyYtwQ+Vlc@|xOIgbRQo7n+`;ZTvN*%f-U~X#kgh z&xeA@ zbRuKEp=pDd?7riG&a!!UyH6Pc?4G!=WfXiFXy$E=AbBUbW}o{jeBLJwV-8ramjZou z-SgI^%gSf~9Zwai7Y%@JEf0U&pdz5-?*`>d?I}ds>!M`D2C5qlKD_6dvJeGK#x<$o zyFY_!)DMO{{A?BlXY1hBTj>)=rIeKgq(u&+?* z|N2_B7XLQ-R+%z%GE%z(d-C0{f3i!lkmG+yMl`(K4927D9s!)X$c(2}P_x^(9o`o? zvNk&nOVC9!{QLz8*9!a^=)2+Ao0p}-kqr@*=?WlNy^yt%b_oAM`=fL z*QP0!AQTVg?(dIEM8@!qAY;N>9N_j4l~Z);;%jBG^0a-VY=qv=6HNg0&DX!n?h$`% zV4`c!gkIO5J~XKT{50+c>^IYts}3K>7Z?*pE!vkFtZz%A(x>7$kSD|!jEXmlO(6u( zU)l7ozj;b+H5ovkm~X8eos;Pv4+r)O(B%PXcOw==+D4elNU7F-Zy4|9d9hun!5VE6u5-l;$2FeWpwLs1=;B_(vQC z{S?FrKgrVzxl?GupYVpJ$R^b_N|E#TDOmK@T~9Vb>yg=1_=a4#Y4;blOqJqlZ}}}s zAZ|^k1^|d59~XKB8cfN~vBYzQ;fj+t5!L}AC*X@@3Vz#fU|TFg5d|E~t-bvo51wCF z2?ZeStoL9#N*xxV7P>?$ed>-O!GR?b9Mf^KaswakvmpMGG_kK6vt4f2Gj_lyc;&Pq zNu%rR1^~2=3j%T9Iyn$Ek1d~P0!H{##em~5<{C@+Hc9L{8w%DiT}_g(&}vrILXchJ zmoA^OhD#FOPrneJ0~KuyAehVv|@{sRSeepk#q)EA?Z^ zXCumzZa{5VQQnTf7;|pl>^(F|jH@NleQXU}N4HAkDJ=x?V6`p-GN2FDbwtJVk)W%E zi#QIlc62vA5g0*$oNUTw$T&@SzA768s3Z!|79zZm-rY)E2$srwvOTXgVu9qO4ry~y zK>u=PtKHW{x=6TVODZY`Q+r;i?c-q6vkJ9H}Y^2}1gE~~c{ zufET*F{faAIYs+UXMFcGW_c@MpG^d~(u*_)Asef&F*BckOly-*juw+z@xg#~VCmK9 zhUm&+q&@<`UMzcF2bNe2h`SGqtMkTsi9?}6(E<-i%%;}8Z(fO&bjN$PEvb!f=AT~L zq%{;QOVDp3)_V09-C!c`^O3L^5SkCKqnqSyH#(ix0C;Ju;WraDo>Hi_g+2&y_M2EBY9BE{ z_-5X8WZ3Rhc=qUXS=0x?Ra;E$N3f*y#d^CW#UT=#_0}|UE=%yCWV4N&tDWW(?57KC zq~7Ok0e}2U^bW2*%I!gW_nx6Ol{RJU&DhZ*$!ES)#k1BxN7csguP-rdx|_vlji8SM z$$%$uR(A^+&Kd^OJh3E6B42?`F^yJLyK>c(^f$&=>(lvjYEDHe5$Rk45Xae2-!M!L*b&s=uq zF(Cnpw79%;Sw?!2bsYA9Jd+?60tLFS4)Pa8t5whLhM6`k`dPBEvHVX4saMGnu#YN9 z*yDUx<$bP~*N3g<%oI?&kmmnTxhzw)2{SBboIA z{Poawg{?-1KAS!+x^r#BK=xt6mPa?D4+&XE8+HQOJ4q7h6jq5&iy>!OS82jrE&seU z!2s}T*lS=-B6=dWOHpk|ztT!wg`PuziyhpZk00kE4A(Pfm&e8_lGv;FCGyBB^smDi zTI*dO=N8^B4ZS#ZgHZ&Y?qaOz;tp|cBH?SV3ds9(LV-5(&<{aHFmOMwe$R9;j`XSx z%Q)VDFcq|){M7hdc*@%lfh3IGoAa>v@d>Ez;P$2&=22>shQ}!}e`<7n5i%B6DUaeZ zb{vv+82a+DND6My4(<5v^f39godIweN~|sc&z@jgitbC4$dCppIwZv00xw3AVTP!3 zd>)N)nZOjZ!3{Dv0u6H%}<0V=!>@7*A6#!O> z`n=&f$B+_EMlBvj_R}(kJ?vL4qWI0jd7|dK4j$HAs=-53sLQ3l#k&?Tc2k=csztZ$Aron3*ErcEd4MKNtCY8G}@zbjY2+vUjt>f4u9 z*8P4uP4^nf=D~AP$&^NkEq}KE-3A^;4q5j6bUHQmN$`J5G#jwsqO#cG-_?sO7u*`YFhr zk*j<;o`;@A5yssXSHQ&>l?V&Y;JmVv?N{+OJh>vu4nD#;v`Hmj_P_3Xb)$)$YWay! zAgi1;_kvwvvW`=gUi`9ETVc+sID5_$a1<6~vyrN+IX7>XEqhp!WM7SAI0I zTmH$Z>OFHGR<+cGS3)iNsE6F?-{Iwt>u2Sq*&8Oj2Sdo0=3?Y;AK={6#+k@eegu4$ z><^wlac@}~s6G$Qyy9}W@m-5?8$2s z9Ry8^S&6EW7{im4Y~vq%e60y9rh=$!P5ONgWX35nhe<0<-M{$HxA6|nQgy!XJmfd3 zLgNo8Q@J)%o!@n*yFOlil)>L|oGsgP2IrqNA8E2&kd}|ay~_eJ(-QX2N8@UY5fAnr zrG1})e#SRr2FiPq_>K=KZ?9@zYt%-W@kau!w z(n~NRc(Tjk&@DC3p6Y$)yQH|zhz+(|$>4=~TexALdNinm104e^4Jp#KnYcyDvG6SQ z8d=v6mU{>As2X@UR%R_zLkd>PPTu&Koe*2=dQHIF)Owq5uzyZ3J(WFl$c#!hin;hG zUSveFz9?^edEM-yk$*(~`Jvl;N4|j+eko$RqJ493&>bE-tIy-`=A_l8g50V3nuGqe8p{O7P)$EVTUq^I{lII^}n0Lqc)MR&ZDn2uuU5XT;4GU8fCBWdUA1C73+ds zwhd|zF0qkLYh9azUsx@!C#o#-HLS!!216xz;pm|wxISGHmB87Xf@B$!o*h%?gJU^| zLa$U5FBbDImoG>pP8!a}CZ8uhCyL9VHxk~DqZS`ZrI^<$Z-?IER60MF0fWi6i#yZ@ zPPG8DZX-c!5{tAC8k6K+J!U;T3ltjFKBxOCl-ephk@)J=OqChY#TP3G;_2pdcKSt_ znT%M(d_3+%4aOI8b(}?Gg_;Ul7J+;)Yj#N?Xt)rGfhqE$hn!m^wX@l?UBiXYg}d5t zxMcY;F{oq82C|yoy+qHU*<7~?=^!7;#B(gURD5E3*H?T1gcmL3j1Qx&z13TkHAGjC z&XM@ov{%IFZZzWM2|%e@x~11n5v46gs6VGEiO`W>=L~cyY4B`7qn_=r) z4Yhcij=ZKFi@VQgRTo#|PVD|sRCeyAwi@p_XIbW;)bq2+M-2}WS^0>4q+JCRxt1MB ztZCTn^d~9Y?Fihk)wpynZlYJ@Fxa#tHYSChdo+I&eBfAOWA4l)R8YjawPtp8JuNOX zOMmtIeGdg|2o-Do=1@-0f35(-d6+Ja-;y_HX$^_@#`hf!I`~-8{HXEgP$}8OyE>Uw zf-$hmL3D*eL0O(;ug46L>p#6lgTre}%^0SI+O-eqBwUc~&)jf+ zTWhg9NzL}MS9y@3znw@*5X0U^#@GwbE@b^so--0Xfem{k5$i)FL+d!6RV5)4$}5=( zZ;OcMpD!HO1QN%x1}KYPlnB?W(P`EP=ucuZIToax*w&Xhl6~ajr4@_L&41IMJo2{) zFkhUKq-c`vJ{7D%?n_)BGS6T6*`QbXI0Y`W-2q-w)^OU^d?ooiiS*0BgsF`@Jd6EJ zycaFC^;>xf;-T7B#RGIiH9dOaLIvbwhkE=#w|a1C)!Y2>v(M+`t3q-_|LqQ}dd@$1 zg#0axxyhcNzG6DX2@kG(ffk0 zE|$G65`J5f4>Axu$jbt)Ux0gsnP48Dytm=H&p_86aC2K^V-S;h;MYm9ZTfm*gZR{w zaT^<&hj>>QpY9>^V}-(I<;Q{T8F7w2GM5zy)!r>Nd_v$6h>Qv}wb1(0AMP4T-&VRB zx$rpcIM+npQYCxd=4KY!vUA9Q$vv7}r?nuk-VpdM)|v2>J&MkdP{|(QN1aJV4580* zWc^Kv>XS`bL$v;Q#|_RtYxH8oO%DniDEp``Du5If{x4Y`Yb!+g*Z=VVRlx&A3TAS!VtH#LH^!X#zwa zFtfX{(i;!xWjtP;MW%ihX8;ECIUe6d&1j#85vt7d4{^~;nHFw}akr~Um{{VF+OO@S z&d%0Jru~v%=O0{f(+pyB7p9jJtQtkC!(r^@j``tM$*oc&LjMB|2O`jN4;XPb#pMo_ zNDRmOt0DojK}*G-%(I&odEqL$mX9qQcHLT$Asklsxcp-JZ?pbj*N_e5+jTbL#PrhgV?-9pp74Zp`GIWmr%{)Jja6!k2H21M-P#>i1QjN(22y$2s?w zQnBdWc54SJFG`-0n;d>dG!^S^XaDdL{L=wPh0TwWF>=Xq?!{iqe5-W);QZ2@8`8p6 zs;w*hc5;*K!ZYGS|77`u3et_Y1!06$%seaTNQjZgHrM79NYSMWwHiDd*3is0FuI3Q zc}o@*>0RW;rAhvd<%I~$g7kc#oWth@lWRV!40A^xIcKJ-?YNFc1-zDBt3<%eE{O4X zDD{fQx7{DKonbMz+)3Q2*uC2!F~iY|ZFX{(BRkriW8z|ma1fyC#-CDz2pyMxt1-Ks z*NXN-_SUIq3#wtCxH7vQlR#0y#Kyr0i}GxhJBRT+@KG=WV$Gn;{l178*FB^Y+j zH1{H{=I5?eJbeWde>^&hEaWaLBhDe zBQcg23DDki%r}K!47HPX-F?s9ifJt6S@wIURY3Q~o)^7JAHTb{KGD*Zpv|4{VtyU) zerc~Py-`J7FpB+E5B?Fgr#u;K;0VkXOrjmUH_yuU{kI8{eZf38YsgcLSPLsPBUL85 zmn}XABx_RNuv11~QG=1QMSXq+LEmWjFGIzD{;CwG8!|^^4_|^rg_!d*lNLW3yVaA! zg8$4t!xnaJdO&iC)b@Jks1EwgU2HV#&RiFTcBcIKh75VNL7JeQ@>IW+V2Ri@ zmoA-jM%oJM6-L0)t3A7ptcCL%6hHqnYms+6jp$!5|k|D0mU*7}M6kFCTE zQ3qO9^)y z_^qHFF+ym-2IMepOLR~9(2L9Jp4-xu7-VS04)mJ8lSGioE{#r30Pcvs{BsIZWD}yh zcI(XS_cCQ-+U2?s2)*k0AL%I6*b_M$6JjT1Y#I@~J=#{ekybQJMD?It7<&V>jT5_Am zQ{fZ*^|ifo|osHDt5Urkt`+f~xVrh}Sa)Hc>%zq3#BXl%lU3(3mw3Qt-EJAaG7 z2^GHDKVX&!(|S&JQ!!BR;Yz0gS2}VR3I(Xf2I>V_|&lMn}>+)9){#XTB^o zYP+@7%`cLxFCcd#{k>}Rx&oYML0FJ>isZB9*YM5O@f5EKbBU5%AQCnIS;5H7|3 znv_>e%0#?(6P&iv5pDE7&xBD~wq(LF56)q>k-(GAVryF8|%Uh9G=|jIpJX zdEvkF z{--9cf>?-9x^U98us!x5k(=-+2P9@ zh6$itvbO=Kms|FCH!@vSt*b^{Ito~B`-~;{$9D4|BTvaokTRG#Ymhjo8$)w{TZA%P zJpxWPm~*ceH94r8YzYQ0iCcb}t)3Hfq{Mr<3-!b_%Q@m+A!(ZzoR0v;(Bo|_dtlww zzE{lhO-`j_`0)oPBFBezElTu^uv6{b5sHX(f*)wMx)+QX_Aq5=IbOX&K?aEV)Ehh+ zH#nC7C@O!ysyHFfw#=%ok#dJK3HNdZdnrMq#?XF5DdilJ%|j8MYHUF)ZlI2G*>YB@ z|Ct(!&qV>Tdv1ejw7Xefh-a38+v5gi_iK^6FY@+FN*lGH#eEYHu~ke|L@({YRWFAf z`K~9#5ysmG^cZ+9BiHH-sJca|b^Jo5ugt!imEpXTw?B(I&Z7JcmU}mamH`O~tGnIJ zAuEH=-y5$Kt#~5|NkA!On?`VM2I1@)co!^h{|GE!MC6_Fp4b;;ZYV|+6-U7!E*a_Y z`CK807+B4cOa6*2JlA~jr^JQf^cVDvZh4dqL==9ffyaCM0RO6 zuM9o>TZ7{*PTAiIKSw=;J%G*QNq%F#&Bg1)t-o0qGrPtA9Dh`i2I43OjMASWu8sg< zd*1@ToZ;9V#xyrvi1pJhRJl6yM01gUHU-tcJ=;sZZ6s~u#XHA%qt6KpXDE%bW{dlC z!flm~37V9~7hH8c=~Sqr-H%Dxlw5fT@CPi$E&5+l{DGM6q9S!shg0w{{*i(*QJX+< zu1-i6_ko~bGZk+HX6GFmG{wc16?jRuKk~%^cQp(z80&JB5SoaIzX3qYE!FUYSM3uF zQG$Ww6lVQ`LhWPmBeA53OtMu^CpZ>Ly+o5M&|dDVU4#5l+fL+(#i_t~>BxIwr6#kO zpM5{gSkANp%8y6a?!HY&)?Uwet#pESSes;?J8|%ozKP=%Z;M66i>rJ-cqO}PsOS;F z?2|#e{8B2>p+?nkt;talq4v5+t9Bi$)*E}|KYHgJLR(T1bz$S;EhqOH#3gPJh2vlm zTtYFtxq2DUJ{Nz@{w6g-`@H`B+$37lyBWIflKCGtRZ!|(pzv@BIJU+Rcbj6wZ%uFo%`bAKVwTY#0h{My z!E&UU$ccs{~HI(K|u0SE!|Gk@oNZ@{gMKYk5C@lgPa; z_B35ZC^*AfwZWxCfli>H44ZY?sx)KR99dVrVbFtQEG^fFwA@~RW{5tLJ)cV)rIaU$ z!kTB|HI$U`V%)j4fRd?3zM>&6Wk(kv=jG8(UwjDR(N}b)<;YwpD29Jc=Uy0jkE6;H@6jv0E4H z4xd|qVu??LePT*mfrJ!m>hX)s9}-n4VqzIt{Z= z_s%thXl6X$$7bSFV8Z~qJOg-H2tI7i^H2HX)|K}{p-z5TWob373d(6I&G>?R?K|s! zt8x}kPxx8GE=?;P`lCi#m#xppNjX9T77;s<;=lF1^lnyjko0HCuOG5bh?mne9e50U zS9);Yj#(F`J5eocB$kx$;UYqU95TJc9!?L!&Tdyv?$zDvN0NBxlNDxqzhisEMw$y| zy6ZkZ1$s)d3%P-_KpV>73_dNKDju4E^`KJ6I7_;U`q_P1sdjt+{P(gWjvzag^~|xj zZghz)v6N;T&v`B=cM~U8VTzgSBNllM3#aI(et_}^CR0fhpXWN@<_8w?r}lGyQt}`f zDtt2Mky_r{winctsGRY0qjF8?qp93RgGZ}s_FJTelW8Wy*b^9D?THyyZma(=P$ZJojp{D z*}K%2_H;KoHd#sG@9Lx$!j4z-CnEckf=pbg$Zr0=8ngPB2LW|WK12AU3mE>}7?e+y z?0b>5+BQS~BzIKQFgSecg91>44;hwCVP6>OHepNX-2L&!+=H6f#1(qy^C+K(!ak9Z zE8?Jq59r;-#oVE!cJgZs?8(C`!e@ndOa~Tkto`*il|*VCweY-ZzJvFY){_8l*_@vg zM?hJ7PL;?6<EQ0}?(R+#+#z^ycZc9`zxu2G!L7QZ zz0avLI-|YUT2Gjgf;1X35i$S(XkTR{Q~&_}zZNbR1pcq&&k^bVSKyW8)Fl6_@9*zL z%Q=GIDp}b**8w=p<8&z*4`e!mX)#VZL2Q^Thi9 zAD9rC2c2=nC&aT!m9$9^Xu7K3fNce>)sjIBMu(kd`6TyAkC@9Zx0l4PGg!{3!ljuVum$7JE7!(%sEq4MK-W&oD&i383y1NkPvBtl>wSJKilljfc zhsX>C#E)7%h|FRtXx@wiVll{(r6)ZQg+f5S>%2t~Fe$5_h2-HPzIl)N5`rr) zr59iGK($4?ru)Wi8PL!1tWX;f!5bRmx9@gwasZies@>F)KmOA{z-PaI z7|qz=hdOWDzKtuYD@PP<=MeWB6 zOnkwJn6#-q9R`TT!hm@q=!D;MpEVGs=@V^?x;W(_K09bZrqJ(xN3sQ; z*xsRKlQU3X7G%NzUm7hVVqU593CjV@deu7V2GM)zR$ozS1*xOv0n7o+m(q3(;E)?c z8kGwVjH#l(o1yyNTG!Q)j=gFP(>Q5T6hx5g4NJ5jP2)7V%Wz4c<}SBM^_$LWDwOvF z+~MSuFw+ijy4_Ojn9KR#)^UYh8`o=@tV~RFh|;uLjQp#kOAr5V(wQ5Tf6#Wkj$dJ2GTOgn=?{c#(mvf0$T5$alcXrqZXD4_ zxeAfA^QE~OH)d=amQUO+W<2q+LA$U|X45b|GUpd*AV=?Lm=Oc^!?hpp{dlY&`>lJ1 zCo%(%h6HF|0u`?u+=FfVSkZ-N*79+5U!NMX3>Lts1J0iW+Tq3wsVr%`&vy)u3qam0 z8hVuMd>ET##>*UzBvrx{-rEpG^<{~#;>4VQYzz954yg7a^iXQ2wv!yME0pWXM|)enf4eEVse5X9z=Qi)-2+f z;e+D(zMTK$<-Ct>rm!H|p~D%UVT>lj5j1@Btf8G(#5TX4V8zXYhIeRh9ew2rA{;b$ z+S*SYq!B+IB>UnM7e$Y_w++2~h7X;jQ26MhhM*LpqbD3Ee*m7tUaU#9E*zOqTxhLb zYXe~IQg{X7n-HJ@vFb@7A0z+l68omPb415~AN0&OMnF|iK_Wg+{AapC5TMlai;6hr%?W0cuzKw2JsSRo8_eKT`w>`ZO(0Kw87tr9l zNPSGB{|9`y@Qb`ff&W51Zz8&LgUZ%MwVsTUIB2dCa3jo(ak!jWXMk=C!(GjncUr>s z5XU^d7cxiyg0Q02Xu1Bj)Wz@d^xS3F6Ys;{_OH5kr_5fdy@ml{SvW$@@@;SWD7%4$ zz=`(7yJi=nT6AdfhTGA27)%#NCp?+Vb|5`V z7fyZM9!twR?8kj;zZIbo4Ce}x;M#U2c$yz@0YvjUesdWzK2kNFjW&Cz%`?qg$`*^i)C?mwP?g};3ilDj7@7eBUCt`h7NeG7S<(u)jwij zm}-7MLn55DN5q4c3tYJwreGYa5z-P=z7Y%ZFwp~X=$Rk<54&DViSKSU~xhHjqYPnM22r)doh3rV8uQ91C7TIE~Lc185x5H z-p=pWmms>eqTUT(Ci$U`nZuk}j5B331nMjrEOYuckC~uJOTSA4Izp%uV@hbt-9;i3 zQP6LP_8oW=si}9up<(uK*Pxi=DFdT!D|+MsE*u>883OQSx1-c==lfJdecyyq{mdp) zxa39Ct90GTGFd~28T{}sX#AHaHrT9IBz6$t3zir2@3~uu44~!O!kxm#0mnp}0Y!ZCYqjcE~T{VX8I#E`4_SM(cc~V4GJMo>n>PZ`U-u z)~d7gzXgDawBj8}CrPBqRPcin@HOE+X4cogBRuWAS5LvHX;UQ3PaeTB4~K`ZQRhYc zN;bhRbE#wpKpi@c`Tp4JV#u?_g+_PrWr8f#{YlJ15Jr^K==$en2B3A$vnYZ!);x15 zoUA$uJ8|hi_wZ(dQQ1n{^5xhFutSiqBi#REtXt}YRKETThoi~;)7=*V5Zzx`62x2q zmOPB^ha$0{urb|UBUYSRQ^uLsnloP*yK_+>!JeGM39e05MICK?IreR8Q5`LmD;nc> zD#ctJV7?Y7V9tvIXRj`tIgDQsyive#U&AtEEL2FT4>=8jCq2(nY0{5+)gG74U?6U8 zP0kasZh%=&{%)*v;YX%DcBm?_2%0nK6n%6Oq$Ded~W6d zB3HcY+qC9IKkIh5e?ouzXj}8*xd#Ka%QJ%E2j%;CMoiUpm2W zCy|9t*5)*A!1Oa+>d0Ol|G=?B$pW60;B{kC0a;Y11lCddWA6VkZTcq=e(s{K2Xarm zeqW=1D=y@MBu2%-P3O?E_|^8Y=bzMm)Srq+=zg=N0;uE_a-O-;nugc29_v=4Zh9&} zkwk<@fHMWD(>dx_)Cb}Eo&Xb%(K2wqB})eBvV{C_8CgqP7v&L5Khh{QJp!1}@F6W4 z0P6Dm6mK-F%d=ghg#77`mS!XGDjsN1UDWwCKg_fQar;ds89{nE6lddZj3*+zK+ku` zsIw^~9hCVOHfs%T>PXo?X1nesWWgF_KlmSdNACXU%s1+xetr4!)9?cW-~_X%5*((v zmr9$hMcPJI7?k%0O;#nvgIKs@f7#wS!UTolFm0hUrljGAA^fuhgnq;1x=zUmdjH_i ztWH*70-LmG<=P?Oe@&`HGCu|%YF9!3JtNMbBwZ>R1`ql=6Vg~at=M3E z08XwdsRVb_I@*}+Jil}nug7gkDr3{5&(-UutX2%~2ZbeIY_qzCB-{idi^g~6rfoX~ z-}%4L*e%`LAy<5cPoomw+A2r_MyaTss#RDt8kTz}kg*{Btwj#2V`mc@JNm}yg=j1F zRNP_!cU(H3kRqrRmG?-G23%jJhU z8|7uk5R>|r2tMt;A2tpa4J+cXWaQhy>{h4R^K>;P1D#{FcvC_|!SGC?jkf#iTU5i! zjW5YjBd>z`Q8MaBn?-$SSZ0$|>-#Wwbqc{ZJ7hlJx`o(*nS{;q%s*auIXj&u3JX}7 zd9joJ@Y6zhT>!Gc&po@4MFCt0v9|G|IUbqk+F!UUW9EMMrqjB+6aTVU5%n+`5#d;R zTQ#3(5K^P|Fb{YA+=oZ};YBM(D{UJ?W{*^0qyjUO4rp&Qfw0uN08LT4*+YW6M+GjnlC zYdlCguZ{(BRT|pApMl7nZncN~i-uIq+T3ru?@;uu$>ak4;skGnjDb}hek%tjq?WJe zP4I-6E)7n6c$f+%fNcH>(q@90Eap^}lA)@i+?GPPwI1s@{y(2`(DariWUmc@{hRmb zB#~^7V1waIeHy`V^%5DRBn$ARUKQDZ?qACCV5srgRh%@T+Toisj z-Ds=Z`#i8x(=3M#&6IF?>g#Bk0JUHx0C;Rp6&82^*T${5Br$O^X)ilM z^1;Wj90m%5OSQQkLmi0j==DUf?vLvf*s0?)qJDj_K*!emSMJLrxrrFnS1Nl&Ml&W$ zR8b9f-uiPo+)NE4x*lE}T!ds(+<0?Dd zgop!%Z)Vyq=oJf=wGi>mmtG!^LRKlqXh~L4Nvq}kM%7N#+L)i_+~F&ZTQ~+#I70ZZ zLr9`>0-)>klNS7d8a^^wf~T7y=(Dh`h1!#X{vRc!a2irqRzN#EtIgnQ;E&0NO>>P_ zwh-ly{K&AtM4y)a_220qFuWCcwu1yNra0}mfg;)&Hc4&SN7%g=v_OB@hoj#`UqxGR zRXP3)e4j$8Uvo|H-FYyP1YcnTf~C|Kzl2ObrFLweW@(B`R@>5DEWX^s7niGqiM@ys z&z+pB=#HOqE$S1Jg?~^=Z!<2|MIZl0m>odWbO^>!%KK1S%0W&Nu|)xt9uKCkH_e*y zc`ghltho%7m;%!BHxpL8X3H}~kO2ZzaC=waNo4IK30=|*HXH->O|8t>_vU(F_UqZl z?e;K&@jN8>hrs1p!MOZDh#+s#I*wc7YYqRA0Wnh%7aAfzpo5W{5CF1XCXZ5m$hsa_ zPRGEgsmA7E_zQ#8Pa`)8gpH4Q*5t3iZz^0)$=T@9=$eaMMe#6?=gjxX^WPr4n(kTB z0P*$w^>mSMXmsO ztRP}{bucR2^9TcFvz`b8T?)H9BO67EpO>z9`bQ}*zsX`2FN>gh4fJAPih^dxkDyc^ zc^j@&e^BYN0unxdg;|#G?o{J1m3(s9ZCIdQ9hRN92OT=g4gFL3I=gsbVz%t8J5g0J zM#mKiBXjz-aIKf;5dXOu2=)K=$QKsGYL~J~DH*k=$cWOV&5+1DBOh*eVm z$%VMdF083HQ5+Q?!Y9D|>TUH)v55#&sLH#hw%wqd5rqy<=jSza>!RrLp8QGR@q1aF zSZs^VV12oI;Wz=HJwoUcEBpa>#~+Z=%w*(qv5U%YiLbn--mBDYQZcgWWPr}M;jY<3 zfT?5Zx%QaR%?}=oYzI~B&9-HN2~JH=Btj@E*>oYH2E$czE*Xh;p`0Akb4H5tuKl!< ze%~Hr#?1%lg)7O>TDPY(u5||-*qxDP6B}fw`)@BSU#`{ZEg8h*d;X74pu|4j%-rn!k>phE4z-U!>F>Z7!6-49+Pjse?((*>l+H6}Py3{Y z@qy9hup-e*D`8RYjx)H!7Md#eKaYai(fLmXKNkeV>i%`WmX>0WZv_FZdjls8e?Hl~ z(MyjMQobSh=IH#%HTWQ-FjIs-4}u6ufJtG1&|#G^0^OAB$MIu!Wg!(=+rLmW>m>$Z zo%FEZaHal{W7%Fg9tsP=1xA(%e}uJr;gLoGZ&N`O#BN2N!)8!r$tMR>k?*F*=PT>F z7kDo75J$1nr@M1UP-cwJ#)aTZ``^zwSMaEI_Q~mHKX+8C;TOI!ZJV+J%iP#}56+9% zl4*|79_CeIJ?+Sqvv@e{ggx#g%orr1Y(XpZ))G$^aH^&*&Lqb$z#+!4dx4pPW(&)5!D(Gt3so&!FdsA6Z;!U?kslRWo})I< zWo;X6Q(`s@tca%~$~*K?A@bIbw8e*=LKfdr*Ep3g0KMOEte?_op?+n@9v>_$_A~&iScHVsMj|h0|p>F&elFEyQS<-QrVw&*x z*9M`faot?DYIgH*urpyi^2)xMnXjmmpq+1~yvdxfLO8tF%{K6D1wOFm*_>Ofr;+U^ zj-8YQgh=T|!C(rp39qc71J{4ehQN_)Q2b;Tes3m$++Y6bNid;p^=P)G3F%)d7)&{v zTe>kJ?|v{!id_O{aJ(uUEVY~K?lAVvNm)tsDfXYW_Fr3t*8VVedahmp%{t~i%=FU8 zf@i``uPJk7FKnYWvJ32VmFV|-7tsjAT~9RS46_|GB(Z#e)u^JZ$1Aam{KY>H&eg>W z0aBxCMCl*fJ&_P;b|KKRSM*fiwp;j0 zxBt24^6L4Djmt_9{X_D}xNw$3z~(64A-uJKTItGqE=6%~IKd3s=3M zyO6F!$^wkmssk#2zKyT)dr0@V?8K;MlZYz`fS^xy6~t}v*lodzb7y~TUJRTCU{g6fC9RV~^sBQIFd~&3*ex{J)0`I@eN9mcYQdN+9o{4?I&OkZj zfJQH1I*abtxf+1_Ge3CGHWxON(P{HS5YQ{WAKEW7^C)|$uWSIld~0u#T1`3XA1YHo zh;>;M(5)e|o07`HB4a)A`+e`7g_G;j{26kotI6@M2=FWQ;inIi{Io8cUF5ys1#n(P z0MYDdFzfd=aorz7kD@W}aFAZotv6TakH@*@VkAr4STiy|`z2-&({N}ytp3>M*rNwYQ$=^pRZuJUawyk?Pe)dOK^90%Tg9s@s zOHXE+Lx7@bT?+>A#lF7X@b2dSy{ZE9>D zoBH#2@FX4)&^nEO^9951!=s`u3&UcW!^$4q~~)aR-0gr zj7otTtL0qr=Cv`F^Qs5+)7{gH!%tUFtYoeb-e0>IgxybqdkZE`ohG$ml6Aka@irB3 z-TcHt0emzgqX`y^>^E(BZtW&MA#(V|$6@rEPE21hS2V-FdbTbjKp(fyKL*zFpHUKn z#A2t0_0~YZ`|CdiWAQ8yBJpg*w5ly@Z1&IO;H-%YJNgfG0(m{RYjOl zNb+EO5=hdAdR)@gveex^McmaisU3M!Q-CRLss=AQk-eUeFOzt3Jd5p{Q=+8ng_*16 zNuh)s-IHjKj{p%~+f+|OC+AxknYjtCaw63@`d{6?*4D>oQ#{SYp1Yf)qOp0qqnNeQ zJ#ze-LLmdoPHXv8w>Lr zdvbCKKo-o(w}Ee-!eD&Mu&?r~MqQuD-hmew;{+H_b>mYE`^HS0y4K6--(}|Qk{vNH z54GC6zWlCb_mg!&0rsRMBlTL z(%iyb%8Pn8_ww?N1a56g53qmwR2J`rGIDRm@TEI=@a;xrp=qHRjj#Gu8_}2WkY(5$ zFUyrZHey4yMKW5Xe}>w80=Vi%JgVJ819^ab*o8`gyb9pbj1Rf^;JK4>VnxBZi>fqV< z=6`(QEqKf1Nu@9z?Z{&Z5~Ker1&<8EuxvHVaJt5kfHY3w-}>rv=meBNK#89gCFi%y zc4*z6KyLvr=h#Kww)iaw2a|<^iT#sdLx873^8L5Dag{f9u7Sj@+{tPfZ&X(qAfb7l zoj3^Ler(^>ZvCly)pFHY2emS0>FZdF&cR7Pp=aBvrYS z{&&f6Fqb?cpEqK!R2=6Y zNF*3xZz2|EbWjsy_-rKBaS5MkL5v7}h?x4M<(ayZqfeEL@YE^3X~UPTs`g=^Rt3Py z84wRGxGSp{97%h+wYZ=c(u&wf&(X|%c@Oyb(Pdomqz6A@Wu_T)DaJ}gIaBx6ewU!K zw?v*?Cs(h@<|TCruD+V9`J5>0(L?Xd@`n2HtGWQgj{8Cl&>x;D$`#j+EsAbUx#Dbs zy~llCW!U&Mtj(P}IufNMnN&OK@(G)2^D4OU0MzbtFNv=W9{qH;*DRDJYzS)ols@p7 zRIWn}7|pN81Kd_5(j+sWX#)6Gwv-W}hB{?lV)tY7?pA~H^`cA-N#IQ|TI`5YTzh0r z$bK7x`i)&j->1T*XbKmC@CTA4L_oV_1H2ZYYKB$P|3I~GJR z0bI5a8m!vAem>IcSRZ8p&}T;B93B^c3TcNb0lBJTJdc_~NK>GeHDAFP4yw^S2-Rtq zK(IS0AnKPwbQGNf>`|qDcKk>D)^`(>Dqd@cMV4ZG64BL~*+5qXxTU-EaUy&#euUr5 z{Z_Y4>Yrb*tQ*jC9rh3A#DevSjLz%Yp#rZXG>HG%07;NPf9~T&!{=XSed-h{tB=++ zqB(Z;%{7g@zjg_g^L=1{@LUS4Nd-ON+f<|q62OOAQsmr$`6)+JsUC=^4V+f$K> zc&@SESASabs#IuFbHrHDV-(4L0kvrbb04E1m>((p8??)Af)X^>t8|dPLgO#5mp%~=+t>XK2>g85AM7w}zjzt{^V)C!V1xj! z_zzYSz_)j;mJFh-)wMh|?jP8GSuT`z#R86e^ZLt1yE4M>?}qXk+{s%{`vbYLAp068 zC~y})taPKl5pnX6NhkH3IjVpAz`iSMPS5yyzTll3y83OBvZFe8{SNS*w@uYny`G;* zn{9c^l_Zso75xdzKmoip$HURSdCxmYHLq(gz%tLR| zKKHu*iXG__rn^ZJTQ8|pCgFPrmU=9Y9-4``{Zs0dU2lP7xHNX{XOGLnc56!QX`>nD z#9*)4MRlR-rEMXEYta|&H0+#VNUj1Wq_K(`lI_;Z7H!*dNqsVO;afKoy%@OG7KyO6 zho#WY)-4=4n9mJFn!{k?jdklaBqnJWi?F-e1Z_}{#e@Q_)pyLGIN-tGl?2u(?pbp$ zks8h)dAj7Np4c`u-uTx{hLV?W5tV}A?QyWl0VP?*^`EP6w&-7H{yF}jf%3Vn)9K@y z=x94tzG-0R%a>RuTy>xY6WR0gs3Z*`DOoLwS$owlgy@VE^_ne=oJ>0UNBVu8Fb%JI z&0rcjXPkpE#sywT)pU#GA^dpnM%qv5uz35a50M1KUh&hVuRg9F^@eusD{CPfEb{s6 zNzTdlip*=>BW|QMOlh`0&BecpljTB@8RW(C^Wlk9JL+v0=}?n3CU<@l9({C6eF3Gc zop)${5ZT*mTvgQGGsfxWL6Ns*X3m(Erx=hy?2Em@MtMV-*Fai;c+7-$ax4Ynzei0q zeSeXQZ&-=2Qn2HJyxh`LEetsg9sX*nT1a_0kF)d+kp4@dkr=er~pX8Qc+ituDaWCg{y z022tv)xG+)_H7_ozMKzl&OS9ChVbUk$d~oYS8+N{99gd$HDSpCW;9omEdEx}Ri!-F zi;~7;XSS!VSjtjS6=I@8KA{-hFtctw8J*l=A#9Kq^ACdnB}*b1oSR3x=NFs3`TLW@ z2DsNM=MwX{ZDXvcZ`Y!fQJ~L*2uz-|3JqteZPN2npBs>(x2LB-n*w8{v3`E7%!`jO z8(Oj>P4X#jEmS#ENuc(kTFB4y6aY{>M1tIj0fdbQql3kq>fCG|V)H)=<-gfkrv1bx zffnhREF|y|YmU9e)_Kxq=za#H9)K92)o_w=mYat`$0%@%8etif6ZmQ4+qqOWT!RoLjDl8vLgee^4m=XDb;T zYbj$FPjBfeWNtz_O!FW#Oc(KA;B)3pQxu`MseqtW1|3ouY9L6S6HT;!r|!zFu)u>r zIr|{^iz52(24RtqowP6pA9fUZS1lkLn)@xS;m8BS6wU~KT|KioBWX7d6)qQ|?#=QS ze)kZo3M_gn2}Clciu<%`#kruXc6*<$@~|v#Ax{LNw(ChGobX+V_+wAy>v7qT3Z&3r zT-IUXI%dLPQmZuY5Rx%yEGj$t#=^#COA2wqmZ4d0jZB;3@fsQtR6a#bMmi&cP<~{!e4wP&s$y9xhH#U zw7!PYb9&;GGn+xVRMFX^y%J_g!BQGLaDt14)sky;2YkuhOE6z02EsEvd^0cNyuA&n zF7HCrAU5_yw+>!10CyHVc8!ncj4sC9sJc=Dce@@QSK0=nz#cp1Rn<_g;h3*ohv0uk zz~A+C47?C^f^D)W5*M`r1=d_l;v%r)nO5sM-{)`D*&VilC`+9K&Y*P!-Y}RR{x4;9 zv?(E|N=&Q$cZNlPDK*%Ii6b9r;N?D&s@Rkpf%M<_mG6MfvGf>Mxj`93)hZYLS@za( zrRz@>P;2tY_ii;X{^RVaDNP9XGvteAN-wL(>(#B7g4tO$3M^GIR2ilF8jAVUN5GQp zA}m=d4# yMSM*l&sfhn91X?dVop*EkrPaq-pQ9pIy=fnn)@q%FYuDpg4||>)lXX z3e^|G%_+>#^z|#m{>T<04@5Cq0Hsd)D4)Dvtp8yb5TmisUJv-5HMGm?a{;SWZ_&vH z17t8>SN|G8x~9+?;9&h>LDlUUhA}Yny79h!u`oVAIe?hP;o*qv)v`OXSnKz0Kc2^O zSPSezi8wq;YmDM)Xtf4y8u^Z|aAT0~B){)GijDlM6Ur_2TV3us zHx+v@42-Ct|7?FtstMm^zZJ~!Xl$XabqB00G`u9VXwIuqa4J19%gZJfG7yoAzx33w zce9g801@c9(x2TjW}PyyO?hflW&*N92_b>gx!;vK8i8^}3$j2$Yh!xFiIlnBbUMM8 zS|Adgv^X-WB2@{uol=-o5_faVj~OfI_2ude(e1$Ybkiwe>v6WwU zcDpf+ly>b0$dndzMyGUossIH_)7_wtxjz;s(4UdG>y`3-4P{=UODsL-@99$y|R(J_ALJnL@58DDlG`0>}{SkL`9lPh#(EfpSxVH_#=6 z!5u_N6>x3TyNs;Cegqjr(n74*JNuKIyi?biG-B7iCdTiJa49)(3cn(gcYgy-hd5yA zy4+6e%zkk2-r7sJ1YP_1?=V0_)w!?X&Fk{I57{Rx%U|0c*-A(TdXBu>A;Y+Wp>}#~$xHPN~_vt-$dIq?1=y{*J z>$)hWe1+xd4mi~U6p8slwy3VjzlGT3ibp2uJ*D^F48a{*>)Xj$AOKl{oOoAOa46rz38w;6+}VLyySS;X1qdh$p}AO- z0S0HXV93@1(QxuCEkW!EWxOm(iYQJXl`Yaq7R2_k&IMrN20EoGfYQe;gWaCOAIK0? z9ML@Dj09d)FstjQX?j492AC`uM#(?fGStC*Ds)N3G^4nn^f4M?293kzSdmC(m3yS6VtNVB=`$Q^Y^zu?PLU)^&i=-M*-`&) z;9Sf(P?Z+jwPrjQvX|P4EzcdrYZz!Io~QJ|+DGFxqnB~A*3jHe4M?Ag5MFea1Ti5z za~?dG2;ByaI|iqfKlZ8U@`G5{S`XO!OSN=XjvQG;bj>T#wD}@pzHY)#8B_J* zCAdIB@n*2-{GMG4X(v%h0LF@FlB~`?wxn}SyALKm;OVGT4sW#Fte8wOeSf~JJ~zrkbA<{&I` zw`|^iL#FI{L%X>AtB)x}E9*fXg^q z=zJa5D{Ku>1bIW8g(L29)&Mq|-mC#_^;Lp)j;biU9bULA&8|ZcZ%+sHav?a><*RPa zqy|@l^u((~kf{Q2(hD7D^E#wSbWVnPcZR)qIG=wOtNdw_L7FQ`7+?TiDYscx!(+=b zyTn(6M!@m+-vO&YKmf4iWWI7>CB5F29#M(r6!ex}sQKr$~yx!VO zn1iqYHZ8LLhK~6_u!4knV%nJmDO*#yu;&whZO&h8|i0HGzi{{>ASbYNI{n~l5qhz^xN>tsnn>8^mA9bX+OnYq+s8N&Q8 zz4m$jpR@}pP#gN^uk7}YZp7lsCxBbJ-l|OI-9i0gllBFi#r~0$1c3eeWWNH#`swNv zN61}474rL*>x-#2;1LbJ~HD2$(`ON!})uMb8mIL#@wNoy@o9ZS+j`+LRPPnXK6p6Jq(@Aht~l+>WqN9 zFFYOwOsV?`&l&D|GE&Qf`RZc(CrNoWT+B*|=>F}SDySKE8;xH5qCuxaBg2vqV55cE zG}5U)HO@_r^U9$2+j5}OY^goE;);LBl+|u}49)F$j=;dm%_!Ja_iiWUlh~HP%eAQQ zKlT;{*NW^uHdisPZRisZt>~)i! z8Fo3iM}OgcMA%+~9TW#jlPgwIA=Hwf)cgH&;tqwlckU6dr3J{(prDFS6XjuG(BQv6 zC>v9&EHdFHm42Dlfq$MiT9dV%VNTP)hg&3|IBE^=H1u}X{9GVtBQ}{PC#*a#Dm!W6 ziz_Tz^DX*7$(rztgSD$$l;rqTHFZA@)(Gd4zlGv{V%ZbPe8G*l(#%TnRIE;ibzcqtU^Ba#}E%52O zKI24x9Iy};S0gNw_Jr^eZwmT?rk{da)shNsN{cD->zWY9BqYrJF8MJtRsHTYMx1h( z3fQ=YmJ|6>Z_Y0PLEhC&{`e4rQJ#dd@8ufbf5^NXFhEOyo6vu*lgB79h}Debd)aye zDU8l7R;Sj@Jjgvq)*ZzE_W1ogb%K`D)wtf><}(Q)E)Nh4N2InKY9M$aT=E!%kEG`5 z+g)B?Nza4-Ge(Jf+o6$RYHG7v(>PmT?e>nw8y^9y=V9i*fV_;Mdha?iAo-il({++o zk>pJ|Zb+`xe6s)v3+6!zF15jY=3ry1KtiZz-%XDsro!TKC9=eY)b8r`ye;%ei~~Bh zc~W_Jg8kXaA)FP8OJ7yjK|xHqTP9RsGCx59cXVv}S_0@8u;YBrH??#AGLOr&BN8ez zdbhEL{N=stal3*B&Rn9G?>)?Y**_AuiA5ewd`;DxS@WjeP-5h-7j}wpOZ2$sE?}Sx z8br2|Hc+-RgSmfmIM`DjSSMGW4rOsVcjwrl7`usmyHaqu)It5Um1&uP?6?A6eX+c| zW7pLAk6YP0ssEMIhV0z6)JBUIZcPA5LaF{l zl%$8QEln!I>ly@jwo`(bfcVG6{GIHcgPx5Wbv)a<_!xA}(HSot_FevkU^PQTKka64 z1|Q(~yK1<|j25k?tfSerSxq~?#Ju2@W{}h~vp@hB3cK0jh%7pnHYglWVaYeB@7wUO zisdo4)~uR_KtPi#V#xsu)FvAyc#F3|hzIittA7`E*3Ul=To7-(-^OJ8r>)EaP`)XK zPuRh1 zzww#p-4h3009s4<;jz+7ftk?!}j=Xu*`5!(jH z#-`>4V!`$GX$EFGdjLAFlpj{+aO1A(ErJ6+WRVrJCn_(3M1I_-7WkR?uj|82Nq)`c z%P)}3Q-Cxm^!v}QwrbdkeL=(zJ{XbwTVKYMOMqBh$d~-xQtQHmh&4ogzQbbSpMQkp z{oDa!z=&2s@Jd_CV>Y|ZYzKUiH1P6)_M;mNkfZ0b&;5gPN`Qt%&rhwJTtPGo0k9<_ zch!F6w!boFv+lhkthoV9;ed9LdQ)e$xr&yxK>kkv4$%7a#6oLQI|&bvHK8uSP5*Be z@Be{$Pp5H{h(@K0BZ_WPKrYv^yFbwXV>CV;2CGKykO>e4P>D|^qA!?esIY*3Y&QO; z1R#|gya{`f%() zO!0D-y(vTAvq}YC3VExY-izsi%Vry~ZZ!F+z;~3pU)7CdUl6HVoqn{RXG9G;BGO%I zvx$24ZubXF_-daUyTq){CRQ;BB3JQZru+I>ie}~er=Epima7I#by=)kxl$qHQ2-;s zpk}tCfI{^X)5oV5#fHgEwGsOoDWSpO{XSCHWj;8lY?L(5pW{TrbY(eI_y@%w?w=+a zwy22SRJWee?WB{Wd$jra!ZaK3NLOZ}X5k7?e;g9z8L-fqyX;vD{D%&OM=s<@!cRCx zZuxR5{wDx=Vq80vFdqCG!>AIb<(gDgJsS}?>)K}oV&uP+O2_?)2y0L1 z=L(=lMN0)S@k;lnsxJAx$GJJThSBf-IJKyl8^w!xFmO>n`Nw$%!#bro?SVx;rXT)s z%t1_f2?=$pv%n5C&A#PX*Pd4laTx=;(k)H1@-T@>Jhr{yA} z3!Mf+nXMtf1H2``(Gw}T&%r6i#(8Xr8S!+Q$c&QKieVxf(MamnMaa?~pGyVi4JOE0 ztJ@=Zq}eWS8wjzG@k(4Th9I8m_@6qk^zFR~W3j`%t32PDdrX zIS__xdrR7n+T`W^>f-VB8h)25DL0{=UF6%+GH`xwkwf%%JIR zN7 zKd#XJ^o$k09-zc?ykgTEC9vEnkOY{vknSKt?PDtg-mzk5D=UaP?tS#R^LiWndNwY% zkl-Ls|GCyysH6>u>&YPRUiMB6+F!`5=NJDvle7`@$T`#FJ)09B3G)tZ1INd=s8U1i zm4~}99LPQAQb-zF;5UoC7fe4`-G8{R_|PRaM&#b@^ySCJLl0vEd99g-IU79Ch0&;N zh**Ct3Mv&p8G7Tik--zA8;J_ey7uNN!OSBqLrmMspiWy>7RQ5xW9M}ggM74db;l_= zKzmZd{5i=JF?lRS9XYYa@yWuECKoqFqY7Iq{`^DFq}yOhx1U6uAUf+_giRi_&5?i} z<)*~^U#&K)A3!+ApR^YLEV9KKDUGHM)@w)Uq&O|xfgkKR#nigS-2^A1Pw91&%CdA! zV|to$_#@415+;rIoi4X0m;7S=h{ZGrGyj}Zi&&#mc z(k1CjC!J-_yfqd*Bn6-{MFs?8?^UjLtxcUX)=GPLtYy;(?ocYhGumS4YHulVVV{6Y z{TNU%H`c3En=Z)lKf=@ti+!K5kB}pf`{jEVMM1pgGqXTI$j^;yo|VZ=N+1}3EPt;S z@3aJtJxqW6Y$@RSAF9qWsI4f@;x7dE7I!P|?hw34aV_prT#E+^6lig$xVyUsDDD)O zLTPb#hh=wXXLrA5CYg8h;oW=x=bYbJ%}yU-o(^uYMNUfwkmUk}s&p|RZprwyBpya{ zuKrXSnc_KoX%j8C_#sMm{cWKtlopSw{eIqeHAyR+7mp!v4qy|Hpm$*1)p?=Bhr~U0 z5|U|vCsCGBLp&#hAU}Yd^c1w>4Qd+G9*Ly|Z3V7JgP`vSe`@ZVXJFu{+^u&Cb+&TV-1 zT~b$^Fu@7Q1t6R(_FHfNBjRbSuwP#X(0^6;osA@NN;IYn(7EEYe2SE`-R~@n3KP;) zB^aMD44K)jUb7jxi&*hoAfi(~r^MZYi+!gXbTIsi&7WoY!D-)QVty`62IDtgimM;Q7pN?tbkOGbuR~bm zNg8-N@Dq&pEkZ1k?bcaI4K8;G9U zx=((a9qZE{yTB1e#%Lipmv)PiuJT?j*r!;t^CpKg0Z-w7iVT!}fU`PzlXy*5OHPE6 z3+i|Wh9c}^U(KAQQMkI-HvW#1p4<_ozx(^!R0LUuA50r=Lwny%R zdAKf9tQf}5hQ_)jlM~AwBTzA5(!j_xfxJPI>AD+r6pMF$m@{)8iFkz+!@RbAp&28r z-{HM2*3_%4+X3#&fSBLym6O!^4GU|fxwm8^#E+pu@kHK7>U;hD)<59jPcbelwfDil z^rE`LaXCbl=k{yw*F1`aD0t_DxH!E95R}@ScK0=F$Y zie9XhgR`?`o@RouiIk(f3^QLbTG$=l&>Zl?ZgQo^ z`%>i2cGrXn!|foT2HHB~G>O!pLS(QZqP>HT(|Ge`g^-HmK%(QIo-IW>L?*%cSLrSn zIBlCskns-w#DoLIADQm|xPC2-3j-5EWy$|s9&T>x*!{#n{;(!E(C>J_h{%K8&#Bt0 zX2}I{B7j8U`-DyR48C_Ir+ogfAuCWPjBa8?b z9ytPRQa#kcJyONOLXXlJEoO!0lO!=9=^mxY|Io7Mum=ty*VQ=O6dUOrE@W%jqGUoU zWk5C27#-g{PZgTT-aIJb< z2bxq4QdtKfS3$6^17ytaa`gZzljATY0Sw$KJrbG0{A-XdY;79gdp*4N;w$T=sf!4c z%;?JBroXM^g zfz)`p&pVG9$n#2fr?)WyHz$5~vbbE(oj{Q4*QpiWF9z+>8-s0c+Ex+F+CH&Rd~SLp zV>okgTrk=web+)LZ&Z)?feUjt8T!2VidLB6|Hgb#`g0iq z`|jowx=AqEL*9Eoh(tK92@&)i<4l&d`Ppo5iesMv_cP6! zHvspQcEP8pD9?T5BFNB9v*QjkWK$RkC?<7dC09oCcrrX_pYlJ)imP<=X6GOLRc`at$~;0_4~?)Gw^IHF^Xi1hdMCp4QeYx z`yWybH_a206V2LNA#&K|+c$DWHMdSg`%8NKHch*(}c;kSdb>2!>ev{Uub_PNV0a39vBJ*JVbB`$XVe zE|(sh<-QM)40;#ulT8DFsq>AnIH`g@*Y*$y5=?;$vKMwNWOMfFL5M*U`PI2JJ8)x&~jh@7f+#65>36xv(n=ijC2+427jUg7VChX}x2 zZz>PGF(_TEgC2&ex}SPvnGpY|NRX(p9@7a0(c9)d}<(HlQb zrf@>s@pqLNnB3{f<2o#&W2caIo`9ZQT1(bxrw-UQnI&MK}-#OuKglA=pJN?OxudrQ@l_) zi}bUJv`H<(zf0-#Vao;HXvBJ-+ zBhyDwZx+SwYcFz*43W{(?8y@_c)h*G8_5x@krF@9p+UYU^HpWWYxTS4ropu_rJa%H~ zK_Skbc}a@ExL5zBKt-syN_Gh`zvk*~3u@T0rOdPxPmaFZn2*OV2T&@S2C2+m`nqX#a@&{MU%U#GuX`p;>`)5WU3ExTB^j1|4pAU@<;&0pnU5uxY#$gfkZ zAa{M&`?G&DTtqp6M6Zl6!$_)Nn2mK0ZnS|{e-qZmna1xiBtB+WbmaEplm5PcLDTr@ z7N>i+-@@$+R^WEi8kJXZd+) zrWOhit+)9JuL_+I&@48XM++s-hYXn!TUcUdbVhEHeVb!wCTN)2fO92#>s1Rha5dY0 zv390lbPcSU7k`&I@MR+vaT8n#5H7>lz@n4VejqFv%BhWu2OIBQF~wB|t1-ds7pp)& z8=Q){GOelHqTo235if2r{n+Cs;NHVSyeJS}*)OVlk(Bh$bJ>?H^dA!2z0IsD-NLxE zah$*mz0=?v8H`1C_#6>OTw%|DixT@09vLq!doFk;aJY~zHpfoo+s=FAm1Ve^!}txx zYoaA2HuHV@0!~yIZsgkyhORg8b;cB{V$gycJ6~ESEi)&T?dpsBzrodRxdXJF3dvof zuhbhNn889GPRfUpSWgrM!la4p^p{(21cGx2VQJ-R9wJBX5h<~P*FPh!zq*x{rPS-E zNxFg@DlRNiZ(Hm?Rto|ar9o(O1gMz#pA53+dS#ox3y5Pe*w*Gb%1&K#uU*pdaQ^^U zu;7r}xLYJxGI>2?&-pu>s#kdm%8;D`M@Yx=rNLdEHn58FA-nT#5e#Mg>*$=r=NzV3R@&UzVK%gz%ikW4*1PUzoG87`*r`oZ6Ok_<;Do9nnUu`l zki%2bM1eV@(z1`$%2E}@DSVqi8mNs;SmB875u2|H__yI;L^9|!od7|V7!5PWzQmAX zn89ixe6=0y?V9@5-4OwNY#d_u;Fz63okmggvUt^E@|4b#egjmbzB3|waWW7C)FijtO z6l9tNe-I*;bEr{-qC3!|sYCD|f_{6!rWLz%BlnUx3&V!vTaOFgZ9J1a<{M^U!d4X& z+O!C^HX-++mcM^6^(AievkB+X-CZM`wbZ;>&)MgNE+f9iR>l-P{VoygW2en$lzW;# z)D=0J%jjxW+UYlAzIxqeLIK>Fp29tfZ10-Q77+?W%XNOl z<+{VhS-A40!riGhEx~YAsTG~KH;u6visfG#kx<1iTjT*++7alRYD*k2c(SrQGZ=l< z=*p|TzO!n^$VdNz!FG-ikv+L|jhp@rDQ6pCI)-GQmt`p?1!?Y2;|}cE)i)7_+N)zj zjgvn$k?XPl(;itytQC7hu9MNC6F`TXfsO&432i+nTwcB>yZZt6L{ayz&wHfYfZz$8 zlvGvZJ7^=ap+}woZ6jeWWosS%fh(G6D~_$5&HBl}lI<8@=O~N9Y@Q4o_;L|Z7kW22 zfi*T&Xz5$eMK&V;n5u31$&tC_Ezkoc`v+8J< z;0nlv4)eaTPn)!8od15POes~jYA6MEvzmLvo;Wk@RcQHne?uKLF_rlV7yaK6)`)mh z2E)+e=Z*I8aRV}F34k8J#hW00T`}gwV6J>289g(%wLUnYjKPc?L=Rl_^>Eu_F08|~ z9j=Mwbw#x6v|3t7cXfFej^TkRkzb~*eBSMqY5`cU@%g5Y&dQ0FE-G2GE&9o79m4bE zBzAMCu4Y{At{sXOI>;dCv(8wNU;J&Q-&@?Y3=zb`NE{6dG6;?lCKEBKA2}s4q33z7 zESq&NowttfCjU;zQ$es%DuoT;J*78lr7%%;lNcqfyvOaG)(&T)ga-z+>Y6^V<)qi? zx1lT8+)2y!2#zBqpL*Dfi4W39MGnt?)6#M;=xShZf9qwV=)$;zJQU$Z5U^svKcHCt zf_MT&zv)D33T#x%lEVUopg*P9TkVAdsuLY*XS;X(e!<~7=DRB&zMT;pDc+CCNILlz zs8scKK)X`wIZ8UHK?c!ef_Fqyg0*Cy69hYeeW#hUS|-->4xGtnaiV@WMD%^8A_pQr`-J;eH#V2c#$XricbzWsB z2SG`OucAm=ty}D>W-8#yRwiq$UCxo=!M^6g}Aorir?-`QE4)Dh-TJTLhIXZu5(3 zJpI`U2Ap3$k_rLU(zz~QC&~3%!o1{od}Tk`5T+zdsM-z-b6vz0W-$KU2_KF&;O9#B z<=}q$z}_Z>B#^txjowPmgxGdcOoMDf1GpPt^0VBq26WThAXvPb#~D|;m{V>T zP8B4$*|lqrm%5V$@S~aKYWojEWA-3H*kK9*D@`P(s8R;Z`Y{@jAwl_f%0%XGq3fKD zR#Os=v^0?d_HoRc`skro78xX+eQ(D-S39_#S5VT69{Oj^}4o+=D=P%$kSINQ%SFdN(zFbPjezA`JnKq$R=jU4lVRB0D z50Cmwab#}m3>ETG5^L4I3y$GRT-@(pNT9UhRY-ehtNp+HwMvJ=(bZ%x2c3MN<=m}b z`Y9q`VN6H;E0q%nv1!7_@fkO_kwlD*8gP%6Q*9@2e?L{MyW++O(ZXDN=LA7hFDJnN zW6}P%{+(jZ9(9yx4M+X5K0JYBGK06n$>wjAz5H7+9pQbJf>+qIKW%kF#?jRsBB${O z8VmMcaXxUBZd`3Nj*GM`SaE!xYe+?TA6IfWSiUg+2wzZ<<8H z@(EGCuhGljw?pnKTvbuM6H(NNHDrvDN+>L70O!NyFX@}vxj4k|zcMe>6$>c8KGEP8 zdvM7CR=YT&eS1dw$YbK$92ux4t4GU{sDwn+n(u33q*DGEoX`Rb^dZxoP1u)X{uWAU zsLl}>&mXjRUu2PUUfkm8jC)^9au_qsHOmFygr|}872>9{=$z-^}*3HyrSq3P=h=zx?$m;w}?~f!E z?RvCL9+-S32f|UF*iMwpnY8iF;Xr?e+rTy~+Bz-k8M2m*J7K5TKoqFP&w<%MEwNRWGawr8>|!k3LlYcYyfK zywqN+2gXio*Z390QSvYRey2#RdF{E-Dz#J4-}~q#4B3PMzx&^#m<*flWHMhlptESu zQ4|z(v|1&u0*uW`RX?EDKQ_KhtaIu<8saW`>>=gaK}Q^nbnmfbj64YKil+$U-!wjB zHDbY7;?`^usftSVlA?5kI_{9c-{G-7^#00K1x;Ke3|6IR5Xmt00_qoD3)d? z*4PB6QhP)Kndu&i{foAL_KmA!m|D@21xoq?sIH6YV?r|9j`)TGqIGX_&YYIR;vI{T zVreX21dIMUZJJ!Wv)3YnrA7Y^&B_vEjfeBJi2{BE;7gRPHGxu1J zJWO3n_^WyT;hW;vJ-Q9xeB?x(*ov9~2&YDzmM z%UQns4k@BfIUL-wpmSC_d&8?N%&0Kh%%?twa#ZzOoLwrJ9nbjwATADV*7UjNJcXIR zbi&l1Z;Ouq8W;O8q(98-61q%NCEL?j;iZ!ah)`r{+ppA+_8030;)@wV z4$?~>$ftvPP{ezqsoCy8vph)V5etQwOR@RZ)-XYiy%`=7Wi`_5sI%F-iyp?VEvx_{ zuR*LrNG$;wNK5m=yZ|Cp=$Xrd?pc%d?3nlEX3R>p#^7N?;hfpmXvDSo=^`-4LL)*= zMT3j}nTrg$4kbzw{cx-IH|^eU6=?$?f;T4>)gauG!45qC7`mstd#K<+Y1Yo{zQ?p8 zYW#?DJc}g-$OaP;=K6|s5hmg^##sJ9Ev#*5qr~Te5Od)8otE)x$hqnD@@i2FB(h&K zx*80(7(43pF7~b8nq&DEGq#|eD;jEa_XgYwTGQ< z;ngzjuP>H^^nNvV#~{y)u29q8O~44heGlzL3=ko*1A{o0^^qu5cEuf8JRHIz150i= z#vL=fEoNDG;0qZO98n;r{qws|-W9AyaYuj2v-y-mmB0+1I3})>h$!%s3?ohM9p4$jWQ@+~Y6$SiRu9vK96=4xP&%dH3Z~mklk(9%wh6#&{aI5hq zPWQqKC625fd;g;^N;xdqe-w`yvbmnjtJZz>@0vMk3|hG8-Lg1>Yu~4wuDjBL1F<1t zOeyz7gS<>ue;;^VZkNB9p7Q{I8SXbhej_n{sOa~rn{8Rirf;XLNR-s?A3p2MD3c1C z0~>A?aQY+&@yJm3w6=F`Sy%UnB}GQ@+5VX8xZzEsstiXY6nAM?Rex;`L7w@WnooyM zUkjPr_*f2yy$j*LXlE6Bk|WyW*CBym(qC{sb*+|;ILVutGi6=6GBzAup7{OydMtdz zP*tIsmhj0;&3)!?B&Y1c??mMIwUUYpK}!5sUWr+N6w(Z5Lh~9ZuACGSQB1OSEiPNw ze46~y-i3l<;zd@9{`p1PB8WyYiMA3`#x4Hs1G~3NR(&aBt`g=19U{!7w?L5C_*^)-d zYoY_*yDI|FtsJaYKZE|rMf)NGv3oeZp@&dJ)m^&k6#*NIjoDu#b1$-CMfDATe6S>I zF?|D9imzn_Z&yb^FmDWitt4n8g;RUIOJb!*tS+xZ8%zlKiVi(xz-*0kkz1!Pe z$@7D(r@!z>CcJ)Izel}6kc8^E9{!6a8N-(s_OjArZH1;sD%{v=Ue(xYkS;qS47sqv z^QX_@;e;AIw*?S?(!!vnm;2XhKJUpBch0wUjibIdN@?q*`2*7f+R)sbBO-8{Dp1>5 zDdfhWP#wgmH<@C3_shjvg9gzA(b);p!0#O;E`7_C332YBf-i1GgxqwYrAAqolXm21 z3?yJeh1zcgElnQDWRXt3odfciJ)nY4Lb=h+*WE#XRq#sAxeh10Vt=!jCR3ZzCD_Hb z{TGr`ji6vR^mgC5Q}d`}nxvqK-z{v-f{eSvpsiO?Y}QWh5BM`SFgsl9UPZDdYP%|8 zu|+=Gv~Yttyx~gI5L^r~kg8+`B9dIeTItd?W0~L>F)Cnhhi>zbZF} zQ7?+>Gy;4P)E|2r@^dp7#UUJ_&i z#8mzTF#zk_&bmy?oNI>j9b%2uQt1ufZz!taup-Saf{3gW8q{zNSA&u^ba!7+psBuB zBdx_{BK+0s5;3EKaCoa`_=NmiEVhnjB)||)6BaPN9gPG4>p$E&X5t(;IGrqWeQns1 z235^CKg&@kYI`1xp$_`yvI`es@>3ZDy{-3MI2s*VNKM-;4LTR-?dd-}?FQLG@^-(?YwNk_1MQV7=fTcGUCYxOE>u&SMvT%IC{>(|DKv`Z%NQnnO$D`|s`b{ERHPA)7@$AYSsu49AAU_r;~-836xWDM?=66$n5?fMfQKYL;mZ& z7A6#e9Z)FVhy$1EhfF0O%GkTU7o~d;YQ`goNOZ->?A`*rXcmRK^uu=!nO2}(^+kVOeWue~fe>%{7)eFA5${*{-D z&cK+77KqL|-}>uk&65ktpM|n;urD}jf3FSYl)?j6q~2QYItN;)gW}1f6K41s>r4d| z2)|N;v8&y2GY5+ELUJPw?)BdEe8+{zTJEv?GvT!cmJL4(9WRpO1JSN7=G|3NVWlIF ze#;q9^lw@iAs5t$y8s_Idt~`+MC(_+clH=BTB0NnvS`;Y8chz80}geh{j3g@Cj9yNvRFGr$+9YwS)eQ+ElYE|8CmT^=cr)zARZ#!JPGxvK_3S<#C@>RZd z|0ueLjIP@&scdK{EFTZ_8C^*|aRC zhclU)%5kLgHH9nyadnVm7L{gRGcE;*ZsS#nZDbp+t`2EaOwUjXp(G+l0ezu$k`9yW zeCgZ-2GbTE%31+7IBvDIlFcZKBGMz5GqUw2tLHNEgBmY3{au?ej@h_KLRE#tn9>4P zjBB6hP;qVeMJX`wi7@Dq>dkh@EK34)=)RuFWs?}<3#5jzVbN=qh1L=s3G`C z-4`1nY{;`S2Yn>UFMe29Q4;_Hyr=NGHb1)ed2U^FHNPWh@hZ=5`3?#i{y8KiRwxC; zp1t}zigdhV7`HkCM4RH;jP~|b6ut;Bf1wcMkj@1!xZ;PLl-o}(jgo`DL~k$O9)3P6(O0Kd=y-V(r?ud{iJ zUs!p-wD?bB#Y>YwUhtU%c$WuThjWvsyDA3VB>A`5sPl98I!cV48YXSXt7|~l+Z-!d zN&NAY+&kFy$Mx^I1y(TOTf$dn#+1oza|$); z6W-{jjt_QE!H&TtWrj2UXo_<`4X*3I4E}Jbjoc6R$&iRc=5;hQQ}hLVP+Af0daZ~K z=PknS-ak?Mlm3(Jq^9mmQgaGJjZMWVRm)*aRrl>82}{3+yTepg$XQ!w$KTQ;`P zW$28|DKt4#Y9m^@BcvtJX$S$|j;`+vB!cRka&><=E2HFp5dtE=Vsfe1CRXB+uxL3PT05mZX44WN3HP zy(A(f7DvWJvzpszRxOf+8nHU0QYVqG^E*gB3Tb*(cIN5lHE8uGw=e;weWLHa(d3+m zR}mx59X3pN-0|%dG2ri#iUzs`yUz8buZQ5aZpW0CZI?@Z=TOn}^Jt`J%f_%rGC#lZqCx22PyVnw^n?8?9ajnv9Qh&aP0v~X)}p%+ozm*Bzo;x z<*vBQf#-;-nlzPfP6{}m_>a-%RM8N1g}lzd+zP9%;sK@-FH~EemNT^7AKrOy3ej6# z0%D!3aL}_;cbG$&y{L9)^YQEC<*wOC?!t#~;2s792N$e1iX8TK1~zeW7z1 ztoTh@tvV5AZ4Zr0xZLtCxwfH{&G7TAx!_!>fQ99HPbzIZjW=-7v&rT)RwCU^J@<9- zxmjKMRvIOEiJdlFq<*)n_`_Yr_&gI$Kpd3*{#g>qIfz)lhpyrMV)L&3abFUMaPj5W zM22e`_wt{epy7F|1_((>0h*MI132|2MV;|0cb(HjLvs=@a;`0?1PVHu3d3tuXa9}kTcT3V5+AiwPti6p%kyob zGJY4^<{gE7gP*l5Jl;dbvBTkK4Qg3~1!(yLG+V>e=-F|^Q&3fNeKqCbN7=BH1~?^L zCWb#pU{qiPKF7fMH zi`7s(N`h!ZJRUnp37xIZWF}>&6q9Q{M-iMK^{|`ou|J##4SeKyDJ<*a&3R>rk(D{f z)#Z>=?r(T8ziTSejOOPakr-mX8I9Gm%t?}|+dr@Wy4NwjN=o_{8Inn@B51t-;YaaM zqex2VDD-9mp-EdRnicsC%snjEmCM=!vBA8`XTCjvn|?^N4!34TukeNoa0=ee*+GH6 zPy^SQp@RB&L~AsBx7&XgySthUetop}^JOc)0=9j%QT(3MC&BouUaot89?NgzxPZU# zMe>GZL~8@Hr~z=;nsX4W4UK>28NZslN@gmRa4|n6`HgxhNUX$Pg9)__GdZoVFUnUz z94C7^0p<*}7^1_&Z7u8#<7_#}`)Cp?K*{#uUESkRN^`o`SR_ZCp3{&L! z7gMoREW!B0!8s;_1zhTqYN)9h9VGAqjMN-579#I=z|xq`=0n=Rd5naS zX;|ATYWZN$RSS}bgOtB=#bu0;Ye zFp3M9*Ut(pRXAQ}`v$8tE~41N19>Ef3$W|+O-Hgn1EaN=sEme{q26{f}7hAM*s!}4G zF;y|bLlF0Afl6Ey?mL(RHzUB=)p_~mulJnGIxjkeArBbnpqc4O>XJW~`Pox0m}|iK zDBNia#QXHY0duUOYUQ(B&?O{C1q2Rz6}XdF0I>OeH2KOW05{$&AghUJi=}(V?PIpXejkkg%I>P#+Mlr4I}jY4Gqm% z63s9s(FT(TGlVCZR~vg}@g1?@Y#-OzG1 z^iLwi&RdmlK!9akK4bLk++pLZpiqkkU~SEN`dml952pk6PF!x1v*SdkK0Cd>(qda+ zB_D8WV8J9mu%#yAst*Yg6^ds32?{Eo1lknQ+occPivIJ{E0MzOQ2 zm`b0{s7-<%fNY^c2;kn??E0a9|2z$oo@7JlE>~qlPkhh0?2tWukxAdk3VhMW0!qL? zWRN&)z`pq}8}qd7|GuQ~*q%le4sKmJ$liaUjNqvnvbHy7#ryJ~l{QoBpPqyrg54tB zk_{9^LXA&C`$pCrZO!MRtvm!6$~w2a@x8Dir=8lrch<8>+|7Qla~v?!Er>Dj?J(Oh zSz<}H8_}cm@An~vKb-OOeP`UD8%c5LLLH!GD&6nX!Bbi6!Vg^%`W+J6Ao1##DuU0m zsPF>wkdc<$X1^5wV*GOdbOuuTO`ze4u{BVMF|=uf36o`PiWS?d+thhU1zzzI4ZPw< zxj2detf&Uc;&sBAg3q~>auY$_fpw2UmxD+(RY-^(;;um@rgq8|LKCnByYR^(wO$IG z*F7@4?c{eULTJ_M5s0HY-~JQv*t9>%pBSvEZmX5Oy&KVh&qjcKxp?5PDLzHk^DWQ? zKx~bkiPpOhz`|3Xe%W(p;(asTh2IKAhB_QLAT3gH$o-CB^{>wJ+<$0SvxXz&EG_l! zdn+L}L?ZV1JUPuE){c|-YX0tPT^=t~)#T%ObpN zl2NF=8ex0g8A_}jepAmz5bpwYVn6&R6Q!eFy|kfY=Y3-_-9Njdg_vQASPm^DlL5ux z5~<9350Lx+mN~LMehmB<=q~xXZ%q+et^C6hB7@2_<8WcPoo8agP$6OxhHkp9UVLtO z?)_|0y@H=;5o=xwFYHTbwX($QKjaq$WwYlB%VSfUh7TLx+B7l^vZQ(_(xXVeLtlH+ znC#TA5xf#_;`D=VG&|vY45{!qBFGvrsC7><>HT@K-g~ddxX1A10*6h~YAhgvs$^tD z^)&&USKj4JFFm3lZzEjyAwg~R=}lzbXDW`Z;&IW#A?<^lf7?8=V+!(Oke-0NsF;8l zLa0jIr}X4rMvpMWq?C?yGDhagzBgP|nTfIcRH z!gt3*tC|>y(QP!?RPMl&Yjg0echiio8$WgfQJb${0dW3{N6>0?c=41heI_aC{dqDz zt^jVTz_gZv(oKXja7lzDg^6cS9Ykga20kw$w}@w<}@CuT1gt=>2eTKWTiXZdcsfMQZ({oOVd1wAI#(3;vCBIrMi1m1d1hb_@`ktRxM`q$&J z$k&!`18hquzhcNb?Q5i@?5;l!MCd5&N<6CAQwowUe9%y1FdpIE1<*xzlD8L>HWCfZ zzx=E$N<<>mL)i|&DMf)j6ce=7_$lh8Z?D#(A5slb!BIN0&B8szeOy<5{0>J;En_M9 zQQ~!#uQS!xX{LX=z#sJFc9a@MKd48B*Vce*HTtRhnVu74aan3m9a(|ydjVipE=rlz zHFA^BffTn8Niq2VJqh8B>*r|r0KDUFaKkjAIti+ zHBIk;I*nw7{J7ochWCkrUoggJrZl8Ukr9}} zLk7G^O*gnIm)h;GD}6vq(P{Ri-FAa-Isfj|7bi}(rZjh;XgR4|z&gDkq_GtbR{#&t z$WjJdwM1Z&nHVg$af!4&&%_C}^Ih~jp7QXHXBV|^E;+@2}lUz6m4hDA*t9?5?k+sKRLmJQw!>~cDk)bE$Z+Dy(?17 zAFqS~Y7-2lAf4Bx9Bbx#<`C7H)nkly65R%;#ul^-&lK-8BfvVmPSKe~D_1oZvKT{- ze$3V&!c1{Ic9Y>PoY*1VG8(S6_D@GO-%{%uv`MU~8{*d-7lx1Vm3A{;uP0%+{gO-! zs9`%yxAK<#aP!b0xHwEQ7?*jm6%#mAnq82>ZH`r__H|9?286I7rRU;L3am8XWrMvF zjx_Q@FH`~5h$xo#r5|Hrj1K2;lATsjuTlettaUPG{GiM7_Mu-6lX*9$`{2RS59eG9 zkVw*cyW8~3UuR7181ZXRf-**^vo)J~OEqsRySmJ>6Qan9pb;I?K99xe8;{VAEQY z0fFoa9AzDO&fqlBhvtQ1tznB;ej4tmwfqpZ4bTJ*hD}UlmTwQ?>xf|4t-I;_PuDT& zQ6%Dm^=-%2cQNe9zz^*jEM)eiNb7|TRM>h&jKEl6>odBnY@id#MS0SxS!XM#?*D(m}EzJpZDB+_&L(<^WS2S{JIx$)eU1@EZyzISA;{G-pTe>?(Rb~dBX+O~h|op!kD z+c>9MA_7aE1W2hD2$#((rQfMgcj5rl6ioB~p3|b?9rDs%`NQ)ckD1p7gf_$tXthN+ z4ZT7Ib40OK5aK(*YWlD0tc2{Kj3i7INIoP?^|uv*-z5o!<#`>+0tsXIk&)R(xy# zHT{sCr~cu3%Q~DW_K#l4EZROh3?{T9T0R)qZ}dJQ)a+;8lDSIFx3|q#vDyWPFRuiD zuyL~T*CuDDST^T=;+J9*H(JZ)-nb71Zo?7xKUOfCUNs|fB(hG2K77mzQvCFKu2N=B zr%Xz^w`i?7&rQc`W*{aU1!`^*qN4ch(h5HSKQK*JG&BZ2J~22gC6&N-JH&oK$A)S=9HIc=u{7#MsdKV&orY z_EqXNiVnlyQ9K~TI0C1d7U)8o)uu{?H#t7`b11-?w5OBHUoegVpymN@y;K3B@6`<) zds+%2*3zH2HE}i*;9*kV!Ix5w7$!|T5dxQD*@!reBT>{QlIA{N9M-k!TiuA+lC;6OBpHZA*J=(T^X}9-%zTwL2Kkg1LYFov%oaO zE*1qLX~z}=cDd=@j;Q_omP`*ir;?OZtVm*sVX$9n3Sv52vuSHF$dm-?^qa$f3~{gn zkY39Qn2l?50u6OB$a_i!6>F!xN+mwiZm6+Y*fqI-^cl7wsmI-w1f1HDSR+lr!`7FD zOy<2KZ6~}X`NV|Hh+{3h54ArnDeT9x6q9{qIj$(vu*&=cxi`v`j0EQeK{c zwSsHu4dHM<&&6j^H<$9yzbx*D(%$_|w4HRWeRV;T^(2rIdCY3AMU4GgHW~svq&?ec z&`hR4gg*s$(8MlBLE^s$yhEA#m!t3jg>t$8^ZPz7YFh_5&ClVF>v_=+W9_DHOKwCl zld4&rHOg;7b>6csM{i# zfG&cNLJhJ8gF!-%TTU9TV!~tu>TD|xs2cbZBHWJS6cr_J`NhfOeV5UwAvb zXu{h1UX*}s%NK5_!F8qvjTS(X)L$sOO_GYQA_oV6IFMU$8Zu9Cs1pcswSFmiRbXL5 zPi`}3SY)Dw21ze|Wzv2DJCVoij90@rV)*9B>%6!CsqOo@gVSnRofIAxNG67flaVE6 zG$FtLV;fahJ+MI*;J}&GERnR{X^ymSucstCc#l%+%5gV%0PwEx?&X)YuT z?Ea&Qd#zE$6zeCsM4|n5_y)0t+W#g&czgCpmPi->?1M`|6^1=|{M~v&z~CAcJ!W%H z^$4eY>F#tmK1bc%`$%bitb{eHhc%&kr&^)yA8WD{T_^f)C?fXal-~9EWO-nLUOO&l z<^x8Z_F1Bm(tQ{HjE;WA&ZF5dWw%v!)^gw(=4+ub!~76rQK^GO_Vk?6XQVb~@BB!% zL^I`2RT{P7zlVgua>X7829EG7_sfvpdgBc^SH46q^wDvE<(QfaI(btDfR*YO9y6c- zJAf=FYb{8%*t}|~Sh|M=pD%STNOf!1(z#1IiCwU}jLOV35m-h3FeR>q z;WN&zOH^@2j`HWV?T$OEv9gyJXS2O>v{CMqnp_oBF)=12)AK=$%vU<%|+ z$hD zE_1A&-x@j4h8OlVP*wy>JSdQ8YiKFV|LX+k?RCK?5d&hm;Mr2H`aC^-T=L{0=A;Gp zm?`?5??wRuoW?@(h(mfjNWe?H@7y78_tz(3{<5VJ?GHc2@lNr4c@!+L*dxv4>RGo& znnbQ5C`ra^SPwBcjvaZ0S)HDTraqN#T&DgaS-1TuU)ztdWnIfxm40ex0o*(Hx52xbSw}gS;y|A(5;tec7Fm}54z#vx z{-TY%#t6M@lA-#@5aB}OSW&&{(k(pmefL}I`2+U6+I!Z0U*~xpM*#P{==3Q|tn;>Q8GTPe%jryQ)Xd+V zgH`ku{5`D;Wg#^1halvbRcR%D_*m>r^Z}a{U(XL%pSeH>$A-=d ztiO2B5@6d}z}A9tS<@nfxi9o~p#~`pAJUw88}go1w9H0WP%cDc0x#r~iH4NLx@)=X z{IHR~eB3^A8{S(uQf`Zi0(M6Fa+Y~8pf8ZJ^xxN?)G~i3Mg=2JVp-apuTM@(0(#O%c&b zLvi>RY)+PvQkO)`@gGd1u1^^rfBDVrH z64AmN7o1D)e_}%J#>K`Rl)Zvi>Oke+`H+& z$iO{jQ9_CvhOy}{01{&@_h?e;>-|^*mH2%+oY*7wA|2{Q8R(?XC2p)R$sf?@JpveqEY;jtAep{DjZdPn>7u37tY!>ZVh7EtkiLcPu z!f_=d$gIcRUllXQ%7p$hy83t_X9BPylE?Q(wWG-8g&RO!?h3S+){Tsu%Q(M48o*Sg zQSUee50D>_{@8V#kpBWqpST{_Y3`=f36vm=$Z8B&DB86<`VIm$3c1FoLe-p2L{vMQ zf2w`F4U?|UTk;HAYNLdP}$D&1XjmJg?r;qruAg09|g zE5C1}@RR$ID+^s&=R2x#3P9Par}!)Hhk3LE+ZK_V@9AMP*Cl&EWlXgh$0yUB82zW|w#ESlzNd#_sL;F;H+b z?vqQYD9=L#R&7`U#UgVW#>N(6h1_H3; zk8Jp(=J2Jn?fr8{@ZXH#t)wM8#>kKQGMiH=miNQS|E-m@(~*p4@>KINzr(0EEJMOB ze1vjyH$B!L#_a`F$^42u9*g0?0Z^?PaQz6reVLW#L2HG1Fvcccryo3K|H>VSlF*rt zHCL4&DJ#(@)}omAIOnkcz=Zzlo0r!D>}T>Ot?oy8WI+0v%PH1O+4UiYW|kdElS9vzXvf$0B}(NrSGEMKY2DA zt)X4{BQE+9#e-Lq#X(mu9F8S=DCDBOFyE&%t%_rUgWoSJ7^PG|!uUzp^Ln~U-l})J zbU)ntS8+gv#NZ|XU>1DZ=3UVLjRjdLJKOIsqRZOrWjm`qmN7)+wXCasYcG$TwFp;x z;pcM|2y*~K$hXbYe$KcK2icRv#L6)SbX^UtUGyykHq$8k!isKiIoF_nIX}#m@7{6h zj{n`)EBqO!7p!xGD&S*Yhd#3-f%%Zy8$_%-n*U$pWE(l4oc$uWSl`1-U*pWk`CEHS z(eYKW9aVZbT?J9JK=2P;5W|rFpDRIV4RE}VG7mn`r5#kb#9-y?c|qnjRI=6HPApLP z$7yX178OtG9h2byD^JgC0KvZC{To+Mc3WJxcSb@FKu=JG5JW%9DA_+wrvW7_P_CoS zrS-_j!rGcq{n}`wvE6?K08|wxf-mB`WE0+wY1j?*H9T5vv z!g3=3XTUSh8~D3GSV^JRjonZ|_&g=>IzYpk{184spt9id7DQf|^)G)$lTRR)O~ZAHXsHtW?G zRz>t+0_Is@SNS5GIHqdpZNmfXD??K+V!xIA#lg~$lO{+%U?ioxU9E1M>`-_Bg;eDE z9g(ptKq=PqUXN`87agjZ&=ypj`fq#**#O~XwCX>ucEUyZ)z0H~KuK_kmM#b$V2ZK} zdaNrIz!C33^KG)Yn#J}J6rb|7U>$M#wp^ofZDdPKIWrDafUSmt6fMy-`PE!R6l~j3 za-ZW+?L(r`?EZN0bHZk<=JNMJ&X50yg#e5W8|R7MYuah$1juhLY0TkmF{gXc!QQ|1 zp$!^UlY|f!>W5zs;0jnWDBqMQYsMANsgLF4A0_=|l(oE-nIh#y{Shf`8#NXLn`&C& zy=!O7Uv;aRMPreXsCfC`*acn8TvXUq?myHO%yE` zqOLrg+$?l870QdfzN=TN0k7^ffiZGK2vMXsV`^$>p-xEphGeM~(XDlKMj4jj!|M~& zf$TT17w``e{A&&#a_S8m3q)W7QkP%r^c6LS^3u;3beYGtG8OGv2lYLw5$Br|ZAiZLZnUuQphFy? zKjb~?8Ze987Hw%a@|<;E<5auUdjvPdR0MrG#lNTFT=^*g&=v7pX0*eFKmx#ORwy6< zGk{mEfG`Msh<`LMv9V5xvk`PF`X;?)wMjZVH%Gzd+U&{t*0z-GNg86i@aJWYi0H8f znZG;**j+f5ViWDOIQfofoCI5oA6G12d@$4e>zas6G;Dj7)0iNTDM}r{2f!h}PRt*O z=r`UF4lGZfj`tVFjHt)D!uMOund$U;|}GUX)cHWrfD z#&n9oKSy6M8fN1={YMXEIJM4p1D8S-p|wyr^^u(*(wzNce^SaD0sy?L$Epypgxy^SR<8~FugJhCr7 z+Qkn+=hP#%riahJbN8+UE!Nm>gKrppuf0$ zQiBz2nWp<|i?2RkUdIHw*TkT7TN6D-y>SultO!>l{>+6KVXb@ay2ePd=6(f|7AxcL z?3pci)j5c`?s){eoPRLYQC9oPL6US<1a4B;VI*YH$NXLK7xeWlMCRdIL z1y^d3w&PwIp&^S@8mImr>oEHw;u`~hFjk-)_)*CTkKeXFUa|NHKj0q2f;7(YUFGrA zpI^DO6qVq&YR5MHoIT?ya?^`1PTfy0OWmvh`f9Y8c@ALDXitl_FteiqN7FuA@cy)N z-(nMW!a=;qp_m!wynUNRl2F`yKx`A00W%K^t?I80W-K9hZ=*0HE1>2Oq-Y0W>OYNq zMVg{rqU!Wge5(R->T{0cy9lnoetRo689uWfpS#tMLhhouF~8~ZPRZ{NX*}bU8@v;N z8t4$wvGjVud2b-mp-%nMi`iir+M3@c(3mHR ze%98O|3ZaKnxc?AJ&Ett9R7TZ@^x6LysVK5a1nO)yAO#hxdv7bs?z-IrS=22(QC$I zY-Fng*c^RfVXmuEx$wA;Z54y?H`634VS)o{4{`v>C|hQ3*WspW_z#g{CfernenZrf zI!$uZwaF|aNb}9BKN(48-C9OvDH9iP@194PeALrAu{n=IVt2PXc&al*viDSS+{3ONQ zI45K<;1jtCBsUwkWnj%MnbneI0KO|NcAl&C@*I)9re0*YCBEp+sm%Pxr zXSYone7LsC0X#jC^{Dk2@v$IV19YvwKA>U6yXAg0^L%!PGnOg+lp=CV>CzQ+0rH+g zF)P^rau&N(PbkRX%9C_<_)`G|ikB&JV}*yU47AWBGX)uviy3(xzv>}ZtaFfs4LXL6^Mzo*dBPL)djV#6 z0Jv{OYGFXXU~hHB0}-~M{fGM3vc9}I)*@?F1`?Jd7*y#-DL0O3ykuQ?>8AbG_ z9UBDrrkyrfv)6=wRYQU1Jg|d)p{NYh@D5O|kiuPTD{X-#18xOTGJQsi@hW)9LAcg z^Kvib-S0mah68Y1OkHX7t~ekgeyO|jK-Sz@xakqLQi(Vv=0BfD7BjR}b-ZzTMPSmC zyAChf>*J?3sr=K+ISk~Vs=hsC3X!L@{)HIzJ`NorlE2*WG-&|tG)}S+f5{eZvZ_p9 z{r9nmL5RlY`d{mXfx+MDOR*;4&3@9}Y4zR(8~$t=76oL{hz$x)ocE(LdmFI?P_LnbauBLcy0u|-r%aKo)9#5Stgos3lPlxb!+)urW zhS$qTkpFB$1yG5dE`vQ~1&bm@tk0FiLMPs@DUwSDighm^8pt}T?&?kPy^+spzp6{} z*aYT1({|oH0*K9zvi@Y^Q(?I8uLNpZb~3))>YS~iApnXwP`l|rt3lIb{g-8&Szi?I zt%Of!L4-$*JP^mMxc3(R5M?YKUU}Av_dOkg9Q54(@w!0{_YMndq@|DC1207%pKWfU zmph9}(~G!X_*Pj7jE%3oM8yAD7aA`fuvsx}U#3~W#aK9sKnmbe>Qw%uPnFp}Vf(x! zu3YHg4map}III5{hf6ksea@QMh3NZKc=SEJ%s6mF>5D;bfWVFf5_>_O3k+3A*THFX zklIy7CPPu@^42}r!B~ZvbGQw%o2?tbA*KPNZ(ZNhxZzfwZPHG?Nj~!cabl`G#!kP2 zbrFnfO!SW$u*#EO4?Z~NBhg4SyxvtR0VqZkSVsg5{@kB~TP&B{h0SEO>QlN;+QWwF zYA;PeF%;>O@r53=5ybC%F=7lC3*eaF|Jz&M0t&PZ_CnB-4|3gr2`|?)oMH80PSJ5H zGyQz!q@H5rZ`Q{DHDEz^#Q+EMNyKk%DJ0D^d`fR!c=l#J zaLz7~zrIx*_Vz9ab6IgrJh))yz%(&->|@GzxzM~VrB4s#kApFO9Dv2u z<${uyvg?JouNv`(SCaci2KlAb>)7os|W`qtbITA&!@4tz(zl%!uy*hSZ953*eo91N#3N|DiZ4-sQgAh6S zLt?1Dtl!_*S_n%PnR^iB5c*oh$=i)jTjw|)O92m}U{R^<&B74%7T-Y@0{!?oWNSMR zB4Lq8xN_XA`X-}p#CSd2~*&i%ZY+tJuC zw*0V7Bv0AjTLg+F44na^>Q3d00i%V2u9a)s#MzJSD<^+broEQ8n|5kYMD6;fB3QM3 zdZ=ZPa5*K+>znYh3t1(108j2gatpkGM8TlTVEeY|_`Harf<~axOL7kByzrR(oD)j+7mEM-iM1{YN16 zyv)d#QzUlC!wLTL*>MWVKN3G}un_2@9z1))CfFNtj>UXIiC1{@03ww-#4tx@+Dfrz zFkIex5ua`&hpux1a0VTe)&D-#iQc~arcbY%o_&6?stGGC4n_Qhr7eoIiHWG}!Zcbd zXeN`$8}6(zrbYdu^@TNX_*^hj;0D%V%i*+Ik@a2x8|P>FH`)xvA4`xDCI1_dF7L%q z-ddJ))=G0_x;Zs?rj=V+&Ns! zLDPr{VC8~-D=U+}UHgYzXsv+Q zo%qhBuv`gzNk)v2As`DjTfjf{${z(bN(%Coj_SHJOic3wD&8XdEF}I!`(Sqyf3csK z%R>UA|JE@GiIGaGK*)}(fNMUU+`z3@nBgRYga8je09kD}aMDDwAD4E+IpJH)IlTtj zBkslAfO}%qGDwu0J;8hUhaXgR`#puUb6qt`gD+WYRw__J2TkcilD)FaI$Ls_gL z&IZWPB5@J}u0sKG4fCypwOr}?WQM`M74@&C&kp;7p79)32U80;VG0_|dq1>;&J#iC zcq_P(>p|c0BQ=&2VLis2>WH^g&4a0g2GDIU(w0w)5bm5;OEM0rEe90xMa74Qvizl% zhGZM(IEzYTu-+OFKMq7m8(1v;?^0UIQ$cHQbb%UJf5ryt3WlSb4^yJmAqWUJL7#jP zV|;6ZmexO0R1X57t4O8DI-Ob{`R)-Y~=na5N$>yrYe)bB%wvGde+y> zJUJ+@vB$Qq{fvO+?IaawaS`gD8&GG*IPq^4oGZDMv7N-*74TO{SdsZp(H(yc5!C*w z)Ma=zbn2MGpw@fnDzMBKj38b@^j8?2{qE)r7w6br5|Qv;ft&M1&At13yhHn$Je-_U zd@3DrwR{Cg8xW*B1;=MN#1pfz{ADidG(7V_Mmp$5tR8!`5fs0qfZvUNIM|Rr^j;$P z3MvRejN@XkrGMuB#t!Hp!K(0Ls(U*Fbi@?NeI+EK51U_(uj2U3k1Asne@%l@Qb~kG z2dP7LauxqE1O@w;X(WRd$)t#Z)H~*($_?Rq%uN{yjkby0{k#!82+7Ejui#;Uu9mqh z0BjDZ9QL7(hbT803`@M#@RA5gjC_GJ#@(*RC8}PQTcmP86=uaD+_rpYIQy)srs`a; z_#=!H-Pob|uRY!;h#aE!M~62yagN$2iqy_BPV$OmmJewkcRxCg_2*y46D}DQ{BWTC z4ss(>Rr%>EiTEZ)%%0A%TfiYl!^BFE;~0eKL()NVeiM(ef712)e>YMJ&w21Rx5GY*g6OYz-vXt_Oz&Mb7vweg#HtD)C0TK|)KJ~=fDmULy7A$*Nar^`bQO_ z{xtopQ0bvdIy$4#s{BLcH6<+LVbZ3j@O&FWcd|o&N>1V&dT9t{e6Kq=+oDaVzE_v%8bxrXo_rPbj+AN4DZ z*|$1aW;n;g&$c~?8xn!SKn*x>!S8l)D!kQTxC{IiOkCEw)?+iD1>gVo{x*a7BTRS+ z8SYxd$oggJ=7R@$UzQzs<3+$vQL|wte0J#-At+Jv_H79tRc>+PoVyhs=|7Hbz#Uce zyrxKsKkFO;^oZIR%YU6od`Um;;bwmb{|VssdGcc9*#?9WjJxx~g7atc6yRzrlnFok z9qC3feeCZKzIUHjXRa!>$)UmAY#Idio|k<R>9o4P%++WJPpp_L!q(OhB^QysR0X zLScN60)UwnMsB!F-NQY(Efk;0a~#0SMXt|*(;!3GH1=?cYNzt|8hRma)cX6Es5IKG zO@IKvgWSX;ZBV$}HCTfl|J+clKm2G>!JS<+3Ee5rJM~^RoP<00I!sG&6ySX$PM!C^SO8n8KW(y~U z7}0lyQHpohncj_*j?8Ht6BLLhfJ!lkjr>+tb1%O@7k5mcir{e2a&+qtv6}oHw z*C-1fCdDluy^G|4;!)bG2DLLB&mh=%`sHSvlR5qBn{BzSqF)MN=I4eCPia9Bn;(&T zkE#2%;?mB=otZThv9P<)9pMVU2}PxLM1r$!7f=Tb@^Yv;(BaW%Bp$ZpmB2fP)x1ob zQ@c_|$8nixzfk&;rM3;;4eUQ^lLWt#q&Hg6T;~Xh16D%m6@L~b;l2vnW3_yXt7GHd z_scau*;Vz&pk!4Af`^K^ml7BaXG~d3snh{=Xn3Lf$I<#_MY(*p4sz+{$pciPw0&Ij zPtn+8f+_ghX~xqa!nfGEus{5o5rcS}QQM{L`(050A#ZM4Vl(rPf3$v{4Ntgkqf~G? zMs0q5W2z>>dv%`$b^OR%g?H1@;=eTMcnxdInml?-U@XZK>I6awpM#K%_g(Gy%Z#L0 za(hVy=M}=r-KW|z0sr`Ya1lCgLJ23asc!2|slptUjQRd+IH7e3o!LjB=>iP|XTcG) z2OXh(HVo^N&%mrM%2-j}O`c&`hV#MGZTdyYTEYbdTs5~k#;KjLe6`3ZVy;zhU}m} zY-!E`-$I|&swRJ>!~^cv0nl}2CNC1(9;g`}RKQjq2(~Hne>tmO;rsH{!DhiBjk!;S zHFU{K^k}_zw;Ex|CS(YZ@}z5Jv!@4;fwnp`0Nm#Eb?#iSiCKW8*D_$AgH{g5v)vJ#d_;jSo1cBdp^ARx(HGE-O0{a?1T z4Qh{>@m&7}d*a2w39t$~*zHBQNYG?>`Q?(hW3kKf4qlbx8qX!#hMKy;R6;1gM}R9} zCO+|)_1k^Scab`sV|E?Pj~5q}0Au=v536!gATa47-nchdHerGqIlYQ}J1o$Cb0cTI z#G?e_E4^L(j6q2TczbL7iD2tzys9l*CSuLez_lqe7t)Po|1)DMot&!ypyWdP>E#DQ zX!elze6YiWM({sms*AjjfgeMfp{a4lkQArrputF%SKuSNTfd`I_v-8BW=`i!x=$&>oOsBTMA;dNS(OZ^NGPFZuf-~Pn@m(qYzOD|D%T5?0> z;Bpp3V{pF$U8vBv3}!MJt01i0Wb)q#TVCFB=EfP;+gOoC$n`eU{$uVwyzn8&a5?v; ziry75qo#XWc0~OYsv**1&Ri8s8S0XNE@OfWz(58?2Q4<#SlQx)d3SstE}YM8gLA#S zZ$hv-~Phoz`AGH$!jYZa&!nW_BOo zyy;jj%@LUE3xb*OZd_E3pVG=|go58^FpB@^I=&kuzWl*L0|#hgLQ|y~N70cFtd)Fr zrB0ePhp$sUkUcx-2=GS*K78CnVjSpjA|$pL)zu0` zhmMaiw&fkp5(Fv#5zCQ8SzSEhk4ALLLT|+fxXi2pq1*wHu3qWP>rhXmC`&&{a&EmN zv@-@JxOXLE(fe0ImAZGUmDAFd$ZG-uN3$({cp1nUm;cGb*cTtsMfEvVx~x%HC-T40 z_lHC*%UtB@rM<2@+u3L);Sa4%1v%vUF3*NgmUDcwTP6iz`b>^S(sMBwk7RUFn*D}+ z|E?2yo$X)r}(i(%3;3r~rz-9M0*SM@q;YMLl zh2y8@z>r&lUl{7X?kbcVJj3}6UV?54_hGS-`yn zW_(BBr`1uNqr{RAl)t`K?{GbEX$fM+`VKWKCjm=?b3~`#kS9MukVa3QqZiPjUcyhowIp)Ci zJX}^E(pl9OKRFi(S0CLcMF49!-5;unV0w0Q%PlYXx5cA}x)`rHrI}b-k4S~*rL*-T z4&o$8865gFTWG~KfWLH*f^VDVUC@CP5Y;p!vx;IuhX5lTaR6Gk?z|pAB zO^6TL#go5-Z0*r2A^~SD=npHiumdS}-C=$$N?C-%mmh-6B;-rY{Q9^qG|u~f=nIiZ zF;5r;TCW~KQ^i{nEuU;6kd?70%SiBnT-BcUvb!i(AUMd9qMhro#c0c9S{-OaWdXxE z7)>Z-r=3P3iZzAJp!`;mvue`RjMoAReK z{OzGEv0708@u0JTLmvpb`e1R!aghJLfjF0{g>iO2Z%I*ev(e9`1^E2{SK;6bLIyaf z6qrsRVo1iGt-kC(OjBp z?d9LaNFGTN(v1KZI<&i#=p8JGyc|lWqD45ns9OQk+!%35l)q0;X%|BmSyFF8&pE;! zcSv6nESAl|A=ckx%ZhD#BWhuoKm`IFACwW+T*jg z$1bcDJ0|xz?MyLteq&pD5IM22ciN^@b&X^8FKKpKj<%%3POn0PN|Wz8*FwXU!qenT z`7wv~X>w}%(IAL2=qiPaXsmLG$nf!ZLv=Mf6APDEiw4N#JT-PQlb!jYIOun4-(VPy=v@0%HCLaO8F-)AwF*!Slmke0<5+_=bw!$P1NJogP zN3fN6#X=tG_%ip?QJi-h&PrRYp&FH7=v%^6hP^xPp4O-ZA%Oh$WSCkhX_6U%KdW2r zZ8vS76>-9(fV^5?!EkCp#R>_|!sogs9~3DRLl^S z8JJ-8EZ!y%TzswM4Z<>}MimW~dYSOO-l#K&{PD$1=ZcpG)m}0qO<(azfe|@d=SkL< z7lK<41mkgH@1eg?ks;3CH@m!!Eu{M1+DH00ixaoa7kWaMRJ*eO_Gb&&(nF-Qw7N6m zUirns*@5HR(?1#r$ICZs6OYQ;X=`oKzTZaqOP;fki$9^$agvS-oJa8ijZ=JgEjLeP z)H_@d6#oK!fqHXA2CthecEdZ$riKNA-(qBl#$}*^yr0)*Xos{8I9DHL|I4{3 z**K08eD=_g{P^AJ zk5+b**ZAr`UjC+WtYYXrbW$UY7mVBM!3|-|*<+By)>OPtxo$+H{<3$C-`DWlMcErA zq<@l_Ha7~f8vcdZ;ZycVu{|-|SM1|kVr9H0X>Fp=Q@Zl5e9%iHAo^_!QcYd=$Y{e8 z`4rH_D&qGXJS9UNm#=*9f&`Ym3Lc5$@>zdO1I5;AGhf02vz&?-DeZH8LX*=ZfV&5#dI}q6d&+ zVQY0dPXFnz_&%e68J>v>y(zX3Q~ZYUW=Yq7Vg_*hEW}^Iv@=MR{6-DLV?x>Q%1c7B zPU|*_XXQYj%(-3y@cwT_-=L!W|4@|1w&@b{M?HcNr#_HC*NK4N?gi+Y8(zq_-{Q9g zGxL@{m_bqCr_f?e&IP5SB4w;>EP)&d@6K%d%9RS9TIpc_i1Mjvs~XVELb%CRca#M; z{-k}tYfxr>7J>>vJ5dv6!5X5Zm-!%xoiiQ`QYAF1m*6ShIy#)@C#2(@-DbB?=7UDZ zTz*ABe78$ZgS45?4n4e1CvDilZxMKKAfOA@3mk%T_$eXI5UiS_pI(n`5{(R@V2A81 zA*8~N%mbn)sICn$@-iVsu1vGmspwO0iF+fOh@2s1_+tc8l&D9Xl$HDG_nIYj!hBu7 zv$_GgwkdMczj5ADN;Ixn=rLN=+&RWX#?$o|px>?Kjw3g>u9xLu`_>cW7Q#@uzXN~y zNvMIzhr|y|>>yj_#cF_RCfyz6^)PgCB5;I@9BiQ){!U{l4aE`P(+RHk-+(U_F@tA< zoJ+oNDq1a0ZBk@9k!(f0LPBo_dikry#q+&fs!V&C9rN8n*fLW`IwwLL{rY+I$g&wE z75a>QY=q7nIT`eRFHFvH*&HJXYyutnUF$9KWu*eH_c2iCXA}PYu0vL|NsK_Q0q~Ah z&3)yxP=qRBeE(dWAq}H_`R?;fdObRJ64_Cl4#pAF`r3DsUO}fVvEm-ZfD$_R!5rm!HT|$^UsPAJ@@)z|Zo_`lH2QN`Qr#!`G$JznXd^XXR z@Hu0s&_x8agQ=-Vgo#XP2I4#N93}h&F7gAG^+)eR;6r)?Kifo<6BR_Nr(v)M^p;1a zMyJ0b3^Wnb$6XD3HSrh2ZsLfp$}&c6aclNi5q?3*!NAZ^xs1Qt3O&guqeE$3K72e&WC#AsfI-vhXnn35fPH~@Pgf3YzUfAE-K zG>Fvc$xq2#Tu5g&;yp;&QVqz^C-~2ify-~$JE#)B4zn1Or6QI~^e>~><>e|9L;c#% z*FDFc!!j%$$b{=ebIs{4Z5QGuJRG(=&PjW;D$w8IcYY%Db>!*4j=&gqEZ=`)EO=_~ za@uV}1ab-J9S53lDW#E&*)v%z)HoLH{-(;Kv%}|rMx8WS?-j*)$=0%_3%K8-Zl-<4!mK#OPAYJkP7uB1fHk~08x^Tj| z2D|3ASR<_NqL(MSrZ4&&gdyvomhn^EQ4@-8&w%U{V;=&5~aRVsl{q|b;*4+=hyVr@*A6JH%ck7<4- z<3-%r>SoT71Fi&9kjDzwwgQ9W6u;3HX2Fx9R8h`WQ&BC@WZ8s+BMAC?M2tfrGI~Gh z)DAsmton#l)W5ZhBWIL*s~oGNTGH!W*DS*JJ)ppi-N9Vr-8ZnP;ke{g0#Z|LziAym zsY4N6`w5dM$_jIH8p@lma|aS>IpI-BSO=s7n{%4}_9-> z70$zdBJY4xYuzxD3}R+rI0!Zb$(EBg8C+L~gG#`FQWpCgs-n<`A>kWybV?R%L1NCs zI^vZwHpw9X^}02a|J&c6lJjJ;GMDr3=?U0{KN(l(r^O8Imy5M#Co(7WhcjxFjEFAP z?2onRM%o@HF|4XInP(`~MZZ$M1JcV6ygqcq3iiA*DcGU_d2sT_gdTnZ=^BV z2z(f`Aou)jR$3I=3IuByt=$?Z8eX9w{|*X1yB?_QYZ04d$Lz^jhk3k>txG#&5Gst8vp*WQF9VJ~Lm>q#2kx+zBZT_z z%TJqoFuA*Z;G~>6ka#@c;IcGWAxl@PN1m_|c49otSC$}w0e*ob1ALjdk!$nD#JZVH zPv@GKAnq)AxRM^=dx1GYKgMLb@(Pgu+^HEx^&VeyEnLD99h?_pl_Q2Usp&^mHy$*a zl<2xB7(!}BxA1#=Bz4AZQ{@Z5KDHWu%!xv2@zs`Pc1%wBm$C^|-UZT5lOvBtL6au` zwFb-6B&cYV4<-q-IT`x?2WUcLo%gaUQ%rvhcU??1GF!u{gqUJlVQ6;)!ry6Mln#J$9Nqask+oG9UQ^%krz6wA^wID{jcWy9+gw)&ICiP;IQEAC7BI`gHX? z+-I%~w?$)uz{X)eF=g9YuVxsWwl51fCsKdQjd?*C7jeA%X7;OoAtHN5Z4(J#w)C|8 za$QWAj9xugbG2)mA*ep$N-6(phaoD;qRzt}E(ZKD@|qCH%!)=z6ZT0h63u7&G7Ur| zfzH~vC;=r6lDIw^h26_tj<%Bl4v(dbGEI=DwJEIRX;sJkgB&`Uk@dzBtjFlDe#?a@ z8`5F+bLbw z*_}xE$a?K?(5Yb>O6vXstPV6QZ5+fV{_~_J>Scg!t|G;brQY#IUkPw@pNav8Ebw!!$Wxas}v~|Sr`Wixu4^J-aLl(XWUSfQ23n0 zr|-zxE)9qB7A;H-$QM1U8yx#9YYJbafz3}vADVKE3x7ZUea3%gyr2{iUx1Sed@vM5 zO!{mC7(14?V&o;^R1sb3Ub2wX%m^mYzu><;O7ykVbtAE7oDC(;f0FdlyHL+i&{f^* z{TX9V#E*^c<@(hb(_^k_eV?PG=HS+dF6amx2tLTv zQg$l8ww)!*UANuoP&W1vqQg$vAj6uGs7jzOvLySyzbtCu`JGwU1tU<@fl^@TCI%pV&vlgPJQ_o~3P<(60vv;E{cZ+H-u zEdl88+TY?)N8{XM*GZR!EY+x5l#x}K@1Eb}>hWmvJZiFGLUpK4tJA)h8`(kgp}W?)zGw60m@YC>kD(G#KQ za02-jrh4OKv+Q{c1P#9wLIYK&sJ$Z#m};RsxR8BKKssFs|0r6lqy9JOF?<8 zaNu^sc@C^&7CAe_KsHcywR3s5awD0OBo<9G1_xC&Usr>CR}XUXa;n{K)HFf{g^C9s zxK0z-D6(w|Zh^JkfcZ$hv<3aHzq2RtIP#}TdlYw9uHa%ewNKTlzjGb{THxjTedLZ? z_FHWerl?!LxjI?TRkooQvXnBImpHXEA?oJm%joO@&haf;AHT8PSR|-!30rj6qSU=5 zkYuv^%+F2Pj|xsypElc1;)en>fDDIT`190Y+P9}!o$kNzvvRtGC zu&gH%9s-h2w4=2cY`P<{stA_9C!72u_0AY;(u(;0v&G}OTYo!7E>ed25trhQ8u^l2|BfK*wA?$F<$}4Oetq|etk!z zApm+IOS7_}1_lR_S65ro5d%VF{?vOr5iuLMf96fUs>N6cAush)Uf6nU7DgMilpjQx z=PIBA=oV9MWvUFB{{o+1{71^OgU|icW#A~o074I6&DYn7Yw5ozz$xE6<=lE37f_9U z)=6`>R^4f|5{MyM`mbWpD{m$fchYlqKpE|eGMd@o$p%C1z-z8rs}Qf;9a5wGU||!& zT_(!CLU=FkOr;p;5wnM)<0B+J4zw?V1lt^5djIQl_w(wKmv#ClITI9JM0RpSkKlzt zB*jBs$c-Jh-vk9YG;j4x9H1QM{rXy?m-j6SU7r$9?3U|L)b{|mAc9f=n>t8%MAP7v z#y_s}0zUrNF7$(6Vt&NP!Kya%d@qW=H~!zC6vu3?r=lEgutyky4z6?a89zEYvGFgz zYVJLk)==ItCu9bpPX^F*MOBepWwkp+^&%CG1C)%wkAX2uiw%(y4gfp)lU%+TKs#W{ z==rEO?63sED82?D8i5b4&YT%Y7Gsxka|K+F1PdYS?f?M-)pyN*>qV)E3 z#O~)79=J>Yi~Y!{N!BuPL@i%b+$`ZQV(m;VU^5`D2IOHPuu9=Wtb8m%KoEAJ4JG?Cu6phGyV&0#Kp9Z%!4a=>e)TT~<;031 z$l6FF&y^f?mf(U?su3NEW4B!Ue5!0j8D4KcgEplt@QqK+g3y1bC0#QZl6N~k`A3j} zj8#C0%h2gn0@)Wxn?4B*hJhkV zOR~ww$*F*JRAY0&!EOfN+kIg}q@lVtAZ*qH1YcTxm;!=PC?KjJZ`Nzdfi_gAK@-jF zwH18h5}gQj>2;m5i2b}J)mJm%7~2>_870_F8G7z+C4g?Gh>RpE{^6@>Mc`aJKlyE~ z42-~Gjq{N&rxJ{0W%lr{c;+V~@sM?H@a3lQ zbPz~k7p34`p(LP@BC)ek*D0jF*AGWm!2vP+3+AgIQ!Y(UxpdP8j(;yaD$Zzlc}nrd zVKM@t$+;svB{ojqQ?Or>XGoL1;M`7J)eng;(m9|FB=1Qm3<@1+T@G$>k7ptGBw-k^ zS>@wP*IpX4&}BQKtDG5Nw>?EDjAe#Cpzt4Obz!H<8tmPmJj4q0~x+SriA zWr;Mq^=H8;MIjLT-O)_t;}qzot)|s!T35=y@2-wTgoMrJ=Xa4^2D52U??rKF>3+I+Dv<8x} znjchqoE~OW<6&8<2l)78E`Axc7^Z@&&W%&{Zo<{Mm^o9ejxHFnAp7E@;l3JFd%$>V z#%sjbgr;ZvC~(FXhE#|;mMNxKa>s^fkM}(qJ&g)lV%aSP#6+B=N#x}Wb2Rvci~T$K zLv5sAA-uWQ6-ErX%mZyZPYPd#Ti`kO{S1m5qnXP7NDZi5>-x~9YwP36EnB`uuLNUp zm?zyb3kNp?bvan6?ZwJI#P%Q*YxB&zP~`iY(P*WbGz(HCctl^xMsM8{O)P+0bGJr` zxt|%KNHWnoHSL2`c zJYG0JJL`4v)qYXDh1sZL_z*6}DX9SrJg@W_cw*GUwMSx5d-Y7qXrzo3XWAE8v!I54 zO1E-o;YLCIIDwl$>z;R9!@_6CE`gR*a`nMj>NkjKfI_N-Oe;#L4*6^m>0mPb^2hQ(JhA&L(sKzBU1Dx->5f{29xJ6MQ<2ID#^z|+?mmSSz%Wt#*LK4D zfd+6UC@ciah#@m-T66ahjrawhMf0Ht{iD}vdZmDvlf&P7eWBn)JHzay%qA>P=fRC- zzvQBc5B#nH03GC2!2qDs05tT_)S5gsip*kxixkEHh;Ok>o7vN%21u%7ESg?A^C`T@ zewSH_XZ=qsP*`>XnV(*IfS~$9EoJg0Ih@+8r+jW1jpf9GV;N*s7CkNdKuZO5Vs#Z| zqf8W&til+V)K}MI04pW=uSnm0y zw4K0C03rE`<{>Kp zidD#l0em_U&#hF~a=fc+?L+%!X3~1MwyP>H){vcPBqUQ%*^XpEO0ECX10nQtR(j?> zH$o#3G9DKhSG6o6)Q^WIq#bVH3t}9;pdd|P2mo=L*oQw!&(^AJ!F+8Vi-9Ez1lZdF zU{e7G1Qz1C@Oil%p-O}?sG8}pT!zss5 zZ)K0iNxWf}vOKN?@aUruk%Q|C5dgq)?&cIf2KaeW1=gU+mL?Tx8tBvhR7&Iw$R*DA zxN!-FegXM|ct^dHmS0_odXN=ip+^HQkJXH$o&Y#52!Mn0=Nsl;j#Rnn(9YwS(l4>E zhLOQhXOJH`ZLs9!Hz_49p6ZtGe8)atUZcFjLCim{0eqB7?p0efAqrXpuu6w1N~>;M zzsQA`N_HqmhXLR-$+sr8S0zcVB9nZknb>>w(*>TZ<(M~P90OaQ&|I=GTLY*j8emay zeIWwi#=mqw&g0vrp3UiEsw$%#pt8dN@SEj9e?0TZij}?|9tGf_82z_fWDkq60n>P? z0+0r9-mU?hCIA=KfLoQtq!Jz%TKQ!zm5wPFK09oA7zq$kFL|GE;O2fCN~^D<17`EQ zb(wJH*nefnp#U&S?VkfEqP0O+w_RTdS#$u=8KvI)RRCjs>8d?A3VFs!VP$Y!Cd=tf zz}Nm`fGqE{W>w_8H%orf-yOjI-`m>pWI3%3?Km0cMFI5VavN(@fC2Dcv36-M{Qciy z0Vv0qU8icF-khnp=ofo$tn&W;o-F491^eup$<>lonV69I0t*>&;$BtGN3m??;hQa3 z10VcAhXvrV2YNaFPWm_&?iw#gX-b6py|M{KQMw@{f^>uc)DZxGYXD{)K=f)MN5rxJ zw*7y;lyD}51@O&x0f4I;Pvu{AwVd2f!o%cq&SB$=^w#M&fv;O&8kC z0Uoqh*#0w?c_w*Yq`s2AyIeL8Cf(WmoN_;%`RZKiFGpjQQ+68U{-Um442erv~^1;OWmxsz$C)SA9K*!h4~=XQ zAoXGZOjYM{@ZmSMyZ;UpF#uV1tQNaFl$GA})(+=E{&V%{Btn`OBmaonS)RuNl3CM0 zX89eOeQiW2vH-@QCJ^UuG2hcXV6Wb2}8bJ3#e{@2m3ww|-EoK(1 zr%ExHQ!Fh~E6o;2GY+*UUem1s#8FA+SseyoU~)k^lB1BhRyO5vzu@gdt(Rm;w->++ zV8Q^=XDHJL9S86-m;CwMYFe4~>C@+ashYwv|Ar|EVdM%*8bDk^k@1?2NXJ!VmY1N} ztAIH&&P)4M0K*FLy>Py)Nt_rL80#Q_rZ>0VReDs3ac65w#T_Tr*R+?sis;Y*&JiC# z9SUH$`n7*jzceZUOl{gW)wd1krdJ1qk*W3@nCm(iKvLvcnWOI2<^nUdxyR-8G+}`(Y%+(12 zLTX(AT>xFJt97-m*44UNSLt3Kb__OT{kk`cjQfjmT}h#YQ-s^BkTTMv*V@jo4p;2nM)0XS7e05(MifGF6U zSVrp5_#y+)OJ}9s5xP|*8n`osjtBsmpXj9LG|P`^h|DViHk!QTKFE!3$Q_`nA{O-( zDha?fVF38G*=nHXVY%OnQ3N22nWEg}GsgYwh+-#d`6L4*oe3H3p#w;oJiKN+mJRtw zHGtT8VMQSA2}vHbE`OIIo9;&UHtLkzCkzb$md!tv?$Tg}#p^TBLT>dQg9gx4gwk?u zQRHqT5wnPd)TnOZ0MHmdYFzLv_)OoL>Qm^B7*p!cIH@T~dHT2OtTc zc_sB2U~ppvsR8#@7^|2LU}9Z#{t%a{;T^G5F)n2gL-)QZJ`8|5EK4)C;y8NW`}x%v zpj&D<0K`ZQb~7-uRe79ATD+ZBRd3%5o-n?G;?q8nDAwSyn&Y@*7~r+)i4g%n6Z@8( z+M$ToeSFFKq0KlWYq?dQI~LzQ0J0EFx$Q)$rj0_TNptkSgmD&70I)e7T*|00K2t4K ztIB^twt$G+8|}^DkZ`mq-TyX@qpq6fBid1Or#%b+D*>1{#N|4s$gvY+KHrZuFzn|n z_?-aQ+Y11yyeRMH(n&Z~vk6#|{I)vewb<+-an4i}_sH>DMMf27saaHqYB@AiUkzhq zyYmNVwNEd5R8#@3Zt|tsu?B?K?V)ZfEII&7!qC3M^OO#ZGlr~Y5^ z*DBlpNj~M=otvAU!P3Ym&pJ?Bak;ejpi~1eZfc(b7`H6|5Ww!9 zSC!%di}$_9lAOBedwuN(kk8$Q5@G)6`UDgg0CR|G4jhqYGbc5}>EG@nw+s*%Q?KLn z4dd8yw|t~|deiOsyx@@uVxMwDp_Y!b*w&;2NVq6~I%KU2ROfls>MZr3#DF#&U(^VV zY7(_==4zi7k$!I{fE*^i@dq@Ji*A*6uK(}yz;hSAzDg{W9#xi|n>SK5$F(V9p&bAw z1wbwbVk+Yso1J!L?`E9_8CN2MoUM_|gElEqU$+Zju%rM4hSxI_RnIyOcgaKLPQ7-{ z&zt4(nQkcsC>^OW>lQV`@{4?HkqV?G@MDF;P=`)s2O?5#)qDo^ls)bVkQy++Rdwww zn>|zp_$4ryFNaRAa@=PBARzyJ&ERc?yBx~Sg>!<9k0q5r+%4aB2AAg{0D+<~rs;Y2 z=6^WPFew2cLt|By_!Al`A2~85{v2N4@A4s#3(ZUJ06@l~wc~OQzakHB^(o=L zFG_!uWg}G#tb|i@vOvlvM{_-ZyHx-`@(0-0tUG)!HPLX$jgT?t+S+u1PLLgL0th|~ z(DPY)`v5=&@C4*p9Cd0)={Ir*(%>1(Mx2Y0WkVJPpjG`@2|%<=`C2ZJWWX>Oz!w0h z6D!MkpF#dKt0X|9i?+(sZ3DI${jLn$((ECfnH^3yvetbXXFd5@}f=DhzMp*$-^JWqNlYCDXo0D(NUL0)f}xRQ0x3IL3D zFq>}Kd6ztD)7JnTj35C17OOp(>)HS?KW8mVIg(*Y4NR_c#@Ivrq1&^SF2(Y@>(#gj z)v*{LmcG~z`PbP3+W56G>)43DPV5x`xcB^*35fc3yFAZ41@qZOvj03=#xj(V7u z0P+_7SipT8MI#g+1*Lheo9st%3a z$;tj(0N~3+0Or$s@rd}#@VL88)O*;40Q}3`foiFF`3uaY)qzrh(eJbGeQemd<~haE z^Eq2$wVhNgqY1!;HBhGm7;kXQD9tQ78!)fezX5no;(pttRBQtf-v0`p;C9`Qd+9!n z02t`t5J3Q>S3C&y-NEhUilygz+5`Qhv!CAExW+2O1IJr``9DRUMiu}N0kB`tiG4bN z$I;3)Gv~9_(WwYsXJ5rkU}n45270`M0m%6+7Xt{s1TDzxd%O?;yi|vfWev5c{OIi8 zjDeB5wjSMf8XTNzH5{eYbpp^$I|cLw0sK0E0fzF#$ACW)FoxJWq+vM%6fFt(bBvP% zD9+YgEYGcNyL15AEy7p;v^Z+sDG1Lw%Eo;Yh!P;YSNzEAl74DYs$z--6}uR8(+<*w zlLf~i6~Kp37IGE}rFogro-0CAnf_ zx-k`@n3u+#%;dOva@ycwN?_Uid1@sifX(Da<2V6mxxSED1!(XEwrOQq;)bd+&pd<- zy-MKc=Nr>+l213ed<6)in>ew%%^Mb6X+zB*p#1@uF{?`6=6z@nLU(>U`<=88xjoeZ z*oP_r;Ocjk$Y}*ABvlP`i&}7u(*uZpgiIfsbEi~nPF%7DAwyEmlKNpkr^^8|&H*e7 zr_`$p2B4Ajb+G^vLOs#!x!Ep%h2a{6`e$3S8uG?|+KWcWxF`Twt>jhS!vF|wQktZV z`a;-*s(tOoAG<$EQAaX_|Gnu z>KhLR08BL;00Zi(!ksN}q4opNXb64jLNTj%6`F=nZ`$SC;Iw658`xP7=K!GmXKh9$ z?4yY~0ISp{Ky=Qek_wyPo3j`p12E}N=0agfK%3E$L;*CZ(Rr==@u>+H4`5XQY;Kf@ z#*R7+;tYcWq08;}UkO?4;@&huG1G1WPV_1PIf2pvumEk9Cb8)N215PM?|%JnN8sMa z@X5-8Ode+20=*h@I36f`F&=9`Jud!cR0VKbJ?J51aUEH-Y(BjRq3(trcP*@%yWO-L zpJWk}{A4fT2mR5Def$$jT=* z9f=>x|B{N3Y3FWg@7AsCkF*?kBoVkAcPIcz1u*+i1|8i^>^F85GS5T9?5T#K^}@Uj zPazaNYO{rbss-`@uLa#YsqDIfOH=`o^#JBmXC{j%9+MxHBkNUp%(*GZT7-P|S=}?4p>@$u1&X}Cp9Ua`-)nLGIKmNIU$U)j z4IFcfJ9TL3-iE*(R8^)R@<7d&r+sj~b>&QRYzP1n1~5qI88k>tJC$>t-Ebu;PVHe_ zH79Li+(d+8i&p_72ug**BnkkIYCa&+ZTNsy2M{&m5KLu{ri=>=dP4aYpqTk-az}$d z>Am{*O(=xs#{dY>G}Pyww8yRnw`n^nLPPiE@D*IFAOjqfK&1A_l{*1ItZSi?sI_eX zORdn>;^>`0=mW5egSz53)AqP7yz|8+M^5(c9aaQ~28qxipI9 zY#<%96l}gR=24D*)FTb2IIIsoH*p@tV~ zid}(?t78%KI46w@w?=~xQR?L=McNueK}`&LbO*OwGX81FVp5 z?FRyYRvOf)-^{gK5Z9#ph`DLQ8vlJ%uD8Z5-x?fgTp|5yR=5BL+`Y}c*uV+N1-UzE z)tuF(mkYXO)A=t3I=!|e*u{N=y^BlM>)zUkZf|5&ctQ_sS=lZ z`TW16tJIb+a3~lEK#e%sW{p zF8{wa4n-8enREQ`U!>wWb!ZVvs+|tv+li~PuYEn>hfDxcwtU0WtHCs@9p!mUKngz? z93`jOM+D*_kpu7}Tb=Y1Bzp4~HTw{xW=Wfw*Tml|j!AB08Ve$@qEca~TOYo>+};iV z9Oh*CnKY@1S%o?PKti^RNdY)A~k(_eI94niXLQmF((bq%1@STR36KTvIdOZ z6Rv1qeIWw@AQ=jg2XI3jB|7h&!ZEDy&*|>lYNXSG#8fwy)5t{aWwKsleym0^L1@K8 zu+lmV-~`n9SC{gx_lDNk1eU(S3!FD^EKGh2A+#}%Ki?<_bQsk`1W!i;0Mykp&F?r} zr;b$a*y-;1SB?b<^7a>IqbTYZhj@kkCVLn!g`KPW9{|kurVKm3a}viT&P0})f(Zci z)!Z5ynFbDidegn1^^6S}zzm-M1pp-!2>k5B44;xt4?TPw_#?oH`7}b^`u+6nkJDpN zsBI_$H|lr*&yr`VpsaDt^xD;{=;5b412Qq}%P02?b@^K0gBSoVlmjVuBz-gEe9ZS!C3=nvh1H2S|g<~CR4QS+H~i%^<$ z%ybjgH0sZTP^SUNPNR;ESzVh}lkaMxyPtAA!2Jtd{;&AX1CT&%6DuTJeP-3RedvlS zV^2>+Mug~2q=D+%-f1hulsSzYk7Df~zltZnvjBL~gdEZT`DSn0^r3j&=~Mu98-Gdn zQl+*3{JHocduIgjE#yCp`62$QXHu`cA9YHAwa*c-P6>ce=LV?ji~#=+SVMRT2*s0F P00000NkvXXu0mjf!y&BI literal 47978 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

Ud*q5g@;?0-r(@zni#9*P*7A=fwSm4qT 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 A?EnA( diff --git a/drool/imgs/spritesheet.json b/drool/imgs/spritesheet.json index 5a3f6de0..b27d5195 100644 --- a/drool/imgs/spritesheet.json +++ b/drool/imgs/spritesheet.json @@ -24,11 +24,11 @@ "frames": [[192, 0], [288, 0], [384, 0], [480, 0]], "switchOut": { "msPerFrame": 100, - "frames": [[768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [0, 96]] + "frames": [[768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [1440, 0]] }, "switchIn": { "msPerFrame": 100, - "frames": [[0, 96], [1344, 0], [1248, 0], [1152, 0], [1056, 0], [960, 0], [864, 0], [768, 0]] + "frames": [[1440, 0], [1344, 0], [1248, 0], [1152, 0], [1056, 0], [960, 0], [864, 0], [768, 0]] }, "damageFront": { "msPerFrame": 100, @@ -41,14 +41,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 69, - "frames": [[960, 0], [1056, 0], [0, 96], [96, 96]], + "frames": [[960, 0], [1056, 0], [1152, 0], [0, 96]], "switchOut": { "msPerFrame": 100, - "frames": [[96, 96], [192, 96], [288, 96], [384, 96], [480, 96], [576, 96], [672, 96], [768, 96]] + "frames": [[0, 96], [96, 96], [192, 96], [288, 96], [384, 96], [480, 96], [576, 96], [672, 96]] }, "switchIn": { "msPerFrame": 100, - "frames": [[768, 96], [672, 96], [576, 96], [480, 96], [384, 96], [288, 96], [192, 96], [96, 96]] + "frames": [[672, 96], [576, 96], [480, 96], [384, 96], [288, 96], [192, 96], [96, 96], [0, 96]] } }, "ekineki_front.gif": { @@ -57,18 +57,18 @@ "contentBottom": 79, "contentLeft": 16, "contentRight": 80, - "frames": [[192, 96], [288, 96], [384, 96], [480, 96]], + "frames": [[96, 96], [192, 96], [288, 96], [384, 96]], "switchOut": { "msPerFrame": 100, - "frames": [[864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [0, 192], [96, 192]] + "frames": [[768, 96], [864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [1440, 96]] }, "switchIn": { "msPerFrame": 100, - "frames": [[96, 192], [0, 192], [1344, 96], [1248, 96], [1152, 96], [1056, 96], [960, 96], [864, 96]] + "frames": [[1440, 96], [1344, 96], [1248, 96], [1152, 96], [1056, 96], [960, 96], [864, 96], [768, 96]] }, "damageFront": { "msPerFrame": 100, - "frames": [[576, 96], [672, 96], [768, 96], [864, 96]] + "frames": [[480, 96], [576, 96], [672, 96], [768, 96]] } }, "embursa_back.gif": { @@ -77,14 +77,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 83, - "frames": [[960, 96], [1056, 96]], + "frames": [[864, 96], [960, 96]], "switchOut": { "msPerFrame": 100, - "frames": [[192, 192], [288, 192], [384, 192], [480, 192], [576, 192], [672, 192], [768, 192], [864, 192]] + "frames": [[0, 192], [96, 192], [192, 192], [288, 192], [384, 192], [480, 192], [576, 192], [672, 192]] }, "switchIn": { "msPerFrame": 100, - "frames": [[864, 192], [768, 192], [672, 192], [576, 192], [480, 192], [384, 192], [288, 192], [192, 192]] + "frames": [[672, 192], [576, 192], [480, 192], [384, 192], [288, 192], [192, 192], [96, 192], [0, 192]] } }, "embursa_front.gif": { @@ -93,18 +93,18 @@ "contentBottom": 95, "contentLeft": 16, "contentRight": 82, - "frames": [[0, 192], [96, 192], [192, 192], [288, 192]], + "frames": [[1056, 96], [1152, 96], [0, 192], [96, 192]], "switchOut": { "msPerFrame": 100, - "frames": [[960, 192], [1056, 192], [1152, 192], [1248, 192], [1344, 192], [0, 288], [96, 288], [192, 288]] + "frames": [[768, 192], [864, 192], [960, 192], [1056, 192], [1152, 192], [1248, 192], [1344, 192], [1440, 192]] }, "switchIn": { "msPerFrame": 100, - "frames": [[192, 288], [96, 288], [0, 288], [1344, 192], [1248, 192], [1152, 192], [1056, 192], [960, 192]] + "frames": [[1440, 192], [1344, 192], [1248, 192], [1152, 192], [1056, 192], [960, 192], [864, 192], [768, 192]] }, "damageFront": { "msPerFrame": 100, - "frames": [[384, 192], [480, 192], [576, 192], [672, 192]] + "frames": [[192, 192], [288, 192], [384, 192], [480, 192]] } }, "ghouliath_back.gif": { @@ -113,14 +113,14 @@ "contentBottom": 96, "contentLeft": 2, "contentRight": 79, - "frames": [[768, 192], [864, 192]], + "frames": [[576, 192], [672, 192]], "switchOut": { "msPerFrame": 100, - "frames": [[288, 288], [384, 288], [480, 288], [576, 288], [672, 288], [768, 288], [864, 288], [960, 288]] + "frames": [[0, 288], [96, 288], [192, 288], [288, 288], [384, 288], [480, 288], [576, 288], [672, 288]] }, "switchIn": { "msPerFrame": 100, - "frames": [[960, 288], [864, 288], [768, 288], [672, 288], [576, 288], [480, 288], [384, 288], [288, 288]] + "frames": [[672, 288], [576, 288], [480, 288], [384, 288], [288, 288], [192, 288], [96, 288], [0, 288]] } }, "ghouliath_front.gif": { @@ -129,18 +129,54 @@ "contentBottom": 78, "contentLeft": 21, "contentRight": 74, - "frames": [[960, 192], [1056, 192], [0, 288], [96, 288]], + "frames": [[768, 192], [864, 192], [960, 192], [1056, 192]], "switchOut": { "msPerFrame": 100, - "frames": [[1056, 288], [1152, 288], [1248, 288], [1344, 288], [0, 384], [96, 384], [192, 384], [288, 384]] + "frames": [[768, 288], [864, 288], [960, 288], [1056, 288], [1152, 288], [1248, 288], [1344, 288], [1440, 288]] }, "switchIn": { "msPerFrame": 100, - "frames": [[288, 384], [192, 384], [96, 384], [0, 384], [1344, 288], [1248, 288], [1152, 288], [1056, 288]] + "frames": [[1440, 288], [1344, 288], [1248, 288], [1152, 288], [1056, 288], [960, 288], [864, 288], [768, 288]] }, "damageFront": { "msPerFrame": 100, - "frames": [[192, 288], [288, 288], [384, 288], [480, 288]] + "frames": [[1152, 192], [0, 288], [96, 288], [192, 288]] + } + }, + "glob_back.gif": { + "msPerFrame": 200, + "contentTop": 21, + "contentBottom": 96, + "contentLeft": 0, + "contentRight": 84, + "frames": [[288, 288], [384, 288], [480, 288], [576, 288]], + "switchOut": { + "msPerFrame": 100, + "frames": [[0, 384], [96, 384], [192, 384], [288, 384], [384, 384], [480, 384], [576, 384], [672, 384]] + }, + "switchIn": { + "msPerFrame": 100, + "frames": [[672, 384], [576, 384], [480, 384], [384, 384], [288, 384], [192, 384], [96, 384], [0, 384]] + } + }, + "glob_front.gif": { + "msPerFrame": 100, + "contentTop": 15, + "contentBottom": 81, + "contentLeft": 12, + "contentRight": 82, + "frames": [[672, 288], [768, 288], [864, 288], [960, 288]], + "switchOut": { + "msPerFrame": 100, + "frames": [[768, 384], [864, 384], [960, 384], [1056, 384], [1152, 384], [1248, 384], [1344, 384], [1440, 384]] + }, + "switchIn": { + "msPerFrame": 100, + "frames": [[1440, 384], [1344, 384], [1248, 384], [1152, 384], [1056, 384], [960, 384], [864, 384], [768, 384]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[1056, 288], [1152, 288], [0, 384], [96, 384]] } }, "gorillax_back.gif": { @@ -149,14 +185,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 94, - "frames": [[576, 288], [672, 288]], + "frames": [[192, 384], [288, 384]], "switchOut": { "msPerFrame": 100, - "frames": [[384, 384], [480, 384], [576, 384], [672, 384], [768, 384], [864, 384], [960, 384], [1056, 384]] + "frames": [[0, 480], [96, 480], [192, 480], [288, 480], [384, 480], [480, 480], [576, 480], [672, 480]] }, "switchIn": { "msPerFrame": 100, - "frames": [[1056, 384], [960, 384], [864, 384], [768, 384], [672, 384], [576, 384], [480, 384], [384, 384]] + "frames": [[672, 480], [576, 480], [480, 480], [384, 480], [288, 480], [192, 480], [96, 480], [0, 480]] } }, "gorillax_front.gif": { @@ -165,18 +201,18 @@ "contentBottom": 89, "contentLeft": 16, "contentRight": 79, - "frames": [[768, 288], [864, 288], [960, 288], [1056, 288]], + "frames": [[384, 384], [480, 384], [576, 384], [672, 384]], "switchOut": { "msPerFrame": 100, - "frames": [[1152, 384], [1248, 384], [1344, 384], [0, 480], [96, 480], [192, 480], [288, 480], [384, 480]] + "frames": [[768, 480], [864, 480], [960, 480], [1056, 480], [1152, 480], [1248, 480], [1344, 480], [1440, 480]] }, "switchIn": { "msPerFrame": 100, - "frames": [[384, 480], [288, 480], [192, 480], [96, 480], [0, 480], [1344, 384], [1248, 384], [1152, 384]] + "frames": [[1440, 480], [1344, 480], [1248, 480], [1152, 480], [1056, 480], [960, 480], [864, 480], [768, 480]] }, "damageFront": { "msPerFrame": 100, - "frames": [[0, 384], [96, 384], [192, 384], [288, 384]] + "frames": [[768, 384], [864, 384], [960, 384], [1056, 384]] } }, "iblivion_back.gif": { @@ -185,14 +221,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 93, - "frames": [[384, 384], [480, 384]], + "frames": [[1152, 384], [0, 480]], "switchOut": { "msPerFrame": 100, - "frames": [[480, 480], [576, 480], [672, 480], [768, 480], [864, 480], [960, 480], [1056, 480], [1152, 480]] + "frames": [[0, 576], [96, 576], [192, 576], [288, 576], [384, 576], [480, 576], [576, 576], [672, 576]] }, "switchIn": { "msPerFrame": 100, - "frames": [[1152, 480], [1056, 480], [960, 480], [864, 480], [768, 480], [672, 480], [576, 480], [480, 480]] + "frames": [[672, 576], [576, 576], [480, 576], [384, 576], [288, 576], [192, 576], [96, 576], [0, 576]] } }, "iblivion_front.gif": { @@ -201,18 +237,18 @@ "contentBottom": 81, "contentLeft": 18, "contentRight": 79, - "frames": [[576, 384], [672, 384], [768, 384], [864, 384]], + "frames": [[96, 480], [192, 480], [288, 480], [384, 480]], "switchOut": { "msPerFrame": 100, - "frames": [[1248, 480], [1344, 480], [0, 576], [96, 576], [192, 576], [288, 576], [384, 576], [480, 576]] + "frames": [[768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576], [1344, 576], [1440, 576]] }, "switchIn": { "msPerFrame": 100, - "frames": [[480, 576], [384, 576], [288, 576], [192, 576], [96, 576], [0, 576], [1344, 480], [1248, 480]] + "frames": [[1440, 576], [1344, 576], [1248, 576], [1152, 576], [1056, 576], [960, 576], [864, 576], [768, 576]] }, "damageFront": { "msPerFrame": 100, - "frames": [[960, 384], [1056, 384], [0, 480], [96, 480]] + "frames": [[480, 480], [576, 480], [672, 480], [768, 480]] } }, "inutia_back.gif": { @@ -221,14 +257,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 78, - "frames": [[192, 480], [288, 480]], + "frames": [[864, 480], [960, 480]], "switchOut": { "msPerFrame": 100, - "frames": [[576, 576], [672, 576], [768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576]] + "frames": [[0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672], [576, 672], [672, 672]] }, "switchIn": { "msPerFrame": 100, - "frames": [[1248, 576], [1152, 576], [1056, 576], [960, 576], [864, 576], [768, 576], [672, 576], [576, 576]] + "frames": [[672, 672], [576, 672], [480, 672], [384, 672], [288, 672], [192, 672], [96, 672], [0, 672]] } }, "inutia_front.gif": { @@ -237,18 +273,18 @@ "contentBottom": 76, "contentLeft": 16, "contentRight": 77, - "frames": [[384, 480], [480, 480], [576, 480], [672, 480]], + "frames": [[1056, 480], [1152, 480], [0, 576], [96, 576]], "switchOut": { "msPerFrame": 100, - "frames": [[1344, 576], [0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672], [576, 672]] + "frames": [[768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672], [1440, 672]] }, "switchIn": { "msPerFrame": 100, - "frames": [[576, 672], [480, 672], [384, 672], [288, 672], [192, 672], [96, 672], [0, 672], [1344, 576]] + "frames": [[1440, 672], [1344, 672], [1248, 672], [1152, 672], [1056, 672], [960, 672], [864, 672], [768, 672]] }, "damageFront": { "msPerFrame": 100, - "frames": [[768, 480], [864, 480], [960, 480], [1056, 480]] + "frames": [[192, 576], [288, 576], [384, 576], [480, 576]] } }, "malalien_back.gif": { @@ -257,14 +293,14 @@ "contentBottom": 96, "contentLeft": 5, "contentRight": 65, - "frames": [[0, 576], [96, 576]], + "frames": [[576, 576], [672, 576]], "switchOut": { "msPerFrame": 100, - "frames": [[672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672]] + "frames": [[0, 768], [96, 768], [192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768]] }, "switchIn": { "msPerFrame": 100, - "frames": [[1344, 672], [1248, 672], [1152, 672], [1056, 672], [960, 672], [864, 672], [768, 672], [672, 672]] + "frames": [[672, 768], [576, 768], [480, 768], [384, 768], [288, 768], [192, 768], [96, 768], [0, 768]] } }, "malalien_front.gif": { @@ -273,18 +309,54 @@ "contentBottom": 80, "contentLeft": 29, "contentRight": 64, - "frames": [[192, 576], [288, 576], [384, 576], [480, 576]], + "frames": [[768, 576], [864, 576], [960, 576], [1056, 576]], "switchOut": { "msPerFrame": 100, - "frames": [[0, 768], [96, 768], [192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768]] + "frames": [[768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [1440, 768]] }, "switchIn": { "msPerFrame": 100, - "frames": [[672, 768], [576, 768], [480, 768], [384, 768], [288, 768], [192, 768], [96, 768], [0, 768]] + "frames": [[1440, 768], [1344, 768], [1248, 768], [1152, 768], [1056, 768], [960, 768], [864, 768], [768, 768]] }, "damageFront": { "msPerFrame": 100, - "frames": [[576, 576], [672, 576], [768, 576], [864, 576]] + "frames": [[1152, 576], [0, 672], [96, 672], [192, 672]] + } + }, + "mulong_back.gif": { + "msPerFrame": 200, + "contentTop": 10, + "contentBottom": 96, + "contentLeft": 0, + "contentRight": 96, + "frames": [[288, 672], [384, 672], [480, 672], [576, 672]], + "switchOut": { + "msPerFrame": 100, + "frames": [[0, 864], [96, 864], [192, 864], [288, 864], [384, 864], [480, 864], [576, 864], [672, 864]] + }, + "switchIn": { + "msPerFrame": 100, + "frames": [[672, 864], [576, 864], [480, 864], [384, 864], [288, 864], [192, 864], [96, 864], [0, 864]] + } + }, + "mulong_front.gif": { + "msPerFrame": 100, + "contentTop": 8, + "contentBottom": 86, + "contentLeft": 2, + "contentRight": 90, + "frames": [[672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [0, 768], [96, 768]], + "switchOut": { + "msPerFrame": 100, + "frames": [[768, 864], [864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864], [1440, 864]] + }, + "switchIn": { + "msPerFrame": 100, + "frames": [[1440, 864], [1344, 864], [1248, 864], [1152, 864], [1056, 864], [960, 864], [864, 864], [768, 864]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[192, 768], [288, 768], [384, 768], [480, 768]] } }, "nirvamma_back.gif": { @@ -293,14 +365,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 65, - "frames": [[960, 576], [1056, 576]], + "frames": [[576, 768], [672, 768]], "switchOut": { "msPerFrame": 100, - "frames": [[768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [0, 864]] + "frames": [[0, 960], [96, 960], [192, 960], [288, 960], [384, 960], [480, 960], [576, 960], [672, 960]] }, "switchIn": { "msPerFrame": 100, - "frames": [[0, 864], [1344, 768], [1248, 768], [1152, 768], [1056, 768], [960, 768], [864, 768], [768, 768]] + "frames": [[672, 960], [576, 960], [480, 960], [384, 960], [288, 960], [192, 960], [96, 960], [0, 960]] } }, "nirvamma_front.gif": { @@ -309,18 +381,18 @@ "contentBottom": 93, "contentLeft": 20, "contentRight": 79, - "frames": [[0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672], [576, 672], [672, 672]], + "frames": [[768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [0, 864], [96, 864], [192, 864]], "switchOut": { "msPerFrame": 100, - "frames": [[96, 864], [192, 864], [288, 864], [384, 864], [480, 864], [576, 864], [672, 864], [768, 864]] + "frames": [[768, 960], [864, 960], [960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960], [1440, 960]] }, "switchIn": { "msPerFrame": 100, - "frames": [[768, 864], [672, 864], [576, 864], [480, 864], [384, 864], [288, 864], [192, 864], [96, 864]] + "frames": [[1440, 960], [1344, 960], [1248, 960], [1152, 960], [1056, 960], [960, 960], [864, 960], [768, 960]] }, "damageFront": { "msPerFrame": 100, - "frames": [[768, 672], [864, 672], [960, 672], [1056, 672]] + "frames": [[288, 864], [384, 864], [480, 864], [576, 864]] } }, "pengym_back.gif": { @@ -329,14 +401,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 91, - "frames": [[0, 768], [96, 768]], + "frames": [[672, 864], [768, 864]], "switchOut": { "msPerFrame": 100, - "frames": [[864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864], [0, 960], [96, 960]] + "frames": [[0, 1056], [96, 1056], [192, 1056], [288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056]] }, "switchIn": { "msPerFrame": 100, - "frames": [[96, 960], [0, 960], [1344, 864], [1248, 864], [1152, 864], [1056, 864], [960, 864], [864, 864]] + "frames": [[672, 1056], [576, 1056], [480, 1056], [384, 1056], [288, 1056], [192, 1056], [96, 1056], [0, 1056]] } }, "pengym_front.gif": { @@ -345,18 +417,18 @@ "contentBottom": 88, "contentLeft": 19, "contentRight": 75, - "frames": [[192, 768], [288, 768], [384, 768], [480, 768]], + "frames": [[864, 864], [960, 864], [1056, 864], [1152, 864]], "switchOut": { "msPerFrame": 100, - "frames": [[192, 960], [288, 960], [384, 960], [480, 960], [576, 960], [672, 960], [768, 960], [864, 960]] + "frames": [[768, 1056], [864, 1056], [960, 1056], [1056, 1056], [1152, 1056], [1248, 1056], [1344, 1056], [1440, 1056]] }, "switchIn": { "msPerFrame": 100, - "frames": [[864, 960], [768, 960], [672, 960], [576, 960], [480, 960], [384, 960], [288, 960], [192, 960]] + "frames": [[1440, 1056], [1344, 1056], [1248, 1056], [1152, 1056], [1056, 1056], [960, 1056], [864, 1056], [768, 1056]] }, "damageFront": { "msPerFrame": 100, - "frames": [[576, 768], [672, 768], [768, 768], [864, 768]] + "frames": [[0, 960], [96, 960], [192, 960], [288, 960]] } }, "sofabbi_back.gif": { @@ -365,14 +437,14 @@ "contentBottom": 96, "contentLeft": 1, "contentRight": 90, - "frames": [[960, 768], [1056, 768]], + "frames": [[384, 960], [480, 960]], "switchOut": { "msPerFrame": 100, - "frames": [[960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960], [0, 1056], [96, 1056], [192, 1056]] + "frames": [[0, 1152], [96, 1152], [192, 1152], [288, 1152], [384, 1152], [480, 1152], [576, 1152], [672, 1152]] }, "switchIn": { "msPerFrame": 100, - "frames": [[192, 1056], [96, 1056], [0, 1056], [1344, 960], [1248, 960], [1152, 960], [1056, 960], [960, 960]] + "frames": [[672, 1152], [576, 1152], [480, 1152], [384, 1152], [288, 1152], [192, 1152], [96, 1152], [0, 1152]] } }, "sofabbi_front.gif": { @@ -381,18 +453,18 @@ "contentBottom": 80, "contentLeft": 20, "contentRight": 69, - "frames": [[0, 864], [96, 864], [192, 864], [288, 864]], + "frames": [[576, 960], [672, 960], [768, 960], [864, 960]], "switchOut": { "msPerFrame": 100, - "frames": [[288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056], [768, 1056], [864, 1056], [960, 1056]] + "frames": [[768, 1152], [864, 1152], [960, 1152], [1056, 1152], [1152, 1152], [1248, 1152], [1344, 1152], [1440, 1152]] }, "switchIn": { "msPerFrame": 100, - "frames": [[960, 1056], [864, 1056], [768, 1056], [672, 1056], [576, 1056], [480, 1056], [384, 1056], [288, 1056]] + "frames": [[1440, 1152], [1344, 1152], [1248, 1152], [1152, 1152], [1056, 1152], [960, 1152], [864, 1152], [768, 1152]] }, "damageFront": { "msPerFrame": 100, - "frames": [[384, 864], [480, 864], [576, 864], [672, 864]] + "frames": [[960, 960], [1056, 960], [1152, 960], [0, 1056]] } }, "volthare_back.gif": { @@ -401,14 +473,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 87, - "frames": [[768, 864], [864, 864]], + "frames": [[96, 1056], [192, 1056]], "switchOut": { "msPerFrame": 100, - "frames": [[1056, 1056], [1152, 1056], [1248, 1056], [1344, 1056], [0, 1152], [96, 1152], [192, 1152], [288, 1152]] + "frames": [[0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248], [480, 1248], [576, 1248], [672, 1248]] }, "switchIn": { "msPerFrame": 100, - "frames": [[288, 1152], [192, 1152], [96, 1152], [0, 1152], [1344, 1056], [1248, 1056], [1152, 1056], [1056, 1056]] + "frames": [[672, 1248], [576, 1248], [480, 1248], [384, 1248], [288, 1248], [192, 1248], [96, 1248], [0, 1248]] } }, "volthare_front.gif": { @@ -417,18 +489,18 @@ "contentBottom": 85, "contentLeft": 23, "contentRight": 74, - "frames": [[960, 864], [1056, 864], [0, 960], [96, 960]], + "frames": [[288, 1056], [384, 1056], [480, 1056], [576, 1056]], "switchOut": { "msPerFrame": 100, - "frames": [[384, 1152], [480, 1152], [576, 1152], [672, 1152], [768, 1152], [864, 1152], [960, 1152], [1056, 1152]] + "frames": [[768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248], [1248, 1248], [1344, 1248], [1440, 1248]] }, "switchIn": { "msPerFrame": 100, - "frames": [[1056, 1152], [960, 1152], [864, 1152], [768, 1152], [672, 1152], [576, 1152], [480, 1152], [384, 1152]] + "frames": [[1440, 1248], [1344, 1248], [1248, 1248], [1152, 1248], [1056, 1248], [960, 1248], [864, 1248], [768, 1248]] }, "damageFront": { "msPerFrame": 100, - "frames": [[192, 960], [288, 960], [384, 960], [480, 960]] + "frames": [[672, 1056], [768, 1056], [864, 1056], [960, 1056]] } }, "xmon_back.gif": { @@ -437,14 +509,14 @@ "contentBottom": 96, "contentLeft": 0, "contentRight": 93, - "frames": [[576, 960], [672, 960]], + "frames": [[1056, 1056], [1152, 1056]], "switchOut": { "msPerFrame": 100, - "frames": [[1152, 1152], [1248, 1152], [1344, 1152], [0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248]] + "frames": [[0, 1344], [96, 1344], [192, 1344], [288, 1344], [384, 1344], [480, 1344], [576, 1344], [672, 1344]] }, "switchIn": { "msPerFrame": 100, - "frames": [[384, 1248], [288, 1248], [192, 1248], [96, 1248], [0, 1248], [1344, 1152], [1248, 1152], [1152, 1152]] + "frames": [[672, 1344], [576, 1344], [480, 1344], [384, 1344], [288, 1344], [192, 1344], [96, 1344], [0, 1344]] } }, "xmon_front.gif": { @@ -453,18 +525,18 @@ "contentBottom": 85, "contentLeft": 10, "contentRight": 86, - "frames": [[768, 960], [864, 960], [960, 960], [1056, 960]], + "frames": [[0, 1152], [96, 1152], [192, 1152], [288, 1152]], "switchOut": { "msPerFrame": 100, - "frames": [[480, 1248], [576, 1248], [672, 1248], [768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248]] + "frames": [[768, 1344], [864, 1344], [960, 1344], [1056, 1344], [1152, 1344], [1248, 1344], [1344, 1344], [1440, 1344]] }, "switchIn": { "msPerFrame": 100, - "frames": [[1152, 1248], [1056, 1248], [960, 1248], [864, 1248], [768, 1248], [672, 1248], [576, 1248], [480, 1248]] + "frames": [[1440, 1344], [1344, 1344], [1248, 1344], [1152, 1344], [1056, 1344], [960, 1344], [864, 1344], [768, 1344]] }, "damageFront": { "msPerFrame": 100, - "frames": [[0, 1056], [96, 1056], [192, 1056], [288, 1056]] + "frames": [[384, 1152], [480, 1152], [576, 1152], [672, 1152]] } }, "aurox_mini.gif": { @@ -499,7 +571,7 @@ "frames": [[0, 16], [16, 16]] } }, - "gorillax_mini.gif": { + "glob_mini.gif": { "msPerFrame": 200, "frames": [[64, 32], [96, 32]], "micro": { @@ -507,7 +579,7 @@ "frames": [[32, 16], [48, 16]] } }, - "iblivion_mini.gif": { + "gorillax_mini.gif": { "msPerFrame": 200, "frames": [[128, 32], [160, 32]], "micro": { @@ -515,7 +587,7 @@ "frames": [[64, 16], [80, 16]] } }, - "inutia_mini.gif": { + "iblivion_mini.gif": { "msPerFrame": 200, "frames": [[0, 64], [32, 64]], "micro": { @@ -523,7 +595,7 @@ "frames": [[0, 32], [16, 32]] } }, - "malalien_mini.gif": { + "inutia_mini.gif": { "msPerFrame": 200, "frames": [[64, 64], [96, 64]], "micro": { @@ -531,7 +603,7 @@ "frames": [[32, 32], [48, 32]] } }, - "nirvamma_mini.gif": { + "malalien_mini.gif": { "msPerFrame": 200, "frames": [[128, 64], [160, 64]], "micro": { @@ -539,7 +611,7 @@ "frames": [[64, 32], [80, 32]] } }, - "pengym_mini.gif": { + "mulong_mini.gif": { "msPerFrame": 200, "frames": [[0, 96], [32, 96]], "micro": { @@ -547,7 +619,7 @@ "frames": [[0, 48], [16, 48]] } }, - "sofabbi_mini.gif": { + "nirvamma_mini.gif": { "msPerFrame": 200, "frames": [[64, 96], [96, 96]], "micro": { @@ -555,7 +627,7 @@ "frames": [[32, 48], [48, 48]] } }, - "volthare_mini.gif": { + "pengym_mini.gif": { "msPerFrame": 200, "frames": [[128, 96], [160, 96]], "micro": { @@ -563,12 +635,28 @@ "frames": [[64, 48], [80, 48]] } }, - "xmon_mini.gif": { + "sofabbi_mini.gif": { "msPerFrame": 200, "frames": [[0, 128], [32, 128]], "micro": { "msPerFrame": 200, "frames": [[0, 64], [16, 64]] } + }, + "volthare_mini.gif": { + "msPerFrame": 200, + "frames": [[64, 128], [96, 128]], + "micro": { + "msPerFrame": 200, + "frames": [[32, 64], [48, 64]] + } + }, + "xmon_mini.gif": { + "msPerFrame": 200, + "frames": [[128, 128], [160, 128]], + "micro": { + "msPerFrame": 200, + "frames": [[64, 64], [80, 64]] + } } } \ No newline at end of file diff --git a/drool/moves.csv b/drool/moves.csv index d9d78933..503fb922 100644 --- a/drool/moves.csv +++ b/drool/moves.csv @@ -62,6 +62,6 @@ Water Claw,Glob,80,2,100,0,Liquid,Physical,Deals damage.,,,none,0,,opponent-slot Holy Water Claw,Glob,90,3,100,0,Faith,Physical,Deals damage.,,,none,0,,opponent-slot Get Slippery,Glob,0,2,100,0,Liquid,Self,Boosts ATK and Speed by {ATTACK_BUFF_PERCENT}.,,,none,0,ATTACK_BUFF_PERCENT=50;SPEED_BUFF_PERCENT=50,self-only Summon Storm,Mulong,0,0,100,0,Lightning,Other,"Summons a Storm for 7 turns that damages all active mons.",,,none,0,,none -King's Respite,Mulong,120,2,100,0,Air,Special,"The next time Mulong rests, it deals damage to the target.",,,none,0,BASE_POWER=120,opponent-slot +Grand Respite,Mulong,120,2,100,0,Air,Special,"The next time Mulong rests, it deals damage to the target.",,,none,0,BASE_POWER=120,opponent-slot Thunderclap,Mulong,120,3,100,0,Lightning,Special,"Deals damage, {EFFECT_ACCURACY} chance to inflict Zap.",,,none,0,EFFECT_ACCURACY=20,opponent-slot Cloud Strike,Mulong,?,2,100,0,Ice,Special,"Deals damage, {EFFECT_ACCURACY} chance to inflict Frostbite. While Storm is active, power rises to {STORM_BASE_POWER} and the chance to {STORM_EFFECT_ACCURACY}.",,,none,0,BASE_POWER=70;STORM_BASE_POWER=100;EFFECT_ACCURACY=10;STORM_EFFECT_ACCURACY=30,opponent-slot diff --git a/foundry.toml b/foundry.toml index 5d6714fb..54124bdc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ src = 'src' out = 'out' libs = ['lib'] optimizer = true -optimizer_runs = 4294967295 +optimizer_runs = 10000 # @see {@link https://github.com/foundry-rs/foundry/issues/4060} auto_detect_remappings = false bytecode_hash = "none" diff --git a/processing/createMonShadows.py b/processing/createMonShadows.py index c68eb677..9e706a8e 100644 --- a/processing/createMonShadows.py +++ b/processing/createMonShadows.py @@ -98,6 +98,17 @@ def create_shadows(imgs_dir: Path) -> None: sheet, positions = build_spritesheet(all_frames, frame_size=FRAME_SIZE) save_and_compress_png(sheet, imgs_dir / "mon_shadow.png", "Shadow spritesheet") + # Ship the packed sheet alongside the other mon spritesheets. + munch_assets_dir = ( + Path(__file__).parent.parent.parent / "munch" / "src" / "assets" / "mons" / "all" + ) + if munch_assets_dir.exists(): + save_and_compress_png( + sheet, munch_assets_dir / "mon_shadow.png", "Munch shadow spritesheet" + ) + else: + print(f"⚠ Munch directory not found, skipping copy: {munch_assets_dir}") + for name, start, count, ms, cx in entries: metadata[name] = { "msPerFrame": ms, diff --git a/processing/deploy.py b/processing/deploy.py index 3eb5040f..66980717 100755 --- a/processing/deploy.py +++ b/processing/deploy.py @@ -192,7 +192,7 @@ def run_forge_script( "--skip-simulation", "--legacy", "--non-interactive", - "-g", "105", + "-g", "100", "--slow", ] # password is None on dry runs (never prompted); only pass it when present. diff --git a/processing/generateMonsTypeScript.py b/processing/generateMonsTypeScript.py index 6fa01711..609ad431 100644 --- a/processing/generateMonsTypeScript.py +++ b/processing/generateMonsTypeScript.py @@ -12,16 +12,18 @@ from types_enum import REAL_TYPE_NAMES from packMoves import find_json_moves, inline_params_from_csv, pack_move, parse_constants +from createAddressAndABIs import to_screaming_snake_case def to_address_key(name: str) -> str: - """Convert name to UPPER_SNAKE_CASE for address key.""" - return re.sub(r"_+", "_", re.sub(r"[^a-zA-Z0-9]", "_", name)).strip("_").upper() + """Address-registry key for a move/ability display name. Shares the slugifier with + createAddressAndABIs so `Address.X` here always names a key that exists there.""" + return to_screaming_snake_case(name) def to_spritesheet_key(name: str) -> str: - """Convert name to lowercase_snake_case for spritesheet key.""" - return re.sub(r"_+", "_", re.sub(r"[^a-zA-Z0-9]", "_", name)).strip("_").lower() + """Spritesheet key for a move name — same slug as the address key, lowercased.""" + return to_screaming_snake_case(name).lower() def _format_percent(value: float) -> str: diff --git a/snapshots/DoublesGasBenchmark.json b/snapshots/DoublesGasBenchmark.json index 6d8d518e..4b7ed712 100644 --- a/snapshots/DoublesGasBenchmark.json +++ b/snapshots/DoublesGasBenchmark.json @@ -1,13 +1,13 @@ { - "DoublesFloor_AllAttack8_rawGas": "909477", - "DoublesFloor_AllAttack_perTurn": "113684", - "DoublesFloor_AllRest8_rawGas": "375215", - "DoublesFloor_AllRest_perTurn": "46901", - "DoublesFloor_OneAttack8_rawGas": "681251", - "DoublesFloor_OneAttack_perTurn": "85156", + "DoublesFloor_AllAttack8_rawGas": "912875", + "DoublesFloor_AllAttack_perTurn": "114109", + "DoublesFloor_AllRest8_rawGas": "376277", + "DoublesFloor_AllRest_perTurn": "47034", + "DoublesFloor_OneAttack8_rawGas": "682873", + "DoublesFloor_OneAttack_perTurn": "85359", "Doubles_Batch12_afterDamageCalls": "0", "Doubles_Batch12_afterMoveCalls": "0", - "Doubles_Batch12_coldGas": "741594", + "Doubles_Batch12_coldGas": "743513", "Doubles_Batch12_coldSload": "34", "Doubles_Batch12_effectDataReads": "0", "Doubles_Batch12_effectDataWrites": "0", @@ -27,7 +27,7 @@ "Doubles_Batch12_totalSstore": "65", "Doubles_Batch12_updateStateCalls": "0", "Doubles_Batch12_zToNz": "7", - "Doubles_Drain6_coldGas": "468239", + "Doubles_Drain6_coldGas": "469369", "Doubles_Drain6_coldSload": "40", "Doubles_Drain6_extraAccounts": "7", "Doubles_Drain6_noop": "11", @@ -38,7 +38,7 @@ "Doubles_Drain6_zToNz": "7", "Doubles_KitBatch10_afterDamageCalls": "0", "Doubles_KitBatch10_afterMoveCalls": "0", - "Doubles_KitBatch10_coldGas": "960316", + "Doubles_KitBatch10_coldGas": "963290", "Doubles_KitBatch10_coldSload": "26", "Doubles_KitBatch10_effectDataReads": "0", "Doubles_KitBatch10_effectDataWrites": "0", @@ -58,7 +58,7 @@ "Doubles_KitBatch10_totalSstore": "89", "Doubles_KitBatch10_updateStateCalls": "0", "Doubles_KitBatch10_zToNz": "8", - "Doubles_Stage6_coldGas": "174531", + "Doubles_Stage6_coldGas": "174981", "Doubles_Stage6_coldSload": "18", "Doubles_Stage6_extraAccounts": "6", "Doubles_Stage6_noop": "0", diff --git a/snapshots/EngineOptimizationTest.json b/snapshots/EngineOptimizationTest.json index 16698b9d..327cb7b9 100644 --- a/snapshots/EngineOptimizationTest.json +++ b/snapshots/EngineOptimizationTest.json @@ -1,4 +1,4 @@ { - "ExternalStaminaRegen": "337784", - "InlineStaminaRegen": "801929" + "ExternalStaminaRegen": "338966", + "InlineStaminaRegen": "804805" } \ No newline at end of file diff --git a/snapshots/FullyOptimizedInlineGasTest.json b/snapshots/FullyOptimizedInlineGasTest.json index cca6e916..0af9e1ad 100644 --- a/snapshots/FullyOptimizedInlineGasTest.json +++ b/snapshots/FullyOptimizedInlineGasTest.json @@ -1,5 +1,5 @@ { - "Fast_Battle1_coldGas": "1075929", + "Fast_Battle1_coldGas": "1079341", "Fast_Battle1_coldSload": "199", "Fast_Battle1_extraAccounts": "52", "Fast_Battle1_noop": "23", @@ -8,7 +8,7 @@ "Fast_Battle1_totalSload": "977", "Fast_Battle1_totalSstore": "112", "Fast_Battle1_zToNz": "21", - "Fast_Battle2_coldGas": "1007012", + "Fast_Battle2_coldGas": "1010592", "Fast_Battle2_coldSload": "209", "Fast_Battle2_extraAccounts": "52", "Fast_Battle2_noop": "25", @@ -17,7 +17,7 @@ "Fast_Battle2_totalSload": "1052", "Fast_Battle2_totalSstore": "127", "Fast_Battle2_zToNz": "13", - "Fast_Battle3_coldGas": "750010", + "Fast_Battle3_coldGas": "753424", "Fast_Battle3_coldSload": "199", "Fast_Battle3_extraAccounts": "52", "Fast_Battle3_noop": "28", diff --git a/snapshots/MainnetReplayGasTest.json b/snapshots/MainnetReplayGasTest.json index d0ca1658..1bdf6ef1 100644 --- a/snapshots/MainnetReplayGasTest.json +++ b/snapshots/MainnetReplayGasTest.json @@ -16,7 +16,7 @@ "MainnetDoublesReplay1_Reused_engineStorageUniqueTouchedSlots": "55", "MainnetDoublesReplay1_Reused_engineStorageUniqueWrittenSlots": "28", "MainnetDoublesReplay1_Reused_engineStorageWrites": "161", - "MainnetDoublesReplay1_Reused_execGas": "1199563", + "MainnetDoublesReplay1_Reused_execGas": "1204616", "MainnetDoublesReplay1_Reused_externalStorageReads": "6", "MainnetDoublesReplay1_Reused_externalStorageWrites": "0", "MainnetDoublesReplay1_Reused_headerFrameCallbackPassthroughOps": "152", @@ -108,7 +108,7 @@ "MainnetReplay_Reused_engineStorageUniqueTouchedSlots": "84", "MainnetReplay_Reused_engineStorageUniqueWrittenSlots": "51", "MainnetReplay_Reused_engineStorageWrites": "403", - "MainnetReplay_Reused_execGas": "2069335", + "MainnetReplay_Reused_execGas": "2079686", "MainnetReplay_Reused_externalStorageReads": "18", "MainnetReplay_Reused_externalStorageWrites": "0", "MainnetReplay_Reused_headerFrameCallbackPassthroughOps": "532", @@ -183,10 +183,10 @@ "MainnetReplay_Reused_storage_staticCatalogUniqueWritten": "0", "MainnetReplay_Reused_storage_staticCatalogWrites": "0", "MainnetReplay_Reused_updateStateCalls": "0", - "SinglesFloor_25Noops_rawGas": "528300", - "SinglesFloor_BatchFixed": "253", - "SinglesFloor_Noop_perTurn": "21132", - "SinglesFloor_OneNoop_rawGas": "21385", - "SinglesFloor_SendIn_rawGas": "164452", - "SinglesFloor_Whole27_rawGas": "666685" + "SinglesFloor_25Noops_rawGas": "530512", + "SinglesFloor_BatchFixed": "295", + "SinglesFloor_Noop_perTurn": "21220", + "SinglesFloor_OneNoop_rawGas": "21515", + "SinglesFloor_SendIn_rawGas": "164765", + "SinglesFloor_Whole27_rawGas": "669250" } \ No newline at end of file diff --git a/snapshots/MatchmakerTest.json b/snapshots/MatchmakerTest.json index b90b54b9..7495442a 100644 --- a/snapshots/MatchmakerTest.json +++ b/snapshots/MatchmakerTest.json @@ -1,5 +1,5 @@ { - "Accept1": "334700", - "Accept2": "33755", - "Propose1": "175377" + "Accept1": "335730", + "Accept2": "34277", + "Propose1": "175947" } \ No newline at end of file diff --git a/snapshots/ProdPvPGasBenchmark.json b/snapshots/ProdPvPGasBenchmark.json index 01ffd650..99d19855 100644 --- a/snapshots/ProdPvPGasBenchmark.json +++ b/snapshots/ProdPvPGasBenchmark.json @@ -1,5 +1,5 @@ { - "BuiltinBare_5Turns_coldGas": "290846", + "BuiltinBare_5Turns_coldGas": "292011", "BuiltinBare_5Turns_coldSload": "75", "BuiltinBare_5Turns_extraAccounts": "15", "BuiltinBare_5Turns_noop": "10", @@ -8,7 +8,7 @@ "BuiltinBare_5Turns_totalSload": "435", "BuiltinBare_5Turns_totalSstore": "45", "BuiltinBare_5Turns_zToNz": "2", - "BuiltinHooked_5Turns_coldGas": "291664", + "BuiltinHooked_5Turns_coldGas": "292829", "BuiltinHooked_5Turns_coldSload": "75", "BuiltinHooked_5Turns_extraAccounts": "15", "BuiltinHooked_5Turns_noop": "10", @@ -17,7 +17,7 @@ "BuiltinHooked_5Turns_totalSload": "435", "BuiltinHooked_5Turns_totalSstore": "45", "BuiltinHooked_5Turns_zToNz": "2", - "Builtin_Drain3_coldGas": "145871", + "Builtin_Drain3_coldGas": "146226", "Builtin_Drain3_coldSload": "21", "Builtin_Drain3_extraAccounts": "3", "Builtin_Drain3_noop": "7", @@ -26,7 +26,7 @@ "Builtin_Drain3_totalSload": "236", "Builtin_Drain3_totalSstore": "24", "Builtin_Drain3_zToNz": "2", - "Builtin_Stage3_coldGas": "90115", + "Builtin_Stage3_coldGas": "90364", "Builtin_Stage3_coldSload": "9", "Builtin_Stage3_extraAccounts": "3", "Builtin_Stage3_noop": "0", diff --git a/snapshots/StartBattleGasTest.json b/snapshots/StartBattleGasTest.json index 24893086..2a1f6cfa 100644 --- a/snapshots/StartBattleGasTest.json +++ b/snapshots/StartBattleGasTest.json @@ -1,5 +1,5 @@ { - "StartBattle_Cold_coldGas": "1282524", + "StartBattle_Cold_coldGas": "1283386", "StartBattle_Cold_coldSload": "119", "StartBattle_Cold_extraAccounts": "3", "StartBattle_Cold_noop": "9", @@ -8,7 +8,7 @@ "StartBattle_Cold_totalSload": "156", "StartBattle_Cold_totalSstore": "84", "StartBattle_Cold_zToNz": "57", - "StartBattle_WarmSame_coldGas": "229518", + "StartBattle_WarmSame_coldGas": "230366", "StartBattle_WarmSame_coldSload": "129", "StartBattle_WarmSame_extraAccounts": "3", "StartBattle_WarmSame_noop": "73", @@ -17,7 +17,7 @@ "StartBattle_WarmSame_totalSload": "162", "StartBattle_WarmSame_totalSstore": "83", "StartBattle_WarmSame_zToNz": "4", - "StartBattle_WarmSteady_coldGas": "229518", + "StartBattle_WarmSteady_coldGas": "230366", "StartBattle_WarmSteady_coldSload": "129", "StartBattle_WarmSteady_extraAccounts": "3", "StartBattle_WarmSteady_noop": "41", diff --git a/src/Engine.sol b/src/Engine.sol index 0f0e15ba..52d71ae4 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -190,6 +190,46 @@ contract Engine is IEngine, MappingAllocator, EIP712 { _startBattle(battle, battleMode); } + /// @dev Sentinel-clears one side's recycled mon states. + function _clearMonStates(mapping(uint256 => MonState) storage states, uint256 count) private { + for (uint256 j = 0; j < count;) { + MonState storage monState = states[j]; + assembly ("memory-safe") { + let slot := monState.slot + let v := sload(slot) + // Clear only when dirty AND not already the sentinel (skip the same-value SSTORE). + if iszero(or(iszero(v), eq(v, PACKED_CLEARED_MON_STATE))) { + sstore(slot, PACKED_CLEARED_MON_STATE) + } + } + unchecked { + ++j; + } + } + } + + /// @dev Writes one side's roster into its fixed lanes, zero-filling unused move lanes. + function _storeTeam(mapping(uint256 => StoredMon) storage lanes, Mon[] memory team, uint256 len) private { + for (uint256 j = 0; j < len;) { + StoredMon storage dst = lanes[j]; + Mon memory src = team[j]; + dst.stats = src.stats; + dst.ability = src.ability; + // Unrolled on purpose — do NOT rewrite as a loop: a dynamic-index store into a + // struct-nested fixed array makes via-IR emit an indexed-store helper that costs one + // extra SLOAD per lane write (+32 per battle, measured); constant indices compile to + // direct slot stores. Lane count is pinned to MOVE_LANES_PER_MON (= 4, test-asserted). + uint256 n = src.moves.length; + dst.moves[0] = n > 0 ? src.moves[0] : 0; + dst.moves[1] = n > 1 ? src.moves[1] : 0; + dst.moves[2] = n > 2 ? src.moves[2] : 0; + dst.moves[3] = n > 3 ? src.moves[3] : 0; + unchecked { + ++j; + } + } + } + function _startBattle(Battle memory battle, uint8 battleMode) internal { // The matchmaker authorization gate is the ONLY matchmaker check: each player approving // the matchmaker is the trust grant. (The old validateMatch callback added no security — @@ -231,34 +271,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Clear previous battle's mon states by setting non-zero values to sentinel // MonState packs into a single 256-bit slot (7 x int32 + 2 x bool = 240 bits) // We use assembly to read/write the entire slot in one operation - for (uint256 j = 0; j < prevP0Size;) { - MonState storage monState = config.p0States[j]; - assembly ("memory-safe") { - let slot := monState.slot - let v := sload(slot) - // Clear only when dirty AND not already the sentinel (skip the same-value SSTORE). - if iszero(or(iszero(v), eq(v, PACKED_CLEARED_MON_STATE))) { - sstore(slot, PACKED_CLEARED_MON_STATE) - } - } - unchecked { - ++j; - } - } - for (uint256 j = 0; j < prevP1Size;) { - MonState storage monState = config.p1States[j]; - assembly ("memory-safe") { - let slot := monState.slot - let v := sload(slot) - // Clear only when dirty AND not already the sentinel (skip the same-value SSTORE). - if iszero(or(iszero(v), eq(v, PACKED_CLEARED_MON_STATE))) { - sstore(slot, PACKED_CLEARED_MON_STATE) - } - } - unchecked { - ++j; - } - } + _clearMonStates(config.p0States, prevP0Size); + _clearMonStates(config.p1States, prevP1Size); // Store the battle config (update fields individually to preserve effects mapping slots). // Writes are grouped by storage slot with no calls in between so via-IR coalesces each @@ -342,42 +356,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // this storage is recycled across battles, so without the fill a shorter-moveset mon would // inherit the previous battle's move words as playable moves. (Moves past // MOVE_LANES_PER_MON are truncated — the lane count matches GAME_MOVES_PER_MON.) - for (uint256 j = 0; j < p0Len;) { - StoredMon storage dst = config.p0Team[j]; - Mon memory src = p0Team[j]; - dst.stats = src.stats; - dst.ability = src.ability; - // Unrolled on purpose — do NOT rewrite as a loop: a dynamic-index store into a - // struct-nested fixed array makes via-IR emit an indexed-store helper that costs one - // extra SLOAD per lane write (+32 per battle, measured); constant indices compile to - // direct slot stores. Lane count is pinned to MOVE_LANES_PER_MON (= 4, test-asserted). - uint256 n = src.moves.length; - dst.moves[0] = n > 0 ? src.moves[0] : 0; - dst.moves[1] = n > 1 ? src.moves[1] : 0; - dst.moves[2] = n > 2 ? src.moves[2] : 0; - dst.moves[3] = n > 3 ? src.moves[3] : 0; - unchecked { - ++j; - } - } - for (uint256 j = 0; j < p1Len;) { - StoredMon storage dst = config.p1Team[j]; - Mon memory src = p1Team[j]; - dst.stats = src.stats; - dst.ability = src.ability; - // Unrolled on purpose — do NOT rewrite as a loop: a dynamic-index store into a - // struct-nested fixed array makes via-IR emit an indexed-store helper that costs one - // extra SLOAD per lane write (+32 per battle, measured); constant indices compile to - // direct slot stores. Lane count is pinned to MOVE_LANES_PER_MON (= 4, test-asserted). - uint256 n = src.moves.length; - dst.moves[0] = n > 0 ? src.moves[0] : 0; - dst.moves[1] = n > 1 ? src.moves[1] : 0; - dst.moves[2] = n > 2 ? src.moves[2] : 0; - dst.moves[3] = n > 3 ? src.moves[3] : 0; - unchecked { - ++j; - } - } + _storeTeam(config.p0Team, p0Team, p0Len); + _storeTeam(config.p1Team, p1Team, p1Len); // Set the global effects and data to start the game if any. // NOTE: hasInlineStaminaRegen AND globalEffectsLength must be (re)written on EVERY branch — diff --git a/src/mons/mulong/KingsRespite.sol b/src/mons/mulong/GrandRespite.sol similarity index 98% rename from src/mons/mulong/KingsRespite.sol rename to src/mons/mulong/GrandRespite.sol index 1321a146..994004f6 100644 --- a/src/mons/mulong/KingsRespite.sol +++ b/src/mons/mulong/GrandRespite.sol @@ -16,7 +16,7 @@ import {ITypeCalculator} from "../../types/ITypeCalculator.sol"; /// @notice Arms a delayed strike at the chosen slot that fires the next time Mulong rests. The /// rest itself is untouched, so it still regenerates stamina as normal. -contract KingsRespite is IMoveSet, BasicEffect { +contract GrandRespite is IMoveSet, BasicEffect { uint32 public constant BASE_POWER = 120; ITypeCalculator immutable TYPE_CALCULATOR; From ae6aa3e1e7534899ad91a3ab16911c0cfd042db5 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:30:29 -0700 Subject: [PATCH 39/41] clean up mons --- deployments.json | 173 ++++++++++++++------------- drool/imgs/glob_front.gif | Bin 7114 -> 4824 bytes drool/imgs/mon_spritesheet.png | Bin 83651 -> 83689 bytes drool/imgs/mon_switch.png | Bin 56230 -> 56424 bytes drool/imgs/mulong_front_damage.gif | Bin 9177 -> 6914 bytes drool/mons.csv | 4 +- drool/moves.csv | 2 +- processing/createMonShadows.py | 15 +-- processing/createMonSpritesheets.py | 57 +++------ processing/deploy.py | 3 +- processing/inputToEnv.py | 3 +- script/mons/SetupGlob.s.sol | 2 +- script/mons/SetupMulong.s.sol | 8 +- src/Engine.sol | 6 +- src/effects/battlefield/Storm.sol | 5 +- src/mons/mulong/KingsLeisure.sol | 2 - src/mons/mulong/SummonStorm.sol | 2 +- test/mons/MulongTest.sol | 52 ++++++-- transpiler/codegen/contract.py | 4 + transpiler/codegen/type_converter.py | 12 +- 20 files changed, 183 insertions(+), 167 deletions(-) diff --git a/deployments.json b/deployments.json index d796fb79..89dc55d5 100644 --- a/deployments.json +++ b/deployments.json @@ -99,112 +99,113 @@ "ZAP_STATUS": "0xaafbb5b230d6e5f16d4c84183d09975f02c7ba9f" }, "TESTNET": { - "ACTUS_REUS": "0x0100000000000000000000009f3b7b3cf7f2eec198902e0568a78f6172b3580d", - "ADAPTOR": "0x0100000000000000000000000c303798a3b08d0f0745aad740a4a64a7cc51dcd", - "ANGERY": "0x0100000000000000000000003508eebfe3a0f3996ef049154327de5efa00e2d5", - "BASELIGHT": "0xd707e3404453bcf3c587d7f8b8d8273a5f7307f9", + "ACTUS_REUS": "0x010000000000000000000000fa875c2fcfb2814a7014dbc01e9ee7f93da1a4fa", + "ADAPTOR": "0x01000000000000000000000007c1a8de61ff01840b148ed79e8fb2f0cefb7f94", + "ANGERY": "0x0100000000000000000000003b51222fedb24e2ce814db2ae955f9bf45289f00", + "BASELIGHT": "0x86bdb24d465676cc74a4dde86f4b3a20d0257b0d", "BATTLE_HISTORY": "0xa4f90f3991f2f53ba77a417b9fc094a545fba8d3", - "BIG_BELLOW": "0x996cd1abdaba80763a543f8ab3e883ec60a33d1d", + "BIG_BELLOW": "0xb24f066f94ab8966bac25773f35fd5e103a816d4", "BIG_BITE": "0x5009200000000000000000000000000000000000000000000000000000000000", - "BLESSED_STATUS": "0x0b8bc685a915dc6eb21447b400f41b5677b6fca8", + "BLESSED_STATUS": "0xa1b38d8c80c69c1602984251f6a0e02bc35b23a1", "BLOW": "0x460a200000000000000000000000000000000000000000000000000000000000", - "BRIGHTBACK": "0xdbcdcaf7a7a27178ba150c6566f831db80c3a1cb", - "BUBBLE_BOP": "0x3eb075b2c19224f76135c055df0c7bbe4a137364", - "BULL_RUSH": "0xd739f019683eb9bf21399fff056f10a42ac49994", - "BURN_STATUS": "0x79bcb8a8028dff6716d51bbc7e64f1896d153d36", - "CARROT_HARVEST": "0x01000000000000000000000096c6bf4778b6c41c78e2d1241daf0317ffe4ab61", - "CHAIN_EXPANSION": "0x170dd7c8f6e92823d0618fcb8b7f81b9a032eaad", - "CHILL_OUT": "0x00c606400000000000000000a57bdd735d684296110115843631385fb70fafe1", - "CHOSEN_ONE": "0x4d947f8c0dee3fead9823c8cfdb199e5d08e80d5", - "CHRONOFFENSE": "0x6b7c9fee59bfa15761804e6c87104e0bb238c48b", - "CLOUD_STRIKE": "0x7850e57c2a48b436b6b6bd10363280b8bd5f2d10", - "COMMIT_MANAGER": "0xcd86b934733d04ce591d2d85bd883c59a4242da7", - "CONTAGIOUS_SLUMBER": "0x2141b85fdc7da9e8b4e15947e97085c357c7ec20", - "CPU": "0x52d21794ce64d27a2755d8fc3ae32648d20ed353", - "DEADLIFT": "0xdfbe23c718f8196f3842873d5f6705e148679e39", - "DEEP_FREEZE": "0x47ea50eef09cf9bae1f7010ea310103697b12f71", + "BRIGHTBACK": "0x6d53e419622802e7ba4a23ddde7d67cc8485a29e", + "BUBBLE_BOP": "0xac4ef17d4bf79daa81facde113e49f439c202636", + "BULL_RUSH": "0x6b4840de8efd5673c31b1fd66e4ce77ee590377f", + "BURN_STATUS": "0xaddade8df8d6931ea62844aed4fe5b2e5c76aa63", + "CARROT_HARVEST": "0x01000000000000000000000052817bbe483f70ff3e668f2b6b8396c118fdd9b8", + "CHAIN_EXPANSION": "0xc084a1b58264d94356706d664c245f15f2343fbe", + "CHILL_OUT": "0x00c6064000000000000000006f5970c0cab44a290a29f6f29a8a697e098128c5", + "CHOSEN_ONE": "0xe331505e1e241ca9b07a5fc8404d7522ae26e2ef", + "CHRONOFFENSE": "0x965d2cb19709ef2c02dff0c78a7a0012d89d6801", + "CLOUD_STRIKE": "0xb52bd6a7eea7e5659559c873aa2da6fdce6b830c", + "COMMIT_MANAGER": "0x4fddc4633a3ce1bc22d06cdd0effc8b9ea677ff2", + "CONTAGIOUS_SLUMBER": "0x5d7d11dd975e7d4cccb2e182b0271a1fa7b7cb30", + "CPU": "0xba5d206e510ae613487062b8b8c13f04d8408525", + "DEADLIFT": "0x50fe1d50fe798c7c9b2711f6eb5cd175f1f9e87b", + "DEEP_FREEZE": "0x5808f4a0f8fb18f9a7ceb1a9063aba88fa018220", "DEFAULT_MATCHMAKER": "0xd89a64a438b913d4cc47a61576d27877bad7f6d6", "DEFAULT_MON_REGISTRY": "0xef5fa1a5e99184df3a379992f6c7e9eee0ef7a74", "DEFAULT_RANDOMNESS_ORACLE": "0x1c9d9769d40eba316009660f2d74b8427843cdf9", "DEFAULT_RULESET": "0xa5e678884033b94509287500a0b8e6da04a8102f", "DEFAULT_VALIDATOR": "0xaa122ac35a19eeded0cfea8e2ddeab674e0f2fe3", - "DREAMCATCHER": "0x010000000000000000000000be73911dc2908517d8eb5bc148c9b51136b5ce9d", - "DUAL_SHOCK": "0x57c36957dab3725063f1385df0725933c869c537", - "ELECTROCUTE": "0x5a4820a00000000000000000afb8129a264878b65d0d4e842771d0a57bca9cbf", - "ENGINE": "0x658182ab52de83adb4b16ab75886bc7822432503", - "ETERNAL_GRUDGE": "0xfe61e4984bbbc28f35b42090fa67aeb7ec1bb625", + "DREAMCATCHER": "0x01000000000000000000000093718c99df09d81ed877c56b52f1f85bb5bd32d8", + "DUAL_SHOCK": "0x6dc3282d059b0411b94f2832c270a70939ad16a0", + "ELECTROCUTE": "0x5a4820a0000000000000000043754744f2b1217eff5bb433727c22027fca8a95", + "ENGINE": "0x4b28758db9be7f96f523d8569ef2155e869606e7", + "ETERNAL_GRUDGE": "0x87fcacdbef61f37ca08106c8562e8c2ce0ea0d74", "FAIR_CPU": "0xc02f904dd32d244d8e60e2a504429f3f4b091de4", "FEDERAL_INVESTIGATION": "0x644c300000000000000000000000000000000000000000000000000000000000", - "FOUL_LANGUAGE": "0xd1aa3fd61ac93931b291fe9bf535f1b5b4e0fbd1", - "FROSTBITE_STATUS": "0xa57bdd735d684296110115843631385fb70fafe1", - "GACHACHACHA": "0x82f9eda4c713d007d879cf4c9db92bb70dcfd0d9", - "GACHA_TEAM_REGISTRY": "0xd3357dbb90451d94cf689ec88b69b64188fafb3c", - "GET_SLIPPERY": "0xfcb758d590978ce1a240149a07bcc27728bfb84b", - "GILDED_RECOVERY": "0x39591a30ea60593a4a47eb31a9f249ba10d7e955", - "GRAVE_AFFLICTION": "0x62db7597af221b607dd0ea582967a77b1a220609", - "GUEST_FEATURE": "0x5b08b1b2e2b55bec525626d5db7455b51a6cb3ef", - "HARD_RESET": "0x6549498c1e9cf90aedfb014c5aba1361b61648a3", - "HEAT_BEACON": "0x93d5c076b77a63487e903e234f434924e65dc047", - "HIT_AND_DIP": "0x1286ae46c229cf9f720304c7a1531b7cf7908cf2", - "HOLY_MOLT": "0x8e4c9053d7bdcdb6ccf3b34476fd888362c6a79e", + "FOUL_LANGUAGE": "0x7662b106f23c177f4b5c298bf899e361f1707477", + "FROSTBITE_STATUS": "0x6f5970c0cab44a290a29f6f29a8a697e098128c5", + "GACHACHACHA": "0x3d8952e98dc2997492162b792d3a90ca267456ee", + "GACHA_TEAM_REGISTRY": "0xe4b3c22643d28e3dbcd05c55170924188984c33a", + "GET_SLIPPERY": "0x2649a249a2235b620a26f3122d9ee79667ee8c33", + "GILDED_RECOVERY": "0x7814acb487ef161926f4ce261c6003ff51a62af6", + "GRAND_RESPITE": "0xbdeb19030491b6ae6f329e7a702c38ad90379381", + "GRAVE_AFFLICTION": "0x0a24b9e8572e534bbf79ddd614871b905f45ea9a", + "GUEST_FEATURE": "0x810ec98fd75ae36532ed06f933f493a95e236c44", + "HARD_RESET": "0x14da591b8fbe8b4b068f785ec6c11d5ac46a8b8a", + "HEAT_BEACON": "0xb4627fc4eeef5aadbbd8d4279d9da706b8168641", + "HIT_AND_DIP": "0xb3c677b3a28ed0b3099d4090a906456ca69926ae", + "HOLY_MOLT": "0x11d9a1283c249e18c6ad1e1929756f58314a3f71", "HOLY_WATER_CLAW": "0x5a09300000000000000000000000000000000000000000000000000000000000", - "HONEY_BRIBE": "0x13ad61482a3c6d0ec1d2e44858d7fa6b090c73fe", - "INFERNAL_FLAME": "0xf1cf792ec713a40d8dc49c1437198a3c4cf6b955", - "INFINITE_LOVE": "0x5a4d30a00000000000000000c8a998cf6c0ccb4f726696c95ab1862eab251f7a", - "INITIALIZE": "0x39f703dac1c2d2900c5df9c08f7f34f95725d233", - "INTERWEAVING": "0xdcf13b32db15011c5a3d7f38e3daff492225763e", - "INVOKE_TABOO": "0x24b64e5043768bf63e5f20b0d6976ab53d86fb97", - "IRON_WALL": "0xd6a57132eaa52656f4a69d9087c0014a0a64e8b7", - "KINGS_LEISURE": "0xc351a294db9b4248cdcd2f1443f75eb746987c21", + "HONEY_BRIBE": "0x577b15a47a1fdc0e0a847d1dcf8161502264cd07", + "INFERNAL_FLAME": "0x49a668b09f4fe15fdcaf2e0eb8085e0481181e75", + "INFINITE_LOVE": "0x5a4d30a00000000000000000ebd8bceba62a46ae12920c6150de4ceebcf1da34", + "INITIALIZE": "0xea31e6448eb1c8b6ce4ef1ccab28f90021328de8", + "INTERWEAVING": "0x4b2c248c1598bfda2af676e6c54d74c5e9b063fa", + "INVOKE_TABOO": "0x8991c329cc9516d4909918959bbacb0307f823af", + "IRON_WALL": "0x2ee718f9f211f0565a6ec7dc271e5a208efb70d8", + "KINGS_LEISURE": "0xc592fa50a0994185df7907a6115fc96863383028", "KINGS_RESPITE": "0xe56793639eb661cf2df0334462e12c0f7c0d321d", - "LOOP": "0xd5e30372ef1710577d33c4402ce910b239c7f83c", - "MEGA_STAR_BLAST": "0x272bdd9f7a0531c462293a452833ec8cc477bb4f", - "MODAL_BOLT": "0x4cf59390f6013e2e880e32a2b4defaade82d57f0", - "NEGATIVE_THOUGHTS": "0x504b30a000000000000000002cad69c1b73c736fdf5642d66020d3110bc5f54b", + "LOOP": "0xbb052e2e142a11ab33bec4aa7222aebe8e1f74d9", + "MEGA_STAR_BLAST": "0xa7d7d7a92309c3e7190fbfddf036342f8491ee53", + "MODAL_BOLT": "0x541cbbadc9151aaa6407bc56f774be61ce50bbc9", + "NEGATIVE_THOUGHTS": "0x504b30a00000000000000000e50dc88a28c9e0bb923b9e08f2357cd78f0ced3c", "NIGHT_TERRORS": "0xd9cc94ce09177091609222b02854f671deaa2579", - "NINE_NINE_NINE": "0x04001f82534b5462e96b3636ecc6562607fd4b7b", + "NINE_NINE_NINE": "0x2342022e2f54456c8adea785c08e0b78bbdea1ce", "OKAY_CPU": "0xc14f1a8cdbf755055fee3576a6d06791f72232bf", - "OLD_VENGEANCE": "0x2b6b3e6618c8edb67850df61f066c2ab9b67a566", + "OLD_VENGEANCE": "0x6b1e305fadc1cf4c43da35db7a9dbaca7ca58a16", "OSTEOPOROSIS": "0x5a00200000000000000000000000000000000000000000000000000000000000", - "OVERCLOCK": "0xc0ea2de762fa2a07f8c89736d33a8c5fd8972dd3", - "OVERFLOW": "0x2e38634bc198561ccc95fa892ee2fd9936d1349d", - "PANIC_STATUS": "0x2cad69c1b73c736fdf5642d66020d3110bc5f54b", - "PISTOL_SQUAT": "0x7ccd5b86109946b3488b20d06777d191e3abe7af", - "POST_WORKOUT": "0x01000000000000000000000058a3415ec3cf85211be9fde7ba82aeed9b8de721", + "OVERCLOCK": "0x0dcdd62e545475233068e9442383fe0ef9db5ef7", + "OVERFLOW": "0x93bd53413e61e24d4188ef57bff9ddab425a74fd", + "PANIC_STATUS": "0xe50dc88a28c9e0bb923b9e08f2357cd78f0ced3c", + "PISTOL_SQUAT": "0xd262aace00d0cc267d64cfd3f4c2009abd774977", + "POST_WORKOUT": "0x0100000000000000000000006a62f5873cda2fe43f7ed940c2acf4952416643c", "POUND_GROUND": "0x5f02300000000000000000000000000000000000000000000000000000000000", - "PREEMPTIVE_SHOCK": "0x1e17a129f325553ccc884779e3ccbae26283dbcf", - "Q5": "0x82572ab5783b5a117b87ecbfe8978c3d6d0cba09", - "QUICKSTORM": "0xfb4d7a254967406ed1c714bef0c06a734b27d4ed", - "RENORMALIZE": "0x2b7d4e68f71523036e40cb1f8e39d8a73278d846", - "RISE_FROM_THE_GRAVE": "0x4d7255f78ba6b9bae695e979173d840c561e94f6", - "ROCK_PULL": "0x739762872302827919e0c9575eb01318dd30a47a", - "ROUND_TRIP": "0xd4893d6d59d8c14ea4b8b4a07a618a96aeb73d73", - "SANCTIFY": "0x5e5a2e7791267cfb34f62f5f4f9a2a479efde004", - "SAVIOR_COMPLEX": "0x90ff72bd13256d57f04c51e83e48b5c23bae2387", - "SCARY_NUMBERS": "0x500b314000000000000000002cad69c1b73c736fdf5642d66020d3110bc5f54b", - "SET_ABLAZE": "0x3675d2ccee3e3796974ac01745fe4e661bf53d93", - "SIGNED_MATCHMAKER": "0xf20ab3832c67bd52cd3e05dc9af01479afd2a8a0", - "SIMPLE_PM": "0xd0b08465b4a259e528de2eac38cab2a2a94dca3c", - "SLEEP_STATUS": "0xc8a998cf6c0ccb4f726696c95ab1862eab251f7a", - "SNACK_BREAK": "0x69ba80b9520d023c46ff1940a7e01c062b55cc59", - "SNEAK_ATTACK": "0xdc79d473a52c9ce0af2680286c981ba8ca0ae343", - "SOMNIPHOBIA": "0x2f703f4d0e173ed3b0fa738fb3e360ec1f0ae15f", + "PREEMPTIVE_SHOCK": "0xd2d20bbc8c9be2f162c8e63d7f511bdc1b2b23b7", + "Q5": "0xe9a9c31c9bc86d6647deb31beb6f16b1b7f8e5ed", + "QUICKSTORM": "0x8ade290e5b72aa5770af7ad143cab8faa041aa57", + "RENORMALIZE": "0x9b95349047e734d11ce6eeeaa8fe10b81da3d6b2", + "RISE_FROM_THE_GRAVE": "0x4cc389702a30109bc0625753657779d9a34348cd", + "ROCK_PULL": "0xd3a0f31b04b70eb9bbdb7c27067d78a9a9dafe60", + "ROUND_TRIP": "0x28ab1b8b1c5e68cf83e7612f5e9d57c5d94eb303", + "SANCTIFY": "0xeb5739ebb33e1397c92dc481c98f25b07e215e91", + "SAVIOR_COMPLEX": "0xe562b4a060ca9d95e23da672b8289e39ddba7894", + "SCARY_NUMBERS": "0x500b31400000000000000000e50dc88a28c9e0bb923b9e08f2357cd78f0ced3c", + "SET_ABLAZE": "0x4b9ae643536d9edddee38573d4b02ca56706584b", + "SIGNED_MATCHMAKER": "0xc41d5ea3b36ee9025b8c7adb95437e0d2de89013", + "SIMPLE_PM": "0x928d58230919cf36baea288e495ee0d2e9a21967", + "SLEEP_STATUS": "0xebd8bceba62a46ae12920c6150de4ceebcf1da34", + "SNACK_BREAK": "0x1ce7ae46c3442ddae00128970c7561a8a36fb765", + "SNEAK_ATTACK": "0x59e593b53be3ac34ddc88363734261949c900ad8", + "SOMNIPHOBIA": "0x2eb9465482bdfcf6e3d867eb19b3be4e0032feba", "STAMINA_REGEN": "0xe9c82033011dedf20d2fe69920b5020403041d0f", "STAT_BOOSTS": "0x8c072f12345b3e95fa52fcdf74610c044c044940", - "STORM": "0x9a6e108254e205c09030a6dd10408065f3983328", - "SUMMON_STORM": "0x083dea143a38a20b5067cdf49c5d1924f172fa56", + "STORM": "0x7b16496037b25d123c2c2f4ec6f765e159d01aac", + "SUMMON_STORM": "0x4294b6f18a141d8aaf8ea3c192cb15b8369cc0e2", "THROW_PEBBLE": "0x2802100000000000000000000000000000000000000000000000000000000000", - "THUNDERCLAP": "0x784831400000000000000000afb8129a264878b65d0d4e842771d0a57bca9cbf", - "TINDERCLAWS": "0x01000000000000000000000041713d25dffaf7d512c3ce7e1e02c4e3514144b0", - "TRIPLE_THINK": "0x04650339cfca6c1f2a170b94eed45abc92ee9084", - "TYPE_CALCULATOR": "0x065da72dd0d9855ba991c20b03c7514cfb7e3abc", - "UNBOUNDED_STRIKE": "0x00faeafa8dc75baaa6d60a0aa27005f33f23fa19", + "THUNDERCLAP": "0x78483140000000000000000043754744f2b1217eff5bb433727c22027fca8a95", + "TINDERCLAWS": "0x0100000000000000000000002d5d9ca9150213567b68a95a3c21ecd56278f216", + "TRIPLE_THINK": "0xacad3b211c235ac3b331a7fa4fbb4e219cfc7f3e", + "TYPE_CALCULATOR": "0x8ec1015b3fc471ebccf3269bf2cf07275bf84745", + "UNBOUNDED_STRIKE": "0xdedbd81a2cd3e6186d8997f4d95e1b0eb2841692", "UNEXPECTED_CARROT": "0x7807400000000000000000000000000000000000000000000000000000000000", - "UP_ONLY": "0x0100000000000000000000009c42f663defcbdb0046f703a271636afaab77ae6", - "VITAL_SIPHON": "0xc573b67221c957ec2d4a3bfb4571c51f96e72605", - "VOLATILE_PUNCH": "0x564dfb46ed2924bc5c6fc18e20cfaeeeb72633cf", + "UP_ONLY": "0x01000000000000000000000047a10b47cf900e5caa67ea22b5c332458bf2f505", + "VITAL_SIPHON": "0x65f66d32ecda5787934531b31ddf207ba50cb34b", + "VOLATILE_PUNCH": "0x97c7fb1a7bfc523a0dcb8b76ef59f526f505446c", "WATER_CLAW": "0x5003200000000000000000000000000000000000000000000000000000000000", - "WITHER_AWAY": "0x379ef31cb4dd355cefc3c596f085e9d81e7e7834", - "ZAP_STATUS": "0xafb8129a264878b65d0d4e842771d0a57bca9cbf" + "WITHER_AWAY": "0x9c82ffa894555ed0867604499ca2c4be24e3e8b9", + "ZAP_STATUS": "0x43754744f2b1217eff5bb433727c22027fca8a95" } } diff --git a/drool/imgs/glob_front.gif b/drool/imgs/glob_front.gif index 7e31b000f18eccc32552a27aba039129f3f38866..ee02a384a3de44fe6ee05e86c466de9553e7c842 100644 GIT binary patch delta 2822 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 diff --git a/drool/imgs/mon_spritesheet.png b/drool/imgs/mon_spritesheet.png index 1a34136c0f58f8819f4df9b3c1dfd4fbba23be6e..1c2903c11fb80eb080ca1b8fc3b6f89fb6882324 100644 GIT binary patch delta 62508 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 delta 62469 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 diff --git a/drool/imgs/mon_switch.png b/drool/imgs/mon_switch.png index 047608f05ce90b02abaa84a1540cf4e49c85a678..2a706b23a3870228039dff76c1f61b4334e44391 100644 GIT binary patch literal 56424 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 P00000NkvXXu0mjf!y&BI diff --git a/drool/imgs/mulong_front_damage.gif b/drool/imgs/mulong_front_damage.gif index 964fbca4bfa0475dc2e30505b03a79884e5e6bcf..4e5cbeade2ae57ad1b853431bb19c8cffded1cd3 100644 GIT binary patch delta 3288 zcmV;}3@7v1M}jsCM@dFFIbmP`U;yU|u?!Le836zx`2-0H0RI3i00000U;tnM00{m7 z{|S>f1GNbJ4|LO1pNd4i|&&&1^fj5ysiC{GzR+x{zk&zs*^Ma z`Ud7AS})(`lQanE5&r-z2LK2FSO87{00{m7{|S>V3}piJpOcgfIRgI)lg|uU0{-`t zDh>UA{u}oAr*>uL^jJtkHl*sRvjO0%j!i-)>uM`j)?=whVS1@VnDXiv02TqDny=J8 zNvyG0A)sxHZ(51#RL-KLfUgTkdjPaoUCZO70cg7|ghnn4Zl`T+My-D29;n%*P$Jmw zw(yB4FG2oC4TIGA_(QSv1OdAKn8p(Pyh+%=wfZ4-Uh5@ z)B=Fjj#%+%F_he9e5Ggbt_5<)BCjmzzQ^KtW6LhT96-z>8MSeY3_GT<(mn26)Um?K zYH`fbRSlD8u^qdmiakRe<(NMc2A!}*&X((0DE(#B(!?dLfRh0vAa??>ehszA?tKJ* zcmCO-eOHj)KrQ(uUpK?q3~-AsbS(+&CM>qVhaa0 z*^NziQBsWCw-lP9A+U9hp2H4wyPy{U;N5wNhbrY<)BdM?3TVr!im^v|F#@0m39oE2 zmwxMujNc5jcFYYRJZ*;dO!2=h2b^Pn+RYDls^o9izTg#G(ATt%k1x^3ah zh^wkPVw>Q^2e{jIW%w!6!o+r!3gIU?dXd@lNK!dqC5>N`>YwY{1}hyc4QTL#5Liqk zCHg%tci&1~`NmYfe!)e3qOqXTnpDCv!S6=$TZ-Va)I8W^#7PT_(C1D#LE035PKEvu z#E9f_l(XXK2Xk(!-^{r7HrZ$mdG6C3$byKg6wOD5n-NX@-t!M8HnDJ_Nna1Qw2<~) zN=74m7;AtRzP@!aM@oZZ>(nFs5uI>D?D+Q%5*G=um0_RQm!q ztId#aPyhK2R(`@ZXZ1~BM$%b--KGdA1?}pCTs#OEL1Iaw$w+z?TwetFXFLB5XJcSQ zBcG_2r42I7mL}|1W(c6Z++EEev4k8MGe*idzG;ejY+q$~`9a7<<|oZ`UJ|+Gm(U2Z zi3v;N(BRigU%m|?g^SH>&X|+37!!<8c^e@Su#g^#Q)%E#;&32`6r>P;PL6T+9HnE@ z2TJEX6Pof=pm>J3HhhLmpJ5Crs~R#&8j5c^4;rL34N6EYzG*uDBuFgBDNTrKseC@P z;#`!Br%I{j7WU1Ev{Z|;T%Js~mcohLlvkfw6{mP=%~b95 zJ;H=j>4ut3cy0tbGL0X9uSO)zN@kQ&K+(OPFa3)<3Z^jKX)Ek^t;736mccU=#la1SqOhVpEJxe_otwaKP21|C5pxhywnKlN=WPfByMBS(%xo z(gnb+04S-Fst_UxVXL2lx+0Fpq3BVqiQO6!0HWb~E0P2LYUE?S3Okpmqk4s`OSoRt zY)#UNwra2eAYiV!Jmyv*N~LnP*R8U3J65!w&3NRw2n2PRQx6&7uXPGYMVGG|?h3$n z@-ARx0HS?L?YQNlt0G~tfBFlQKp$<@XQN1=x@3UPO11Et5mKA)#1x;FA6Wqe%(2L- zj#g~|3BOEj$<$()TBxUqHI={uBUWw9KUMoOv^W=_vq*9&Ht4KA6ReuXGaC)<#$%0( zBi218rlVyqEhbXXtC7uQ012d>K(ALDz1WS$1+4L9m{mtnU94f*fAs#qE#v8{+G&%V zbf0ebouF>UVmy|b-*s5)-+@cDHrq+l#5LE3s&?@|Uj>R(RpDWVKxStHPW7ANK4zKQ zDkf)aQ>PPYqT}YSCT`UYe~JLZ0mdB{%1mQcVAg!IYGiZ+)Gl<55AP1&Q;miVd~TD{ zH>JH2?s)*dqX!&ef9_J5%ll}ncO2gI8a8Pr!}U74uxRHd#;~ul1r0O#D)n7e!|NS3 zarF#I3EytlH%Eyz9Zwio3oP<~7D9+wF4 zz{PPaG*}8A%H%?*0tt8lqRR}I815ZPAvc6d@QAplmw+TcKk-@=7r2_V&4$yOkO0AG zrY9wmR;5~6e;5ZgFkTgKNk#fp!w7&cFhcPlRNUef|98E^AjxHg>DA4~6TkEX43$po znrbxZKlvdih%YptXGrNo4Kb!5eY2(Tm_o5H3QdP~#60pf@&oP7zND)DYXA+_N>hi#&8F8;q<(a1whJ9K zq#s4re+uO%v@rn{jZwwu|EOw6t6uer3%RI0DTz{%7PU~il;10JYABfAlR97RY9Xbn z%7a#wGGCe6N=vFaoywFrPtBl1)=ER44vaV$i;7$$;;XqH@u7$nj9AOI)5zMUTBMul zSuNX@nH{oP9SH~kA^8Le3IP8AECc`w09XJ{2LJ#F{s8|8lOGi-lQ$k00`uRKP#!M< z|C4|ohywlwlL{aGfBq$UWofCE8EF*&xax*wkq?U4rmJHP1?!}v(u$i)m$~>E02cv( zC2YNJ#%llwSZI@Rv3f;oVxc{I0Itlk39Ynl&WOOd)pnU+NR-l6E0HONRArXA>NX>Q zFP1Cqxy7k_;hYW`Ag}=g33{2FTcWC~b5~B=CWKd}dtPaje5K8Sb~$`y$#JpUkcCvyNaJZhx?J;G<kb? zmW^FDk$3;aMt>Z;b|qp)w0!|r8(=|Xq$`f=di(*ntBu8CY4xoXN=qBDj_hppTDBZIOycb zMI?6LnJl@OPBl6A&)&v{`MIn=JGJMa*Yq=Xa4Nq%f4ijCz3OY^KZkD1RiYm?`mt*t zRCm&QZ)xePUnb;}GFEBWVGM07o2Mf1TF%q*CGRU$rug&qSH8UL63Fh?L@8 z3KBIPx$TD_soMoVco`7ts{o;D+hq{)v(k`GDwJ_v?)I3wTK16uX)B>GTgJ;_jn0J_ zu})M%xxOl@v04gIrY?7R5Kc<*PC%rS4*5tqt!;z^tJli)oo6HqT2OPI#GR40u|q=(FNatSq9LMQsJRSwix1^uI; zC@QwPMdYCzEsrZhTDMwB1f(H7P(qt{sA^i&AxR^t7N4ombp>>hU=mhJ4T@2l*3YE| z%BUwpYO-2A?PPCLN66n=@e-rfBgqaQRCI&bS1Czgex}< W8<@Z@^0ADKYzG4?SjuW7AOJg6rWo=7 delta 3273 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 None: print(f"{name}: {len(shadows)} shadow frames, footprint ~{w0}px wide") sheet, positions = build_spritesheet(all_frames, frame_size=FRAME_SIZE) - save_and_compress_png(sheet, imgs_dir / "mon_shadow.png", "Shadow spritesheet") - - # Ship the packed sheet alongside the other mon spritesheets. - munch_assets_dir = ( - Path(__file__).parent.parent.parent / "munch" / "src" / "assets" / "mons" / "all" - ) - if munch_assets_dir.exists(): - save_and_compress_png( - sheet, munch_assets_dir / "mon_shadow.png", "Munch shadow spritesheet" - ) - else: - print(f"⚠ Munch directory not found, skipping copy: {munch_assets_dir}") + save_packed_sheet(sheet, imgs_dir, "mon_shadow.png", "Shadow spritesheet") for name, start, count, ms, cx in entries: metadata[name] = { diff --git a/processing/createMonSpritesheets.py b/processing/createMonSpritesheets.py index b793caa3..7565c79b 100644 --- a/processing/createMonSpritesheets.py +++ b/processing/createMonSpritesheets.py @@ -158,9 +158,21 @@ def save_and_compress_png(sheet: Image.Image, path: Path, description: str) -> N run_pngout(path) +# Packed sheets are consumed from the sibling munch checkout, which may not be present. +MUNCH_MON_ASSETS_DIR = Path(__file__).parent.parent.parent / "munch" / "src" / "assets" / "mons" / "all" + + +def save_packed_sheet(sheet: Image.Image, imgs_dir: Path, filename: str, description: str) -> None: + """Write a packed sheet to the imgs dir and mirror it into munch's assets.""" + save_and_compress_png(sheet, imgs_dir / filename, description) + if MUNCH_MON_ASSETS_DIR.exists(): + save_and_compress_png(sheet, MUNCH_MON_ASSETS_DIR / filename, f"Munch {description.lower()}") + else: + print(f"⚠ Munch directory not found, skipping copy: {MUNCH_MON_ASSETS_DIR}") + + def create_mini_spritesheet( imgs_dir: Path, - munch_assets_dir: Path, metadata: dict, ) -> None: """Pack every frame of every 32x32 *_mini.gif into one shared sheet. @@ -198,12 +210,7 @@ def create_mini_spritesheet( return sheet, positions = build_spritesheet(all_frames, frame_size=MINI_FRAME_SIZE) - drool_mini_path = imgs_dir / "mon_mini.png" - save_and_compress_png(sheet, drool_mini_path, "Mini spritesheet") - - if munch_assets_dir.exists(): - munch_mini_path = munch_assets_dir / "mon_mini.png" - save_and_compress_png(sheet, munch_mini_path, "Munch mini spritesheet") + save_packed_sheet(sheet, imgs_dir, "mon_mini.png", "Mini spritesheet") for mini_key, start, count, rate in mini_entries: frames_list = [list(positions[start + i]) for i in range(count)] @@ -214,7 +221,6 @@ def create_mini_spritesheet( def create_micro_spritesheet( imgs_dir: Path, - munch_assets_dir: Path, metadata: dict, ) -> None: """Generate 16x16 micro spritesheet from icons/*_icon.gif. @@ -262,12 +268,7 @@ def create_micro_spritesheet( print(f"Extracted {len(frames)} micro frames from {icon_path.name} (rate: {frame_rate}ms)") sheet, positions = build_spritesheet(all_frames, frame_size=MICRO_FRAME_SIZE) - drool_micro_path = imgs_dir / "mon_micro.png" - save_and_compress_png(sheet, drool_micro_path, "Micro spritesheet") - - if munch_assets_dir.exists(): - munch_micro_path = munch_assets_dir / "mon_micro.png" - save_and_compress_png(sheet, munch_micro_path, "Munch micro spritesheet") + save_packed_sheet(sheet, imgs_dir, "mon_micro.png", "Micro spritesheet") for mini_key, start, count, rate in mini_entries: frames_list = [list(positions[start + i]) for i in range(count)] @@ -330,31 +331,11 @@ def create_spritesheets(gif_files: list[str], output_dir: str): sheet, positions = build_spritesheet(all_frames) output_path = Path(output_dir) - # Save to output directory - main_sheet_path = output_path / "mon_spritesheet.png" - save_and_compress_png(sheet, main_sheet_path, "Spritesheet") + save_packed_sheet(sheet, output_path, "mon_spritesheet.png", "Spritesheet") # Build switch spritesheet switch_sheet, switch_positions = build_spritesheet(all_switch_frames) - switch_sheet_path = output_path / "mon_switch.png" - save_and_compress_png(switch_sheet, switch_sheet_path, "Switch spritesheet") - - # Determine munch output location (similar to drool/combine.py) - base_path = Path(__file__).parent - game_dir = base_path.parent.parent - munch_assets_dir = game_dir / "munch" / "src" / "assets" / "mons" / "all" - - # Copy to munch if directory exists - if munch_assets_dir.exists(): - print(f"\n📋 Copying to munch repository: {munch_assets_dir}") - - munch_main_path = munch_assets_dir / "mon_spritesheet.png" - save_and_compress_png(sheet, munch_main_path, "Munch spritesheet") - - munch_switch_path = munch_assets_dir / "mon_switch.png" - save_and_compress_png(switch_sheet, munch_switch_path, "Munch switch spritesheet") - else: - print(f"\n⚠ Munch directory not found, skipping copy: {munch_assets_dir}") + save_packed_sheet(switch_sheet, output_path, "mon_switch.png", "Switch spritesheet") # Finalize metadata for name, data in metadata.items(): @@ -375,11 +356,11 @@ def create_spritesheets(gif_files: list[str], output_dir: str): # Build 32x32 mini spritesheet (raw *_mini.gif frames) and attach top-level # frames / msPerFrame to each *_mini.gif entry. - create_mini_spritesheet(Path(output_dir), munch_assets_dir, metadata) + create_mini_spritesheet(Path(output_dir), metadata) # Build 16x16 micro spritesheet (downscaled icons) and attach a `micro` # sub-key on each *_mini.gif entry. - create_micro_spritesheet(Path(output_dir), munch_assets_dir, metadata) + create_micro_spritesheet(Path(output_dir), metadata) # Save JSON with compact coordinate arrays json_path = Path(output_dir) / "spritesheet.json" diff --git a/processing/deploy.py b/processing/deploy.py index 66980717..622fb8bf 100755 --- a/processing/deploy.py +++ b/processing/deploy.py @@ -93,7 +93,8 @@ def update_env_file(matches: list[tuple[str, str]], env_path: Path): # Add new addresses for name, address in matches: - key = name.upper().replace(" ", "_").replace("-", "_") + # Drop characters that are invalid in an env var name (e.g. "King's Leisure"). + key = re.sub(r'[^A-Z0-9_]', '', name.upper().replace(" ", "_").replace("-", "_")) existing[key] = address # Write back sorted diff --git a/processing/inputToEnv.py b/processing/inputToEnv.py index d43ccc49..c64f39b3 100644 --- a/processing/inputToEnv.py +++ b/processing/inputToEnv.py @@ -31,7 +31,8 @@ def parse_content_to_env_lines(content): deploy_data_matches = parse_deploy_data_array(content) if deploy_data_matches: for name, address in deploy_data_matches: - name = name.upper().replace(" ", "_").replace("-", "_") + # Drop characters that are invalid in an env var name (e.g. "King's Leisure"). + name = re.sub(r'[^A-Z0-9_]', '', name.upper().replace(" ", "_").replace("-", "_")) output.append(f"{name}={address}") return output diff --git a/script/mons/SetupGlob.s.sol b/script/mons/SetupGlob.s.sol index f8977fc6..da0adc8f 100644 --- a/script/mons/SetupGlob.s.sol +++ b/script/mons/SetupGlob.s.sol @@ -41,7 +41,7 @@ abstract contract GlobDeploy is Script { function _registerGlob(GachaTeamRegistry registry, address[3] memory addrs) internal { MonStats memory stats = MonStats({ - hp: 301, + hp: 256, stamina: 5, speed: 151, attack: 180, diff --git a/script/mons/SetupMulong.s.sol b/script/mons/SetupMulong.s.sol index 87eebc25..02588e5e 100644 --- a/script/mons/SetupMulong.s.sol +++ b/script/mons/SetupMulong.s.sol @@ -10,8 +10,8 @@ import {GachaTeamRegistry} from "../../src/game-layer/GachaTeamRegistry.sol"; import {IEffect} from "../../src/effects/IEffect.sol"; import {Storm} from "../../src/effects/battlefield/Storm.sol"; import {CloudStrike} from "../../src/mons/mulong/CloudStrike.sol"; +import {GrandRespite} from "../../src/mons/mulong/GrandRespite.sol"; import {KingsLeisure} from "../../src/mons/mulong/KingsLeisure.sol"; -import {KingsRespite} from "../../src/mons/mulong/KingsRespite.sol"; import {SummonStorm} from "../../src/mons/mulong/SummonStorm.sol"; import {ITypeCalculator} from "../../src/types/ITypeCalculator.sol"; @@ -29,8 +29,8 @@ abstract contract MulongDeploy is Script { deployedContracts[0] = DeployData({name: "Summon Storm", contractAddress: addrs[0]}); } { - addrs[1] = address(new KingsRespite(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); - deployedContracts[1] = DeployData({name: "King's Respite", contractAddress: addrs[1]}); + addrs[1] = address(new GrandRespite(ITypeCalculator(vm.envAddress("TYPE_CALCULATOR")))); + deployedContracts[1] = DeployData({name: "Grand Respite", contractAddress: addrs[1]}); } { addrs[2] = address(new CloudStrike(Storm(storm), IEffect(vm.envAddress("FROSTBITE_STATUS")))); @@ -59,7 +59,7 @@ abstract contract MulongDeploy is Script { type2: Type.Lightning }); uint256[] memory moves = new uint256[](4); - moves[0] = 0x0030000000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); + moves[0] = 0x0030400000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[0])); moves[1] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[1])); moves[2] = 0x7848314000000000000000000000000000000000000000000000000000000000 | uint256(uint160(vm.envAddress("ZAP_STATUS"))); moves[3] = 0x0030200000000000000000010000000000000000000000000000000000000000 | uint256(uint160(addrs[2])); diff --git a/src/Engine.sol b/src/Engine.sol index 52d71ae4..f0b354e0 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -3100,10 +3100,14 @@ contract Engine is IEngine, MappingAllocator, EIP712 { DamageCalcContext memory ctx; bool ctxSeeded; + // Mode-gated lanes, not raw _slotActive: singles leaves activeMonExt at 0, so odd slots + // would resolve to "mon 0" and let a full mask hit a benched mon (or the active twice). + uint256 actives = _hookActivesWord(battle); + for (uint256 s; s < 4;) { if (targetBits & (1 << s) != 0) { uint256 defenderPlayerIndex = s >> 1; - uint256 defenderMonIndex = _slotActive(battle, s); + uint256 defenderMonIndex = TargetLib.activeAt(actives, s); if ( defenderMonIndex != EMPTY_ACTIVE_LANE && !_getMonState(config, defenderPlayerIndex, defenderMonIndex).isKnockedOut diff --git a/src/effects/battlefield/Storm.sol b/src/effects/battlefield/Storm.sol index 304737c1..3d12fb17 100644 --- a/src/effects/battlefield/Storm.sol +++ b/src/effects/battlefield/Storm.sol @@ -13,10 +13,11 @@ import {IEffect} from "../IEffect.sol"; /// Damage runs the normal formula off the summoner's stats, so it tracks their buffs and /// debuffs and keeps resolving after they leave the field. contract Storm is BasicEffect { - uint256 public constant DEFAULT_DURATION = 7; + uint256 public constant DEFAULT_DURATION = 5; uint32 public constant BASE_POWER = 50; - // Every absolute slot; the engine skips empty lanes and KO'd mons. + // Every absolute slot; the engine skips vacant lanes and KO'd mons, so this resolves to the two + // actives in singles and all four slots in 2-slot modes. uint256 private constant ALL_SLOTS = 0xF; // extraData: [duration 0-7 | casterPlayerIndex 8-15 | casterMonIndex 16-23] diff --git a/src/mons/mulong/KingsLeisure.sol b/src/mons/mulong/KingsLeisure.sol index 5db6b438..919a2b5d 100644 --- a/src/mons/mulong/KingsLeisure.sol +++ b/src/mons/mulong/KingsLeisure.sol @@ -31,8 +31,6 @@ contract KingsLeisure is IAbility, BasicEffect { } engine.setGlobalKV(armedKey, 1); - // Perm, not Temp: the debuff has to survive bench cycles, so it is applied once here and - // cleared once in onRemove. StatBoostToApply[] memory statBoosts = new StatBoostToApply[](2); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.Attack, boostPercent: ATTACK_DEBUFF_PERCENT, boostType: StatBoostType.Divide diff --git a/src/mons/mulong/SummonStorm.sol b/src/mons/mulong/SummonStorm.sol index 60808cd3..5a4068b1 100644 --- a/src/mons/mulong/SummonStorm.sol +++ b/src/mons/mulong/SummonStorm.sol @@ -31,7 +31,7 @@ contract SummonStorm is IMoveSet { } function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) { - return 0; + return 4; } function priority(IEngine, bytes32, uint256) public pure returns (uint32) { diff --git a/test/mons/MulongTest.sol b/test/mons/MulongTest.sol index 06ca94e1..2fb075d8 100644 --- a/test/mons/MulongTest.sol +++ b/test/mons/MulongTest.sol @@ -17,7 +17,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {CloudStrike} from "../../src/mons/mulong/CloudStrike.sol"; import {KingsLeisure} from "../../src/mons/mulong/KingsLeisure.sol"; -import {KingsRespite} from "../../src/mons/mulong/KingsRespite.sol"; +import {GrandRespite} from "../../src/mons/mulong/GrandRespite.sol"; import {SummonStorm} from "../../src/mons/mulong/SummonStorm.sol"; import {defaultBattle, sideWord, targetBits} from "../abstract/SlotWire.sol"; @@ -40,7 +40,7 @@ contract MulongTest is Test { Storm storm; FrostbiteStatus frostbite; SummonStorm summonStorm; - KingsRespite kingsRespite; + GrandRespite grandRespite; CloudStrike cloudStrike; KingsLeisure kingsLeisure; IMoveSet filler; // BP 40 Fire physical @@ -53,7 +53,7 @@ contract MulongTest is Test { storm = new Storm(); frostbite = new FrostbiteStatus(); summonStorm = new SummonStorm(storm); - kingsRespite = new KingsRespite(typeCalc); + grandRespite = new GrandRespite(typeCalc); cloudStrike = new CloudStrike(storm, frostbite); kingsLeisure = new KingsLeisure(); filler = new CustomAttack( @@ -97,11 +97,15 @@ contract MulongTest is Test { } function _start(Mon[] memory aTeam, Mon[] memory bTeam) internal { + _startWithMode(aTeam, bTeam, BATTLE_MODE_DOUBLES); + } + + function _startWithMode(Mon[] memory aTeam, Mon[] memory bTeam, uint8 battleMode) internal { registry.setTeam(ALICE, aTeam); registry.setTeam(BOB, bTeam); Battle memory battle = defaultBattle(ALICE, BOB, registry, address(this), IMatchmaker(address(this))); (battleKey,) = engine.computeBattleKey(ALICE, BOB); - engine.startBattleWithMode(battle, BATTLE_MODE_DOUBLES); + engine.startBattleWithMode(battle, battleMode); vm.warp(vm.getBlockTimestamp() + 1); } @@ -281,13 +285,41 @@ contract MulongTest is Test { assertTrue(debuffedTick > undebuffedTick, "halved SpATK weakens the storm"); } + /// @dev Singles has no slot-1 lanes, so the storm reaches exactly the two actives. Roster mon 0 is + /// the mon an unwritten activeMonExt would alias into those lanes: benched on side 0, active + /// on side 1, so one run covers both ways the alias leaks. + function test_storm_singles_hitsOnlyTheTwoActives() public { + Mon[] memory aTeam = new Mon[](3); + aTeam[0] = _mkMon(2000, 50, filler, address(0)); + aTeam[1] = _mkMon(2000, 100, IMoveSet(address(summonStorm)), address(0)); + aTeam[2] = _mkMon(2000, 40, filler, address(0)); + Mon[] memory bTeam = new Mon[](3); + bTeam[0] = _mkMon(2000, 10, filler, address(0)); + bTeam[1] = _mkMon(2000, 8, filler, address(0)); + bTeam[2] = _mkMon(2000, 5, filler, address(0)); + _startWithMode(aTeam, bTeam, BATTLE_MODE_SINGLES); + + // Leads: A sends in mon 1, B keeps mon 0 on the field. Then A summons. + engine.executeWithMoves(battleKey, SWITCH_MOVE_INDEX, 0, 1, SWITCH_MOVE_INDEX, 0, 0); + engine.executeWithMoves(battleKey, 0, 0, 0, NO_OP_MOVE_INDEX, 0, 0); + + assertTrue(_stormIsUp(), "storm summoned"); + assertEq(_hp(0, 0), 0, "benched mon 0 is outside the storm"); + assertTrue(_hp(0, 1) < 0, "caster takes its own storm"); + assertTrue(_hp(1, 0) < 0, "opposing active hit"); + + // Both are single ticks on identical mons, so only DEFAULT_VOL's +/-10% separates them — + // a second tick from an aliased lane cannot hide inside this bound. + assertLt(-_hp(1, 0), (-_hp(0, 1) * 3) / 2, "the active is hit once, not once per lane"); + } + // --------------------------------------------------------------------- - // King's Respite + // Grand Respite // --------------------------------------------------------------------- /// @dev Arms silently, then detonates on the next rest — and the rest still regenerates stamina. - function test_kingsRespite_firesOnNextRest() public { - _field(IMoveSet(address(kingsRespite)), address(0)); + function test_grandRespite_firesOnNextRest() public { + _field(IMoveSet(address(grandRespite)), address(0)); engine.executeWithSlotMoves(battleKey, _side(0, targetBits(B0), NO_OP_MOVE_INDEX, 0), _rest()); assertEq(_hp(1, 0), 0, "arming does no damage"); @@ -297,15 +329,15 @@ contract MulongTest is Test { assertTrue(_hp(1, 0) < 0, "the rest detonated the strike"); assertEq(_stamina(0, 0), -2, "the strike rides the rest for free"); - (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(kingsRespite)); + (bool exists,,) = engine.getEffectData(battleKey, 0, 0, address(grandRespite)); assertFalse(exists, "one-shot"); } /// @dev Slot-bound: a replacement occupying the marked slot eats the strike. B0 outspeeds Mulong /// so its switch resolves first — rest and switch share SWITCH_PRIORITY, so speed decides. - function test_kingsRespite_followsTheLatchedSlot() public { + function test_grandRespite_followsTheLatchedSlot() public { Mon[] memory aTeam = new Mon[](3); - aTeam[0] = _mkMon(2000, 100, IMoveSet(address(kingsRespite)), address(0)); + aTeam[0] = _mkMon(2000, 100, IMoveSet(address(grandRespite)), address(0)); aTeam[1] = _mkMon(2000, 50, filler, address(0)); aTeam[2] = _mkMon(2000, 40, filler, address(0)); Mon[] memory bTeam = new Mon[](3); diff --git a/transpiler/codegen/contract.py b/transpiler/codegen/contract.py index 2287888e..88bcea03 100644 --- a/transpiler/codegen/contract.py +++ b/transpiler/codegen/contract.py @@ -529,6 +529,10 @@ def generate_state_variable(self, var: StateVariableDeclaration) -> str: if var.initial_value else self._type_converter.default_value(ts_type, var.type_name) ) + if var.initial_value: + default_val = self._type_converter.convert_bytes_string_literal( + var.type_name, var.initial_value, default_val + ) return f'{self.indent()}{modifier}{var.name}: {ts_type} = {default_val};' def _generate_mapping_variable( diff --git a/transpiler/codegen/type_converter.py b/transpiler/codegen/type_converter.py index 1de07410..131eb989 100644 --- a/transpiler/codegen/type_converter.py +++ b/transpiler/codegen/type_converter.py @@ -675,7 +675,7 @@ def convert_bytes_string_literal( initial_value: Expression, init_expr: str, ) -> str: - """Convert string literals assigned to bytesN into right-padded hex.""" + """Convert string literals assigned to bytesN into right-padded hex, as Solidity does.""" if not ( type_name and getattr(type_name, 'name', '') @@ -686,11 +686,15 @@ def convert_bytes_string_literal( return init_expr string_val = initial_value.value.strip('"\'') - hex_bytes = string_val.encode('utf-8').hex() + encoded = string_val.encode('utf-8') size_str = type_name.name[5:] byte_size = int(size_str) if size_str.isdigit() else 32 - hex_bytes = hex_bytes[:byte_size * 2].ljust(byte_size * 2, '0') - return f'"0x{hex_bytes}"' + # Truncating here would silently ship a different value than the chain computes. + if len(encoded) > byte_size: + raise ValueError( + f'string literal "{string_val}" is {len(encoded)} bytes, too wide for {type_name.name}' + ) + return f'"0x{encoded.hex().ljust(byte_size * 2, "0")}"' # ========================================================================= # INDEX CONVERSION From 13b1825f14d8ad63338839989334613e64db545f Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:18:23 -0700 Subject: [PATCH 40/41] wip --- script/EngineAndPeriphery.s.sol | 59 +++++- src/Constants.sol | 12 ++ src/Engine.sol | 51 +++-- src/Structs.sol | 2 +- src/effects/battlefield/ElementalField.sol | 89 ++++++++ src/effects/battlefield/FluxField.sol | 84 ++++++++ src/effects/battlefield/UnstableField.sol | 67 ++++++ .../battlefield/ElementalFieldTest.sol | 148 +++++++++++++ test/effects/battlefield/FieldHarness.sol | 198 ++++++++++++++++++ test/effects/battlefield/FluxFieldTest.sol | 128 +++++++++++ .../battlefield/InlineRegenFlagsTest.sol | 97 +++++++++ .../effects/battlefield/UnstableFieldTest.sol | 120 +++++++++++ test/mons/XmonTest.sol | 2 +- 13 files changed, 1036 insertions(+), 21 deletions(-) create mode 100644 src/effects/battlefield/ElementalField.sol create mode 100644 src/effects/battlefield/FluxField.sol create mode 100644 src/effects/battlefield/UnstableField.sol create mode 100644 test/effects/battlefield/ElementalFieldTest.sol create mode 100644 test/effects/battlefield/FieldHarness.sol create mode 100644 test/effects/battlefield/FluxFieldTest.sol create mode 100644 test/effects/battlefield/InlineRegenFlagsTest.sol create mode 100644 test/effects/battlefield/UnstableFieldTest.sol diff --git a/script/EngineAndPeriphery.s.sol b/script/EngineAndPeriphery.s.sol index 39342e66..7aa4f505 100644 --- a/script/EngineAndPeriphery.s.sol +++ b/script/EngineAndPeriphery.s.sol @@ -15,8 +15,13 @@ import {IGachaRNG} from "../src/rng/IGachaRNG.sol"; import {TypeCalculator} from "../src/types/TypeCalculator.sol"; // Shared effects +import {DefaultRuleset} from "../src/DefaultRuleset.sol"; +import {IEffect} from "../src/effects/IEffect.sol"; +import {ElementalField} from "../src/effects/battlefield/ElementalField.sol"; +import {FluxField} from "../src/effects/battlefield/FluxField.sol"; import {Overclock} from "../src/effects/battlefield/Overclock.sol"; import {Storm} from "../src/effects/battlefield/Storm.sol"; +import {UnstableField} from "../src/effects/battlefield/UnstableField.sol"; import {BlessedStatus} from "../src/effects/status/BlessedStatus.sol"; import {BurnStatus} from "../src/effects/status/BurnStatus.sol"; import {FrostbiteStatus} from "../src/effects/status/FrostbiteStatus.sol"; @@ -79,13 +84,13 @@ contract EngineAndPeriphery is Script { SimplePM simplePM = new SimplePM(engine); deployedContracts.push(DeployData({name: "SIMPLE PM", contractAddress: address(simplePM)})); - deployGameFundamentals(); + deployGameFundamentals(engine); vm.stopBroadcast(); return deployedContracts; } - function deployGameFundamentals() public { + function deployGameFundamentals(Engine engine) public { Overclock overclock = new Overclock(); deployedContracts.push(DeployData({name: "OVERCLOCK", contractAddress: address(overclock)})); @@ -109,5 +114,55 @@ contract EngineAndPeriphery is Script { BlessedStatus blessedStatus = new BlessedStatus(); deployedContracts.push(DeployData({name: "BLESSED STATUS", contractAddress: address(blessedStatus)})); + + deployBattlefieldFields(engine, sleepStatus, panicStatus, frostbiteStatus, burnStatus, zapStatus); + } + + /// @dev Optional battlefield fields, each paired with the ruleset a client selects it by. The + /// marker entry in each ruleset is what keeps the Engine's inline stamina regen: full for + /// Elemental / Unstable, resting-only for Flux (which owns round-end stamina itself). + function deployBattlefieldFields( + Engine engine, + SleepStatus sleepStatus, + PanicStatus panicStatus, + FrostbiteStatus frostbiteStatus, + BurnStatus burnStatus, + ZapStatus zapStatus + ) internal { + ElementalField elementalField = + new ElementalField(burnStatus, frostbiteStatus, sleepStatus, panicStatus, zapStatus); + deployedContracts.push(DeployData({name: "ELEMENTAL FIELD", contractAddress: address(elementalField)})); + + UnstableField unstableField = new UnstableField(); + deployedContracts.push(DeployData({name: "UNSTABLE FIELD", contractAddress: address(unstableField)})); + + FluxField fluxField = new FluxField(); + deployedContracts.push(DeployData({name: "FLUX FIELD", contractAddress: address(fluxField)})); + + deployedContracts.push( + DeployData({ + name: "ELEMENTAL FIELD RULESET", + contractAddress: address(_fieldRuleset(engine, elementalField, INLINE_STAMINA_REGEN_RULESET)) + }) + ); + deployedContracts.push( + DeployData({ + name: "UNSTABLE FIELD RULESET", + contractAddress: address(_fieldRuleset(engine, unstableField, INLINE_STAMINA_REGEN_RULESET)) + }) + ); + deployedContracts.push( + DeployData({ + name: "FLUX FIELD RULESET", + contractAddress: address(_fieldRuleset(engine, fluxField, INLINE_REST_REGEN_MARKER)) + }) + ); + } + + function _fieldRuleset(Engine engine, IEffect field, address regenMarker) internal returns (DefaultRuleset) { + IEffect[] memory effects = new IEffect[](2); + effects[0] = IEffect(regenMarker); + effects[1] = field; + return new DefaultRuleset(engine, effects); } } diff --git a/src/Constants.sol b/src/Constants.sol index f419117e..4a00b5ad 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -49,8 +49,20 @@ address constant TOMBSTONE_ADDRESS = address(0xdead); // Sentinel ruleset address: when passed as battle.ruleset, the Engine adds // inline StaminaRegen as a global effect without calling an external contract. +// Also valid as a marker ENTRY in a ruleset's effect array (see INLINE_REST_REGEN_MARKER), +// which is how a ruleset carrying real global effects keeps the inline regen path. address constant INLINE_STAMINA_REGEN_RULESET = address(0x57A); // "STA"mina +// Marker entry for a ruleset that wants only the resting (NO_OP AfterMove) half of inline regen — +// for global effects that own round-end stamina themselves. Marker entries are folded into +// BattleConfig.inlineRegenFlags at startBattle and are never stored or called. +address constant INLINE_REST_REGEN_MARKER = address(0x57B); + +// BattleConfig.inlineRegenFlags bits: which halves of the Engine's inline stamina regen run. +uint8 constant INLINE_REGEN_REST = 0x1; // AfterMove, when the mon rested (NO_OP) +uint8 constant INLINE_REGEN_ROUND_END = 0x2; // RoundEnd, on full two-player turns +uint8 constant INLINE_REGEN_ALL = INLINE_REGEN_REST | INLINE_REGEN_ROUND_END; + // Sentinel moveManager address: when a battle's moveManager is set to this, the battle uses the // Engine's built-in dual-signed buffer flow (submitTurnMoves / executeBuffered) instead of an // external ICommitManager contract. The built-in submit/drain entrypoints require this sentinel. diff --git a/src/Engine.sol b/src/Engine.sol index f0b354e0..663b2a4a 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -360,28 +360,45 @@ contract Engine is IEngine, MappingAllocator, EIP712 { _storeTeam(config.p1Team, p1Team, p1Len); // Set the global effects and data to start the game if any. - // NOTE: hasInlineStaminaRegen AND globalEffectsLength must be (re)written on EVERY branch — + // NOTE: inlineRegenFlags AND globalEffectsLength must be (re)written on EVERY branch — // config storage is recycled across battles, so a stale value from the previous occupant // would otherwise leak into this battle (e.g. inline regen running on top of an external // ruleset, or a previous battle's global effect staying live when the new ruleset is empty). - bool hasInlineRegen; + // A ruleset opts into either half of inline regen by listing a marker address in its effects + // array; markers are folded into the flags here and never stored or called. + uint8 newRegenFlags; uint8 newGlobalEffectsLength; if (address(battle.ruleset) == INLINE_STAMINA_REGEN_RULESET) { - hasInlineRegen = true; + newRegenFlags = INLINE_REGEN_ALL; } else if (address(battle.ruleset) != address(0)) { (IEffect[] memory effects, bytes32[] memory data) = battle.ruleset.getInitialGlobalEffects(); uint256 numEffects = effects.length; + uint256 stored; for (uint256 i = 0; i < numEffects;) { IEffect effect = effects[i]; - uint32 metadata = effect.getStepsBitmap(); - _writeEffectInstance( - config.globalEffects[i], effect, uint16(metadata), data[i], uint16(metadata >> EFFECT_CONTEXT_SHIFT) - ); + address effectAddr = address(effect); + if (effectAddr == INLINE_STAMINA_REGEN_RULESET) { + newRegenFlags |= INLINE_REGEN_ALL; + } else if (effectAddr == INLINE_REST_REGEN_MARKER) { + newRegenFlags |= INLINE_REGEN_REST; + } else { + uint32 metadata = effect.getStepsBitmap(); + _writeEffectInstance( + config.globalEffects[stored], + effect, + uint16(metadata), + data[i], + uint16(metadata >> EFFECT_CONTEXT_SHIFT) + ); + unchecked { + ++stored; + } + } unchecked { ++i; } } - newGlobalEffectsLength = uint8(numEffects); + newGlobalEffectsLength = uint8(stored); } // Set the engine hooks to start the game if any, folding their bitmaps into the union @@ -413,7 +430,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { config.globalKVCount = 0; config.p0BoostCounts = 0; config.teamSizes = newTeamSizes; - config.hasInlineStaminaRegen = hasInlineRegen; + config.inlineRegenFlags = newRegenFlags; config.globalEffectsLength = newGlobalEffectsLength; config.engineHooksLength = uint8(numHooks); config.startTimestamp = uint40(block.timestamp); @@ -1283,8 +1300,8 @@ contract Engine is IEngine, MappingAllocator, EIP712 { } tempRNG = rng; - // Cache `hasInlineStaminaRegen` once instead of re-reading config slot 2 three times below. - bool inlineStaminaRegen = config.hasInlineStaminaRegen; + // Cache `inlineRegenFlags` once instead of re-reading config slot 2 three times below. + uint8 inlineRegenFlags = config.inlineRegenFlags; // Calculate the priority and non-priority player indices. Use the internal helper // with already-resolved config/battle/moves to skip redundant storage re-resolution. @@ -1373,7 +1390,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { // Stamina regen decision is encapsulated in _inlineStaminaRegen, which reads the move FRESH // — required because effects (SleepStatus) can rewrite the move to a resting NO_OP mid-turn. - if (inlineStaminaRegen) { + if (inlineRegenFlags & INLINE_REGEN_REST != 0) { playerSwitchForTurnFlag = _inlineStaminaRegen( config, EffectStep.AfterMove, @@ -1441,7 +1458,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ); } - if (inlineStaminaRegen) { + if (inlineRegenFlags & INLINE_REGEN_REST != 0) { playerSwitchForTurnFlag = _inlineStaminaRegen( config, EffectStep.AfterMove, @@ -1498,7 +1515,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { ); } - if (inlineStaminaRegen) { + if (inlineRegenFlags & INLINE_REGEN_ROUND_END != 0) { uint256 p0Mon = _unpackActiveMonIndex(battle.activeMonIndex, 0); uint256 p1Mon = _unpackActiveMonIndex(battle.activeMonIndex, 1); playerSwitchForTurnFlag = @@ -4417,7 +4434,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { bool turnZero = battle.turnId == 0; uint256 lockedPriorities = _lockSlotPriorities(battleKey, config, battle); - bool inlineRegen = config.hasInlineStaminaRegen; + uint8 inlineRegenFlags = config.inlineRegenFlags; // RoundStart: global pass, then per-slot lists in current speed order (D29). uint256 gLen = config.globalEffectsLength; @@ -4480,7 +4497,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { if (battle.winnerIndex != 2) { return 2; } - if (inlineRegen && actorAlive) { + if (inlineRegenFlags & INLINE_REGEN_REST != 0 && actorAlive) { if (StaminaRegenLogic._isRestingMove(uint8(_currentTurnMoveWordForSlot(side, slot & 1)))) { // Re-read the lane: an AfterMove effect may have swapped the rester out, and // the regen follows the lane (matching the singles regen's fresh read). @@ -4525,7 +4542,7 @@ contract Engine is IEngine, MappingAllocator, EIP712 { return 2; } } - if (inlineRegen) { + if (inlineRegenFlags & INLINE_REGEN_ROUND_END != 0) { for (uint256 s; s < 4;) { uint256 mon = _slotActive(battle, s); if (mon != EMPTY_ACTIVE_LANE && !_getMonState(config, s >> 1, mon).isKnockedOut) { diff --git a/src/Structs.sol b/src/Structs.sol index d214bae0..ed306513 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -171,7 +171,7 @@ struct BattleConfig { uint8 engineHooksLength; // 8 uint16 koBitmaps; // 16 — Packed: lower 8 bits = p0 KO bitmap, upper 8 bits = p1 KO bitmap uint40 startTimestamp; // 40 — battle start time; overflows in year ~36825 (shrunk from uint48 for slot-2 packing) - bool hasInlineStaminaRegen; // 8 + uint8 inlineRegenFlags; // 8 — INLINE_REGEN_* bits (rest / round-end halves) uint8 globalKVCount; // 8 — live entry count in the current battle's globalKV key buffer // Per-mon stat-boost source counts, 4 bits per mon (max 15 sources/mon — distinct callers). // MUST reset every startBattle (recycled keys); rides the slot-2 consolidated write. diff --git a/src/effects/battlefield/ElementalField.sol b/src/effects/battlefield/ElementalField.sol new file mode 100644 index 00000000..1ed75c2a --- /dev/null +++ b/src/effects/battlefield/ElementalField.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {ALWAYS_APPLIES_BIT, EMPTY_ACTIVE_LANE} from "../../Constants.sol"; +import {IEngine} from "../../IEngine.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; +import {BasicEffect} from "../BasicEffect.sol"; +import {IEffect} from "../IEffect.sol"; + +/// @notice Battlefield field: from turn 1 on, each status-free active picks up a random status at +/// the end of every full turn. Forced-switch turns run no effect hooks at all, so they are +/// skipped by construction. +contract ElementalField is BasicEffect { + uint256 private constant NUM_STATUSES = 5; + + IEffect private immutable BURN; + IEffect private immutable FROSTBITE; + IEffect private immutable SLEEP; + IEffect private immutable PANIC; + IEffect private immutable ZAP; + + constructor(IEffect burn, IEffect frostbite, IEffect sleep, IEffect panic, IEffect zap) { + BURN = burn; + FROSTBITE = frostbite; + SLEEP = sleep; + PANIC = panic; + ZAP = zap; + } + + // Steps: RoundEnd + fresh RoundEnd context (status lanes) + ALWAYS_APPLIES. + function getStepsBitmap() external pure override returns (uint32) { + return 0x00040004 | uint32(ALWAYS_APPLIES_BIT); + } + + function _statusAt(uint256 index) internal view returns (IEffect) { + if (index == 0) { + return BURN; + } else if (index == 1) { + return FROSTBITE; + } else if (index == 2) { + return SLEEP; + } else if (index == 3) { + return PANIC; + } + return ZAP; + } + + function onRoundEnd( + IEngine engine, + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256, + uint256, + uint256 hookContext + ) external override returns (bytes32, bool) { + // Turn-0 latch, kept in this effect's own extraData (which the Engine persists) so no turn + // id read is needed: the leads only just arrived, so the first round end arms the field + // instead of statusing them. + if (extraData == bytes32(0)) { + return (bytes32(uint256(1)), false); + } + + // KO state is not part of the RoundEnd context, and a KO'd active is about to be replaced. + uint256 p0KOs = engine.getKOBitmap(battleKey, 0); + uint256 p1KOs = engine.getKOBitmap(battleKey, 1); + + for (uint256 slot; slot < 4; ++slot) { + uint256 monIndex = TargetLib.activeAt(hookContext, slot); + if (monIndex == EMPTY_ACTIVE_LANE) { + continue; + } + uint256 side = TargetLib.sideOf(slot); + if ((((side == 0 ? p0KOs : p1KOs) >> monIndex) & 1) != 0) { + continue; + } + // Statuses are exclusive: skipping an already-statused mon here is what keeps a repeat + // roll from escalating an existing status via the Engine's re-apply route. + if (TargetLib.hookStatusClass(hookContext, side, monIndex) != 0) { + continue; + } + IEffect status = _statusAt(uint256(keccak256(abi.encode(rng, slot))) % NUM_STATUSES); + // Statuses build their own extraData in onApply. A status with an extra apply condition + // (Sleep's one-sleeper-per-side rule) can reject the roll — the mon then gets nothing. + engine.addEffect(side, monIndex, status, bytes32(0)); + } + return (extraData, false); + } +} diff --git a/src/effects/battlefield/FluxField.sol b/src/effects/battlefield/FluxField.sol new file mode 100644 index 00000000..257a26bf --- /dev/null +++ b/src/effects/battlefield/FluxField.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {ALWAYS_APPLIES_BIT, EMPTY_ACTIVE_LANE} from "../../Constants.sol"; +import {MonStateIndexName} from "../../Enums.sol"; +import {IEngine} from "../../IEngine.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; +import {BasicEffect} from "../BasicEffect.sol"; + +/// @notice Battlefield field: replaces round-end stamina regen with a per-mon roll in [-2, +2]. +/// Ships with INLINE_REST_REGEN_MARKER, so the Engine still grants the resting (NO_OP) +/// regen and this effect owns round end alone. +contract FluxField is BasicEffect { + // Cumulative weights out of 100 over [-2, -1, 0, +1, +2] = [15, 20, 20, 25, 20]. + uint256 private constant CUM_MINUS_TWO = 15; + uint256 private constant CUM_MINUS_ONE = 35; + uint256 private constant CUM_ZERO = 55; + uint256 private constant CUM_PLUS_ONE = 80; + + // Steps: RoundEnd + ALWAYS_APPLIES. + function getStepsBitmap() external pure override returns (uint32) { + return 0x0004 | uint32(ALWAYS_APPLIES_BIT); + } + + function roll(uint256 rng, uint256 slot) public pure returns (int32) { + uint256 h = uint256(keccak256(abi.encode(rng, slot))) % 100; + if (h < CUM_MINUS_TWO) { + return -2; + } else if (h < CUM_MINUS_ONE) { + return -1; + } else if (h < CUM_ZERO) { + return 0; + } else if (h < CUM_PLUS_ONE) { + return 1; + } + return 2; + } + + function onRoundEnd( + IEngine engine, + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256, + uint256, + uint256 activesPacked + ) external override returns (bytes32, bool) { + uint256 p0KOs = engine.getKOBitmap(battleKey, 0); + uint256 p1KOs = engine.getKOBitmap(battleKey, 1); + + for (uint256 slot; slot < 4; ++slot) { + uint256 monIndex = TargetLib.activeAt(activesPacked, slot); + if (monIndex == EMPTY_ACTIVE_LANE) { + continue; + } + uint256 side = TargetLib.sideOf(slot); + if ((((side == 0 ? p0KOs : p1KOs) >> monIndex) & 1) != 0) { + continue; + } + int32 amount = roll(rng, slot); + if (amount == 0) { + continue; + } + _applyFlux(engine, battleKey, side, monIndex, amount); + } + return (extraData, false); + } + + /// @dev Clamped to [0, base stamina]: the ceiling matches vanilla regen (which never pushes the + /// delta above zero), the floor matches Panic's drain. + function _applyFlux(IEngine engine, bytes32 battleKey, uint256 side, uint256 monIndex, int32 amount) internal { + int32 base = int32(engine.getMonValueForBattle(battleKey, side, monIndex, MonStateIndexName.Stamina)); + int32 delta = engine.getMonStateForBattle(battleKey, side, monIndex, MonStateIndexName.Stamina); + int32 newDelta = delta + amount; + if (newDelta > 0) { + newDelta = 0; + } else if (newDelta < -base) { + newDelta = -base; + } + if (newDelta != delta) { + engine.updateMonState(side, monIndex, MonStateIndexName.Stamina, newDelta - delta); + } + } +} diff --git a/src/effects/battlefield/UnstableField.sol b/src/effects/battlefield/UnstableField.sol new file mode 100644 index 00000000..298239bd --- /dev/null +++ b/src/effects/battlefield/UnstableField.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {ALWAYS_APPLIES_BIT, EMPTY_ACTIVE_LANE} from "../../Constants.sol"; +import {IEngine} from "../../IEngine.sol"; +import {SwitchTargetLib} from "../../lib/SwitchTargetLib.sol"; +import {TargetLib} from "../../lib/TargetLib.sol"; +import {BasicEffect} from "../BasicEffect.sol"; + +/// @notice Battlefield field: at the end of every full turn, every live active is dragged out for a +/// random benched mon. Forced-switch turns run no effect hooks at all, so they are skipped +/// by construction. +contract UnstableField is BasicEffect { + // Steps: RoundEnd + ALWAYS_APPLIES. + function getStepsBitmap() external pure override returns (uint32) { + return 0x0004 | uint32(ALWAYS_APPLIES_BIT); + } + + function onRoundEnd( + IEngine engine, + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256, + uint256, + uint256 activesPacked + ) external override returns (bytes32, bool) { + // Turn-0 latch, kept in this effect's own extraData (which the Engine persists) so no turn + // id read is needed: switch-in abilities do not fire on turn 0, so the first round end only + // arms the field instead of dragging. + if (extraData == bytes32(0)) { + return (bytes32(uint256(1)), false); + } + + uint256 p0KOs = engine.getKOBitmap(battleKey, 0); + uint256 p1KOs = engine.getKOBitmap(battleKey, 1); + + for (uint256 slot; slot < 4; ++slot) { + uint256 monIndex = TargetLib.activeAt(activesPacked, slot); + if (monIndex == EMPTY_ACTIVE_LANE) { + continue; + } + uint256 side = TargetLib.sideOf(slot); + // A KO'd active already gets a forced-switch turn, where its own player picks. + if ((((side == 0 ? p0KOs : p1KOs) >> monIndex) & 1) != 0) { + continue; + } + int32 target = SwitchTargetLib.findRandomNonKOed( + engine, + battleKey, + side, + slot & 1, + monIndex, + TargetLib.activeAt(activesPacked, slot ^ 1), + uint256(keccak256(abi.encode(rng, slot))) + ); + if (target != -1) { + uint256 newMonIndex = uint256(uint32(target)); + engine.switchActiveMonForSlot(side, slot & 1, newMonIndex); + // Track the drag locally: the ally lane read above must see this side's new active, + // not the mon that just left. + activesPacked = TargetLib.withLane(activesPacked, slot, newMonIndex); + } + } + return (extraData, false); + } +} diff --git a/test/effects/battlefield/ElementalFieldTest.sol b/test/effects/battlefield/ElementalFieldTest.sol new file mode 100644 index 00000000..37e6c3b5 --- /dev/null +++ b/test/effects/battlefield/ElementalFieldTest.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../../../src/Constants.sol"; +import "../../../src/Enums.sol"; +import "../../../src/Structs.sol"; + +import {IRuleset} from "../../../src/IRuleset.sol"; +import {IEffect} from "../../../src/effects/IEffect.sol"; +import {ElementalField} from "../../../src/effects/battlefield/ElementalField.sol"; +import {BurnStatus} from "../../../src/effects/status/BurnStatus.sol"; +import {FrostbiteStatus} from "../../../src/effects/status/FrostbiteStatus.sol"; +import {PanicStatus} from "../../../src/effects/status/PanicStatus.sol"; +import {SleepStatus} from "../../../src/effects/status/SleepStatus.sol"; +import {ZapStatus} from "../../../src/effects/status/ZapStatus.sol"; + +import {FieldHarness} from "./FieldHarness.sol"; + +/// @notice ElementalField: every status-free live active picks up a random status at round end. +contract ElementalFieldTest is FieldHarness { + // Status class ids, as folded into each status's getStepsBitmap(). + uint256 constant BURN = 1; + uint256 constant FROSTBITE = 2; + uint256 constant SLEEP = 3; + uint256 constant PANIC = 4; + + // Salts precomputed offline against the engine's rng stream (keccak(p0Salt, p1Salt) then + // keccak(rng, absSlot) % 5). Side 1 always salts 0. + uint104 constant SALT_FROSTBITE_PANIC = 10; // singles: slot A0 -> Frostbite, slot B0 -> Panic + uint104 constant SALT_B0_BURN = 5; // singles: slot B0 -> Burn (slot A0 -> Frostbite) + uint104 constant SALT_ALL_SLEEP = 26; // doubles: every slot -> Sleep + + ElementalField field; + BurnStatus burnStatus; + FrostbiteStatus frostbiteStatus; + SleepStatus sleepStatus; + PanicStatus panicStatus; + ZapStatus zapStatus; + IRuleset ruleset; + + function setUp() public { + _setUpHarness(); + burnStatus = new BurnStatus(); + frostbiteStatus = new FrostbiteStatus(); + sleepStatus = new SleepStatus(); + panicStatus = new PanicStatus(); + zapStatus = new ZapStatus(); + field = new ElementalField(burnStatus, frostbiteStatus, sleepStatus, panicStatus, zapStatus); + ruleset = _ruleset(IEffect(address(field)), INLINE_STAMINA_REGEN_RULESET); + } + + function _startTwoMonSingles(IRuleset rules) internal { + _startSingles(_mkTeam(2, 1000, 5, 10, weakAttack), _mkTeam(2, 1000, 5, 5, weakAttack), rules); + } + + /// @dev A full turn where both sides rest, carrying the salt that decides the round-end rolls. + function _rollTurn(uint104 salt) internal { + _singlesTurn(NO_OP_MOVE_INDEX, 0, salt, NO_OP_MOVE_INDEX, 0, 0); + } + + /// @dev Burn's extraData packs the degree in its low byte. + function _burnDegree(uint256 side, uint256 monIndex) internal view returns (uint256) { + (bool exists,, bytes32 data) = engine.getEffectData(battleKey, side, monIndex, address(burnStatus)); + assertTrue(exists, "expected a burn entry"); + return uint256(data) & 0xFF; + } + + function test_turnZeroArmsTheFieldWithoutStatusing() public { + _startTwoMonSingles(ruleset); + _singlesLeads(SALT_FROSTBITE_PANIC); + + assertEq(_statusClass(0, 0), 0, "the leads arrive clean"); + assertEq(_statusClass(1, 0), 0, "the leads arrive clean"); + } + + function test_bothActivesGetTheirRolledStatus() public { + _startTwoMonSingles(ruleset); + _singlesLeads(0); + _rollTurn(SALT_FROSTBITE_PANIC); + + assertEq(_statusClass(0, 0), FROSTBITE, "A0 rolled Frostbite"); + assertEq(_statusClass(1, 0), PANIC, "B0 rolled Panic"); + } + + function test_alreadyStatusedMonIsSkippedAndNotEscalated() public { + _startTwoMonSingles(ruleset); + _singlesLeads(0); + _rollTurn(SALT_B0_BURN); + assertEq(_statusClass(1, 0), BURN, "B0 rolled Burn"); + assertEq(_burnDegree(1, 0), 1, "a fresh burn is degree 1"); + + // The same salt re-rolls Burn for the same slot: skipping the already-statused mon is what + // keeps it off the Engine's re-apply route, which would otherwise escalate the degree. + _rollTurn(SALT_B0_BURN); + assertEq(_burnDegree(1, 0), 1, "a re-rolled Burn never reaches the escalation path"); + } + + function test_blockedRollFizzlesInsteadOfRerolling() public { + // Every slot rolls Sleep, but only one mon per side may sleep at a time. + Mon[] memory team = _mkTeam(2, 1000, 5, 10, weakAttack); + _startDoubles(team, team, ruleset); + _slotLeads(1, 0); + _restTurn(SALT_ALL_SLEEP); + + assertEq(_statusClass(0, 0), SLEEP, "side 0 slot 0 sleeps"); + assertEq(_statusClass(0, 1), 0, "side 0 slot 1's roll is blocked and fizzles"); + assertEq(_statusClass(1, 0), SLEEP, "side 1 slot 0 sleeps"); + assertEq(_statusClass(1, 1), 0, "side 1 slot 1's roll is blocked and fizzles"); + } + + function test_koedActiveIsSkipped() public { + Mon[] memory aTeam = _mkTeam(2, 1000, 5, 10, killAttack); + Mon[] memory bTeam = _mkTeam(2, 1, 5, 5, weakAttack); + _startSingles(aTeam, bTeam, ruleset); + _singlesLeads(0); + _rollTurn(SALT_FROSTBITE_PANIC); + assertEq(_statusClass(1, 0), PANIC, "the first roll turn statused B0"); + + // Alice KOs Bob's active; the round-end roll must leave the corpse alone. + _singlesTurn(0, 0, SALT_B0_BURN, NO_OP_MOVE_INDEX, 0, 0); + assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), 1, "B0 is KO'd"); + assertEq(_statusClass(1, 0), PANIC, "KO'd active keeps its old status and gets no new roll"); + } + + function test_forcedSwitchTurnAppliesNothing() public { + Mon[] memory aTeam = _mkTeam(2, 1000, 5, 10, killAttack); + Mon[] memory bTeam = _mkTeam(2, 1, 5, 5, weakAttack); + _startSingles(aTeam, bTeam, ruleset); + _singlesLeads(0); + _singlesTurn(0, 0, SALT_B0_BURN, NO_OP_MOVE_INDEX, 0, 0); + + // Bob's replacement arrives on a forced-switch turn, which runs no effects at all. + engine.executeWithSingleMove(battleKey, SWITCH_MOVE_INDEX, 0, 1); + assertEq(_active(B0), 1, "Bob switched in mon 1"); + assertEq(_statusClass(1, 1), 0, "no status is applied on a forced-switch turn"); + } + + function test_multiStatusesEverySeatsActive() public { + _startMulti(ruleset); + _slotLeads(4, 0); + _restTurn(SALT_FROSTBITE_PANIC); + + assertTrue(_statusClass(0, 0) != 0, "side 0 slot 0 statused"); + assertTrue(_statusClass(0, 4) != 0, "side 0 slot 1 statused"); + assertTrue(_statusClass(1, 0) != 0, "side 1 slot 0 statused"); + assertTrue(_statusClass(1, 4) != 0, "side 1 slot 1 statused"); + } +} diff --git a/test/effects/battlefield/FieldHarness.sol b/test/effects/battlefield/FieldHarness.sol new file mode 100644 index 00000000..9a6b5034 --- /dev/null +++ b/test/effects/battlefield/FieldHarness.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "../../../src/Constants.sol"; +import "../../../src/Enums.sol"; +import "../../../src/Structs.sol"; + +import {DefaultRuleset} from "../../../src/DefaultRuleset.sol"; +import {Engine} from "../../../src/Engine.sol"; +import {IRuleset} from "../../../src/IRuleset.sol"; +import {IEffect} from "../../../src/effects/IEffect.sol"; +import {IMatchmaker} from "../../../src/matchmaker/IMatchmaker.sol"; +import {IMoveSet} from "../../../src/moves/IMoveSet.sol"; + +import {defaultBattle, sideWord, targetBits} from "../../abstract/SlotWire.sol"; +import {CustomAttack} from "../../mocks/CustomAttack.sol"; +import {TestTeamRegistry} from "../../mocks/TestTeamRegistry.sol"; +import {TestTypeCalculator} from "../../mocks/TestTypeCalculator.sol"; + +/// @dev Shared plumbing for the battlefield-field tests: this contract is both the matchmaker and +/// the move manager, so every turn's salts (and therefore the field rng) are chosen directly. +/// Mock mons only — these are engine-level tests. +abstract contract FieldHarness is Test { + address constant ALICE = address(0x1); + address constant BOB = address(0x2); + address constant CARL = address(0x3); + address constant DAVE = address(0x4); + + // Absolute slots + uint256 constant A0 = 0; + uint256 constant A1 = 1; + uint256 constant B0 = 2; + uint256 constant B1 = 3; + + Engine engine; + TestTeamRegistry registry; + TestTypeCalculator typeCalc; + IMoveSet weakAttack; // BP 10, 1 stamina + IMoveSet killAttack; // BP 1000, 1 stamina + IMoveSet costlyAttack; // BP 10, 2 stamina — separates the two inline-regen halves + bytes32 battleKey; + + function _setUpHarness() internal { + engine = new Engine(GAME_MONS_PER_TEAM, GAME_MOVES_PER_MON); + registry = new TestTeamRegistry(); + typeCalc = new TestTypeCalculator(); + weakAttack = new CustomAttack( + typeCalc, + CustomAttack.Args({ + TYPE: Type.Fire, BASE_POWER: 10, ACCURACY: 100, STAMINA_COST: 1, PRIORITY: DEFAULT_PRIORITY + }) + ); + killAttack = new CustomAttack( + typeCalc, + CustomAttack.Args({ + TYPE: Type.Fire, BASE_POWER: 1000, ACCURACY: 100, STAMINA_COST: 1, PRIORITY: DEFAULT_PRIORITY + }) + ); + + costlyAttack = new CustomAttack( + typeCalc, + CustomAttack.Args({ + TYPE: Type.Fire, BASE_POWER: 10, ACCURACY: 100, STAMINA_COST: 2, PRIORITY: DEFAULT_PRIORITY + }) + ); + + address[] memory toAdd = new address[](1); + toAdd[0] = address(this); + address[4] memory seats = [ALICE, BOB, CARL, DAVE]; + for (uint256 i; i < 4; ++i) { + vm.prank(seats[i]); + engine.updateMatchmakers(toAdd, new address[](0)); + } + } + + /// @dev A ruleset carrying `field` plus the inline-regen marker the field ships with. + function _ruleset(IEffect field, address regenMarker) internal returns (IRuleset) { + IEffect[] memory effects = new IEffect[](2); + effects[0] = IEffect(regenMarker); + effects[1] = field; + return new DefaultRuleset(engine, effects); + } + + function _mkMon(uint32 hp, uint32 stamina, uint32 speed, IMoveSet move0) internal pure returns (Mon memory mon) { + uint256[] memory moves = new uint256[](1); + moves[0] = uint256(uint160(address(move0))); + mon = Mon({ + stats: MonStats({ + hp: hp, + stamina: stamina, + speed: speed, + attack: 10, + defense: 10, + specialAttack: 10, + specialDefense: 10, + type1: Type.Air, + type2: Type.None + }), + ability: 0, + moves: moves + }); + } + + function _mkTeam(uint256 size, uint32 hp, uint32 stamina, uint32 speed, IMoveSet move0) + internal + pure + returns (Mon[] memory team) + { + team = new Mon[](size); + for (uint256 i; i < size; ++i) { + team[i] = _mkMon(hp, stamina, speed, move0); + } + } + + function _battle(address p0, address p1, IRuleset ruleset) internal view returns (Battle memory battle) { + battle = defaultBattle(p0, p1, registry, address(this), IMatchmaker(address(this))); + battle.ruleset = ruleset; + } + + function _startSingles(Mon[] memory aTeam, Mon[] memory bTeam, IRuleset ruleset) internal { + registry.setTeam(ALICE, aTeam); + registry.setTeam(BOB, bTeam); + (battleKey,) = engine.computeBattleKey(ALICE, BOB); + engine.startBattle(_battle(ALICE, BOB, ruleset)); + vm.warp(vm.getBlockTimestamp() + 1); + } + + function _startDoubles(Mon[] memory aTeam, Mon[] memory bTeam, IRuleset ruleset) internal { + registry.setTeam(ALICE, aTeam); + registry.setTeam(BOB, bTeam); + (battleKey,) = engine.computeBattleKey(ALICE, BOB); + engine.startBattleWithMode(_battle(ALICE, BOB, ruleset), BATTLE_MODE_DOUBLES); + vm.warp(vm.getBlockTimestamp() + 1); + } + + /// @dev Multi seats each bring their own 4-mon team; side 0 = ALICE + CARL, side 1 = BOB + DAVE. + function _startMulti(IRuleset ruleset) internal { + registry.setTeam(ALICE, _mkTeam(4, 1000, 5, 40, weakAttack)); + registry.setTeam(CARL, _mkTeam(4, 1000, 5, 30, weakAttack)); + registry.setTeam(BOB, _mkTeam(4, 1000, 5, 20, weakAttack)); + registry.setTeam(DAVE, _mkTeam(4, 1000, 5, 10, weakAttack)); + Battle memory battle = _battle(ALICE, BOB, ruleset); + battle.p2 = CARL; + battle.p3 = DAVE; + (battleKey,) = engine.computePartyKey(ALICE, BOB, CARL, DAVE); + engine.startBattleWithMode(battle, BATTLE_MODE_MULTI); + vm.warp(vm.getBlockTimestamp() + 1); + } + + // --- turn drivers ----------------------------------------------------- + + function _singlesTurn(uint8 aMove, uint16 aExtra, uint104 aSalt, uint8 bMove, uint16 bExtra, uint104 bSalt) + internal + { + engine.executeWithMoves(battleKey, aMove, aSalt, aExtra, bMove, bSalt, bExtra); + } + + /// @dev Singles turn 0: both sides send in roster mon 0. + function _singlesLeads(uint104 salt) internal { + _singlesTurn(SWITCH_MOVE_INDEX, 0, salt, SWITCH_MOVE_INDEX, 0, 0); + } + + function _slotTurn(uint256 side0, uint256 side1) internal { + engine.executeWithSlotMoves(battleKey, side0, side1); + } + + /// @dev 2-slot turn 0: each slot sends in the lead of its own roster range. + function _slotLeads(uint256 slot1Mon, uint104 salt) internal { + _slotTurn( + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, uint16(slot1Mon), salt), + sideWord(SWITCH_MOVE_INDEX, 0, SWITCH_MOVE_INDEX, uint16(slot1Mon), 0) + ); + } + + /// @dev Every slot rests, with side 0 carrying the turn salt. + function _restTurn(uint104 salt) internal { + _slotTurn( + sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, salt), + sideWord(NO_OP_MOVE_INDEX, 0, NO_OP_MOVE_INDEX, 0, 0) + ); + } + + // --- reads ------------------------------------------------------------ + + function _statusClass(uint256 side, uint256 monIndex) internal view returns (uint256) { + return engine.getMonStatusClass(battleKey, side, monIndex); + } + + function _stamina(uint256 side, uint256 monIndex) internal view returns (int32) { + return engine.getMonStateForBattle(battleKey, side, monIndex, MonStateIndexName.Stamina); + } + + function _active(uint256 absSlot) internal view returns (uint256) { + return engine.getActiveSlots(battleKey)[absSlot]; + } +} diff --git a/test/effects/battlefield/FluxFieldTest.sol b/test/effects/battlefield/FluxFieldTest.sol new file mode 100644 index 00000000..f23e1f82 --- /dev/null +++ b/test/effects/battlefield/FluxFieldTest.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../../../src/Constants.sol"; +import "../../../src/Enums.sol"; +import "../../../src/Structs.sol"; + +import {IRuleset} from "../../../src/IRuleset.sol"; +import {IEffect} from "../../../src/effects/IEffect.sol"; +import {FluxField} from "../../../src/effects/battlefield/FluxField.sol"; + +import {FieldHarness} from "./FieldHarness.sol"; + +/// @notice FluxField: round-end stamina becomes a per-mon [-2, +2] roll; the resting regen stays. +contract FluxFieldTest is FieldHarness { + // Salts precomputed offline against the engine's rng stream (keccak(p0Salt, p1Salt) then + // keccak(rng, absSlot) % 100 against the weight table). Side 1 always salts 0. + uint104 constant SALT_NEUTRAL = 99; // singles: A0 and B0 both roll 0 + uint104 constant SALT_PLUS2_MINUS2 = 5; // singles: A0 +2, B0 -2 + uint104 constant SALT_PLUS1_MINUS1 = 9; // singles: A0 +1, B0 -1 + uint104 constant SALT_DOUBLES_DRAIN = 769; // doubles: slots roll -2, -1, -1, -2 + + FluxField field; + IRuleset ruleset; + + function setUp() public { + _setUpHarness(); + field = new FluxField(); + ruleset = _ruleset(IEffect(address(field)), INLINE_REST_REGEN_MARKER); + } + + function _startRestedSingles() internal { + _startSingles(_mkTeam(2, 1000, 8, 10, weakAttack), _mkTeam(2, 1000, 8, 5, weakAttack), ruleset); + _singlesLeads(SALT_NEUTRAL); + } + + function _bothAttack(uint104 salt) internal { + _singlesTurn(0, 0, salt, 0, 0, 0); + } + + function test_rollAppliesPerMonAndReplacesRoundEndRegen() public { + _startRestedSingles(); + + // A neutral roll leaves the spent stamina alone: no vanilla +1 round-end regen under flux. + _bothAttack(SALT_NEUTRAL); + assertEq(_stamina(0, 0), -1, "attacker keeps the full stamina cost"); + assertEq(_stamina(1, 0), -1, "attacker keeps the full stamina cost"); + + _bothAttack(SALT_NEUTRAL); + assertEq(_stamina(0, 0), -2, "still no round-end regen"); + + // Each active rolls independently: A0 gains 2, B0 loses 2, on top of this turn's -1. + _bothAttack(SALT_PLUS2_MINUS2); + assertEq(_stamina(0, 0), -1, "A0: -3 spent, +2 flux"); + assertEq(_stamina(1, 0), -5, "B0: -3 spent, -2 flux"); + } + + function test_gainIsClampedAtFullStamina() public { + // Turn 0 leaves both mons at full stamina, so A0's +1 has nowhere to go and B0's -1 lands. + _startSingles(_mkTeam(2, 1000, 8, 10, weakAttack), _mkTeam(2, 1000, 8, 5, weakAttack), ruleset); + _singlesLeads(SALT_PLUS1_MINUS1); + + assertEq(_stamina(0, 0), 0, "a gain never pushes stamina above base"); + assertEq(_stamina(1, 0), -1, "the drain lands normally"); + } + + function test_drainIsClampedAtZeroStamina() public { + // Bob's mon has a single point of stamina, so one attack empties it. + _startSingles(_mkTeam(2, 1000, 8, 10, weakAttack), _mkTeam(2, 1000, 1, 5, weakAttack), ruleset); + _singlesLeads(SALT_NEUTRAL); + + _singlesTurn(NO_OP_MOVE_INDEX, 0, SALT_NEUTRAL, 0, 0, 0); + assertEq(_stamina(1, 0), -1, "Bob's mon is empty"); + + // Bob re-submits the move he can no longer afford (the engine skips it, and a skipped move + // is not a rest, so no resting regen), then eats a -1 roll that has nowhere to go. + _singlesTurn(NO_OP_MOVE_INDEX, 0, SALT_PLUS1_MINUS1, 0, 0, 0); + assertEq(_stamina(1, 0), -1, "a drain never pushes current stamina below zero"); + } + + function test_restingRegenStillApplies() public { + _startRestedSingles(); + _bothAttack(SALT_NEUTRAL); + assertEq(_stamina(0, 0), -1, "spent one stamina"); + + // Resting is the Engine's inline AfterMove regen, which the field's marker keeps enabled. + _singlesTurn(NO_OP_MOVE_INDEX, 0, SALT_NEUTRAL, NO_OP_MOVE_INDEX, 0, 0); + assertEq(_stamina(0, 0), 0, "resting still regenerates one stamina"); + } + + function test_koedActiveIsSkipped() public { + Mon[] memory aTeam = _mkTeam(2, 1000, 8, 10, killAttack); + Mon[] memory bTeam = _mkTeam(2, 1, 8, 5, weakAttack); + _startSingles(aTeam, bTeam, ruleset); + _singlesLeads(SALT_NEUTRAL); + + // B0 is KO'd this turn, so the -2 roll for its slot must not touch its stamina. + _bothAttack(SALT_PLUS2_MINUS2); + assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.IsKnockedOut), 1, "B0 is KO'd"); + // Alice outspeeds and KOs before Bob ever moves, so nothing but a flux roll could touch it. + assertEq(_stamina(1, 0), 0, "the KO'd mon's stamina is untouched"); + } + + function test_doublesRollsEachSlotIndependently() public { + Mon[] memory team = _mkTeam(2, 1000, 8, 10, weakAttack); + _startDoubles(team, team, ruleset); + _slotLeads(1, SALT_DOUBLES_DRAIN); + + // Send-ins cost no stamina, so these deltas are the round-end rolls alone. + assertEq(_stamina(0, 0), -2, "side 0 slot 0 rolled -2"); + assertEq(_stamina(0, 1), -1, "side 0 slot 1 rolled -1 from its own draw"); + assertEq(_stamina(1, 0), -1, "side 1 slot 0 rolled -1"); + assertEq(_stamina(1, 1), -2, "side 1 slot 1 rolled -2"); + } + + function test_rollDistributionMatchesTheWeightTable() public view { + // Deterministic tally over a fixed rng window; the table is [15, 20, 20, 25, 20] out of 100. + uint256[5] memory counts; + for (uint256 i; i < 1000; ++i) { + counts[uint256(int256(field.roll(i, 0)) + 2)] += 1; + } + assertEq(counts[0], 150, "-2"); + assertEq(counts[1], 203, "-1"); + assertEq(counts[2], 210, "0"); + assertEq(counts[3], 229, "+1"); + assertEq(counts[4], 208, "+2"); + } +} diff --git a/test/effects/battlefield/InlineRegenFlagsTest.sol b/test/effects/battlefield/InlineRegenFlagsTest.sol new file mode 100644 index 00000000..3fbdfef0 --- /dev/null +++ b/test/effects/battlefield/InlineRegenFlagsTest.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../../../src/Constants.sol"; +import "../../../src/Enums.sol"; +import "../../../src/Structs.sol"; + +import {DefaultRuleset} from "../../../src/DefaultRuleset.sol"; +import {IRuleset} from "../../../src/IRuleset.sol"; +import {IEffect} from "../../../src/effects/IEffect.sol"; +import {Storm} from "../../../src/effects/battlefield/Storm.sol"; + +import {FieldHarness} from "./FieldHarness.sol"; + +/// @notice The inline-regen marker plumbing: a ruleset carrying real global effects can opt into +/// either half of the Engine's inline stamina regen, or neither. +contract InlineRegenFlagsTest is FieldHarness { + Storm storm; + + function setUp() public { + _setUpHarness(); + storm = new Storm(); + } + + function _rulesetOf(address entry0) internal returns (IRuleset) { + IEffect[] memory effects = new IEffect[](1); + effects[0] = IEffect(entry0); + return new DefaultRuleset(engine, effects); + } + + function _rulesetOf(address entry0, address entry1) internal returns (IRuleset) { + IEffect[] memory effects = new IEffect[](2); + effects[0] = IEffect(entry0); + effects[1] = IEffect(entry1); + return new DefaultRuleset(engine, effects); + } + + /// @dev Turn 0 sends in the leads, turn 1 spends 2 stamina on a move. + function _startAndSpend(IRuleset ruleset) internal { + Mon[] memory team = _mkTeam(2, 1000, 8, 10, costlyAttack); + _startSingles(team, team, ruleset); + _singlesLeads(0); + _singlesTurn(0, 0, 1, 0, 0, 0); + } + + function test_sentinelRulesetKeepsBothRegenHalves() public { + _startAndSpend(IRuleset(INLINE_STAMINA_REGEN_RULESET)); + assertEq(_stamina(0, 0), -1, "round-end regen refunded one of the two spent"); + + _singlesTurn(NO_OP_MOVE_INDEX, 0, 2, NO_OP_MOVE_INDEX, 0, 0); + assertEq(_stamina(0, 0), 0, "resting regen refunded the other"); + } + + function test_fullMarkerInsideARulesetKeepsRegenAndInstallsTheEffect() public { + _startAndSpend(_rulesetOf(INLINE_STAMINA_REGEN_RULESET, address(storm))); + + assertEq(_stamina(0, 0), -1, "round-end regen runs alongside a real global effect"); + // Storm's round-end damage proves the effect was installed and that the marker neither + // consumed its slot nor was called as an effect itself. + assertLt(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp), 0, "Storm ticked"); + } + + function test_restOnlyMarkerSuppressesRoundEndRegen() public { + _startAndSpend(_rulesetOf(INLINE_REST_REGEN_MARKER, address(storm))); + assertEq(_stamina(0, 0), -2, "no round-end regen under the rest-only marker"); + + _singlesTurn(NO_OP_MOVE_INDEX, 0, 2, NO_OP_MOVE_INDEX, 0, 0); + assertEq(_stamina(0, 0), -1, "resting regen still fires"); + } + + function test_rulesetWithoutAMarkerGetsNoInlineRegen() public { + _startAndSpend(_rulesetOf(address(storm))); + assertEq(_stamina(0, 0), -2, "no inline regen at all"); + + _singlesTurn(NO_OP_MOVE_INDEX, 0, 2, NO_OP_MOVE_INDEX, 0, 0); + assertEq(_stamina(0, 0), -2, "not even the resting half"); + } + + /// @dev Recycled config storage must never leak a previous battle's regen flags. + function test_regenFlagsAreRewrittenPerBattle() public { + _startAndSpend(IRuleset(INLINE_STAMINA_REGEN_RULESET)); + assertEq(_stamina(0, 0), -1, "full regen in the first battle"); + + // Run the first battle to game over so its storage key is freed for reuse. + _singlesTurn(SWITCH_MOVE_INDEX, 1, 3, SWITCH_MOVE_INDEX, 1, 0); + Mon[] memory frail = _mkTeam(2, 1, 8, 10, killAttack); + _startSingles(frail, frail, IRuleset(INLINE_STAMINA_REGEN_RULESET)); + _singlesLeads(0); + _singlesTurn(0, 0, 1, 0, 0, 0); + _singlesTurn(SWITCH_MOVE_INDEX, 1, 2, SWITCH_MOVE_INDEX, 1, 0); + _singlesTurn(0, 0, 3, 0, 0, 0); + assertTrue(engine.getWinner(battleKey) != address(0), "first battles concluded"); + + _startAndSpend(_rulesetOf(address(storm))); + assertEq(_stamina(0, 0), -2, "a marker-less ruleset starts with no inline regen"); + } +} diff --git a/test/effects/battlefield/UnstableFieldTest.sol b/test/effects/battlefield/UnstableFieldTest.sol new file mode 100644 index 00000000..e54677c5 --- /dev/null +++ b/test/effects/battlefield/UnstableFieldTest.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../../../src/Constants.sol"; +import "../../../src/Enums.sol"; +import "../../../src/Structs.sol"; + +import {IRuleset} from "../../../src/IRuleset.sol"; +import {IEffect} from "../../../src/effects/IEffect.sol"; +import {UnstableField} from "../../../src/effects/battlefield/UnstableField.sol"; + +import {sideWord, targetBits} from "../../abstract/SlotWire.sol"; +import {FieldHarness} from "./FieldHarness.sol"; + +/// @notice UnstableField: every live active is dragged out for a random benched mon at round end. +contract UnstableFieldTest is FieldHarness { + UnstableField field; + IRuleset ruleset; + + function setUp() public { + _setUpHarness(); + field = new UnstableField(); + ruleset = _ruleset(IEffect(address(field)), INLINE_STAMINA_REGEN_RULESET); + } + + function _startFourMonSingles(IRuleset rules) internal { + _startSingles(_mkTeam(4, 1000, 5, 10, weakAttack), _mkTeam(4, 1000, 5, 5, weakAttack), rules); + } + + function test_turnZeroArmsTheFieldWithoutDragging() public { + _startFourMonSingles(ruleset); + _singlesLeads(0); + + // Switch-in abilities do not fire on turn 0, so the leads stay put. + assertEq(_active(A0), 0, "Alice keeps her lead through turn 0"); + assertEq(_active(B0), 0, "Bob keeps his lead through turn 0"); + } + + function test_bothActivesAreDraggedEveryFullTurn() public { + _startFourMonSingles(ruleset); + _singlesLeads(0); + + _singlesTurn(NO_OP_MOVE_INDEX, 0, 1, NO_OP_MOVE_INDEX, 0, 0); + uint256 aFirst = _active(A0); + uint256 bFirst = _active(B0); + assertTrue(aFirst != 0, "Alice's lead was dragged out"); + assertTrue(bFirst != 0, "Bob's lead was dragged out"); + + _singlesTurn(NO_OP_MOVE_INDEX, 0, 2, NO_OP_MOVE_INDEX, 0, 0); + assertTrue(_active(A0) != aFirst, "Alice is dragged again the next turn"); + assertTrue(_active(B0) != bFirst, "Bob is dragged again the next turn"); + } + + function test_dragTargetIsNeverKOed() public { + // Bob's roster is all 1 HP, so Alice's kill move leaves only KO'd benchers behind. + Mon[] memory aTeam = _mkTeam(4, 1000, 5, 10, killAttack); + Mon[] memory bTeam = _mkTeam(2, 1, 5, 5, weakAttack); + _startSingles(aTeam, bTeam, ruleset); + _singlesLeads(0); + + // Alice KOs Bob's active. The KO'd slot is skipped (Bob picks on his forced-switch turn), + // and Alice's own drag must not pull a KO'd mon either. + _singlesTurn(0, 0, 1, NO_OP_MOVE_INDEX, 0, 0); + assertEq(_active(B0), 0, "the KO'd active stays put for the forced switch"); + assertEq(engine.getMonStateForBattle(battleKey, 0, _active(A0), MonStateIndexName.IsKnockedOut), 0); + + // Bob sends in his last mon on the forced-switch turn (no effects run there). + engine.executeWithSingleMove(battleKey, SWITCH_MOVE_INDEX, 0, 1); + assertEq(_active(B0), 1, "Bob's replacement arrives untouched by the field"); + } + + function test_soleSurvivorIsNotDragged() public { + // A one-mon side has no legal replacement: the drag silently no-ops. + _startSingles(_mkTeam(1, 1000, 5, 10, weakAttack), _mkTeam(4, 1000, 5, 5, weakAttack), ruleset); + _singlesLeads(0); + _singlesTurn(NO_OP_MOVE_INDEX, 0, 1, NO_OP_MOVE_INDEX, 0, 0); + + assertEq(_active(A0), 0, "Alice's only mon stays in"); + assertTrue(_active(B0) != 0, "Bob still gets dragged"); + } + + function test_doublesSlotsDragToDistinctMons() public { + Mon[] memory team = _mkTeam(4, 1000, 5, 10, weakAttack); + _startDoubles(team, team, ruleset); + _slotLeads(1, 0); + assertEq(_active(A0), 0, "no drags on turn 0"); + assertEq(_active(A1), 1, "no drags on turn 0"); + + _restTurn(1); + assertTrue(_active(A0) != 0 && _active(A1) != 1, "both of side 0's slots were dragged"); + assertTrue(_active(A0) != _active(A1), "the two slots never land on the same mon"); + assertTrue(_active(B0) != _active(B1), "same for side 1"); + } + + function test_multiDragsStayInsideEachSeatsQuarter() public { + _startMulti(ruleset); + _slotLeads(4, 0); + _restTurn(1); + + // Seat 0 owns roster [0..3], seat 1 owns [4..7] on each side. + assertTrue(_active(A0) < 4, "side 0 slot 0 stays in seat 0's quarter"); + assertTrue(_active(A1) >= 4, "side 0 slot 1 stays in seat 1's quarter"); + assertTrue(_active(B0) < 4, "side 1 slot 0 stays in seat 0's quarter"); + assertTrue(_active(B1) >= 4, "side 1 slot 1 stays in seat 1's quarter"); + assertTrue(_active(A0) != 0 || _active(A1) != 4, "at least one side-0 slot moved"); + } + + function test_regenStillRunsAlongsideTheDrags() public { + _startFourMonSingles(ruleset); + _singlesLeads(0); + + // Alice attacks (-1 stamina) and is then dragged out; the round-end regen follows the lane, + // so it lands on whoever is standing there at the end of the turn. + _singlesTurn(0, 0, 1, NO_OP_MOVE_INDEX, 0, 0); + uint256 aNew = _active(A0); + assertTrue(aNew != 0, "Alice was dragged"); + assertEq(_stamina(0, 0), -1, "the mon that acted keeps its spent stamina"); + assertEq(_stamina(0, aNew), 0, "the incoming mon is at full stamina"); + } +} diff --git a/test/mons/XmonTest.sol b/test/mons/XmonTest.sol index 3946ecf9..bc010ff0 100644 --- a/test/mons/XmonTest.sol +++ b/test/mons/XmonTest.sol @@ -968,7 +968,7 @@ contract XmonTest is Test, BattleHelper { defaultRegistry.setTeam(ALICE, team); defaultRegistry.setTeam(BOB, team); - // Use the sentinel address that flips Engine into `hasInlineStaminaRegen` mode. + // Use the sentinel address that flips Engine into full inline-regen mode. bytes32 battleKey = _startBattle( engine, mockOracle, From 92cf5c7cbcbc02d1a2196dab551518047738efd1 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:48:53 -0700 Subject: [PATCH 41/41] fix --- deployments.json | 180 +++++++++++---------- src/effects/battlefield/ElementalField.sol | 18 ++- transpiler/dependency_resolver/resolver.py | 1 + 3 files changed, 106 insertions(+), 93 deletions(-) diff --git a/deployments.json b/deployments.json index 89dc55d5..cdfcc237 100644 --- a/deployments.json +++ b/deployments.json @@ -99,113 +99,119 @@ "ZAP_STATUS": "0xaafbb5b230d6e5f16d4c84183d09975f02c7ba9f" }, "TESTNET": { - "ACTUS_REUS": "0x010000000000000000000000fa875c2fcfb2814a7014dbc01e9ee7f93da1a4fa", - "ADAPTOR": "0x01000000000000000000000007c1a8de61ff01840b148ed79e8fb2f0cefb7f94", - "ANGERY": "0x0100000000000000000000003b51222fedb24e2ce814db2ae955f9bf45289f00", - "BASELIGHT": "0x86bdb24d465676cc74a4dde86f4b3a20d0257b0d", + "ACTUS_REUS": "0x0100000000000000000000004d2493ffbfa85d1df080a295b300927085228e10", + "ADAPTOR": "0x0100000000000000000000007068a7f1224c1b6bb3ad0dd86db349019f7d2202", + "ANGERY": "0x01000000000000000000000008d1e090c0602e0c0ef6b45255b9a623c9e1c993", + "BASELIGHT": "0xf0b6fc2e9f2436dc534d4252445a97eeb9131e18", "BATTLE_HISTORY": "0xa4f90f3991f2f53ba77a417b9fc094a545fba8d3", - "BIG_BELLOW": "0xb24f066f94ab8966bac25773f35fd5e103a816d4", + "BIG_BELLOW": "0x87b212142ef6ee24b039623dca6c8f184b5df04e", "BIG_BITE": "0x5009200000000000000000000000000000000000000000000000000000000000", - "BLESSED_STATUS": "0xa1b38d8c80c69c1602984251f6a0e02bc35b23a1", + "BLESSED_STATUS": "0xcc5da7f34010a9253c60e3a487be26777ce7d6ec", "BLOW": "0x460a200000000000000000000000000000000000000000000000000000000000", - "BRIGHTBACK": "0x6d53e419622802e7ba4a23ddde7d67cc8485a29e", - "BUBBLE_BOP": "0xac4ef17d4bf79daa81facde113e49f439c202636", - "BULL_RUSH": "0x6b4840de8efd5673c31b1fd66e4ce77ee590377f", - "BURN_STATUS": "0xaddade8df8d6931ea62844aed4fe5b2e5c76aa63", - "CARROT_HARVEST": "0x01000000000000000000000052817bbe483f70ff3e668f2b6b8396c118fdd9b8", - "CHAIN_EXPANSION": "0xc084a1b58264d94356706d664c245f15f2343fbe", - "CHILL_OUT": "0x00c6064000000000000000006f5970c0cab44a290a29f6f29a8a697e098128c5", - "CHOSEN_ONE": "0xe331505e1e241ca9b07a5fc8404d7522ae26e2ef", - "CHRONOFFENSE": "0x965d2cb19709ef2c02dff0c78a7a0012d89d6801", - "CLOUD_STRIKE": "0xb52bd6a7eea7e5659559c873aa2da6fdce6b830c", - "COMMIT_MANAGER": "0x4fddc4633a3ce1bc22d06cdd0effc8b9ea677ff2", - "CONTAGIOUS_SLUMBER": "0x5d7d11dd975e7d4cccb2e182b0271a1fa7b7cb30", - "CPU": "0xba5d206e510ae613487062b8b8c13f04d8408525", - "DEADLIFT": "0x50fe1d50fe798c7c9b2711f6eb5cd175f1f9e87b", - "DEEP_FREEZE": "0x5808f4a0f8fb18f9a7ceb1a9063aba88fa018220", + "BRIGHTBACK": "0xa648691fae8bd4c7b960a2427dcbeec99018ce65", + "BUBBLE_BOP": "0x361eb0db08dc9f0263da5fcd2e12a15975888744", + "BULL_RUSH": "0xf0136b5ae40731895a0112345961bd589ca4fdf9", + "BURN_STATUS": "0x59984c3ff8c3f97c2d34c8a235100848e4e1b79c", + "CARROT_HARVEST": "0x0100000000000000000000007f4b4a45861552c38db521494ec0a8a0ae77ed14", + "CHAIN_EXPANSION": "0x1437870a7b2abaef5116b1a7422e82e46233c734", + "CHILL_OUT": "0x00c606400000000000000000d4a296655a0884d42fece890475e3a475c2d7582", + "CHOSEN_ONE": "0x7d9c2c9812c1a47016dd7231814fa33a6a9b2300", + "CHRONOFFENSE": "0x1208616351f2f879e3f986d606fb317b3ed8ebc0", + "CLOUD_STRIKE": "0x95ea97ab03cd67e165c034683ae0b098341c5456", + "COMMIT_MANAGER": "0xebd60dd2b377e61ca90f2b130d6d71054b4d9bb8", + "CONTAGIOUS_SLUMBER": "0x120f16f13eacc6fb2b5b2251161cc6c7a107076d", + "CPU": "0x2a113cd91933484916e1173133be394693448f5f", + "DEADLIFT": "0x5431537c40d92c5bacaebcff5ad93effff149608", + "DEEP_FREEZE": "0xa0fab5f793393126dea21fa0d1b4c120f2212a90", "DEFAULT_MATCHMAKER": "0xd89a64a438b913d4cc47a61576d27877bad7f6d6", "DEFAULT_MON_REGISTRY": "0xef5fa1a5e99184df3a379992f6c7e9eee0ef7a74", "DEFAULT_RANDOMNESS_ORACLE": "0x1c9d9769d40eba316009660f2d74b8427843cdf9", "DEFAULT_RULESET": "0xa5e678884033b94509287500a0b8e6da04a8102f", "DEFAULT_VALIDATOR": "0xaa122ac35a19eeded0cfea8e2ddeab674e0f2fe3", - "DREAMCATCHER": "0x01000000000000000000000093718c99df09d81ed877c56b52f1f85bb5bd32d8", - "DUAL_SHOCK": "0x6dc3282d059b0411b94f2832c270a70939ad16a0", - "ELECTROCUTE": "0x5a4820a0000000000000000043754744f2b1217eff5bb433727c22027fca8a95", - "ENGINE": "0x4b28758db9be7f96f523d8569ef2155e869606e7", - "ETERNAL_GRUDGE": "0x87fcacdbef61f37ca08106c8562e8c2ce0ea0d74", + "DREAMCATCHER": "0x010000000000000000000000fda1253ff9aa1cfdd3cf165047e2b604053fad19", + "DUAL_SHOCK": "0x861d2c805c13f68da2df3dc191c6434e14b224ac", + "ELECTROCUTE": "0x5a4820a000000000000000009b13774b605224d8d91f2b43c7391af90e0861e4", + "ELEMENTAL_FIELD": "0xcf3890376eb29137a81f63b2a23ccd12bf0c7801", + "ELEMENTAL_FIELD_RULESET": "0x0a6d6253c47ef032f4d0980a63f053edaecad480", + "ENGINE": "0x0962c2402639e253dffd10e2b480246ca533c433", + "ETERNAL_GRUDGE": "0xb62b94205db6b3eb06ef30a0280fefef6a5e77b8", "FAIR_CPU": "0xc02f904dd32d244d8e60e2a504429f3f4b091de4", "FEDERAL_INVESTIGATION": "0x644c300000000000000000000000000000000000000000000000000000000000", - "FOUL_LANGUAGE": "0x7662b106f23c177f4b5c298bf899e361f1707477", - "FROSTBITE_STATUS": "0x6f5970c0cab44a290a29f6f29a8a697e098128c5", - "GACHACHACHA": "0x3d8952e98dc2997492162b792d3a90ca267456ee", - "GACHA_TEAM_REGISTRY": "0xe4b3c22643d28e3dbcd05c55170924188984c33a", - "GET_SLIPPERY": "0x2649a249a2235b620a26f3122d9ee79667ee8c33", - "GILDED_RECOVERY": "0x7814acb487ef161926f4ce261c6003ff51a62af6", - "GRAND_RESPITE": "0xbdeb19030491b6ae6f329e7a702c38ad90379381", - "GRAVE_AFFLICTION": "0x0a24b9e8572e534bbf79ddd614871b905f45ea9a", - "GUEST_FEATURE": "0x810ec98fd75ae36532ed06f933f493a95e236c44", - "HARD_RESET": "0x14da591b8fbe8b4b068f785ec6c11d5ac46a8b8a", - "HEAT_BEACON": "0xb4627fc4eeef5aadbbd8d4279d9da706b8168641", - "HIT_AND_DIP": "0xb3c677b3a28ed0b3099d4090a906456ca69926ae", - "HOLY_MOLT": "0x11d9a1283c249e18c6ad1e1929756f58314a3f71", + "FLUX_FIELD": "0xe0c02b34ee439179c028b1c6e5d8f1f997710855", + "FLUX_FIELD_RULESET": "0x5151b3084d9c772a37d232da87ad52b6681b3736", + "FOUL_LANGUAGE": "0x7e924c422c20d33cccb5a115a35d29c9f98591bc", + "FROSTBITE_STATUS": "0xd4a296655a0884d42fece890475e3a475c2d7582", + "GACHACHACHA": "0x9ef2ec1fcff32f790fa77dfaa7ed1d1662824176", + "GACHA_TEAM_REGISTRY": "0xdfa4d66dc6437ab38ab9f4361d34bcf69775eb7d", + "GET_SLIPPERY": "0x1f075c0f62f389ba45ab4b0aca291b87a730c1f3", + "GILDED_RECOVERY": "0xd6e44c02f7aeb8aef83e5822176cf7ca7693e477", + "GRAND_RESPITE": "0x5b592435fb8e9936ade4a1977d011296a4ea3592", + "GRAVE_AFFLICTION": "0x778f4a9c9a55a55a027a0579f69b9df39b1b1bfb", + "GUEST_FEATURE": "0x5cd9da0f5b333624a8eec0875eae1fe3490cedec", + "HARD_RESET": "0xdecf429e4a9dd5ee8742a4e1478f4015ba5ac88f", + "HEAT_BEACON": "0xd370d73a2a91e76d473e9ca743f624d670552432", + "HIT_AND_DIP": "0x5c8ccf2fd4dd2dff930b96b566d67831962a22a0", + "HOLY_MOLT": "0x05ebea1006db9b84e18597992c263974bf194c84", "HOLY_WATER_CLAW": "0x5a09300000000000000000000000000000000000000000000000000000000000", - "HONEY_BRIBE": "0x577b15a47a1fdc0e0a847d1dcf8161502264cd07", - "INFERNAL_FLAME": "0x49a668b09f4fe15fdcaf2e0eb8085e0481181e75", - "INFINITE_LOVE": "0x5a4d30a00000000000000000ebd8bceba62a46ae12920c6150de4ceebcf1da34", - "INITIALIZE": "0xea31e6448eb1c8b6ce4ef1ccab28f90021328de8", - "INTERWEAVING": "0x4b2c248c1598bfda2af676e6c54d74c5e9b063fa", - "INVOKE_TABOO": "0x8991c329cc9516d4909918959bbacb0307f823af", - "IRON_WALL": "0x2ee718f9f211f0565a6ec7dc271e5a208efb70d8", - "KINGS_LEISURE": "0xc592fa50a0994185df7907a6115fc96863383028", + "HONEY_BRIBE": "0x1b09f3e346b3a8caade3f17eb5e0a98e2c249f27", + "INFERNAL_FLAME": "0xe4a7b2c4952110a9b6b9232094ca5793c77d65f5", + "INFINITE_LOVE": "0x5a4d30a00000000000000000a85952fcc9ae653b1830fc54e661f981d38cd57e", + "INITIALIZE": "0xe3f69b920aed0c4d5ba1479900f3d063731c8ff2", + "INTERWEAVING": "0x82988f8cd67e3b3790a0c055b7d16628ac8c4b45", + "INVOKE_TABOO": "0xc7e0b6b676019571ac9199cbe3115621e7fdcb5f", + "IRON_WALL": "0xebf396368343c8859eba2c43245bf730a9b5bd1f", + "KINGS_LEISURE": "0x07617ea5fbaa934b9acdc26e3ff8881657ea4269", "KINGS_RESPITE": "0xe56793639eb661cf2df0334462e12c0f7c0d321d", - "LOOP": "0xbb052e2e142a11ab33bec4aa7222aebe8e1f74d9", - "MEGA_STAR_BLAST": "0xa7d7d7a92309c3e7190fbfddf036342f8491ee53", - "MODAL_BOLT": "0x541cbbadc9151aaa6407bc56f774be61ce50bbc9", - "NEGATIVE_THOUGHTS": "0x504b30a00000000000000000e50dc88a28c9e0bb923b9e08f2357cd78f0ced3c", + "LOOP": "0x27bb9ebeab203a205549c2b51f5c5f20bf668e7e", + "MEGA_STAR_BLAST": "0xc0af3ab2ab334d440c09732dc61f51b4dd22bc6c", + "MODAL_BOLT": "0x3bb682737fdf898a8c2aa811618c12233fe6ff48", + "NEGATIVE_THOUGHTS": "0x504b30a00000000000000000a12662b2823502c74c8381a4a419ff28307a4536", "NIGHT_TERRORS": "0xd9cc94ce09177091609222b02854f671deaa2579", - "NINE_NINE_NINE": "0x2342022e2f54456c8adea785c08e0b78bbdea1ce", + "NINE_NINE_NINE": "0xccf463f527580d05aef9c1cfcc4e27e4e87c71df", "OKAY_CPU": "0xc14f1a8cdbf755055fee3576a6d06791f72232bf", - "OLD_VENGEANCE": "0x6b1e305fadc1cf4c43da35db7a9dbaca7ca58a16", + "OLD_VENGEANCE": "0x5fea5fc08cc70fca29c0beabe3ea5e87dfa2dd06", "OSTEOPOROSIS": "0x5a00200000000000000000000000000000000000000000000000000000000000", - "OVERCLOCK": "0x0dcdd62e545475233068e9442383fe0ef9db5ef7", - "OVERFLOW": "0x93bd53413e61e24d4188ef57bff9ddab425a74fd", - "PANIC_STATUS": "0xe50dc88a28c9e0bb923b9e08f2357cd78f0ced3c", - "PISTOL_SQUAT": "0xd262aace00d0cc267d64cfd3f4c2009abd774977", - "POST_WORKOUT": "0x0100000000000000000000006a62f5873cda2fe43f7ed940c2acf4952416643c", + "OVERCLOCK": "0x36b4f5adc1648836f07496da4f36dbebb10df064", + "OVERFLOW": "0x93c75c63bb1ae1f6b1f76e81b7ed3b252e98724c", + "PANIC_STATUS": "0xa12662b2823502c74c8381a4a419ff28307a4536", + "PISTOL_SQUAT": "0x708e85d1130fbbd69d4dda2a8406ff6610413f45", + "POST_WORKOUT": "0x010000000000000000000000e4a2798e2c989a00b031dab610c80f77bb96f604", "POUND_GROUND": "0x5f02300000000000000000000000000000000000000000000000000000000000", - "PREEMPTIVE_SHOCK": "0xd2d20bbc8c9be2f162c8e63d7f511bdc1b2b23b7", - "Q5": "0xe9a9c31c9bc86d6647deb31beb6f16b1b7f8e5ed", - "QUICKSTORM": "0x8ade290e5b72aa5770af7ad143cab8faa041aa57", - "RENORMALIZE": "0x9b95349047e734d11ce6eeeaa8fe10b81da3d6b2", - "RISE_FROM_THE_GRAVE": "0x4cc389702a30109bc0625753657779d9a34348cd", - "ROCK_PULL": "0xd3a0f31b04b70eb9bbdb7c27067d78a9a9dafe60", - "ROUND_TRIP": "0x28ab1b8b1c5e68cf83e7612f5e9d57c5d94eb303", - "SANCTIFY": "0xeb5739ebb33e1397c92dc481c98f25b07e215e91", - "SAVIOR_COMPLEX": "0xe562b4a060ca9d95e23da672b8289e39ddba7894", - "SCARY_NUMBERS": "0x500b31400000000000000000e50dc88a28c9e0bb923b9e08f2357cd78f0ced3c", - "SET_ABLAZE": "0x4b9ae643536d9edddee38573d4b02ca56706584b", - "SIGNED_MATCHMAKER": "0xc41d5ea3b36ee9025b8c7adb95437e0d2de89013", - "SIMPLE_PM": "0x928d58230919cf36baea288e495ee0d2e9a21967", - "SLEEP_STATUS": "0xebd8bceba62a46ae12920c6150de4ceebcf1da34", - "SNACK_BREAK": "0x1ce7ae46c3442ddae00128970c7561a8a36fb765", - "SNEAK_ATTACK": "0x59e593b53be3ac34ddc88363734261949c900ad8", - "SOMNIPHOBIA": "0x2eb9465482bdfcf6e3d867eb19b3be4e0032feba", + "PREEMPTIVE_SHOCK": "0x19a1521d33dd4576b334a974369c590d7d8cc36c", + "Q5": "0x581d1899fb7b8be5e3d9b5ac0e093a6c2c52911c", + "QUICKSTORM": "0x999cf29f2724f2f5f56c37a27921db0e181d3165", + "RENORMALIZE": "0xba3263a280e8a1cda9a1408b3c1804004c035944", + "RISE_FROM_THE_GRAVE": "0x1ad66235b5d062085bae2cbfb2c9307021f42f94", + "ROCK_PULL": "0x21ce2d4524e4213029e2c3352a16fa9b8767dd91", + "ROUND_TRIP": "0xeb6e3f8339f770a4bef6faa597df9b036b03d314", + "SANCTIFY": "0x57327c43af6fbda0978b0129933d4e1a635c9c79", + "SAVIOR_COMPLEX": "0xb345b262cd4dfc95cedb58f6b4940ed1cf208097", + "SCARY_NUMBERS": "0x500b31400000000000000000a12662b2823502c74c8381a4a419ff28307a4536", + "SET_ABLAZE": "0x6e4370dda26c9333be1b6dc16ac4f5b864807db7", + "SIGNED_MATCHMAKER": "0x014719908a86ef4a69aecc29a0844a43e2553ef9", + "SIMPLE_PM": "0xa265afbb253a9f0de64267f9f21fd002e40112d4", + "SLEEP_STATUS": "0xa85952fcc9ae653b1830fc54e661f981d38cd57e", + "SNACK_BREAK": "0xd4691882bddcff0a7ba13c5846e65c999e78981d", + "SNEAK_ATTACK": "0x542fe28a75275eaff9a75990aa10d7a35e40e50e", + "SOMNIPHOBIA": "0x4a52f0cbaf8021658cc24b510339329554149c10", "STAMINA_REGEN": "0xe9c82033011dedf20d2fe69920b5020403041d0f", "STAT_BOOSTS": "0x8c072f12345b3e95fa52fcdf74610c044c044940", - "STORM": "0x7b16496037b25d123c2c2f4ec6f765e159d01aac", - "SUMMON_STORM": "0x4294b6f18a141d8aaf8ea3c192cb15b8369cc0e2", + "STORM": "0x5abf94c8848a535349000d41461dbe67004451e0", + "SUMMON_STORM": "0x1b83f212fc9166b64815c2703e7025f4d3750eb0", "THROW_PEBBLE": "0x2802100000000000000000000000000000000000000000000000000000000000", - "THUNDERCLAP": "0x78483140000000000000000043754744f2b1217eff5bb433727c22027fca8a95", - "TINDERCLAWS": "0x0100000000000000000000002d5d9ca9150213567b68a95a3c21ecd56278f216", - "TRIPLE_THINK": "0xacad3b211c235ac3b331a7fa4fbb4e219cfc7f3e", - "TYPE_CALCULATOR": "0x8ec1015b3fc471ebccf3269bf2cf07275bf84745", - "UNBOUNDED_STRIKE": "0xdedbd81a2cd3e6186d8997f4d95e1b0eb2841692", + "THUNDERCLAP": "0x7848314000000000000000009b13774b605224d8d91f2b43c7391af90e0861e4", + "TINDERCLAWS": "0x01000000000000000000000057083aa501e4cd80c515c983b8377e7d3d0c7f1e", + "TRIPLE_THINK": "0x73f5efc17e1977b8893235a33aaf13d83d3e57f4", + "TYPE_CALCULATOR": "0xa6db52a3f08b334509d5cb60b0866910c0f7fd6e", + "UNBOUNDED_STRIKE": "0xc70621c6e586a22147bb7205265ba1ece470f3a2", "UNEXPECTED_CARROT": "0x7807400000000000000000000000000000000000000000000000000000000000", - "UP_ONLY": "0x01000000000000000000000047a10b47cf900e5caa67ea22b5c332458bf2f505", - "VITAL_SIPHON": "0x65f66d32ecda5787934531b31ddf207ba50cb34b", - "VOLATILE_PUNCH": "0x97c7fb1a7bfc523a0dcb8b76ef59f526f505446c", + "UNSTABLE_FIELD": "0x11b03269846160c417538e18544f3ca1f5e096eb", + "UNSTABLE_FIELD_RULESET": "0x9ca08c17e257ed30aa2ac96f144210563d650c62", + "UP_ONLY": "0x0100000000000000000000006d08e5f81e728634f5cb09deafae5dd44a313370", + "VITAL_SIPHON": "0x2e9a2adc0ec74f3bd460b8f4a986fe9d4a3f4239", + "VOLATILE_PUNCH": "0xbf707a5b1867c1dc5b62a2837fd409b6748b96f2", "WATER_CLAW": "0x5003200000000000000000000000000000000000000000000000000000000000", - "WITHER_AWAY": "0x9c82ffa894555ed0867604499ca2c4be24e3e8b9", - "ZAP_STATUS": "0x43754744f2b1217eff5bb433727c22027fca8a95" + "WITHER_AWAY": "0xda9606960c2574d8a160f2d2029bdd593eeef0f8", + "ZAP_STATUS": "0x9b13774b605224d8d91f2b43c7391af90e0861e4" } } diff --git a/src/effects/battlefield/ElementalField.sol b/src/effects/battlefield/ElementalField.sol index 1ed75c2a..a4fb6d7b 100644 --- a/src/effects/battlefield/ElementalField.sol +++ b/src/effects/battlefield/ElementalField.sol @@ -19,12 +19,18 @@ contract ElementalField is BasicEffect { IEffect private immutable PANIC; IEffect private immutable ZAP; - constructor(IEffect burn, IEffect frostbite, IEffect sleep, IEffect panic, IEffect zap) { - BURN = burn; - FROSTBITE = frostbite; - SLEEP = sleep; - PANIC = panic; - ZAP = zap; + constructor( + IEffect _BURN_STATUS, + IEffect _FROSTBITE_STATUS, + IEffect _SLEEP_STATUS, + IEffect _PANIC_STATUS, + IEffect _ZAP_STATUS + ) { + BURN = _BURN_STATUS; + FROSTBITE = _FROSTBITE_STATUS; + SLEEP = _SLEEP_STATUS; + PANIC = _PANIC_STATUS; + ZAP = _ZAP_STATUS; } // Steps: RoundEnd + fresh RoundEnd context (status lanes) + ALWAYS_APPLIES. diff --git a/transpiler/dependency_resolver/resolver.py b/transpiler/dependency_resolver/resolver.py index 387bf71b..4bccc80f 100644 --- a/transpiler/dependency_resolver/resolver.py +++ b/transpiler/dependency_resolver/resolver.py @@ -9,6 +9,7 @@ Unresolved dependencies are tracked and can be exported for user action. """ +import json from typing import Dict, List, Optional, Set, Tuple, Union from dataclasses import dataclass

l3h<$a#N6|K6hl7~8A^SUhPZ&K1@*M{(+eNlV!* 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 z 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