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

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