@pkmn/engine 0.1.0-dev.a5a9f4b2 → 0.1.0-dev.a77ab8e2

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