Letter lab · JavaScript word game

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html><html lang="en-US"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Letter lab · JavaScript word game</title><style>*{box-sizing:border-box}body{margin:0;background:#f3f1eb;color:#202e35;font:16px/1.5 system-ui,sans-serif}main{max-width:1160px;margin:auto;padding:32px 28px 70px}h1{font-size:clamp(26px,4vw,42px);line-height:1.12;margin:8px 0 12px;letter-spacing:-.04em}h2{font-size:22px;margin:0 0 16px}h3{font-size:17px;margin:0 0 8px}p{margin:8px 0 16px}.eyebrow{text-transform:uppercase;letter-spacing:.15em;font-size:12px;font-weight:750;color:#45665e}.lede{color:#55636a;max-width:780px}.panel{background:#fff;border:1px solid #d9dfd9;border-radius:16px;padding:24px;margin:20px 0}.hero{background:#193f38;color:white}.hero .muted{color:#bfd6cb}.split{display:grid;grid-template-columns:1.1fr 1fr;gap:24px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.row{display:flex;flex-wrap:wrap;gap:12px;align-items:center}.big{font-size:clamp(32px,5vw,58px);line-height:1.15;font-weight:750;letter-spacing:-.04em}.muted,small{color:#61716e}.badge{display:inline-block;padding:4px 9px;border-radius:20px;background:#e5efe8;color:#284e40;font-size:13px;font-weight:650}.warn{background:#fff2d2;color:#795213}.rule{border-top:1px solid #dce2dd;padding-top:16px;margin-top:16px}label{display:block;font-size:14px;font-weight:650;margin:12px 0 5px}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;border:1px solid #aebbb5;border-radius:7px;background:white;color:#20332c;padding:10px;min-height:44px}textarea{min-height:96px;resize:vertical}button{min-height:44px;border:1px solid #80968c;border-radius:7px;padding:9px 14px;background:white;color:#1c4637;cursor:pointer}button.primary{background:#245d47;color:white;border-color:#245d47}button:disabled{opacity:.5;cursor:default}button[aria-pressed=true]{background:#d8eb90;color:#1c3d29;border-color:#536a2d}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:3px solid #d5782a;outline-offset:3px}.error{color:#913920;min-height:24px}pre,.record{white-space:pre-wrap;overflow-wrap:anywhere;font:14px/1.6 ui-monospace,monospace;background:#eef2ec;border-radius:8px;padding:16px}table{border-collapse:collapse;width:100%;font-size:14px}th,td{border-bottom:1px solid #dce2dd;text-align:left;padding:12px 8px;vertical-align:top}th{font-size:12px;text-transform:uppercase;letter-spacing:.06em}.scroll{overflow:auto}details{margin:24px 0}summary{cursor:pointer;min-height:44px;font-weight:650}svg{max-width:100%;height:auto}.small{font-size:13px}.stack>*+*{margin-top:12px}@media(max-width:720px){main{padding:22px 16px 48px}.split{grid-template-columns:1fr}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.panel{padding:18px;margin:16px 0}h1{font-size:32px}.desktop-table{min-width:680px}}@media(max-width:400px){.grid{grid-template-columns:1fr}main{padding:18px 14px}.panel{padding:16px}.row>button{flex:1}h1{font-size:29px}}.word{display:flex;flex-wrap:wrap;gap:10px;margin:8px 0 30px}.word span{font:700 clamp(24px,4vw,44px)/1.4 ui-monospace,monospace;border-bottom:3px solid #b5c99e;min-width:32px;text-align:center}.letters{display:grid;grid-template-columns:repeat(13,minmax(0,1fr));gap:7px}.letters button{padding:8px 0;font-weight:700}@media(max-width:720px){.letters{grid-template-columns:repeat(7,minmax(0,1fr))}.word{gap:7px}}@media(max-width:400px){.word span{min-width:28px;font-size:26px}.word{gap:5px}}</style></head><body><main><div class="eyebrow">Original JavaScript example · Word game</div><h1>One letter. Every matching spot.</h1><p class="lede">Reveal the word before six different incorrect letters are used.</p><section class="panel hero"><div id="word" class="word" aria-label="Hidden word"></div><div class="row"><strong id="misses">0 of 6 misses</strong><span id="dots" aria-hidden="true"></span></div><p id="status" role="status" style="margin-bottom:0">Choose a letter to begin.</p></section><div id="letters" class="letters" aria-label="Letter choices"></div><section class="panel"><form id="guessForm"><label for="guessInput">Or type one English letter</label><div class="row"><input id="guessInput" maxlength="20" autocomplete="off" style="flex:1;min-width:150px"><button id="submit" class="primary" type="submit">Guess</button></div></form><p id="used">Guessed: none</p><div class="row"><button id="reset">Reset BALLOON example</button><button id="new">New game</button></div></section><details><summary>Rules and source boundaries</summary><p>Letters are normalized to uppercase. Repeated guesses count once; invalid input changes nothing. All occurrences appear together. After a win or six misses, further guesses are ignored. Reset always starts BALLOON. New game alternates between BALLOON and PLANET, the two words in this small teaching list. The word list is visible in the source; this is a local demonstration, not a secure quiz or saved score.</p></details></main><script>const $=id=>document.getElementById(id),put=(id,v)=>$(id).textContent=String(v),esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const words=['BALLOON','PLANET'];let state={word:words[0],guessed:new Set()},gameIndex=0;function outcome(s){const misses=[...s.guessed].filter(c=>!s.word.includes(c));return{misses:misses.length,phase:[...s.word].every(c=>s.guessed.has(c))?'won':misses.length>=6?'lost':'playing',display:[...s.word].map(c=>s.guessed.has(c)?c:'_')}}function guess(s,raw){const o=outcome(s);if(o.phase!=='playing')return 'This game has ended. Reset or start a new game.';const c=String(raw).trim().toUpperCase();if(!/^[A-Z]$/.test(c))return 'Enter exactly one English letter (A–Z).';if(s.guessed.has(c))return c+' was already guessed; no change.';s.guessed.add(c);const next=outcome(s);return next.phase==='won'?'Word complete! '+s.word:next.phase==='lost'?'Six misses. The word was '+s.word+'.':s.word.includes(c)?c+' is in the word.':c+' is not in the word.'}
function render(message){const o=outcome(state);$('word').innerHTML=o.display.map((c,i)=>'<span aria-label="Position '+(i+1)+': '+(c==='_'?'blank':c)+'">'+c+'</span>').join('');put('misses',o.misses+' of 6 misses');put('dots','●'.repeat(o.misses)+'○'.repeat(6-o.misses));put('status',message);put('used','Guessed: '+([...state.guessed].join(', ')||'none'));$('letters').innerHTML=[...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'].map(c=>'<button id="letter-'+c+'" data-letter="'+c+'" '+(o.phase!=='playing'||state.guessed.has(c)?'disabled':'')+'>'+c+'</button>').join('');$('guessInput').disabled=o.phase!=='playing';$('submit').disabled=o.phase!=='playing'}
function submit(raw){render(guess(state,raw));$('guessInput').value='';if(outcome(state).phase==='playing')$('guessInput').focus();else $('reset').focus()}$('letters').addEventListener('click',e=>{if(e.target.dataset.letter)submit(e.target.dataset.letter)});$('guessForm').addEventListener('submit',e=>{e.preventDefault();submit($('guessInput').value)});function reset(i){gameIndex=i;state={word:words[i],guessed:new Set()};$('guessInput').value='';render('New blank word. Choose a letter.');$('guessInput').focus()}$('reset').addEventListener('click',()=>reset(0));$('new').addEventListener('click',()=>reset((gameIndex+1)%words.length));globalThis.Tool={guess,outcome,getState:()=>state};render('Choose a letter to begin.');</script>
<script>
// Local calculation controls: no native form submission or sandbox permissions required.
(()=>{const form=document.getElementById('guessForm');if(!form)throw Error('Calculation form missing');const dispatch=()=>form.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));const buttons=[...form.querySelectorAll('button')].filter(button=>button.type==='submit');if(buttons.length!==1)throw Error('Expected one calculation control');for(const button of buttons){button.type='button';button.addEventListener('click',dispatch);}form.addEventListener('keydown',event=>{if(event.key==='Enter'&&!event.isComposing&&['INPUT','SELECT'].includes(event.target.tagName)){event.preventDefault();dispatch();}});})();
</script>
</body></html>