@pkmn/engine 0.1.0-dev.36b5d56b → 0.1.0-dev.3846c8e6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +28 -13
  3. package/build/pkg/addon/browser.js +4 -1
  4. package/build/pkg/addon/browser.js.map +1 -1
  5. package/build/pkg/addon/node.js +20 -7
  6. package/build/pkg/addon/node.js.map +1 -1
  7. package/build/pkg/addon/wasm.js +41 -9
  8. package/build/pkg/addon/wasm.js.map +1 -1
  9. package/build/pkg/addon.d.ts +7 -7
  10. package/build/pkg/addon.js +8 -17
  11. package/build/pkg/addon.js.map +1 -1
  12. package/build/pkg/data/layout.json +15 -2
  13. package/build/pkg/data/protocol.json +23 -9
  14. package/build/pkg/gen1.d.ts +15 -18
  15. package/build/pkg/gen1.js +28 -19
  16. package/build/pkg/gen1.js.map +1 -1
  17. package/build/pkg/index.d.ts +37 -100
  18. package/build/pkg/index.js +23 -75
  19. package/build/pkg/index.js.map +1 -1
  20. package/build/pkg/protocol.d.ts +11 -2
  21. package/build/pkg/protocol.js.map +1 -1
  22. package/build/tools/debug.js +54 -11
  23. package/build/tools/debug.js.map +1 -1
  24. package/build/tools/display/debug.jsx +19 -94
  25. package/build/tools/display/debug.jsx.map +1 -1
  26. package/build/tools/display/index.js +18 -7
  27. package/build/tools/display/index.js.map +1 -1
  28. package/build/tools/display/ui.jsx +20 -9
  29. package/build/tools/display/ui.jsx.map +1 -1
  30. package/build/tools/display/util.js +140 -1
  31. package/build/tools/display/util.js.map +1 -1
  32. package/build.zig +200 -201
  33. package/package.json +31 -130
  34. package/src/bin/install-pkmn-engine +25 -9
  35. package/src/lib/bindings/c.zig +200 -0
  36. package/src/lib/bindings/node.zig +117 -0
  37. package/src/lib/bindings/wasm.zig +50 -0
  38. package/src/lib/common/array.zig +5 -12
  39. package/src/lib/common/debug.zig +33 -59
  40. package/src/lib/common/js.zig +3 -9
  41. package/src/lib/common/optional.zig +11 -21
  42. package/src/lib/common/protocol.zig +224 -189
  43. package/src/lib/common/rational.zig +275 -284
  44. package/src/lib/common/util.zig +14 -31
  45. package/src/lib/gen1/calc.zig +139 -117
  46. package/src/lib/gen1/chance.zig +112 -75
  47. package/src/lib/gen1/data/types.zig +2 -2
  48. package/src/lib/gen1/data.zig +5 -5
  49. package/src/lib/gen1/helpers.zig +8 -10
  50. package/src/lib/gen1/mechanics.zig +50 -43
  51. package/src/lib/gen2/calc.zig +3 -6
  52. package/src/lib/gen2/chance.zig +12 -18
  53. package/src/lib/gen2/data/types.zig +6 -3
  54. package/src/lib/gen2/data.zig +8 -7
  55. package/src/lib/gen2/helpers.zig +4 -6
  56. package/src/lib/gen2/mechanics.zig +36 -25
  57. package/src/lib/napi.h +2 -0
  58. package/src/lib/node.zig +3 -118
  59. package/src/lib/pkmn.zig +94 -19
  60. package/src/lib/wasm.zig +10 -9
  61. package/src/tools/display/dom.ts +46 -34
@@ -1,5 +1,6 @@
1
1
  const DEBUG = @import("../common/debug.zig").print;
2
2
 
3
+ const builtin = @import("builtin");
3
4
  const chance = @import("chance.zig");
4
5
  const common = @import("../common/data.zig");
5
6
  const data = @import("data.zig");
