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

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 (79) 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 +47 -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 +110 -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 +53 -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 +15 -18
  19. package/build/pkg/gen1.js +28 -19
  20. package/build/pkg/gen1.js.map +1 -1
  21. package/build/pkg/index.d.ts +40 -90
  22. package/build/pkg/index.js +23 -70
  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 +52 -76
  28. package/build/tools/debug.js.map +1 -1
  29. package/build/tools/display/debug.jsx +152 -0
  30. package/build/tools/display/debug.jsx.map +1 -0
  31. package/build/tools/display/index.js +152 -0
  32. package/build/tools/display/index.js.map +1 -0
  33. package/build/tools/display/ui.jsx +359 -0
  34. package/build/tools/display/ui.jsx.map +1 -0
  35. package/build/tools/display/util.js +33 -0
  36. package/build/tools/display/util.js.map +1 -0
  37. package/build.zig +330 -85
  38. package/package.json +54 -135
  39. package/src/bin/install-pkmn-engine +10 -7
  40. package/src/bin/pkmn-debug +1 -1
  41. package/src/lib/bindings/c.zig +255 -0
  42. package/src/lib/bindings/node.zig +121 -0
  43. package/src/lib/bindings/wasm.zig +59 -0
  44. package/src/lib/common/array.zig +77 -0
  45. package/src/lib/common/data.zig +8 -8
  46. package/src/lib/common/debug.zig +39 -8
  47. package/src/lib/common/js.zig +4 -2
  48. package/src/lib/common/optional.zig +13 -9
  49. package/src/lib/common/options.zig +4 -0
  50. package/src/lib/common/protocol.zig +45 -44
  51. package/src/lib/common/rational.zig +256 -181
  52. package/src/lib/common/rng.zig +5 -7
  53. package/src/lib/common/util.zig +60 -1
  54. package/src/lib/gen1/calc.zig +314 -164
  55. package/src/lib/gen1/chance.zig +477 -119
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +1 -3
  58. package/src/lib/gen1/data/types.zig +1 -2
  59. package/src/lib/gen1/data.zig +35 -38
  60. package/src/lib/gen1/helpers.zig +17 -27
  61. package/src/lib/gen1/mechanics.zig +249 -156
  62. package/src/lib/gen2/calc.zig +37 -27
  63. package/src/lib/gen2/chance.zig +87 -67
  64. package/src/lib/gen2/data/items.zig +1 -3
  65. package/src/lib/gen2/data/moves.zig +2 -4
  66. package/src/lib/gen2/data/species.zig +1 -3
  67. package/src/lib/gen2/data/types.zig +1 -2
  68. package/src/lib/gen2/data.zig +26 -29
  69. package/src/lib/gen2/generated.zig +18 -23
  70. package/src/lib/gen2/helpers.zig +9 -18
  71. package/src/lib/gen2/mechanics.zig +81 -81
  72. package/src/lib/node.zig +3 -116
  73. package/src/lib/pkmn.zig +14 -7
  74. package/src/lib/wasm.zig +13 -9
  75. package/src/tools/display/dom.ts +93 -0
  76. package/src/tools/display/ui.css +354 -0
  77. package/build/test/display.js +0 -400
  78. package/build/test/display.js.map +0 -1
  79. 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,24 +201,22 @@ 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);
220
217
  const p2 = b.side(.P2);
221
218
 
222
- var p: Rational(u128) = .{ .p = 0, .q = 1 };
219
+ var p: Rational(u256) = .{ .p = 0, .q = 1 };
223
220
  try frontier.append(opts.chance.actions);
224
221
 
225
222
  // zig fmt: off
@@ -231,9 +228,12 @@ 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;
233
+
234
+ var r: Rational(u256) = .{ .p = 0, .q = 1 };
235
235
 
