@pkmn/engine 0.1.0-dev.f3aab252 → 0.1.0-dev.f3b63b86

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 (80) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +35 -8
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +26 -4
  13. package/build/pkg/addon.js +34 -65
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/layout.json +15 -2
  16. package/build/pkg/data/protocol.json +23 -9
  17. package/build/pkg/data.d.ts +23 -1
  18. package/build/pkg/data.js.map +1 -1
  19. package/build/pkg/gen1.d.ts +15 -18
  20. package/build/pkg/gen1.js +28 -19
  21. package/build/pkg/gen1.js.map +1 -1
  22. package/build/pkg/index.d.ts +39 -89
  23. package/build/pkg/index.js +23 -70
  24. package/build/pkg/index.js.map +1 -1
  25. package/build/pkg/protocol.d.ts +20 -1
  26. package/build/pkg/protocol.js +105 -103
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +68 -74
  29. package/build/tools/debug.js.map +1 -1
  30. package/build/tools/display/debug.jsx +152 -0
  31. package/build/tools/display/debug.jsx.map +1 -0
  32. package/build/tools/display/index.js +152 -0
  33. package/build/tools/display/index.js.map +1 -0
  34. package/build/tools/display/ui.jsx +359 -0
  35. package/build/tools/display/ui.jsx.map +1 -0
  36. package/build/tools/display/util.js +33 -0
  37. package/build/tools/display/util.js.map +1 -0
  38. package/build.zig +235 -213
  39. package/package.json +59 -141
  40. package/src/bin/install-pkmn-engine +25 -9
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +200 -0
  43. package/src/lib/bindings/node.zig +117 -0
  44. package/src/lib/bindings/wasm.zig +50 -0
  45. package/src/lib/common/array.zig +5 -12
  46. package/src/lib/common/debug.zig +30 -9
  47. package/src/lib/common/js.zig +3 -9
  48. package/src/lib/common/optional.zig +11 -21
  49. package/src/lib/common/options.zig +4 -0
  50. package/src/lib/common/protocol.zig +180 -171
  51. package/src/lib/common/rational.zig +278 -285
  52. package/src/lib/common/rng.zig +1 -3
  53. package/src/lib/common/util.zig +14 -31
  54. package/src/lib/gen1/calc.zig +258 -106
  55. package/src/lib/gen1/chance.zig +57 -54
  56. package/src/lib/gen1/data/moves.zig +2 -4
  57. package/src/lib/gen1/data/species.zig +1 -3
  58. package/src/lib/gen1/data/types.zig +3 -4
  59. package/src/lib/gen1/data.zig +12 -16
  60. package/src/lib/gen1/helpers.zig +13 -28
  61. package/src/lib/gen1/mechanics.zig +131 -101
  62. package/src/lib/gen2/calc.zig +12 -23
  63. package/src/lib/gen2/chance.zig +12 -18
  64. package/src/lib/gen2/data/items.zig +1 -3
  65. package/src/lib/gen2/data/moves.zig +2 -4
  66. package/src/lib/gen2/data/species.zig +1 -3
  67. package/src/lib/gen2/data/types.zig +7 -5
  68. package/src/lib/gen2/data.zig +19 -22
  69. package/src/lib/gen2/generated.zig +18 -23
  70. package/src/lib/gen2/helpers.zig +9 -18
  71. package/src/lib/gen2/mechanics.zig +50 -47
  72. package/src/lib/napi.h +2 -0
  73. package/src/lib/node.zig +4 -121
  74. package/src/lib/pkmn.zig +98 -19
  75. package/src/lib/wasm.zig +9 -9
  76. package/src/tools/display/dom.ts +93 -0
  77. package/src/tools/display/ui.css +354 -0
  78. package/build/test/display.js +0 -405
  79. package/build/test/display.js.map +0 -1
  80. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,48 +1,40 @@
1
- const std = @import("std");
2
-
3
- const pkmn = @import("../pkmn.zig");
1
+ const DEBUG = @import("../common/debug.zig").print;
4
2
 
3
+ const chance = @import("chance.zig");
5
4
  const common = @import("../common/data.zig");