@@ -83,11 +84,11 @@ fn start(battle: anytype, options: anytype) !Result {
83
84
 
84
85
  const p1_slot = findFirstAlive(p1);
85
86
  assert(!showdown or p1_slot == 1);
86
- if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
87
+ if (p1_slot == 0) return if (findFirstAlive(p2) == 0) .Tie else .Lose;
87
88
 
88
89
  const p2_slot = findFirstAlive(p2);
89
90
  assert(!showdown or p2_slot == 1);
90
- if (p2_slot == 0) return Result.Win;
91
+ if (p2_slot == 0) return .Win;
91
92
 
92
93
  try switchIn(battle, .P1, p1_slot, true, options);
93
94
  try switchIn(battle, .P2, p2_slot, true, options);
@@ -166,7 +167,7 @@ fn selectMove(
166
167
  const last = side.active.move(battle.lastMove(player).index);
167
168
  if (last.id == .Metronome) side.last_selected_move = last.id;
168
169
  if (last.id == .MirrorMove) {
169
- if (!pkmn.options.mod) return Result.Error;
170
+ if (!pkmn.options.mod) return .Error;
170
171
  side.last_selected_move = last.id;
171
172
  }
172
173
  }
@@ -431,7 +432,7 @@ fn executeMove(
431
432
  // technically be from this Pokémon because it must be exactly 1 to not desync and
432
433
  // every Pokémon must have at least one move
433
434
  if (mslot != 1 or side.active.move(mslot).id != side.last_selected_move) {
434
- return Result.Error;
435
+ return .Error;
435
436
  } else {
436
437
  auto = true;
437
438
  }
@@ -449,14 +450,14 @@ fn executeMove(
449
450
  .skip_can => skip_can = true,
450
451
  .skip_pp => skip_pp = true,
451
452
  .ok => {},
452
- .err => return @as(?Result, Result.Error),
453
+ .err => return .Error,
453
454
  }
454
455
 
455
456
  // Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
456
457
  // selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
457
458
  // from the Hyper Beam slot if present or skips PP deduction entirely...
458
459
  if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
459
- side.last_selected_move != side.active.move(battle.lastMove(player).index).id)
460
+ side.last_selected_move != side.active.move(mslot).id)
460
461
  {
461
462
  assert(mslot == 1);
462
463
  const has_beam = has_beam: {
@@ -617,17 +618,10 @@ fn beforeMove(
617
618
  return .err;
618
619
  }
619
620
  }
620
- // Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
621
- // on the targeting behavior of the confused Pokémon's selected move which results
622
- // in the wrong behavior with respect to the Substitute + Confusion glitch
623
- const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
624
- player
625
- else
626
- player.foe();
627
621
 
628
622
  const uncapped = battle.last_damage;
629
623
  // Skipping adjustDamage / randomizeDamage / checkHit
630
- _ = try applyDamage(battle, player, target, .Confusion, options);
624
+ _ = try applyDamage(battle, player, player.foe(), .Confusion, options);
631
625
  // Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
632
626
  if (showdown) battle.last_damage = uncapped;
633
627
 
@@ -884,7 +878,7 @@ fn doMove(
884
878
  var miss = showdown and miss: {
885
879
  immune = move.target != .Self and !status and !counter and
886
880
  (@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
887
- @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
881
+ @intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
888
882
  if (immune and move.effect != .Binding) break :miss true;
889
883
  if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
890
884
  battle.last_damage = 0;
@@ -922,7 +916,7 @@ fn doMove(
922
916
  if (showdown) break :damage; // skip adjustDamage / randomizeDamage
923
917
  } else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
924
918
  if (!showdown) try options.chance.commit(player, .err);
925
- return @as(?Result, Result.Error);
919
+ return .Error;
926
920
  }
927
921
  if (battle.last_damage == 0) {
928
922
  immune = true;
@@ -1025,7 +1019,7 @@ fn doMove(
1025
1019
  const sub = showdown and foe.active.volatiles.Substitute;
1026
1020
 
1027
1021
  var nullified = false;
1028
- var hit: u4 = 0;
1022
+ var hit: usize = 0;
1029
1023
  while (hit < hits) {
1030
1024
  if (hit == 0) {
1031
1025
  if (crit) try log.crit(.{battle.active(player.foe())});
@@ -1053,7 +1047,7 @@ fn doMove(
1053
1047
  if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
1054
1048
  try Effects.poison(battle, player, Move.get(.PoisonSting), options);
1055
1049
  }
1056
- try log.hitcount(.{ battle.active(player.foe()), hit });
1050
+ try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
1057
1051
  } else if (showdown and move.effect == .Binding) {
1058
1052
  // This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
1059
1053
  try Effects.binding(battle, player, rewrap, options);
@@ -1169,7 +1163,7 @@ fn calcDamage(
1169
1163
  // encountered so we need to make sure that in the event of a miss we commit the status of the
1170
1164
  // crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
1171
1165
  // that results in counter intuitive probabilities in the common case
1172
- if (pkmn.options.chance and !pkmn.options.mod) {
1166
+ if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
1173
1167
  atk = attack(side, !crit, special);
1174
1168
  def = defense(target, !crit, special, false);
1175
1169
  if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
@@ -1254,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1254
1248
  .Psywave => power: {
1255
1249
  const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
1256
1250
  // GLITCH: Psywave infinite glitch loop
1257
- if (!pkmn.options.mod and max <= 1) return Result.Error;
1251
+ if (!pkmn.options.mod and max <= 1) return .Error;
1258
1252
  break :power try Rolls.psywave(battle, player, max, options);
1259
1253
  },
1260
1254
  else => unreachable,
1261
1255
  };
1262
1256
 
1263
- if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
1257
+ if (battle.last_damage == 0) return if (pkmn.options.mod) null else .Error;
1264
1258
 
1265
1259
  const sub = showdown and foe.active.volatiles.Substitute;
1266
1260
  _ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
@@ -1280,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1280
1274
  // Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
1281
1275
  const foe_last_selected_move =
1282
1276
  Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
1283
- .Splash
1284
- else
1285
- foe.last_selected_move);
1277
+ .Splash
1278
+ else
1279
+ foe.last_selected_move);
1286
1280
 
1287
1281
  const used = battle.lastMove(player.foe()).counterable != 0;
1288
1282
  const selected = foe_last_selected_move.bp > 0 and
1289
1283
  foe.last_selected_move != .Counter and
1290
1284
  (foe_last_selected_move.type == .Normal or
1291
- foe_last_selected_move.type == .Fighting);
1285
+ foe_last_selected_move.type == .Fighting);
1292
1286
 
1293
1287
  if (!used and !selected) {
1294
1288
  try options.log.fail(.{ battle.active(player), .None });
@@ -1297,12 +1291,16 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
1297
1291
 
1298
1292
  if (!used or !selected) {
1299
1293
  // GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
1300
- if (!pkmn.options.mod) return Result.Error;
1294
+ if (!pkmn.options.mod) return .Error;
1301
1295
  try options.log.fail(.{ battle.active(player), .None });
1302
1296
  return null;
1303
1297
  }
1304
1298
 
1305
- battle.last_damage *|= 2;
1299
+ // NOTE: llvm/llvm-project#58557
1300
+ battle.last_damage = if (comptime builtin.target.cpu.arch.isWasm())
1301
+ @max(battle.last_damage *% 2, std.math.maxInt(u16))
1302
+ else
1303
+ battle.last_damage *| 2;
1306
1304
 
1307
1305
  // Pokémon Showdown calls moveHit before Counter
1308
1306
  if (!showdown and !try checkHit(battle, player, move, options)) return null;
@@ -1448,7 +1446,16 @@ fn moveHit(
1448
1446
  break :miss true;
1449
1447
  }
1450
1448
  if (!showdown) {
1451
- if (move.effect == .Swift) return true;
1449
+ if (move.effect == .Swift) {
1450
+ // If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
1451
+ // Showdown, hence why the block above doesn't include this code), we need the
1452
+ // damage roll to be committed. However, the commit code will not save this roll if
1453
+ // the glitch occurs and the move was not marked as having hit. To work around this
1454
+ // we flag that the move hit (as Swift trivially does), but pass the probability of
1455
+ // the hit as zero to ensure it doesn't actually get saved.
1456
+ try options.chance.hit(player, true, 0);
1457
+ return true;
1458
+ }
1452
1459
  if (foe.active.volatiles.Invulnerable) break :miss true;
1453
1460
  }
1454
1461
 
@@ -1521,13 +1528,13 @@ fn checkFaint(
1521
1528
 
1522
1529
  if (player_out and foe_out) {
1523
1530
  try options.log.tie(.{});
1524
- return Result.Tie;
1531
+ return .Tie;
1525
1532
  } else if (player_out) {
1526
1533
  try options.log.win(.{player.foe()});
1527
- return if (player == .P1) Result.Lose else Result.Win;
1534
+ return if (player == .P1) .Lose else .Win;
1528
1535
  } else if (foe_out) {
1529
1536
  try options.log.win(.{player});
1530
- return if (player == .P1) Result.Win else Result.Lose;
1537
+ return if (player == .P1) .Win else .Lose;
1531
1538
  }
1532
1539
 
1533
1540
  const foe_choice: Choice.Type = if (foe_fainted) .Switch else .Pass;
@@ -1545,7 +1552,7 @@ fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytyp
1545
1552
  if (foe_volatiles.Bide) {
1546
1553
  assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
1547
1554
  foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
1548
- if (foe_volatiles.state != 0) return Result.Error;
1555
+ if (foe_volatiles.state != 0) return .Error;
1549
1556
  }
1550
1557
 
1551
1558
  // Clearing these is not strictly necessary as provided the battle hasn't ended the side that
@@ -1637,19 +1644,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
1637
1644
 
1638
1645
  if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
1639
1646
  try options.log.tie(.{});
1640
- return Result.Tie;
1647
+ return .Tie;
1641
1648
  }
1642
1649
 
1643
1650
  if (pkmn.options.mod and battle.turn >= 1000) {
1644
1651
  try options.log.tie(.{});
1645
- return Result.Tie;
1652
+ return .Tie;
1646
1653
  } else if (battle.turn >= 65535) {
1647
- return Result.Error;
1654
+ return .Error;
1648
1655
  }
1649
1656
 
1650
1657
  try options.log.turn(.{battle.turn});
1651
1658
 
1652
- return Result.Default;
1659
+ return .Default;
1653
1660
  }
1654
1661
 
1655
1662
  fn checkEBC(battle: anytype) bool {
@@ -1940,7 +1947,7 @@ pub const Effects = struct {
1940
1947
  return null;
1941
1948
  } else if (err) {
1942
1949
  // GLITCH: Transform + Mirror Move / Metronome PP softlock
1943
- return Result.Error;
1950
+ return .Error;
1944
1951
  }
1945
1952
 
1946
1953
  volatiles.disable_move =
@@ -3138,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3138
3145
  },
3139
3146
  .Switch => {
3140
3147
  const side = battle.side(player);
3141
- var slot: u4 = 2;
3148
+ var slot: usize = 2;
3142
3149
  while (slot <= 6) : (slot += 1) {
3143
3150
  const id = side.order[slot - 1];
3144
3151
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3145
- out[n] = .{ .type = .Switch, .data = slot };
3152
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3146
3153
  n += 1;
3147
3154
  }
3148
3155
  if (n == 0) {
@@ -3173,11 +3180,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3173
3180
  return n;
3174
3181
  }
3175
3182
 
3176
- var slot: u4 = 2;
3183
+ var slot: usize = 2;
3177
3184
  while (slot <= 6) : (slot += 1) {
3178
3185
  const id = side.order[slot - 1];
3179
3186
  if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
3180
- out[n] = .{ .type = .Switch, .data = slot };
3187
+ out[n] = .{ .type = .Switch, .data = @intCast(slot) };
3181
3188
  n += 1;
3182
3189
  }
3183
3190
 
@@ -3210,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3210
3217
  const struggle =
3211
3218
  m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
3212
3219
  const s = if (struggle) 0 else slot;
3213
- out[n] = .{ .type = .Move, .data = s };
3220
+ out[n] = .{ .type = .Move, .data = @intCast(s) };
3214
3221
  n += 1;
3215
3222
  return n;
3216
3223
  }
@@ -3224,7 +3231,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
3224
3231
  if (m.id == .None) break;
3225
3232
  if (m.pp == 0) continue;
3226
3233
  if (active.volatiles.disable_move == slot) continue;
3227
- out[n] = .{ .type = .Move, .data = slot };
3234
+ out[n] = .{ .type = .Move, .data = @intCast(slot) };
3228
3235
  n += 1;
3229
3236
  }
3230
3237
  // Struggle (Pokémon Showdown would use 'move 1' here)
@@ -15,9 +15,6 @@ const isPointerTo = util.isPointerTo;
15
15
  const Player = common.Player;
16
16
  const PointerType = util.PointerType;
17
17
 
18
- const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
19
- const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
20
-
21
18
  /// Information relevant to damage calculation that occured during a Generation II battle `update`.
22
19
  pub const Summaries = extern struct {
23
20
  /// Relevant information for Player 1.
@@ -91,8 +88,8 @@ pub const Calc = struct {
91
88
  };
92
89
 
93
90
  return if (switch (@typeInfo(@TypeOf(val))) {
94
- Enum => val != .None,
95
- Int => val != 0,
91
+ .@"enum" => val != .None,
92
+ .int => val != 0,
96
93
  else => unreachable,
97
94
  }) val else null;
98
95
  }
@@ -148,6 +145,6 @@ fn ReturnType(comptime field: Action.Field) type {
148
145
  return ?(switch (field) {
149
146
  .damages => u8,
150
147
  .critical_hits => Optional(bool),
151
- else => std.meta.FieldType(Action, field),
148
+ else => @FieldType(Action, @tagName(field)),
152
149
  });
153
150
  }
@@ -27,10 +27,6 @@ const Type = data.Type;
27
27
  const Effectiveness = data.Effectiveness;
28
28
  const TriAttack = data.TriAttack;
29
29
 
30
- const Int = if (@hasField(std.builtin.Type, "int")) .int else .Int;
31
- const Enum = if (@hasField(std.builtin.Type, "enum")) .@"enum" else .Enum;
32
- const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
33
-
34
30
  /// Actions taken by a hypothetical "chance player" that convey information about which RNG events
35
31
  /// were observed during a Generation II battle `update`. This can additionally be provided as input
36
32
  /// to the `update` call to override the normal behavior of the RNG in order to force specific
@@ -57,15 +53,15 @@ pub const Actions = extern struct {
57
53
  /// same shape if they have the same fields set (though those fields need not necessarily be
58
54
  /// set to the same value).
59
55
  pub fn matches(a: Actions, b: Actions) bool {
60
- inline for (@field(@typeInfo(Actions), @tagName(Struct)).fields) |player| {
61
- inline for (@field(@typeInfo(Action), @tagName(Struct)).fields) |field| {
56
+ inline for (@typeInfo(Actions).@"struct".fields) |player| {
57
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
62
58
  const a_val = @field(@field(a, player.name), field.name);
63
59
  const b_val = @field(@field(b, player.name), field.name);
64
60
 
65
61
  switch (@typeInfo(@TypeOf(a_val))) {
66
- Enum => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
62
+ .@"enum" => if ((@intFromEnum(a_val) > 0) != (@intFromEnum(b_val) > 0))
67
63
  return false,
68
- Int => if ((a_val > 0) != (b_val > 0)) return false,
64
+ .int => if ((a_val > 0) != (b_val > 0)) return false,
69
65
  else => unreachable,
70
66
  }
71
67
  }
@@ -73,7 +69,7 @@ pub const Actions = extern struct {
73
69
  return true;
74
70
  }
75
71
 
76
- pub fn fmt(self: Actions, writer: anytype, shape: bool) !void {
72
+ pub fn fmt(self: Actions, writer: *std.Io.Writer, shape: bool) !void {
77
73
  try writer.writeAll("<P1 = ");
78
74
  try self.p1.fmt(writer, shape);
79
75
  try writer.writeAll(", P2 = ");
@@ -81,8 +77,7 @@ pub const Actions = extern struct {
81
77
  try writer.writeAll(">");
82
78
  }
83
79
 
84
- pub fn format(a: Actions, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
85
- _ = .{ f, o };
80
+ pub fn format(a: Actions, w: *std.Io.Writer) !void {
86
81
  try fmt(a, w, false);
87
82
  }
88
83
  };
@@ -186,18 +181,17 @@ pub const Action = packed struct(u128) {
186
181
 
187
182
  pub const Field = std.meta.FieldEnum(Action);
188
183
 
189
- pub fn format(a: Action, comptime f: []const u8, o: std.fmt.FormatOptions, w: anytype) !void {
190
- _ = .{ f, o };
184
+ pub fn format(a: Action, w: *std.Io.Writer) !void {
191
185
  try fmt(a, w, false);
192
186
  }
193
187
 
194
- pub fn fmt(self: Action, writer: anytype, shape: bool) !void {
188
+ pub fn fmt(self: Action, writer: *std.Io.Writer, shape: bool) !void {
195
189
  try writer.writeByte('(');
196
190
  var printed = false;
197
- inline for (@field(@typeInfo(Action), @tagName(Struct)).fields) |field| {
191
+ inline for (@typeInfo(Action).@"struct".fields) |field| {
198
192
  const val = @field(self, field.name);
199
193
  switch (@typeInfo(@TypeOf(val))) {
200
- Enum => if (val != .None) {
194
+ .@"enum" => if (val != .None) {
201
195
  if (printed) try writer.writeAll(", ");
202
196
  if (shape) {
203
197
  try writer.print("{s}:?", .{field.name});
@@ -211,7 +205,7 @@ pub const Action = packed struct(u128) {
211
205
  }
212
206
  printed = true;
213
207
  },
214
- Int => if (val != 0) {
208
+ .int => if (val != 0) {
215
209
  if (printed) try writer.writeAll(", ");
216
210
  if (shape) {
217
211
  try writer.print("{s}:?", .{field.name});
@@ -786,7 +780,7 @@ pub fn expectProbability(r: anytype, p: u64, q: u64) !void {
786
780
 
787
781
  r.reduce();
788
782
  if (r.p != p or r.q != q) {
789
- print("expected {d}/{d}, found {}\n", .{ p, q, r });
783
+ print("expected {d}/{d}, found {f}\n", .{ p, q, r });
790
784
  return error.TestExpectedEqual;
791
785
  }
792
786
  }
@@ -172,9 +172,12 @@ pub const Type = enum(u8) {
172
172
  return CHART[@intFromEnum(t1)][@intFromEnum(t2)];
173
173
  }
174
174
 
175
- /// The precedence order of Type `type`.
176
- pub fn precedence(self: Type) u8 {
177
- return PRECEDENCE[@intFromEnum(self)];
175
+ /// The precedence order of Type `t1` vs. Type `t2`.
176
+ pub fn precedence(t1: Type, t2: Type) u8 {
177
+ // The Ice vs. Fire matchup is out of order - return a higher
178
+ // number than anything else so that it sorts last correctly
179
+ if (t1 == .Ice and t2 == .Fire) return 18;
180
+ return PRECEDENCE[@intFromEnum(t2)];
178
181
  }
179
182
 
180
183
  /// The internal index of this Type used by Present.
@@ -57,7 +57,7 @@ pub fn Battle(comptime RNG: anytype) type {
57
57
  turn: u16 = 0,
58
58
  field: Field = .{},
59
59
 
60
- _: [padding]u8 = .{0} ** padding,
60
+ _: [padding]u8 = @splat(0),
61
61
 
62
62
  pub fn side(self: anytype, player: Player) PointerType(@TypeOf(self), Side) {
63
63
  assert(isPointerTo(self, Self));
@@ -100,13 +100,13 @@ pub const Weather = enum(u4) {
100
100
  };
101
101
 
102
102
  pub const Side = extern struct {
103
- pokemon: [6]Pokemon = [_]Pokemon{.{}} ** 6,
103
+ pokemon: [6]Pokemon = @splat(.{}),
104
104
  active: ActivePokemon align(8) = .{},
105
105
  conditions: Conditions = .{},
106
106
  // NB: wLastPlayerCounterMove / wLastEnemyCounterMove (wLastPlayerMove / wLastEnemyMove)
107
107
  last_used_move: Move = .None,
108
108
 
109
- _: [7]u8 = .{0} ** 7,
109
+ _: [7]u8 = @splat(0),
110
110
 
111
111
  const Conditions = packed struct(u32) {
112
112
  Spikes: bool = false,
@@ -161,7 +161,7 @@ pub const Side = extern struct {
161
161
  pub const ActivePokemon = extern struct {
162
162
  volatiles: Volatiles align(4) = .{},
163
163
  stats: Stats(u16) = .{},
164
- moves: [4]MoveSlot = [_]MoveSlot{.{}} ** 4,
164
+ moves: [4]MoveSlot = @splat(.{}),
165
165
  boosts: Boosts = .{},
166
166
  types: Types = .{},
167
167
  species: Species = .None,
@@ -182,7 +182,7 @@ pub const ActivePokemon = extern struct {
182
182
 
183
183
  pub const Pokemon = extern struct {
184
184
  stats: Stats(u16) = .{},
185
- moves: [4]MoveSlot = [_]MoveSlot{.{}} ** 4,
185
+ moves: [4]MoveSlot = @splat(.{}),
186
186
  types: Types = .{},
187
187
  dvs: DVs = .{},
188
188
  hp: u16 = 0,
@@ -449,8 +449,9 @@ test Types {
449
449
  try expect(!Type.Steel.special());
450
450
  try expect(Type.Dark.special());
451
451
 
452
- try expectEqual(@as(u8, 3), Type.Water.precedence());
453
- try expect(Type.Bug.precedence() > Type.Poison.precedence());
452
+ try expectEqual(@as(u8, 3), Type.precedence(.Water, .Water));
453
+ try expect(Type.precedence(.Poison, .Bug) > Type.precedence(.Poison, .Poison));
454
+ try expect(Type.precedence(.Ice, .Fire) > Type.precedence(.Ice, .Water));
454
455
 
455
456
  try expectEqual(Effectiveness.Super, Type.effectiveness(.Ghost, .Psychic));
456
457
  try expectEqual(Effectiveness.Super, Type.effectiveness(.Water, .Fire));
@@ -16,8 +16,6 @@ const showdown = options.showdown;
16
16
  const Species = data.Species;
17
17
  const Stats = data.Stats;
18
18
 
19
- const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
20
-
21
19
  pub const Options = struct {
22
20
  cleric: bool = showdown,
23
21
  };
@@ -74,7 +72,7 @@ pub fn prng(rand: *PSRNG) data.PRNG {
74
72
  var carry: u8 = 0;
75
73
  var div: u8 = 0;
76
74
 
77
- var i: u8 = 0;
75
+ var i: usize = 0;
78
76
  while (i < seed.len) {
79
77
  div = @intCast(timer >> 8);
80
78
  // This is effectively @addWithOverflow but with three operands
@@ -142,7 +140,7 @@ pub const Pokemon = struct {
142
140
  var pokemon = data.Pokemon{};
143
141
  pokemon.species = p.species;
144
142
  const species = Species.get(p.species);
145
- inline for (@field(@typeInfo(@TypeOf(pokemon.stats)), @tagName(Struct)).fields) |field| {
143
+ inline for (@typeInfo(@TypeOf(pokemon.stats)).@"struct".fields) |field| {
146
144
  const hp = comptime std.mem.eql(u8, field.name, "hp");
147
145
  const spc =
148
146
  comptime std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
@@ -183,7 +181,7 @@ pub const Pokemon = struct {
183
181
 
184
182
  var stats: Stats(u16) = .{};
185
183
  const dvs = gen1.DVs.random(rand);
186
- inline for (@field(@typeInfo(@TypeOf(stats)), @tagName(Struct)).fields) |field| {
184
+ inline for (@typeInfo(@TypeOf(stats)).@"struct".fields) |field| {
187
185
  const hp = std.mem.eql(u8, field.name, "hp");
188
186
  const spc = std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
189
187
  @field(stats, field.name) = Stats(u16).calc(
@@ -195,7 +193,7 @@ pub const Pokemon = struct {
195
193
  );
196
194
  }
197
195
 
198
- var ms = [_]MoveSlot{.{}} ** 4;
196
+ var ms: [4]MoveSlot = @splat(.{});
199
197
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 3 + 1) else 4;
200
198
  for (0..n) |i| {
201
199
  var m: Move = .None;