@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.
- 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 +27 -5
- 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.d.ts +2 -1
- package/build/pkg/gen1.js +38 -20
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +7 -4
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +4 -2
- package/build/pkg/protocol.js +65 -44
- package/build/pkg/protocol.js.map +1 -1
- package/build/test/display.js +15 -6
- 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 +454 -0
- package/src/lib/common/rng.zig +35 -26
- package/src/lib/common/util.zig +12 -0
- package/src/lib/gen1/calc.zig +802 -0
- package/src/lib/gen1/chance.zig +1159 -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 +104 -90
- package/src/lib/gen1/helpers.zig +140 -42
- package/src/lib/gen1/mechanics.zig +1135 -773
- package/src/lib/gen2/calc.zig +135 -0
- package/src/lib/gen2/chance.zig +895 -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 +3568 -12
- package/src/lib/node.zig +131 -0
- package/src/lib/pkmn.zig +81 -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 -804
- 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,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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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] = @
|
|
128
|
+
side.order[i] = @as(u4, @intCast(i)) + 1;
|
|
99
129
|
}
|
|
100
130
|
return side;
|
|
101
131
|
}
|
|
102
132
|
|
|
103
|
-
|
|
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,
|
|
109
|
-
side.order[i] = @
|
|
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(
|
|
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 = @
|
|
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 = @
|
|
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(
|
|
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
|
-
|
|
259
|
+
// Binding moves are borked but only via Mirror Move / Metronome which are already blocked
|
|
225
260
|
return switch (m) {
|
|
226
|
-
.
|
|
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
|
+
}
|