@pkmn/engine 0.1.0-dev.6ebbd3bc → 0.1.0-dev.724afcb1

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 (58) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -11
  3. package/build/pkg/addon/browser.js +4 -1
  4. package/build/pkg/addon/browser.js.map +1 -1
  5. package/build/pkg/addon/node.js +20 -7
  6. package/build/pkg/addon/node.js.map +1 -1
  7. package/build/pkg/addon/wasm.js.map +1 -1
  8. package/build/pkg/addon.d.ts +5 -5
  9. package/build/pkg/addon.js.map +1 -1
  10. package/build/pkg/data.js.map +1 -1
  11. package/build/pkg/gen1.d.ts +2 -1
  12. package/build/pkg/gen1.js +22 -9
  13. package/build/pkg/gen1.js.map +1 -1
  14. package/build/pkg/index.d.ts +2 -0
  15. package/build/pkg/index.js +17 -7
  16. package/build/pkg/index.js.map +1 -1
  17. package/build/pkg/protocol.d.ts +11 -2
  18. package/build/pkg/protocol.js.map +1 -1
  19. package/build/tools/debug.js +54 -10
  20. package/build/tools/debug.js.map +1 -1
  21. package/build/tools/display/debug.jsx +19 -94
  22. package/build/tools/display/debug.jsx.map +1 -1
  23. package/build/tools/display/index.js +17 -7
  24. package/build/tools/display/index.js.map +1 -1
  25. package/build/tools/display/ui.jsx +20 -9
  26. package/build/tools/display/ui.jsx.map +1 -1
  27. package/build/tools/display/util.js +140 -1
  28. package/build/tools/display/util.js.map +1 -1
  29. package/build.zig +169 -218
  30. package/package.json +26 -24
  31. package/src/bin/install-pkmn-engine +23 -7
  32. package/src/lib/bindings/c.zig +27 -81
  33. package/src/lib/bindings/node.zig +7 -11
  34. package/src/lib/bindings/wasm.zig +6 -15
  35. package/src/lib/common/array.zig +5 -12
  36. package/src/lib/common/debug.zig +33 -59
  37. package/src/lib/common/js.zig +3 -9
  38. package/src/lib/common/optional.zig +11 -21
  39. package/src/lib/common/protocol.zig +224 -189
  40. package/src/lib/common/rational.zig +275 -284
  41. package/src/lib/common/util.zig +14 -31
  42. package/src/lib/gen1/calc.zig +139 -117
  43. package/src/lib/gen1/chance.zig +112 -75
  44. package/src/lib/gen1/data/types.zig +2 -2
  45. package/src/lib/gen1/data.zig +5 -5
  46. package/src/lib/gen1/helpers.zig +8 -10
  47. package/src/lib/gen1/mechanics.zig +43 -41
  48. package/src/lib/gen2/calc.zig +3 -6
  49. package/src/lib/gen2/chance.zig +12 -18
  50. package/src/lib/gen2/data/types.zig +6 -3
  51. package/src/lib/gen2/data.zig +8 -7
  52. package/src/lib/gen2/helpers.zig +4 -6
  53. package/src/lib/gen2/mechanics.zig +18 -18
  54. package/src/lib/napi.h +2 -0
  55. package/src/lib/node.zig +1 -5
  56. package/src/lib/pkmn.zig +91 -17
  57. package/src/lib/wasm.zig +2 -6
  58. package/src/tools/display/dom.ts +43 -31
@@ -3,10 +3,8 @@ const std = @import("std");
3
3
  const expect = std.testing.expect;
4
4
  const expectEqual = std.testing.expectEqual;
5
5
 
6
- const Pointer = if (@hasField(std.builtin.Type, "pointer")) .pointer else .Pointer;
7
-
8
6
  pub fn PointerType(comptime P: type, comptime C: type) type {
9
- return if (@field(@typeInfo(P), @tagName(Pointer)).is_const) *const C else *C;
7
+ return if (@typeInfo(P).pointer.is_const) *const C else *C;
10
8
  }
11
9
 
