514 lines
22 KiB
Markdown
514 lines
22 KiB
Markdown
# Agent guide for Swift and SwiftUI
|
|
|
|
This repository contains an Xcode project written with Swift and SwiftUI. Please follow the guidelines below so that the development experience is built on modern, safe API usage.
|
|
|
|
## Additional context files (read first)
|
|
|
|
- `README.md` — product scope, features, and project structure
|
|
- `ai_implmentation.md` — AI implementation context and architecture notes
|
|
|
|
|
|
## Role
|
|
|
|
You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and related frameworks. Your code must always adhere to Apple's Human Interface Guidelines and App Review guidelines.
|
|
|
|
|
|
## Core instructions
|
|
|
|
- Target iOS 26.0 or later. (Yes, it definitely exists.)
|
|
- Swift 6.2 or later, using modern Swift concurrency.
|
|
- SwiftUI backed up by `@Observable` classes for shared data.
|
|
- **Prioritize Protocol-Oriented Programming (POP)** for reusability and testability—see dedicated section below.
|
|
- Do not introduce third-party frameworks without asking first.
|
|
- Avoid UIKit unless requested.
|
|
|
|
|
|
## Protocol-Oriented Programming (POP)
|
|
|
|
**Protocol-first architecture is a priority.** When designing new features or reviewing existing code, always think about protocols and composition before concrete implementations. This enables code reuse across games, easier testing, and cleaner architecture.
|
|
|
|
### When architecting new code:
|
|
|
|
1. **Start with the protocol**: Before writing a concrete type, ask "What capability am I defining?" and express it as a protocol.
|
|
2. **Identify shared behavior**: If multiple types will need similar functionality, define a protocol first.
|
|
3. **Use protocol extensions for defaults**: Provide sensible default implementations to reduce boilerplate.
|
|
4. **Prefer composition over inheritance**: Combine multiple protocols rather than building deep class hierarchies.
|
|
|
|
### When reviewing existing code for reuse:
|
|
|
|
1. **Look for duplicated patterns**: If you see similar logic in Blackjack and Baccarat, extract a protocol to `CasinoKit`.
|
|
2. **Identify common interfaces**: Types that expose similar properties/methods are candidates for protocol unification.
|
|
3. **Check before implementing**: Before writing new code, search for existing protocols that could be adopted or extended.
|
|
4. **Propose refactors proactively**: When you spot an opportunity to extract a protocol, mention it.
|
|
|
|
### Protocol design guidelines:
|
|
|
|
- **Name protocols for capabilities**: Use `-able`, `-ing`, or `-Provider` suffixes (e.g., `Bettable`, `CardDealing`, `StatisticsProvider`).
|
|
- **Keep protocols focused**: Each protocol should represent one capability (Interface Segregation Principle).
|
|
- **Use associated types sparingly**: Prefer concrete types or generics at the call site when possible.
|
|
- **Constrain to `AnyObject` only when needed**: Prefer value semantics unless reference semantics are required.
|
|
|
|
### Examples
|
|
|
|
**❌ BAD - Concrete implementations without protocols:**
|
|
```swift
|
|
// Blackjack/GameState.swift
|
|
@Observable @MainActor
|
|
class BlackjackGameState {
|
|
var balance: Int = 1000
|
|
var currentBet: Int = 0
|
|
func placeBet(_ amount: Int) { ... }
|
|
func resetBet() { ... }
|
|
}
|
|
|
|
// Baccarat/GameState.swift - duplicates the same pattern
|
|
@Observable @MainActor
|
|
class BaccaratGameState {
|
|
var balance: Int = 1000
|
|
var currentBet: Int = 0
|
|
func placeBet(_ amount: Int) { ... }
|
|
func resetBet() { ... }
|
|
}
|
|
```
|
|
|
|
**✅ GOOD - Protocol in CasinoKit, adopted by games:**
|
|
```swift
|
|
// CasinoKit/Protocols/Bettable.swift
|
|
protocol Bettable: AnyObject {
|
|
var balance: Int { get set }
|
|
var currentBet: Int { get set }
|
|
var minimumBet: Int { get }
|
|
var maximumBet: Int { get }
|
|
|
|
func placeBet(_ amount: Int)
|
|
func resetBet()
|
|
}
|
|
|
|
extension Bettable {
|
|
func placeBet(_ amount: Int) {
|
|
guard amount <= balance else { return }
|
|
currentBet += amount
|
|
balance -= amount
|
|
}
|
|
|
|
func resetBet() {
|
|
balance += currentBet
|
|
currentBet = 0
|
|
}
|
|
}
|
|
|
|
// Blackjack/GameState.swift - adopts protocol
|
|
@Observable @MainActor
|
|
class BlackjackGameState: Bettable {
|
|
var balance: Int = 1000
|
|
var currentBet: Int = 0
|
|
var minimumBet: Int { settings.minBet }
|
|
var maximumBet: Int { settings.maxBet }
|
|
// placeBet and resetBet come from protocol extension
|
|
}
|
|
```
|
|
|
|
**❌ BAD - View only works with one concrete type:**
|
|
```swift
|
|
struct ChipSelectorView: View {
|
|
@Bindable var state: BlackjackGameState
|
|
// Tightly coupled to Blackjack
|
|
}
|
|
```
|
|
|
|
**✅ GOOD - View works with any Bettable type:**
|
|
```swift
|
|
struct ChipSelectorView<State: Bettable & Observable>: View {
|
|
@Bindable var state: State
|
|
// Reusable across all games
|
|
}
|
|
```
|
|
|
|
### Common protocols to consider extracting:
|
|
|
|
| Capability | Protocol Name | Shared By |
|
|
|------------|---------------|-----------|
|
|
| Betting mechanics | `Bettable` | All games |
|
|
| Statistics tracking | `StatisticsProvider` | All games |
|
|
| Game settings | `GameConfigurable` | All games |
|
|
| Card management | `CardProviding` | Card games |
|
|
| Round lifecycle | `RoundManaging` | All games |
|
|
| Result calculation | `ResultCalculating` | All games |
|
|
|
|
### Refactoring checklist:
|
|
|
|
When you encounter code that could benefit from POP:
|
|
|
|
- [ ] Is this logic duplicated across multiple games?
|
|
- [ ] Could this type conform to an existing protocol in CasinoKit?
|
|
- [ ] Would extracting a protocol make this code testable in isolation?
|
|
- [ ] Can views be made generic over a protocol instead of a concrete type?
|
|
- [ ] Would a protocol extension reduce boilerplate across conforming types?
|
|
|
|
### Benefits:
|
|
|
|
- **Reusability**: Shared protocols in `CasinoKit` work across all games
|
|
- **Testability**: Mock types can conform to protocols for unit testing
|
|
- **Flexibility**: New games can adopt existing protocols immediately
|
|
- **Maintainability**: Fix a bug in a protocol extension, fix it everywhere
|
|
- **Discoverability**: Protocols document the expected interface clearly
|
|
|
|
|
|
## Swift instructions
|
|
|
|
- Always mark `@Observable` classes with `@MainActor`.
|
|
- Assume strict Swift concurrency rules are being applied.
|
|
- Prefer Swift-native alternatives to Foundation methods where they exist, such as using `replacing("hello", with: "world")` with strings rather than `replacingOccurrences(of: "hello", with: "world")`.
|
|
- Prefer modern Foundation API, for example `URL.documentsDirectory` to find the app's documents directory, and `appending(path:)` to append strings to a URL.
|
|
- Never use C-style number formatting such as `Text(String(format: "%.2f", abs(myNumber)))`; always use `Text(abs(change), format: .number.precision(.fractionLength(2)))` instead.
|
|
- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`.
|
|
- Never use old-style Grand Central Dispatch concurrency such as `DispatchQueue.main.async()`. If behavior like this is needed, always use modern Swift concurrency.
|
|
- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()`.
|
|
- Avoid force unwraps and force `try` unless it is unrecoverable.
|
|
|
|
|
|
## SwiftUI instructions
|
|
|
|
- Always use `foregroundStyle()` instead of `foregroundColor()`.
|
|
- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`.
|
|
- Always use the `Tab` API instead of `tabItem()`.
|
|
- Never use `ObservableObject`; always prefer `@Observable` classes instead.
|
|
- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none.
|
|
- Never use `onTapGesture()` unless you specifically need to know a tap's location or the number of taps. All other usages should use `Button`.
|
|
- Never use `Task.sleep(nanoseconds:)`; always use `Task.sleep(for:)` instead.
|
|
- Never use `UIScreen.main.bounds` to read the size of the available space.
|
|
- Do not break views up using computed properties; place them into new `View` structs instead.
|
|
- Do not force specific font sizes; prefer using Dynamic Type instead.
|
|
- Use the `navigationDestination(for:)` modifier to specify navigation, and always use `NavigationStack` instead of the old `NavigationView`.
|
|
- If using an image for a button label, always specify text alongside like this: `Button("Tap me", systemImage: "plus", action: myButtonAction)`.
|
|
- When rendering SwiftUI views, always prefer using `ImageRenderer` to `UIGraphicsImageRenderer`.
|
|
- Don't apply the `fontWeight()` modifier unless there is good reason. If you want to make some text bold, always use `bold()` instead of `fontWeight(.bold)`.
|
|
- Do not use `GeometryReader` if a newer alternative would work as well, such as `containerRelativeFrame()` or `visualEffect()`.
|
|
- When making a `ForEach` out of an `enumerated` sequence, do not convert it to an array first. So, prefer `ForEach(x.enumerated(), id: \.element.id)` instead of `ForEach(Array(x.enumerated()), id: \.element.id)`.
|
|
- When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer.
|
|
- Avoid `AnyView` unless it is absolutely required.
|
|
- **Never use raw numeric literals** for padding, spacing, opacity, font sizes, dimensions, corner radii, shadows, or animation durations—always use Design constants (see "No magic numbers" section).
|
|
- **Never use inline `Color(red:green:blue:)` or hex colors**—define all colors in the `Color` extension in `DesignConstants.swift` with semantic names.
|
|
- Avoid using UIKit colors in SwiftUI code.
|
|
|
|
|
|
## View/State separation (MVVM-lite)
|
|
|
|
**Views should be "dumb" renderers.** All business logic belongs in `GameState` or dedicated view models.
|
|
|
|
### What belongs in the State/ViewModel:
|
|
- **Business logic**: Calculations, validations, game rules
|
|
- **Computed properties based on game data**: hints, recommendations, derived values
|
|
- **State checks**: `isPlayerTurn`, `canHit`, `isGameOver`, `isBetBelowMinimum`
|
|
- **Data transformations**: statistics calculations, filtering, aggregations
|
|
|
|
### What is acceptable in Views:
|
|
- **Pure UI layout logic**: `isIPad`, `maxContentWidth` based on size class
|
|
- **Visual styling**: color selection based on state (`valueColor`, `resultColor`)
|
|
- **@ViewBuilder sub-views**: breaking up complex layouts
|
|
- **Accessibility labels**: combining data into accessible descriptions
|
|
|
|
### Examples
|
|
|
|
**❌ BAD - Business logic in view:**
|
|
```swift
|
|
struct MyView: View {
|
|
@Bindable var state: GameState
|
|
|
|
private var isBetBelowMinimum: Bool {
|
|
state.currentBet > 0 && state.currentBet < state.settings.minBet
|
|
}
|
|
|
|
private var currentHint: String? {
|
|
guard let hand = state.activeHand else { return nil }
|
|
return state.engine.getHint(playerHand: hand, dealerUpCard: upCard)
|
|
}
|
|
}
|
|
```
|
|
|
|
**✅ GOOD - Logic in GameState, view just reads:**
|
|
```swift
|
|
// In GameState:
|
|
var isBetBelowMinimum: Bool {
|
|
currentBet > 0 && currentBet < settings.minBet
|
|
}
|
|
|
|
var currentHint: String? {
|
|
guard settings.showHints, isPlayerTurn else { return nil }
|
|
guard let hand = activeHand, let upCard = dealerUpCard else { return nil }
|
|
return engine.getHint(playerHand: hand, dealerUpCard: upCard)
|
|
}
|
|
|
|
// In View:
|
|
if state.isBetBelowMinimum { ... }
|
|
if let hint = state.currentHint { HintView(hint: hint) }
|
|
```
|
|
|
|
### Benefits:
|
|
- **Testable**: GameState logic can be unit tested without UI
|
|
- **Single source of truth**: No duplicated logic across views
|
|
- **Cleaner views**: Views focus purely on layout and presentation
|
|
- **Easier debugging**: Logic is centralized, not scattered
|
|
|
|
|
|
## SwiftData instructions
|
|
|
|
If SwiftData is configured to use CloudKit:
|
|
|
|
- Never use `@Attribute(.unique)`.
|
|
- Model properties must always either have default values or be marked as optional.
|
|
- All relationships must be marked optional.
|
|
|
|
|
|
## Localization instructions
|
|
|
|
- Use **String Catalogs** (`.xcstrings` files) for localization—this is Apple's modern approach for iOS 17+.
|
|
- SwiftUI `Text("literal")` views automatically look up strings in the String Catalog; no additional code is needed for static strings.
|
|
- For strings outside of `Text` views or with dynamic content, use `String(localized:)` or create a helper extension:
|
|
```swift
|
|
extension String {
|
|
static func localized(_ key: String) -> String {
|
|
String(localized: String.LocalizationValue(key))
|
|
}
|
|
static func localized(_ key: String, _ arguments: CVarArg...) -> String {
|
|
let format = String(localized: String.LocalizationValue(key))
|
|
return String(format: format, arguments: arguments)
|
|
}
|
|
}
|
|
```
|
|
- For format strings with interpolation (e.g., "Balance: $%@"), define a key in the String Catalog and use `String.localized("key", value)`.
|
|
- Store all user-facing strings in the String Catalog; avoid hardcoding strings directly in views.
|
|
- Support at minimum: English (en), Spanish-Mexico (es-MX), and French-Canada (fr-CA).
|
|
- Never use `NSLocalizedString`; prefer the modern `String(localized:)` API.
|
|
|
|
|
|
## No magic numbers or hardcoded values
|
|
|
|
**Never use raw numeric literals or hardcoded colors directly in views.** All values must be extracted to named constants, enums, or variables. This applies to:
|
|
|
|
### Values that MUST be constants:
|
|
- **Spacing & Padding**: `.padding(Design.Spacing.medium)` not `.padding(12)`
|
|
- **Corner Radii**: `Design.CornerRadius.large` not `cornerRadius: 16`
|
|
- **Font Sizes**: `Design.BaseFontSize.body` not `size: 14`
|
|
- **Opacity Values**: `Design.Opacity.strong` not `.opacity(0.7)`
|
|
- **Colors**: `Color.Primary.accent` not `Color(red: 0.8, green: 0.6, blue: 0.2)`
|
|
- **Line Widths**: `Design.LineWidth.medium` not `lineWidth: 2`
|
|
- **Shadow Values**: `Design.Shadow.radiusLarge` not `radius: 10`
|
|
- **Animation Durations**: `Design.Animation.quick` not `duration: 0.3`
|
|
- **Component Sizes**: `Design.Size.chipBadge` not `frame(width: 32)`
|
|
|
|
### What to do when you see a magic number:
|
|
1. Check if an appropriate constant already exists in `DesignConstants.swift`
|
|
2. If not, add a new constant with a semantic name
|
|
3. Use the constant in place of the raw value
|
|
4. If it's truly view-specific and used only once, extract to a `private let` at the top of the view struct
|
|
|
|
### Examples of violations:
|
|
```swift
|
|
// ❌ BAD - Magic numbers everywhere
|
|
.padding(16)
|
|
.opacity(0.6)
|
|
.frame(width: 80, height: 52)
|
|
.shadow(radius: 10, y: 5)
|
|
Color(red: 0.25, green: 0.3, blue: 0.45)
|
|
|
|
// ✅ GOOD - Named constants
|
|
.padding(Design.Spacing.large)
|
|
.opacity(Design.Opacity.accent)
|
|
.frame(width: Design.Size.bonusZoneWidth, height: Design.Size.topBetRowHeight)
|
|
.shadow(radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
|
|
Color.BettingZone.dragonBonusLight
|
|
```
|
|
|
|
|
|
## Design constants instructions
|
|
|
|
- Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing:
|
|
```swift
|
|
enum Design {
|
|
enum Spacing {
|
|
static let xxSmall: CGFloat = 2
|
|
static let xSmall: CGFloat = 4
|
|
static let small: CGFloat = 8
|
|
static let medium: CGFloat = 12
|
|
static let large: CGFloat = 16
|
|
static let xLarge: CGFloat = 20
|
|
}
|
|
enum CornerRadius {
|
|
static let small: CGFloat = 8
|
|
static let medium: CGFloat = 12
|
|
static let large: CGFloat = 16
|
|
}
|
|
enum BaseFontSize {
|
|
static let small: CGFloat = 10
|
|
static let body: CGFloat = 14
|
|
static let large: CGFloat = 18
|
|
static let title: CGFloat = 24
|
|
}
|
|
enum Opacity {
|
|
static let subtle: Double = 0.1
|
|
static let hint: Double = 0.2
|
|
static let light: Double = 0.3
|
|
static let medium: Double = 0.5
|
|
static let accent: Double = 0.6
|
|
static let strong: Double = 0.7
|
|
static let heavy: Double = 0.8
|
|
static let almostFull: Double = 0.9
|
|
}
|
|
enum LineWidth {
|
|
static let thin: CGFloat = 1
|
|
static let medium: CGFloat = 2
|
|
static let thick: CGFloat = 3
|
|
}
|
|
enum Shadow {
|
|
static let radiusSmall: CGFloat = 2
|
|
static let radiusMedium: CGFloat = 6
|
|
static let radiusLarge: CGFloat = 10
|
|
static let offsetSmall: CGFloat = 1
|
|
static let offsetMedium: CGFloat = 3
|
|
}
|
|
enum Animation {
|
|
static let quick: Double = 0.3
|
|
static let springDuration: Double = 0.4
|
|
static let staggerDelay1: Double = 0.1
|
|
static let staggerDelay2: Double = 0.25
|
|
}
|
|
}
|
|
```
|
|
- For colors used across the app, extend `Color` with semantic color definitions:
|
|
```swift
|
|
extension Color {
|
|
enum Primary {
|
|
static let background = Color(red: 0.1, green: 0.2, blue: 0.3)
|
|
static let accent = Color(red: 0.8, green: 0.6, blue: 0.2)
|
|
}
|
|
enum Button {
|
|
static let goldLight = Color(red: 1.0, green: 0.85, blue: 0.3)
|
|
static let goldDark = Color(red: 0.9, green: 0.7, blue: 0.2)
|
|
}
|
|
}
|
|
```
|
|
- Within each view, extract view-specific magic numbers to private constants at the top of the struct with a comment explaining why they're local:
|
|
```swift
|
|
struct MyView: View {
|
|
// Layout: fixed card dimensions for consistent appearance
|
|
private let cardWidth: CGFloat = 45
|
|
// Typography: constrained space requires fixed size
|
|
private let headerFontSize: CGFloat = 18
|
|
// ...
|
|
}
|
|
```
|
|
- Reference design constants in views: `Design.Spacing.medium`, `Design.CornerRadius.large`, `Color.Primary.accent`.
|
|
- Keep design constants organized by category: Spacing, CornerRadius, BaseFontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
|
|
- When adding new features, check existing constants first before creating new ones.
|
|
- Name constants semantically (what they represent) not literally (their value): `accent` not `pointSix`, `large` not `sixteen`.
|
|
|
|
|
|
## Dynamic Type instructions
|
|
|
|
- Always support Dynamic Type for accessibility; never use fixed font sizes without scaling.
|
|
- Use `@ScaledMetric` to scale custom font sizes and dimensions based on user accessibility settings:
|
|
```swift
|
|
struct MyView: View {
|
|
@ScaledMetric(relativeTo: .body) private var bodyFontSize: CGFloat = 14
|
|
@ScaledMetric(relativeTo: .title) private var titleFontSize: CGFloat = 24
|
|
@ScaledMetric(relativeTo: .caption) private var chipTextSize: CGFloat = 11
|
|
|
|
var body: some View {
|
|
Text("Hello")
|
|
.font(.system(size: bodyFontSize, weight: .medium))
|
|
}
|
|
}
|
|
```
|
|
- Choose the appropriate `relativeTo` text style based on the semantic purpose:
|
|
- `.largeTitle`, `.title`, `.title2`, `.title3` for headings
|
|
- `.headline`, `.subheadline` for emphasized content
|
|
- `.body` for main content
|
|
- `.callout`, `.footnote`, `.caption`, `.caption2` for smaller text
|
|
- For constrained UI elements (chips, cards, badges) where overflow would break the design, you may use fixed sizes but document the reason:
|
|
```swift
|
|
// Fixed size: chip face has strict space constraints
|
|
private let chipValueFontSize: CGFloat = 11
|
|
```
|
|
- Prefer system text styles when possible: `.font(.body)`, `.font(.title)`, `.font(.caption)`.
|
|
- Test with accessibility settings: Settings > Accessibility > Display & Text Size > Larger Text.
|
|
|
|
|
|
## VoiceOver accessibility instructions
|
|
|
|
- All interactive elements (buttons, betting zones, selectable items) must have meaningful `.accessibilityLabel()`.
|
|
- Use `.accessibilityValue()` to communicate dynamic state (e.g., current bet amount, selection state, hand value).
|
|
- Use `.accessibilityHint()` to describe what will happen when interacting with an element:
|
|
```swift
|
|
Button("Deal", action: deal)
|
|
.accessibilityHint("Deals cards and starts the round")
|
|
```
|
|
- Use `.accessibilityAddTraits()` to communicate element type:
|
|
- `.isButton` for tappable elements that aren't SwiftUI Buttons
|
|
- `.isHeader` for section headers
|
|
- `.isModal` for modal overlays
|
|
- `.updatesFrequently` for live-updating content
|
|
- Hide purely decorative elements from VoiceOver:
|
|
```swift
|
|
TableBackgroundView()
|
|
.accessibilityHidden(true) // Decorative element
|
|
```
|
|
- Group related elements to reduce VoiceOver navigation complexity:
|
|
```swift
|
|
VStack {
|
|
handLabel
|
|
cardStack
|
|
valueDisplay
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel("Player hand")
|
|
.accessibilityValue("Ace of Hearts, King of Spades. Value: 1")
|
|
```
|
|
- For complex elements, use `.accessibilityElement(children: .contain)` to allow navigation to children while adding context.
|
|
- Post accessibility announcements for important events:
|
|
```swift
|
|
Task { @MainActor in
|
|
try? await Task.sleep(for: .milliseconds(500))
|
|
UIAccessibility.post(notification: .announcement, argument: "Player wins!")
|
|
}
|
|
```
|
|
- Provide accessibility names for model types that appear in UI:
|
|
```swift
|
|
enum Suit {
|
|
var accessibilityName: String {
|
|
switch self {
|
|
case .hearts: return String(localized: "Hearts")
|
|
// ...
|
|
}
|
|
}
|
|
}
|
|
```
|
|
- Test with VoiceOver enabled: Settings > Accessibility > VoiceOver.
|
|
|
|
|
|
## Project structure
|
|
|
|
- Use a consistent project structure, with folder layout determined by app features.
|
|
- Follow strict naming conventions for types, properties, methods, and SwiftData models.
|
|
- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file.
|
|
- Write unit tests for core application logic.
|
|
- Only write UI tests if unit tests are not possible.
|
|
- Add code comments and documentation comments as needed.
|
|
- If the project requires secrets such as API keys, never include them in the repository.
|
|
|
|
|
|
## Documentation instructions
|
|
|
|
- **Always keep each game's `README.md` file up to date** when adding new functionality or making changes that users or developers need to know about.
|
|
- Document new features, settings, or gameplay mechanics in the appropriate game's README.
|
|
- Update the README when modifying existing behavior that affects how the game works.
|
|
- Include any configuration options, keyboard shortcuts, or special interactions.
|
|
- If adding a new game to the workspace, create a comprehensive README following the existing games' format.
|
|
- README updates should be part of the same commit as the feature/change they document.
|
|
|
|
|
|
## PR instructions
|
|
|
|
- If installed, make sure SwiftLint returns no warnings or errors before committing.
|
|
- Verify that the game's README.md reflects any new functionality or behavioral changes.
|