@pkmn/engine 0.1.0-dev.b01cb48c → 0.1.0-dev.b14f2221

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 (81) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +21 -11
  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 +40 -90
  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 +255 -102
  39. package/package.json +59 -138
  40. package/src/bin/install-pkmn-engine +25 -7
  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 +70 -0
  46. package/src/lib/common/data.zig +8 -8
  47. package/src/lib/common/debug.zig +30 -9
  48. package/src/lib/common/js.zig +3 -7
  49. package/src/lib/common/optional.zig +11 -17
  50. package/src/lib/common/options.zig +4 -0
  51. package/src/lib/common/protocol.zig +195 -159
  52. package/src/lib/common/rational.zig +303 -310
  53. package/src/lib/common/rng.zig +5 -7
  54. package/src/lib/common/util.zig +33 -1
  55. package/src/lib/gen1/calc.zig +324 -180
  56. package/src/lib/gen1/chance.zig +536 -140
  57. package/src/lib/gen1/data/moves.zig +2 -4
  58. package/src/lib/gen1/data/species.zig +1 -3
  59. package/src/lib/gen1/data/types.zig +3 -4
  60. package/src/lib/gen1/data.zig +40 -43
  61. package/src/lib/gen1/helpers.zig +17 -29
  62. package/src/lib/gen1/mechanics.zig +240 -187
  63. package/src/lib/gen2/calc.zig +35 -28
  64. package/src/lib/gen2/chance.zig +88 -74
  65. package/src/lib/gen2/data/items.zig +1 -3
  66. package/src/lib/gen2/data/moves.zig +2 -4
  67. package/src/lib/gen2/data/species.zig +1 -3
  68. package/src/lib/gen2/data/types.zig +7 -5
  69. package/src/lib/gen2/data.zig +33 -35
  70. package/src/lib/gen2/generated.zig +18 -23
  71. package/src/lib/gen2/helpers.zig +9 -20
  72. package/src/lib/gen2/mechanics.zig +98 -98
  73. package/src/lib/napi.h +2 -0
  74. package/src/lib/node.zig +4 -121
  75. package/src/lib/pkmn.zig +103 -22
  76. package/src/lib/wasm.zig +9 -9
  77. package/src/tools/display/dom.ts +93 -0
  78. package/src/tools/display/ui.css +354 -0
  79. package/build/test/display.js +0 -405
  80. package/build/test/display.js.map +0 -1
  81. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,26 +1,27 @@
1
- const std = @import("std");
2
-
3
- const assert = std.debug.assert;
4
- const print = std.debug.print;
5
-
6
- const expect = std.testing.expect;
7
- const expectEqual = std.testing.expectEqual;
8
-
9
1
  const DEBUG = @import("../common/debug.zig").print;
2
+
3
+ const array = @import("../common/array.zig");
4
+ const common = @import("../common/data.zig");
5
+ const data = @import("data.zig");
6
+ const optional = @import("../common/optional.zig");
10
7
  const options = @import("../common/options.zig");
11
8
  const rational = @import("../common/rational.zig");
9
+ const std = @import("std");
12
10
  const util = @import("../common/util.zig");
13
11
 
14
- const Player = @import("../common/data.zig").Player;
15
- const Optional = @import("../common/optional.zig").Optional;
16
-
17
- const data = @import("data.zig");
18
-
12
+ const Array = array.Array;
13
+ const assert = std.debug.assert;
19
14
  const enabled = options.chance;
20
- const showdown = options.showdown;
21
-
15
+ const expect = std.testing.expect;
16
+ const expectEqual = std.testing.expectEqual;
17
+ const isPointerTo = util.isPointerTo;
22
18
  const Move = data.Move;
23
19
  const MoveSlot = data.MoveSlot;
20
+ const Optional = optional.Optional;
21
+ const Player = common.Player;
22
+ const PointerType = util.PointerType;
23
+ const print = std.debug.print;
24
+ const showdown = options.showdown;
24
25
 
25
26
  /// Actions taken by a hypothetical "chance player" that convey information about which RNG events
26
27
  /// were observed during a Generation I battle `update`. This can additionally be provided as input
@@ -37,8 +38,8 @@ pub const Actions = extern struct {
37
38
  }
38
39
 
39
40
  /// Returns the `Action` for the given `player`.
40
- pub fn get(self: anytype, player: Player) util.PointerType(@TypeOf(self), Action) {
41
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Actions);
41
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Action) {
42
+ assert(isPointerTo(self, Actions));
42
43
  return if (player == .P1) &self.p1 else &self.p2;
43
44
  }
44
45
 
@@ -51,15 +52,15 @@ pub const Actions = extern struct {
51
52
  /// same shape if they have the same fields set (though those fields need not necessarily be
52
53
  /// set to the same value).
53
54
  pub fn matches(a: Actions, b: Actions) bool {
54
- inline for (@typeInfo(Actions).Struct.fields) |player| {
55
- inline for (@typeInfo(Action).Struct.fields) |field| {
55
+ inline for (@typeInfo(Actions).@"struct".fields) |player| {
56
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
56
57
  const a_val = @field(@field(a, player.name), field.name);
57
58
  const b_val = @field(@field(b, player.name), field.name);
58
59
 
59
60
  switch (@typeInfo(@TypeOf(a_val))) {
60
- .Enum => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
61
+ .@"enum" => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
61
62
  return false,
62
- .Int => if ((a_val > 0) != (b_val > 0)) return false,
63
+ .int => if ((a_val > 0) != (b_val > 0)) return false,
63
64
  else => unreachable,
64
65
  }
65
66
  }
@@ -67,7 +68,26 @@ pub const Actions = extern struct {
67
68
  return true;
68
69
  }
69
70
 
70
- pub fn fmt(self: Actions, writer: anytype, shape: bool) !void {
71
+ /// TODO
72
+ pub fn matchesAny(a: Actions, bs: []Actions, i: usize) bool {
73
+ for (bs, 0..) |b, j| {
74
+ // TODO: is skipping this redundant check worth it?
75
+ if (i == j) continue;
76
+ if (b.matches(a)) return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ /// TODO
82
+ pub fn relax(actions: Actions) Actions {
83
+ if (!options.overwrite) return actions;
84
+ var a = actions;
85
+ if (a.p1.confusion == .overwritten) a.p1.confusion = .continuing;
86
+ if (a.p2.confusion == .overwritten) a.p2.confusion = .continuing;
87
+ return a;
88
+ }
89
+
90
+ pub fn fmt(self: Actions, writer: *std.Io.Writer, shape: bool) !void {
71
91
  try writer.writeAll("<P1 = ");
72
92
  try self.p1.fmt(writer, shape);
73
93
  try writer.writeAll(", P2 = ");
@@ -75,8 +95,7 @@ pub const Actions = extern struct {
75
95
  try writer.writeAll(">");
76
96
  }
77
97
 
78
- pub fn format(a: Actions, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
79
- _ = .{ f, o };
98
+ pub fn format(a: Actions, w: *std.Io.Writer) !void {
80
99
  try fmt(a, w, false);
81
100
  }
82
101
  };
@@ -99,8 +118,42 @@ test Actions {
99
118
  try expect(!c.matches(a));
100
119
  }
101
120
 
102
- pub const Observation = enum { started, continuing, ended };
103
- pub const Confusion = enum { started, continuing, ended, overwritten };
121
+ /// Observation made about a duration - whether the duration has started, been continued, or ended.
122
+ pub fn Observation(comptime field: Duration.Field) type {
123
+ return switch (field) {
124
+ .attacking, .binding => enum(u2) {
125
+ started,
126
+ continuing,
127
+ ended,
128
+ },
129
+ .sleep, .disable => enum(u3) {
130
+ started,
131
+ continuing,
132
+ ended,
133
+ // TODO haze support
134
+ unused1,
135
+ unused2,
136
+ unused3,
137
+ unused4,
138
+ },
139
+ .confusion => enum(u4) {
140
+ started,
141
+ continuing,
142
+ ended,
143
+ /// An indicator that an existing confusion duration has been overwritten by a new
144
+ /// confusion duration due to a Thrashing move ending ("hidden" confusion). This is not
145
+ /// immediately observable to an opponent, to compute probabilities from an opponent's
146
+ /// point of view based solely on public information extra work must be done to correct
147
+ /// for the information leak
148
+ overwritten,
149
+ // TODO haze support
150
+ unused1,
151
+ unused2,
152
+ unused3,
153
+ unused4,
154
+ },
155
+ };
156
+ }
104
157
 
105
158
  /// Information about the RNG that was observed during a Generation I battle `update` for a
106
159
  /// single player.
@@ -126,21 +179,25 @@ pub const Action = packed struct(u64) {
126
179
  /// or Rolls.{sleepDuration,disableDuration,confusionDuration,attackingDuration} otherwise.
127
180
  duration: u4 = 0,
128
181
 
129
- // TODO
130
- sleep: Optional(Observation) = .None,
131
- confusion: Optional(Confusion) = .None,
132
- disable: Optional(Observation) = .None,
133
- bide: Optional(Observation) = .None,
134
- binding: Optional(Observation) = .None,
135
- thrashing: Optional(Observation) = .None,
136
-
137
- _: u3 = 0, // TODO
182
+ /// TODO
183
+ sleep: Optional(Observation(.sleep)) = .None,
184
+ /// TODO
185
+ confusion: Optional(Observation(.confusion)) = .None,
186
+ /// TODO
187
+ disable: Optional(Observation(.disable)) = .None,
188
+ /// TODO
189
+ attacking: Optional(Observation(.attacking)) = .None,
190
+ /// TODO
191
+ binding: Optional(Observation(.binding)) = .None,
138
192
 
139
193
  /// If not 0, the move slot (1-4) to return in Rolls.moveSlot. If present as an override,
140
194
  /// invalid values (eg. due to empty move slots or 0 PP) will be ignored.
141
- move_slot: u4 = 0,
195
+ move_slot: u3 = 0,
142
196
  /// If not 0, the value (2-5) to return for Rolls.distribution for multi hit.
143
- multi_hit: u4 = 0,
197
+ multi_hit: u3 = 0,
198
+
199
+ /// TODO
200
+ pp: u4 = 0,
144
201
 
145
202
  /// If not 0, psywave - 1 should be returned as the damage roll for Rolls.psywave.
146
203
  psywave: u8 = 0,
@@ -150,18 +207,19 @@ pub const Action = packed struct(u64) {
150
207
 
151
208
  pub const Field = std.meta.FieldEnum(Action);
152
209
 
153
- pub fn format(a: Action, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
154
- _ = .{ f, o };
210
+ pub fn format(a: Action, w: *std.Io.Writer) !void {
155
211
  try fmt(a, w, false);
156
212
  }
157
213
 
158
- pub fn fmt(self: Action, writer: anytype, shape: bool) !void {
214
+ const SYMBOLS = [_][]const u8{ "+", "", "-", "#" };
215
+
216
+ pub fn fmt(self: Action, writer: *std.Io.Writer, shape: bool) !void {
159
217
  try writer.writeByte('(');
160
218
  var printed = false;
161
- inline for (@typeInfo(Action).Struct.fields) |field| {
219
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
162
220
  const val = @field(self, field.name);
163
221
  switch (@typeInfo(@TypeOf(val))) {
164
- .Enum => if (val != .None) {
222
+ .@"enum" => if (val != .None) {
165
223
  if (printed) try writer.writeAll(", ");
166
224
  if (shape) {
167
225
  try writer.print("{s}:?", .{field.name});
@@ -170,33 +228,14 @@ pub const Action = packed struct(u64) {
170
228
  if (val == .false) "!" else "",
171
229
  field.name,
172
230
  });
173
- } else if (@TypeOf(val) == Optional(Observation)) {
174
- try writer.print("{s}{s}", .{
175
- switch (val) {
176
- .started => "+",
177
- .continuing => "",
178
- .ended => "-",
179
- else => unreachable,
180
- },
181
- field.name,
182
- });
183
- } else if (@TypeOf(val) == Optional(Confusion)) {
184
- try writer.print("{s}{s}", .{
185
- switch (val) {
186
- .started => "+",
187
- .continuing => "",
188
- .ended => "-",
189
- .overwritten => "#",
190
- else => unreachable,
191
- },
192
- field.name,
193
- });
231
+ } else if (@TypeOf(val) != Move and @TypeOf(val) != Optional(Player)) {
232
+ try writer.print("{s}{s}", .{ SYMBOLS[@intFromEnum(val) - 1], field.name });
194
233
  } else {
195
234
  try writer.print("{s}:{s}", .{ field.name, @tagName(val) });
196
235
  }
197
236
  printed = true;
198
237
  },
199
- .Int => if (val != 0) {
238
+ .int => if (val != 0) {
200
239
  if (printed) try writer.writeAll(", ");
201
240
  if (shape) {
202
241
  try writer.print("{s}:?", .{field.name});
@@ -213,68 +252,63 @@ pub const Action = packed struct(u64) {
213
252
  };
214
253
 
215
254
  /// TODO
216
- pub const Durations = struct {
255
+ pub const Durations = extern struct {
217
256
  /// TODO
218
257
  p1: Duration = .{},
219
258
  /// TODO
220
259
  p2: Duration = .{},
221
260
 
261
+ comptime {
262
+ assert(@sizeOf(Durations) == 8);
263
+ }
264
+
222
265
  /// TODO
223
- pub fn get(self: anytype, player: Player) util.PointerType(@TypeOf(self), Duration) {
224
- assert(@typeInfo(@TypeOf(self)).Pointer.child == Durations);
266
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Duration) {
267
+ assert(isPointerTo(self, Durations));
225
268
  return if (player == .P1) &self.p1 else &self.p2;
226
269
  }
227
270
 
228
- pub fn format(
229
- self: Durations,
230
- comptime fmt: []const u8,
231
- opts: std.fmt.FormatOptions,
232
- writer: anytype,
233
- ) !void {
234
- _ = .{ fmt, opts };
235
- try writer.print("[P1 = {}, P2 = {}]", .{ self.p1, self.p2 });
271
+ pub fn format(self: Durations, writer: *std.Io.Writer) !void {
272
+ try writer.print("[P1 = {f}, P2 = {f}]", .{ self.p1, self.p2 });
236
273
  }
237
274
  };
238
275
 
276
+ pub const Sleeps = Array(6, u3);
277
+
239
278
  /// TODO
240
- pub const Duration = struct {
241
- sleeps: [6]u3 = .{0} ** 6,
279
+ pub const Duration = packed struct(u32) {
280
+ sleeps: Sleeps.T = 0,
242
281
  confusion: u3 = 0,
243
282
  disable: u4 = 0,
244
- bide: u3 = 0,
245
- thrashing: u3 = 0,
283
+ attacking: u3 = 0,
246
284
  binding: u3 = 0,
247
285
 
286
+ _: u1 = 0,
287
+
288
+ pub const Field = enum { attacking, binding, sleep, disable, confusion };
289
+
248
290
  pub fn format(
249
291
  self: Duration,
250
- comptime fmt: []const u8,
251
- opts: std.fmt.FormatOptions,
252
- writer: anytype,
292
+ writer: *std.Io.Writer,
253
293
  ) !void {
254
- _ = .{ fmt, opts };
255
294
  try writer.writeByte('(');
256
295
  var printed = false;
257
- inline for (@typeInfo(Duration).Struct.fields) |field| {
296
+ inline for (@typeInfo(Duration).@"struct".fields) |field| {
258
297
  const val = @field(self, field.name);
259
298
  switch (@typeInfo(@TypeOf(val))) {
260
- .Int => if (val != 0) {
299
+ .int => if (val != 0) {
261
300
  if (printed) try writer.writeAll(", ");
262
- try writer.print("{s}:{d}", .{ field.name, val });
263
- printed = true;
264
- },
265
- .Array => {
266
- var total: usize = 0;
267
- for (val) |v| total += v;
268
- if (total > 0) {
269
- if (printed) try writer.writeAll(", ");
301
+ if (comptime std.mem.eql(u8, field.name, "sleeps")) {
270
302
  try writer.print("{s}:[", .{field.name});
271
- for (val, 0..) |v, i| {
303
+ for (0..6) |i| {
272
304
  if (i != 0) try writer.writeByte(',');
273
- try writer.print("{d}", .{v});
305
+ try writer.print("{d}", .{Sleeps.get(val, i)});
274
306
  }
275
307
  try writer.writeAll("]");
276
- printed = true;
308
+ } else {
309
+ try writer.print("{s}:{d}", .{ field.name, val });
277
310
  }
311
+ printed = true;
278
312
  },
279
313
  else => unreachable,
280
314
  }
@@ -283,7 +317,7 @@ pub const Duration = struct {
283
317
  }
284
318
  };
285
319
 
286
- pub const Commit = enum { hit, miss, binding };
320
+ pub const Commit = enum { hit, miss, binding, err, glitch };
287
321
 
288
322
  const uN = if (options.miss) u8 else u9;
289
323
 
@@ -323,6 +357,7 @@ pub fn Chance(comptime Rational: type) type {
323
357
  damage_roll: u8 = 0,
324
358
  hit: bool = false,
325
359
  hit_probablity: uN = 0,
360
+ glitch: bool = false,
326
361
  } = .{},
327
362
 
328
363
  /// Possible error returned by operations tracking chance probability.
@@ -342,10 +377,18 @@ pub fn Chance(comptime Rational: type) type {
342
377
 
343
378
  assert(!showdown);
344
379
 
380
+ if (kind == .glitch and !self.pending.glitch) return;
381
+
345
382
  var action = self.actions.get(player);
346
383
 
384
+ if (kind == .err and self.pending.crit_probablity != 0) {
385
+ try self.probability.update(self.pending.crit_probablity, 256);
386
+ action.critical_hit = if (self.pending.crit) .true else .false;
387
+ return;
388
+ }
389
+
347
390
  // Always commit the hit result if we make it here (commit won't be called at all if the
348
- // target is immune/behind a sub/etc)
391
+ // target is immune*/behind a sub/etc)
349
392
  if (self.pending.hit_probablity != 0) {
350
393
  try self.probability.update(self.pending.hit_probablity, 256);
351
394
  action.hit = if (self.pending.hit) .true else .false;
@@ -353,28 +396,61 @@ pub fn Chance(comptime Rational: type) type {
353
396
 
354
397
  // If the move actually lands we can commit any past critical hit / damage rolls. Note
355
398
  // we can't rely on the presence or absence of a damage roll to determine whether a
356
- // "critical hit" was spurious or not as moves that do 1 or less damage don't rol for
399
+ // "critical hit" was spurious or not as moves that do 1 or less damage don't roll for
357
400
  // damage but can still crit (instead, whether or not a roll is deemed to be a no-op is
358
- // passed to the critical hit helper)
359
- if (kind != .hit) return assert(!self.pending.crit or self.pending.crit_probablity > 0);
401
+ // passed to the critical hit helper).
402
+ //
403
+ // There is a special edge case with the division-by-zero freeze where we need to commit
404
+ // the status of critical hit roll the occured before the move missed to avoid missing
405
+ // out on the possibility that the alternative branch encountered an error
406
+ if (kind != .hit and !self.pending.glitch) {
407
+ return assert(!self.pending.crit or self.pending.crit_probablity > 0);
408
+ }
360
409
 
361
410
  if (self.pending.crit_probablity != 0) {
362
411
  try self.probability.update(self.pending.crit_probablity, 256);
363
412
  action.critical_hit = if (self.pending.crit) .true else .false;
364
- }
413
+ } else assert(!self.pending.glitch);
365
414
 
366
- if (self.pending.damage_roll > 0) {
415
+ if (self.pending.damage_roll > 0 and (!self.pending.glitch or self.pending.hit)) {
367
416
  try self.probability.update(1, 39);
368
417
  action.damage = self.pending.damage_roll;
369
418
  }
370
419
  }
371
420
 
421
+ pub fn glitch(self: *Self) void {
422
+ if (!enabled) return;
423
+
424
+ assert(!showdown);
425
+ self.pending.glitch = true;
426
+ }
427
+
372
428
  pub fn clearPending(self: *Self) void {
373
429
  if (!enabled) return;
374
430
 
431
+ assert(!showdown);
375
432
  self.pending = .{};
376
433
  }
377
434
 
435
+ pub fn switched(self: *Self, player: Player, slot: u8) void {
436
+ if (!enabled) return;
437
+
438
+ assert(slot >= 1 and slot <= 6);
439
+
440
+ var d = self.durations.get(player);
441
+
442
+ const out = Sleeps.get(d.sleeps, 0);
443
+ d.sleeps = Sleeps.set(d.sleeps, 0, Sleeps.get(d.sleeps, slot - 1));
444
+ d.sleeps = Sleeps.set(d.sleeps, slot - 1, out);
445
+
446
+ d.confusion = 0;
447
+ d.disable = 0;
448
+ d.attacking = 0;
449
+ d.binding = 0;
450
+
451
+ self.durations.get(player.foe()).binding = 0;
452
+ }
453
+
378
454
  pub fn speedTie(self: *Self, p1: bool) Error!void {
379
455
  if (!enabled) return;
380
456
 
@@ -474,36 +550,185 @@ pub fn Chance(comptime Rational: type) type {
474
550
  pub fn duration(self: *Self, player: Player, turns: u4) void {
475
551
  if (!enabled) return;
476
552
 
477
- self.actions.get(player).duration = if (options.key) 1 else turns;
553
+ self.actions.get(player).duration = if (options.key) 2 else turns;
478
554
  }
479
555
 
480
556
  pub fn observe(
481
557
  self: *Self,
482
558
  comptime field: Action.Field,
483
559
  player: Player,
484
- observation: Optional(Confusion),
560
+ obs: Optional(Observation(.confusion)),
485
561
  ) void {
486
562
  if (!enabled) return;
487
563
 
488
- const val = @field(self.actions.get(player), @tagName(field));
489
- if (observation == .overwritten) {
490
- assert(field == .confusion);
491
- assert(val == .started or val == .continuing);
492
- assert(self.actions.get(player).duration > 0);
493
- self.actions.get(player).confusion = observation;
564
+ var a = self.actions.get(player);
565
+ const val = @field(a, @tagName(field));
566
+ switch (obs) {
567
+ .overwritten => {
568
+ assert(field == .confusion);
569
+ assert(val == .started or val == .continuing);
570
+ assert(a.duration > 0);
571
+ a.confusion = obs;
572
+ self.durations.get(player).confusion = 1;
573
+ },
574
+ .None => {
575
+ @field(a, @tagName(field)) = .None;
576
+ if (field == .sleep) {
577
+ var d = self.durations.get(player);
578
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
579
+ } else {
580
+ @field(self.durations.get(player), @tagName(field)) = 0;
581
+ }
582
+ },
583
+ .started => {
584
+ assert(val == .None or val == .ended);
585
+ assert(switch (field) {
586
+ .confusion => a.duration > 0 or self.actions.get(player.foe()).duration > 0,
587
+ .sleep, .disable => self.actions.get(player.foe()).duration > 0,
588
+ else => a.duration > 0,
589
+ });
590
+ @field(a, @tagName(field)) = .started;
591
+ if (field == .sleep) {
592
+ var d = self.durations.get(player);
593
+ d.sleeps = Sleeps.set(d.sleeps, 0, 1);
594
+ } else {
595
+ @field(self.durations.get(player), @tagName(field)) = 1;
596
+ }
597
+ },
598
+ else => unreachable,
599
+ }
600
+ }
601
+
602
+ pub fn sleep(self: *Self, player: Player, obs: Optional(Observation(.sleep))) Error!void {
603
+ if (!enabled) return;
604
+
605
+ var a = self.actions.get(player);
606
+ assert(a.sleep == .None or a.sleep == .started);
607
+ a.sleep = obs;
608
+
609
+ var d = self.durations.get(player);
610
+ const n = Sleeps.get(d.sleeps, 0);
611
+
612
+ if (obs == .None) {
613
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
614
+ } else if (obs == .ended) {
615
+ assert(n >= 1 and n <= 7);
616
+ if (n != 7) try self.probability.update(1, 8 - @as(u4, n));
617
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
494
618
  } else {
495
- assert(observation == .None or val == .None or
496
- (val == .started and observation != .started) or
497
- (val == .ended and observation == .started));
498
- assert(observation != .started or (if (field == .confusion)
499
- self.actions.get(player).duration > 0 or
500
- self.actions.get(player.foe()).duration > 0
501
- else if (field == .sleep or field == .disable)
502
- self.actions.get(player.foe()).duration > 0
503
- else
504
- self.actions.get(player).duration > 0));
505
- @field(self.actions.get(player), @tagName(field)) =
506
- @enumFromInt(@intFromEnum(observation));
619
+ assert(obs == .continuing);
620
+ assert(n >= 1 and n < 7);
621
+ try self.probability.update(8 - @as(u4, n) - 1, 8 - @as(u4, n));
622
+ d.sleeps = Sleeps.set(d.sleeps, 0, n + 1);
623
+ }
624
+ }
625
+
626
+ pub fn confusion(
627
+ self: *Self,
628
+ player: Player,
629
+ obs: Optional(Observation(.confusion)),
630
+ ) Error!void {
631
+ if (!enabled) return;
632
+
633
+ var a = self.actions.get(player);
634
+ assert(a.confusion == .None or a.confusion == .started);
635
+ a.confusion = obs;
636
+
637
+ var d = self.durations.get(player);
638
+ const n = d.confusion;
639
+
640
+ if (obs == .ended) {
641
+ assert(n >= 2 and n <= 5);
642
+ if (n != 5) try self.probability.update(1, 6 - @as(u4, n));
643
+ d.confusion = 0;
644
+ } else {
645
+ assert(obs == .continuing);
646
+ assert(n >= 1 and n < 5);
647
+ if (n > 1) try self.probability.update(6 - @as(u4, n) - 1, 6 - @as(u4, n));
648
+ d.confusion += 1;
649
+ }
650
+ }
651
+
652
+ pub fn disable(
653
+ self: *Self,
654
+ player: Player,
655
+ obs: Optional(Observation(.disable)),
656
+ ) Error!void {
657
+ if (!enabled) return;
658
+
659
+ var a = self.actions.get(player);
660
+ assert(a.disable == .None or a.disable == .started);
661
+ a.disable = obs;
662
+
663
+ var d = self.durations.get(player);
664
+ const n = d.disable;
665
+
666
+ if (obs == .ended) {
667
+ assert(n >= 1 and n <= 8);
668
+ if (n != 8) try self.probability.update(1, 9 - @as(u4, n));
669
+ d.disable = 0;
670
+ } else {
671
+ assert(obs == .continuing);
672
+ assert(n >= 1 and n < 8);
673
+ try self.probability.update(9 - @as(u4, n) - 1, 9 - @as(u4, n));
674
+ d.disable += 1;
675
+ }
676
+ }
677
+
678
+ pub fn attacking(
679
+ self: *Self,
680
+ player: Player,
681
+ obs: Optional(Observation(.attacking)),
682
+ ) Error!void {
683
+ if (!enabled) return;
684
+
685
+ var a = self.actions.get(player);
686
+ assert(a.attacking == .None or a.attacking == .started);
687
+ a.attacking = obs;
688
+
689
+ var d = self.durations.get(player);
690
+ const n = d.attacking;
691
+ if (obs == .ended) {
692
+ // "3-4 turns" includes the turn for the initial attack *plus* the forced attacks
693
+ assert(n >= 2 and n <= 3);
694
+ if (n != 3) try self.probability.update(1, 4 - @as(u4, n));
695
+ d.attacking = 0;
696
+ } else {
697
+ assert(obs == .continuing);
698
+ assert(n >= 1 and n < 3);
699
+ if (n > 1) try self.probability.update(4 - @as(u4, n) - 1, 4 - @as(u4, n));
700
+ d.attacking += 1;
701
+ }
702
+ }
703
+
704
+ pub fn binding(
705
+ self: *Self,
706
+ player: Player,
707
+ obs: Optional(Observation(.binding)),
708
+ ) Error!void {
709
+ if (!enabled) return;
710
+
711
+ var a = self.actions.get(player);
712
+ assert(a.binding == .None or a.binding == .started);
713
+ a.binding = obs;
714
+
715
+ var d = self.durations.get(player);
716
+ const n = d.binding;
717
+
718
+ assert(n > 0);
719
+ const p: u4 = if (n < 3) 3 else 1;
720
+ const q: u4 = if (n < 3) 8 - ((n - 1) * p) else 2;
721
+
722
+ if (obs == .ended) {
723
+ // "2-5 turns" includes the turn for the initial attack *plus* the forced attacks
724
+ assert(n >= 1 and n <= 4);
725
+ if (n != 4) try self.probability.update(p, q);
726
+ d.binding = 0;
727
+ } else {
728
+ assert(obs == .continuing);
729
+ assert(n >= 1 and n < 4);
730
+ try self.probability.update(q - p, q);
731
+ d.binding += 1;
507
732
  }
508
733
  }
509
734
 
@@ -520,6 +745,15 @@ pub fn Chance(comptime Rational: type) type {
520
745
  try self.probability.update(1, Move.METRONOME.len);
521
746
  self.actions.get(player).metronome = move;
522
747
  }
748
+
749
+ pub fn pp(self: *Self, player: Player, n: u4) bool {
750
+ if (!enabled) return true;
751
+
752
+ var a = self.actions.get(player);
753
+ if (n > 1 and a.pp == n) return false;
754
+ if (a.metronome != .None) a.pp += n;
755
+ return true;
756
+ }
523
757
  };
524
758
  }
525
759
 
@@ -620,16 +854,16 @@ test "Chance.damage" {
620
854
 
621
855
  try chance.damage(.P1, 217);
622
856
  if (showdown) {
623
- try expectValue(217, chance.actions.p1.damage);
857
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
624
858
  try expectProbability(&chance.probability, 1, 39);
625
859
  } else {
626
860
  try expectProbability(&chance.probability, 1, 1);
627
- try expectValue(0, chance.actions.p1.damage);
861
+ try expectValue(@as(u8, 0), chance.actions.p1.damage);
628
862
 
629
863
  try chance.commit(.P1, .hit);
630
864
 
631
865
  try expectProbability(&chance.probability, 1, 39);
632
- try expectValue(217, chance.actions.p1.damage);
866
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
633
867
  }
634
868
  }
635
869
 
@@ -681,13 +915,13 @@ test "Chance.moveSlot" {
681
915
 
682
916
  try chance.moveSlot(.P2, 2, &ms, 2);
683
917
  try expectProbability(&chance.probability, 1, 2);
684
- try expectValue(2, chance.actions.p2.move_slot);
918
+ try expectValue(@as(u3, 2), chance.actions.p2.move_slot);
685
919
 
686
920
  chance.reset();
687
921
 
688
922
  try chance.moveSlot(.P1, 1, &ms, 0);
689
923
  try expectProbability(&chance.probability, 1, 3);
690
- try expectValue(1, chance.actions.p1.move_slot);
924
+ try expectValue(@as(u3, 1), chance.actions.p1.move_slot);
691
925
  }
692
926
 
693
927
  test "Chance.multiHit" {
@@ -695,25 +929,142 @@ test "Chance.multiHit" {
695
929
 
696
930
  try chance.multiHit(.P1, 3);
697
931
  try expectProbability(&chance.probability, 3, 8);
698
- try expectValue(3, chance.actions.p1.multi_hit);
932
+ try expectValue(@as(u4, 3), chance.actions.p1.multi_hit);
699
933
 
700
934
  chance.reset();
701
935
 
702
936
  try chance.multiHit(.P2, 5);
703
937
  try expectProbability(&chance.probability, 1, 8);
704
- try expectValue(5, chance.actions.p2.multi_hit);
938
+ try expectValue(@as(u4, 5), chance.actions.p2.multi_hit);
705
939
  }
706
940
 
707
941
  test "Chance.duration" {
708
942
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
709
943
 
710
944
  chance.duration(.P1, 2);
711
- try expectValue(2, chance.actions.p1.duration);
945
+ try expectValue(@as(u4, 2), chance.actions.p1.duration);
712
946
 
713
947
  chance.reset();
714
948
 
715
949
  chance.duration(.P2, 4);
716
- try expectValue(4, chance.actions.p2.duration);
950
+ try expectValue(@as(u4, 4), chance.actions.p2.duration);
951
+ }
952
+
953
+ test "Chance.sleep" {
954
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
955
+
956
+ for ([_]u8{ 7, 6, 5, 4, 3, 2, 1 }, 1..8) |d, i| {
957
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
958
+ try chance.sleep(.P1, .ended);
959
+ try expectProbability(&chance.probability, 1, d);
960
+ try expectValue(@as(u3, 0), Sleeps.get(chance.durations.p1.sleeps, 0));
961
+
962
+ chance.reset();
963
+
964
+ if (i < 7) {
965
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
966
+ try chance.sleep(.P1, .continuing);
967
+ try expectProbability(&chance.probability, d - 1, d);
968
+ try expectValue(@as(u3, @intCast(i)) + 1, Sleeps.get(chance.durations.p1.sleeps, 0));
969
+
970
+ chance.reset();
971
+ }
972
+ }
973
+ }
974
+
975
+ test "Chance.confusion" {
976
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
977
+
978
+ for ([_]u8{ 1, 4, 3, 2, 1 }, 1..6) |d, i| {
979
+ if (i > 1) {
980
+ chance.durations.p2.confusion = @intCast(i);
981
+ try chance.confusion(.P2, .ended);
982
+ try expectProbability(&chance.probability, 1, d);
983
+ try expectValue(@as(u3, 0), chance.durations.p2.confusion);
984
+
985
+ chance.reset();
986
+ }
987
+
988
+ if (i < 5) {
989
+ chance.durations.p2.confusion = @intCast(i);
990
+ try chance.confusion(.P2, .continuing);
991
+ try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
992
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.confusion);
993
+
994
+ chance.reset();
995
+ }
996
+ }
997
+ }
998
+
999
+ test "Chance.disable" {
1000
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1001
+
1002
+ for ([_]u8{ 8, 7, 6, 5, 4, 3, 2, 1 }, 1..9) |d, i| {
1003
+ chance.durations.p1.disable = @intCast(i);
1004
+ try chance.disable(.P1, .ended);
1005
+ try expectProbability(&chance.probability, 1, d);
1006
+ try expectValue(@as(u4, 0), chance.durations.p1.disable);
1007
+
1008
+ chance.reset();
1009
+
1010
+ if (i < 8) {
1011
+ chance.durations.p1.disable = @intCast(i);
1012
+ try chance.disable(.P1, .continuing);
1013
+ try expectProbability(&chance.probability, d - 1, d);
1014
+ try expectValue(@as(u4, @intCast(i)) + 1, chance.durations.p1.disable);
1015
+
1016
+ chance.reset();
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ test "Chance.attacking" {
1022
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1023
+
1024
+ for ([_]u8{ 1, 2, 1 }, 1..4) |d, i| {
1025
+ if (i > 1) {
1026
+ chance.durations.p2.attacking = @intCast(i);
1027
+ try chance.attacking(.P2, .ended);
1028
+ try expectProbability(&chance.probability, 1, d);
1029
+ try expectValue(@as(u3, 0), chance.durations.p2.attacking);
1030
+
1031
+ chance.reset();
1032
+ }
1033
+
1034
+ if (i < 3) {
1035
+ chance.durations.p2.attacking = @intCast(i);
1036
+ try chance.attacking(.P2, .continuing);
1037
+ try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
1038
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.attacking);
1039
+
1040
+ chance.reset();
1041
+ }
1042
+ }
1043
+ }
1044
+
1045
+ test "Chance.binding" {
1046
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1047
+
1048
+ const ps = [_]u8{ 3, 3, 1, 1 };
1049
+ const qs = [_]u8{ 8, 5, 2, 1 };
1050
+
1051
+ for (ps, qs, 1..5) |p, q, i| {
1052
+ chance.durations.p1.binding = @intCast(i);
1053
+ try chance.binding(.P1, .ended);
1054
+ try expectProbability(&chance.probability, p, q);
1055
+ try expectValue(@as(u3, 0), chance.durations.p1.binding);
1056
+
1057
+ chance.reset();
1058
+
1059
+ if (i < 4) {
1060
+ chance.durations.p1.binding = @intCast(i);
1061
+ try chance.binding(.P1, .continuing);
1062
+ try expectProbability(&chance.probability, q - p, q);
1063
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p1.binding);
1064
+
1065
+ chance.reset();
1066
+ }
1067
+ }
717
1068
  }
718
1069
 
719
1070
  test "Chance.psywave" {
@@ -721,7 +1072,7 @@ test "Chance.psywave" {
721
1072
 
722
1073
  try chance.psywave(.P2, 100, 150);
723
1074
  try expectProbability(&chance.probability, 1, 150);
724
- try expectValue(101, chance.actions.p2.psywave);
1075
+ try expectValue(@as(u8, 101), chance.actions.p2.psywave);
725
1076
  }
726
1077
 
727
1078
  test "Chance.metronome" {
@@ -737,7 +1088,7 @@ pub fn expectProbability(r: anytype, p: u64, q: u64) !void {
737
1088
 
738
1089
  r.reduce();
739
1090
  if (r.p != p or r.q != q) {
740
- print("expected {d}/{d}, found {}\n", .{ p, q, r });
1091
+ print("expected {d}/{d}, found {f}\n", .{ p, q, r });
741
1092
  return error.TestExpectedEqual;
742
1093
  }
743
1094
  }
@@ -759,12 +1110,16 @@ const Null = struct {
759
1110
  _ = .{ self, player, kind };
760
1111
  }
761
1112
 
1113
+ pub fn glitch(self: Null) void {
1114
+ _ = .{self};
1115
+ }
1116
+
762
1117
  pub fn clearPending(self: Null) void {
763
1118
  _ = .{self};
764
1119
  }
765
1120
 
766
- pub fn switched(self: Null, player: Player, in: u8, out: u8) void {
767
- _ = .{ self, player, in, out };
1121
+ pub fn switched(self: Null, player: Player, slot: u8) void {
1122
+ _ = .{ self, player, slot };
768
1123
  }
769
1124
 
770
1125
  pub fn speedTie(self: Null, p1: bool) Error!void {
@@ -803,17 +1158,53 @@ const Null = struct {
803
1158
  _ = .{ self, player, n };
804
1159
  }
805
1160
 
1161
+ pub fn duration(self: Null, player: Player, turns: u4) void {
1162
+ _ = .{ self, player, turns };
1163
+ }
1164
+
806
1165
  pub fn observe(
807
1166
  self: Null,
808
1167
  comptime field: Action.Field,
809
1168
  player: Player,
810
- observation: Optional(Confusion),
1169
+ obs: Optional(Observation(.confusion)),
811
1170
  ) void {
812
- _ = .{ self, field, player, observation };
1171
+ _ = .{ self, field, player, obs };
813
1172
  }
814
1173
 
815
- pub fn duration(self: Null, player: Player, turns: u4) void {
816
- _ = .{ self, player, turns };
1174
+ pub fn sleep(self: Null, player: Player, obs: Optional(Observation(.sleep))) Error!void {
1175
+ _ = .{ self, player, obs };
1176
+ }
1177
+
1178
+ pub fn confusion(
1179
+ self: Null,
1180
+ player: Player,
1181
+ obs: Optional(Observation(.confusion)),
1182
+ ) Error!void {
1183
+ _ = .{ self, player, obs };
1184
+ }
1185
+
1186
+ pub fn disable(
1187
+ self: Null,
1188
+ player: Player,
1189
+ obs: Optional(Observation(.disable)),
1190
+ ) Error!void {
1191
+ _ = .{ self, player, obs };
1192
+ }
1193
+
1194
+ pub fn attacking(
1195
+ self: Null,
1196
+ player: Player,
1197
+ obs: Optional(Observation(.attacking)),
1198
+ ) Error!void {
1199
+ _ = .{ self, player, obs };
1200
+ }
1201
+
1202
+ pub fn binding(
1203
+ self: Null,
1204
+ player: Player,
1205
+ obs: Optional(Observation(.binding)),
1206
+ ) Error!void {
1207
+ _ = .{ self, player, obs };
817
1208
  }
818
1209
 
819
1210
  pub fn psywave(self: Null, player: Player, power: u8, max: u8) Error!void {
@@ -823,4 +1214,9 @@ const Null = struct {
823
1214
  pub fn metronome(self: Null, player: Player, move: Move) Error!void {
824
1215
  _ = .{ self, player, move };
825
1216
  }
1217
+
1218
+ pub fn pp(self: Null, player: Player, n: u4) bool {
1219
+ _ = .{ self, player, n };
1220
+ return true;
1221
+ }
826
1222
  };