@pkmn/engine 0.1.0-dev.15f64810 → 0.1.0-dev.1cea12bf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +4 -2
  2. package/build/pkg/addon/browser.d.ts +3 -0
  3. package/build/pkg/addon/browser.js +43 -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 +96 -0
  7. package/build/pkg/addon/node.js.map +1 -0
  8. package/build/pkg/addon.d.ts +2 -0
  9. package/build/pkg/addon.js +29 -51
  10. package/build/pkg/addon.js.map +1 -1
  11. package/build/pkg/data.d.ts +23 -1
  12. package/build/pkg/data.js.map +1 -1
  13. package/build/pkg/gen1.js +1 -1
  14. package/build/pkg/gen1.js.map +1 -1
  15. package/build/pkg/index.d.ts +22 -9
  16. package/build/pkg/index.js +15 -10
  17. package/build/pkg/index.js.map +1 -1
  18. package/build/pkg/protocol.d.ts +20 -1
  19. package/build/pkg/protocol.js +105 -103
  20. package/build/pkg/protocol.js.map +1 -1
  21. package/build/tools/debug.js +34 -67
  22. package/build/tools/debug.js.map +1 -1
  23. package/build/tools/display/debug.jsx +128 -0
  24. package/build/tools/display/debug.jsx.map +1 -0
  25. package/build/tools/display/index.js +132 -0
  26. package/build/tools/display/index.js.map +1 -0
  27. package/build/tools/display/ui.jsx +349 -0
  28. package/build/tools/display/ui.jsx.map +1 -0
  29. package/build/tools/display/util.js +18 -0
  30. package/build/tools/display/util.js.map +1 -0
  31. package/build.zig +199 -66
  32. package/package.json +35 -22
  33. package/src/bin/install-pkmn-engine +7 -5
  34. package/src/bin/pkmn-debug +1 -1
  35. package/src/lib/common/array.zig +77 -0
  36. package/src/lib/common/data.zig +8 -8
  37. package/src/lib/common/js.zig +4 -2
  38. package/src/lib/common/optional.zig +13 -9
  39. package/src/lib/common/protocol.zig +41 -36
  40. package/src/lib/common/rational.zig +113 -22
  41. package/src/lib/common/rng.zig +4 -4
  42. package/src/lib/common/util.zig +50 -1
  43. package/src/lib/gen1/calc.zig +428 -153
  44. package/src/lib/gen1/chance.zig +332 -224
  45. package/src/lib/gen1/data.zig +28 -27
  46. package/src/lib/gen1/helpers.zig +12 -542
  47. package/src/lib/gen1/mechanics.zig +189 -120
  48. package/src/lib/gen2/calc.zig +34 -16
  49. package/src/lib/gen2/chance.zig +90 -503
  50. package/src/lib/gen2/data.zig +15 -14
  51. package/src/lib/gen2/helpers.zig +4 -6
  52. package/src/lib/gen2/mechanics.zig +47 -77
  53. package/src/lib/pkmn.zig +7 -5
  54. package/src/tools/display/dom.ts +96 -0
  55. package/src/tools/display/ui.css +351 -0
  56. package/build/test/display.js +0 -400
  57. package/build/test/display.js.map +0 -1
  58. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -17,6 +17,7 @@ const assert = std.debug.assert;
17
17
 
18
18
  const expect = std.testing.expect;
19
19
  const expectEqual = std.testing.expectEqual;
20
+ const expectEqualSlices = std.testing.expectEqualSlices;
20
21
 
21
22
  const enabled = pkmn.options.calc;
22
23
 
@@ -29,18 +30,19 @@ const Optional = optional.Optional;
29
30
  const Rational = rational.Rational;
30
31
 
31
32
  const PointerType = util.PointerType;
33
+ const isPointerTo = util.isPointerTo;
32
34
 
33
35
  const Actions = chance.Actions;
34
36
  const Action = chance.Action;
35
37
  const Chance = chance.Chance;
36
38
  const Durations = chance.Durations;
37
- const Duration = chance.Duration;
38
-
39
- const Rolls = helpers.Rolls;
40
39
 
41
40
  const tty = true; // DEBUG
42
41
  const summary = false; // DEBUG
43
42
 
43
+ const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
44
+ const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
45
+
44
46
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
45
47
  pub const Summaries = extern struct {
46
48
  /// Relevant information for Player 1.
@@ -54,7 +56,7 @@ pub const Summaries = extern struct {
54
56
 
55
57
  /// Returns the `Summary` for the given `player`.
56
58
  pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Summary) {
57
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Summaries);
59
+ assert(isPointerTo(self, Summaries));
58
60
  return if (player == .P1) &self.p1 else &self.p2;
59
61
  }
60
62
  };
@@ -72,8 +74,10 @@ pub const Summary = extern struct {
72
74
  /// The final computed damage that gets applied to the Pokémon. May exceed the target's HP
73
75
  // (to determine the *actual* damage done compare the target's stored HP before and after).
74
76
  final: u16 = 0,
75
- /// Whether higher damage will saturate / result in the same outcome (e.g. additional damage
76
- /// gets ignored due to it already breaking a Substitute or causing the target to faint).
77
+ /// Whether higher direct damage will saturate / result in the same outcome (e.g. additional
78
+ /// direct damage gets ignored due to it already breaking a Substitute or causing the target
79
+ /// to faint). Note that this field does not get set in scenarios where the target would
80
+ /// only be guaranteed to faint due to some sort of subsequent recoil or residual damage.
77
81
  capped: bool = false,
78
82
 
79
83
  // NOTE: 15 bits padding
@@ -88,25 +92,11 @@ pub const Summary = extern struct {
88
92
  }
89
93
  };
90
94
 
91
- /// TODO
92
- pub const Overrides = extern struct {
93
- /// TODO
94
- actions: Actions = .{},
95
- /// TODO
96
- durations: Durations = .{},
97
-
98
- _: u32 = 0,
99
-
100
- comptime {
101
- assert(@sizeOf(Overrides) == 24);
102
- }
103
- };
104
-
105
95
  /// Allows for forcing the value of specific RNG events during a Generation I battle `update` via
106
96
  /// `overrides` and tracks `summaries` of information relevant to damage calculation.
