@pkmn/engine 0.1.0-dev.207fa86a → 0.1.0-dev.2ec67dba

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.
package/README.md CHANGED
@@ -22,7 +22,7 @@ implementation of both Pokémon battles as defined by the original game code, an
22
22
  Showdown](https://pokemonshowdown.com/)[^1] simulator which represents Pokémon battling as practically
23
23
  interpreted online.
24
24
 
25
- The pkmn engine is up to [**XXXX× faster**](docs/TESTING.md#results) than the
25
+ The pkmn engine is [**more than 1000× faster**](docs/TESTING.md#results) than the
26
26
  [patched](docs/TESTING.md#patches) [Pokémon Showdown simulator
27
27
  code](https://github.com/smogon/pokemon-showdown) when playing out supported formats in
28
28
  compatibility mode and is extensively [tested](docs/TESTING.md) and [documented](docs). Note,
@@ -74,7 +74,7 @@ compiler (see above regarding minimum version) and download one to
74
74
  `node_module/@pkmn/engine/build/bin` if it can't find one, as well as looking for (and downloading,
75
75
  if necessary) the required Node headers needed to successfully build the addons natively.
76
76
 
77
- **If you have configured NPM to `--ignore-scripts` you must either run `npx install-pkmn-engine`
77
+ **If you have configured `npm` to `--ignore-scripts` you must either run `npx install-pkmn-engine`
78
78
  directly or build the addons manually and place the artifacts in the expected paths.**
79
79
 
80
80
  ### `pkmn`
@@ -362,7 +362,7 @@ export declare class Choice {
362
362
  * Only numeric choice data is supported.
363
363
  */
364
364
  static parse(choice: string): Choice;
365
- /** Formats a `Choice` ino a Pokémon Showdown compatible choice string. */
365
+ /** Formats a `Choice` into a Pokémon Showdown compatible choice string. */
366
366
  static format(choice: Choice): string;
367
367
  /** Returns the "pass" `Choice`. */
368
368
  static pass(): Choice;
@@ -80,7 +80,7 @@ class Choice {
80
80
  const data = +(m[4] ?? m[7] ?? 0);
81
81
  return { type, data };
82
82
  }
83
- /** Formats a `Choice` ino a Pokémon Showdown compatible choice string. */
83
+ /** Formats a `Choice` into a Pokémon Showdown compatible choice string. */
84
84
  static format(choice) {
85
85
  return choice.type === 'pass' ? choice.type : `${choice.type} ${choice.data}`;
86
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pkg/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pkg/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pkmn/engine",
3
- "version": "0.1.0-dev.207fa86a",
3
+ "version": "0.1.0-dev.2ec67dba",
4
4
  "description": "A minimal, complete, Pokémon battle simulation engine optimized for performance",
5
5
  "repository": "github:pkmn/engine",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "src/lib/binding/node.zig",
14
14
  "src/lib/binding/wasm.zig",
15
15
  "src/lib/common/data.zig",
16
+ "src/lib/common/debug.zig",
16
17
  "src/lib/common/options.zig",
17
18
  "src/lib/common/protocol.zig",
18
19
  "src/lib/common/rng.zig",
@@ -56,14 +57,14 @@
56
57
  "access": "public"
57
58
  },
58
59
  "dependencies": {
59
- "@pkmn/data": "^0.7.33",
60
+ "@pkmn/data": "^0.7.34",
60
61
  "@pkmn/protocol": "^0.6.11"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@pkmn/eslint-config": "^3.0.0-alpha.0",
64
65
  "@pkmn/img": "^0.2.8",
65
66
  "@pkmn/sets": "^5.0.4",
66
- "@pkmn/sim": "0.7.33",
67
+ "@pkmn/sim": "0.7.34",
67
68
  "@types/html-minifier": "^4.0.2",
68
69
  "@types/jest": "^29.5.0",
69
70
  "@types/minimist": "^1.2.2",
@@ -188,4 +189,4 @@
188
189
  }
189
190
  ]
190
191
  }
191
- }
192
+ }
@@ -0,0 +1,64 @@
1
+ const std = @import("std");
2
+
3
+ const debug = std.debug;
4
+ const io = std.io;
5
+
6
+ pub fn print(value: anytype) void {
7
+ debug.getStderrMutex().lock();
8
+ defer debug.getStderrMutex().unlock();
9
+ const stderr = io.getStdErr().writer();
10
+
11
+ nosuspend {
12
+ stderr.writeAll("\x1b[41m") catch return;
13
+ switch (@typeInfo(@TypeOf(value))) {
14
+ .Struct => |info| {
15
+ if (info.is_tuple) {
16
+ inline for (info.fields, 0..) |f, i| {
17
+ inspect(@field(value, f.name));
18
+ if (i < info.fields.len - 1) stderr.writeAll(" ") catch return;
19
+ }
20
+ } else {
21
+ inspect(value);
22
+ }
23
+ },
24
+ else => inspect(value),
25
+ }
26
+ stderr.writeAll("\x1b[K\x1b[0m\n") catch return;
27
+ }
28
+ }
29
+
30
+ fn inspect(value: anytype) void {
31
+ const stderr = io.getStdErr().writer();
32
+
33
+ nosuspend {
34
+ const err = "Unable to format type '" ++ @typeName(@TypeOf(value)) ++ "'";
35
+ switch (@typeInfo(@TypeOf(value))) {
36
+ .Array => |info| {
37
+ if (info.child == u8) return stderr.print("{s}", .{value}) catch return;
38
+ @compileError(err);
39
+ },
40
+ .Pointer => |ptr_info| switch (ptr_info.size) {
41
+ .One => switch (@typeInfo(ptr_info.child)) {
42
+ .Array => |info| {
43
+ if (info.child == u8) return stderr.print("{s}", .{value}) catch return;
44
+ @compileError(err);
45
+ },
46
+ .Enum, .Union, .Struct => return inspect(value.*),
47
+ else => @compileError(err),
48
+ },
49
+ .Many, .C => {
50
+ if (ptr_info.sentinel) |_| return inspect(std.mem.span(value));
51
+ if (ptr_info.child == u8) {
52
+ return stderr.print("{s}", .{std.mem.span(value)}) catch return;
53
+ }
54
+ @compileError(err);
55
+ },
56
+ .Slice => {
57
+ if (ptr_info.child == u8) return stderr.print("{s}", .{value}) catch return;
58
+ @compileError(err);
59
+ },
60
+ },
61
+ else => stderr.print("{}", .{value}) catch return,
62
+ }
63
+ }
64
+ }
@@ -515,7 +515,8 @@ the pkmn engine, but the following moves have their broken behavior preserved in
515
515
  glitch](https://glitchcity.wiki/Substitute_%C2%BC_HP_glitch) also fails in many cases due to
516
516
  Pokémon Showdown implementing the health check based on floating point division instead of integer
517
517
  divison like on the cartridge (meaning the Substitute 1/4 glitch will only occur if the Pokémon's
518
- maximum HP is evenly divisible by 4).
518
+ maximum HP is evenly divisible by 4). Finally, Substitute also incorrectly blocks Dream Eater on
519
+ Pokémon Showdown.
519
520
 
