@pkmn/engine 0.1.0-dev.62399ed5 → 0.1.0-dev.6be9a775

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.
@@ -11,6 +11,7 @@ const options = @import("../common/options.zig");
11
11
  const rational = @import("../common/rational.zig");
12
12
  const util = @import("../common/util.zig");
13
13
 
14
+ const Array = @import("../common/array.zig").Array;
14
15
  const Player = @import("../common/data.zig").Player;
15
16
  const Optional = @import("../common/optional.zig").Optional;
16
17
 
@@ -19,9 +20,16 @@ const data = @import("data.zig");
19
20
  const enabled = options.chance;
20
21
  const showdown = options.showdown;
21
22
 
23
+ const PointerType = util.PointerType;
24
+ const isPointerTo = util.isPointerTo;
25
+
22
26
  const Move = data.Move;
23
27
  const MoveSlot = data.MoveSlot;
24
28
 
29
+ const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
30
+ const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
31
+ const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
32
+
25
33
  /// Actions taken by a hypothetical "chance player" that convey information about which RNG events
26
34
  /// were observed during a Generation I battle `update`. This can additionally be provided as input
27
35
  /// to the `update` call to override the normal behavior of the RNG in order to force specific
@@ -37,17 +45,11 @@ pub const Actions = extern struct {
37
45
  }
38
46
 
39
47
  /// 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);
48
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Action) {
49
+ assert(isPointerTo(self, Actions));
42
50
  return if (player == .P1) &self.p1 else &self.p2;
43
51
  }
44
52
 
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
53
  /// Returns true if `a` is equal to `b`.
