modify files
This commit is contained in:
@ -28,7 +28,6 @@
|
|||||||
555027392C81C0ED00A05441 /* QCloudTTS.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 555027382C81C0ED00A05441 /* QCloudTTS.xcframework */; };
|
555027392C81C0ED00A05441 /* QCloudTTS.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 555027382C81C0ED00A05441 /* QCloudTTS.xcframework */; };
|
||||||
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5550273B2C8322F800A05441 /* PushHandler.swift */; };
|
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5550273B2C8322F800A05441 /* PushHandler.swift */; };
|
||||||
5586E0882C80AD2D00026733 /* TTSManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5586E0872C80AD2D00026733 /* TTSManager.swift */; };
|
5586E0882C80AD2D00026733 /* TTSManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5586E0872C80AD2D00026733 /* TTSManager.swift */; };
|
||||||
558DB7A32E27A91A004D6ADB /* WordPuzzleGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DB7A22E27A91A004D6ADB /* WordPuzzleGameView.swift */; };
|
|
||||||
558DB7A52E27B049004D6ADB /* WordPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DB7A42E27B049004D6ADB /* WordPuzzleView.swift */; };
|
558DB7A52E27B049004D6ADB /* WordPuzzleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DB7A42E27B049004D6ADB /* WordPuzzleView.swift */; };
|
||||||
558DB7B22E2A78A5004D6ADB /* wordlist.txt in Resources */ = {isa = PBXBuildFile; fileRef = 558DB7B12E2A78A5004D6ADB /* wordlist.txt */; };
|
558DB7B22E2A78A5004D6ADB /* wordlist.txt in Resources */ = {isa = PBXBuildFile; fileRef = 558DB7B12E2A78A5004D6ADB /* wordlist.txt */; };
|
||||||
559E6D7C2C34EAE700C971B9 /* IapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E6D7B2C34EAE700C971B9 /* IapManager.swift */; };
|
559E6D7C2C34EAE700C971B9 /* IapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E6D7B2C34EAE700C971B9 /* IapManager.swift */; };
|
||||||
@ -40,6 +39,8 @@
|
|||||||
55BC47492C3A383A00120A7D /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC47482C3A383A00120A7D /* LoadingView.swift */; };
|
55BC47492C3A383A00120A7D /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC47482C3A383A00120A7D /* LoadingView.swift */; };
|
||||||
55BC474F2C3A4E5E00120A7D /* ToastUI in Frameworks */ = {isa = PBXBuildFile; productRef = 55BC474E2C3A4E5E00120A7D /* ToastUI */; };
|
55BC474F2C3A4E5E00120A7D /* ToastUI in Frameworks */ = {isa = PBXBuildFile; productRef = 55BC474E2C3A4E5E00120A7D /* ToastUI */; };
|
||||||
55BC47512C3D431300120A7D /* IAPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC47502C3D431300120A7D /* IAPView.swift */; };
|
55BC47512C3D431300120A7D /* IAPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC47502C3D431300120A7D /* IAPView.swift */; };
|
||||||
|
55C6DB912E2E2AEA00D4C5E5 /* puzzle.txt in Resources */ = {isa = PBXBuildFile; fileRef = 55C6DB902E2E2AEA00D4C5E5 /* puzzle.txt */; };
|
||||||
|
55C6DB952E2E2ED700D4C5E5 /* PuzzleGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C6DB942E2E2ED700D4C5E5 /* PuzzleGenerator.swift */; };
|
||||||
55D632FA2C0F125D00443894 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D632F92C0F125D00443894 /* NetworkManager.swift */; };
|
55D632FA2C0F125D00443894 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D632F92C0F125D00443894 /* NetworkManager.swift */; };
|
||||||
55DAC6552BBA959500BDD4C8 /* ResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DAC6542BBA959500BDD4C8 /* ResultView.swift */; };
|
55DAC6552BBA959500BDD4C8 /* ResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DAC6542BBA959500BDD4C8 /* ResultView.swift */; };
|
||||||
55DAC6572BBA984B00BDD4C8 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DAC6562BBA984B00BDD4C8 /* InputView.swift */; };
|
55DAC6572BBA984B00BDD4C8 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DAC6562BBA984B00BDD4C8 /* InputView.swift */; };
|
||||||
@ -99,7 +100,6 @@
|
|||||||
5550273B2C8322F800A05441 /* PushHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushHandler.swift; sourceTree = "<group>"; };
|
5550273B2C8322F800A05441 /* PushHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushHandler.swift; sourceTree = "<group>"; };
|
||||||
5586E0832C8092C400026733 /* AIGrammar-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AIGrammar-Bridging-Header.h"; sourceTree = "<group>"; };
|
5586E0832C8092C400026733 /* AIGrammar-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AIGrammar-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
5586E0872C80AD2D00026733 /* TTSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSManager.swift; sourceTree = "<group>"; };
|
5586E0872C80AD2D00026733 /* TTSManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSManager.swift; sourceTree = "<group>"; };
|
||||||
558DB7A22E27A91A004D6ADB /* WordPuzzleGameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPuzzleGameView.swift; sourceTree = "<group>"; };
|
|
||||||
558DB7A42E27B049004D6ADB /* WordPuzzleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPuzzleView.swift; sourceTree = "<group>"; };
|
558DB7A42E27B049004D6ADB /* WordPuzzleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPuzzleView.swift; sourceTree = "<group>"; };
|
||||||
558DB7B12E2A78A5004D6ADB /* wordlist.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = wordlist.txt; sourceTree = "<group>"; };
|
558DB7B12E2A78A5004D6ADB /* wordlist.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = wordlist.txt; sourceTree = "<group>"; };
|
||||||
559E6D7B2C34EAE700C971B9 /* IapManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IapManager.swift; sourceTree = "<group>"; };
|
559E6D7B2C34EAE700C971B9 /* IapManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IapManager.swift; sourceTree = "<group>"; };
|
||||||
@ -110,6 +110,8 @@
|
|||||||
55BB127C2BBD6D0600D2BEA4 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
55BB127C2BBD6D0600D2BEA4 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||||
55BC47482C3A383A00120A7D /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
55BC47482C3A383A00120A7D /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
||||||
55BC47502C3D431300120A7D /* IAPView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPView.swift; sourceTree = "<group>"; };
|
55BC47502C3D431300120A7D /* IAPView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPView.swift; sourceTree = "<group>"; };
|
||||||
|
55C6DB902E2E2AEA00D4C5E5 /* puzzle.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = puzzle.txt; sourceTree = "<group>"; };
|
||||||
|
55C6DB942E2E2ED700D4C5E5 /* PuzzleGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PuzzleGenerator.swift; sourceTree = "<group>"; };
|
||||||
55C73D7E2C157C2200041C66 /* AIGrammar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AIGrammar.entitlements; sourceTree = "<group>"; };
|
55C73D7E2C157C2200041C66 /* AIGrammar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AIGrammar.entitlements; sourceTree = "<group>"; };
|
||||||
55D632F92C0F125D00443894 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
|
55D632F92C0F125D00443894 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
|
||||||
55DAC6542BBA959500BDD4C8 /* ResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultView.swift; sourceTree = "<group>"; };
|
55DAC6542BBA959500BDD4C8 /* ResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultView.swift; sourceTree = "<group>"; };
|
||||||
@ -193,7 +195,8 @@
|
|||||||
5500A38E2BB3C7E80065A1D3 /* AIGrammar */ = {
|
5500A38E2BB3C7E80065A1D3 /* AIGrammar */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
558DB7B12E2A78A5004D6ADB /* wordlist.txt */,
|
55C6DB962E2E2F3200D4C5E5 /* Resources */,
|
||||||
|
55C6DB932E2E2EA400D4C5E5 /* Models */,
|
||||||
5550273A2C8311CB00A05441 /* Info.plist */,
|
5550273A2C8311CB00A05441 /* Info.plist */,
|
||||||
5586E0842C8093DF00026733 /* third-party */,
|
5586E0842C8093DF00026733 /* third-party */,
|
||||||
5586E0832C8092C400026733 /* AIGrammar-Bridging-Header.h */,
|
5586E0832C8092C400026733 /* AIGrammar-Bridging-Header.h */,
|
||||||
@ -247,7 +250,6 @@
|
|||||||
5500A3C52BB40AD30065A1D3 /* TranslateView.swift */,
|
5500A3C52BB40AD30065A1D3 /* TranslateView.swift */,
|
||||||
5500A3C72BB40ADE0065A1D3 /* SettingsView.swift */,
|
5500A3C72BB40ADE0065A1D3 /* SettingsView.swift */,
|
||||||
55BC47502C3D431300120A7D /* IAPView.swift */,
|
55BC47502C3D431300120A7D /* IAPView.swift */,
|
||||||
558DB7A22E27A91A004D6ADB /* WordPuzzleGameView.swift */,
|
|
||||||
558DB7A42E27B049004D6ADB /* WordPuzzleView.swift */,
|
558DB7A42E27B049004D6ADB /* WordPuzzleView.swift */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
@ -300,6 +302,23 @@
|
|||||||
path = CommView;
|
path = CommView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
55C6DB932E2E2EA400D4C5E5 /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
55C6DB942E2E2ED700D4C5E5 /* PuzzleGenerator.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
55C6DB962E2E2F3200D4C5E5 /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
558DB7B12E2A78A5004D6ADB /* wordlist.txt */,
|
||||||
|
55C6DB902E2E2AEA00D4C5E5 /* puzzle.txt */,
|
||||||
|
);
|
||||||
|
path = Resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
55DAC6532BBA956100BDD4C8 /* GrammarSubView */ = {
|
55DAC6532BBA956100BDD4C8 /* GrammarSubView */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -443,6 +462,7 @@
|
|||||||
files = (
|
files = (
|
||||||
5500A3972BB3C7EB0065A1D3 /* Preview Assets.xcassets in Resources */,
|
5500A3972BB3C7EB0065A1D3 /* Preview Assets.xcassets in Resources */,
|
||||||
5500A3942BB3C7EB0065A1D3 /* Assets.xcassets in Resources */,
|
5500A3942BB3C7EB0065A1D3 /* Assets.xcassets in Resources */,
|
||||||
|
55C6DB912E2E2AEA00D4C5E5 /* puzzle.txt in Resources */,
|
||||||
558DB7B22E2A78A5004D6ADB /* wordlist.txt in Resources */,
|
558DB7B22E2A78A5004D6ADB /* wordlist.txt in Resources */,
|
||||||
551C8C342C79946700B1A88C /* GoogleService-Info.plist in Resources */,
|
551C8C342C79946700B1A88C /* GoogleService-Info.plist in Resources */,
|
||||||
);
|
);
|
||||||
@ -581,10 +601,10 @@
|
|||||||
55BC47512C3D431300120A7D /* IAPView.swift in Sources */,
|
55BC47512C3D431300120A7D /* IAPView.swift in Sources */,
|
||||||
550B85A22C2BC624008834E5 /* InitAPP.swift in Sources */,
|
550B85A22C2BC624008834E5 /* InitAPP.swift in Sources */,
|
||||||
5500A3C42BB40AC40065A1D3 /* WordsView.swift in Sources */,
|
5500A3C42BB40AC40065A1D3 /* WordsView.swift in Sources */,
|
||||||
558DB7A32E27A91A004D6ADB /* WordPuzzleGameView.swift in Sources */,
|
|
||||||
5509CEF12BB54DD10056C5C2 /* Config.swift in Sources */,
|
5509CEF12BB54DD10056C5C2 /* Config.swift in Sources */,
|
||||||
55BB127B2BBD653100D2BEA4 /* ShareSheet.swift in Sources */,
|
55BB127B2BBD653100D2BEA4 /* ShareSheet.swift in Sources */,
|
||||||
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */,
|
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */,
|
||||||
|
55C6DB952E2E2ED700D4C5E5 /* PuzzleGenerator.swift in Sources */,
|
||||||
5500A3C82BB40ADE0065A1D3 /* SettingsView.swift in Sources */,
|
5500A3C82BB40ADE0065A1D3 /* SettingsView.swift in Sources */,
|
||||||
55E4E8F52C60CFFC00988503 /* CommFunc.swift in Sources */,
|
55E4E8F52C60CFFC00988503 /* CommFunc.swift in Sources */,
|
||||||
55DAC6552BBA959500BDD4C8 /* ResultView.swift in Sources */,
|
55DAC6552BBA959500BDD4C8 /* ResultView.swift in Sources */,
|
||||||
|
|||||||
Binary file not shown.
@ -40,6 +40,7 @@ struct AllTabView: View {
|
|||||||
WordPuzzleView()
|
WordPuzzleView()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Image(systemName: "timer.circle")
|
Image(systemName: "timer.circle")
|
||||||
|
//Image(systemName: "rectangle.3.group")
|
||||||
Text("Puzzel")
|
Text("Puzzel")
|
||||||
}
|
}
|
||||||
.tag(3)
|
.tag(3)
|
||||||
|
|||||||
112
AIGrammar/Models/PuzzleGenerator.swift
Normal file
112
AIGrammar/Models/PuzzleGenerator.swift
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// PuzzleGenerator.swift
|
||||||
|
// AIGrammar
|
||||||
|
//
|
||||||
|
// Created by oscar on 2025/7/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class PuzzleGenerator {
|
||||||
|
private(set) var validationWords: Set<String> = []
|
||||||
|
private(set) var puzzleWords: [String] = []
|
||||||
|
|
||||||
|
init() {
|
||||||
|
//loadValidationWords()
|
||||||
|
//loadPuzzleWords()
|
||||||
|
validationWords = Set(loadWords(from: "wordlist"))
|
||||||
|
puzzleWords = loadWords(from: "puzzle")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通用加载逻辑
|
||||||
|
private func loadWords(from fileName: String) -> [String] {
|
||||||
|
if let url = Bundle.main.url(forResource: fileName, withExtension: "txt") {
|
||||||
|
do {
|
||||||
|
let content = try String(contentsOf: url)
|
||||||
|
let words = content
|
||||||
|
.components(separatedBy: .newlines)
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
logger.info("Loaded \(words.count) words from \(fileName).txt")
|
||||||
|
return words
|
||||||
|
} catch {
|
||||||
|
logger.warning("Failed to load \(fileName).txt: \(error)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warning("\(fileName).txt not found!")
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载验证单词库
|
||||||
|
private func loadValidationWords() {
|
||||||
|
if let url = Bundle.main.url(forResource: "wordlist", withExtension: "txt") {
|
||||||
|
do {
|
||||||
|
let content = try String(contentsOf: url)
|
||||||
|
validationWords = Set(content
|
||||||
|
.components(separatedBy: .newlines)
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() }
|
||||||
|
.filter { !$0.isEmpty })
|
||||||
|
logger.info("Loaded validation words: \(validationWords.count)")
|
||||||
|
} catch {
|
||||||
|
logger.warning("Failed to load wordlist.txt: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载出题单词库
|
||||||
|
private func loadPuzzleWords() {
|
||||||
|
if let url = Bundle.main.url(forResource: "puzzle", withExtension: "txt") {
|
||||||
|
do {
|
||||||
|
let content = try String(contentsOf: url)
|
||||||
|
puzzleWords = content
|
||||||
|
.components(separatedBy: .newlines)
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
logger.info("Loaded puzzle words: \(puzzleWords.count)")
|
||||||
|
} catch {
|
||||||
|
logger.warning("Failed to load puzzle.txt: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 随机生成一套题目
|
||||||
|
func generatePuzzle() -> (Set<String>, [[String]]) {
|
||||||
|
var chosenWords: Set<String> = []
|
||||||
|
var uniqueLetters: Set<Character> = []
|
||||||
|
|
||||||
|
while (uniqueLetters.count < 16 && chosenWords.count < 6) || chosenWords.count < 4 {
|
||||||
|
if let word = puzzleWords.randomElement() {
|
||||||
|
chosenWords.insert(word)
|
||||||
|
uniqueLetters.formUnion(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保证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()
|
||||||
|
|
||||||
|
var letters: [[String]] = 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (chosenWords, letters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验用户提交的单词
|
||||||
|
func validate(word: String) -> Bool {
|
||||||
|
validationWords.contains(word.uppercased())
|
||||||
|
}
|
||||||
|
}
|
||||||
35774
AIGrammar/Resources/wordlist.txt
Normal file
35774
AIGrammar/Resources/wordlist.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,153 +0,0 @@
|
|||||||
//
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
@ -175,83 +175,31 @@ class WordPuzzleViewModel: ObservableObject {
|
|||||||
@Published var showToast = false
|
@Published var showToast = false
|
||||||
@Published var toastFlag = false
|
@Published var toastFlag = false
|
||||||
@Published var toastMessage = ""
|
@Published var toastMessage = ""
|
||||||
@Published var toastColor = Color.green
|
|
||||||
|
|
||||||
private var timer: Timer?
|
private var timer: Timer?
|
||||||
private var allWords: Set<String> = ["WORD", "HAVE", "SWIFT", "CODE"] // 示例字典
|
private var allWords: Set<String> = []
|
||||||
private var wordList: [String] = []
|
private var submittedWords: Set<String> = []
|
||||||
|
private let generator = PuzzleGenerator()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadWordList()
|
|
||||||
generateNewPuzzle()
|
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() {
|
func generateNewPuzzle() {
|
||||||
if wordList.isEmpty {
|
let (words, grid) = generator.generatePuzzle()
|
||||||
logger.warning("Word list is empty")
|
allWords = words
|
||||||
wordList = ["WORD", "HAVE", "SWIFT", "CODE"] // 给个示例的
|
letters = grid
|
||||||
}
|
|
||||||
|
|
||||||
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
|
guessedWords = 0
|
||||||
selectedPositions.removeAll()
|
selectedPositions.removeAll()
|
||||||
timeRemaining = 60
|
timeRemaining = 60
|
||||||
//shuffleCount = 0
|
//shuffleCount = 0
|
||||||
|
|
||||||
logger.info("New puzzle generated with words: \(allWords) and letters: \(shuffledLetters)")
|
submittedWords.removeAll()
|
||||||
|
logger.info("New puzzle with words: \(allWords)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTimer() {
|
func startTimer() {
|
||||||
stopTimer()
|
stopTimer()
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||||
@ -275,22 +223,32 @@ class WordPuzzleViewModel: ObservableObject {
|
|||||||
guard !word.isEmpty else { return }
|
guard !word.isEmpty else { return }
|
||||||
|
|
||||||
if timeRemaining <= 0 {
|
if timeRemaining <= 0 {
|
||||||
showToast(message: "Time's up!", succ: false)
|
showToast(message: "Time's up! Shuffle for new game.", succ: false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if allWords.contains(word) {
|
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
|
guessedWords += 1
|
||||||
|
submittedWords.insert(word) // 记录
|
||||||
showToast(message: "Correct!", succ: true)
|
showToast(message: "Correct!", succ: true)
|
||||||
} else {
|
} else {
|
||||||
showToast(message: "Please try again", succ: false)
|
showToast(message: "Please try again", succ: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if guessedWords >= 4 {
|
if guessedWords >= 4 {
|
||||||
//stopTimer()
|
|
||||||
dailyWins += 1
|
dailyWins += 1
|
||||||
showToast(message: "Congratulations! \nStart next puzzle...", succ: true)
|
showToast(message: "Congratulations! Next puzzle...", succ: true)
|
||||||
generateNewPuzzle() // 新开一局
|
generateNewPuzzle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,14 +260,11 @@ class WordPuzzleViewModel: ObservableObject {
|
|||||||
|
|
||||||
shuffleCount += 1
|
shuffleCount += 1
|
||||||
generateNewPuzzle()
|
generateNewPuzzle()
|
||||||
|
startTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectedWord() -> String {
|
private func selectedWord() -> String {
|
||||||
var result = ""
|
selectedPositions.map { letters[$0.row][$0.col] }.joined()
|
||||||
for pos in selectedPositions {
|
|
||||||
result += letters[pos.row][pos.col]
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showToast(message: String, succ: Bool) {
|
private func showToast(message: String, succ: Bool) {
|
||||||
|
|||||||
2868
Resource/puzzle.txt
Normal file
2868
Resource/puzzle.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user