520
521
  In addition to numerous cases where Pokémon Showdown uses the wrong type of message (e.g. `|-fail`
521
522
  vs. `|-miss|` vs. `|-immune|`, e.g. in the case of Leech Seed) which are not documented here, Pokémon
@@ -272,7 +272,7 @@ pub const Status = enum(u8) {
272
272
  return num > 0;
273
273
  }
274
274
 
275
- /// Retunrns a human-readable representation of the status `num`.
275
+ /// Returns a human-readable representation of the status `num`.
276
276
  pub fn name(num: u8) []const u8 {
277
277
  if (Status.is(num, .EXT)) return if (num == Status.TOX) "TOX" else "SLF";
278
278
  if (Status.is(num, .SLP)) return "SLP";
@@ -442,7 +442,9 @@ fn executeMove(
442
442
  .err => return @as(?Result, Result.Error),
443
443
  }
444
444
 
445
- if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, log)) return null;
445
+ if (!skip_can and !try canMove(battle, player, mslot, auto, skip_pp, from, residual, log)) {
446
+ return null;
447
+ }
446
448
 
447
449
  return doMove(battle, player, mslot, auto, residual, log);
448
450
  }
@@ -654,6 +656,7 @@ fn canMove(
654
656
  auto: bool,
655
657
  skip_pp: bool,
656
658
  from: ?Move,
659
+ residual: *bool,
657
660
  log: anytype,
658
661
  ) !bool {
659
662
  var side = battle.side(player);
@@ -688,7 +691,7 @@ fn canMove(
688
691
  try log.move(player_ident, side.last_selected_move, battle.active(target), f);
689
692
 
690
693
  if (move.effect.onBegin()) {
691
- try onBegin(battle, player, move, mslot, log);
694
+ try onBegin(battle, player, move, mslot, residual, log);
692
695
  return false;
693
696
  }
694
697
 
@@ -1216,7 +1219,7 @@ fn mirrorMove(
1216
1219
 
1217
1220
  incrementPP(side, mslot);
1218
1221
 
1219
- if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, log)) return null;
1222
+ if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, log)) return null;
1220
1223
  return doMove(battle, player, mslot, auto, residual, log);
1221
1224
  }
1222
1225
 
@@ -1245,7 +1248,7 @@ fn metronome(
1245
1248
 
1246
1249
  incrementPP(side, mslot);
1247
1250
 
1248
- if (!try canMove(battle, player, mslot, auto, false, .Metronome, log)) return null;
1251
+ if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
1249
1252
  return doMove(battle, player, mslot, auto, residual, log);
1250
1253
  }
1251
1254
 
@@ -1278,7 +1281,9 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
1278
1281
  if (move.effect == .Swift) return true;
1279
1282
  if (foe.active.volatiles.Invulnerable) break :miss true;
1280
1283
  }
1281
- if (move.effect == .DreamEater and !Status.is(foe.stored().status, .SLP)) {
1284
+ if (move.effect == .DreamEater and (!Status.is(foe.stored().status, .SLP) or
1285
+ (showdown and foe.active.volatiles.Substitute)))
1286
+ {
1282
1287
  immune.* = true;
1283
1288
  if (showdown) return false;
1284
1289
  break :miss true;
@@ -1480,16 +1485,20 @@ fn endTurn(battle: anytype, log: anytype) @TypeOf(log).Error!Result {
1480
1485
 
1481
1486
  fn checkEBC(battle: anytype) bool {
1482
1487
  for (battle.sides, 0..) |side, i| {
1488
+ const foe = battle.sides[~@intCast(u1, i)];
1489
+
1483
1490
  var foe_all_ghosts = true;
1484
1491
  var foe_all_transform = true;
1492
+ for (foe.order, 0..) |id, j| {
1493
+ if (id == 0) break;
1494
+ const active = j == 0;
1495
+ const pokemon = foe.pokemon[id - 1];
1485
1496
 
1486
- for (battle.sides[~@intCast(u1, i)].pokemon) |pokemon| {
1487
- if (pokemon.species == .None) continue;
1488
-
1489
- const ghost = pokemon.hp == 0 or pokemon.types.includes(.Ghost);
1497
+ const ghost = pokemon.hp == 0 or
1498
+ (if (active) foe.active.types else pokemon.types).includes(.Ghost);
1490
1499
  foe_all_ghosts = foe_all_ghosts and ghost;
1491
1500
  foe_all_transform = foe_all_transform and pokemon.hp == 0 or transform: {
1492
- for (pokemon.moves) |m| {
1501
+ for (if (active) foe.active.moves else pokemon.moves) |m| {
1493
1502
  if (m.id == .None) break :transform true;
1494
1503
  if (m.id != .Transform) break :transform false;
1495
1504
  }
@@ -1497,10 +1506,14 @@ fn checkEBC(battle: anytype) bool {
1497
1506
  };
1498
1507
  }
1499
1508
 
1500
- for (side.pokemon) |pokemon| {
1509
+ for (side.order, 0..) |id, j| {
1510
+ if (id == 0) break;
1511
+ const active = j == 0;
1512
+ const pokemon = side.pokemon[id - 1];
1513
+
1501
1514
  if (pokemon.hp == 0 or Status.is(pokemon.status, .FRZ)) continue;
1502
1515
  const transform = foe_all_transform and transform: {
1503
- for (pokemon.moves) |m| {
1516
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1504
1517
  if (m.id == .None) break :transform true;
1505
1518
  if (m.id != .Transform) break :transform false;
1506
1519
  }
@@ -1508,7 +1521,7 @@ fn checkEBC(battle: anytype) bool {
1508
1521
  };
1509
1522
  if (transform) continue;
1510
1523
  const no_pp = foe_all_ghosts and no_pp: {
1511
- for (pokemon.moves) |m| {
1524
+ for (if (active) side.active.moves else pokemon.moves) |m| {
1512
1525
  if (m.pp != 0) break :no_pp false;
1513
1526
  }
1514
1527
  break :no_pp true;
@@ -1524,7 +1537,14 @@ fn checkEBC(battle: anytype) bool {
1524
1537
  return false;
1525
1538
  }
1526
1539
 
1527
- inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, log: anytype) !void {
1540
+ inline fn onBegin(
1541
+ battle: anytype,
1542
+ player: Player,
1543
+ move: Move.Data,
1544
+ mslot: u8,
1545
+ residual: *bool,
1546
+ log: anytype,
1547
+ ) !void {
1528
1548
  assert(move.effect.onBegin());
1529
1549
  return switch (move.effect) {
1530
1550
  .Confusion => Effects.confusion(battle, player, move, log),
@@ -1540,7 +1560,7 @@ inline fn onBegin(battle: anytype, player: Player, move: Move.Data, mslot: u8, l
1540
1560
  .Poison => Effects.poison(battle, player, move, log),
1541
1561
  .Reflect => Effects.reflect(battle, player, log),
1542
1562
  .Splash => Effects.splash(battle, player, log),
1543
- .Substitute => Effects.substitute(battle, player, log),
1563
+ .Substitute => Effects.substitute(battle, player, residual, log),
1544
1564
  .SwitchAndTeleport => Effects.switchAndTeleport(battle, player, move, log),
1545
1565
  .Transform => Effects.transform(battle, player, log),
1546
1566
  else => unreachable,
@@ -1753,6 +1773,11 @@ pub const Effects = struct {
1753
1773
  var side = battle.side(player);
1754
1774
  var stored = side.stored();
1755
1775
 
1776
+ if (battle.last_damage == 0) {
1777
+ assert(showdown);
1778
+ return;
1779
+ }
1780
+
1756
1781
  const drain = @max(battle.last_damage / 2, 1);
1757
1782
  battle.last_damage = drain;
1758
1783
 
@@ -2063,7 +2088,7 @@ pub const Effects = struct {
2063
2088
  return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
2064
2089
  if (!showdown)
2065
2090
  .Poison
2066
- else if (toxic == foe.active.volatiles.Toxic)
2091
+ else if (toxic == (foe_stored.status == Status.TOX))
2067
2092
  if (toxic) .Toxic else .Poison
2068
2093
  else
2069
2094
  .None
@@ -2171,7 +2196,7 @@ pub const Effects = struct {
2171
2196
  try log.activate(battle.active(player), .Splash);
2172
2197
  }
2173
2198
 
2174
- fn substitute(battle: anytype, player: Player, log: anytype) !void {
2199
+ fn substitute(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
2175
2200
  var side = battle.side(player);
2176
2201
  if (side.active.volatiles.Substitute) {
2177
2202
  try log.fail(battle.active(player), .Substitute);
@@ -2196,7 +2221,10 @@ pub const Effects = struct {
2196
2221
  side.active.volatiles.substitute = hp + 1;
2197
2222
  side.active.volatiles.Substitute = true;
2198
2223
  try log.start(battle.active(player), .Substitute);
2199
- if (hp > 0) try log.damage(battle.active(player), side.stored(), .None);
2224
+ if (hp > 0) {
2225
+ try log.damage(battle.active(player), side.stored(), .None);
2226
+ if (showdown and side.stored().hp == 0) residual.* = false;
2227
+ }
2200
2228
  }
2201
2229
 
2202
2230
  fn switchAndTeleport(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
@@ -2514,13 +2542,9 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2514
2542
  volatiles.LeechSeed = false;
2515
2543
  try log.end(ident, .LeechSeed);
2516
2544
  }
2517
- if (volatiles.Toxic) {
2545
+ if (!showdown and volatiles.Toxic) {
2518
2546
  volatiles.Toxic = false;
2519
- // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it
2520
- if (showdown) {
2521
- volatiles.toxic = 0;
2522
- try log.end(ident, .Toxic);
2523
- }
2547
+ // volatiles.toxic is left unchanged, except on Pokémon Showdown which clears it (below)
2524
2548
  }
2525
2549
  if (volatiles.LightScreen) {
2526
2550
  volatiles.LightScreen = false;
@@ -2530,6 +2554,12 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
2530
2554
  volatiles.Reflect = false;
2531
2555
  try log.end(ident, .Reflect);
2532
2556
  }
2557
+ if (showdown and volatiles.Toxic) {
2558
+ volatiles.Toxic = false;
2559
+ // Pokémon Showdown erroneously clears the toxic counter
2560
+ volatiles.toxic = 0;
2561
+ try log.end(ident, .Toxic);
2562
+ }
2533
2563
  }
2534
2564
 
2535
2565
  fn disabled(side: *Side, ident: ID, log: anytype) !bool {