// // WordPuzzleViewB.swift // AIGrammar // // Created by oscar on 2025/7/16. // import AVFoundation var soundID: SystemSoundID = 1104 // 系统键盘点击声 func playKeyClickSound() { AudioServicesPlaySystemSound(soundID) } import SwiftUI struct WordPuzzleView: View { @StateObject private var viewModel = WordPuzzleViewModel() var body: some View { ZStack { Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all) VStack(spacing: 16) { // Top: score & timer Text("Find the Word!") .font(.largeTitle) .bold() .padding(.bottom, 30) HStack { Text("Wins: \(viewModel.dailyWins)") .font(.headline) Spacer() Text("Words: \(viewModel.guessedWords)/4") .font(.headline) Spacer() Text("Time: \(viewModel.timeRemaining)s") .font(.headline) } .padding(.horizontal, 30) .padding(.bottom, 20) // Grid GridView(letters: viewModel.letters, selectedPositions: $viewModel.selectedPositions) Spacer() // Toast toastView .frame(height: 50) .frame(maxWidth: .infinity) //.background(viewModel.showToast ? viewModel.toastColor : Color.pink.opacity(0.0)) .cornerRadius(8) .padding(.bottom, 10) HStack { Button("Shuffle") { viewModel.shuffle() } .padding() .background(Color.orange.opacity(0.7)) .foregroundColor(.white) .cornerRadius(10) .font(.subheadline) // 调整字体大小为标题大小 Spacer() Button("Submit") { viewModel.submit() } .padding() .background(Color.green.opacity(0.7)) .foregroundColor(.white) .cornerRadius(10) .font(.subheadline) // 调整字体大小为标题大小 } .padding(.horizontal) .padding(.bottom, 20) } .padding() .onAppear { viewModel.startTimer() } .onDisappear { viewModel.stopTimer() } } } var toastView: some View { HStack { if viewModel.showToast { if viewModel.toastFlag { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) } else { Image(systemName: "xmark.octagon.fill") .foregroundColor(.red) } Text(viewModel.toastMessage) .font(.body) } } .opacity(viewModel.showToast ? 1 : 0) .animation(.easeInOut, value: viewModel.showToast) } } // MARK: - GridView struct GridView: View { let letters: [[String]] @Binding var selectedPositions: [(row: Int, col: Int)] var body: some View { GeometryReader { geo in let size = geo.size.width * 0.8 VStack(spacing: 2) { ForEach(0.. = [] private var submittedWords: Set = [] private let generator = PuzzleGenerator() init() { generateNewPuzzle() } /// 生成新题 func generateNewPuzzle() { let (words, grid) = generator.generatePuzzle() allWords = words letters = grid guessedWords = 0 selectedPositions.removeAll() timeRemaining = 60 //shuffleCount = 0 submittedWords.removeAll() logger.info("New puzzle with words: \(allWords)") } func startTimer() { stopTimer() timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in if self.timeRemaining > 0 { self.timeRemaining -= 1 } else { self.stopTimer() } } } func stopTimer() { timer?.invalidate() timer = nil } func submit() { let word = selectedWord().uppercased() selectedPositions.removeAll() guard !word.isEmpty else { return } if timeRemaining <= 0 { showToast(message: "Time's up! Shuffle for new game.", succ: false) return } if word.count < 3 { showToast(message: "At least three words.", succ: false) return } if submittedWords.contains(word) { showToast(message: "Already submitted!", succ: false) return } if generator.validate(word: word) { guessedWords += 1 submittedWords.insert(word) // 记录 showToast(message: "Correct!", succ: true) } else { showToast(message: "Please try again", succ: false) } if guessedWords >= 4 { dailyWins += 1 showToast(message: "Congratulations! Next puzzle...", succ: true) generateNewPuzzle() } } func shuffle() { if shuffleCount >= 10 { showToast(message: "No more shuffles today", succ: false) return } shuffleCount += 1 generateNewPuzzle() startTimer() } private func selectedWord() -> String { selectedPositions.map { letters[$0.row][$0.col] }.joined() } private func showToast(message: String, succ: Bool) { toastMessage = message toastFlag = succ showToast = true DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.showToast = false } } } struct WordPuzzleView_Previews: PreviewProvider { static var previews: some View { WordPuzzleView() } } #Preview { WordPuzzleView() }