Spaces:
Paused
Difficulty levels: fold AI speed + tactics into Easy / Medium / Hard
Browse filesPlaytest feedback: the Tactical AI reads as very smart, so surface it through a
single, standard difficulty pick instead of two separate expert dials. The old
"AI speed" slider and "AI tactics" toggle are replaced by one Difficulty selector:
Easy β 20 APM, no micro (slow, holds formation)
Medium β 65 APM, no micro (a fair fight; the default)
Hard β 140 APM, Tactical (fast; focus-fires, kites, scouts with a Ranger)
Each level maps to {aiApm, aiMicro}, which flow through unchanged β so Hard is the
tactical opponent, Easy/Medium are approachable, and the setup is simpler. Per-
world opponent doctrine (Rusher/Economist/Balanced) is unchanged and orthogonal.
UI-only; the engine, determinism, and the resolve guarantee are untouched (279
tests pass). Browser-verified: the selector shows Easy/Medium/Hard with Medium
active, and starting on Hard boots clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEQUVetLk31Jj7niDQAsXL
|
@@ -136,18 +136,20 @@ const RESOURCE_OPTIONS = [
|
|
| 136 |
{ label: "Normal", mult: 1.0, note: "balanced" },
|
| 137 |
{ label: "Abundant", mult: 1.5, note: "rich deposits" },
|
| 138 |
];
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
];
|
| 143 |
-
const
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
return "Relentless";
|
| 150 |
-
}
|
| 151 |
|
| 152 |
// A one-of-N pick rendered as a row of buttons; clicking one selects it and
|
| 153 |
// stores its value via onPick.
|
|
@@ -173,25 +175,17 @@ function renderSetupPanel() {
|
|
| 173 |
const panel = document.createElement("div");
|
| 174 |
panel.className = "setup";
|
| 175 |
|
| 176 |
-
const
|
| 177 |
-
|
| 178 |
-
const
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
slider.value = String(setup.aiApm); slider.className = "apm-slider";
|
| 184 |
-
const apmValue = document.createElement("span");
|
| 185 |
-
apmValue.className = "setup-value";
|
| 186 |
-
const showApm = () => { apmValue.textContent = `${setup.aiApm} APM Β· ${apmDescriptor(setup.aiApm)}`; };
|
| 187 |
-
showApm();
|
| 188 |
-
slider.addEventListener("input", () => { setup.aiApm = Number(slider.value); showApm(); });
|
| 189 |
-
apmRow.append(apmLabel, slider, apmValue);
|
| 190 |
-
panel.appendChild(apmRow);
|
| 191 |
|
| 192 |
const hint = document.createElement("p");
|
| 193 |
hint.className = "setup-hint";
|
| 194 |
-
hint.textContent = "
|
| 195 |
panel.appendChild(hint);
|
| 196 |
|
| 197 |
const sizeRow = document.createElement("div");
|
|
@@ -210,16 +204,6 @@ function renderSetupPanel() {
|
|
| 210 |
resRow.append(resLabel, optionGroup(setup.resourceMult, RESOURCE_OPTIONS, m => { setup.resourceMult = m; }));
|
| 211 |
panel.appendChild(resRow);
|
| 212 |
|
| 213 |
-
// AI tactics β orthogonal to speed (APM): whether the opponent micros its army
|
| 214 |
-
// (focus-fire, kiting) like a skilled player, or just holds formation.
|
| 215 |
-
const tacRow = document.createElement("div");
|
| 216 |
-
tacRow.className = "setup-row";
|
| 217 |
-
const tacLabel = document.createElement("span");
|
| 218 |
-
tacLabel.className = "setup-label";
|
| 219 |
-
tacLabel.textContent = "AI tactics";
|
| 220 |
-
tacRow.append(tacLabel, optionGroup(setup.aiMicro, TACTICS_OPTIONS, v => { setup.aiMicro = v; }));
|
| 221 |
-
panel.appendChild(tacRow);
|
| 222 |
-
|
| 223 |
// Optional seed: leave blank for a fresh random map, or enter a seed (shown on
|
| 224 |
// the seed chip / game-over screen) to replay the exact same world.
|
| 225 |
const seedRow = document.createElement("div");
|
|
@@ -292,8 +276,9 @@ function startGame(planetId) {
|
|
| 292 |
// UI layer (Math.random is fine here β it's not the sim); everything downstream
|
| 293 |
// flows from the seeded mulberry32, so same seed β same world.
|
| 294 |
const seed = (setup.seed != null ? setup.seed : Math.floor(Math.random() * 0x100000000)) >>> 0;
|
|
|
|
| 295 |
state = createGameState({ planetId, seed, rng: mulberry32(seed),
|
| 296 |
-
aiApm:
|
| 297 |
showSeedChip(seed);
|
| 298 |
showObjectives();
|
| 299 |
input = attachInput(canvas, state, () => renderHUD());
|
|
|
|
| 136 |
{ label: "Normal", mult: 1.0, note: "balanced" },
|
| 137 |
{ label: "Abundant", mult: 1.5, note: "rich deposits" },
|
| 138 |
];
|
| 139 |
+
// Difficulty bundles the two dials β how FAST the opponent acts (aiApm) and
|
| 140 |
+
// whether it MICROS its army (aiMicro: focus-fire, kiting, Ranger scouting) β into
|
| 141 |
+
// one Easy/Medium/Hard pick. Hard is the tactical opponent.
|
| 142 |
+
const DIFFICULTY_OPTIONS = [
|
| 143 |
+
{ label: "Easy", mult: "easy", note: "slow Β· no micro" },
|
| 144 |
+
{ label: "Medium", mult: "medium", note: "a fair fight" },
|
| 145 |
+
{ label: "Hard", mult: "hard", note: "fast Β· focus-fire Β· kite" },
|
| 146 |
];
|
| 147 |
+
const DIFFICULTY = {
|
| 148 |
+
easy: { aiApm: 20, aiMicro: false },
|
| 149 |
+
medium: { aiApm: 65, aiMicro: false },
|
| 150 |
+
hard: { aiApm: 140, aiMicro: true },
|
| 151 |
+
};
|
| 152 |
+
const setup = { difficulty: "medium", sizeMult: 1, resourceMult: 1, seed: null };
|
|
|
|
|
|
|
| 153 |
|
| 154 |
// A one-of-N pick rendered as a row of buttons; clicking one selects it and
|
| 155 |
// stores its value via onPick.
|
|
|
|
| 175 |
const panel = document.createElement("div");
|
| 176 |
panel.className = "setup";
|
| 177 |
|
| 178 |
+
const diffRow = document.createElement("div");
|
| 179 |
+
diffRow.className = "setup-row";
|
| 180 |
+
const diffLabel = document.createElement("span");
|
| 181 |
+
diffLabel.className = "setup-label";
|
| 182 |
+
diffLabel.textContent = "Difficulty";
|
| 183 |
+
diffRow.append(diffLabel, optionGroup(setup.difficulty, DIFFICULTY_OPTIONS, key => { setup.difficulty = key; }));
|
| 184 |
+
panel.appendChild(diffRow);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
const hint = document.createElement("p");
|
| 187 |
hint.className = "setup-hint";
|
| 188 |
+
hint.textContent = "Easy is slow and holds formation; Medium fights at a fair pace; Hard is fast and micros its army β it focus-fires, kites, and scouts with a Ranger.";
|
| 189 |
panel.appendChild(hint);
|
| 190 |
|
| 191 |
const sizeRow = document.createElement("div");
|
|
|
|
| 204 |
resRow.append(resLabel, optionGroup(setup.resourceMult, RESOURCE_OPTIONS, m => { setup.resourceMult = m; }));
|
| 205 |
panel.appendChild(resRow);
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
// Optional seed: leave blank for a fresh random map, or enter a seed (shown on
|
| 208 |
// the seed chip / game-over screen) to replay the exact same world.
|
| 209 |
const seedRow = document.createElement("div");
|
|
|
|
| 276 |
// UI layer (Math.random is fine here β it's not the sim); everything downstream
|
| 277 |
// flows from the seeded mulberry32, so same seed β same world.
|
| 278 |
const seed = (setup.seed != null ? setup.seed : Math.floor(Math.random() * 0x100000000)) >>> 0;
|
| 279 |
+
const diff = DIFFICULTY[setup.difficulty] || DIFFICULTY.medium;
|
| 280 |
state = createGameState({ planetId, seed, rng: mulberry32(seed),
|
| 281 |
+
aiApm: diff.aiApm, aiMicro: diff.aiMicro, sizeMult: setup.sizeMult, resourceMult: setup.resourceMult });
|
| 282 |
showSeedChip(seed);
|
| 283 |
showObjectives();
|
| 284 |
input = attachInput(canvas, state, () => renderHUD());
|