Building Minesweeper 3
Building a Board
Now that the game logic is in place, I'm ready to build a game board. Since there's a new layout system to work with, I'm going to use a Grid
. The first thing that I need to do is to make an Identifiable
type that stores a row worth of grid cells, and expose it in the GameService:
struct GridPointRow: Hashable, Identifiable {
var id: Int { row }
let row: Int
let points: [GridPoint]
init(row: Int, width: Int) {
self.row = row
points = (0..<width).map { column in
GridPoint(x: column, y: row)
}
}
}
class GameService: ObservableObject {
var gridRows: [GridPointRow] {
(0..<height).map { row in
GridPointRow(row: row, width: width)
}
}
}
Since GridRow
is already Identifiable
, this puts everything in place to be able to put together the board. Since I've done work to put the game logic together in other places, the board view is very simple:
struct GameView: View {
@EnvironmentObject var gameService: GameService
var body: some View {
Grid(horizontalSpacing: 1, verticalSpacing: 1) {
ForEach(gameService.gridRows) { row in
GridRow {
ForEach(row.points) { point in
Cell(gridPoint: point, gameService: gameService)
}
}
}
}
.padding(1)
.background(Theme.gameBackground)
.disabled(gameService.state.isDisabled)
}
}
private extension Cell {
@MainActor
init(gridPoint: GridPoint, gameService: GameService) {
let value = gameService[gridPoint]
self = Cell(value: value) {
if case .revealed = value {
gameService.revealSurroundingIfSafe(gridPoint)
} else {
gameService.reveal(gridPoint)
}
} flag: {
gameService.toggleFlag(gridPoint)
}
}
}
private extension Game.State {
var isDisabled: Bool {
switch self {
case .lose, .win:
return true
case .active:
return false
}
}
}
This creates a grid with a 1px border and 1px line between all of the cells, so the next thing to do is to create the cell. for the first pass implementation, I created a simple version of the view that works on iOS and macOS, with the intention of integrating the ability to right click later. The body switches when the view is disabled to avoid having the button style change the opacity of the content when disabled. I've also split the creation of the content and colors out of this view so that when we switch based on the OS, there won't be duplicated code:
struct Cell: View {
let value: Game.SpaceState
let click: () -> Void
let flag: () -> Void
var body: some View {
if isDisabled {
buttonContent
} else {
Button(action: click) {
buttonContent
}
.foregroundColor(value.foregroundColor)
.highPriorityGesture(flagGesture)
.buttonStyle(.plain)
}
}
}
private extension Cell {
var buttonContent: some View {
value.content
.frame(width: 30, height: 30)
.background { value.fillColor }
.foregroundColor(value.foregroundColor)
}
var isDisabled: Bool {
switch value {
case .revealed(let count):
return count == 0
case .flaggedMine,
.incorrectFlag,
.unrevealedMine,
.revealedMine:
return true
case .unrevealed, .flagged:
return false
}
}
var flagGesture: some Gesture {
LongPressGesture()
.onEnded { _ in
flag()
}
}
}
extension Game.SpaceState {
@ViewBuilder var content: some View {
switch self {
case .unrevealed:
Color.clear
case .revealedMine:
Image(systemName: "xmark.seal.fill")
case .flagged:
Image(systemName: "flag")
case .revealed(let count):
if count > 0 {
Text(count, format: .number)
.fontWeight(.bold)
} else {
Color.clear
}
case .flaggedMine:
Image(systemName: "flag.fill")
case .incorrectFlag:
Image(systemName: "flag.slash")
case .unrevealedMine:
Image(systemName: "seal.fill")
}
}
var fillColor: Color {
switch self {
case .flagged, .unrevealed, .unrevealedMine, .incorrectFlag, .flaggedMine:
return Theme.unrevealedFill
case .revealed:
return Theme.revealedFill
case .revealedMine:
return Theme.failure
}
}
var foregroundColor: Color {
switch self {
case .revealedMine, .unrevealedMine, .unrevealed, .flagged:
return Theme.primary
case .revealed(let count):
return Theme.foreground(for: count)
case .incorrectFlag:
return Theme.failure
case .flaggedMine:
return Theme.success
}
}
So with all of this, the base game is working, so next is going to be adding right click, and possibly some other small playability enhancements.