solitaire: detect a won board and finish it in one press
A drained, all-face-up board is a guaranteed clear that auto() sweeps home in a single cascade, but the only way to trigger it was double-clicking thirty cards home by hand. Add State.Won(), surface it in the view, and swap the ordinary controls for one pulsing finish button when it's true.
This commit is contained in:
@@ -620,6 +620,31 @@ func (s State) Net() int64 {
|
||||
// cleared reports whether every card is home.
|
||||
func (s State) cleared() bool { return s.Home() == FullDeck }
|
||||
|
||||
// Won reports that the board is a guaranteed clear: nothing left in the stock or
|
||||
// waste, and not a single face-down card under any column. From here every card
|
||||
// is a face-up run and a single auto() drains the whole board to the foundations
|
||||
// without a choice left to make — which is exactly the tedious tail the felt
|
||||
// should offer to finish in one gesture instead of thirty double-clicks.
|
||||
//
|
||||
// It's deliberately narrower than "solvable": a draw-one board with cards still
|
||||
// in the stock is winnable too, but auto() won't turn the stock over, so calling
|
||||
// that won would light a finish button that then stalls. This is the state the
|
||||
// finish button actually finishes.
|
||||
func (s State) Won() bool {
|
||||
if s.Phase == PhaseDone || s.cleared() {
|
||||
return false
|
||||
}
|
||||
if len(s.Stock) > 0 || len(s.Waste) > 0 {
|
||||
return false
|
||||
}
|
||||
for _, p := range s.Table {
|
||||
if len(p.Down) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CanAuto reports whether anything can go home at all — which is what greys the
|
||||
// finish button out rather than letting it be pressed at a board that has nothing
|
||||
// for it.
|
||||
|
||||
@@ -423,6 +423,52 @@ func TestAutoSendsEverythingItCanHome(t *testing.T) {
|
||||
refuses(t, next, Move{Kind: "auto"}, ErrNothingHome)
|
||||
}
|
||||
|
||||
// Won is the state the finish button finishes: every card face up, the stock and
|
||||
// waste drained, so a single auto sweeps the lot home. The button is only honest
|
||||
// if these two agree — Won lighting up on a board auto can't clear would stall.
|
||||
func TestWonIsExactlyWhatAutoCanFinish(t *testing.T) {
|
||||
s := board(patient(), 5200)
|
||||
// A won board: three short face-up runs across the columns, nothing face down,
|
||||
// nothing in the stock or waste. Every card is reachable.
|
||||
s.Table[0].Up = []cards.Card{card(2, cards.Spades), card(cards.Ace, cards.Hearts)}
|
||||
s.Table[1].Up = []cards.Card{card(2, cards.Hearts), card(cards.Ace, cards.Spades)}
|
||||
s.Table[2].Up = []cards.Card{card(2, cards.Diamonds), card(cards.Ace, cards.Clubs)}
|
||||
s.Table[3].Up = []cards.Card{card(2, cards.Clubs), card(cards.Ace, cards.Diamonds)}
|
||||
|
||||
if !s.Won() {
|
||||
t.Fatalf("a board with everything face up and the piles empty is won")
|
||||
}
|
||||
|
||||
next, _ := apply(t, s, Move{Kind: "auto"})
|
||||
if next.Home() != 8 { // the eight cards laid out above
|
||||
t.Fatalf("auto homed %d cards, want 8 — the whole won board", next.Home())
|
||||
}
|
||||
|
||||
// Anything still hidden or still in a pile is not won: auto would stall on it.
|
||||
withStock := s.clone()
|
||||
withStock.Stock = cards.Deck{card(5, cards.Spades)}
|
||||
if withStock.Won() {
|
||||
t.Errorf("a board with a card still in the stock is not won — auto won't turn it over")
|
||||
}
|
||||
withWaste := s.clone()
|
||||
withWaste.Waste = []cards.Card{card(5, cards.Spades)}
|
||||
if withWaste.Won() {
|
||||
t.Errorf("a board with a card still in the waste is not won")
|
||||
}
|
||||
withDown := s.clone()
|
||||
withDown.Table[5].Down = []cards.Card{card(5, cards.Spades)}
|
||||
if withDown.Won() {
|
||||
t.Errorf("a board with a face-down card is not won")
|
||||
}
|
||||
|
||||
// A finished board is never won: the button belongs to a game still in play.
|
||||
cleared := s.clone()
|
||||
cleared.Phase = PhaseDone
|
||||
if cleared.Won() {
|
||||
t.Errorf("a settled board reports won")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the money -------------------------------------------------------------
|
||||
|
||||
// The number the felt quotes while you play and the number settle() lands on are
|
||||
|
||||
@@ -60,6 +60,7 @@ type solitaireView struct {
|
||||
Passes int `json:"passes"` // through the stock, counting this one; -1 unlimited
|
||||
Moves int `json:"moves"`
|
||||
CanAuto bool `json:"can_auto"`
|
||||
Won bool `json:"won"` // every card face up, stock and waste empty: one press finishes it
|
||||
|
||||
Home int `json:"home"` // cards on the foundations
|
||||
PerCard float64 `json:"per_card"` // what one more is worth
|
||||
@@ -86,6 +87,7 @@ func viewSolitaire(g klondike.State) solitaireView {
|
||||
Passes: g.PassesLeft(),
|
||||
Moves: g.Moves,
|
||||
CanAuto: g.CanAuto(),
|
||||
Won: g.Won(),
|
||||
Home: g.Home(),
|
||||
PerCard: g.PerCard(),
|
||||
BreakEven: g.Tier.BreakEven(),
|
||||
|
||||
@@ -1456,6 +1456,14 @@ html[data-phase="night"] {
|
||||
100% { box-shadow: 0 0 0 1.4rem rgba(242, 181, 61, 0); }
|
||||
}
|
||||
|
||||
/* The finish button on a won board breathes, so the one press left to make is
|
||||
the one the eye lands on. */
|
||||
.pete-finish { animation: pete-finish-pulse 1.6s ease-in-out infinite; }
|
||||
@keyframes pete-finish-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(242, 181, 61, 0.55); }
|
||||
50% { box-shadow: 0 0 0 0.9rem rgba(242, 181, 61, 0); }
|
||||
}
|
||||
|
||||
/* The rail: the house's rack, what you've banked, the meter. On a wide screen
|
||||
it's a column down the right of the felt; on a narrow one it lies down and
|
||||
sits under the board. */
|
||||
@@ -2130,7 +2138,8 @@ html[data-phase="night"] {
|
||||
.pete-tile-hit,
|
||||
.pete-meter[data-hit="1"] { animation: none; }
|
||||
.pete-nope,
|
||||
.pete-home-flash { animation: none; }
|
||||
.pete-home-flash,
|
||||
.pete-finish { animation: none; }
|
||||
.pete-card[data-held="1"] { transition: none; }
|
||||
/* The clock still drains — it is information, not decoration — but it stops
|
||||
pulsing at you, and a wrong answer stops shaking. */
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -44,7 +44,10 @@
|
||||
|
||||
var playing = root.querySelector("[data-playing]");
|
||||
var betting = root.querySelector("[data-betting]");
|
||||
var playControls = root.querySelector("[data-play-controls]");
|
||||
var wonControls = root.querySelector("[data-won-controls]");
|
||||
var autoBtn = root.querySelector("[data-auto]");
|
||||
var finishBtn = root.querySelector("[data-finish]");
|
||||
var cashBtn = root.querySelector("[data-cash]");
|
||||
var cashAmountEl = root.querySelector("[data-cash-amount]");
|
||||
var startBtn = root.querySelector("[data-start]");
|
||||
@@ -258,6 +261,11 @@
|
||||
playing.classList.toggle("hidden", !live);
|
||||
betting.classList.toggle("hidden", live);
|
||||
if (!live) return;
|
||||
// A won board has nothing left to decide, so the ordinary controls step aside
|
||||
// for the one button that finishes it.
|
||||
var won = !!v.won;
|
||||
wonControls.classList.toggle("hidden", !won);
|
||||
playControls.classList.toggle("hidden", won);
|
||||
autoBtn.disabled = !v.can_auto;
|
||||
cashAmountEl.textContent = (v.stands || 0).toLocaleString();
|
||||
}
|
||||
@@ -545,6 +553,10 @@
|
||||
|
||||
autoBtn.addEventListener("click", function () { drop(); send({ kind: "auto" }); });
|
||||
|
||||
// The finish button. On a won board a single auto drains every card home in one
|
||||
// cascade, and the board settles cleared on the far end of it.
|
||||
finishBtn.addEventListener("click", function () { drop(); send({ kind: "auto" }); });
|
||||
|
||||
cashBtn.addEventListener("click", function () {
|
||||
drop();
|
||||
send({ kind: "concede" });
|
||||
|
||||
@@ -86,7 +86,9 @@
|
||||
|
||||
<!-- Playing: shown while a board is live. -->
|
||||
<section data-playing class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
|
||||
<!-- The ordinary controls, while there's still a game to play. -->
|
||||
<div data-play-controls class="flex flex-wrap items-center gap-3">
|
||||
<button type="button" data-auto
|
||||
class="rounded-full bg-[color:var(--ink)]/5 px-5 py-2.5 font-display font-bold border-2 border-[color:var(--ink)]/10
|
||||
hover:bg-[color:var(--ink)]/10 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||
@@ -103,6 +105,22 @@
|
||||
Cash the board · <span data-cash-amount class="tabular-nums">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- The board's won: every card's face up and the piles are drained, so
|
||||
there's nothing left to decide. One press sends the lot home. -->
|
||||
<div data-won-controls class="hidden">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button type="button" data-finish
|
||||
class="pete-finish rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||
hover:brightness-105 active:translate-y-px disabled:pointer-events-none transition">
|
||||
You've cracked it. Send them all home 🎉
|
||||
</button>
|
||||
<p class="text-xs text-[color:var(--ink)]/45">
|
||||
The whole board's face up now, so the rest plays itself.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user