52
54
  pub fn eql(a: Actions, b: Actions) bool {
53
55
  return @as(u128, @bitCast(a)) == @as(u128, @bitCast(b));
@@ -57,20 +59,15 @@ pub const Actions = extern struct {
57
59
  /// same shape if they have the same fields set (though those fields need not necessarily be
58
60
  /// set to the same value).
59
61
  pub fn matches(a: Actions, b: Actions) bool {
60
- inline for (@typeInfo(Actions).Struct.fields) |player| {
61
- inline for (@typeInfo(Action).Struct.fields) |field| {
62
+ inline for (@field(@typeInfo(Actions), @tagName(Struct)).fields) |player| {
63
+ inline for (@field(@typeInfo(Action), @tagName(Struct)).fields) |field| {
62
64
  const a_val = @field(@field(a, player.name), field.name);
63
65
  const b_val = @field(@field(b, player.name), field.name);
64
66
 
65
67
  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))
68
+ Enum => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
72
69
  return false,
73
- .Int => if ((a_val > 0) != (b_val > 0)) return false,
70
+ Int => if ((a_val > 0) != (b_val > 0)) return false,
74
71
  else => unreachable,
75
72
  }
76
73
  }
@@ -92,14 +89,10 @@ pub const Actions = extern struct {
92
89
  }
93
90
  };
94
91
 
95
- // TODO: ziglang/zig#19730
96
92
  test Actions {
97
93
  const a: Actions = .{ .p1 = .{ .hit = .true, .critical_hit = .false, .damage = 245 } };
98
94
  const b: Actions = .{ .p1 = .{ .hit = .false, .critical_hit = .true, .damage = 246 } };
99
95
  const c: Actions = .{ .p1 = .{ .hit = .true } };
100
- // const d: Actions = .{ .p2 = .{ .hit = .true, .durations = .{ .sleep = 2 } } };
101
- // const e: Actions = .{ .p2 = .{ .hit = .false, .durations = .{ .sleep = 4 } } };
102
- // const f: Actions = .{ .p1 = .{ .hit = .false, .durations = .{ .sleep = 4 } } };
103
96
 
104
97
  try expect(a.eql(a));
105
98
  try expect(!a.eql(b));
@@ -112,10 +105,22 @@ test Actions {
112
105
  try expect(b.matches(a));
113
106
  try expect(!a.matches(c));
114
107
  try expect(!c.matches(a));
115
- // try expect(d.matches(e));
116
- // try expect(!d.matches(f));
117
108
  }
118
109
 
110
+ /// Observation made about a duration - whether the duration has started, been continued, or ended.
111
+ pub const Observation = enum { started, continuing, ended };
112
+ /// An extension of Observation to account for "hidden" confusion.
113
+ pub const Confusion = enum {
114
+ started,
115
+ continuing,
116
+ ended,
117
+ /// An indicator that an existing confusion duration has been overwritten by a new confusion
118
+ /// duration due to a Thrashing move ending. This is not immediately observable to an opponent,
119
+ /// to compute probabilities from an opponent's point of view based solely on public information
120
+ /// extra work must be done to correct for the information leak
121
+ overwritten,
122
+ };
123
+
119
124
  /// Information about the RNG that was observed during a Generation I battle `update` for a
120
125
  /// single player.
121
126
  pub const Action = packed struct(u64) {
@@ -140,12 +145,25 @@ pub const Action = packed struct(u64) {
140
145
  /// or Rolls.{sleepDuration,disableDuration,confusionDuration,attackingDuration} otherwise.
141
146
  duration: u4 = 0,
142
147
 
143
- /// Observed values of various durations. Does not influence future RNG calls. TODO
144
- durations: Duration = .{},
148
+ /// TODO
149
+ sleep: Optional(Observation) = .None,
150
+ /// TODO
151
+ confusion: Optional(Confusion) = .None,
152
+ /// TODO
153
+ disable: Optional(Observation) = .None,
154
+ /// TODO
155
+ attacking: Optional(Observation) = .None,
156
+ /// TODO
157
+ binding: Optional(Observation) = .None,
158
+
159
+ _: u1 = 0,
145
160
 
146
161
  /// If not 0, the move slot (1-4) to return in Rolls.moveSlot. If present as an override,
147
162
  /// invalid values (eg. due to empty move slots or 0 PP) will be ignored.
148
163
  move_slot: u4 = 0,
164
+
165
+ /// TODO
166
+ pp: u4 = 0,
149
167
  /// If not 0, the value (2-5) to return for Rolls.distribution for multi hit.
150
168
  multi_hit: u4 = 0,
151
169
 
@@ -155,15 +173,8 @@ pub const Action = packed struct(u64) {
155
173
  /// If not None, the Move to return for Rolls.metronome.
156
174
  metronome: Move = .None,
157
175
 
158
- pub const DURATIONS: u64 = 0x000000FFFF000000;
159
-
160
176
  pub const Field = std.meta.FieldEnum(Action);
161
177
 
162
- /// Perform a reset by clearing fields which should not persist across updates.
163
- pub fn reset(self: *Action) void {
164
- self.* = @bitCast(@as(u64, @bitCast(self.*)) & DURATIONS);
165
- }
166
-
167
178
  pub fn format(a: Action, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
168
179
  _ = .{ f, o };
169
180
  try fmt(a, w, false);
@@ -172,22 +183,10 @@ pub const Action = packed struct(u64) {
172
183
  pub fn fmt(self: Action, writer: anytype, shape: bool) !void {
173
184
  try writer.writeByte('(');
174
185
  var printed = false;
175
- inline for (@typeInfo(Action).Struct.fields) |field| {
186
+ inline for (@field(@typeInfo(Action), @tagName(Struct)).fields) |field| {
176
187
  const val = @field(self, field.name);
177
188
  switch (@typeInfo(@TypeOf(val))) {
178
- .Struct => inline for (@typeInfo(@TypeOf(val)).Struct.fields) |f| {
179
- const v = @field(val, f.name);
180
- if (v != 0) {
181
- if (printed) try writer.writeAll(", ");
182
- if (shape) {
183
- try writer.print("{s}:?", .{f.name});
184
- } else {
185
- try writer.print("{s}:{d}", .{ f.name, v });
186
- }
187
- printed = true;
188
- }
189
- },
190
- .Enum => if (val != .None) {
189
+ Enum => if (val != .None) {
191
190
  if (printed) try writer.writeAll(", ");
192
191
  if (shape) {
193
192
  try writer.print("{s}:?", .{field.name});
@@ -196,12 +195,19 @@ pub const Action = packed struct(u64) {
196
195
  if (val == .false) "!" else "",
197
196
  field.name,
198
197
  });
198
+ } else if (@TypeOf(val) == Optional(Observation) or
199
+ @TypeOf(val) == Optional(Confusion))
200
+ {
201
+ try writer.print("{s}{s}", .{
202
+ ([_][]const u8{ "+", "", "-", "#" })[@intFromEnum(val) - 1],
203
+ field.name,
204
+ });
199
205
  } else {
200
206
  try writer.print("{s}:{s}", .{ field.name, @tagName(val) });
201
207
  }
202
208
  printed = true;
203
209
  },
204
- .Int => if (val != 0) {
210
+ Int => if (val != 0) {
205
211
  if (printed) try writer.writeAll(", ");
206
212
  if (shape) {
207
213
  try writer.print("{s}:?", .{field.name});
@@ -217,43 +223,80 @@ pub const Action = packed struct(u64) {
217
223
  }
218
224
  };
219
225
 
220
- test Action {
221
- var a: Action = .{ .hit = .true, .durations = .{ .sleep = 3 }, .damage = 225 };
222
- a.reset();
223
-
224
- try expectEqual(Action{ .hit = .None, .durations = .{ .sleep = 3 }, .damage = 0 }, a);
225
- }
226
-
227
226
  /// TODO
228
- pub const Durations = extern struct {
229
- /// Information about the durations for Player 1.
227
+ pub const Durations = struct {
228
+ /// TODO
230
229
  p1: Duration = .{},
231
- /// Information about the durations for Player 2.
230
+ /// TODO
232
231
  p2: Duration = .{},
233
232
 
234
233
  comptime {
235
- assert(@sizeOf(Durations) == 4);
234
+ assert(@sizeOf(Durations) == 8);
235
+ }
236
+
237
+ /// TODO
238
+ pub fn get(self: anytype, player: Player) PointerType(@TypeOf(self), Duration) {
239
+ assert(isPointerTo(self, Durations));
240
+ return if (player == .P1) &self.p1 else &self.p2;
241
+ }
242
+
243
+ pub fn format(
244
+ self: Durations,
245
+ comptime fmt: []const u8,
246
+ opts: std.fmt.FormatOptions,
247
+ writer: anytype,
248
+ ) !void {
249
+ _ = .{ fmt, opts };
250
+ try writer.print("[P1 = {}, P2 = {}]", .{ self.p1, self.p2 });
236
251
  }
237
252
  };
238
253
 
239
- /// Observed values for various durations that need to be tracked in order to properly
240
- /// deduplicate transitions with a primary key. TODO
241
- pub const Duration = packed struct(u16) {
242
- /// The number of turns a Pokémon has been observed to be sleeping.
243
- sleep: u3 = 0,
244
- /// The number of turns a Pokémon has been observed to be confused.
254
+ const Sleeps = Array(6, u3);
255
+
256
+ /// TODO
257
+ pub const Duration = packed struct(u32) {
258
+ sleeps: Sleeps.T = 0,
245
259
  confusion: u3 = 0,
246
- /// The number of turns a Pokémon has been observed to be disabled.
247
260
  disable: u4 = 0,
248
- /// The number of turns a Pokémon has been observed to be attacking.
249
261
  attacking: u3 = 0,
250
- /// The number of turns a Pokémon has been observed to be binding their opponent.
251
262
  binding: u3 = 0,
252
263
 
253
- pub const Field = std.meta.FieldEnum(Duration);
264
+ _: u1 = 0,
265
+
266
+ pub fn format(
267
+ self: Duration,
268
+ comptime fmt: []const u8,
269
+ opts: std.fmt.FormatOptions,
270
+ writer: anytype,
271
+ ) !void {
272
+ _ = .{ fmt, opts };
273
+ try writer.writeByte('(');
274
+ var printed = false;
275
+ inline for (@field(@typeInfo(Duration), @tagName(Struct)).fields) |field| {
276
+ const val = @field(self, field.name);
277
+ switch (@typeInfo(@TypeOf(val))) {
278
+ Int => if (val != 0) {
279
+ if (printed) try writer.writeAll(", ");
280
+ if (comptime std.mem.eql(u8, field.name, "sleeps")) {
281
+ try writer.print("{s}:[", .{field.name});
282
+ for (0..6) |i| {
283
+ if (i != 0) try writer.writeByte(',');
284
+ try writer.print("{d}", .{Sleeps.get(val, i)});
285
+ }
286
+ try writer.writeAll("]");
287
+ } else {
288
+ try writer.print("{s}:{d}", .{ field.name, val });
289
+ }
290
+ printed = true;
291
+ },
292
+ else => unreachable,
293
+ }
294
+ }
295
+ try writer.writeByte(')');
296
+ }
254
297
  };
255
298
 
256
- pub const Commit = enum { hit, miss, binding };
299
+ pub const Commit = enum { hit, miss, binding, err, glitch };
257
300
 
258
301
  const uN = if (options.miss) u8 else u9;
259
302
 
@@ -268,14 +311,8 @@ pub fn Chance(comptime Rational: type) type {
268
311
  /// The actions taken by a hypothetical "chance player" that convey information about which
269
312
  /// RNG events were observed during a battle `update`.
270
313
  actions: Actions = .{},
271
-
272
- // Tracks the observed sleep durations for the Pokémon in both player's parties. Unlike
273
- // other durations which are all tied to volatiles, sleep's counter persists through
274
- // switching and so we must store it here. The indices of these arrays correspond to the
275
- // `order` field of a Side. This information could be stored in actions but size is a
276
- // concern so this is tracked separately as actions only purports to track RNG events
277
- // observed during a single `update` (not across updates).
278
- sleeps: [2][6]u3 = .{ .{0} ** 6, .{0} ** 6 },
314
+ /// TODO
315
+ durations: Durations = .{},
279
316
 
280
317
  // In many cases on both the cartridge and on Pokémon Showdown rolls are made even though
281
318
  // later checks render their result irrelevant (missing when the target is actually immune,
@@ -299,7 +336,7 @@ pub fn Chance(comptime Rational: type) type {
299
336
  damage_roll: u8 = 0,
300
337
  hit: bool = false,
301
338
  hit_probablity: uN = 0,
302
- binding: u8 = 0,
339
+ glitch: bool = false,
303
340
  } = .{},
304
341
 
305
342
  /// Possible error returned by operations tracking chance probability.
@@ -310,7 +347,7 @@ pub fn Chance(comptime Rational: type) type {
310
347
  if (!enabled) return;
311
348
 
312
349
  self.probability.reset();
313
- self.actions.reset();
350
+ self.actions = .{};
314
351
  self.pending = .{};
315
352
  }
316
353
 
@@ -319,20 +356,18 @@ pub fn Chance(comptime Rational: type) type {
319
356
 
320
357
  assert(!showdown);
321
358
 
359
+ if (kind == .glitch and !self.pending.glitch) return;
360
+
322
361
  var action = self.actions.get(player);
323
362
 
324
- // We need to handle binding specially because we still need to commit it when attacking
325
- // into a Pokémon that is immune
326
- if (kind != .miss and self.pending.binding != 0) {
327
- assert(action.duration == 0);
328
- action.duration = @intCast(self.pending.binding);
329
- assert(action.durations.binding == 0);
330
- action.durations.binding = 1;
363
+ if (kind == .err and self.pending.crit_probablity != 0) {
364
+ try self.probability.update(self.pending.crit_probablity, 256);
365
+ action.critical_hit = if (self.pending.crit) .true else .false;
366
+ return;
331
367
  }
332
- if (kind == .binding) return;
333
368
 
334
369
  // Always commit the hit result if we make it here (commit won't be called at all if the
335
- // target is immune/behind a sub/etc)
370
+ // target is immune*/behind a sub/etc)
336
371
  if (self.pending.hit_probablity != 0) {
337
372
  try self.probability.update(self.pending.hit_probablity, 256);
338
373
  action.hit = if (self.pending.hit) .true else .false;
@@ -342,13 +377,19 @@ pub fn Chance(comptime Rational: type) type {
342
377
  // we can't rely on the presence or absence of a damage roll to determine whether a
343
378
  // "critical hit" was spurious or not as moves that do 1 or less damage don't rol for
344
379
  // damage but can still crit (instead, whether or not a roll is deemed to be a no-op is
345
- // passed to the critical hit helper)
346
- if (kind != .hit) return assert(!self.pending.crit or self.pending.crit_probablity > 0);
380
+ // passed to the critical hit helper).
381
+ //
382
+ // There is a special edge case with the division-by-zero freeze where we need to commit
383
+ // the status of critical hit roll the occured before the move missed to avoid missing
384
+ // out on the possibility that the alternative branch encountered an error
385
+ if (kind != .hit and !self.pending.glitch) {
386
+ return assert(!self.pending.crit or self.pending.crit_probablity > 0);
387
+ }
347
388
 
348
389
  if (self.pending.crit_probablity != 0) {
349
390
  try self.probability.update(self.pending.crit_probablity, 256);
350
391
  action.critical_hit = if (self.pending.crit) .true else .false;
351
- }
392
+ } else assert(!self.pending.glitch);
352
393
 
353
394
  if (self.pending.damage_roll > 0) {
354
395
  try self.probability.update(1, 39);
@@ -356,41 +397,37 @@ pub fn Chance(comptime Rational: type) type {
356
397
  }
357
398
  }
358
399
 
359
- pub fn clearPending(self: *Self) void {
400
+ pub fn glitch(self: *Self) void {
360
401
  if (!enabled) return;
361
402
 
362
- self.pending = .{};
403
+ assert(!showdown);
404
+ self.pending.glitch = true;
363
405
  }
364
406
 
365
- pub fn clearDurations(self: *Self, player: Player, haze: ?bool) void {
407
+ pub fn clearPending(self: *Self) void {
366
408
  if (!enabled) return;
367
409
 
368
- var action = self.actions.get(player);
369
- if (haze) |status| {
370
- if (status) action.durations.sleep = 0;
371
- action.durations.confusion = 0;
372
- action.durations.disable = 0;
373
- } else {
374
- action.durations.attacking = 0;
375
- action.durations.binding = 0;
376
- }
410
+ assert(!showdown);
411
+ self.pending = .{};
377
412
  }
378
413
 
379
- pub fn switched(self: *Self, player: Player, in: u8, out: u8) void {
414
+ pub fn switched(self: *Self, player: Player, slot: u8) void {
380
415
  if (!enabled) return;
381
416
 
382
- assert(in >= 1 and in <= 6);
383
- assert(out >= 1 and out <= 6);
417
+ assert(slot >= 1 and slot <= 6);
384
418
 
385
- var action = self.actions.get(player);
419
+ var d = self.durations.get(player);
386
420
 
387
- const slp = action.durations.sleep;
388
- action.durations = .{};
421
+ const out = Sleeps.get(d.sleeps, 0);
422
+ d.sleeps = Sleeps.set(d.sleeps, 0, Sleeps.get(d.sleeps, slot - 1));
423
+ d.sleeps = Sleeps.set(d.sleeps, slot - 1, out);
389
424
 
390
- self.sleeps[@intFromEnum(player)][out - 1] = slp;
391
- action.durations.sleep = @intCast(self.sleeps[@intFromEnum(player)][in - 1]);
425
+ d.confusion = 0;
426
+ d.disable = 0;
427
+ d.attacking = 0;
428
+ d.binding = 0;
392
429
 
393
- self.actions.get(player.foe()).durations.binding = 0;
430
+ self.durations.get(player.foe()).binding = 0;
394
431
  }
395
432
 
396
433
  pub fn speedTie(self: *Self, p1: bool) Error!void {
@@ -489,108 +526,170 @@ pub fn Chance(comptime Rational: type) type {
489
526
  self.actions.get(player).multi_hit = n;
490
527
  }
491
528
 
492
- pub fn duration(
529
+ pub fn duration(self: *Self, player: Player, turns: u4) void {
530
+ if (!enabled) return;
531
+
532
+ self.actions.get(player).duration = if (options.key) 2 else turns;
533
+ }
534
+
535
+ pub fn observe(
493
536
  self: *Self,
494
- comptime field: Duration.Field,
537
+ comptime field: Action.Field,
495
538
  player: Player,
496
- target: Player,
497
- turns: u4,
539
+ obs: Optional(Confusion),
498
540
  ) void {
499
541
  if (!enabled) return;
500
542
 
501
- if (!showdown and field == .binding) {
502
- self.pending.binding = if (options.key) 1 else turns;
503
- } else {
504
- var durations = &self.actions.get(target).durations;
505
- assert(@field(durations, @tagName(field)) == 0 or
506
- (field == .confusion and player == target));
507
- @field(durations, @tagName(field)) = 1;
508
- self.actions.get(player).duration = if (options.key) 1 else turns;
543
+ var a = self.actions.get(player);
544
+ const val = @field(a, @tagName(field));
545
+ switch (obs) {
546
+ .overwritten => {
547
+ assert(field == .confusion);
548
+ assert(val == .started or val == .continuing);
549
+ assert(a.duration > 0);
550
+ a.confusion = obs;
551
+ self.durations.get(player).confusion = 1;
552
+ },
553
+ .None => {
554
+ @field(a, @tagName(field)) = .None;
555
+ if (field == .sleep) {
556
+ var d = self.durations.get(player);
557
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
558
+ } else {
559
+ @field(self.durations.get(player), @tagName(field)) = 0;
560
+ }
561
+ },
562
+ .started => {
563
+ assert(val == .None or val == .ended);
564
+ assert(switch (field) {
565
+ .confusion => a.duration > 0 or self.actions.get(player.foe()).duration > 0,
566
+ .sleep, .disable => self.actions.get(player.foe()).duration > 0,
567
+ else => a.duration > 0,
568
+ });
569
+ @field(a, @tagName(field)) = .started;
570
+ if (field == .sleep) {
571
+ var d = self.durations.get(player);
572
+ d.sleeps = Sleeps.set(d.sleeps, 0, 1);
573
+ } else {
574
+ @field(self.durations.get(player), @tagName(field)) = 1;
575
+ }
576
+ },
577
+ else => unreachable,
509
578
  }
510
579
  }
511
580
 
512
- pub fn sleep(self: *Self, player: Player, turns: u4) Error!void {
581
+ pub fn sleep(self: *Self, player: Player, obs: Optional(Observation)) Error!void {
513
582
  if (!enabled) return;
514
583
 
515
- var durations = &self.actions.get(player).durations;
516
- const n = durations.sleep;
517
- if (turns == 0) {
584
+ var a = self.actions.get(player);
585
+ assert(a.sleep == .None or a.sleep == .started);
586
+ a.sleep = obs;
587
+
588
+ var d = self.durations.get(player);
589
+ const n = Sleeps.get(d.sleeps, 0);
590
+
591
+ if (obs == .None) {
592
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
593
+ } else if (obs == .ended) {
518
594
  assert(n >= 1 and n <= 7);
519
595
  if (n != 7) try self.probability.update(1, 8 - @as(u4, n));
520
- durations.sleep = 0;
596
+ d.sleeps = Sleeps.set(d.sleeps, 0, 0);
521
597
  } else {
598
+ assert(obs == .continuing);
522
599
  assert(n >= 1 and n < 7);
523
600
  try self.probability.update(8 - @as(u4, n) - 1, 8 - @as(u4, n));
524
- durations.sleep += 1;
601
+ d.sleeps = Sleeps.set(d.sleeps, 0, n + 1);
525
602
  }
526
603
  }
527
604
 
528
- pub fn confusion(self: *Self, player: Player, turns: u4) Error!void {
605
+ pub fn confusion(self: *Self, player: Player, obs: Optional(Confusion)) Error!void {
529
606
  if (!enabled) return;
530
607
 
531
- var durations = &self.actions.get(player).durations;
532
- const n = durations.confusion;
533
- if (turns == 0) {
608
+ var a = self.actions.get(player);
609
+ assert(a.confusion == .None or a.confusion == .started);
610
+ a.confusion = obs;
611
+
612
+ var d = self.durations.get(player);
613
+ const n = d.confusion;
614
+
615
+ if (obs == .ended) {
534
616
  assert(n >= 2 and n <= 5);
535
617
  if (n != 5) try self.probability.update(1, 6 - @as(u4, n));
536
- durations.confusion = 0;
618
+ d.confusion = 0;
537
619
  } else {
620
+ assert(obs == .continuing);
538
621
  assert(n >= 1 and n < 5);
539
622
  if (n > 1) try self.probability.update(6 - @as(u4, n) - 1, 6 - @as(u4, n));
540
- durations.confusion += 1;
623
+ d.confusion += 1;
541
624
  }
542
625
  }
543
626
 
544
- pub fn disable(self: *Self, player: Player, turns: u4) Error!void {
627
+ pub fn disable(self: *Self, player: Player, obs: Optional(Observation)) Error!void {
545
628
  if (!enabled) return;
546
629
 
547
- var durations = &self.actions.get(player).durations;
548
- const n = durations.disable;
549
- if (turns == 0) {
630
+ var a = self.actions.get(player);
631
+ assert(a.disable == .None or a.disable == .started);
632
+ a.disable = obs;
633
+
634
+ var d = self.durations.get(player);
635
+ const n = d.disable;
636
+
637
+ if (obs == .ended) {
550
638
  assert(n >= 1 and n <= 8);
551
639
  if (n != 8) try self.probability.update(1, 9 - @as(u4, n));
552
- durations.disable = 0;
640
+ d.disable = 0;
553
641
  } else {
642
+ assert(obs == .continuing);
554
643
  assert(n >= 1 and n < 8);
555
644
  try self.probability.update(9 - @as(u4, n) - 1, 9 - @as(u4, n));
556
- durations.disable += 1;
645
+ d.disable += 1;
557
646
  }
558
647
  }
559
648
 
560
- pub fn attacking(self: *Self, player: Player, turns: u4) Error!void {
649
+ pub fn attacking(self: *Self, player: Player, obs: Optional(Observation)) Error!void {
561
650
  if (!enabled) return;
562
651
 
563
- var durations = &self.actions.get(player).durations;
564
- const n = durations.attacking;
565
- if (turns == 0) {
652
+ var a = self.actions.get(player);
653
+ assert(a.attacking == .None or a.attacking == .started);
654
+ a.attacking = obs;
655
+
656
+ var d = self.durations.get(player);
657
+ const n = d.attacking;
658
+ if (obs == .ended) {
566
659
  assert(n >= 2 and n <= 3);
567
660
  if (n != 3) try self.probability.update(1, 4 - @as(u4, n));
568
- durations.attacking = 0;
661
+ d.attacking = 0;
569
662
  } else {
663
+ assert(obs == .continuing);
570
664
  assert(n >= 1 and n < 3);
571
665
  if (n > 1) try self.probability.update(4 - @as(u4, n) - 1, 4 - @as(u4, n));
572
- durations.attacking += 1;
666
+ d.attacking += 1;
573
667
  }
574
668
  }
575
669
 
576
- pub fn binding(self: *Self, player: Player, turns: u4) Error!void {
670
+ pub fn binding(self: *Self, player: Player, obs: Optional(Observation)) Error!void {
577
671
  if (!enabled) return;
578
672
 
579
- var durations = &self.actions.get(player).durations;
580
- const n = durations.binding;
673
+ var a = self.actions.get(player);
674
+ assert(a.binding == .None or a.binding == .started);
675
+ a.binding = obs;
676
+
677
+ var d = self.durations.get(player);
678
+ const n = d.binding;
581
679
 
582
680
  assert(n > 0);
583
681
  const p: u4 = if (n < 3) 3 else 1;
584
682
  const q: u4 = if (n < 3) 8 - ((n - 1) * p) else 2;
585
683
 
586
- if (turns == 0) {
684
+ if (obs == .ended) {
587
685
  assert(n >= 1 and n <= 4);
588
686
  if (n != 4) try self.probability.update(p, q);
589
- durations.binding = 0;
687
+ d.binding = 0;
590
688
  } else {
689
+ assert(obs == .continuing);
591
690
  assert(n >= 1 and n < 4);
592
691
  try self.probability.update(q - p, q);
593
- durations.binding += 1;
692
+ d.binding += 1;
594
693
  }
595
694
  }
596
695
 
@@ -607,6 +706,15 @@ pub fn Chance(comptime Rational: type) type {
607
706
  try self.probability.update(1, Move.METRONOME.len);
608
707
  self.actions.get(player).metronome = move;
609
708
  }
709
+
710
+ pub fn pp(self: *Self, player: Player, n: u4) bool {
711
+ if (!enabled) return true;
712
+
713
+ var a = self.actions.get(player);
714
+ if (n > 1 and a.pp == n) return false;
715
+ if (a.metronome != .None) a.pp += n;
716
+ return true;
717
+ }
610
718
  };
611
719
  }
612
720
 
@@ -707,16 +815,16 @@ test "Chance.damage" {
707
815
 
708
816
  try chance.damage(.P1, 217);
709
817
  if (showdown) {
710
- try expectValue(217, chance.actions.p1.damage);
818
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
711
819
  try expectProbability(&chance.probability, 1, 39);
712
820
  } else {
713
821
  try expectProbability(&chance.probability, 1, 1);
714
- try expectValue(0, chance.actions.p1.damage);
822
+ try expectValue(@as(u8, 0), chance.actions.p1.damage);
715
823
 
716
824
  try chance.commit(.P1, .hit);
717
825
 
718
826
  try expectProbability(&chance.probability, 1, 39);
719
- try expectValue(217, chance.actions.p1.damage);
827
+ try expectValue(@as(u8, 217), chance.actions.p1.damage);
720
828
  }
721
829
  }
722
830
 
@@ -768,13 +876,13 @@ test "Chance.moveSlot" {
768
876
 
769
877
  try chance.moveSlot(.P2, 2, &ms, 2);
770
878
  try expectProbability(&chance.probability, 1, 2);
771
- try expectValue(2, chance.actions.p2.move_slot);
879
+ try expectValue(@as(u4, 2), chance.actions.p2.move_slot);
772
880
 
773
881
  chance.reset();
774
882
 
775
883
  try chance.moveSlot(.P1, 1, &ms, 0);
776
884
  try expectProbability(&chance.probability, 1, 3);
777
- try expectValue(1, chance.actions.p1.move_slot);
885
+ try expectValue(@as(u4, 1), chance.actions.p1.move_slot);
778
886
  }
779
887
 
780
888
  test "Chance.multiHit" {
@@ -782,51 +890,43 @@ test "Chance.multiHit" {
782
890
 
783
891
  try chance.multiHit(.P1, 3);
784
892
  try expectProbability(&chance.probability, 3, 8);
785
- try expectValue(3, chance.actions.p1.multi_hit);
893
+ try expectValue(@as(u4, 3), chance.actions.p1.multi_hit);
786
894
 
787
895
  chance.reset();
788
896
 
789
897
  try chance.multiHit(.P2, 5);
790
898
  try expectProbability(&chance.probability, 1, 8);
791
- try expectValue(5, chance.actions.p2.multi_hit);
899
+ try expectValue(@as(u4, 5), chance.actions.p2.multi_hit);
792
900
  }
793
901
 
794
902
  test "Chance.duration" {
795
903
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
796
904
 
797
- chance.duration(.sleep, .P1, .P2, 2);
798
- try expectValue(2, chance.actions.p1.duration);
799
- try expectValue(1, chance.actions.p2.durations.sleep);
905
+ chance.duration(.P1, 2);
906
+ try expectValue(@as(u4, 2), chance.actions.p1.duration);
800
907
 
801
908
  chance.reset();
802
909
 
803
- chance.duration(.binding, .P2, .P2, 4);
804
- if (!showdown) {
805
- try expectValue(0, chance.actions.p2.duration);
806
- try expectValue(0, chance.actions.p2.durations.binding);
807
-
808
- try chance.commit(.P2, .hit);
809
- }
810
- try expectValue(4, chance.actions.p2.duration);
811
- try expectValue(1, chance.actions.p2.durations.binding);
910
+ chance.duration(.P2, 4);
911
+ try expectValue(@as(u4, 4), chance.actions.p2.duration);
812
912
  }
813
913
 
814
914
  test "Chance.sleep" {
815
915
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
816
916
 
817
917
  for ([_]u8{ 7, 6, 5, 4, 3, 2, 1 }, 1..8) |d, i| {
818
- chance.actions.p1.durations.sleep = @intCast(i);
819
- try chance.sleep(.P1, 0);
918
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
919
+ try chance.sleep(.P1, .ended);
820
920
  try expectProbability(&chance.probability, 1, d);
821
- try expectValue(0, chance.actions.p1.durations.sleep);
921
+ try expectValue(@as(u3, 0), Sleeps.get(chance.durations.p1.sleeps, 0));
822
922
 
823
923
  chance.reset();
824
924
 
825
925
  if (i < 7) {
826
- chance.actions.p1.durations.sleep = @intCast(i);
827
- try chance.sleep(.P1, 1);
926
+ chance.durations.p1.sleeps = Sleeps.set(chance.durations.p1.sleeps, 0, @intCast(i));
927
+ try chance.sleep(.P1, .continuing);
828
928
  try expectProbability(&chance.probability, d - 1, d);
829
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p1.durations.sleep);
929
+ try expectValue(@as(u3, @intCast(i)) + 1, Sleeps.get(chance.durations.p1.sleeps, 0));
830
930
 
831
931
  chance.reset();
832
932
  }
@@ -838,19 +938,19 @@ test "Chance.confusion" {
838
938
 
839
939
  for ([_]u8{ 1, 4, 3, 2, 1 }, 1..6) |d, i| {
840
940
  if (i > 1) {
841
- chance.actions.p2.durations.confusion = @intCast(i);
842
- try chance.confusion(.P2, 0);
941
+ chance.durations.p2.confusion = @intCast(i);
942
+ try chance.confusion(.P2, .ended);
843
943
  try expectProbability(&chance.probability, 1, d);
844
- try expectValue(0, chance.actions.p2.durations.confusion);
944
+ try expectValue(@as(u3, 0), chance.durations.p2.confusion);
845
945
 
846
946
  chance.reset();
847
947
  }
848
948
 
849
949
  if (i < 5) {
850
- chance.actions.p2.durations.confusion = @intCast(i);
851
- try chance.confusion(.P2, 1);
950
+ chance.durations.p2.confusion = @intCast(i);
951
+ try chance.confusion(.P2, .continuing);
852
952
  try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
853
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p2.durations.confusion);
953
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.confusion);
854
954
 
855
955
  chance.reset();
856
956
  }
@@ -861,18 +961,18 @@ test "Chance.disable" {
861
961
  var chance: Chance(rational.Rational(u64)) = .{ .probability = .{} };
862
962
 
863
963
  for ([_]u8{ 8, 7, 6, 5, 4, 3, 2, 1 }, 1..9) |d, i| {
864
- chance.actions.p1.durations.disable = @intCast(i);
865
- try chance.disable(.P1, 0);
964
+ chance.durations.p1.disable = @intCast(i);
965
+ try chance.disable(.P1, .ended);
866
966
  try expectProbability(&chance.probability, 1, d);
867
- try expectValue(0, chance.actions.p1.durations.disable);
967
+ try expectValue(@as(u4, 0), chance.durations.p1.disable);
868
968
 
869
969
  chance.reset();
870
970
 
871
971
  if (i < 8) {
872
- chance.actions.p1.durations.disable = @intCast(i);
873
- try chance.disable(.P1, 1);
972
+ chance.durations.p1.disable = @intCast(i);
973
+ try chance.disable(.P1, .continuing);
874
974
  try expectProbability(&chance.probability, d - 1, d);
875
- try expectValue(@as(u4, @intCast(i)) + 1, chance.actions.p1.durations.disable);
975
+ try expectValue(@as(u4, @intCast(i)) + 1, chance.durations.p1.disable);
876
976
 
877
977
  chance.reset();
878
978
  }
@@ -884,19 +984,19 @@ test "Chance.attacking" {
884
984
 
885
985
  for ([_]u8{ 1, 2, 1 }, 1..4) |d, i| {
886
986
  if (i > 1) {
887
- chance.actions.p2.durations.attacking = @intCast(i);
888
- try chance.attacking(.P2, 0);
987
+ chance.durations.p2.attacking = @intCast(i);
988
+ try chance.attacking(.P2, .ended);
889
989
  try expectProbability(&chance.probability, 1, d);
890
- try expectValue(0, chance.actions.p2.durations.attacking);
990
+ try expectValue(@as(u3, 0), chance.durations.p2.attacking);
891
991
 
892
992
  chance.reset();
893
993
  }
894
994
 
895
995
  if (i < 3) {
896
- chance.actions.p2.durations.attacking = @intCast(i);
897
- try chance.attacking(.P2, 1);
996
+ chance.durations.p2.attacking = @intCast(i);
997
+ try chance.attacking(.P2, .continuing);
898
998
  try expectProbability(&chance.probability, if (d > 1) d - 1 else d, d);
899
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p2.durations.attacking);
999
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p2.attacking);
900
1000
 
901
1001
  chance.reset();
902
1002
  }
@@ -910,18 +1010,18 @@ test "Chance.binding" {
910
1010
  const qs = [_]u8{ 8, 5, 2, 1 };
911
1011
 
912
1012
  for (ps, qs, 1..5) |p, q, i| {
913
- chance.actions.p1.durations.binding = @intCast(i);
914
- try chance.binding(.P1, 0);
1013
+ chance.durations.p1.binding = @intCast(i);
1014
+ try chance.binding(.P1, .ended);
915
1015
  try expectProbability(&chance.probability, p, q);
916
- try expectValue(0, chance.actions.p1.durations.binding);
1016
+ try expectValue(@as(u3, 0), chance.durations.p1.binding);
917
1017
 
918
1018
  chance.reset();
919
1019
 
920
1020
  if (i < 4) {
921
- chance.actions.p1.durations.binding = @intCast(i);
922
- try chance.binding(.P1, 1);
1021
+ chance.durations.p1.binding = @intCast(i);
1022
+ try chance.binding(.P1, .continuing);
923
1023
  try expectProbability(&chance.probability, q - p, q);
924
- try expectValue(@as(u3, @intCast(i)) + 1, chance.actions.p1.durations.binding);
1024
+ try expectValue(@as(u3, @intCast(i)) + 1, chance.durations.p1.binding);
925
1025
 
926
1026
  chance.reset();
927
1027
  }
@@ -933,7 +1033,7 @@ test "Chance.psywave" {
933
1033
 
934
1034
  try chance.psywave(.P2, 100, 150);
935
1035
  try expectProbability(&chance.probability, 1, 150);
936
- try expectValue(101, chance.actions.p2.psywave);
1036
+ try expectValue(@as(u8, 101), chance.actions.p2.psywave);
937
1037
  }
938
1038
 
939
1039
  test "Chance.metronome" {
@@ -971,16 +1071,16 @@ const Null = struct {
971
1071
  _ = .{ self, player, kind };
972
1072
  }
973
1073
 
974
- pub fn clearPending(self: Null) void {
1074
+ pub fn glitch(self: Null) void {
975
1075
  _ = .{self};
976
1076
  }
977
1077
 
978
- pub fn clearDurations(self: Null, player: Player, haze: ?bool) void {
979
- _ = .{ self, player, haze };
1078
+ pub fn clearPending(self: Null) void {
1079
+ _ = .{self};
980
1080
  }
981
1081
 
982
- pub fn switched(self: Null, player: Player, in: u8, out: u8) void {
983
- _ = .{ self, player, in, out };
1082
+ pub fn switched(self: Null, player: Player, slot: u8) void {
1083
+ _ = .{ self, player, slot };
984
1084
  }
985
1085
 
986
1086
  pub fn speedTie(self: Null, p1: bool) Error!void {
@@ -1019,34 +1119,37 @@ const Null = struct {
1019
1119
  _ = .{ self, player, n };
1020
1120
  }
1021
1121
 
1022
- pub fn duration(
1122
+ pub fn duration(self: Null, player: Player, turns: u4) void {
1123
+ _ = .{ self, player, turns };
1124
+ }
1125
+
1126
+ pub fn observe(
1023
1127
  self: Null,
1024
- comptime field: Duration.Field,
1128
+ comptime field: Action.Field,
1025
1129
  player: Player,
1026
- target: Player,
1027
- turns: u4,
1130
+ obs: Optional(Confusion),
1028
1131
  ) void {
1029
- _ = .{ self, field, player, target, turns };
1132
+ _ = .{ self, field, player, obs };
1030
1133
  }
1031
1134
 
1032
- pub fn sleep(self: Null, player: Player, turns: u4) Error!void {
1033
- _ = .{ self, player, turns };
1135
+ pub fn sleep(self: Null, player: Player, obs: Optional(Observation)) Error!void {
1136
+ _ = .{ self, player, obs };
1034
1137
  }
1035
1138
 
1036
- pub fn disable(self: Null, player: Player, turns: u4) Error!void {
1037
- _ = .{ self, player, turns };
1139
+ pub fn confusion(self: Null, player: Player, obs: Optional(Confusion)) Error!void {
1140
+ _ = .{ self, player, obs };
1038
1141
  }
1039
1142
 
1040
- pub fn confusion(self: Null, player: Player, turns: u4) Error!void {
1041
- _ = .{ self, player, turns };
1143
+ pub fn disable(self: Null, player: Player, obs: Optional(Observation)) Error!void {
1144
+ _ = .{ self, player, obs };
1042
1145
  }
1043
1146
 
1044
- pub fn attacking(self: Null, player: Player, turns: u4) Error!void {
1045
- _ = .{ self, player, turns };
1147
+ pub fn attacking(self: Null, player: Player, obs: Optional(Observation)) Error!void {
1148
+ _ = .{ self, player, obs };
1046
1149
  }
1047
1150
 
1048
- pub fn binding(self: Null, player: Player, turns: u4) Error!void {
1049
- _ = .{ self, player, turns };
1151
+ pub fn binding(self: Null, player: Player, obs: Optional(Observation)) Error!void {
1152
+ _ = .{ self, player, obs };
1050
1153
  }
1051
1154
 
1052
1155
  pub fn psywave(self: Null, player: Player, power: u8, max: u8) Error!void {
@@ -1056,4 +1159,9 @@ const Null = struct {
1056
1159
  pub fn metronome(self: Null, player: Player, move: Move) Error!void {
1057
1160
  _ = .{ self, player, move };
1058
1161
  }
1162
+
1163
+ pub fn pp(self: Null, player: Player, n: u4) bool {
1164
+ _ = .{ self, player, n };
1165
+ return true;
1166
+ }
1059
1167
  };