@pkmn/engine 0.1.0-dev.8b192003 → 0.1.0-dev.8cf7e679

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 (76) hide show
  1. package/README.md +9 -6
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +44 -0
  4. package/build/pkg/addon/browser.js.map +1 -0
  5. package/build/pkg/addon/node.d.ts +3 -0
  6. package/build/pkg/addon/node.js +97 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon/wasm.d.ts +2 -0
  9. package/build/pkg/addon/wasm.js +44 -0
  10. package/build/pkg/addon/wasm.js.map +1 -0
  11. package/build/pkg/addon.d.ts +25 -3
  12. package/build/pkg/addon.js +34 -65
  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.d.ts +23 -1
  17. package/build/pkg/data.js.map +1 -1
  18. package/build/pkg/gen1.d.ts +13 -17
  19. package/build/pkg/gen1.js +6 -10
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +38 -90
  22. package/build/pkg/index.js +6 -63
  23. package/build/pkg/index.js.map +1 -1
  24. package/build/pkg/protocol.d.ts +20 -1
  25. package/build/pkg/protocol.js +105 -103
  26. package/build/pkg/protocol.js.map +1 -1
  27. package/build/tools/debug.js +35 -69
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +141 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +141 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +349 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +18 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +254 -77
  38. package/package.json +50 -134
  39. package/src/bin/install-pkmn-engine +9 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/common/array.zig +77 -0
  42. package/src/lib/common/data.zig +8 -8
  43. package/src/lib/common/debug.zig +39 -8
  44. package/src/lib/common/js.zig +4 -2
  45. package/src/lib/common/optional.zig +13 -9
  46. package/src/lib/common/options.zig +4 -0
  47. package/src/lib/common/protocol.zig +45 -44
  48. package/src/lib/common/rational.zig +53 -51
  49. package/src/lib/common/rng.zig +5 -7
  50. package/src/lib/common/util.zig +50 -1
  51. package/src/lib/gen1/calc.zig +295 -154
  52. package/src/lib/gen1/chance.zig +477 -119
  53. package/src/lib/gen1/data/moves.zig +2 -4
  54. package/src/lib/gen1/data/species.zig +1 -3
  55. package/src/lib/gen1/data/types.zig +1 -2
  56. package/src/lib/gen1/data.zig +35 -38
  57. package/src/lib/gen1/helpers.zig +17 -27
  58. package/src/lib/gen1/mechanics.zig +248 -148
  59. package/src/lib/gen2/calc.zig +37 -27
  60. package/src/lib/gen2/chance.zig +87 -67
  61. package/src/lib/gen2/data/items.zig +1 -3
  62. package/src/lib/gen2/data/moves.zig +2 -4
  63. package/src/lib/gen2/data/species.zig +1 -3
  64. package/src/lib/gen2/data/types.zig +1 -2
  65. package/src/lib/gen2/data.zig +26 -29
  66. package/src/lib/gen2/generated.zig +18 -23
  67. package/src/lib/gen2/helpers.zig +9 -18
  68. package/src/lib/gen2/mechanics.zig +81 -81
  69. package/src/lib/node.zig +2 -4
  70. package/src/lib/pkmn.zig +11 -5
  71. package/src/lib/wasm.zig +36 -1
  72. package/src/tools/display/dom.ts +96 -0
  73. package/src/tools/display/ui.css +354 -0
  74. package/build/test/display.js +0 -400
  75. package/build/test/display.js.map +0 -1
  76. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,44 +1,43 @@
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 Confusion = chance.Confusion;
20
+ const Duration = chance.Duration;
21
+ const Durations = chance.Durations;
22
+ const enabled = pkmn.options.calc;
18
23
  const expect = std.testing.expect;
19
24
  const expectEqual = std.testing.expectEqual;
20
25
  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
-
26
+ const isPointerTo = util.isPointerTo;
27
+ const Observation = chance.Observation;
28
28
  const Optional = optional.Optional;
