@pkmn/engine 0.1.0-dev.a324c50f → 0.1.0-dev.a93847b2

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 (41) hide show
  1. package/README.md +45 -37
  2. package/build/pkg/data/ids.json +12 -12
  3. package/build/pkg/data/layout.json +2 -2
  4. package/build/pkg/data/protocol.json +69 -12
  5. package/build/pkg/data.d.ts +2 -2
  6. package/build/pkg/gen1.js +2 -2
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/protocol.js +56 -37
  9. package/build/pkg/protocol.js.map +1 -1
  10. package/build.zig +53 -44
  11. package/package.json +40 -19
  12. package/src/bin/install-pkmn-engine +2 -2
  13. package/src/lib/binding/node.zig +1 -1
  14. package/src/lib/common/battle.zig +17 -0
  15. package/src/lib/common/data.zig +4 -16
  16. package/src/lib/common/js.zig +118 -0
  17. package/src/lib/common/optional.zig +67 -0
  18. package/src/lib/common/protocol.zig +561 -80
  19. package/src/lib/common/rational.zig +353 -0
  20. package/src/lib/common/rng.zig +6 -6
  21. package/src/lib/common/util.zig +12 -0
  22. package/src/lib/gen1/README.md +273 -193
  23. package/src/lib/gen1/calc.zig +426 -0
  24. package/src/lib/gen1/chance.zig +1060 -0
  25. package/src/lib/gen1/data/moves.zig +183 -171
  26. package/src/lib/gen1/data/species.zig +1 -1
  27. package/src/lib/gen1/data/types.zig +3 -3
  28. package/src/lib/gen1/data.zig +31 -38
  29. package/src/lib/gen1/helpers.zig +241 -103
  30. package/src/lib/gen1/mechanics.zig +111 -166
  31. package/src/lib/gen2/README.md +106 -85
  32. package/src/lib/gen2/calc.zig +143 -0
  33. package/src/lib/gen2/chance.zig +1314 -0
  34. package/src/lib/gen2/data/items.zig +11 -17
  35. package/src/lib/gen2/data/moves.zig +662 -568
  36. package/src/lib/gen2/data/species.zig +1 -1
  37. package/src/lib/gen2/data/types.zig +129 -26
  38. package/src/lib/gen2/data.zig +89 -64
  39. package/src/lib/gen2/generated.zig +857 -0
  40. package/src/lib/gen2/helpers.zig +8 -12
  41. package/src/lib/gen2/mechanics.zig +2994 -9
@@ -7,21 +7,19 @@ const DEBUG = @import("../common/debug.zig").print;
7
7
  const protocol = @import("../common/protocol.zig");
8
8
  const rng = @import("../common/rng.zig");
9
9
 
10
- const Action = @import("chance.zig").Action;
11
-
12
10
  const data = @import("data.zig");
13
11
 
14
12
  const assert = std.debug.assert;
15
13
 
16
14
  const expectEqual = std.testing.expectEqual;
17
15
 
16
+ const showdown = pkmn.options.showdown;
17
+
18
18
  const Choice = common.Choice;
19
19
  const ID = common.ID;
20
20
  const Player = common.Player;
21
21
  const Result = common.Result;
22
22
 
23
- const showdown = pkmn.options.showdown;
24
-
25
23
  const Boost = protocol.Boost;
26
24
  const Damage = protocol.Damage;
27
25
  const Heal = protocol.Heal;
@@ -59,23 +57,15 @@ const BOOSTS = &[_][2]u8{
59
57
 
60
58
  const MAX_STAT_VALUE = 999;
61
59
 
62
- const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
63
-
64
60
  pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result {
65
- // FIXME
66
- // std.debug.print("\n", .{});
67
- // DEBUG(battle.turn);
68
- // std.debug.print("// ", .{});
69
61
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
70
62
  if (battle.turn == 0) return start(battle, options);
71
63
 
72
64
  var s1 = false;
73
65
  var s2 = false;
74
- var f1: ?Move = null;
75
- var f2: ?Move = null;
76
66
 
77
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
78
- if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
67
+ if (selectMove(battle, .P1, c1, c2, &s1)) |r| return r;
68
+ if (selectMove(battle, .P2, c2, c1, &s2)) |r| return r;
79
69
 
80
70
  var p1 = battle.side(.P1);
81
71
  var p2 = battle.side(.P2);
@@ -84,9 +74,9 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
84
74
  var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
85
75
 
86
76
  if (try turnOrder(battle, c1, c2, options) == .P1) {
87
- if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, options)) |r| return r;
77
+ if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
88
78
  } else {
89
- if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, options)) |r| return r;
79
+ if (try doTurn(battle, .P2, c2, r2, s2, .P1, c1, r1, s1, options)) |r| return r;
90
80
  }
91
81
 
92
82
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -124,7 +114,6 @@ fn selectMove(
124
114
  choice: Choice,
125
115
  foe_choice: Choice,
126
116
  skip_turn: *bool,
127
- from: *?Move,
128
117
  ) ?Result {
129
118
  if (choice.type == .Pass) return null;
130
119
 
@@ -141,7 +130,6 @@ fn selectMove(
141
130
  return null;
142
131
  }
143
132
  if (volatiles.Rage) {
144
- from.* = side.last_used_move;
145
133
  if (showdown) {
146
134
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
135
  saveMove(battle, player, null);
@@ -151,7 +139,6 @@ fn selectMove(
151
139
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
152
140
  if (!showdown) volatiles.Flinch = false;
153
141
  if (volatiles.Thrashing or volatiles.Charging) {
154
- from.* = side.last_selected_move;
155
142
  if (showdown) {
156
143
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
157
144
  saveMove(battle, player, null);
@@ -172,9 +159,7 @@ fn selectMove(
172
159
  return null;
173
160
  }
174
161
  if (volatiles.Binding) {
175
- from.* = side.last_used_move;
176
162
  if (showdown) {
177
- if (foe_choice.type == .Switch) from.* = null;
178
163
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
179
164
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
180
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
@@ -186,10 +171,7 @@ fn selectMove(
186
171
  assert(choice.data == 0);
187
172
  // GLITCH: https://glitchcity.wiki/Partial_trapping_move_Mirror_Move_link_battle_glitch
188
173
  if (foe_choice.type == .Switch) {
189
- const last = side.active.move(if (player == .P1)
190
- battle.last_moves.p1_index
191
- else
192
- battle.last_moves.p2_index);
174
+ const last = side.active.move(battle.lastMove(player).index);
193
175
  if (last.id == .Metronome) side.last_selected_move = last.id;
194
176
  if (last.id == .MirrorMove) return Result.Error;
195
177
  }
@@ -213,16 +195,13 @@ fn selectMove(
213
195
  if (choice.data == 0) {
214
196
  const struggle = ok: {
215
197
  for (side.active.moves, 0..) |move, i| {
216
- if (move.pp > 0 and volatiles.disabled_move != i + 1) break :ok false;
198
+ if (move.pp > 0 and volatiles.disable_move != i + 1) break :ok false;
217
199
  }
218
200
  break :ok true;
219
201
  };
220
-
221
202
  assert(struggle);
222
- saveMove(battle, player, choice);
223
- } else {
224
- saveMove(battle, player, choice);
225
203
  }
204
+ saveMove(battle, player, choice);
226
205
 
227
206
  return null;
228
207
  }
@@ -235,18 +214,14 @@ fn saveMove(battle: anytype, player: Player, choice: ?Choice) void {
235
214
  if (c.data == 0) {
236
215
  side.last_selected_move = .Struggle;
237
216
  } else {
238
- assert(showdown or side.active.volatiles.disabled_move != c.data);
217
+ assert(showdown or side.active.volatiles.disable_move != c.data);
239
218
  const move = side.active.move(c.data);
240
219
  // You cannot *select* a move with 0 PP (except on Pokémon Showdown where that is
241
220
  // sometimes required...), but a 0 PP move can be used automatically
242
221
  assert(showdown or move.pp != 0);
243
222
 
244
223
  side.last_selected_move = move.id;
245
- if (player == .P1) {
246
- battle.last_moves.p1_index = @intCast(c.data);
247
- } else {
248
- battle.last_moves.p2_index = @intCast(c.data);
249
- }
224
+ battle.lastMove(player).index = @intCast(c.data);
250
225
  }
251
226
  }
252
227
  }
@@ -264,11 +239,7 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
264
239
  side.order[0] = side.order[slot - 1];
265
240
  side.order[slot - 1] = out;
266
241
 
267
- if (player == .P1) {
268
- battle.last_moves.p1_index = 1;
269
- } else {
270
- battle.last_moves.p2_index = 1;
271
- }
242
+ battle.lastMove(player).index = 1;
272
243
 
273
244
  side.last_used_move = .None;
274
245
  foe.last_used_move = .None;
@@ -354,12 +325,10 @@ fn doTurn(
354
325
  player_choice: Choice,
355
326
  player_rewrap: bool,
356
327
  player_skip: bool,
357
- player_from: ?Move,
358
328
  foe_player: Player,
359
329
  foe_choice: Choice,
360
330
  foe_rewrap: bool,
361
331
  foe_skip: bool,
362
- foe_from: ?Move,
363
332
  options: anytype,
364
333
  ) !?Result {
365
334
  assert(player_choice.type != .Pass);
@@ -372,7 +341,6 @@ fn doTurn(
372
341
  player_choice,
373
342
  player_rewrap,
374
343
  player_skip,
375
- player_from,
376
344
  &residual,
377
345
  options,
378
346
  )) |r| return r;
@@ -395,7 +363,6 @@ fn doTurn(
395
363
  foe_choice,
396
364
  foe_rewrap,
397
365
  foe_skip,
398
- foe_from,
399
366
  &residual,
400
367
  options,
401
368
  )) |r| return r;
@@ -423,7 +390,6 @@ fn executeMove(
423
390
  choice: Choice,
424
391
  rewrap: bool,
425
392
  skip: bool,
426
- from: ?Move,
427
393
  residual: *bool,
428
394
  options: anytype,
429
395
  ) !?Result {
@@ -455,14 +421,13 @@ fn executeMove(
455
421
  if (mslot == 0 and side.last_selected_move != .None and side.last_selected_move != .Struggle) {
456
422
  // choice.data == 0 only happens with Struggle on Pokémon Showdown
457
423
  assert(!showdown);
458
- mslot = @intCast(if (player == .P1)
459
- battle.last_moves.p1_index
460
- else
461
- battle.last_moves.p2_index);
424
+ mslot = @intCast(battle.lastMove(player).index);
462
425
  const stored = side.stored();
463
426
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
464
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
465
- from != null or Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
427
+ auto = isForced(&side.active) or
428
+ side.active.volatiles.Binding or side.active.volatiles.Bide or
429
+ side.last_selected_move == .HyperBeam or
430
+ Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
466
431
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
467
432
  // thawed, in which case we will desync unless the last_selected_move happened to be at
468
433
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -479,15 +444,12 @@ fn executeMove(
479
444
  } else if (showdown and side.active.volatiles.Charging) {
480
445
  // Incorrect mslot with Pokémon Showdown choice semantics so we need to recover from index
481
446
  assert(mslot == 1);
482
- mslot = @intCast(if (player == .P1)
483
- battle.last_moves.p1_index
484
- else
485
- battle.last_moves.p2_index);
447
+ mslot = @intCast(battle.lastMove(player).index);
486
448
  }
487
449
 
488
450
  var skip_can = false;
489
451
  var skip_pp = false;
490
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
452
+ switch (try beforeMove(battle, player, skip, residual, options)) {
491
453
  .done => return null,
492
454
  .skip_can => skip_can = true,
493
455
  .skip_pp => skip_pp = true,
@@ -495,9 +457,9 @@ fn executeMove(
495
457
  .err => return @as(?Result, Result.Error),
496
458
  }
497
459
 
498
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
499
- return null;
500
- }
460
+ const can = skip_can or
461
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
462
+ if (!can) return null;
501
463
 
502
464
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
503
465
  }
@@ -508,7 +470,6 @@ fn beforeMove(
508
470
  battle: anytype,
509
471
  player: Player,
510
472
  skip: bool,
511
- from: ?Move,
512
473
  residual: *bool,
513
474
  options: anytype,
514
475
  ) !BeforeMove {
@@ -569,16 +530,16 @@ fn beforeMove(
569
530
  return .done;
570
531
  }
571
532
 
572
- if (volatiles.disabled_duration > 0) {
533
+ if (volatiles.disable_duration > 0) {
573
534
  if (options.calc.modify(player, .disable)) |extend| {
574
- if (!extend) volatiles.disabled_duration = 0;
535
+ if (!extend) volatiles.disable_duration = 0;
575
536
  } else {
576
- volatiles.disabled_duration -= 1;
537
+ volatiles.disable_duration -= 1;
577
538
  }
578
- try options.chance.disable(player, volatiles.disabled_duration);
539
+ try options.chance.disable(player, volatiles.disable_duration);
579
540
 
580
- if (volatiles.disabled_duration == 0) {
581
- volatiles.disabled_move = 0;
541
+ if (volatiles.disable_duration == 0) {
542
+ volatiles.disable_move = 0;
582
543
  try log.end(ident, .Disable);
583
544
  }
584
545
  }
@@ -715,7 +676,6 @@ fn beforeMove(
715
676
 
716
677
  if (volatiles.Thrashing) {
717
678
  assert(volatiles.attacks > 0);
718
- assert(from != null);
719
679
 
720
680
  if (options.calc.modify(player, .attacking)) |extend| {
721
681
  if (!extend) volatiles.attacks = 0;
@@ -727,7 +687,7 @@ fn beforeMove(
727
687
  if (!showdown and handleThrashing(battle, active, player, options)) {
728
688
  try log.start(battle.active(player), .ConfusionSilent);
729
689
  }
730
- try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
690
+ try log.move(ident, side.last_selected_move, battle.active(player.foe()));
731
691
  if (showdown) {
732
692
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
733
693
  // used in useMove and doesn't have the notion of skipping canMove semantics
@@ -749,12 +709,7 @@ fn beforeMove(
749
709
  }
750
710
  try options.chance.binding(player, volatiles.attacks);
751
711
 
752
- try log.move(
753
- battle.active(player),
754
- side.last_selected_move,
755
- battle.active(player.foe()),
756
- side.last_selected_move,
757
- );
712
+ try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
758
713
  if (showdown or battle.last_damage != 0) {
759
714
  const sub = showdown and foe.active.volatiles.Substitute;
760
715
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -774,7 +729,7 @@ fn canMove(
774
729
  mslot: u4,
775
730
  auto: bool,
776
731
  skip_pp: bool,
777
- from: ?Move,
732
+ from: Move,
778
733
  residual: *bool,
779
734
  options: anytype,
780
735
  ) !bool {
@@ -782,33 +737,22 @@ fn canMove(
782
737
  const player_ident = battle.active(player);
783
738
  const move = Move.get(side.last_selected_move);
784
739
 
785
- const foe_last = battle.foe(player).last_used_move;
786
- const special = from != null and (from.? == .Metronome and
787
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
788
-
789
740
  var skip = skip_pp;
790
741
  if (side.active.volatiles.Charging) {
791
742
  side.active.volatiles.Charging = false;
792
743
  side.active.volatiles.Invulnerable = false;
793
744
  } else if (move.effect == .Charge) {
794
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
745
+ try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
795
746
  setCounterable(battle, player, side, move);
796
747
  try Effects.charge(battle, player, options);
797
748
  return false;
798
749
  }
799
750
 
800
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
751
+ side.last_used_move = side.last_selected_move;
801
752
  if (!skip) decrementPP(side, mslot, auto);
802
753
 
803
754
  const target = if (move.target == .Self) player else player.foe();
804
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
805
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
806
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
807
- // meaning sometimes we lose this information. from is still set because it not being null is
808
- // relevant for control flow, but we don't have anything useful to put in the log. This is
809
- // regrettable, but this information is more "nice to have" than required
810
- const f = if (from != null and from.? == .None) null else from;
811
- try options.log.move(player_ident, side.last_selected_move, battle.active(target), f);
755
+ try options.log.moveFrom(player_ident, side.last_selected_move, battle.active(target), from);
812
756
  setCounterable(battle, player, side, move);
813
757
 
814
758
  if (move.effect.onBegin()) {
@@ -834,11 +778,7 @@ fn setCounterable(battle: anytype, player: Player, side: *Side, move: Move.Data)
834
778
  // as the desync results from actually having two separate battle states that subtly disagree
835
779
  const counterable = side.last_selected_move != .Counter and move.bp > 0 and
836
780
  (move.type == .Normal or move.type == .Fighting);
837
- if (player == .P1) {
838
- battle.last_moves.p1_counterable = @intFromBool(counterable);
839
- } else {
840
- battle.last_moves.p2_counterable = @intFromBool(counterable);
841
- }
781
+ battle.lastMove(player).counterable = @intFromBool(counterable);
842
782
  }
843
783
 
844
784
  fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
@@ -850,12 +790,14 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
850
790
  assert(!volatiles.Rage and !volatiles.Thrashing and !volatiles.MultiHit);
851
791
  if (volatiles.Bide) return;
852
792
 
853
- assert(active.move(mslot).pp > 0 or auto);
854
- active.move(mslot).pp = @as(u6, @intCast(active.move(mslot).pp)) -% 1;
793
+ var move_slot = active.move(mslot);
794
+ assert(move_slot.pp > 0 or auto);
795
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
855
796
  if (volatiles.Transform) return;
856
797
 
857
- assert(side.stored().move(mslot).pp > 0 or auto);
858
- side.stored().move(mslot).pp = @as(u6, @intCast(side.stored().move(mslot).pp)) -% 1;
798
+ move_slot = side.stored().move(mslot);
799
+ assert(move_slot.pp > 0 or auto);
800
+ move_slot.pp = @as(u6, @intCast(move_slot.pp)) -% 1;
859
801
  assert(active.move(mslot).pp == side.stored().move(mslot).pp);
860
802
  }
861
803
 
@@ -1278,11 +1220,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1278
1220
  else
1279
1221
  foe.last_selected_move);
1280
1222
 
1281
- const used = (if (player.foe() == .P1)
1282
- battle.last_moves.p1_counterable
1283
- else
1284
- battle.last_moves.p2_counterable) != 0;
1285
-
1223
+ const used = battle.lastMove(player.foe()).counterable != 0;
1286
1224
  const selected = foe_last_selected_move.bp > 0 and
1287
1225
  foe.last_selected_move != .Counter and
1288
1226
  (foe_last_selected_move.type == .Normal or
@@ -1357,7 +1295,7 @@ fn mirrorMove(
1357
1295
  auto: bool,
1358
1296
  residual: *bool,
1359
1297
  options: anytype,
1360
- ) !?Result {
1298
+ ) anyerror!?Result {
1361
1299
  var side = battle.side(player);
1362
1300
  const foe = battle.foe(player);
1363
1301
 
@@ -1384,7 +1322,7 @@ fn metronome(
1384
1322
  auto: bool,
1385
1323
  residual: *bool,
1386
1324
  options: anytype,
1387
- ) !?Result {
1325
+ ) anyerror!?Result {
1388
1326
  var side = battle.side(player);
1389
1327
 
1390
1328
  side.last_selected_move = try Rolls.metronome(battle, player, options);
@@ -1463,7 +1401,7 @@ fn moveHit(
1463
1401
  const overwritten = overwrite and state > 0;
1464
1402
  assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
1465
1403
 
1466
- var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
1404
+ var accuracy = if (overwritten) state else @as(u16, move.accuracy);
1467
1405
  var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
1468
1406
  accuracy = accuracy * boost[0] / boost[1];
1469
1407
  boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
@@ -1502,9 +1440,8 @@ fn checkFaint(
1502
1440
  const foe_fainted = foe.stored().hp == 0;
1503
1441
 
1504
1442
  const player_out = findFirstAlive(side) == 0;
1505
- const foe_out = findFirstAlive(foe) == 0;
1506
- const tie = player_out and foe_out;
1507
- const more = tie or player_out or foe_out;
1443
+ const foe_out = foe_fainted and findFirstAlive(foe) == 0;
1444
+ const more = player_out or foe_out;
1508
1445
 
1509
1446
  if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
1510
1447
  if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
@@ -1512,7 +1449,7 @@ fn checkFaint(
1512
1449
  assert(!side.active.volatiles.MultiHit);
1513
1450
  assert(!foe.active.volatiles.MultiHit);
1514
1451
 
1515
- if (tie) {
1452
+ if (player_out and foe_out) {
1516
1453
  try options.log.tie();
1517
1454
  return Result.Tie;
1518
1455
  } else if (player_out) {
@@ -1524,8 +1461,8 @@ fn checkFaint(
1524
1461
  }
1525
1462
 
1526
1463
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
1527
- if (player == .P1) return Result{ .p1 = .Switch, .p2 = foe_choice };
1528
- return Result{ .p1 = foe_choice, .p2 = .Switch };
1464
+ if (player == .P1) return .{ .p1 = .Switch, .p2 = foe_choice };
1465
+ return .{ .p1 = foe_choice, .p2 = .Switch };
1529
1466
  }
1530
1467
 
1531
1468
  fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
@@ -1573,21 +1510,19 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
1573
1510
  var volatiles = &side.active.volatiles;
1574
1511
 
1575
1512
  const brn = Status.is(stored.status, .BRN);
1576
- if (brn or Status.is(stored.status, .PSN)) blk: {
1513
+ if (brn or Status.is(stored.status, .PSN)) {
1577
1514
  var damage = @max(stored.stats.hp / 16, 1);
1578
-
1579
1515
  if (volatiles.Toxic) {
1580
1516
  volatiles.toxic += 1;
1581
1517
  damage *= volatiles.toxic;
1582
1518
  }
1519
+ damage = @min(damage, stored.hp);
1583
1520
 
1584
- const amount = @min(damage, stored.hp);
1585
- if (showdown and amount == 0) break :blk;
1586
-
1587
- stored.hp -= amount;
1588
-
1589
- // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1590
- try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1521
+ if (!showdown or damage > 0) {
1522
+ stored.hp -= damage;
1523
+ // Pokémon Showdown uses damageOf here but its not relevant in Generation I
1524
+ try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
1525
+ }
1591
1526
  }
1592
1527
 
1593
1528
  if (volatiles.LeechSeed) {
@@ -1700,9 +1635,9 @@ fn checkEBC(battle: anytype) bool {
1700
1635
  }
1701
1636
 
1702
1637
  fn disabled(side: *Side, ident: ID, options: anytype) !bool {
1703
- if (side.active.volatiles.disabled_move != 0) {
1638
+ if (side.active.volatiles.disable_move != 0) {
1704
1639
  // A Pokémon that transforms after being disabled may end up with less move slots
1705
- const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
1640
+ const m = side.active.moves[side.active.volatiles.disable_move - 1].id;
1706
1641
  // side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
1707
1642
  const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
1708
1643
  m
@@ -1930,7 +1865,7 @@ pub const Effects = struct {
1930
1865
  try options.chance.commit(player, .hit);
1931
1866
  }
1932
1867
 
1933
- if (volatiles.disabled_move != 0) {
1868
+ if (volatiles.disable_move != 0) {
1934
1869
  try options.log.fail(foe_ident, .None);
1935
1870
  return null;
1936
1871
  }
@@ -1955,18 +1890,17 @@ pub const Effects = struct {
1955
1890
  return Result.Error;
1956
1891
  }
1957
1892
 
1958
- volatiles.disabled_move =
1893
+ volatiles.disable_move =
1959
1894
  @intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
1960
- volatiles.disabled_duration = Rolls.disableDuration(battle, player, options);
1895
+ volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
1961
1896
 
1962
- const id = foe.active.move(volatiles.disabled_move).id;
1897
+ const id = foe.active.move(volatiles.disable_move).id;
1963
1898
  try options.log.startEffect(foe_ident, .Disable, id);
1964
1899
  return null;
1965
1900
  }
1966
1901
 
1967
1902
  fn drainHP(battle: anytype, player: Player, options: anytype) !void {
1968
- var side = battle.side(player);
1969
- var stored = side.stored();
1903
+ var stored = battle.side(player).stored();
1970
1904
 
1971
1905
  if (battle.last_damage == 0) {
1972
1906
  assert(showdown);
@@ -2420,12 +2354,17 @@ pub const Effects = struct {
2420
2354
  }
2421
2355
 
2422
2356
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
2423
- if (!showdown or battle.side(player).last_selected_move == .Teleport) return;
2424
-
2425
2357
  // Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
2426
- if (try checkHit(battle, player, move, options)) try options.chance.commit(player, .hit);
2427
-
2428
- battle.last_damage = 0;
2358
+ if (showdown) {
2359
+ if (battle.side(player).last_selected_move == .Teleport) return;
2360
+ if (try checkHit(battle, player, move, options)) {
2361
+ try options.chance.commit(player, .hit);
2362
+ }
2363
+ battle.last_damage = 0;
2364
+ } else {
2365
+ try options.log.fail(battle.active(player), .None);
2366
+ try options.log.laststill();
2367
+ }
2429
2368
  }
2430
2369
 
2431
2370
  fn thrashing(battle: anytype, player: Player, options: anytype) void {
@@ -2705,7 +2644,7 @@ fn statusModify(status: u8, stats: *Stats(u16)) void {
2705
2644
  }
2706
2645
  }
2707
2646
 
2708
- inline fn isForced(active: ActivePokemon) bool {
2647
+ inline fn isForced(active: anytype) bool {
2709
2648
  return active.volatiles.Recharging or active.volatiles.Rage or
2710
2649
  active.volatiles.Thrashing or active.volatiles.Charging;
2711
2650
  }
@@ -2718,9 +2657,9 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2718
2657
 
2719
2658
  options.chance.clearDurations(who, clear);
2720
2659
 
2721
- if (volatiles.disabled_move != 0) {
2722
- volatiles.disabled_move = 0;
2723
- volatiles.disabled_duration = 0;
2660
+ if (volatiles.disable_move != 0) {
2661
+ volatiles.disable_move = 0;
2662
+ volatiles.disable_duration = 0;
2724
2663
  try log.end(ident, .DisableSilent);
2725
2664
  }
2726
2665
  if (volatiles.Confusion) {
@@ -2730,15 +2669,15 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2730
2669
  }
2731
2670
  if (volatiles.Mist) {
2732
2671
  volatiles.Mist = false;
2733
- try log.end(ident, .Mist);
2672
+ try log.end(ident, .MistSilent);
2734
2673
  }
2735
2674
  if (volatiles.FocusEnergy) {
2736
2675
  volatiles.FocusEnergy = false;
2737
- try log.end(ident, .FocusEnergy);
2676
+ try log.end(ident, .FocusEnergySilent);
2738
2677
  }
2739
2678
  if (volatiles.LeechSeed) {
2740
2679
  volatiles.LeechSeed = false;
2741
- try log.end(ident, .LeechSeed);
2680
+ try log.end(ident, .LeechSeedSilent);
2742
2681
  }
2743
2682
  if (!showdown and volatiles.Toxic) {
2744
2683
  volatiles.Toxic = false;
@@ -2746,17 +2685,17 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
2746
2685
  }
2747
2686
  if (volatiles.LightScreen) {
2748
2687
  volatiles.LightScreen = false;
2749
- try log.end(ident, .LightScreen);
2688
+ try log.end(ident, .LightScreenSilent);
2750
2689
  }
2751
2690
  if (volatiles.Reflect) {
2752
2691
  volatiles.Reflect = false;
2753
- try log.end(ident, .Reflect);
2692
+ try log.end(ident, .ReflectSilent);
2754
2693
  }
2755
2694
  if (showdown and volatiles.Toxic) {
2756
2695
  volatiles.Toxic = false;
2757
2696
  // Pokémon Showdown erroneously clears the toxic counter
2758
2697
  volatiles.toxic = 0;
2759
- try log.end(ident, .Toxic);
2698
+ try log.end(ident, .ToxicSilent);
2760
2699
  }
2761
2700
  }
2762
2701
 
@@ -2785,7 +2724,7 @@ pub const Rolls = struct {
2785
2724
  return crit;
2786
2725
  }
2787
2726
 
2788
- inline fn damage(battle: anytype, player: anytype, options: anytype) !u8 {
2727
+ inline fn damage(battle: anytype, player: Player, options: anytype) !u8 {
2789
2728
  const roll = if (options.calc.overridden(player, .damage)) |val|
2790
2729
  val
2791
2730
  else roll: {
@@ -2796,6 +2735,7 @@ pub const Rolls = struct {
2796
2735
  }
2797
2736
  };
2798
2737
 
2738
+ assert(roll >= 217 and roll <= 255);
2799
2739
  try options.chance.damage(player, roll);
2800
2740
  return roll;
2801
2741
  }
@@ -2891,11 +2831,9 @@ pub const Rolls = struct {
2891
2831
  inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2892
2832
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2893
2833
  val
2894
- else if (showdown) move: {
2895
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2896
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2897
- break :move @enumFromInt(r + mod);
2898
- } else move: {
2834
+ else if (showdown)
2835
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2836
+ else move: {
2899
2837
  while (true) {
2900
2838
  const r = battle.rng.next();
2901
2839
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2920,6 +2858,7 @@ pub const Rolls = struct {
2920
2858
  }
2921
2859
  };
2922
2860
 
2861
+ assert(power < max);
2923
2862
  try options.chance.psywave(player, power, max);
2924
2863
  return power;
2925
2864
  }
@@ -2928,7 +2867,7 @@ pub const Rolls = struct {
2928
2867
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2929
2868
  @intCast(val)
2930
2869
  else if (showdown)
2931
- @intCast(battle.rng.range(u8, 1, 8))
2870
+ battle.rng.range(u3, 1, 8)
2932
2871
  else duration: {
2933
2872
  while (true) {
2934
2873
  const r = battle.rng.next() & 7;
@@ -2936,6 +2875,7 @@ pub const Rolls = struct {
2936
2875
  }
2937
2876
  };
2938
2877
 
2878
+ assert(duration >= 1 and duration <= 7);
2939
2879
  options.chance.duration(.sleep, player, player.foe(), duration);
2940
2880
  return duration;
2941
2881
  }
@@ -2944,10 +2884,11 @@ pub const Rolls = struct {
2944
2884
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2945
2885
  val
2946
2886
  else if (showdown)
2947
- @intCast(battle.rng.range(u8, 1, 9))
2887
+ battle.rng.range(u4, 1, 9)
2948
2888
  else
2949
2889
  @intCast((battle.rng.next() & 7) + 1);
2950
2890
 
2891
+ assert(duration >= 1 and duration <= 8);
2951
2892
  options.chance.duration(.disable, player, player.foe(), duration);
2952
2893
  return duration;
2953
2894
  }
@@ -2956,10 +2897,11 @@ pub const Rolls = struct {
2956
2897
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2957
2898
  @intCast(val)
2958
2899
  else if (showdown)
2959
- @intCast(battle.rng.range(u8, 2, 6))
2900
+ battle.rng.range(u3, 2, 6)
2960
2901
  else
2961
2902
  @intCast((battle.rng.next() & 3) + 2);
2962
2903
 
2904
+ assert(duration >= 2 and duration <= 5);
2963
2905
  options.chance.duration(.confusion, player, if (self) player else player.foe(), duration);
2964
2906
  return duration;
2965
2907
  }
@@ -2968,14 +2910,17 @@ pub const Rolls = struct {
2968
2910
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2969
2911
  @intCast(val)
2970
2912
  else if (showdown)
2971
- @intCast(battle.rng.range(u4, 2, 4))
2913
+ battle.rng.range(u3, 2, 4)
2972
2914
  else
2973
2915
  @intCast((battle.rng.next() & 1) + 2);
2974
2916
 
2917
+ assert(duration >= 2 and duration <= 3);
2975
2918
  options.chance.duration(.attacking, player, player, duration);
2976
2919
  return duration;
2977
2920
  }
2978
2921
 
2922
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2923
+
2979
2924
  inline fn distribution(
2980
2925
  battle: anytype,
2981
2926
  comptime effect: Move.Effect,
@@ -2986,12 +2931,13 @@ pub const Rolls = struct {
2986
2931
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2987
2932
  @intCast(val)
2988
2933
  else if (showdown)
2989
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2934
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2990
2935
  else n: {
2991
2936
  const r = (battle.rng.next() & 3);
2992
2937
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
2993
2938
  };
2994
2939
 
2940
+ assert(n >= 2 and n <= 5);
2995
2941
  if (effect == .Binding) {
2996
2942
  options.chance.duration(.binding, player, player, n);
2997
2943
  } else {
@@ -3032,10 +2978,9 @@ pub const Rolls = struct {
3032
2978
  while (i < moves.len and r > 0) : (i += 1) {
3033
2979
  if (moves[i].pp > 0) {
3034
2980
  r -= 1;
3035
- if (r == 0) break;
2981
+ if (r == 0) break :slot @intCast(i + 1);
3036
2982
  }
3037
2983
  }
3038
- break :slot @intCast(i + 1);
3039
2984
  }
3040
2985
  }
3041
2986
 
@@ -3057,11 +3002,11 @@ test "RNG agreement" {
3057
3002
  expected[i] = @intCast(i * 0x1000000);
3058
3003
  }
3059
3004
 
3060
- var spe = rng.FixedRNG(1, expected.len){ .rolls = expected };
3061
- var cfz = rng.FixedRNG(1, expected.len){ .rolls = expected };
3062
- var par = rng.FixedRNG(1, expected.len){ .rolls = expected };
3063
- var brn = rng.FixedRNG(1, expected.len){ .rolls = expected };
3064
- var eff = rng.FixedRNG(1, expected.len){ .rolls = expected };
3005
+ var spe: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3006
+ var cfz: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3007
+ var par: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3008
+ var brn: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3009
+ var eff: rng.FixedRNG(1, expected.len) = .{ .rolls = expected };
3065
3010
 
3066
3011
  for (0..expected.len) |i| {
3067
3012
  try expectEqual(spe.range(u8, 0, 2) == 0, i < Gen12.percent(50) + 1);
@@ -3097,7 +3042,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3097
3042
  const side = battle.side(player);
3098
3043
  const foe = battle.foe(player);
3099
3044
 
3100
- var active = side.active;
3045
+ var active = &side.active;
3101
3046
  const stored = side.stored();
3102
3047
 
3103
3048
  // While players are not given any input options on the cartridge in these cases,
@@ -3151,7 +3096,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3151
3096
  if (m.id == if (active.volatiles.Bide) .Bide else side.last_selected_move) {
3152
3097
  // Pokémon Showdown displays Struggle if limited to Bide but unable to pick
3153
3098
  const struggle =
3154
- m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
3099
+ m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3155
3100
  const s = if (struggle) 0 else slot;
3156
3101
  out[n] = .{ .type = .Move, .data = s };
3157
3102
  n += 1;
@@ -3166,7 +3111,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3166
3111
  const m = active.moves[slot - 1];
3167
3112
  if (m.id == .None) break;
3168
3113
  if (m.pp == 0) continue;
3169
- if (active.volatiles.disabled_move == slot) continue;
3114
+ if (active.volatiles.disable_move == slot) continue;
3170
3115
  out[n] = .{ .type = .Move, .data = slot };
3171
3116
  n += 1;
3172
3117
  }