Throw away and redo most of board

+ filling notation points to score...
Boards
Mathieu GROUSSEAU 1 month ago
parent 9a2495c874
commit 98e4ad2232

@ -1,11 +1,11 @@
import Model import Model
extension Piece: CustomStringConvertible { extension Player: CustomStringConvertible {
public var description: String { public var description: String {
switch self { switch self {
case .PlayerA: case .A:
"🔴" "🔴"
case .PlayerB: case .B:
"🟡" "🟡"
} }
} }
@ -17,7 +17,7 @@ extension Board: CustomStringConvertible, CustomDebugStringConvertible {
for row in 0..<self.rows { for row in 0..<self.rows {
for col in 0..<self.columns { for col in 0..<self.columns {
str += self[col, row]?.description ?? " " str += self[col, row]?.owner.description ?? ""
} }
str += "\n" str += "\n"
@ -27,8 +27,7 @@ extension Board: CustomStringConvertible, CustomDebugStringConvertible {
} }
public var debugDescription: String { public var debugDescription: String {
let (a, b) = self.countPieces() return "Board[\(self.columns)x\(self.rows), \(self.countPieces { p in p.owner == .A }) A and \(self.countPieces { p in p.owner == .B }) B pieces]"
return "Board[\(self.columns)x\(self.rows), \(a) A and \(b) B pieces]"
} }
} }

@ -9,13 +9,13 @@ final class CustomTypesTests: XCTestCase {
return return
} }
board[1, 1] = .PlayerA board[1, 1] = Piece(owner: .A)
board[2, 1] = .PlayerA board[2, 1] = Piece(owner: .A)
board[2, 2] = .PlayerA board[2, 2] = Piece(owner: .A)
board[1, 2] = .PlayerB board[1, 2] = Piece(owner: .B)
let text: String = board.description let text: String = board.description
XCTAssertEqual(text, " \n 🔴🔴 \n 🟡🔴 \n \n") XCTAssertEqual(text, "⚪⚪⚪⚪\n⚪🔴🔴⚪\n⚪🟡🔴⚪\n⚪⚪⚪⚪\n")
} }
} }