29
-
30
- const Rational = rational.Rational;
31
-
29
+ const Player = common.Player;
32
30
  const PointerType = util.PointerType;
33
-
34
- const Actions = chance.Actions;
35
- const Action = chance.Action;
36
- const Chance = chance.Chance;
37
- const Durations = chance.Durations;
31
+ const Rational = rational.Rational;
32
+ const Result = common.Result;
33
+ const Sleeps = chance.Sleeps;
38
34
 
39
35
  const tty = true; // DEBUG
40
36
  const summary = false; // DEBUG
41
37
 
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
+
42
41
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
43
42
  pub const Summaries = extern struct {
44
43
  /// Relevant information for Player 1.
@@ -52,7 +51,7 @@ pub const Summaries = extern struct {
52
51
 
53
52
  /// Returns the `Summary` for the given `player`.
54
53
  pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
55
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Summaries);
54
+ assert(isPointerTo(self, Summaries));
56
55
  return if (player == .P1) &self.p1 else &self.p2;
57
56
  }
58
57
  };
@@ -88,21 +87,11 @@ pub const Summary = extern struct {
88
87
  }
89
88
  };
90
89
 
91
- /// TODO
92
- pub const Overrides = extern struct {
93
- /// TODO
94
- actions: Actions = .{},
95
-
96
- comptime {
97
- assert(@sizeOf(Overrides) == 16);
98
- }
99
- };
100
-
101
90
  /// Allows for forcing the value of specific RNG events during a Generation I battle `update` via
102
91
  /// `overrides` and tracks `summaries` of information relevant to damage calculation.
