Skip to content
This repository was archived by the owner on Apr 23, 2025. It is now read-only.

Commit a7e4c7a

Browse files
authored
MiniGo style pass and and minor logic enhancements. (#131)
Logic changes: * In LibertyTracker.swift, use dictionary indices to avoid force unwrapping. Removed a lot of bangs! * Make `ModelConfiguration.boardSize` public since the initializer takes a `boardSize` for better transparency. It's a little weird for an initializer to take a constant, store it and make it private. * Reduce nesting in `LibertyTracker.updateLibertiesAfterRemovingCapturedStones(_:)` with `compactMap(_:)`. * Simplify `assignNewGroupID()` with a `defer` block. Formatting changes: * Make every file end with exactly 1 empty line. * Add some minor comments. * Remove empty lines between the start of a type declaration or extension and the first declaration for consistency. * Make `GoModel` code fit within 100 columns.
1 parent a53966a commit a7e4c7a

File tree

10 files changed

+80
-95
lines changed

10 files changed

+80
-95
lines changed

MiniGo/GameLib/Board.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ struct Board: Hashable {
4646
}
4747

4848
extension Board: CustomStringConvertible {
49-
5049
var description: String {
5150
var output = ""
5251

@@ -103,4 +102,3 @@ extension Board: CustomStringConvertible {
103102
return output
104103
}
105104
}
106-

MiniGo/GameLib/BoardState.swift

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ private enum PositionStatus: Equatable {
3838
/// `BoardState` checks whether a new placed stone is legal or not. If so,
3939
/// creates a new snapshot.
4040
public struct BoardState {
41-
41+
/// The game configuration.
4242
let gameConfiguration: GameConfiguration
43+
/// The color of the next player.
4344
let nextPlayerColor: Color
4445

4546
/// The position of potential `ko`. See `IllegalMove.ko` for details.
@@ -218,7 +219,6 @@ extension BoardState: Equatable {
218219
}
219220

220221
extension Board {
221-
222222
/// Calculates all legal moves on board.
223223
fileprivate func allLegalMoves(
224224
ko: Position?,
@@ -276,8 +276,7 @@ extension Board {
276276

277277
for neighbor in position.neighbors(boardSize: self.size) {
278278
guard let group = libertyTracker.group(at: neighbor) else {
279-
// If the neighbor is not occupied, no liberty group, the position is
280-
// OK.
279+
// If the neighbor is not occupied, no liberty group, the position is OK.
281280
return false
282281
}
283282
if group.color == nextPlayerColor {
@@ -288,14 +287,14 @@ extension Board {
288287
}
289288
}
290289

291-
// After removing the new postion from liberties, if there is no liberties
292-
// left, this move is suicide.
290+
// After removing the new postion from liberties, if there is no liberties left, this move
291+
// is suicide.
293292
possibleLiberties.remove(position)
294293
return possibleLiberties.isEmpty
295294
}
296295

297-
/// Checks whether the position is a potential ko, i.e., whether the position is surrounded by all
298-
/// sides belonging to the opponent.
296+
/// Checks whether the position is a potential ko, i.e., whether the position is surrounded by
297+
/// all sides belonging to the opponent.
299298
///
300299
/// This is an approximated algorithm to find `ko`. See https://en.wikipedia.org/wiki/Ko_fight
301300
/// for details.
@@ -320,7 +319,6 @@ extension Board {
320319
/// `komi` is the points added to the score of the player with the white stones as compensation
321320
/// for playing second.
322321
fileprivate func scoreForBlackPlayer(komi: Float) -> Float {
323-
324322
// Makes a copy as we will modify it over time.
325323
var scoreBoard = self
326324

@@ -335,8 +333,9 @@ extension Board {
335333
}
336334
}
337335

338-
// Second pass: Calculates the territory and borders for each empty position, if there is any.
339-
// If territory is surrounded by the stones in same color, fills that color in territory.
336+
// Second pass: Calculates the territory and borders for each empty position, if there is
337+
// any. If territory is surrounded by the stones in same color, fills that color in
338+
// territory.
340339
while !emptyPositions.isEmpty {
341340
let emptyPosition = emptyPositions.removeFirst()
342341

@@ -402,7 +401,8 @@ extension Board {
402401
for neighbor in currentPosition.neighbors(boardSize: self.size) {
403402
if self.color(at: neighbor) == nil {
404403
if !territory.contains(neighbor) {
405-
// We have not explored this (empty) position, so queue it up for processing.
404+
// We have not explored this (empty) position, so queue it up for
405+
// processing.
406406
candidates.insert(neighbor)
407407
}
408408
} else {
@@ -412,12 +412,10 @@ extension Board {
412412
}
413413
} while !candidates.isEmpty
414414

415-
precondition(
416-
territory.allSatisfy { self.color(at: $0) == nil },
417-
"territory must be all empty (no stones).")
418-
precondition(
419-
borders.allSatisfy { self.color(at: $0) != nil },
420-
"borders cannot have empty positions.")
415+
precondition(territory.allSatisfy { self.color(at: $0) == nil },
416+
"territory must be all empty (no stones).")
417+
precondition(borders.allSatisfy { self.color(at: $0) != nil },
418+
"borders cannot have empty positions.")
421419
return (territory, borders)
422420
}
423421
}

MiniGo/GameLib/Color.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,3 @@ extension Color: CustomStringConvertible {
2828
}
2929
}
3030
}
31-

MiniGo/GameLib/GameConfiguration.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@
1515
/// Represents an (immutable) configuration of a Go game.
1616
public struct GameConfiguration {
1717
/// The board size of the game.
18-
let size: Int
18+
public let size: Int
1919

2020
/// The points added to the score of the player with the white stones as compensation for playing
2121
/// second.
22-
let komi: Float
22+
public let komi: Float
2323

2424
/// The maximum number of board states to track.
2525
///
2626
/// This does not include the the current board.
27-
let maxHistoryCount: Int
27+
public let maxHistoryCount: Int
2828

2929
/// If true, enables debugging information.
30-
let isVerboseDebuggingEnabled: Bool
30+
public let isVerboseDebuggingEnabled: Bool
3131

3232
public init(
3333
size: Int,
@@ -41,4 +41,3 @@ public struct GameConfiguration {
4141
self.isVerboseDebuggingEnabled = isVerboseDebuggingEnabled
4242
}
4343
}
44-

MiniGo/GameLib/LibertyTracker.swift

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,13 @@ struct LibertyGroup {
3333
/// placing a new stone, we make a copy, update it and then attach it to new
3434
/// board snapshot to track state.
3535
struct LibertyTracker {
36-
36+
/// The game configuration.
3737
private let gameConfiguration: GameConfiguration
3838

39-
// Tracks the liberty groups. For a position (stone) having no group,
40-
// groupIndex[stone] should be nil. Otherwise, the group ID should be
41-
// groupIndex[stone] and its group is groups[groupIndex[stone]].
42-
// The invariance check can be done via checkLibertyGroupsInvariance helper
43-
// method.
39+
// Tracks the liberty groups. For a position (stone) having no group, `groupIndex[stone]` should
40+
// be `nil`. Otherwise, the group ID should be `groupIndex[stone]` and its group is
41+
// `groups[groupIndex[stone]]`. The invariance check can be done via the
42+
// `checkLibertyGroupsInvariance()` helper method.
4443
private var nextGroupIDToAssign = 0
4544
private var groupIndex: [[Int?]]
4645
private var groups: [Int: LibertyGroup] = [:]
@@ -64,9 +63,8 @@ struct LibertyTracker {
6463
}
6564
}
6665

67-
/// Extend `LibertyTracker` to have a mutating method by placing a new stone.
66+
// Extend `LibertyTracker` to have a mutating method by placing a new stone.
6867
extension LibertyTracker {
69-
7068
/// Adds a new stone to the board and returns all captured stones.
7169
mutating func addStone(at position: Position, withColor color: Color) throws -> Set<Position> {
7270
precondition(groupIndex(for: position) == nil)
@@ -81,7 +79,6 @@ extension LibertyTracker {
8179
var friendlyNeighboringGroupIDs = Set<Int>()
8280

8381
for neighbor in position.neighbors(boardSize: gameConfiguration.size) {
84-
8582
// First, handle the case neighbor has no group.
8683
guard let neighborGroupID = groupIndex(for: neighbor) else {
8784
emptyNeighbors.insert(neighbor)
@@ -184,11 +181,9 @@ extension LibertyTracker {
184181

185182
/// Assigns a new unique group ID.
186183
mutating private func assignNewGroupID() -> Int {
187-
let newID = nextGroupIDToAssign
188-
precondition(groups[newID] == nil)
189-
190-
nextGroupIDToAssign += 1
191-
return newID
184+
defer { nextGroupIDToAssign += 1 }
185+
precondition(!groups.keys.contains(nextGroupIDToAssign))
186+
return nextGroupIDToAssign
192187
}
193188

194189
/// Creates a new group for the single stone with liberties.
@@ -200,7 +195,7 @@ extension LibertyTracker {
200195
let newID = assignNewGroupID()
201196
let newGroup = LibertyGroup(id: newID, color: color, stones: [stone], liberties: liberties)
202197

203-
precondition(groups[newID] == nil)
198+
precondition(!groups.keys.contains(newID))
204199
groups[newID] = newGroup
205200
groupIndex[stone.x][stone.y] = newID
206201
assert(checkLibertyGroupsInvariance())
@@ -243,27 +238,27 @@ extension LibertyTracker {
243238

244239
/// Captures the whole group and returns all stones in it.
245240
mutating private func captureGroup(_ groupID: Int) -> Set<Position> {
246-
let deadGroup = groups.removeValue(forKey: groupID)!
241+
guard let index = groups.index(forKey: groupID) else {
242+
fatalErrorForGroupsInvariance(groupID: groupID)
243+
}
244+
let deadGroup = groups.remove(at: index).value
247245
for stone in deadGroup.stones {
248246
groupIndex[stone.x][stone.y] = nil
249247
}
250248
return deadGroup.stones
251249
}
252250

253251
/// Updates all neighboring groups' liberties.
254-
mutating private func updateLibertiesAfterRemovingCapturedStones(_ capturedStones: Set<Position>) {
252+
mutating private func updateLibertiesAfterRemovingCapturedStones(
253+
_ capturedStones: Set<Position>
254+
) {
255255
let size = gameConfiguration.size
256256
for capturedStone in capturedStones {
257-
for neighbor in capturedStone.neighbors(boardSize: size) {
258-
if let neighborGroupdID = groupIndex(for: neighbor) {
259-
guard groups.keys.contains(neighborGroupdID) else {
260-
fatalErrorForGroupsInvariance(groupID:neighborGroupdID)
261-
}
262-
// This force unwrap is safe as we checked the key existence above. As
263-
// the value in the groups is struct. We need the force unwrap to do
264-
// mutation in place.
265-
groups[neighborGroupdID]!.liberties.insert(capturedStone)
257+
for neighborGroupID in capturedStone.neighbors(boardSize: size).compactMap(groupIndex) {
258+
guard let index = groups.index(forKey: neighborGroupID) else {
259+
fatalErrorForGroupsInvariance(groupID: neighborGroupID)
266260
}
261+
groups.values[index].liberties.insert(capturedStone)
267262
}
268263
}
269264
assert(checkLibertyGroupsInvariance())
@@ -277,7 +272,7 @@ extension LibertyTracker {
277272

278273
print(message)
279274

280-
/// Prints the group index for the board.
275+
// Prints the group index for the board.
281276
let size = gameConfiguration.size
282277
for x in 0..<size {
283278
for y in 0..<size {
@@ -298,5 +293,3 @@ extension LibertyTracker {
298293
}
299294
}
300295
}
301-
302-

MiniGo/GameLib/Position.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,24 @@
1414

1515
/// Represents a position in a Go game.
1616
public struct Position: Hashable, Equatable {
17-
var x: Int
18-
var y: Int
17+
public var x: Int
18+
public var y: Int
19+
20+
public init(x: Int, y: Int) {
21+
self.x = x
22+
self.y = y
23+
}
1924
}
2025

2126
/// Returns all valid neighbors for the given position on board.
2227
extension Position {
23-
2428
func neighbors(boardSize size: Int) -> [Position] {
2529
let neighbors = [
2630
Position(x: x+1, y: y),
2731
Position(x: x-1, y: y),
2832
Position(x: x, y: y+1),
2933
Position(x: x, y: y-1),
3034
]
31-
3235
return neighbors.filter { 0..<size ~= $0.x && 0..<size ~= $0.y }
3336
}
3437
}

MiniGo/Models/GoModel.swift

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@
1717
import TensorFlow
1818

1919
public struct ModelConfiguration {
20-
/// size of Go board (typically 9 or 19)
21-
let boardSize: Int
22-
/// output feature count of conv layers in shared trunk
20+
/// The size of the Go board (typically `9` or `19`).
21+
public let boardSize: Int
22+
/// The number of output features of conv layers in shared trunk.
2323
let convWidth: Int
24-
/// output feature count of conv layer in policy head
24+
/// The output feature count of conv layer in policy head.
2525
let policyConvWidth: Int
26-
/// output feature count of conv layer in value head
26+
/// The output feature count of conv layer in value head.
2727
let valueConvWidth: Int
28-
/// output feature count of dense layer in value head
28+
/// The output feature count of dense layer in value head.
2929
let valueDenseWidth: Int
30-
/// number of layers (typically equal to boardSize)
30+
/// The number of layers (typically equal to `boardSize`).
3131
let layerCount: Int
3232

3333
public init(boardSize: Int) {
@@ -143,32 +143,32 @@ public struct GoModel: Layer {
143143

144144
public init(configuration: ModelConfiguration) {
145145
self.configuration = configuration
146+
146147
initialConv = ConvBN(
147148
filterShape: (3, 3, 17, configuration.convWidth),
148149
padding: .same,
149150
bias: false)
150-
151151
residualBlocks = (1...configuration.boardSize).map { _ in
152152
ResidualIdentityBlock(featureCounts: (configuration.convWidth, configuration.convWidth))
153153
}
154-
155154
policyConv = ConvBN(
156155
filterShape: (1, 1, configuration.convWidth, configuration.policyConvWidth),
157156
padding: .same,
158157
bias: false,
159158
affine: false)
160159
policyDense = Dense<Float>(
161-
inputSize: configuration.policyConvWidth * configuration.boardSize * configuration.boardSize,
160+
inputSize: configuration.policyConvWidth * configuration.boardSize
161+
* configuration.boardSize,
162162
outputSize: configuration.boardSize * configuration.boardSize + 1,
163163
activation: {$0})
164-
165164
valueConv = ConvBN(
166165
filterShape: (1, 1, configuration.convWidth, configuration.valueConvWidth),
167166
padding: .same,
168167
bias: false,
169168
affine: false)
170169
valueDense1 = Dense<Float>(
171-
inputSize: configuration.valueConvWidth * configuration.boardSize * configuration.boardSize,
170+
inputSize: configuration.valueConvWidth * configuration.boardSize
171+
* configuration.boardSize,
172172
outputSize: configuration.valueDenseWidth,
173173
activation: relu)
174174
valueDense2 = Dense<Float>(
@@ -188,16 +188,14 @@ public struct GoModel: Layer {
188188

189189
let policyConvOutput = relu(policyConv(output))
190190
let logits = policyDense(policyConvOutput.reshaped(to:
191-
[batchSize,
192-
configuration.policyConvWidth * configuration.boardSize * configuration.boardSize
193-
]))
191+
[batchSize,
192+
configuration.policyConvWidth * configuration.boardSize * configuration.boardSize]))
194193
let policyOutput = softmax(logits)
195194

196195
let valueConvOutput = relu(valueConv(output))
197196
let valueHidden = valueDense1(valueConvOutput.reshaped(to:
198-
[batchSize,
199-
configuration.valueConvWidth * configuration.boardSize * configuration.boardSize
200-
]))
197+
[batchSize,
198+
configuration.valueConvWidth * configuration.boardSize * configuration.boardSize]))
201199
let valueOutput = valueDense2(valueHidden).reshaped(to: [batchSize])
202200

203201
return GoModelOutput(policy: policyOutput, value: valueOutput, logits: logits)

0 commit comments

Comments
 (0)