6
- const DEBUG = @import("../common/debug.zig").print;
5
+ const data = @import("data.zig");
6
+ const helpers = @import("helpers.zig");
7
7
  const optional = @import("../common/optional.zig");
8
+ const pkmn = @import("../pkmn.zig");
8
9
  const protocol = @import("../common/protocol.zig");
9
10
  const rational = @import("../common/rational.zig");
11
+ const std = @import("std");
10
12
  const util = @import("../common/util.zig");
11
13
 
12
- const chance = @import("chance.zig");
13
- const data = @import("data.zig");
14
- const helpers = @import("helpers.zig");
15
-
14
+ const Action = chance.Action;
15
+ const Actions = chance.Actions;
16
16
  const assert = std.debug.assert;
17
-
17
+ const Chance = chance.Chance;
18
+ const Choice = common.Choice;
19
+ const Confusion = chance.Confusion;
20
+ const Duration = chance.Duration;
21
+ const Durations = chance.Durations;
22
+ const enabled = pkmn.options.calc;
18
23
  const expect = std.testing.expect;
19
24
  const expectEqual = std.testing.expectEqual;
20
25
  const expectEqualSlices = std.testing.expectEqualSlices;
21
-
22
- const enabled = pkmn.options.calc;
23
-
24
- const Player = common.Player;
25
- const Result = common.Result;
26
- const Choice = common.Choice;
27
-
26
+ const isPointerTo = util.isPointerTo;
27
+ const Observation = chance.Observation;
28
28
  const Optional = optional.Optional;
29
-
30
- const Rational = rational.Rational;
31
-
29
+ const Player = common.Player;
32
30
  const PointerType = util.PointerType;
33
- const isPointerTo = util.isPointerTo;
34
-
35
- const Actions = chance.Actions;
36
- const Action = chance.Action;
37
- const Chance = chance.Chance;
38
- const Durations = chance.Durations;
31
+ const Rational = rational.Rational;
32
+ const Result = common.Result;
33
+ const Sleeps = chance.Sleeps;
39
34
 
40
35
  const tty = true; // DEBUG
41
36
  const summary = false; // DEBUG
42
37
 
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
-
46
38
  /// Information relevant to damage calculation that occured during a Generation I battle `update`.
