@pkmn/engine 0.1.0-dev.ce4c0b2a → 0.1.0-dev.d127cb34

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 (73) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +23 -15
  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/data.js.map +1 -1
  17. package/build/pkg/gen1.d.ts +15 -18
  18. package/build/pkg/gen1.js +28 -19
  19. package/build/pkg/gen1.js.map +1 -1
  20. package/build/pkg/index.d.ts +37 -100
  21. package/build/pkg/index.js +23 -75
  22. package/build/pkg/index.js.map +1 -1
  23. package/build/pkg/protocol.d.ts +11 -2
  24. package/build/pkg/protocol.js.map +1 -1
  25. package/build/tools/debug.js +56 -13
  26. package/build/tools/debug.js.map +1 -1
  27. package/build/tools/display/debug.jsx +19 -81
  28. package/build/tools/display/debug.jsx.map +1 -1
  29. package/build/tools/display/index.js +35 -15
  30. package/build/tools/display/index.js.map +1 -1
  31. package/build/tools/display/ui.jsx +27 -16
  32. package/build/tools/display/ui.jsx.map +1 -1
  33. package/build/tools/display/util.js +141 -2
  34. package/build/tools/display/util.js.map +1 -1
  35. package/build.zig +330 -245
  36. package/package.json +36 -128
  37. package/src/bin/install-pkmn-engine +25 -9
  38. package/src/lib/bindings/c.zig +201 -0
  39. package/src/lib/bindings/node.zig +117 -0
  40. package/src/lib/bindings/wasm.zig +51 -0
  41. package/src/lib/common/array.zig +5 -12
  42. package/src/lib/common/debug.zig +45 -34
  43. package/src/lib/common/js.zig +3 -9
  44. package/src/lib/common/optional.zig +26 -26
  45. package/src/lib/common/options.zig +2 -0
  46. package/src/lib/common/protocol.zig +228 -197
  47. package/src/lib/common/rational.zig +278 -285
  48. package/src/lib/common/rng.zig +1 -3
  49. package/src/lib/common/util.zig +34 -31
  50. package/src/lib/gen1/calc.zig +220 -195
  51. package/src/lib/gen1/chance.zig +184 -108
  52. package/src/lib/gen1/data/moves.zig +2 -4
  53. package/src/lib/gen1/data/species.zig +1 -3
  54. package/src/lib/gen1/data/types.zig +3 -4
  55. package/src/lib/gen1/data.zig +12 -16
  56. package/src/lib/gen1/helpers.zig +35 -40
  57. package/src/lib/gen1/mechanics.zig +147 -127
  58. package/src/lib/gen2/calc.zig +12 -23
  59. package/src/lib/gen2/chance.zig +38 -26
  60. package/src/lib/gen2/data/items.zig +1 -3
  61. package/src/lib/gen2/data/moves.zig +2 -4
  62. package/src/lib/gen2/data/species.zig +1 -3
  63. package/src/lib/gen2/data/types.zig +7 -5
  64. package/src/lib/gen2/data.zig +19 -22
  65. package/src/lib/gen2/generated.zig +18 -23
  66. package/src/lib/gen2/helpers.zig +32 -31
  67. package/src/lib/gen2/mechanics.zig +58 -55
  68. package/src/lib/napi.h +2 -0
  69. package/src/lib/node.zig +4 -121
  70. package/src/lib/pkmn.zig +98 -19
  71. package/src/lib/wasm.zig +9 -9
  72. package/src/tools/display/dom.ts +43 -31
  73. package/src/tools/display/ui.css +12 -9
@@ -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,17 +95,25 @@ 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
  }
