@pkmn/engine 0.1.0-dev.b01cb48c → 0.1.0-dev.b14f2221

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 (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +21 -11
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +26 -4
  13. package/build/pkg/addon.js +34 -65
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +23 -1
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +15 -18
  20. package/build/pkg/gen1.js +28 -19
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +40 -90
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +105 -103
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +68 -74
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +152 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +152 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +359 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +33 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +255 -102
  39. package/package.json +59 -138
  40. package/src/bin/install-pkmn-engine +25 -7
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +200 -0
  43. package/src/lib/bindings/node.zig +117 -0
  44. package/src/lib/bindings/wasm.zig +50 -0
  45. package/src/lib/common/array.zig +70 -0
  46. package/src/lib/common/data.zig +8 -8
  47. package/src/lib/common/debug.zig +30 -9
  48. package/src/lib/common/js.zig +3 -7
  49. package/src/lib/common/optional.zig +11 -17
  50. package/src/lib/common/options.zig +4 -0
  51. package/src/lib/common/protocol.zig +195 -159
  52. package/src/lib/common/rational.zig +303 -310
  53. package/src/lib/common/rng.zig +5 -7
  54. package/src/lib/common/util.zig +33 -1
  55. package/src/lib/gen1/calc.zig +324 -180
  56. package/src/lib/gen1/chance.zig +536 -140
  57. package/src/lib/gen1/data/moves.zig +2 -4
  58. package/src/lib/gen1/data/species.zig +1 -3
  59. package/src/lib/gen1/data/types.zig +3 -4
  60. package/src/lib/gen1/data.zig +40 -43
  61. package/src/lib/gen1/helpers.zig +17 -29
  62. package/src/lib/gen1/mechanics.zig +240 -187
  63. package/src/lib/gen2/calc.zig +35 -28
  64. package/src/lib/gen2/chance.zig +88 -74
  65. package/src/lib/gen2/data/items.zig +1 -3
  66. package/src/lib/gen2/data/moves.zig +2 -4
  67. package/src/lib/gen2/data/species.zig +1 -3
  68. package/src/lib/gen2/data/types.zig +7 -5
  69. package/src/lib/gen2/data.zig +33 -35
  70. package/src/lib/gen2/generated.zig +18 -23
  71. package/src/lib/gen2/helpers.zig +9 -20
  72. package/src/lib/gen2/mechanics.zig +98 -98
  73. package/src/lib/napi.h +2 -0
  74. package/src/lib/node.zig +4 -121
  75. package/src/lib/pkmn.zig +103 -22
  76. package/src/lib/wasm.zig +9 -9
  77. package/src/tools/display/dom.ts +93 -0
  78. package/src/tools/display/ui.css +354 -0
  79. package/build/test/display.js +0 -405
  80. package/build/test/display.js.map +0 -1
  81. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,40 +1,35 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const chance = @import("chance.zig");
5
4
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
5
+ const data = @import("data.zig");
6
+ const helpers = @import("helpers.zig");
7
7
  const optional = @import("../common/optional.zig");
8
+ const pkmn = @import("../pkmn.zig");
8
9
  const protocol = @import("../common/protocol.zig");
9
10
  const rational = @import("../common/rational.zig");
11
+ const std = @import("std");
10
12
  const util = @import("../common/util.zig");
11
13
 
12
- const chance = @import("chance.zig");
13
- const data = @import("data.zig");
14
- const helpers = @import("helpers.zig");
15
-
14
+ const Action = chance.Action;
15
+ const Actions = chance.Actions;
16
16
  const assert = std.debug.assert;
17
-
17
+ const Chance = chance.Chance;
18
+ const Choice = common.Choice;
19
+ const Duration = chance.Duration;
20
+ const Durations = chance.Durations;
21
+ const enabled = pkmn.options.calc;
18
22
  const expect = std.testing.expect;
19
23
  const expectEqual = std.testing.expectEqual;
20
24
  const expectEqualSlices = std.testing.expectEqualSlices;
21
-
22
- const enabled = pkmn.options.calc;
23
-
24
- const Player = common.Player;
25
- const Result = common.Result;
26
- const Choice = common.Choice;
27
-
25
+ const isPointerTo = util.isPointerTo;
26
+ const Observation = chance.Observation;
28
27
  const Optional = optional.Optional;
29
-
30
- const Rational = rational.Rational;
31
-
28
+ const Player = common.Player;
32
29
  const PointerType = util.PointerType;
33
-
34
- const Actions = chance.Actions;
35
- const Action = chance.Action;
36
- const Chance = chance.Chance;
37
- const Durations = chance.Durations;
30
+ const Rational = rational.Rational;
31
+ const Result = common.Result;
32
+ const Sleeps = chance.Sleeps;
38
33
 
39
34
  const tty = true; // DEBUG
40
35
  const summary = false; // DEBUG
@@ -52,7 +47,7 @@ pub const Summaries = extern struct {
52
47
 
53
48
  /// Returns the `Summary` for the given `player`.
54
49
  pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
55
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Summaries);
50
+ assert(isPointerTo(self, Summaries));
56
51
  return if (player == .P1) &self.p1 else &self.p2;
57
52
  }
58
53
  };
@@ -88,21 +83,11 @@ pub const Summary = extern struct {
88
83
  }
