Merge branch 'feat/end-hint'
All checks were successful
CI / test (push) Successful in 22s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lukas Müllner 2026-11-13 16:52:52 +01:00
commit 7e79bb84bb
4 changed files with 45 additions and 1 deletions

View file

@ -197,3 +197,6 @@ Abgabe: **9. Dezember 2026, 03:00 MEZ**. Eingereicht wird in „Best Daily Game
„All 3 stars! Keep going for more gold – or end the game.“
- Erreichte Ziele werden **golden** hervorgehoben (wie `goal-chip.done` im Hauptspiel).
- `devvit playtest` trägt `dev.subreddit` (`ninelands_game_dev`) in `devvit.json` ein.
- Rückmeldung Besitzer: Bei „One farm per land“ bleiben am Ende oft nur Ritter, die Gold kosten. Darum prüft der
Client nach jedem Zug, ob noch ein Zug Gold bringt (`gainLeft`, alle gültigen Züge per `preview`, < 1 ms). Wenn
nicht: Hinweis „No move left gains gold – end the game to keep what you have.“ und „End game“ leuchtet golden.

View file

@ -181,6 +181,23 @@ button.end {
white-space: nowrap;
}
button.end.suggest {
background: var(--px-gold);
animation: pulse 1.2s steps(2) infinite;
}
@keyframes pulse {
50% {
outline: 2px solid var(--px-parchment);
}
}
@media (prefers-reduced-motion: reduce) {
button.end.suggest {
animation: none;
}
}
button.end.armed {
background: var(--px-red);
color: var(--px-parchment);

View file

@ -260,6 +260,11 @@ function update(): void {
bothGoal.classList.toggle('done', stars.both)
undoBtn.disabled = finished || !game.canUndo
endBtn.disabled = finished || game.moves.length === 0
gainCache = null
endBtn.classList.toggle(
'suggest',
!finished && game.moves.length > 0 && !gainLeft(),
)
const hasCastle = game.count('HIVEMOTHER') > 0
for (const [unit, btn] of toolButtons) {
btn.classList.toggle('active', unit === tool)
@ -272,6 +277,23 @@ function update(): void {
renderHint()
}
/** Gibt es noch einen Zug, der Gold bringt? (je Spielstand einmal berechnet) */
let gainCache: boolean | null = null
function gainLeft(): boolean {
if (gainCache !== null) return gainCache
gainCache = game.moves.length === 0
for (const unit of UNIT_TYPES) {
if (gainCache) break
const legal = game.legalMoves(unit)
for (let cell = 0; cell < legal.length && !gainCache; cell++) {
if (!legal[cell]) continue
const p = game.preview(unit, cell)
if (p.ok && p.net > 0) gainCache = true
}
}
return gainCache
}
/** Feld, für das gerade eine Vorschau gilt: angetippt (Touch) oder unter der Maus */
function activeCell(): number | null {
return pendingTouch ?? hovered
@ -320,7 +342,8 @@ function renderHint(): void {
)
return
}
if (starsOf(game, day.goals).both) setHint(HINTS.allStars, 'good')
if (!gainLeft()) setHint(HINTS.noGain, 'warn')
else if (starsOf(game, day.goals).both) setHint(HINTS.allStars, 'good')
else if (game.count('HIVEMOTHER') === 0) setHint(HINTS.castle)
else if (game.moves.length < 3) setHint(HINTS.farmer)
else setHint(HINTS.general)

View file

@ -114,6 +114,7 @@ export const HINTS = {
general:
'Ore ⛰ and herbs 🌿 pay 20 gold. Knights pay double. Keep going until no move is left.',
touch: 'Tap again to place.',
noGain: 'No move left gains gold – end the game to keep what you have.',
allStars: 'All 3 stars! Keep going for more gold – or end the game.',
} as const