121
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
+
122
117
  pub fn base(self: *Calc, player: Player, val: u16) void {
123
118
  if (!enabled) return;
124
119
 
@@ -147,11 +142,15 @@ const Null = struct {
147
142
  self: Null,
148
143
  player: Player,
149
144
  comptime field: Action.Field,
150
- ) ?std.meta.FieldType(Action, field) {
145
+ ) ?@FieldType(Action, @tagName(field)) {
151
146
  _ = .{ self, player };
152
147
  return null;
153
148
  }
154
149
 
150
+ pub fn confusion(self: Null, player: Player) void {
151
+ _ = .{ self, player };
152
+ }
153
+
155
154
  pub fn base(self: Null, player: Player, val: u16) void {
156
155
  _ = .{ self, player, val };
157
156
  }
@@ -179,30 +178,31 @@ pub const Options = struct {
179
178
  metronome: bool = false,
180
179
  };
181
180
 
182
- pub const MAX_FRONTIER = 19;
181
+ pub const MAX_FRONTIER = 86;
183
182
 
184
183
  pub fn transitions(
185
184
  battle: anytype,
186
185
  c1: Choice,
187
186
  c2: Choice,
188
187
  allocator: std.mem.Allocator,
189
- writer: anytype,
188
+ out: *std.Io.Writer,
189
+ err: *std.Io.Writer,
190
190
  options: Options,
191
191
  ) !?Stats {
192
192
  var stats: Stats = .{};
193
193
 
194
194
  const cap = options.cap;
195
195
 
196
- var seen = std.AutoHashMap(Actions, void).init(allocator);
196
+ var seen: std.AutoHashMap(Actions, void) = .init(allocator);
197
197
  defer seen.deinit();
198
- var frontier = std.ArrayList(Actions).init(allocator);
199
- defer frontier.deinit();
198
+ var frontier: std.array_list.Aligned(Actions, null) = .empty;
199
+ defer frontier.deinit(allocator);
200
200
 
201
- const durations = options.durations;
201
+ const d = options.durations;
202
202
 
203
203
  var opts = pkmn.battle.options(
204
204
  protocol.NULL,
205
- Chance(Rational(u128)){ .probability = .{}, .durations = durations },
205
+ Chance(Rational(u128)){ .probability = .{}, .durations = d },
206
206
  Calc{},
207
207
  );
208
208
 
@@ -213,8 +213,8 @@ pub fn transitions(
213
213
  const p1 = b.side(.P1);
214
214
  const p2 = b.side(.P2);
215
215
 
216
- var p: Rational(u128) = .{ .p = 0, .q = 1 };
217
- try frontier.append(opts.chance.actions);
216
+ var p: Rational(u256) = .{ .p = 0, .q = 1 };
217
+ try frontier.append(allocator, opts.chance.actions);
218
218
 
219
219
  // zig fmt: off
220
220
  for (Rolls.metronome(frontier.items[0].p1)) |p1_move| {
@@ -228,7 +228,9 @@ pub fn transitions(
228
228
  const f = frontier.items[i];
229
229
  const saved = stats.saved;
230
230
 
231
- try debug(writer, f, .{
231
+ var r: Rational(u256) = .{ .p = 0, .q = 1 };
232
+
233
+ try debug(out, f, .{
232
234
  .shape = true,
233
235
  .color = i,
234
236
  .bold = true,
@@ -242,20 +244,20 @@ pub fn transitions(
242
244
  };
243
245
 
244
246
  for (Rolls.speedTie(f.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
245
- for (Rolls.sleep(f.p1, durations.p1)) |p1_slp| { a.p1.sleep = p1_slp;
246
- for (Rolls.sleep(f.p2, durations.p2)) |p2_slp| { a.p2.sleep = p2_slp;
247
- for (Rolls.disable(f.p1, durations.p1, p1_slp)) |p1_dis| { a.p1.disable = p1_dis;
248
- for (Rolls.disable(f.p2, durations.p2, p2_slp)) |p2_dis| { a.p2.disable = p2_dis;
249
- for (Rolls.confusion(f.p1, durations.p1, p1_slp)) |p1_cfz| { a.p1.confusion = p1_cfz;
250
- for (Rolls.confusion(f.p2, durations.p2, p1_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;
251
255
  for (Rolls.confused(f.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
252
256
  for (Rolls.confused(f.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
253
257
  for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
254
258
  for (Rolls.paralyzed(f.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
255
- for (Rolls.attacking(f.p1, durations.p1, p1_par)) |p1_atk| { a.p1.attacking = p1_atk;
256
- for (Rolls.attacking(f.p2, durations.p2, p2_par)) |p2_atk| { a.p2.attacking = p2_atk;
257
- for (Rolls.binding(f.p1, durations.p1, p1_par)) |p1_bind| { a.p1.binding = p1_bind;
258
- 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;
259
261
  for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
260
262
  for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
261
263
  for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
@@ -275,15 +277,15 @@ pub fn transitions(
275
277
 
276
278
  var p2_dmg = Rolls.damage(f.p2, p2_hit);
277
279
 
278
- const p1_min: u9 = p1_dmg.min;
279
- 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);
280
282
 
281
283
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
282
284
  a.p2.damage = @intCast(p2_dmg.min);
283
285
 
284
286
  opts.calc.overrides = a;
285
287
  opts.calc.summaries = .{};
286
- opts.chance = .{ .probability = .{}, .durations = durations };
288
+ opts.chance = .{ .probability = .{}, .durations = d };
287
289
  const q = &opts.chance.probability;
288
290
 
289
291
  b = battle;
@@ -293,16 +295,16 @@ pub fn transitions(
293
295
 
294
296
  const summaries = &opts.calc.summaries;
295
297
  const p1_max: u9 = if (p2_dmg.min != p2_min)
296
- p1_dmg.min
298
+ @intCast(p1_dmg.min)
297
299
  else
298
- try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
300
+ Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
299
301
  const p2_max: u9 =
300
- try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
302
+ Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
301
303
 
302
304
  if (opts.chance.actions.matches(f)) {
303
- if (!map(opts.chance.actions).eql(a)) {
305
+ if (!opts.chance.actions.relax().eql(a)) {
304
306
  if (!summary) {
305
- try debug(writer, opts.chance.actions, .{
307
+ try debug(out, opts.chance.actions, .{
306
308
  .p1_max = p1_max,
307
309
  .p2_max = p2_max,
308
310
  .color = i,
@@ -316,7 +318,7 @@ pub fn transitions(
316
318
  }
317
319
 
318
320
  if (!summary) {
319
- try debug(writer, opts.chance.actions, .{
321
+ try debug(out, opts.chance.actions, .{
320
322
  .p1_max = p1_max,
321
323
  .p2_max = p2_max,
322
324
  .color = i,
@@ -329,33 +331,38 @@ pub fn transitions(
329
331
  acts.p1.damage = @intCast(p1d);
330
332
  acts.p2.damage = @intCast(p2d);
331
333
  if ((try seen.getOrPut(acts)).found_existing) {
332
- err("already seen {}", .{acts}, options.seed);
334
+ die(err, "already seen {f}", .{acts}, options.seed);
333
335
  return error.TestUnexpectedResult;
334
336
  }
335
337
  }
336
338
  }
337
-
338
339
  if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
339
340
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
341
+
342
+ q.reduce();
340
343
  try p.add(q);
344
+ p.reduce();
345
+ try r.add(q);
346
+ r.reduce();
347
+
341
348
  stats.saved += 1;
342
349
 
343
350
  if (p.q < p.p) {
344
- err("improper fraction {}", .{p}, options.seed);
351
+ die(err, "improper fraction {f}", .{p}, options.seed);
345
352
  return error.TestUnexpectedResult;
346
353
  }
347
354
  } else {
348
- if (!matches(opts.chance.actions, i, frontier.items)) {
349
- try frontier.append(opts.chance.actions);
355
+ if (!opts.chance.actions.matchesAny(frontier.items, i)) {
356
+ try frontier.append(allocator, opts.chance.actions);
350
357
 
351
- try debug(writer, opts.chance.actions, .{
358
+ try debug(out, opts.chance.actions, .{
352
359
  .p1_max = p1_max,
353
360
  .p2_max = p2_max,
354
361
  .dim = true,
355
362
  .newline = false,
356
363
  });
357
- try writer.writeAll(" → ");
358
- try debug(writer, opts.chance.actions, .{
364
+ try out.writeAll(" → ");
365
+ try debug(out, opts.chance.actions, .{
359
366
  .shape = true,
360
367
  .color = frontier.items.len - 1,
361
368
  .dim = true,
@@ -363,7 +370,7 @@ pub fn transitions(
363
370
  .indent = false,
364
371
  });
365
372
  } else if (!summary) {
366
- try debug(writer, opts.chance.actions, .{
373
+ try debug(out, opts.chance.actions, .{
367
374
  .p1_max = p1_max,
368
375
  .p2_max = p2_max,
369
376
  .dim = true,
@@ -378,14 +385,12 @@ pub fn transitions(
378
385
  }}}}}}}}}}}}}}}}}}}}}}}}}}}}
379
386
 
380
387
  assert(stats.saved > saved);
381
- const old = @hasDecl(std.fmt, "formatFloatDecimal");
382
- if (!old and @TypeOf(writer) != @TypeOf(std.io.null_writer)) {
383
- p.reduce();
384
- try writer.print(
385
- " = {s} ({d:.2}%)\n\n",
386
- .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
387
- );
388
- }
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
+ // );
389
394
 
390
395
  }
391
396
 
@@ -400,7 +405,7 @@ pub fn transitions(
400
405
 
401
406
  p.reduce();
402
407
  if (p.p != 1 or p.q != 1) {
403
- err("expected 1, found {}", .{p}, options.seed);
408
+ die(err, "expected 1, found {f}", .{p}, options.seed);
404
409
  return error.TestExpectedEqual;
405
410
  }
406
411
 
@@ -413,7 +418,8 @@ pub fn update(
413
418
  c2: Choice,
414
419
  options: anytype,
415
420
  allocator: std.mem.Allocator,
416
- writer: anytype,
421
+ out: *std.Io.Writer,
422
+ err: *std.Io.Writer,
417
423
  transition: bool,
418
424
  ) !Result {
419
425
  const durations = options.chance.durations;
@@ -431,8 +437,8 @@ pub fn update(
431
437
  if (@TypeOf(battle.rng) == data.PRNG) {
432
438
  // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
433
439
  var buf: [helpers.MAX_DIFFS]u8 = undefined;
434
- var stream: protocol.ByteStream = .{ .buffer = &buf };
435
- 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);
436
442
 
437
443
  // Applying the diff to the battle should take us back to the original copy
438
444
  // of the battle (ignoring the RNG).
@@ -446,7 +452,7 @@ pub fn update(
446
452
  // Ensure we can generate all transitions from the same original state
447
453
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
448
454
  // the transitions function relies on RNG for discovery of states)
449
- if (try transitions(unfix(original), c1, c2, allocator, writer, .{
455
+ if (try transitions(unfix(original), c1, c2, allocator, out, err, .{
450
456
  .durations = durations,
451
457
  .cap = true,
452
458
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
@@ -493,10 +499,9 @@ pub fn update(
493
499
  fn expectEqualActions(expected: Actions, actual: Actions) !void {
494
500
  return expectEqual(expected, actual) catch |e| switch (e) {
495
501
  error.TestExpectedEqual => {
496
- std.debug.print("expected {}, found {}\n", .{ expected, actual });
502
+ std.debug.print("expected {f}, found {f}\n", .{ expected, actual });
497
503
  return e;
498
504
  },
499
- else => return e,
500
505
  };
501
506
  }
502
507
 
@@ -515,27 +520,9 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
515
520
  };
516
521
  }
517
522
 
518
- fn map(actions: Actions) Actions {
519
- if (!pkmn.options.overwrite) return actions;
520
- var a = actions;
521
- if (a.p1.confusion == .overwritten) a.p1.confusion = .continuing;
522
- if (a.p2.confusion == .overwritten) a.p2.confusion = .continuing;
523
- return a;
524
- }
525
-
526
- fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
527
- for (frontier, 0..) |f, j| {
528
- // TODO: is skipping this redundant check worth it?
529
- if (i == j) continue;
530
- if (f.matches(actions)) return true;
531
- }
532
- return false;
533
- }
534
-
535
- fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
536
- const w = std.io.getStdErr().writer();
523
+ fn die(w: *std.Io.Writer, comptime fmt: []const u8, v: anytype, seed: ?u64) void {
537
524
  w.print(fmt, v) catch return;
538
- if (seed) |s| return w.print("{}\n", .{s}) catch return;
525
+ if (seed) |s| return w.print("{d}\n", .{s}) catch return;
539
526
  return w.writeByte('\n') catch return;
540
527
  }
541
528
 
@@ -551,7 +538,7 @@ const Style = struct {
551
538
  indent: bool = true,
552
539
  };
553
540
 
554
- fn debug(writer: anytype, actions: Actions, style: Style) !void {
541
+ fn debug(writer: *std.Io.Writer, actions: Actions, style: Style) !void {
555
542
  if (style.indent) try writer.writeAll(" ");
556
543
  if (tty) {
557
544
  const mod: usize = if (style.dim) 2 else 1;
@@ -576,13 +563,13 @@ fn debug(writer: anytype, actions: Actions, style: Style) !void {
576
563
  if (style.newline) try writer.writeByte('\n');
577
564
  }
578
565
 
579
- 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 {
580
567
  var input: [1024]u8 = undefined;
581
568
  var output: [1024]u8 = undefined;
582
569
 
583
- var stream = std.io.fixedBufferStream(&input);
584
- try actions.fmt(&stream.writer(), false);
585
- 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;
586
573
  @memcpy(output[0..len], input[0..len]);
587
574
 
588
575
  var find: [16]u8 = undefined;
@@ -615,85 +602,113 @@ pub const Rolls = struct {
615
602
  return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
616
603
  }
617
604
 
618
- const OBS_NONE = [_]Optional(Observation){.None};
619
- const OBS_STARTED = [_]Optional(Observation){.started};
620
- const OBS_CONTINUING = [_]Optional(Observation){.continuing};
621
- const OBS_ENDED = [_]Optional(Observation){.ended};
622
- 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 };
623
610
 
624
611
  /// Returns a slice with a range of values for sleep given the `action` state and observed
625
612
  /// `durations`.
626
- pub fn sleep(action: Action, duration: Duration) []const Optional(Observation) {
627
- return switch (action.sleep) {
628
- .None => &OBS_NONE,
629
- .started => &OBS_STARTED,
630
- else => if (Sleeps.get(duration.sleeps, 0) >= 7) &OBS_ENDED else &OBS,
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,
631
620
  };
632
621
  }
633
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
+
634
630
  /// Returns a slice with a range of values for disable given the `action`, observed `durations`,
635
631
  /// and the state of the `parent` (observation of the player's Pokémon sleep status).
636
632
  pub fn disable(
637
633
  action: Action,
638
634
  duration: Duration,
639
- parent: Optional(Observation),
640
- ) []const Optional(Observation) {
641
- // if (parent == .started or parent == .continuing) return &OBS_NONE;
642
- _ = parent; // FIXME
643
- return switch (action.disable) {
644
- .None => &OBS_NONE,
645
- .started => &OBS_STARTED,
646
- else => if (duration.disable >= 8) &OBS_ENDED else &OBS,
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,
647
648
  };
648
649
  }
649
650
 
650
- const CFZ_NONE = [_]Optional(Confusion){.None};
651
- const CFZ_STARTED = [_]Optional(Confusion){.started};
652
- const CFZ_CONTINUING = [_]Optional(Confusion){.continuing};
653
- const CFZ_ENDED = [_]Optional(Confusion){.ended};
654
- const CFZ = [_]Optional(Confusion){ .continuing, .ended };
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 };
655
656
 
656
- /// Returns a slice with a range of values for confusion given the `action`, observed `durations`,
657
- /// and the state of the `parent` (observation of the player's Pokémon sleep status).
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
658
684
  pub fn confusion(
659
685
  action: Action,
660
686
  duration: Duration,
661
- parent: Optional(Observation),
662
- ) []const Optional(Confusion) {
663
- // if (parent == .started or parent == .continuing) return &CFZ_NONE;
664
- _ = parent; // FIXME
687
+ tie: Optional(Player),
688
+ sibling: Optional(Observation(.attacking)),
689
+ parent: Optional(Observation(.sleep)),
690
+ ) []const Optional(Observation(.confusion)) {
665
691
  return switch (action.confusion) {
666
692
  .None => &CFZ_NONE,
667
- .started => &CFZ_STARTED,
693
+ .started => if (pkmn.options.overwrite and sibling == .ended and duration.confusion < 5)
694
+ &CFZ_OVERWRITTEN
695
+ else
696
+ &CFZ_STARTED,
668
697
  else => if (duration.confusion >= 5)
669
698
  &CFZ_ENDED
670
- else if (duration.confusion < 2)
699
+ else if ((parent == .continuing and parent == .ended) or duration.confusion < 2)
671
700
  &CFZ_CONTINUING
701
+ else if (tie != .None)
702
+ &CFZ_TIE
672
703
  else
673
704
  &CFZ,
674
705
  };
675
706
  }
676
707
 
677
- /// Returns a slice with a range of values for attacking given the `action`, observed
678
- /// `durations`, and the state of the `parent` (whether the player's Pokémon was fully
679
- /// paralyzed).
680
- pub fn attacking(
681
- action: Action,
682
- duration: Duration,
683
- parent: Optional(bool),
684
- ) []const Optional(Observation) {
685
- if (parent == .true) return &OBS_NONE;
686
- return switch (action.attacking) {
687
- .None => &OBS_NONE,
688
- .started => &OBS_STARTED,
689
- else => if (duration.attacking >= 3)
690
- &OBS_ENDED
691
- else if (duration.attacking < 2)
692
- &OBS_CONTINUING
693
- else
694
- &OBS,
695
- };
696
- }
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 };
697
712
 
698
713
  /// Returns a slice with a range of values for binding given the `action`, observed `durations`,
699
714
  /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
@@ -701,12 +716,12 @@ pub const Rolls = struct {
701
716
  action: Action,
702
717
  duration: Duration,
703
718
  parent: Optional(bool),
704
- ) []const Optional(Observation) {
705
- if (parent == .true) return &OBS_NONE;
719
+ ) []const Optional(Observation(.binding)) {
720
+ if (parent == .true) return &BND_NONE;
706
721
  return switch (action.binding) {
707
- .None => &OBS_NONE,
708
- .started => &OBS_STARTED,
709
- 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,
710
725
  };
711
726
  }
712
727
 
@@ -723,7 +738,7 @@ pub const Rolls = struct {
723
738
  /// Returns a slice with the correct range of values for critical hits given the `action` state
724
739
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
725
740
  pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
726
- if (parent == .false) return &BOOL_NONE;
741
+ if (pkmn.options.showdown and parent == .false) return &BOOL_NONE;
727
742
  return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
728
743
  }
729
744
 
@@ -735,7 +750,7 @@ pub const Rolls = struct {
735
750
  }
736
751
 
737
752
  /// The min and max bounds on iteration over damage rolls.
738
- pub const Range = struct { min: u9, max: u9 };
753
+ pub const Range = struct { min: usize, max: usize };
739
754
 
740
755
  /// Returns the range bounding damage rolls given the `action` state and the state of
741
756
  /// the `parent` (whether the player's Pokémon's move hit).
@@ -748,7 +763,7 @@ pub const Rolls = struct {
748
763
 
749
764
  /// Returns the max damage roll which will produce the same damage as `roll`
750
765
  /// given the base damage in `summaries`.
751
- 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 {
752
767
  if (roll == 0) return roll;
753
768
 
754
769
  const dmg = summaries.get(player.foe()).damage;
@@ -760,9 +775,11 @@ pub const Rolls = struct {
760
775
 
761
776
  /// Returns a slice with the correct range of values for confused given the `action` state and
762
777
  /// the state of the `parent` (observation of the player's Pokémon confusion status).
763
- pub fn confused(action: Action, parent: Optional(Confusion)) []const Optional(bool) {
764
- const done = false; // parent == .None or parent == .ended;
765
- _ = parent; // FIXME
778
+ pub fn confused(
779
+ action: Action,
780
+ parent: Optional(Observation(.confusion)),
781
+ ) []const Optional(bool) {
782
+ const done = parent == .None or parent == .ended;
766
783
  return if (done or action.confused == .None) &BOOL_NONE else &BOOLS;
767
784
  }
768
785
 
@@ -773,24 +790,24 @@ pub const Rolls = struct {
773
790
  return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
774
791
  }
775
792
 
776
- const SLOT_NONE = [_]u4{0};
777
- const SLOT = [_]u4{ 1, 2, 3, 4 };
793
+ const SLOT_NONE = [_]u3{0};
794
+ const SLOT = [_]u3{ 1, 2, 3, 4 };
778
795
 
779
796
  /// Returns a slice with a range of values for move slots given the `action` state
780
797
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
781
798
  ///
782
799
  /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
783
- pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
800
+ pub fn moveSlot(action: Action, parent: Optional(bool)) []const u3 {
784
801
  if (parent == .false) return &SLOT_NONE;
785
802
  return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
786
803
  }
787
804
 
788
- const MULTI_NONE = [_]u4{0};
789
- const MULTI = [_]u4{ 2, 3, 4, 5 };
805
+ const MULTI_NONE = [_]u3{0};
806
+ const MULTI = [_]u3{ 2, 3, 4, 5 };
790
807
 
791
808
  /// Returns a slice with the correct range of values for multi hit given the `action` state
792
809
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
793
- pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
810
+ pub fn multiHit(action: Action, parent: Optional(bool)) []const u3 {
794
811
  if (parent == .false) return &MULTI_NONE;
795
812
  return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
796
813
  }
@@ -859,12 +876,12 @@ test "Rolls.damage" {
859
876
 
860
877
  test "Rolls.coalesce" {
861
878
  var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
862
- try expectEqual(@as(u8, 0), try Rolls.coalesce(.P2, 0, &summaries, false));
863
- try expectEqual(@as(u8, 241), try Rolls.coalesce(.P2, 238, &summaries, false));
864
- 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));
865
882
  summaries.p1.damage.final = 74;
866
- try expectEqual(@as(u8, 217), try Rolls.coalesce(.P2, 217, &summaries, false));
867
- 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));
868
885
  }
869
886
 
870
887
  test "Rolls.hit" {
@@ -893,15 +910,23 @@ test "Rolls.criticalHit" {
893
910
  &.{ .false, .true },
894
911
  Rolls.criticalHit(actions.p1, .None),
895
912
  );
896
- 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
+ );
897
918
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
898
919
  }
899
920
 
900
921
  test "Rolls.confused" {
901
922
  const actions: Actions = .{ .p2 = .{ .confused = .true } };
902
923
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1, .None));
903
- try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.confused(actions.p2, .continuing));
904
- // FIXME try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p2, .ended));
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));
905
930
  }
906
931
 
907
932
  test "Rolls.paralyzed" {
@@ -914,16 +939,16 @@ test "Rolls.paralyzed" {
914
939
 
915
940
  test "Rolls.moveSlot" {
916
941
  const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
917
- try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
918
- try expectEqualSlices(u4, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
919
- 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));
920
945
  }
921
946
 
922
947
  test "Rolls.multiHit" {
923
948
  const actions: Actions = .{ .p2 = .{ .multi_hit = 3 } };
924
- try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p1, .None));
925
- try expectEqualSlices(u4, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
926
- 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));
927
952
  }
928
953
 
929
954
  test "Rolls.metronome" {