Skip to content

Add Speedy Circles game#50

Open
muvaf wants to merge 1 commit into
mainfrom
speedy-circles-game
Open

Add Speedy Circles game#50
muvaf wants to merge 1 commit into
mainfrom
speedy-circles-game

Conversation

@muvaf

@muvaf muvaf commented May 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the sample SwiftUI screen with the Speedy Circles native iOS game.
  • Adds interval selection, randomized bounded targets, shrinking circle progression, score/time HUD, timeout game-over state, retry flow, and smooth SwiftUI animations.

Demo

speedy-circles-demo.mp4

Preview

Platform Commit Preview
iOS 8b81ba0 Open Simulator Preview →
iOS 8b81ba0 Install to Device →

Validation

  • Built successfully with lim xcode build ..
  • Validated start screen, interval selection, gameplay, target tap/score increment, timeout final score, and retry UI.
  • Recorded demo with Limrun simulator.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Limrun Preview

Platform Commit Preview
iOS 8b81ba0 Open Simulator Preview →
iOS 8b81ba0 Install to Device →

Reviewer must be a member of the organization on Limrun.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8b81ba0. Configure here.

private func hitCircle(in size: CGSize) {
guard phase == .playing else { return }

score += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Score increment outside animation prevents numeric text transition

Low Severity

score += 1 in hitCircle is placed outside the withAnimation block, so the .contentTransition(.numericText()) modifier applied in statCard never triggers its animated digit-rolling effect for the score display. The modifier requires the driving state change to occur within an animation context to produce any visible transition.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8b81ba0. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the placeholder SwiftUI screen with a complete "Speedy Circles" tap game, adding interval selection, a shrinking/randomised target circle, a score+time HUD with an animated progress bar, a game-over screen, and a retry flow — all within a single ContentView.swift.

  • Game loop: a 50 Hz Timer.publish drives wall-clock countdown via a deadline Date; hitting the circle calls resetTimer() and shrinks the target until it reaches the 44 pt minimum.
  • Phase state machine: three GamePhase cases (.start, .playing, .finished) gate all mutations with guard phase == .playing checks, preventing double-finish or stale taps.
  • Transition & animation: every phase change is wrapped in withAnimation; contentTransition(.numericText()) is applied to both the HUD score and game-over score, though the game-over one won't animate (value is pre-set before the view appears).

Confidence Score: 4/5

The game is functionally correct and safe to merge; both findings are polish-level concerns that do not affect core gameplay.

The game logic is self-contained and well-guarded throughout. The wall-clock deadline approach is sound, circle placement math handles edge cases, and phase guards prevent double-finish or stale taps. The two findings — a silent no-op animation on the game-over score and potential timer subscription churn from the 50 Hz publisher being re-created on every render — are cosmetic or performance-adjacent rather than game-breaking.

sample-native-app/ContentView.swift — specifically the timer publisher declaration and the game-over score transition.

Important Files Changed

Filename Overview
sample-native-app/ContentView.swift Full game implementation — start screen, interval picker, gameplay, HUD, and game-over flow. Two minor issues: the contentTransition(.numericText()) on the final score won't fire since the value is pre-set before the view appears, and the 50 Hz timer publisher is re-instantiated on every render.
.github/assets/speedy-circles-demo.mp4 Binary video asset added for the PR demo — no code concerns.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([App Launch]) --> S[Start Screen - interval picker]
    S -- startGame --> P[Playing - HUD + circle]
    P -- hitCircle - score++ resetTimer --> P
    P -- timer tick updatedTime==0 finishGame --> F[Game Over - finalScore display]
    F -- retry - phase=.start --> S

    subgraph Timer [50 Hz Timer onReceive]
        T1[compute deadline.timeIntervalSince now]
        T1 --> T2{updatedTime == 0?}
        T2 -- No --> T3[remainingTime = updatedTime]
        T2 -- Yes --> T4[finishGame]
    end

    P -.-> T1
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A([App Launch]) --> S[Start Screen - interval picker]
    S -- startGame --> P[Playing - HUD + circle]
    P -- hitCircle - score++ resetTimer --> P
    P -- timer tick updatedTime==0 finishGame --> F[Game Over - finalScore display]
    F -- retry - phase=.start --> S

    subgraph Timer [50 Hz Timer onReceive]
        T1[compute deadline.timeIntervalSince now]
        T1 --> T2{updatedTime == 0?}
        T2 -- No --> T3[remainingTime = updatedTime]
        T2 -- Yes --> T4[finishGame]
    end

    P -.-> T1
Loading

Fix All in Codex Fix All in Cursor Fix All in Claude Code

Reviews (1): Last reviewed commit: "Add Speedy Circles game" | Re-trigger Greptile

Comment on lines +274 to +277
Text("\(finalScore)")
.font(.system(size: 74, weight: .black, design: .rounded))
.foregroundStyle(.white)
.contentTransition(.numericText())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 contentTransition(.numericText()) will not animate on the game-over screen

finalScore is set synchronously before phase = .finished is assigned in finishGame(). When gameOverScreen is first inserted into the view hierarchy, the text already holds the final value, so there is no numeric change for the transition to interpolate — the score pops in at full value rather than counting up. To get the intended count-up effect, initialize finalScore to 0 and drive it to the real score inside an onAppear animation.

Fix in Codex Fix in Cursor Fix in Claude Code

@State private var circleSize = Constants.startCircleSize
@State private var circleColor = Color.pink

private let timer = Timer.publish(every: 0.02, on: .main, in: .common).autoconnect()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Timer publisher recreated on every render pass

private let timer = Timer.publish(every: 0.02, ...) is a stored property of a value-type View struct. SwiftUI recreates the struct on every render, and because the 50 Hz timer drives a state change on each tick, the publisher object gets a new identity each render cycle. onReceive compares publisher identity; a new Timer.TimerPublisher instance on every render causes continuous subscribe/unsubscribe cycling, which can introduce missed ticks and drains battery even when phase != .playing. The idiomatic fix is to lift the publisher to module scope (a let constant outside the struct) or wrap it in a @StateObject, so a single publisher instance is reused for the view's lifetime.

Fix in Codex Fix in Cursor Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant