Skip to content

Commit

Permalink
Merge pull request #22 from g-liu/hard-mode
Browse files Browse the repository at this point in the history
hard mode
  • Loading branch information
g-liu authored Mar 10, 2022
2 parents 31a6e68 + 9c295b5 commit b03bd94
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 26 deletions.
8 changes: 6 additions & 2 deletions WordleWithFriends.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@
6C94887827D0AAD9005252F1 /* LetterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94887727D0AAD9005252F1 /* LetterState.swift */; };
6C94887C27D0AB15005252F1 /* LetterStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94887B27D0AB15005252F1 /* LetterStateTests.swift */; };
6C94887E27D0B4E4005252F1 /* KeyboardRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94887D27D0B4E4005252F1 /* KeyboardRow.swift */; };
6C94F50227D5C23D00C19D43 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F50127D5C23D00C19D43 /* Array+Extension.swift */; };
6C94F4F727D4494C00C19D43 /* GameInstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F4F627D4494C00C19D43 /* GameInstructionsViewController.swift */; };
6C94F4FA27D44A8100C19D43 /* HorizontalSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F4F927D44A8100C19D43 /* HorizontalSeparatorView.swift */; };
6C94F50227D5C23D00C19D43 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F50127D5C23D00C19D43 /* Array+Extension.swift */; };
6C94F50627D9D88F00C19D43 /* Set+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C94F50527D9D88F00C19D43 /* Set+Extension.swift */; };
6CBBCECF279BDC7D00875C30 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBBCECE279BDC7D00875C30 /* Double+Extension.swift */; };
6CBCC3CD2797565D005EB254 /* DismissableAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBCC3CC2797565D005EB254 /* DismissableAlertController.swift */; };
6CBCC3D1279764FF005EB254 /* CGPoint+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CBCC3D0279764FF005EB254 /* CGPoint+Extension.swift */; };
Expand Down Expand Up @@ -155,9 +156,10 @@
6C94887727D0AAD9005252F1 /* LetterState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterState.swift; sourceTree = "<group>"; };
6C94887B27D0AB15005252F1 /* LetterStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterStateTests.swift; sourceTree = "<group>"; };
6C94887D27D0B4E4005252F1 /* KeyboardRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardRow.swift; sourceTree = "<group>"; };
6C94F50127D5C23D00C19D43 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = "<group>"; };
6C94F4F627D4494C00C19D43 /* GameInstructionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameInstructionsViewController.swift; sourceTree = "<group>"; };
6C94F4F927D44A8100C19D43 /* HorizontalSeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalSeparatorView.swift; sourceTree = "<group>"; };
6C94F50127D5C23D00C19D43 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = "<group>"; };
6C94F50527D9D88F00C19D43 /* Set+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Extension.swift"; sourceTree = "<group>"; };
6CBBCECE279BDC7D00875C30 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
6CBCC3CC2797565D005EB254 /* DismissableAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableAlertController.swift; sourceTree = "<group>"; };
6CBCC3D0279764FF005EB254 /* CGPoint+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Extension.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -344,6 +346,7 @@
6C834C8F279C897F0024CB13 /* IndexPath+Extension.swift */,
6C889A1127D33CA2001AC677 /* UIViewController+Extension.swift */,
6C94F50127D5C23D00C19D43 /* Array+Extension.swift */,
6C94F50527D9D88F00C19D43 /* Set+Extension.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -567,6 +570,7 @@
60D948102793E2BC00086C51 /* GameSettingTableViewCell.swift in Sources */,
6C3955A1279288E800B0EF29 /* LetterGuess.swift in Sources */,
6C19F5F027923C5900062083 /* NSLayoutConstraint+Extension.swift in Sources */,
6C94F50627D9D88F00C19D43 /* Set+Extension.swift in Sources */,
6C19F5FD27923CBB00062083 /* UIView+Extension.swift in Sources */,
6C0009E02792AD8F00BCC9B6 /* Character+Extension.swift in Sources */,
6C889A1427D33CEB001AC677 /* ToastView.swift in Sources */,
Expand Down
48 changes: 28 additions & 20 deletions WordleWithFriends/ClueGuessViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,32 +152,15 @@ final class ClueGuessViewController: UIViewController {
}

private func submitGuess() {
// TODO: Move some checks to view model???
guard let wordGuess = guessInputTextField.text,
wordGuess.count == GameSettings.clueLength.readIntValue(),
GameSettings.allowNonDictionaryGuesses.readBoolValue() || wordGuess.isARealWord() else {
gameGuessesModel.markInvalidGuess()
let currentIndexPath = IndexPath.Row(gameGuessesModel.numberOfGuesses)
guessTable.reloadRows(at: [currentIndexPath], with: .none)

AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))

return
}

let gameState = gameGuessesModel.submitGuess()

if let mostRecentGuess = gameGuessesModel.mostRecentGuess {
wordleKeyboard.updateState(with: mostRecentGuess)
}

guessTable.reloadData()

guessInputTextField.text = ""
guessInputTextField.becomeFirstResponder()

switch gameState {
case .win:
guessTable.reloadData()
wordleKeyboard.gameDidEnd()
if gameGuessesModel.gamemode == .infinite {
presentToast("Good job! \(gameGuessesModel.numberOfGuesses) guess(es)")
Expand All @@ -188,15 +171,40 @@ final class ClueGuessViewController: UIViewController {
shareButton.isEnabled = true
gameMessagingVC.showWin(numGuesses: gameGuessesModel.numberOfGuesses)
}
guessInputTextField.text = ""

case .lose:
guessTable.reloadData()
guessInputTextField.text = ""

forceLoss()
case .keepGuessing:
shareButton.isEnabled = false
guessTable.reloadData()
guessInputTextField.text = ""

guessTable.scrollToRow(at: IndexPath.Row(gameGuessesModel.numberOfGuesses), at: .bottom, animated: true)
break
case .invalidGuess(let missingCharacters):
indicateInvalidGuess(reason: "Guess must contain letters: \(missingCharacters.asCommaSeparatedList)")
case .notAWord:
indicateInvalidGuess(reason: "That's not a word in our dictionary.")
case .invalidLength:
indicateInvalidGuess(reason: "Guess must be exactly \(GameSettings.clueLength.readIntValue()) letters")
}
}

private func indicateInvalidGuess(reason: String) {
gameGuessesModel.markInvalidGuess()
let currentIndexPath = IndexPath.Row(gameGuessesModel.numberOfGuesses)
guessTable.reloadRows(at: [currentIndexPath], with: .none)

dismissAllToasts()
presentToast(reason)

AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))

return
}

