@pkmn/engine 0.1.0-dev.c544e10e → 0.1.0-dev.cbdbb719

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +41 -14
  3. package/build/pkg/addon.js +6 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data.d.ts +4 -4
  6. package/build/pkg/data.js.map +1 -1
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/index.d.ts +3 -2
  9. package/build/pkg/index.js.map +1 -1
  10. package/build/pkg/protocol.js +4 -4
  11. package/build/pkg/protocol.js.map +1 -1
  12. package/build/test/display.js +8 -3
  13. package/build/test/display.js.map +1 -1
  14. package/build/tools/debug.js +2 -3
  15. package/build/tools/debug.js.map +1 -1
  16. package/build.zig +208 -81
  17. package/package.json +32 -21
  18. package/src/bin/install-pkmn-engine +25 -17
  19. package/src/lib/common/array.zig +77 -0
  20. package/src/lib/common/data.zig +4 -4
  21. package/src/lib/common/debug.zig +2 -2
  22. package/src/lib/common/js.zig +4 -2
  23. package/src/lib/common/optional.zig +14 -10
  24. package/src/lib/common/options.zig +2 -0
  25. package/src/lib/common/protocol.zig +73 -73
  26. package/src/lib/common/rational.zig +121 -27
  27. package/src/lib/common/rng.zig +7 -7
  28. package/src/lib/common/util.zig +50 -1
  29. package/src/lib/gen1/calc.zig +511 -131
  30. package/src/lib/gen1/chance.zig +374 -267
  31. package/src/lib/gen1/data/species.zig +1 -1
  32. package/src/lib/gen1/data.zig +15 -14
  33. package/src/lib/gen1/helpers.zig +96 -536
  34. package/src/lib/gen1/mechanics.zig +285 -189
  35. package/src/lib/gen2/calc.zig +34 -16
  36. package/src/lib/gen2/chance.zig +85 -492
  37. package/src/lib/gen2/data.zig +8 -7
  38. package/src/lib/gen2/generated.zig +1 -3
  39. package/src/lib/gen2/helpers.zig +41 -9
  40. package/src/lib/gen2/mechanics.zig +178 -121
  41. package/src/lib/{binding/node.zig → node.zig} +9 -12
  42. package/src/lib/pkmn.zig +32 -15
  43. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  44. package/src/test/display/pkmn.html.tmpl +5 -5
  45. package/src/lib/gen1/README.md +0 -894
  46. package/src/lib/gen2/README.md +0 -329
@@ -4,10 +4,12 @@ const pkmn = @import("../pkmn.zig");
4
4
 
5
5
  const common = @import("../common/data.zig");
6
6
  const DEBUG = @import("../common/debug.zig").print;
7
+ const optional = @import("../common/optional.zig");
7
8
  const protocol = @import("../common/protocol.zig");
8
9
  const rng = @import("../common/rng.zig");
9
10
 
10
11
  const data = @import("data.zig");
12
+ const chance = @import("chance.zig");
11
13
 
12
14
  const assert = std.debug.assert;
13
15
 
@@ -71,8 +73,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
71
73
  var p1 = battle.side(.P1);
72
74
  var p2 = battle.side(.P2);
73
75
 
74
- var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
75
- var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
76
+ const r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
+ const r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
76
78
 
