Claude Claude Fable 5 commited on
Commit
72fd796
Β·
unverified Β·
1 Parent(s): fea25dd

Difficulty levels: fold AI speed + tactics into Easy / Medium / Hard

Browse files

Playtest 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

Files changed (1) hide show
  1. main.js +23 -38
main.js CHANGED
@@ -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
- const TACTICS_OPTIONS = [
140
- { label: "Standard", mult: false, note: "holds formation" },
141
- { label: "Tactical", mult: true, note: "focus-fires Β· kites" },
 
 
 
 
142
  ];
143
- const setup = { aiApm: 60, sizeMult: 1, resourceMult: 1, seed: null, aiMicro: false };
144
-
145
- function apmDescriptor(apm) {
146
- if (apm <= 20) return "Sluggish";
147
- if (apm <= 55) return "Casual";
148
- if (apm <= 100) return "Sharp";
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 apmRow = document.createElement("div");
177
- apmRow.className = "setup-row";
178
- const apmLabel = document.createElement("span");
179
- apmLabel.className = "setup-label";
180
- apmLabel.textContent = "AI speed";
181
- const slider = document.createElement("input");
182
- slider.type = "range"; slider.min = "1"; slider.max = "150"; slider.step = "1";
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 = "Actions per minute the opponent can take β€” a click, a selection, a command. 1 is a crawl; 150 is relentless.";
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: setup.aiApm, aiMicro: setup.aiMicro, sizeMult: setup.sizeMult, resourceMult: setup.resourceMult });
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());