@pkmn/engine 0.1.0-dev.0cabf16e → 0.1.0-dev.0e82a5ac

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 (72) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +28 -13
  3. package/build/pkg/addon/browser.d.ts +1 -1
  4. package/build/pkg/addon/browser.js +6 -2
  5. package/build/pkg/addon/browser.js.map +1 -1
  6. package/build/pkg/addon/node.js +22 -8
  7. package/build/pkg/addon/node.js.map +1 -1
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +53 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +25 -5
  12. package/build/pkg/addon.js +8 -17
  13. package/build/pkg/addon.js.map +1 -1
  14. package/build/pkg/data/layout.json +15 -2
  15. package/build/pkg/data/protocol.json +23 -9
  16. package/build/pkg/gen1.d.ts +15 -18
  17. package/build/pkg/gen1.js +28 -19
  18. package/build/pkg/gen1.js.map +1 -1
  19. package/build/pkg/index.d.ts +37 -100
  20. package/build/pkg/index.js +23 -75
  21. package/build/pkg/index.js.map +1 -1
  22. package/build/pkg/protocol.d.ts +11 -2
  23. package/build/pkg/protocol.js.map +1 -1
  24. package/build/tools/debug.js +55 -12
  25. package/build/tools/debug.js.map +1 -1
  26. package/build/tools/display/debug.jsx +19 -94
  27. package/build/tools/display/debug.jsx.map +1 -1
  28. package/build/tools/display/index.js +35 -15
  29. package/build/tools/display/index.js.map +1 -1
  30. package/build/tools/display/ui.jsx +18 -8
  31. package/build/tools/display/ui.jsx.map +1 -1
  32. package/build/tools/display/util.js +140 -1
  33. package/build/tools/display/util.js.map +1 -1
  34. package/build.zig +236 -216
  35. package/package.json +35 -131
  36. package/src/bin/install-pkmn-engine +25 -9
  37. package/src/lib/bindings/c.zig +200 -0
  38. package/src/lib/bindings/node.zig +117 -0
  39. package/src/lib/bindings/wasm.zig +50 -0
  40. package/src/lib/common/array.zig +5 -12
  41. package/src/lib/common/debug.zig +33 -59
  42. package/src/lib/common/js.zig +3 -9
  43. package/src/lib/common/optional.zig +11 -21
  44. package/src/lib/common/options.zig +2 -0
  45. package/src/lib/common/protocol.zig +228 -197
  46. package/src/lib/common/rational.zig +278 -285
  47. package/src/lib/common/rng.zig +1 -3
  48. package/src/lib/common/util.zig +14 -31
  49. package/src/lib/gen1/calc.zig +185 -182
  50. package/src/lib/gen1/chance.zig +146 -93
  51. package/src/lib/gen1/data/moves.zig +2 -4
  52. package/src/lib/gen1/data/species.zig +1 -3
  53. package/src/lib/gen1/data/types.zig +3 -4
  54. package/src/lib/gen1/data.zig +12 -16
  55. package/src/lib/gen1/helpers.zig +12 -21
  56. package/src/lib/gen1/mechanics.zig +103 -78
  57. package/src/lib/gen2/calc.zig +12 -23
  58. package/src/lib/gen2/chance.zig +12 -18
  59. package/src/lib/gen2/data/items.zig +1 -3
  60. package/src/lib/gen2/data/moves.zig +2 -4
  61. package/src/lib/gen2/data/species.zig +1 -3
  62. package/src/lib/gen2/data/types.zig +7 -5
  63. package/src/lib/gen2/data.zig +19 -22
  64. package/src/lib/gen2/generated.zig +18 -23
  65. package/src/lib/gen2/helpers.zig +9 -18
  66. package/src/lib/gen2/mechanics.zig +58 -55
  67. package/src/lib/napi.h +2 -0
  68. package/src/lib/node.zig +4 -121
  69. package/src/lib/pkmn.zig +98 -19
  70. package/src/lib/wasm.zig +9 -9
  71. package/src/tools/display/dom.ts +5 -8
  72. package/src/tools/display/ui.css +0 -5
@@ -1,52 +1,39 @@
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
-
28
- const Optional = optional.Optional;
29
-
30
- const Rational = rational.Rational;
31
-
32
- const PointerType = util.PointerType;
33
25
  const isPointerTo = util.isPointerTo;
34
-
35
- const Actions = chance.Actions;
36
- const Action = chance.Action;
37
- const Chance = chance.Chance;
38
- const Confusion = chance.Confusion;
39
- const Duration = chance.Duration;
40
- const Durations = chance.Durations;
41
26
  const Observation = chance.Observation;
27
+ const Optional = optional.Optional;
28
+ const Player = common.Player;
29
+ const PointerType = util.PointerType;
30
+ const Rational = rational.Rational;
31
+ const Result = common.Result;
42
32
  const Sleeps = chance.Sleeps;
43
33
 
44
34
  const tty = true; // DEBUG
45
35
  const summary = false; // DEBUG
46
36
 
47
- const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
48
- const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
49
-
50
37
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
51
38
  pub const Summaries = extern struct {
52
39
  /// Relevant information for Player 1.
@@ -108,13 +95,13 @@ pub const Calc = struct {
108
95
  self: Calc,
109
96
  player: Player,
110
97
  comptime field: Action.Field,
111
- ) ?std.meta.FieldType(Action, field) {
98
+ ) ?@FieldType(Action, @tagName(field)) {
112
99
  if (!enabled) return null;
113
100
 
114
101
  const val = @field(self.overrides.get(player), @tagName(field));
115
102
  return if (switch (@typeInfo(@TypeOf(val))) {
116
- Enum => val != .None,
117
- Int => val != 0,
103
+ .@"enum" => val != .None,
104
+ .int => val != 0,
118
105
  else => unreachable,
119
106
  }) val else null;
120
107
  }
@@ -155,7 +142,7 @@ const Null = struct {
155
142
  self: Null,
156
143
  player: Player,
157
144
  comptime field: Action.Field,
158
- ) ?std.meta.FieldType(Action, field) {
145
+ ) ?@FieldType(Action, @tagName(field)) {
159
146
  _ = .{ self, player };
160
147
  return null;
161
148
  }
@@ -191,30 +178,31 @@ pub const Options = struct {
191
178
  metronome: bool = false,
192
179
  };
193
180
 
194
- pub const MAX_FRONTIER = 49;
181
+ pub const MAX_FRONTIER = 83;
195
182
 
196
183
  pub fn transitions(
197
184
  battle: anytype,
198
185
  c1: Choice,
199
186
  c2: Choice,
200
187
  allocator: std.mem.Allocator,
201
- writer: anytype,
188
+ out: *std.Io.Writer,
189
+ err: *std.Io.Writer,
202
190
  options: Options,
203
191
  ) !?Stats {
204
192
  var stats: Stats = .{};
205
193
 
206
194
  const cap = options.cap;
207
195
 
208
- var seen = std.AutoHashMap(Actions, void).init(allocator);
196
+ var seen: std.AutoHashMap(Actions, void) = .init(allocator);
209
197
  defer seen.deinit();
210
- var frontier = std.ArrayList(Actions).init(allocator);
211
- defer frontier.deinit();
198
+ var frontier: std.array_list.Aligned(Actions, null) = .empty;
199
+ defer frontier.deinit(allocator);
212
200
 
213
- const durations = options.durations;
201
+ const d = options.durations;
214
202
 
215
203
  var opts = pkmn.battle.options(
216
204
  protocol.NULL,
217
- Chance(Rational(u128)){ .probability = .{}, .durations = durations },
205
+ Chance(Rational(u128)){ .probability = .{}, .durations = d },
218
206
  Calc{},
219
207
  );
220
208
 
@@ -225,8 +213,8 @@ pub fn transitions(
225
213
  const p1 = b.side(.P1);
226
214
  const p2 = b.side(.P2);
227
215
 
228
- var p: Rational(u128) = .{ .p = 0, .q = 1 };
229
- try frontier.append(opts.chance.actions);
216
+ var p: Rational(u256) = .{ .p = 0, .q = 1 };
217
+ try frontier.append(allocator, opts.chance.actions);
230
218
 
231
219
  // zig fmt: off
232
220
  for (Rolls.metronome(frontier.items[0].p1)) |p1_move| {
@@ -240,7 +228,9 @@ pub fn transitions(
240
228
  const f = frontier.items[i];
241
229
  const saved = stats.saved;
242
230
 
243
- try debug(writer, f, .{
231
+ var r: Rational(u256) = .{ .p = 0, .q = 1 };
232
+
233
+ try debug(out, f, .{
244
234
  .shape = true,
245
235
  .color = i,
246
236
  .bold = true,
@@ -254,20 +244,20 @@ pub fn transitions(
254
244
  };
255
245
 
256
246
  for (Rolls.speedTie(f.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
257
- for (Rolls.sleep(f.p1, durations.p1)) |p1_slp| { a.p1.sleep = p1_slp;
258
- for (Rolls.sleep(f.p2, durations.p2)) |p2_slp| { a.p2.sleep = p2_slp;
259
- for (Rolls.disable(f.p1, durations.p1, p1_slp)) |p1_dis| { a.p1.disable = p1_dis;
260
- for (Rolls.disable(f.p2, durations.p2, p2_slp)) |p2_dis| { a.p2.disable = p2_dis;
261
- for (Rolls.attacking(f.p1, durations.p1)) |p1_atk| { a.p1.attacking = p1_atk;
262
- for (Rolls.attacking(f.p2, durations.p2)) |p2_atk| { a.p2.attacking = p2_atk;
263
- for (Rolls.confusion(f.p1, durations.p1, p1_atk, p1_slp)) |p1_cfz| { a.p1.confusion = p1_cfz;
264
- for (Rolls.confusion(f.p2, durations.p2, p2_atk, p2_slp)) |p2_cfz| { a.p2.confusion = p2_cfz;
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;
265
255
  for (Rolls.confused(f.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
266
256
  for (Rolls.confused(f.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
267
257
  for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
268
258
  for (Rolls.paralyzed(f.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
269
- for (Rolls.binding(f.p1, durations.p1, p1_par)) |p1_bind| { a.p1.binding = p1_bind;
270
- for (Rolls.binding(f.p2, durations.p2, p2_par)) |p2_bind| { a.p2.binding = p2_bind;
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;
271
261
  for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
272
262
  for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
273
263
  for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
@@ -287,15 +277,15 @@ pub fn transitions(
287
277
 
288
278
  var p2_dmg = Rolls.damage(f.p2, p2_hit);
289
279
 
290
- const p1_min: u9 = p1_dmg.min;
291
- 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);
292
282
 
293
283
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
294
284
  a.p2.damage = @intCast(p2_dmg.min);
295
285
 
296
286
  opts.calc.overrides = a;
297
287
  opts.calc.summaries = .{};
298
- opts.chance = .{ .probability = .{}, .durations = durations };
288
+ opts.chance = .{ .probability = .{}, .durations = d };
299
289
  const q = &opts.chance.probability;
300
290
 
301
291
  b = battle;
@@ -305,16 +295,16 @@ pub fn transitions(
305
295
 
306
296
  const summaries = &opts.calc.summaries;
307
297
  const p1_max: u9 = if (p2_dmg.min != p2_min)
308
- p1_dmg.min
298
+ @intCast(p1_dmg.min)
309
299
  else
310
- try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
300
+ Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
311
301
  const p2_max: u9 =
312
- try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
302
+ Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
313
303
 
314
304
  if (opts.chance.actions.matches(f)) {
315
- if (!map(opts.chance.actions).eql(a)) {
305
+ if (!opts.chance.actions.relax().eql(a)) {
316
306
  if (!summary) {
317
- try debug(writer, opts.chance.actions, .{
307
+ try debug(out, opts.chance.actions, .{
318
308
  .p1_max = p1_max,
319
309
  .p2_max = p2_max,
320
310
  .color = i,
@@ -328,7 +318,7 @@ pub fn transitions(
328
318
  }
329
319
 
330
320
  if (!summary) {
331
- try debug(writer, opts.chance.actions, .{
321
+ try debug(out, opts.chance.actions, .{
332
322
  .p1_max = p1_max,
333
323
  .p2_max = p2_max,
334
324
  .color = i,
@@ -341,33 +331,38 @@ pub fn transitions(
341
331
  acts.p1.damage = @intCast(p1d);
342
332
  acts.p2.damage = @intCast(p2d);
343
333
  if ((try seen.getOrPut(acts)).found_existing) {
344
- err("already seen {}", .{acts}, options.seed);
334
+ die(err, "already seen {f}", .{acts}, options.seed);
345
335
  return error.TestUnexpectedResult;
346
336
  }
347
337
  }
348
338
  }
349
-
350
339
  if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
351
340
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
341
+
342
+ q.reduce();
352
343
  try p.add(q);
344
+ p.reduce();
345
+ try r.add(q);
346
+ r.reduce();
347
+
353
348
  stats.saved += 1;
354
349
 
355
350
  if (p.q < p.p) {
356
- err("improper fraction {}", .{p}, options.seed);
351
+ die(err, "improper fraction {f}", .{p}, options.seed);
357
352
  return error.TestUnexpectedResult;
358
353
  }
359
354
  } else {
360
- if (!matches(opts.chance.actions, i, frontier.items)) {
361
- try frontier.append(opts.chance.actions);
355
+ if (!opts.chance.actions.matchesAny(frontier.items, i)) {
356
+ try frontier.append(allocator, opts.chance.actions);
362
357
 
363
- try debug(writer, opts.chance.actions, .{
358
+ try debug(out, opts.chance.actions, .{
364
359
  .p1_max = p1_max,
365
360
  .p2_max = p2_max,
366
361
  .dim = true,
367
362
  .newline = false,
368
363
  });
369
- try writer.writeAll(" → ");
370
- try debug(writer, opts.chance.actions, .{
364
+ try out.writeAll(" → ");
365
+ try debug(out, opts.chance.actions, .{
371
366
  .shape = true,
372
367
  .color = frontier.items.len - 1,
373
368
  .dim = true,
@@ -375,7 +370,7 @@ pub fn transitions(
375
370
  .indent = false,
376
371
  });
377
372
  } else if (!summary) {
378
- try debug(writer, opts.chance.actions, .{
373
+ try debug(out, opts.chance.actions, .{
379
374
  .p1_max = p1_max,
380
375
  .p2_max = p2_max,
381
376
  .dim = true,
@@ -390,14 +385,12 @@ pub fn transitions(
390
385
  }}}}}}}}}}}}}}}}}}}}}}}}}}}}
391
386
 
392
387
  assert(stats.saved > saved);
393
- const old = @hasDecl(std.fmt, "formatFloatDecimal");
394
- if (!old and @TypeOf(writer) != @TypeOf(std.io.null_writer)) {
395
- p.reduce();
396
- try writer.print(
397
- " = {s} ({d:.2}%)\n\n",
398
- .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
399
- );
400
- }
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
+ // );
401
394
 
402
395
  }
403
396
 
@@ -412,7 +405,7 @@ pub fn transitions(
412
405
 
413
406
  p.reduce();
414
407
  if (p.p != 1 or p.q != 1) {
415
- err("expected 1, found {}", .{p}, options.seed);
408
+ die(err, "expected 1, found {f}", .{p}, options.seed);
416
409
  return error.TestExpectedEqual;
417
410
  }
418
411
 
@@ -425,7 +418,8 @@ pub fn update(
425
418
  c2: Choice,
426
419
  options: anytype,
427
420
  allocator: std.mem.Allocator,
428
- writer: anytype,
421
+ out: *std.Io.Writer,
422
+ err: *std.Io.Writer,
429
423
  transition: bool,
430
424
  ) !Result {
431
425
  const durations = options.chance.durations;
@@ -443,8 +437,8 @@ pub fn update(
443
437
  if (@TypeOf(battle.rng) == data.PRNG) {
444
438
  // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
445
439
  var buf: [helpers.MAX_DIFFS]u8 = undefined;
446
- var stream: protocol.ByteStream = .{ .buffer = &buf };
447
- 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);
448
442
 
449
443
  // Applying the diff to the battle should take us back to the original copy
450
444
  // of the battle (ignoring the RNG).
@@ -458,7 +452,7 @@ pub fn update(
458
452
  // Ensure we can generate all transitions from the same original state
459
453
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
460
454
  // the transitions function relies on RNG for discovery of states)
461
- if (try transitions(unfix(original), c1, c2, allocator, writer, .{
455
+ if (try transitions(unfix(original), c1, c2, allocator, out, err, .{
462
456
  .durations = durations,
463
457
  .cap = true,
464
458
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
@@ -505,10 +499,9 @@ pub fn update(
505
499
  fn expectEqualActions(expected: Actions, actual: Actions) !void {
506
500
  return expectEqual(expected, actual) catch |e| switch (e) {
507
501
  error.TestExpectedEqual => {
508
- std.debug.print("expected {}, found {}\n", .{ expected, actual });
502
+ std.debug.print("expected {f}, found {f}\n", .{ expected, actual });
509
503
  return e;
510
504
  },
511
- else => return e,
512
505
  };
513
506
  }
514
507
 
@@ -527,27 +520,9 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
527
520
  };
528
521
  }
529
522
 
530
- fn map(actions: Actions) Actions {
531
- if (!pkmn.options.overwrite) return actions;
532
- var a = actions;
533
- if (a.p1.confusion == .overwritten) a.p1.confusion = .continuing;
534
- if (a.p2.confusion == .overwritten) a.p2.confusion = .continuing;
535
- return a;
536
- }
537
-
538
- fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
539
- for (frontier, 0..) |f, j| {
540
- // TODO: is skipping this redundant check worth it?
541
- if (i == j) continue;
542
- if (f.matches(actions)) return true;
543
- }
544
- return false;
545
- }
546
-
547
- fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
548
- const w = std.io.getStdErr().writer();
523
+ fn die(w: *std.Io.Writer, comptime fmt: []const u8, v: anytype, seed: ?u64) void {
549
524
  w.print(fmt, v) catch return;
550
- if (seed) |s| return w.print("{}\n", .{s}) catch return;
525
+ if (seed) |s| return w.print("{d}\n", .{s}) catch return;
551
526
  return w.writeByte('\n') catch return;
552
527
  }
553
528
 
@@ -563,7 +538,7 @@ const Style = struct {
563
538
  indent: bool = true,
564
539
  };
565
540
 
566
- fn debug(writer: anytype, actions: Actions, style: Style) !void {
541
+ fn debug(writer: *std.Io.Writer, actions: Actions, style: Style) !void {
567
542
  if (style.indent) try writer.writeAll(" ");
568
543
  if (tty) {
569
544
  const mod: usize = if (style.dim) 2 else 1;
@@ -588,13 +563,13 @@ fn debug(writer: anytype, actions: Actions, style: Style) !void {
588
563
  if (style.newline) try writer.writeByte('\n');
589
564
  }
590
565
 
591
- 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 {
592
567
  var input: [1024]u8 = undefined;
593
568
  var output: [1024]u8 = undefined;
594
569
 
595
- var stream = std.io.fixedBufferStream(&input);
596
- try actions.fmt(&stream.writer(), false);
597
- 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;
598
573
  @memcpy(output[0..len], input[0..len]);
599
574
 
600
575
  var find: [16]u8 = undefined;
@@ -627,88 +602,113 @@ pub const Rolls = struct {
627
602
  return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
628
603
  }
629
604
 
630
- const OBS_NONE = [_]Optional(Observation){.None};
631
- const OBS_STARTED = [_]Optional(Observation){.started};
632
- const OBS_CONTINUING = [_]Optional(Observation){.continuing};
633
- const OBS_ENDED = [_]Optional(Observation){.ended};
634
- const OBS_ALL = [_]Optional(Observation){ .started, .continuing, .ended };
635
- const OBS = [_]Optional(Observation){ .continuing, .ended };
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 };
636
610
 
637
611
  /// Returns a slice with a range of values for sleep given the `action` state and observed
638
612
  /// `durations`.
639
- pub fn sleep(action: Action, duration: Duration) []const Optional(Observation) {
640
- if (action.sleep == .None) return &OBS_NONE;
613
+ pub fn sleep(action: Action, duration: Duration) []const Optional(Observation(.sleep)) {
614
+ if (action.sleep == .None) return &SLP_NONE;
641
615
  return if (Sleeps.get(duration.sleeps, 0) == 0 and action.speed_tie != .None)
642
- &OBS_ALL
616
+ &SLP_ALL
643
617
  else switch (action.sleep) {
644
- .started => &OBS_STARTED,
645
- else => if (Sleeps.get(duration.sleeps, 0) >= 7) &OBS_ENDED else &OBS,
618
+ .started => &SLP_STARTED,
619
+ else => if (Sleeps.get(duration.sleeps, 0) >= 7) &SLP_ENDED else &SLP,
646
620
  };
647
621
  }
648
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
+
649
630
  /// Returns a slice with a range of values for disable given the `action`, observed `durations`,
650
631
  /// and the state of the `parent` (observation of the player's Pokémon sleep status).
651
632
  pub fn disable(
652
633
  action: Action,
653
634
  duration: Duration,
654
- parent: Optional(Observation),
655
- ) []const Optional(Observation) {
656
- if (action.disable == .None) return &OBS_NONE;
635
+ parent: Optional(Observation(.sleep)),
636
+ ) []const Optional(Observation(.disable)) {
637
+ if (action.disable == .None) return &DIS_NONE;
657
638
  return if (duration.disable == 0 and action.speed_tie != .None)
658
- &OBS_ALL
639
+ &DIS_ALL
659
640
  else switch (action.disable) {
660
- .started => &OBS_STARTED,
641
+ .started => &DIS_STARTED,
661
642
  else => if (duration.disable >= 8)
662
- &OBS_ENDED
643
+ &DIS_ENDED
663
644
  else if (parent != .None and parent != .started)
664
- &OBS_CONTINUING
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
665
670
  else
666
- &OBS,
671
+ &ATK,
667
672
  };
668
673
  }
669
674
 
670
- const CFZ_NONE = [_]Optional(Confusion){.None};
671
- const CFZ_STARTED = [_]Optional(Confusion){.started};
672
- const CFZ_OVERWRITTEN = [_]Optional(Confusion){ .started, .overwritten };
673
- const CFZ_CONTINUING = [_]Optional(Confusion){.continuing};
674
- const CFZ_ENDED = [_]Optional(Confusion){.ended};
675
- const CFZ = [_]Optional(Confusion){ .continuing, .ended };
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 };
676
682
 
677
- /// FIXME
683
+ /// FIXME document
678
684
  pub fn confusion(
679
685
  action: Action,
680
686
  duration: Duration,
681
- sibling: Optional(Observation),
682
- parent: Optional(Observation),
683
- ) []const Optional(Confusion) {
687
+ tie: Optional(Player),
688
+ sibling: Optional(Observation(.attacking)),
689
+ parent: Optional(Observation(.sleep)),
690
+ ) []const Optional(Observation(.confusion)) {
684
691
  return switch (action.confusion) {
685
692
  .None => &CFZ_NONE,
686
- .started => if (sibling == .ended)
693
+ .started => if (pkmn.options.overwrite and sibling == .ended and duration.confusion < 5)
687
694
  &CFZ_OVERWRITTEN
688
695
  else
689
696
  &CFZ_STARTED,
690
697
  else => if (duration.confusion >= 5)
691
698
  &CFZ_ENDED
692
- else if ((parent != .None and parent != .started) or duration.confusion < 2)
699
+ else if ((parent == .continuing and parent == .ended) or duration.confusion < 2)
693
700
  &CFZ_CONTINUING
701
+ else if (tie != .None)
702
+ &CFZ_TIE
694
703
  else
695
704
  &CFZ,
696
705
  };
697
706
  }
698
707
 
699
- /// FIXME
700
- pub fn attacking(action: Action, duration: Duration) []const Optional(Observation) {
701
- return switch (action.attacking) {
702
- .None => &OBS_NONE,
703
- .started => &OBS_STARTED,
704
- else => if (duration.attacking >= 3)
705
- &OBS_ENDED
706
- else if (duration.attacking < 2)
707
- &OBS_CONTINUING
708
- else
709
- &OBS,
710
- };
711
- }
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
712
 
713
713
  /// Returns a slice with a range of values for binding given the `action`, observed `durations`,
714
714
  /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
@@ -716,12 +716,12 @@ pub const Rolls = struct {
716
716
  action: Action,
717
717
  duration: Duration,
718
718
  parent: Optional(bool),
719
- ) []const Optional(Observation) {
720
- if (parent == .true) return &OBS_NONE;
719
+ ) []const Optional(Observation(.binding)) {
720
+ if (parent == .true) return &BND_NONE;
721
721
  return switch (action.binding) {
722
- .None => &OBS_NONE,
723
- .started => &OBS_STARTED,
724
- else => if (duration.binding >= 4) &OBS_ENDED else &OBS,
722
+ .None => &BND_NONE,
723
+ .started => &BND_STARTED,
724
+ else => if (duration.binding >= 4) &BND_ENDED else &BND,
725
725
  };
726
726
  }
727
727
 
@@ -750,7 +750,7 @@ pub const Rolls = struct {
750
750
  }
751
751
 
752
752
  /// The min and max bounds on iteration over damage rolls.
753
- pub const Range = struct { min: u9, max: u9 };
753
+ pub const Range = struct { min: usize, max: usize };
754
754
 
755
755
  /// Returns the range bounding damage rolls given the `action` state and the state of
756
756
  /// the `parent` (whether the player's Pokémon's move hit).
@@ -763,7 +763,7 @@ pub const Rolls = struct {
763
763
 
764
764
  /// Returns the max damage roll which will produce the same damage as `roll`
765
765
  /// given the base damage in `summaries`.
766
- 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 {
767
767
  if (roll == 0) return roll;
768
768
 
769
769
  const dmg = summaries.get(player.foe()).damage;
@@ -775,7 +775,10 @@ pub const Rolls = struct {
775
775
 
776
776
  /// Returns a slice with the correct range of values for confused given the `action` state and
777
777
  /// the state of the `parent` (observation of the player's Pokémon confusion status).
778
- pub fn confused(action: Action, parent: Optional(Confusion)) []const Optional(bool) {
778
+ pub fn confused(
779
+ action: Action,
780
+ parent: Optional(Observation(.confusion)),
781
+ ) []const Optional(bool) {
779
782
  const done = parent == .None or parent == .ended;
780
783
  return if (done or action.confused == .None) &BOOL_NONE else &BOOLS;
781
784
  }
@@ -787,24 +790,24 @@ pub const Rolls = struct {
787
790
  return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
788
791
  }
789
792
 
790
- const SLOT_NONE = [_]u4{0};
791
- const SLOT = [_]u4{ 1, 2, 3, 4 };
793
+ const SLOT_NONE = [_]u3{0};
794
+ const SLOT = [_]u3{ 1, 2, 3, 4 };
792
795
 
793
796
  /// Returns a slice with a range of values for move slots given the `action` state
794
797
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
795
798
  ///
796
799
  /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
797
- pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
800
+ pub fn moveSlot(action: Action, parent: Optional(bool)) []const u3 {
798
801
  if (parent == .false) return &SLOT_NONE;
799
802
  return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
800
803
  }
801
804
 
802
- const MULTI_NONE = [_]u4{0};
803
- const MULTI = [_]u4{ 2, 3, 4, 5 };
805
+ const MULTI_NONE = [_]u3{0};
806
+ const MULTI = [_]u3{ 2, 3, 4, 5 };
804
807
 
805
808
  /// Returns a slice with the correct range of values for multi hit given the `action` state
806
809
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
807
- pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
810
+ pub fn multiHit(action: Action, parent: Optional(bool)) []const u3 {
808
811
  if (parent == .false) return &MULTI_NONE;
809
812
  return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
810
813
  }
@@ -873,12 +876,12 @@ test "Rolls.damage" {
873
876
 
874
877
  test "Rolls.coalesce" {
875
878
  var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
876
- try expectEqual(@as(u8, 0), try Rolls.coalesce(.P2, 0, &summaries, false));
877
- try expectEqual(@as(u8, 241), try Rolls.coalesce(.P2, 238, &summaries, false));
878
- try expectEqual(@as(u8, 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));
879
882
  summaries.p1.damage.final = 74;
880
- try expectEqual(@as(u8, 217), try Rolls.coalesce(.P2, 217, &summaries, false));
881
- try expectEqual(@as(u8, 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));
882
885
  }
883
886
 
884
887
  test "Rolls.hit" {
@@ -936,16 +939,16 @@ test "Rolls.paralyzed" {
936
939
 
937
940
  test "Rolls.moveSlot" {
938
941
  const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
939
- try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
940
- try expectEqualSlices(u4, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
941
- 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));
942
945
  }
943
946
 
944
947
  test "Rolls.multiHit" {
945
948
  const actions: Actions = .{ .p2 = .{ .multi_hit = 3 } };
946
- try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p1, .None));
947
- try expectEqualSlices(u4, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
948
- 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));
949
952
  }
950
953
 
951
954
  test "Rolls.metronome" {