Skip to content
RPRichard PruittTech & Cyber
Navigation
repo://simon-says-duo/MyApp/PromptDelivery.swift

MyApp/PromptDelivery.swift

8.6 KB · 280 lines · swift · Download from the repository page

MyApp/PromptDelivery.swift
import Foundationimport AVFoundation enum CuePlacement: String, Codable, CaseIterable, Identifiable, Sendable {    case beginning    case middle    case end    case random     var id: String { rawValue }     var title: String {        switch self {        case .beginning: "Beginning"        case .middle: "Middle"        case .end: "End"        case .random: "Random per challenge"        }    }     static let concrete: [CuePlacement] = [.beginning, .middle, .end]     func resolve(randomIndex: () -> Int) -> CuePlacement {        PromptDelivery.resolvePlacement(self, randomIndex: randomIndex)    }} enum SpeechSpeed: Int, Codable, CaseIterable, Identifiable, Sendable {    case slow = 0    case normal = 1    case fast = 2    case veryFast = 3     var id: Int { rawValue }     var title: String {        switch self {        case .slow: "Slow"        case .normal: "Normal"        case .fast: "Fast"        case .veryFast: "Very fast"        }    }     var rate: Float {        let base = AVSpeechUtteranceDefaultSpeechRate        let raw: Float = switch self {        case .slow: base - 0.08        case .normal: base        case .fast: base + 0.06        case .veryFast: base + 0.11        }        return min(AVSpeechUtteranceMaximumSpeechRate, max(AVSpeechUtteranceMinimumSpeechRate, raw))    }} struct DeliverySnapshot: Codable, Equatable, Sendable {    let speed: SpeechSpeed    let rate: Float    let voiceID: String    let language: String    let configuredPlacement: CuePlacement    let resolvedPlacement: CuePlacement} enum PromptDelivery {    static let leadIn = "For this challenge"     static func cleanAction(_ raw: String) -> String {        let collapsed = raw            .split(whereSeparator: \.isWhitespace)            .joined(separator: " ")            .trimmingCharacters(in: CharacterSet(charactersIn: ".!?"))            .trimmingCharacters(in: .whitespacesAndNewlines)        guard let first = collapsed.first else { return "" }        return first.lowercased() + collapsed.dropFirst()    }     static func capitalized(_ text: String) -> String {        guard let first = text.first else { return "" }        return first.uppercased() + text.dropFirst()    }     static func validationMessage(for customText: String) -> String? {        let trimmed = customText.trimmingCharacters(in: .whitespacesAndNewlines)        guard !trimmed.isEmpty else { return nil }        guard trimmed.unicodeScalars.contains(where: CharacterSet.alphanumerics.contains) else {            return "Enter an action with letters or numbers."        }        let pattern = #"(?i)\bsimon\b[\s\p{P}]*\bsays\b"#        if trimmed.range(of: pattern, options: .regularExpression) != nil {            return "Enter only the action. Use SIMON SAYS or SEND to control the cue."        }        return nil    }     static func resolvePlacement(_ configured: CuePlacement, randomIndex: () -> Int) -> CuePlacement {        guard configured == .random else { return configured }        let choices = CuePlacement.concrete        let index = abs(randomIndex()) % choices.count        return choices[index]    }     static func transcript(action rawAction: String, includesCue: Bool, placement: CuePlacement) -> String {        let action = cleanAction(rawAction)        guard !action.isEmpty else { return "" }        let resolved = placement == .random ? .beginning : placement        switch (includesCue, resolved) {        case (true, .beginning):            return "Simon says, \(action)."        case (false, .beginning):            return "\(capitalized(action))."        case (true, .middle):            return "\(leadIn), Simon says, \(action)."        case (false, .middle):            return "\(leadIn), \(action)."        case (true, .end):            return "\(capitalized(action)). Simon says."        case (false, .end):            return "\(capitalized(action))."        case (_, .random):            return "\(capitalized(action))."        }    }} enum HostTab: String, CaseIterable, Identifiable {    case game = "Game"    case scoreboard = "Scoreboard"    case clips = "Clips"    var id: String { rawValue }    var symbol: String {        switch self {        case .game: "gamecontroller"        case .scoreboard: "list.number"        case .clips: "film.stack"        }    }} enum ClipScope: String, CaseIterable, Identifiable {    case currentGame = "Current Game"    case allSaved = "All Saved"    var id: String { rawValue }} struct ScoreboardEntry: Identifiable, Equatable {    var id: UUID { player.id }    let rank: Int    let player: Player    let points: Int    let correct: Int    let wrong: Int    let completed: Int    var successRate: String {        guard completed > 0 else { return "--" }        let percent = Int((Double(correct) / Double(completed) * 100).rounded())        return "\(percent)%"    }} enum GameQueries {    static func scoreboard(players: [Player], attempts: [Attempt], sessionID: UUID) -> [ScoreboardEntry] {        let raw = players.enumerated().map { offset, player in            let judged = attempts.filter {                $0.snapshot.sessionID == sessionID &&                $0.snapshot.player.id == player.id &&                !$0.voided &&                $0.verdict != nil            }            let correct = judged.filter { $0.verdict == .correct }.count            let wrong = judged.filter { $0.verdict == .wrong }.count            return (offset: offset, player: player, points: correct, correct: correct, wrong: wrong, completed: judged.count)        }.sorted {            if $0.points != $1.points { return $0.points > $1.points }            if $0.correct != $1.correct { return $0.correct > $1.correct }            return $0.offset < $1.offset        }        var rank = 1        return raw.enumerated().map { index, item in            if index > 0 && (item.points != raw[index - 1].points || item.correct != raw[index - 1].correct) {                rank = index + 1            }            return ScoreboardEntry(rank: rank, player: item.player, points: item.points,                                   correct: item.correct, wrong: item.wrong, completed: item.completed)        }    }     static func clips(attempts: [Attempt], sessionID: UUID, scope: ClipScope, playerID: UUID?) -> [Attempt] {        attempts.filter { attempt in            guard attempt.rawFile != nil else { return false }            if scope == .currentGame && attempt.snapshot.sessionID != sessionID { return false }            if let playerID, attempt.snapshot.player.id != playerID { return false }            return true        }    }} struct SavedGame: Codable, Equatable {    var id: UUID    var players: [Player]    var selectedPlayerID: UUID    var hostID: UUID?    var challengesPerTurn: Int    var responseSeconds: Int    var recordingMode: Bool    var deleteAtEnd: Bool    var currentAttemptID: UUID?    var finished: Bool    var turnProgress: [UUID: Int]    var completedTurns: Set<UUID>    var speed: SpeechSpeed    var placement: CuePlacement    var selectedPromptID: String    var customText: String    var fingers: Int    var cueMode: CueMode?    var cueDuration: CueDuration?    var recordingAcknowledgement: RecordingAssent?    var participantAssents: [UUID: RecordingAssent]?    var selectedCategory: ActionCategory? = nil    var hand: HandSelection? = nil    var actionSource: ActionSelectionSource? = nil    var enabledCategories: [ActionCategory]? = nil    var seatedOnly: Bool? = nil} enum CueMode: String, Codable, CaseIterable, Identifiable, Sendable {    case spoken    case visual     var id: String { rawValue }     var title: String {        switch self {        case .spoken: "Spoken Simon Says"        case .visual: "Visual S/Z"        }    }} enum CueDuration: String, Codable, CaseIterable, Identifiable, Sendable {    case brief    case standard    case extended     var id: String { rawValue }     var title: String {        switch self {        case .brief: "Brief (0.5s)"        case .standard: "Standard (0.75s)"        case .extended: "Extended (1.5s)"        }    }     var seconds: Double {        switch self {        case .brief: 0.5        case .standard: 0.75        case .extended: 1.5        }    }} struct VisualCue: Codable, Equatable, Sendable {    let id: UUID    let letter: String    let duration: Double    var requiresAction: Bool { letter == "S" }} struct CueTiming: Codable, Equatable, Sendable {    let start: Double    let duration: Double    func visible(at time: Double) -> Bool {        start.isFinite && duration > 0 && time >= start && time < start + duration    }}