Skip to content

Commit

Permalink
feat: add determineDeadStones helper function
Browse files Browse the repository at this point in the history
  • Loading branch information
mesqueeb committed Jun 11, 2024
1 parent c4aca02 commit a6a3a77
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 43 deletions.
16 changes: 16 additions & 0 deletions FuegoOnAppleSilicon.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 0 additions & 43 deletions FuegoOnAppleSilicon/SwiftBridge/FuegoBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,49 +19,6 @@ public enum GenMoveResponse: Sendable {
}
}

/// The Go Board Coordinates from A1 until T19
///
/// When a Coordinate is supposed to be returned but something is returned instead, it will be case `Other(String?)`
public enum GoBoardCoordinate: String, CustomStringConvertible, Sendable {
case A19, B19, C19, D19, E19, F19, G19, H19, J19, K19, L19, M19, N19, O19, P19, Q19, R19, S19, T19,
A18, B18, C18, D18, E18, F18, G18, H18, J18, K18, L18, M18, N18, O18, P18, Q18, R18, S18, T18,
A17, B17, C17, D17, E17, F17, G17, H17, J17, K17, L17, M17, N17, O17, P17, Q17, R17, S17, T17,
A16, B16, C16, D16, E16, F16, G16, H16, J16, K16, L16, M16, N16, O16, P16, Q16, R16, S16, T16,
A15, B15, C15, D15, E15, F15, G15, H15, J15, K15, L15, M15, N15, O15, P15, Q15, R15, S15, T15,
A14, B14, C14, D14, E14, F14, G14, H14, J14, K14, L14, M14, N14, O14, P14, Q14, R14, S14, T14,
A13, B13, C13, D13, E13, F13, G13, H13, J13, K13, L13, M13, N13, O13, P13, Q13, R13, S13, T13,
A12, B12, C12, D12, E12, F12, G12, H12, J12, K12, L12, M12, N12, O12, P12, Q12, R12, S12, T12,
A11, B11, C11, D11, E11, F11, G11, H11, J11, K11, L11, M11, N11, O11, P11, Q11, R11, S11, T11,
A10, B10, C10, D10, E10, F10, G10, H10, J10, K10, L10, M10, N10, O10, P10, Q10, R10, S10, T10,
A9, B9, C9, D9, E9, F9, G9, H9, J9, K9, L9, M9, N9, O9, P9, Q9, R9, S9, T9,
A8, B8, C8, D8, E8, F8, G8, H8, J8, K8, L8, M8, N8, O8, P8, Q8, R8, S8, T8,
A7, B7, C7, D7, E7, F7, G7, H7, J7, K7, L7, M7, N7, O7, P7, Q7, R7, S7, T7,
A6, B6, C6, D6, E6, F6, G6, H6, J6, K6, L6, M6, N6, O6, P6, Q6, R6, S6, T6,
A5, B5, C5, D5, E5, F5, G5, H5, J5, K5, L5, M5, N5, O5, P5, Q5, R5, S5, T5,
A4, B4, C4, D4, E4, F4, G4, H4, J4, K4, L4, M4, N4, O4, P4, Q4, R4, S4, T4,
A3, B3, C3, D3, E3, F3, G3, H3, J3, K3, L3, M3, N3, O3, P3, Q3, R3, S3, T3,
A2, B2, C2, D2, E2, F2, G2, H2, J2, K2, L2, M2, N2, O2, P2, Q2, R2, S2, T2,
A1, B1, C1, D1, E1, F1, G1, H1, J1, K1, L1, M1, N1, O1, P1, Q1, R1, S1, T1

/// The string used when this enum is interpolated
public var description: String {
return self.rawValue
}
}

/// The Go Stone color
public enum GoStoneColor: String, CustomStringConvertible, Sendable {
case black, white

/// The string used when this enum is interpolated
public var description: String {
switch self {
case .black: return "b"
case .white: return "w"
}
}
}

public actor FuegoBridge {
private var cookie: UnsafeMutableRawPointer?

Expand Down
112 changes: 112 additions & 0 deletions FuegoOnAppleSilicon/SwiftBridge/Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import JustSugar

let BOARDSIZE = 19

/// `nil` when there's no stone, or the color
typealias BoardState = [TileIndexes: GoStoneColor?]

/// Represents the indexes of a tile
struct TileIndexes: Equatable, Hashable {
let ri: Int
let ci: Int

init(_ ri: Int, _ ci: Int) {
self.ri = ri
self.ci = ci
}
}

/// Helper function to convert TileIndexes to GoBoardCoordinate
func tileToGoBoardCoordinate(_ tile: TileIndexes) -> GoBoardCoordinate {
let colChar = Character(UnicodeScalar(tile.ci + (tile.ci >= 8 ? 66 : 65))!)
return GoBoardCoordinate(rawValue: "\(colChar)\(tile.ri + 1)")!
}

/// Helper function to convert GoBoardCoordinate to TileIndexes
func goBoardCoordinateToTileIndexes(_ coordinate: GoBoardCoordinate) -> TileIndexes {
let ri = Int(coordinate.rawValue.dropFirst().prefix(2))! - 1
let colChar = coordinate.rawValue.prefix(1).utf8.first!
let col = colChar > Character("I").utf8.first! ? Int(colChar - Character("A").utf8.first!) - 1 : Int(colChar - Character("A").utf8.first!)
return TileIndexes(ri, col)
}

/// Helper function to check if a position is within bounds
func inBounds(_ ri: Int, _ col: Int) -> Bool {
return ri >= 0 && ri < BOARDSIZE && col >= 0 && col < BOARDSIZE
}

/// Helper function to perform flood-fill and find connected groups
func findGroup(
_ tile: TileIndexes,
_ boardState: BoardState,
visited: inout Set<TileIndexes>
) -> (group: [TileIndexes], hasLiberty: Bool) {
var group: [TileIndexes] = []
var hasLiberty = false

if !inBounds(tile.ri, tile.ci) { return (group: group, hasLiberty: hasLiberty) }
if visited.contains(tile) { return (group: group, hasLiberty: hasLiberty) }

let directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
visited.insert(tile)

guard let tileColor = boardState[tile] else { return (group: group, hasLiberty: hasLiberty) }

group.append(tile)

for (rowDirection, colDirection) in directions {
let nextRi = tile.ri + rowDirection
let nextCi = tile.ci + colDirection
let nextT = TileIndexes(nextRi, nextCi)

if let nextColor = boardState[nextT] {
if nextColor == tileColor {
let nestedResults = findGroup(nextT, boardState, visited: &visited)
group.append(contentsOf: nestedResults.group)
if nestedResults.hasLiberty { hasLiberty = true }
} else {
// black stone
continue
}
} else {
// no stone
hasLiberty = true
}
}

return (group: group, hasLiberty: hasLiberty)
}

/// Given an array of Go Stones and their coordinates, determine the stones that are dead.
public func determineDeadStones(board: [(GoStoneColor, GoBoardCoordinate)], lastStonePlaced: GoBoardCoordinate?) -> [(GoStoneColor, GoBoardCoordinate)] {
// Initialize the board
var boardState: BoardState = [:]

// Populate the board with the given stones
for (color, coordinate) in board {
let t = goBoardCoordinateToTileIndexes(coordinate)
boardState[t] = color
}

var visited: Set<TileIndexes> = Set()
var dead: [TileIndexes] = []

// Traverse the board to find all groups and check their liberties
for tile in boardState.keys {
if !visited.contains(tile) {
let (tileGroup, hasLiberty) = findGroup(tile, boardState, visited: &visited)
if !hasLiberty {
dead.append(contentsOf: tileGroup)
}
}
}

var result: [(GoStoneColor, GoBoardCoordinate)] = []
for deadTile in dead {
let coordinate = tileToGoBoardCoordinate(deadTile)
if coordinate == lastStonePlaced { continue }
guard let color = boardState[deadTile] ?? nil else { continue }
result.append((color, coordinate))
}
return result
}
43 changes: 43 additions & 0 deletions FuegoOnAppleSilicon/SwiftBridge/Types.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

/// The Go Board Coordinates from A1 until T19
///
/// When a Coordinate is supposed to be returned but something is returned instead, it will be case `Other(String?)`
public enum GoBoardCoordinate: String, CustomStringConvertible, Sendable {
case A19, B19, C19, D19, E19, F19, G19, H19, J19, K19, L19, M19, N19, O19, P19, Q19, R19, S19, T19,
A18, B18, C18, D18, E18, F18, G18, H18, J18, K18, L18, M18, N18, O18, P18, Q18, R18, S18, T18,
A17, B17, C17, D17, E17, F17, G17, H17, J17, K17, L17, M17, N17, O17, P17, Q17, R17, S17, T17,
A16, B16, C16, D16, E16, F16, G16, H16, J16, K16, L16, M16, N16, O16, P16, Q16, R16, S16, T16,
A15, B15, C15, D15, E15, F15, G15, H15, J15, K15, L15, M15, N15, O15, P15, Q15, R15, S15, T15,
A14, B14, C14, D14, E14, F14, G14, H14, J14, K14, L14, M14, N14, O14, P14, Q14, R14, S14, T14,
A13, B13, C13, D13, E13, F13, G13, H13, J13, K13, L13, M13, N13, O13, P13, Q13, R13, S13, T13,
A12, B12, C12, D12, E12, F12, G12, H12, J12, K12, L12, M12, N12, O12, P12, Q12, R12, S12, T12,
A11, B11, C11, D11, E11, F11, G11, H11, J11, K11, L11, M11, N11, O11, P11, Q11, R11, S11, T11,
A10, B10, C10, D10, E10, F10, G10, H10, J10, K10, L10, M10, N10, O10, P10, Q10, R10, S10, T10,
A9, B9, C9, D9, E9, F9, G9, H9, J9, K9, L9, M9, N9, O9, P9, Q9, R9, S9, T9,
A8, B8, C8, D8, E8, F8, G8, H8, J8, K8, L8, M8, N8, O8, P8, Q8, R8, S8, T8,
A7, B7, C7, D7, E7, F7, G7, H7, J7, K7, L7, M7, N7, O7, P7, Q7, R7, S7, T7,
A6, B6, C6, D6, E6, F6, G6, H6, J6, K6, L6, M6, N6, O6, P6, Q6, R6, S6, T6,
A5, B5, C5, D5, E5, F5, G5, H5, J5, K5, L5, M5, N5, O5, P5, Q5, R5, S5, T5,
A4, B4, C4, D4, E4, F4, G4, H4, J4, K4, L4, M4, N4, O4, P4, Q4, R4, S4, T4,
A3, B3, C3, D3, E3, F3, G3, H3, J3, K3, L3, M3, N3, O3, P3, Q3, R3, S3, T3,
A2, B2, C2, D2, E2, F2, G2, H2, J2, K2, L2, M2, N2, O2, P2, Q2, R2, S2, T2,
A1, B1, C1, D1, E1, F1, G1, H1, J1, K1, L1, M1, N1, O1, P1, Q1, R1, S1, T1

/// The string used when this enum is interpolated
public var description: String {
return self.rawValue
}
}

/// The Go Stone color
public enum GoStoneColor: String, CustomStringConvertible, Sendable {
case black, white

/// The string used when this enum is interpolated
public var description: String {
switch self {
case .black: return "b"
case .white: return "w"
}
}
}
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,9 @@ let package = Package(
dependencies: ["FuegoCBridge"],
path: "FuegoOnAppleSilicon/SwiftBridge"
),
.testTarget(
name: "FuegoOnAppleSiliconTests",
dependencies: ["FuegoOnAppleSilicon"]
),
]
)
18 changes: 18 additions & 0 deletions Tests.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"configurations" : [
{
"id" : "41A54099-914E-4B75-98F5-7DD6A1C9803B",
"name" : "Configuration 1",
"options" : {

}
}
],
"defaultOptions" : {
"testTimeoutsEnabled" : true
},
"testTargets" : [

],
"version" : 1
}
117 changes: 117 additions & 0 deletions Tests/Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
@testable import FuegoOnAppleSilicon
import XCTest

