Created
May 7, 2016 17:44
-
-
Save ryanbraganza/b3916283bbfc4a3d99c44695e1b7906d to your computer and use it in GitHub Desktop.
connect4
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 | |
| # Usage: | |
| # | |
| # ruby connect4.rb test # run the tests | |
| # ruby connect4.rb play # launch a CLI-based game | |
| # ruby connect4.rb # Simulate a simple game | |
| require 'set' | |
| PLAYER_ONE = 'X' | |
| PLAYER_TWO = 'O' | |
| EMPTY = '*' | |
| WIDTH = 7 | |
| HEIGHT = 6 | |
| class Connect4 | |
| attr_accessor :board, :current_player | |
| def initialize(board: nil) | |
| # TODO validate input | |
| if board | |
| self.board = board | |
| else | |
| # TODO consider supporting variable-sized boards | |
| self.board = HEIGHT.times.map { WIDTH.times.map { EMPTY } } | |
| end | |
| @player_order = [PLAYER_ONE, PLAYER_TWO].cycle | |
| self.current_player = @player_order.next | |
| end | |
| # Play into the given column | |
| # Return true on success, false if the move is illegal | |
| def play(col_num) | |
| # TODO validate input and assert game not completed | |
| column = board.transpose[col_num] | |
| empty_row = column.rindex(EMPTY) | |
| if empty_row | |
| board[empty_row][col_num] = current_player | |
| self.current_player = @player_order.next | |
| true | |
| else | |
| false | |
| end | |
| end | |
| def completed? | |
| !!winner || board.all? { |row| !row.any? { |cell| cell == EMPTY } } | |
| end | |
| def to_s | |
| output = '' | |
| if completed? | |
| if winner | |
| output += "winner: #{winner}" | |
| else | |
| output += "Draw!" | |
| end | |
| else | |
| output += "current player: #{current_player}\n" | |
| end | |
| output += "\n\n" | |
| output += board.map do |row| | |
| row.join | |
| end.join("\n") | |
| output | |
| end | |
| # Return the winner or nil | |
| def winner | |
| # TODO cache | |
| # check horizontally | |
| board.each do |row| | |
| return PLAYER_ONE if row.join.match(PLAYER_ONE * 4) | |
| return PLAYER_TWO if row.join.match(PLAYER_TWO * 4) | |
| end | |
| # check vertically | |
| board.transpose.each do |col| | |
| return PLAYER_ONE if col.join.match(PLAYER_ONE * 4) | |
| return PLAYER_TWO if col.join.match(PLAYER_TWO * 4) | |
| end | |
| # check diagonally | |
| board.each_with_index do |row, m| | |
| row.each_with_index do |cell, n| | |
| # searching down-right | |
| if n + 3 < row.length && m + 3 < board.length | |
| potential_match = [cell, board[m+1][n+1], board[m+2][n+2], board[m+3][n+3]].join | |
| return PLAYER_ONE if (potential_match == PLAYER_ONE * 4) | |
| return PLAYER_TWO if (potential_match == PLAYER_TWO * 4) | |
| end | |
| # searching down-left | |
| if n - 3 >= 0 && m + 3 < board.length | |
| potential_match = [cell, board[m+1][n-1], board[m+2][n-2], board[m+3][n-3]].join | |
| return PLAYER_ONE if (potential_match == PLAYER_ONE * 4) | |
| return PLAYER_TWO if (potential_match == PLAYER_TWO * 4) | |
| end | |
| end | |
| end | |
| # no winner | |
| nil | |
| end | |
| end | |
| class ConsoleGame | |
| def initialize | |
| @connect4 = Connect4.new | |
| end | |
| def run | |
| puts @connect4 | |
| while !@connect4.completed? | |
| column = prompt_column | |
| @connect4.play(column) | |
| puts @connect4 | |
| end | |
| puts 'game over!' | |
| end | |
| private | |
| def prompt_column | |
| loop do | |
| print "Enter a column [0 - #{WIDTH}]> " | |
| user_input = $stdin.readline | |
| begin | |
| int = Integer(user_input) | |
| p int | |
| if (0...WIDTH).cover? int | |
| return int | |
| end | |
| rescue ArgumentError | |
| end | |
| end | |
| end | |
| end | |
| if ARGV.include? 'test' | |
| require 'minitest/autorun' | |
| class TestConnect4 < Minitest::Test | |
| def test_initial_game | |
| g = load_game(' | |
| ******* | |
| ******* | |
| ******* | |
| ******* | |
| ******* | |
| ******* | |
| ') | |
| assert_equal nil, g.winner | |
| assert !g.completed? | |
| end | |
| def test_horizontal_winner | |
| g = load_game(' | |
| ******* | |
| ******* | |
| ******* | |
| ******* | |
| **OO*** | |
| XXXXO** | |
| ') | |
| assert_equal PLAYER_ONE, g.winner | |
| assert g.completed? | |
| end | |
| def test_vertical_winner | |
| g = load_game(' | |
| ******* | |
| ******* | |
| ***XO** | |
| **XOO** | |
| *XOOO** | |
| XOXXO** | |
| ') | |
| assert_equal PLAYER_TWO, g.winner | |
| assert g.completed? | |
| end | |
| def test_draw | |
| g = load_game(' | |
| XXOOXXO | |
| OXXOOXX | |
| XOOXXXO | |
| XXXOOOX | |
| OXOOXXX | |
| OOOXOOX | |
| ') | |
| assert_equal nil, g.winner | |
| assert g.completed? | |
| end | |
| private | |
| def load_game str | |
| rows = str.strip.split("\n") | |
| grid = rows.map{ |row| row.strip.split('')} | |
| Connect4.new(board: grid) | |
| end | |
| end | |
| end | |
| def main | |
| return if ARGV.include?('test') | |
| if ARGV.include?('play') | |
| ConsoleGame.new.run | |
| else | |
| # Play a game where player one always plays the first column | |
| # and player two always plays the second column | |
| connect4 = Connect4.new | |
| puts connect4 | |
| 3.times do | |
| connect4.play(0) | |
| puts connect4 | |
| connect4.play(1) | |
| puts connect4 | |
| end | |
| # The winning move | |
| connect4.play(0) | |
| puts connect4 | |
| end | |
| end | |
| if __FILE__ == $0 | |
| main | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment