@pkmn/engine 0.1.0-dev.b05836cd → 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 (84) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +56 -21
  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 +39 -71
  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 +27 -5
  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 +42 -91
  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 +109 -107
  27. package/build/pkg/protocol.js.map +1 -1
  28. package/build/tools/debug.js +69 -76
  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 +256 -122
  39. package/package.json +62 -134
  40. package/src/bin/install-pkmn-engine +47 -21
  41. package/src/bin/pkmn-debug +1 -1
  42. package/src/lib/bindings/c.zig +200 -0
  43. package/src/lib/{binding → bindings}/node.zig +15 -32
  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 +4 -4
  47. package/src/lib/common/debug.zig +32 -11
  48. package/src/lib/common/js.zig +3 -7
  49. package/src/lib/common/optional.zig +12 -18
  50. package/src/lib/common/options.zig +6 -0
  51. package/src/lib/common/protocol.zig +209 -178
  52. package/src/lib/common/rational.zig +340 -253
  53. package/src/lib/common/rng.zig +7 -9
  54. package/src/lib/common/util.zig +33 -1
  55. package/src/lib/gen1/calc.zig +724 -175
  56. package/src/lib/gen1/chance.zig +453 -291
  57. package/src/lib/gen1/data/moves.zig +2 -4
  58. package/src/lib/gen1/data/species.zig +2 -4
  59. package/src/lib/gen1/data/types.zig +3 -4
  60. package/src/lib/gen1/data.zig +29 -32
  61. package/src/lib/gen1/helpers.zig +91 -549
  62. package/src/lib/gen1/mechanics.zig +417 -294
  63. package/src/lib/gen2/calc.zig +35 -28
  64. package/src/lib/gen2/chance.zig +85 -503
  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 +27 -29
  70. package/src/lib/gen2/generated.zig +19 -26
  71. package/src/lib/gen2/helpers.zig +45 -26
  72. package/src/lib/gen2/mechanics.zig +234 -180
  73. package/src/lib/napi.h +2 -0
  74. package/src/lib/node.zig +14 -0
  75. package/src/lib/pkmn.zig +125 -29
  76. package/src/lib/wasm.zig +13 -0
  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 -400
  80. package/build/test/display.js.map +0 -1
  81. package/src/lib/binding/wasm.zig +0 -12
  82. package/src/lib/gen1/README.md +0 -894
  83. package/src/lib/gen2/README.md +0 -329
  84. 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,17 +38,11 @@ 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
 
45
- /// Perform a reset by clearing fields which should not persist across updates.
46
- pub fn reset(self: *Actions) void {
47
- self.p1.reset();
48
- self.p2.reset();
49
- }
50
-
51
46
  /// Returns true if `a` is equal to `b`.