47
39
  pub const Summaries = extern struct {
48
40
  /// Relevant information for Player 1.
@@ -104,17 +96,25 @@ pub const Calc = struct {
104
96
  self: Calc,
105
97
  player: Player,
106
98
  comptime field: Action.Field,
107
- ) ?std.meta.FieldType(Action, field) {
99
+ ) ?@FieldType(Action, @tagName(field)) {
108
100
  if (!enabled) return null;
109
101
 
110
102
  const val = @field(self.overrides.get(player), @tagName(field));
111
103
  return if (switch (@typeInfo(@TypeOf(val))) {
112
- Enum => val != .None,
113
- Int => val != 0,
104
+ .@"enum" => val != .None,
105
+ .int => val != 0,
114
106
  else => unreachable,
115
107
  }) val else null;
116
108
  }
117
109
 
110
+ pub fn confusion(self: *Calc, player: Player) void {
111
+ if (!enabled) return;
112
+
113
+ if (self.overrides.get(player).confusion == .started) {
114
+ self.overrides.get(player).confusion = .None;
115
+ }
116
+ }
117
+
118
118
  pub fn base(self: *Calc, player: Player, val: u16) void {
119
119
  if (!enabled) return;
120
120
 
@@ -143,11 +143,15 @@ const Null = struct {
143
143
  self: Null,
144
144
  player: Player,
145
145
  comptime field: Action.Field,
146
- ) ?std.meta.FieldType(Action, field) {
146
+ ) ?@FieldType(Action, @tagName(field)) {
147
147
  _ = .{ self, player };
148
148
  return null;
149
149
  }
150
150
 
151
+ pub fn confusion(self: Null, player: Player) void {
152
+ _ = .{ self, player };
153
+ }
154
+
151
155
  pub fn base(self: Null, player: Player, val: u16) void {
152
156
  _ = .{ self, player, val };
153
157
  }
@@ -175,14 +179,15 @@ pub const Options = struct {
175
179
  metronome: bool = false,
176
180
  };
177
181
 
178
- pub const MAX_FRONTIER = 19;
182
+ pub const MAX_FRONTIER = 83;
179
183
 
180
184
  pub fn transitions(
181
185
  battle: anytype,
182
186
  c1: Choice,
183
187
  c2: Choice,
184
188
  allocator: std.mem.Allocator,
185
- writer: anytype,
189
+ out: *std.Io.Writer,
190
+ err: *std.Io.Writer,
186
191
  options: Options,
187
192
  ) !?Stats {
188
193
  var stats: Stats = .{};
@@ -191,12 +196,14 @@ pub fn transitions(
191
196
 
192
197
  var seen = std.AutoHashMap(Actions, void).init(allocator);
193
198
  defer seen.deinit();
194
- var frontier = std.ArrayList(Actions).init(allocator);
195
- defer frontier.deinit();
199
+ var frontier: std.array_list.Aligned(Actions, null) = .empty;
200
+ defer frontier.deinit(allocator);
201
+
202
+ const d = options.durations;
196
203
 
197
204
  var opts = pkmn.battle.options(
198
205
  protocol.NULL,
199
- Chance(Rational(u128)){ .probability = .{}, .durations = options.durations },
206
+ Chance(Rational(u128)){ .probability = .{}, .durations = d },
200
207
  Calc{},
201
208
  );
202
209
 
@@ -207,8 +214,8 @@ pub fn transitions(
207
214
  const p1 = b.side(.P1);
208
215
  const p2 = b.side(.P2);
209
216
 
210
- var p: Rational(u128) = .{ .p = 0, .q = 1 };
211
- try frontier.append(opts.chance.actions);
217
+ var p: Rational(u256) = .{ .p = 0, .q = 1 };
218
+ try frontier.append(allocator, opts.chance.actions);
212
219
 
213
220
  // zig fmt: off
214
221
  for (Rolls.metronome(frontier.items[0].p1)) |p1_move| {
@@ -220,8 +227,11 @@ pub fn transitions(
220
227
  assert(frontier.items.len == 1);
221
228
  while (i < frontier.items.len) : (i += 1) {
222
229
  const f = frontier.items[i];
230
+ const saved = stats.saved;
231
+
232
+ var r: Rational(u256) = .{ .p = 0, .q = 1 };
223
233
 
224
- try debug(writer, f, .{
234
+ try debug(out, f, .{
225
235
  .shape = true,
226
236
  .color = i,
227
237
  .bold = true,
@@ -235,10 +245,20 @@ pub fn transitions(
235
245
  };
236
246
 
237
247
  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;
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, tie, p1_atk, p1_slp)) |p1_cfz| { a.p1.confusion = p1_cfz;
255
+ for (Rolls.confusion(f.p2, d.p2, tie, 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;
240
258
  for (Rolls.paralyzed(f.p1, p1_cfzd)) |p1_par| { a.p1.paralyzed = p1_par;
241
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;
242
262
  for (Rolls.hit(f.p1, p1_par)) |p1_hit| { a.p1.hit = p1_hit;
243
263
  for (Rolls.hit(f.p2, p2_par)) |p2_hit| { a.p2.hit = p2_hit;
244
264
  for (Rolls.psywave(f.p1, p1, p1_hit)) |p1_psywave| { a.p1.psywave = p1_psywave;
@@ -258,15 +278,15 @@ pub fn transitions(
258
278
 
259
279
  var p2_dmg = Rolls.damage(f.p2, p2_hit);
260
280
 
261
- const p1_min: u9 = p1_dmg.min;
262
- const p2_min: u9 = p2_dmg.min;
281
+ const p1_min: u9 = @intCast(p1_dmg.min);
282
+ const p2_min: u9 = @intCast(p2_dmg.min);
263
283
 
264
284
  while (p2_dmg.min < p2_dmg.max) : (p2_dmg.min += 1) {
265
285
  a.p2.damage = @intCast(p2_dmg.min);
266
286
 
267
287
  opts.calc.overrides = a;
268
288
  opts.calc.summaries = .{};
269
- opts.chance = .{ .probability = .{}, .durations = options.durations };
289
+ opts.chance = .{ .probability = .{}, .durations = d };
270
290
  const q = &opts.chance.probability;
271
291
 
272
292
  b = battle;
@@ -276,16 +296,16 @@ pub fn transitions(
276
296
 
277
297
  const summaries = &opts.calc.summaries;
278
298
  const p1_max: u9 = if (p2_dmg.min != p2_min)
279
- p1_dmg.min
299
+ @intCast(p1_dmg.min)
280
300
  else
281
- try Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
301
+ Rolls.coalesce(.P1, @as(u8, @intCast(p1_dmg.min)), summaries, cap);
282
302
  const p2_max: u9 =
283
- try Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
303
+ Rolls.coalesce(.P2, @as(u8, @intCast(p2_dmg.min)), summaries, cap);
284
304
 
285
305
  if (opts.chance.actions.matches(f)) {
286
- if (!opts.chance.actions.eql(a)) {
306
+ if (!opts.chance.actions.relax().eql(a)) {
287
307
  if (!summary) {
288
- try debug(writer, opts.chance.actions, .{
308
+ try debug(out, opts.chance.actions, .{
289
309
  .p1_max = p1_max,
290
310
  .p2_max = p2_max,
291
311
  .color = i,
@@ -299,7 +319,7 @@ pub fn transitions(
299
319
  }
300
320
 
301
321
  if (!summary) {
302
- try debug(writer, opts.chance.actions, .{
322
+ try debug(out, opts.chance.actions, .{
303
323
  .p1_max = p1_max,
304
324
  .p2_max = p2_max,
305
325
  .color = i,
@@ -312,33 +332,38 @@ pub fn transitions(
312
332
  acts.p1.damage = @intCast(p1d);
313
333
  acts.p2.damage = @intCast(p2d);
314
334
  if ((try seen.getOrPut(acts)).found_existing) {
315
- err("already seen {}", .{acts}, options.seed);
335
+ die(err, "already seen {f}", .{acts}, options.seed);
316
336
  return error.TestUnexpectedResult;
317
337
  }
318
338
  }
319
339
  }
320
-
321
340
  if (p1_max != p1_min) try q.update(p1_max - p1_min + 1, 1);
322
341
  if (p2_max != p2_dmg.min) try q.update(p2_max - p2_dmg.min + 1, 1);
342
+
343
+ q.reduce();
323
344
  try p.add(q);
345
+ p.reduce();
346
+ try r.add(q);
347
+ r.reduce();
348
+
324
349
  stats.saved += 1;
325
350
 
326
351
  if (p.q < p.p) {
327
- err("improper fraction {}", .{p}, options.seed);
352
+ die(err, "improper fraction {f}", .{p}, options.seed);
328
353
  return error.TestUnexpectedResult;
329
354
  }
330
355
  } else {
331
- if (!matches(opts.chance.actions, i, frontier.items)) {
332
- try frontier.append(opts.chance.actions);
356
+ if (!opts.chance.actions.matchesAny(frontier.items, i)) {
357
+ try frontier.append(allocator, opts.chance.actions);
333
358
 
334
- try debug(writer, opts.chance.actions, .{
359
+ try debug(out, opts.chance.actions, .{
335
360
  .p1_max = p1_max,
336
361
  .p2_max = p2_max,
337
362
  .dim = true,
338
363
  .newline = false,
339
364
  });
340
- try writer.writeAll(" → ");
341
- try debug(writer, opts.chance.actions, .{
365
+ try out.writeAll(" → ");
366
+ try debug(out, opts.chance.actions, .{
342
367
  .shape = true,
343
368
  .color = frontier.items.len - 1,
344
369
  .dim = true,
@@ -346,7 +371,7 @@ pub fn transitions(
346
371
  .indent = false,
347
372
  });
348
373
  } else if (!summary) {
349
- try debug(writer, opts.chance.actions, .{
374
+ try debug(out, opts.chance.actions, .{
350
375
  .p1_max = p1_max,
351
376
  .p2_max = p2_max,
352
377
  .dim = true,
@@ -358,15 +383,15 @@ pub fn transitions(
358
383
  p2_dmg.min = p2_max;
359
384
  }
360
385
 
361
- }}}}}}}}}}}}}}}}}}
386
+ }}}}}}}}}}}}}}}}}}}}}}}}}}}}
362
387
 
363
- if (@TypeOf(writer) != @TypeOf(std.io.null_writer)) {
364
- p.reduce();
365
- try writer.print(
366
- " = {s} ({d:.2}%)\n\n",
367
- .{p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
368
- );
369
- }
388
+ assert(stats.saved > saved);
389
+ // TODO: LLVM - float_from_int from 'u256' without intrinsics
390
+ // try out.print(
391
+ // " {f} ({d:.2}%)\n = ──────────\n {f} ({d:.2}%)\n\n",
392
+ // .{r, 100 * @as(f128, @floatFromInt(r.p)) / @as(f128, @floatFromInt(r.q)),
393
+ // p, 100 * @as(f128, @floatFromInt(p.p)) / @as(f128, @floatFromInt(p.q))},
394
+ // );
370
395
 
371
396
  }
372
397
 
@@ -381,7 +406,7 @@ pub fn transitions(
381
406
 
382
407
  p.reduce();
383
408
  if (p.p != 1 or p.q != 1) {
384
- err("expected 1, found {}", .{p}, options.seed);
409
+ die(err, "expected 1, found {f}", .{p}, options.seed);
385
410
  return error.TestExpectedEqual;
386
411
  }
387
412
 
@@ -394,7 +419,8 @@ pub fn update(
394
419
  c2: Choice,
395
420
  options: anytype,
396
421
  allocator: std.mem.Allocator,
397
- writer: anytype,
422
+ out: *std.Io.Writer,
423
+ err: *std.Io.Writer,
398
424
  transition: bool,
399
425
  ) !Result {
400
426
  const durations = options.chance.durations;
@@ -412,8 +438,8 @@ pub fn update(
412
438
  if (@TypeOf(battle.rng) == data.PRNG) {
413
439
  // Ensure we can encode the diffs in less than MAX_DIFFS bytes.
414
440
  var buf: [helpers.MAX_DIFFS]u8 = undefined;
415
- var stream: protocol.ByteStream = .{ .buffer = &buf };
416
- const n = try helpers.diff(&original, battle, stream.writer());
441
+ var pw: protocol.Writer = .{ .buffer = &buf };
442
+ const n = try helpers.diff(&original, battle, &pw);
417
443
 
418
444
  // Applying the diff to the battle should take us back to the original copy
419
445
  // of the battle (ignoring the RNG).
@@ -427,7 +453,7 @@ pub fn update(
427
453
  // Ensure we can generate all transitions from the same original state
428
454
  // (we must change the battle's RNG from a FixedRNG to a PRNG because
429
455
  // the transitions function relies on RNG for discovery of states)
430
- if (try transitions(unfix(original), c1, c2, allocator, writer, .{
456
+ if (try transitions(unfix(original), c1, c2, allocator, out, err, .{
431
457
  .durations = durations,
432
458
  .cap = true,
433
459
  })) |stats| try expect(stats.frontier <= MAX_FRONTIER);
@@ -474,10 +500,9 @@ pub fn update(
474
500
  fn expectEqualActions(expected: Actions, actual: Actions) !void {
475
501
  return expectEqual(expected, actual) catch |e| switch (e) {
476
502
  error.TestExpectedEqual => {
477
- std.debug.print("expected {}, found {}\n", .{ expected, actual });
503
+ std.debug.print("expected {f}, found {f}\n", .{ expected, actual });
478
504
  return e;
479
505
  },
480
- else => return e,
481
506
  };
482
507
  }
483
508
 
@@ -496,19 +521,9 @@ fn unfix(actual: anytype) data.Battle(data.PRNG) {
496
521
  };
497
522
  }
498
523
 
499
- fn matches(actions: Actions, i: usize, frontier: []Actions) bool {
500
- for (frontier, 0..) |f, j| {
501
- // TODO: is skipping this redundant check worth it?
502
- if (i == j) continue;
503
- if (f.matches(actions)) return true;
504
- }
505
- return false;
506
- }
507
-
508
- fn err(comptime fmt: []const u8, v: anytype, seed: ?u64) void {
509
- const w = std.io.getStdErr().writer();
524
+ fn die(w: *std.Io.Writer, comptime fmt: []const u8, v: anytype, seed: ?u64) void {
510
525
  w.print(fmt, v) catch return;
511
- if (seed) |s| return w.print("{}\n", .{s}) catch return;
526
+ if (seed) |s| return w.print("{d}\n", .{s}) catch return;
512
527
  return w.writeByte('\n') catch return;
513
528
  }
514
529
 
@@ -524,7 +539,7 @@ const Style = struct {
524
539
  indent: bool = true,
525
540
  };
526
541
 
527
- fn debug(writer: anytype, actions: Actions, style: Style) !void {
542
+ fn debug(writer: *std.Io.Writer, actions: Actions, style: Style) !void {
528
543
  if (style.indent) try writer.writeAll(" ");
529
544
  if (tty) {
530
545
  const mod: usize = if (style.dim) 2 else 1;
@@ -549,13 +564,13 @@ fn debug(writer: anytype, actions: Actions, style: Style) !void {
549
564
  if (style.newline) try writer.writeByte('\n');
550
565
  }
551
566
 
552
- fn format(writer: anytype, actions: Actions, p1: u9, p2: u9) !void {
567
+ fn format(writer: *std.Io.Writer, actions: Actions, p1: u9, p2: u9) !void {
553
568
  var input: [1024]u8 = undefined;
554
569
  var output: [1024]u8 = undefined;
555
570
 
556
- var stream = std.io.fixedBufferStream(&input);
557
- try actions.fmt(&stream.writer(), false);
558
- var len = stream.getWritten().len;
571
+ var w = std.Io.Writer.fixed(&input);
572
+ try actions.fmt(&w, false);
573
+ var len = w.buffered().len;
559
574
  @memcpy(output[0..len], input[0..len]);
560
575
 
561
576
  var find: [16]u8 = undefined;
@@ -588,6 +603,112 @@ pub const Rolls = struct {
588
603
  return if (action.speed_tie == .None) &PLAYER_NONE else &PLAYERS;
589
604
  }
590
605
 
606
+ const OBS_NONE = [_]Optional(Observation){.None};
607
+ const OBS_STARTED = [_]Optional(Observation){.started};
608
+ const OBS_CONTINUING = [_]Optional(Observation){.continuing};
609
+ const OBS_ENDED = [_]Optional(Observation){.ended};
610
+ const OBS_ALL = [_]Optional(Observation){ .started, .continuing, .ended };
611
+ const OBS = [_]Optional(Observation){ .continuing, .ended };
612
+
613
+ /// Returns a slice with a range of values for sleep given the `action` state and observed
614
+ /// `durations`.
615
+ pub fn sleep(action: Action, duration: Duration) []const Optional(Observation) {
616
+ if (action.sleep == .None) return &OBS_NONE;
617
+ return if (Sleeps.get(duration.sleeps, 0) == 0 and action.speed_tie != .None)
618
+ &OBS_ALL
619
+ else switch (action.sleep) {
620
+ .started => &OBS_STARTED,
621
+ else => if (Sleeps.get(duration.sleeps, 0) >= 7) &OBS_ENDED else &OBS,
622
+ };
623
+ }
624
+
625
+ /// Returns a slice with a range of values for disable given the `action`, observed `durations`,
626
+ /// and the state of the `parent` (observation of the player's Pokémon sleep status).
627
+ pub fn disable(
628
+ action: Action,
629
+ duration: Duration,
630
+ parent: Optional(Observation),
631
+ ) []const Optional(Observation) {
632
+ if (action.disable == .None) return &OBS_NONE;
633
+ return if (duration.disable == 0 and action.speed_tie != .None)
634
+ &OBS_ALL
635
+ else switch (action.disable) {
636
+ .started => &OBS_STARTED,
637
+ else => if (duration.disable >= 8)
638
+ &OBS_ENDED
639
+ else if (parent != .None and parent != .started)
640
+ &OBS_CONTINUING
641
+ else
642
+ &OBS,
643
+ };
644
+ }
645
+
646
+ /// FIXME document
647
+ pub fn attacking(
648
+ action: Action,
649
+ duration: Duration,
650
+ parent: Optional(Observation),
651
+ ) []const Optional(Observation) {
652
+ return switch (action.attacking) {
653
+ .None => &OBS_NONE,
654
+ .started => &OBS_STARTED,
655
+ else => if (duration.attacking >= 3)
656
+ &OBS_ENDED
657
+ else if ((parent == .continuing and parent == .ended) or duration.attacking < 2)
658
+ &OBS_CONTINUING
659
+ else
660
+ &OBS,
661
+ };
662
+ }
663
+
664
+ const CFZ_NONE = [_]Optional(Confusion){.None};
665
+ const CFZ_STARTED = [_]Optional(Confusion){.started};
666
+ const CFZ_OVERWRITTEN = [_]Optional(Confusion){ .started, .overwritten };
667
+ const CFZ_CONTINUING = [_]Optional(Confusion){.continuing};
668
+ const CFZ_ENDED = [_]Optional(Confusion){.ended};
669
+ const CFZ = [_]Optional(Confusion){ .continuing, .ended };
670
+ const CFZ_TIE = [_]Optional(Confusion){ .started, .continuing, .ended };
671
+
672
+ /// FIXME document
673
+ pub fn confusion(
674
+ action: Action,
675
+ duration: Duration,
676
+ tie: Optional(Player),
677
+ sibling: Optional(Observation),
678
+ parent: Optional(Observation),
679
+ ) []const Optional(Confusion) {
680
+ return switch (action.confusion) {
681
+ .None => &CFZ_NONE,
682
+ .started => if (pkmn.options.overwrite and sibling == .ended and duration.confusion < 5)
683
+ &CFZ_OVERWRITTEN
684
+ else
685
+ &CFZ_STARTED,
686
+ else => if (duration.confusion >= 5)
687
+ &CFZ_ENDED
688
+ else if ((parent == .continuing and parent == .ended) or duration.confusion < 2)
689
+ &CFZ_CONTINUING
690
+ else if (tie != .None)
691
+ &CFZ_TIE
692
+ else
693
+ &CFZ,
694
+ };
695
+ }
696
+
697
+ /// Returns a slice with a range of values for binding given the `action`, observed `durations`,
698
+ /// and the state of the `parent` (whether the player's Pokémon was fully paralyzed).
699
+ pub fn binding(
700
+ action: Action,
701
+ duration: Duration,
702
+ parent: Optional(bool),
703
+ ) []const Optional(Observation) {
704
+ if (parent == .true) return &OBS_NONE;
705
+ return switch (action.binding) {
706
+ .None => &OBS_NONE,
707
+ .started => &OBS_STARTED,
708
+ else => if (duration.binding >= 4) &OBS_ENDED else &OBS,
709
+ };
710
+ }
711
+
591
712
  const BOOL_NONE = [_]Optional(bool){.None};
592
713
  const BOOLS = [_]Optional(bool){ .false, .true };
593
714
 
@@ -601,7 +722,7 @@ pub const Rolls = struct {
601
722
  /// Returns a slice with the correct range of values for critical hits given the `action` state
602
723
  /// and the state of the `parent` (whether the player's Pokémon's move hit).
603
724
  pub fn criticalHit(action: Action, parent: Optional(bool)) []const Optional(bool) {
604
- if (parent == .false) return &BOOL_NONE;
725
+ if (pkmn.options.showdown and parent == .false) return &BOOL_NONE;
605
726
  return if (action.critical_hit == .None) &BOOL_NONE else &BOOLS;
606
727
  }
607
728
 
@@ -613,7 +734,7 @@ pub const Rolls = struct {
613
734
  }
614
735
 
615
736
  /// The min and max bounds on iteration over damage rolls.
616
- pub const Range = struct { min: u9, max: u9 };
737
+ pub const Range = struct { min: usize, max: usize };
617
738
 
618
739
  /// Returns the range bounding damage rolls given the `action` state and the state of
619
740
  /// the `parent` (whether the player's Pokémon's move hit).
@@ -626,7 +747,7 @@ pub const Rolls = struct {
626
747
 
627
748
  /// Returns the max damage roll which will produce the same damage as `roll`
628
749
  /// given the base damage in `summaries`.
629
- pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) !u8 {
750
+ pub fn coalesce(player: Player, roll: u8, summaries: *Summaries, cap: bool) u8 {
630
751
  if (roll == 0) return roll;
631
752
 
632
753
  const dmg = summaries.get(player.foe()).damage;
@@ -636,9 +757,11 @@ pub const Rolls = struct {
636
757
  return @min(255, roll + ((254 - ((@as(u32, dmg.base) * roll) % 255)) / dmg.base));
637
758
  }
638
759
 
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;
760
+ /// Returns a slice with the correct range of values for confused given the `action` state and
761
+ /// the state of the `parent` (observation of the player's Pokémon confusion status).
762
+ pub fn confused(action: Action, parent: Optional(Confusion)) []const Optional(bool) {
763
+ const done = parent == .None or parent == .ended;
764
+ return if (done or action.confused == .None) &BOOL_NONE else &BOOLS;
642
765
  }
643
766
 
644
767
  /// Returns a slice with the correct range of values for paralysis given the `action` state
@@ -705,6 +828,26 @@ test "Rolls.speedTie" {
705
828
  try expectEqualSlices(Optional(Player), &.{.None}, Rolls.speedTie(actions.p2));
706
829
  }
707
830
 
831
+ test "Rolls.sleep" {
832
+ return error.SkipZigTest; // TODO
833
+ }
834
+
835
+ test "Rolls.disable" {
836
+ return error.SkipZigTest; // TODO
837
+ }
838
+
839
+ test "Rolls.confusion" {
840
+ return error.SkipZigTest; // TODO
841
+ }
842
+
843
+ test "Rolls.atacking" {
844
+ return error.SkipZigTest; // TODO
845
+ }
846
+
847
+ test "Rolls.binding" {
848
+ return error.SkipZigTest; // TODO
849
+ }
850
+
708
851
  test "Rolls.damage" {
709
852
  const actions: Actions = .{ .p2 = .{ .damage = 221 } };
710
853
  try expectEqual(Rolls.Range{ .min = 0, .max = 1 }, Rolls.damage(actions.p1, .None));
@@ -714,12 +857,12 @@ test "Rolls.damage" {
714
857
 
715
858
  test "Rolls.coalesce" {
716
859
  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));
860
+ try expectEqual(@as(u8, 0), Rolls.coalesce(.P2, 0, &summaries, false));
861
+ try expectEqual(@as(u8, 241), Rolls.coalesce(.P2, 238, &summaries, false));
862
+ try expectEqual(@as(u8, 255), Rolls.coalesce(.P2, 238, &summaries, true));
720
863
  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));
864
+ try expectEqual(@as(u8, 217), Rolls.coalesce(.P2, 217, &summaries, false));
865
+ try expectEqual(@as(u8, 255), Rolls.coalesce(.P2, 217, &summaries, true));
723
866
  }
724
867
 
725
868
  test "Rolls.hit" {
@@ -748,14 +891,23 @@ test "Rolls.criticalHit" {
748
891
  &.{ .false, .true },
749
892
  Rolls.criticalHit(actions.p1, .None),
750
893
  );
751
- try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p1, .false));
894
+ try expectEqualSlices(
895
+ Optional(bool),
896
+ if (pkmn.options.showdown) &.{.None} else &.{ .false, .true },
897
+ Rolls.criticalHit(actions.p1, .false),
898
+ );
752
899
  try expectEqualSlices(Optional(bool), &.{.None}, Rolls.criticalHit(actions.p2, .None));
753
900
  }
754
901
 
755
902
  test "Rolls.confused" {
756
903
  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));
904
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p1, .None));
905
+ try expectEqualSlices(
906
+ Optional(bool),
907
+ &.{ .false, .true },
908
+ Rolls.confused(actions.p2, .continuing),
909
+ );
910
+ try expectEqualSlices(Optional(bool), &.{.None}, Rolls.confused(actions.p2, .ended));
759
911
  }
760
912
 
761
913
  test "Rolls.paralyzed" {