289 lines
8.9 KiB
Swift
289 lines
8.9 KiB
Swift
//
|
|
// 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..<letters.count, id: \.self) { row in
|
|
HStack(spacing: 2) {
|
|
ForEach(0..<letters[row].count, id: \.self) { col in
|
|
let selected = selectedPositions.contains { $0.row == row && $0.col == col }
|
|
Text(letters[row][col])
|
|
.frame(width: size / CGFloat(letters.count),
|
|
height: size / CGFloat(letters.count))
|
|
.background(
|
|
ZStack {
|
|
if selected {
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(Color.blue.opacity(0.7))
|
|
} else {
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(Color.white)
|
|
.shadow(color: .gray.opacity(0.3), radius: 2, x: 0, y: 2)
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.stroke(Color.gray.opacity(0.3), lineWidth: 0.5)
|
|
}
|
|
}
|
|
)
|
|
.foregroundColor(selected ? .white : .black)
|
|
.onTapGesture {
|
|
toggleSelection(row: row, col: col)
|
|
playKeyClickSound()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(width: size, height: size)
|
|
.padding()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
|
}
|
|
.frame(height: 300) // 给个固定高度
|
|
}
|
|
|
|
private func toggleSelection(row: Int, col: Int) {
|
|
if let index = selectedPositions.firstIndex(where: { $0.row == row && $0.col == col }) {
|
|
selectedPositions.remove(at: index)
|
|
} else {
|
|
selectedPositions.append((row, col))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewModel
|
|
class WordPuzzleViewModel: ObservableObject {
|
|
@Published var letters: [[String]] = []
|
|
@Published var selectedPositions: [(row: Int, col: Int)] = []
|
|
@Published var guessedWords: Int = 0
|
|
@Published var dailyWins: Int = 0
|
|
@Published var timeRemaining: Int = 60
|
|
@Published var shuffleCount: Int = 0
|
|
|
|
@Published var showToast = false
|
|
@Published var toastFlag = false
|
|
@Published var toastMessage = ""
|
|
|
|
private var timer: Timer?
|
|
private var allWords: Set<String> = []
|
|
private var submittedWords: Set<String> = []
|
|
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()
|
|
}
|