@pkmn/engine 0.1.0-dev.9f75c7ae → 0.1.0-dev.a0167a9d

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 (42) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +44 -19
  3. package/build/pkg/addon.js +6 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data.d.ts +4 -4
  6. package/build/pkg/data.js.map +1 -1
  7. package/build/pkg/gen1.js.map +1 -1
  8. package/build/pkg/index.d.ts +2 -1
  9. package/build/pkg/index.js.map +1 -1
  10. package/build/pkg/protocol.js +5 -5
  11. package/build/pkg/protocol.js.map +1 -1
  12. package/build/test/display.js +2 -2
  13. package/build/test/display.js.map +1 -1
  14. package/build/tools/debug.js +5 -4
  15. package/build/tools/debug.js.map +1 -1
  16. package/build.zig +82 -90
  17. package/package.json +31 -23
  18. package/src/bin/install-pkmn-engine +26 -18
  19. package/src/lib/common/data.zig +4 -4
  20. package/src/lib/common/debug.zig +2 -2
  21. package/src/lib/common/optional.zig +1 -1
  22. package/src/lib/common/options.zig +2 -0
  23. package/src/lib/common/protocol.zig +393 -396
  24. package/src/lib/common/rational.zig +109 -15
  25. package/src/lib/common/rng.zig +11 -11
  26. package/src/lib/gen1/calc.zig +269 -97
  27. package/src/lib/gen1/chance.zig +126 -160
  28. package/src/lib/gen1/data/species.zig +1 -1
  29. package/src/lib/gen1/data.zig +21 -21
  30. package/src/lib/gen1/helpers.zig +191 -206
  31. package/src/lib/gen1/mechanics.zig +308 -285
  32. package/src/lib/gen2/chance.zig +211 -121
  33. package/src/lib/gen2/data.zig +13 -11
  34. package/src/lib/gen2/generated.zig +15 -24
  35. package/src/lib/gen2/helpers.zig +37 -7
  36. package/src/lib/gen2/mechanics.zig +855 -413
  37. package/src/lib/{binding/node.zig → node.zig} +9 -12
  38. package/src/lib/pkmn.zig +27 -12
  39. package/src/lib/{binding/wasm.zig → wasm.zig} +2 -1
  40. package/src/test/display/pkmn.html.tmpl +5 -5
  41. package/src/lib/gen1/README.md +0 -894
  42. package/src/lib/gen2/README.md +0 -330
@@ -38,6 +38,9 @@ const Duration = chance.Duration;
38
38
 
39
39
  const Rolls = helpers.Rolls;
40
40
 
41
+ const tty = true; // DEBUG
42
+ const summary = false; // DEBUG
43
+
41
44
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
42
45
  pub const Summaries = extern struct {
43
46
  /// Relevant information for Player 1.
@@ -69,8 +72,10 @@ pub const Summary = extern struct {
69
72
  /// The final computed damage that gets applied to the Pokémon. May exceed the target's HP
70
73
  // (to determine the *actual* damage done compare the target's stored HP before and after).
71
74
  final: u16 = 0,
72
- /// Whether higher damage will saturate / result in the same outcome (e.g. additional damage
73
- /// gets ignored due to it already breaking a Substitute or causing the target to faint).
75
+ /// Whether higher direct damage will saturate / result in the same outcome (e.g. additional
76
+ /// direct damage gets ignored due to it already breaking a Substitute or causing the target
77
+ /// to faint). Note that this field does not get set in scenarios where the target would
78
+ /// only be guaranteed to faint due to some sort of subsequent recoil or residual damage.
74
79
  capped: bool = false,
75
80
 
76
81
  // NOTE: 15 bits padding
@@ -131,9 +136,7 @@ pub const Calc = struct {
131
136
  const set = @field(if (player == .P1) durations.p1 else durations.p2, @tagName(field));
132
137
  if (set == 0) return null;
133
138
 
134
- const actions = self.overrides.actions;
135
- const overrides = if (player == .P1) actions.p1.durations else actions.p2.durations;
136
- return @field(overrides, @tagName(field)) != 0;
139
+ return null; // TODO
137
140
  }
138
141
 
139
142
  pub fn base(self: *Calc, player: Player, val: u16) void {
@@ -196,11 +199,19 @@ pub const Stats = struct {
196
199
 
197
200
  pub const Options = struct {
198
201
  actions: Actions = .{},
202
+ durations: Durations = .{},
199
203
  seed: ?u64 = null,
200
204
  cap: bool = false,
201
205
  metronome: bool = false,
202
206
  };
203
207
 
208
+ pub const MAX_FRONTIER = 19;
209
+
210
+ const Element = struct {
211
+ actions: Actions,
212
+ durations: Durations,
213
+ };
214
+
204
215
  pub fn transitions(
205
216
  battle: anytype,
206
217
  c1: Choice,
@@ -211,92 +222,97 @@ pub fn transitions(
211
222
  ) !?Stats {
212
223
  var stats: Stats = .{};
213
224
 
214
- var actions = options.actions;
215
- var cap = options.cap;
225
+ const actions = options.actions;
226
+ const durations = options.durations;
227
+ const cap = options.cap;
216
228
 
217
229
  var seen = std.AutoHashMap(Actions, void).init(allocator);
218
230
  defer seen.deinit();
219
- var frontier = std.ArrayList(Actions).init(allocator);
231
+ var frontier = std.ArrayList(Element).init(allocator);
220
232
  defer frontier.deinit();
221
233
 
222
234
  var opts = pkmn.battle.options(
223
235
  protocol.NULL,
224
- Chance(Rational(u128)){ .probability = .{}, .actions = actions },
225
- Calc{ .overrides = .{ .actions = actions, .durations = .{
226
- .p1 = actions.p1.durations,
227
- .p2 = actions.p2.durations,
228
- } } },
236
+ Chance(Rational(u128)){ .probability = .{}, .actions = actions, .durations = durations },
237
+ Calc{ .overrides = .{ .actions = actions, .durations = durations } },
229
238
  );
230
239
 
231
240
  var b = battle;
232
241
  _ = try b.update(c1, c2, &opts);
233
242
  stats.updates += 1;
234
243
 
235
- var p1 = b.side(.P1);
236
- var p2 = b.side(.P2);
244
+ const p1 = b.side(.P1);
245
+ const p2 = b.side(.P2);
237
246
 
238
247
  var p: Rational(u128) = .{ .p = 0, .q = 1 };
239
- try frontier.append(opts.chance.actions);
248
+ try frontier.append(.{ .actions = opts.chance.actions, .durations = opts.chance.durations });
240
249
 
241
250
  // zig fmt: off
242
- for (Rolls.metronome(frontier.items[0].p1)) |p1_move| {
243
- for (Rolls.metronome(frontier.items[0].p2)) |p2_move| {
251
+ for (Rolls.metronome(frontier.items[0].actions.p1)) |p1_move| {
252
+ for (Rolls.metronome(frontier.items[0].actions.p2)) |p2_move| {
244
253
 
245
254
  if (!options.metronome and (p1_move != .None or p2_move != .None)) return null;
246
255
 
247
256
  var i: usize = 0;
248
257
  assert(frontier.items.len == 1);
249
258
  while (i < frontier.items.len) : (i += 1) {
250
- var template = frontier.items[i];
251
- opts.calc.overrides.durations = .{
252
- .p1 = template.p1.durations,
253
- .p2 = template.p2.durations,
254
- };
259
+ const template = frontier.items[i];
255
260
 
256
- try debug(writer, template, true, .{ .color = i, .bold = true, .background = true });
261
+ try debug(writer, template.actions, .{
262
+ .shape = true,
263
+ .color = i,
264
+ .bold = true,
265
+ .background = true,
266
+ .indent = false,
267
+ });
257
268
 
258
269
  var a: Actions = .{ .p1 = .{ .metronome = p1_move }, .p2 = .{ .metronome = p2_move } };
259
-
260
- for (Rolls.speedTie(template.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
261
- for (Rolls.sleep(template.p1, actions.p1)) |p1_slp| { a.p1.durations.sleep = p1_slp;
262
- for (Rolls.sleep(template.p2, actions.p2)) |p2_slp| { a.p2.durations.sleep = p2_slp;
263
- for (Rolls.disable(template.p1, actions.p1, p1_slp)) |p1_disable| { a.p1.durations.disable = p1_disable;
264
- for (Rolls.disable(template.p2, actions.p2, p2_slp)) |p2_disable| { a.p2.durations.disable = p2_disable;
265
- for (Rolls.confusion(template.p1, actions.p1, p1_slp)) |p1_cfz| { a.p1.durations.confusion = p1_cfz;
266
- for (Rolls.confusion(template.p2, actions.p2, p1_slp)) |p2_cfz| { a.p2.durations.confusion = p2_cfz;
267
- for (Rolls.confused(template.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
268
- for (Rolls.confused(template.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
269
- for (Rolls.paralyzed(template.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
270
- for (Rolls.paralyzed(template.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
271
- for (Rolls.attacking(template.p1, actions.p1, p1_par)) |p1_atk| { a.p1.durations.attacking = p1_atk;
272
- for (Rolls.attacking(template.p2, actions.p2, p2_par)) |p2_atk| { a.p2.durations.attacking = p2_atk;
273
- for (Rolls.binding(template.p1, actions.p1, p1_par)) |p1_bind| { a.p1.durations.binding = p1_bind;
274
- for (Rolls.binding(template.p2, actions.p2, p2_par)) |p2_bind| { a.p2.durations.binding = p2_bind;
275
- for (Rolls.duration(template.p1, p1_par)) |p1_dur| { a.p1.duration = p1_dur;
276
- for (Rolls.duration(template.p2, p2_par)) |p2_dur| { a.p2.duration = p2_dur;
277
- for (Rolls.hit(template.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
278
- for (Rolls.hit(template.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
279
- for (Rolls.psywave(template.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
280
- for (Rolls.psywave(template.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
281
- for (Rolls.moveSlot(template.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
282
- for (Rolls.moveSlot(template.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
283
- for (Rolls.multiHit(template.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
284
- for (Rolls.multiHit(template.p2, p1_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
285
- for (Rolls.secondaryChance(template.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
286
- for (Rolls.secondaryChance(template.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
287
- for (Rolls.criticalHit(template.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
288
- for (Rolls.criticalHit(template.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
289
-
290
- var p1_dmg = Rolls.damage(template.p1, p1_hit);
270
+ var d: Durations = .{};
271
+
272
+ for (Rolls.speedTie(template.actions.p1)) |tie| { a.p1.speed_tie = tie; a.p2.speed_tie = tie;
273
+ for (Rolls.sleep(template.durations.p1, durations.p1)) |p1_slp| { d.p1.sleep = p1_slp;
274
+ for (Rolls.sleep(template.durations.p2, durations.p2)) |p2_slp| { d.p2.sleep = p2_slp;
275
+ for (Rolls.disable(template.durations.p1, durations.p1, p1_slp)) |p1_disable| { d.p1.disable = p1_disable;
276
+ for (Rolls.disable(template.durations.p2, durations.p2, p2_slp)) |p2_disable| { d.p2.disable = p2_disable;
277
+ for (Rolls.confusion(template.durations.p1, durations.p1, p1_slp)) |p1_cfz| { d.p1.confusion = p1_cfz;
278
+ for (Rolls.confusion(template.durations.p2, durations.p2, p1_slp)) |p2_cfz| { d.p2.confusion = p2_cfz;
279
+ for (Rolls.confused(template.actions.p1, p1_cfz)) |p1_cfzd| { a.p1.confused = p1_cfzd;
280
+ for (Rolls.confused(template.actions.p2, p2_cfz)) |p2_cfzd| { a.p2.confused = p2_cfzd;
281
+ for (Rolls.paralyzed(template.actions.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
282
+ for (Rolls.paralyzed(template.actions.p2, p2_cfzd)) |p2_par| { a.p2.paralyzed = p2_par;
283
+ for (Rolls.attacking(template.durations.p1, durations.p1, p1_par)) |p1_atk| { d.p1.attacking = p1_atk;
284
+ for (Rolls.attacking(template.durations.p2, durations.p2, p2_par)) |p2_atk| { d.p2.attacking = p2_atk;
285
+ for (Rolls.binding(template.durations.p1, durations.p1, p1_par)) |p1_bind| { d.p1.binding = p1_bind;
286
+ for (Rolls.binding(template.durations.p2, durations.p2, p2_par)) |p2_bind| { d.p2.binding = p2_bind;
287
+ for (Rolls.duration(template.actions.p1, p1_par)) |p1_dur| { a.p1.duration = p1_dur;
288
+ for (Rolls.duration(template.actions.p2, p2_par)) |p2_dur| { a.p2.duration = p2_dur;
289
+ for (Rolls.hit(template.actions.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
290
+ for (Rolls.hit(template.actions.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
291
+ for (Rolls.psywave(template.actions.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
292
+ for (Rolls.psywave(template.actions.p2, p2, p2_hit)) |p2_psywave| { a.p2.psywave = p2_psywave;
293
+ for (Rolls.moveSlot(template.actions.p1, p1_hit)) |p1_slot| { a.p1.move_slot = p1_slot;
294
+ for (Rolls.moveSlot(template.actions.p2, p2_hit)) |p2_slot| { a.p2.move_slot = p2_slot;
295
+ for (Rolls.multiHit(template.actions.p1, p1_hit)) |p1_multi| { a.p1.multi_hit = p1_multi;
296
+ for (Rolls.multiHit(template.actions.p2, p2_hit)) |p2_multi| { a.p2.multi_hit = p2_multi;
297
+ for (Rolls.secondaryChance(template.actions.p1, p1_hit)) |p1_sec| { a.p1.secondary_chance = p1_sec;
298
+ for (Rolls.secondaryChance(template.actions.p2, p2_hit)) |p2_sec| { a.p2.secondary_chance = p2_sec;
299
+ for (Rolls.criticalHit(template.actions.p1, p1_hit)) |p1_crit| { a.p1.critical_hit = p1_crit;
300
+ for (Rolls.criticalHit(template.actions.p2, p2_hit)) |p2_crit| { a.p2.critical_hit = p2_crit;
301
+
302
+ var p1_dmg = Rolls.damage(template.actions.p1, p1_hit);
291
303
  while (p1_dmg.min < p1_dmg.max) : (p1_dmg.min += 1) {
292
304
  a.p1.damage = @intCast(p1_dmg.min);
293
- var p1_min: u8 = 0;
294
305
 
295
- var p2_dmg = Rolls.damage(template.p2, p2_hit);
306
+ var p2_dmg = Rolls.damage(template.actions.p2, p2_hit);
307
+
308
+ const p1_min: u9 = p1_dmg.min;
309
+ const p2_min: u9 = p2_dmg.min;
310
+
296
311
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
297
312
  a.p2.damage = @intCast(p2_dmg.min);
298
313
 
299
314
  opts.calc.overrides.actions = a;
315
+ opts.calc.summaries = .{};
300
316
  opts.chance = .{ .probability = .{}, .actions = actions };
301
317
  const q = &opts.chance.probability;
302
318
 
@@ -304,28 +320,40 @@ pub fn transitions(
304
320
  _ = try b.update(c1, c2, &opts);
305
321
  stats.updates += 1;
306
322
 
307
- const p1_max: u8 = @intCast(p1_dmg.min);
308
- const p2_max: u8 = @intCast(p2_dmg.min);
309
- _ = cap;
310
-
311
- // var p1_max = if (p1_min != 0) p1_min else
312
- // try Rolls.coalesce(.P1, @intCast(u8, p1_dmg.min), &opts.calc.summaries, cap);
313
- // var p2_max =
314
- // try Rolls.coalesce(.P2, @intCast(u8, p2_dmg.min), &opts.calc.summaries, cap);
323
+ const summaries = &opts.calc.summaries;
324
+ const p1_max: u9 = if (p2_dmg.min != p2_min)
325
+ p1_dmg.min
326
+ else
327
+ try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
328
+ const p2_max: u9 =
329
+ try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
315
330
 
316
- if (opts.chance.actions.matches(template)) {
331
+ if (opts.chance.actions.matches(template.actions)) {
317
332
  if (!opts.chance.actions.eql(a)) {
318
- try debug(writer, opts.chance.actions, false, .{ .color = i, .dim = true });
333
+ if (!summary) {
334
+ try debug(writer, opts.chance.actions, .{
335
+ .p1_max = p1_max,
336
+ .p2_max = p2_max,
337
+ .color = i,
338
+ .dim = true,
339
+ });
340
+ }
319
341
 
320
- p1_min = p1_max;
342
+ p1_dmg.min = p1_max;
321
343
  p2_dmg.min = p2_max;
322
344
  continue;
323
345
  }
324
346
 
325
- try debug(writer, opts.chance.actions, false, .{ .color = i });
347
+ if (!summary) {
348
+ try debug(writer, opts.chance.actions, .{
349
+ .p1_max = p1_max,
350
+ .p2_max = p2_max,
351
+ .color = i,
352
+ });
353
+ }
326
354
 
327
- for (p1_dmg.min..@as(u9, p1_max) + 1) |p1d| {
328
- for (p2_dmg.min..@as(u9, p2_max) + 1) |p2d| {
355
+ for (p1_min..p1_max + 1) |p1d| {
356
+ for (p2_dmg.min..p2_max + 1) |p2d| {
329
357
  var acts = opts.chance.actions;
330
358
  acts.p1.damage = @intCast(p1d);
331
359
  acts.p2.damage = @intCast(p2d);
@@ -336,7 +364,7 @@ pub fn transitions(
336
364
  }
337
365
  }
338
366
 
339
- if (p1_max != p1_dmg.min) try q.update(p1_max - p1_dmg.min + 1, 1);
367
+ if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
340
368
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
341
369
  try p.add(q);
342
370
  stats.saved += 1;
@@ -346,27 +374,49 @@ pub fn transitions(
346
374
  return error.TestUnexpectedResult;
347
375
  }
348
376
  } else {
349
- try debug(writer, opts.chance.actions, false, .{ .dim = true });
350
-
351
377
  if (!matches(opts.chance.actions, i, frontier.items)) {
352
- try frontier.append(opts.chance.actions);
378
+ try frontier.append(.{
379
+ .actions = opts.chance.actions,
380
+ .durations = opts.chance.durations,
381
+ });
353
382
 
354
- try debug(writer, opts.chance.actions, true, .{
383
+ try debug(writer, opts.chance.actions, .{
384
+ .p1_max = p1_max,
385
+ .p2_max = p2_max,
386
+ .dim = true,
387
+ .newline = false,
388
+ });
389
+ try writer.writeAll(" → ");
390
+ try debug(writer, opts.chance.actions, .{
391
+ .shape = true,
355
392
  .color = frontier.items.len - 1,
356
393
  .dim = true,
357
- .background = true
394
+ .background = true,
395
+ .indent = false,
396
+ });
397
+ } else if (!summary) {
398
+ try debug(writer, opts.chance.actions, .{
399
+ .p1_max = p1_max,
400
+ .p2_max = p2_max,
401
+ .dim = true,
358
402
  });
359
403
  }
360
404
  }
361
405
 
362
- p1_min = p1_max;
406
+ p1_dmg.min = p1_max;
363
407
  p2_dmg.min = p2_max;
364
408
  }
365
409
 
366
- assert(p1_min > 0 or p1_dmg.min == 0);
367
- p1_dmg.min = p1_min;
368
-
369
410
  }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
411
+
412
+ if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
413
+ p.reduce();
414
+ try writer.print(
415
+ " = {s} ({d:.2}%)\n\n",
416
+ .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
417
+ );
418
+ }
419
+
370
420
  }
371
421
 
372
422
  assert(frontier.items.len == i);
@@ -387,11 +437,90 @@ pub fn transitions(
387
437
  return stats;
388
438
  }
389
439
 
390
- fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
440
+ pub fn update(
441
+ battle: anytype,
442
+ c1: Choice,
443
+ c2: Choice,
444
+ options: anytype,
445
+ allocator: std.mem.Allocator,
446
+ writer: anytype,
447
+ transition: bool,
448
+ ) !Result {
449
+ options.chance.reset();
450
+
451
+ if (!pkmn.options.chance or !pkmn.options.calc or @TypeOf(battle.rng) != data.PRNG) {
452
+ return battle.update(c1, c2, options);
453
+ }
454
+
455
+ var copy = battle.*;
456
+ const actions = options.chance.actions;
457
+
458
+ // Perfom the actual update
459
+ const result = battle.update(c1, c2, options);
460
+
461
+ // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
462
+ var buf: [helpers.MAX_DIFFS]u8 = undefined;
463
+ var stream: protocol.ByteStream = .{ .buffer = &buf };
464
+ const n = try helpers.diff(&copy, battle, stream.writer());
465
+
466
+ // Applying the diff to the battle should take us back to the original copy
467
+ // of the battle (ignoring the RNG).
468
+ var patched = battle.*;
469
+ patched.rng = copy.rng;
470
+ helpers.patch(&patched, buf[0..n]);
471
+ try expectEqual(copy, patched);
472
+
473
+ if (!pkmn.options.chance or !pkmn.options.calc) return result;
474
+
475
+ if (transition) {
476
+ // Ensure we can generate all transitions from the same original state
477
+ // (we must change the battle's RNG from a FixedRNG to a PRNG because
478
+ // the transitions function relies on RNG for discovery of states)
479
+ if (try transitions(unfix(copy), c1, c2, allocator, writer, .{
480
+ .actions = actions,
481
+ .cap = true,
482
+ })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
483
+ }
484
+
485
+ // Demonstrate that we can produce the same state by forcing the RNG to behave the
486
+ // same as we observed - note that because we do not pass in a durations override
487
+ // mask none of the durations will be extended.
488
+ var override = pkmn.battle.options(
489
+ protocol.NULL,
490
+ chance.NULL,
491
+ Calc{ .overrides = .{ .actions = actions } },
492
+ );
493
+ const overridden = copy.update(c1, c2, &override);
494
+ try expectEqual(result, overridden);
495
+
496
+ // The actual battle excluding its RNG field should match a copy updated with
497
+ // overridden RNG (the copy RNG may have advanced because of no-ops)
498
+ copy.rng = battle.rng;
499
+ try expectEqual(copy, battle.*);
500
+
501
+ return result;
502
+ }
503
+
504
+ fn unfix(actual: anytype) data.Battle(data.PRNG) {
505
+ return .{
506
+ .sides = actual.sides,
507
+ .turn = actual.turn,
508
+ .last_damage = actual.last_damage,
509
+ .last_moves = actual.last_moves,
510
+ .rng = .{ .src = .{
511
+ .seed = if (pkmn.options.showdown)
512
+ 0x12345678
513
+ else
514
+ .{ 123, 234, 56, 78, 9, 101, 112, 131, 4 },
515
+ } },
516
+ };
517
+ }
518
+
519
+ fn matches(actions: Actions, i: usize, frontier: []Element) bool {
391
520
  for (frontier, 0..) |f, j| {
392
521
  // TODO: is skipping this redundant check worth it?
393
522
  if (i == j) continue;
394
- if (f.matches(actions)) return true;
523
+ if (f.actions.matches(actions)) return true;
395
524
  }
396
525
  return false;
397
526
  }
@@ -404,23 +533,66 @@ fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
404
533
  }
405
534
 
406
535
  const Style = struct {
536
+ shape: bool = false,
537
+ p1_max: u9 = 0,
538
+ p2_max: u9 = 0,
407
539
  color: ?usize = null,
408
540
  bold: bool = false,
409
541
  background: bool = false,
410
542
  dim: bool = false,
543
+ newline: bool = true,
544
+ indent: bool = true,
411
545
  };
412
546
 
413
- fn debug(writer: anytype, actions: Actions, shape: bool, style: Style) !void {
414
- _ = .{ writer, actions, shape, style };
415
- // const mod: usize = if (style.dim) 2 else 1;
416
- // const background: usize = if (style.background) 4 else 3;
417
- // const color: usize = if (style.color) |c| (c % 6) + 1 else 7;
418
- // if (style.dim or style.bold) try writer.print("\x1b[{d}m", .{mod});
419
- // try writer.print("\x1b[{d}{d}m", .{ background, color });
420
- // try actions.fmt(writer, shape);
421
- // try writer.writeAll("\x1b[0m\n");
422
- // DEBUG
423
- // if (style.dim) try writer.writeAll(" ");
424
- // try actions.fmt(writer, shape);
425
- // try writer.writeByte('\n');
547
+ fn debug(writer: anytype, actions: Actions, style: Style) !void {
548
+ if (style.indent) try writer.writeAll(" ");
549
+ if (tty) {
550
+ const mod: usize = if (style.dim) 2 else 1;
551
+ const background: usize = if (style.background) 4 else 3;
552
+ const color: usize = if (style.color) |c| (c % 6) + 1 else 7;
553
+
554
+ if (style.dim or style.bold) try writer.print("\x1b[{d}m", .{mod});
555
+ try writer.print("\x1b[{d}{d}m", .{ background, color });
556
+ const p1 = if (style.p1_max != 0 and style.p1_max != actions.p1.damage) style.p1_max else 0;
557
+ const p2 = if (style.p2_max != 0 and style.p2_max != actions.p2.damage) style.p2_max else 0;
558
+ if (p1 != 0 or p2 != 0) {
559
+ assert(!style.shape);
560
+ try format(writer, actions, p1, p2);
561
+ } else {
562
+ try actions.fmt(writer, style.shape);
563
+ }
564
+ try writer.writeAll("\x1b[0m");
565
+ } else {
566
+ if (style.dim) try writer.writeAll(" ");
567
+ try actions.fmt(writer, style.shape);
568
+ }
569
+ if (style.newline) try writer.writeByte('\n');
570
+ }
571
+
572
+ fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
573
+ var input: [1024]u8 = undefined;
574
+ var output: [1024]u8 = undefined;
575
+
576
+ var stream = std.io.fixedBufferStream(&input);
577
+ try actions.fmt(&stream.writer(), false);
578
+ var len = stream.getWritten().len;
579
+ @memcpy(output[0..len], input[0..len]);
580
+
581
+ var find: [16]u8 = undefined;
582
+ var repl: [22]u8 = undefined;
583
+ if (p1 != 0 and actions.p1.damage != 0) {
584
+ const n = try std.fmt.bufPrint(&find, "P1 = (damage:{d}", .{actions.p1.damage});
585
+ const r = try std.fmt.bufPrint(&repl, "P1 = (damage:{d}…{d}", .{ actions.p1.damage, p1 });
586
+ assert(std.mem.replace(u8, input[0..len], n, r, output[0 .. len + 6]) == 1);
587
+ @memcpy(input[0 .. len + 6], output[0 .. len + 6]);
588
+ len += 6;
589
+ }
590
+ if (p2 != 0 and actions.p2.damage != 0) {
591
+ const n = try std.fmt.bufPrint(&find, "P2 = (damage:{d}", .{actions.p2.damage});
592
+ const r = try std.fmt.bufPrint(&repl, "P2 = (damage:{d}…{d}", .{ actions.p2.damage, p2 });
593
+ assert(std.mem.replace(u8, input[0..len], n, r, output[0 .. len + 6]) == 1);
594
+ len += 6;
595
+ }
596
+
597
+ try writer.writeAll(output[0..len]);
426
598
  }