236
- try debug(writer, template, .{
236
+ try debug(writer, f, .{
237
237
  .shape = true,
238
238
  .color = i,
239
239
  .bold = true,
@@ -241,33 +241,44 @@ pub fn transitions(
241
241
  .indent = false,
242
242
  });
243
243
 
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);
244
+ var a: Actions = .{
245
+ .p1 = .{ .metronome = p1_move, .pp = f.p1.pp, .duration = f.p1.duration },
246
+ .p2 = .{ .metronome = p2_move, .pp = f.p2.pp, .duration = f.p2.duration },
247
+ };
248
+
249
+ for (Rolls.speedTie(f.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
250
+ for (Rolls.sleep(f.p1, d.p1)) |p1_slp| { a.p1.sleep = p1_slp;
251
+ for (Rolls.sleep(f.p2, d.p2)) |p2_slp| { a.p2.sleep = p2_slp;
252
+ for (Rolls.disable(f.p1, d.p1, p1_slp)) |p1_dis| { a.p1.disable = p1_dis;
253
+ for (Rolls.disable(f.p2, d.p2, p2_slp)) |p2_dis| { a.p2.disable = p2_dis;
254
+ for (Rolls.attacking(f.p1, d.p1, p1_slp)) |p1_atk| { a.p1.attacking = p1_atk;
255
+ for (Rolls.attacking(f.p2, d.p2, p2_slp)) |p2_atk| { a.p2.attacking = p2_atk;
256
+ for (Rolls.confusion(f.p1, d.p1, tie, p1_atk, p1_slp)) |p1_cfz| { a.p1.confusion = p1_cfz;
257
+ for (Rolls.confusion(f.p2, d.p2, tie, p2_atk, p2_slp)) |p2_cfz| { a.p2.confusion = p2_cfz;
258
+ for (Rolls.confused(f.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
259
+ for (Rolls.confused(f.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
260
+ for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
261
+ for (Rolls.paralyzed(f.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
262
+ for (Rolls.binding(f.p1, d.p1, p1_par)) |p1_bind| { a.p1.binding = p1_bind;
263
+ for (Rolls.binding(f.p2, d.p2, p2_par)) |p2_bind| { a.p2.binding = p2_bind;
264
+ for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
265
+ for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
266
+ for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
267
+ for (Rolls.psywave(f.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
268
+ for (Rolls.moveSlot(f.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
269
+ for (Rolls.moveSlot(f.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
270
+ for (Rolls.multiHit(f.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
271
+ for (Rolls.multiHit(f.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
272
+ for (Rolls.secondaryChance(f.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
273
+ for (Rolls.secondaryChance(f.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
274
+ for (Rolls.criticalHit(f.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
275
+ for (Rolls.criticalHit(f.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
276
+
277
+ var p1_dmg = Rolls.damage(f.p1, p1_hit);
267
278
  while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
268
279
  a.p1.damage = @intCast(p1_dmg.min);
269
280
 
270
- var p2_dmg = Rolls.damage(template.p2, p2_hit);
281
+ var p2_dmg = Rolls.damage(f.p2, p2_hit);
271
282
 
272
283
  const p1_min: u9 = p1_dmg.min;
273
284
  const p2_min: u9 = p2_dmg.min;
@@ -275,29 +286,26 @@ pub fn transitions(
275
286
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
276
287
  a.p2.damage = @intCast(p2_dmg.min);
277
288
 
278
- opts.calc.overrides.actions = a;
289
+ opts.calc.overrides = a;
279
290
  opts.calc.summaries = .{};
280
- opts.chance = .{ .probability = .{} };
291
+ opts.chance = .{ .probability = .{}, .durations = d };
281
292
  const q = &opts.chance.probability;
282
293
 
283
294
  b = battle;
284
295
  _ = try b.update(c1, c2, &opts);
285
296
 
286
- d = options.durations;
287
- try d.update(&b, &q, opts.chance.actions);
288
-
289
297
  stats.updates += 1;
290
298
 
291
299
  const summaries = &opts.calc.summaries;
292
300
  const p1_max: u9 = if (p2_dmg.min != p2_min)
293
301
  p1_dmg.min
294
302
  else
295
- try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
303
+ Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
296
304
  const p2_max: u9 =
297
- try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
305
+ Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
298
306
 
299
- if (opts.chance.actions.matches(template)) {
300
- if (!opts.chance.actions.eql(a)) {
307
+ if (opts.chance.actions.matches(f)) {
308
+ if (!opts.chance.actions.relax().eql(a)) {
301
309
  if (!summary) {
302
310
  try debug(writer, opts.chance.actions, .{
303
311
  .p1_max = p1_max,
@@ -331,10 +339,15 @@ pub fn transitions(
331
339
  }
332
340
  }
333
341
  }
334
-
335
342
  if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
336
343
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
344
+
345
+ q.reduce();
337
346
  try p.add(q);
347
+ p.reduce();
348
+ try r.add(q);
349
+ r.reduce();
350
+
338
351
  stats.saved += 1;
339
352
 
340
353
  if (p.q < p.p) {
@@ -342,7 +355,7 @@ pub fn transitions(
342
355
  return error.TestUnexpectedResult;
343
356
  }
344
357
  } else {
345
- if (!matches(opts.chance.actions, i, frontier.items)) {
358
+ if (!opts.chance.actions.matchesAny(frontier.items, i)) {
346
359
  try frontier.append(opts.chance.actions);
347
360
 
348
361
  try debug(writer, opts.chance.actions, .{
@@ -372,15 +385,18 @@ pub fn transitions(
372
385
  p2_dmg.min = p2_max;
373
386
  }
374
387
 
375
- }}}}}}}}}}}}}}}}}}}}
388
+ }}}}}}}}}}}}}}}}}}}}}}}}}}}}
376
389
 
377
- if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
378
- p.reduce();
379
- try writer.print(
380
- " = {s} ({d:.2}%)\n\n",
381
- .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
382
- );
383
- }
390
+ assert(stats.saved > saved);
391
+ // TODO: ziglang/zig#22667
392
+ // const old = @hasDecl(std.fmt, "formatFloatDecimal");
393
+ // if (!old and @TypeOf(writer) != @TypeOf(std.io.null_writer)) {
394
+ // try writer.print(
395
+ // " {} ({d:.2}%)\n = ──────────\n {} ({d:.2}%)\n\n",
396
+ // .{r, 100 * @as(f128, @floatFromInt(r.p)) / @as(f128, @floatFromInt(r.q)),
397
+ // p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
398
+ // );
399
+ // }
384
400
 
