@pkmn/engine 0.1.0-dev.fb5bf955 → 0.1.0-dev.fb6653d6

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