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

@ -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 */,

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)
}
}
}

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()
}

2868
AIGrammar/wordlist.txt Normal file

File diff suppressed because it is too large Load Diff

98
Resource/wordlist.txt Normal file
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