385
401
  }
386
402
 
@@ -404,7 +420,6 @@ pub fn transitions(
404
420
 
405
421
  pub fn update(
406
422
  battle: anytype,
407
- durations: *Durations,
408
423
  c1: Choice,
409
424
  c2: Choice,
410
425
  options: anytype,
@@ -412,40 +427,38 @@ pub fn update(
412
427
  writer: anytype,
413
428
  transition: bool,
414
429
  ) !Result {
430
+ const durations = options.chance.durations;
415
431
  options.chance.reset();
416
432
 
417
- if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
418
- return battle.update(c1, c2, options);
419
- }
433
+ if (!pkmn.options.chance or !pkmn.options.calc) return battle.update(c1, c2, options);
420
434
 
421
- var copy = battle.*;
422
- const saved = durations.*;
423
- const actions = options.chance.actions;
435
+ const original = battle.*;
424
436
 
425
437
  // Perfom the actual update
426
438
  const result = battle.update(c1, c2, options);
427
- try durations.update(&battle, actions, &options.chance.probability);
428
-
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());
439
+ var overrides = options.chance.actions;
440
+ const updated = options.chance.durations;
433
441
 
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);
442
+ if (@TypeOf(battle.rng) == data.PRNG) {
443
+ // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
444
+ var buf: [helpers.MAX_DIFFS]u8 = undefined;
445
+ var stream: protocol.ByteStream = .{ .buffer = &buf };
446
+ const n = try helpers.diff(&original, battle, stream.writer());
440
447
 
441
- if (!pkmn.options.chance or !pkmn.options.calc) return result;
448
+ // Applying the diff to the battle should take us back to the original copy
449
+ // of the battle (ignoring the RNG).
450
+ var patched = battle.*;
451
+ patched.rng = original.rng;
452
+ helpers.patch(&patched, buf[0..n]);
453
+ try expectEqual(original, patched);
454
+ }
442
455
 
