@pkmn/engine 0.1.0-dev.d716e940 → 0.1.0-dev.d73cd76c

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 +114 -69
  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 +26 -4
  13. package/build/pkg/addon.js +39 -70
  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 +39 -4
  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 +15 -18
  21. package/build/pkg/gen1.js +43 -37
  22. package/build/pkg/gen1.js.map +1 -1
  23. package/build/pkg/index.d.ts +42 -91
  24. package/build/pkg/index.js +23 -70
  25. package/build/pkg/index.js.map +1 -1
  26. package/build/pkg/protocol.d.ts +29 -1
  27. package/build/pkg/protocol.js +123 -102
  28. package/build/pkg/protocol.js.map +1 -1
  29. package/build/tools/debug.js +84 -73
  30. package/build/tools/debug.js.map +1 -1
  31. package/build/tools/display/debug.jsx +66 -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 +360 -0
  36. package/build/tools/display/ui.jsx.map +1 -0
  37. package/build/tools/display/util.js +157 -0
  38. package/build/tools/display/util.js.map +1 -0
  39. package/build.zig +428 -176
  40. package/package.json +72 -122
  41. package/src/bin/install-pkmn-engine +56 -25
  42. package/src/bin/pkmn-debug +1 -1
  43. package/src/lib/bindings/c.zig +201 -0
  44. package/src/lib/bindings/node.zig +117 -0
  45. package/src/lib/bindings/wasm.zig +51 -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 +58 -33
  50. package/src/lib/common/js.zig +114 -0
  51. package/src/lib/common/optional.zig +71 -0
  52. package/src/lib/common/options.zig +15 -0
  53. package/src/lib/common/protocol.zig +1037 -462
  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 +64 -0
  57. package/src/lib/gen1/calc.zig +975 -0
  58. package/src/lib/gen1/chance.zig +1246 -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 +98 -89
  63. package/src/lib/gen1/helpers.zig +159 -51
  64. package/src/lib/gen1/mechanics.zig +1211 -846
  65. package/src/lib/gen2/calc.zig +150 -0
  66. package/src/lib/gen2/chance.zig +914 -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 +136 -100
  72. package/src/lib/gen2/generated.zig +801 -0
  73. package/src/lib/gen2/helpers.zig +81 -58
  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 +172 -17
  78. package/src/lib/wasm.zig +13 -0
  79. package/src/tools/display/dom.ts +108 -0
  80. package/src/tools/display/ui.css +354 -0
  81. package/build/test/display.js +0 -400
  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 -812
  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,84 +60,121 @@ pub const Battle = struct {
54
60
  };
55
61
  }
56
62
 
57
- pub fn random(rand: *PSRNG, opt: Options) 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) {
58
65
  return .{
59
66
  .rng = prng(rand),
60
67
  .turn = 0,
61
68
  .last_damage = 0,
62
- .sides = .{ Side.random(rand, opt), Side.random(rand, opt) },
69
+ .sides = .{ Side.random(rand, opts), Side.random(rand, opts) },
63
70
  };
64
71
  }
65
72
  };
66
73
 
67
- pub fn prng(rand: *PSRNG) data.PRNG {
68
- // GLITCH: initial bytes in seed can only range from 0-252, not 0-255
69
- const max: u8 = 253;
74
+ fn prng(rand: *PSRNG) data.PRNG {
70
75
  return .{
71
76
  .src = .{
72
77
  .seed = if (showdown)
73
78
  rand.newSeed()
74
- else
75
- .{
76
- rand.range(u8, 0, max), rand.range(u8, 0, max),
77
- rand.range(u8, 0, max), rand.range(u8, 0, max),
78
- rand.range(u8, 0, max), rand.range(u8, 0, max),
79
- rand.range(u8, 0, max), rand.range(u8, 0, max),
80
- rand.range(u8, 0, max), rand.range(u8, 0, max),
81
- },
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
+ },
82
107
  },
83
108
  };
84
109
  }
