@pkmn/engine 0.1.0-dev.5ec64ea5 → 0.1.0-dev.5fbc7aef

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 (87) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +141 -76
  3. package/build/pkg/addon/browser.d.ts +3 -0
  4. package/build/pkg/addon/browser.js +47 -0
  5. package/build/pkg/addon/browser.js.map +1 -0
  6. package/build/pkg/addon/node.d.ts +3 -0
  7. package/build/pkg/addon/node.js +110 -0
  8. package/build/pkg/addon/node.js.map +1 -0
  9. package/build/pkg/addon/wasm.d.ts +2 -0
  10. package/build/pkg/addon/wasm.js +53 -0
  11. package/build/pkg/addon/wasm.js.map +1 -0
  12. package/build/pkg/addon.d.ts +28 -6
  13. package/build/pkg/addon.js +42 -73
  14. package/build/pkg/addon.js.map +1 -1
  15. package/build/pkg/data/ids.json +12 -12
  16. package/build/pkg/data/layout.json +40 -5
  17. package/build/pkg/data/protocol.json +84 -13
  18. package/build/pkg/data.d.ts +27 -5
  19. package/build/pkg/data.js.map +1 -1
  20. package/build/pkg/gen1.d.ts +18 -20
  21. package/build/pkg/gen1.js +76 -51
  22. package/build/pkg/gen1.js.map +1 -1
  23. package/build/pkg/index.d.ts +57 -103
  24. package/build/pkg/index.js +23 -70
  25. package/build/pkg/index.js.map +1 -1
  26. package/build/pkg/protocol.d.ts +24 -3
  27. package/build/pkg/protocol.js +127 -104
  28. package/build/pkg/protocol.js.map +1 -1
  29. package/build/tools/debug.js +69 -74
  30. package/build/tools/debug.js.map +1 -1
  31. package/build/tools/display/debug.jsx +152 -0
  32. package/build/tools/display/debug.jsx.map +1 -0
  33. package/build/tools/display/index.js +152 -0
  34. package/build/tools/display/index.js.map +1 -0
  35. package/build/tools/display/ui.jsx +359 -0
  36. package/build/tools/display/ui.jsx.map +1 -0
  37. package/build/tools/display/util.js +33 -0
  38. package/build/tools/display/util.js.map +1 -0
  39. package/build.zig +337 -165
  40. package/package.json +71 -120
  41. package/src/bin/install-pkmn-engine +57 -26
  42. package/src/bin/pkmn-debug +3 -4
  43. package/src/lib/bindings/c.zig +200 -0
  44. package/src/lib/bindings/node.zig +117 -0
  45. package/src/lib/bindings/wasm.zig +50 -0
  46. package/src/lib/common/array.zig +70 -0
  47. package/src/lib/common/battle.zig +17 -0
  48. package/src/lib/common/data.zig +19 -31
  49. package/src/lib/common/debug.zig +51 -20
  50. package/src/lib/common/js.zig +114 -0
  51. package/src/lib/common/optional.zig +61 -0
  52. package/src/lib/common/options.zig +18 -3
  53. package/src/lib/common/protocol.zig +1050 -497
  54. package/src/lib/common/rational.zig +440 -0
  55. package/src/lib/common/rng.zig +31 -24
  56. package/src/lib/common/util.zig +44 -0
  57. package/src/lib/gen1/calc.zig +956 -0
  58. package/src/lib/gen1/chance.zig +1170 -0
  59. package/src/lib/gen1/data/moves.zig +203 -193
  60. package/src/lib/gen1/data/species.zig +6 -8
  61. package/src/lib/gen1/data/types.zig +12 -13
  62. package/src/lib/gen1/data.zig +104 -93
  63. package/src/lib/gen1/helpers.zig +138 -51
  64. package/src/lib/gen1/mechanics.zig +1364 -925
  65. package/src/lib/gen2/calc.zig +150 -0
  66. package/src/lib/gen2/chance.zig +896 -0
  67. package/src/lib/gen2/data/items.zig +15 -23
  68. package/src/lib/gen2/data/moves.zig +665 -573
  69. package/src/lib/gen2/data/species.zig +3 -5
  70. package/src/lib/gen2/data/types.zig +141 -36
  71. package/src/lib/gen2/data.zig +140 -104
  72. package/src/lib/gen2/generated.zig +801 -0
  73. package/src/lib/gen2/helpers.zig +58 -45
  74. package/src/lib/gen2/mechanics.zig +3582 -30
  75. package/src/lib/napi.h +2 -0
  76. package/src/lib/node.zig +14 -0
  77. package/src/lib/pkmn.zig +177 -22
  78. package/src/lib/wasm.zig +13 -0
  79. package/src/tools/display/dom.ts +93 -0
  80. package/src/tools/display/ui.css +354 -0
  81. package/build/test/display.js +0 -382
  82. package/build/test/display.js.map +0 -1
  83. package/src/lib/binding/node.zig +0 -229
  84. package/src/lib/binding/wasm.zig +0 -10
  85. package/src/lib/gen1/README.md +0 -784
  86. package/src/lib/gen2/README.md +0 -309
  87. package/src/test/display/pkmn.html.tmpl +0 -368
