@pkmn/engine 0.1.0-dev.3a273268 → 0.1.0-dev.47feedd9

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.
@@ -62,26 +62,28 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
62
62
  assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
63
63
  if (battle.turn == 0) return start(battle, log);
64
64
 
65
+ var s1 = false;
66
+ var s2 = false;
65
67
  var f1: ?Move = null;
66
68
  var f2: ?Move = null;
67
69
 
68
- if (selectMove(battle, .P1, c1, c2, &f1)) |r| return r;
69
- if (selectMove(battle, .P2, c2, c1, &f2)) |r| return r;
70
+ if (selectMove(battle, .P1, c1, c2, &s1, &f1)) |r| return r;
71
+ if (selectMove(battle, .P2, c2, c1, &s2, &f2)) |r| return r;
72
+
73
+ var p1 = battle.side(.P1);
74
+ var p2 = battle.side(.P2);
75
+
76
+ var r1 = showdown and p1.active.volatiles.Binding and c2.type == .Switch;
77
+ var r2 = showdown and p2.active.volatiles.Binding and c1.type == .Switch;
70
78
 
71
79
  if (turnOrder(battle, c1, c2) == .P1) {
72
- if (try doTurn(battle, .P1, c1, f1, .P2, c2, f2, log)) |r| return r;
80
+ if (try doTurn(battle, .P1, c1, r1, s1, f1, .P2, c2, r2, s2, f2, log)) |r| return r;
73
81
  } else {
74
- if (try doTurn(battle, .P2, c2, f2, .P1, c1, f1, log)) |r| return r;
82
+ if (try doTurn(battle, .P2, c2, r2, s2, f2, .P1, c1, r1, s1, f1, log)) |r| return r;
75
83
  }
76
84
 
77
- var p1 = battle.side(.P1);
78
- if (p1.active.volatiles.attacks == 0) {
79
- p1.active.volatiles.Binding = false;
80
- }
81
- var p2 = battle.side(.P2);
82
- if (p2.active.volatiles.attacks == 0) {
83
- p2.active.volatiles.Binding = false;
84
- }
85
+ if (p1.active.volatiles.attacks == 0) p1.active.volatiles.Binding = false;
86
+ if (p2.active.volatiles.attacks == 0) p2.active.volatiles.Binding = false;
85
87
 
86
88
  return endTurn(battle, log);
87
89
  }