89
84
  };
90
85
 
91
- /// TODO
92
- pub const Overrides = extern struct {
93
- /// TODO
94
- actions: Actions = .{},
95
-
96
- comptime {
97
- assert(@sizeOf(Overrides) == 16);
98
- }
99
- };
100
-
101
86
  /// Allows for forcing the value of specific RNG events during a Generation I battle `update` via
102
87
  /// `overrides` and tracks `summaries` of information relevant to damage calculation.
103
88
  pub const Calc = struct {
104
89
  /// Overrides the normal behavior of the RNG during an `update` to force specific outcomes.
105
- overrides: Overrides = .{},
90
+ overrides: Actions = .{},
106
91
  /// Information relevant to damage calculation.
107
92
  summaries: Summaries = .{},
108
93
 
@@ -110,19 +95,25 @@ pub const Calc = struct {
110
95
  self: Calc,
111
96
  player: Player,
112
97
  comptime field: Action.Field,
113
- ) ?std.meta.FieldType(Action, field) {
98
+ ) ?@FieldType(Action, @tagName(field)) {
114
99
  if (!enabled) return null;
115
100
 
116
- const overrides =
117
- if (player == .P1) self.overrides.actions.p1 else self.overrides.actions.p2;
118
- const val = @field(overrides, @tagName(field));
101
+ const val = @field(self.overrides.get(player), @tagName(field));
119
102
  return if (switch (@typeInfo(@TypeOf(val))) {
120
- .Enum => val != .None,
121
- .Int => val != 0,
103
+ .@"enum" => val != .None,
104
+ .int => val != 0,
122
105
  else => unreachable,
123
106
  }) val else null;
124
107
  }
125
108
 
109
+ pub fn confusion(self: *Calc, player: Player) void {
110
+ if (!enabled) return;
111
+
112
+ if (self.overrides.get(player).confusion == .started) {
113
+ self.overrides.get(player).confusion = .None;
114
+ }
115
+ }
116
+
126
117
  pub fn base(self: *Calc, player: Player, val: u16) void {
127
118
  if (!enabled) return;
128
119
 
@@ -151,11 +142,15 @@ const Null = struct {
151
142
  self: Null,
152
143
  player: Player,
153
144
  comptime field: Action.Field,
154
- ) ?std.meta.FieldType(Action, field) {
145
+ ) ?@FieldType(Action, @tagName(field)) {
155
146
  _ = .{ self, player };
156
147
  return null;
157
148
  }
158
149
 
150
+ pub fn confusion(self: Null, player: Player) void {
151
+ _ = .{ self, player };
152
+ }
153
+
159
154
  pub fn base(self: Null, player: Player, val: u16) void {
160
155
  _ = .{ self, player, val };
161
156
  }
@@ -183,28 +178,31 @@ pub const Options = struct {
183
178
  metronome: bool = false,
184
179
  };
185
180
 
186
- pub const MAX_FRONTIER = 19;
181
+ pub const MAX_FRONTIER = 83;
187
182
 
188
183
  pub fn transitions(
189
184
  battle: anytype,
190
185
  c1: Choice,
191
186
  c2: Choice,
192
187
  allocator: std.mem.Allocator,
193
- writer: anytype,
188
+ out: *std.Io.Writer,
189
+ err: *std.Io.Writer,
194
190
  options: Options,
195
191
  ) !?Stats {
196
192
  var stats: Stats = .{};
197
193
 
198
194
  const cap = options.cap;
199
195
 
200
- var seen = std.AutoHashMap(Actions, void).init(allocator);
196
+ var seen: std.AutoHashMap(Actions, void) = .init(allocator);
201
197
  defer seen.deinit();
202
- var frontier = std.ArrayList(Actions).init(allocator);
203
- defer frontier.deinit();
198
+ var frontier: std.array_list.Aligned(Actions, null) = .empty;
199
+ defer frontier.deinit(allocator);
200
+
201
+ const d = options.durations;
204
202
 
205
203
  var opts = pkmn.battle.options(
206
204
  protocol.NULL,
207
- Chance(Rational(u128)){ .probability = .{}, .durations = options.durations },
205
+ Chance(Rational(u128)){ .probability = .{}, .durations = d },
208
206
  Calc{},
209
207
  );
210
208
 
@@ -215,8 +213,8 @@ pub fn transitions(
215
213
  const p1 = b.side(.P1);
216
214
  const p2 = b.side(.P2);
217
215
 
218
- var p: Rational(u128) = .{ .p = 0, .q = 1 };
219
- try frontier.append(opts.chance.actions);
216
+ var p: Rational(u256) = .{ .p = 0, .q = 1 };
217
+ try frontier.append(allocator, opts.chance.actions);
220
218
 
221
219
  // zig fmt: off
222
220
  for (Rolls.metronome(frontier.items[0].p1)) |p1_move| {
@@ -227,9 +225,12 @@ pub fn transitions(
227
225
  var i: usize = 0;
228
226
  assert(frontier.items.len == 1);
229
227
  while (i < frontier.items.len) : (i += 1) {
230
- const template = frontier.items[i];
228
+ const f = frontier.items[i];
229
+ const saved = stats.saved;
230
+
231
+ var r: Rational(u256) = .{ .p = 0, .q = 1 };
231
232
 
232
- try debug(writer, template, .{
233
+ try debug(out, f, .{
233
234
  .shape = true,
234
235
  .color = i,
235
236
  .bold = true,
@@ -237,43 +238,54 @@ pub fn transitions(
237
238
  .indent = false,
238
239
  });
239
240
 
240
- var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
241
-
242
- for (Rolls.speedTie(template.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
243
- for (Rolls.confused(template.p1)) |p1_cfzd| { a.p1.confused = p1_cfzd;
244
- for (Rolls.confused(template.p2)) |p2_cfzd| { a.p2.confused = p2_cfzd;
245
- for (Rolls.paralyzed(template.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
246
- for (Rolls.paralyzed(template.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
247
- for (Rolls.duration(template.p1, p1_par)) |p1_dur| { a.p1.duration = p1_dur;
248
- for (Rolls.duration(template.p2, p2_par)) |p2_dur| { a.p2.duration = p2_dur;
249
- for (Rolls.hit(template.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
250
- for (Rolls.hit(template.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
251
- for (Rolls.psywave(template.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
252
- for (Rolls.psywave(template.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
253
- for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
254
- for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
255
- for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
256
- for (Rolls.multiHit(template.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
257
- for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
258
- for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
259
- for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
260
- for (Rolls.criticalHit(template.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
261
-
262
- var p1_dmg = Rolls.damage(template.p1, p1_hit);
241
+ var a: Actions = .{
242
+ .p1 = .{ .metronome = p1_move, .pp = f.p1.pp, .duration = f.p1.duration },
243
+ .p2 = .{ .metronome = p2_move, .pp = f.p2.pp, .duration = f.p2.duration },
244
+ };
245
+
246
+ for (Rolls.speedTie(f.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
247
+ for (Rolls.sleep(f.p1, d.p1)) |p1_slp| { a.p1.sleep = p1_slp;
248
+ for (Rolls.sleep(f.p2, d.p2)) |p2_slp| { a.p2.sleep = p2_slp;
249
+ for (Rolls.disable(f.p1, d.p1, p1_slp)) |p1_dis| { a.p1.disable = p1_dis;
250
+ for (Rolls.disable(f.p2, d.p2, p2_slp)) |p2_dis| { a.p2.disable = p2_dis;
251
+ for (Rolls.attacking(f.p1, d.p1, p1_slp)) |p1_atk| { a.p1.attacking = p1_atk;
252
+ for (Rolls.attacking(f.p2, d.p2, p2_slp)) |p2_atk| { a.p2.attacking = p2_atk;
253
+ for (Rolls.confusion(f.p1, d.p1, tie, p1_atk, p1_slp)) |p1_cfz| { a.p1.confusion = p1_cfz;
254
+ for (Rolls.confusion(f.p2, d.p2, tie, p2_atk, p2_slp)) |p2_cfz| { a.p2.confusion = p2_cfz;
255
+ for (Rolls.confused(f.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
256
+ for (Rolls.confused(f.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
257
+ for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
258
+ for (Rolls.paralyzed(f.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
259
+ for (Rolls.binding(f.p1, d.p1, p1_par)) |p1_bind| { a.p1.binding = p1_bind;
260
+ for (Rolls.binding(f.p2, d.p2, p2_par)) |p2_bind| { a.p2.binding = p2_bind;
261
+ for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
262
+ for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
263
+ for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
264
+ for (Rolls.psywave(f.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
265
+ for (Rolls.moveSlot(f.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
266
+ for (Rolls.moveSlot(f.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
267
+ for (Rolls.multiHit(f.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
268
+ for (Rolls.multiHit(f.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
269
+ for (Rolls.secondaryChance(f.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
270
+ for (Rolls.secondaryChance(f.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
271
+ for (Rolls.criticalHit(f.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
272
+ for (Rolls.criticalHit(f.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
273
+
274
+ var p1_dmg = Rolls.damage(f.p1, p1_hit);
263
275
  while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
264
276
  a.p1.damage = @intCast(p1_dmg.min);
265
277
 
266
- var p2_dmg = Rolls.damage(template.p2, p2_hit);
278
+ var p2_dmg = Rolls.damage(f.p2, p2_hit);
267
279
 
268
- const p1_min: u9 = p1_dmg.min;
269
- const p2_min: u9 = p2_dmg.min;
280
+ const p1_min: u9 = @intCast(p1_dmg.min);
281
+ const p2_min: u9 = @intCast(p2_dmg.min);
270
282
 
271
283
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
272
284
  a.p2.damage = @intCast(p2_dmg.min);
273
285
 
274
- opts.calc.overrides.actions = a;
286
+ opts.calc.overrides = a;
275
287
  opts.calc.summaries = .{};
276
- opts.chance = .{ .probability = .{}, .durations = options.durations };
288
+ opts.chance = .{ .probability = .{}, .durations = d };
277
289
  const q = &opts.chance.probability;
278
290
 
279
291
  b = battle;
@@ -283,16 +295,16 @@ pub fn transitions(
283
295
 
284
296
  const summaries = &opts.calc.summaries;
285
297
  const p1_max: u9 = if (p2_dmg.min != p2_min)
286
- p1_dmg.min
298
+ @intCast(p1_dmg.min)
287
299
  else
288
- try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
300
+ Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
289
301
  const p2_max: u9 =
290
- try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
302
+ Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
291
303
 
292
- if (opts.chance.actions.matches(template)) {
293
- if (!opts.chance.actions.eql(a)) {
304
+ if (opts.chance.actions.matches(f)) {
305
+ if (!opts.chance.actions.relax().eql(a)) {
294
306
  if (!summary) {
295
- try debug(writer, opts.chance.actions, .{
307
+ try debug(out, opts.chance.actions, .{
296
308
  .p1_max = p1_max,
297
309
  .p2_max = p2_max,
298
310
  .color = i,
@@ -306,7 +318,7 @@ pub fn transitions(
306
318
  }
307
319
 
308
320
  if (!summary) {
309
- try debug(writer, opts.chance.actions, .{
321
+ try debug(out, opts.chance.actions, .{
310
322
  .p1_max = p1_max,
311
323
  .p2_max = p2_max,
312
324
  .color = i,
@@ -319,33 +331,38 @@ pub fn transitions(
319
331
  acts.p1.damage = @intCast(p1d);
320
332
  acts.p2.damage = @intCast(p2d);
321
333
  if ((try seen.getOrPut(acts)).found_existing) {
322
- err("already seen {}", .{acts}, options.seed);
334
+ die(err, "already seen {f}", .{acts}, options.seed);
323
335
  return error.TestUnexpectedResult;
324
336
  }
325
337
  }
326
338
  }
327
-
328
339
  if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
329
340
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
341
+
342
+ q.reduce();
330
343
  try p.add(q);
344
+ p.reduce();
345
+ try r.add(q);
346
+ r.reduce();
347
+
331
348
  stats.saved += 1;
332
349
 
333
350
  if (p.q < p.p) {
334
- err("improper fraction {}", .{p}, options.seed);
351
+ die(err, "improper fraction {f}", .{p}, options.seed);
335
352
  return error.TestUnexpectedResult;
336
353
  }
337
354
  } else {
338
- if (!matches(opts.chance.actions, i, frontier.items)) {
339
- try frontier.append(opts.chance.actions);
355
+ if (!opts.chance.actions.matchesAny(frontier.items, i)) {
356
+ try frontier.append(allocator, opts.chance.actions);
340
357
 
341
- try debug(writer, opts.chance.actions, .{
358
+ try debug(out, opts.chance.actions, .{
342
359
  .p1_max = p1_max,
343
360
  .p2_max = p2_max,
344
361
  .dim = true,
345
362
  .newline = false,
346
363
  });
347
- try writer.writeAll(" → ");
348
- try debug(writer, opts.chance.actions, .{
364
+ try out.writeAll(" → ");
365
+ try debug(out, opts.chance.actions, .{
349
366
  .shape = true,
350
367
  .color = frontier.items.len - 1,
351
368
  .dim = true,
@@ -353,7 +370,7 @@ pub fn transitions(
353
370
  .indent = false,
354
371
  });
355
372
  } else if (!summary) {
356
- try debug(writer, opts.chance.actions, .{
373
+ try debug(out, opts.chance.actions, .{
357
374
  .p1_max = p1_max,
358
375
  .p2_max = p2_max,
359
376
  .dim = true,
@@ -365,15 +382,15 @@ pub fn transitions(
365
382
  p2_dmg.min = p2_max;
366
383
  }
367
384
 
368
- }}}}}}}}}}}}}}}}}}}}
385
+ }}}}}}}}}}}}}}}}}}}}}}}}}}}}
369
386
 
370
- if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
371
- p.reduce();
372
- try writer.print(
373
- " = {s} ({d:.2}%)\n\n",
374
- .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
375
- );
376
- }
387
+ assert(stats.saved > saved);
388
+ // TODO: LLVM - float_from_int from 'u256' without intrinsics
389
+ // try out.print(
390
+ // " {f} ({d:.2}%)\n = ──────────\n {f} ({d:.2}%)\n\n",
391
+ // .{r, 100 * @as(f128, @floatFromInt(r.p)) / @as(f128, @floatFromInt(r.q)),
392
+ // p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
393
+ // );
377
394
 
378
395
  }
379
396
 
@@ -388,7 +405,7 @@ pub fn transitions(
388
405
 
389
406
  p.reduce();
390
407
  if (p.p != 1 or p.q != 1) {
391
- err("expected 1, found {}", .{p}, options.seed);
408
+ die(err, "expected 1, found {f}", .{p}, options.seed);
392
409
  return error.TestExpectedEqual;
393
410
  }
394
411
 
@@ -401,7 +418,8 @@ pub fn update(
401
418
  c2: Choice,
402
419
  options: anytype,
403
420
  allocator: std.mem.Allocator,
404
- writer: anytype,
421
+ out: *std.Io.Writer,
422
+ err: *std.Io.Writer,
405
423
  transition: bool,
406
424
  ) !Result {
407
425
  const durations = options.chance.durations;
@@ -419,8 +437,8 @@ pub fn update(
419
437
  if (@TypeOf(battle.rng) == data.PRNG) {
420
438
  // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
421
439
  var buf: [helpers.MAX_DIFFS]u8 = undefined;
422
- var stream: protocol.ByteStream = .{ .buffer = &buf };
423
- const n = try helpers.diff(&original, battle, stream.writer());
440
+ var pw: protocol.Writer = .{ .buffer = &buf };
441
+ const n = try helpers.diff(&original, battle, &pw);
424
442
 
425
443
  // Applying the diff to the battle should take us back to the original copy
426
444
  // of the battle (ignoring the RNG).
@@ -434,7 +452,7 @@ pub fn update(
434
452
  // Ensure we can generate all transitions from the same original state
435
453
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
436
454
  // the transitions function relies on RNG for discovery of states)
437
- if (try transitions(unfix(original), c1, c2, allocator, writer, .{
455
+ if (try transitions(unfix(original), c1, c2, allocator, out, err, .{
438
456
  .durations = durations,
439
457
  .cap = true,
440
458
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
@@ -445,7 +463,7 @@ pub fn update(
445
463
  var override = pkmn.battle.options(
446
464
  protocol.NULL,
447
465
  Chance(Rational(u128)){ .probability = .{}, .durations = durations },
448
- Calc{ .overrides = .{ .actions = overrides } },
466
+ Calc{ .overrides = overrides },
449
467
  );
450
468
  var copy = original;
451
469
  var overridden = copy.update(c1, c2, &override);
@@ -466,8 +484,8 @@ pub fn update(
466
484
 
467
485
  override = pkmn.battle.options(
468
486
  protocol.NULL,
469
- Chance(Rational(u128)){ .probability = .{} },
470
- Calc{ .overrides = .{ .actions = overrides } },
487
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
488
+ Calc{ .overrides = overrides },
471
489
  );
472
490
  copy = original;
473
491
  overridden = copy.update(c1, c2, &override);
@@ -481,10 +499,9 @@ pub fn update(
481
499
  fn expectEqualActions(expected: Actions, actual: Actions) !void {
482
500
  return expectEqual(expected, actual) catch |e| switch (e) {
483
501
  error.TestExpectedEqual => {
484
- std.debug.print("expected {}, found {}\n", .{ expected, actual });
502
+ std.debug.print("expected {f}, found {f}\n", .{ expected, actual });
485
503
  return e;
486
504
  },
487
- else => return e,
488
505
  };
489
506
  }
490
507
 
@@ -503,19 +520,9 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
503
520
  };
504
521
  }
505
522
 
506
- fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
507
- for (frontier, 0..) |f, j| {
508
- // TODO: is skipping this redundant check worth it?
509
- if (i == j) continue;
510
- if (f.matches(actions)) return true;
511
- }
512
- return false;
513
- }
514
-
515
- fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
516
- const w = std.io.getStdErr().writer();
523
+ fn die(w: *std.Io.Writer, comptime fmt: []const u8, v: anytype, seed: ?u64) void {
517
524
  w.print(fmt, v) catch return;
518
- if (seed) |s| return w.print("{}\n", .{s}) catch return;
525
+ if (seed) |s| return w.print("{d}\n", .{s}) catch return;
519
526
  return w.writeByte('\n') catch return;
520
527
  }
521
528
 
@@ -531,7 +538,7 @@ const Style = struct {
531
538
  indent: bool = true,
532
539
  };
533
540
 
534
- fn debug(writer: anytype, actions: Actions, style: Style) !void {
541
+ fn debug(writer: *std.Io.Writer, actions: Actions, style: Style) !void {
535
542
  if (style.indent) try writer.writeAll(" ");
536
543
  if (tty) {
537
544
  const mod: usize = if (style.dim) 2 else 1;
@@ -556,13 +563,13 @@ fn debug(writer: anytype, actions: Actions, style: Style) !void {
556
563
  if (style.newline) try writer.writeByte('\n');
557
564
  }
558
565
 
559
- fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
566
+ fn format(writer: *std.Io.Writer, actions: Actions, p1: u9, p2: u9) !void {
560
567
  var input: [1024]u8 = undefined;
561
568
  var output: [1024]u8 = undefined;
562
569
 
563
- var stream = std.io.fixedBufferStream(&input);
564
- try actions.fmt(&stream.writer(), false);
565
- var len = stream.getWritten().len;
570
+ var w = std.Io.Writer.fixed(&input);
571
+ try actions.fmt(&w, false);
572
+ var len = w.buffered().len;
566
573
  @memcpy(output[0..len], input[0..len]);
567
574
 
568
575
  var find: [16]u8 = undefined;
@@ -584,8 +591,6 @@ fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
584
591
  try writer.writeAll(output[0..len]);
585
592
  }
586
593
 
587
- const NONE = @intFromEnum(Optional(bool).None);
588
-
589
594
  /// Helper functions that efficiently return valid ranges for various RNG events based on the
590
595
  /// state of an `Action` and other events to be used to construct a "transitions" function.
591
596
  pub const Rolls = struct {
@@ -597,6 +602,129 @@ pub const Rolls = struct {
597
602
  return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
598
603
  }
599
604
 
605
+ const SLP_NONE = [_]Optional(Observation(.sleep)){.None};
606
+ const SLP_STARTED = [_]Optional(Observation(.sleep)){.started};
607
+ const SLP_ENDED = [_]Optional(Observation(.sleep)){.ended};
608
+ const SLP_ALL = [_]Optional(Observation(.sleep)){ .started, .continuing, .ended };
609
+ const SLP = [_]Optional(Observation(.sleep)){ .continuing, .ended };
610
+
611
+ /// Returns a slice with a range of values for sleep given the `action` state and observed
612
+ /// `durations`.
613
+ pub fn sleep(action: Action, duration: Duration) []const Optional(Observation(.sleep)) {
614
+ if (action.sleep == .None) return &SLP_NONE;
615
+ return if (Sleeps.get(duration.sleeps, 0) == 0 and action.speed_tie != .None)
616
+ &SLP_ALL
617
+ else switch (action.sleep) {
618
+ .started => &SLP_STARTED,
619
+ else => if (Sleeps.get(duration.sleeps, 0) >= 7) &SLP_ENDED else &SLP,
620
+ };
621
+ }
622
+
623
+ const DIS_NONE = [_]Optional(Observation(.disable)){.None};
624
+ const DIS_STARTED = [_]Optional(Observation(.disable)){.started};
625
+ const DIS_CONTINUING = [_]Optional(Observation(.disable)){.continuing};
626
+ const DIS_ENDED = [_]Optional(Observation(.disable)){.ended};
627
+ const DIS_ALL = [_]Optional(Observation(.disable)){ .started, .continuing, .ended };
628
+ const DIS = [_]Optional(Observation(.disable)){ .continuing, .ended };
629
+
630
+ /// Returns a slice with a range of values for disable given the `action`, observed `durations`,
631
+ /// and the state of the `parent` (observation of the player's Pokémon sleep status).
632
+ pub fn disable(
633
+ action: Action,
634
+ duration: Duration,
635
+ parent: Optional(Observation(.sleep)),
636
+ ) []const Optional(Observation(.disable)) {
637
+ if (action.disable == .None) return &DIS_NONE;
638
+ return if (duration.disable == 0 and action.speed_tie != .None)
639
+ &DIS_ALL
640
+ else switch (action.disable) {
641
+ .started => &DIS_STARTED,
642
+ else => if (duration.disable >= 8)
643
+ &DIS_ENDED
644
+ else if (parent != .None and parent != .started)
645
+ &DIS_CONTINUING
646
+ else
647
+ &DIS,
648
+ };
649
+ }
650
+
651
+ const ATK_NONE = [_]Optional(Observation(.attacking)){.None};
652
+ const ATK_STARTED = [_]Optional(Observation(.attacking)){.started};
653
+ const ATK_CONTINUING = [_]Optional(Observation(.attacking)){.continuing};
654
+ const ATK_ENDED = [_]Optional(Observation(.attacking)){.ended};
655
+ const ATK = [_]Optional(Observation(.attacking)){ .continuing, .ended };
656
+
657
+ /// FIXME document
658
+ pub fn attacking(
659
+ action: Action,
660
+ duration: Duration,
661
+ parent: Optional(Observation(.sleep)),
662
+ ) []const Optional(Observation(.attacking)) {
663
+ return switch (action.attacking) {
664
+ .None => &ATK_NONE,
665
+ .started => &ATK_STARTED,
666
+ else => if (duration.attacking >= 3)
667
+ &ATK_ENDED
668
+ else if ((parent == .continuing and parent == .ended) or duration.attacking < 2)
669
+ &ATK_CONTINUING
670
+ else
671
+ &ATK,
672
+ };
673
+ }
674
+
675
+ const CFZ_NONE = [_]Optional(Observation(.confusion)){.None};
676
+ const CFZ_STARTED = [_]Optional(Observation(.confusion)){.started};
677
+ const CFZ_OVERWRITTEN = [_]Optional(Observation(.confusion)){ .started, .overwritten };
678
+ const CFZ_CONTINUING = [_]Optional(Observation(.confusion)){.continuing};
679
+ const CFZ_ENDED = [_]Optional(Observation(.confusion)){.ended};
680
+ const CFZ = [_]Optional(Observation(.confusion)){ .continuing, .ended };
681
+ const CFZ_TIE = [_]Optional(Observation(.confusion)){ .started, .continuing, .ended };
682
+
683
+ /// FIXME document
684
+ pub fn confusion(
685
+ action: Action,
686
+ duration: Duration,
687
+ tie: Optional(Player),
688
+ sibling: Optional(Observation(.attacking)),
689
+ parent: Optional(Observation(.sleep)),
690
+ ) []const Optional(Observation(.confusion)) {
691
+ return switch (action.confusion) {
692
+ .None => &CFZ_NONE,
693
+ .started => if (pkmn.options.overwrite and sibling == .ended and duration.confusion < 5)
694
+ &CFZ_OVERWRITTEN
695
+ else
696
+ &CFZ_STARTED,
697
+ else => if (duration.confusion >= 5)
698
+ &CFZ_ENDED
699
+ else if ((parent == .continuing and parent == .ended) or duration.confusion < 2)
700
+ &CFZ_CONTINUING
701
+ else if (tie != .None)
702
+ &CFZ_TIE
703
+ else
704
+ &CFZ,
705
+ };
706
+ }
707
+
708
+ const BND_NONE = [_]Optional(Observation(.binding)){.None};
709
+ const BND_STARTED = [_]Optional(Observation(.binding)){.started};
710
+ const BND_ENDED = [_]Optional(Observation(.binding)){.ended};
711
+ const BND = [_]Optional(Observation(.binding)){ .continuing, .ended };
712
+
713
+ /// Returns a slice with a range of values for binding given the `action`, observed `durations`,
714
+ /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
715
+ pub fn binding(
716
+ action: Action,
717
+ duration: Duration,
718
+ parent: Optional(bool),
719
+ ) []const Optional(Observation(.binding)) {
720
+ if (parent == .true) return &BND_NONE;
721
+ return switch (action.binding) {
722
+ .None => &BND_NONE,
723
+ .started => &BND_STARTED,
724
+ else => if (duration.binding >= 4) &BND_ENDED else &BND,
725
+ };
726
+ }
727
+
600
728
  const BOOL_NONE = [_]Optional(bool){.None};
601
729
  const BOOLS = [_]Optional(bool){ .false, .true };
602
730
 
@@ -610,7 +738,7 @@ pub const Rolls = struct {
610
738
  /// Returns a slice with the correct range of values for critical hits given the `action` state
611
739
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
612
740
  pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
613
- if (parent == .false) return &BOOL_NONE;
741
+ if (pkmn.options.showdown and parent == .false) return &BOOL_NONE;
614
742
  return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
615
743
  }
616
744
 
@@ -622,7 +750,7 @@ pub const Rolls = struct {
622
750
  }
623
751
 
624
752
  /// The min and max bounds on iteration over damage rolls.
625
- pub const Range = struct { min: u9, max: u9 };
753
+ pub const Range = struct { min: usize, max: usize };
626
754
 
627
755
  /// Returns the range bounding damage rolls given the `action` state and the state of
628
756
  /// the `parent` (whether the player's Pokémon's move hit).
@@ -635,7 +763,7 @@ pub const Rolls = struct {
635
763
 
636
764
  /// Returns the max damage roll which will produce the same damage as `roll`
637
765
  /// given the base damage in `summaries`.
638
- pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) !u8 {
766
+ pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) u8 {
639
767
  if (roll == 0) return roll;
640
768
 
641
769
  const dmg = summaries.get(player.foe()).damage;
@@ -645,9 +773,14 @@ pub const Rolls = struct {
645
773
  return @min(255, roll + ((254 - ((@as(u32, dmg.base) * roll) % 255)) / dmg.base));
646
774
  }
647
775
 
648
- /// Returns a slice with the correct range of values for confused.
649
- pub fn confused(action: Action) []const Optional(bool) {
650
- return if (action.confused == .None) &BOOL_NONE else &BOOLS;
776
+ /// Returns a slice with the correct range of values for confused given the `action` state and
777
+ /// the state of the `parent` (observation of the player's Pokémon confusion status).
778
+ pub fn confused(
779
+ action: Action,
780
+ parent: Optional(Observation(.confusion)),
781
+ ) []const Optional(bool) {
782
+ const done = parent == .None or parent == .ended;
783
+ return if (done or action.confused == .None) &BOOL_NONE else &BOOLS;
651
784
  }
652
785
 
653
786
  /// Returns a slice with the correct range of values for paralysis given the `action` state
@@ -657,35 +790,24 @@ pub const Rolls = struct {
657
790
  return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
658
791
  }
659
792
 
660
- const DURATION_NONE = [_]u2{0};
661
- // NB: DURATION must be at least 2 because that is the maximum of the minimums for all rolls
662
- const DURATION = [_]u2{2};
663
-
664
- /// Returns a slice with a range of values for duration given the `action` state
665
- /// and the state of the `parent` (whether the player's Pokémon's move hit).
666
- pub fn duration(action: Action, parent: Optional(bool)) []const u2 {
667
- if (parent == .false) return &DURATION_NONE;
668
- return if (action.duration == 0) &DURATION_NONE else &DURATION;
669
- }
670
-
671
- const SLOT_NONE = [_]u4{0};
672
- const SLOT = [_]u4{ 1, 2, 3, 4 };
793
+ const SLOT_NONE = [_]u3{0};
794
+ const SLOT = [_]u3{ 1, 2, 3, 4 };
673
795
 
674
796
  /// Returns a slice with a range of values for move slots given the `action` state
675
797
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
676
798
  ///
677
799
  /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
678
- pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
800
+ pub fn moveSlot(action: Action, parent: Optional(bool)) []const u3 {
679
801
  if (parent == .false) return &SLOT_NONE;
680
802
  return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
681
803
  }
682
804
 
683
- const MULTI_NONE = [_]u4{0};
684
- const MULTI = [_]u4{ 2, 3, 4, 5 };
805
+ const MULTI_NONE = [_]u3{0};
806
+ const MULTI = [_]u3{ 2, 3, 4, 5 };
685
807
 
686
808
  /// Returns a slice with the correct range of values for multi hit given the `action` state
687
809
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
688
- pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
810
+ pub fn multiHit(action: Action, parent: Optional(bool)) []const u3 {
689
811
  if (parent == .false) return &MULTI_NONE;
690
812
  return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
691
813
  }
@@ -725,6 +847,26 @@ test "Rolls.speedTie" {
725
847
  try expectEqualSlices(Optional(Player), &.{.None}, Rolls.speedTie(actions.p2));
726
848
  }
727
849
 
850
+ test "Rolls.sleep" {
851
+ return error.SkipZigTest; // TODO
852
+ }
853
+
854
+ test "Rolls.disable" {
855
+ return error.SkipZigTest; // TODO
856
+ }
857
+
858
+ test "Rolls.confusion" {
859
+ return error.SkipZigTest; // TODO
860
+ }
861
+
862
+ test "Rolls.atacking" {
863
+ return error.SkipZigTest; // TODO
864
+ }
865
+
866
+ test "Rolls.binding" {
867
+ return error.SkipZigTest; // TODO
868
+ }
869
+
728
870
  test "Rolls.damage" {
729
871
  const actions: Actions = .{ .p2 = .{ .damage = 221 } };
730
872
  try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p1, .None));
@@ -734,12 +876,12 @@ test "Rolls.damage" {
734
876
 
735
877
  test "Rolls.coalesce" {
736
878
  var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
737
- try expectEqual(0, try Rolls.coalesce(.P2, 0, &summaries, false));
738
- try expectEqual(241, try Rolls.coalesce(.P2, 238, &summaries, false));
739
- try expectEqual(255, try Rolls.coalesce(.P2, 238, &summaries, true));
879
+ try expectEqual(@as(u8, 0), Rolls.coalesce(.P2, 0, &summaries, false));
880
+ try expectEqual(@as(u8, 241), Rolls.coalesce(.P2, 238, &summaries, false));
881
+ try expectEqual(@as(u8, 255), Rolls.coalesce(.P2, 238, &summaries, true));
740
882
  summaries.p1.damage.final = 74;
741
- try expectEqual(217, try Rolls.coalesce(.P2, 217, &summaries, false));
742
- try expectEqual(255, try Rolls.coalesce(.P2, 217, &summaries, true));
883
+ try expectEqual(@as(u8, 217), Rolls.coalesce(.P2, 217, &summaries, false));
884
+ try expectEqual(@as(u8, 255), Rolls.coalesce(.P2, 217, &summaries, true));
743
885
  }
744
886
 
745
887
  test "Rolls.hit" {
@@ -768,14 +910,23 @@ test "Rolls.criticalHit" {
768
910
  &.{ .false, .true },
769
911
  Rolls.criticalHit(actions.p1, .None),
770
912
  );
771
- try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p1, .false));
913
+ try expectEqualSlices(
914
+ Optional(bool),
915
+ if (pkmn.options.showdown) &.{.None} else &.{ .false, .true },
916
+ Rolls.criticalHit(actions.p1, .false),
917
+ );
772
918
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
773
919
  }
774
920
 
775
921
  test "Rolls.confused" {
776
922
  const actions: Actions = .{ .p2 = .{ .confused = .true } };
777
- try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1));
778
- try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.confused(actions.p2));
923
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1, .None));
924
+ try expectEqualSlices(
925
+ Optional(bool),
926
+ &.{ .false, .true },
927
+ Rolls.confused(actions.p2, .continuing),
928
+ );
929
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p2, .ended));
779
930
  }
780
931
 
781
932
  test "Rolls.paralyzed" {
@@ -786,25 +937,18 @@ test "Rolls.paralyzed" {
786
937
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p2, .true));
787
938
  }
788
939
 
789
- test "Rolls.duration" {
790
- const actions: Actions = .{ .p2 = .{ .duration = 3 } };
791
- try expectEqualSlices(u2, &.{0}, Rolls.duration(actions.p1, .None));
792
- try expectEqualSlices(u2, &.{2}, Rolls.duration(actions.p2, .None));
793
- try expectEqualSlices(u2, &.{0}, Rolls.duration(actions.p2, .false));
794
- }
795
-
796
940
  test "Rolls.moveSlot" {
797
941
  const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
798
- try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
799
- try expectEqualSlices(u4, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
800
- try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p2, .false));
942
+ try expectEqualSlices(u3, &.{0}, Rolls.moveSlot(actions.p1, .None));
943
+ try expectEqualSlices(u3, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
944
+ try expectEqualSlices(u3, &.{0}, Rolls.moveSlot(actions.p2, .false));
801
945
  }
802
946
 
803
947
  test "Rolls.multiHit" {
804
948
  const actions: Actions = .{ .p2 = .{ .multi_hit = 3 } };
805
- try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p1, .None));
806
- try expectEqualSlices(u4, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
807
- try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p2, .false));
949
+ try expectEqualSlices(u3, &.{0}, Rolls.multiHit(actions.p1, .None));
950
+ try expectEqualSlices(u3, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
951
+ try expectEqualSlices(u3, &.{0}, Rolls.multiHit(actions.p2, .false));
808
952
  }
809
953
 
810
954
  test "Rolls.metronome" {
@@ -823,9 +967,9 @@ test "Rolls.psywave" {
823
967
 
824
968
  try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p1, &side, .None));
825
969
  var rolls = Rolls.psywave(actions.p2, &side, .None);
826
- try expectEqual(150, rolls[rolls.len - 1]);
970
+ try expectEqual(@as(u8, 150), rolls[rolls.len - 1]);
827
971
  side.stored().level = 81;
828
972
  rolls = Rolls.psywave(actions.p2, &side, .None);
829
- try expectEqual(121, rolls[rolls.len - 1]);
973
+ try expectEqual(@as(u8, 121), rolls[rolls.len - 1]);
830
974
  try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p2, &side, .false));
831
975
  }