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