Dice button picks a random seed in the new-world dialog (t100)
This commit is contained in:
@@ -644,3 +644,9 @@ above, including:
|
||||
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.
|
||||
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
@@ -622,7 +622,7 @@ Shared world colours, minimap and full map.
|
||||
Browser-only clocks; simulation code stays usable without a browser or a clock.
|
||||
- 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.
|
||||
- functions: scanVisible, frame, boot
|
||||
- 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_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_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_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.
|
||||
|
||||
@@ -94,6 +94,10 @@ screenshots go to `shots/t72-{retrace,escape,open}.png`.
|
||||
|
||||
## 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.
|
||||
- `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,
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
- Reused one scratch view, its entry array and get/has methods in src/sim/joins.js;
|
||||
|
||||
+8
-1
@@ -56,7 +56,14 @@
|
||||
<button class="btn" id="btn-new">New world <small>generate</small></button>
|
||||
<div class="plate newworld" id="newworld" hidden>
|
||||
<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
|
||||
<select id="nw-size">
|
||||
<option value="2048x1024">Small · 2048 × 1024</option>
|
||||
|
||||
@@ -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 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; }
|
||||
.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; }
|
||||
.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); }
|
||||
|
||||
+24
-4
@@ -113,10 +113,21 @@ function boot(hotData) {
|
||||
initRender(canvas);
|
||||
setupBuildBar();
|
||||
buildTitleCells(); drawTitle();
|
||||
$('nw-seed').value = String(Math.floor(Math.random() * 900000) + 100000);
|
||||
$('wallet').onclick = () => { playSfx('ui'); openNotebook('Market'); };
|
||||
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 () => {
|
||||
auInit(); playSfx('ui'); $('newworld').hidden = !$('newworld').hidden;
|
||||
if ($('newworld').hidden) return;
|
||||
@@ -125,8 +136,17 @@ function boot(hotData) {
|
||||
$('nw-name').value = autoName = claimName(newWorldSeed(), worldNames);
|
||||
$('nw-name').focus(); $('nw-name').select();
|
||||
};
|
||||
$('nw-seed').oninput = () => {
|
||||
if ($('nw-name').value === autoName) $('nw-name').value = autoName = claimName(newWorldSeed(), worldNames);
|
||||
seedBox.oninput = () => {
|
||||
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(); };
|
||||
$('btn-generate').onclick = () => {
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ def main():
|
||||
if browser:
|
||||
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
|
||||
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))
|
||||
bad = 0
|
||||
for name, code, out, secs in results:
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user