parent
e5caaed004
commit
9ef00e4859
@ -1,54 +1,163 @@
|
|||||||
import SpriteKit
|
import SpriteKit
|
||||||
|
import Combine
|
||||||
|
import Connect4Core
|
||||||
|
|
||||||
public class GameScene: SKScene {
|
public class GameScene: SKScene {
|
||||||
public static let boardTopMargin: CGFloat = PieceNode.pieceSize / 2
|
public static let boardTopMargin: CGFloat = PieceNode.pieceSize / 2
|
||||||
|
|
||||||
|
private let vm: IngameVM
|
||||||
|
|
||||||
private var boardNode: BoardNode
|
private var boardNode: BoardNode
|
||||||
private var cells: [PieceNode]
|
private var cells: [PieceNode]
|
||||||
private var draggablePiece: PieceNode
|
private var draggablePiece: PieceNode
|
||||||
|
|
||||||
public init(s: (w: Int, h: Int)) {
|
private var cancellables: [AnyCancellable] = []
|
||||||
let boardHeight = CGFloat(s.h) * PieceNode.pieceSize
|
|
||||||
let gWidth = CGFloat(s.w) * PieceNode.pieceSize
|
init(viewModel vm: IngameVM) {
|
||||||
|
self.vm = vm
|
||||||
|
|
||||||
|
let boardHeight = CGFloat(vm.height) * PieceNode.pieceSize
|
||||||
|
let gWidth = CGFloat(vm.width) * PieceNode.pieceSize
|
||||||
let gHeight = boardHeight + Self.boardTopMargin + PieceNode.pieceSize
|
let gHeight = boardHeight + Self.boardTopMargin + PieceNode.pieceSize
|
||||||
|
|
||||||
boardNode = BoardNode(cells: s, size: CGSize(width: gWidth, height: boardHeight))
|
boardNode = BoardNode(cells: (vm.width, vm.height), size: CGSize(width: gWidth, height: boardHeight))
|
||||||
|
|
||||||
draggablePiece = PieceNode()
|
draggablePiece = PieceNode()
|
||||||
draggablePiece.piece = .init(withOwner: .noOne)
|
draggablePiece.piece = .init(withOwner: .noOne)
|
||||||
draggablePiece.position = CGPoint(x: 0, y: boardHeight + Self.boardTopMargin + PieceNode.pieceSize / 2)
|
|
||||||
|
cells = []
|
||||||
cells = (0..<(s.w * s.h)).map({ index in
|
|
||||||
let piece = PieceNode()
|
|
||||||
piece.boardPosition = (x: index % s.w, y: index / s.w)
|
|
||||||
piece.piece = switch(index % 3) {
|
|
||||||
case 1: .init(withOwner: .player1)
|
|
||||||
case 2: .init(withOwner: .player2)
|
|
||||||
default: .init(withOwner: .noOne)
|
|
||||||
}
|
|
||||||
return piece
|
|
||||||
})
|
|
||||||
|
|
||||||
super.init(size: CGSize(width: gWidth, height: gHeight))
|
super.init(size: CGSize(width: gWidth, height: gHeight))
|
||||||
draggablePiece.onDragHandler = self.resetDragPiece
|
|
||||||
|
|
||||||
// anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
// anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
|
||||||
addChild(boardNode)
|
addChild(boardNode)
|
||||||
addChild(draggablePiece)
|
|
||||||
for cell in cells {
|
|
||||||
addChild(cell)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.resetDragPiece(piece: draggablePiece)
|
draggablePiece.position = draggablePiecePlacement
|
||||||
|
self.cancellables.append(vm.$currentPlayer.sink {
|
||||||
|
self.draggablePiece.piece = .init(withOwner: $0)
|
||||||
|
})
|
||||||
|
self.cancellables.append(vm.$currentBoard.sink(receiveValue: self.updateBoard))
|
||||||
|
|
||||||
|
vm.game.addGameStartedListener { _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.addChild(self.draggablePiece)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.game.addMoveChosenCallbacksListener { board, move, player in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.onMove(board: board, move: move, player: player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.game.addInvalidMoveCallbacksListener { board, move, player, result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.onInvalidMove(board: board, move: move, player: player, result: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.game.addPlayerNotifiedListener { _, player in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.onPlayerTurn(player: player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.game.addGameOverListener { board, result, player in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.onGameOver(board: board, result: result, player: player)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder aDecoder: NSCoder) {
|
public required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("Unreachable")
|
fatalError("Unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resetDragPiece(piece: PieceNode) {
|
private var draggablePiecePlacement: CGPoint { CGPoint(
|
||||||
piece.position = CGPoint(x: self.size.width / 2, y: self.size.height - PieceNode.pieceSize / 2)
|
x: self.size.width / 2,
|
||||||
|
y: self.size.height - PieceNode.pieceSize / 2
|
||||||
|
) }
|
||||||
|
|
||||||
|
private func onPlayerTurn(player: Player) {
|
||||||
|
print("player \(player)")
|
||||||
|
self.draggablePiece.onDragHandler = if player is HumanPlayer {
|
||||||
|
self.onPieceDragEnd
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onPieceDragEnd(_: PieceNode) {
|
||||||
|
draggablePiece.onDragHandler = nil
|
||||||
|
|
||||||
|
let at = draggablePiece.clampedPosition()
|
||||||
|
if (0..<vm.currentBoard.nbRows).contains(at.row) &&
|
||||||
|
(0..<vm.currentBoard.nbColumns).contains(at.column) {
|
||||||
|
vm.pieceDropped(at: at)
|
||||||
|
} else {
|
||||||
|
self.cancelMove(mayRetry: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateBoard(board: Board) {
|
||||||
|
assert(vm.columns == board.nbColumns && vm.rows == board.nbRows, "Board size mismatch")
|
||||||
|
|
||||||
|
for cell in cells {
|
||||||
|
cell.removeFromParent()
|
||||||
|
}
|
||||||
|
cells.removeAll()
|
||||||
|
|
||||||
|
for row in 0..<board.nbRows {
|
||||||
|
for column in 0..<board.nbColumns {
|
||||||
|
guard let piece = board[row, column] else { continue }
|
||||||
|
|
||||||
|
let node = PieceNode()
|
||||||
|
node.boardPosition = (column, row)
|
||||||
|
node.piece = piece
|
||||||
|
cells.append(node)
|
||||||
|
|
||||||
|
addChild(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onMove(board: Board, move: Move, player: Player) {
|
||||||
|
draggablePiece.position = draggablePiecePlacement
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onInvalidMove(board: Board, move: Move, player: Player, result: Bool) {
|
||||||
|
if result { return }
|
||||||
|
|
||||||
|
self.cancelMove(mayRetry: player is HumanPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelMove(mayRetry: Bool) {
|
||||||
|
draggablePiece.run(SKAction.move(to: draggablePiecePlacement, duration: 0.25)) {
|
||||||
|
if mayRetry {
|
||||||
|
self.draggablePiece.onDragHandler = self.onPieceDragEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onGameOver(board: Board, result: Connect4Core.Result, player: Player?) {
|
||||||
|
self.draggablePiece.removeFromParent()
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case .winner(_, let alignment):
|
||||||
|
for cell in cells {
|
||||||
|
if alignment.contains(where: {
|
||||||
|
$0.row == cell.boardPosition!.y && $0.col == cell.boardPosition!.x
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.alpha = 0.25
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
for c in cancellables { c.cancel() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
enum RulesType {
|
enum RulesType: CaseIterable, Identifiable {
|
||||||
case Classic
|
var id: Self { self }
|
||||||
|
|
||||||
|
case Classic, TicTacToe, PopOut
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
@ViewBuilder func `if`<Content: View>(_ condition: Bool, _ modifier: (Self) -> Content) -> some View {
|
||||||
|
if condition {
|
||||||
|
modifier(self)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Binding {
|
||||||
|
func map<T>(_ get: @escaping (Value) -> T) -> Binding<T> {
|
||||||
|
Binding<T>(
|
||||||
|
get: { get(self.wrappedValue) },
|
||||||
|
set: { _ in fatalError("Not implemented") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func map<T>(
|
||||||
|
get: @escaping (Value) -> T,
|
||||||
|
set: @escaping (T) -> Value
|
||||||
|
) -> Binding<T> {
|
||||||
|
Binding<T>(
|
||||||
|
get: { get(self.wrappedValue) },
|
||||||
|
set: { self.wrappedValue = set($0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import Foundation
|
||||||
|
import Connect4Core
|
||||||
|
import Connect4Players
|
||||||
|
|
||||||
|
class PlayerVM: ObservableObject {
|
||||||
|
private let inner: Player
|
||||||
|
|
||||||
|
var name: String { inner.name }
|
||||||
|
var type: PlayerType
|
||||||
|
|
||||||
|
init(inner: Player) {
|
||||||
|
self.inner = inner
|
||||||
|
self.type = switch(inner) {
|
||||||
|
case is RandomPlayer: .AIRandom
|
||||||
|
case is FinnishHimPlayer: .AIFinnishHim
|
||||||
|
case is SimpleNegaMaxPlayer: .AISimpleNegaMax
|
||||||
|
default: .Human
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue