blockyard 0.1.1 → 0.1.2
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/CHANGELOG.md +7 -3
- package/README.md +4 -4
- package/docs/GETTING-STARTED.md +1 -1
- package/docs/USER-GUIDE.md +21 -13
- package/package.json +1 -1
- package/public/css/app.css +2 -2
- package/public/js/scorched.js +21 -1
- package/public/js/scorchedai.js +47 -6
- package/public/js/scorchedyard.js +31 -8
- package/public/js/settings.js +2 -2
- package/scripts/shots.mjs +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,7 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [0.1.
|
|
9
|
+
## [0.1.2] — 2026-09-17
|
|
10
10
|
|
|
11
11
|
A day of hardening, and a new game. The headline is the **third AI security audit**, run on
|
|
12
12
|
2026-09-16 over the whole codebase by four auditors in parallel, and every one of its findings
|
|
@@ -18,9 +18,10 @@ implementation; property tests over Bitcoin Core's file formats found and fixed
|
|
|
18
18
|
reading undo data. The **skies** were made clear: every board picks the Galaxy or the Earth sky in
|
|
19
19
|
one place, and the Earth sky gained a warm sun with a halo, a moon that glows through its real
|
|
20
20
|
phases, and softer clouds. And under Diversions, **Scorched Yard**: Scorched Earth on the block
|
|
21
|
-
engine, with the manual's weapons and computer players
|
|
21
|
+
engine, with the manual's weapons and computer players who each wear their own colour and aim like
|
|
22
|
+
people, up to eight tanks, a cheat mode that draws
|
|
22
23
|
the firing solution, and wind that is a real fluid simulation flowing over the hills. The games now
|
|
23
|
-
load only when their page is first opened. 1,
|
|
24
|
+
load only when their page is first opened. 1,070 tests, from 945.
|
|
24
25
|
|
|
25
26
|
### Upgrading from 0.1.0
|
|
26
27
|
|
|
@@ -73,6 +74,9 @@ load only when their page is first opened. 1,068 tests, from 945.
|
|
|
73
74
|
|
|
74
75
|
### Scorched Yard
|
|
75
76
|
|
|
77
|
+
- **Fixed: Scorched Yard's Shooter and Chooser were far too accurate.** Both fired the exact shot the physics search found, with no error: over 200 simulated games the Shooter hit with 60% of its first shots and the Chooser with 95%, as often as the Spoiler the manual calls nearly perfect. Every personality that searches now aims the way a person does, off by an error that grows with the distance and shrinks with each shot at the same target. First-shot hit rates now: Moron and Tosser about 5%, Shooter and Poolshark about 23%, Chooser about 43%, Spoiler about 84%, Cyborg about 76%, and the Shooter closes from 23% to 32% by its third shot. A test holds the order and the bands
|
|
78
|
+
- **Scorched Yard: each personality has its own colour.** A tank took its colour from its seat, so the first opponent was blue and the second green whatever played there. Now You are orange, the Moron yellow, the Shooter blue, the Poolshark cyan, the Tosser green, the Chooser purple, the Spoiler red, the Cyborg pink and the Unknown grey (which gives nothing away about the personality it draws), and a second or third of one kind is a lighter or darker shade. The scoreboard's dot follows the tank's colour too; it had been a colour per seat, so a red Spoiler was listed with a blue dot
|
|
79
|
+
- **Scorched Yard's opponents are shuffled every game.** With the shipped mix, the seats were filled from a fixed list in order, so two opponents were a Shooter and a Tosser in every game. Each new game now deals the six personalities (Shooter, Tosser, Chooser, Spoiler, Cyborg, Poolshark) in a fresh order: two opponents are a different pair each time, up to six are all different kinds, and a seventh starts a new deal. Naming one kind in *Their kind* still fills every seat with it
|
|
76
80
|
- **Scorched Yard: the rest of the control plan.** *Correcting*: click an enemy tank to mark it; after your shot the panel says how it did against it (*10 over*, *6 short*, *close*, *hit*) and **C** sets the power that miss implies, from your own last shot. *Weapons*: **W** opens a grid of what you own, biggest blast first, with counts and keys; **1–9** pick in the shop's order; **Q** picks your last weapon; the item pills are buttons. *The aim guide*: a ghost of the first fifth of the flight while you aim, or the whole flight, or off. *The fire button* names what it will send, and the last of a weapon sold one at a time asks once before it goes. A press on your own tank turns the barrel without touching the power. The keys are a table. Three new settings: Aim guide, Correction helper, Confirm the last of a weapon
|
|
77
81
|
- **Scorched Yard: the old wind code is gone.** The four earlier wind pictures (marching particles, dashed currents, a plasma wash, bowing bands), the smoke dye the fluid first carried, and the air clock only they used are deleted, with the tests that covered only them: the wind module drops from about 400 lines to 69, and the air module loses its dye grid. What is drawn is unchanged — the simulated air, its flow lines, and the sky's faint ripple
|
|
78
82
|
- **Fixed: Scorched Yard's computer players seemed to aim at tanks already destroyed.** A computer player chose its shot only when its thinking pause ran out, so for the whole pause its gauge (and, in cheat mode, its trajectory) still showed its previous shot, usually aimed at the tank it had just destroyed. It now plans the moment its turn begins, against the tanks alive then, and its barrel and power ease from the old aim to the new one while it thinks. Cheat mode draws only your own firing solution. And the random shot that the Moron fires, and that the Shooter and Poolshark fall back on, heads toward a living enemy instead of the middle of the field, which had kept shelling the empty half once the tanks there were gone: in 4,357 simulated shots none now fires away from every tank still standing, against 20 before
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ top of the node's own ~875 GB of block files.
|
|
|
47
47
|
```bash
|
|
48
48
|
git clone https://github.com/BobClawblaw/blockyard.git
|
|
49
49
|
cd blockyard
|
|
50
|
-
npm test # optional:
|
|
50
|
+
npm test # optional: 1070 unit tests, all built in
|
|
51
51
|
npm run setup # reads the node's bitcoin.conf, checks the node, writes config/local.json
|
|
52
52
|
npm start # builds the address index in the background (a few hours); open https://127.0.0.1:21000
|
|
53
53
|
# and sign in as admin with the password the first start prints once
|
|
@@ -136,7 +136,7 @@ Details in [docs/SECURITY.md](docs/SECURITY.md). To report a vulnerability, see
|
|
|
136
136
|
npm run dev # fake node doing a simulated sync, port 18088
|
|
137
137
|
npm run setup # interactive install: read bitcoin.conf, check the node, write config/local.json
|
|
138
138
|
npm run check # the same checks (every call timed) against every configured node; exits 1 on a FAIL
|
|
139
|
-
npm test #
|
|
139
|
+
npm test # 1070 unit tests (node:test, no dependencies)
|
|
140
140
|
npm run smoke # boots the real server and checks the HTTP contract
|
|
141
141
|
npm run counts:fix # keep the documented test count in step with the suite
|
|
142
142
|
```
|
|
@@ -146,7 +146,7 @@ RPC etiquette) and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for how it fits
|
|
|
146
146
|
|
|
147
147
|
## Status
|
|
148
148
|
|
|
149
|
-
Version **0.1.
|
|
149
|
+
Version **0.1.2** (2026-09-17) — a hardening release after 0.1.0: the third security audit with
|
|
150
150
|
every finding fixed, a resumable address index build, and Scorched Yard. Pre-release software: the
|
|
151
151
|
word is meant literally. Releases are published on [npm](https://www.npmjs.com/package/blockyard)
|
|
152
152
|
as `blockyard`, as [GitHub releases](https://github.com/BobClawblaw/blockyard/releases),
|
|
@@ -157,7 +157,7 @@ It ships **hardened**: bound to this machine, sign-in on, HTTPS with a certifica
|
|
|
157
157
|
and **zero telemetry** — no outbound connection to anyone but your node until you tick the market
|
|
158
158
|
polling switch yourself.
|
|
159
159
|
The test suite is
|
|
160
|
-
comprehensive (
|
|
160
|
+
comprehensive (1070 tests, plus a live smoke run), the monitoring side is solid, and the
|
|
161
161
|
explorer's biggest gap is closed: **address history and balances**, which Bitcoin Core cannot
|
|
162
162
|
answer at any setting, now come from an **address index BlockYard builds itself** from the
|
|
163
163
|
node's block and undo files and keeps current as blocks arrive. It is checked against the node
|
package/docs/GETTING-STARTED.md
CHANGED
|
@@ -165,7 +165,7 @@ it is not echoed.
|
|
|
165
165
|
npm start
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
The log says `BlockYard 0.1.
|
|
168
|
+
The log says `BlockYard 0.1.2 listening on https://127.0.0.1:21000` (its own self-signed
|
|
169
169
|
certificate, made on this first start; the browser warns once and remembers it), then `created the
|
|
170
170
|
first admin account (admin)` with a generated password **shown once** — copy it, or set
|
|
171
171
|
`BLOCKYARD_ADMIN_PASSWORD` before the first start to choose it — then `address index: building
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -724,7 +724,8 @@ Scorched Earth, the 1991 DOS artillery game, on the block engine: a landscape of
|
|
|
724
724
|
craters and falls, tanks that aim by angle and power, wind, and a round that ends when one tank is
|
|
725
725
|
left. Under **Diversions**, after Blockanoid. You play against two computer opponents out of the
|
|
726
726
|
box (**Display settings → Scorched Yard → Computer players** fields up to seven, for the original's
|
|
727
|
-
eight seats
|
|
727
|
+
eight seats). Every personality has its own colour, so you can see who you are up against; a second
|
|
728
|
+
or third of one kind is a lighter or darker shade of it.
|
|
728
729
|
|
|
729
730
|
The rules are the original's (docs/PLAN-SCORCHED-YARD.md is the whole plan): the manual's roster
|
|
730
731
|
of thirty-three weapons and eleven accessories at the manual's prices, packs and blast radii,
|
|
@@ -786,21 +787,28 @@ and 5% interest, which the settings allow.
|
|
|
786
787
|
|
|
787
788
|

|
|
788
789
|
|
|
789
|
-
Out of the box you face a **mix
|
|
790
|
-
|
|
790
|
+
Out of the box you face a **mix**: every new game deals a Shooter, a Tosser, a Chooser, a Spoiler,
|
|
791
|
+
a Cyborg and a Poolshark in a fresh order, so two opponents are a different pair each time, and up
|
|
792
|
+
to six opponents are all different kinds (a seventh starts a new deal). **Display settings → Scorched Yard
|
|
791
793
|
→ Their kind** fields one kind in every seat instead. They are the manual's eight, and they
|
|
792
794
|
behave as it says:
|
|
793
795
|
|
|
794
|
-
| kind | how it plays |
|
|
795
|
-
|
|
796
|
-
| Moron | picks an angle and a power, and shoots; buys at random |
|
|
797
|
-
| Shooter | takes low, straight shots when it has a line of fire, and shoots like a Moron when it does not |
|
|
798
|
-
| Poolshark | a Shooter, except that under rubber or spring walls it looks for the bank shot too |
|
|
799
|
-
| Tosser | lobs high, then corrects its power from each miss, by halves, until it hits |
|
|
800
|
-
| Chooser | tries the straight shot, the lob and the bank, and takes whichever lands closest |
|
|
801
|
-
| Spoiler | works the shot out from the wind and gravity and is nearly perfect
|
|
802
|
-
| Cyborg | a Spoiler with a grudge: it fires at whoever hit it last, else at the weakest, else at the leader |
|
|
803
|
-
| Unknown | one of the others, drawn afresh each round and never announced |
|
|
796
|
+
| kind | colour | how it plays |
|
|
797
|
+
|---|---|---|
|
|
798
|
+
| Moron | yellow | picks an angle and a power, and shoots; buys at random |
|
|
799
|
+
| Shooter | blue | takes low, straight shots when it has a line of fire, and shoots like a Moron when it does not; its aim is rough at first and closes in on the same target |
|
|
800
|
+
| Poolshark | cyan | a Shooter, except that under rubber or spring walls it looks for the bank shot too |
|
|
801
|
+
| Tosser | green | lobs high, then corrects its power from each miss, by halves, until it hits |
|
|
802
|
+
| Chooser | purple | tries the straight shot, the lob and the bank, and takes whichever lands closest; a steadier hand than the Shooter's, not a perfect one |
|
|
803
|
+
| Spoiler | red | works the shot out from the wind and gravity and is nearly perfect |
|
|
804
|
+
| Cyborg | pink | a Spoiler with a grudge: it fires at whoever hit it last, else at the weakest, else at the leader |
|
|
805
|
+
| Unknown | grey | one of the others, drawn afresh each round and never announced |
|
|
806
|
+
|
|
807
|
+
How often each lands its first shot at a target, measured over hundreds of simulated games: the
|
|
808
|
+
Moron and the Tosser about one in twenty, the Shooter and the Poolshark about one in four, the
|
|
809
|
+
Chooser about two in five, and the Spoiler and the Cyborg about four in five. The Shooter, the
|
|
810
|
+
Poolshark, the Chooser, the Spoiler and the Cyborg aim better with each shot at the same target and
|
|
811
|
+
start afresh each round; the Tosser corrects in its own way. You are orange.
|
|
804
812
|
|
|
805
813
|
All but the Moron use a battery when hurt and raise a shield before firing, and each shops to its
|
|
806
814
|
own taste between rounds: Shooters buy missiles, Tossers MIRVs and Funky Bombs, Cyborgs the
|
package/package.json
CHANGED
package/public/css/app.css
CHANGED
|
@@ -2001,8 +2001,8 @@ table.sykeys td { padding: 2px 0; }
|
|
|
2001
2001
|
padding: 4px 9px; border-radius: 10px; background: rgba(14, 16, 22, .88); border: 1px solid var(--line); color: var(--fg);
|
|
2002
2002
|
font-size: 12px; white-space: nowrap; pointer-events: none; }
|
|
2003
2003
|
.sytalk.hidden { display: none; }
|
|
2004
|
-
|
|
2005
|
-
|
|
2004
|
+
/* each dot takes its tank's own colour, set through the CSSOM by scorchedyard.js drawTanks (the
|
|
2005
|
+
colour follows the personality, and a class per seat painted the seat's old colour instead) */
|
|
2006
2006
|
/* the items line: pills, lit when armed */
|
|
2007
2007
|
.syitems { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; font-size: 11px; min-height: 18px; }
|
|
2008
2008
|
.sypill { padding: 2px 7px; border-radius: 999px; border: 1px solid var(--line); color: var(--fg-dim); }
|
package/public/js/scorched.js
CHANGED
|
@@ -34,6 +34,25 @@ const UNKNOWN_POOL = Object.freeze(['moron', 'shooter', 'poolshark', 'tosser', '
|
|
|
34
34
|
// EIGHT SEATS, EIGHT COLOURS (operator, 2026-09-16: "I would like to add up to 8 players total like
|
|
35
35
|
// the original"). Distinct at a glance on dirt, and none of them the dirt's own browns.
|
|
36
36
|
export const TANK_COLOURS = Object.freeze(['#f7931a', '#4d8dff', '#2ecc8f', '#ef5a5a', '#c78bff', '#f0c419', '#32d4e0', '#ff86b8']);
|
|
37
|
+
// A COLOUR FOR EACH PERSONALITY (operator, 2026-09-17: "assign a color to each personality"). A tank
|
|
38
|
+
// used to take its colour from its seat, so the first opponent was blue and the second green whatever
|
|
39
|
+
// played there. Now the colour says who you are up against: you are BlockYard orange, and each of
|
|
40
|
+
// the manual's eight has its own. The Unknown has one too, so its colour gives nothing away about
|
|
41
|
+
// the personality it draws each round. A second or third of one kind is a lighter, then a darker,
|
|
42
|
+
// shade of the same colour.
|
|
43
|
+
export const PERSONA_COLOURS = Object.freeze({
|
|
44
|
+
human: '#f7931a', moron: '#f0c419', shooter: '#4d8dff', poolshark: '#32d4e0', tosser: '#2ecc8f',
|
|
45
|
+
chooser: '#c78bff', spoiler: '#ef5a5a', cyborg: '#ff86b8', unknown: '#b7c0cc',
|
|
46
|
+
});
|
|
47
|
+
const mixHex = (hex, to, k) => `#${[1, 3, 5].map((i) => Math.round(parseInt(hex.slice(i, i + 2), 16) * (1 - k) + to * k).toString(16).padStart(2, '0')).join('')}`;
|
|
48
|
+
/** The colour of the `nth` tank (1 = the first) of a kind: its own colour, then shades of it. */
|
|
49
|
+
export function colourFor(kind, nth = 1) {
|
|
50
|
+
const base = PERSONA_COLOURS[kind] ?? TANK_COLOURS[0];
|
|
51
|
+
if (nth <= 1) return base;
|
|
52
|
+
const shades = [[255, 0.35], [0, 0.35], [255, 0.6], [0, 0.55], [255, 0.8], [0, 0.7]];
|
|
53
|
+
const [to, k] = shades[(nth - 2) % shades.length];
|
|
54
|
+
return mixHex(base, to, k);
|
|
55
|
+
}
|
|
37
56
|
export const MAX_PLAYERS = 8;
|
|
38
57
|
export const MAX_HEALTH = 100;
|
|
39
58
|
export const V_MAX = 56; // cells/s at power 1000
|
|
@@ -192,6 +211,7 @@ export function newGame(players, opts = {}) {
|
|
|
192
211
|
kills: 0, score: 0, cash: Number.isFinite(opts.cash) ? opts.cash : START_CASH, damageDealt: 0,
|
|
193
212
|
lastHitBy: null, // who hurt this tank last (the Cyborg holds a grudge)
|
|
194
213
|
memory: null, // a computer player's last shot at its target, for correcting
|
|
214
|
+
aimAt: null, // a computer player's shots so far at one target: its aim tightens (scorchedai.js AIM)
|
|
195
215
|
persona: p.kind ?? 'human', // what an Unknown is playing as this round
|
|
196
216
|
})),
|
|
197
217
|
round: 1, rounds: Math.max(1, Math.round(opts.rounds ?? 5)),
|
|
@@ -216,7 +236,7 @@ export function startRound(g) {
|
|
|
216
236
|
placeTanks(g);
|
|
217
237
|
for (const t of g.tanks) {
|
|
218
238
|
t.health = MAX_HEALTH; t.alive = true; t.shield = null; t.armed = { contactTrigger: false, heatGuidance: false };
|
|
219
|
-
t.lastHitBy = null; t.memory = null;
|
|
239
|
+
t.lastHitBy = null; t.memory = null; t.aimAt = null;
|
|
220
240
|
// an Unknown is one of the others, drawn each round and never announced (the manual)
|
|
221
241
|
t.persona = t.kind === 'unknown' ? UNKNOWN_POOL[Math.floor(g.rnd() * UNKNOWN_POOL.length)] : t.kind;
|
|
222
242
|
}
|
package/public/js/scorchedai.js
CHANGED
|
@@ -77,6 +77,48 @@ function pickWeapon(tank, target, { thrifty = false } = {}) {
|
|
|
77
77
|
return owned.sort((a, b) => WEAPONS[b].damage - WEAPONS[a].damage)[0];
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// ------------------------------------------------------------------- the aim, as a person aims
|
|
81
|
+
/**
|
|
82
|
+
* HOW WELL EACH OF THEM AIMS (operator, 2026-09-17: "Chooser and Shoot are both too fucking accurate").
|
|
83
|
+
* The search above finds the shot the physics says lands closest, and the Shooter and the Chooser
|
|
84
|
+
* fired exactly that: measured over 200 games, the Shooter hit with 60% of its first shots (every
|
|
85
|
+
* miss was a game with no low line of fire) and the Chooser with 95%, more than the Spoiler the
|
|
86
|
+
* manual calls the one that "gets a perfect shot almost every time". So each personality now
|
|
87
|
+
* aims the way a person does: the found angle and power, off by an error drawn from the game's own
|
|
88
|
+
* stream. The error grows with the distance to the target and shrinks with every shot at the same
|
|
89
|
+
* target, down to a floor, so they close in on you instead of starting perfect.
|
|
90
|
+
*
|
|
91
|
+
* angle standard deviation of the angle error, degrees, at middle distance
|
|
92
|
+
* power standard deviation of the power error, as a fraction of the power
|
|
93
|
+
* learn how much of the error each earlier shot at this target removes
|
|
94
|
+
* floor the least of the error that remains, however many shots
|
|
95
|
+
*
|
|
96
|
+
* Tuned by simulation to first-shot hit rates of about 5% Moron and Tosser, 25-30% Shooter and
|
|
97
|
+
* Poolshark, 40% Chooser and 85% Spoiler and Cyborg (test/scorched.test.js holds the bands).
|
|
98
|
+
*/
|
|
99
|
+
export const AIM = Object.freeze({
|
|
100
|
+
shooter: Object.freeze({ angle: 3.5, power: 0.07, learn: 0.22, floor: 0.35 }),
|
|
101
|
+
poolshark: Object.freeze({ angle: 3.5, power: 0.07, learn: 0.22, floor: 0.35 }),
|
|
102
|
+
chooser: Object.freeze({ angle: 2.9, power: 0.055, learn: 0.25, floor: 0.3 }),
|
|
103
|
+
spoiler: Object.freeze({ angle: 0.5, power: 0.008, learn: 0.3, floor: 0.4 }),
|
|
104
|
+
});
|
|
105
|
+
function gauss(rnd) {
|
|
106
|
+
let u = 0;
|
|
107
|
+
while (u === 0) u = rnd();
|
|
108
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * rnd());
|
|
109
|
+
}
|
|
110
|
+
/** `shot` as this tank would really fire it at `target`, with its personality's error; counts the shot. */
|
|
111
|
+
export function aimed(g, tank, target, shot, profile, rnd = g.rnd) {
|
|
112
|
+
if (!profile) return shot;
|
|
113
|
+
const dist = Math.abs(centreX(target) - centreX(tank)) / COLS; // 0 .. 1
|
|
114
|
+
const before = tank.aimAt?.target === target.id ? tank.aimAt.shots : 0;
|
|
115
|
+
tank.aimAt = { target: target.id, shots: before + 1 };
|
|
116
|
+
const scale = (0.5 + dist) * Math.max(profile.floor, 1 - profile.learn * before);
|
|
117
|
+
const angle = Math.max(0, Math.min(180, shot.angle + gauss(rnd) * profile.angle * scale));
|
|
118
|
+
const power = Math.max(50, Math.min(1000, Math.round(shot.power * (1 + gauss(rnd) * profile.power * scale))));
|
|
119
|
+
return { ...shot, angle, power };
|
|
120
|
+
}
|
|
121
|
+
|
|
80
122
|
// ------------------------------------------------------------------- the personalities
|
|
81
123
|
/** The Moron: "pick an angle and power, and shoot". Whatever it happens to own. */
|
|
82
124
|
export function moron(g, tank, rnd = g.rnd) {
|
|
@@ -98,7 +140,7 @@ export function shooter(g, tank, rnd = g.rnd) {
|
|
|
98
140
|
if (!target) return moron(g, tank, rnd);
|
|
99
141
|
const best = solve(g, tank, target, [12, 20, 28, 36], 25);
|
|
100
142
|
if (!best || best.miss > 12) return { ...moron(g, tank, rnd), weapon: 'babyMissile', fallback: true }; // no line of fire: a Moron's shot
|
|
101
|
-
return { angle: best.angle, power: best.power, weapon: pickWeapon(tank, target, { thrifty: best.miss > 4 }) };
|
|
143
|
+
return aimed(g, tank, target, { angle: best.angle, power: best.power, weapon: pickWeapon(tank, target, { thrifty: best.miss > 4 }) }, AIM.shooter, rnd);
|
|
102
144
|
}
|
|
103
145
|
|
|
104
146
|
/** The Poolshark: a Shooter, unless the walls rebound -- then it looks for the bank shot as well. */
|
|
@@ -111,7 +153,7 @@ export function poolshark(g, tank, rnd = g.rnd) {
|
|
|
111
153
|
const bank = solve(g, tank, target, [110, 125, 140, 155], 25);
|
|
112
154
|
const best = [direct, bank].filter(Boolean).sort((a, b) => a.miss - b.miss)[0];
|
|
113
155
|
if (!best || best.miss > 12) return { ...moron(g, tank, rnd), weapon: 'babyMissile', fallback: true };
|
|
114
|
-
return { angle: best.angle, power: best.power, weapon: pickWeapon(tank, target, { thrifty: best.miss > 4 }) };
|
|
156
|
+
return aimed(g, tank, target, { angle: best.angle, power: best.power, weapon: pickWeapon(tank, target, { thrifty: best.miss > 4 }) }, AIM.poolshark, rnd);
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
/**
|
|
@@ -147,7 +189,7 @@ export function chooser(g, tank, rnd = g.rnd) {
|
|
|
147
189
|
const bank = (g.walls === 'rubber' || g.walls === 'spring') ? solve(g, tank, target, [115, 135, 155], 25) : null;
|
|
148
190
|
const best = [low, lob, bank].filter(Boolean).sort((a, b) => a.miss - b.miss)[0];
|
|
149
191
|
if (!best) return moron(g, tank, rnd);
|
|
150
|
-
return { angle: best.angle, power: best.power, weapon: pickWeapon(tank, target) };
|
|
192
|
+
return aimed(g, tank, target, { angle: best.angle, power: best.power, weapon: pickWeapon(tank, target) }, AIM.chooser, rnd);
|
|
151
193
|
}
|
|
152
194
|
|
|
153
195
|
/** The Spoiler: "taking into account the wind factor and gravity, they will get a perfect shot almost every time". */
|
|
@@ -156,9 +198,8 @@ export function spoiler(g, tank, rnd = g.rnd, target = nearest(g, tank)) {
|
|
|
156
198
|
const coarse = solve(g, tank, target, [15, 25, 35, 45, 55, 65, 75], 20);
|
|
157
199
|
const best = coarse ? refine(g, tank, target, coarse) : null;
|
|
158
200
|
if (!best) return moron(g, tank, rnd);
|
|
159
|
-
// "almost": a
|
|
160
|
-
|
|
161
|
-
return { angle: Math.max(0, Math.min(180, best.angle + wobble)), power: best.power, weapon: pickWeapon(tank, target) };
|
|
201
|
+
// "almost": a small error, drawn from the game's own stream, that tightens as it keeps firing at you
|
|
202
|
+
return aimed(g, tank, target, { angle: best.angle, power: best.power, weapon: pickWeapon(tank, target) }, AIM.spoiler, rnd);
|
|
162
203
|
}
|
|
163
204
|
|
|
164
205
|
/** The Cyborg: a Spoiler that "attacks tanks who are weakened, winning, or have attacked them". */
|
|
@@ -14,7 +14,7 @@ import { paintRipple, skyBrightness } from './scorchedwind.js';
|
|
|
14
14
|
import { makeFluid, stepFluid, setSolid, warmFluid, makeTracers, stepTracers, paintTracers } from './scorchedair.js';
|
|
15
15
|
import {
|
|
16
16
|
newGame, current, aim, fire, step, settled, nextRound, cycleWeapon, useItem, drive, landTiles, actorTiles, leader, buy,
|
|
17
|
-
trajectory, dirtAt, shellLook, simulateShot,
|
|
17
|
+
trajectory, dirtAt, shellLook, simulateShot, colourFor,
|
|
18
18
|
WEAPONS, WEAPON_ORDER, ITEMS, COLS, ROWS, TANK_W,
|
|
19
19
|
} from './scorched.js';
|
|
20
20
|
import { SHOP } from './scorchedshop.js';
|
|
@@ -605,11 +605,21 @@ function drawTanks() {
|
|
|
605
605
|
if (!box) return;
|
|
606
606
|
const html = g ? g.tanks.map((t) => {
|
|
607
607
|
const cur = g.phase !== 'over' && current(g) === t;
|
|
608
|
-
return `<div class="sytank${t.alive ? '' : ' dead'}${cur ? ' now' : ''}"><i class="sydot
|
|
608
|
+
return `<div class="sytank${t.alive ? '' : ' dead'}${cur ? ' now' : ''}"><i class="sydot" data-tank="${t.id}"></i><b>${t.name}</b>`
|
|
609
609
|
+ `<meter min="0" max="100" low="34" high="67" optimum="100" value="${t.health}" title="${t.health} health"></meter>`
|
|
610
610
|
+ `<span>${t.alive ? `${t.health}${t.shield ? '🛡' : ''}` : '☠'}</span><small>${t.score} pts · ${t.kills} kills · ${money(t.cash)}</small></div>`;
|
|
611
611
|
}).join('') : '';
|
|
612
612
|
setHtml(box, html, 'tanks');
|
|
613
|
+
// THE DOT IS THE TANK'S OWN COLOUR (operator, 2026-09-17, with a capture: the red Spoiler listed
|
|
614
|
+
// with a blue dot). The CSP forbids a style attribute in markup, so the colour was a class per
|
|
615
|
+
// SEAT, which kept painting the seat's old colour once colours followed the personality. The
|
|
616
|
+
// CSSOM is allowed, and it is the tank's colour whatever seat it sits in.
|
|
617
|
+
if (g && box.querySelectorAll) {
|
|
618
|
+
for (const dot of box.querySelectorAll('.sydot[data-tank]')) {
|
|
619
|
+
const colour = g.tanks[Number(dot.dataset.tank)]?.colour;
|
|
620
|
+
if (colour && dot.dataset.colour !== colour) { dot.style?.setProperty?.('background-color', colour); dot.dataset.colour = colour; }
|
|
621
|
+
}
|
|
622
|
+
}
|
|
613
623
|
}
|
|
614
624
|
|
|
615
625
|
function drawScores(highlightAt = null) {
|
|
@@ -902,8 +912,14 @@ function frame(t) {
|
|
|
902
912
|
if (g.phase !== 'over') G.raf = requestAnimationFrame(frame);
|
|
903
913
|
}
|
|
904
914
|
|
|
905
|
-
// THE OPPONENTS (M3): a mix
|
|
906
|
-
//
|
|
915
|
+
// THE OPPONENTS (M3): a mix of the manual's personalities, or every seat the one kind the setting
|
|
916
|
+
// names. Named for what they are, the manual's way.
|
|
917
|
+
//
|
|
918
|
+
// THE MIX IS SHUFFLED EACH GAME (operator, 2026-09-17: "the enemies I play against in Scorched Yard
|
|
919
|
+
// are always the same and never randomized"). It used to fill the seats from this list in order, so
|
|
920
|
+
// the shipped two opponents were a Shooter and a Tosser in every game ever played. Now each new game
|
|
921
|
+
// deals the six in a fresh order: the first seats get six different kinds, and a table bigger than
|
|
922
|
+
// the pool deals again from a new shuffle.
|
|
907
923
|
const MIX = ['shooter', 'tosser', 'chooser', 'spoiler', 'cyborg', 'poolshark'];
|
|
908
924
|
const NAMES = { moron: 'Moron', shooter: 'Shooter', poolshark: 'Poolshark', tosser: 'Tosser', chooser: 'Chooser', spoiler: 'Spoiler', cyborg: 'Cyborg', unknown: 'Unknown' };
|
|
909
925
|
/**
|
|
@@ -911,14 +927,21 @@ const NAMES = { moron: 'Moron', shooter: 'Shooter', poolshark: 'Poolshark', toss
|
|
|
911
927
|
* taken by another computer player, so the war plays itself on a wall. Everything else is the
|
|
912
928
|
* same game: the same shop, the same rounds, the same rules.
|
|
913
929
|
*/
|
|
914
|
-
export function
|
|
915
|
-
const
|
|
930
|
+
export function shuffled(list, rnd = Math.random) {
|
|
931
|
+
const a = list.slice();
|
|
932
|
+
for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
|
|
933
|
+
return a;
|
|
934
|
+
}
|
|
935
|
+
export function players(t = scorchedOptions(loadSettings()), rnd = Math.random) {
|
|
936
|
+
const list = t.demo ? [] : [{ name: 'You', kind: 'human', colour: colourFor('human') }];
|
|
916
937
|
const seen = {};
|
|
917
938
|
const seats = t.opponents + (t.demo ? 1 : 0);
|
|
939
|
+
let deck = [];
|
|
918
940
|
for (let i = 0; i < seats; i++) {
|
|
919
|
-
|
|
941
|
+
if (t.opponentKind === 'mix' && deck.length === 0) deck = shuffled(MIX, rnd);
|
|
942
|
+
const kind = t.opponentKind === 'mix' ? deck.shift() : t.opponentKind;
|
|
920
943
|
seen[kind] = (seen[kind] ?? 0) + 1;
|
|
921
|
-
list.push({ name: seen[kind] > 1 ? `${NAMES[kind] ?? kind} ${seen[kind]}` : (NAMES[kind] ?? kind), kind });
|
|
944
|
+
list.push({ name: seen[kind] > 1 ? `${NAMES[kind] ?? kind} ${seen[kind]}` : (NAMES[kind] ?? kind), kind, colour: colourFor(kind, seen[kind]) });
|
|
922
945
|
}
|
|
923
946
|
return list;
|
|
924
947
|
}
|
package/public/js/settings.js
CHANGED
|
@@ -315,7 +315,7 @@ export const DEFAULTS = Object.freeze({
|
|
|
315
315
|
gridColour: '#2a5a8f',
|
|
316
316
|
gridBrightness: 1,
|
|
317
317
|
opponents: 2, // computer players against the one human; up to seven, for the original's eight seats
|
|
318
|
-
opponentKind: 'mix', // 'mix' (Shooter, Tosser, Chooser, Spoiler, Cyborg
|
|
318
|
+
opponentKind: 'mix', // 'mix' (Shooter, Tosser, Chooser, Spoiler, Cyborg and Poolshark, shuffled each game) or one of the manual's eight
|
|
319
319
|
rounds: 5,
|
|
320
320
|
walls: 'none', // the manual's default: 'none' | 'concrete' | 'padded' | 'rubber' | 'spring' | 'wrap'
|
|
321
321
|
wind: 'round', // 'round' (one wind a round, the shipped mode) | 'turn' | 'shot' | 'none'
|
|
@@ -705,7 +705,7 @@ const PANEL_GROUPS = Object.freeze([
|
|
|
705
705
|
Object.freeze({ key: 'gridBrightness', label: 'Grid intensity', kind: 'range', min: 0, max: 2, step: 0.05, hint: 'How strongly the grid shows; 0 hides it' }),
|
|
706
706
|
Object.freeze({ key: 'opponents', label: 'Computer players', kind: 'range', min: 1, max: 7, step: 1, hint: 'How many tanks the computer fields against you. Two is the shipped game; seven fills the original\u2019s eight seats' }),
|
|
707
707
|
Object.freeze({
|
|
708
|
-
key: 'opponentKind', label: 'Their kind', kind: 'choice', hint: 'The manual\u2019s personalities: a mix
|
|
708
|
+
key: 'opponentKind', label: 'Their kind', kind: 'choice', hint: 'The manual\u2019s personalities: a mix dealt in a new order every game, or every seat the one you name. Moron fires at random; Shooter takes straight shots; Poolshark banks off rubber walls; Tosser lobs and corrects; Chooser picks its method; Spoiler nearly never misses; Cyborg is a Spoiler with a grudge; Unknown is one of them, drawn each round',
|
|
709
709
|
options: Object.freeze([['mix', 'A mix'], ['moron', 'Morons'], ['shooter', 'Shooters'], ['poolshark', 'Poolsharks'], ['tosser', 'Tossers'], ['chooser', 'Choosers'], ['spoiler', 'Spoilers'], ['cyborg', 'Cyborgs'], ['unknown', 'Unknowns']]),
|
|
710
710
|
}),
|
|
711
711
|
Object.freeze({ key: 'rounds', label: 'Rounds', kind: 'range', min: 1, max: 10, step: 1, hint: 'A game is this many rounds; the highest score at the end wins' }),
|
package/scripts/shots.mjs
CHANGED
|
@@ -243,6 +243,12 @@ if (doing('scorched')) {
|
|
|
243
243
|
await metrics(1000);
|
|
244
244
|
await evl(`document.getElementById('syResume')?.click()`);
|
|
245
245
|
await sleep(1500);
|
|
246
|
+
// A DAYTIME ROUND (2026-09-17). Each round draws its sky hour from the seed (scorchedyard.js
|
|
247
|
+
// ROUND_HOURS[(seed + round*7) % 8]), so a shot taken at whatever the seed gave was as likely
|
|
248
|
+
// night as day; the Earth sky's sun, clouds and the wind's flow lines read best at 15:00 (index 3).
|
|
249
|
+
// The land is already generated, so only the sky's hour follows the new seed.
|
|
250
|
+
await evl(`import('/js/scorchedyard.js').then((m) => { const g = m.currentGame(); if (g) g.seed = g.seed - ((g.seed + g.round * 7) % 8 + 8) % 8 + 3; })`);
|
|
251
|
+
await sleep(2500);
|
|
246
252
|
await evl(`import('/js/scorchedyard.js').then(async (m) => { const g = m.currentGame(); const t = g.tanks[0]; const ai = await import('/js/scorchedai.js'); const target = ai.nearest(g, t); t.inventory.nuke = 1; t.weapon = 'nuke'; const s = ai.spoiler(g, t, () => 0.9, target); t.angle = s.angle; t.power = s.power; })`);
|
|
247
253
|
await sleep(300);
|
|
248
254
|
await evl(`document.getElementById('syFire')?.click()`);
|