@pkmn/engine 0.1.0-dev.10889360 → 0.1.0-dev.115ecc94

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