engine.go
package minesweeper
import (
"container/list"
"crypto/rand"
"encoding/binary"
"fmt"
"sync"
"github.com/rrborja/minesweeper/visited"
)
type eventType uint8
type blocks [][]Block
const consecutiveRandomLimit = 3
const easyMultiplier = 0.1
const mediumMultiplier = 0.2
const hardMultiplier = 0.5
type board struct {
*Grid
blocks
difficultyMultiplier float32
}
type game struct {
Event
board
Difficulty
recordedActions
*sync.Mutex
}
var singleton Minesweeper
func (game *game) SetGrid(width, height int) error {
if game.Grid != nil {
return new(GameAlreadyStartedError)
}
game.Grid = &Grid{width, height}
createBoard(game)
return nil
}
func (game *game) Flag(x, y int) {
blockPtr := &game.blocks[x][y]
if !blockPtr.visited {
blockPtr.flagged = !blockPtr.flagged
}
}
func (game *game) Visit(x, y int) ([]Block, error) {
game.validateGameEnvironment()
game.Lock()
defer game.Unlock()
block := &game.blocks[x][y]
if block.Node == Number && block.visited {
countedFlaggedBlock := 0
resultedBlocks := make([]Block, 0)
blocksToBeVisited := make([]*Block, 0)
game.traverseAdjacentCells(x, y, func(cell *Block) {
if cell.flagged {
countedFlaggedBlock++
} else {
blocksToBeVisited = append(blocksToBeVisited, cell)
}
})
if countedFlaggedBlock == block.Value {
for _, block := range blocksToBeVisited {
blocks, err := game.visit(block.X(), block.Y())
if err != nil {
return blocks, err
}
resultedBlocks = append(resultedBlocks, blocks...)
}
}
return resultedBlocks, nil
}
return game.visit(x, y)
}
func (game *game) visit(x, y int) ([]Block, error) {
block := &game.blocks[x][y]
if !block.flagged && !block.visited {
block.visited = true
defer func() {
go game.validateSolution()
}()
switch block.Node {
case Number:
defer game.add(visited.Record{
Position: *block, Action: visited.Number})
return []Block{*block}, nil
case Bomb:
defer game.add(visited.Record{
Position: *block, Action: visited.Bomb})
bombLocations := make([]Block, 0, game.totalBombs()-1)
for _, bombLocation := range game.BombLocations() {
if bombLocation != *block {
bombLocations = append(bombLocations, bombLocation.(Block))
}
}
bombLocations = append([]Block{*block}, bombLocations...)
return bombLocations, &ExplodedError{x: x, y: y}
case Unknown:
defer game.add(visited.Record{
Position: *block, Action: visited.Unknown})
block.visited = false //to avoid infinite recursion, first is to set the base case
visitedList := list.New()
autoRevealUnmarkedBlock(game, visitedList, x, y)
visitedBlocks := make([]Block, visitedList.Len())
var counter int
for e := visitedList.Front(); e != nil; e = e.Next() {
visitedBlocks[counter] = e.Value.(Block)
counter++
}
return visitedBlocks, nil
}
}
return nil, nil
}
func (game *game) SetDifficulty(difficulty Difficulty) error {
if game.Mutex != nil {
return new(GameAlreadyStartedError)
}
game.Difficulty = difficulty
switch difficulty {
case Easy:
game.difficultyMultiplier = easyMultiplier
case Medium:
game.difficultyMultiplier = mediumMultiplier
case Hard:
game.difficultyMultiplier = hardMultiplier
}
return nil
}
func (game *game) Play() error {
if game.Difficulty == notSet {
return new(UnspecifiedDifficultyError)
}
if game.Grid == nil {
return new(UnspecifiedGridError)
}
if game.Mutex != nil {
return new(GameAlreadyStartedError)
}
game.Mutex = new(sync.Mutex)
createBombs(game)
tallyHints(game)
return nil
}
// X returns the X coordinate of the block in the minesweeper grid
func (block Block) X() int {
return block.location.x
}
// Y returns the Y coordinate of the block in the minesweeper grid
func (block Block) Y() int {
return block.location.y
}
// Shifts to the right
func shiftPosition(grid *Grid, x, y int) (_x, _y int) {
width := grid.Width
height := grid.Height
if x+1 >= width {
if y+1 >= height {
_x, _y = 0, 0
} else {
_x, _y = 0, y+1
}
} else {
_x, _y = x+1, y
}
return
}
func createBombs(game *game) {
area := int(game.Width * game.Height)
for i := 0; i < int(float32(area)*game.difficultyMultiplier); i++ {
for {
randomPos := randomNumber(area)
x, y := randomPos%game.Width, randomPos/game.Width
countLimit := 0
for game.board.blocks[x][y].Node != Unknown {
x, y = shiftPosition(game.Grid, x, y)
countLimit++
}
if countLimit <= consecutiveRandomLimit {
game.blocks[x][y].Node = Bomb
break
}
}
}
}
func tallyHints(game *game) {
game.iterateBlocksWhen(Bomb, func(block *Block) {
game.traverseAdjacentCells(block.X(), block.Y(), func(cell *Block) {
if cell.Node != Bomb {
cell.Node = Number
cell.Value++
}
})
})
}
func createBoard(game *game) {
game.blocks = make([][]Block, game.Width)
for x := range game.blocks {
game.blocks[x] = make([]Block, game.Height)
}
for x, row := range game.blocks {
for y := range row {
block := &game.blocks[x][y]
block.Value = 0
block.Node = Unknown
block.location = struct{ x, y int }{x: x, y: y}
}
}
}
func autoRevealUnmarkedBlock(game *game, visitedBlocks *list.List, x, y int) {
blocks := game.blocks
game.withinBounds(x, y, func() {
if blocks[x][y].visited {
return
}
switch blocks[x][y].Node {
case Unknown:
blocks[x][y].visited = true
visitedBlocks.PushBack(blocks[x][y])
game.traverseAdjacentCells(x, y, func(cell *Block) {
autoRevealUnmarkedBlock(game, visitedBlocks, cell.X(), cell.Y())
})
case Number:
blocks[x][y].visited = true
visitedBlocks.PushBack(blocks[x][y])
}
})
}
func (game *game) validateSolution() {
defer skipIterate()
var visitTally int
game.iterateVisitedBlocks(func(block *Block) {
switch block.Node {
case Bomb:
game.Event <- Lose
panic("")
default:
visitTally++
}
})
if visitTally == game.totalNonBombs() {
game.Event <- Win
}
}
func (game *game) validateGameEnvironment() {
if game.Grid == nil {
panic(UnspecifiedGridError{})
}
if game.Difficulty == notSet {
panic(UnspecifiedDifficultyError{})
}
}
func (game *game) traverseAdjacentCells(x, y int, do func(*Block)) {
game.recursivelyTraverseAdjacentCells(x-1, y-1, do)
game.recursivelyTraverseAdjacentCells(x, y-1, do)
game.recursivelyTraverseAdjacentCells(x+1, y-1, do)
game.recursivelyTraverseAdjacentCells(x-1, y, do)
game.recursivelyTraverseAdjacentCells(x+1, y, do)
game.recursivelyTraverseAdjacentCells(x-1, y+1, do)
game.recursivelyTraverseAdjacentCells(x, y+1, do)
game.recursivelyTraverseAdjacentCells(x+1, y+1, do)
}
func (game *game) recursivelyTraverseAdjacentCells(x, y int, do func(*Block)) {
game.withinBounds(x, y, func() {
do(&game.blocks[x][y])
})
}
func (game *game) withinBounds(x, y int, do func()) {
width := game.Width
height := game.Height
if x >= 0 && y >= 0 && x < width && y < height {
do()
}
}
func (game *game) iterateBlocks(do func(*Block) bool) bool {
defer skipIterate()
for x := 0; x < game.Width; x++ {
for y := 0; y < game.Height; y++ {
do(&game.blocks[x][y])
}
}
return true
}
func (game *game) iterateBlocksWhen(condition Node, do func(*Block)) bool {
return game.iterateBlocks(func(block *Block) bool {
defer skipIterate()
if block.Node&condition == condition {
do(block)
}
return true
})
}
func (game *game) iterateVisitedBlocks(do func(*Block)) bool {
return game.iterateBlocks(func(block *Block) bool {
defer skipIterate()
if block.visited {
do(block)
}
return true
})
}
func skipIterate() bool {
recover()
return false
}
func (game *game) area() int {
return len(game.blocks) * len(game.blocks[0])
}
func (game *game) areaInFloat() float32 {
return float32(game.area())
}
func (game *game) totalBombs() int {
return int(game.areaInFloat() * game.difficultyMultiplier)
}
func (game *game) totalNonBombs() int {
return game.area() - game.totalBombs()
}
// Visited responds if a cell is visited or not
func (block *Block) Visited() bool {
return block.visited
}
// Flagged responds if a cell is visited or not
func (block *Block) Flagged() bool {
return block.flagged
}
func (block Block) String() string {
var nodeType string
switch block.Node {
case Unknown:
nodeType = "blank"
case Number:
nodeType = "number"
case Bomb:
nodeType = "bomb"
}
var value string
if block.Value > 0 {
value = string(block.Value)
}
return fmt.Sprintf("\n\nBlock: \n\tValue\t :\t%v\n\tLocation :\tx:%v y:%v\n\tType\t :\t%v\n\tVisited? :\t%v\n\tFlagged? :\t%v\n\n",
value, block.location.x, block.location.y, nodeType, block.visited, block.flagged)
}
func randomNumber(max int) int {
var number uint16
binary.Read(rand.Reader, binary.LittleEndian, &number)
return int(number) % max
}