@pkmn/engine 0.1.0-dev.fb5bf955 → 0.1.0-dev.fc150752

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 (49) hide show
  1. package/README.md +71 -60
  2. package/build/pkg/addon.d.ts +2 -2
  3. package/build/pkg/addon.js +8 -7
  4. package/build/pkg/addon.js.map +1 -1
  5. package/build/pkg/data/layout.json +25 -3
  6. package/build/pkg/gen1.d.ts +5 -4
  7. package/build/pkg/gen1.js +47 -31
  8. package/build/pkg/gen1.js.map +1 -1
  9. package/build/pkg/index.d.ts +15 -12
  10. package/build/pkg/index.js.map +1 -1
  11. package/build/pkg/protocol.d.ts +4 -2
  12. package/build/pkg/protocol.js +4 -2
  13. package/build/pkg/protocol.js.map +1 -1
  14. package/build/test/display.js +31 -13
  15. package/build/test/display.js.map +1 -1
  16. package/build.zig +95 -76
  17. package/package.json +26 -16
  18. package/src/bin/install-pkmn-engine +11 -6
  19. package/src/bin/pkmn-debug +29 -6
  20. package/src/lib/binding/node.zig +46 -141
  21. package/src/lib/binding/wasm.zig +6 -4
  22. package/src/lib/common/battle.zig +17 -0
  23. package/src/lib/common/data.zig +14 -26
  24. package/src/lib/common/debug.zig +21 -11
  25. package/src/lib/common/js.zig +118 -0
  26. package/src/lib/common/optional.zig +60 -0
  27. package/src/lib/common/options.zig +12 -3
  28. package/src/lib/common/protocol.zig +315 -197
  29. package/src/lib/common/rational.zig +353 -0
  30. package/src/lib/common/rng.zig +11 -11
  31. package/src/lib/common/util.zig +12 -0
  32. package/src/lib/gen1/README.md +265 -155
  33. package/src/lib/gen1/calc.zig +426 -0
  34. package/src/lib/gen1/chance.zig +1036 -0
  35. package/src/lib/gen1/data/moves.zig +178 -180
  36. package/src/lib/gen1/data/species.zig +2 -2
  37. package/src/lib/gen1/data/types.zig +3 -3
  38. package/src/lib/gen1/data.zig +73 -59
  39. package/src/lib/gen1/helpers.zig +592 -32
  40. package/src/lib/gen1/mechanics.zig +994 -641
  41. package/src/lib/gen2/README.md +87 -13
  42. package/src/lib/gen2/data/items.zig +3 -3
  43. package/src/lib/gen2/data/moves.zig +635 -563
  44. package/src/lib/gen2/data/species.zig +1 -1
  45. package/src/lib/gen2/data/types.zig +3 -3
  46. package/src/lib/gen2/data.zig +104 -74
  47. package/src/lib/gen2/helpers.zig +16 -22
  48. package/src/lib/gen2/mechanics.zig +916 -11
  49. package/src/lib/pkmn.zig +68 -9
