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
+20
View File
@@ -28,6 +28,9 @@
555027392C81C0ED00A05441 /* QCloudTTS.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 555027382C81C0ED00A05441 /* QCloudTTS.xcframework */; };
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5550273B2C8322F800A05441 /* PushHandler.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 */; };
558DB7B22E2A78A5004D6ADB /* wordlist.txt in Resources */ = {isa = PBXBuildFile; fileRef = 558DB7B12E2A78A5004D6ADB /* wordlist.txt */; };
559E6D7C2C34EAE700C971B9 /* IapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E6D7B2C34EAE700C971B9 /* IapManager.swift */; };
559E6D7E2C35355200C971B9 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E6D7D2C35355200C971B9 /* LogManager.swift */; };
55A954A22BBBFD0C00BF181E /* GrammarData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A954A12BBBFD0C00BF181E /* GrammarData.swift */; };
@@ -96,6 +99,9 @@
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>"; };
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>"; };
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>"; };
559E6D7D2C35355200C971B9 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; };
55A954A12BBBFD0C00BF181E /* GrammarData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrammarData.swift; sourceTree = "<group>"; };
@@ -170,6 +176,7 @@
5500A38D2BB3C7E80065A1D3 /* Products */,
B164443BE53434F82C385E52 /* Pods */,
40FEAE98949AC44A40547B20 /* Frameworks */,
558DB7A72E292893004D6ADB /* Resource */,
);
sourceTree = "<group>";
};
@@ -186,6 +193,7 @@
5500A38E2BB3C7E80065A1D3 /* AIGrammar */ = {
isa = PBXGroup;
children = (
558DB7B12E2A78A5004D6ADB /* wordlist.txt */,
5550273A2C8311CB00A05441 /* Info.plist */,
5586E0842C8093DF00026733 /* third-party */,
5586E0832C8092C400026733 /* AIGrammar-Bridging-Header.h */,
@@ -239,6 +247,8 @@
5500A3C52BB40AD30065A1D3 /* TranslateView.swift */,
5500A3C72BB40ADE0065A1D3 /* SettingsView.swift */,
55BC47502C3D431300120A7D /* IAPView.swift */,
558DB7A22E27A91A004D6ADB /* WordPuzzleGameView.swift */,
558DB7A42E27B049004D6ADB /* WordPuzzleView.swift */,
);
path = View;
sourceTree = "<group>";
@@ -275,6 +285,13 @@
path = "third-party";
sourceTree = "<group>";
};
558DB7A72E292893004D6ADB /* Resource */ = {
isa = PBXGroup;
children = (
);
path = Resource;
sourceTree = "<group>";
};
55BC47472C3A380C00120A7D /* CommView */ = {
isa = PBXGroup;
children = (
@@ -426,6 +443,7 @@
files = (
5500A3972BB3C7EB0065A1D3 /* Preview Assets.xcassets in Resources */,
5500A3942BB3C7EB0065A1D3 /* Assets.xcassets in Resources */,
558DB7B22E2A78A5004D6ADB /* wordlist.txt in Resources */,
551C8C342C79946700B1A88C /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -555,6 +573,7 @@
buildActionMask = 2147483647;
files = (
5500A3992BB3C7EB0065A1D3 /* Persistence.swift in Sources */,
558DB7A52E27B049004D6ADB /* WordPuzzleView.swift in Sources */,
55DAC6572BBA984B00BDD4C8 /* InputView.swift in Sources */,
55A954A22BBBFD0C00BF181E /* GrammarData.swift in Sources */,
55BB12792BBD4C9900D2BEA4 /* RichText.swift in Sources */,
@@ -562,6 +581,7 @@
55BC47512C3D431300120A7D /* IAPView.swift in Sources */,
550B85A22C2BC624008834E5 /* InitAPP.swift in Sources */,
5500A3C42BB40AC40065A1D3 /* WordsView.swift in Sources */,
558DB7A32E27A91A004D6ADB /* WordPuzzleGameView.swift in Sources */,
5509CEF12BB54DD10056C5C2 /* Config.swift in Sources */,
55BB127B2BBD653100D2BEA4 /* ShareSheet.swift in Sources */,
5550273C2C8322F800A05441 /* PushHandler.swift in Sources */,
+8 -1
View File
@@ -37,12 +37,19 @@ struct AllTabView: View {
}
.tag(2)
WordPuzzleView()
.tabItem {
Image(systemName: "timer.circle")
Text("Puzzel")
}
.tag(3)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}
.tag(3)
.tag(4)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
//
// Untitled.swift
// AIGrammar
//
// Created by oscar on 2025/7/16.
//
+153
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()
}
+333
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()
}
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
WORD
GAME
FUN
PLAY
SWIFT
CODE
TASK
JUMP
CLIMB
BRAVE
CHART
INDEX
QUICK
WORLD
FRESH
PLANT
LIGHT
MOUNT
BRISK
CLEAN
GLOVE
THINK
WASTE
PRIZE
SHINE
VEX
CROWN
BLAZE
SHOUT
FLOCK
PRINT
GLARE
CRISP
DREAM
VOLT
BRING
MATCH
DANCE
PLUCK
GRAND
FLAME
STONE
TRICK
WHILE
BROAD
MARCH
CLEFT
SHARP
GRIND
VOICE
WIND
SPLIT
JOKER
TRAMP
BLANK
CRANE
SPARK
FLOAT
THIRD
GHOST
TWICE
FLOUR
COVET
BLUSH
JUMPY
HOVER
PRANK
STORM
FLOCK
NIGHT
GRACE
FRONT
CLEFT
BLOWN
PRINT
VOUCH
FRESH
CHIME
DWARF
SLICK
HUMAN
GLOVE
THUMP
BLACK
FLICK
CRUSH
SPEND
BRINK
LUNCH
MOUTH
GRASP
THIRD
SPARK
TWIRL
QUICK
PLANT
FIGHT
CRISP