-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
determineDeadStones
helper function
- Loading branch information
Showing
10 changed files
with
406 additions
and
43 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)") | ||
} | ||
} | ||
} |
Oops, something went wrong.