private func forceLoss() {
gameGuessesModel.forceGameOver()
wordleKeyboard.gameDidEnd()
Expand Down
14 changes: 14 additions & 0 deletions WordleWithFriends/Extensions/Set+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// Set+Extension.swift
// WordleWithFriends
//
// Created by Geoffrey Liu on 3/9/22.
//

import Foundation

extension Set where Element == Character {
var asCommaSeparatedList: String {
map { String($0) }.joined(separator: ", ")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ extension UIViewController {
toastView.removeFromSuperview()
}
}

}

func dismissAllToasts() {
view.subviews.forEach { subview in
guard let toastView = subview as? ToastView else { return }
toastView.removeFromSuperview()
}
}
}
51 changes: 50 additions & 1 deletion WordleWithFriends/Models/GameGuessesModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ struct GameGuessesModel {

private var letterGuesses: [WordGuess] = [WordGuess()]

private var givenHints: Set<Character> = .init()

/// The number of completed guesses
var numberOfGuesses: Int { letterGuesses.count - 1 }

Expand All @@ -40,11 +42,40 @@ struct GameGuessesModel {
isGameOver = true
}

// Determines whether the guess is valid or not
// TODO: Roll this into submitGuess
func validateGuess() -> Bool {
guard let guess = letterGuesses.last else { return false }

let wordGuess = guess.word

return wordGuess.count == GameSettings.clueLength.readIntValue()
&& (GameSettings.allowNonDictionaryGuesses.readBoolValue() || wordGuess.isARealWord())
}

/// Submit a guess
/// - Returns: if the user guessed the word correctly
@discardableResult
mutating func submitGuess() -> GameState {
let didGuessCorrectly = letterGuesses[letterGuesses.count - 1].checkGuess(against: clue)
guard let wordGuess = letterGuesses.last?.word else {
return .invalidLength
}

guard wordGuess.count == GameSettings.clueLength.readIntValue() else {
return .invalidLength
}

guard (GameSettings.allowNonDictionaryGuesses.readBoolValue() || wordGuess.isARealWord()) else {
return .notAWord
}

if GameSettings.isHardModeEnabled.readBoolValue(),
let missingClues = missingCluesFromGuess(),
!missingClues.isEmpty {
return .invalidGuess(missingClues)
}

let didGuessCorrectly = letterGuesses[letterGuesses.count - 1].checkGuess(against: clue, givenHints: &givenHints)

letterGuesses.append(WordGuess())
if didGuessCorrectly {
Expand All @@ -59,6 +90,14 @@ struct GameGuessesModel {
}
}

private func missingCluesFromGuess() -> Set<Character>? {
guard numberOfGuesses > 0,
!givenHints.isEmpty,
let wordGuess = letterGuesses.last?.word else { return nil }

return givenHints.subtracting(wordGuess)
}

func copyResult() {
let header = "Wordle With Friends - \(letterGuesses.count-1)/\(GameSettings.maxGuesses.readIntValue())\n"
UIPasteboard.general.string = letterGuesses.reduce(header) { copyString, guess in
Expand All @@ -76,9 +115,19 @@ struct GameGuessesModel {
}

enum GameState {
// Guess is correct
case win
// Guess is incorrect and player is out of guesses
case lose
// Guess is valid but incorrect
case keepGuessing
// Guess does not meet length requirements
case invalidLength
// Guess is not a dictionary word
case notAWord
// Guess does not meet previous clue requirements
// Param contains a list of characters missing
case invalidGuess(Set<Character>)
}

enum GameMode {
Expand Down
4 changes: 3 additions & 1 deletion WordleWithFriends/Models/WordGuess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ struct WordGuess {
/// - Parameter clue: the word to check against
/// - Returns: true if the user guessed correctly; false otherwise
@discardableResult
mutating func checkGuess(against clue: String) -> Bool {
mutating func checkGuess(against clue: String, givenHints: inout Set<Character>) -> Bool {
var clue = clue
var didGuessCorrectly = true

// pass 1: find exact matches and incorrect guesses
guess.enumerated().forEach { index, letterGuess in
if letterGuess.letter == clue[index] {
clue.replaceAt(index, with: "#")
givenHints.insert(letterGuess.letter)
mark(index, as: .correct)
} else if !clue.contains(letterGuess.letter) {
mark(index, as: .incorrect)
Expand All @@ -64,6 +65,7 @@ struct WordGuess {
didGuessCorrectly = false
if let indexInClue = clue.firstIndex(of: guess[index].letter) {
clue.replaceAt(indexInClue, with: "#")
givenHints.insert(guess[index].letter)
mark(index, as: .misplaced)
} else {
mark(index, as: .incorrect)
Expand Down
2 changes: 2 additions & 0 deletions WordleWithFriends/Settings/GameSettingsDefaults.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<integer>6</integer>
<key>clueLength</key>
<integer>5</integer>
<key>hardMode</key>
<false/>
<key>allowNonDictionaryGuesses</key>
<false/>
</dict>
Expand Down
5 changes: 4 additions & 1 deletion WordleWithFriends/Settings/Models/GameSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

struct GameSettings {
/* TODO: Can be placed into plist? */
static var clueLength = GameSettingIntRange(key: "clueLength",
description: "Length of clue",
minValue: 3,
Expand All @@ -16,11 +17,13 @@ struct GameSettings {
description: "Max guesses",
minValue: 1,
maxValue: 20)
static var isHardModeEnabled = GameSettingBool(key: "hardMode",
description: "Hard mode")
static var allowNonDictionaryGuesses = GameSettingBool(key: "allowNonDictionaryGuesses",
description: "Allow non-word guesses")

static var allSettings: [GameSetting] {
return [clueLength, maxGuesses, allowNonDictionaryGuesses]
return [clueLength, maxGuesses, isHardModeEnabled, allowNonDictionaryGuesses]
}

static var numSettings: Int {
Expand Down
1 change: 1 addition & 0 deletions WordleWithFriends/Views/ToastView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ final class ToastView: UIView {
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
label.text = text
label.numberOfLines = 2

addSubview(label)
label.pin(to: self, margins: UIEdgeInsets(top: 12, left: 18, bottom: 12, right: 18))
Expand Down

0 comments on commit b03bd94

Please sign in to comment.