@@ -0,0 +1,1036 @@
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
+ const DEBUG = @import("../common/debug.zig").print;
10
+ const options = @import("../common/options.zig");
11
+ const rational = @import("../common/rational.zig");
12
+ const util = @import("../common/util.zig");
13
+
14
+ const Player = @import("../common/data.zig").Player;
15
+ const Optional = @import("../common/optional.zig").Optional;
16
+
17
+ const Move = @import("data.zig").Move;
18
+ const MoveSlot = @import("data.zig").MoveSlot;
19
+
20
+ const enabled = options.chance;
21
+ const showdown = options.showdown;
22
+
23
+ /// Actions taken by a hypothetical "chance player" that convey information about which RNG events
24
+ /// were observed during a Generation I battle `update`. This can additionally be provided as input
25
+ /// to the `update` call to override the normal behavior of the RNG in order to force specific
26
+ /// outcomes.
27
+ pub const Actions = extern struct {
28
+ /// Information about the RNG activity for Player 1.
29
+ p1: Action = .{},
30
+ /// Information about the RNG activity for Player 2.
31
+ p2: Action = .{},
32
+
33
+ comptime {
34
+ assert(@sizeOf(Actions) == 16);
35
+ }
36
+
37
+ /// Returns the `Action` for the given `player`.
38
+ pub inline fn get(self: anytype, player: Player) util.PointerType(@TypeOf(self), Action) {
39
+ assert(@typeInfo(@TypeOf(self)).Pointer.child == Actions);
40
+ return if (player == .P1) &self.p1 else &self.p2;
41
+ }
42
+
43
+ /// Perform a reset by clearing fields which should not persist across updates.
44
+ pub inline fn reset(self: *Actions) void {
45
+ self.p1.reset();
46
+ self.p2.reset();
47
+ }
48
+
49
+ /// Returns true if `a` is equal to `b`.
50
+ pub inline fn eql(a: Actions, b: Actions) bool {
51
+ return @as(u128, @bitCast(a)) == @as(u128, @bitCast(b));
52
+ }
53
+
54
+ /// Returns true if `a` has the same "shape" as `b`, where `Actions` are defined to have the
55
+ /// same shape if they have the same fields set (though those fields need not necessarily be
56
+ /// set to the same value).
57
+ pub fn matches(a: Actions, b: Actions) bool {
58
+ inline for (@typeInfo(Actions).Struct.fields) |player| {
59
+ inline for (@typeInfo(Action).Struct.fields) |field| {
60
+ const a_val = @field(@field(a, player.name), field.name);
61
+ const b_val = @field(@field(b, player.name), field.name);
62
+
63
+ switch (@typeInfo(@TypeOf(a_val))) {
64
+ .Struct => inline for (@typeInfo(@TypeOf(a_val)).Struct.fields) |f| {
65
+ if ((@field(a_val, f.name) > 0) != (@field(b_val, f.name) > 0)) {
66
+ return false;
67
+ }
68
+ },
69
+ .Enum => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
70
+ return false,
71
+ .Int => if ((a_val > 0) != (b_val > 0)) return false,
72
+ else => unreachable,
73
+ }
74
+ }
75
+ }
76
+ return true;
77
+ }
78
+
79
+ pub fn fmt(self: Actions, writer: anytype, shape: bool) !void {
80
+ try writer.writeAll("<P1 = ");
81
+ try self.p1.fmt(writer, shape);
82
+ try writer.writeAll(", P2 = ");
83
+ try self.p2.fmt(writer, shape);
84
+ try writer.writeAll(">");
85
+ }
86
+
87
+ pub fn format(a: Actions, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
88
+ _ = .{ f, o };
89
+ try fmt(a, w, false);
90
+ }
91
+ };
92
+
93
+ test Actions {
94
+ const a: Actions = .{ .p1 = .{ .hit = .true, .critical_hit = .false, .damage = 245 } };
95
+ const b: Actions = .{ .p1 = .{ .hit = .false, .critical_hit = .true, .damage = 246 } };
96
+ const c: Actions = .{ .p1 = .{ .hit = .true } };
97
+ const d: Actions = .{ .p2 = .{ .hit = .true, .durations = .{ .sleep = 2 } } };
98
+ const e: Actions = .{ .p2 = .{ .hit = .false, .durations = .{ .sleep = 4 } } };
99
+ const f: Actions = .{ .p1 = .{ .hit = .false, .durations = .{ .sleep = 4 } } };
100
+
101
+ try expect(a.eql(a));
102
+ try expect(!a.eql(b));
103
+ try expect(!b.eql(a));
104
+ try expect(!a.eql(c));
105
+ try expect(!c.eql(a));
106
+
107
+ try expect(a.matches(a));
108
+ try expect(a.matches(b));
109
+ try expect(b.matches(a));
110
+ try expect(!a.matches(c));
111
+ try expect(!c.matches(a));
112
+ try expect(d.matches(e));
113
+ try expect(!d.matches(f));
114
+ }
115
+
116
+ /// Information about the RNG that was observed during a Generation I battle `update` for a
117
+ /// single player.
118
+ pub const Action = packed struct(u64) {
119
+ /// If not 0, the roll to be returned Rolls.damage.
120
+ damage: u8 = 0,
121
+
122
+ /// If not None, the value to return for Rolls.hit.
123
+ hit: Optional(bool) = .None,
124
+ /// If not None, the value to be returned by Rolls.criticalHit.
125
+ critical_hit: Optional(bool) = .None,
126
+ /// If not None, the value to be returned for
127
+ /// Rolls.{confusionChance,secondaryChance,poisonChance}.
128
+ secondary_chance: Optional(bool) = .None,
129
+ /// If not None, the Player to be returned by Rolls.speedTie.
130
+ speed_tie: Optional(Player) = .None,
131
+
132
+ /// If not None, the value to return for Rolls.confused.
133
+ confused: Optional(bool) = .None,
134
+ /// If not None, the value to return for Rolls.paralyzed.
135
+ paralyzed: Optional(bool) = .None,
136
+ /// If not 0, the value to be returned by Rolls.distribution in the case of binding moves
137
+ /// or Rolls.{sleepDuration,disableDuration,confusionDuration,attackingDuration} otherwise.
138
+ duration: u4 = 0,
139
+
140
+ /// Observed values of various durations. Does not influence future RNG calls. TODO
141
+ durations: Duration = .{},
142
+
143
+ /// If not 0, the move slot (1-4) to return in Rolls.moveSlot. If present as an override,
144
+ /// invalid values (eg. due to empty move slots or 0 PP) will be ignored.
145
+ move_slot: u4 = 0,
146
+ /// If not 0, the value (2-5) to return for Rolls.distribution for multi hit.
147
+ multi_hit: u4 = 0,
148
+
149
+ /// If not 0, psywave - 1 should be returned as the damage roll for Rolls.psywave.
150
+ psywave: u8 = 0,
151
+
152
+ /// If not None, the Move to return for Rolls.metronome.
153
+ metronome: Move = .None,
154
+
155
+ pub const DURATIONS: u64 = 0x000000FFFF000000;
156
+
157
+ pub const Field = std.meta.FieldEnum(Action);
158
+
159
+ /// Perform a reset by clearing fields which should not persist across updates.
160
+ pub inline fn reset(self: *Action) void {
161
+ self.* = @bitCast(@as(u64, @bitCast(self.*)) & DURATIONS);
162
+ }
163
+
164
+ pub fn format(a: Action, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
165
+ _ = .{ f, o };
166
+ try fmt(a, w, false);
167
+ }
168
+
169
+ pub fn fmt(self: Action, writer: anytype, shape: bool) !void {
170
+ try writer.writeByte('(');
171
+ var printed = false;
172
+ inline for (@typeInfo(Action).Struct.fields) |field| {
173
+ const val = @field(self, field.name);
174
+ switch (@typeInfo(@TypeOf(val))) {
175
+ .Struct => inline for (@typeInfo(@TypeOf(val)).Struct.fields) |f| {
176
+ const v = @field(val, f.name);
177
+ if (v != 0) {
178
+ if (printed) try writer.writeAll(", ");
179
+ if (shape) {
180
+ try writer.print("{s}:?", .{f.name});
181
+ } else {
182
+ try writer.print("{s}:{d}", .{ f.name, v });
183
+ }
184
+ printed = true;
185
+ }
186
+ },
187
+ .Enum => if (val != .None) {
188
+ if (printed) try writer.writeAll(", ");
189
+ if (shape) {
190
+ try writer.print("{s}:?", .{field.name});
191
+ } else if (@TypeOf(val) == Optional(bool)) {
192
+ try writer.print("{s}{s}", .{
193
+ if (val == .false) "!" else "",
194
+ field.name,
195
+ });
196
+ } else {
197
+ try writer.print("{s}:{s}", .{ field.name, @tagName(val) });
198
+ }
199
+ printed = true;
200
+ },
201
+ .Int => if (val != 0) {
202
+ if (printed) try writer.writeAll(", ");
203
+ if (shape) {
204
+ try writer.print("{s}:?", .{field.name});
205
+ } else {
206
+ try writer.print("{s}:{d}", .{ field.name, val });
207
+ }
208
+ printed = true;
209
+ },
210
+ else => unreachable,
211
+ }
212
+ }
213
+ try writer.writeByte(')');
214
+ }
215
+ };
216
+
217
+ test Action {
218
+ var a = Action{ .hit = .true, .durations = .{ .sleep = 3 }, .damage = 225 };
219
+ a.reset();
220
+
221
+ try expectEqual(Action{ .hit = .None, .durations = .{ .sleep = 3 }, .damage = 0 }, a);
222
+ }
223
+
224
+ /// TODO
225
+ pub const Durations = extern struct {
226
+ /// Information about the durations for Player 1.
227
+ p1: Duration = .{},
228
+ /// Information about the durations for Player 2.
229
+ p2: Duration = .{},
230
+
231
+ comptime {
232
+ assert(@sizeOf(Durations) == 4);
233
+ }
234
+ };
235
+
236
+ /// Observed values for various durations that need to be tracked in order to properly
237
+ /// deduplicate transitions with a primary key. TODO
238
+ pub const Duration = packed struct(u16) {
239
+ /// The number of turns a Pokémon has been observed to be sleeping.
240
+ sleep: u3 = 0,
241
+ /// The number of turns a Pokémon has been observed to be confused.
242
+ confusion: u3 = 0,
243
+ /// The number of turns a Pokémon has been observed to be disabled.
244
+ disable: u4 = 0,
245
+ /// The number of turns a Pokémon has been observed to be attacking.
246
+ attacking: u3 = 0,
247
+ /// The number of turns a Pokémon has been observed to be binding their opponent.
248
+ binding: u3 = 0,
249
+
250
+ pub const Field = std.meta.FieldEnum(Duration);
251
+ };
252
+
253
+ pub const Commit = enum { hit, miss, binding };
254
+
255
+ /// Tracks chance actions and their associated probability during a Generation I battle update when
256
+ /// `options.chance` is enabled.
257
+ pub fn Chance(comptime Rational: type) type {
258
+ return struct {
259
+ const Self = @This();
260
+
261
+ /// The probability of the actions taken by a hypothetical "chance player" occurring.
262
+ probability: Rational,
263
+ /// The actions taken by a hypothetical "chance player" that convey information about which
264
+ /// RNG events were observed during a battle `update`.
265
+ actions: Actions = .{},
266
+
267
+ // Tracks the observed sleep durations for the Pokémon in both player's parties. Unlike
268
+ // other durations which are all tied to volatiles, sleep's counter persists through
269
+ // switching and so we must store it here. The indices of these arrays correspond to the
270
+ // `order` field of a Side. This information could be stored in actions but size is a
271
+ // concern so this is tracked separately as actions only purports to track RNG events
272
+ // observed during a single `update` (not across updates).
273
+ sleeps: [2][6]u3 = .{ .{0} ** 6, .{0} ** 6 },
274
+
275
+ // In many cases on both the cartridge and on Pokémon Showdown rolls are made even though
276
+ // later checks render their result irrelevant (missing when the target is actually immune,
277
+ // rolling for damage when the move is later revealed to have missed, etc). We can't reorder
278
+ // the logic to ensure we only ever make rolls when required due to our desire to maintain
279
+ // RNG frame accuracy, so instead we save these "pending" updates to the probability and
280
+ // only commit them once we know they are relevant (a naive solution would be to instead
281
+ // update the probability eagerly and later "undo" the update by multiplying by the inverse
282
+ // but this would be more costly and also error prone in the presence of
283
+ // overflow/normalization).
284
+ //
285
+ // This design deliberately make use of the fact that recursive moves on the cartridge may
286
+ // overwrite critical hit information multiple times as only the last update actually
287
+ // matters for the purposes of the logical results.
288
+ //
289
+ // Finally, because each player's actions are processed sequentially we only need a single
290
+ // shared structure to track this information (though it needs to be cleared appropriately)
291
+ pending: if (showdown) struct {
292
+ hit: bool = false,
293
+ hit_probablity: u8 = 0,
294
+ } else struct {
295
+ crit: bool = false,
296
+ crit_probablity: u8 = 0,
297
+ damage_roll: u8 = 0,
298
+ hit: bool = false,
299
+ hit_probablity: u8 = 0,
300
+ binding: u8 = 0,
301
+ } = .{},
302
+
303
+ /// Possible error returned by operations tracking chance probability.
304
+ pub const Error = Rational.Error;
305
+
306
+ /// Convenience helper to clear fields which typically should be cleared between updates.
307
+ pub fn reset(self: *Self) void {
308
+ self.probability.reset();
309
+ self.actions.reset();
310
+ self.pending = .{};
311
+ }
312
+
313
+ pub fn commit(self: *Self, player: Player, kind: Commit) Error!void {
314
+ var action = self.actions.get(player);
315
+
316
+ // We need to handle binding specially because we still need to commit it when attacking
317
+ // into a Pokémon that is immune
318
+ if (!showdown) {
319
+ if (kind != .miss and self.pending.binding != 0) {
320
+ assert(action.duration == 0);
321
+ action.duration = @intCast(self.pending.binding);
322
+ assert(action.durations.binding == 0);
323
+ action.durations.binding = 1;
324
+ }
325
+ if (kind == .binding) return;
326
+ } else assert(kind != .binding);
327
+
328
+ // Always commit the hit result if we make it here (commit won't be called at all if the
329
+ // target is immune/behind a sub/etc)
330
+ if (self.pending.hit_probablity != 0) {
331
+ try self.probability.update(self.pending.hit_probablity, 256);
332
+ action.hit = if (self.pending.hit) .true else .false;
333
+ }
334
+
335
+ if (showdown) return;
336
+
337
+ // If the move actually lands we can commit any past critical hit / damage rolls. We
338
+ // avoid updating anything if there wasn't a damage roll as any "critical hit" not tied
339
+ // to a damage roll is actually a no-op
340
+ if (kind == .hit and self.pending.damage_roll > 0) {
341
+ assert(!self.pending.crit or self.pending.crit_probablity > 0);
342
+
343
+ if (self.pending.crit_probablity != 0) {
344
+ try self.probability.update(self.pending.crit_probablity, 256);
345
+ action.critical_hit = if (self.pending.crit) .true else .false;
346
+ }
347
+
348
+ try self.probability.update(1, 39);
349
+ action.damage = self.pending.damage_roll;
350
+ }
351
+ }
352
+
353
+ pub fn clearPending(self: *Self) void {
354
+ if (!enabled) return;
355
+
356
+ self.pending = .{};
357
+ }
358
+
359
+ pub fn clearDurations(self: *Self, player: Player, haze: ?bool) void {
360
+ if (!enabled) return;
361
+
362
+ var action = self.actions.get(player);
363
+ if (haze) |status| {
364
+ if (status) action.durations.sleep = 0;
365
+ action.durations.confusion = 0;
366
+ action.durations.disable = 0;
367
+ } else {
368
+ action.durations.attacking = 0;
369
+ action.durations.binding = 0;
370
+ }
371
+ }
372
+
373
+ pub fn switched(self: *Self, player: Player, in: u8, out: u8) void {
374
+ if (!enabled) return;
375
+
376
+ assert(in >= 1 and in <= 6);
377
+ assert(out >= 1 and out <= 6);
378
+
379
+ var action = self.actions.get(player);
380
+
381
+ const slp = action.durations.sleep;
382
+ action.durations = .{};
383
+
384
+ self.sleeps[@intFromEnum(player)][out - 1] = slp;
385
+ action.durations.sleep = @intCast(self.sleeps[@intFromEnum(player)][in - 1]);
386
+
387
+ self.actions.get(player.foe()).durations.binding = 0;
388
+ }
389
+
390
+ pub fn speedTie(self: *Self, p1: bool) Error!void {
391
+ if (!enabled) return;
392
+
393
+ try self.probability.update(1, 2);
394
+ self.actions.p1.speed_tie = if (p1) .P1 else .P2;
395
+ self.actions.p2.speed_tie = self.actions.p1.speed_tie;
396
+ }
397
+
398
+ pub fn hit(self: *Self, ok: bool, accuracy: u8) void {
399
+ if (!enabled) return;
400
+
401
+ const p = if (ok) accuracy else @as(u8, @intCast(256 - @as(u9, accuracy)));
402
+ self.pending.hit = ok;
403
+ self.pending.hit_probablity = p;
404
+ }
405
+
406
+ pub fn criticalHit(self: *Self, player: Player, crit: bool, rate: u8) Error!void {
407
+ if (!enabled) return;
408
+
409
+ const n = if (crit) rate else @as(u8, @intCast(256 - @as(u9, rate)));
410
+ if (showdown) {
411
+ try self.probability.update(n, 256);
412
+ self.actions.get(player).critical_hit = if (crit) .true else .false;
413
+ } else {
414
+ self.pending.crit = crit;
415
+ self.pending.crit_probablity = n;
416
+ }
417
+ }
418
+
419
+ pub fn damage(self: *Self, player: Player, roll: u8) Error!void {
420
+ if (!enabled) return;
421
+
422
+ if (showdown) {
423
+ try self.probability.update(1, 39);
424
+ self.actions.get(player).damage = roll;
425
+ } else {
426
+ self.pending.damage_roll = roll;
427
+ }
428
+ }
429
+
430
+ pub fn secondaryChance(self: *Self, player: Player, proc: bool, rate: u8) Error!void {
431
+ if (!enabled) return;
432
+
433
+ const n = if (proc) rate else @as(u8, @intCast(256 - @as(u9, rate)));
434
+ try self.probability.update(n, 256);
435
+ self.actions.get(player).secondary_chance = if (proc) .true else .false;
436
+ }
437
+
438
+ pub fn confused(self: *Self, player: Player, cfz: bool) Error!void {
439
+ if (!enabled) return;
440
+
441
+ try self.probability.update(1, 2);
442
+ self.actions.get(player).confused = if (cfz) .true else .false;
443
+ }
444
+
445
+ pub fn paralyzed(self: *Self, player: Player, par: bool) Error!void {
446
+ if (!enabled) return;
447
+
448
+ try self.probability.update(@as(u8, if (par) 1 else 3), 4);
449
+ self.actions.get(player).paralyzed = if (par) .true else .false;
450
+ }
451
+
452
+ pub fn moveSlot(
453
+ self: *Self,
454
+ player: Player,
455
+ slot: u4,
456
+ ms: []const MoveSlot,
457
+ n: u4,
458
+ ) Error!void {
459
+ if (!enabled) return;
460
+
461
+ const denominator = if (n != 0) n else denominator: {
462
+ var i: usize = ms.len;
463
+ while (i > 0) {
464
+ i -= 1;
465
+ if (ms[i].id != .None) break :denominator i + 1;
466
+ }
467
+ unreachable;
468
+ };
469
+
470
+ if (denominator != 1) try self.probability.update(1, denominator);
471
+ self.actions.get(player).move_slot = @intCast(slot);
472
+ }
473
+
474
+ pub fn multiHit(self: *Self, player: Player, n: u3) Error!void {
475
+ if (!enabled) return;
476
+
477
+ try self.probability.update(@as(u8, if (n > 3) 1 else 3), 8);
478
+ self.actions.get(player).multi_hit = n;
479
+ }
480
+
481
+ pub fn duration(
482
+ self: *Self,
483
+ comptime field: Duration.Field,
484
+ player: Player,
485
+ target: Player,
486
+ turns: u4,
487
+ ) void {
488
+ if (!enabled) return;
489
+
490
+ if (!showdown and field == .binding) {
491
+ self.pending.binding = if (options.key) 1 else turns;
492
+ } else {
493
+ var durations = &self.actions.get(target).durations;
494
+ assert(@field(durations, @tagName(field)) == 0 or
495
+ (field == .confusion and player == target));
496
+ @field(durations, @tagName(field)) = 1;
497
+ self.actions.get(player).duration = if (options.key) 1 else turns;
498
+ }
499
+ }
500
+
501
+ pub fn sleep(self: *Self, player: Player, turns: u4) Error!void {
502
+ if (!enabled) return;
503
+
504
+ var durations = &self.actions.get(player).durations;
505
+ const n = durations.sleep;
506
+ if (turns == 0) {
507
+ assert(n >= 1 and n <= 7);
508
+ if (n != 7) try self.probability.update(1, 8 - @as(u4, n));
509
+ durations.sleep = 0;
510
+ } else {
511
+ assert(n >= 1 and n < 7);
512
+ try self.probability.update(8 - @as(u4, n) - 1, 8 - @as(u4, n));
513
+ durations.sleep += 1;
514
+ }
515
+ }
516
+
517
+ pub fn disable(self: *Self, player: Player, turns: u4) Error!void {
518
+ if (!enabled) return;
519
+
520
+ var durations = &self.actions.get(player).durations;
521
+ const n = durations.disable;
522
+ if (turns == 0) {
523
+ assert(n >= 1 and n <= 8);
524
+ if (n != 8) try self.probability.update(1, 9 - @as(u4, n));
525
+ durations.disable = 0;
526
+ } else {
527
+ assert(n >= 1 and n < 8);
528
+ try self.probability.update(9 - @as(u4, n) - 1, 9 - @as(u4, n));
529
+ durations.disable += 1;
530
+ }
531
+ }
532
+
533
+ pub fn confusion(self: *Self, player: Player, turns: u4) Error!void {
534
+ if (!enabled) return;
535
+
536
+ var durations = &self.actions.get(player).durations;
537
+ const n = durations.confusion;
538
+ if (turns == 0) {
539
+ assert(n >= 2 and n <= 5);
540
+ if (n != 5) try self.probability.update(1, 6 - @as(u4, n));
541
+ durations.confusion = 0;
542
+ } else {
543
+ assert(n >= 1 and n < 5);
544
+ if (n > 1) try self.probability.update(6 - @as(u4, n) - 1, 6 - @as(u4, n));
545
+ durations.confusion += 1;
546
+ }
547
+ }
548
+
549
+ pub fn attacking(self: *Self, player: Player, turns: u4) Error!void {
550
+ if (!enabled) return;
551
+
552
+ var durations = &self.actions.get(player).durations;
553
+ const n = durations.attacking;
554
+ if (turns == 0) {
555
+ assert(n >= 2 and n <= 3);
556
+ if (n != 3) try self.probability.update(1, 4 - @as(u4, n));
557
+ durations.attacking = 0;
558
+ } else {
559
+ assert(n >= 1 and n < 3);
560
+ if (n > 1) try self.probability.update(4 - @as(u4, n) - 1, 4 - @as(u4, n));
561
+ durations.attacking += 1;
562
+ }
563
+ }
564
+
565
+ pub fn binding(self: *Self, player: Player, turns: u4) Error!void {
566
+ if (!enabled) return;
567
+
568
+ var durations = &self.actions.get(player).durations;
569
+ const n = durations.binding;
570
+
571
+ assert(n > 0);
572
+ const p: u4 = if (n < 3) 3 else 1;
573
+ const q: u4 = if (n < 3) 8 - ((n - 1) * p) else 2;
574
+
575
+ if (turns == 0) {
576
+ assert(n >= 1 and n <= 4);
577
+ if (n != 4) try self.probability.update(p, q);
578
+ durations.binding = 0;
579
+ } else {
580
+ assert(n >= 1 and n < 4);
581
+ try self.probability.update(q - p, q);
582
+ durations.binding += 1;
583
+ }
584
+ }
585
+
586
+ pub fn psywave(self: *Self, player: Player, power: u8, max: u8) Error!void {
587
+ if (!enabled) return;
588
+
589
+ try self.probability.update(1, max);
590
+ self.actions.get(player).psywave = power + 1;
591
+ }
592
+
593
+ pub fn metronome(self: *Self, player: Player, move: Move) Error!void {
594
+ if (!enabled) return;
595
+
596
+ try self.probability.update(1, Move.size - 2);
597
+ self.actions.get(player).metronome = move;
598
+ }
599
+ };
600
+ }
601
+
602
+ test "Chance.speedTie" {
603
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
604
+
605
+ try chance.speedTie(true);
606
+ try expectProbability(&chance.probability, 1, 2);
607
+ try expectValue(Optional(Player).P1, chance.actions.p1.speed_tie);
608
+ try expectValue(chance.actions.p1.speed_tie, chance.actions.p2.speed_tie);
609
+
610
+ chance.reset();
611
+
612
+ try chance.speedTie(false);
613
+ try expectProbability(&chance.probability, 1, 2);
614
+ try expectValue(Optional(Player).P2, chance.actions.p1.speed_tie);
615
+ try expectValue(chance.actions.p1.speed_tie, chance.actions.p2.speed_tie);
616
+ }
617
+
618
+ test "Chance.hit" {
619
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
620
+
621
+ chance.hit(true, 229);
622
+ try expectProbability(&chance.probability, 1, 1);
623
+ try expectValue(Optional(bool).None, chance.actions.p1.hit);
624
+ try chance.commit(.P1, .hit);
625
+ try expectValue(Optional(bool).true, chance.actions.p1.hit);
626
+ try expectProbability(&chance.probability, 229, 256);
627
+
628
+ chance.reset();
629
+
630
+ chance.hit(false, 255);
631
+ try expectProbability(&chance.probability, 1, 1);
632
+ try expectValue(Optional(bool).None, chance.actions.p2.hit);
633
+ try chance.commit(.P2, .miss);
634
+ try expectValue(Optional(bool).false, chance.actions.p2.hit);
635
+ try expectProbability(&chance.probability, 1, 256);
636
+ }
637
+
638
+ test "Chance.criticalHit" {
639
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
640
+
641
+ try chance.criticalHit(.P1, true, 17);
642
+ if (showdown) {
643
+ try expectValue(Optional(bool).true, chance.actions.p1.critical_hit);
644
+ try expectProbability(&chance.probability, 17, 256);
645
+ } else {
646
+ try expectProbability(&chance.probability, 1, 1);
647
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
648
+
649
+ try chance.commit(.P1, .hit);
650
+ try expectProbability(&chance.probability, 1, 1);
651
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
652
+
653
+ chance.hit(true, 128);
654
+
655
+ try chance.commit(.P1, .hit);
656
+ try expectProbability(&chance.probability, 1, 2);
657
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
658
+
659
+ chance.probability.reset();
660
+ try chance.damage(.P1, 217);
661
+
662
+ try chance.commit(.P1, .miss);
663
+ try expectProbability(&chance.probability, 1, 2);
664
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
665
+
666
+ chance.probability.reset();
667
+
668
+ try chance.commit(.P1, .hit);
669
+ try expectProbability(&chance.probability, 17, 19968); // (1/2) * (17/256) * (1/39)
670
+ try expectValue(Optional(bool).true, chance.actions.p1.critical_hit);
671
+ }
672
+
673
+ chance.reset();
674
+
675
+ try chance.criticalHit(.P2, false, 5);
676
+ if (showdown) {
677
+ try expectProbability(&chance.probability, 251, 256);
678
+ try expectValue(Optional(bool).false, chance.actions.p2.critical_hit);
679
+ } else {
680
+ try expectProbability(&chance.probability, 1, 1);
681
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
682
+
683
+ try chance.commit(.P1, .hit);
684
+ try expectProbability(&chance.probability, 1, 1);
685
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
686
+
687
+ chance.hit(true, 128);
688
+
689
+ try chance.commit(.P1, .hit);
690
+ try expectProbability(&chance.probability, 1, 2);
691
+ try expectValue(Optional(bool).None, chance.actions.p1.critical_hit);
692
+
693
+ chance.probability.reset();
694
+ try chance.damage(.P1, 217);
695
+
696
+ try chance.commit(.P1, .hit);
697
+ try expectProbability(&chance.probability, 251, 19968); // (1/2) * (251/256) * (1/39)
698
+ try expectValue(Optional(bool).false, chance.actions.p1.critical_hit);
699
+ }
700
+ }
701
+
702
+ test "Chance.damage" {
703
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
704
+
705
+ try chance.damage(.P1, 217);
706
+ if (showdown) {
707
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
708
+ try expectProbability(&chance.probability, 1, 39);
709
+ } else {
710
+ try expectProbability(&chance.probability, 1, 1);
711
+ try expectValue(@as(u8, 0), chance.actions.p1.damage);
712
+
713
+ try chance.commit(.P1, .miss);
714
+ try expectProbability(&chance.probability, 1, 1);
715
+ try expectValue(@as(u8, 0), chance.actions.p1.damage);
716
+
717
+ try chance.commit(.P1, .hit);
718
+ try expectProbability(&chance.probability, 1, 39);
719
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
720
+ }
721
+ }
722
+
723
+ test "Chance.secondaryChance" {
724
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
725
+
726
+ try chance.secondaryChance(.P1, true, 25);
727
+ try expectProbability(&chance.probability, 25, 256);
728
+ try expectValue(Optional(bool).true, chance.actions.p1.secondary_chance);
729
+
730
+ chance.reset();
731
+
732
+ try chance.secondaryChance(.P2, false, 77);
733
+ try expectProbability(&chance.probability, 179, 256);
734
+ try expectValue(Optional(bool).false, chance.actions.p2.secondary_chance);
735
+ }
736
+
737
+ test "Chance.confused" {
738
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
739
+
740
+ try chance.confused(.P1, false);
741
+ try expectProbability(&chance.probability, 1, 2);
742
+ try expectValue(Optional(bool).false, chance.actions.p1.confused);
743
+
744
+ chance.reset();
745
+
746
+ try chance.confused(.P2, true);
747
+ try expectProbability(&chance.probability, 1, 2);
748
+ try expectValue(Optional(bool).true, chance.actions.p2.confused);
749
+ }
750
+
751
+ test "Chance.paralyzed" {
752
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
753
+
754
+ try chance.paralyzed(.P1, false);
755
+ try expectProbability(&chance.probability, 3, 4);
756
+ try expectValue(Optional(bool).false, chance.actions.p1.paralyzed);
757
+
758
+ chance.reset();
759
+
760
+ try chance.paralyzed(.P2, true);
761
+ try expectProbability(&chance.probability, 1, 4);
762
+ try expectValue(Optional(bool).true, chance.actions.p2.paralyzed);
763
+ }
764
+
765
+ test "Chance.moveSlot" {
766
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
767
+ var ms = [_]MoveSlot{ .{ .id = Move.Surf }, .{ .id = Move.Psychic }, .{ .id = Move.Recover } };
768
+
769
+ try chance.moveSlot(.P2, 2, &ms, 2);
770
+ try expectProbability(&chance.probability, 1, 2);
771
+ try expectValue(@as(u4, 2), chance.actions.p2.move_slot);
772
+
773
+ chance.reset();
774
+
775
+ try chance.moveSlot(.P1, 1, &ms, 0);
776
+ try expectProbability(&chance.probability, 1, 3);
777
+ try expectValue(@as(u4, 1), chance.actions.p1.move_slot);
778
+ }
779
+
780
+ test "Chance.multiHit" {
781
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
782
+
783
+ try chance.multiHit(.P1, 3);
784
+ try expectProbability(&chance.probability, 3, 8);
785
+ try expectValue(@as(u4, 3), chance.actions.p1.multi_hit);
786
+
787
+ chance.reset();
788
+
789
+ try chance.multiHit(.P2, 5);
790
+ try expectProbability(&chance.probability, 1, 8);
791
+ try expectValue(@as(u4, 5), chance.actions.p2.multi_hit);
792
+ }
793
+
794
+ test "Chance.duration" {
795
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
796
+
797
+ chance.duration(.sleep, .P1, .P2, 2);
798
+ try expectValue(@as(u4, 2), chance.actions.p1.duration);
799
+ try expectValue(@as(u3, 1), chance.actions.p2.durations.sleep);
800
+
801
+ chance.reset();
802
+
803
+ chance.duration(.binding, .P2, .P2, 4);
804
+ if (!showdown) {
805
+ try expectValue(@as(u4, 0), chance.actions.p2.duration);
806
+ try expectValue(@as(u3, 0), chance.actions.p2.durations.binding);
807
+ try chance.commit(.P2, .hit);
808
+ }
809
+ try expectValue(@as(u4, 4), chance.actions.p2.duration);
810
+ try expectValue(@as(u3, 1), chance.actions.p2.durations.binding);
811
+ }
812
+
813
+ test "Chance.sleep" {
814
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
815
+
816
+ for ([_]u8{ 7, 6, 5, 4, 3, 2, 1 }, 1..8) |d, i| {
817
+ chance.actions.p1.durations.sleep = @intCast(i);
818
+ try chance.sleep(.P1, 0);
819
+ try expectProbability(&chance.probability, 1, d);
820
+ try expectValue(@as(u3, 0), chance.actions.p1.durations.sleep);
821
+
822
+ chance.reset();
823
+
824
+ if (i < 7) {
825
+ chance.actions.p1.durations.sleep = @intCast(i);
826
+ try chance.sleep(.P1, 1);
827
+ try expectProbability(&chance.probability, d - 1, d);
828
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p1.durations.sleep);
829
+
830
+ chance.reset();
831
+ }
832
+ }
833
+ }
834
+
835
+ test "Chance.confusion" {
836
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
837
+
838
+ for ([_]u8{ 1, 4, 3, 2, 1 }, 1..6) |d, i| {
839
+ if (i > 1) {
840
+ chance.actions.p2.durations.confusion = @intCast(i);
841
+ try chance.confusion(.P2, 0);
842
+ try expectProbability(&chance.probability, 1, d);
843
+ try expectValue(@as(u3, 0), chance.actions.p2.durations.confusion);
844
+
845
+ chance.reset();
846
+ }
847
+
848
+ if (i < 5) {
849
+ chance.actions.p2.durations.confusion = @intCast(i);
850
+ try chance.confusion(.P2, 1);
851
+ try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
852
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p2.durations.confusion);
853
+
854
+ chance.reset();
855
+ }
856
+ }
857
+ }
858
+
859
+ test "Chance.attacking" {
860
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
861
+
862
+ for ([_]u8{ 1, 2, 1 }, 1..4) |d, i| {
863
+ if (i > 1) {
864
+ chance.actions.p2.durations.attacking = @intCast(i);
865
+ try chance.attacking(.P2, 0);
866
+ try expectProbability(&chance.probability, 1, d);
867
+ try expectValue(@as(u3, 0), chance.actions.p2.durations.attacking);
868
+
869
+ chance.reset();
870
+ }
871
+
872
+ if (i < 3) {
873
+ chance.actions.p2.durations.attacking = @intCast(i);
874
+ try chance.attacking(.P2, 1);
875
+ try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
876
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p2.durations.attacking);
877
+
878
+ chance.reset();
879
+ }
880
+ }
881
+ }
882
+
883
+ test "Chance.binding" {
884
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
885
+
886
+ const ps = [_]u8{ 3, 3, 1, 1 };
887
+ const qs = [_]u8{ 8, 5, 2, 1 };
888
+
889
+ for (ps, qs, 1..5) |p, q, i| {
890
+ chance.actions.p1.durations.binding = @intCast(i);
891
+ try chance.binding(.P1, 0);
892
+ try expectProbability(&chance.probability, p, q);
893
+ try expectValue(@as(u3, 0), chance.actions.p1.durations.binding);
894
+
895
+ chance.reset();
896
+
897
+ if (i < 4) {
898
+ chance.actions.p1.durations.binding = @intCast(i);
899
+ try chance.binding(.P1, 1);
900
+ try expectProbability(&chance.probability, q - p, q);
901
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p1.durations.binding);
902
+
903
+ chance.reset();
904
+ }
905
+ }
906
+ }
907
+
908
+ test "Chance.psywave" {
909
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
910
+
911
+ try chance.psywave(.P2, 100, 150);
912
+ try expectProbability(&chance.probability, 1, 150);
913
+ try expectValue(@as(u8, 101), chance.actions.p2.psywave);
914
+ }
915
+
916
+ test "Chance.metronome" {
917
+ var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
918
+
919
+ try chance.metronome(.P1, Move.HornAttack);
920
+ try expectProbability(&chance.probability, 1, 163);
921
+ try expectValue(Move.HornAttack, chance.actions.p1.metronome);
922
+ }
923
+
924
+ pub fn expectProbability(r: anytype, p: u64, q: u64) !void {
925
+ if (!enabled) return;
926
+
927
+ r.reduce();
928
+ if (r.p != p or r.q != q) {
929
+ print("expected {d}/{d}, found {}\n", .{ p, q, r });
930
+ return error.TestExpectedEqual;
931
+ }
932
+ }
933
+
934
+ pub fn expectValue(a: anytype, b: anytype) !void {
935
+ if (!enabled) return;
936
+
937
+ try expectEqual(a, b);
938
+ }
939
+
940
+ /// Null object pattern implementation of Generation I `Chance` which does nothing, though chance
941
+ /// tracking should additionally be turned off entirely via `options.chance`.
942
+ pub const NULL = Null{};
943
+
944
+ const Null = struct {
945
+ pub const Error = error{};
946
+
947
+ pub fn commit(self: Null, player: Player, kind: Commit) Error!void {
948
+ _ = .{ self, player, kind };
949
+ }
950
+
951
+ pub fn clearPending(self: Null) void {
952
+ _ = .{self};
953
+ }
954
+
955
+ pub fn clearDurations(self: Null, player: Player, haze: ?bool) void {
956
+ _ = .{ self, player, haze };
957
+ }
958
+
959
+ pub fn switched(self: Null, player: Player, in: u8, out: u8) void {
960
+ _ = .{ self, player, in, out };
961
+ }
962
+
963
+ pub fn speedTie(self: Null, p1: bool) Error!void {
964
+ _ = .{ self, p1 };
965
+ }
966
+
967
+ pub fn hit(self: Null, ok: bool, accuracy: u8) void {
968
+ _ = .{ self, ok, accuracy };
969
+ }
970
+
971
+ pub fn criticalHit(self: Null, player: Player, crit: bool, rate: u8) Error!void {
972
+ _ = .{ self, player, crit, rate };
973
+ }
974
+
975
+ pub fn damage(self: Null, player: Player, roll: u8) Error!void {
976
+ _ = .{ self, player, roll };
977
+ }
978
+
979
+ pub fn secondaryChance(self: Null, player: Player, proc: bool, rate: u8) Error!void {
980
+ _ = .{ self, player, proc, rate };
981
+ }
982
+
983
+ pub fn confused(self: Null, player: Player, ok: bool) Error!void {
984
+ _ = .{ self, player, ok };
985
+ }
986
+
987
+ pub fn paralyzed(self: Null, player: Player, ok: bool) Error!void {
988
+ _ = .{ self, player, ok };
989
+ }
990
+
991
+ pub fn moveSlot(self: Null, player: Player, slot: u4, ms: []const MoveSlot, n: u4) Error!void {
992
+ _ = .{ self, player, slot, ms, n };
993
+ }
994
+
995
+ pub fn multiHit(self: Null, player: Player, n: u3) Error!void {
996
+ _ = .{ self, player, n };
997
+ }
998
+
999
+ pub fn duration(
1000
+ self: Null,
1001
+ comptime field: Duration.Field,
1002
+ player: Player,
1003
+ target: Player,
1004
+ turns: u4,
1005
+ ) void {
1006
+ _ = .{ self, field, player, target, turns };
1007
+ }
1008
+
1009
+ pub fn sleep(self: Null, player: Player, turns: u4) Error!void {
1010
+ _ = .{ self, player, turns };
1011
+ }
1012
+
1013
+ pub fn disable(self: Null, player: Player, turns: u4) Error!void {
1014
+ _ = .{ self, player, turns };
1015
+ }
1016
+
1017
+ pub fn confusion(self: Null, player: Player, turns: u4) Error!void {
1018
+ _ = .{ self, player, turns };
1019
+ }
1020
+
1021
+ pub fn attacking(self: Null, player: Player, turns: u4) Error!void {
1022
+ _ = .{ self, player, turns };
1023
+ }
1024
+
1025
+ pub fn binding(self: Null, player: Player, turns: u4) Error!void {
1026
+ _ = .{ self, player, turns };
1027
+ }
1028
+
1029
+ pub fn psywave(self: Null, player: Player, power: u8, max: u8) Error!void {
1030
+ _ = .{ self, player, power, max };
1031
+ }
1032
+
1033
+ pub fn metronome(self: Null, player: Player, move: Move) Error!void {
1034
+ _ = .{ self, player, move };
1035
+ }
1036
+ };