443
456
  if (transition) {
444
457
  // Ensure we can generate all transitions from the same original state
445
458
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
446
459
  // the transitions function relies on RNG for discovery of states)
447
- if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
448
- .durations = saved,
460
+ if (try transitions(unfix(original), c1, c2, allocator, writer, .{
461
+ .durations = durations,
449
462
  .cap = true,
450
463
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
451
464
  }
@@ -454,21 +467,50 @@ pub fn update(
454
467
  // same as we observed.
455
468
  var override = pkmn.battle.options(
456
469
  protocol.NULL,
457
- Chance(Rational(u128)){ .probability = .{} },
458
- Calc{ .overrides = .{ .actions = actions } },
470
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
471
+ Calc{ .overrides = overrides },
459
472
  );
460
- const overridden = copy.update(c1, c2, &override);
473
+ var copy = original;
474
+ var overridden = copy.update(c1, c2, &override);
461
475
  try expectEqual(result, overridden);
462
- try expectEqual(override.chance.actions, actions);
476
+ try expectEqualActions(overrides, override.chance.actions);
477
+ try expectEqual(updated, override.chance.durations);
463
478
 
464
479
  // The actual battle excluding its RNG field should match a copy updated with
465
480
  // overridden RNG (the copy RNG may have advanced because of no-ops)
466
481
  copy.rng = battle.rng;
467
- try expectEqual(copy, battle.*);
482
+ try expectEqual(battle.*, copy);
483
+
484
+ // Demonstrate that even with the duration rolls changed, the duration
485
+ // fields on their own are sufficient to achieve the same outcomes
486
+ // NB: The actual battle bytes will differ because the durations won't match
487
+ overrides.p1.duration = if (overrides.p1.duration > 0) 2 else 0;
488
+ overrides.p2.duration = if (overrides.p2.duration > 0) 2 else 0;
489
+
490
+ override = pkmn.battle.options(
491
+ protocol.NULL,
492
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
493
+ Calc{ .overrides = overrides },
494
+ );
495
+ copy = original;
496
+ overridden = copy.update(c1, c2, &override);
497
+ try expectEqual(result, overridden);
498
+ try expectEqualActions(overrides, override.chance.actions);
499
+ try expectEqual(updated, override.chance.durations);
468
500
 
469
501
  return result;
470
502
  }
471
503
 
504
+ fn expectEqualActions(expected: Actions, actual: Actions) !void {
505
+ return expectEqual(expected, actual) catch |e| switch (e) {
506
+ error.TestExpectedEqual => {
507
+ std.debug.print("expected {}, found {}\n", .{ expected, actual });
508
+ return e;
509
+ },
510
+ else => return e,
511
+ };
512
+ }
513
+
472
514
  fn unfix(actual: anytype) data.Battle(data.PRNG) {
473
515
  return .{
474
516
  .sides = actual.sides,
@@ -484,15 +526,6 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
484
526
  };
485
527
  }
486
528
 
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
529
  fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
497
530
  const w = std.io.getStdErr().writer();
498
531
  w.print(fmt, v) catch return;
@@ -565,8 +598,6 @@ fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
565
598
  try writer.writeAll(output[0..len]);
566
599
  }
567
600
 
568
- const NONE = @intFromEnum(Optional(bool).None);
569
-
570
601
  /// Helper functions that efficiently return valid ranges for various RNG events based on the
571
602
  /// state of an `Action` and other events to be used to construct a "transitions" function.
572
603
  pub const Rolls = struct {
@@ -578,6 +609,112 @@ pub const Rolls = struct {
578
609
  return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
579
610
  }
580
611
 
612
+ const OBS_NONE = [_]Optional(Observation){.None};
613
+ const OBS_STARTED = [_]Optional(Observation){.started};
614
+ const OBS_CONTINUING = [_]Optional(Observation){.continuing};
615
+ const OBS_ENDED = [_]Optional(Observation){.ended};
616
+ const OBS_ALL = [_]Optional(Observation){ .started, .continuing, .ended };
617
+ const OBS = [_]Optional(Observation){ .continuing, .ended };
618
+
619
+ /// Returns a slice with a range of values for sleep given the `action` state and observed
620
+ /// `durations`.
621
+ pub fn sleep(action: Action, duration: Duration) []const Optional(Observation) {
622
+ if (action.sleep == .None) return &OBS_NONE;
623
+ return if (Sleeps.get(duration.sleeps, 0) == 0 and action.speed_tie != .None)
624
+ &OBS_ALL
625
+ else switch (action.sleep) {
626
+ .started => &OBS_STARTED,
627
+ else => if (Sleeps.get(duration.sleeps, 0) >= 7) &OBS_ENDED else &OBS,
628
+ };
629
+ }
630
+
631
+ /// Returns a slice with a range of values for disable given the `action`, observed `durations`,
632
+ /// and the state of the `parent` (observation of the player's Pokémon sleep status).
633
+ pub fn disable(
634
+ action: Action,
635
+ duration: Duration,
636
+ parent: Optional(Observation),
637
+ ) []const Optional(Observation) {
638
+ if (action.disable == .None) return &OBS_NONE;
639
+ return if (duration.disable == 0 and action.speed_tie != .None)
640
+ &OBS_ALL
641
+ else switch (action.disable) {
642
+ .started => &OBS_STARTED,
643
+ else => if (duration.disable >= 8)
644
+ &OBS_ENDED
645
+ else if (parent != .None and parent != .started)
646
+ &OBS_CONTINUING
647
+ else
648
+ &OBS,
649
+ };
650
+ }
651
+
652
+ /// FIXME
653
+ pub fn attacking(
654
+ action: Action,
655
+ duration: Duration,
656
+ parent: Optional(Observation),
657
+ ) []const Optional(Observation) {
658
+ return switch (action.attacking) {
659
+ .None => &OBS_NONE,
660
+ .started => &OBS_STARTED,
661
+ else => if (duration.attacking >= 3)
662
+ &OBS_ENDED
663
+ else if ((parent == .continuing and parent == .ended) or duration.attacking < 2)
664
+ &OBS_CONTINUING
665
+ else
666
+ &OBS,
667
+ };
668
+ }
669
+
670
+ const CFZ_NONE = [_]Optional(Confusion){.None};
671
+ const CFZ_STARTED = [_]Optional(Confusion){.started};
672
+ const CFZ_OVERWRITTEN = [_]Optional(Confusion){ .started, .overwritten };
673
+ const CFZ_CONTINUING = [_]Optional(Confusion){.continuing};
674
+ const CFZ_ENDED = [_]Optional(Confusion){.ended};
675
+ const CFZ = [_]Optional(Confusion){ .continuing, .ended };
676
+ const CFZ_TIE = [_]Optional(Confusion){ .started, .continuing, .ended };
677
+
678
+ /// FIXME
679
+ pub fn confusion(
680
+ action: Action,
681
+ duration: Duration,
682
+ tie: Optional(Player),
683
+ sibling: Optional(Observation),
684
+ parent: Optional(Observation),
685
+ ) []const Optional(Confusion) {
686
+ return switch (action.confusion) {
687
+ .None => &CFZ_NONE,
688
+ .started => if (pkmn.options.overwrite and sibling == .ended and duration.confusion < 5)
689
+ &CFZ_OVERWRITTEN
690
+ else
691
+ &CFZ_STARTED,
692
+ else => if (duration.confusion >= 5)
693
+ &CFZ_ENDED
694
+ else if ((parent == .continuing and parent == .ended) or duration.confusion < 2)
695
+ &CFZ_CONTINUING
696
+ else if (tie != .None)
697
+ &CFZ_TIE
698
+ else
699
+ &CFZ,
700
+ };
701
+ }
702
+
703
+ /// Returns a slice with a range of values for binding given the `action`, observed `durations`,
704
+ /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
705
+ pub fn binding(
706
+ action: Action,
707
+ duration: Duration,
708
+ parent: Optional(bool),
709
+ ) []const Optional(Observation) {
710
+ if (parent == .true) return &OBS_NONE;
711
+ return switch (action.binding) {
712
+ .None => &OBS_NONE,
713
+ .started => &OBS_STARTED,
714
+ else => if (duration.binding >= 4) &OBS_ENDED else &OBS,
715
+ };
716
+ }
717
+
581
718
  const BOOL_NONE = [_]Optional(bool){.None};
582
719
  const BOOLS = [_]Optional(bool){ .false, .true };
583
720
 
@@ -591,7 +728,7 @@ pub const Rolls = struct {
591
728
  /// Returns a slice with the correct range of values for critical hits given the `action` state
592
729
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
593
730
  pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
594
- if (parent == .false) return &BOOL_NONE;
731
+ if (pkmn.options.showdown and parent == .false) return &BOOL_NONE;
595
732
  return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
596
733
  }
597
734
 
@@ -616,7 +753,7 @@ pub const Rolls = struct {
616
753
 
617
754
  /// Returns the max damage roll which will produce the same damage as `roll`
618
755
  /// given the base damage in `summaries`.
619
- pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) !u8 {
756
+ pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) u8 {
620
757
  if (roll == 0) return roll;
621
758
 
622
759
  const dmg = summaries.get(player.foe()).damage;
@@ -626,9 +763,11 @@ pub const Rolls = struct {
626
763
  return @min(255, roll + ((254 - ((@as(u32, dmg.base) * roll) % 255)) / dmg.base));
627
764
  }
628
765
 
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;
766
+ /// Returns a slice with the correct range of values for confused given the `action` state and
767
+ /// the state of the `parent` (observation of the player's Pokémon confusion status).
768
+ pub fn confused(action: Action, parent: Optional(Confusion)) []const Optional(bool) {
769
+ const done = parent == .None or parent == .ended;
770
+ return if (done or action.confused == .None) &BOOL_NONE else &BOOLS;
632
771
  }
633
772
 
634
773
  /// Returns a slice with the correct range of values for paralysis given the `action` state
@@ -638,17 +777,6 @@ pub const Rolls = struct {
638
777
  return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
639
778
  }
640
779
 
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
780
  const SLOT_NONE = [_]u4{0};
653
781
  const SLOT = [_]u4{ 1, 2, 3, 4 };
654
782
 
@@ -706,6 +834,26 @@ test "Rolls.speedTie" {
706
834
  try expectEqualSlices(Optional(Player), &.{.None}, Rolls.speedTie(actions.p2));
707
835
  }
708
836
 
837
+ test "Rolls.sleep" {
838
+ return error.SkipZigTest; // TODO
839
+ }
840
+
841
+ test "Rolls.disable" {
842
+ return error.SkipZigTest; // TODO
843
+ }
844
+
845
+ test "Rolls.confusion" {
846
+ return error.SkipZigTest; // TODO
847
+ }
848
+
849
+ test "Rolls.atacking" {
850
+ return error.SkipZigTest; // TODO
851
+ }
852
+
853
+ test "Rolls.binding" {
854
+ return error.SkipZigTest; // TODO
855
+ }
856
+
709
857
  test "Rolls.damage" {
710
858
  const actions: Actions = .{ .p2 = .{ .damage = 221 } };
711
859
  try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p1, .None));
@@ -715,12 +863,12 @@ test "Rolls.damage" {
715
863
 
716
864
  test "Rolls.coalesce" {
717
865
  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));
866
+ try expectEqual(@as(u8, 0), Rolls.coalesce(.P2, 0, &summaries, false));
867
+ try expectEqual(@as(u8, 241), Rolls.coalesce(.P2, 238, &summaries, false));
868
+ try expectEqual(@as(u8, 255), Rolls.coalesce(.P2, 238, &summaries, true));
721
869
  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));
870
+ try expectEqual(@as(u8, 217), Rolls.coalesce(.P2, 217, &summaries, false));
871
+ try expectEqual(@as(u8, 255), Rolls.coalesce(.P2, 217, &summaries, true));
724
872
  }