52
47
  pub fn eql(a: Actions, b: Actions) bool {
53
48
  return @as(u128, @bitCast(a)) == @as(u128, @bitCast(b));
@@ -57,20 +52,15 @@ pub const Actions = extern struct {
57
52
  /// same shape if they have the same fields set (though those fields need not necessarily be
58
53
  /// set to the same value).
59
54
  pub fn matches(a: Actions, b: Actions) bool {
60
- inline for (@typeInfo(Actions).Struct.fields) |player| {
61
- inline for (@typeInfo(Action).Struct.fields) |field| {
55
+ inline for (@typeInfo(Actions).@"struct".fields) |player| {
56
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
62
57
  const a_val = @field(@field(a, player.name), field.name);
63
58
  const b_val = @field(@field(b, player.name), field.name);
64
59
 
65
60
  switch (@typeInfo(@TypeOf(a_val))) {
66
- .Struct => inline for (@typeInfo(@TypeOf(a_val)).Struct.fields) |f| {
67
- if ((@field(a_val, f.name) > 0) != (@field(b_val, f.name) > 0)) {
68
- return false;
69
- }
70
- },
71
- .Enum => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
61
+ .@"enum" => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
72
62
  return false,
73
- .Int => if ((a_val > 0) != (b_val > 0)) return false,
63
+ .int => if ((a_val > 0) != (b_val > 0)) return false,
74
64
  else => unreachable,
75
65
  }
76
66
  }
@@ -78,7 +68,26 @@ pub const Actions = extern struct {
78
68
  return true;
79
69
  }
80
70
 
81
- 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 {
82
91
  try writer.writeAll("<P1 = ");
83
92
  try self.p1.fmt(writer, shape);
84
93
  try writer.writeAll(", P2 = ");
@@ -86,8 +95,7 @@ pub const Actions = extern struct {
86
95
  try writer.writeAll(">");
87
96
  }
88
97
 
89
- pub fn format(a: Actions, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
90
- _ = .{ f, o };
98
+ pub fn format(a: Actions, w: *std.Io.Writer) !void {
91
99
  try fmt(a, w, false);
92
100
  }
93
101
  };
@@ -96,9 +104,6 @@ test Actions {
96
104
  const a: Actions = .{ .p1 = .{ .hit = .true, .critical_hit = .false, .damage = 245 } };
97
105
  const b: Actions = .{ .p1 = .{ .hit = .false, .critical_hit = .true, .damage = 246 } };
98
106
  const c: Actions = .{ .p1 = .{ .hit = .true } };
99
- const d: Actions = .{ .p2 = .{ .hit = .true, .durations = .{ .sleep = 2 } } };
100
- const e: Actions = .{ .p2 = .{ .hit = .false, .durations = .{ .sleep = 4 } } };
101
- const f: Actions = .{ .p1 = .{ .hit = .false, .durations = .{ .sleep = 4 } } };
102
107
 
103
108
  try expect(a.eql(a));
104
109
  try expect(!a.eql(b));
@@ -111,8 +116,43 @@ test Actions {
111
116
  try expect(b.matches(a));
112
117
  try expect(!a.matches(c));
113
118
  try expect(!c.matches(a));
114
- try expect(d.matches(e));
115
- try expect(!d.matches(f));
119
+ }
120
+
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
+ };
116
156
  }
117
157
 
118
158
  /// Information about the RNG that was observed during a Generation I battle `update` for a
@@ -139,14 +179,25 @@ pub const Action = packed struct(u64) {
139
179
  /// or Rolls.{sleepDuration,disableDuration,confusionDuration,attackingDuration} otherwise.
140
180
  duration: u4 = 0,
141
181
 
142
- /// Observed values of various durations. Does not influence future RNG calls. TODO
143
- durations: Duration = .{},
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,
144
192
 
145
193
  /// If not 0, the move slot (1-4) to return in Rolls.moveSlot. If present as an override,
146
194
  /// invalid values (eg. due to empty move slots or 0 PP) will be ignored.
147
- move_slot: u4 = 0,
195
+ move_slot: u3 = 0,
148
196
  /// If not 0, the value (2-5) to return for Rolls.distribution for multi hit.
149
- multi_hit: u4 = 0,
197
+ multi_hit: u3 = 0,
198
+
199
+ /// TODO
200
+ pp: u4 = 0,
150
201
 
151
202
  /// If not 0, psywave - 1 should be returned as the damage roll for Rolls.psywave.
152
203
  psywave: u8 = 0,
@@ -154,39 +205,21 @@ pub const Action = packed struct(u64) {
154
205
  /// If not None, the Move to return for Rolls.metronome.
155
206
  metronome: Move = .None,
156
207
 
157
- pub const DURATIONS: u64 = 0x000000FFFF000000;
158
-
159
208
  pub const Field = std.meta.FieldEnum(Action);
160
209
 
161
- /// Perform a reset by clearing fields which should not persist across updates.
162
- pub fn reset(self: *Action) void {
163
- self.* = @bitCast(@as(u64, @bitCast(self.*)) & DURATIONS);
164
- }
165
-
166
- pub fn format(a: Action, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
167
- _ = .{ f, o };
210
+ pub fn format(a: Action, w: *std.Io.Writer) !void {
168
211
  try fmt(a, w, false);
169
212
  }
170
213
 
171
- 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 {
172
217
  try writer.writeByte('(');
173
218
  var printed = false;
174
- inline for (@typeInfo(Action).Struct.fields) |field| {
219
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
175
220
  const val = @field(self, field.name);
176
221
  switch (@typeInfo(@TypeOf(val))) {
177
- .Struct => inline for (@typeInfo(@TypeOf(val)).Struct.fields) |f| {
178
- const v = @field(val, f.name);
179
- if (v != 0) {
180
- if (printed) try writer.writeAll(", ");
181
- if (shape) {
182
- try writer.print("{s}:?", .{f.name});
183
- } else {
184
- try writer.print("{s}:{d}", .{ f.name, v });
185
- }
186
- printed = true;
187
- }
188
- },
189
- .Enum => if (val != .None) {
222
+ .@"enum" => if (val != .None) {
190
223
  if (printed) try writer.writeAll(", ");
191
224
  if (shape) {
192
225
  try writer.print("{s}:?", .{field.name});
@@ -195,12 +228,14 @@ pub const Action = packed struct(u64) {
195
228
  if (val == .false) "!" else "",
196
229
  field.name,
197
230
  });
231
+ } else if (@TypeOf(val) != Move and @TypeOf(val) != Optional(Player)) {
232
+ try writer.print("{s}{s}", .{ SYMBOLS[@intFromEnum(val) - 1], field.name });
198
233
  } else {
199
234
  try writer.print("{s}:{s}", .{ field.name, @tagName(val) });
200
235
  }
201
236
  printed = true;
202
237
  },
203
- .Int => if (val != 0) {
238
+ .int => if (val != 0) {
204
239
  if (printed) try writer.writeAll(", ");
205
240
  if (shape) {
206
241
  try writer.print("{s}:?", .{field.name});
@@ -216,43 +251,75 @@ pub const Action = packed struct(u64) {
216
251
  }
217
252
  };
218
253
 
219
- test Action {
220
- var a: Action = .{ .hit = .true, .durations = .{ .sleep = 3 }, .damage = 225 };
221
- a.reset();
222
-
223
- try expectEqual(Action{ .hit = .None, .durations = .{ .sleep = 3 }, .damage = 0 }, a);
224
- }
225
-
226
254
  /// TODO
227
255
  pub const Durations = extern struct {
228
- /// Information about the durations for Player 1.
256
+ /// TODO
229
257
  p1: Duration = .{},
230
- /// Information about the durations for Player 2.
258
+ /// TODO
231
259
  p2: Duration = .{},
232
260
 
233
261
  comptime {
234
- assert(@sizeOf(Durations) == 4);
262
+ assert(@sizeOf(Durations) == 8);
263
+ }
264
+
265
+ /// TODO
266
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Duration) {
267
+ assert(isPointerTo(self, Durations));
268
+ return if (player == .P1) &self.p1 else &self.p2;
269
+ }
270
+
271
+ pub fn format(self: Durations, writer: *std.Io.Writer) !void {
272
+ try writer.print("[P1 = {f}, P2 = {f}]", .{ self.p1, self.p2 });
235
273
  }
236
274
  };
237
275
 
238
- /// Observed values for various durations that need to be tracked in order to properly
239
- /// deduplicate transitions with a primary key. TODO
240
- pub const Duration = packed struct(u16) {
241
- /// The number of turns a Pokémon has been observed to be sleeping.
242
- sleep: u3 = 0,
243
- /// The number of turns a Pokémon has been observed to be confused.
276
+ pub const Sleeps = Array(6, u3);
277
+
278
+ /// TODO
279
+ pub const Duration = packed struct(u32) {
280
+ sleeps: Sleeps.T = 0,
244
281
  confusion: u3 = 0,
245
- /// The number of turns a Pokémon has been observed to be disabled.
246
282
  disable: u4 = 0,
247
- /// The number of turns a Pokémon has been observed to be attacking.
248
283
  attacking: u3 = 0,
249
- /// The number of turns a Pokémon has been observed to be binding their opponent.
250
284
  binding: u3 = 0,
251
285
 
252
- pub const Field = std.meta.FieldEnum(Duration);
286
+ _: u1 = 0,
287
+
288
+ pub const Field = enum { attacking, binding, sleep, disable, confusion };
289
+
290
+ pub fn format(
291
+ self: Duration,
292
+ writer: *std.Io.Writer,
293
+ ) !void {
294
+ try writer.writeByte('(');
295
+ var printed = false;
296
+ inline for (@typeInfo(Duration).@"struct".fields) |field| {
297
+ const val = @field(self, field.name);
298
+ switch (@typeInfo(@TypeOf(val))) {
299
+ .int => if (val != 0) {
300
+ if (printed) try writer.writeAll(", ");
301
+ if (comptime std.mem.eql(u8, field.name, "sleeps")) {
302
+ try writer.print("{s}:[", .{field.name});
303
+ for (0..6) |i| {
304
+ if (i != 0) try writer.writeByte(',');
305
+ try writer.print("{d}", .{Sleeps.get(val, i)});
306
+ }
307
+ try writer.writeAll("]");
308
+ } else {
309
+ try writer.print("{s}:{d}", .{ field.name, val });
310
+ }
311
+ printed = true;
312
+ },
313
+ else => unreachable,
314
+ }
315
+ }
316
+ try writer.writeByte(')');
317
+ }
253
318
  };
254
319
 
255
- pub const Commit = enum { hit, miss, binding };
320
+ pub const Commit = enum { hit, miss, binding, err, glitch };
321
+
322
+ const uN = if (options.miss) u8 else u9;
256
323
 
257
324
  /// Tracks chance actions and their associated probability during a Generation I battle update when
258
325
  /// `options.chance` is enabled.
@@ -265,14 +332,8 @@ pub fn Chance(comptime Rational: type) type {
265
332
  /// The actions taken by a hypothetical "chance player" that convey information about which
266
333
  /// RNG events were observed during a battle `update`.
267
334
  actions: Actions = .{},
268
-
269
- // Tracks the observed sleep durations for the Pokémon in both player's parties. Unlike
270
- // other durations which are all tied to volatiles, sleep's counter persists through
271
- // switching and so we must store it here. The indices of these arrays correspond to the
272
- // `order` field of a Side. This information could be stored in actions but size is a
273
- // concern so this is tracked separately as actions only purports to track RNG events
274
- // observed during a single `update` (not across updates).
275
- sleeps: [2][6]u3 = .{ .{0} ** 6, .{0} ** 6 },
335
+ /// TODO
336
+ durations: Durations = .{},
276
337
 
277
338
  // In many cases on both the cartridge and on Pokémon Showdown rolls are made even though
278
339
  // later checks render their result irrelevant (missing when the target is actually immune,
@@ -290,16 +351,13 @@ pub fn Chance(comptime Rational: type) type {
290
351
  //
291
352
  // Finally, because each player's actions are processed sequentially we only need a single
292
353
  // shared structure to track this information (though it needs to be cleared appropriately)
293
- pending: if (showdown) struct {
294
- hit: bool = false,
295
- hit_probablity: u8 = 0,
296
- } else struct {
354
+ pending: if (showdown) struct {} else struct {
297
355
  crit: bool = false,
298
356
  crit_probablity: u8 = 0,
299
357
  damage_roll: u8 = 0,
300
358
  hit: bool = false,
301
- hit_probablity: u8 = 0,
302
- binding: u8 = 0,
359
+ hit_probablity: uN = 0,
360
+ glitch: bool = false,
303
361
  } = .{},
304
362
 
305
363
  /// Possible error returned by operations tracking chance probability.
@@ -307,86 +365,90 @@ pub fn Chance(comptime Rational: type) type {
307
365
 
308
366
  /// Convenience helper to clear fields which typically should be cleared between updates.
309
367
  pub fn reset(self: *Self) void {
368
+ if (!enabled) return;
369
+
310
370
  self.probability.reset();
311
- self.actions.reset();
371
+ self.actions = .{};
312
372
  self.pending = .{};
313
373
  }
314
374
 
315
375
  pub fn commit(self: *Self, player: Player, kind: Commit) Error!void {
376
+ if (!enabled) return;
377
+
378
+ assert(!showdown);
379
+
380
+ if (kind == .glitch and !self.pending.glitch) return;
381
+
316
382
  var action = self.actions.get(player);
317
383
 
318
- // We need to handle binding specially because we still need to commit it when attacking
319
- // into a Pokémon that is immune
320
- if (!showdown) {
321
- if (kind != .miss and self.pending.binding != 0) {
322
- assert(action.duration == 0);
323
- action.duration = @intCast(self.pending.binding);
324
- assert(action.durations.binding == 0);
325
- action.durations.binding = 1;
326
- }
327
- if (kind == .binding) return;
328
- } else assert(kind != .binding);
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
+ }
329
389
 
330
390
  // Always commit the hit result if we make it here (commit won't be called at all if the
331
- // target is immune/behind a sub/etc)
391
+ // target is immune*/behind a sub/etc)
332
392
  if (self.pending.hit_probablity != 0) {
333
393
  try self.probability.update(self.pending.hit_probablity, 256);
334
394
  action.hit = if (self.pending.hit) .true else .false;
335
395
  }
336
396
 
337
- if (showdown) return;
338
-
339
- // If the move actually lands we can commit any past critical hit / damage rolls. We
340
- // avoid updating anything if there wasn't a damage roll as any "critical hit" not tied
341
- // to a damage roll is actually a no-op
342
- if (kind == .hit and self.pending.damage_roll > 0) {
343
- assert(!self.pending.crit or self.pending.crit_probablity > 0);
397
+ // If the move actually lands we can commit any past critical hit / damage rolls. Note
398
+ // we can't rely on the presence or absence of a damage roll to determine whether a
399
+ // "critical hit" was spurious or not as moves that do 1 or less damage don't roll for
400
+ // damage but can still crit (instead, whether or not a roll is deemed to be a no-op is
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
+ }
344
409
 
345
- if (self.pending.crit_probablity != 0) {
346
- try self.probability.update(self.pending.crit_probablity, 256);
347
- action.critical_hit = if (self.pending.crit) .true else .false;
348
- }
410
+ if (self.pending.crit_probablity != 0) {
411
+ try self.probability.update(self.pending.crit_probablity, 256);
412
+ action.critical_hit = if (self.pending.crit) .true else .false;
413
+ } else assert(!self.pending.glitch);
349
414
 
415
+ if (self.pending.damage_roll > 0 and (!self.pending.glitch or self.pending.hit)) {
350
416
  try self.probability.update(1, 39);
351
417
  action.damage = self.pending.damage_roll;
352
418
  }
353
419
  }
354
420
 
355
- pub fn clearPending(self: *Self) void {
421
+ pub fn glitch(self: *Self) void {
356
422
  if (!enabled) return;
357
423
 
358
- self.pending = .{};
424
+ assert(!showdown);
425
+ self.pending.glitch = true;
359
426
  }
360
427
 
361
- pub fn clearDurations(self: *Self, player: Player, haze: ?bool) void {
428
+ pub fn clearPending(self: *Self) void {
362
429
  if (!enabled) return;
363
430
 
364
- var action = self.actions.get(player);
365
- if (haze) |status| {
366
- if (status) action.durations.sleep = 0;
367
- action.durations.confusion = 0;
368
- action.durations.disable = 0;
369
- } else {
370
- action.durations.attacking = 0;
371
- action.durations.binding = 0;
372
- }
431
+ assert(!showdown);
432
+ self.pending = .{};
373
433
  }
374
434
 
375
- pub fn switched(self: *Self, player: Player, in: u8, out: u8) void {
435
+ pub fn switched(self: *Self, player: Player, slot: u8) void {
376
436
  if (!enabled) return;
377
437
 
378
- assert(in >= 1 and in <= 6);
379
- assert(out >= 1 and out <= 6);
438
+ assert(slot >= 1 and slot <= 6);
380
439
 
381
- var action = self.actions.get(player);
440
+ var d = self.durations.get(player);
382
441
 
383
- const slp = action.durations.sleep;
384
- action.durations = .{};
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);
385
445
 
386
- self.sleeps[@intFromEnum(player)][out - 1] = slp;
387
- action.durations.sleep = @intCast(self.sleeps[@intFromEnum(player)][in - 1]);
446
+ d.confusion = 0;
447
+ d.disable = 0;
448
+ d.attacking = 0;
449
+ d.binding = 0;
388
450
 
389
- self.actions.get(player.foe()).durations.binding = 0;
451
+ self.durations.get(player.foe()).binding = 0;
390
452
  }
391
453
 
392
454
  pub fn speedTie(self: *Self, p1: bool) Error!void {
@@ -397,12 +459,17 @@ pub fn Chance(comptime Rational: type) type {
397
459
  self.actions.p2.speed_tie = self.actions.p1.speed_tie;
398
460
  }
399
461
 
400
- pub fn hit(self: *Self, ok: bool, accuracy: u8) void {
462
+ pub fn hit(self: *Self, player: Player, ok: bool, accuracy: uN) Error!void {
401
463
  if (!enabled) return;
402
464
 
403
465
  const p = if (ok) accuracy else @as(u8, @intCast(256 - @as(u9, accuracy)));
404
- self.pending.hit = ok;
405
- self.pending.hit_probablity = p;
466
+ if (showdown) {
467
+ try self.probability.update(p, 256);
468
+ self.actions.get(player).hit = if (ok) .true else .false;
469
+ } else {
470
+ self.pending.hit = ok;
471
+ self.pending.hit_probablity = p;
472
+ }
406
473
  }
407
474
 
408
475
  pub fn criticalHit(self: *Self, player: Player, crit: bool, rate: u8) Error!void {
@@ -480,108 +547,188 @@ pub fn Chance(comptime Rational: type) type {
480
547
  self.actions.get(player).multi_hit = n;
481
548
  }
482
549
 
483
- pub fn duration(
550
+ pub fn duration(self: *Self, player: Player, turns: u4) void {
551
+ if (!enabled) return;
552
+
553
+ self.actions.get(player).duration = if (options.key) 2 else turns;
554
+ }
555
+
556
+ pub fn observe(
484
557
  self: *Self,
485
- comptime field: Duration.Field,
558
+ comptime field: Action.Field,
486
559
  player: Player,
487
- target: Player,
488
- turns: u4,
560
+ obs: Optional(Observation(.confusion)),
489
561
  ) void {
490
562
  if (!enabled) return;
491
563
 
492
- if (!showdown and field == .binding) {
493
- self.pending.binding = if (options.key) 1 else turns;
494
- } else {
495
- var durations = &self.actions.get(target).durations;
496
- assert(@field(durations, @tagName(field)) == 0 or
497
- (field == .confusion and player == target));
498
- @field(durations, @tagName(field)) = 1;
499
- self.actions.get(player).duration = if (options.key) 1 else turns;
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,
500
599
  }
501
600
  }
502
601
 
503
- pub fn sleep(self: *Self, player: Player, turns: u4) Error!void {
602
+ pub fn sleep(self: *Self, player: Player, obs: Optional(Observation(.sleep))) Error!void {
504
603
  if (!enabled) return;
505
604
 
506
- var durations = &self.actions.get(player).durations;
507
- const n = durations.sleep;
508
- if (turns == 0) {
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) {
509
615
  assert(n >= 1 and n <= 7);
510
616
  if (n != 7) try self.probability.update(1, 8 - @as(u4, n));
511
- durations.sleep = 0;
617
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
512
618
  } else {
619
+ assert(obs == .continuing);
513
620
  assert(n >= 1 and n < 7);
514
621
  try self.probability.update(8 - @as(u4, n) - 1, 8 - @as(u4, n));
515
- durations.sleep += 1;
622
+ d.sleeps = Sleeps.set(d.sleeps, 0, n + 1);
516
623
  }
517
624
  }
518
625
 
519
- pub fn confusion(self: *Self, player: Player, turns: u4) Error!void {
626
+ pub fn confusion(
627
+ self: *Self,
628
+ player: Player,
629
+ obs: Optional(Observation(.confusion)),
630
+ ) Error!void {
520
631
  if (!enabled) return;
521
632
 
522
- var durations = &self.actions.get(player).durations;
523
- const n = durations.confusion;
524
- if (turns == 0) {
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) {
525
641
  assert(n >= 2 and n <= 5);
526
642
  if (n != 5) try self.probability.update(1, 6 - @as(u4, n));
527
- durations.confusion = 0;
643
+ d.confusion = 0;
528
644
  } else {
645
+ assert(obs == .continuing);
529
646
  assert(n >= 1 and n < 5);
530
647
  if (n > 1) try self.probability.update(6 - @as(u4, n) - 1, 6 - @as(u4, n));
531
- durations.confusion += 1;
648
+ d.confusion += 1;
532
649
  }
533
650
  }
534
651
 
535
- pub fn disable(self: *Self, player: Player, turns: u4) Error!void {
652
+ pub fn disable(
653
+ self: *Self,
654
+ player: Player,
655
+ obs: Optional(Observation(.disable)),
656
+ ) Error!void {
536
657
  if (!enabled) return;
537
658
 
538
- var durations = &self.actions.get(player).durations;
539
- const n = durations.disable;
540
- if (turns == 0) {
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) {
541
667
  assert(n >= 1 and n <= 8);
542
668
  if (n != 8) try self.probability.update(1, 9 - @as(u4, n));
543
- durations.disable = 0;
669
+ d.disable = 0;
544
670
  } else {
671
+ assert(obs == .continuing);
545
672
  assert(n >= 1 and n < 8);
546
673
  try self.probability.update(9 - @as(u4, n) - 1, 9 - @as(u4, n));
547
- durations.disable += 1;
674
+ d.disable += 1;
548
675
  }
549
676
  }
550
677
 
551
- pub fn attacking(self: *Self, player: Player, turns: u4) Error!void {
678
+ pub fn attacking(
679
+ self: *Self,
680
+ player: Player,
681
+ obs: Optional(Observation(.attacking)),
682
+ ) Error!void {
552
683
  if (!enabled) return;
553
684
 
554
- var durations = &self.actions.get(player).durations;
555
- const n = durations.attacking;
556
- if (turns == 0) {
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
557
693
  assert(n >= 2 and n <= 3);
558
694
  if (n != 3) try self.probability.update(1, 4 - @as(u4, n));
559
- durations.attacking = 0;
695
+ d.attacking = 0;
560
696
  } else {
697
+ assert(obs == .continuing);
561
698
  assert(n >= 1 and n < 3);
562
699
  if (n > 1) try self.probability.update(4 - @as(u4, n) - 1, 4 - @as(u4, n));
563
- durations.attacking += 1;
700
+ d.attacking += 1;
564
701
  }
565
702
  }
566
703
 
567
- pub fn binding(self: *Self, player: Player, turns: u4) Error!void {
704
+ pub fn binding(
705
+ self: *Self,
706
+ player: Player,
707
+ obs: Optional(Observation(.binding)),
708
+ ) Error!void {
568
709
  if (!enabled) return;
569
710
 
570
- var durations = &self.actions.get(player).durations;
571
- const n = durations.binding;
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;
572
717
 
573
718
  assert(n > 0);
574
719
  const p: u4 = if (n < 3) 3 else 1;
575
720
  const q: u4 = if (n < 3) 8 - ((n - 1) * p) else 2;
576
721
 
577
- if (turns == 0) {
722
+ if (obs == .ended) {
723
+ // "2-5 turns" includes the turn for the initial attack *plus* the forced attacks
578
724
  assert(n >= 1 and n <= 4);
579
725
  if (n != 4) try self.probability.update(p, q);
580
- durations.binding = 0;
726
+ d.binding = 0;
581
727
  } else {
728
+ assert(obs == .continuing);
582
729
  assert(n >= 1 and n < 4);
583
730
  try self.probability.update(q - p, q);
584
- durations.binding += 1;
731
+ d.binding += 1;
585
732
  }
586
733
  }
587
734
 
@@ -598,6 +745,15 @@ pub fn Chance(comptime Rational: type) type {
598
745
  try self.probability.update(1, Move.METRONOME.len);
599
746
  self.actions.get(player).metronome = move;
600
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
+ }
601
757
  };
602
758
  }
603
759
 
@@ -620,19 +776,25 @@ test "Chance.speedTie" {
620
776
  test "Chance.hit" {
621
777
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
622
778
 
623
- chance.hit(true, 229);
624
- try expectProbability(&chance.probability, 1, 1);
625
- try expectValue(Optional(bool).None, chance.actions.p1.hit);
626
- try chance.commit(.P1, .hit);
779
+ try chance.hit(.P1, true, 229);
780
+ if (!showdown) {
781
+ try expectProbability(&chance.probability, 1, 1);
782
+ try expectValue(Optional(bool).None, chance.actions.p1.hit);
783
+
784
+ try chance.commit(.P1, .hit);
785
+ }
627
786
  try expectValue(Optional(bool).true, chance.actions.p1.hit);
628
787
  try expectProbability(&chance.probability, 229, 256);
629
788
 
630
789
  chance.reset();
631
790
 
632
- chance.hit(false, 255);
633
- try expectProbability(&chance.probability, 1, 1);
634
- try expectValue(Optional(bool).None, chance.actions.p2.hit);
635
- try chance.commit(.P2, .miss);
791
+ try chance.hit(.P2, false, 255);
792
+ if (!showdown) {
793
+ try expectProbability(&chance.probability, 1, 1);
794
+ try expectValue(Optional(bool).None, chance.actions.p2.hit);
795
+
796
+ try chance.commit(.P2, .miss);
797
+ }
636
798
  try expectValue(Optional(bool).false, chance.actions.p2.hit);
637
799
  try expectProbability(&chance.probability, 1, 256);
638
800
  }
@@ -648,26 +810,18 @@ test "Chance.criticalHit" {
648
810
  try expectProbability(&chance.probability, 1, 1);
649
811
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
650
812
 
651
- try chance.commit(.P1, .hit);
652
- try expectProbability(&chance.probability, 1, 1);
653
- try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
654
-
655
- chance.hit(true, 128);
813
+ try chance.hit(.P1, true, 128);
656
814
 
657
- try chance.commit(.P1, .hit);
658
- try expectProbability(&chance.probability, 1, 2);
815
+ try expectProbability(&chance.probability, 1, 1);
659
816
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
660
817
 
661
- chance.probability.reset();
662
818
  try chance.damage(.P1, 217);
663
819
 
664
- try chance.commit(.P1, .miss);
665
- try expectProbability(&chance.probability, 1, 2);
820
+ try expectProbability(&chance.probability, 1, 1);
666
821
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
667
822
 
668
- chance.probability.reset();
669
-
670
823
  try chance.commit(.P1, .hit);
824
+
671
825
  try expectProbability(&chance.probability, 17, 19968); // (1/2) * (17/256) * (1/39)
672
826
  try expectValue(Optional(bool).true, chance.actions.p1.critical_hit);
673
827
  }
@@ -682,20 +836,14 @@ test "Chance.criticalHit" {
682
836
  try expectProbability(&chance.probability, 1, 1);
683
837
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
684
838
 
685
- try chance.commit(.P1, .hit);
686
- try expectProbability(&chance.probability, 1, 1);
687
- try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
688
-
689
- chance.hit(true, 128);
839
+ try chance.hit(.P1, true, 128);
690
840
 
691
- try chance.commit(.P1, .hit);
692
- try expectProbability(&chance.probability, 1, 2);
841
+ try expectProbability(&chance.probability, 1, 1);
693
842
  try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
694
843
 
695
- chance.probability.reset();
696
844
  try chance.damage(.P1, 217);
697
-
698
845
  try chance.commit(.P1, .hit);
846
+
699
847
  try expectProbability(&chance.probability, 251, 19968); // (1/2) * (251/256) * (1/39)
700
848
  try expectValue(Optional(bool).false, chance.actions.p1.critical_hit);
701
849
  }
@@ -712,11 +860,8 @@ test "Chance.damage" {
712
860
  try expectProbability(&chance.probability, 1, 1);
713
861
  try expectValue(@as(u8, 0), chance.actions.p1.damage);
714
862
 
715
- try chance.commit(.P1, .miss);
716
- try expectProbability(&chance.probability, 1, 1);
717
- try expectValue(@as(u8, 0), chance.actions.p1.damage);
718
-
719
863
  try chance.commit(.P1, .hit);
864
+
720
865
  try expectProbability(&chance.probability, 1, 39);
721
866
  try expectValue(@as(u8, 217), chance.actions.p1.damage);
722
867
  }
@@ -770,13 +915,13 @@ test "Chance.moveSlot" {
770
915
 
771
916
  try chance.moveSlot(.P2, 2, &ms, 2);
772
917
  try expectProbability(&chance.probability, 1, 2);
773
- try expectValue(@as(u4, 2), chance.actions.p2.move_slot);
918
+ try expectValue(@as(u3, 2), chance.actions.p2.move_slot);
774
919
 
775
920
  chance.reset();
776
921
 
777
922
  try chance.moveSlot(.P1, 1, &ms, 0);
778
923
  try expectProbability(&chance.probability, 1, 3);
779
- try expectValue(@as(u4, 1), chance.actions.p1.move_slot);
924
+ try expectValue(@as(u3, 1), chance.actions.p1.move_slot);
780
925
  }
781
926
 
782
927
  test "Chance.multiHit" {
@@ -796,38 +941,31 @@ test "Chance.multiHit" {
796
941
  test "Chance.duration" {
797
942
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
798
943
 
799
- chance.duration(.sleep, .P1, .P2, 2);
944
+ chance.duration(.P1, 2);
800
945
  try expectValue(@as(u4, 2), chance.actions.p1.duration);
801
- try expectValue(@as(u3, 1), chance.actions.p2.durations.sleep);
802
946
 
803
947
  chance.reset();
804
948
 
805
- chance.duration(.binding, .P2, .P2, 4);
806
- if (!showdown) {
807
- try expectValue(@as(u4, 0), chance.actions.p2.duration);
808
- try expectValue(@as(u3, 0), chance.actions.p2.durations.binding);
809
- try chance.commit(.P2, .hit);
810
- }
949
+ chance.duration(.P2, 4);
811
950
  try expectValue(@as(u4, 4), chance.actions.p2.duration);
812
- try expectValue(@as(u3, 1), chance.actions.p2.durations.binding);
813
951
  }
814
952
 
815
953
  test "Chance.sleep" {
816
954
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
817
955
 
818
956
  for ([_]u8{ 7, 6, 5, 4, 3, 2, 1 }, 1..8) |d, i| {
819
- chance.actions.p1.durations.sleep = @intCast(i);
820
- try chance.sleep(.P1, 0);
957
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
958
+ try chance.sleep(.P1, .ended);
821
959
  try expectProbability(&chance.probability, 1, d);
822
- try expectValue(@as(u3, 0), chance.actions.p1.durations.sleep);
960
+ try expectValue(@as(u3, 0), Sleeps.get(chance.durations.p1.sleeps, 0));
823
961
 
824
962
  chance.reset();
825
963
 
826
964
  if (i < 7) {
827
- chance.actions.p1.durations.sleep = @intCast(i);
828
- try chance.sleep(.P1, 1);
965
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
966
+ try chance.sleep(.P1, .continuing);
829
967
  try expectProbability(&chance.probability, d - 1, d);
830
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p1.durations.sleep);
968
+ try expectValue(@as(u3, @intCast(i)) + 1, Sleeps.get(chance.durations.p1.sleeps, 0));
831
969
 
832
970
  chance.reset();
833
971
  }
@@ -839,19 +977,19 @@ test "Chance.confusion" {
839
977
 
840
978
  for ([_]u8{ 1, 4, 3, 2, 1 }, 1..6) |d, i| {
841
979
  if (i > 1) {
842
- chance.actions.p2.durations.confusion = @intCast(i);
843
- try chance.confusion(.P2, 0);
980
+ chance.durations.p2.confusion = @intCast(i);
981
+ try chance.confusion(.P2, .ended);
844
982
  try expectProbability(&chance.probability, 1, d);
845
- try expectValue(@as(u3, 0), chance.actions.p2.durations.confusion);
983
+ try expectValue(@as(u3, 0), chance.durations.p2.confusion);
846
984
 
847
985
  chance.reset();
848
986
  }
849
987
 
850
988
  if (i < 5) {
851
- chance.actions.p2.durations.confusion = @intCast(i);
852
- try chance.confusion(.P2, 1);
989
+ chance.durations.p2.confusion = @intCast(i);
990
+ try chance.confusion(.P2, .continuing);
853
991
  try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
854
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p2.durations.confusion);
992
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.confusion);
855
993
 
856
994
  chance.reset();
857
995
  }
@@ -862,18 +1000,18 @@ test "Chance.disable" {
862
1000
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
863
1001
 
864
1002
  for ([_]u8{ 8, 7, 6, 5, 4, 3, 2, 1 }, 1..9) |d, i| {
865
- chance.actions.p1.durations.disable = @intCast(i);
866
- try chance.disable(.P1, 0);
1003
+ chance.durations.p1.disable = @intCast(i);
1004
+ try chance.disable(.P1, .ended);
867
1005
  try expectProbability(&chance.probability, 1, d);
868
- try expectValue(@as(u4, 0), chance.actions.p1.durations.disable);
1006
+ try expectValue(@as(u4, 0), chance.durations.p1.disable);
869
1007
 
870
1008
  chance.reset();
871
1009
 
872
1010
  if (i < 8) {
873
- chance.actions.p1.durations.disable = @intCast(i);
874
- try chance.disable(.P1, 1);
1011
+ chance.durations.p1.disable = @intCast(i);
1012
+ try chance.disable(.P1, .continuing);
875
1013
  try expectProbability(&chance.probability, d - 1, d);
876
- try expectValue(@as(u4, @intCast(i)) + 1, chance.actions.p1.durations.disable);
1014
+ try expectValue(@as(u4, @intCast(i)) + 1, chance.durations.p1.disable);
877
1015
 
878
1016
  chance.reset();
879
1017
  }
@@ -885,19 +1023,19 @@ test "Chance.attacking" {
885
1023
 
886
1024
  for ([_]u8{ 1, 2, 1 }, 1..4) |d, i| {
887
1025
  if (i > 1) {
888
- chance.actions.p2.durations.attacking = @intCast(i);
889
- try chance.attacking(.P2, 0);
1026
+ chance.durations.p2.attacking = @intCast(i);
1027
+ try chance.attacking(.P2, .ended);
890
1028
  try expectProbability(&chance.probability, 1, d);
891
- try expectValue(@as(u3, 0), chance.actions.p2.durations.attacking);
1029
+ try expectValue(@as(u3, 0), chance.durations.p2.attacking);
892
1030
 
893
1031
  chance.reset();
894
1032
  }
895
1033
 
896
1034
  if (i < 3) {
897
- chance.actions.p2.durations.attacking = @intCast(i);
898
- try chance.attacking(.P2, 1);
1035
+ chance.durations.p2.attacking = @intCast(i);
1036
+ try chance.attacking(.P2, .continuing);
899
1037
  try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
900
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p2.durations.attacking);
1038
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.attacking);
901
1039
 
902
1040
  chance.reset();
903
1041
  }
@@ -911,18 +1049,18 @@ test "Chance.binding" {
911
1049
  const qs = [_]u8{ 8, 5, 2, 1 };
912
1050
 
913
1051
  for (ps, qs, 1..5) |p, q, i| {
914
- chance.actions.p1.durations.binding = @intCast(i);
915
- try chance.binding(.P1, 0);
1052
+ chance.durations.p1.binding = @intCast(i);
1053
+ try chance.binding(.P1, .ended);
916
1054
  try expectProbability(&chance.probability, p, q);
917
- try expectValue(@as(u3, 0), chance.actions.p1.durations.binding);
1055
+ try expectValue(@as(u3, 0), chance.durations.p1.binding);
918
1056
 
919
1057
  chance.reset();
920
1058
 
921
1059
  if (i < 4) {
922
- chance.actions.p1.durations.binding = @intCast(i);
923
- try chance.binding(.P1, 1);
1060
+ chance.durations.p1.binding = @intCast(i);
1061
+ try chance.binding(.P1, .continuing);
924
1062
  try expectProbability(&chance.probability, q - p, q);
925
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p1.durations.binding);
1063
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p1.binding);
926
1064
 
927
1065
  chance.reset();
928
1066
  }
@@ -950,7 +1088,7 @@ pub fn expectProbability(r: anytype, p: u64, q: u64) !void {
950
1088
 
951
1089
  r.reduce();
952
1090
  if (r.p != p or r.q != q) {
953
- print("expected {d}/{d}, found {}\n", .{ p, q, r });
1091
+ print("expected {d}/{d}, found {f}\n", .{ p, q, r });
954
1092
  return error.TestExpectedEqual;
955
1093
  }
956
1094
  }
@@ -972,24 +1110,24 @@ const Null = struct {
972
1110
  _ = .{ self, player, kind };
973
1111
  }
974
1112
 
975
- pub fn clearPending(self: Null) void {
1113
+ pub fn glitch(self: Null) void {
976
1114
  _ = .{self};
977
1115
  }
978
1116
 
979
- pub fn clearDurations(self: Null, player: Player, haze: ?bool) void {
980
- _ = .{ self, player, haze };
1117
+ pub fn clearPending(self: Null) void {
1118
+ _ = .{self};
981
1119
  }
982
1120
 
983
- pub fn switched(self: Null, player: Player, in: u8, out: u8) void {
984
- _ = .{ self, player, in, out };
1121
+ pub fn switched(self: Null, player: Player, slot: u8) void {
1122
+ _ = .{ self, player, slot };
985
1123
  }
986
1124
 
987
1125
  pub fn speedTie(self: Null, p1: bool) Error!void {
988
1126
  _ = .{ self, p1 };
989
1127
  }
990
1128
 
991
- pub fn hit(self: Null, ok: bool, accuracy: u8) void {
992
- _ = .{ self, ok, accuracy };
1129
+ pub fn hit(self: Null, player: Player, ok: bool, accuracy: uN) Error!void {
1130
+ _ = .{ self, player, ok, accuracy };
993
1131
  }
994
1132
 
995
1133
  pub fn criticalHit(self: Null, player: Player, crit: bool, rate: u8) Error!void {
@@ -1020,34 +1158,53 @@ const Null = struct {
1020
1158
  _ = .{ self, player, n };
1021
1159
  }
1022
1160
 
1023
- pub fn duration(
1161
+ pub fn duration(self: Null, player: Player, turns: u4) void {
1162
+ _ = .{ self, player, turns };
1163
+ }
1164
+
1165
+ pub fn observe(
1024
1166
  self: Null,
1025
- comptime field: Duration.Field,
1167
+ comptime field: Action.Field,
1026
1168
  player: Player,
1027
- target: Player,
1028
- turns: u4,
1169
+ obs: Optional(Observation(.confusion)),
1029
1170
  ) void {
1030
- _ = .{ self, field, player, target, turns };
1171
+ _ = .{ self, field, player, obs };
1031
1172
  }
1032
1173
 
1033
- pub fn sleep(self: Null, player: Player, turns: u4) Error!void {
1034
- _ = .{ self, player, turns };
1174
+ pub fn sleep(self: Null, player: Player, obs: Optional(Observation(.sleep))) Error!void {
1175
+ _ = .{ self, player, obs };
1035
1176
  }
1036
1177
 
1037
- pub fn disable(self: Null, player: Player, turns: u4) Error!void {
1038
- _ = .{ self, player, turns };
1178
+ pub fn confusion(
1179
+ self: Null,
1180
+ player: Player,
1181
+ obs: Optional(Observation(.confusion)),
1182
+ ) Error!void {
1183
+ _ = .{ self, player, obs };
1039
1184
  }
1040
1185
 
1041
- pub fn confusion(self: Null, player: Player, turns: u4) Error!void {
1042
- _ = .{ self, player, turns };
1186
+ pub fn disable(
1187
+ self: Null,
1188
+ player: Player,
1189
+ obs: Optional(Observation(.disable)),
1190
+ ) Error!void {
1191
+ _ = .{ self, player, obs };
1043
1192
  }
1044
1193
 
1045
- pub fn attacking(self: Null, player: Player, turns: u4) Error!void {
1046
- _ = .{ self, player, turns };
1194
+ pub fn attacking(
1195
+ self: Null,
1196
+ player: Player,
1197
+ obs: Optional(Observation(.attacking)),
1198
+ ) Error!void {
1199
+ _ = .{ self, player, obs };
1047
1200
  }
1048
1201
 
1049
- pub fn binding(self: Null, player: Player, turns: u4) Error!void {
1050
- _ = .{ self, player, turns };
1202
+ pub fn binding(
1203
+ self: Null,
1204
+ player: Player,
1205
+ obs: Optional(Observation(.binding)),
1206
+ ) Error!void {
1207
+ _ = .{ self, player, obs };
1051
1208
  }
1052
1209
 
1053
1210
  pub fn psywave(self: Null, player: Player, power: u8, max: u8) Error!void {
@@ -1057,4 +1214,9 @@ const Null = struct {
1057
1214
  pub fn metronome(self: Null, player: Player, move: Move) Error!void {
1058
1215
  _ = .{ self, player, move };
1059
1216
  }
1217
+
1218
+ pub fn pp(self: Null, player: Player, n: u4) bool {
1219
+ _ = .{ self, player, n };
1220
+ return true;
1221
+ }
1060
1222
  };