@pkmn/engine 0.1.0-dev.cf6f5d2c → 0.1.0-dev.d127cb34

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 (87) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +112 -66
  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 -70
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/ids.json +12 -12
  16. package/build/pkg/data/layout.json +40 -5
  17. package/build/pkg/data/protocol.json +84 -13
  18. package/build/pkg/data.d.ts +27 -5
  19. package/build/pkg/data.js.map +1 -1
  20. package/build/pkg/gen1.d.ts +17 -19
  21. package/build/pkg/gen1.js +66 -39
  22. package/build/pkg/gen1.js.map +1 -1
  23. package/build/pkg/index.d.ts +46 -93
  24. package/build/pkg/index.js +23 -70
  25. package/build/pkg/index.js.map +1 -1
  26. package/build/pkg/protocol.d.ts +33 -3
  27. package/build/pkg/protocol.js +127 -104
  28. package/build/pkg/protocol.js.map +1 -1
  29. package/build/tools/debug.js +84 -73
  30. package/build/tools/debug.js.map +1 -1
  31. package/build/tools/display/debug.jsx +66 -0
  32. package/build/tools/display/debug.jsx.map +1 -0
  33. package/build/tools/display/index.js +152 -0
  34. package/build/tools/display/index.js.map +1 -0
  35. package/build/tools/display/ui.jsx +360 -0
  36. package/build/tools/display/ui.jsx.map +1 -0
  37. package/build/tools/display/util.js +157 -0
  38. package/build/tools/display/util.js.map +1 -0
  39. package/build.zig +414 -174
  40. package/package.json +72 -122
  41. package/src/bin/install-pkmn-engine +56 -25
  42. package/src/bin/pkmn-debug +1 -1
  43. package/src/lib/bindings/c.zig +201 -0
  44. package/src/lib/bindings/node.zig +117 -0
  45. package/src/lib/bindings/wasm.zig +51 -0
  46. package/src/lib/common/array.zig +70 -0
  47. package/src/lib/common/battle.zig +17 -0
  48. package/src/lib/common/data.zig +19 -31
  49. package/src/lib/common/debug.zig +58 -33
  50. package/src/lib/common/js.zig +114 -0
  51. package/src/lib/common/optional.zig +71 -0
  52. package/src/lib/common/options.zig +15 -0
  53. package/src/lib/common/protocol.zig +1037 -462
  54. package/src/lib/common/rational.zig +440 -0
  55. package/src/lib/common/rng.zig +31 -24
  56. package/src/lib/common/util.zig +64 -0
  57. package/src/lib/gen1/calc.zig +975 -0
  58. package/src/lib/gen1/chance.zig +1245 -0
  59. package/src/lib/gen1/data/moves.zig +203 -193
  60. package/src/lib/gen1/data/species.zig +6 -8
  61. package/src/lib/gen1/data/types.zig +12 -13
  62. package/src/lib/gen1/data.zig +100 -89
  63. package/src/lib/gen1/helpers.zig +162 -68
  64. package/src/lib/gen1/mechanics.zig +1237 -848
  65. package/src/lib/gen2/calc.zig +150 -0
  66. package/src/lib/gen2/chance.zig +914 -0
  67. package/src/lib/gen2/data/items.zig +15 -23
  68. package/src/lib/gen2/data/moves.zig +665 -573
  69. package/src/lib/gen2/data/species.zig +3 -5
  70. package/src/lib/gen2/data/types.zig +141 -36
  71. package/src/lib/gen2/data.zig +136 -100
  72. package/src/lib/gen2/generated.zig +801 -0
  73. package/src/lib/gen2/helpers.zig +81 -58
  74. package/src/lib/gen2/mechanics.zig +3582 -30
  75. package/src/lib/napi.h +2 -0
  76. package/src/lib/node.zig +14 -0
  77. package/src/lib/pkmn.zig +172 -17
  78. package/src/lib/wasm.zig +13 -0
  79. package/src/tools/display/dom.ts +108 -0
  80. package/src/tools/display/ui.css +354 -0
  81. package/build/test/display.js +0 -396
  82. package/build/test/display.js.map +0 -1
  83. package/src/lib/binding/node.zig +0 -229
  84. package/src/lib/binding/wasm.zig +0 -10
  85. package/src/lib/gen1/README.md +0 -804
  86. package/src/lib/gen2/README.md +0 -309
  87. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -0,0 +1,1245 @@
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");
7
+ const options = @import("../common/options.zig");
8
+ const rational = @import("../common/rational.zig");
9
+ const std = @import("std");
10
+ const util = @import("../common/util.zig");
11
+
12
+ const Array = array.Array;
13
+ const assert = std.debug.assert;
14
+ const enabled = options.chance;
15
+ const expect = std.testing.expect;
16
+ const expectEqual = std.testing.expectEqual;
17
+ const isPointerTo = util.isPointerTo;
18
+ const Move = data.Move;
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;
25
+
26
+ /// Actions taken by a hypothetical "chance player" that convey information about which RNG events
27
+ /// were observed during a Generation I battle `update`. This can additionally be provided as input
28
+ /// to the `update` call to override the normal behavior of the RNG in order to force specific
29
+ /// outcomes.
30
+ pub const Actions = extern struct {
31
+ /// Information about the RNG activity for Player 1.
32
+ p1: Action = .{},
33
+ /// Information about the RNG activity for Player 2.
34
+ p2: Action = .{},
35
+
36
+ comptime {
37
+ assert(@sizeOf(Actions) == 16);
38
+ }
39
+
40
+ /// Returns the `Action` for the given `player`.
41
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Action) {
42
+ assert(isPointerTo(self, Actions));
43
+ return if (player == .P1) &self.p1 else &self.p2;
44
+ }
45
+
46
+ /// Returns true if `a` is equal to `b`.
47
+ pub fn eql(a: Actions, b: Actions) bool {
48
+ return @as(u128, @bitCast(a)) == @as(u128, @bitCast(b));
49
+ }
50
+
51
+ /// Returns true if `a` has the same "shape" as `b`, where `Actions` are defined to have the
52
+ /// same shape if they have the same fields set (though those fields need not necessarily be
53
+ /// set to the same value).
54
+ pub fn matches(a: Actions, b: Actions) bool {
55
+ inline for (if (@hasField(@TypeOf(@typeInfo(Actions).@"struct"), "fields"))
56
+ @typeInfo(Actions).@"struct".fields
57
+ else
58
+ @typeInfo(Actions).@"struct".field_names) |player|
59
+ {
60
+ const player_name = if (@hasField(@TypeOf(player), "name"))
61
+ player.name
62
+ else
63
+ player;
64
+ inline for (if (@hasField(@TypeOf(@typeInfo(Action).@"struct"), "fields"))
65
+ @typeInfo(Action).@"struct".fields
66
+ else
67
+ @typeInfo(Action).@"struct".field_names) |field|
68
+ {
69
+ const field_name = if (@hasField(@TypeOf(field), "name")) field.name else field;
70
+ const a_val = @field(@field(a, player_name), field_name);
71
+ const b_val = @field(@field(b, player_name), field_name);
72
+
73
+ switch (@typeInfo(@TypeOf(a_val))) {
74
+ .@"enum" => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
75
+ return false,
76
+ .int => if ((a_val > 0) != (b_val > 0)) return false,
77
+ else => unreachable,
78
+ }
79
+ }
80
+ }
81
+ return true;
82
+ }
83
+
84
+ /// TODO
85
+ pub fn matchesAny(a: Actions, bs: []Actions, i: usize) bool {
86
+ for (bs, 0..) |b, j| {
87
+ // TODO: is skipping this redundant check worth it?
88
+ if (i == j) continue;
89
+ if (b.matches(a)) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ /// TODO
95
+ pub fn relax(actions: Actions) Actions {
96
+ if (!options.overwrite) return actions;
97
+ var a = actions;
98
+ if (a.p1.confusion == .overwritten) a.p1.confusion = .continuing;
99
+ if (a.p2.confusion == .overwritten) a.p2.confusion = .continuing;
100
+ return a;
101
+ }
102
+
103
+ pub fn fmt(self: Actions, writer: *std.Io.Writer, shape: bool) !void {
104
+ try writer.writeAll("<P1 = ");
105
+ try self.p1.fmt(writer, shape);
106
+ try writer.writeAll(", P2 = ");
107
+ try self.p2.fmt(writer, shape);
108
+ try writer.writeAll(">");
109
+ }
110
+
111
+ pub fn format(a: Actions, w: *std.Io.Writer) !void {
112
+ try fmt(a, w, false);
113
+ }
114
+ };
115
+
116
+ test Actions {
117
+ const a: Actions = .{ .p1 = .{ .hit = .true, .critical_hit = .false, .damage = 245 } };
118
+ const b: Actions = .{ .p1 = .{ .hit = .false, .critical_hit = .true, .damage = 246 } };
119
+ const c: Actions = .{ .p1 = .{ .hit = .true } };
120
+
121
+ try expect(a.eql(a));
122
+ try expect(!a.eql(b));
123
+ try expect(!b.eql(a));
124
+ try expect(!a.eql(c));
125
+ try expect(!c.eql(a));
126
+
127
+ try expect(a.matches(a));
128
+ try expect(a.matches(b));
129
+ try expect(b.matches(a));
130
+ try expect(!a.matches(c));
131
+ try expect(!c.matches(a));
132
+ }
133
+
134
+ /// Observation made about a duration - whether the duration has started, been continued, or ended.
135
+ pub fn Observation(comptime field: Duration.Field) type {
136
+ return switch (field) {
137
+ .attacking, .binding => enum(u2) {
138
+ started,
139
+ continuing,
140
+ ended,
141
+ },
142
+ .sleep, .disable => enum(u3) {
143
+ started,
144
+ continuing,
145
+ ended,
146
+ // TODO haze support
147
+ unused1,
148
+ unused2,
149
+ unused3,
150
+ unused4,
151
+ },
152
+ .confusion => enum(u4) {
153
+ started,
154
+ continuing,
155
+ ended,
156
+ /// An indicator that an existing confusion duration has been overwritten by a new
157
+ /// confusion duration due to a Thrashing move ending ("hidden" confusion). This is not
158
+ /// immediately observable to an opponent, to compute probabilities from an opponent's
159
+ /// point of view based solely on public information extra work must be done to correct
160
+ /// for the information leak
161
+ overwritten,
162
+ // TODO haze support
163
+ unused1,
164
+ unused2,
165
+ unused3,
166
+ unused4,
167
+ },
168
+ };
169
+ }
170
+
171
+ /// Information about the RNG that was observed during a Generation I battle `update` for a
172
+ /// single player.
173
+ pub const Action = packed struct(u64) {
174
+ /// If not 0, the roll to be returned Rolls.damage.
175
+ damage: u8 = 0,
176
+
177
+ /// If not None, the value to return for Rolls.hit.
178
+ hit: Optional(bool) = .None,
179
+ /// If not None, the value to be returned by Rolls.criticalHit.
180
+ critical_hit: Optional(bool) = .None,
181
+ /// If not None, the value to be returned for
182
+ /// Rolls.{confusionChance,secondaryChance,poisonChance}.
183
+ secondary_chance: Optional(bool) = .None,
184
+ /// If not None, the Player to be returned by Rolls.speedTie.
185
+ speed_tie: Optional(Player) = .None,
186
+
187
+ /// If not None, the value to return for Rolls.confused.
188
+ confused: Optional(bool) = .None,
189
+ /// If not None, the value to return for Rolls.paralyzed.
190
+ paralyzed: Optional(bool) = .None,
191
+ /// If not 0, the value to be returned by Rolls.distribution in the case of binding moves
192
+ /// or Rolls.{sleepDuration,disableDuration,confusionDuration,attackingDuration} otherwise.
193
+ duration: u4 = 0,
194
+
195
+ /// TODO
196
+ sleep: Optional(Observation(.sleep)) = .None,
197
+ /// TODO
198
+ confusion: Optional(Observation(.confusion)) = .None,
199
+ /// TODO
200
+ disable: Optional(Observation(.disable)) = .None,
201
+ /// TODO
202
+ attacking: Optional(Observation(.attacking)) = .None,
203
+ /// TODO
204
+ binding: Optional(Observation(.binding)) = .None,
205
+
206
+ /// If not 0, the move slot (1-4) to return in Rolls.moveSlot. If present as an override,
207
+ /// invalid values (eg. due to empty move slots or 0 PP) will be ignored.
208
+ move_slot: u3 = 0,
209
+ /// If not 0, the value (2-5) to return for Rolls.distribution for multi hit.
210
+ multi_hit: u3 = 0,
211
+
212
+ /// TODO
213
+ pp: u4 = 0,
214
+
215
+ /// If not 0, psywave - 1 should be returned as the damage roll for Rolls.psywave.
216
+ psywave: u8 = 0,
217
+
218
+ /// If not None, the Move to return for Rolls.metronome.
219
+ metronome: Move = .None,
220
+
221
+ pub const Field = std.meta.FieldEnum(Action);
222
+
223
+ pub fn format(a: Action, w: *std.Io.Writer) !void {
224
+ try fmt(a, w, false);
225
+ }
226
+
227
+ const SYMBOLS = [_][]const u8{ "+", "", "-", "#" };
228
+
229
+ pub fn fmt(self: Action, writer: *std.Io.Writer, shape: bool) !void {
230
+ try writer.writeByte('(');
231
+ var printed = false;
232
+ inline for (if (@hasField(@TypeOf(@typeInfo(Action).@"struct"), "fields"))
233
+ @typeInfo(Action).@"struct".fields
234
+ else
235
+ @typeInfo(Action).@"struct".field_names) |field|
236
+ {
237
+ const field_name = if (@hasField(@TypeOf(field), "name")) field.name else field;
238
+ const val = @field(self, field_name);
239
+ switch (@typeInfo(@TypeOf(val))) {
240
+ .@"enum" => if (val != .None) {
241
+ if (printed) try writer.writeAll(", ");
242
+ if (shape) {
243
+ try writer.print("{s}:?", .{field_name});
244
+ } else if (@TypeOf(val) == Optional(bool)) {
245
+ try writer.print("{s}{s}", .{
246
+ if (val == .false) "!" else "",
247
+ field_name,
248
+ });
249
+ } else if (@TypeOf(val) != Move and @TypeOf(val) != Optional(Player)) {
250
+ try writer.print("{s}{s}", .{ SYMBOLS[@intFromEnum(val) - 1], field_name });
251
+ } else {
252
+ try writer.print("{s}:{s}", .{ field_name, @tagName(val) });
253
+ }
254
+ printed = true;
255
+ },
256
+ .int => if (val != 0) {
257
+ if (printed) try writer.writeAll(", ");
258
+ if (shape) {
259
+ try writer.print("{s}:?", .{field_name});
260
+ } else {
261
+ try writer.print("{s}:{d}", .{ field_name, val });
262
+ }
263
+ printed = true;
264
+ },
265
+ else => unreachable,
266
+ }
267
+ }
268
+ try writer.writeByte(')');
269
+ }
270
+ };
271
+
272
+ /// TODO
273
+ pub const Durations = extern struct {
274
+ /// TODO
275
+ p1: Duration = .{},
276
+ /// TODO
277
+ p2: Duration = .{},
278
+
279
+ comptime {
280
+ assert(@sizeOf(Durations) == 8);
281
+ }
282
+
283
+ /// TODO
284
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Duration) {
285
+ assert(isPointerTo(self, Durations));
286
+ return if (player == .P1) &self.p1 else &self.p2;
287
+ }
288
+
289
+ pub fn format(self: Durations, writer: *std.Io.Writer) !void {
290
+ try writer.print("[P1 = {f}, P2 = {f}]", .{ self.p1, self.p2 });
291
+ }
292
+ };
293
+
294
+ pub const Sleeps = Array(6, u3);
295
+
296
+ /// TODO
297
+ pub const Duration = packed struct(u32) {
298
+ sleeps: Sleeps.T = 0,
299
+ confusion: u3 = 0,
300
+ disable: u4 = 0,
301
+ attacking: u3 = 0,
302
+ binding: u3 = 0,
303
+
304
+ _: u1 = 0,
305
+
306
+ pub const Field = enum { attacking, binding, sleep, disable, confusion };
307
+
308
+ pub fn format(
309
+ self: Duration,
310
+ writer: *std.Io.Writer,
311
+ ) !void {
312
+ try writer.writeByte('(');
313
+ var printed = false;
314
+ inline for (if (@hasField(@TypeOf(@typeInfo(Duration).@"struct"), "fields"))
315
+ @typeInfo(Duration).@"struct".fields
316
+ else
317
+ @typeInfo(Duration).@"struct".field_names) |field|
318
+ {
319
+ const field_name = if (@hasField(@TypeOf(field), "name")) field.name else field;
320
+ const val = @field(self, field_name);
321
+ switch (@typeInfo(@TypeOf(val))) {
322
+ .int => if (val != 0) {
323
+ if (printed) try writer.writeAll(", ");
324
+ if (comptime std.mem.eql(u8, field_name, "sleeps")) {
325
+ try writer.print("{s}:[", .{field_name});
326
+ for (0..6) |i| {
327
+ if (i != 0) try writer.writeByte(',');
328
+ try writer.print("{d}", .{Sleeps.get(val, i)});
329
+ }
330
+ try writer.writeAll("]");
331
+ } else {
332
+ try writer.print("{s}:{d}", .{ field_name, val });
333
+ }
334
+ printed = true;
335
+ },
336
+ else => unreachable,
337
+ }
338
+ }
339
+ try writer.writeByte(')');
340
+ }
341
+ };
342
+
343
+ pub const Commit = enum { hit, miss, binding, err, glitch };
344
+
345
+ const uN = if (options.miss) u8 else u9;
346
+
347
+ /// Tracks chance actions and their associated probability during a Generation I battle update when
348
+ /// `options.chance` is enabled.
349
+ pub fn Chance(comptime Rational: type) type {
350
+ return struct {
351
+ const Self = @This();
352
+
353
+ /// The probability of the actions taken by a hypothetical "chance player" occurring.
354
+ probability: Rational,
355
+ /// The actions taken by a hypothetical "chance player" that convey information about which
356
+ /// RNG events were observed during a battle `update`.
357
+ actions: Actions = .{},
358
+ /// TODO
359
+ durations: Durations = .{},
360
+
361
+ // In many cases on both the cartridge and on Pokémon Showdown rolls are made even though
362
+ // later checks render their result irrelevant (missing when the target is actually immune,
363
+ // rolling for damage when the move is later revealed to have missed, etc). We can't reorder
364
+ // the logic to ensure we only ever make rolls when required due to our desire to maintain
365
+ // RNG frame accuracy, so instead we save these "pending" updates to the probability and
366
+ // only commit them once we know they are relevant (a naive solution would be to instead
367
+ // update the probability eagerly and later "undo" the update by multiplying by the inverse
368
+ // but this would be more costly and also error prone in the presence of
369
+ // overflow/normalization).
370
+ //
371
+ // This design deliberately make use of the fact that recursive moves on the cartridge may
372
+ // overwrite critical hit information multiple times as only the last update actually
373
+ // matters for the purposes of the logical results.
374
+ //
375
+ // Finally, because each player's actions are processed sequentially we only need a single
376
+ // shared structure to track this information (though it needs to be cleared appropriately)
377
+ pending: if (showdown) struct {} else struct {
378
+ crit: bool = false,
379
+ crit_probablity: u8 = 0,
380
+ damage_roll: u8 = 0,
381
+ hit: bool = false,
382
+ hit_probablity: uN = 0,
383
+ glitch: bool = false,
384
+ } = .{},
385
+
386
+ /// Possible error returned by operations tracking chance probability.
387
+ pub const Error = Rational.Error;
388
+
389
+ /// Convenience helper to clear fields which typically should be cleared between updates.
390
+ pub fn reset(self: *Self) void {
391
+ if (!enabled) return;
392
+
393
+ self.probability.reset();
394
+ self.actions = .{};
395
+ self.pending = .{};
396
+ }
397
+
398
+ pub fn commit(self: *Self, player: Player, kind: Commit) Error!void {
399
+ if (!enabled) return;
400
+
401
+ assert(!showdown);
402
+
403
+ if (kind == .glitch and !self.pending.glitch) return;
404
+
405
+ var action = self.actions.get(player);
406
+
407
+ if (kind == .err and self.pending.crit_probablity != 0) {
408
+ try self.probability.update(self.pending.crit_probablity, 256);
409
+ action.critical_hit = if (self.pending.crit) .true else .false;
410
+ return;
411
+ }
412
+
413
+ // Always commit the hit result if we make it here (commit won't be called at all if the
414
+ // target is immune*/behind a sub/etc)
415
+ if (self.pending.hit_probablity != 0) {
416
+ try self.probability.update(self.pending.hit_probablity, 256);
417
+ action.hit = if (self.pending.hit) .true else .false;
418
+ }
419
+
420
+ // If the move actually lands we can commit any past critical hit / damage rolls. Note
421
+ // we can't rely on the presence or absence of a damage roll to determine whether a
422
+ // "critical hit" was spurious or not as moves that do 1 or less damage don't roll for
423
+ // damage but can still crit (instead, whether or not a roll is deemed to be a no-op is
424
+ // passed to the critical hit helper).
425
+ //
426
+ // There is a special edge case with the division-by-zero freeze where we need to commit
427
+ // the status of critical hit roll the occured before the move missed to avoid missing
428
+ // out on the possibility that the alternative branch encountered an error
429
+ if (kind != .hit and !self.pending.glitch) {
430
+ return assert(!self.pending.crit or self.pending.crit_probablity > 0);
431
+ }
432
+
433
+ if (self.pending.crit_probablity != 0) {
434
+ try self.probability.update(self.pending.crit_probablity, 256);
435
+ action.critical_hit = if (self.pending.crit) .true else .false;
436
+ } else assert(!self.pending.glitch);
437
+
438
+ if (self.pending.damage_roll > 0 and (!self.pending.glitch or self.pending.hit)) {
439
+ try self.probability.update(1, 39);
440
+ action.damage = self.pending.damage_roll;
441
+ }
442
+ }
443
+
444
+ pub fn glitch(self: *Self) void {
445
+ if (!enabled) return;
446
+
447
+ assert(!showdown);
448
+ self.pending.glitch = true;
449
+ }
450
+
451
+ pub fn clearPending(self: *Self) void {
452
+ if (!enabled) return;
453
+
454
+ assert(!showdown);
455
+ self.pending = .{};
456
+ }
457
+
458
+ pub fn switched(self: *Self, player: Player, slot: u8) void {
459
+ if (!enabled) return;
460
+
461
+ assert(slot >= 1 and slot <= 6);
462
+
463
+ var d = self.durations.get(player);
464
+
465
+ const out = Sleeps.get(d.sleeps, 0);
466
+ d.sleeps = Sleeps.set(d.sleeps, 0, Sleeps.get(d.sleeps, slot - 1));
467
+ d.sleeps = Sleeps.set(d.sleeps, slot - 1, out);
468
+
469
+ d.confusion = 0;
470
+ d.disable = 0;
471
+ d.attacking = 0;
472
+ d.binding = 0;
473
+
474
+ self.durations.get(player.foe()).binding = 0;
475
+ }
476
+
477
+ pub fn speedTie(self: *Self, p1: bool) Error!void {
478
+ if (!enabled) return;
479
+
480
+ try self.probability.update(1, 2);
481
+ self.actions.p1.speed_tie = if (p1) .P1 else .P2;
482
+ self.actions.p2.speed_tie = self.actions.p1.speed_tie;
483
+ }
484
+
485
+ pub fn hit(self: *Self, player: Player, ok: bool, accuracy: uN) Error!void {
486
+ if (!enabled) return;
487
+
488
+ const p = if (ok) accuracy else @as(u8, @intCast(256 - @as(u9, accuracy)));
489
+ if (showdown) {
490
+ try self.probability.update(p, 256);
491
+ self.actions.get(player).hit = if (ok) .true else .false;
492
+ } else {
493
+ self.pending.hit = ok;
494
+ self.pending.hit_probablity = p;
495
+ }
496
+ }
497
+
498
+ pub fn criticalHit(self: *Self, player: Player, crit: bool, rate: u8) Error!void {
499
+ if (!enabled) return;
500
+
501
+ const n = if (crit) rate else @as(u8, @intCast(256 - @as(u9, rate)));
502
+ if (showdown) {
503
+ try self.probability.update(n, 256);
504
+ self.actions.get(player).critical_hit = if (crit) .true else .false;
505
+ } else {
506
+ self.pending.crit = crit;
507
+ self.pending.crit_probablity = n;
508
+ }
509
+ }
510
+
511
+ pub fn damage(self: *Self, player: Player, roll: u8) Error!void {
512
+ if (!enabled) return;
513
+
514
+ if (showdown) {
515
+ try self.probability.update(1, 39);
516
+ self.actions.get(player).damage = roll;
517
+ } else {
518
+ self.pending.damage_roll = roll;
519
+ }
520
+ }
521
+
522
+ pub fn secondaryChance(self: *Self, player: Player, proc: bool, rate: u8) Error!void {
523
+ if (!enabled) return;
524
+
525
+ const n = if (proc) rate else @as(u8, @intCast(256 - @as(u9, rate)));
526
+ try self.probability.update(n, 256);
527
+ self.actions.get(player).secondary_chance = if (proc) .true else .false;
528
+ }
529
+
530
+ pub fn confused(self: *Self, player: Player, cfz: bool) Error!void {
531
+ if (!enabled) return;
532
+
533
+ try self.probability.update(1, 2);
534
+ self.actions.get(player).confused = if (cfz) .true else .false;
535
+ }
536
+
537
+ pub fn paralyzed(self: *Self, player: Player, par: bool) Error!void {
538
+ if (!enabled) return;
539
+
540
+ try self.probability.update(@as(u8, if (par) 1 else 3), 4);
541
+ self.actions.get(player).paralyzed = if (par) .true else .false;
542
+ }
543
+
544
+ pub fn moveSlot(
545
+ self: *Self,
546
+ player: Player,
547
+ slot: u4,
548
+ ms: []const MoveSlot,
549
+ n: u4,
550
+ ) Error!void {
551
+ if (!enabled) return;
552
+
553
+ const denominator = if (n != 0) n else denominator: {
554
+ var i: usize = ms.len;
555
+ while (i > 0) {
556
+ i -= 1;
557
+ if (ms[i].id != .None) break :denominator i + 1;
558
+ }
559
+ unreachable;
560
+ };
561
+
562
+ if (denominator != 1) try self.probability.update(1, denominator);
563
+ self.actions.get(player).move_slot = @intCast(slot);
564
+ }
565
+
566
+ pub fn multiHit(self: *Self, player: Player, n: u3) Error!void {
567
+ if (!enabled) return;
568
+
569
+ try self.probability.update(@as(u8, if (n > 3) 1 else 3), 8);
570
+ self.actions.get(player).multi_hit = n;
571
+ }
572
+
573
+ pub fn duration(self: *Self, player: Player, turns: u4) void {
574
+ if (!enabled) return;
575
+
576
+ self.actions.get(player).duration = if (options.key) 2 else turns;
577
+ }
578
+
579
+ pub fn observe(
580
+ self: *Self,
581
+ comptime field: Action.Field,
582
+ player: Player,
583
+ obs: Optional(Observation(.confusion)),
584
+ ) void {
585
+ if (!enabled) return;
586
+
587
+ var a = self.actions.get(player);
588
+ const val = @field(a, @tagName(field));
589
+ switch (obs) {
590
+ .overwritten => {
591
+ assert(field == .confusion);
592
+ assert(val == .started or val == .continuing);
593
+ assert(a.duration > 0);
594
+ a.confusion = obs;
595
+ self.durations.get(player).confusion = 1;
596
+ },
597
+ .None => {
598
+ @field(a, @tagName(field)) = .None;
599
+ if (field == .sleep) {
600
+ var d = self.durations.get(player);
601
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
602
+ } else {
603
+ @field(self.durations.get(player), @tagName(field)) = 0;
604
+ }
605
+ },
606
+ .started => {
607
+ assert(val == .None or val == .ended);
608
+ assert(switch (field) {
609
+ .confusion => a.duration > 0 or self.actions.get(player.foe()).duration > 0,
610
+ .sleep, .disable => self.actions.get(player.foe()).duration > 0,
611
+ else => a.duration > 0,
612
+ });
613
+ @field(a, @tagName(field)) = .started;
614
+ if (field == .sleep) {
615
+ var d = self.durations.get(player);
616
+ d.sleeps = Sleeps.set(d.sleeps, 0, 1);
617
+ } else {
618
+ @field(self.durations.get(player), @tagName(field)) = 1;
619
+ }
620
+ },
621
+ else => unreachable,
622
+ }
623
+ }
624
+
625
+ pub fn sleep(self: *Self, player: Player, obs: Optional(Observation(.sleep))) Error!void {
626
+ if (!enabled) return;
627
+
628
+ var a = self.actions.get(player);
629
+ assert(a.sleep == .None or a.sleep == .started);
630
+ a.sleep = obs;
631
+
632
+ var d = self.durations.get(player);
633
+ const n = Sleeps.get(d.sleeps, 0);
634
+
635
+ if (obs == .None) {
636
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
637
+ } else if (obs == .ended) {
638
+ assert(n >= 1 and n <= 7);
639
+ if (n != 7) try self.probability.update(1, 8 - @as(u4, n));
640
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
641
+ } else {
642
+ assert(obs == .continuing);
643
+ assert(n >= 1 and n < 7);
644
+ try self.probability.update(8 - @as(u4, n) - 1, 8 - @as(u4, n));
645
+ d.sleeps = Sleeps.set(d.sleeps, 0, n + 1);
646
+ }
647
+ }
648
+
649
+ pub fn confusion(
650
+ self: *Self,
651
+ player: Player,
652
+ obs: Optional(Observation(.confusion)),
653
+ ) Error!void {
654
+ if (!enabled) return;
655
+
656
+ var a = self.actions.get(player);
657
+ assert(a.confusion == .None or a.confusion == .started);
658
+ a.confusion = obs;
659
+
660
+ var d = self.durations.get(player);
661
+ const n = d.confusion;
662
+
663
+ if (obs == .ended) {
664
+ assert(n >= 2 and n <= 5);
665
+ if (n != 5) try self.probability.update(1, 6 - @as(u4, n));
666
+ d.confusion = 0;
667
+ } else {
668
+ assert(obs == .continuing);
669
+ assert(n >= 1 and n < 5);
670
+ if (n > 1) try self.probability.update(6 - @as(u4, n) - 1, 6 - @as(u4, n));
671
+ d.confusion += 1;
672
+ }
673
+ }
674
+
675
+ pub fn disable(
676
+ self: *Self,
677
+ player: Player,
678
+ obs: Optional(Observation(.disable)),
679
+ ) Error!void {
680
+ if (!enabled) return;
681
+
682
+ var a = self.actions.get(player);
683
+ assert(a.disable == .None or a.disable == .started);
684
+ a.disable = obs;
685
+
686
+ var d = self.durations.get(player);
687
+ const n = d.disable;
688
+
689
+ if (obs == .ended) {
690
+ assert(n >= 1 and n <= 8);
691
+ if (n != 8) try self.probability.update(1, 9 - @as(u4, n));
692
+ d.disable = 0;
693
+ } else {
694
+ assert(obs == .continuing);
695
+ assert(n >= 1 and n < 8);
696
+ try self.probability.update(9 - @as(u4, n) - 1, 9 - @as(u4, n));
697
+ d.disable += 1;
698
+ }
699
+ }
700
+
701
+ pub fn attacking(
702
+ self: *Self,
703
+ player: Player,
704
+ obs: Optional(Observation(.attacking)),
705
+ ) Error!void {
706
+ if (!enabled) return;
707
+
708
+ var a = self.actions.get(player);
709
+ assert(a.attacking == .None or a.attacking == .started);
710
+ a.attacking = obs;
711
+
712
+ var d = self.durations.get(player);
713
+ const n = d.attacking;
714
+ if (obs == .ended) {
715
+ // "3-4 turns" includes the turn for the initial attack *plus* the forced attacks
716
+ assert(n >= 2 and n <= 3);
717
+ if (n != 3) try self.probability.update(1, 4 - @as(u4, n));
718
+ d.attacking = 0;
719
+ } else {
720
+ assert(obs == .continuing);
721
+ assert(n >= 1 and n < 3);
722
+ if (n > 1) try self.probability.update(4 - @as(u4, n) - 1, 4 - @as(u4, n));
723
+ d.attacking += 1;
724
+ }
725
+ }
726
+
727
+ pub fn binding(
728
+ self: *Self,
729
+ player: Player,
730
+ obs: Optional(Observation(.binding)),
731
+ ) Error!void {
732
+ if (!enabled) return;
733
+
734
+ var a = self.actions.get(player);
735
+ assert(a.binding == .None or a.binding == .started);
736
+ a.binding = obs;
737
+
738
+ var d = self.durations.get(player);
739
+ const n = d.binding;
740
+
741
+ assert(n > 0);
742
+ const p: u4 = if (n < 3) 3 else 1;
743
+ const q: u4 = if (n < 3) 8 - ((n - 1) * p) else 2;
744
+
745
+ if (obs == .ended) {
746
+ // "2-5 turns" includes the turn for the initial attack *plus* the forced attacks
747
+ assert(n >= 1 and n <= 4);
748
+ if (n != 4) try self.probability.update(p, q);
749
+ d.binding = 0;
750
+ } else {
751
+ assert(obs == .continuing);
752
+ assert(n >= 1 and n < 4);
753
+ try self.probability.update(q - p, q);
754
+ d.binding += 1;
755
+ }
756
+ }
757
+
758
+ pub fn psywave(self: *Self, player: Player, power: u8, max: u8) Error!void {
759
+ if (!enabled) return;
760
+
761
+ try self.probability.update(1, max);
762
+ self.actions.get(player).psywave = power + 1;
763
+ }
764
+
765
+ pub fn metronome(self: *Self, player: Player, move: Move) Error!void {
766
+ if (!enabled) return;
767
+
768
+ try self.probability.update(1, Move.METRONOME.len);
769
+ self.actions.get(player).metronome = move;
770
+ }
771
+
772
+ pub fn pp(self: *Self, player: Player, n: u4) bool {
773
+ if (!enabled) return true;
774
+
775
+ var a = self.actions.get(player);
776
+ if (n > 1 and a.pp == n) return false;
777
+ if (a.metronome != .None) a.pp += n;
778
+ return true;
779
+ }
780
+ };
781
+ }
782
+
783
+ test "Chance.speedTie" {
784
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
785
+
786
+ try chance.speedTie(true);
787
+ try expectProbability(&chance.probability, 1, 2);
788
+ try expectValue(Optional(Player).P1, chance.actions.p1.speed_tie);
789
+ try expectValue(chance.actions.p1.speed_tie, chance.actions.p2.speed_tie);
790
+
791
+ chance.reset();
792
+
793
+ try chance.speedTie(false);
794
+ try expectProbability(&chance.probability, 1, 2);
795
+ try expectValue(Optional(Player).P2, chance.actions.p1.speed_tie);
796
+ try expectValue(chance.actions.p1.speed_tie, chance.actions.p2.speed_tie);
797
+ }
798
+
799
+ test "Chance.hit" {
800
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
801
+
802
+ try chance.hit(.P1, true, 229);
803
+ if (!showdown) {
804
+ try expectProbability(&chance.probability, 1, 1);
805
+ try expectValue(Optional(bool).None, chance.actions.p1.hit);
806
+
807
+ try chance.commit(.P1, .hit);
808
+ }
809
+ try expectValue(Optional(bool).true, chance.actions.p1.hit);
810
+ try expectProbability(&chance.probability, 229, 256);
811
+
812
+ chance.reset();
813
+
814
+ try chance.hit(.P2, false, 255);
815
+ if (!showdown) {
816
+ try expectProbability(&chance.probability, 1, 1);
817
+ try expectValue(Optional(bool).None, chance.actions.p2.hit);
818
+
819
+ try chance.commit(.P2, .miss);
820
+ }
821
+ try expectValue(Optional(bool).false, chance.actions.p2.hit);
822
+ try expectProbability(&chance.probability, 1, 256);
823
+ }
824
+
825
+ test "Chance.criticalHit" {
826
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
827
+
828
+ try chance.criticalHit(.P1, true, 17);
829
+ if (showdown) {
830
+ try expectValue(Optional(bool).true, chance.actions.p1.critical_hit);
831
+ try expectProbability(&chance.probability, 17, 256);
832
+ } else {
833
+ try expectProbability(&chance.probability, 1, 1);
834
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
835
+
836
+ try chance.hit(.P1, true, 128);
837
+
838
+ try expectProbability(&chance.probability, 1, 1);
839
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
840
+
841
+ try chance.damage(.P1, 217);
842
+
843
+ try expectProbability(&chance.probability, 1, 1);
844
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
845
+
846
+ try chance.commit(.P1, .hit);
847
+
848
+ try expectProbability(&chance.probability, 17, 19968); // (1/2) * (17/256) * (1/39)
849
+ try expectValue(Optional(bool).true, chance.actions.p1.critical_hit);
850
+ }
851
+
852
+ chance.reset();
853
+
854
+ try chance.criticalHit(.P2, false, 5);
855
+ if (showdown) {
856
+ try expectProbability(&chance.probability, 251, 256);
857
+ try expectValue(Optional(bool).false, chance.actions.p2.critical_hit);
858
+ } else {
859
+ try expectProbability(&chance.probability, 1, 1);
860
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
861
+
862
+ try chance.hit(.P1, true, 128);
863
+
864
+ try expectProbability(&chance.probability, 1, 1);
865
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
866
+
867
+ try chance.damage(.P1, 217);
868
+ try chance.commit(.P1, .hit);
869
+
870
+ try expectProbability(&chance.probability, 251, 19968); // (1/2) * (251/256) * (1/39)
871
+ try expectValue(Optional(bool).false, chance.actions.p1.critical_hit);
872
+ }
873
+ }
874
+
875
+ test "Chance.damage" {
876
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
877
+
878
+ try chance.damage(.P1, 217);
879
+ if (showdown) {
880
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
881
+ try expectProbability(&chance.probability, 1, 39);
882
+ } else {
883
+ try expectProbability(&chance.probability, 1, 1);
884
+ try expectValue(@as(u8, 0), chance.actions.p1.damage);
885
+
886
+ try chance.commit(.P1, .hit);
887
+
888
+ try expectProbability(&chance.probability, 1, 39);
889
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
890
+ }
891
+ }
892
+
893
+ test "Chance.secondaryChance" {
894
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
895
+
896
+ try chance.secondaryChance(.P1, true, 25);
897
+ try expectProbability(&chance.probability, 25, 256);
898
+ try expectValue(Optional(bool).true, chance.actions.p1.secondary_chance);
899
+
900
+ chance.reset();
901
+
902
+ try chance.secondaryChance(.P2, false, 77);
903
+ try expectProbability(&chance.probability, 179, 256);
904
+ try expectValue(Optional(bool).false, chance.actions.p2.secondary_chance);
905
+ }
906
+
907
+ test "Chance.confused" {
908
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
909
+
910
+ try chance.confused(.P1, false);
911
+ try expectProbability(&chance.probability, 1, 2);
912
+ try expectValue(Optional(bool).false, chance.actions.p1.confused);
913
+
914
+ chance.reset();
915
+
916
+ try chance.confused(.P2, true);
917
+ try expectProbability(&chance.probability, 1, 2);
918
+ try expectValue(Optional(bool).true, chance.actions.p2.confused);
919
+ }
920
+
921
+ test "Chance.paralyzed" {
922
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
923
+
924
+ try chance.paralyzed(.P1, false);
925
+ try expectProbability(&chance.probability, 3, 4);
926
+ try expectValue(Optional(bool).false, chance.actions.p1.paralyzed);
927
+
928
+ chance.reset();
929
+
930
+ try chance.paralyzed(.P2, true);
931
+ try expectProbability(&chance.probability, 1, 4);
932
+ try expectValue(Optional(bool).true, chance.actions.p2.paralyzed);
933
+ }
934
+
935
+ test "Chance.moveSlot" {
936
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
937
+ var ms = [_]MoveSlot{ .{ .id = Move.Surf }, .{ .id = Move.Psychic }, .{ .id = Move.Recover } };
938
+
939
+ try chance.moveSlot(.P2, 2, &ms, 2);
940
+ try expectProbability(&chance.probability, 1, 2);
941
+ try expectValue(@as(u3, 2), chance.actions.p2.move_slot);
942
+
943
+ chance.reset();
944
+
945
+ try chance.moveSlot(.P1, 1, &ms, 0);
946
+ try expectProbability(&chance.probability, 1, 3);
947
+ try expectValue(@as(u3, 1), chance.actions.p1.move_slot);
948
+ }
949
+
950
+ test "Chance.multiHit" {
951
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
952
+
953
+ try chance.multiHit(.P1, 3);
954
+ try expectProbability(&chance.probability, 3, 8);
955
+ try expectValue(@as(u4, 3), chance.actions.p1.multi_hit);
956
+
957
+ chance.reset();
958
+
959
+ try chance.multiHit(.P2, 5);
960
+ try expectProbability(&chance.probability, 1, 8);
961
+ try expectValue(@as(u4, 5), chance.actions.p2.multi_hit);
962
+ }
963
+
964
+ test "Chance.duration" {
965
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
966
+
967
+ chance.duration(.P1, 2);
968
+ try expectValue(@as(u4, 2), chance.actions.p1.duration);
969
+
970
+ chance.reset();
971
+
972
+ chance.duration(.P2, 4);
973
+ try expectValue(@as(u4, 4), chance.actions.p2.duration);
974
+ }
975
+
976
+ test "Chance.sleep" {
977
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
978
+
979
+ for ([_]u8{ 7, 6, 5, 4, 3, 2, 1 }, 1..8) |d, i| {
980
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
981
+ try chance.sleep(.P1, .ended);
982
+ try expectProbability(&chance.probability, 1, d);
983
+ try expectValue(@as(u3, 0), Sleeps.get(chance.durations.p1.sleeps, 0));
984
+
985
+ chance.reset();
986
+
987
+ if (i < 7) {
988
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
989
+ try chance.sleep(.P1, .continuing);
990
+ try expectProbability(&chance.probability, d - 1, d);
991
+ try expectValue(@as(u3, @intCast(i)) + 1, Sleeps.get(chance.durations.p1.sleeps, 0));
992
+
993
+ chance.reset();
994
+ }
995
+ }
996
+ }
997
+
998
+ test "Chance.confusion" {
999
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1000
+
1001
+ for ([_]u8{ 1, 4, 3, 2, 1 }, 1..6) |d, i| {
1002
+ if (i > 1) {
1003
+ chance.durations.p2.confusion = @intCast(i);
1004
+ try chance.confusion(.P2, .ended);
1005
+ try expectProbability(&chance.probability, 1, d);
1006
+ try expectValue(@as(u3, 0), chance.durations.p2.confusion);
1007
+
1008
+ chance.reset();
1009
+ }
1010
+
1011
+ if (i < 5) {
1012
+ chance.durations.p2.confusion = @intCast(i);
1013
+ try chance.confusion(.P2, .continuing);
1014
+ try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
1015
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.confusion);
1016
+
1017
+ chance.reset();
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ test "Chance.disable" {
1023
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1024
+
1025
+ for ([_]u8{ 8, 7, 6, 5, 4, 3, 2, 1 }, 1..9) |d, i| {
1026
+ chance.durations.p1.disable = @intCast(i);
1027
+ try chance.disable(.P1, .ended);
1028
+ try expectProbability(&chance.probability, 1, d);
1029
+ try expectValue(@as(u4, 0), chance.durations.p1.disable);
1030
+
1031
+ chance.reset();
1032
+
1033
+ if (i < 8) {
1034
+ chance.durations.p1.disable = @intCast(i);
1035
+ try chance.disable(.P1, .continuing);
1036
+ try expectProbability(&chance.probability, d - 1, d);
1037
+ try expectValue(@as(u4, @intCast(i)) + 1, chance.durations.p1.disable);
1038
+
1039
+ chance.reset();
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ test "Chance.attacking" {
1045
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1046
+
1047
+ for ([_]u8{ 1, 2, 1 }, 1..4) |d, i| {
1048
+ if (i > 1) {
1049
+ chance.durations.p2.attacking = @intCast(i);
1050
+ try chance.attacking(.P2, .ended);
1051
+ try expectProbability(&chance.probability, 1, d);
1052
+ try expectValue(@as(u3, 0), chance.durations.p2.attacking);
1053
+
1054
+ chance.reset();
1055
+ }
1056
+
1057
+ if (i < 3) {
1058
+ chance.durations.p2.attacking = @intCast(i);
1059
+ try chance.attacking(.P2, .continuing);
1060
+ try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
1061
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.attacking);
1062
+
1063
+ chance.reset();
1064
+ }
1065
+ }
1066
+ }
1067
+
1068
+ test "Chance.binding" {
1069
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1070
+
1071
+ const ps = [_]u8{ 3, 3, 1, 1 };
1072
+ const qs = [_]u8{ 8, 5, 2, 1 };
1073
+
1074
+ for (ps, qs, 1..5) |p, q, i| {
1075
+ chance.durations.p1.binding = @intCast(i);
1076
+ try chance.binding(.P1, .ended);
1077
+ try expectProbability(&chance.probability, p, q);
1078
+ try expectValue(@as(u3, 0), chance.durations.p1.binding);
1079
+
1080
+ chance.reset();
1081
+
1082
+ if (i < 4) {
1083
+ chance.durations.p1.binding = @intCast(i);
1084
+ try chance.binding(.P1, .continuing);
1085
+ try expectProbability(&chance.probability, q - p, q);
1086
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p1.binding);
1087
+
1088
+ chance.reset();
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ test "Chance.psywave" {
1094
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1095
+
1096
+ try chance.psywave(.P2, 100, 150);
1097
+ try expectProbability(&chance.probability, 1, 150);
1098
+ try expectValue(@as(u8, 101), chance.actions.p2.psywave);
1099
+ }
1100
+
1101
+ test "Chance.metronome" {
1102
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
1103
+
1104
+ try chance.metronome(.P1, Move.HornAttack);
1105
+ try expectProbability(&chance.probability, 1, 163);
1106
+ try expectValue(Move.HornAttack, chance.actions.p1.metronome);
1107
+ }
1108
+
1109
+ pub fn expectProbability(r: anytype, p: u64, q: u64) !void {
1110
+ if (!enabled) return;
1111
+
1112
+ r.reduce();
1113
+ if (r.p != p or r.q != q) {
1114
+ print("expected {d}/{d}, found {f}\n", .{ p, q, r });
1115
+ return error.TestExpectedEqual;
1116
+ }
1117
+ }
1118
+
1119
+ pub fn expectValue(a: anytype, b: anytype) !void {
1120
+ if (!enabled) return;
1121
+
1122
+ try expectEqual(a, b);
1123
+ }
1124
+
1125
+ /// Null object pattern implementation of Generation I `Chance` which does nothing, though chance
1126
+ /// tracking should additionally be turned off entirely via `options.chance`.
1127
+ pub const NULL: Null = .{};
1128
+
1129
+ const Null = struct {
1130
+ pub const Error = error{};
1131
+
1132
+ pub fn commit(self: Null, player: Player, kind: Commit) Error!void {
1133
+ _ = .{ self, player, kind };
1134
+ }
1135
+
1136
+ pub fn glitch(self: Null) void {
1137
+ _ = .{self};
1138
+ }
1139
+
1140
+ pub fn clearPending(self: Null) void {
1141
+ _ = .{self};
1142
+ }
1143
+
1144
+ pub fn switched(self: Null, player: Player, slot: u8) void {
1145
+ _ = .{ self, player, slot };
1146
+ }
1147
+
1148
+ pub fn speedTie(self: Null, p1: bool) Error!void {
1149
+ _ = .{ self, p1 };
1150
+ }
1151
+
1152
+ pub fn hit(self: Null, player: Player, ok: bool, accuracy: uN) Error!void {
1153
+ _ = .{ self, player, ok, accuracy };
1154
+ }
1155
+
1156
+ pub fn criticalHit(self: Null, player: Player, crit: bool, rate: u8) Error!void {
1157
+ _ = .{ self, player, crit, rate };
1158
+ }
1159
+
1160
+ pub fn damage(self: Null, player: Player, roll: u8) Error!void {
1161
+ _ = .{ self, player, roll };
1162
+ }
1163
+
1164
+ pub fn confused(self: Null, player: Player, ok: bool) Error!void {
1165
+ _ = .{ self, player, ok };
1166
+ }
1167
+
1168
+ pub fn paralyzed(self: Null, player: Player, ok: bool) Error!void {
1169
+ _ = .{ self, player, ok };
1170
+ }
1171
+
1172
+ pub fn secondaryChance(self: Null, player: Player, proc: bool, rate: u8) Error!void {
1173
+ _ = .{ self, player, proc, rate };
1174
+ }
1175
+
1176
+ pub fn moveSlot(self: Null, player: Player, slot: u4, ms: []const MoveSlot, n: u4) Error!void {
1177
+ _ = .{ self, player, slot, ms, n };
1178
+ }
1179
+
1180
+ pub fn multiHit(self: Null, player: Player, n: u3) Error!void {
1181
+ _ = .{ self, player, n };
1182
+ }
1183
+
1184
+ pub fn duration(self: Null, player: Player, turns: u4) void {
1185
+ _ = .{ self, player, turns };
1186
+ }
1187
+
1188
+ pub fn observe(
1189
+ self: Null,
1190
+ comptime field: Action.Field,
1191
+ player: Player,
1192
+ obs: Optional(Observation(.confusion)),
1193
+ ) void {
1194
+ _ = .{ self, field, player, obs };
1195
+ }
1196
+
1197
+ pub fn sleep(self: Null, player: Player, obs: Optional(Observation(.sleep))) Error!void {
1198
+ _ = .{ self, player, obs };
1199
+ }
1200
+
1201
+ pub fn confusion(
1202
+ self: Null,
1203
+ player: Player,
1204
+ obs: Optional(Observation(.confusion)),
1205
+ ) Error!void {
1206
+ _ = .{ self, player, obs };
1207
+ }
1208
+
1209
+ pub fn disable(
1210
+ self: Null,
1211
+ player: Player,
1212
+ obs: Optional(Observation(.disable)),
1213
+ ) Error!void {
1214
+ _ = .{ self, player, obs };
1215
+ }
1216
+
1217
+ pub fn attacking(
1218
+ self: Null,
1219
+ player: Player,
1220
+ obs: Optional(Observation(.attacking)),
1221
+ ) Error!void {
1222
+ _ = .{ self, player, obs };
1223
+ }
1224
+
1225
+ pub fn binding(
1226
+ self: Null,
1227
+ player: Player,
1228
+ obs: Optional(Observation(.binding)),
1229
+ ) Error!void {
1230
+ _ = .{ self, player, obs };
1231
+ }
1232
+
1233
+ pub fn psywave(self: Null, player: Player, power: u8, max: u8) Error!void {
1234
+ _ = .{ self, player, power, max };
1235
+ }
1236
+
1237
+ pub fn metronome(self: Null, player: Player, move: Move) Error!void {
1238
+ _ = .{ self, player, move };
1239
+ }
1240
+
1241
+ pub fn pp(self: Null, player: Player, n: u4) bool {
1242
+ _ = .{ self, player, n };
1243
+ return true;
1244
+ }
1245
+ };