725
873
 
726
874
  test "Rolls.hit" {
@@ -749,14 +897,23 @@ test "Rolls.criticalHit" {
749
897
  &.{ .false, .true },
750
898
  Rolls.criticalHit(actions.p1, .None),
751
899
  );
752
- try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p1, .false));
900
+ try expectEqualSlices(
901
+ Optional(bool),
902
+ if (pkmn.options.showdown) &.{.None} else &.{ .false, .true },
903
+ Rolls.criticalHit(actions.p1, .false),
904
+ );
753
905
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
754
906
  }
755
907
 
756
908
  test "Rolls.confused" {
757
909
  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));
910
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1, .None));
911
+ try expectEqualSlices(
912
+ Optional(bool),
913
+ &.{ .false, .true },
914
+ Rolls.confused(actions.p2, .continuing),
915
+ );
916
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p2, .ended));
760
917
  }
761
918
 
762
919
  test "Rolls.paralyzed" {
@@ -767,13 +924,6 @@ test "Rolls.paralyzed" {
767
924
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p2, .true));
768
925
  }
769
926
 
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
927
  test "Rolls.moveSlot" {
778
928
  const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
779
929
  try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
@@ -804,9 +954,9 @@ test "Rolls.psywave" {
804
954
 
805
955
  try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p1, &side, .None));
806
956
  var rolls = Rolls.psywave(actions.p2, &side, .None);
807
- try expectEqual(150, rolls[rolls.len - 1]);
957
+ try expectEqual(@as(u8, 150), rolls[rolls.len - 1]);
808
958
  side.stored().level = 81;
809
959
  rolls = Rolls.psywave(actions.p2, &side, .None);
810
- try expectEqual(121, rolls[rolls.len - 1]);
960
+ try expectEqual(@as(u8, 121), rolls[rolls.len - 1]);
811
961
  try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p2, &side, .false));
812
962
  }