@@ -114,6 +116,7 @@ fn selectMove(
114
116
  player: Player,
115
117
  choice: Choice,
116
118
  foe_choice: Choice,
119
+ skip_turn: *bool,
117
120
  from: *?Move,
118
121
  ) ?Result {
119
122
  if (choice.type == .Pass) return null;
@@ -129,14 +132,20 @@ fn selectMove(
129
132
  if (volatiles.Recharging) return null;
130
133
  if (volatiles.Rage) {
131
134
  from.* = side.last_used_move;
132
- if (showdown) saveMove(battle, player, null);
135
+ if (showdown) {
136
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
137
+ saveMove(battle, player, null);
138
+ }
133
139
  return null;
134
140
  }
135
141
  // Pokémon Showdown removes Flinch at the end-of-turn in its residual handler
136
142
  if (!showdown) volatiles.Flinch = false;
137
143
  if (volatiles.Thrashing or volatiles.Charging) {
138
144
  from.* = side.last_selected_move;
139
- if (showdown) saveMove(battle, player, null);
145
+ if (showdown) {
146
+ if (battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
147
+ saveMove(battle, player, null);
148
+ }
140
149
  return null;
141
150
  }
142
151
 
@@ -146,7 +155,10 @@ fn selectMove(
146
155
  // pre-move select
147
156
  if (Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP) or volatiles.Bide) {
148
157
  assert(showdown or choice.data == 0);
149
- if (showdown) saveMove(battle, player, choice);
158
+ if (showdown) {
159
+ if (volatiles.Bide and battle.foe(player).active.volatiles.Binding) skip_turn.* = true;
160
+ saveMove(battle, player, choice);
161
+ }
150
162
  return null;
151
163
  }
152
164
  if (volatiles.Binding) {
@@ -177,6 +189,7 @@ fn selectMove(
177
189
  }
178
190
 
179
191
  if (battle.foe(player).active.volatiles.Binding) {
192
+ skip_turn.* = true;
180
193
  if (showdown) {
181
194
  saveMove(battle, player, choice);
182
195
  } else {
@@ -327,9 +340,13 @@ fn doTurn(
327
340
  battle: anytype,
328
341
  player: Player,
329
342
  player_choice: Choice,
343
+ player_rewrap: bool,
344
+ player_skip: bool,
330
345
  player_from: ?Move,
331
346
  foe_player: Player,
332
347
  foe_choice: Choice,
348
+ foe_rewrap: bool,
349
+ foe_skip: bool,
333
350
  foe_from: ?Move,
334
351
  log: anytype,
335
352
  ) !?Result {
@@ -337,7 +354,16 @@ fn doTurn(
337
354
 
338
355
  var residual = true;
339
356
  var replace = battle.side(player).stored().hp == 0;
340
- if (try executeMove(battle, player, player_choice, player_from, &residual, log)) |r| return r;
357
+ if (try executeMove(
358
+ battle,
359
+ player,
360
+ player_choice,
361
+ player_rewrap,
362
+ player_skip,
363
+ player_from,
364
+ &residual,
365
+ log,
366
+ )) |r| return r;
341
367
  if (!replace) {
342
368
  if (player_choice.type != .Switch) {
343
369
  if (try checkFaint(battle, foe_player, log)) |r| return r;
@@ -348,7 +374,16 @@ fn doTurn(
348
374
 
349
375
  residual = true;
350
376
  replace = battle.side(foe_player).stored().hp == 0;
351
- if (try executeMove(battle, foe_player, foe_choice, foe_from, &residual, log)) |r| return r;
377
+ if (try executeMove(
378
+ battle,
379
+ foe_player,
380
+ foe_choice,
381
+ foe_rewrap,
382
+ foe_skip,
383
+ foe_from,
384
+ &residual,
385
+ log,
386
+ )) |r| return r;
352
387
  if (!replace) {
353
388
  if (foe_choice.type != .Switch) {
354
389
  if (try checkFaint(battle, player, log)) |r| return r;
@@ -371,6 +406,8 @@ fn executeMove(
371
406
  battle: anytype,
372
407
  player: Player,
373
408
  choice: Choice,
409
+ rewrap: bool,
410
+ skip: bool,
374
411
  from: ?Move,
375
412
  residual: *bool,
376
413
  log: anytype,
@@ -382,9 +419,10 @@ fn executeMove(
382
419
  return null;
383
420
  }
384
421
 
422
+ // This is the correct place to check for SKIP_TURN and abort early, however since Pokémon
423
+ // Showdown overwrites the SKIP_TURN sentinel with its botched move select we need to add an
424
+ // additional skip boolean to accomplish the same thing in the Binding check of BeforeMove
385
425
  if (side.last_selected_move == .SKIP_TURN) {
386
- // Pokémon Showdown overwrites the SKIP_TURN sentinel with its botched move select,
387
- // Binding instead gets handled in beforeMove after sleep and freeze
388
426
  assert(!showdown);
389
427
  if (battle.foe(player).active.volatiles.Binding) {
390
428
  try log.cant(battle.active(player), .Bound);
@@ -434,7 +472,7 @@ fn executeMove(
434
472
 
435
473
  var skip_can = false;
436
474
  var skip_pp = false;
437
- switch (try beforeMove(battle, player, from, residual, log)) {
475
+ switch (try beforeMove(battle, player, skip, from, residual, log)) {
438
476
  .done => return null,
439
477
  .skip_can => skip_can = true,
440
478
  .skip_pp => skip_pp = true,
@@ -446,14 +484,15 @@ fn executeMove(
446
484
  return null;
447
485
  }
448
486
 
449
- return doMove(battle, player, mslot, auto, residual, log);
487
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
450
488
  }
451
489
 
452
- const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
490
+ const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
453
491
 
454
492
  fn beforeMove(
455
493
  battle: anytype,
456
494
  player: Player,
495
+ skip: bool,
457
496
  from: ?Move,
458
497
  residual: *bool,
459
498
  log: anytype,
@@ -485,13 +524,15 @@ fn beforeMove(
485
524
  return .done;
486
525
  }
487
526
 
488
- if (foe.active.volatiles.Binding) {
527
+ if (skip or foe.active.volatiles.Binding) {
489
528
  try log.cant(ident, .Bound);
490
529
  return .done;
491
530
  }
492
531
 
493
- if (!showdown and volatiles.Flinch) {
494
- volatiles.Flinch = false;
532
+ if (volatiles.Flinch) {
533
+ // Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
534
+ // Pokémon can sometimes flinch multiple times from the same original hit
535
+ if (!showdown) volatiles.Flinch = false;
495
536
  try log.cant(ident, .Flinch);
496
537
  return .done;
497
538
  }
@@ -512,13 +553,6 @@ fn beforeMove(
512
553
  // Pokémon Showdown's disable condition has a single onBeforeMove handler
513
554
  if (showdown and try disabled(side, ident, log)) return .done;
514
555
 
515
- // Pokémon Showdown checks for Flinch *after* instead of before
516
- if (showdown and volatiles.Flinch) {
517
- volatiles.Flinch = false;
518
- try log.cant(ident, .Flinch);
519
- return .done;
520
- }
521
-
522
556
  // This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
523
557
  // before the side had a selected a move - we simply need to assume this leads to a desync
524
558
  if (side.last_selected_move == .None) {
@@ -634,8 +668,13 @@ fn beforeMove(
634
668
  try log.start(battle.active(player), .ConfusionSilent);
635
669
  }
636
670
  try log.move(ident, side.last_selected_move, battle.active(player.foe()), from);
637
- if (showdown and handleThrashing(battle, active)) {
638
- try log.start(battle.active(player), .ConfusionSilent);
671
+ if (showdown) {
672
+ // This shouldn't actually set last_used_move, but Pokémon Showdown sets last
673
+ // used in useMove and doesn't have the notion of skipping canMove semantics
674
+ side.last_used_move = side.last_selected_move;
675
+ if (handleThrashing(battle, active)) {
676
+ try log.start(battle.active(player), .ConfusionSilent);
677
+ }
639
678
  }
640
679
  return .skip_can;
641
680
  }
@@ -650,7 +689,11 @@ fn beforeMove(
650
689
  side.last_selected_move,
651
690
  );
652
691
  if (showdown or battle.last_damage != 0) {
692
+ const sub = showdown and foe.active.volatiles.Substitute;
653
693
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
694
+ if (battle.foe(player).stored().hp > 0 and !sub) {
695
+ try buildRage(battle, player.foe(), log);
696
+ }
654
697
  }
655
698
  return .done;
656
699
  }
@@ -708,7 +751,7 @@ fn canMove(
708
751
  Effects.thrashing(battle, player);
709
752
  } else if (!showdown and move.effect == .Binding) {
710
753
  // Pokémon Showdown handles this after hit/miss checks and damage calculation
711
- Effects.binding(battle, player);
754
+ Effects.binding(battle, player, false);
712
755
  }
713
756
 
714
757
  return true;
@@ -749,6 +792,7 @@ fn doMove(
749
792
  battle: anytype,
750
793
  player: Player,
751
794
  mslot: u4,
795
+ rewrap: bool,
752
796
  auto: bool,
753
797
  residual: *bool,
754
798
  log: anytype,
@@ -840,9 +884,9 @@ fn doMove(
840
884
 
841
885
  if (!showdown or !miss) {
842
886
  if (move.effect == .MirrorMove) {
843
- return mirrorMove(battle, player, mslot, auto, residual, log);
887
+ return mirrorMove(battle, player, mslot, rewrap, auto, residual, log);
844
888
  } else if (move.effect == .Metronome) {
845
- return metronome(battle, player, mslot, auto, residual, log);
889
+ return metronome(battle, player, mslot, rewrap, auto, residual, log);
846
890
  } else if (move.effect.onEnd()) {
847
891
  try onEnd(battle, player, move, log);
848
892
  return null;
@@ -857,7 +901,7 @@ fn doMove(
857
901
  move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
858
902
  if (ohko) {
859
903
  try log.immune(foe_ident, .OHKO);
860
- } else if (immune and !invulnerable) {
904
+ } else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
861
905
  try log.immune(foe_ident, .None);
862
906
  } else if (mist) {
863
907
  if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
@@ -923,7 +967,7 @@ fn doMove(
923
967
  } else if (showdown) {
924
968
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
925
969
  if (move.effect == .Binding) {
926
- Effects.binding(battle, player);
970
+ Effects.binding(battle, player, rewrap);
927
971
  if (immune) {
928
972
  battle.last_damage = 0;
929
973
  // Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
@@ -1160,7 +1204,9 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, log: anytype)
1160
1204
  // Pokémon Showdown calls checkHit before Counter
1161
1205
  if (!showdown and !try checkHit(battle, player, move, log)) return null;
1162
1206
 
1207
+ const sub = showdown and foe.active.volatiles.Substitute;
1163
1208
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
1209
+ if (battle.foe(player).stored().hp > 0 and !sub) try buildRage(battle, player.foe(), log);
1164
1210
  return null;
1165
1211
  }
1166
1212
 
@@ -1204,6 +1250,7 @@ fn mirrorMove(
1204
1250
  battle: anytype,
1205
1251
  player: Player,
1206
1252
  mslot: u4,
1253
+ rewrap: bool,
1207
1254
  auto: bool,
1208
1255
  residual: *bool,
1209
1256
  log: anytype,
@@ -1216,18 +1263,19 @@ fn mirrorMove(
1216
1263
  if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
1217
1264
  try log.fail(battle.active(player), .None);
1218
1265
  return null;
1266
+ } else if (!showdown or foe.last_used_move != .Struggle) {
1267
+ incrementPP(side, mslot);
1219
1268
  }
1220
1269
 
1221
- incrementPP(side, mslot);
1222
-
1223
1270
  if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1224
- return doMove(battle, player, mslot, auto, residual, log);
1271
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1225
1272
  }
1226
1273
 
1227
1274
  fn metronome(
1228
1275
  battle: anytype,
1229
1276
  player: Player,
1230
1277
  mslot: u4,
1278
+ rewrap: bool,
1231
1279
  auto: bool,
1232
1280
  residual: *bool,
1233
1281
  log: anytype,
@@ -1238,7 +1286,7 @@ fn metronome(
1238
1286
  incrementPP(side, mslot);
1239
1287
 
1240
1288
  if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1241
- return doMove(battle, player, mslot, auto, residual, log);
1289
+ return doMove(battle, player, mslot, rewrap, auto, residual, log);
1242
1290
  }
1243
1291
 
1244
1292
  fn checkHit(battle: anytype, player: Player, move: Move.Data, log: anytype) !bool {
@@ -1445,11 +1493,7 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
1445
1493
  stored.hp -= amount;
1446
1494
 
1447
1495
  // As above, Pokémon Showdown uses damageOf but its not relevant
1448
- if (amount > 0) {
1449
- try log.damage(ident, stored, .LeechSeed);
1450
- // Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
1451
- if (showdown) battle.last_damage = damage;
1452
- }
1496
+ if (amount > 0) try log.damage(ident, stored, .LeechSeed);
1453
1497
 
1454
1498
  const before = foe_stored.hp;
1455
1499
  // Uncapped damage is added back to the foe
@@ -1662,7 +1706,7 @@ pub const Effects = struct {
1662
1706
  side.active.volatiles.Bide = true;
1663
1707
  assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
1664
1708
  side.active.volatiles.state = 0;
1665
- side.active.volatiles.attacks = Rolls.duration(battle);
1709
+ side.active.volatiles.attacks = Rolls.bideThrashDuration(battle);
1666
1710
 
1667
1711
  try log.start(battle.active(player), .Bide);
1668
1712
  }
@@ -1982,9 +2026,9 @@ pub const Effects = struct {
1982
2026
  }
1983
2027
  break :has_mimic false;
1984
2028
  };
1985
- // If the foe is Invulnerable we still want to fall through to checkHit to be able to
1986
- // trigger |-miss| instead of |-fail|
1987
- if (!has_mimic and !foe.active.volatiles.Invulnerable) {
2029
+ if (!has_mimic) {
2030
+ // Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
2031
+ if (!try checkHit(battle, player, move, log)) return;
1988
2032
  return try log.fail(battle.active(player.foe()), .None);
1989
2033
  }
1990
2034
  }
@@ -2167,7 +2211,7 @@ pub const Effects = struct {
2167
2211
  }
2168
2212
  foe.active.volatiles.Recharging = false;
2169
2213
 
2170
- foe_stored.status = Status.slp(Rolls.sleep(battle));
2214
+ foe_stored.status = Status.slp(Rolls.sleepDuration(battle));
2171
2215
  try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
2172
2216
  }
2173
2217
 
@@ -2220,7 +2264,7 @@ pub const Effects = struct {
2220
2264
  assert(!volatiles.Bide);
2221
2265
 
2222
2266
  volatiles.Thrashing = true;
2223
- volatiles.attacks = Rolls.duration(battle);
2267
+ volatiles.attacks = Rolls.bideThrashDuration(battle);
2224
2268
  }
2225
2269
 
2226
2270
  fn transform(battle: anytype, player: Player, log: anytype) !void {
@@ -2252,7 +2296,7 @@ pub const Effects = struct {
2252
2296
  try log.transform(battle.active(player), foe_ident);
2253
2297
  }
2254
2298
 
2255
- fn binding(battle: anytype, player: Player) void {
2299
+ fn binding(battle: anytype, player: Player, rewrap: bool) void {
2256
2300
  var side = battle.side(player);
2257
2301
  var foe = battle.foe(player);
2258
2302
 
@@ -2260,7 +2304,9 @@ pub const Effects = struct {
2260
2304
  side.active.volatiles.Binding = true;
2261
2305
  // GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
2262
2306
  // (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
2263
- if (!showdown) foe.active.volatiles.Recharging = false;
2307
+ if (!showdown) {
2308
+ foe.active.volatiles.Recharging = false;
2309
+ } else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
2264
2310
 
2265
2311
  side.active.volatiles.attacks = Rolls.distribution(battle) - 1;
2266
2312
  }
@@ -2589,7 +2635,7 @@ pub const Rolls = struct {
2589
2635
  return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
2590
2636
  }
2591
2637
 
2592
- inline fn sleep(battle: anytype) u3 {
2638
+ inline fn sleepDuration(battle: anytype) u3 {
2593
2639
  if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
2594
2640
  while (true) {
2595
2641
  const r = battle.rng.next() & 7;
@@ -2597,7 +2643,7 @@ pub const Rolls = struct {
2597
2643
  }
2598
2644
  }
2599
2645
 
2600
- inline fn duration(battle: anytype) u3 {
2646
+ inline fn bideThrashDuration(battle: anytype) u3 {
2601
2647
  if (showdown) return @intCast(u3, battle.rng.range(u4, 2, 4));
2602
2648
  return @intCast(u3, (battle.rng.next() & 1) + 2);
2603
2649
  }
package/src/lib/pkmn.zig CHANGED
@@ -25,12 +25,12 @@ pub const Result = @import("common/data.zig").Result;
25
25
  /// Pokémon Showdown's RNG (backed by a Generation V & VI RNG).
26
26
  pub const PSRNG = @import("common/rng.zig").PSRNG;
27
27
 
28
- /// Namespace for helpers related to protocol trace logging.
28
+ /// Namespace for helpers related to protocol message logging.
29
29
  pub const protocol = if (options.internal) struct {
30
30
  pub usingnamespace @import("common/protocol.zig");
31
31
  } else struct {
32
32
  /// Logs protocol information to its `Writer` during a battle update when
33
- /// `options.trace` is enabled.
33
+ /// `options.log` is enabled.
34
34
  pub const Log = @import("common/protocol.zig").Log;
35
35
  /// Stripped down version of `std.io.FixedBufferStream` optimized for
36
36
  /// efficiently writing the individual protocol bytes. Note that the
@@ -41,8 +41,8 @@ pub const protocol = if (options.internal) struct {
41
41
  /// intialized with a `LOGS_SIZE`-sized buffer.
42
42
  pub const FixedLog = @import("common/protocol.zig").FixedLog;
43
43
  /// Null object pattern implementation of `Log` backed by a
44
- /// `std.io.null_writer`. Ignores anything sent to it, though trace logging
45
- /// should addtionally be turned off entirely by using `options.trace`.
44
+ /// `std.io.null_writer`. Ignores anything sent to it, though protocol
45
+ /// logging should additionally be turned off entirely with `options.log`.
46
46
  pub const NULL = @import("common/protocol.zig").NULL;
47
47
  };
48
48