Last active
September 3, 2017 18:56
-
-
Save amitkgupta/d243718fbc7011116002d477a82a3487 to your computer and use it in GitHub Desktop.
Simplistic text-based minesweeper game
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
package main | |
import ( | |
"errors" | |
"fmt" | |
"math/rand" | |
"os" | |
"os/exec" | |
) | |
type cell struct { | |
mine bool | |
adjacentMines int | |
revealed bool | |
} | |
type minefield struct { | |
height, width, numMines int | |
grid [][]*cell | |
} | |
func newMinefield(height, width, numMines int) (*minefield, error) { | |
if height < 0 || width < 0 || numMines < 0 || numMines > height*width { | |
return nil, errors.New("invalid minefield parameters.") | |
} | |
grid := make([][]*cell, height) | |
for i := range grid { | |
grid[i] = make([]*cell, width) | |
for j := range grid[i] { | |
grid[i][j] = new(cell) | |
} | |
} | |
mineIndices := rand.Perm(height * width)[:numMines] | |
for _, idx := range mineIndices { | |
grid[idx%height][idx/height].mine = true | |
} | |
for x := range grid { | |
for y := range grid[x] { | |
for i := x - 1; i <= x+1; i++ { | |
for j := y - 1; j <= y+1; j++ { | |
if i >= 0 && i < height && j >= 0 && j < width && grid[i][j].mine { | |
grid[x][y].adjacentMines++ | |
} | |
} | |
} | |
} | |
} | |
return &minefield{ | |
height: height, | |
width: width, | |
numMines: numMines, | |
grid: grid, | |
}, nil | |
} | |
func (m *minefield) reveal(x, y int) (bool, error) { | |
if x < 0 || x >= m.height || y < 0 || y >= m.width { | |
return false, errors.New("invalid coordinates.") | |
} | |
m.grid[x][y].revealed = true | |
return m.grid[x][y].mine, nil | |
} | |
func (m *minefield) clear() bool { | |
numRevealed := 0 | |
for _, row := range m.grid { | |
for _, cell := range row { | |
if cell.revealed { | |
numRevealed++ | |
} | |
} | |
} | |
return numRevealed == m.height*m.width-m.numMines | |
} | |
func (m minefield) render() { | |
c := exec.Command("clear") | |
c.Stdout = os.Stdout | |
c.Run() | |
for _, row := range m.grid { | |
for _, cell := range row { | |
if !cell.revealed { | |
fmt.Printf("x ") | |
} else if cell.mine { | |
fmt.Printf("* ") | |
} else { | |
fmt.Printf("%d ", cell.adjacentMines) | |
} | |
} | |
fmt.Println() | |
} | |
} | |
type gameState int | |
const ( | |
unready gameState = iota | |
ready | |
won | |
lost | |
) | |
type minesweeper struct { | |
field *minefield | |
state gameState | |
} | |
func (m *minesweeper) setup() { | |
m.state = unready | |
fmt.Println("enter height.") | |
var height int | |
_, err := fmt.Scanf("%d\n", &height) | |
if err != nil { | |
fmt.Println("invalid parameters, try again.") | |
return | |
} | |
fmt.Println("enter width.") | |
var width int | |
_, err = fmt.Scanf("%d\n", &width) | |
if err != nil { | |
fmt.Println("invalid parameters, try again.") | |
return | |
} | |
fmt.Println("enter number of mines.") | |
var numMines int | |
_, err = fmt.Scanf("%d\n", &numMines) | |
if err != nil { | |
fmt.Println("invalid parameters, try again.") | |
return | |
} | |
field, err := newMinefield(height, width, numMines) | |
if err != nil { | |
fmt.Println("invalid parameters, try again.") | |
return | |
} | |
m.field = field | |
m.state = ready | |
} | |
func (m *minesweeper) play() { | |
for m.state != ready { m.setup() } | |
for m.state != won && m.state != lost { m.run_one_iteration() } | |
if m.state == won { | |
fmt.Println("you win.") | |
} | |
if m.state == lost { | |
fmt.Println("you lose.") | |
} | |
} | |
func (m *minesweeper) run_one_iteration() { | |
fmt.Println("enter row & column coordinates, e.g. '2 5'.") | |
var x, y int | |
_, err := fmt.Scanf("%d %d\n", &x, &y) | |
if err != nil { | |
fmt.Println("unparseable input, try again.") | |
return | |
} | |
x-- | |
y-- | |
mine, err := m.field.reveal(x, y) | |
if err != nil { | |
fmt.Println("coordinates don't fit in minefield, try again.") | |
return | |
} | |
m.field.render() | |
if mine { | |
m.state = lost | |
} | |
if m.field.clear() { | |
m.state = won | |
} | |
} | |
func main() { | |
new(minesweeper).play() | |
} |
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 represents the minefield. | |
class Minefield | |
Cell = Struct.new(:mine, :adjacent_mines, :revealed) | |
class InvalidCoordinates < StandardError; end | |
class TooManyMines < StandardError; end | |
def initialize(height:, width:, num_mines:) | |
@height = Integer(height) | |
@width = Integer(width) | |
@num_mines = Integer(num_mines) | |
raise TooManyMines if @num_mines > @height * @width | |
@grid = Array.new(height) do | |
Array.new(width) { Cell.new(false, 0, false) } | |
end | |
@grid.flatten.sample(num_mines).each { |c| c.mine = true } | |
populate_adjacency_counts | |
end | |
def reveal(x, y) | |
raise InvalidCoordinates unless valid(x, y) | |
cell = @grid[x][y] | |
cell.revealed = true | |
cell.mine | |
end | |
def clear? | |
@grid.map { |row| row.count(&:revealed) }.inject(:+) \ | |
== @height * @width - @num_mines | |
end | |
def render | |
system('clear') | |
@grid.each do |row| | |
row.each do |cell| | |
print 'x ' unless cell.revealed | |
print '* ' if cell.revealed && cell.mine | |
print "#{cell.adjacent_mines} " if cell.revealed && !cell.mine | |
end | |
puts | |
end | |
end | |
private | |
def populate_adjacency_counts | |
@grid.each_index do |x| | |
@grid[x].each_index do |y| | |
(x - 1..x + 1).each do |i| | |
(y - 1..y + 1).each do |j| | |
@grid[x][y].adjacent_mines += 1 if mine_at?(i, j) | |
end | |
end | |
end | |
end | |
end | |
def mine_at?(i, j) | |
valid(i, j) && @grid[i][j].mine | |
end | |
def valid(i, j) | |
i >= 0 && i < @height && j >= 0 && j < @width | |
end | |
end | |
# Represents the Minesweeper game. | |
class Minesweeper | |
def setup | |
puts 'enter height.' | |
height = Integer(gets) | |
puts 'enter width.' | |
width = Integer(gets) | |
puts 'enter number of mines.' | |
num_mines = Integer(gets) | |
@field = Minefield.new(height: height, width: width, num_mines: num_mines) | |
@state = :ready | |
rescue ArgumentError, Minefield::TooManyMines | |
puts 'invalid parameters, try again.' | |
end | |
def play | |
setup until @state == :ready | |
loop do | |
run_one_iteration | |
end_game('you lose.') if @state == :lost | |
end_game('you win.') if @state == :won | |
end | |
end | |
private | |
def end_game(message) | |
puts message | |
exit | |
end | |
def run_one_iteration | |
puts 'enter row & column coordinates, e.g. \'2 5\'.' | |
x, y = extract_coordinates(gets) | |
mine = @field.reveal(x, y) | |
@field.render | |
@state = :lost if mine | |
@state = :won if @field.clear? | |
rescue InvalidInput | |
puts 'unparseable input, try again.' | |
rescue Minefield::InvalidCoordinates | |
puts 'coordinates don\'t fit in minefield, try again.' | |
end | |
class InvalidInput < StandardError; end | |
def extract_coordinates(input) | |
parts = input.split(' ') | |
raise InvalidInput if parts.count != 2 | |
return Integer(parts[0]) - 1, Integer(parts[1]) - 1 | |
rescue ArgumentError | |
raise InvalidInput | |
end | |
end | |
def main | |
Minesweeper.new.play | |
end | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment