modify files

This commit is contained in:
2025-07-18 20:44:44 +08:00
parent 197dcbfc03
commit 01a5ae0042
8 changed files with 3487 additions and 1 deletions

View File

@ -0,0 +1,7 @@
//
// Untitled.swift
// AIGrammar
//
// Created by oscar on 2025/7/16.
//

View File

@ -0,0 +1,153 @@
//
// 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()
}

View File

@ -0,0 +1,333 @@
//
// 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 = ""
@Published var toastColor = Color.green
private var timer: Timer?
private var allWords: Set<String> = ["WORD", "HAVE", "SWIFT", "CODE"] //
private var wordList: [String] = []
init() {
loadWordList()
generateNewPuzzle()
}
///
private func loadWordList() {
if let url = Bundle.main.url(forResource: "wordlist", withExtension: "txt") {
do {
let content = try String(contentsOf: url)
wordList = content
.components(separatedBy: .newlines)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() }
.filter { !$0.isEmpty }
} catch {
logger.warning("Failed to load word list: \(error)")
}
} else {
logger.warning("wordlist.txt not found!")
}
logger.info("load wordlist. total words: \(wordList.count)")
}
///
func generateNewPuzzle() {
if wordList.isEmpty {
logger.warning("Word list is empty")
wordList = ["WORD", "HAVE", "SWIFT", "CODE"] //
}
var chosenWords: Set<String> = []
var uniqueLetters: Set<Character> = []
while (uniqueLetters.count < 16 && chosenWords.count < 6) || chosenWords.count < 4 {
if let word = wordList.randomElement() {
chosenWords.insert(word)
uniqueLetters.formUnion(word)
}
}
allWords = chosenWords
// uniqueLetters 16
var selectedLetters = Array(uniqueLetters)
let alphabet = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
while selectedLetters.count < 16 {
let randomLetter = alphabet.randomElement()!
if !selectedLetters.contains(randomLetter) {
selectedLetters.append(randomLetter)
}
}
//
let shuffledLetters = selectedLetters.shuffled()
letters = Array(repeating: Array(repeating: "", count: 4), count: 4)
for i in 0..<4 {
for j in 0..<4 {
letters[i][j] = String(shuffledLetters[i * 4 + j])
}
}
guessedWords = 0
selectedPositions.removeAll()
timeRemaining = 60
//shuffleCount = 0
logger.info("New puzzle generated with words: \(allWords) and letters: \(shuffledLetters)")
}
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!", succ: false)
return
}
if allWords.contains(word) {
guessedWords += 1
showToast(message: "Correct!", succ: true)
} else {
showToast(message: "Please try again", succ: false)
}
if guessedWords >= 4 {
//stopTimer()
dailyWins += 1
showToast(message: "Congratulations! \nStart next puzzle...", succ: true)
generateNewPuzzle() //
}
}
func shuffle() {
if shuffleCount >= 10 {
showToast(message: "No more shuffles today", succ: false)
return
}
shuffleCount += 1
generateNewPuzzle()
}
private func selectedWord() -> String {
var result = ""
for pos in selectedPositions {
result += letters[pos.row][pos.col]
}
return result
}
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()
}