final class FuegoOnAppleSiliconTests: XCTestCase {
func testTileIndexesToGoBoardCoordinate() {
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(0, 0)).rawValue, "A1", "Expected A1 for tile (0, 0)")
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(18, 18)).rawValue, "T19", "Expected T19 for tile (18, 18)")
// Test tiles around 'I'
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(0, 7)).rawValue, "H1", "Expected H1 for tile (0, 7)")
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(0, 8)).rawValue, "J1", "Expected J1 for tile (0, 8)")
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(1, 7)).rawValue, "H2", "Expected H2 for tile (1, 7)")
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(1, 8)).rawValue, "J2", "Expected J2 for tile (1, 8)")
// Additional tests to ensure surrounding tiles are correctly converted
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(0, 6)).rawValue, "G1", "Expected G1 for tile (0, 6)")
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(0, 9)).rawValue, "K1", "Expected K1 for tile (0, 9)")
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(1, 6)).rawValue, "G2", "Expected G2 for tile (1, 6)")
XCTAssertEqual(tileToGoBoardCoordinate(TileIndexes(1, 9)).rawValue, "K2", "Expected K2 for tile (1, 9)")
}

func testGoBoardCoordinateToTileIndexes() {
XCTAssertEqual(goBoardCoordinateToTileIndexes(.A1), TileIndexes(0, 0), "Expected tile (0, 0) for coordinate A1")
XCTAssertEqual(goBoardCoordinateToTileIndexes(.T19), TileIndexes(18, 18), "Expected tile (18, 18) for coordinate T19")
// Test coordinates around 'I'
XCTAssertEqual(goBoardCoordinateToTileIndexes(.H1), TileIndexes(0, 7), "Expected tile (0, 7) for coordinate H1")
XCTAssertEqual(goBoardCoordinateToTileIndexes(.J1), TileIndexes(0, 8), "Expected tile (0, 8) for coordinate J1")
XCTAssertEqual(goBoardCoordinateToTileIndexes(.H2), TileIndexes(1, 7), "Expected tile (1, 7) for coordinate H2")
XCTAssertEqual(goBoardCoordinateToTileIndexes(.J2), TileIndexes(1, 8), "Expected tile (1, 8) for coordinate J2")
// Additional tests to ensure surrounding coordinates are correctly converted
XCTAssertEqual(goBoardCoordinateToTileIndexes(.G1), TileIndexes(0, 6), "Expected tile (0, 6) for coordinate G1")
XCTAssertEqual(goBoardCoordinateToTileIndexes(.K1), TileIndexes(0, 9), "Expected tile (0, 9) for coordinate K1")
XCTAssertEqual(goBoardCoordinateToTileIndexes(.G2), TileIndexes(1, 6), "Expected tile (1, 6) for coordinate G2")
XCTAssertEqual(goBoardCoordinateToTileIndexes(.K2), TileIndexes(1, 9), "Expected tile (1, 9) for coordinate K2")
}

