@pkmn/engine 0.1.0-dev.bcb16a6c → 0.1.0-dev.c42deb42

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pkmn/engine",
3
- "version": "0.1.0-dev.bcb16a6c",
3
+ "version": "0.1.0-dev.c42deb42",
4
4
  "description": "A minimal, complete, Pokémon battle simulation engine optimized for performance",
5
5
  "repository": "github:pkmn/engine",
6
6
  "license": "MIT",
@@ -15,19 +15,17 @@ pub fn Optional(comptime T: type) type {
15
15
 
16
16
  enumFields[0] = .{ .name = "None", .value = 0 };
17
17
 
18
- var i: usize = 0;
19
- inline for (fields) |field| {
18
+ inline for (fields, 1..) |field, i| {
20
19
  assert(!std.mem.eql(u8, field.name, "None"));
21
- enumFields[i + 1] = .{
20
+ enumFields[i] = .{
22
21
  .name = field.name,
23
- .value = i + 1,
22
+ .value = i,
24
23
  };
25
- i += 1;
26
24
  }
27
25
 
28
26
  return @Type(.{
29
27
  .Enum = .{
30
- .tag_type = std.math.IntFittingRange(0, i),
28
+ .tag_type = std.math.IntFittingRange(0, fields.len),
31
29
  .fields = &enumFields,
32
30
  .decls = &decls,
33
31
  .is_exhaustive = true,
@@ -222,6 +222,7 @@ pub const Start = enum(u8) {
222
222
  Disable,
223
223
  Mimic,
224
224
 
225
+ Attract,
225
226
  Nightmare,
226
227
  Curse,
227
228
  Foresight,
@@ -305,7 +306,7 @@ pub fn Log(comptime Writer: type) type {
305
306
 
306
307
  pub const Error = Writer.Error;
307
308
 
308
- pub fn move(self: Self, source: ID, m: anytype, target: ID, from: anytype) Error!void {
309
+ pub fn move(self: Self, source: ID, m: anytype, target: ID) Error!void {
309
310
  if (!enabled) return;
310
311
 
311
312
  assert(m != .None);
@@ -314,19 +315,24 @@ pub fn Log(comptime Writer: type) type {
314
315
  @as(u8, @bitCast(source)),
315
316
  @intFromEnum(m),
316
317
  @as(u8, @bitCast(target)),
318
+ @intFromEnum(Move.None),
317
319
  });
318
- const none = &[_]u8{@intFromEnum(Move.None)};
319
- try self.writer.writeAll(switch (@typeInfo(@TypeOf(from))) {
320
- .Null => none,
321
- .Optional => if (from) |f| blk: {
322
- assert(@intFromEnum(f) != 0);
323
- break :blk &[_]u8{ @intFromEnum(Move.From), @intFromEnum(f) };
324
- } else none,
325
- else => blk: {
326
- assert(@intFromEnum(from) != 0);
327
- break :blk &[_]u8{ @intFromEnum(Move.From), @intFromEnum(from) };
328
- },
320
+ }
321
+
322
+ pub fn moveFrom(self: Self, source: ID, m: anytype, target: ID, from: anytype) Error!void {
323
+ if (!enabled) return;
324
+
325
+ assert(m != .None);
326
+ try self.writer.writeAll(&.{
327
+ @intFromEnum(ArgType.Move),
328
+ @as(u8, @bitCast(source)),
329
+ @intFromEnum(m),
330
+ @as(u8, @bitCast(target)),
329
331
  });
332
+ try self.writer.writeAll(if (@intFromEnum(from) == 0)
333
+ &.{@intFromEnum(Move.None)}
334
+ else
335
+ &.{ @intFromEnum(Move.From), @intFromEnum(from) });
330
336
  }
331
337
 
332
338
  pub fn switched(self: Self, ident: ID, pokemon: anytype) Error!void {
@@ -1288,22 +1294,21 @@ fn formatter(comptime gen: anytype, kind: Kind, byte: u8) []const u8 {
1288
1294
  }
1289
1295
 
1290
1296
  test "|move|" {
1291
- try log.move(p2.ident(4), M1.Thunderbolt, p1.ident(5), null);
1297
+ try log.move(p2.ident(4), M1.Thunderbolt, p1.ident(5));
1292
1298
  try expectLog1(
1293
1299
  &.{ N(ArgType.Move), 0b1100, N(M1.Thunderbolt), 0b0101, N(Move.None) },
1294
1300
  buf[0..5],
1295
1301
  );
1296
1302
  stream.reset();
1297
1303
 
1298
- try log.move(p2.ident(4), M1.Wrap, p1.ident(5), M1.Wrap);
1299
- const wrap = N(M1.Wrap);
1304
+ try log.moveFrom(p2.ident(4), M1.Pound, p1.ident(5), M1.Metronome);
1300
1305
  try expectLog1(
1301
- &.{ N(ArgType.Move), 0b1100, wrap, 0b0101, N(Move.From), wrap },
1306
+ &.{ N(ArgType.Move), 0b1100, N(M1.Pound), 0b0101, N(Move.From), N(M1.Metronome) },
1302
1307
  buf[0..6],
1303
1308
  );
1304
1309
  stream.reset();
1305
1310
 
1306
- try log.move(p2.ident(4), M1.SkullBash, .{}, null);
1311
+ try log.move(p2.ident(4), M1.SkullBash, .{});
1307
1312
  try log.laststill();
1308
1313
  try expectLog1(
1309
1314
  &.{
@@ -1318,7 +1323,7 @@ test "|move|" {
1318
1323
  );
1319
1324
  stream.reset();
1320
1325
 
1321
- try log.move(p2.ident(4), M1.Tackle, p1.ident(5), null);
1326
+ try log.move(p2.ident(4), M1.Tackle, p1.ident(5));
1322
1327
  try log.lastmiss();
1323
1328
  try expectLog1(
1324
1329
  &.{
@@ -14,12 +14,14 @@ const util = @import("../common/util.zig");
14
14
  const Player = @import("../common/data.zig").Player;
15
15
  const Optional = @import("../common/optional.zig").Optional;
16
16
 
17
- const Move = @import("data.zig").Move;
18
- const MoveSlot = @import("data.zig").MoveSlot;
17
+ const data = @import("./data.zig");
19
18
 
20
19
  const enabled = options.chance;
21
20
  const showdown = options.showdown;
22
21
 
22
+ const Move = data.Move;
23
+ const MoveSlot = data.MoveSlot;
24
+
23
25
  /// Actions taken by a hypothetical "chance player" that convey information about which RNG events
24
26
  /// were observed during a Generation I battle `update`. This can additionally be provided as input
25
27
  /// to the `update` call to override the normal behavior of the RNG in order to force specific
@@ -514,22 +516,6 @@ pub fn Chance(comptime Rational: type) type {
514
516
  }
515
517
  }
516
518
 
517
- pub fn disable(self: *Self, player: Player, turns: u4) Error!void {
518
- if (!enabled) return;
519
-
520
- var durations = &self.actions.get(player).durations;
521
- const n = durations.disable;
522
- if (turns == 0) {
523
- assert(n >= 1 and n <= 8);
524
- if (n != 8) try self.probability.update(1, 9 - @as(u4, n));
525
- durations.disable = 0;
526
- } else {
527
- assert(n >= 1 and n < 8);
528
- try self.probability.update(9 - @as(u4, n) - 1, 9 - @as(u4, n));
529
- durations.disable += 1;
530
- }
531
- }
532
-
533
519
  pub fn confusion(self: *Self, player: Player, turns: u4) Error!void {
534
520
  if (!enabled) return;
535
521
 
@@ -546,6 +532,22 @@ pub fn Chance(comptime Rational: type) type {
546
532
  }
547
533
  }
548
534
 
535
+ pub fn disable(self: *Self, player: Player, turns: u4) Error!void {
536
+ if (!enabled) return;
537
+
538
+ var durations = &self.actions.get(player).durations;
539
+ const n = durations.disable;
540
+ if (turns == 0) {
541
+ assert(n >= 1 and n <= 8);
542
+ if (n != 8) try self.probability.update(1, 9 - @as(u4, n));
543
+ durations.disable = 0;
544
+ } else {
545
+ assert(n >= 1 and n < 8);
546
+ try self.probability.update(9 - @as(u4, n) - 1, 9 - @as(u4, n));
547
+ durations.disable += 1;
548
+ }
549
+ }
550
+
549
551
  pub fn attacking(self: *Self, player: Player, turns: u4) Error!void {
550
552
  if (!enabled) return;
551
553
 
@@ -593,7 +595,7 @@ pub fn Chance(comptime Rational: type) type {
593
595
  pub fn metronome(self: *Self, player: Player, move: Move) Error!void {
594
596
  if (!enabled) return;
595
597
 
596
- try self.probability.update(1, Move.size - 2);
598
+ try self.probability.update(1, Move.METRONOME.len);
597
599
  self.actions.get(player).metronome = move;
598
600
  }
599
601
  };
@@ -832,28 +834,6 @@ test "Chance.sleep" {
832
834
  }
833
835
  }
834
836
 
835
- test "Chance.disable" {
836
- var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
837
-
838
- for ([_]u8{ 8, 7, 6, 5, 4, 3, 2, 1 }, 1..9) |d, i| {
839
- chance.actions.p1.durations.disable = @intCast(i);
840
- try chance.disable(.P1, 0);
841
- try expectProbability(&chance.probability, 1, d);
842
- try expectValue(@as(u4, 0), chance.actions.p1.durations.disable);
843
-
844
- chance.reset();
845
-
846
- if (i < 8) {
847
- chance.actions.p1.durations.disable = @intCast(i);
848
- try chance.disable(.P1, 1);
849
- try expectProbability(&chance.probability, d - 1, d);
850
- try expectValue(@as(u4, @intCast(i)) + 1, chance.actions.p1.durations.disable);
851
-
852
- chance.reset();
853
- }
854
- }
855
- }
856
-
857
837
  test "Chance.confusion" {
858
838
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
859
839
 
@@ -878,6 +858,28 @@ test "Chance.confusion" {
878
858
  }
879
859
  }
880
860
 
861
+ test "Chance.disable" {
862
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
863
+
864
+ for ([_]u8{ 8, 7, 6, 5, 4, 3, 2, 1 }, 1..9) |d, i| {
865
+ chance.actions.p1.durations.disable = @intCast(i);
866
+ try chance.disable(.P1, 0);
867
+ try expectProbability(&chance.probability, 1, d);
868
+ try expectValue(@as(u4, 0), chance.actions.p1.durations.disable);
869
+
870
+ chance.reset();
871
+
872
+ if (i < 8) {
873
+ chance.actions.p1.durations.disable = @intCast(i);
874
+ try chance.disable(.P1, 1);
875
+ try expectProbability(&chance.probability, d - 1, d);
876
+ try expectValue(@as(u4, @intCast(i)) + 1, chance.actions.p1.durations.disable);
877
+
878
+ chance.reset();
879
+ }
880
+ }
881
+ }
882
+
881
883
  test "Chance.attacking" {
882
884
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
883
885
 
@@ -1838,6 +1838,20 @@ pub const Move = enum(u8) {
1838
1838
  /// The number of Pokémon moves in this generation.
1839
1839
  pub const size = 165;
1840
1840
 
1841
+ /// Pokémon moves that can be called by Metronome in this generation.
1842
+ pub const METRONOME = init: {
1843
+ var num = 0;
1844
+ var moves: [163]Move = undefined;
1845
+ for (1..Move.size) |i| {
1846
+ if (i != @intFromEnum(Move.Metronome)) {
1847
+ moves[num] = @enumFromInt(i);
1848
+ num += 1;
1849
+ }
1850
+ }
1851
+ assert(num == moves.len);
1852
+ break :init moves;
1853
+ };
1854
+
1841
1855
  /// Returns the `Data` corresponding to the move.
1842
1856
  pub inline fn get(id: Move) Data {
1843
1857
  assert(id != .None and id != .SKIP_TURN);
@@ -455,25 +455,10 @@ pub const Rolls = struct {
455
455
  }
456
456
 
457
457
  const MOVE_NONE = [_]Move{.None};
458
- const MOVES = init: {
459
- var moves: [Move.size - 2]Move = undefined;
460
- var i: usize = 0;
461
- for (@typeInfo(Move).Enum.fields) |f| {
462
- if (!(std.mem.eql(u8, f.name, "None") or
463
- std.mem.eql(u8, f.name, "Metronome") or
464
- std.mem.eql(u8, f.name, "Struggle") or
465
- std.mem.eql(u8, f.name, "SKIP_TURN")))
466
- {
467
- moves[i] = @field(Move, f.name);
468
- i += 1;
469
- }
470
- }
471
- break :init moves;
472
- };
473
458
 
474
459
  /// Returns a slice with the correct range of values for metronome given the `action` state.
475
460
  pub inline fn metronome(action: Action) []const Move {
476
- return if (@field(action, "metronome") == .None) &MOVE_NONE else &MOVES;
461
+ return if (@field(action, "metronome") == .None) &MOVE_NONE else &Move.METRONOME;
477
462
  }
478
463
  };
479
464
 
@@ -7,8 +7,6 @@ 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;
@@ -59,19 +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
61
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
66
62
  if (battle.turn == 0) return start(battle, options);
67
63
 
68
64
  var s1 = false;
69
65
  var s2 = false;
70
- var f1: ?Move = null;
71
- var f2: ?Move = null;
72
66
 
73
- if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
74
- 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;
75
69
 
76
70
  var p1 = battle.side(.P1);
77
71
  var p2 = battle.side(.P2);
@@ -80,9 +74,9 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
80
74
  var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
81
75
 
82
76
  if (try turnOrder(battle, c1, c2, options) == .P1) {
83
- 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;
84
78
  } else {
85
- 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;
86
80
  }
87
81
 
88
82
  if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
@@ -120,7 +114,6 @@ fn selectMove(
120
114
  choice: Choice,
121
115
  foe_choice: Choice,
122
116
  skip_turn: *bool,
123
- from: *?Move,
124
117
  ) ?Result {
125
118
  if (choice.type == .Pass) return null;
126
119
 
@@ -137,7 +130,6 @@ fn selectMove(
137
130
  return null;
138
131
  }
139
132
  if (volatiles.Rage) {
140
- from.* = side.last_used_move;
141
133
  if (showdown) {
142
134
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
143
135
  saveMove(battle, player, null);
@@ -147,7 +139,6 @@ fn selectMove(
147
139
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
148
140
  if (!showdown) volatiles.Flinch = false;
149
141
  if (volatiles.Thrashing or volatiles.Charging) {
150
- from.* = side.last_selected_move;
151
142
  if (showdown) {
152
143
  if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
153
144
  saveMove(battle, player, null);
@@ -168,9 +159,7 @@ fn selectMove(
168
159
  return null;
169
160
  }
170
161
  if (volatiles.Binding) {
171
- from.* = side.last_used_move;
172
162
  if (showdown) {
173
- if (foe_choice.type == .Switch) from.* = null;
174
163
  // Pokémon Showdown overwrites Mirror Move with whatever was selected - really this
175
164
  // should set side.last_selected_move = last.id to reuse Mirror Move and fail in order
176
165
  // to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
@@ -336,12 +325,10 @@ fn doTurn(
336
325
  player_choice: Choice,
337
326
  player_rewrap: bool,
338
327
  player_skip: bool,
339
- player_from: ?Move,
340
328
  foe_player: Player,
341
329
  foe_choice: Choice,
342
330
  foe_rewrap: bool,
343
331
  foe_skip: bool,
344
- foe_from: ?Move,
345
332
  options: anytype,
346
333
  ) !?Result {
347
334
  assert(player_choice.type != .Pass);
@@ -354,7 +341,6 @@ fn doTurn(
354
341
  player_choice,
355
342
  player_rewrap,
356
343
  player_skip,
357
- player_from,
358
344
  &residual,
359
345
  options,
360
346
  )) |r| return r;
@@ -377,7 +363,6 @@ fn doTurn(
377
363
  foe_choice,
378
364
  foe_rewrap,
379
365
  foe_skip,
380
- foe_from,
381
366
  &residual,
382
367
  options,
383
368
  )) |r| return r;
@@ -405,7 +390,6 @@ fn executeMove(
405
390
  choice: Choice,
406
391
  rewrap: bool,
407
392
  skip: bool,
408
- from: ?Move,
409
393
  residual: *bool,
410
394
  options: anytype,
411
395
  ) !?Result {
@@ -440,8 +424,10 @@ fn executeMove(
440
424
  mslot = @intCast(battle.lastMove(player).index);
441
425
  const stored = side.stored();
442
426
  // GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
443
- auto = side.last_selected_move == .HyperBeam or side.active.volatiles.Bide or
444
- 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);
445
431
  // If it wasn't Hyper Beam or the continuation of a move effect then we must have just
446
432
  // thawed, in which case we will desync unless the last_selected_move happened to be at
447
433
  // index 1 and the current Pokémon has the same move in its first slot.
@@ -463,7 +449,7 @@ fn executeMove(
463
449
 
464
450
  var skip_can = false;
465
451
  var skip_pp = false;
466
- switch (try beforeMove(battle, player, skip, from, residual, options)) {
452
+ switch (try beforeMove(battle, player, skip, residual, options)) {
467
453
  .done => return null,
468
454
  .skip_can => skip_can = true,
469
455
  .skip_pp => skip_pp = true,
@@ -471,9 +457,9 @@ fn executeMove(
471
457
  .err => return @as(?Result, Result.Error),
472
458
  }
473
459
 
474
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, options)) {
475
- return null;
476
- }
460
+ const can = skip_can or
461
+ try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
462
+ if (!can) return null;
477
463
 
478
464
  return doMove(battle, player, mslot, rewrap, auto, residual, options);
479
465
  }
@@ -484,7 +470,6 @@ fn beforeMove(
484
470
  battle: anytype,
485
471
  player: Player,
486
472
  skip: bool,
487
- from: ?Move,
488
473
  residual: *bool,
489
474
  options: anytype,
490
475
  ) !BeforeMove {
@@ -691,7 +676,6 @@ fn beforeMove(
691
676
 
692
677
  if (volatiles.Thrashing) {
693
678
  assert(volatiles.attacks > 0);
694
- assert(from != null);
695
679
 
696
680
  if (options.calc.modify(player, .attacking)) |extend| {
697
681
  if (!extend) volatiles.attacks = 0;
@@ -703,7 +687,7 @@ fn beforeMove(
703
687
  if (!showdown and handleThrashing(battle, active, player, options)) {
704
688
  try log.start(battle.active(player), .ConfusionSilent);
705
689
  }
706
- 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()));
707
691
  if (showdown) {
708
692
  // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
709
693
  // used in useMove and doesn't have the notion of skipping canMove semantics
@@ -725,12 +709,7 @@ fn beforeMove(
725
709
  }
726
710
  try options.chance.binding(player, volatiles.attacks);
727
711
 
728
- try log.move(
729
- battle.active(player),
730
- side.last_selected_move,
731
- battle.active(player.foe()),
732
- side.last_selected_move,
733
- );
712
+ try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
734
713
  if (showdown or battle.last_damage != 0) {
735
714
  const sub = showdown and foe.active.volatiles.Substitute;
736
715
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -750,7 +729,7 @@ fn canMove(
750
729
  mslot: u4,
751
730
  auto: bool,
752
731
  skip_pp: bool,
753
- from: ?Move,
732
+ from: Move,
754
733
  residual: *bool,
755
734
  options: anytype,
756
735
  ) !bool {
@@ -758,33 +737,22 @@ fn canMove(
758
737
  const player_ident = battle.active(player);
759
738
  const move = Move.get(side.last_selected_move);
760
739
 
761
- const foe_last = battle.foe(player).last_used_move;
762
- const special = from != null and (from.? == .Metronome and
763
- (from.? == .MirrorMove and !(foe_last == .None or foe_last == .MirrorMove)));
764
-
765
740
  var skip = skip_pp;
766
741
  if (side.active.volatiles.Charging) {
767
742
  side.active.volatiles.Charging = false;
768
743
  side.active.volatiles.Invulnerable = false;
769
744
  } else if (move.effect == .Charge) {
770
- try options.log.move(player_ident, side.last_selected_move, .{}, from);
745
+ try options.log.moveFrom(player_ident, side.last_selected_move, .{}, from);
771
746
  setCounterable(battle, player, side, move);
772
747
  try Effects.charge(battle, player, options);
773
748
  return false;
774
749
  }
775
750
 
776
- if (!showdown or !special) side.last_used_move = side.last_selected_move;
751
+ side.last_used_move = side.last_selected_move;
777
752
  if (!skip) decrementPP(side, mslot, auto);
778
753
 
779
754
  const target = if (move.target == .Self) player else player.foe();
780
- // Pokémon Showdown's protocol for Rage and Binding moves should come with a [from], though we
781
- // don't have space to be able to track it in all circumstances and thus usually try to infer it
782
- // from side.last_used_move. However, this gets reset by the opponent switching or fainting,
783
- // meaning sometimes we lose this information. from is still set because it not being null is
784
- // relevant for control flow, but we don't have anything useful to put in the log. This is
785
- // regrettable, but this information is more "nice to have" than required
786
- const f = if (from != null and from.? == .None) null else from;
787
- 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);
788
756
  setCounterable(battle, player, side, move);
789
757
 
790
758
  if (move.effect.onBegin()) {
@@ -2863,11 +2831,9 @@ pub const Rolls = struct {
2863
2831
  inline fn metronome(battle: anytype, player: Player, options: anytype) !Move {
2864
2832
  const move: Move = if (options.calc.overridden(player, .metronome)) |val|
2865
2833
  val
2866
- else if (showdown) move: {
2867
- const r = battle.rng.range(u8, 0, @intFromEnum(Move.Struggle) - 2);
2868
- const mod = @as(u2, (if (r < @intFromEnum(Move.Metronome) - 1) 1 else 2));
2869
- break :move @enumFromInt(r + mod);
2870
- } else move: {
2834
+ else if (showdown)
2835
+ Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
2836
+ else move: {
2871
2837
  while (true) {
2872
2838
  const r = battle.rng.next();
2873
2839
  if (r == 0 or r == @intFromEnum(Move.Metronome)) continue;
@@ -2901,7 +2867,7 @@ pub const Rolls = struct {
2901
2867
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2902
2868
  @intCast(val)
2903
2869
  else if (showdown)
2904
- @intCast(battle.rng.range(u8, 1, 8))
2870
+ battle.rng.range(u3, 1, 8)
2905
2871
  else duration: {
2906
2872
  while (true) {
2907
2873
  const r = battle.rng.next() & 7;
@@ -2918,7 +2884,7 @@ pub const Rolls = struct {
2918
2884
  const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
2919
2885
  val
2920
2886
  else if (showdown)
2921
- @intCast(battle.rng.range(u8, 1, 9))
2887
+ battle.rng.range(u4, 1, 9)
2922
2888
  else
2923
2889
  @intCast((battle.rng.next() & 7) + 1);
2924
2890
 
@@ -2931,7 +2897,7 @@ pub const Rolls = struct {
2931
2897
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2932
2898
  @intCast(val)
2933
2899
  else if (showdown)
2934
- @intCast(battle.rng.range(u8, 2, 6))
2900
+ battle.rng.range(u3, 2, 6)
2935
2901
  else
2936
2902
  @intCast((battle.rng.next() & 3) + 2);
2937
2903
 
@@ -2944,7 +2910,7 @@ pub const Rolls = struct {
2944
2910
  const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
2945
2911
  @intCast(val)
2946
2912
  else if (showdown)
2947
- @intCast(battle.rng.range(u4, 2, 4))
2913
+ battle.rng.range(u3, 2, 4)
2948
2914
  else
2949
2915
  @intCast((battle.rng.next() & 1) + 2);
2950
2916
 
@@ -2953,6 +2919,8 @@ pub const Rolls = struct {
2953
2919
  return duration;
2954
2920
  }
2955
2921
 
2922
+ const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
2923
+
2956
2924
  inline fn distribution(
2957
2925
  battle: anytype,
2958
2926
  comptime effect: Move.Effect,
@@ -2963,7 +2931,7 @@ pub const Rolls = struct {
2963
2931
  const n: u3 = if (options.calc.overridden(player, roll)) |val|
2964
2932
  @intCast(val)
2965
2933
  else if (showdown)
2966
- DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)]
2934
+ DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
2967
2935
  else n: {
2968
2936
  const r = (battle.rng.next() & 3);
2969
2937
  break :n @intCast((if (r < 2) r else battle.rng.next() & 3) + 2);
@@ -3010,10 +2978,9 @@ pub const Rolls = struct {
3010
2978
  while (i < moves.len and r > 0) : (i += 1) {
3011
2979
  if (moves[i].pp > 0) {
3012
2980
  r -= 1;
3013
- if (r == 0) break;
2981
+ if (r == 0) break :slot @intCast(i + 1);
3014
2982
  }
3015
2983
  }
3016
- break :slot @intCast(i + 1);
3017
2984
  }
3018
2985
  }
3019
2986