77
79
  if (try turnOrder(battle, c1, c2, options) == .P1) {
78
80
  if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
@@ -90,11 +92,11 @@ fn start(battle: anytype, options: anytype) !Result {
90
92
  const p1 = battle.side(.P1);
91
93
  const p2 = battle.side(.P2);
92
94
 
93
- var p1_slot = findFirstAlive(p1);
95
+ const p1_slot = findFirstAlive(p1);
94
96
  assert(!showdown or p1_slot == 1);
95
97
  if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
96
98
 
97
- var p2_slot = findFirstAlive(p2);
99
+ const p2_slot = findFirstAlive(p2);
98
100
  assert(!showdown or p2_slot == 1);
99
101
  if (p2_slot == 0) return Result.Win;
100
102
 
@@ -166,7 +168,7 @@ fn selectMove(
166
168
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
167
169
  // set the selected move will not actually be used, it will just be reported as having
168
170
  // been used (this differs from how Pokémon Showdown works, but its impossible to
169
- // replicate the incorrect behavior with the correct mechanisms).
171
+ // replicate the incorrect behavior with the correct mechanisms)
170
172
  saveMove(battle, player, choice);
171
173
  } else {
172
174
  assert(choice.data == 0);
@@ -253,11 +255,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
253
255
  active.moves = incoming.moves;
254
256
 
255
257
  statusModify(incoming.status, &active.stats);
258
+ options.chance.switched(player, slot);
256
259
 
257
260
  foe.active.volatiles.Binding = false;
258
261
 
259
262
  try options.log.switched(.{ battle.active(player), incoming });
260
- options.chance.switched(player, side.order[0], out);
261
263
 
262
264
  if (showdown and incoming.status == Status.TOX) {
263
265
  incoming.status = Status.init(.PSN);
@@ -347,13 +349,13 @@ fn doTurn(
347
349
  )) |r| return r;
348
350
  if (!replace) {
349
351
  if (player_choice.type != .Switch) {
350
- if (try checkFaint(battle, foe_player, options)) |r| return r;
352
+ if (try checkFaint(battle, foe_player, true, options)) |r| return r;
351
353
  }
352
354
  if (residual) try handleResidual(battle, player, options);
353
- if (try checkFaint(battle, player, options)) |r| return r;
355
+ if (try checkFaint(battle, player, false, options)) |r| return r;
354
356
  } else if (foe_choice.type == .Pass) return null;
355
357
 
356
- options.chance.clearPending();
358
+ if (!showdown) options.chance.clearPending();
357
359
 
358
360
  residual = true;
359
361
  replace = battle.side(foe_player).stored().hp == 0;
@@ -369,10 +371,10 @@ fn doTurn(
369
371
  )) |r| return r;
370
372
  if (!replace) {
371
373
  if (!calc and foe_choice.type != .Switch) {
372
- if (try checkFaint(battle, player, options)) |r| return r;
374
+ if (try checkFaint(battle, player, true, options)) |r| return r;
373
375
  }
374
376
  if (residual) try handleResidual(battle, foe_player, options);
375
- if (try checkFaint(battle, foe_player, options)) |r| return r;
377
+ if (try checkFaint(battle, foe_player, false, options)) |r| return r;
376
378
  }
377
379
 
378
380
  // Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
@@ -431,7 +433,7 @@ fn executeMove(
431
433
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
432
434
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
433
435
  // thawed, in which case we will desync unless the last_selected_move happened to be at
434
- // index 1 and the current Pokémon has the same move in its first slot.
436
+ // index 1 and the current Pokémon has the same move in its first slot
435
437
  if (!auto) {
436
438
  // side.active.moves(slot) is safe to check even though the slot in question might not
437
439
  // technically be from this Pokémon because it must be exactly 1 to not desync and
@@ -484,17 +486,22 @@ fn beforeMove(
484
486
 
485
487
  if (Status.is(stored.status, .SLP)) {
486
488
  const before = stored.status;
489
+ const slf = Status.is(stored.status, .EXT);
487
490
  // Even if the EXT bit is set this will still correctly modify the sleep duration
488
- if (options.calc.modify(player, .sleep)) |extend| {
489
- if (!extend) stored.status = 0;
491
+ if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
492
+ .started, .ended => stored.status = 0,
493
+ .continuing => if (Status.duration(stored.status) > 1) {
494
+ stored.status -= 1;
495
+ },
496
+ else => unreachable,
490
497
  } else {
491
498
  stored.status -= 1;
492
499
  }
493
500
 
494
501
  const duration = Status.duration(stored.status);
495
- if (!Status.is(stored.status, .EXT)) {
496
- try options.chance.sleep(player, duration);
497
- }
502
+ try options.chance.sleep(player, if (slf)
503
+ .None
504
+ else if (duration == 0) .ended else .continuing);
498
505
 
499
506
  if (duration == 0) {
500
507
  try log.curestatus(.{ ident, before, .Message });
@@ -532,12 +539,12 @@ fn beforeMove(
532
539
  }
533
540
 
534
541
  if (volatiles.disable_duration > 0) {
535
- if (options.calc.modify(player, .disable)) |extend| {
536
- if (!extend) volatiles.disable_duration = 0;
537
- } else {
538
- volatiles.disable_duration -= 1;
539
- }
540
- try options.chance.disable(player, volatiles.disable_duration);
542
+ volatiles.disable_duration =
543
+ decrement(.disable, player, options, volatiles.disable_duration);
544
+ try options.chance.disable(
545
+ player,
546
+ if (volatiles.disable_duration == 0) .ended else .continuing,
547
+ );
541
548
 
542
549
  if (volatiles.disable_duration == 0) {
543
550
  volatiles.disable_move = 0;
@@ -556,13 +563,19 @@ fn beforeMove(
556
563
 
557
564
  if (volatiles.Confusion) {
558
565
  assert(volatiles.confusion > 0);
559
-
560
- if (options.calc.modify(player, .confusion)) |extend| {
561
- if (!extend) volatiles.confusion = 0;
566
+ if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
567
+ .started => volatiles.confusion -= 1,
568
+ .continuing => if (volatiles.confusion > 1) {
569
+ volatiles.confusion -= 1;
570
+ },
571
+ .ended => volatiles.confusion = 0,
572
+ .overwritten => {},
573
+ else => unreachable,
562
574
  } else {
563
575
  volatiles.confusion -= 1;
564
576
  }
565
- try options.chance.confusion(player, volatiles.confusion);
577
+
578
+ try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
566
579
 
567
580
  if (volatiles.confusion == 0) {
568
581
  volatiles.Confusion = false;
@@ -580,7 +593,9 @@ fn beforeMove(
580
593
  volatiles.Charging = false;
581
594
  volatiles.Binding = false;
582
595
  volatiles.Invulnerable = false;
583
- options.chance.clearDurations(player, null);
596
+
597
+ options.chance.observe(.attacking, player, .None);
598
+ options.chance.observe(.binding, player, .None);
584
599
  {
585
600
  // This feels (and is) disgusting but the cartridge literally just overwrites
586
601
  // the opponent's defense with the user's defense and resets it after. As a
@@ -588,7 +603,9 @@ fn beforeMove(
588
603
  const def = foe.active.stats.def;
589
604
  foe.active.stats.def = active.stats.def;
590
605
  defer foe.active.stats.def = def;
591
- if (!calcDamage(battle, player, player.foe(), null, false)) return .err;
606
+ if (!calcDamage(battle, player, player.foe(), null, false, options)) {
607
+ return .err;
608
+ }
592
609
  }
593
610
  // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
594
611
  // on the targeting behavior of the confused Pokémon's selected move which results
@@ -617,7 +634,10 @@ fn beforeMove(
617
634
  volatiles.Thrashing = false;
618
635
  volatiles.Charging = false;
619
636
  volatiles.Binding = false;
620
- options.chance.clearDurations(player, null);
637
+
638
+ options.chance.observe(.attacking, player, .None);
639
+ options.chance.observe(.binding, player, .None);
640
+
621
641
  // GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
622
642
  try log.cant(.{ ident, .Paralysis });
623
643
  return .done;
@@ -635,14 +655,8 @@ fn beforeMove(
635
655
  volatiles.state +%= battle.last_damage;
636
656
  }
637
657
 
638
- assert(volatiles.attacks > 0);
639
-
640
- if (options.calc.modify(player, .attacking)) |extend| {
641
- if (!extend) volatiles.attacks = 0;
642
- } else {
643
- volatiles.attacks -= 1;
644
- }
645
- try options.chance.attacking(player, volatiles.attacks);
658
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
659
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
646
660
 
647
661
  if (volatiles.attacks != 0) {
648
662
  try log.activate(.{ ident, .Bide });
@@ -676,39 +690,36 @@ fn beforeMove(
676
690
  }
677
691
 
678
692
  if (volatiles.Thrashing) {
679
- assert(volatiles.attacks > 0);
693
+ try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
680
694
 
681
- if (options.calc.modify(player, .attacking)) |extend| {
682
- if (!extend) volatiles.attacks = 0;
683
- } else {
684
- volatiles.attacks -= 1;
685
- }
686
- try options.chance.attacking(player, volatiles.attacks);
695
+ volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
696
+ try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
697
+
698
+ if (volatiles.attacks == 0) {
699
+ const overwritten = volatiles.Confusion;
687
700
 
688
- if (!showdown and handleThrashing(battle, active, player, options)) {
701
+ volatiles.Thrashing = false;
702
+ volatiles.Confusion = true;
703
+ volatiles.confusion = Rolls.confusionDuration(battle, player, options);
704
+
705
+ // On Pokémon Showdown this could leak information to the opponent that thrashing has
706
+ // ended (the engine's protocol is always from the omniscient perspective and clients
707
+ // can be expected to recognize this pattern and pass the information only to the
708
+ // correct player)
689
709
  try log.start(.{ battle.active(player), .ConfusionSilent });
710
+ options.chance.observe(.confusion, player, if (overwritten) .overwritten else .started);
690
711
  }
691
- try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
692
- if (showdown) {
693
- // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
694
- // used in useMove and doesn't have the notion of skipping canMove semantics
695
- side.last_used_move = side.last_selected_move;
696
- if (handleThrashing(battle, active, player, options)) {
697
- try log.start(.{ battle.active(player), .ConfusionSilent });
698
- }
699
- }
712
+
713
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
714
+ // used in useMove and doesn't have the notion of skipping canMove semantics
715
+ if (showdown) side.last_used_move = side.last_selected_move;
716
+
700
717
  return .skip_can;
701
718
  }
702
719
 
703
720
  if (volatiles.Binding) {
704
- assert(volatiles.attacks > 0);
705
-
706
- if (options.calc.modify(player, .binding)) |extend| {
707
- if (!extend) volatiles.attacks = 0;
708
- } else {
709
- volatiles.attacks -= 1;
710
- }
711
- try options.chance.binding(player, volatiles.attacks);
721
+ volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
722
+ try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
712
723
 
713
724
  try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
714
725
  if (showdown or battle.last_damage != 0) {
@@ -738,7 +749,6 @@ fn canMove(
738
749
  const player_ident = battle.active(player);
739
750
  const move = Move.get(side.last_selected_move);
740
751
 
741
- var skip = skip_pp;
742
752
  if (side.active.volatiles.Charging) {
743
753
  side.active.volatiles.Charging = false;
744
754
  side.active.volatiles.Invulnerable = false;
@@ -750,7 +760,7 @@ fn canMove(
750
760
  }
751
761
 
752
762
  side.last_used_move = side.last_selected_move;
753
- if (!skip) decrementPP(side, mslot, auto);
763
+ if (!skip_pp) decrementPP(side, mslot, auto);
754
764
 
755
765
  const target = if (move.target == .Self) player else player.foe();
756
766
  try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
@@ -802,7 +812,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
802
812
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
803
813
  }
804
814
 
805
- fn incrementPP(side: *Side, mslot: u4) void {
815
+ fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
806
816
  var active = &side.active;
807
817
  const volatiles = &active.volatiles;
808
818
 
@@ -810,8 +820,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
810
820
  // GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
811
821
  if (showdown and volatiles.Transform) return;
812
822
 
823
+ const n = options.calc.overridden(player, .pp) orelse 1;
824
+ if (volatiles.Transform and !options.chance.pp(player, n)) return;
825
+
813
826
  assert(mslot > 0 and mslot <= 4);
814
- side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% 1;
827
+ side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
815
828
  }
816
829
 
817
830
  // Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
@@ -859,13 +872,13 @@ fn doMove(
859
872
  break :miss true;
860
873
  }
861
874
  break :miss (move.target != .Self and
862
- !moveHit(battle, player, move, &immune, &mist, options));
875
+ !try moveHit(battle, player, move, &immune, &mist, options));
863
876
  };
864
877
  assert(!immune or miss or (showdown and move.effect == .Binding));
865
878
 
866
879
  var late = showdown and move.effect != .Explode;
867
880
  const skip = status or immune;
868
- if ((!showdown or (!skip or counter)) and !miss) blk: {
881
+ if ((!showdown or (!skip or counter)) and !miss) damage: {
869
882
  if (showdown and move.effect.isMulti()) {
870
883
  try Effects.multiHit(battle, player, move, options);
871
884
  hits = side.active.volatiles.attacks;
@@ -887,8 +900,9 @@ fn doMove(
887
900
  ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
888
901
  // This can overflow after adjustDamage, but will still be sufficient to OHKO
889
902
  battle.last_damage = if (ohko) 65535 else 0;
890
- if (showdown) break :blk; // skip adjustDamage / randomizeDamage
891
- } else if (!calcDamage(battle, player, player.foe(), move, crit)) {
903
+ if (showdown) break :damage; // skip adjustDamage / randomizeDamage
904
+ } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
905
+ if (!showdown) try options.chance.commit(player, .err);
892
906
  return @as(?Result, Result.Error);
893
907
  }
894
908
  if (battle.last_damage == 0) {
@@ -902,11 +916,12 @@ fn doMove(
902
916
  }
903
917
  }
904
918
 
905
- var zero = battle.last_damage == 0;
906
- miss = if (showdown or skip)
907
- miss
908
- else
909
- (!moveHit(battle, player, move, &immune, &mist, options) or zero);
919
+ var missed: bool = undefined;
920
+ const zero = battle.last_damage == 0;
921
+ if (!showdown and !skip) {
922
+ missed = !try moveHit(battle, player, move, &immune, &mist, options);
923
+ miss = missed or zero;
924
+ }
910
925
 
911
926
  assert(showdown or miss or battle.last_damage > 0 or skip);
912
927
  assert((!showdown and miss) or !(ohko and immune));
@@ -933,11 +948,32 @@ fn doMove(
933
948
  try log.immune(.{ foe_ident, .OHKO });
934
949
  } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
935
950
  try log.immune(.{ foe_ident, .None });
951
+ if (!showdown) {
952
+ if (move.effect == .Binding) {
953
+ // Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
954
+ // then simply clears it later on if the move happens to miss in moveHit.
955
+ // However, moveHit can return true and we'll still end up in the outer miss
956
+ // handling block because attacking into an immune Pokemon does zero damage
957
+ // (considered missing)
958
+ assert(zero);
959
+ // Thus we still need to actually save the hit result (but not the crit roll).
960
+ // Using .miss for this commit would accomplish what we want, but is somewhat
961
+ // misleading as its entirely possible the move actually did hit (and the target
962
+ // is now trapped) so we use the .binding value instead which is effectively
963
+ // identical, just doesn't claim to know about the actual results of the hit
964
+ // roll)
965
+ try options.chance.commit(player, .binding);
966
+ } else {
967
+ // The .glitch option allows for conditionally committing if we are in a
968
+ // division-by-zero freeze alternative timeline scenario
969
+ try options.chance.commit(player, .glitch);
970
+ }
971
+ }
936
972
  } else if (mist) {
937
973
  if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
938
974
  try log.fail(.{ foe_ident, .None });
939
975
  } else {
940
- if (showdown or !zero) try options.chance.commit(player, .miss);
976
+ if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
941
977
  try log.lastmiss(.{});
942
978
  try log.miss(.{battle.active(player)});
943
979
  }
@@ -952,17 +988,15 @@ fn doMove(
952
988
  try buildRage(battle, player.foe(), options);
953
989
  } else if (showdown and move.effect == .Disable) {
954
990
  try buildRage(battle, player.foe(), options);
955
- } else if (!showdown and immune and move.effect == .Binding) {
956
- try options.chance.commit(player, .binding);
957
991
  }
958
992
  return null;
959
- } else {
993
+ } else if (!showdown) {
960
994
  try options.chance.commit(player, .hit);
961
995
  }
962
996
 
963
997
  // On the cartridge MultiHit doesn't get set up until after damage has been applied for the
964
998
  // first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
965
- // it up above before damage calculation).
999
+ // it up above before damage calculation)
966
1000
  if (!showdown and move.effect.isMulti()) {
967
1001
  try Effects.multiHit(battle, player, move, options);
968
1002
  hits = side.active.volatiles.attacks;
@@ -1021,12 +1055,12 @@ fn doMove(
1021
1055
  }
1022
1056
 
1023
1057
  // Substitute being broken nullifies the move's effect completely so even
1024
- // if an effect was intended to "always happen" it will still get skipped.
1058
+ // if an effect was intended to "always happen" it will still get skipped
1025
1059
  if (nullified) return null;
1026
1060
 
1027
1061
  // On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
1028
1062
  // but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
1029
- // setup before the loop means we can avoid having to waste time doing no-op handler searches.
1063
+ // setup before the loop means we can avoid having to waste time doing no-op handler searches
1030
1064
  if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
1031
1065
 
1032
1066
  if (foe.stored().hp == 0) return null;
@@ -1041,7 +1075,7 @@ fn doMove(
1041
1075
  // below). For Twineedle we change the data to that of one with PoisonChance1 given its
1042
1076
  // MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
1043
1077
  // Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
1044
- // puts the |-status| message before |-hitcount| instead of after.
1078
+ // puts the |-status| message before |-hitcount| instead of after
1045
1079
  if (move.effect == .Twineedle) {
1046
1080
  if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1047
1081
  } else if (move.effect == .Disable) {
@@ -1060,12 +1094,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
1060
1094
  const side = battle.side(player);
1061
1095
 
1062
1096
  // Base speed is used for the critical hit calculation, even when Transform-ed
1063
- var rate = @as(u16, Species.chance(side.stored().species));
1097
+ var rate: u16 = Species.chance(side.stored().species);
1064
1098
  // GLITCH: Focus Energy reduces critical hit rate instead of increasing it
1065
1099
  rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
1066
1100
  rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
1067
1101
 
1068
- return Rolls.criticalHit(battle, player, @intCast(rate), options);
1102
+ const spurious =
1103
+ !showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
1104
+ return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
1069
1105
  }
1070
1106
 
1071
1107
  fn calcDamage(
@@ -1074,6 +1110,7 @@ fn calcDamage(
1074
1110
  target_player: Player,
1075
1111
  m: ?Move.Data,
1076
1112
  crit: bool,
1113
+ options: anytype,
1077
1114
  ) bool {
1078
1115
  // Confusion (indicated when m == null) just needs a 40 BP physical move
1079
1116
  const cfz = m == null;
@@ -1085,28 +1122,8 @@ fn calcDamage(
1085
1122
 
1086
1123
  const special = move.type.special();
1087
1124
 
1088
- // zig fmt: off
1089
- var atk: u32 =
1090
- if (crit)
1091
- if (special) side.stored().stats.spc
1092
- else side.stored().stats.atk
1093
- else
1094
- if (special) side.active.stats.spc
1095
- else side.active.stats.atk;
1096
- var def: u32 =
1097
- if (crit)
1098
- if (special) target.stored().stats.spc
1099
- else target.stored().stats.def
1100
- else
1101
- // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1102
- if (special)
1103
- target.active.stats.spc *
1104
- @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1105
- // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1106
- else
1107
- target.active.stats.def *
1108
- @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1109
- // zig fmt: on
1125
+ var atk = attack(side, crit, special);
1126
+ var def = defense(target, crit, special, cfz);
1110
1127
 
1111
1128
  // Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
1112
1129
  // will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
@@ -1117,9 +1134,9 @@ fn calcDamage(
1117
1134
  def = @max((def / 4) & 255, if (showdown) 1 else 0);
1118
1135
  }
1119
1136
 
1120
- const lvl = @as(u32, side.stored().level * @as(u2, if (crit) 2 else 1));
1137
+ const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
1121
1138
 
1122
- def = @as(u32, if (move.effect == .Explode) @max(def / 2, 1) else def);
1139
+ def = if (move.effect == .Explode) @max(def / 2, 1) else def;
1123
1140
 
1124
1141
  if (def == 0) return false;
1125
1142
 
@@ -1131,11 +1148,41 @@ fn calcDamage(
1131
1148
  d = @min(997, d);
1132
1149
  d += 2;
1133
1150
 
1151
+ // Whether or not a critical hit occurred can change whether a division-by-zero freeze is
1152
+ // encountered so we need to make sure that in the event of a miss we commit the status of the
1153
+ // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1154
+ // that results in counter intuitive probabilities in the common case
1155
+ if (pkmn.options.chance and !showdown) {
1156
+ atk = attack(side, !crit, special);
1157
+ def = defense(target, !crit, special, false);
1158
+ if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
1159
+ }
1160
+
1134
1161
  battle.last_damage = @intCast(d);
1135
1162
 
1136
1163
  return true;
1137
1164
  }
1138
1165
 
1166
+ fn attack(side: *Side, crit: bool, special: bool) u32 {
1167
+ return if (crit)
1168
+ if (special) side.stored().stats.spc else side.stored().stats.atk
1169
+ else if (special) side.active.stats.spc else side.active.stats.atk;
1170
+ }
1171
+
1172
+ fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
1173
+ return if (crit)
1174
+ if (special) target.stored().stats.spc else target.stored().stats.def
1175
+ else
1176
+ // GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
1177
+ if (special)
1178
+ target.active.stats.spc *
1179
+ @as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
1180
+ else
1181
+ // Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
1182
+ target.active.stats.def *
1183
+ @as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
1184
+ }
1185
+
1139
1186
  fn adjustDamage(battle: anytype, player: Player) u16 {
1140
1187
  const side = battle.side(player);
1141
1188
  const foe = battle.foe(player);
@@ -1180,7 +1227,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1180
1227
  const foe = battle.foe(player);
1181
1228
 
1182
1229
  if (!try checkHit(battle, player, move, options)) return null;
1183
- try options.chance.commit(player, .hit);
1184
1230
 
1185
1231
  battle.last_damage = switch (side.last_selected_move) {
1186
1232
  .SuperFang => @max(foe.stored().hp / 2, 1),
@@ -1241,9 +1287,8 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1241
1287
 
1242
1288
  battle.last_damage *|= 2;
1243
1289
 
1244
- // Pokémon Showdown calls checkHit before Counter
1290
+ // Pokémon Showdown calls moveHit before Counter
1245
1291
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
1246
- try options.chance.commit(player, .hit);
1247
1292
 
1248
1293
  const sub = showdown and foe.active.volatiles.Substitute;
1249
1294
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1272,7 +1317,7 @@ fn applyDamage(
1272
1317
  subbed.active.volatiles.Substitute = false;
1273
1318
  // battle.last_damage is not updated with the amount of HP the Substitute had
1274
1319
  try options.log.end(.{ battle.active(sub_player), .Substitute });
1275
- options.calc.capped(target_player);
1320
+ options.calc.capped(sub_player);
1276
1321
  return true;
1277
1322
  } else {
1278
1323
  // Safe to truncate since less than subbed.volatiles.substitute which is a u8
@@ -1306,7 +1351,7 @@ fn mirrorMove(
1306
1351
  try options.log.fail(.{ battle.active(player), .None });
1307
1352
  return null;
1308
1353
  } else if (!showdown or foe.last_used_move != .Struggle) {
1309
- incrementPP(side, mslot);
1354
+ incrementPP(side, player, mslot, options);
1310
1355
  }
1311
1356
 
1312
1357
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
@@ -1327,7 +1372,7 @@ fn metronome(
1327
1372
  var side = battle.side(player);
1328
1373
 
1329
1374
  side.last_selected_move = try Rolls.metronome(battle, player, options);
1330
- incrementPP(side, mslot);
1375
+ incrementPP(side, player, mslot, options);
1331
1376
 
1332
1377
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
1333
1378
  return null;
@@ -1339,7 +1384,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1339
1384
  var immune = false;
1340
1385
  var mist = false;
1341
1386
 
1342
- if (moveHit(battle, player, move, &immune, &mist, options)) return true;
1387
+ if (try moveHit(battle, player, move, &immune, &mist, options)) {
1388
+ if (!showdown) try options.chance.commit(player, .hit);
1389
+ return true;
1390
+ }
1343
1391
 
1344
1392
  assert(!immune);
1345
1393
  if (mist) {
@@ -1348,11 +1396,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
1348
1396
  try options.log.activate(.{ foe_ident, .Mist });
1349
1397
  try options.log.fail(.{ foe_ident, .None });
1350
1398
  } else {
1351
- try options.chance.commit(player, .miss);
1352
1399
  try options.log.lastmiss(.{});
1353
1400
  try options.log.miss(.{battle.active(player)});
1354
1401
  }
1355
1402
 
1403
+ if (!showdown) try options.chance.commit(player, .miss);
1356
1404
  return false;
1357
1405
  }
1358
1406
 
@@ -1363,7 +1411,7 @@ fn moveHit(
1363
1411
  immune: *bool,
1364
1412
  mist: *bool,
1365
1413
  options: anytype,
1366
- ) bool {
1414
+ ) !bool {
1367
1415
  var side = battle.side(player);
1368
1416
  const foe = battle.foe(player);
1369
1417
 
@@ -1402,7 +1450,7 @@ fn moveHit(
1402
1450
  const overwritten = overwrite and state > 0;
1403
1451
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1404
1452
 
1405
- var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1453
+ var accuracy: u16 = if (overwritten) state else move.accuracy;
1406
1454
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1407
1455
  accuracy = accuracy * boost[0] / boost[1];
1408
1456
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1414,7 +1462,7 @@ fn moveHit(
1414
1462
  if (overwrite) side.active.volatiles.state = accuracy;
1415
1463
 
1416
1464
  // GLITCH: max accuracy is 255 so 1/256 chance of miss
1417
- break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
1465
+ break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
1418
1466
  };
1419
1467
 
1420
1468
  // Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
@@ -1425,13 +1473,17 @@ fn moveHit(
1425
1473
 
1426
1474
  if (!miss) return true;
1427
1475
  if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
1476
+
1428
1477
  side.active.volatiles.Binding = false;
1478
+ options.chance.observe(.binding, player, .None);
1479
+
1429
1480
  return false;
1430
1481
  }
1431
1482
 
1432
1483
  fn checkFaint(
1433
1484
  battle: anytype,
1434
1485
  player: Player,
1486
+ cap: bool,
1435
1487
  options: anytype,
1436
1488
  ) @TypeOf(options.log).Error!?Result {
1437
1489
  const side = battle.side(player);
@@ -1444,8 +1496,8 @@ fn checkFaint(
1444
1496
  const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1445
1497
  const more = player_out or foe_out;
1446
1498
 
1447
- if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1448
- if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
1499
+ if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
1500
+ if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
1449
1501
 
1450
1502
  assert(!side.active.volatiles.MultiHit);
1451
1503
  assert(!foe.active.volatiles.MultiHit);
@@ -1466,7 +1518,7 @@ fn checkFaint(
1466
1518
  return .{ .p1 = foe_choice, .p2 = .Switch };
1467
1519
  }
1468
1520
 
1469
- fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
1521
+ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
1470
1522
  var side = battle.side(player);
1471
1523
  var foe = battle.foe(player);
1472
1524
  assert(side.stored().hp == 0);
@@ -1500,7 +1552,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
1500
1552
  }
1501
1553
 
1502
1554
  try options.log.faint(.{ battle.active(player), done });
1503
- options.calc.capped(player);
1555
+ // We can only safely set capped if the faint occured as the direct result of an opponent's
1556
+ // move (as opposed to due to residual damage, recoil, Explosion, etc)
1557
+ if (cap) options.calc.capped(player);
1504
1558
  return null;
1505
1559
  }
1506
1560
 
@@ -1660,17 +1714,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
1660
1714
  }
1661
1715
  }
1662
1716
 
1663
- fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
1664
- var volatiles = &active.volatiles;
1665
- assert(volatiles.Thrashing);
1666
- if (volatiles.attacks > 0) return false;
1667
-
1668
- volatiles.Thrashing = false;
1669
- volatiles.Confusion = true;
1670
- volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
1671
- return true;
1672
- }
1673
-
1674
1717
  fn onBegin(
1675
1718
  battle: anytype,
1676
1719
  player: Player,
@@ -1679,7 +1722,6 @@ fn onBegin(
1679
1722
  residual: *bool,
1680
1723
  options: anytype,
1681
1724
  ) !void {
1682
- assert(move.effect.onBegin());
1683
1725
  return switch (move.effect) {
1684
1726
  .Confusion => Effects.confusion(battle, player, move, options),
1685
1727
  .Conversion => Effects.conversion(battle, player, options),
@@ -1702,7 +1744,6 @@ fn onBegin(
1702
1744
  }
1703
1745
 
1704
1746
  fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
1705
- assert(move.effect.onEnd());
1706
1747
  return switch (move.effect) {
1707
1748
  .Bide => Effects.bide(battle, player, options),
1708
1749
  // zig fmt: off
@@ -1724,7 +1765,6 @@ fn alwaysHappens(
1724
1765
  residual: *bool,
1725
1766
  options: anytype,
1726
1767
  ) !void {
1727
- assert(move.effect.alwaysHappens());
1728
1768
  return switch (move.effect) {
1729
1769
  .DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
1730
1770
  .Explode => Effects.explode(battle, player),
@@ -1760,8 +1800,10 @@ pub const Effects = struct {
1760
1800
 
1761
1801
  side.active.volatiles.Bide = true;
1762
1802
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1803
+
1763
1804
  side.active.volatiles.state = 0;
1764
1805
  side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
1806
+ options.chance.observe(.attacking, player, .started);
1765
1807
 
1766
1808
  try options.log.start(.{ battle.active(player), .Bide });
1767
1809
  }
@@ -1823,12 +1865,13 @@ pub const Effects = struct {
1823
1865
  if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
1824
1866
  if (!try checkHit(battle, player, move, options)) return;
1825
1867
  }
1826
- try options.chance.commit(player, .hit);
1827
1868
  }
1828
1869
 
1829
1870
  if (foe.active.volatiles.Confusion) return;
1830
1871
  foe.active.volatiles.Confusion = true;
1831
- foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
1872
+
1873
+ foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
1874
+ options.chance.observe(.confusion, player.foe(), .started);
1832
1875
 
1833
1876
  try options.log.start(.{ battle.active(player.foe()), .Confusion });
1834
1877
  }
@@ -1856,10 +1899,7 @@ pub const Effects = struct {
1856
1899
  const foe_ident = battle.active(player.foe());
1857
1900
 
1858
1901
  // Pokémon Showdown handles hit/miss earlier in doMove
1859
- if (!showdown) {
1860
- if (!try checkHit(battle, player, move, options)) return null;
1861
- try options.chance.commit(player, .hit);
1862
- }
1902
+ if (!showdown and !try checkHit(battle, player, move, options)) return null;
1863
1903
 
1864
1904
  if (volatiles.disable_move != 0) {
1865
1905
  try options.log.fail(.{ foe_ident, .None });
@@ -1889,6 +1929,7 @@ pub const Effects = struct {
1889
1929
  volatiles.disable_move =
1890
1930
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1891
1931
  volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1932
+ options.chance.observe(.disable, player.foe(), .started);
1892
1933
 
1893
1934
  const id = foe.active.move(volatiles.disable_move).id;
1894
1935
  try options.log.start(.{ foe_ident, .Disable, id });
@@ -1972,7 +2013,7 @@ pub const Effects = struct {
1972
2013
  var side = battle.side(player);
1973
2014
  var foe = battle.foe(player);
1974
2015
 
1975
- var side_stored = side.stored();
2016
+ const side_stored = side.stored();
1976
2017
  var foe_stored = foe.stored();
1977
2018
 
1978
2019
  const player_ident = battle.active(player);
@@ -1995,22 +2036,24 @@ pub const Effects = struct {
1995
2036
  if (p != player and Status.any(s.stored().status)) {
1996
2037
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
1997
2038
  s.stored().status = 0;
2039
+ options.chance.observe(.sleep, p, .None);
1998
2040
  } else if (showdown and s.stored().status == Status.TOX) {
1999
2041
  s.stored().status = Status.init(.PSN);
2000
2042
  try log.status(.{ battle.active(p), s.stored().status, .None });
2001
2043
  }
2002
- try clearVolatiles(battle, p, p != player, options);
2044
+ try clearVolatiles(battle, p, options);
2003
2045
  }
2004
2046
  } else {
2005
2047
  if (Status.any(foe_stored.status)) {
2006
2048
  if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
2007
2049
  foe.last_selected_move = .SKIP_TURN;
2050
+ options.chance.observe(.sleep, player.foe(), .None);
2008
2051
  }
2009
2052
  try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
2010
2053
  foe_stored.status = 0;
2011
2054
  }
2012
- try clearVolatiles(battle, player, false, options);
2013
- try clearVolatiles(battle, player.foe(), true, options);
2055
+ try clearVolatiles(battle, player, options);
2056
+ try clearVolatiles(battle, player.foe(), options);
2014
2057
  }
2015
2058
  }
2016
2059
 
@@ -2061,7 +2104,6 @@ pub const Effects = struct {
2061
2104
  }
2062
2105
  }
2063
2106
 
2064
- try options.chance.commit(player, .hit);
2065
2107
  foe.active.volatiles.LeechSeed = true;
2066
2108
 
2067
2109
  try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
@@ -2102,12 +2144,10 @@ pub const Effects = struct {
2102
2144
  if (!has_mimic) {
2103
2145
  // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2104
2146
  if (!try checkHit(battle, player, move, options)) return;
2105
- try options.chance.commit(player, .hit);
2106
2147
  return options.log.fail(.{ battle.active(player.foe()), .None });
2107
2148
  }
2108
2149
  }
2109
2150
  if (!try checkHit(battle, player, move, options)) return;
2110
- try options.chance.commit(player, .hit);
2111
2151
 
2112
2152
  const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
2113
2153
  side.active.move(oslot).id = foe.active.move(rslot).id;
@@ -2161,7 +2201,6 @@ pub const Effects = struct {
2161
2201
  if (immune) return log.immune(.{ foe_ident, .None });
2162
2202
  if (!try checkHit(battle, player, move, options)) return;
2163
2203
  }
2164
- try options.chance.commit(player, .hit);
2165
2204
 
2166
2205
  foe_stored.status = Status.init(.PAR);
2167
2206
  foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
@@ -2229,7 +2268,6 @@ pub const Effects = struct {
2229
2268
 
2230
2269
  if (move.effect == .Poison) {
2231
2270
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2232
- try options.chance.commit(player, .hit);
2233
2271
  } else {
2234
2272
  if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
2235
2273
  return;
@@ -2298,9 +2336,8 @@ pub const Effects = struct {
2298
2336
  if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
2299
2337
  });
2300
2338
  }
2301
- // If checkHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2339
+ // If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
2302
2340
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2303
- try options.chance.commit(player, .hit);
2304
2341
  }
2305
2342
 
2306
2343
  // Sleep Clause Mod
@@ -2312,6 +2349,8 @@ pub const Effects = struct {
2312
2349
  foe.active.volatiles.Recharging = false;
2313
2350
 
2314
2351
  foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
2352
+ options.chance.observe(.sleep, player.foe(), .started);
2353
+
2315
2354
  const last = battle.side(player).last_selected_move;
2316
2355
  try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
2317
2356
  }
@@ -2353,9 +2392,7 @@ pub const Effects = struct {
2353
2392
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2354
2393
  if (showdown) {
2355
2394
  if (battle.side(player).last_selected_move == .Teleport) return;
2356
- if (try checkHit(battle, player, move, options)) {
2357
- try options.chance.commit(player, .hit);
2358
- }
2395
+ _ = try checkHit(battle, player, move, options);
2359
2396
  battle.last_damage = 0;
2360
2397
  } else {
2361
2398
  try options.log.fail(.{ battle.active(player), .None });
@@ -2370,6 +2407,8 @@ pub const Effects = struct {
2370
2407
 
2371
2408
  volatiles.Thrashing = true;
2372
2409
  volatiles.attacks = Rolls.attackingDuration(battle, player, options);
2410
+
2411
+ options.chance.observe(.attacking, player, .started);
2373
2412
  }
2374
2413
 
2375
2414
  fn transform(battle: anytype, player: Player, options: anytype) !void {
@@ -2415,6 +2454,7 @@ pub const Effects = struct {
2415
2454
 
2416
2455
  side.active.volatiles.attacks =
2417
2456
  try Rolls.distribution(battle, .Binding, player, options) - 1;
2457
+ options.chance.observe(.binding, player, .started);
2418
2458
  }
2419
2459
 
2420
2460
  fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
@@ -2441,7 +2481,7 @@ pub const Effects = struct {
2441
2481
  }
2442
2482
  return log.fail(.{ ident, .None });
2443
2483
  }
2444
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2484
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2445
2485
  const stat = unmodifiedStats(battle, side).atk;
2446
2486
  stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2447
2487
  try log.boost(.{ ident, reason, n });
@@ -2461,7 +2501,7 @@ pub const Effects = struct {
2461
2501
  }
2462
2502
  return log.fail(.{ ident, .None });
2463
2503
  }
2464
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2504
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2465
2505
  const stat = unmodifiedStats(battle, side).def;
2466
2506
  stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2467
2507
  try log.boost(.{ ident, .Defense, n });
@@ -2478,7 +2518,7 @@ pub const Effects = struct {
2478
2518
  }
2479
2519
  return log.fail(.{ ident, .None });
2480
2520
  }
2481
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2521
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2482
2522
  const stat = unmodifiedStats(battle, side).spe;
2483
2523
  stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2484
2524
  try log.boost(.{ ident, .Speed, 2 });
@@ -2498,7 +2538,7 @@ pub const Effects = struct {
2498
2538
  }
2499
2539
  return log.fail(.{ ident, .None });
2500
2540
  }
2501
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2541
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2502
2542
  const stat = unmodifiedStats(battle, side).spc;
2503
2543
  stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
2504
2544
  try log.boost(.{ ident, .SpecialAttack, n });
@@ -2531,7 +2571,6 @@ pub const Effects = struct {
2531
2571
  } else {
2532
2572
  // checkHit already checks for Invulnerable
2533
2573
  if (!showdown and !try checkHit(battle, player, move, options)) return;
2534
- try options.chance.commit(player, .hit);
2535
2574
  }
2536
2575
 
2537
2576
  var stats = &foe.active.stats;
@@ -2551,7 +2590,7 @@ pub const Effects = struct {
2551
2590
  }
2552
2591
  return log.fail(.{ foe_ident, .None });
2553
2592
  }
2554
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2593
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
2555
2594
  const stat = unmodifiedStats(battle, foe).atk;
2556
2595
  stats.atk = @max(1, stat * mod[0] / mod[1]);
2557
2596
  try log.boost(.{ foe_ident, .Attack, -1 });
@@ -2569,7 +2608,7 @@ pub const Effects = struct {
2569
2608
  }
2570
2609
  return log.fail(.{ foe_ident, .None });
2571
2610
  }
2572
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2611
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
2573
2612
  const stat = unmodifiedStats(battle, foe).def;
2574
2613
  stats.def = @max(1, stat * mod[0] / mod[1]);
2575
2614
  try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
@@ -2586,7 +2625,7 @@ pub const Effects = struct {
2586
2625
  }
2587
2626
  return log.fail(.{ foe_ident, .None });
2588
2627
  }
2589
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2628
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
2590
2629
  const stat = unmodifiedStats(battle, foe).spe;
2591
2630
  stats.spe = @max(1, stat * mod[0] / mod[1]);
2592
2631
  try log.boost(.{ foe_ident, .Speed, -1 });
@@ -2606,7 +2645,7 @@ pub const Effects = struct {
2606
2645
  }
2607
2646
  return log.fail(.{ foe_ident, .None });
2608
2647
  }
2609
- var mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2648
+ const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
2610
2649
  const stat = unmodifiedStats(battle, foe).spc;
2611
2650
  stats.spc = @max(1, stat * mod[0] / mod[1]);
2612
2651
  try log.boost(.{ foe_ident, .SpecialAttack, -1 });
@@ -2645,22 +2684,22 @@ fn isForced(active: anytype) bool {
2645
2684
  active.volatiles.Thrashing or active.volatiles.Charging;
2646
2685
  }
2647
2686
 
2648
- fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !void {
2687
+ fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
2649
2688
  var log = options.log;
2650
2689
  var side = battle.side(who);
2651
2690
  var volatiles = &side.active.volatiles;
2652
2691
  const ident = battle.active(who);
2653
2692
 
2654
- options.chance.clearDurations(who, clear);
2655
-
2656
2693
  if (volatiles.disable_move != 0) {
2657
2694
  volatiles.disable_move = 0;
2658
2695
  volatiles.disable_duration = 0;
2696
+ options.chance.observe(.disable, who, .None);
2659
2697
  try log.end(.{ ident, .DisableSilent });
2660
2698
  }
2661
2699
  if (volatiles.Confusion) {
2662
2700
  // volatiles.confusion is left unchanged
2663
2701
  volatiles.Confusion = false;
2702
+ options.chance.observe(.confusion, who, .None);
2664
2703
  try log.end(.{ ident, .ConfusionSilent });
2665
2704
  }
2666
2705
  if (volatiles.Mist) {
@@ -2695,10 +2734,25 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2695
2734
  }
2696
2735
  }
2697
2736
 
2737
+ fn decrement(
2738
+ comptime field: chance.Action.Field,
2739
+ player: Player,
2740
+ options: anytype,
2741
+ n: anytype,
2742
+ ) @TypeOf(n) {
2743
+ return if (options.calc.overridden(player, field)) |obs| switch (obs) {
2744
+ .started, .ended => 0,
2745
+ .continuing => if (n > 1) n - 1 else n,
2746
+ else => unreachable,
2747
+ } else n - 1;
2748
+ }
2749
+
2698
2750
  pub const Rolls = struct {
2699
2751
  fn speedTie(battle: anytype, options: anytype) !bool {
2700
2752
  const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
2701
2753
  player == .P1
2754
+ else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
2755
+ battle.rng.speedTie()
2702
2756
  else if (showdown)
2703
2757
  battle.rng.range(u8, 0, 2) == 0
2704
2758
  else
@@ -2708,21 +2762,31 @@ pub const Rolls = struct {
2708
2762
  return p1;
2709
2763
  }
2710
2764
 
2711
- fn criticalHit(battle: anytype, player: Player, rate: u8, options: anytype) !bool {
2765
+ fn criticalHit(
2766
+ battle: anytype,
2767
+ player: Player,
2768
+ rate: u8,
2769
+ spurious: bool,
2770
+ options: anytype,
2771
+ ) !bool {
2712
2772
  const crit = if (options.calc.overridden(player, .critical_hit)) |val|
2713
2773
  val == .true
2774
+ else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
2775
+ battle.rng.criticalHit(player, rate)
2714
2776
  else if (showdown)
2715
2777
  battle.rng.chance(u8, rate, 256)
2716
2778
  else
2717
2779
  std.math.rotl(u8, battle.rng.next(), 3) < rate;
2718
2780
 
2719
- try options.chance.criticalHit(player, crit, rate);
2781
+ if (!spurious) try options.chance.criticalHit(player, crit, rate);
2720
2782
  return crit;
2721
2783
  }
2722
2784
 
2723
2785
  fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2724
2786
  const roll = if (options.calc.overridden(player, .damage)) |val|
2725
2787
  val
2788
+ else if (@hasDecl(@TypeOf(battle.rng), "damage"))
2789
+ battle.rng.damage(player)
2726
2790
  else roll: {
2727
2791
  if (showdown) break :roll battle.rng.range(u8, 217, 256);
2728
2792
  while (true) {
@@ -2736,21 +2800,27 @@ pub const Rolls = struct {
2736
2800
  return roll;
2737
2801
  }
2738
2802
 
2739
- fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) bool {
2803
+ const uN = if (pkmn.options.miss) u8 else u9;
2804
+ fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
2805
+ const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
2740
2806
  const ok = if (options.calc.overridden(player, .hit)) |val|
2741
2807
  val == .true
2808
+ else if (@hasDecl(@TypeOf(battle.rng), "hit"))
2809
+ battle.rng.hit(player, acc)
2742
2810
  else if (showdown)
2743
- battle.rng.chance(u8, accuracy, 256)
2811
+ battle.rng.chance(uN, acc, 256)
2744
2812
  else
2745
- battle.rng.next() < accuracy;
2813
+ battle.rng.next() < acc;
2746
2814
 
2747
- options.chance.hit(ok, accuracy);
2815
+ try options.chance.hit(player, ok, acc);
2748
2816
  return ok;
2749
2817
  }
2750
2818
 
2751
2819
  fn confused(battle: anytype, player: Player, options: anytype) !bool {
2752
2820
  const cfz = if (options.calc.overridden(player, .confused)) |val|
2753
2821
  val == .true
2822
+ else if (@hasDecl(@TypeOf(battle.rng), "confused"))
2823
+ battle.rng.confused(player)
2754
2824
  else if (showdown)
2755
2825
  !battle.rng.chance(u8, 128, 256)
2756
2826
  else
@@ -2763,6 +2833,8 @@ pub const Rolls = struct {
2763
2833
  fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
2764
2834
  const par = if (options.calc.overridden(player, .paralyzed)) |val|
2765
2835
  val == .true
2836
+ else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
2837
+ battle.rng.paralyzed(player)
2766
2838
  else if (showdown)
2767
2839
  battle.rng.chance(u8, 63, 256)
2768
2840
  else
@@ -2775,6 +2847,8 @@ pub const Rolls = struct {
2775
2847
  fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
2776
2848
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2777
2849
  val == .true
2850
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2851
+ battle.rng.secondaryChance(player, 25)
2778
2852
  else if (showdown)
2779
2853
  battle.rng.chance(u8, 25, 256)
2780
2854
  else
@@ -2789,6 +2863,8 @@ pub const Rolls = struct {
2789
2863
 
2790
2864
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2791
2865
  val == .true
2866
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2867
+ battle.rng.secondaryChance(player, rate)
2792
2868
  else if (showdown)
2793
2869
  battle.rng.chance(u8, rate, 256)
2794
2870
  else
@@ -2803,6 +2879,8 @@ pub const Rolls = struct {
2803
2879
 
2804
2880
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2805
2881
  val == .true
2882
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2883
+ battle.rng.secondaryChance(player, rate)
2806
2884
  else if (showdown)
2807
2885
  battle.rng.chance(u8, rate, 256)
2808
2886
  else
@@ -2815,6 +2893,8 @@ pub const Rolls = struct {
2815
2893
  fn unboost(battle: anytype, player: Player, options: anytype) !bool {
2816
2894
  const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
2817
2895
  val == .true
2896
+ else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
2897
+ battle.rng.secondaryChance(player, 85)
2818
2898
  else if (showdown)
2819
2899
  battle.rng.chance(u8, 85, 256)
2820
2900
  else
@@ -2827,6 +2907,8 @@ pub const Rolls = struct {
2827
2907
  fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2828
2908
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2829
2909
  val
2910
+ else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
2911
+ battle.rng.metronome(player)
2830
2912
  else if (showdown)
2831
2913
  Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2832
2914
  else move: {
@@ -2845,6 +2927,8 @@ pub const Rolls = struct {
2845
2927
  fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
2846
2928
  const power = if (options.calc.overridden(player, .psywave)) |val|
2847
2929
  val - 1
2930
+ else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
2931
+ battle.rng.psywave(player)
2848
2932
  else if (showdown)
2849
2933
  battle.rng.range(u8, 0, max)
2850
2934
  else power: {
@@ -2862,6 +2946,8 @@ pub const Rolls = struct {
2862
2946
  fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
2863
2947
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2864
2948
  @intCast(val)
2949
+ else if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
2950
+ battle.rng.sleepDuration(player)
2865
2951
  else if (showdown)
2866
2952
  battle.rng.range(u3, 1, 8)
2867
2953
  else duration: {
@@ -2872,46 +2958,52 @@ pub const Rolls = struct {
2872
2958
  };
2873
2959
 
2874
2960
  assert(duration >= 1 and duration <= 7);
2875
- options.chance.duration(.sleep, player, player.foe(), duration);
2961
+ options.chance.duration(player, duration);
2876
2962
  return duration;
2877
2963
  }
2878
2964
 
2879
2965
  fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
2880
2966
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2881
2967
  val
2968
+ else if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
2969
+ battle.rng.disableDuration(player)
2882
2970
  else if (showdown)
2883
2971
  battle.rng.range(u4, 1, 9)
2884
2972
  else
2885
2973
  @intCast((battle.rng.next() & 7) + 1);
2886
2974
 
2887
2975
  assert(duration >= 1 and duration <= 8);
2888
- options.chance.duration(.disable, player, player.foe(), duration);
2976
+ options.chance.duration(player, duration);
2889
2977
  return duration;
2890
2978
  }
2891
2979
 
2892
- fn confusionDuration(battle: anytype, player: Player, self: bool, options: anytype) u3 {
2980
+ fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
2893
2981
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2894
2982
  @intCast(val)
2983
+ else if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
2984
+ battle.rng.confusionDuration(player)
2895
2985
  else if (showdown)
2896
2986
  battle.rng.range(u3, 2, 6)
2897
2987
  else
2898
2988
  @intCast((battle.rng.next() & 3) + 2);
2899
2989
 
2900
2990
  assert(duration >= 2 and duration <= 5);
2901
- options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2991
+ options.chance.duration(player, duration);
2902
2992
  return duration;
2903
2993
  }
2904
2994
 
2905
2995
  fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
2906
2996
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2907
2997
  @intCast(val)
2998
+ else if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
2999
+ battle.rng.attackingDuration(player)
2908
3000
  else if (showdown)
2909
3001
  battle.rng.range(u3, 2, 4)
2910
3002
  else
2911
3003
  @intCast((battle.rng.next() & 1) + 2);
2912
3004
 
2913
3005
  assert(duration >= 2 and duration <= 3);
2914
- options.chance.duration(.attacking, player, player, duration);
3006
+ options.chance.duration(player, duration);
2915
3007
  return duration;
2916
3008
  }
2917
3009
 
@@ -2926,6 +3018,8 @@ pub const Rolls = struct {
2926
3018
  const roll = if (effect == .Binding) .duration else .multi_hit;
2927
3019
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2928
3020
  @intCast(val)
3021
+ else if (@hasDecl(@TypeOf(battle.rng), "distribution"))
3022
+ battle.rng.distribution(player)
2929
3023
  else if (showdown)
2930
3024
  DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2931
3025
  else n: {
@@ -2935,7 +3029,7 @@ pub const Rolls = struct {
2935
3029
 
2936
3030
  assert(n >= 2 and n <= 5);
2937
3031
  if (effect == .Binding) {
2938
- options.chance.duration(.binding, player, player, n);
3032
+ options.chance.duration(player, n);
2939
3033
  } else {
2940
3034
  try options.chance.multiHit(player, n);
2941
3035
  }
@@ -2958,7 +3052,9 @@ pub const Rolls = struct {
2958
3052
  else
2959
3053
  null;
2960
3054
 
2961
- const slot: u4 = overridden orelse slot: {
3055
+ const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
3056
+ battle.rng.moveSlot(player, moves, check_pp)
3057
+ else slot: {
2962
3058
  if (showdown) {
2963
3059
  if (check_pp == 0) {
2964
3060
  var i: usize = moves.len;
@@ -3038,11 +3134,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3038
3134
  const side = battle.side(player);
3039
3135
  const foe = battle.foe(player);
3040
3136
 
3041
- var active = &side.active;
3137
+ const active = &side.active;
3042
3138
  const stored = side.stored();
3043
3139
 
3044
3140
  // While players are not given any input options on the cartridge in these cases,
3045
- // Pokémon Showdown instead produces a list with a single move that must be chosen.
3141
+ // Pokémon Showdown instead produces a list with a single move that must be chosen
3046
3142
  //
3047
3143
  // Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
3048
3144
  // here when not in compatibility mode, but the engine needs to be able to differentiate
@@ -3050,7 +3146,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3050
3146
  // passing due to being forced into using a move. Instead of introducing another option
3051
3147
  // we simply repurpose Move with no move slot, even though pedantically this is not
3052
3148
  // strictly correct as the player would not have been presented the option to move or
3053
- // switch at all.
3149
+ // switch at all
3054
3150
  if (isForced(active)) {
3055
3151
  out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
3056
3152
  n += 1;
@@ -3069,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3069
3165
  // On the cartridge, all of these happen after "FIGHT" (indicating you are not
3070
3166
  // switching) but before you are allowed to select a move. Pokémon Showdown instead
3071
3167
  // either disables all other moves in the case of limited or requires you to select a
3072
- // move normally if sleeping/frozen/bound.
3168
+ // move normally if sleeping/frozen/bound
3073
3169
  if (!showdown and (limited or foe.active.volatiles.Binding or
3074
3170
  Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
3075
3171
  {
@@ -3082,7 +3178,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3082
3178
  // Pokémon Showdown handles Bide and Binding moves by checking if the move in question
3083
3179
  // is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
3084
3180
  // Move will not result in forcing the subsequent use unless the user also had the
3085
- // proc-ed move in their moveset) and disabling all other moves.
3181
+ // proc-ed move in their moveset) and disabling all other moves
3086
3182
  if (limited) {
3087
3183
  assert(showdown);
3088
3184
  assert(side.last_selected_move != .None);