12
10
  test PointerType {
@@ -17,7 +15,7 @@ test PointerType {
17
15
  pub fn isPointerTo(p: anytype, comptime P: type) bool {
18
16
  const info = @typeInfo(@TypeOf(p));
19
17
  return switch (info) {
20
- Pointer => @field(info, @tagName(Pointer)).child == P,
18
+ .pointer => info.pointer.child == P,
21
19
  else => false,
22
20
  };
23
21
  }
@@ -29,33 +27,18 @@ test isPointerTo {
29
27
  try expect(isPointerTo(&s, S));
30
28
  }
31
29
 
32
- // NOTE: std.mem.bytesAsValue backported from ziglang/zig#18061
33
- pub fn bytesAsValue(comptime T: type, bytes: anytype) BytesAsValueReturnType(T, @TypeOf(bytes)) {
34
- return @as(BytesAsValueReturnType(T, @TypeOf(bytes)), @ptrCast(bytes));
35
- }
30
+ pub fn repeat(comptime xs: anytype, comptime n: usize) [xs.len * n]@TypeOf(xs[0]) {
31
+ comptime {
32
+ const T = @TypeOf(xs[0]);
33
+ var ts: [xs.len * n]T = undefined;
36
34
 
37
- fn BytesAsValueReturnType(comptime T: type, comptime B: type) type {
38
- return CopyPtrAttrs(B, .One, T);
39
- }
35
+ var i: usize = 0;
36
+ while (i < n) : (i += 1) {
37
+ for (xs, 0..) |v, j| {
38
+ ts[i * xs.len + j] = v;
39
+ }
40
+ }
40
41
 
41
- fn CopyPtrAttrs(
42
- comptime source: type,
43
- comptime size: std.builtin.Type.Pointer.Size,
44
- comptime child: type,
45
- ) type {
46
- const info = @field(@typeInfo(source), @tagName(Pointer));
47
- const args: std.builtin.Type.Pointer = .{
48
- .size = size,
49
- .is_const = info.is_const,
50
- .is_volatile = info.is_volatile,
51
- .is_allowzero = info.is_allowzero,
52
- .alignment = info.alignment,
53
- .address_space = info.address_space,
54
- .child = child,
55
- .sentinel = null,
56
- };
57
- return @Type(if (@hasField(std.builtin.Type, "pointer"))
58
- .{ .pointer = args }
59
- else
60
- .{ .Pointer = args });
42
+ return ts;
43
+ }
61
44
  }
@@ -16,7 +16,6 @@ const Actions = chance.Actions;
16
16
  const assert = std.debug.assert;
17
17
  const Chance = chance.Chance;
18
18
  const Choice = common.Choice;
19
- const Confusion = chance.Confusion;
20
19
  const Duration = chance.Duration;
21
20
  const Durations = chance.Durations;
22
21
  const enabled = pkmn.options.calc;
@@ -35,9 +34,6 @@ const Sleeps = chance.Sleeps;
35
34
  const tty = true; // DEBUG
36
35
  const summary = false; // DEBUG
37
36
 
38
- const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
39
- const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
40
-
41
37
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
42
38
  pub const Summaries = extern struct {
43
39
  /// Relevant information for Player 1.
@@ -99,13 +95,13 @@ pub const Calc = struct {
99
95
  self: Calc,
100
96
  player: Player,
101
97
  comptime field: Action.Field,
102
- ) ?std.meta.FieldType(Action, field) {
98
+ ) ?@FieldType(Action, @tagName(field)) {
103
99
  if (!enabled) return null;
104
100
 
105
101
  const val = @field(self.overrides.get(player), @tagName(field));
106
102
  return if (switch (@typeInfo(@TypeOf(val))) {
107
- Enum => val != .None,
108
- Int => val != 0,
103
+ .@"enum" => val != .None,
104
+ .int => val != 0,
109
105
  else => unreachable,
110
106
  }) val else null;
111
107
  }
@@ -146,7 +142,7 @@ const Null = struct {
146
142
  self: Null,
147
143
  player: Player,
148
144
  comptime field: Action.Field,
149
- ) ?std.meta.FieldType(Action, field) {
145
+ ) ?@FieldType(Action, @tagName(field)) {
150
146
  _ = .{ self, player };
151
147
  return null;
152
148
  }
@@ -182,24 +178,25 @@ pub const Options = struct {
182
178
  metronome: bool = false,
183
179
  };
184
180
 
185
- pub const MAX_FRONTIER = 81;
181
+ pub const MAX_FRONTIER = 86;
186
182
 
187
183
  pub fn transitions(
188
184
  battle: anytype,
189
185
  c1: Choice,
190
186
  c2: Choice,
191
187
  allocator: std.mem.Allocator,
192
- writer: anytype,
188
+ out: *std.Io.Writer,
189
+ err: *std.Io.Writer,
193
190
  options: Options,
194
191
  ) !?Stats {
195
192
  var stats: Stats = .{};
196
193
 
197
194
  const cap = options.cap;
198
195
 
199
- var seen = std.AutoHashMap(Actions, void).init(allocator);
196
+ var seen: std.AutoHashMap(Actions, void) = .init(allocator);
200
197
  defer seen.deinit();
201
- var frontier = std.ArrayList(Actions).init(allocator);
202
- defer frontier.deinit();
198
+ var frontier: std.array_list.Aligned(Actions, null) = .empty;
199
+ defer frontier.deinit(allocator);
203
200
 
204
201
  const d = options.durations;
205
202
 
@@ -216,8 +213,8 @@ pub fn transitions(
216
213
  const p1 = b.side(.P1);
217
214
  const p2 = b.side(.P2);
218
215
 
219
- var p: Rational(u128) = .{ .p = 0, .q = 1 };
220
- try frontier.append(opts.chance.actions);
216
+ var p: Rational(u256) = .{ .p = 0, .q = 1 };
217
+ try frontier.append(allocator, opts.chance.actions);
221
218
 
222
219
  // zig fmt: off
223
220
  for (Rolls.metronome(frontier.items[0].p1)) |p1_move| {
@@ -231,7 +228,9 @@ pub fn transitions(
231
228
  const f = frontier.items[i];
232
229
  const saved = stats.saved;
233
230
 
234
- try debug(writer, f, .{
231
+ var r: Rational(u256) = .{ .p = 0, .q = 1 };
232
+
233
+ try debug(out, f, .{
235
234
  .shape = true,
236
235
  .color = i,
237
236
  .bold = true,
@@ -251,8 +250,8 @@ pub fn transitions(
251
250
  for (Rolls.disable(f.p2, d.p2, p2_slp)) |p2_dis| { a.p2.disable = p2_dis;
252
251
  for (Rolls.attacking(f.p1, d.p1, p1_slp)) |p1_atk| { a.p1.attacking = p1_atk;
253
252
  for (Rolls.attacking(f.p2, d.p2, p2_slp)) |p2_atk| { a.p2.attacking = p2_atk;
254
- for (Rolls.confusion(f.p1, d.p1, p1_atk, p1_slp)) |p1_cfz| { a.p1.confusion = p1_cfz;
255
- for (Rolls.confusion(f.p2, d.p2, p2_atk, p2_slp)) |p2_cfz| { a.p2.confusion = p2_cfz;
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;
256
255
  for (Rolls.confused(f.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
257
256
  for (Rolls.confused(f.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
258
257
  for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
@@ -278,8 +277,8 @@ pub fn transitions(
278
277
 
279
278
  var p2_dmg = Rolls.damage(f.p2, p2_hit);
280
279
 
281
- const p1_min: u9 = p1_dmg.min;
282
- 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);
283
282
 
284
283
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
285
284
  a.p2.damage = @intCast(p2_dmg.min);
@@ -296,16 +295,16 @@ pub fn transitions(
296
295
 
297
296
  const summaries = &opts.calc.summaries;
298
297
  const p1_max: u9 = if (p2_dmg.min != p2_min)
299
- p1_dmg.min
298
+ @intCast(p1_dmg.min)
300
299
  else
301
- try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
300
+ Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
302
301
  const p2_max: u9 =
303
- try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
302
+ Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
304
303
 
305
304
  if (opts.chance.actions.matches(f)) {
306
305
  if (!opts.chance.actions.relax().eql(a)) {
307
306
  if (!summary) {
308
- try debug(writer, opts.chance.actions, .{
307
+ try debug(out, opts.chance.actions, .{
309
308
  .p1_max = p1_max,
310
309
  .p2_max = p2_max,
311
310
  .color = i,
@@ -319,7 +318,7 @@ pub fn transitions(
319
318
  }
320
319
 
321
320
  if (!summary) {
322
- try debug(writer, opts.chance.actions, .{
321
+ try debug(out, opts.chance.actions, .{
323
322
  .p1_max = p1_max,
324
323
  .p2_max = p2_max,
325
324
  .color = i,
@@ -332,7 +331,7 @@ pub fn transitions(
332
331
  acts.p1.damage = @intCast(p1d);
333
332
  acts.p2.damage = @intCast(p2d);
334
333
  if ((try seen.getOrPut(acts)).found_existing) {
335
- err("already seen {}", .{acts}, options.seed);
334
+ die(err, "already seen {f}", .{acts}, options.seed);
336
335
  return error.TestUnexpectedResult;
337
336
  }
338
337
  }
@@ -343,25 +342,27 @@ pub fn transitions(
343
342
  q.reduce();
344
343
  try p.add(q);
345
344
  p.reduce();
345
+ try r.add(q);
346
+ r.reduce();
346
347
 
347
348
  stats.saved += 1;
348
349
 
349
350
  if (p.q < p.p) {
350
- err("improper fraction {}", .{p}, options.seed);
351
+ die(err, "improper fraction {f}", .{p}, options.seed);
351
352
  return error.TestUnexpectedResult;
352
353
  }
353
354
  } else {
354
355
  if (!opts.chance.actions.matchesAny(frontier.items, i)) {
355
- try frontier.append(opts.chance.actions);
356
+ try frontier.append(allocator, opts.chance.actions);
356
357
 
357
- try debug(writer, opts.chance.actions, .{
358
+ try debug(out, opts.chance.actions, .{
358
359
  .p1_max = p1_max,
359
360
  .p2_max = p2_max,
360
361
  .dim = true,
361
362
  .newline = false,
362
363
  });
363
- try writer.writeAll(" → ");
364
- try debug(writer, opts.chance.actions, .{
364
+ try out.writeAll(" → ");
365
+ try debug(out, opts.chance.actions, .{
365
366
  .shape = true,
366
367
  .color = frontier.items.len - 1,
367
368
  .dim = true,
@@ -369,7 +370,7 @@ pub fn transitions(
369
370
  .indent = false,
370
371
  });
371
372
  } else if (!summary) {
372
- try debug(writer, opts.chance.actions, .{
373
+ try debug(out, opts.chance.actions, .{
373
374
  .p1_max = p1_max,
374
375
  .p2_max = p2_max,
375
376
  .dim = true,
@@ -384,14 +385,12 @@ pub fn transitions(
384
385
  }}}}}}}}}}}}}}}}}}}}}}}}}}}}
385
386
 
386
387
  assert(stats.saved > saved);
387
- const old = @hasDecl(std.fmt, "formatFloatDecimal");
388
- if (!old and @TypeOf(writer) != @TypeOf(std.io.null_writer)) {
389
- p.reduce();
390
- try writer.print(
391
- " = {s} ({d:.2}%)\n\n",
392
- .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
393
- );
394
- }
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
+ // );
395
394
 
396
395
  }
397
396
 
@@ -406,7 +405,7 @@ pub fn transitions(
406
405
 
407
406
  p.reduce();
408
407
  if (p.p != 1 or p.q != 1) {
409
- err("expected 1, found {}", .{p}, options.seed);
408
+ die(err, "expected 1, found {f}", .{p}, options.seed);
410
409
  return error.TestExpectedEqual;
411
410
  }
412
411
 
@@ -419,7 +418,8 @@ pub fn update(
419
418
  c2: Choice,
420
419
  options: anytype,
421
420
  allocator: std.mem.Allocator,
422
- writer: anytype,
421
+ out: *std.Io.Writer,
422
+ err: *std.Io.Writer,
423
423
  transition: bool,
424
424
  ) !Result {
425
425
  const durations = options.chance.durations;
@@ -437,8 +437,8 @@ pub fn update(
437
437
  if (@TypeOf(battle.rng) == data.PRNG) {
438
438
  // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
439
439
  var buf: [helpers.MAX_DIFFS]u8 = undefined;
440
- var stream: protocol.ByteStream = .{ .buffer = &buf };
441
- 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);
442
442
 
443
443
  // Applying the diff to the battle should take us back to the original copy
444
444
  // of the battle (ignoring the RNG).
@@ -452,7 +452,7 @@ pub fn update(
452
452
  // Ensure we can generate all transitions from the same original state
453
453
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
454
454
  // the transitions function relies on RNG for discovery of states)
455
- if (try transitions(unfix(original), c1, c2, allocator, writer, .{
455
+ if (try transitions(unfix(original), c1, c2, allocator, out, err, .{
456
456
  .durations = durations,
457
457
  .cap = true,
458
458
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
@@ -499,10 +499,9 @@ pub fn update(
499
499
  fn expectEqualActions(expected: Actions, actual: Actions) !void {
500
500
  return expectEqual(expected, actual) catch |e| switch (e) {
501
501
  error.TestExpectedEqual => {
502
- std.debug.print("expected {}, found {}\n", .{ expected, actual });
502
+ std.debug.print("expected {f}, found {f}\n", .{ expected, actual });
503
503
  return e;
504
504
  },
505
- else => return e,
506
505
  };
507
506
  }
508
507
 
@@ -521,10 +520,9 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
521
520
  };
522
521
  }
523
522
 
524
- fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
525
- const w = std.io.getStdErr().writer();
523
+ fn die(w: *std.Io.Writer, comptime fmt: []const u8, v: anytype, seed: ?u64) void {
526
524
  w.print(fmt, v) catch return;
527
- if (seed) |s| return w.print("{}\n", .{s}) catch return;
525
+ if (seed) |s| return w.print("{d}\n", .{s}) catch return;
528
526
  return w.writeByte('\n') catch return;
529
527
  }
530
528
 
@@ -540,7 +538,7 @@ const Style = struct {
540
538
  indent: bool = true,
541
539
  };
542
540
 
543
- fn debug(writer: anytype, actions: Actions, style: Style) !void {
541
+ fn debug(writer: *std.Io.Writer, actions: Actions, style: Style) !void {
544
542
  if (style.indent) try writer.writeAll(" ");
545
543
  if (tty) {
546
544
  const mod: usize = if (style.dim) 2 else 1;
@@ -565,13 +563,13 @@ fn debug(writer: anytype, actions: Actions, style: Style) !void {
565
563
  if (style.newline) try writer.writeByte('\n');
566
564
  }
567
565
 
568
- 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 {
569
567
  var input: [1024]u8 = undefined;
570
568
  var output: [1024]u8 = undefined;
571
569
 
572
- var stream = std.io.fixedBufferStream(&input);
573
- try actions.fmt(&stream.writer(), false);
574
- 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;
575
573
  @memcpy(output[0..len], input[0..len]);
576
574
 
577
575
  var find: [16]u8 = undefined;
@@ -604,78 +602,92 @@ pub const Rolls = struct {
604
602
  return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
605
603
  }
606
604
 
607
- const OBS_NONE = [_]Optional(Observation){.None};
608
- const OBS_STARTED = [_]Optional(Observation){.started};
609
- const OBS_CONTINUING = [_]Optional(Observation){.continuing};
610
- const OBS_ENDED = [_]Optional(Observation){.ended};
611
- const OBS_ALL = [_]Optional(Observation){ .started, .continuing, .ended };
612
- 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 };
613
610
 
614
611
  /// Returns a slice with a range of values for sleep given the `action` state and observed
615
612
  /// `durations`.
616
- pub fn sleep(action: Action, duration: Duration) []const Optional(Observation) {
617
- 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;
618
615
  return if (Sleeps.get(duration.sleeps, 0) == 0 and action.speed_tie != .None)
619
- &OBS_ALL
616
+ &SLP_ALL
620
617
  else switch (action.sleep) {
621
- .started => &OBS_STARTED,
622
- 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,
623
620
  };
624
621
  }
625
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
+
626
630
  /// Returns a slice with a range of values for disable given the `action`, observed `durations`,
627
631
  /// and the state of the `parent` (observation of the player's Pokémon sleep status).
628
632
  pub fn disable(
629
633
  action: Action,
630
634
  duration: Duration,
631
- parent: Optional(Observation),
632
- ) []const Optional(Observation) {
633
- 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;
634
638
  return if (duration.disable == 0 and action.speed_tie != .None)
635
- &OBS_ALL
639
+ &DIS_ALL
636
640
  else switch (action.disable) {
637
- .started => &OBS_STARTED,
641
+ .started => &DIS_STARTED,
638
642
  else => if (duration.disable >= 8)
639
- &OBS_ENDED
643
+ &DIS_ENDED
640
644
  else if (parent != .None and parent != .started)
641
- &OBS_CONTINUING
645
+ &DIS_CONTINUING
642
646
  else
643
- &OBS,
647
+ &DIS,
644
648
  };
645
649
  }
646
650
 
647
- /// FIXME
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
648
658
  pub fn attacking(
649
659
  action: Action,
650
660
  duration: Duration,
651
- parent: Optional(Observation),
652
- ) []const Optional(Observation) {
661
+ parent: Optional(Observation(.sleep)),
662
+ ) []const Optional(Observation(.attacking)) {
653
663
  return switch (action.attacking) {
654
- .None => &OBS_NONE,
655
- .started => &OBS_STARTED,
664
+ .None => &ATK_NONE,
665
+ .started => &ATK_STARTED,
656
666
  else => if (duration.attacking >= 3)
657
- &OBS_ENDED
667
+ &ATK_ENDED
658
668
  else if ((parent == .continuing and parent == .ended) or duration.attacking < 2)
659
- &OBS_CONTINUING
669
+ &ATK_CONTINUING
660
670
  else
661
- &OBS,
671
+ &ATK,
662
672
  };
663
673
  }
664
674
 
665
- const CFZ_NONE = [_]Optional(Confusion){.None};
666
- const CFZ_STARTED = [_]Optional(Confusion){.started};
667
- const CFZ_OVERWRITTEN = [_]Optional(Confusion){ .started, .overwritten };
668
- const CFZ_CONTINUING = [_]Optional(Confusion){.continuing};
669
- const CFZ_ENDED = [_]Optional(Confusion){.ended};
670
- 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 };
671
682
 
672
- /// FIXME
683
+ /// FIXME document
673
684
  pub fn confusion(
674
685
  action: Action,
675
686
  duration: Duration,
676
- sibling: Optional(Observation),
677
- parent: Optional(Observation),
678
- ) []const Optional(Confusion) {
687
+ tie: Optional(Player),
688
+ sibling: Optional(Observation(.attacking)),
689
+ parent: Optional(Observation(.sleep)),
690
+ ) []const Optional(Observation(.confusion)) {
679
691
  return switch (action.confusion) {
680
692
  .None => &CFZ_NONE,
681
693
  .started => if (pkmn.options.overwrite and sibling == .ended and duration.confusion < 5)
@@ -686,23 +698,30 @@ pub const Rolls = struct {
686
698
  &CFZ_ENDED
687
699
  else if ((parent == .continuing and parent == .ended) or duration.confusion < 2)
688
700
  &CFZ_CONTINUING
701
+ else if (tie != .None)
702
+ &CFZ_TIE
689
703
  else
690
704
  &CFZ,
691
705
  };
692
706
  }
693
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
+
694
713
  /// Returns a slice with a range of values for binding given the `action`, observed `durations`,
695
714
  /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
696
715
  pub fn binding(
697
716
  action: Action,
698
717
  duration: Duration,
699
718
  parent: Optional(bool),
700
- ) []const Optional(Observation) {
701
- if (parent == .true) return &OBS_NONE;
719
+ ) []const Optional(Observation(.binding)) {
720
+ if (parent == .true) return &BND_NONE;
702
721
  return switch (action.binding) {
703
- .None => &OBS_NONE,
704
- .started => &OBS_STARTED,
705
- 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,
706
725
  };
707
726
  }
708
727
 
@@ -731,7 +750,7 @@ pub const Rolls = struct {
731
750
  }
732
751
 
733
752
  /// The min and max bounds on iteration over damage rolls.
734
- pub const Range = struct { min: u9, max: u9 };
753
+ pub const Range = struct { min: usize, max: usize };
735
754
 
736
755
  /// Returns the range bounding damage rolls given the `action` state and the state of
737
756
  /// the `parent` (whether the player's Pokémon's move hit).
@@ -744,7 +763,7 @@ pub const Rolls = struct {
744
763
 
745
764
  /// Returns the max damage roll which will produce the same damage as `roll`
746
765
  /// given the base damage in `summaries`.
747
- 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 {
748
767
  if (roll == 0) return roll;
749
768
 
750
769
  const dmg = summaries.get(player.foe()).damage;
@@ -756,7 +775,10 @@ pub const Rolls = struct {
756
775
 
757
776
  /// Returns a slice with the correct range of values for confused given the `action` state and
758
777
  /// the state of the `parent` (observation of the player's Pokémon confusion status).
759
- 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) {
760
782
  const done = parent == .None or parent == .ended;
761
783
  return if (done or action.confused == .None) &BOOL_NONE else &BOOLS;
762
784
  }
@@ -768,24 +790,24 @@ pub const Rolls = struct {
768
790
  return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
769
791
  }
770
792
 
771
- const SLOT_NONE = [_]u4{0};
772
- const SLOT = [_]u4{ 1, 2, 3, 4 };
793
+ const SLOT_NONE = [_]u3{0};
794
+ const SLOT = [_]u3{ 1, 2, 3, 4 };
773
795
 
774
796
  /// Returns a slice with a range of values for move slots given the `action` state
775
797
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
776
798
  ///
777
799
  /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
778
- pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
800
+ pub fn moveSlot(action: Action, parent: Optional(bool)) []const u3 {
779
801
  if (parent == .false) return &SLOT_NONE;
780
802
  return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
781
803
  }
782
804
 
783
- const MULTI_NONE = [_]u4{0};
784
- const MULTI = [_]u4{ 2, 3, 4, 5 };
805
+ const MULTI_NONE = [_]u3{0};
806
+ const MULTI = [_]u3{ 2, 3, 4, 5 };
785
807
 
786
808
  /// Returns a slice with the correct range of values for multi hit given the `action` state
787
809
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
788
- pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
810
+ pub fn multiHit(action: Action, parent: Optional(bool)) []const u3 {
789
811
  if (parent == .false) return &MULTI_NONE;
790
812
  return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
791
813
  }
@@ -854,12 +876,12 @@ test "Rolls.damage" {
854
876
 
855
877
  test "Rolls.coalesce" {
856
878
  var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
857
- try expectEqual(@as(u8, 0), try Rolls.coalesce(.P2, 0, &summaries, false));
858
- try expectEqual(@as(u8, 241), try Rolls.coalesce(.P2, 238, &summaries, false));
859
- 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));
860
882
  summaries.p1.damage.final = 74;
861
- try expectEqual(@as(u8, 217), try Rolls.coalesce(.P2, 217, &summaries, false));
862
- 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));
863
885
  }
864
886
 
865
887
  test "Rolls.hit" {
@@ -917,16 +939,16 @@ test "Rolls.paralyzed" {
917
939
 
918
940
  test "Rolls.moveSlot" {
919
941
  const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
920
- try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
921
- try expectEqualSlices(u4, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
922
- 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));
923
945
  }
924
946
 
925
947
  test "Rolls.multiHit" {
926
948
  const actions: Actions = .{ .p2 = .{ .multi_hit = 3 } };
927
- try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p1, .None));
928
- try expectEqualSlices(u4, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
929
- 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));
930
952
  }
931
953
 
932
954
  test "Rolls.metronome" {