@ -16,128 +16,105 @@ public struct Board {
self.grid = grid self.grid = grid
} }
private func checkBounds(_ column: Int, _ row: Int) { private func isInBounds(_ pos: Coords) -> Bool {
precondition(column >= 0 && column < self.columns && row >= 0 && row < self.rows, "Coordinates out of bounds") pos.col >= 0 && pos.col < self.columns && pos.row >= 0 && pos.row < self.rows
}
private func ensureBounds(_ pos: Coords) {
precondition(isInBounds(pos), "Coordinates out of bounds")
} }
public subscript(column: Int, row: Int) -> Piece? { public subscript(column: Int, row: Int) -> Piece? {
get { self[Coords(column, row)] }
set { self[Coords(column, row)] = newValue }
}
public subscript(pos: Coords) -> Piece? {
get { get {
checkBounds(column, row) ensureBounds(pos)
return grid[column][row] return grid[pos.col][pos.row]
} }
set { set(piece) {
checkBounds(column, row) ensureBounds(pos)
grid[column][row] = newValue grid[pos.col][pos.row] = piece
} }
} }
public func countPieces() -> (a: Int, b: Int) { public func countPieces(filter: (Piece) -> Bool = { piece in true }) -> Int {
var a = 0 var count = 0
var b = 0
for column in grid { for column in grid {
for piece in column { for piece in column {
switch piece { if let piece = piece {
case .PlayerA: if filter(piece) {
a += 1 count += 1
case .PlayerB: }
b += 1
case nil:
break
} }
} }
} }
return (a, b) return count
} }
@discardableResult // insertion coordinate computation is intentionally separated from actual insertion because the user may want to
public mutating func insert(piece: Piece, side: Side = .Top, offset: Int, pushing push: Bool = false) -> Bool { // display a where the piece would end-up before doing the actual insertion
public func getInsertionCoordinates(direction: Direction, offset: Int) -> Coords {
precondition(offset >= 0, "Offset out of bounds") precondition(offset >= 0, "Offset out of bounds")
switch side { switch direction {
case .Top: case .Top:
precondition(offset < self.columns, "Offset (column) out of bounds") precondition(offset < self.columns, "Offset (column) out of bounds")
return Coords(offset, 0)
if self.grid[offset].first! != nil {
if (!push) {
return false
}
Board.shiftDown(column: &self.grid[offset])
}
self.grid[offset][0] = piece
return true
case .Left: case .Left:
precondition(offset < self.rows, "Offset (row) out of bounds") precondition(offset < self.rows, "Offset (row) out of bounds")
return Coords(0, offset)
if self.grid.first![offset] != nil {
if (!push) {
return false
}
self.shiftRight(row: offset)
}
self.grid[0][offset] = piece
return true
case .Bottom: case .Bottom:
precondition(offset < self.columns, "Offset (column) out of bounds") precondition(offset < self.columns, "Offset (column) out of bounds")
return Coords(offset, self.rows - 1)
if self.grid[offset].last! != nil {
if (!push) {
return false
}
Board.shiftUp(column: &self.grid[offset])
}
let lastIndex = self.grid[offset].count - 1
self.grid[offset][lastIndex] = piece
return true
case .Right: case .Right:
precondition(offset < self.rows, "Offset (row) out of bounds") precondition(offset < self.rows, "Offset (row) out of bounds")
return Coords(self.columns - 1, offset)
if self.grid.last![offset] != nil {
if (!push) {
return false
}
self.shiftLeft(row: offset)
}
self.grid[self.columns - 1][offset] = piece
return true
} }
} }
private static func shiftDown(column: inout [Piece?]) { public enum FallResult: Equatable {
for i in (1..<column.count).reversed() { case Border(at: Coords)
column[i] = column[i - 1] case Piece(at: Coords, touched: Coords)
} case Occupied
} }
private static func shiftUp(column: inout [Piece?]) { public func fallCoordinates(initialCoords: Coords, direction: Direction) -> FallResult {
for i in 0..<(column.count - 1) { ensureBounds(initialCoords)
column[i] = column[i + 1]
let dir: (dc: Int, dr: Int) = switch direction {
case .Top: (0, -1)
case .Left: (-1, 0)
case .Bottom: (0, 1)
case .Right: (1, 0)
} }
}
if (self[initialCoords] != nil) {
private mutating func shiftLeft(row: Int) { return FallResult.Occupied
for i in 0..<(grid.count - 1) {
self.grid[i][row] = self.grid[i + 1][row]
} }
}
var coords = initialCoords
private mutating func shiftRight(row: Int) { var next: Coords
for i in 1..<grid.count { while true {
self.grid[i - 1][row] = self.grid[i][row] next = Coords(coords.col + dir.dc, coords.row + dir.dr)
if !isInBounds(next) {
return FallResult.Border(at: coords)
} else if self[next] != nil {
return FallResult.Piece(at: coords, touched: next)
}
coords.col = next.col
coords.row = next.row
} }
} }
// TODO push
} }

@ -0,0 +1,14 @@
// public typealias Coords = (col: Int, row: Int)
public struct Coords: Equatable {
var col: Int
var row: Int
public init(_ col: Int, _ row: Int) {
self.col = col
self.row = row
}
public init(pair: (Int, Int)) {
self.init(pair.0, pair.1)
}
}

@ -0,0 +1,16 @@
public enum Direction {
case Top, Left, Bottom, Right
var opposite: Self {
switch self {
case .Top:
.Bottom
case .Left:
.Right
case .Bottom:
.Top
case .Right:
.Left
}
}
}

@ -1,3 +1,8 @@
public enum Piece { public struct Piece {
case PlayerA, PlayerB public let owner: Player
// Required for public visibility
public init(owner: Player) {
self.owner = owner
}
} }

@ -0,0 +1,3 @@
public enum Player {
case A, B
}

@ -1,3 +0,0 @@
public enum Side {
case Top, Left, Bottom, Right
}