@@ -1,36 +1,40 @@
1
- const std = @import("std");
2
-
1
+ const builtin = @import("builtin");
3
2
  const common = @import("../common/data.zig");
3
+ const data = @import("data.zig");
4
4
  const options = @import("../common/options.zig");
5
5
  const protocol = @import("../common/protocol.zig");
6
6
  const rng = @import("../common/rng.zig");
7
-
8
- const data = @import("data.zig");
7
+ const std = @import("std");
8
+ const util = @import("../common/util.zig");
9
9
 
10
10
  const assert = std.debug.assert;
11
-
12
- const expectEqual = std.testing.expectEqual;
13
-
14
11
  const Choice = common.Choice;
15
-
16
- const showdown = options.showdown;
17
-
18
- const PSRNG = rng.PSRNG;
19
-
20
12
  const DVs = data.DVs;
21
13
  const Move = data.Move;
22
14
  const MoveSlot = data.MoveSlot;
15
+ const PSRNG = rng.PSRNG;
16
+ const showdown = options.showdown;
23
17
  const Species = data.Species;
24
18
  const Stats = data.Stats;
25
- const Status = data.Status;
26
19
 
27
- /// Options with dictate
20
+ const endian = builtin.cpu.arch.endian();
21
+
22
+ /// Configuration options controlling the domain of valid Pokémon that can be generated by the
23
+ /// random generator helpers.
28
24
  pub const Options = struct {
25
+ /// Whether to generate Pokémon adhering to "Cleric Clause".
29
26
  cleric: bool = showdown,
27
+ /// Whether to generate Pokémon with moves that contain bugs on Pokémon Showdown that are
28
+ /// unimplementable in the engine.
30
29
  block: bool = showdown,
30
+ // FIXME: remove once durations work with -Dchance
31
+ durations: bool = false,
31
32
  };
32
33
 
34
+ /// Helpers to simplify initialization of a Generation I battle.
33
35
  pub const Battle = struct {
36
+ /// Initializes a Generation I battle with the teams specified by `p1` and `p2`
37
+ /// and an RNG whose seed is derived from `seed`.
34
38
  pub fn init(
35
39
  seed: u64,
36
40
  p1: []const Pokemon,
@@ -43,6 +47,8 @@ pub const Battle = struct {
43
47
  };
44
48
  }
45
49
 
50
+ /// Initializes a Generation I battle with the teams specified by `p1` and `p2`
51
+ /// and a FixedRNG that returns the provided `rolls`.
46
52
  pub fn fixed(
47
53
  comptime rolls: anytype,
48
54
  p1: []const Pokemon,
@@ -54,81 +60,108 @@ pub const Battle = struct {
54
60
  };
55
61
  }
56
62
 
57
- pub fn random(rand: *PSRNG, opt: Options) data.Battle(data.PRNG) {
58
- var p1: u4 = 0;
59
- var p2: u4 = 0;
60
- var battle: data.Battle(data.PRNG) = .{
63
+ /// Returns a Generation I battle that is randomly generated based on the `rand` and `opts`.
64
+ pub fn random(rand: *PSRNG, opts: Options) data.Battle(data.PRNG) {
65
+ return .{
61
66
  .rng = prng(rand),
62
67
  .turn = 0,
63
68
  .last_damage = 0,
64
- .sides = .{ Side.random(rand, opt), Side.random(rand, opt) },
69
+ .sides = .{ Side.random(rand, opts), Side.random(rand, opts) },
65
70
  };
66
- battle.last_selected_indexes.p1 = p1;
67
- battle.last_selected_indexes.p2 = p2;
68
- return battle;
69
71
  }
70
72
  };
71
73
 
72
- pub fn prng(rand: *PSRNG) data.PRNG {
73
- // GLITCH: initial bytes in seed can only range from 0-252, not 0-255
74
- const max: u8 = 253;
74
+ fn prng(rand: *PSRNG) data.PRNG {
75
75
  return .{
76
76
  .src = .{
77
77
  .seed = if (showdown)
78
78
  rand.newSeed()
79
- else
80
- .{
81
- rand.range(u8, 0, max), rand.range(u8, 0, max),
82
- rand.range(u8, 0, max), rand.range(u8, 0, max),
83
- rand.range(u8, 0, max), rand.range(u8, 0, max),
84
- rand.range(u8, 0, max), rand.range(u8, 0, max),
85
- rand.range(u8, 0, max), rand.range(u8, 0, max),
86
- },
79
+ else seed: {
80
+ // GLITCH: The initial RNG seed in Gen I link battles is heavily constrained
81
+ // https://www.smogon.com/forums/threads/rng-in-rby-link-battles.3746779
82
+ var seed = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0 };
83
+
84
+ // Bytes in seed can only range from 0-252, not 0-255
85
+ const max: u8 = 253;
86
+ // Instructions take multiples of 4 cycles to execute
87
+ var timer: u16 = 4 * rand.range(u16, 0, 16384);
88
+ var add: u8 = rand.range(u8, 0, 256);
89
+ var carry: u8 = 0;
90
+
91
+ var i: u8 = 0;
92
+ while (i < seed.len) {
93
+ const div: u8 = @intCast(timer >> 8);
94
+ add +%= div +% carry;
95
+ if (add < max) {
96
+ seed[i] = add;
97
+ i += 1;
98
+ timer +%= 472;
99
+ carry = 1;
100
+ } else {
101
+ timer +%= 452;
102
+ carry = 0;
103
+ }
104
+ }
105
+ break :seed seed;
106
+ },
87
107
  },
88
108
  };
89
109
  }
90
110
 
111
+ /// Helpers to simplify initialization of a Generation I side.
91
112
  pub const Side = struct {
113
+ /// Initializes a Generation I side with the team specified by `ps`.
92
114
  pub fn init(ps: []const Pokemon) data.Side {
93
115
  assert(ps.len > 0 and ps.len <= 6);
94
116
  var side = data.Side{};
95
117
 
96
118
  for (0..ps.len) |i| {
97
119
  side.pokemon[i] = Pokemon.init(ps[i]);
98
- side.order[i] = @intCast(u4, i) + 1;
120
+ side.order[i] = @as(u4, @intCast(i)) + 1;
99
121
  }
100
122
  return side;
101
123
  }
102
124
 
103
- pub fn random(rand: *PSRNG, opt: Options) data.Side {
125
+ /// Returns a Generation I side that is randomly generated based on the `rand` and `opts`.
126
+ pub fn random(rand: *PSRNG, opts: Options) data.Side {
104
127
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 5 + 1) else 6;
105
128
  var side = data.Side{};
106
129
 
107
130
  for (0..n) |i| {
108
- side.pokemon[i] = Pokemon.random(rand, opt);
109
- side.order[i] = @intCast(u4, i) + 1;
131
+ side.pokemon[i] = Pokemon.random(rand, opts);
132
+ side.order[i] = @as(u4, @intCast(i)) + 1;
110
133
  }
111
134
 
112
135
  return side;
113
136
  }
114
137
  };
115
138
 
139
+ /// The maximum stat experience possible in Generation I.
116
140
  pub const EXP = 0xFFFF;
117
141
 
142
+ /// Helpers to simplify initialization of a Generation I Pokémon.
118
143
  pub const Pokemon = struct {
144
+ /// The Pokémon's species.
119
145
  species: Species,
146
+ /// The Pokémon's moves (assumed to all have the max possible PP).
120
147
  moves: []const Move,
148
+ /// The Pokémon's current HP (defaults to 100% if not specified).
121
149
  hp: ?u16 = null,
150
+ /// The Pokémon's current status.
122
151
  status: u8 = 0,
152
+ /// The Pokémon's level.
123
153
  level: u8 = 100,
154
+ /// The Pokémon's DVs.
124
155
  dvs: DVs = .{},
156
+ /// The Pokémon's stat experience.
125
157
  stats: Stats(u16) = .{ .hp = EXP, .atk = EXP, .def = EXP, .spe = EXP, .spc = EXP },
126
158
 
159
+ /// Initializes a Generation I Pokémon based on the information in `p`.
127
160
  pub fn init(p: Pokemon) data.Pokemon {
128
161
  var pokemon = data.Pokemon{};
129
162
  pokemon.species = p.species;
130
163
  const species = Species.get(p.species);
131
- inline for (@typeInfo(@TypeOf(pokemon.stats)).Struct.fields) |field| {
164
+ inline for (@typeInfo(@TypeOf(pokemon.stats)).@"struct".fields) |field| {
132
165
  const hp = comptime std.mem.eql(u8, field.name, "hp");
133
166
  const spc =
134
167
  comptime std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
@@ -144,7 +177,7 @@ pub const Pokemon = struct {
144
177
  for (p.moves, 0..) |m, j| {
145
178
  pokemon.moves[j].id = m;
146
179
  // NB: PP can be at most 61 legally (though can overflow to 63)
147
- pokemon.moves[j].pp = @intCast(u8, @min(Move.pp(m) / 5 * 8, 61));
180
+ pokemon.moves[j].pp = @intCast(@min(Move.pp(m) / 5 * 8, 61));
148
181
  }
149
182
  if (p.hp) |hp| {
150
183
  pokemon.hp = hp;
@@ -157,13 +190,14 @@ pub const Pokemon = struct {
157
190
  return pokemon;
158
191
  }
159
192
 
193
+ /// Returns a Generation I Pokémon that is randomly generated based on the `rand` and `opts`.
160
194
  pub fn random(rand: *PSRNG, opt: Options) data.Pokemon {
161
- const s = @intToEnum(Species, rand.range(u8, 1, Species.size + 1));
195
+ const s: Species = @enumFromInt(rand.range(u8, 1, Species.size + 1));
162
196
  const species = Species.get(s);
163
197
  const lvl = if (rand.chance(u8, 1, 20)) rand.range(u8, 1, 99 + 1) else 100;
164
198
  var stats: Stats(u16) = .{};
165
199
  const dvs = DVs.random(rand);
166
- inline for (@typeInfo(@TypeOf(stats)).Struct.fields) |field| {
200
+ inline for (@typeInfo(@TypeOf(stats)).@"struct".fields) |field| {
167
201
  @field(stats, field.name) = Stats(u16).calc(
168
202
  field.name,
169
203
  @field(species.stats, field.name),
@@ -176,20 +210,21 @@ pub const Pokemon = struct {
176
210
  );
177
211
  }
178
212
 
179
- var ms = [_]MoveSlot{.{}} ** 4;
213
+ var ms: [4]MoveSlot = @splat(.{});
180
214
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 3 + 1) else 4;
181
215
  for (0..n) |i| {
182
216
  var m: Move = .None;
183
217
  sample: while (true) {
184
- m = @intToEnum(Move, rand.range(u8, 1, Move.size - 1 + 1));
218
+ m = @enumFromInt(rand.range(u8, 1, Move.size - 1 + 1));
219
+ if (opt.durations and durations(m)) continue :sample;
185
220
  if (opt.block and blocked(m)) continue :sample;
186
221
  for (0..i) |j| if (ms[j].id == m) continue :sample;
187
222
  break;
188
223
  }
189
- const pp_ups =
224
+ const pp_ups: u8 =
190
225
  if (!opt.cleric and rand.chance(u8, 1, 10)) rand.range(u2, 0, 2 + 1) else 3;
191
226
  // NB: PP can be at most 61 legally (though can overflow to 63)
192
- const max_pp = @intCast(u8, Move.pp(m) + @as(u8, pp_ups) * @min(Move.pp(m) / 5, 7));
227
+ const max_pp: u8 = @intCast(Move.pp(m) + pp_ups * @min(Move.pp(m) / 5, 7));
193
228
  ms[i] = .{
194
229
  .id = m,
195
230
  .pp = if (opt.cleric) max_pp else rand.range(u8, 0, max_pp + 1),
@@ -203,6 +238,7 @@ pub const Pokemon = struct {
203
238
  .stats = stats,
204
239
  .hp = if (opt.cleric) stats.hp else rand.range(u16, 0, stats.hp + 1),
205
240
  .status = if (!opt.cleric and rand.chance(u8, 1, 6 + 1))
241
+ // NB: This will only assign sleep as 2 or 4 when rolled
206
242
  0 | (@as(u8, 1) << rand.range(u3, 1, 6 + 1))
207
243
  else
208
244
  0,
@@ -211,24 +247,75 @@ pub const Pokemon = struct {
211
247
  }
212
248
  };
213
249
 
214
- // cat src/test/showdown/blocklist.json | jq '."1".moves'
215
250
  fn blocked(m: Move) bool {
216
- if (Move.get(m).effect == .Binding) return true;
251
+ // Binding moves are borked but only via Mirror Move / Metronome which are already blocked
217
252
  return switch (m) {
218
- .Counter,
219
- .Mimic,
253
+ .Mimic, .Metronome, .MirrorMove, .Transform => true,
254
+ else => false,
255
+ };
256
+ }
257
+
258
+ fn durations(m: Move) bool {
259
+ // Any moves that have effects that result in duration tracking with -Dchance
260
+ return switch (Move.get(m).effect) {
261
+ .Haze,
220
262
  .Metronome,
221
- .MirrorMove,
222
- .Transform,
223
263
  => true,
224
264
  else => false,
225
265
  };
226
266
  }
227
267
 
268
+ /// Convenience helper to create a move-type choice for the provided move slot.
228
269
  pub fn move(slot: u4) Choice {
229
270
  return .{ .type = .Move, .data = slot };
230
271
  }
231
272
 
273
+ /// Convenience helper to create a switch-type choice for the provided team slot.
232
274
  pub fn swtch(slot: u4) Choice {
233
275
  return .{ .type = .Switch, .data = slot };
234
276
  }
277
+
278
+ /// Empirically determined maximum number of bytes possibly required to store the diff between two
279
+ /// Generation I battles.
280
+ pub const MAX_DIFFS: usize = 120;
281
+
282
+ /// The optimal size in bytes required to hold the diff between two Generation I battles.
283
+ /// At least as large as MAX_DIFFS.
284
+ pub const DIFFS_SIZE = if (builtin.mode == .ReleaseSmall)
285
+ MAX_DIFFS
286
+ else
287
+ std.math.ceilPowerOfTwo(usize, MAX_DIFFS) catch unreachable;
288
+
289
+ /// TODO
290
+ pub fn diff(
291
+ a: *const data.Battle(data.PRNG),
292
+ b: *const data.Battle(data.PRNG),
293
+ w: *protocol.Writer,
294
+ ) !usize {
295
+ const pos = w.pos;
296
+ const size = @sizeOf(data.Battle(data.PRNG)) - @sizeOf(data.PRNG);
297
+ assert(size % 2 == 0);
298
+ assert(size / 2 < 255);
299
+
300
+ var i: usize = 0;
301
+ while (i < size) : (i += 2) {
302
+ const val = std.mem.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
303
+ if (val != std.mem.bytesAsValue(u16, std.mem.asBytes(b)[i .. i + 2]).*) {
304
+ try w.writeByte(@intCast(i / 2));
305
+ try w.writeInt(u16, val, endian);
306
+ }
307
+ }
308
+
309
+ return w.pos - pos;
310
+ }
311
+
312
+ /// TODO
313
+ pub fn patch(battle: *data.Battle(data.PRNG), buf: []u8) void {
314
+ var i: usize = 0;
315
+ while (i < buf.len) : (i += 3) {
316
+ const off = @as(u16, @intCast(buf[i])) * 2;
317
+ const val = std.mem.readInt(u16, @as(*const [2]u8, @ptrCast(buf[i + 1 .. i + 3])), endian);
318
+ const bytes: *[2]u8 = @ptrCast(std.mem.asBytes(battle)[off .. off + 2]);
319
+ std.mem.writeInt(u16, bytes, val, endian);
320
+ }
321
+ }