@pkmn/engine 0.1.0-dev.3a62fd35 → 0.1.0-dev.3de821b9

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