Commit Graph

8 Commits

Author SHA1 Message Date
funman300 92a5ebb15e fix(android): lower MIN_WINDOW floor so phone viewports lay out correctly
`compute_layout` runs `window.max(MIN_WINDOW)`, which acts as a
component-wise floor: any window smaller than MIN_WINDOW on either
axis gets clamped up. The previous floor of 800x600 was set with
desktop in mind, but on Android the OS-provided window size is the
device resolution (~360 dp wide on a typical phone) and the clamp
silently re-laid the board for an 800 dp width.

Side effect: total grid width (9 * card_width) became ~800 px on a
360 dp viewport, so the leftmost foundation x-position fell past
-180 and the rightmost tableau pile past +180 — both clipped at
the visible edges, matching the v0.22.3 hardware screenshot.

Lowered MIN_WINDOW to 320x400, below the smallest reasonable phone
(~360x640), so every real device flows through compute_layout
unclamped. The floor is preserved as a sentinel against degenerate
windows (Bevy can briefly report 0-size during startup or after
minimisation on some compositors). Desktop's "minimum supported
playable size" is enforced separately via WindowResizeConstraints
in solitaire_app.

Updates `layout_below_minimum_clamps_to_minimum` to use values
below the new floor, and adds a new regression test
`phone_portrait_layout_fits_horizontally` that asserts all 13
piles fit inside a 360 x 800 dp viewport.

Closes P0 #4 of docs/android/PLAYABILITY_TODO.md. 855 engine tests
pass; clippy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:48:56 -07:00
funman300 13aa0fd833 fix(engine): match CARD_ASPECT to hayeah SVG dimensions (1.4 → 1.4523)
Cards rendered ~3.6 % squashed vertically because layout.rs assumed a
1.4 height/width ratio while the bundled hayeah/playing-cards-assets
SVGs are natively 167.087 × 242.667 (= 1.4523). The mismatch meant
every face was scaled to fit a too-short box; pip arrangements and
court-card art read slightly compressed.

Bumps CARD_ASPECT to 1.4523 to match the SVG. The vertical-budget
math in compute_layout (the height-based card_width candidate) uses
CARD_ASPECT algebraically, so the tableau-fits-on-screen check
adapts automatically — slightly smaller cards on aspect-ratio-tight
windows, no visible regression on standard 16:9.

Doc comments referencing the old 1.4 literal updated to point at
CARD_ASPECT instead so this can't drift again.

All 982 tests pass — the existing layout/test sentinel
(card_size.y / card_size.x - CARD_ASPECT) used the constant by name
and adapted for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:03:12 +00:00
funman300 95df5421c9 feat(core): unlock foundations — Foundation(u8) slots, suit derived from contents
Standard Klondike behaviour: any Ace can land in any empty foundation,
and that slot then claims the suit until the pile empties. The
previous PileType::Foundation(Suit) variant pre-assigned each of the
four foundations to a fixed suit ("C / D / H / S" placeholders) and
rejected mismatched Aces — non-standard and (per the smoke-test
feedback) confusing.

Replaces the variant payload with a slot index Foundation(u8) (0..=3)
and derives the claimed suit from the bottom card via a new
Pile::claimed_suit() method. The bottom card is, by construction,
the Ace that established the claim; using it directly eliminates an
entire class of "stuck claim after undo" bugs that a separate
claimed_suit field would have introduced.

can_place_on_foundation drops its suit parameter — the rule reduces
to "empty pile accepts any Ace; non-empty pile accepts the next
rank up of the bottom card's suit." Iteration sites across
input_plugin, cursor_plugin, selection_plugin, card_plugin,
auto_complete_plugin, game_plugin, layout, and hud_plugin all swap
the four-suit list for `(0..4u8).map(PileType::Foundation)`.

next_auto_complete_move now prefers a slot whose claimed_suit matches
the candidate card before falling back to the first empty slot for
an Ace — so the same suit consistently auto-targets the same slot
across the whole game, matching player expectations.

The HUD selection label and the hint toast read claimed_suit() and
fall back to "Foundation N" / "move to foundation" only when the
slot is empty. Empty foundation pile markers no longer render the
suit-letter children — they're plain translucent rectangles, matching
empty tableau placeholders.

Save-format invalidation: GameState gains a schema_version field
(serde-default to 1 for back-compat parsing of old files), the
constant is bumped to 2, and load_game_state_from rejects mismatched
schemas. Old in-progress saves silently fall through to "fresh game
on launch" — the user accepted this loss given the mechanic change.
Stats / progress / achievements / settings live in separate files,
contain no PileType data, and are unaffected.

9 new tests pin the contract:
- Pile::claimed_suit returns None for empty / non-foundation, Some
  for non-empty foundation
- Any Ace lands in the first empty foundation; successive Aces
  distribute across slots 0..3
- Claim drops when the slot is emptied via undo
- Auto-complete picks the slot with a matching claim, not the first
  empty slot
- A v1-format game_state.json is rejected; sibling stats save/load
  is unaffected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:17:17 +00:00
