Implement a CLI game of Minesweeper using Ruby. Make sure to cover it with tests. The game should work by generating a minesweeper board, printing it and asking the user for the coordinates of the cell they wish to uncover. The board should be printed using the following notation: # for a still hidden field, 1-8 for a field with that many mines in the neighborhood, . for a field with no neighbors with mines and * for a mine. The program should correctly detect losing the game by hitting a mine and winning the game by uncovering all the fields without mines.
Last active
May 19, 2025 12:26
-
-
Save radanskoric/3609d411cbc035eaaaaf314eb6c4cd9a to your computer and use it in GitHub Desktop.
Test run of a simple coding agent with the prompt to create an ASCII minesweeper.
This file contains hidden or 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
#!/usr/bin/env ruby | |
class Minesweeper | |
MINE = '*' | |
HIDDEN = '#' | |
EMPTY = '.' | |
attr_reader :board, :visible_board, :width, :height, :mines_count, :game_over, :game_won | |
def initialize(width = 9, height = 9, mines_count = 10) | |
@width = width | |
@height = height | |
@mines_count = mines_count | |
@game_over = false | |
@game_won = false | |
@mines_placed = false | |
# Initialize the boards | |
@board = Array.new(height) { Array.new(width, EMPTY) } | |
@visible_board = Array.new(height) { Array.new(width, HIDDEN) } | |
# Place mines on the board | |
place_mines | |
end | |
def place_mines | |
mines_placed = 0 | |
while mines_placed < @mines_count | |
x = rand(@width) | |
y = rand(@height) | |
if @board[y][x] != MINE | |
@board[y][x] = MINE | |
mines_placed += 1 | |
end | |
end | |
# Calculate numbers for adjacent cells | |
calculate_numbers | |
@mines_placed = true | |
end | |
def calculate_numbers | |
@height.times do |y| | |
@width.times do |x| | |
if @board[y][x] != MINE | |
mines_around = count_adjacent_mines(x, y) | |
@board[y][x] = mines_around.to_s if mines_around > 0 | |
end | |
end | |
end | |
end | |
def count_adjacent_mines(x, y) | |
count = 0 | |
# Check all 8 adjacent cells | |
(-1..1).each do |dy| | |
(-1..1).each do |dx| | |
next if dx == 0 && dy == 0 # Skip the cell itself | |
new_x = x + dx | |
new_y = y + dy | |
# Check if the cell is within bounds | |
if new_x >= 0 && new_x < @width && new_y >= 0 && new_y < @height | |
count += 1 if @board[new_y][new_x] == MINE | |
end | |
end | |
end | |
count | |
end | |
def uncover(x, y) | |
return if @game_over || @game_won | |
return if x < 0 || x >= @width || y < 0 || y >= @height | |
return if @visible_board[y][x] != HIDDEN | |
# Uncover the cell | |
@visible_board[y][x] = @board[y][x] | |
# Check if it's a mine | |
if @board[y][x] == MINE | |
@game_over = true | |
reveal_all_mines | |
return | |
end | |
# If empty, recursively uncover adjacent cells | |
if @board[y][x] == EMPTY | |
(-1..1).each do |dy| | |
(-1..1).each do |dx| | |
uncover(x + dx, y + dy) | |
end | |
end | |
end | |
# Check if the game is won | |
check_win | |
end | |
def reveal_all_mines | |
@height.times do |y| | |
@width.times do |x| | |
@visible_board[y][x] = @board[y][x] if @board[y][x] == MINE | |
end | |
end | |
end | |
def check_win | |
hidden_count = 0 | |
@height.times do |y| | |
@width.times do |x| | |
hidden_count += 1 if @visible_board[y][x] == HIDDEN | |
end | |
end | |
if hidden_count == @mines_count | |
@game_won = true | |
reveal_all_mines | |
end | |
end | |
def display | |
puts "\nMinesweeper Board:" | |
puts " " + (0...@width).map { |i| i.to_s }.join(" ") | |
@height.times do |y| | |
print "#{y} " | |
@width.times do |x| | |
print "#{@visible_board[y][x]} " | |
end | |
puts | |
end | |
if @game_over | |
puts "Game Over! You hit a mine." | |
elsif @game_won | |
puts "Congratulations! You won!" | |
end | |
end | |
end | |
# Game runner | |
if __FILE__ == $PROGRAM_NAME | |
puts "Welcome to Minesweeper!" | |
puts "Enter the board size (width height):" | |
width, height = gets.chomp.split.map(&:to_i) | |
width = 9 if width.nil? || width < 3 | |
height = 9 if height.nil? || height < 3 | |
puts "Enter the number of mines:" | |
mines = gets.chomp.to_i | |
# Ensure mines don't exceed half the board size | |
max_mines = (width * height) / 2 | |
mines = [mines, max_mines].min | |
mines = 10 if mines < 1 | |
game = Minesweeper.new(width, height, mines) | |
until game.game_over || game.game_won | |
game.display | |
puts "Enter coordinates to uncover (x y):" | |
input = gets.chomp | |
# Allow the player to exit | |
break if input.downcase == "exit" || input.downcase == "quit" | |
x, y = input.split.map(&:to_i) | |
if x.nil? || y.nil? || x < 0 || x >= game.width || y < 0 || y >= game.height | |
puts "Invalid coordinates. Try again." | |
next | |
end | |
game.uncover(x, y) | |
end | |
# Final board display | |
game.display | |
end |
This file contains hidden or 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
#!/usr/bin/env ruby | |
require 'minitest/autorun' | |
require_relative 'minesweeper' | |
class MinesweeperTest < Minitest::Test | |
def setup | |
# For testing, we'll use a deterministic board rather than random mines | |
@game = Minesweeper.new(5, 5, 5) | |
# Override the random mine placement with a known pattern | |
@game.instance_variable_set(:@board, [ | |
['.', '.', '.', '.', '.'], | |
['.', '*', '2', '1', '.'], | |
['.', '2', '*', '1', '.'], | |
['.', '1', '1', '1', '.'], | |
['.', '.', '.', '.', '*'] | |
]) | |
# Reset visible board | |
@game.instance_variable_set(:@visible_board, Array.new(5) { Array.new(5, Minesweeper::HIDDEN) }) | |
end | |
def test_initialization | |
game = Minesweeper.new | |
assert_equal 9, game.width | |
assert_equal 9, game.height | |
assert_equal 10, game.mines_count | |
assert_equal false, game.game_over | |
assert_equal false, game.game_won | |
end | |
def test_uncover_empty_cell | |
@game.uncover(0, 0) | |
assert_equal '.', @game.visible_board[0][0] | |
# Uncovering an empty cell should also uncover adjacent cells | |
assert_equal '.', @game.visible_board[0][1] | |
assert_equal '.', @game.visible_board[1][0] | |
end | |
def test_uncover_numbered_cell | |
@game.uncover(2, 1) | |
assert_equal '2', @game.visible_board[1][2] | |
# Adjacent cells should not be automatically uncovered | |
assert_equal Minesweeper::HIDDEN, @game.visible_board[0][2] | |
end | |
def test_uncover_mine | |
@game.uncover(1, 1) | |
assert_equal Minesweeper::MINE, @game.visible_board[1][1] | |
assert_equal true, @game.game_over | |
assert_equal false, @game.game_won | |
# All mines should be revealed | |
assert_equal Minesweeper::MINE, @game.visible_board[2][2] | |
assert_equal Minesweeper::MINE, @game.visible_board[4][4] | |
end | |
def test_win_condition | |
# Uncover all non-mine cells to win | |
@game.uncover(0, 0) # This should reveal many cells due to flood fill | |
# Manually uncover remaining non-mine cells | |
@game.uncover(3, 1) | |
@game.uncover(3, 2) | |
# Check if game is won | |
assert_equal true, @game.game_won | |
assert_equal false, @game.game_over | |
# All mines should be revealed when the game is won | |
assert_equal Minesweeper::MINE, @game.visible_board[1][1] | |
assert_equal Minesweeper::MINE, @game.visible_board[2][2] | |
assert_equal Minesweeper::MINE, @game.visible_board[4][4] | |
end | |
def test_count_adjacent_mines | |
assert_equal 1, @game.count_adjacent_mines(0, 1) | |
assert_equal 2, @game.count_adjacent_mines(1, 2) | |
assert_equal 0, @game.count_adjacent_mines(0, 0) | |
end | |
def test_invalid_coordinates | |
# Out of bounds coordinates should be ignored | |
@game.uncover(-1, 0) | |
@game.uncover(0, -1) | |
@game.uncover(5, 0) | |
@game.uncover(0, 5) | |
# Board should remain unchanged | |
@game.visible_board.each do |row| | |
row.each do |cell| | |
assert_equal Minesweeper::HIDDEN, cell | |
end | |
end | |
end | |
def test_cannot_uncover_after_game_over | |
@game.uncover(1, 1) # Hit a mine | |
assert_equal true, @game.game_over | |
# Try to uncover another cell | |
@game.uncover(0, 0) | |
# The cell should remain hidden as game is over | |
assert_equal Minesweeper::MINE, @game.visible_board[1][1] # Mine is visible | |
# Other cells remain as they were after game over | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment