New claims get camp names from their seed, renameable from the Claims tab; add t30 (t3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 18:38:26 -04:00
co-authored by Claude Opus 5.5
parent c302b0cf42
commit 4d5fca6dae
4 changed files with 58 additions and 8 deletions
+2 -2
View File
@@ -381,9 +381,9 @@ Saves in IndexedDB, world files (export / import) and the blueprint library.
- functions: newWorldId, worldWrite, listWorlds, putWorldEntry, worldNewest, deleteWorld, cleanWorldName, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, packBuffers, unpackBuffers, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode - functions: newWorldId, worldWrite, listWorlds, putWorldEntry, worldNewest, deleteWorld, cleanWorldName, renameWorld, migrateWorlds, saveWorld, newestSave, b64FromBytes, bytesFromB64, gz, packBuffers, unpackBuffers, exportWorld, importWorldFile, libLoad, libSave, bpEncode, bpValid, bpDecode
- variables: DB, worldIdTime, worldIdCounter, worldWrites - variables: DB, worldIdTime, worldIdCounter, worldWrites
### src/ui/claims.js (115 lines) ### src/ui/claims.js (141 lines)
Shared player state; the world save remains a complete standalone fallback. Shared player state; the world save remains a complete standalone fallback.
- functions: careerState, validCareer, applyCareerState, loadCareer, openNewClaim, travel, renderClaimsTab - functions: careerState, validCareer, applyCareerState, loadCareer, claimName, openNewClaim, travel, renderClaimsTab
- variables: career, claimBusy - variables: career, claimBusy
### src/ui/session.js (133 lines) ### src/ui/session.js (133 lines)
+8
View File
@@ -4,6 +4,14 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-24: t3 follow-up: claim names; t30 added (Claude Code, Codex)
- New claims get a camp name from their seed ("Cinder Gulch"; `claimName` in `src/ui/claims.js`,
skipping names already in the world list) instead of "Claim N". The Claims tab shows "Claim N"
beside the name and has a Rename button on every row (reuses `renameWorld`).
- Owner's request added to Todo as t30: progress meters while generating, loading and saving.
- Tests: play.py claims checks extended; t_smooth half-resolution still fails under load.
## 2026-10-24: t3 fix: deep ore burned and melted after a reload (Claude Code) ## 2026-10-24: t3 fix: deep ore burned and melted after a reload (Claude Code)
- Owner: a Huge world ran at about 10 fps after travelling between claims, even after a refresh. - Owner: a Huge world ran at about 10 fps after travelling between claims, even after a refresh.
+29 -3
View File
@@ -56,6 +56,21 @@ async function loadCareer() {
GAME.careerId = career.id; GAME.careerId = career.id;
} catch (e) { career = null; toast('Career could not be loaded. Using this world’s own money and bag.'); } } catch (e) { career = null; toast('Career could not be loaded. Using this world’s own money and bag.'); }
} }
// Seeded camp names stay the same unless an existing world already uses one.
function claimName(seed, taken = []) {
const first = ['Copper', 'Iron', 'Red', 'Black', 'Grey', 'Silver', 'Deep', 'Cinder', 'Ash', 'Granite', 'Basalt', 'Slate', 'Rust', 'Ember', 'Flint', 'Cold', 'Hollow', 'Lone', 'Twin', 'Broken'];
const last = ['Hollow', 'Ridge', 'Flats', 'Gulch', 'Mesa', 'Seam', 'Reach', 'Bluff', 'Basin', 'Crag', 'Shelf', 'Draw', 'Cut', 'Rise', 'Pit', 'Vein', 'Wash', 'Fork', 'Run', 'Bench'];
const count = first.length * last.length, start = Math.floor(mulberry32(seed)() * count);
for (let offset = 0; offset < count; offset++) {
const i = (start + offset) % count, a = first[Math.floor(i / last.length)], b = last[i % last.length];
const name = a + ' ' + b;
if (a !== b && !taken.includes(name)) return name;
}
// every camp name is taken: fall back to numbered names
let n = 1;
while (taken.includes('Mining Camp ' + n)) n++;
return 'Mining Camp ' + n;
}
async function openNewClaim() { async function openNewClaim() {
if (claimBusy || !career || debt > 0 || money < 5000) return false; if (claimBusy || !career || debt > 0 || money < 5000) return false;
claimBusy = true; const paused = GAME.paused; GAME.paused = true; claimBusy = true; const paused = GAME.paused; GAME.paused = true;
@@ -66,13 +81,14 @@ async function openNewClaim() {
const carried = careerState(), n = career.claimNo + 1, w = W, h = H; const carried = careerState(), n = career.claimNo + 1, w = W, h = H;
carried.money -= 5000; carried.money -= 5000;
let seed; do { seed = Math.floor(Math.random() * 900000) + 100000; } while (seed === worldSeed); let seed; do { seed = Math.floor(Math.random() * 900000) + 100000; } while (seed === worldSeed);
await setupNewWorld(seed, w, h, `Claim ${n}`, carried); const name = claimName(seed, (await listWorlds()).map(w => w.name));
await setupNewWorld(seed, w, h, name, carried);
debt = CLAIM_DEBT * 2 ** (n - 1); debt = CLAIM_DEBT * 2 ** (n - 1);
career = { ...career, claims: [...career.claims, GAME.worldId], claimNo: n }; career = { ...career, claims: [...career.claims, GAME.worldId], claimNo: n };
GAME.claimNo = n; GAME.claimNo = n;
if (!await saveWorld('auto', true)) throw new Error('Save failed'); if (!await saveWorld('auto', true)) throw new Error('Save failed');
showLoading(false); closeAllModals(); enterGame(); showLoading(false); closeAllModals(); enterGame();
showCard({ kind: 'New claim', title: `Claim ${n}`, body: [`You owe ${fmtMoney(debt)} on this claim. Your money, bag, upgrades and what you’ve learned came along. Buildings stayed on the old claim.`], hold: 1.8 }); showCard({ kind: 'New claim', title: name, body: [`Claim ${n}. You owe ${fmtMoney(debt)} on this claim. Your money, bag, upgrades and what you’ve learned came along. Buildings stayed on the old claim.`], hold: 1.8 });
return true; return true;
} catch (e) { } catch (e) {
if (previous) { if (previous) {
@@ -101,9 +117,19 @@ async function renderClaimsTab(body) {
try { try {
const list = (await listWorlds()).filter(w => career && w.career === GAME.careerId && career.claims.includes(w.id)); const list = (await listWorlds()).filter(w => career && w.career === GAME.careerId && career.claims.includes(w.id));
if (nbTab !== 'Claims') return; if (nbTab !== 'Claims') return;
body.innerHTML = list.map(w => `<div class="world-row"><div class="world-info"><strong>${esc(w.name)}${w.id === GAME.worldId ? ' · here' : ''}</strong><div class="note">${esc(w.w)}×${esc(w.h)} grains · ${(w.id === GAME.worldId ? debt : w.debt) <= 0 ? 'paid off ✓' : fmtMoney(w.id === GAME.worldId ? debt : w.debt || 0) + ' owed'} · Last played: ${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Just opened'}</div></div>${w.id === GAME.worldId ? '' : `<button class="btn" data-travel="${esc(w.id)}">Travel</button>`}</div>`).join('') + body.innerHTML = list.map(w => `<div class="world-row"><div class="world-info"><strong>${esc(w.name)}${w.id === GAME.worldId ? ' · here' : ''}</strong> <small class="note">Claim ${esc(w.claimNo)}</small><div class="note">${esc(w.w)}×${esc(w.h)} grains · ${(w.id === GAME.worldId ? debt : w.debt) <= 0 ? 'paid off ✓' : fmtMoney(w.id === GAME.worldId ? debt : w.debt || 0) + ' owed'} · Last played: ${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Just opened'}</div></div><button class="btn" data-rename="${esc(w.id)}">Rename</button>${w.id === GAME.worldId ? '' : `<button class="btn" data-travel="${esc(w.id)}">Travel</button>`}</div>`).join('') +
(debt <= 0 ? `<button class="btn primary" id="claim-open" ${money < 5000 || !career ? 'disabled' : ''}>Open a new claim: $5,000 down, then ${fmtMoney(CLAIM_DEBT * 2 ** (career ? career.claimNo : 1))} owed</button>${money < 5000 ? '<p class=note>You need $5,000 for the down payment.</p>' : ''}` : '<p class=note>A new claim opens once this one is paid off.</p>') + (debt <= 0 ? `<button class="btn primary" id="claim-open" ${money < 5000 || !career ? 'disabled' : ''}>Open a new claim: $5,000 down, then ${fmtMoney(CLAIM_DEBT * 2 ** (career ? career.claimNo : 1))} owed</button>${money < 5000 ? '<p class=note>You need $5,000 for the down payment.</p>' : ''}` : '<p class=note>A new claim opens once this one is paid off.</p>') +
'<p class=note>Machines on other claims wait until you come back.</p>'; '<p class=note>Machines on other claims wait until you come back.</p>';
body.querySelectorAll('[data-rename]').forEach(b => b.onclick = () => {
const w = list.find(w => w.id === b.dataset.rename);
dialog('Rename claim', `<input class="text" type="text" id="claim-name" aria-label="Claim name" maxlength="40" value="${esc(w.name)}">`, [
{ label: 'Cancel', cls: 'ghost' }, { label: 'Save', cls: 'primary', fn: async () => {
const name = cleanWorldName($('claim-name').value, w.name);
try { await renameWorld(w.id, name); closeModal('m-dialog'); renderClaimsTab(body); }
catch (e) { toast('Could not rename this claim. Try again.'); }
} },
]);
});
const button = $('claim-open'); const button = $('claim-open');
if (button) button.onclick = () => dialog('Open a new claim?', `<p class="note">Pay $5,000 down and owe ${fmtMoney(CLAIM_DEBT * 2 ** career.claimNo)} on fresh land. Your buildings stay here.</p>`, [ if (button) button.onclick = () => dialog('Open a new claim?', `<p class="note">Pay $5,000 down and owe ${fmtMoney(CLAIM_DEBT * 2 ** career.claimNo)} on fresh land. Your buildings stay here.</p>`, [
{ label: 'Cancel', cls: 'ghost' }, { label: 'Open claim', cls: 'primary', fn: () => { closeModal('m-dialog'); openNewClaim(); } }, { label: 'Cancel', cls: 'ghost' }, { label: 'Open claim', cls: 'primary', fn: () => { closeModal('m-dialog'); openNewClaim(); } },
+19 -3
View File
@@ -850,11 +850,27 @@ with sync_playwright() as p:
ev('GAME.paused = true') ev('GAME.paused = true')
check('new claim carries money, bag, upgrades and knowledge', ev("money === 5000 && earned === 12345 && JSON.stringify(Array.from(bag)) === JSON.stringify(claimTest.bag) && player.bitTier === 2 && seen[GOLD] === 1 && ledger.dig === 42 && stats.dug === 321")) check('new claim carries money, bag, upgrades and knowledge', ev("money === 5000 && earned === 12345 && JSON.stringify(Array.from(bag)) === JSON.stringify(claimTest.bag) && player.bitTier === 2 && seen[GOLD] === 1 && ledger.dig === 42 && stats.dug === 321"))
check('claim 2 has doubled debt, a new seed and the same dimensions', ev('debt === 40000 && worldSeed !== claimTest.seed && W === 512 && H === 384')) check('claim 2 has doubled debt, a new seed and the same dimensions', ev('debt === 40000 && worldSeed !== claimTest.seed && W === 512 && H === 384'))
check('claim 2 is stored with its career and name', ev("async () => { const c = await DB.get('career:' + GAME.careerId); const w = (await listWorlds()).find(w => w.id === GAME.worldId); return c.claims.length === 2 && c.claimNo === 2 && w.name === 'Claim 2' && w.career === c.id && w.claimNo === 2; }")) check('claim 2 is stored with its career and name', ev("async () => { const c = await DB.get('career:' + GAME.careerId); const w = (await listWorlds()).find(w => w.id === GAME.worldId); return c.claims.length === 2 && c.claimNo === 2 && w.name !== 'Claim 2' && /^[A-Z][a-z]+ [A-Z][a-z]+$/.test(w.name) && w.name === GAME.worldName && w.career === c.id && w.claimNo === 2; }"))
check('new claim shows its arrival card', ev("$('cards').textContent.includes('New claim') && $('cards').textContent.includes('$40,000')")) check('new claim shows its arrival card', ev("$('cards').textContent.includes(GAME.worldName) && $('cards').textContent.includes('Claim 2.') && $('cards').textContent.includes('$40,000')"))
ev("openNotebook('Claims')") ev("openNotebook('Claims')")
cp.wait_for_function("$('nb-body').querySelectorAll('[data-travel]').length === 1") cp.wait_for_function("$('nb-body').querySelectorAll('[data-travel]').length === 1")
check('Claims lists both claims with here, size and paid off', ev("$('nb-body').textContent.includes('Claim 2 · here') && $('nb-body').textContent.includes('512×384') && $('nb-body').textContent.includes('paid off ✓')")) check('Claims lists both claims with here, size and paid off', ev("$('nb-body').textContent.includes(GAME.worldName + ' · here') && $('nb-body').textContent.includes('Claim 2') && $('nb-body').textContent.includes('512×384') && $('nb-body').textContent.includes('paid off ✓')"))
check('camp names are stable and varied', ev("claimName(424242) === claimName(424242) && new Set(Array.from({length: 20}, (_, i) => claimName(i))).size >= 16"))
check('camp names skip taken names and repeated words', ev("""() => {
const names = Array.from({length: 1000}, (_, i) => claimName(i));
return names.every(n => n.split(' ')[0] !== n.split(' ')[1]) && claimName(42, [claimName(42)]) !== claimName(42);
}"""))
check('every claim has a rename button', cp.locator('[data-rename]').count() == 2)
cp.locator('[data-rename="' + ev('GAME.worldId') + '"]').click()
check('rename is prefilled with the current name', cp.locator('#claim-name').input_value() == ev('GAME.worldName'))
cp.fill('#claim-name', 'Silver Camp')
cp.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
check('cancel keeps the claim name', ev("GAME.worldName !== 'Silver Camp'"))
cp.locator('[data-rename="' + ev('GAME.worldId') + '"]').click()
cp.fill('#claim-name', ' Silver Camp ')
cp.locator('#dlg-foot').get_by_role('button', name='Save', exact=True).click()
cp.wait_for_function("$('nb-body').textContent.includes('Silver Camp · here')")
check('renaming the current claim updates storage and game', ev("async () => GAME.worldName === 'Silver Camp' && (await listWorlds()).find(w => w.id === GAME.worldId).name === 'Silver Camp'"))
cp.screenshot(path=str(out / '09-claims.png')) cp.screenshot(path=str(out / '09-claims.png'))
cp.click('[data-travel]'); cp.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click() cp.click('[data-travel]'); cp.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
check('travelling asks for confirmation', ev('GAME.worldId !== claimTest.id')) check('travelling asks for confirmation', ev('GAME.worldId !== claimTest.id'))