107
97
  pub const Calc = struct {
108
98
  /// Overrides the normal behavior of the RNG during an `update` to force specific outcomes.
109
- overrides: Overrides = .{},
99
+ overrides: Actions = .{},
110
100
  /// Information relevant to damage calculation.
111
101
  summaries: Summaries = .{},
112
102
 
@@ -117,28 +107,14 @@ pub const Calc = struct {
117
107
  ) ?std.meta.FieldType(Action, field) {
118
108
  if (!enabled) return null;
119
109
 
120
- const overrides =
121
- if (player == .P1) self.overrides.actions.p1 else self.overrides.actions.p2;
122
- const val = @field(overrides, @tagName(field));
110
+ const val = @field(self.overrides.get(player), @tagName(field));
123
111
  return if (switch (@typeInfo(@TypeOf(val))) {
124
- .Enum => val != .None,
125
- .Int => val != 0,
112
+ Enum => val != .None,
113
+ Int => val != 0,
126
114
  else => unreachable,
127
115
  }) val else null;
128
116
  }
129
117
 
130
- pub fn modify(self: Calc, player: Player, comptime field: Duration.Field) ?bool {
131
- if (!enabled) return null;
132
-
133
- const durations = self.overrides.durations;
134
- const set = @field(if (player == .P1) durations.p1 else durations.p2, @tagName(field));
135
- if (set == 0) return null;
136
-
137
- const actions = self.overrides.actions;
138
- const overrides = if (player == .P1) actions.p1.durations else actions.p2.durations;
139
- return @field(overrides, @tagName(field)) != 0;
140
- }
141
-
142
118
  pub fn base(self: *Calc, player: Player, val: u16) void {
143
119
  if (!enabled) return;
144
120
 
@@ -172,11 +148,6 @@ const Null = struct {
172
148
  return null;
173
149
  }
174
150
 
175
- pub fn modify(self: Null, player: Player, comptime field: Duration.Field) ?bool {
176
- _ = .{ self, player, field };
177
- return null;
178
- }
179
-
180
151
  pub fn base(self: Null, player: Player, val: u16) void {
181
152
  _ = .{ self, player, val };
182
153
  }
@@ -198,13 +169,13 @@ pub const Stats = struct {
198
169
  };
199
170
 
200
171
  pub const Options = struct {
201
- actions: Actions = .{},
172
+ durations: Durations,
202
173
  seed: ?u64 = null,
203
174
  cap: bool = false,
204
175
  metronome: bool = false,
205
176
  };
206
177
 
207
- pub const MAX_FRONTIER = 14;
178
+ pub const MAX_FRONTIER = 19;
208
179
 
209
180
  pub fn transitions(
210
181
  battle: anytype,
@@ -216,7 +187,6 @@ pub fn transitions(
216
187
  ) !?Stats {
217
188
  var stats: Stats = .{};
218
189
 
219
- const actions = options.actions;
220
190
  const cap = options.cap;
221
191
 
222
192
  var seen = std.AutoHashMap(Actions, void).init(allocator);
@@ -226,11 +196,8 @@ pub fn transitions(
226
196
 
227
197
  var opts = pkmn.battle.options(
228
198
  protocol.NULL,
229
- Chance(Rational(u128)){ .probability = .{}, .actions = actions },
230
- Calc{ .overrides = .{ .actions = actions, .durations = .{
231
- .p1 = actions.p1.durations,
232
- .p2 = actions.p2.durations,
233
- } } },
199
+ Chance(Rational(u128)){ .probability = .{}, .durations = options.durations },
200
+ Calc{},
234
201
  );
235
202
 
236
203
  var b = battle;
@@ -252,85 +219,95 @@ pub fn transitions(
252
219
  var i: usize = 0;
253
220
  assert(frontier.items.len == 1);
254
221
  while (i < frontier.items.len) : (i += 1) {
255
- const template = frontier.items[i];
256
- opts.calc.overrides.durations = .{
257
- .p1 = template.p1.durations,
258
- .p2 = template.p2.durations,
222
+ const f = frontier.items[i];
223
+
224
+ try debug(writer, f, .{
225
+ .shape = true,
226
+ .color = i,
227
+ .bold = true,
228
+ .background = true,
229
+ .indent = false,
230
+ });
231
+
232
+ var a: Actions = .{
233
+ .p1 = .{ .metronome = p1_move, .pp = f.p1.pp, .duration = f.p1.duration },
234
+ .p2 = .{ .metronome = p2_move, .pp = f.p2.pp, .duration = f.p2.duration },
259
235
  };
260
236
 
261
- try debug(writer, template, true, .{ .color = i, .bold = true, .background = true, .indent = false });
262
-
263
- var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
264
-
265
- for (Rolls.speedTie(template.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
266
- for (Rolls.sleep(template.p1, actions.p1)) |p1_slp| { a.p1.durations.sleep = p1_slp;
267
- for (Rolls.sleep(template.p2, actions.p2)) |p2_slp| { a.p2.durations.sleep = p2_slp;
268
- for (Rolls.disable(template.p1, actions.p1, p1_slp)) |p1_disable| { a.p1.durations.disable = p1_disable;
269
- for (Rolls.disable(template.p2, actions.p2, p2_slp)) |p2_disable| { a.p2.durations.disable = p2_disable;
270
- for (Rolls.confusion(template.p1, actions.p1, p1_slp)) |p1_cfz| { a.p1.durations.confusion = p1_cfz;
271
- for (Rolls.confusion(template.p2, actions.p2, p1_slp)) |p2_cfz| { a.p2.durations.confusion = p2_cfz;
272
- for (Rolls.confused(template.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
273
- for (Rolls.confused(template.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
274
- for (Rolls.paralyzed(template.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
275
- for (Rolls.paralyzed(template.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
276
- for (Rolls.attacking(template.p1, actions.p1, p1_par)) |p1_atk| { a.p1.durations.attacking = p1_atk;
277
- for (Rolls.attacking(template.p2, actions.p2, p2_par)) |p2_atk| { a.p2.durations.attacking = p2_atk;
278
- for (Rolls.binding(template.p1, actions.p1, p1_par)) |p1_bind| { a.p1.durations.binding = p1_bind;
279
- for (Rolls.binding(template.p2, actions.p2, p2_par)) |p2_bind| { a.p2.durations.binding = p2_bind;
280
- for (Rolls.duration(template.p1, p1_par)) |p1_dur| { a.p1.duration = p1_dur;
281
- for (Rolls.duration(template.p2, p2_par)) |p2_dur| { a.p2.duration = p2_dur;
282
- for (Rolls.hit(template.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
283
- for (Rolls.hit(template.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
284
- for (Rolls.psywave(template.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
285
- for (Rolls.psywave(template.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
286
- for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
287
- for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
288
- for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
289
- for (Rolls.multiHit(template.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
290
- for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
291
- for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
292
- for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
293
- for (Rolls.criticalHit(template.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
294
-
295
- var p1_dmg = Rolls.damage(template.p1, p1_hit);
237
+ for (Rolls.speedTie(f.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
238
+ for (Rolls.confused(f.p1)) |p1_cfzd| { a.p1.confused = p1_cfzd;
239
+ for (Rolls.confused(f.p2)) |p2_cfzd| { a.p2.confused = p2_cfzd;
240
+ for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
241
+ for (Rolls.paralyzed(f.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
242
+ for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
243
+ for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
244
+ for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
245
+ for (Rolls.psywave(f.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
246
+ for (Rolls.moveSlot(f.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
247
+ for (Rolls.moveSlot(f.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
248
+ for (Rolls.multiHit(f.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
249
+ for (Rolls.multiHit(f.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
250
+ for (Rolls.secondaryChance(f.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
251
+ for (Rolls.secondaryChance(f.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
252
+ for (Rolls.criticalHit(f.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
253
+ for (Rolls.criticalHit(f.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
254
+
255
+ var p1_dmg = Rolls.damage(f.p1, p1_hit);
296
256
  while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
297
257
  a.p1.damage = @intCast(p1_dmg.min);
298
- var p1_min: u8 = 0;
299
258
 
300
- var p2_dmg = Rolls.damage(template.p2, p2_hit);
259
+ var p2_dmg = Rolls.damage(f.p2, p2_hit);
260
+
261
+ const p1_min: u9 = p1_dmg.min;
262
+ const p2_min: u9 = p2_dmg.min;
263
+
301
264
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
302
265
  a.p2.damage = @intCast(p2_dmg.min);
303
266
 
304
- opts.calc.overrides.actions = a;
305
- opts.chance = .{ .probability = .{}, .actions = actions };
267
+ opts.calc.overrides = a;
268
+ opts.calc.summaries = .{};
269
+ opts.chance = .{ .probability = .{}, .durations = options.durations };
306
270
  const q = &opts.chance.probability;
307
271
 
308
272
  b = battle;
309
273
  _ = try b.update(c1, c2, &opts);
310
- stats.updates += 1;
311
274
 
312
- const p1_max: u8 = @intCast(p1_dmg.min);
313
- const p2_max: u8 = @intCast(p2_dmg.min);
314
- _ = cap;
275
+ stats.updates += 1;
315
276
 
316
- // const p1_max = if (p1_min != 0) p1_min else
317
- // try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), &opts.calc.summaries, cap);
318
- // const p2_max =
319
- // try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), &opts.calc.summaries, cap);
277
+ const summaries = &opts.calc.summaries;
278
+ const p1_max: u9 = if (p2_dmg.min != p2_min)
279
+ p1_dmg.min
280
+ else
281
+ try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
282
+ const p2_max: u9 =
283
+ try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
320
284
 
321
- if (opts.chance.actions.matches(template)) {
285
+ if (opts.chance.actions.matches(f)) {
322
286
  if (!opts.chance.actions.eql(a)) {
323
- if (!summary) try debug(writer, opts.chance.actions, false, .{ .color = i, .dim = true });
287
+ if (!summary) {
288
+ try debug(writer, opts.chance.actions, .{
289
+ .p1_max = p1_max,
290
+ .p2_max = p2_max,
291
+ .color = i,
292
+ .dim = true,
293
+ });
294
+ }
324
295
 
325
- p1_min = p1_max;
296
+ p1_dmg.min = p1_max;
326
297
  p2_dmg.min = p2_max;
327
298
  continue;
328
299
  }
329
300
 
330
- if (!summary) try debug(writer, opts.chance.actions, false, .{ .color = i });
301
+ if (!summary) {
302
+ try debug(writer, opts.chance.actions, .{
303
+ .p1_max = p1_max,
304
+ .p2_max = p2_max,
305
+ .color = i,
306
+ });
307
+ }
331
308
 
332
- for (p1_dmg.min..@as(u9, p1_max) + 1) |p1d| {
333
- for (p2_dmg.min..@as(u9, p2_max) + 1) |p2d| {
309
+ for (p1_min..p1_max + 1) |p1d| {
310
+ for (p2_dmg.min..p2_max + 1) |p2d| {
334
311
  var acts = opts.chance.actions;
335
312
  acts.p1.damage = @intCast(p1d);
336
313
  acts.p2.damage = @intCast(p2d);
@@ -341,7 +318,7 @@ pub fn transitions(
341
318
  }
342
319
  }
343
320
 
344
- if (p1_max != p1_dmg.min) try q.update(p1_max - p1_dmg.min + 1, 1);
321
+ if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
345
322
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
346
323
  try p.add(q);
347
324
  stats.saved += 1;
@@ -351,39 +328,44 @@ pub fn transitions(
351
328
  return error.TestUnexpectedResult;
352
329
  }
353
330
  } else {
354
-
355
-
356
331
  if (!matches(opts.chance.actions, i, frontier.items)) {
357
332
  try frontier.append(opts.chance.actions);
358
333
 
359
- try debug(writer, opts.chance.actions, false, .{ .dim = true, .newline = false });
334
+ try debug(writer, opts.chance.actions, .{
335
+ .p1_max = p1_max,
336
+ .p2_max = p2_max,
337
+ .dim = true,
338
+ .newline = false,
339
+ });
360
340
  try writer.writeAll(" → ");
361
- try debug(writer, opts.chance.actions, true, .{
341
+ try debug(writer, opts.chance.actions, .{
342
+ .shape = true,
362
343
  .color = frontier.items.len - 1,
363
344
  .dim = true,
364
345
  .background = true,
365
346
  .indent = false,
366
347
  });
367
348
  } else if (!summary) {
368
- try debug(writer, opts.chance.actions, false, .{ .dim = true });
349
+ try debug(writer, opts.chance.actions, .{
350
+ .p1_max = p1_max,
351
+ .p2_max = p2_max,
352
+ .dim = true,
353
+ });
369
354
  }
370
-
371
- // if (opts.chance.actions.p1.damage == 0) p1_max = 255;
372
- // if (opts.chance.actions.p2.damage == 0) p2_max = 255;
373
355
  }
374
356
 
375
- p1_min = p1_max;
357
+ p1_dmg.min = p1_max;
376
358
  p2_dmg.min = p2_max;
377
359
  }
378
360
 
379
- assert(p1_min > 0 or p1_dmg.min == 0);
380
- p1_dmg.min = p1_min;
381
-
382
- }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
361
+ }}}}}}}}}}}}}}}}}}
383
362
 
384
363
  if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
385
364
  p.reduce();
386
- try writer.print(" = {s} ({d:.2}%)\n\n", .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))});
365
+ try writer.print(
366
+ " = {s} ({d:.2}%)\n\n",
367
+ .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
368
+ );
387
369
  }
388
370
 
389
371
  }
@@ -415,61 +397,90 @@ pub fn update(
415
397
  writer: anytype,
416
398
  transition: bool,
417
399
  ) !Result {
400
+ const durations = options.chance.durations;
418
401
  options.chance.reset();
419
402
 
420
- if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
421
- return battle.update(c1, c2, options);
422
- }
403
+ if (!pkmn.options.chance or !pkmn.options.calc) return battle.update(c1, c2, options);
423
404
 
424
- var copy = battle.*;
425
- const actions = options.chance.actions;
405
+ const original = battle.*;
426
406
 
427
407
  // Perfom the actual update
428
408
  const result = battle.update(c1, c2, options);
429
-
430
- // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
431
- var buf: [helpers.MAX_DIFFS]u8 = undefined;
432
- var stream: protocol.ByteStream = .{ .buffer = &buf };
433
- const n = try helpers.diff(&copy, battle, stream.writer());
434
-
435
- // Applying the diff to the battle should take us back to the original copy
436
- // of the battle (ignoring the RNG).
437
- var patched = battle.*;
438
- patched.rng = copy.rng;
439
- helpers.patch(&patched, buf[0..n]);
440
- try expectEqual(copy, patched);
441
-
442
- if (!pkmn.options.chance or !pkmn.options.calc) return result;
409
+ var overrides = options.chance.actions;
410
+ const updated = options.chance.durations;
411
+
412
+ if (@TypeOf(battle.rng) == data.PRNG) {
413
+ // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
414
+ var buf: [helpers.MAX_DIFFS]u8 = undefined;
415
+ var stream: protocol.ByteStream = .{ .buffer = &buf };
416
+ const n = try helpers.diff(&original, battle, stream.writer());
417
+
418
+ // Applying the diff to the battle should take us back to the original copy
419
+ // of the battle (ignoring the RNG).
420
+ var patched = battle.*;
421
+ patched.rng = original.rng;
422
+ helpers.patch(&patched, buf[0..n]);
423
+ try expectEqual(original, patched);
424
+ }
443
425
 
444
426
  if (transition) {
445
427
  // Ensure we can generate all transitions from the same original state
446
428
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
447
429
  // the transitions function relies on RNG for discovery of states)
448
- if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
449
- .actions = actions,
430
+ if (try transitions(unfix(original), c1, c2, allocator, writer, .{
431
+ .durations = durations,
450
432
  .cap = true,
451
433
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
452
434
  }
453
435
 
454
436
  // Demonstrate that we can produce the same state by forcing the RNG to behave the
455
- // same as we observed - note that because we do not pass in a durations override
456
- // mask none of the durations will be extended.
437
+ // same as we observed.
457
438
  var override = pkmn.battle.options(
458
439
  protocol.NULL,
459
- chance.NULL,
460
- Calc{ .overrides = .{ .actions = actions } },
440
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
441
+ Calc{ .overrides = overrides },
461
442
  );
462
- const overridden = copy.update(c1, c2, &override);
443
+ var copy = original;
444
+ var overridden = copy.update(c1, c2, &override);
463
445
  try expectEqual(result, overridden);
446
+ try expectEqualActions(overrides, override.chance.actions);
447
+ try expectEqual(updated, override.chance.durations);
464
448
 
465
449
  // The actual battle excluding its RNG field should match a copy updated with
466
450
  // overridden RNG (the copy RNG may have advanced because of no-ops)
467
451
  copy.rng = battle.rng;
468
- try expectEqual(copy, battle.*);
452
+ try expectEqual(battle.*, copy);
453
+
454
+ // Demonstrate that even with the duration rolls changed, the duration
455
+ // fields on their own are sufficient to achieve the same outcomes
456
+ // NB: The actual battle bytes will differ because the durations won't match
457
+ overrides.p1.duration = if (overrides.p1.duration > 0) 2 else 0;
458
+ overrides.p2.duration = if (overrides.p2.duration > 0) 2 else 0;
459
+
460
+ override = pkmn.battle.options(
461
+ protocol.NULL,
462
+ Chance(Rational(u128)){ .probability = .{}, .durations = durations },
463
+ Calc{ .overrides = overrides },
464
+ );
465
+ copy = original;
466
+ overridden = copy.update(c1, c2, &override);
467
+ try expectEqual(result, overridden);
468
+ try expectEqualActions(overrides, override.chance.actions);
469
+ try expectEqual(updated, override.chance.durations);
469
470
 
470
471
  return result;
471
472
  }
472
473
 
474
+ fn expectEqualActions(expected: Actions, actual: Actions) !void {
475
+ return expectEqual(expected, actual) catch |e| switch (e) {
476
+ error.TestExpectedEqual => {
477
+ std.debug.print("expected {}, found {}\n", .{ expected, actual });
478
+ return e;
479
+ },
480
+ else => return e,
481
+ };
482
+ }
483
+
473
484
  fn unfix(actual: anytype) data.Battle(data.PRNG) {
474
485
  return .{
475
486
  .sides = actual.sides,
@@ -502,6 +513,9 @@ fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
502
513
  }
503
514
 
504
515
  const Style = struct {
516
+ shape: bool = false,
517
+ p1_max: u9 = 0,
518
+ p2_max: u9 = 0,
505
519
  color: ?usize = null,
506
520
  bold: bool = false,
507
521
  background: bool = false,
@@ -510,7 +524,7 @@ const Style = struct {
510
524
  indent: bool = true,
511
525
  };
512
526
 
513
- fn debug(writer: anytype, actions: Actions, shape: bool, style: Style) !void {
527
+ fn debug(writer: anytype, actions: Actions, style: Style) !void {
514
528
  if (style.indent) try writer.writeAll(" ");
515
529
  if (tty) {
516
530
  const mod: usize = if (style.dim) 2 else 1;
@@ -519,11 +533,272 @@ fn debug(writer: anytype, actions: Actions, shape: bool, style: Style) !void {
519
533
 
520
534
  if (style.dim or style.bold) try writer.print("\x1b[{d}m", .{mod});
521
535
  try writer.print("\x1b[{d}{d}m", .{ background, color });
522
- try actions.fmt(writer, shape);
536
+ const p1 = if (style.p1_max != 0 and style.p1_max != actions.p1.damage) style.p1_max else 0;
537
+ const p2 = if (style.p2_max != 0 and style.p2_max != actions.p2.damage) style.p2_max else 0;
538
+ if (p1 != 0 or p2 != 0) {
539
+ assert(!style.shape);
540
+ try format(writer, actions, p1, p2);
541
+ } else {
542
+ try actions.fmt(writer, style.shape);
543
+ }
523
544
  try writer.writeAll("\x1b[0m");
524
545
  } else {
525
546
  if (style.dim) try writer.writeAll(" ");
526
- try actions.fmt(writer, shape);
547
+ try actions.fmt(writer, style.shape);
527
548
  }
528
549
  if (style.newline) try writer.writeByte('\n');
529
550
  }
551
+
552
+ fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
553
+ var input: [1024]u8 = undefined;
554
+ var output: [1024]u8 = undefined;
555
+
556
+ var stream = std.io.fixedBufferStream(&input);
557
+ try actions.fmt(&stream.writer(), false);
558
+ var len = stream.getWritten().len;
559
+ @memcpy(output[0..len], input[0..len]);
560
+
561
+ var find: [16]u8 = undefined;
562
+ var repl: [22]u8 = undefined;
563
+ if (p1 != 0 and actions.p1.damage != 0) {
564
+ const n = try std.fmt.bufPrint(&find, "P1 = (damage:{d}", .{actions.p1.damage});
565
+ const r = try std.fmt.bufPrint(&repl, "P1 = (damage:{d}…{d}", .{ actions.p1.damage, p1 });
566
+ assert(std.mem.replace(u8, input[0..len], n, r, output[0 .. len + 6]) == 1);
567
+ @memcpy(input[0 .. len + 6], output[0 .. len + 6]);
568
+ len += 6;
569
+ }
570
+ if (p2 != 0 and actions.p2.damage != 0) {
571
+ const n = try std.fmt.bufPrint(&find, "P2 = (damage:{d}", .{actions.p2.damage});
572
+ const r = try std.fmt.bufPrint(&repl, "P2 = (damage:{d}…{d}", .{ actions.p2.damage, p2 });
573
+ assert(std.mem.replace(u8, input[0..len], n, r, output[0 .. len + 6]) == 1);
574
+ len += 6;
575
+ }
576
+
577
+ try writer.writeAll(output[0..len]);
578
+ }
579
+
580
+ /// Helper functions that efficiently return valid ranges for various RNG events based on the
581
+ /// state of an `Action` and other events to be used to construct a "transitions" function.
582
+ pub const Rolls = struct {
583
+ const PLAYER_NONE = [_]Optional(Player){.None};
584
+ const PLAYERS = [_]Optional(Player){ .P1, .P2 };
585
+
586
+ /// Returns a slice with the correct range of values for speed ties given the `action` state.
587
+ pub fn speedTie(action: Action) []const Optional(Player) {
588
+ return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
589
+ }
590
+
591
+ const BOOL_NONE = [_]Optional(bool){.None};
592
+ const BOOLS = [_]Optional(bool){ .false, .true };
593
+
594
+ /// Returns a slice with the correct range of values for hits given the `action` state
595
+ /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
596
+ pub fn hit(action: Action, parent: Optional(bool)) []const Optional(bool) {
597
+ if (parent == .true) return &BOOL_NONE;
598
+ return if (action.hit == .None) &BOOL_NONE else &BOOLS;
599
+ }
600
+
601
+ /// Returns a slice with the correct range of values for critical hits given the `action` state
602
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
603
+ pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
604
+ if (parent == .false) return &BOOL_NONE;
605
+ return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
606
+ }
607
+
608
+ /// Returns a slice with the correct range of values for secondary chances hits given the
609
+ /// `action` state and the state of the `parent` (whether the player's Pokémon's move hit).
610
+ pub fn secondaryChance(action: Action, parent: Optional(bool)) []const Optional(bool) {
611
+ if (parent == .false) return &BOOL_NONE;
612
+ return if (@field(action, "secondary_chance") == .None) &BOOL_NONE else &BOOLS;
613
+ }
614
+
615
+ /// The min and max bounds on iteration over damage rolls.
616
+ pub const Range = struct { min: u9, max: u9 };
617
+
618
+ /// Returns the range bounding damage rolls given the `action` state and the state of
619
+ /// the `parent` (whether the player's Pokémon's move hit).
620
+ pub fn damage(action: Action, parent: Optional(bool)) Range {
621
+ return if (parent == .false or action.damage == 0)
622
+ .{ .min = 0, .max = 1 }
623
+ else
624
+ .{ .min = 217, .max = 256 };
625
+ }
626
+
627
+ /// Returns the max damage roll which will produce the same damage as `roll`
628
+ /// given the base damage in `summaries`.
629
+ pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) !u8 {
630
+ if (roll == 0) return roll;
631
+
632
+ const dmg = summaries.get(player.foe()).damage;
633
+ if (dmg.base == 0 or (cap and dmg.capped)) return 255;
634
+
635
+ // Closed form solution for max damage roll provided by Orion Taylor (taylorott)
636
+ return @min(255, roll + ((254 - ((@as(u32, dmg.base) * roll) % 255)) / dmg.base));
637
+ }
638
+
639
+ /// Returns a slice with the correct range of values for confused.
640
+ pub fn confused(action: Action) []const Optional(bool) {
641
+ return if (action.confused == .None) &BOOL_NONE else &BOOLS;
642
+ }
643
+
644
+ /// Returns a slice with the correct range of values for paralysis given the `action` state
645
+ /// and the state of the `parent` (whether the player's Pokémon was confused).
646
+ pub fn paralyzed(action: Action, parent: Optional(bool)) []const Optional(bool) {
647
+ if (parent == .true) return &BOOL_NONE;
648
+ return if (action.paralyzed == .None) &BOOL_NONE else &BOOLS;
649
+ }
650
+
651
+ const SLOT_NONE = [_]u4{0};
652
+ const SLOT = [_]u4{ 1, 2, 3, 4 };
653
+
654
+ /// Returns a slice with a range of values for move slots given the `action` state
655
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
656
+ ///
657
+ /// These slots may or **may not be valid** as slots may be unset / have 0 PP.
658
+ pub fn moveSlot(action: Action, parent: Optional(bool)) []const u4 {
659
+ if (parent == .false) return &SLOT_NONE;
660
+ return if (action.move_slot == 0) &SLOT_NONE else &SLOT;
661
+ }
662
+
663
+ const MULTI_NONE = [_]u4{0};
664
+ const MULTI = [_]u4{ 2, 3, 4, 5 };
665
+
666
+ /// Returns a slice with the correct range of values for multi hit given the `action` state
667
+ /// and the state of the `parent` (whether the player's Pokémon's move hit).
668
+ pub fn multiHit(action: Action, parent: Optional(bool)) []const u4 {
669
+ if (parent == .false) return &MULTI_NONE;
670
+ return if (action.multi_hit == 0) &MULTI_NONE else &MULTI;
671
+ }
672
+
673
+ const PSYWAVE_NONE = [_]u8{0};
674
+ const PSYWAVE = init: {
675
+ var rolls: [150]u8 = undefined;
676
+ for (0..150) |i| rolls[i] = i + 1;
677
+ break :init rolls;
678
+ };
679
+
680
+ /// Returns a slice with the correct range of values for psywave given the `action` state,
681
+ /// the `side`, and the state of the `parent` (whether the player's Pokémon's move hit).
682
+ pub fn psywave(
683
+ action: Action,
684
+ side: *data.Side,
685
+ parent: Optional(bool),
686
+ ) []const u8 {
687
+ if (parent == .false) return &PSYWAVE_NONE;
688
+ return if (action.psywave == 0)
689
+ &PSYWAVE_NONE
690
+ else
691
+ PSYWAVE[0 .. @as(u16, side.stored().level) * 3 / 2];
692
+ }
693
+
694
+ const MOVE_NONE = [_]data.Move{.None};
695
+
696
+ /// Returns a slice with the correct range of values for metronome given the `action` state.
697
+ pub fn metronome(action: Action) []const data.Move {
698
+ return if (action.metronome == .None) &MOVE_NONE else &data.Move.METRONOME;
699
+ }
700
+ };
701
+
702
+ test "Rolls.speedTie" {
703
+ const actions: Actions = .{ .p1 = .{ .speed_tie = .P2 } };
704
+ try expectEqualSlices(Optional(Player), &.{ .P1, .P2 }, Rolls.speedTie(actions.p1));
705
+ try expectEqualSlices(Optional(Player), &.{.None}, Rolls.speedTie(actions.p2));
706
+ }
707
+
708
+ test "Rolls.damage" {
709
+ const actions: Actions = .{ .p2 = .{ .damage = 221 } };
710
+ try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p1, .None));
711
+ try expectEqual(Rolls.Range{ .min = 217, .max = 256 }, Rolls.damage(actions.p2, .None));
712
+ try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p2, .false));
713
+ }
714
+
715
+ test "Rolls.coalesce" {
716
+ var summaries = Summaries{ .p1 = .{ .damage = .{ .base = 74, .final = 69, .capped = true } } };
717
+ try expectEqual(@as(u8, 0), try Rolls.coalesce(.P2, 0, &summaries, false));
718
+ try expectEqual(@as(u8, 241), try Rolls.coalesce(.P2, 238, &summaries, false));
719
+ try expectEqual(@as(u8, 255), try Rolls.coalesce(.P2, 238, &summaries, true));
720
+ summaries.p1.damage.final = 74;
721
+ try expectEqual(@as(u8, 217), try Rolls.coalesce(.P2, 217, &summaries, false));
722
+ try expectEqual(@as(u8, 255), try Rolls.coalesce(.P2, 217, &summaries, true));
723
+ }
724
+
725
+ test "Rolls.hit" {
726
+ const actions: Actions = .{ .p2 = .{ .hit = .true } };
727
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.hit(actions.p1, .None));
728
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.hit(actions.p2, .None));
729
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.hit(actions.p2, .true));
730
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.hit(actions.p2, .false));
731
+ }
732
+
733
+ test "Rolls.secondaryChance" {
734
+ const actions: Actions = .{ .p1 = .{ .secondary_chance = .true } };
735
+ try expectEqualSlices(
736
+ Optional(bool),
737
+ &.{ .false, .true },
738
+ Rolls.secondaryChance(actions.p1, .None),
739
+ );
740
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.secondaryChance(actions.p1, .false));
741
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.secondaryChance(actions.p2, .None));
742
+ }
743
+
744
+ test "Rolls.criticalHit" {
745
+ const actions: Actions = .{ .p1 = .{ .critical_hit = .true } };
746
+ try expectEqualSlices(
747
+ Optional(bool),
748
+ &.{ .false, .true },
749
+ Rolls.criticalHit(actions.p1, .None),
750
+ );
751
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p1, .false));
752
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
753
+ }
754
+
755
+ test "Rolls.confused" {
756
+ const actions: Actions = .{ .p2 = .{ .confused = .true } };
757
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1));
758
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.confused(actions.p2));
759
+ }
760
+
761
+ test "Rolls.paralyzed" {
762
+ const actions: Actions = .{ .p2 = .{ .paralyzed = .true } };
763
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p1, .None));
764
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.paralyzed(actions.p2, .None));
765
+ try expectEqualSlices(Optional(bool), &.{ .false, .true }, Rolls.paralyzed(actions.p2, .false));
766
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.paralyzed(actions.p2, .true));
767
+ }
768
+
769
+ test "Rolls.moveSlot" {
770
+ const actions: Actions = .{ .p2 = .{ .move_slot = 3 } };
771
+ try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p1, .None));
772
+ try expectEqualSlices(u4, &.{ 1, 2, 3, 4 }, Rolls.moveSlot(actions.p2, .None));
773
+ try expectEqualSlices(u4, &.{0}, Rolls.moveSlot(actions.p2, .false));
774
+ }
775
+
776
+ test "Rolls.multiHit" {
777
+ const actions: Actions = .{ .p2 = .{ .multi_hit = 3 } };
778
+ try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p1, .None));
779
+ try expectEqualSlices(u4, &.{ 2, 3, 4, 5 }, Rolls.multiHit(actions.p2, .None));
780
+ try expectEqualSlices(u4, &.{0}, Rolls.multiHit(actions.p2, .false));
781
+ }
782
+
783
+ test "Rolls.metronome" {
784
+ const actions: Actions = .{ .p2 = .{ .metronome = .Surf } };
785
+ try expectEqualSlices(data.Move, &.{.None}, Rolls.metronome(actions.p1));
786
+ try expectEqual(@as(data.Move, @enumFromInt(24)), Rolls.metronome(actions.p2)[23]);
787
+ }
788
+
789
+ test "Rolls.psywave" {
790
+ const actions: Actions = .{ .p2 = .{ .psywave = 79 } };
791
+ var side = helpers.Side.init(&[_]helpers.Pokemon{.{
792
+ .species = .Bulbasaur,
793
+ .level = 100,
794
+ .moves = &[_]data.Move{.Tackle},
795
+ }});
796
+
797
+ try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p1, &side, .None));
798
+ var rolls = Rolls.psywave(actions.p2, &side, .None);
799
+ try expectEqual(@as(u8, 150), rolls[rolls.len - 1]);
800
+ side.stored().level = 81;
801
+ rolls = Rolls.psywave(actions.p2, &side, .None);
802
+ try expectEqual(@as(u8, 121), rolls[rolls.len - 1]);
803
+ try expectEqualSlices(u8, &.{0}, Rolls.psywave(actions.p2, &side, .false));
804
+ }