funman300 2c72e1fc87 feat(engine): reserve top band for HUD so it stops crowding the cards
Player report: the action button bar (Menu / Undo / Pause / Help /
Modes / New Game) and Score / Moves / Timer text were sharing the
same vertical band as the stock + foundation row, with no visual
separation. The HUD read as part of the play surface.

Two-part fix:

1. layout.rs reserves HUD_BAND_HEIGHT (64 px) at the top of the
   window. Card-grid math takes that off the available vertical
   budget so cards still fit; top_y shifts down by the same amount.
   New layout test pins the reservation. Existing
   worst_case_tableau_fits_vertically tests verify the height-budget
   arithmetic still holds.

2. hud_plugin.rs spawns a translucent purple band (BG_HUD_BAND, new
   token in ui_theme.rs at the BG_BASE hue with 0.70 alpha) filling
   that reserved zone. Z-index sits one rung below Z_HUD so action
   buttons paint on top while the band reads as their container. The
   band's bottom edge lines up with the top edge of the highest
   playable card, so the buttons feel anchored to a "tools strip"
   rather than floating in the play area.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:57:51 +00:00
funman300 8dda9541a3 fix(engine): constrain card size so worst-case tableau fits vertically
The previous formula card_width = window.x / 9 with card_height = 1.4 *
card_width ignored the window height entirely. On a 1920×1080 window a
13-card face-up tableau column extended ~377 px below the viewport
bottom — visible reproduction in the smoke test.

compute_layout now derives two card_width candidates: one from the
horizontal grid budget (window.x / 9, unchanged) and one from the
vertical budget needed to seat 13 fanned cards plus the foundation
row, vertical_gap, and h_gap bottom margin. The smaller of the two
wins, so width remains the limiter on standard landscape windows and
height takes over on tall or short-wide aspect ratios. The math is
solved algebraically in a single substitution to avoid iteration.

When height is the limiter the original layout would have squished the
grid against the left edge; col_x now folds in a horizontal centring
offset that collapses to the existing geometry whenever width is the
limiter, so no other module needed an update.

Adds MAX_TABLEAU_CARDS = 13.0 (King-down-to-Ace worst case) and a
locally mirrored TABLEAU_FAN_FRAC = 0.25 — the original lives in
card_plugin and importing it would have created a circular dep with
layout. The duplication is doc-flagged so future drift gets noticed.

Four new tests pin both regimes: the height-limiter activates on a
1920×1080 window, stays inactive on a 900×1600 portrait window, and
the worst-case 13-card column fits on both 1280×800 and 1920×1080
within the bottom margin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:05:57 +00:00
funman300 366fd6d127 fix(engine): snap cards directly on window resize
CI / Test & Lint (push) Failing after 19s
CI / Release Build (push) Has been skipped
on_window_resized was firing StateChangedEvent on every WindowResized
event. That ran sync_cards_on_change → update_card_entity, which
inserts a CardAnim slide tween for every card whose target moves >1
unit. During a corner drag the resize fires every frame, retargeting
the slide each time from the cards' current mid-tween positions, so
cards never reach steady state — the visible "snap back and forth"
jitter reported during the 2026-04-29 smoke test.

Replace the StateChangedEvent emit with a direct snap path:

- Add LayoutSystem::UpdateOnResize SystemSet in layout.rs so cross-
  plugin ordering is explicit (Bevy's automatic conflict-based order
  only forces non-parallel execution, not a particular order).
- table_plugin::on_window_resized: drop the StateChangedEvent emit;
  mark the system in_set(LayoutSystem::UpdateOnResize). It already
  snaps backgrounds and pile markers directly, so this aligns cards
  with the same instant-snap policy.
- card_plugin: new snap_cards_on_window_resize system listens for
  WindowResized, runs .after(LayoutSystem::UpdateOnResize), writes
  fresh transforms via the existing card_positions() helper, and
  removes any in-flight CardAnim. It also reapplies the stock-empty
  indicator so the "↺" label's font_size (derived from
  layout.card_size.x) still rescales on resize.

Other StateChangedEvent listeners — start_settle_anim,
detect_auto_complete, clear_selection_on_state_change, check_no_moves,
reset_hint_cycle_on_state_change, clear_right_click_highlights — no
longer fire spuriously on resize. They should not fire on a layout
change anyway; that was a pre-existing minor bug masked by the
jitter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:44:08 +00:00
funman300 735d8766a2 docs(engine): add missing doc comments on layout, ProgressPlugin; fix audio format in ARCHITECTURE.md
- Add field-level doc comments to Layout::card_size and Layout::pile_positions
- Add struct-level doc comment to ProgressPlugin
- Fix ARCHITECTURE.md Section 14: .ogg → .wav throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:37:07 +00:00
funman300 d92b4a8648 feat(engine): add layout, LayoutResource, and TablePlugin
compute_layout is a pure function that maps window size to card size and
the 13 pile positions, with clamping at the 800x600 minimum and seven
tableau columns horizontally aligned with stock/waste (cols 0,1) and the
four foundations (cols 3,4,5,6). TablePlugin spawns a 2D camera, a felt
background sprite, and 13 translucent pile-marker sprites, and
repositions them on WindowResized. Plugin registers WindowResized
explicitly so it works under MinimalPlugins in tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 16:18:24 -07:00