Implementation companion to FUNCTIONAL_SPEC.md. This document is implementation-only; functional rules live in the spec.
@Observable macro (iOS 17 Observation framework)withAnimation; Lottie not usedfarkle/
Farkle.xcodeproj
Farkle/
App/
FarkleApp.swift // @main + scene + ModelContainer wiring
RootView.swift // routes onboarding / home
DesignSystem/
Theme.swift // colors, typography
Fonts.swift // font registration
PaperBackground.swift // paper-grain modifier
FeltBackground.swift // felt-table modifier (future)
Buttons.swift // WalnutButton, ChipButton
DieView.swift // bone die with pips
AvatarView.swift // initial-circle avatar
BigScoreText.swift // tabular display number
Confetti.swift // particle overlay
Models/
Player.swift
HouseRules.swift
Game.swift
Turn.swift
ActionLog.swift // ActionLogEntry + Undo
ScoreHelperEngine.swift // combo detection
Persistence.swift // SwiftData ModelContainer factory
Features/
Onboarding/OnboardingView.swift
Home/HomeView.swift
NewGame/NewGameView.swift
ActiveGame/
ActiveGameView.swift
NowRollingBanner.swift
PendingTurnCard.swift
StandingsLadder.swift
RecentActionsLog.swift
BankConfirmSheet.swift
ScoreHelperSheet.swift
KeypadSheet.swift
GameOver/GameOverView.swift
History/HistoryView.swift
Stats/StatsView.swift
Settings/SettingsView.swift
Rules/RulesView.swift
Resources/
Fonts/ // .ttf files bundled
Assets.xcassets // colors, app icon
FarkleTests/
ScoringTests.swift
GameFlowTests.swift
UndoTests.swift
README.md
FUNCTIONAL_SPEC.md
DEV_DESIGN.md
@Model final class Game {
@Attribute(.unique) var id: UUID
var name: String // auto-generated "Tuesday Night Roll"
var createdAt: Date
var endedAt: Date?
var targetScore: Int
var rules: HouseRules // value-type, codable, stored as JSON
@Relationship(deleteRule: .cascade) var players: [Player]
@Relationship(deleteRule: .cascade) var actions: [ActionLogEntry] // ordered
var activePlayerIndex: Int
var pendingTurnScore: Int // dice-not-banked-yet
var pendingRollCount: Int
var finalRoundTriggeredBy: UUID? // player id; nil until target hit
var finalRoundTurnsRemaining: Int
}
@Model final class Player {
@Attribute(.unique) var id: UUID
var name: String
var avatarIndex: Int // 0..7 maps to AVATAR_COLORS
var orderIndex: Int // turn order in this game
var bankedScore: Int // derived but cached for perf
}
@Model final class ActionLogEntry {
@Attribute(.unique) var id: UUID
var game: Game?
var playerId: UUID
var kind: ActionKind // .bank, .bust, .startFinalRound, .endGame
var amount: Int // 0 for bust
var timestamp: Date
var orderIndex: Int // monotonic per game
var roundNumber: Int
var hotDice: Bool // marked at bank time
}
enum ActionKind: String, Codable {
case bank, bust, startFinalRound, endGame
}
struct HouseRules: Codable, Equatable {
var threePair: Bool = true
var straight: Bool = true
var twoTriples: Bool = false
var mustOpenWith: Int? = 500 // nil = no minimum
}
ActiveGameViewModel reads/writes via ModelContext.try context.save() after each action to make persistence visible across launches.Player.bankedScore is derived (sum of action amounts for kind == .bank for that player). We cache it on the Player for fast standings render, recomputed any time the action log mutates. Undo recomputes by reversing.ActionLogEntry array on Game..bust restores the turn’s pendingTurnScore to what it was at bust-time. We snapshot pending state inside the log entry’s metadata when busting..bank rewinds activePlayerIndex and restores pendingTurnScore similarly..startFinalRound clears the trigger and resets finalRoundTurnsRemaining..endGame from the “Wait — that’s wrong” button on Game Over is equivalent to undoing the most recent bank that triggered it; the game returns to active state, ready for the same player to roll again.Pure value-type engine, fully unit-tested.
struct ScoreHelperEngine {
let rules: HouseRules
func score(dice: [Int]) -> ScoreBreakdown
}
struct ScoreBreakdown {
let combos: [Combo] // [(name: "Three 5s", points: 500)]
let total: Int
let usesAllDice: Bool // for Hot Dice marking
}
Rules implemented:
Theme.swift)Hard-coded Color extensions mapped to the Felt & Bone palette. Defined for both light only (v1 has no dark mode — paper is the brand).
.ttf files in Resources/Fonts/. Register via Info.plist UIAppFonts.Font.display(_:), Font.ui(_:), Font.mono(_:) helpers; sizes pass through to Dynamic Type by scaling against UIFontMetrics.default.PaperBackground view modifier: cream fill + 4-radial-gradient overlay of paper grain dots, matching the CSS spec.FeltBackground: ellipse highlight + bottom shadow + speckle (deferred to v1.1).DieView(value:size:held:scoring:) draws the bone face + pip layout from the same pip arrangement table the prototype uses.AvatarView(name:colorIndex:size:active:) renders an initial in a colored circle with the active “ring” outline.WalnutButton wraps a primary CTA with chunky drop shadow + walnut grain background.ChipButton for quick-add scores.withAnimation(.spring(...)) on activePlayerIndex change.phase animation via TimelineView.Canvas + particle array driven by TimelineView(.animation) for game-over.NavigationStack rooted on Home (after Onboarding)..navigationBarBackButtonHidden; “back” requires explicit confirm to leave the in-progress game (we save state, but want user intent)..sheet(isPresented:)):
ModelContainer initialized at app launch with schema = [Game, Player, ActionLogEntry].Game where endedAt == nil; if found, route to Active Game; else Home..dynamicTypeSize(...DynamicTypeSize.accessibility3) upper bound.accessibilityElement(children: .combine) + label “Now rolling: Maya. Banked 4,250 of 10,000.”accessibilityAction(named: "Undo this action") for assistive tech to surface the undo without tap..frame(minWidth: 44, minHeight: 44) on icon-only buttons.Anything in FUNCTIONAL_SPEC.md §2.1. When in doubt, ship the spec, not the design canvas. The canvas has 14 screens; we ship 11.