85
110
 
111
+ /// Helpers to simplify initialization of a Generation I side.
86
112
  pub const Side = struct {
113
+ /// Initializes a Generation I side with the team specified by `ps`.
87
114
  pub fn init(ps: []const Pokemon) data.Side {
88
115
  assert(ps.len > 0 and ps.len <= 6);
89
116
  var side = data.Side{};
90
117
 
91
118
  for (0..ps.len) |i| {
92
119
  side.pokemon[i] = Pokemon.init(ps[i]);
93
- side.order[i] = @intCast(u4, i) + 1;
120
+ side.order[i] = @as(u4, @intCast(i)) + 1;
94
121
  }
95
122
  return side;
96
123
  }
97
124
 
98
- 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 {
99
127
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 5 + 1) else 6;
100
128
  var side = data.Side{};
101
129
 
102
130
  for (0..n) |i| {
103
- side.pokemon[i] = Pokemon.random(rand, opt);
104
- side.order[i] = @intCast(u4, i) + 1;
131
+ side.pokemon[i] = Pokemon.random(rand, opts);
132
+ side.order[i] = @as(u4, @intCast(i)) + 1;
105
133
  }
106
134
 
107
135
  return side;
108
136
  }
109
137
  };
110
138
 
139
+ /// The maximum stat experience possible in Generation I.
111
140
  pub const EXP = 0xFFFF;
112
141
 
