@pkmn/engine 0.1.0-dev.62399ed5 → 0.1.0-dev.683fba11

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 -68
  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 +52 -31
  39. package/src/bin/install-pkmn-engine +8 -6
  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 +509 -152
  52. package/src/lib/gen1/chance.zig +366 -240
  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 -554
  58. package/src/lib/gen1/mechanics.zig +280 -185
  59. package/src/lib/gen2/calc.zig +37 -27
  60. package/src/lib/gen2/chance.zig +90 -503
  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 +69 -107
  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,46 +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
-
21
- const enabled = pkmn.options.calc;
22
-
23
- const Player = common.Player;
24
- const Result = common.Result;
25
- const Choice = common.Choice;
26
-
25
+ const expectEqualSlices = std.testing.expectEqualSlices;
26
+ const isPointerTo = util.isPointerTo;
27
+ const Observation = chance.Observation;
27
28
  const Optional = optional.Optional;
28
-
29
- const Rational = rational.Rational;
30
-
29
+ const Player = common.Player;
31
30
  const PointerType = util.PointerType;
32
-
33
- const Actions = chance.Actions;
34
- const Action = chance.Action;
35
- const Chance = chance.Chance;
36
- const Durations = chance.Durations;
37
- const Duration = chance.Duration;
38
-
39
- const Rolls = helpers.Rolls;
31
+ const Rational = rational.Rational;
32
+ const Result = common.Result;
33
+ const Sleeps = chance.Sleeps;
40
34
 
41
35
  const tty = true; // DEBUG
42
36
  const summary = false; // DEBUG
43
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
+
44
41
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
45
42
  pub const Summaries = extern struct {
46
43
  /// Relevant information for Player 1.
@@ -54,7 +51,7 @@ pub const Summaries = extern struct {
54
51
 
55
52
  /// Returns the `Summary` for the given `player`.
56
53
  pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
57
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Summaries);
54
+ assert(isPointerTo(self, Summaries));
58
55
  return if (player == .P1) &self.p1 else &self.p2;
59
56
  }
60
57
  };
@@ -90,25 +87,11 @@ pub const Summary = extern struct {
90
87
  }
91
88
  };
92
89
 
93
- /// TODO
94
- pub const Overrides = extern struct {
95
- /// TODO
96
- actions: Actions = .{},
97
- /// TODO
98
- durations: Durations = .{},
99
-
100
- _: u32 = 0,
101
-
102
- comptime {
103
- assert(@sizeOf(Overrides) == 24);
104
- }
105
- };
106
-
107
90
  /// Allows for forcing the value of specific RNG events during a Generation I battle `update` via
108
91
  /// `overrides` and tracks `summaries` of information relevant to damage calculation.
109
92
  pub const Calc = struct {
110
93
  /// Overrides the normal behavior of the RNG during an `update` to force specific outcomes.
111
- overrides: Overrides = .{},
94
+ overrides: Actions = .{},
112
95
  /// Information relevant to damage calculation.
113
96
  summaries: Summaries = .{},
114
97
 
@@ -119,26 +102,20 @@ pub const Calc = struct {
119
102
  ) ?std.meta.FieldType(Action, field) {
120
103
  if (!enabled) return null;
121
104
 
122
- const overrides =
123
- if (player == .P1) self.overrides.actions.p1 else self.overrides.actions.p2;
124
- const val = @field(overrides, @tagName(field));
105
+ const val = @field(self.overrides.get(player), @tagName(field));
125
106
  return if (switch (@typeInfo(@TypeOf(val))) {
126
- .Enum => val != .None,
127
- .Int => val != 0,
107
+ Enum => val != .None,
108
+ Int => val != 0,
128
109
  else => unreachable,
129
110
  }) val else null;
130
111
  }
131
112
 
132
- pub fn modify(self: Calc, player: Player, comptime field: Duration.Field) ?bool {
133
- if (!enabled) return null;
134
-
135
- const durations = self.overrides.durations;
136
- const set = @field(if (player == .P1) durations.p1 else durations.p2, @tagName(field));
137
- if (set == 0) return null;
113
+ pub fn confusion(self: *Calc, player: Player) void {
114
+ if (!enabled) return;
138
115
 
139
- const actions = self.overrides.actions;
140
- const overrides = if (player == .P1) actions.p1.durations else actions.p2.durations;
141
- return @field(overrides, @tagName(field)) != 0;
116
+ if (self.overrides.get(player).confusion == .started) {
117
+ self.overrides.get(player).confusion = .None;
118
+ }
142
119
  }
143
120
 
144
121
  pub fn base(self: *Calc, player: Player, val: u16) void {
@@ -174,9 +151,8 @@ const Null = struct {
174
151
  return null;
175
152
  }
176
153
 
177
- pub fn modify(self: Null, player: Player, comptime field: Duration.Field) ?bool {
178
- _ = .{ self, player, field };
179
- return null;
154
+ pub fn confusion(self: Null, player: Player) void {
155
+ _ = .{ self, player };
180
156
  }
181
157
 
182
158
  pub fn base(self: Null, player: Player, val: u16) void {
@@ -200,13 +176,13 @@ pub const Stats = struct {
200
176
  };
201
177
 
202
178
  pub const Options = struct {
203
- actions: Actions = .{},
179
+ durations: Durations,
204
180
  seed: ?u64 = null,
205
181
  cap: bool = false,
206
182
  metronome: bool = false,
207
183
  };
208
184
 
209
- pub const MAX_FRONTIER = 19;
185
+ pub const MAX_FRONTIER = 81;
210
186
 
211
187
  pub fn transitions(
212
188
  battle: anytype,
@@ -218,7 +194,6 @@ pub fn transitions(
218
194
  ) !?Stats {
219
195
  var stats: Stats = .{};
220
196
 
221
- const actions = options.actions;
222
197
  const cap = options.cap;
223
198
 
224
199
  var seen = std.AutoHashMap(Actions, void).init(allocator);
@@ -226,13 +201,12 @@ pub fn transitions(
226
201
  var frontier = std.ArrayList(Actions).init(allocator);
227
202
  defer frontier.deinit();
228
203
 
204
+ const d = options.durations;
205
+
229
206
  var opts = pkmn.battle.options(
230
207
  protocol.NULL,
231
- Chance(Rational(u128)){ .probability = .{}, .actions = actions },
232
- Calc{ .overrides = .{ .actions = actions, .durations = .{
233
- .p1 = actions.p1.durations,
234
- .p2 = actions.p2.durations,
235
- } } },
208
+ Chance(Rational(u128)){ .probability = .{}, .durations = d },
209
+ Calc{},
236
210
  );
237
211
 
238
212
  var b = battle;
@@ -254,13 +228,10 @@ pub fn transitions(
254
228
  var i: usize = 0;
255
229
  assert(frontier.items.len == 1);
256
230
  while (i < frontier.items.len) : (i += 1) {
257
- const template = frontier.items[i];
258
- opts.calc.overrides.durations = .{
259
- .p1 = template.p1.durations,
260
- .p2 = template.p2.durations,
261
- };
231
+ const f = frontier.items[i];
232
+ const saved = stats.saved;
262
233
 
263
- try debug(writer, template, .{
234
+ try debug(writer, f, .{
264
235
  .shape = true,
265
236
  .color = i,
266
237
  .bold = true,
@@ -268,43 +239,44 @@ pub fn transitions(
268
239
  .indent = false,
269
240
  });
270
241
 
271
- var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
272
-
273
- for (Rolls.speedTie(template.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
274
- for (Rolls.sleep(template.p1, actions.p1)) |p1_slp| { a.p1.durations.sleep = p1_slp;
275
- for (Rolls.sleep(template.p2, actions.p2)) |p2_slp| { a.p2.durations.sleep = p2_slp;
276
- for (Rolls.disable(template.p1, actions.p1, p1_slp)) |p1_disable| { a.p1.durations.disable = p1_disable;
277
- for (Rolls.disable(template.p2, actions.p2, p2_slp)) |p2_disable| { a.p2.durations.disable = p2_disable;
278
- for (Rolls.confusion(template.p1, actions.p1, p1_slp)) |p1_cfz| { a.p1.durations.confusion = p1_cfz;
279
- for (Rolls.confusion(template.p2, actions.p2, p1_slp)) |p2_cfz| { a.p2.durations.confusion = p2_cfz;
280
- for (Rolls.confused(template.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
281
- for (Rolls.confused(template.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
282
- for (Rolls.paralyzed(template.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
283
- for (Rolls.paralyzed(template.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
284
- for (Rolls.attacking(template.p1, actions.p1, p1_par)) |p1_atk| { a.p1.durations.attacking = p1_atk;
285
- for (Rolls.attacking(template.p2, actions.p2, p2_par)) |p2_atk| { a.p2.durations.attacking = p2_atk;
286
- for (Rolls.binding(template.p1, actions.p1, p1_par)) |p1_bind| { a.p1.durations.binding = p1_bind;
287
- for (Rolls.binding(template.p2, actions.p2, p2_par)) |p2_bind| { a.p2.durations.binding = p2_bind;
288
- for (Rolls.duration(template.p1, p1_par)) |p1_dur| { a.p1.duration = p1_dur;
289
- for (Rolls.duration(template.p2, p2_par)) |p2_dur| { a.p2.duration = p2_dur;
290
- for (Rolls.hit(template.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
291
- for (Rolls.hit(template.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
292
- for (Rolls.psywave(template.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
293
- for (Rolls.psywave(template.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
294
- for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
295
- for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
296
- for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
297
- for (Rolls.multiHit(template.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
298
- for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
299
- for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
300
- for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
301
- for (Rolls.criticalHit(template.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
302
-
303
- 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);
304
276
  while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
305
277
  a.p1.damage = @intCast(p1_dmg.min);
306
278
 
307
- var p2_dmg = Rolls.damage(template.p2, p2_hit);
279
+ var p2_dmg = Rolls.damage(f.p2, p2_hit);
308
280
 
309
281
  const p1_min: u9 = p1_dmg.min;
310
282
  const p2_min: u9 = p2_dmg.min;
@@ -312,13 +284,14 @@ pub fn transitions(
312
284
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
313
285
  a.p2.damage = @intCast(p2_dmg.min);
314
286
 
315
- opts.calc.overrides.actions = a;
287
+ opts.calc.overrides = a;
316
288
  opts.calc.summaries = .{};
317
- opts.chance = .{ .probability = .{}, .actions = actions };
289
+ opts.chance = .{ .probability = .{}, .durations = d };
318
290
  const q = &opts.chance.probability;
319
291
 
320
292
  b = battle;
321
293
  _ = try b.update(c1, c2, &opts);
294
+
322
295
  stats.updates += 1;
323
296
 
324
297
  const summaries = &opts.calc.summaries;
@@ -329,8 +302,8 @@ pub fn transitions(
329
302
  const p2_max: u9 =
330
303
  try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
331
304
 
332
- if (opts.chance.actions.matches(template)) {
333
- if (!opts.chance.actions.eql(a)) {
305
+ if (opts.chance.actions.matches(f)) {
306
+ if (!opts.chance.actions.relax().eql(a)) {
334
307
  if (!summary) {
335
308
  try debug(writer, opts.chance.actions, .{
336
309
  .p1_max = p1_max,
@@ -364,10 +337,13 @@ pub fn transitions(
364
337
  }
365
338
  }
366
339
  }
367
-
368
340
  if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
369
341
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
342
+
343
+ q.reduce();
370
344
  try p.add(q);
345
+ p.reduce();
346
+
371
347
  stats.saved += 1;
372
348
 
373
349
  if (p.q < p.p) {
@@ -375,7 +351,7 @@ pub fn transitions(
375
351
  return error.TestUnexpectedResult;
376
352
  }
377
353
  } else {
378
- if (!matches(opts.chance.actions, i, frontier.items)) {
354
+ if (!opts.chance.actions.matchesAny(frontier.items, i)) {
379
355
  try frontier.append(opts.chance.actions);
380
356
 
381
357
  try debug(writer, opts.chance.actions, .{
@@ -405,9 +381,11 @@ pub fn transitions(
405
381
  p2_dmg.min = p2_max;
406
382
  }
407
383
 
408
- }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
384
+ }}}}}}}}}}}}}}}}}}}}}}}}}}}}
409
385
 
410
- 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)) {
411
389
  p.reduce();
412
390
  try writer.print(
413
391
  " = {s} ({d:.2}%)\n\n",
@@ -444,61 +422,90 @@ pub fn update(
444
422
  writer: anytype,
445
423
  transition: bool,
446
424
  ) !Result {
425
+ const durations = options.chance.durations;
447
426
  options.chance.reset();
448
427
 
449
- if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
450
- return battle.update(c1, c2, options);
451
- }
428
+ if (!pkmn.options.chance or !pkmn.options.calc) return battle.update(c1, c2, options);
452
429
 
453
- var copy = battle.*;
454
- const actions = options.chance.actions;
430
+ const original = battle.*;
455
431
 
456
432
  // Perfom the actual update
457
433
  const result = battle.update(c1, c2, options);
458
-
459
- // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
460
- var buf: [helpers.MAX_DIFFS]u8 = undefined;
461
- var stream: protocol.ByteStream = .{ .buffer = &buf };
462
- const n = try helpers.diff(&copy, battle, stream.writer());
463
-
464
- // Applying the diff to the battle should take us back to the original copy
465
- // of the battle (ignoring the RNG).
466
- var patched = battle.*;
467
- patched.rng = copy.rng;
468
- helpers.patch(&patched, buf[0..n]);
469
- try expectEqual(copy, patched);
470
-
471
- if (!pkmn.options.chance or !pkmn.options.calc) return result;
434
+ var overrides = options.chance.actions;
435
+ const updated = options.chance.durations;
436
+
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());
442
+
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
+ }
472
450
 
473
451
  if (transition) {
474
452
  // Ensure we can generate all transitions from the same original state
475
453
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
476
454
  // the transitions function relies on RNG for discovery of states)
477
- if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
478
- .actions = actions,
455
+ if (try transitions(unfix(original), c1, c2, allocator, writer, .{
456
+ .durations = durations,
479
457
  .cap = true,
480
458
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
481
459
  }
482
460
 
483
461
  // Demonstrate that we can produce the same state by forcing the RNG to behave the
484
- // same as we observed - note that because we do not pass in a durations override
485
- // mask none of the durations will be extended.
462
+ // same as we observed.
486
463
  var override = pkmn.battle.options(
487
464
  protocol.NULL,
488
- chance.NULL,
489
- Calc{ .overrides = .{ .actions = actions } },
465
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
466
+ Calc{ .overrides = overrides },
490
467
  );
491
- const overridden = copy.update(c1, c2, &override);
468
+ var copy = original;
469
+ var overridden = copy.update(c1, c2, &override);
492
470
  try expectEqual(result, overridden);
471
+ try expectEqualActions(overrides, override.chance.actions);
472
+ try expectEqual(updated, override.chance.durations);
493
473
 
494
474
  // The actual battle excluding its RNG field should match a copy updated with
495
475
  // overridden RNG (the copy RNG may have advanced because of no-ops)
496
476
  copy.rng = battle.rng;
497
- 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);
498
495
 
499
496
  return result;
500
497
  }
501
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
+
502
509
  fn unfix(actual: anytype) data.Battle(data.PRNG) {
503
510
  return .{
504
511
  .sides = actual.sides,
@@ -514,15 +521,6 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
514
521
  };
515
522
  }
516
523
 
517
- fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
518
- for (frontier, 0..) |f, j| {
519
- // TODO: is skipping this redundant check worth it?
520
- if (i == j) continue;
521
- if (f.matches(actions)) return true;
522
- }
523
- return false;
524
- }
525
-
526
524
  fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
527
525
  const w = std.io.getStdErr().writer();
528
526
  w.print(fmt, v) catch return;
@@ -594,3 +592,362 @@ fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
594
592
 
595
593
  try writer.writeAll(output[0..len]);
596
594
  }
595
+
596
+ /// Helper functions that efficiently return valid ranges for various RNG events based on the
597
+ /// state of an `Action` and other events to be used to construct a "transitions" function.
598
+ pub const Rolls = struct {
599
+ const PLAYER_NONE = [_]Optional(Player){.None};
600
+ const PLAYERS = [_]Optional(Player){ .P1, .P2 };
601
+
602
+ /// Returns a slice with the correct range of values for speed ties given the `action` state.
603
+ pub fn speedTie(action: Action) []const Optional(Player) {
604
+ return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
605
+ }
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
+
709
+ const BOOL_NONE = [_]Optional(bool){.None};
710
+ const BOOLS = [_]Optional(bool){ .false, .true };
711
+
712
+ /// Returns a slice with the correct range of values for hits given the `action` state
713
+ /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
714
+ pub fn hit(action: Action, parent: Optional(bool)) []const Optional(bool) {
715
+ if (parent == .true) return &BOOL_NONE;
716
+ return if (action.hit == .None) &BOOL_NONE else &BOOLS;
717
+ }
718
+
719
+ /// Returns a slice with the correct range of values for critical hits given the `action` state
720
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
721
+ pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
722
+ if (pkmn.options.showdown and parent == .false) return &BOOL_NONE;
723
+ return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
724
+ }
725
+
726
+ /// Returns a slice with the correct range of values for secondary chances hits given the
727
+ /// `action` state and the state of the `parent` (whether the player's Pokémon's move hit).
728
+ pub fn secondaryChance(action: Action, parent: Optional(bool)) []const Optional(bool) {
729
+ if (parent == .false) return &BOOL_NONE;
730
+ return if (@field(action, "secondary_chance") == .None) &BOOL_NONE else &BOOLS;
731
+ }
732
+
733
+ /// The min and max bounds on iteration over damage rolls.
734
+ pub const Range = struct { min: u9, max: u9 };
735
+
736
+ /// Returns the range bounding damage rolls given the `action` state and the state of
737
+ /// the `parent` (whether the player's Pokémon's move hit).
738
+ pub fn damage(action: Action, parent: Optional(bool)) Range {
739
+ return if (parent == .false or action.damage == 0)
740
+ .{ .min = 0, .max = 1 }
741
+ else
742
+ .{ .min = 217, .max = 256 };
743
+ }
744
+
745
+ /// Returns the max damage roll which will produce the same damage as `roll`
746
+ /// given the base damage in `summaries`.
747
+ pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) !u8 {
748
+ if (roll == 0) return roll;
749
+
750
+ const dmg = summaries.get(player.foe()).damage;
751
+ if (dmg.base == 0 or (cap and dmg.capped)) return 255;
752
+
753
+ // Closed form solution for max damage roll provided by Orion Taylor (taylorott)
754
+ return @min(255, roll + ((254 - ((@as(u32, dmg.base) * roll) % 255)) / dmg.base));
755
+ }
756
+
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;
762
+ }
763
+
764
+ /// Returns a slice with the correct range of values for paralysis given the `action` state
765
+ /// and the state of the `parent` (whether the player's Pokémon was confused).
766
+ pub fn paralyzed(action: Action, parent: Optional(bool)) []const Optional(bool) {
767
+ if (parent == .true) return &BOOL_NONE;
768
+ return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
769
+ }
770
+
771
+ const SLOT_NONE = [_]u4{0};
772
+ const SLOT = [_]u4{ 1, 2, 3, 4 };
773
+
774
+ /// Returns a slice with a range of values for move slots given the `action` state
775
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
776
+ ///
777
+ /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
778
+ pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
779
+ if (parent == .false) return &SLOT_NONE;
780
+ return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
781
+ }
782
+
783
+ const MULTI_NONE = [_]u4{0};
784
+ const MULTI = [_]u4{ 2, 3, 4, 5 };
785
+
786
+ /// Returns a slice with the correct range of values for multi hit given the `action` state
787
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
788
+ pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
789
+ if (parent == .false) return &MULTI_NONE;
790
+ return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
791
+ }
792
+
793
+ const PSYWAVE_NONE = [_]u8{0};
794
+ const PSYWAVE = init: {
795
+ var rolls: [150]u8 = undefined;
796
+ for (0..150) |i| rolls[i] = i + 1;
797
+ break :init rolls;
798
+ };
799
+
800
+ /// Returns a slice with the correct range of values for psywave given the `action` state,
801
+ /// the `side`, and the state of the `parent` (whether the player's Pokémon's move hit).
802
+ pub fn psywave(
803
+ action: Action,
804
+ side: *data.Side,
805
+ parent: Optional(bool),
806
+ ) []const u8 {
807
+ if (parent == .false) return &PSYWAVE_NONE;
808
+ return if (action.psywave == 0)
809
+ &PSYWAVE_NONE
810
+ else
811
+ PSYWAVE[0 .. @as(u16, side.stored().level) * 3 / 2];
812
+ }
813
+
814
+ const MOVE_NONE = [_]data.Move{.None};
815
+
816
+ /// Returns a slice with the correct range of values for metronome given the `action` state.
817
+ pub fn metronome(action: Action) []const data.Move {
818
+ return if (action.metronome == .None) &MOVE_NONE else &data.Move.METRONOME;
819
+ }
820
+ };
821
+
822
+ test "Rolls.speedTie" {
823
+ const actions: Actions = .{ .p1 = .{ .speed_tie = .P2 } };
824
+ try expectEqualSlices(Optional(Player), &.{ .P1, .P2 }, Rolls.speedTie(actions.p1));
825
+ try expectEqualSlices(Optional(Player), &.{.None}, Rolls.speedTie(actions.p2));
826
+ }
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
+
848
+ test "Rolls.damage" {
849
+ const actions: Actions = .{ .p2 = .{ .damage = 221 } };
850
+ try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p1, .None));
851
+ try expectEqual(Rolls.Range{ .min = 217, .max = 256 }, Rolls.damage(actions.p2, .None));
852
+ try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p2, .false));
853
+ }
854
+
855
+ test "Rolls.coalesce" {
856
+ var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
857
+ try expectEqual(@as(u8, 0), try Rolls.coalesce(.P2, 0, &summaries, false));
858
+ try expectEqual(@as(u8, 241), try Rolls.coalesce(.P2, 238, &summaries, false));
859
+ try expectEqual(@as(u8, 255), try Rolls.coalesce(.P2, 238, &summaries, true));
860
+ summaries.p1.damage.final = 74;
861
+ try expectEqual(@as(u8, 217), try Rolls.coalesce(.P2, 217, &summaries, false));
862
+ try expectEqual(@as(u8, 255), try Rolls.coalesce(.P2, 217, &summaries, true));
863
+ }
864
+
865
+ test "Rolls.hit" {
866
+ const actions: Actions = .{ .p2 = .{ .hit = .true } };
867
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.hit(actions.p1, .None));
868
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.hit(actions.p2, .None));
869
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.hit(actions.p2, .true));
870
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.hit(actions.p2, .false));
871
+ }
872
+
873
+ test "Rolls.secondaryChance" {
874
+ const actions: Actions = .{ .p1 = .{ .secondary_chance = .true } };
875
+ try expectEqualSlices(
876
+ Optional(bool),
877
+ &.{ .false, .true },
878
+ Rolls.secondaryChance(actions.p1, .None),
879
+ );
880
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.secondaryChance(actions.p1, .false));
881
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.secondaryChance(actions.p2, .None));
882
+ }
883
+
884
+ test "Rolls.criticalHit" {
885
+ const actions: Actions = .{ .p1 = .{ .critical_hit = .true } };
886
+ try expectEqualSlices(
887
+ Optional(bool),
888
+ &.{ .false, .true },
889
+ Rolls.criticalHit(actions.p1, .None),
890
+ );
891
+ try expectEqualSlices(
892
+ Optional(bool),
893
+ if (pkmn.options.showdown) &.{.None} else &.{ .false, .true },
894
+ Rolls.criticalHit(actions.p1, .false),
895
+ );
896
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
897
+ }
898
+
899
+ test "Rolls.confused" {
900
+ const actions: Actions = .{ .p2 = .{ .confused = .true } };
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));
908
+ }
909
+
910
+ test "Rolls.paralyzed" {
911
+ const actions: Actions = .{ .p2 = .{ .paralyzed = .true } };
912
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p1, .None));
913
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.paralyzed(actions.p2, .None));
914
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.paralyzed(actions.p2, .false));
915
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p2, .true));
916
+ }
917
+
918
+ test "Rolls.moveSlot" {
919
+ const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
920
+ try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
921
+ try expectEqualSlices(u4, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
922
+ try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p2, .false));
923
+ }
924
+
925
+ test "Rolls.multiHit" {
926
+ const actions: Actions = .{ .p2 = .{ .multi_hit = 3 } };
927
+ try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p1, .None));
928
+ try expectEqualSlices(u4, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
929
+ try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p2, .false));
930
+ }
931
+
932
+ test "Rolls.metronome" {
933
+ const actions: Actions = .{ .p2 = .{ .metronome = .Surf } };
934
+ try expectEqualSlices(data.Move, &.{.None}, Rolls.metronome(actions.p1));
935
+ try expectEqual(@as(data.Move, @enumFromInt(24)), Rolls.metronome(actions.p2)[23]);
936
+ }
937
+
938
+ test "Rolls.psywave" {
939
+ const actions: Actions = .{ .p2 = .{ .psywave = 79 } };
940
+ var side = helpers.Side.init(&[_]helpers.Pokemon{.{
941
+ .species = .Bulbasaur,
942
+ .level = 100,
943
+ .moves = &[_]data.Move{.Tackle},
944
+ }});
945
+
946
+ try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p1, &side, .None));
947
+ var rolls = Rolls.psywave(actions.p2, &side, .None);
948
+ try expectEqual(@as(u8, 150), rolls[rolls.len - 1]);
949
+ side.stored().level = 81;
950
+ rolls = Rolls.psywave(actions.p2, &side, .None);
951
+ try expectEqual(@as(u8, 121), rolls[rolls.len - 1]);
952
+ try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p2, &side, .false));
953
+ }