bf46b9492d
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m25s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m2s
Board-highlight bug (reported on the contour): formedGeometry walked cross words
unconditionally, so in a single-word (one-word-per-turn) game a staged tile sitting
next to a committed tile lit up a green "cross word" the engine ignores — and which
need not even be a real word (the reported "БО lights up green in a one-word ПОПА
play"). Gate the cross-word walk on the game's multipleWordsPerTurn flag. The score
(8) was already correct — a premium square under the main word.
Also, from review:
- the turn strip reads the staged play's "WORD+WORD = N" while composing a legal move,
reverting to the turn / result text otherwise;
- the Exchange/Pass dialog shows the bag count ("In the bag: N" / "Bag is empty")
right-aligned in the title row, via a new optional Modal `titleAside`;
- cosmetics: half the turn strip's bottom padding (the plaques below carry their own
top pad); a top gap above the landscape rack (it sat flush under the docked history);
more horizontal padding on tab count badges so a 2-3 digit bag count clears the pill
ends;
- admin console: the game Summary now shows the single-word / multiple-words rule.
Tests: formed single-word case added; full unit (584) + e2e (chromium + webkit, 113
each) green; backend build + adminconsole templates parse. Docs (FUNCTIONAL +_ru,
UI_DESIGN) updated.
115 lines
6.4 KiB
Go Template
115 lines
6.4 KiB
Go Template
{{define "content" -}}
|
|
{{with .Data}}
|
|
<h1>Game {{.ID}}</h1>
|
|
<nav class="subnav"><a href="/_gm/games">« games</a> · <a href="/_gm/messages?game={{.ID}}">messages</a></nav>
|
|
<section class="panel"><h2>Summary</h2>
|
|
<ul class="kv">
|
|
<li><b>Variant</b> {{.Variant}}</li>
|
|
<li><b>Dictionary</b> {{.DictVersion}}</li>
|
|
<li><b>Status</b> {{.Status}}{{if .EndReason}} ({{.EndReason}}){{end}}</li>
|
|
<li><b>AI game</b> {{if .VsAI}}🤖 yes{{else}}no{{end}}</li>
|
|
<li><b>Word rule</b> {{if .MultipleWordsPerTurn}}multiple words per turn{{else}}single word per turn{{end}}</li>
|
|
<li><b>Players</b> {{.Players}}</li>
|
|
<li><b>To move</b> seat {{.ToMove}}</li>
|
|
<li><b>Moves</b> {{.MoveCount}}</li>
|
|
<li><b>Created</b> {{.CreatedAt}}</li>
|
|
<li><b>Updated</b> {{.UpdatedAt}}</li>
|
|
{{if .FinishedAt}}<li><b>Finished</b> {{.FinishedAt}}</li>{{end}}
|
|
</ul>
|
|
</section>
|
|
<section class="panel"><h2>Seats</h2>
|
|
<table class="list">
|
|
<thead><tr><th>Seat</th><th>Player</th><th>Score</th><th>Hints used</th><th>Winner</th><th>Robot</th></tr></thead>
|
|
<tbody>
|
|
{{range .Seats}}
|
|
<tr><td>{{.Seat}}</td><td><a href="/_gm/users/{{.AccountID}}">{{.DisplayName}}</a></td><td>{{.Score}}</td><td>{{.HintsUsed}}</td><td>{{if .Winner}}<span class="ok">winner</span>{{end}}</td><td>{{if .IsRobot}}🤖 {{.RobotIntent}}{{if .NextMove}}<br><small>next move {{.NextMove}}</small>{{end}}{{end}}</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{if .HasRobot}}<p><small>Play-to-win is decided once per game from the bag seed; robots play to win in ~{{.RobotTargetPct}}% of games.</small></p>{{end}}
|
|
</section>
|
|
{{if .SetupDraws}}
|
|
<section class="panel"><h2>First-move draw</h2>
|
|
<p class="note">Each player draws a tile; the one closest to “A” moves first (a blank beats every letter), ties re-drawing until a single leader remains.{{if .FirstMover}} <b>{{.FirstMover}}</b> leads.{{end}}</p>
|
|
<table class="list">
|
|
<thead><tr><th>Round</th><th>Player</th><th>Tile</th><th>Rank</th></tr></thead>
|
|
<tbody>
|
|
{{range .SetupDraws}}
|
|
<tr><td>{{.Round}}</td><td>{{if .AccountID}}<a href="/_gm/users/{{.AccountID}}">{{.Name}}</a>{{else}}{{.Name}}{{end}}</td><td>{{.Letter}}{{if .Blank}} <small>(blank)</small>{{end}}</td><td>{{.Rank}}</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
{{end}}
|
|
{{if .HasReplay}}
|
|
<section class="panel"><h2>Replay</h2>
|
|
<div class="replay-stage">
|
|
<div class="rack-slot rack-top" data-seat="0"></div>
|
|
<div class="rack-slot rack-left" data-seat="2"></div>
|
|
<div class="replay-board" id="replay-board"></div>
|
|
<div class="rack-slot rack-right" data-seat="3"></div>
|
|
<div class="rack-slot rack-bottom" data-seat="1"></div>
|
|
</div>
|
|
<div class="replay-controls">
|
|
<button type="button" id="replay-prev">◀ prev</button>
|
|
<span class="replay-pos" id="replay-pos"></span>
|
|
<button type="button" id="replay-next">next ▶</button>
|
|
</div>
|
|
<ol class="replay-log" id="replay-log"></ol>
|
|
<script>
|
|
const REPLAY = {{.ReplayJSON}};
|
|
(function(){
|
|
if(!REPLAY||!REPLAY.steps){return;}
|
|
const N=15, COLS="ABCDEFGHIJKLMNO", PREM={tw:"3W",dw:"2W",tl:"3L",dl:"2L"};
|
|
const boardEl=document.getElementById("replay-board"), logEl=document.getElementById("replay-log");
|
|
const posEl=document.getElementById("replay-pos"), prevBtn=document.getElementById("replay-prev"), nextBtn=document.getElementById("replay-next");
|
|
let step=0;
|
|
function esc(s){const d=document.createElement("div");d.textContent=s==null?"":s;return d.innerHTML;}
|
|
function tileHTML(t){const sub=(t.v&&t.v>0)?"<sub>"+t.v+"<\/sub>":"";return "<span class=\"tile"+(t.b?" blank":"")+"\">"+esc(t.l)+sub+"<\/span>";}
|
|
function placedAt(k){const m={};for(let s=1;s<=k;s++){const mv=REPLAY.steps[s]&&REPLAY.steps[s].move;if(mv&&mv.placements){for(const p of mv.placements){m[p.r+","+p.c]=p;}}}return m;}
|
|
function renderBoard(){
|
|
const placed=placedAt(step);let h="<div class=\"board-grid\"><div class=\"bh corner\"><\/div>";
|
|
for(let c=0;c<N;c++){h+="<div class=\"bh\">"+COLS[c]+"<\/div>";}
|
|
for(let r=0;r<N;r++){h+="<div class=\"bh\">"+(r+1)+"<\/div>";
|
|
for(let c=0;c<N;c++){const p=placed[r+","+c];
|
|
if(p){h+="<div class=\"cell filled\">"+tileHTML(p)+"<\/div>";continue;}
|
|
const prem=REPLAY.premium[r][c], centre=(r===REPLAY.centre[0]&&c===REPLAY.centre[1]);
|
|
const label=centre?"★":(prem?PREM[prem]:"");
|
|
h+="<div class=\"cell "+(prem||"")+(centre?" centre":"")+"\">"+(label?"<span class=\"prem\">"+label+"<\/span>":"")+"<\/div>";}}
|
|
h+="<\/div>";boardEl.innerHTML=h;
|
|
}
|
|
function renderRacks(){
|
|
const st=REPLAY.steps[step];
|
|
document.querySelectorAll(".rack-slot").forEach(function(slot){
|
|
const seat=parseInt(slot.dataset.seat,10), info=REPLAY.seats.find(function(s){return s.seat===seat;}), rack=st.racks[seat];
|
|
if(!info||!rack){slot.style.display="none";slot.innerHTML="";return;}
|
|
slot.style.display="";slot.classList.toggle("active",st.toMove===seat);
|
|
const nm=info.accountId?"<a href=\"/_gm/users/"+info.accountId+"\">"+esc(info.name)+"<\/a>":esc(info.name||("seat "+seat));
|
|
slot.innerHTML="<div class=\"rack-name\">"+nm+" · "+(st.scores[seat]||0)+"<\/div><div class=\"rack-tiles\">"+rack.map(tileHTML).join("")+"<\/div>";
|
|
});
|
|
}
|
|
function renderLog(){
|
|
let h="";
|
|
for(let s=1;s<=step;s++){const st=REPLAY.steps[s], m=st.move;if(!m){continue;}
|
|
const who=((REPLAY.seats.find(function(x){return x.seat===m.seat;})||{}).name)||("seat "+m.seat);
|
|
let desc;
|
|
if(m.action==="play"){desc="played "+((m.words&&m.words.length)?m.words.join(", "):"")+" for "+m.score;}
|
|
else if(m.action==="exchange"){desc="exchanged "+((m.exchanged&&m.exchanged.length)||0)+" tiles";}
|
|
else if(m.action==="pass"){desc="passed";}
|
|
else{desc=m.action;}
|
|
const drew=(st.drawn&&st.drawn.length)?" · drew "+st.drawn.map(function(t){return t.l;}).join(""):"";
|
|
h+="<li class=\""+(s===step?"cur":"")+"\">"+esc(who)+" "+esc(desc)+drew+" · bag "+st.bagLen+"<\/li>";}
|
|
logEl.innerHTML=h||"<li class=\"note\">opening position<\/li>";
|
|
}
|
|
function render(){renderBoard();renderRacks();renderLog();posEl.textContent=step+" / "+(REPLAY.steps.length-1);prevBtn.disabled=step<=0;nextBtn.disabled=step>=REPLAY.steps.length-1;}
|
|
prevBtn.onclick=function(){if(step>0){step--;render();}};
|
|
nextBtn.onclick=function(){if(step<REPLAY.steps.length-1){step++;render();}};
|
|
document.addEventListener("keydown",function(e){if(e.key==="ArrowLeft"){prevBtn.click();}else if(e.key==="ArrowRight"){nextBtn.click();}});
|
|
render();
|
|
})();
|
|
</script>
|
|
</section>
|
|
{{end}}
|
|
{{end}}
|
|
{{- end}}
|