154 lines
5.1 KiB
Swift
154 lines
5.1 KiB
Swift
//
|
|
// WordPuzzleView.swift
|
|
// AIGrammar
|
|
//
|
|
// Created by oscar on 2025/7/16.
|
|
//
|
|
import SwiftUI
|
|
import AVFoundation
|
|
|
|
struct WordPuzzleGameView: View {
|
|
@State private var letters: [[String]] = []
|
|
@State private var selectedLetters: [(Int, Int)] = []
|
|
@State private var foundWords: [String] = []
|
|
@State private var score: Int = 0
|
|
@State private var remainingTime = 60
|
|
@State private var isGameOver = false
|
|
|
|
@GestureState private var dragLocation: CGPoint? = nil
|
|
@State private var audioPlayer: AVAudioPlayer?
|
|
|
|
let validWords = ["WORD", "GAME", "PUZZLE", "MET", "ME", "GO"]
|
|
let gridSize = 4
|
|
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
|
|
|
init() {
|
|
var chars = Array("WORDPUZZLEGAMETO").map { String($0) }
|
|
chars.shuffle()
|
|
_letters = State(initialValue: stride(from: 0, to: chars.count, by: gridSize).map {
|
|
Array(chars[$0..<$0+gridSize])
|
|
})
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
Text("Time: \(remainingTime)s Score: \(score)")
|
|
.font(.headline)
|
|
|
|
if isGameOver {
|
|
Text("Game Over!")
|
|
.font(.largeTitle)
|
|
.foregroundColor(.red)
|
|
} else {
|
|
VStack(spacing: 4) {
|
|
ForEach(0..<gridSize, id: \.self) { row in
|
|
HStack(spacing: 4) {
|
|
ForEach(0..<gridSize, id: \.self) { col in
|
|
Text(letters[row][col])
|
|
.font(.title)
|
|
.frame(width: 50, height: 50)
|
|
.background(
|
|
selectedLetters.contains(where: { $0 == (row, col) }) ?
|
|
Color.blue.opacity(0.7) : Color.gray.opacity(0.2)
|
|
)
|
|
.cornerRadius(8)
|
|
.onTapGesture {
|
|
handleSelect(row: row, col: col)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.gesture(
|
|
DragGesture()
|
|
.updating($dragLocation) { value, state, _ in
|
|
state = value.location
|
|
selectLetter(at: value.location)
|
|
}
|
|
.onEnded { _ in
|
|
submitWord()
|
|
}
|
|
)
|
|
}
|
|
|
|
ScrollView(.horizontal) {
|
|
HStack {
|
|
ForEach(foundWords, id: \.self) { word in
|
|
Text(word)
|
|
.padding(6)
|
|
.background(Color.green.opacity(0.3))
|
|
.cornerRadius(6)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
.padding()
|
|
.onReceive(timer) { _ in
|
|
guard !isGameOver else { return }
|
|
if remainingTime > 0 {
|
|
remainingTime -= 1
|
|
} else {
|
|
isGameOver = true
|
|
timer.upstream.connect().cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 点击选择
|
|
private func handleSelect(row: Int, col: Int) {
|
|
if !selectedLetters.contains(where: { $0 == (row, col) }) {
|
|
selectedLetters.append((row, col))
|
|
}
|
|
}
|
|
|
|
/// 拖动选择
|
|
private func selectLetter(at point: CGPoint?) {
|
|
guard let point = point else { return }
|
|
for row in 0..<gridSize {
|
|
for col in 0..<gridSize {
|
|
let cellSize: CGFloat = 50 + 4
|
|
let cellOrigin = CGPoint(x: CGFloat(col) * cellSize + 20,
|
|
y: CGFloat(row) * cellSize + 120)
|
|
let cellRect = CGRect(origin: cellOrigin, size: CGSize(width: 50, height: 50))
|
|
if cellRect.contains(point) {
|
|
if !selectedLetters.contains(where: { $0 == (row, col) }) {
|
|
selectedLetters.append((row, col))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 提交选中的单词
|
|
private func submitWord() {
|
|
let word = selectedLetters.map { letters[$0.0][$0.1] }.joined()
|
|
|
|
if validWords.contains(word) && !foundWords.contains(word) {
|
|
foundWords.append(word)
|
|
score += word.count * 10
|
|
playSound(named: "success")
|
|
} else {
|
|
playSound(named: "fail")
|
|
}
|
|
selectedLetters.removeAll()
|
|
}
|
|
|
|
/// 播放提示音
|
|
private func playSound(named name: String) {
|
|
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else { return }
|
|
audioPlayer = try? AVAudioPlayer(contentsOf: url)
|
|
audioPlayer?.play()
|
|
}
|
|
}
|
|
|
|
struct WordPuzzleGameView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
WordPuzzleGameView()
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
WordPuzzleGameView()
|
|
}
|