From ff47367146369e1cc3e7b5933e46c24661cdd822 Mon Sep 17 00:00:00 2001 From: FredericXS Date: Sat, 18 Mar 2023 16:18:57 -0300 Subject: [PATCH] Rewriting logic --- TicTacToe.xcodeproj/project.pbxproj | 8 +- TicTacToe/Model/GameLogicModel.swift | 87 +++++++++++++++++++++ TicTacToe/ViewModel/VersusAIViewModel.swift | 76 +++++++++--------- 3 files changed, 133 insertions(+), 38 deletions(-) create mode 100644 TicTacToe/Model/GameLogicModel.swift diff --git a/TicTacToe.xcodeproj/project.pbxproj b/TicTacToe.xcodeproj/project.pbxproj index 0546e5a..5830827 100644 --- a/TicTacToe.xcodeproj/project.pbxproj +++ b/TicTacToe.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 5F27EC4829BE819E00A3E8CC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F27EC4729BE819E00A3E8CC /* Assets.xcassets */; }; 5F27EC4B29BE819E00A3E8CC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F27EC4A29BE819E00A3E8CC /* Preview Assets.xcassets */; }; 5F970CDE29BE9BD0002D3758 /* AlertsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F970CDD29BE9BCF002D3758 /* AlertsView.swift */; }; + 5FBDEC0329C62E5E001DE90E /* GameLogicModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FBDEC0229C62E5E001DE90E /* GameLogicModel.swift */; }; 5FE2A60C29BEB4E8007FED0A /* VersusAIViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE2A60B29BEB4E8007FED0A /* VersusAIViewModel.swift */; }; 5FE2A60E29BEB8E6007FED0A /* VersusAIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE2A60D29BEB8E6007FED0A /* VersusAIModel.swift */; }; 5FE2A61129BEBF1D007FED0A /* DesignView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE2A61029BEBF1D007FED0A /* DesignView.swift */; }; @@ -27,6 +28,7 @@ 5F27EC4729BE819E00A3E8CC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 5F27EC4A29BE819E00A3E8CC /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 5F970CDD29BE9BCF002D3758 /* AlertsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertsView.swift; sourceTree = ""; }; + 5FBDEC0229C62E5E001DE90E /* GameLogicModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameLogicModel.swift; sourceTree = ""; }; 5FE2A60B29BEB4E8007FED0A /* VersusAIViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersusAIViewModel.swift; sourceTree = ""; }; 5FE2A60D29BEB8E6007FED0A /* VersusAIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersusAIModel.swift; sourceTree = ""; }; 5FE2A61029BEBF1D007FED0A /* DesignView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignView.swift; sourceTree = ""; }; @@ -108,6 +110,7 @@ children = ( 5FE2A60D29BEB8E6007FED0A /* VersusAIModel.swift */, 5FE2A61629BEC39B007FED0A /* VersusFriendModel.swift */, + 5FBDEC0229C62E5E001DE90E /* GameLogicModel.swift */, ); path = Model; sourceTree = ""; @@ -186,6 +189,7 @@ 5FE2A60C29BEB4E8007FED0A /* VersusAIViewModel.swift in Sources */, 5FE2A61929BEC402007FED0A /* VersusFriendViewModel.swift in Sources */, 5FE2A61129BEBF1D007FED0A /* DesignView.swift in Sources */, + 5FBDEC0329C62E5E001DE90E /* GameLogicModel.swift in Sources */, 5FE2A61729BEC39B007FED0A /* VersusFriendModel.swift in Sources */, 5FE2A61329BEC1EA007FED0A /* GameVersusFriend.swift in Sources */, 5F27EC4629BE818200A3E8CC /* GameVersusAIView.swift in Sources */, @@ -334,7 +338,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.ashborn.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -366,7 +370,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.ashborn.TicTacToe; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/TicTacToe/Model/GameLogicModel.swift b/TicTacToe/Model/GameLogicModel.swift new file mode 100644 index 0000000..74c9f68 --- /dev/null +++ b/TicTacToe/Model/GameLogicModel.swift @@ -0,0 +1,87 @@ +// +// GameLogicModel.swift +// TicTacToe +// +// Created by Ashborn on 18/03/23. +// + +import Foundation + +struct GameLogicModel { + private(set) var moves: [VersusAIModel.Move?] + private var aiVM: VersusAIViewModel + private var humanMoves: [VersusAIModel.Move] + private var computerMoves: [VersusAIModel.Move] + + init(currentMove: [VersusAIModel.Move?]) { + moves = currentMove + aiVM = VersusAIViewModel() + + // Get human positions to block + humanMoves = moves.compactMap { $0 }.filter { $0.player == .human } + + // Get computer positions to win + computerMoves = moves.compactMap { $0 }.filter { $0.player == .computer } + } + + func pickRandomSquare() -> Int { + var randomPosition = Int.random(in: 0..<9) + + // If square is occupied, pick any other + while aiVM.isSquareOccupied(in: moves, forIndex: randomPosition) { + randomPosition = Int.random(in: 0..<9) + } + + return randomPosition + } + + func pickMiddleSquare() -> Int { + let centerSquare = 4 + if !aiVM.isSquareOccupied(in: moves, forIndex: centerSquare) { + return centerSquare + } + + return -1 + } + + func pickCornerSquare() -> Int { + let corners: [Int] = [0, 2, 6, 8] + for corner in corners { + if !aiVM.isSquareOccupied(in: moves, forIndex: corner) { + return corner + } + } + + return -1 + } + + func blockWin(patterns: Set>) -> Int { + let humanPositions = Set(humanMoves.map { $0.boardIndex }) + + for pattern in patterns { + let blockPositions = pattern.subtracting(humanPositions) + + if blockPositions.count == 1 { + let isBlockable = !aiVM.isSquareOccupied(in: moves, forIndex: blockPositions.first!) + if isBlockable { return blockPositions.first! } + } + } + + return -1 + } + + func winGame(patterns: Set>) -> Int { + let computerPositions = Set(computerMoves.map { $0.boardIndex }) + + for pattern in patterns { + let winPositions = pattern.subtracting(computerPositions) + + if winPositions.count == 1 { + let isAvailable = !aiVM.isSquareOccupied(in: moves, forIndex: winPositions.first!) + if isAvailable { return winPositions.first! } + } + } + + return -1 + } +} diff --git a/TicTacToe/ViewModel/VersusAIViewModel.swift b/TicTacToe/ViewModel/VersusAIViewModel.swift index 678f1ff..21ebbdc 100644 --- a/TicTacToe/ViewModel/VersusAIViewModel.swift +++ b/TicTacToe/ViewModel/VersusAIViewModel.swift @@ -9,7 +9,7 @@ import SwiftUI final class VersusAIViewModel: ObservableObject { let columns: [GridItem] = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())] - let levels: [String] = ["Easy", "Medium", "Hard"] + let levels: [String] = ["Easy", "Medium", "Hard", "Impossible"] @Published var moves: [VersusAIModel.Move?] = Array(repeating: nil, count: 9) @Published var isGameboardDisabled = false @@ -56,59 +56,61 @@ final class VersusAIViewModel: ObservableObject { } func determineComputerMovePosistion(in moves: [VersusAIModel.Move?]) -> Int { - // Setting Win conditions + let logicModel = GameLogicModel(currentMove: moves) let winPatterns: Set> = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]] var movePosition: Int = 0 func easyMode() -> Int { // Pick a random square - var movePosition = Int.random(in: 0..<9) - - while isSquareOccupied(in: moves, forIndex: movePosition) { - movePosition = Int.random(in: 0..<9) - } - - return movePosition + logicModel.pickRandomSquare() } func mediumMode() -> Int { - // Try pick the middle square - let centerSquare = 4 - if !isSquareOccupied(in: moves, forIndex: centerSquare) { - return centerSquare - } + // Try block possible player win + let blockPossibleWin = logicModel.blockWin(patterns: winPatterns) + if (blockPossibleWin != -1) { return blockPossibleWin } + + // If there's no win to be blocked, then pick the middle square + let middleSquare = logicModel.pickMiddleSquare() + if (middleSquare != -1) { return middleSquare } // If AI can't pick middle square, then pick a random square return easyMode() } func hardMode() -> Int { - // Win variables - let computerMoves = moves.compactMap { $0 }.filter { $0.player == .computer } - let computerPositions = Set(computerMoves.map { $0.boardIndex }) + // If AI can win, then win the game + let winGame = logicModel.winGame(patterns: winPatterns) + if (winGame != -1) { return winGame } - // Block variables - let humanMoves = moves.compactMap { $0 }.filter { $0.player == .human } - let humanPositions = Set(humanMoves.map { $0.boardIndex }) + // If AI can't win the game, then block a possible win + return mediumMode() + } + + func impossibleMode() -> Int { + // If AI can win, then win the game + let winGame = logicModel.winGame(patterns: winPatterns) + if (winGame != -1) { return winGame } - for pattern in winPatterns { - let winPositions = pattern.subtracting(computerPositions) - let blockPositions = pattern.subtracting(humanPositions) - - if winPositions.count == 1 { - // If AI can win, then win - let isAvailable = !isSquareOccupied(in: moves, forIndex: winPositions.first!) - if isAvailable { return winPositions.first! } - } else if blockPositions.count == 1 { - // If AI can't win, then block - let isBlockable = !isSquareOccupied(in: moves, forIndex: blockPositions.first!) - if isBlockable { return blockPositions.first! } - } - } + // If AI can't win the game, then block a possible win + let blockPossibleWin = logicModel.blockWin(patterns: winPatterns) + if (blockPossibleWin != -1) { return blockPossibleWin } - // If AI can't win and can't block, then try pick middle square - return mediumMode() + // If there's no win to be blocked, then pick the middle square + let middleSquare = logicModel.pickMiddleSquare() + if (middleSquare != -1) { return middleSquare } + + // If AI can't pick middle square because is yours, then pick a square above the middle + let secondSquare = 1 // 1 is the second square in array [0, 1, 2...] + if moves.contains(where: { $0?.boardIndex == 4 && $0?.player == .computer }) { return secondSquare } + + // If AI can't pick middle square because is not yours, then pick a corner square + let cornerSquare = logicModel.pickCornerSquare() + if (cornerSquare != -1) { return cornerSquare } + + // If AI can't pick corner square, then pick a random square + return easyMode() } switch(selectedLevelIndex) { @@ -118,6 +120,8 @@ final class VersusAIViewModel: ObservableObject { movePosition = mediumMode() case 2: movePosition = hardMode() + case 3: + movePosition = impossibleMode() default: print("Something is wrong") }