@pkmn/engine 0.1.0-dev.84cc1a02 → 0.1.0-dev.886adbe3

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 (65) hide show
  1. package/LICENSE +1 -3
  2. package/README.md +137 -71
  3. package/build/pkg/addon.d.ts +7 -0
  4. package/build/pkg/addon.js +111 -0
  5. package/build/pkg/addon.js.map +1 -0
  6. package/build/pkg/data/ids.json +12 -12
  7. package/build/pkg/data/layout.json +27 -5
  8. package/build/pkg/data/protocol.json +69 -12
  9. package/build/pkg/data.d.ts +5 -6
  10. package/build/pkg/data.js +3 -5
  11. package/build/pkg/data.js.map +1 -1
  12. package/build/pkg/gen1.d.ts +5 -4
  13. package/build/pkg/gen1.js +49 -33
  14. package/build/pkg/gen1.js.map +1 -1
  15. package/build/pkg/index.d.ts +20 -20
  16. package/build/pkg/index.js +2 -5
  17. package/build/pkg/index.js.map +1 -1
  18. package/build/pkg/protocol.d.ts +4 -2
  19. package/build/pkg/protocol.js +65 -44
  20. package/build/pkg/protocol.js.map +1 -1
  21. package/build/test/display.js +405 -0
  22. package/build/test/display.js.map +1 -0
  23. package/build/tools/debug.js +134 -0
  24. package/build/tools/debug.js.map +1 -0
  25. package/build.zig +300 -153
  26. package/package.json +89 -46
  27. package/src/bin/install-pkmn-engine +71 -37
  28. package/src/bin/pkmn-debug +62 -0
  29. package/src/lib/common/array.zig +77 -0
  30. package/src/lib/common/battle.zig +17 -0
  31. package/src/lib/common/data.zig +19 -31
  32. package/src/lib/common/debug.zig +23 -13
  33. package/src/lib/common/js.zig +120 -0
  34. package/src/lib/common/optional.zig +71 -0
  35. package/src/lib/common/options.zig +14 -3
  36. package/src/lib/common/protocol.zig +947 -403
  37. package/src/lib/common/rational.zig +447 -0
  38. package/src/lib/common/rng.zig +31 -22
  39. package/src/lib/common/util.zig +61 -0
  40. package/src/lib/gen1/calc.zig +804 -0
  41. package/src/lib/gen1/chance.zig +1167 -0
  42. package/src/lib/gen1/data/moves.zig +202 -190
  43. package/src/lib/gen1/data/species.zig +5 -5
  44. package/src/lib/gen1/data/types.zig +10 -10
  45. package/src/lib/gen1/data.zig +98 -83
  46. package/src/lib/gen1/helpers.zig +142 -40
  47. package/src/lib/gen1/mechanics.zig +1296 -871
  48. package/src/lib/gen2/calc.zig +161 -0
  49. package/src/lib/gen2/chance.zig +902 -0
  50. package/src/lib/gen2/data/items.zig +14 -20
  51. package/src/lib/gen2/data/moves.zig +664 -570
  52. package/src/lib/gen2/data/species.zig +2 -2
  53. package/src/lib/gen2/data/types.zig +137 -34
  54. package/src/lib/gen2/data.zig +129 -90
  55. package/src/lib/gen2/generated.zig +806 -0
  56. package/src/lib/gen2/helpers.zig +54 -32
  57. package/src/lib/gen2/mechanics.zig +3564 -15
  58. package/src/lib/node.zig +131 -0
  59. package/src/lib/pkmn.zig +88 -12
  60. package/src/lib/wasm.zig +13 -0
  61. package/src/test/display/pkmn.html.tmpl +368 -0
  62. package/src/lib/binding/node.zig +0 -229
  63. package/src/lib/binding/wasm.zig +0 -10
  64. package/src/lib/gen1/README.md +0 -778
  65. package/src/lib/gen2/README.md +0 -309
@@ -1,16 +1,16 @@
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");
5
6
  const protocol = @import("../common/protocol.zig");
6
7
  const rng = @import("../common/rng.zig");
8
+ const util = @import("../common/util.zig");
7
9
 
8
10
  const data = @import("data.zig");
9
11
 
10
12
  const assert = std.debug.assert;
11
13
 
12
- const expectEqual = std.testing.expectEqual;
13
-
14
14
  const Choice = common.Choice;
15
15
 
16
16
  const showdown = options.showdown;
@@ -22,15 +22,27 @@ const Move = data.Move;
22
22
  const MoveSlot = data.MoveSlot;
23
23
  const Species = data.Species;
24
24
  const Stats = data.Stats;
25
- const Status = data.Status;
26
25
 
27
- /// Options with dictate
26
+ const endian = builtin.cpu.arch.endian();
27
+
28
+ const Struct = if (@hasField(std.builtin.Type, "struct")) .@"struct" else .Struct;
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,81 +68,108 @@ 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;
130
171
  const species = Species.get(p.species);
131
- inline for (@typeInfo(@TypeOf(pokemon.stats)).Struct.fields) |field| {
172
+ inline for (@field(@typeInfo(@TypeOf(pokemon.stats)), @tagName(Struct)).fields) |field| {
132
173
  const hp = comptime std.mem.eql(u8, field.name, "hp");
133
174
  const spc =
134
175
  comptime std.mem.eql(u8, field.name, "spa") or std.mem.eql(u8, field.name, "spd");
@@ -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,13 +198,14 @@ 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) = .{};
165
207
  const dvs = DVs.random(rand);
166
- inline for (@typeInfo(@TypeOf(stats)).Struct.fields) |field| {
208
+ inline for (@field(@typeInfo(@TypeOf(stats)), @tagName(Struct)).fields) |field| {
167
209
  @field(stats, field.name) = Stats(u16).calc(
168
210
  field.name,
169
211
  @field(species.stats, field.name),
@@ -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,24 +255,82 @@ pub const Pokemon = struct {
211
255
  }
212
256
  };
213
257
 
214
- // cat src/test/showdown/blocklist.json | jq '."1".moves'
215
258
  fn blocked(m: Move) bool {
216
- if (Move.get(m).effect == .Binding) return true;
259
+ // Binding moves are borked but only via Mirror Move / Metronome which are already blocked
217
260
  return switch (m) {
218
- .Counter,
219
- .Mimic,
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,
220
277
  .Metronome,
221
- .MirrorMove,
222
- .Transform,
223
278
  => true,
224
279
  else => false,
225
280
  };
226
281
  }
227
282
 
283
+ /// Convenience helper to create a move-type choice for the provided move slot.
228
284
  pub fn move(slot: u4) Choice {
229
285
  return .{ .type = .Move, .data = slot };
230
286
  }
231
287
 
288
+ /// Convenience helper to create a switch-type choice for the provided team slot.
232
289
  pub fn swtch(slot: u4) Choice {
233
290
  return .{ .type = .Switch, .data = slot };
234
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 = util.bytesAsValue(u16, std.mem.asBytes(a)[i .. i + 2]).*;
318
+ if (val != util.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, @as(*const [2]u8, @ptrCast(buf[i + 1 .. i + 3])), endian);
333
+ const bytes: *[2]u8 = @ptrCast(std.mem.asBytes(battle)[off .. off + 2]);
334
+ std.mem.writeInt(u16, bytes, val, endian);
335
+ }
336
+ }