103
92
  pub const Calc = struct {
104
93
  /// Overrides the normal behavior of the RNG during an `update` to force specific outcomes.
105
- overrides: Overrides = .{},
94
+ overrides: Actions = .{},
106
95
  /// Information relevant to damage calculation.
107
96
  summaries: Summaries = .{},
108
97
 
@@ -113,16 +102,22 @@ pub const Calc = struct {
113
102
  ) ?std.meta.FieldType(Action, field) {
114
103
  if (!enabled) return null;
115
104
 
116
- const overrides =
117
- if (player == .P1) self.overrides.actions.p1 else self.overrides.actions.p2;
118
- const val = @field(overrides, @tagName(field));
105
+ const val = @field(self.overrides.get(player), @tagName(field));
119
106
  return if (switch (@typeInfo(@TypeOf(val))) {
120
- .Enum => val != .None,
121
- .Int => val != 0,
107
+ Enum => val != .None,
108
+ Int => val != 0,
122
109
  else => unreachable,
123
110
  }) val else null;
124
111
  }
125
112
 
113
+ pub fn confusion(self: *Calc, player: Player) void {
114
+ if (!enabled) return;
115
+
116
+ if (self.overrides.get(player).confusion == .started) {
117
+ self.overrides.get(player).confusion = .None;
118
+ }
119
+ }
120
+
126
121
  pub fn base(self: *Calc, player: Player, val: u16) void {
127
122
  if (!enabled) return;
128
123
 
@@ -156,6 +151,10 @@ const Null = struct {
156
151
  return null;
157
152
  }
158
153
 
154
+ pub fn confusion(self: Null, player: Player) void {
155
+ _ = .{ self, player };
156
+ }
157
+
159
158
  pub fn base(self: Null, player: Player, val: u16) void {
160
159
  _ = .{ self, player, val };
161
160
  }
@@ -183,7 +182,7 @@ pub const Options = struct {
183
182
  metronome: bool = false,
184
183
  };
185
184
 
186
- pub const MAX_FRONTIER = 19;
185
+ pub const MAX_FRONTIER = 81;
187
186
 
188
187
  pub fn transitions(
189
188
  battle: anytype,
@@ -202,18 +201,16 @@ pub fn transitions(
202
201
  var frontier = std.ArrayList(Actions).init(allocator);
203
202
  defer frontier.deinit();
204
203
 
204
+ const d = options.durations;
205
+
205
206
  var opts = pkmn.battle.options(
206
207
  protocol.NULL,
207
- Chance(Rational(u128)){ .probability = .{} },
208
+ Chance(Rational(u128)){ .probability = .{}, .durations = d },
208
209
  Calc{},
209
210
  );
210
211
 
211
212
  var b = battle;
212
213
  _ = try b.update(c1, c2, &opts);
213
-
214
- var d = options.durations;
215
- try d.update(&battle, &opts.chance.probability, opts.chance.actions);
216
-
217
214
  stats.updates += 1;
218
215
 
219
216
  const p1 = b.side(.P1);
@@ -231,9 +228,10 @@ pub fn transitions(
231
228
  var i: usize = 0;
232
229
  assert(frontier.items.len == 1);
233
230
  while (i < frontier.items.len) : (i += 1) {
234
- const template = frontier.items[i];
231
+ const f = frontier.items[i];
232
+ const saved = stats.saved;
235
233
 
236
- try debug(writer, template, .{
234
+ try debug(writer, f, .{
237
235
  .shape = true,
238
236
  .color = i,
239
237
  .bold = true,
@@ -241,33 +239,44 @@ pub fn transitions(
241
239
  .indent = false,
242
240
  });
243
241
 
244
- var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
245
-
246
- for (Rolls.speedTie(template.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
247
- for (Rolls.confused(template.p1)) |p1_cfzd| { a.p1.confused = p1_cfzd;
248
- for (Rolls.confused(template.p2)) |p2_cfzd| { a.p2.confused = p2_cfzd;
249
- for (Rolls.paralyzed(template.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
250
- for (Rolls.paralyzed(template.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
251
- for (Rolls.duration(template.p1, p1_par)) |p1_dur| { a.p1.duration = p1_dur;
252
- for (Rolls.duration(template.p2, p2_par)) |p2_dur| { a.p2.duration = p2_dur;
253
- for (Rolls.hit(template.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
254
- for (Rolls.hit(template.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
255
- for (Rolls.psywave(template.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
256
- for (Rolls.psywave(template.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
257
- for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
258
- for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
259
- for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
260
- for (Rolls.multiHit(template.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
261
- for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
262
- for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
263
- for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
264
- for (Rolls.criticalHit(template.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
265
-
266
- var p1_dmg = Rolls.damage(template.p1, p1_hit);
242
+ var a: Actions = .{
243
+ .p1 = .{ .metronome = p1_move, .pp = f.p1.pp, .duration = f.p1.duration },
244
+ .p2 = .{ .metronome = p2_move, .pp = f.p2.pp, .duration = f.p2.duration },
245
+ };
246
+
247
+ for (Rolls.speedTie(f.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
248
+ for (Rolls.sleep(f.p1, d.p1)) |p1_slp| { a.p1.sleep = p1_slp;
249
+ for (Rolls.sleep(f.p2, d.p2)) |p2_slp| { a.p2.sleep = p2_slp;
250
+ for (Rolls.disable(f.p1, d.p1, p1_slp)) |p1_dis| { a.p1.disable = p1_dis;
251
+ for (Rolls.disable(f.p2, d.p2, p2_slp)) |p2_dis| { a.p2.disable = p2_dis;
252
+ for (Rolls.attacking(f.p1, d.p1, p1_slp)) |p1_atk| { a.p1.attacking = p1_atk;
253
+ 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;
256
+ for (Rolls.confused(f.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
257
+ for (Rolls.confused(f.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
258
+ for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
259
+ for (Rolls.paralyzed(f.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
260
+ for (Rolls.binding(f.p1, d.p1, p1_par)) |p1_bind| { a.p1.binding = p1_bind;
261
+ for (Rolls.binding(f.p2, d.p2, p2_par)) |p2_bind| { a.p2.binding = p2_bind;
262
+ for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
263
+ for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
264
+ for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
265
+ for (Rolls.psywave(f.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
266
+ for (Rolls.moveSlot(f.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
267
+ for (Rolls.moveSlot(f.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
268
+ for (Rolls.multiHit(f.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
269
+ for (Rolls.multiHit(f.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
270
+ for (Rolls.secondaryChance(f.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
271
+ for (Rolls.secondaryChance(f.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
272
+ for (Rolls.criticalHit(f.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
273
+ for (Rolls.criticalHit(f.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
274
+
275
+ var p1_dmg = Rolls.damage(f.p1, p1_hit);
267
276
  while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
268
277
  a.p1.damage = @intCast(p1_dmg.min);
269
278
 
270
- var p2_dmg = Rolls.damage(template.p2, p2_hit);
279
+ var p2_dmg = Rolls.damage(f.p2, p2_hit);
271
280
 
272
281
  const p1_min: u9 = p1_dmg.min;
273
282
  const p2_min: u9 = p2_dmg.min;
@@ -275,17 +284,14 @@ pub fn transitions(
275
284
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
276
285
  a.p2.damage = @intCast(p2_dmg.min);
277
286
 
278
- opts.calc.overrides.actions = a;
287
+ opts.calc.overrides = a;
279
288
  opts.calc.summaries = .{};
280
- opts.chance = .{ .probability = .{} };
289
+ opts.chance = .{ .probability = .{}, .durations = d };
281
290
  const q = &opts.chance.probability;
282
291
 
283
292
  b = battle;
284
293
  _ = try b.update(c1, c2, &opts);
285
294
 
286
- d = options.durations;
287
- try d.update(&b, &q, opts.chance.actions);
288
-
289
295
  stats.updates += 1;
290
296
 
291
297
  const summaries = &opts.calc.summaries;
@@ -296,8 +302,8 @@ pub fn transitions(
296
302
  const p2_max: u9 =
297
303
  try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
298
304
 
299
- if (opts.chance.actions.matches(template)) {
300
- if (!opts.chance.actions.eql(a)) {
305
+ if (opts.chance.actions.matches(f)) {
306
+ if (!opts.chance.actions.relax().eql(a)) {
301
307
  if (!summary) {
302
308
  try debug(writer, opts.chance.actions, .{
303
309
  .p1_max = p1_max,
@@ -331,10 +337,13 @@ pub fn transitions(
331
337
  }
332
338
  }
333
339
  }
334
-
335
340
  if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
336
341
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
342
+
343
+ q.reduce();
337
344
  try p.add(q);
345
+ p.reduce();
346
+
338
347
  stats.saved += 1;
339
348
 
340
349
  if (p.q < p.p) {
@@ -342,7 +351,7 @@ pub fn transitions(
342
351
  return error.TestUnexpectedResult;
343
352
  }
344
353
  } else {
345
- if (!matches(opts.chance.actions, i, frontier.items)) {
354
+ if (!opts.chance.actions.matchesAny(frontier.items, i)) {
346
355
  try frontier.append(opts.chance.actions);
347
356
 
348
357
  try debug(writer, opts.chance.actions, .{
@@ -372,9 +381,11 @@ pub fn transitions(
372
381
  p2_dmg.min = p2_max;
373
382
  }
374
383
 
375
- }}}}}}}}}}}}}}}}}}}}
384
+ }}}}}}}}}}}}}}}}}}}}}}}}}}}}
376
385
 
377
- if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
386
+ assert(stats.saved > saved);
387
+ const old = @hasDecl(std.fmt, "formatFloatDecimal");
388
+ if (!old and @TypeOf(writer) != @TypeOf(std.io.null_writer)) {
378
389
  p.reduce();
379
390
  try writer.print(
380
391
  " = {s} ({d:.2}%)\n\n",
@@ -404,7 +415,6 @@ pub fn transitions(
404
415
 
405
416
  pub fn update(
406
417
  battle: anytype,
407
- durations: *Durations,
408
418
  c1: Choice,
409
419
  c2: Choice,
410
420
  options: anytype,
@@ -412,40 +422,38 @@ pub fn update(
412
422
  writer: anytype,
413
423
  transition: bool,
414
424
  ) !Result {
425
+ const durations = options.chance.durations;
415
426
  options.chance.reset();
416
427
 
417
- if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
418
- return battle.update(c1, c2, options);
419
- }
428
+ if (!pkmn.options.chance or !pkmn.options.calc) return battle.update(c1, c2, options);
420
429
 
421
- var copy = battle.*;
422
- const saved = durations.*;
423
- const actions = options.chance.actions;
430
+ const original = battle.*;
424
431
 
425
432
  // Perfom the actual update
426
433
  const result = battle.update(c1, c2, options);
427
- try durations.update(&battle, actions, &options.chance.probability);
434
+ var overrides = options.chance.actions;
435
+ const updated = options.chance.durations;
428
436
 
429
- // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
430
- var buf: [helpers.MAX_DIFFS]u8 = undefined;
431
- var stream: protocol.ByteStream = .{ .buffer = &buf };
432
- const n = try helpers.diff(&copy, battle, stream.writer());
437
+ if (@TypeOf(battle.rng) == data.PRNG) {
438
+ // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
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());
433
442
 
434
- // Applying the diff to the battle should take us back to the original copy
435
- // of the battle (ignoring the RNG).
436
- var patched = battle.*;
437
- patched.rng = copy.rng;
438
- helpers.patch(&patched, buf[0..n]);
439
- try expectEqual(copy, patched);
440
-
441
- if (!pkmn.options.chance or !pkmn.options.calc) return result;
443
+ // Applying the diff to the battle should take us back to the original copy
444
+ // of the battle (ignoring the RNG).
445
+ var patched = battle.*;
446
+ patched.rng = original.rng;
447
+ helpers.patch(&patched, buf[0..n]);
448
+ try expectEqual(original, patched);
449
+ }
442
450
 
443
451
  if (transition) {
444
452
  // Ensure we can generate all transitions from the same original state
445
453
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
446
454
  // the transitions function relies on RNG for discovery of states)
447
- if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
448
- .durations = saved,
455
+ if (try transitions(unfix(original), c1, c2, allocator, writer, .{
456
+ .durations = durations,
449
457
  .cap = true,
450
458
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
451
459
  }
@@ -454,21 +462,50 @@ pub fn update(
454
462
  // same as we observed.
455
463
  var override = pkmn.battle.options(
456
464
  protocol.NULL,
457
- Chance(Rational(u128)){ .probability = .{} },
458
- Calc{ .overrides = .{ .actions = actions } },
465
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
466
+ Calc{ .overrides = overrides },
459
467
  );
460
- const overridden = copy.update(c1, c2, &override);
468
+ var copy = original;
469
+ var overridden = copy.update(c1, c2, &override);
461
470
  try expectEqual(result, overridden);
462
- try expectEqual(override.chance.actions, actions);
471
+ try expectEqualActions(overrides, override.chance.actions);
472
+ try expectEqual(updated, override.chance.durations);
463
473
 
464
474
  // The actual battle excluding its RNG field should match a copy updated with
465
475
  // overridden RNG (the copy RNG may have advanced because of no-ops)
466
476
  copy.rng = battle.rng;
467
- try expectEqual(copy, battle.*);
477
+ try expectEqual(battle.*, copy);
478
+
479
+ // Demonstrate that even with the duration rolls changed, the duration
480
+ // fields on their own are sufficient to achieve the same outcomes
481
+ // NB: The actual battle bytes will differ because the durations won't match
482
+ overrides.p1.duration = if (overrides.p1.duration > 0) 2 else 0;
483
+ overrides.p2.duration = if (overrides.p2.duration > 0) 2 else 0;
484
+
485
+ override = pkmn.battle.options(
486
+ protocol.NULL,
487
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
488
+ Calc{ .overrides = overrides },
489
+ );
490
+ copy = original;
491
+ overridden = copy.update(c1, c2, &override);
492
+ try expectEqual(result, overridden);
493
+ try expectEqualActions(overrides, override.chance.actions);
494
+ try expectEqual(updated, override.chance.durations);
468
495
 
469
496
  return result;
470
497
  }
471
498
 
499
+ fn expectEqualActions(expected: Actions, actual: Actions) !void {
500
+ return expectEqual(expected, actual) catch |e| switch (e) {
501
+ error.TestExpectedEqual => {
502
+ std.debug.print("expected {}, found {}\n", .{ expected, actual });
503
+ return e;
504
+ },
505
+ else => return e,
506
+ };
507
+ }
508
+
472
509
  fn unfix(actual: anytype) data.Battle(data.PRNG) {
473
510
  return .{
474
511
  .sides = actual.sides,
@@ -484,15 +521,6 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
484
521
  };
485
522
  }
486
523
 
487
- fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
488
- for (frontier, 0..) |f, j| {
489
- // TODO: is skipping this redundant check worth it?
490
- if (i == j) continue;
491
- if (f.matches(actions)) return true;
492
- }
493
- return false;
494
- }
495
-
496
524
  fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
497
525
  const w = std.io.getStdErr().writer();
498
526
  w.print(fmt, v) catch return;
@@ -565,8 +593,6 @@ fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
565
593
  try writer.writeAll(output[0..len]);
566
594
  }
567
595
 
568
- const NONE = @intFromEnum(Optional(bool).None);
569
-
570
596
  /// Helper functions that efficiently return valid ranges for various RNG events based on the
571
597
  /// state of an `Action` and other events to be used to construct a "transitions" function.
572
598
  pub const Rolls = struct {
@@ -578,6 +604,108 @@ pub const Rolls = struct {
578
604
  return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
579
605
  }
580
606
 
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 };
613
+
614
+ /// Returns a slice with a range of values for sleep given the `action` state and observed
615
+ /// `durations`.
616
+ pub fn sleep(action: Action, duration: Duration) []const Optional(Observation) {
617
+ if (action.sleep == .None) return &OBS_NONE;
618
+ return if (Sleeps.get(duration.sleeps, 0) == 0 and action.speed_tie != .None)
619
+ &OBS_ALL
620
+ else switch (action.sleep) {
621
+ .started => &OBS_STARTED,
622
+ else => if (Sleeps.get(duration.sleeps, 0) >= 7) &OBS_ENDED else &OBS,
623
+ };
624
+ }
625
+
626
+ /// Returns a slice with a range of values for disable given the `action`, observed `durations`,
627
+ /// and the state of the `parent` (observation of the player's Pokémon sleep status).
628
+ pub fn disable(
629
+ action: Action,
630
+ duration: Duration,
631
+ parent: Optional(Observation),
632
+ ) []const Optional(Observation) {
633
+ if (action.disable == .None) return &OBS_NONE;
634
+ return if (duration.disable == 0 and action.speed_tie != .None)
635
+ &OBS_ALL
636
+ else switch (action.disable) {
637
+ .started => &OBS_STARTED,
638
+ else => if (duration.disable >= 8)
639
+ &OBS_ENDED
640
+ else if (parent != .None and parent != .started)
641
+ &OBS_CONTINUING
642
+ else
643
+ &OBS,
644
+ };
645
+ }
646
+
647
+ /// FIXME
648
+ pub fn attacking(
649
+ action: Action,
650
+ duration: Duration,
651
+ parent: Optional(Observation),
652
+ ) []const Optional(Observation) {
653
+ return switch (action.attacking) {
654
+ .None => &OBS_NONE,
655
+ .started => &OBS_STARTED,
656
+ else => if (duration.attacking >= 3)
657
+ &OBS_ENDED
658
+ else if ((parent == .continuing and parent == .ended) or duration.attacking < 2)
659
+ &OBS_CONTINUING
660
+ else
661
+ &OBS,
662
+ };
663
+ }
664
+
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 };
671
+
672
+ /// FIXME
673
+ pub fn confusion(
674
+ action: Action,
675
+ duration: Duration,
676
+ sibling: Optional(Observation),
677
+ parent: Optional(Observation),
678
+ ) []const Optional(Confusion) {
679
+ return switch (action.confusion) {
680
+ .None => &CFZ_NONE,
681
+ .started => if (pkmn.options.overwrite and sibling == .ended and duration.confusion < 5)
682
+ &CFZ_OVERWRITTEN
683
+ else
684
+ &CFZ_STARTED,
685
+ else => if (duration.confusion >= 5)
686
+ &CFZ_ENDED
687
+ else if ((parent == .continuing and parent == .ended) or duration.confusion < 2)
688
+ &CFZ_CONTINUING
689
+ else
690
+ &CFZ,
691
+ };
692
+ }
693
+
694
+ /// Returns a slice with a range of values for binding given the `action`, observed `durations`,
695
+ /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
696
+ pub fn binding(
697
+ action: Action,
698
+ duration: Duration,
699
+ parent: Optional(bool),
700
+ ) []const Optional(Observation) {
701
+ if (parent == .true) return &OBS_NONE;
702
+ return switch (action.binding) {
703
+ .None => &OBS_NONE,
704
+ .started => &OBS_STARTED,
705
+ else => if (duration.binding >= 4) &OBS_ENDED else &OBS,
706
+ };
707
+ }
708
+
581
709
  const BOOL_NONE = [_]Optional(bool){.None};
582
710
  const BOOLS = [_]Optional(bool){ .false, .true };
583
711
 
@@ -591,7 +719,7 @@ pub const Rolls = struct {
591
719
  /// Returns a slice with the correct range of values for critical hits given the `action` state
592
720
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
593
721
  pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
594
- if (parent == .false) return &BOOL_NONE;
722
+ if (pkmn.options.showdown and parent == .false) return &BOOL_NONE;
595
723
  return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
596
724
  }
597
725
 
@@ -626,9 +754,11 @@ pub const Rolls = struct {
626
754
  return @min(255, roll + ((254 - ((@as(u32, dmg.base) * roll) % 255)) / dmg.base));
627
755
  }
628
756
 
629
- /// Returns a slice with the correct range of values for confused.
630
- pub fn confused(action: Action) []const Optional(bool) {
631
- return if (action.confused == .None) &BOOL_NONE else &BOOLS;
757
+ /// Returns a slice with the correct range of values for confused given the `action` state and
758
+ /// 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) {
760
+ const done = parent == .None or parent == .ended;
761
+ return if (done or action.confused == .None) &BOOL_NONE else &BOOLS;
632
762
  }
633
763
 
634
764
  /// Returns a slice with the correct range of values for paralysis given the `action` state
@@ -638,17 +768,6 @@ pub const Rolls = struct {
638
768
  return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
639
769
  }
640
770
 
641
- const DURATION_NONE = [_]u2{0};
642
- // NB: DURATION must be at least 2 because that is the maximum of the minimums for all rolls
643
- const DURATION = [_]u2{2};
644
-
645
- /// Returns a slice with a range of values for duration given the `action` state
646
- /// and the state of the `parent` (whether the player's Pokémon's move hit).
647
- pub fn duration(action: Action, parent: Optional(bool)) []const u2 {
648
- if (parent == .false) return &DURATION_NONE;
649
- return if (action.duration == 0) &DURATION_NONE else &DURATION;
650
- }
651
-
652
771
  const SLOT_NONE = [_]u4{0};
653
772
  const SLOT = [_]u4{ 1, 2, 3, 4 };
654
773
 
@@ -706,6 +825,26 @@ test "Rolls.speedTie" {
706
825
  try expectEqualSlices(Optional(Player), &.{.None}, Rolls.speedTie(actions.p2));
707
826
  }
708
827
 
828
+ test "Rolls.sleep" {
829
+ return error.SkipZigTest; // TODO
830
+ }
831
+
832
+ test "Rolls.disable" {
833
+ return error.SkipZigTest; // TODO
834
+ }
835
+
836
+ test "Rolls.confusion" {
837
+ return error.SkipZigTest; // TODO
838
+ }
839
+
840
+ test "Rolls.atacking" {
841
+ return error.SkipZigTest; // TODO
842
+ }
843
+
844
+ test "Rolls.binding" {
845
+ return error.SkipZigTest; // TODO
846
+ }
847
+
709
848
  test "Rolls.damage" {
710
849
  const actions: Actions = .{ .p2 = .{ .damage = 221 } };
711
850
  try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p1, .None));
@@ -715,12 +854,12 @@ test "Rolls.damage" {
715
854
 
716
855
  test "Rolls.coalesce" {
717
856
  var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
718
- try expectEqual(0, try Rolls.coalesce(.P2, 0, &summaries, false));
719
- try expectEqual(241, try Rolls.coalesce(.P2, 238, &summaries, false));
720
- try expectEqual(255, try Rolls.coalesce(.P2, 238, &summaries, 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));
721
860
  summaries.p1.damage.final = 74;
722
- try expectEqual(217, try Rolls.coalesce(.P2, 217, &summaries, false));
723
- try expectEqual(255, try Rolls.coalesce(.P2, 217, &summaries, true));
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));
724
863
  }
725
864
 
726
865
  test "Rolls.hit" {
@@ -749,14 +888,23 @@ test "Rolls.criticalHit" {
749
888
  &.{ .false, .true },
750
889
  Rolls.criticalHit(actions.p1, .None),
751
890
  );
752
- try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p1, .false));
891
+ try expectEqualSlices(
892
+ Optional(bool),
893
+ if (pkmn.options.showdown) &.{.None} else &.{ .false, .true },
894
+ Rolls.criticalHit(actions.p1, .false),
895
+ );
753
896
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
754
897
  }
755
898
 
756
899
  test "Rolls.confused" {
757
900
  const actions: Actions = .{ .p2 = .{ .confused = .true } };
758
- try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1));
759
- try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.confused(actions.p2));
901
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1, .None));
902
+ try expectEqualSlices(
903
+ Optional(bool),
904
+ &.{ .false, .true },
905
+ Rolls.confused(actions.p2, .continuing),
906
+ );
907
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p2, .ended));
760
908
  }
761
909
 
762
910
  test "Rolls.paralyzed" {
@@ -767,13 +915,6 @@ test "Rolls.paralyzed" {
767
915
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p2, .true));
768
916
  }
769
917
 
770
- test "Rolls.duration" {
771
- const actions: Actions = .{ .p2 = .{ .duration = 3 } };
772
- try expectEqualSlices(u2, &.{0}, Rolls.duration(actions.p1, .None));
773
- try expectEqualSlices(u2, &.{2}, Rolls.duration(actions.p2, .None));
774
- try expectEqualSlices(u2, &.{0}, Rolls.duration(actions.p2, .false));
775
- }
776
-
777
918
  test "Rolls.moveSlot" {
778
919
  const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
779
920
  try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
@@ -804,9 +945,9 @@ test "Rolls.psywave" {
804
945
 
805
946
  try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p1, &side, .None));
806
947
  var rolls = Rolls.psywave(actions.p2, &side, .None);
807
- try expectEqual(150, rolls[rolls.len - 1]);
948
+ try expectEqual(@as(u8, 150), rolls[rolls.len - 1]);
808
949
  side.stored().level = 81;
809
950
  rolls = Rolls.psywave(actions.p2, &side, .None);
810
- try expectEqual(121, rolls[rolls.len - 1]);
951
+ try expectEqual(@as(u8, 121), rolls[rolls.len - 1]);
811
952
  try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p2, &side, .false));
812
953
  }