func testSingleBlackStoneSurroundedByWhite() {
let deadStones = determineDeadStones(board: [
/* */ (.white, .E4),
(.white, .D5), (.black, .E5), (.white, .F5),
/* */ (.white, .E6)
], lastStonePlaced: .E6)

let expectedDeadStones: [(GoStoneColor, GoBoardCoordinate)] = [
(.black, .E5)
]

XCTAssertEqual(deadStones.count, expectedDeadStones.count, "The number of dead stones is not as expected.")
for deadStone in deadStones {
XCTAssertTrue(expectedDeadStones.contains(where: { $0 == deadStone }), "Unexpected dead stone found: \(deadStone)")
}
}

func testTwoBlackStonesSurroundedByWhite() {
let deadStones = determineDeadStones(board: [
/* */ (.white, .E3),
(.white, .D4), (.black, .E4), (.white, .F4),
(.white, .D5), (.black, .E5), (.white, .F5),
/* */ (.white, .E6)
], lastStonePlaced: .E6)

let expectedDeadStones: [(GoStoneColor, GoBoardCoordinate)] = [
(.black, .E4), (.black, .E5)
]

XCTAssertEqual(deadStones.count, expectedDeadStones.count, "The number of dead stones is not as expected.")
for deadStone in deadStones {
XCTAssertTrue(expectedDeadStones.contains(where: { $0 == deadStone }), "Unexpected dead stone found: \(deadStone)")
}
}

func testThreeBlackStonesWithOneFreeSpotSurroundedByWhite() {
let deadStones = determineDeadStones(board: [
/* */ (.white, .D3), (.white, .E3),
(.white, .C4), (.black, .D4), (.black, .E4), (.white, .F4),
(.white, .C5), /* */ (.black, .E5), (.white, .F5),
/* */ (.white, .D6), (.white, .E6)
], lastStonePlaced: .E6)

let expectedDeadStones: [(GoStoneColor, GoBoardCoordinate)] = []

XCTAssertEqual(deadStones.count, expectedDeadStones.count, "The number of dead stones is not as expected.")
}

func testBlackStonesInCircleWithOneFreeSpotNotDead() {
let deadStones = determineDeadStones(board: [
/* */ (.white, .D3), (.white, .E3), (.white, .F3),
(.white, .C4), (.black, .D4), (.black, .E4), (.black, .F4), (.white, .G4),
(.white, .C5), (.black, .D5), /* */ (.black, .F5), (.white, .G5),
(.white, .C6), (.black, .D6), (.black, .E6), (.black, .F6), (.white, .G6),
/* */ (.white, .D7), (.white, .E7), (.white, .F7)
], lastStonePlaced: .E7)

let expectedDeadStones: [(GoStoneColor, GoBoardCoordinate)] = []

XCTAssertEqual(deadStones.count, expectedDeadStones.count, "The number of dead stones is not as expected.")
}

func testBlackStonesInCircleWithOneWhiteStoneInMiddleDead() {
let deadStones = determineDeadStones(board: [
/* */ (.white, .D3), (.white, .E3), (.white, .F3),
(.white, .C4), (.black, .D4), (.black, .E4), (.black, .F4), (.white, .G4),
(.white, .C5), (.black, .D5), (.white, .E5), (.black, .F5), (.white, .G5),
(.white, .C6), (.black, .D6), (.black, .E6), (.black, .F6), (.white, .G6),
/* */ (.white, .D7), (.white, .E7), (.white, .F7)
], lastStonePlaced: .E5)

let expectedDeadStones: [(GoStoneColor, GoBoardCoordinate)] = [
(.black, .D4), (.black, .E4), (.black, .F4),
(.black, .D5), /* */ (.black, .F5),
(.black, .D6), (.black, .E6), (.black, .F6)
]

XCTAssertEqual(deadStones.count, expectedDeadStones.count, "The number of dead stones is not as expected.")
for deadStone in deadStones {
XCTAssertTrue(expectedDeadStones.contains(where: { $0 == deadStone }), "Unexpected dead stone found: \(deadStone)")
}
}
}
Loading

0 comments on commit a6a3a77

Please sign in to comment.