@ -4,7 +4,7 @@ import XCTest
final class EmptyBoardTests: XCTestCase { final class EmptyBoardTests: XCTestCase {
private var board: Board! private var board: Board!
override func setUp() { override func setUpWithError() throws {
super.setUp() super.setUp()
guard let board = Board(columns: 5, rows: 6) else { guard let board = Board(columns: 5, rows: 6) else {
@ -26,41 +26,128 @@ final class EmptyBoardTests: XCTestCase {
func testEmptyByDefault() throws { func testEmptyByDefault() throws {
XCTAssertNil(board[1, 1]) XCTAssertNil(board[1, 1])
let result = board.countPieces() let result = board.countPieces()
XCTAssertEqual(result.a, 0) XCTAssertEqual(result, 0)
XCTAssertEqual(result.b, 0)
} }
func testSetGet() throws { func testCustomSubscript() throws {
board[1, 1] = Piece.PlayerA; for r in 0..<board.rows {
for c in 0..<board.columns {
for p: Player in [.A, .B] {
try self.setUpWithError()
try self._testCustomSubscript(at: Coords(c, r), player: p)
try self.tearDownWithError()
}
}
}
}
private func _testCustomSubscript(at: Coords, player: Player) throws {
board[at] = Piece(owner: player)
XCTAssertEqual(board[1, 1], .PlayerA) XCTAssertEqual(board[at]?.owner, player)
} }
func testCounts() throws { func testCounts() throws {
board[1, 2] = .PlayerB board[1, 2] = Piece(owner: .B)
board[0, 2] = Piece(owner: .A)
board[1, 1] = Piece(owner: .A)
let counts = board.countPieces() XCTAssertEqual(board.countPieces { piece in piece.owner == .A }, 2)
XCTAssertEqual(counts.a, 0) XCTAssertEqual(board.countPieces { piece in piece.owner == .B }, 1)
XCTAssertEqual(counts.b, 1) }
func testInsertsSides() throws {
for (side, offset, expected): (Direction, Int, (Int, Int)) in [
(.Top, 0, (0, 0)),
(.Left, 0, (0, 0)),
(.Bottom, 0, (0, 5)),
(.Right, 0, (4, 0)),
/* ... */
(.Top, 4, (4, 0)),
(.Left, 5, (0, 5)),
(.Bottom, 4, (4, 5)),
(.Right, 5, (4, 5)),
] {
try self.setUpWithError()
try self._testInsertSide(side: side, offset: offset, expectedCoords: Coords(pair: expected))
try self.tearDownWithError()
}
} }
func testInsertTopNoPush() { private func _testInsertSide(side: Direction, offset: Int, expectedCoords: Coords) throws {
XCTAssert(board.insert(piece: .PlayerA, side: .Top, offset: 2)) let result = board.getInsertionCoordinates(direction: side, offset: offset)
XCTAssertEqual(board[2, 0], .PlayerA)
// Note: Not sure I want Coordinates to be a simple tuple or a struct, as both have advantages that the other dont:
// Tuples can be initialized like so (col, row) which I find quite handy
// Structs supports == operators
// ...
XCTAssertEqual(result.col, expectedCoords.col)
XCTAssertEqual(result.row, expectedCoords.row)
} }
func testInsertBottomNoPush() { func testInsertsFallNoPiece() throws {
XCTAssert(board.insert(piece: .PlayerB, side: .Bottom, offset: 2)) for (from, direction, expected): ((Int, Int), Direction, (Int, Int)) in [
XCTAssertEqual(board[2, board.rows - 1], .PlayerB) ((0, 0), .Bottom, (0, 5)),
((0, 0), .Left, (0, 0)),
((0, 0), .Right, (4, 0)),
((0, 0), .Top, (0, 0)),
/* ... */
((4, 5), .Bottom, (4, 5)),
((4, 5), .Left, (0, 5)),
((4, 5), .Right, (4, 5)),
((4, 5), .Top, (4, 0)),
] {
try self.setUpWithError()
try self._testInsertFall(from: Coords(pair: from), dir: direction, expectedCoords: .Border(at: Coords(pair: expected)))
try self.tearDownWithError()
}
} }
func testInsertLeftNoPush() { func testInsertsFallOnPiece() throws {
XCTAssert(board.insert(piece: .PlayerA, side: .Left, offset: 2)) for (from, direction, (expected, touched)): ((Int, Int), Direction, ((Int, Int), (Int, Int))) in [
XCTAssertEqual(board[0, 2], .PlayerA) ((0, 0), .Bottom, ((0, 1), (0, 2))),
((0, 0), .Right, ((1, 0), (2, 0))),
((4, 5), .Left, ((3, 5), (2, 5))),
((4, 5), .Top, ((4, 3), (4, 2))),
] {
try self.setUpWithError()
// Place some pieces
for pos in [
(2, 0),
(2, 1),
(0, 2), (1, 2), (2, 2), (3, 2), (4, 2),
(2, 3),
(2, 4),
(2, 5),
] {
// ensure it works with any player
board[Coords(pair: pos)] = if (pos.0 + pos.1) & 1 == 1 {
Piece(owner: .A)
} else {
Piece(owner: .B)
}
}
try self._testInsertFall(
from: Coords(pair: from),
dir: direction,
expectedCoords: .Piece(at: Coords(pair: expected), touched: Coords(pair: touched))
)
try self.tearDownWithError()
}
} }
func testInsertRightNoPush() { private func _testInsertFall(from: Coords, dir: Direction, expectedCoords: Board.FallResult) throws {
XCTAssert(board.insert(piece: .PlayerB, side: .Right, offset: 2)) let result = board.fallCoordinates(initialCoords: from, direction: dir)
XCTAssertEqual(board[board.columns - 1, 2], .PlayerB)
XCTAssertEqual(result, expectedCoords)
} }
} }

@ -15,9 +15,9 @@ final class FilledBoardTests: XCTestCase {
for row in 0..<board.rows { for row in 0..<board.rows {
for col in 0..<board.columns { for col in 0..<board.columns {
board[col, row] = if (row & 1) == 1 { board[col, row] = if (row & 1) == 1 {
.PlayerA Piece(owner: .A)
} else { } else {
.PlayerB Piece(owner: .B)
} }
} }
} }
@ -25,29 +25,33 @@ final class FilledBoardTests: XCTestCase {
self.board = board self.board = board
} }
func testInsertFailTopNoPush() { func testRemovePiece() throws {
let before = board[2, 0] board[1, 2] = nil
XCTAssertFalse(board.insert(piece: .PlayerA, side: .Top, offset: 2))
XCTAssertEqual(before, board[2, 0]) XCTAssertNil(board[1, 2])
}
func testInsertFailBottomNoPush() {
let before = board[2, board.rows - 1]
XCTAssertFalse(board.insert(piece: .PlayerB, side: .Bottom, offset: 2))
XCTAssertEqual(before, board[2, board.rows - 1])
} }
func testInsertFailLeftNoPush() { func testInsertsFallFail() throws {
let before = board[2, 0] for (from, direction): ((Int, Int), Direction) in [
XCTAssertFalse(board.insert(piece: .PlayerA, side: .Left, offset: 2)) ((0, 0), .Bottom),
XCTAssertEqual(before, board[0, 2]) ((0, 0), .Right),
((4, 5), .Left),
((4, 5), .Top),
((4, 5), .Bottom),
((4, 5), .Right),
((0, 0), .Left),
((0, 0), .Top),
] {
try self.setUpWithError()
try self._testInsertFallFail(from: Coords(pair: from), dir: direction)
try self.tearDownWithError()
}
} }
func testInsertFailRightNoPush() { private func _testInsertFallFail(from: Coords, dir: Direction) throws {
let before = board[board.columns - 1, 2] XCTAssertEqual(board.fallCoordinates(initialCoords: from, direction: dir), .Occupied)
XCTAssertFalse(board.insert(piece: .PlayerB, side: .Right, offset: 2))
XCTAssertEqual(before, board[board.columns - 1, 2])
} }
// TODO Test shifting
} }

Loading…
Cancel
Save