Dice button picks a random seed in the new-world dialog (t100)

This commit is contained in:
2026-10-29 22:14:40 -04:00
parent de6d2c1b33
commit 974e5a6272
9 changed files with 154 additions and 7 deletions
+6
View File
@@ -644,3 +644,9 @@ above, including:
requires fully explored space and prefers up on equal distances. A 256-point trail records requires fully explored space and prefers up on equal distances. A 256-point trail records
every four grains without per-tick allocations; loading rebuilds it from the bot position. every four grains without per-tick allocations; loading rebuilds it from the bot position.
Searches happen only on burial, capped at 440 nudge and 256 trail probes per escape episode. Searches happen only on burial, capped at 440 nudge and 256 trail probes per escape episode.
- **New-world seed controls (t100, 2026-10-29).** The owner asked for a dice icon beside
the seed and numeric validation matching the internal limit. Keep only digits and clamp
above 4294967295 with a nearby hint. Zero is a seed; empty means random on Generate.
The dice uses the same six-digit random rule as the initial field and refreshes the
automatic camp name while preserving a name the player typed.
+2 -1
View File
@@ -622,7 +622,7 @@ Shared world colours, minimap and full map.
Browser-only clocks; simulation code stays usable without a browser or a clock. Browser-only clocks; simulation code stays usable without a browser or a clock.
- variables: PERF_MARKS, PERF_FRAME, perfMarks, perfTimes, perfLast - variables: PERF_MARKS, PERF_FRAME, perfMarks, perfTimes, perfLast
### src/ui/loop.js (150 lines) ### src/ui/loop.js (170 lines)
The frame loop and boot. The frame loop and boot.
- functions: scanVisible, frame, boot - functions: scanVisible, frame, boot
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT - variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
@@ -678,6 +678,7 @@ The frame loop and boot.
- `test/t_render_dirty.js`: Render invalidation must not depend on whether a grain moved or the chunk stayed awake. - `test/t_render_dirty.js`: Render invalidation must not depend on whether a grain moved or the chunk stayed awake.
- `test/t_screw_heat.js`: t69: melt feedback, full-cost repair without disturbing the load, and repair undo. - `test/t_screw_heat.js`: t69: melt feedback, full-cost repair without disturbing the load, and repair undo.
- `test/t_screw_heat.py`: t69 only: both looks, persistent damage, hover, first message and real click/G repair. - `test/t_screw_heat.py`: t69 only: both looks, persistent damage, hover, first message and real click/G repair.
- `test/t_seed.py`: t100: new-world seed editing, random button and generation, in both looks.
- `test/t_silo_contents.js`: Stored snow keeps its identity and remains in the world's water budget. - `test/t_silo_contents.js`: Stored snow keeps its identity and remains in the world's water budget.
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly. - `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
- `test/t_slope_belt_art.js`: Drawing geometry against real parts, without changing transport. - `test/t_slope_belt_art.js`: Drawing geometry against real parts, without changing transport.
+4
View File
@@ -94,6 +94,10 @@ screenshots go to `shots/t72-{retrace,escape,open}.png`.
## Browser tests ## Browser tests
- `python3 test/t_seed.py` checks the new-world seed field: typed and pasted text,
real text dragging, clamping at 4294967295, zero and empty seeds, keyboard dice
activation, automatic/custom names and real generation with seed zero. Both-look
screenshots of the dice and limit hint go to `shots/t100-{pixel,smooth}.png`.
- They need the `.venv` from README.md → Develop; plain `python3` switches to it on its own. - They need the `.venv` from README.md → Develop; plain `python3` switches to it on its own.
- `test/play.py` (about 2.5 minutes) builds a world through the title screen and plays it with real - `test/play.py` (about 2.5 minutes) builds a world through the title screen and plays it with real
input. It checks the tools, building, copy/cut/paste, undo, the build menu, filters, pipes, input. It checks the tools, building, copy/cut/paste, undo, the build menu, filters, pipes,
+12
View File
@@ -4,6 +4,18 @@ 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-29: t100 new-world seed controls (Codex)
- Added a labeled dice button and shared seed parsing/random rule; ignore non-digits,
clamp above 4294967295 with a hint, preserve zero, and randomize empty on Generate.
- Files: src/shell.html, src/styles/style.css, src/ui/loop.js, test/t_seed.py,
test/run_all.py, docs/{DESIGN,TESTING,PROJECT_MAP}.md, tasks/t100* and this log.
- Ten seed checks failed against the unchanged build first. All 41 headless suites pass;
test/t_seed.py passes 21 checks (real clipboard/text drag and zero-world generation),
and pixel test/play.py shots/t100-play passes with no console errors. Browser runs
used host GPU access after sandbox WebGPU was unavailable. Inspected both-look
shots/t100-{pixel,smooth}.png and play/new-world.png. Map regenerated; nothing open.
## 2026-10-29: t87 reuse the near-miss view (Codex) ## 2026-10-29: t87 reuse the near-miss view (Codex)
- Reused one scratch view, its entry array and get/has methods in src/sim/joins.js; - Reused one scratch view, its entry array and get/has methods in src/sim/joins.js;
+8 -1
View File
@@ -56,7 +56,14 @@
<button class="btn" id="btn-new">New world <small>generate</small></button> <button class="btn" id="btn-new">New world <small>generate</small></button>
<div class="plate newworld" id="newworld" hidden> <div class="plate newworld" id="newworld" hidden>
<label>Name <input id="nw-name" type="text" maxlength="40" autocomplete="off" spellcheck="false"></label> <label>Name <input id="nw-name" type="text" maxlength="40" autocomplete="off" spellcheck="false"></label>
<label>Seed <input id="nw-seed" type="text" inputmode="numeric" autocomplete="off"></label> <div class="seed-field">
<label for="nw-seed">Seed</label>
<div class="seed-row">
<input id="nw-seed" type="text" inputmode="numeric" maxlength="10" autocomplete="off" aria-describedby="nw-seed-hint">
<button class="btn" id="btn-random-seed" type="button" aria-label="Random seed" title="Random seed">⚄</button>
</div>
<small id="nw-seed-hint" role="status" hidden>Maximum seed: 4294967295.</small>
</div>
<label>Size <label>Size
<select id="nw-size"> <select id="nw-size">
<option value="2048x1024">Small · 2048 × 1024</option> <option value="2048x1024">Small · 2048 × 1024</option>
+5
View File
@@ -301,6 +301,11 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
.newworld label { display: flex; flex-direction: column; gap: 4px; font-size: 0.86em; color: var(--ash); } .newworld label { display: flex; flex-direction: column; gap: 4px; font-size: 0.86em; color: var(--ash); }
.newworld select, .newworld input { background: var(--soot-0); border: 1px solid var(--soot-4); border-radius: 4px; padding: 6px 8px; color: var(--bone); } .newworld select, .newworld input { background: var(--soot-0); border: 1px solid var(--soot-4); border-radius: 4px; padding: 6px 8px; color: var(--bone); }
.newworld label:first-child { grid-column: 1 / -1; } .newworld label:first-child { grid-column: 1 / -1; }
.seed-field { display: flex; flex-direction: column; gap: 4px; color: var(--ash); font-size: 0.86em; }
.seed-field label { font-size: inherit; }
.seed-row { display: flex; gap: 4px; }
.seed-row input { min-width: 0; width: 100%; }
.seed-row .btn { padding: 2px 8px; font-size: 1.4em; }
.newworld .full { grid-column: 1 / -1; } .newworld .full { grid-column: 1 / -1; }
.tfoot { text-align: center; color: var(--ash-dim); font-size: 0.82em; font-family: var(--font-mono); } .tfoot { text-align: center; color: var(--ash-dim); font-size: 0.82em; font-family: var(--font-mono); }
#loading { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; background: rgba(11, 9, 8, 0.86); } #loading { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; background: rgba(11, 9, 8, 0.86); }
+24 -4
View File
@@ -113,10 +113,21 @@ function boot(hotData) {
initRender(canvas); initRender(canvas);
setupBuildBar(); setupBuildBar();
buildTitleCells(); drawTitle(); buildTitleCells(); drawTitle();
$('nw-seed').value = String(Math.floor(Math.random() * 900000) + 100000);
$('wallet').onclick = () => { playSfx('ui'); openNotebook('Market'); }; $('wallet').onclick = () => { playSfx('ui'); openNotebook('Market'); };
let autoName = '', worldNames = []; let autoName = '', worldNames = [];
const newWorldSeed = () => parseInt(String($('nw-seed').value).replace(/\D/g, ''), 10) || Math.floor(Math.random() * 1e6); const seedBox = $('nw-seed');
const newWorldSeed = (value = seedBox.value) => {
const digits = value.replace(/\D/g, '');
const clamped = Number(digits) > 4294967295;
$('nw-seed-hint').hidden = !clamped;
if (value === seedBox.value) {
const caret = seedBox.value.slice(0, seedBox.selectionStart).replace(/\D/g, '').length;
const clean = clamped ? '4294967295' : digits;
if (seedBox.value !== clean) { seedBox.value = clean; seedBox.setSelectionRange(caret, caret); }
}
return digits === '' ? Math.floor(Math.random() * 900000) + 100000 : Math.min(Number(digits), 4294967295);
};
seedBox.value = String(newWorldSeed(''));
$('btn-new').onclick = async () => { $('btn-new').onclick = async () => {
auInit(); playSfx('ui'); $('newworld').hidden = !$('newworld').hidden; auInit(); playSfx('ui'); $('newworld').hidden = !$('newworld').hidden;
if ($('newworld').hidden) return; if ($('newworld').hidden) return;
@@ -125,8 +136,17 @@ function boot(hotData) {
$('nw-name').value = autoName = claimName(newWorldSeed(), worldNames); $('nw-name').value = autoName = claimName(newWorldSeed(), worldNames);
$('nw-name').focus(); $('nw-name').select(); $('nw-name').focus(); $('nw-name').select();
}; };
$('nw-seed').oninput = () => { seedBox.oninput = () => {
if ($('nw-name').value === autoName) $('nw-name').value = autoName = claimName(newWorldSeed(), worldNames); const seed = newWorldSeed();
if ($('nw-name').value === autoName) $('nw-name').value = autoName = claimName(seed, worldNames);
};
$('btn-random-seed').onclick = () => { seedBox.value = String(newWorldSeed('')); seedBox.oninput(); };
// Read the whole transfer before maxlength can truncate its digits.
seedBox.onpaste = seedBox.ondrop = e => {
e.preventDefault();
const text = (e.clipboardData || e.dataTransfer).getData('text/plain');
seedBox.setRangeText(text, seedBox.selectionStart, seedBox.selectionEnd, 'end');
seedBox.oninput();
}; };
for (const id of ['nw-name', 'nw-seed']) $(id).onkeydown = e => { if (e.key === 'Enter') $('btn-generate').click(); }; for (const id of ['nw-name', 'nw-seed']) $(id).onkeydown = e => { if (e.key === 'Enter') $('btn-generate').click(); };
$('btn-generate').onclick = () => { $('btn-generate').onclick = () => {
+1 -1
View File
@@ -45,7 +45,7 @@ def main():
if browser: if browser:
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True) subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
# one at a time: two headless browsers at once can starve each other and flake # one at a time: two headless browsers at once can starve each other and flake
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_vacuum_collapse.py', 't_blow_collapse.py', 't_lakes.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']: for name in ['play.py', 't_seed.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_vacuum_collapse.py', 't_blow_collapse.py', 't_lakes.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
results.append(run_py(name)) results.append(run_py(name))
bad = 0 bad = 0
for name, code, out, secs in results: for name, code, out, secs in results:
+92
View File
@@ -0,0 +1,92 @@
"""t100: new-world seed editing, random button and generation, in both looks."""
import pathlib, sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
from use_venv import ensure_playwright; ensure_playwright()
from browser import launch, consume_gpu_option
consume_gpu_option()
root = pathlib.Path(__file__).resolve().parent.parent
out = root / 'shots'; out.mkdir(exist_ok=True)
fails = []
def check(name, ok):
print(('PASS ' if ok else 'FAIL ') + name)
if not ok: fails.append(name)
with launch(viewport={'width': 1280, 'height': 800}) as (_, pg, url):
errors = []
pg.on('pageerror', lambda e: errors.append(str(e)))
pg.goto(url)
pg.wait_for_function("!$('title').hidden && CAPS.ready")
pg.click('#btn-new')
seed = pg.locator('#nw-seed')
seed.fill('12'); seed.press('Home'); pg.keyboard.type('a!')
check('typed letters and symbols are ignored', seed.input_value() == '12')
pg.keyboard.type('3')
check('ignoring characters preserves the insertion point', seed.input_value() == '312')
# Real clipboard paste exercises maxlength before the input event.
pg.context.grant_permissions(['clipboard-read', 'clipboard-write'])
def paste(text):
seed.focus(); seed.press('ControlOrMeta+a')
pg.evaluate('text => navigator.clipboard.writeText(text)', text)
seed.press('ControlOrMeta+v')
paste('ab12 cd34!56 ef78')
check('paste keeps all digits even beyond ten source characters', seed.input_value() == '12345678')
paste('4294967296')
check('4294967296 clamps to max', seed.input_value() == '4294967295')
paste('99999999999')
check('eleven digits clamp to max', seed.input_value() == '4294967295')
check('maxlength fits ten digits', seed.get_attribute('maxlength') == '10')
check('clamp hint is shown by the box', pg.locator('#nw-seed-hint').count() == 1 and pg.is_visible('#nw-seed-hint'))
for look in ['pixel', 'smooth']:
pg.evaluate('look => SET.look = look', look)
pg.screenshot(path=str(out / f't100-{look}.png'))
# A drop with text transfer data uses the same editing path.
seed.fill('')
seed.evaluate("""e => {
const data = new DataTransfer(); data.setData('text/plain', 'ab4294967296!');
e.dispatchEvent(new DragEvent('drop', {bubbles:true, cancelable:true, dataTransfer:data}));
}""")
check('dropped text is filtered and clamped', seed.input_value() == '4294967295')
# Also exercise browser drag delivery rather than only a dispatched event.
pg.evaluate("""() => {
const source = document.createElement('div'); source.id = 'seed-drag';
source.textContent = 'Drag seed'; source.draggable = true;
source.style = 'position:fixed;z-index:99999;top:0;left:0;padding:20px;background:white';
source.ondragstart = e => e.dataTransfer.setData('text/plain', 'seed: 1234!');
document.body.append(source);
}""")
seed.fill(''); pg.locator('#seed-drag').drag_to(seed)
check('real text drag keeps only digits', seed.input_value() == '1234')
pg.locator('#seed-drag').evaluate('e => e.remove()')
seed.fill('0')
check('zero stays zero', seed.input_value() == '0')
# Record Generate's arguments without generating several large worlds.
pg.evaluate("""() => {
window.__start = startNewWorld;
startNewWorld = (...args) => { window.__generated = args; };
window.__random = Math.random; Math.random = () => 0.5;
}""")
seed.press('Enter')
check('Enter generates seed zero', pg.evaluate('__generated[0] === 0'))
seed.fill('4294967295'); pg.click('#btn-generate')
check('maximum seed generates without wrapping', pg.evaluate('__generated[0] === 4294967295'))
seed.evaluate("e => e.value = '99999999999'"); pg.click('#btn-generate')
check('Generate applies the same clamp even without an input event', pg.evaluate('__generated[0] === 4294967295'))
seed.fill(''); pg.click('#btn-generate')
check('empty generates a random six-digit seed', pg.evaluate('__generated[0] === 550000'))
dice = pg.get_by_role('button', name='Random seed', exact=True)
check('dice is a labeled button with tooltip', dice.count() == 1 and dice.get_attribute('title') == 'Random seed')
if dice.count():
seed.fill('42'); dice.focus(); pg.keyboard.press('Enter')
check('keyboard dice uses the same random rule', seed.input_value() == '550000')
check('dice updates the automatic name', pg.evaluate("$('nw-name').value === claimName(550000, [])"))
pg.fill('#nw-name', 'My camp'); pg.evaluate('Math.random = () => 0.25'); dice.click()
check('dice changes seed and preserves a typed name', seed.input_value() == '325000' and pg.input_value('#nw-name') == 'My camp')
check('valid input clears clamp hint', not pg.is_visible('#nw-seed-hint'))
# Exercise the real UI-to-worldgen boundary once, including zero's stored seed.
pg.evaluate('() => { startNewWorld = __start; Math.random = __random; SET.look = "pixel"; }')
seed.fill('0'); pg.select_option('#nw-size', '2048x1024'); seed.press('Enter')
pg.wait_for_function('GAME.running', timeout=60000)
check('real generated world retains seed zero', pg.evaluate('worldSeed === 0'))
check('no page errors', not errors)
print('FAILS:', fails)
sys.exit(bool(fails))