@pkmn/engine 0.1.0-dev.cf6f5d2c → 0.1.0-dev.d127cb34

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 +112 -66
  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 +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 +17 -19
  21. package/build/pkg/gen1.js +66 -39
  22. package/build/pkg/gen1.js.map +1 -1
  23. package/build/pkg/index.d.ts +46 -93
  24. package/build/pkg/index.js +23 -70
  25. package/build/pkg/index.js.map +1 -1
  26. package/build/pkg/protocol.d.ts +33 -3
  27. package/build/pkg/protocol.js +127 -104
  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 +414 -174
  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 +1245 -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 +100 -89
  63. package/src/lib/gen1/helpers.zig +162 -68
  64. package/src/lib/gen1/mechanics.zig +1237 -848
  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 -396
  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 -804
  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,89 +60,121 @@ 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| {
132
- 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");
133
171
  const spc =
134
- comptime std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
135
- @field(pokemon.stats, field.name) = Stats(u16).calc(
136
- field.name,
137
- @field(species.stats, field.name),
138
- if (hp) p.dvs.hp() else if (spc) p.dvs.spc else @field(p.dvs, field.name),
139
- @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),
140
178
  p.level,
141
179
  );
142
180
  }
@@ -144,7 +182,7 @@ pub const Pokemon = struct {
144
182
  for (p.moves, 0..) |m, j| {
145
183
  pokemon.moves[j].id = m;
146
184
  // 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));
185
+ pokemon.moves[j].pp = @intCast(@min(Move.pp(m) / 5 * 8, 61));
148
186
  }
149
187
  if (p.hp) |hp| {
150
188
  pokemon.hp = hp;
@@ -157,39 +195,46 @@ pub const Pokemon = struct {
157
195
  return pokemon;
158
196
  }
159
197
 
198
+ /// Returns a Generation I Pokémon that is randomly generated based on the `rand` and `opts`.
160
199
  pub fn random(rand: *PSRNG, opt: Options) data.Pokemon {
161
- const s = @intToEnum(Species, rand.range(u8, 1, Species.size + 1));
200
+ const s: Species = @enumFromInt(rand.range(u8, 1, Species.size + 1));
162
201
  const species = Species.get(s);
163
202
  const lvl = if (rand.chance(u8, 1, 20)) rand.range(u8, 1, 99 + 1) else 100;
164
203
  var stats: Stats(u16) = .{};
165
204
  const dvs = DVs.random(rand);
166
- inline for (@typeInfo(@TypeOf(stats)).Struct.fields) |field| {
167
- @field(stats, field.name) = Stats(u16).calc(
168
- field.name,
169
- @field(species.stats, field.name),
170
- 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"))
171
215
  dvs.hp()
172
216
  else
173
- @field(dvs, field.name),
217
+ @field(dvs, field_name),
174
218
  if (rand.chance(u8, 1, 20)) rand.range(u16, 0, EXP + 1) else EXP,
175
219
  lvl,
176
220
  );
177
221
  }
178
222
 
179
- var ms = [_]MoveSlot{.{}} ** 4;
223
+ var ms: [4]MoveSlot = @splat(.{});
180
224
  const n = if (rand.chance(u8, 1, 100)) rand.range(u4, 1, 3 + 1) else 4;
181
225
  for (0..n) |i| {
182
226
  var m: Move = .None;
183
227
  sample: while (true) {
184
- 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;
185
230
  if (opt.block and blocked(m)) continue :sample;
186
231
  for (0..i) |j| if (ms[j].id == m) continue :sample;
187
232
  break;
188
233
  }
189
- const pp_ups =
234
+ const pp_ups: u8 =
190
235
  if (!opt.cleric and rand.chance(u8, 1, 10)) rand.range(u2, 0, 2 + 1) else 3;
191
236
  // 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));
237
+ const max_pp: u8 = @intCast(Move.pp(m) + pp_ups * @min(Move.pp(m) / 5, 7));
193
238
  ms[i] = .{
194
239
  .id = m,
195
240
  .pp = if (opt.cleric) max_pp else rand.range(u8, 0, max_pp + 1),
@@ -203,6 +248,7 @@ pub const Pokemon = struct {
203
248
  .stats = stats,
204
249
  .hp = if (opt.cleric) stats.hp else rand.range(u16, 0, stats.hp + 1),
205
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
206
252
  0 | (@as(u8, 1) << rand.range(u3, 1, 6 + 1))
207
253
  else
208
254
  0,
@@ -211,27 +257,75 @@ pub const Pokemon = struct {
211
257
  }
212
258
  };
213
259
 
214
- // TODO: uncomment once integration tests pass
215
- // fn blocked(m: Move) bool {
216
- // // Binding moves are borked but only via Mirror Move / Metronome which are already blocked
217
- // return switch (m) {
218
- // .Mimic, .Metronome, .MirrorMove, .Transform => true,
219
- // else => false,
220
- // };
221
- // }
222
-
223
260
  fn blocked(m: Move) bool {
224
- if (Move.get(m).effect == .Binding) return true;
261
+ // Binding moves are borked but only via Mirror Move / Metronome which are already blocked
225
262
  return switch (m) {
226
- .Counter, .Mimic, .Bide, .Metronome, .MirrorMove, .Transform => true,
263
+ .Mimic, .Metronome, .MirrorMove, .Transform => true,
227
264
  else => false,
228
265
  };
229
266
  }
230
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.
231
279
  pub fn move(slot: u4) Choice {
232
280
  return .{ .type = .Move, .data = slot };
233
281
  }
234
282
 
283
+ /// Convenience helper to create a switch-type choice for the provided team slot.
235
284
  pub fn swtch(slot: u4) Choice {
236
285
  return .{ .type = .Switch, .data = slot };
237
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
+ }