142
+ /// Helpers to simplify initialization of a Generation I Pokémon.
113
143
  pub const Pokemon = struct {
144
+ /// The Pokémon's species.
114
145
  species: Species,
146
+ /// The Pokémon's moves (assumed to all have the max possible PP).
115
147
  moves: []const Move,
148
+ /// The Pokémon's current HP (defaults to 100% if not specified).
116
149
  hp: ?u16 = null,
150
+ /// The Pokémon's current status.
117
151
  status: u8 = 0,
152
+ /// The Pokémon's level.
118
153
  level: u8 = 100,
154
+ /// The Pokémon's DVs.
119
155
  dvs: DVs = .{},
156
+ /// The Pokémon's stat experience.
120
157
  stats: Stats(u16) = .{ .hp = EXP, .atk = EXP, .def = EXP, .spe = EXP, .spc = EXP },
121
158
 
159
+ /// Initializes a Generation I Pokémon based on the information in `p`.
122
160
  pub fn init(p: Pokemon) data.Pokemon {
123
161
  var pokemon = data.Pokemon{};
124
162
  pokemon.species = p.species;
125
163
  const species = Species.get(p.species);
126
- inline for (@typeInfo(@TypeOf(pokemon.stats)).Struct.fields) |field| {
127
- const hp = comptime std.mem.eql(u8, field.name, "hp");
164
+ inline for (if (@hasField(@TypeOf(@typeInfo(@TypeOf(pokemon.stats)).@"struct"), "fields"))
165
+ @typeInfo(@TypeOf(pokemon.stats)).@"struct".fields
166
+ else
167
+ @typeInfo(@TypeOf(pokemon.stats)).@"struct".field_names) |field|
168
+ {
169
+ const field_name = if (@hasField(@TypeOf(field), "name")) field.name else field;
170
+ const hp = comptime std.mem.eql(u8, field_name, "hp");
128
171
  const spc =
129
- comptime std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
130
- @field(pokemon.stats, field.name) = Stats(u16).calc(
131
- field.name,
132
- @field(species.stats, field.name),
133
- if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs, field.name),
134
- @field(p.stats, field.name),
172
+ comptime std.mem.eql(u8, field_name, "spa") or std.mem.eql(u8, field_name, "spd");
173
+ @field(pokemon.stats, field_name) = Stats(u16).calc(
174
+ field_name,
175
+ @field(species.stats, field_name),
176
+ if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs, field_name),
177
+ @field(p.stats, field_name),
135
178
  p.level,
136
179
  );
137
180
  }
@@ -139,7 +182,7 @@ pub const Pokemon = struct {
139
182
  for (p.moves, 0..) |m, j| {
140
183
  pokemon.moves[j].id = m;
141
184
  // NB: PP can be at most 61 legally (though can overflow to 63)
142
- pokemon.moves[j].pp = @intCast(u8, @min(Move.pp(m) / 5 * 8, 61));
185
+ pokemon.moves[j].pp = @intCast(@min(Move.pp(m) / 5 * 8, 61));
143
186
  }
144
187
  if (p.hp) |hp| {
145
188
  pokemon.hp = hp;
@@ -152,39 +195,46 @@ pub const Pokemon = struct {
152
195
  return pokemon;
153
196
  }
154
197
 
198
+ /// Returns a Generation I Pokémon that is randomly generated based on the `rand` and `opts`.
155
199
  pub fn random(rand: *PSRNG, opt: Options) data.Pokemon {
156
- const s = @intToEnum(Species, rand.range(u8, 1, Species.size + 1));
200
+ const s: Species = @enumFromInt(rand.range(u8, 1, Species.size + 1));
157
201
  const species = Species.get(s);
158
202
  const lvl = if (rand.chance(u8, 1, 20)) rand.range(u8, 1, 99 + 1) else 100;
159
203
  var stats: Stats(u16) = .{};
160
204
  const dvs = DVs.random(rand);
161
- inline for (@typeInfo(@TypeOf(stats)).Struct.fields) |field| {
162
- @field(stats, field.name) = Stats(u16).calc(
163
- field.name,
164
- @field(species.stats, field.name),
165
- if (comptime std.mem.eql(u8, field.name, "hp"))
205
+ inline for (if (@hasField(@TypeOf(@typeInfo(@TypeOf(stats)).@"struct"), "fields"))
206
+ @typeInfo(@TypeOf(stats)).@"struct".fields
207
+ else
208
+ @typeInfo(@TypeOf(stats)).@"struct".field_names) |field|
209
+ {
210
+ const field_name = if (@hasField(@TypeOf(field), "name")) field.name else field;
211
+ @field(stats, field_name) = Stats(u16).calc(
212
+ field_name,
213
+ @field(species.stats, field_name),
214
+ if (comptime std.mem.eql(u8, field_name, "hp"))
166
215
  dvs.hp()
167
216
  else
168
- @field(dvs, field.name),
217
+ @field(dvs, field_name),
169
218
  if (rand.chance(u8, 1, 20)) rand.range(u16, 0, EXP + 1) else EXP,
170
219
  lvl,
171
220
  );
172
221
  }
173
222
 
174
- var ms = [_]MoveSlot{.{}} ** 4;
223
+ var ms: [4]MoveSlot = @splat(.{});
175
224
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 3 + 1) else 4;
176
225
  for (0..n) |i| {
177
226
  var m: Move = .None;
178
227
  sample: while (true) {
179
- m = @intToEnum(Move, rand.range(u8, 1, Move.size - 1 + 1));
228
+ m = @enumFromInt(rand.range(u8, 1, Move.size - 1 + 1));
229
+ if (opt.durations and durations(m)) continue :sample;
180
230
  if (opt.block and blocked(m)) continue :sample;
181
231
  for (0..i) |j| if (ms[j].id == m) continue :sample;
182
232
  break;
183
233
  }
184
- const pp_ups =
234
+ const pp_ups: u8 =
185
235
  if (!opt.cleric and rand.chance(u8, 1, 10)) rand.range(u2, 0, 2 + 1) else 3;
186
236
  // NB: PP can be at most 61 legally (though can overflow to 63)
187
- const max_pp = @intCast(u8, Move.pp(m) + @as(u8, pp_ups) * @min(Move.pp(m) / 5, 7));
237
+ const max_pp: u8 = @intCast(Move.pp(m) + pp_ups * @min(Move.pp(m) / 5, 7));
188
238
  ms[i] = .{
189
239
  .id = m,
190
240
  .pp = if (opt.cleric) max_pp else rand.range(u8, 0, max_pp + 1),
@@ -198,6 +248,7 @@ pub const Pokemon = struct {
198
248
  .stats = stats,
199
249
  .hp = if (opt.cleric) stats.hp else rand.range(u16, 0, stats.hp + 1),
200
250
  .status = if (!opt.cleric and rand.chance(u8, 1, 6 + 1))
251
+ // NB: This will only assign sleep as 2 or 4 when rolled
201
252
  0 | (@as(u8, 1) << rand.range(u3, 1, 6 + 1))
202
253
  else
203
254
  0,
@@ -214,10 +265,67 @@ fn blocked(m: Move) bool {
214
265
  };
215
266
  }
216
267
 
268
+ fn durations(m: Move) bool {
269
+ // Any moves that have effects that result in duration tracking with -Dchance
270
+ return switch (Move.get(m).effect) {
271
+ .Haze,
272
+ .Metronome,
273
+ => true,
274
+ else => false,
275
+ };
276
+ }
277
+
278
+ /// Convenience helper to create a move-type choice for the provided move slot.
217
279
  pub fn move(slot: u4) Choice {
218
280
  return .{ .type = .Move, .data = slot };
219
281
  }
220
282
 
283
+ /// Convenience helper to create a switch-type choice for the provided team slot.
221
284
  pub fn swtch(slot: u4) Choice {
222
285
  return .{ .type = .Switch, .data = slot };
223
286
  }
287
+
288
+ /// Empirically determined maximum number of bytes possibly required to store the diff between two
289
+ /// Generation I battles.
290
+ pub const MAX_DIFFS: usize = 120;
291
+
292
+ /// The optimal size in bytes required to hold the diff between two Generation I battles.
293
+ /// At least as large as MAX_DIFFS.
294
+ pub const DIFFS_SIZE = if (builtin.mode == .ReleaseSmall)
295
+ MAX_DIFFS
296
+ else
297
+ std.math.ceilPowerOfTwo(usize, MAX_DIFFS) catch unreachable;
298
+
299
+ /// TODO
300
+ pub fn diff(
301
+ a: *const data.Battle(data.PRNG),
302
+ b: *const data.Battle(data.PRNG),
303
+ w: *protocol.Writer,
304
+ ) !usize {
305
+ const pos = w.pos;
306
+ const size = @sizeOf(data.Battle(data.PRNG)) - @sizeOf(data.PRNG);
307
+ assert(size % 2 == 0);
308
+ assert(size / 2 < 255);
309
+
310
+ var i: usize = 0;
311
+ while (i < size) : (i += 2) {
312
+ const val = std.mem.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
313
+ if (val != std.mem.bytesAsValue(u16, std.mem.asBytes(b)[i .. i + 2]).*) {
314
+ try w.writeByte(@intCast(i / 2));
315
+ try w.writeInt(u16, val, endian);
316
+ }
317
+ }
318
+
319
+ return w.pos - pos;
320
+ }
321
+
322
+ /// TODO
323
+ pub fn patch(battle: *data.Battle(data.PRNG), buf: []u8) void {
324
+ var i: usize = 0;
325
+ while (i < buf.len) : (i += 3) {
326
+ const off = @as(u16, @intCast(buf[i])) * 2;
327
+ const val = std.mem.readInt(u16, @as(*const [2]u8, @ptrCast(buf[i + 1 .. i + 3])), endian);
328
+ const bytes: *[2]u8 = @ptrCast(std.mem.asBytes(battle)[off .. off + 2]);
329
+ std.mem.writeInt(u16, bytes, val, endian);
330
+ }
331
+ }