Skip to content

Commit

Permalink
Removed the view model class and added an icon.
Browse files Browse the repository at this point in the history
  • Loading branch information
abdel-17 committed Jun 29, 2022
1 parent a709bf9 commit 72c4a96
Show file tree
Hide file tree
Showing 33 changed files with 155 additions and 330 deletions.
Binary file modified Shared/Assets.xcassets/.DS_Store
Binary file not shown.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/114.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/144.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/152.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/180.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/29.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/40.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/50.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/57.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/58.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/60.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/72.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/76.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/80.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Shared/Assets.xcassets/AppIcon.appiconset/87.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 1 addition & 148 deletions Shared/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -1,148 +1 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]}
53 changes: 38 additions & 15 deletions Shared/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import SwiftUI

struct ContentView: View {
@StateObject var model = ViewModel()
@State private var game = TicTacToe(startingPlayer: .x)

/// A Boolean value to check whether the game
/// mode is player-vs-enemy or player-vs-player.
@State private var isPVE = true

/// True iff the grid will reset.
@State private var willReset = false

var body: some View {
VStack {
Text(model.displayedMessage)
Text(displayedMessage)
.font(.title2)
.bold()
.opacity(model.isAnimating ? 0 : 1)
.animation(.default, value: model.isAnimating)

Spacer()

GridView(model: model,
GridView(game: $game,
willReset: $willReset,
isPVE: isPVE,
lineWidth: 7.5)
.padding()

Spacer()

HStack {
Button {
Task {
await model.switchGameMode()
}
isPVE.toggle()
willReset.toggle()
} label: {
Image(systemName: model.isPVE ? "person" : "person.2")
Image(systemName: isPVE ? "person" : "person.2")
.foregroundColor(.blue)
}

Spacer()

Button {
Task {
await model.startNewGame()
}
willReset.toggle()
} label: {
Image(systemName: "arrow.counterclockwise")
.foregroundColor(.red)
Expand All @@ -46,9 +50,28 @@ struct ContentView: View {
.font(.title)
}
.padding()
#if os(macOS)
.frame(minWidth: 350, minHeight: 400)
#endif
}

/// The up-to-date displayed message.
private var displayedMessage: String {
if game.hasNotEnded {
// If we are in pve mode, we don't need to check
// if the current player is x because the text
// is hidden while the opponent is playing.
return isPVE ? "Your turn!" : "Player \(game.currentPlayer)"
}
// Game has ended. Check for draws first.
guard let winner = game.winner else { return "Draw!" }
if isPVE {
// The AI is always player o.
switch winner {
case .x:
return "You won!"
case .o:
return "You lost!"
}
}
return "Player \(winner) won!"
}
}

Expand Down
123 changes: 98 additions & 25 deletions Shared/GridView.swift
Original file line number Diff line number Diff line change
@@ -1,48 +1,121 @@
import SwiftUI

extension Task where Success == Never, Failure == Never {
/// Suspends the current task for the given time in seconds.
static func sleep(seconds: Double) async {
do {
try await Task.sleep(nanoseconds: UInt64(seconds * 1e9))
} catch {
#if DEBUG
print("Task interrupted unexpectedly")
#endif
}
}
}

/// A 3x3 grid of evenly-spaced cells.
struct GridView: View {
@ObservedObject var model: ViewModel
@Binding var game: TicTacToe

@Binding var willReset: Bool

let isPVE: Bool

/// The width of the drawing line.
let lineWidth: Double

/// True iff an animation is occuring.
@State private var isAnimating = true

/// The completion percentage of the
/// grid lines animation.
@State private var animationCompletion = 0.0

/// The completion percentage of each
/// cell's drawing animation.
@State private var cellAnimationCompletions = Array(repeating: 0.0,
count: 9)

var body: some View {
VStack(spacing: lineWidth) {
ForEach(0..<3) { row in
HStack(spacing: lineWidth) {
ForEach(0..<3) { column in
let index = 3 * row + column
Button {
Task {
guard !model.game.hasEnded else {
await model.startNewGame()
return
}
await model.play(at: index)
}
} label: {
Cell(player: model.game.grid[index],
isMatching: model.game.isMatching(at: index),
lineWidth: lineWidth,
animationCompletion: model.cellAnimationCompletions[index])
}
.buttonStyle(.borderless)
.disabled(!model.game.isEmpty(at: index) &&
!model.game.hasEnded)
button(at: 3 * row + column)
}
}
}
}
.aspectRatio(1, contentMode: .fit)
.background(GridLines(lineWidth: lineWidth,
animationCompletion: model.gridAnimationCompletion))
animationCompletion: animationCompletion))
.onChange(of: willReset) { willReset in
if willReset {
Task {
await self.reset()
self.willReset = false
}
}
}
.task {
await model.startGridAnimation()
animationCompletion = 1
await Task.sleep(seconds: 1)
isAnimating.toggle()
}
.disabled(isAnimating)
}

/// Returns the button at the given index.
private func button(at index: Int) -> some View {
Button {
Task {
guard game.hasNotEnded else {
await self.reset()
return
}
await play(at: index)
}
} label: {
Cell(player: game.grid[index],
isMatching: game.isMatching(at: index),
lineWidth: lineWidth,
animationCompletion: cellAnimationCompletions[index])
}
.buttonStyle(.borderless)
.disabled(!game.isEmpty(at: index) && game.hasNotEnded)
}

/// Sets the current-turn player at `index`,
/// and returns after the cell's animation completes.
private func setPlayer(at index: Int) async {
game.play(at: index)
withAnimation(Cell.animation) {
cellAnimationCompletions[index] = 1
}
await Task.sleep(seconds: Cell.animationDuration)
}

/// Plays the game at `index`.
private func play(at index: Int) async {
isAnimating.toggle()
await setPlayer(at: index)
if isPVE, let move = game.moveHavingBestHeuristic() {
await setPlayer(at: move)
}
isAnimating.toggle()
}

/// Resets the grid.
func reset() async {
// Only reset when needed.
guard !game.isFirstTurn else { return }
isAnimating.toggle()
withAnimation(Cell.animation) {
cellAnimationCompletions = Array(repeating: 0,
count: 9)
}
.disabled(model.isAnimating)
#if os(macOS)
.frame(minWidth: 300)
#endif
await Task.sleep(seconds: Cell.animationDuration)
game = TicTacToe(startingPlayer: .x)
isAnimating.toggle()
}
}
Loading

0 comments on commit 72c4a96

Please sign in to comment.