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