parent
e5caaed004
commit
9ef00e4859
@ -1,54 +1,163 @@
|
||||
import SpriteKit
|
||||
import Combine
|
||||
import Connect4Core
|
||||
|
||||
public class GameScene: SKScene {
|
||||
public static let boardTopMargin: CGFloat = PieceNode.pieceSize / 2
|
||||
|
||||
private let vm: IngameVM
|
||||
|
||||
private var boardNode: BoardNode
|
||||
private var cells: [PieceNode]
|
||||
private var draggablePiece: PieceNode
|
||||
|
||||
public init(s: (w: Int, h: Int)) {
|
||||
let boardHeight = CGFloat(s.h) * PieceNode.pieceSize
|
||||
let gWidth = CGFloat(s.w) * PieceNode.pieceSize
|
||||
private var cancellables: [AnyCancellable] = []
|
||||
|
||||
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
|
||||
|
||||
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.piece = .init(withOwner: .noOne)
|
||||
draggablePiece.position = CGPoint(x: 0, y: boardHeight + Self.boardTopMargin + PieceNode.pieceSize / 2)
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
cells = []
|
||||
|
||||
super.init(size: CGSize(width: gWidth, height: gHeight))
|
||||
draggablePiece.onDragHandler = self.resetDragPiece
|
||||
|
||||
|
||||
// anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
backgroundColor = .clear
|
||||
|
||||
|
||||
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) {
|
||||
fatalError("Unreachable")
|
||||
}
|
||||
|
||||
private func resetDragPiece(piece: PieceNode) {
|
||||
piece.position = CGPoint(x: self.size.width / 2, y: self.size.height - PieceNode.pieceSize / 2)
|
||||
private var draggablePiecePlacement: CGPoint { CGPoint(
|
||||
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 {
|
||||
case Classic
|
||||
enum RulesType: CaseIterable, Identifiable {
|
||||
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