Last active
December 16, 2015 20:38
-
-
Save bhauman/5493451 to your computer and use it in GitHub Desktop.
My entry for the asheville coders league event: http://www.meetup.com/Asheville-Coders-League/events/113701462/ I added a simple minmax based player as well.
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
| class TicTacToe | |
| attr_accessor :move_pos, :prev_ttt | |
| @@winning_patterns = nil | |
| def initialize(_move_pos = nil, _prev_ttt = nil) | |
| @move_pos, @prev_ttt = _move_pos, _prev_ttt | |
| end | |
| def game_over? | |
| valid_moves.empty? | |
| end | |
| def valid_moves | |
| @valid_moves ||= (0..8).find_all { |mv| valid_move? mv} | |
| end | |
| def valid_move?(position) | |
| position > -1 && position < 9 && board[position].nil? | |
| end | |
| def next_turn | |
| !turn ? "X" : "O" | |
| end | |
| def move(position) | |
| TicTacToe.new(position, self) if valid_move?(position) | |
| end | |
| def turn | |
| @turn ||= prev_ttt.nil? ? true : !prev_ttt.turn | |
| end | |
| def board | |
| @board ||= prev_ttt.nil? ? ([nil] * 9) : prev_ttt.board.clone.tap { |b| b[move_pos] = (turn ? "X" : "O") } | |
| end | |
| # win detection | |
| def winning_patterns | |
| @@winning_patterns ||= | |
| (0..8).each_slice(3).to_a.tap { |s| (s.transpose + [[0,4,8], [6,4,2]]).each { |p| s.push(p) } } | |
| end | |
| def pattern_wins?(pattern) | |
| pattern.collect { |p| board[p] }.join =~ /^(XXX|OOO)$/ | |
| end | |
| def winner? | |
| relevant_patterns = winning_patterns.select {|p| p.index(move_pos) } | |
| if winning_pat = relevant_patterns.detect { |pat| pattern_wins?(pat) } | |
| board[winning_pat.first] | |
| else | |
| false | |
| end | |
| end | |
| def to_s | |
| (0..8).to_a.each_slice(3).collect do |row| | |
| row.collect { |p| board[p] || p.next}.join(" | ") + "\n" | |
| end.join("---------\n") | |
| end | |
| def self.from_array(moves) | |
| moves.inject(TicTacToe.new) { |accum, mv| accum.move(mv) } | |
| end | |
| end | |
| class Game | |
| def initialize(game = TicTacToe, ai_player = MinMaxPlayer.new) | |
| @game = game | |
| @ai_player = ai_player | |
| end | |
| def start(game = @game.new) | |
| puts "\nWould you like to play a game?\n" if game.valid_moves.length == 9 | |
| puts "\n" + game.to_s + "\n" | |
| if winner = game.winner? | |
| puts "The winner is #{winner}!" | |
| return | |
| end | |
| if game.game_over? | |
| puts "It's a tie" | |
| return | |
| end | |
| position = get_valid_move(game) | |
| start(game.move(position)) | |
| end | |
| def get_valid_move(game) | |
| if @ai_player && game.next_turn == @ai_player.player | |
| return @ai_player.best_move(game) | |
| end | |
| puts "Choose next move for #{game.next_turn} #{(game.valid_moves.map &:next).inspect}:" | |
| move = gets().to_i - 1 | |
| if game.valid_move? move | |
| move | |
| else | |
| puts "Invalid move!" | |
| get_valid_move(game) | |
| end | |
| end | |
| end | |
| class MinMaxPlayer | |
| attr_accessor :player | |
| def initialize(player = "X") | |
| @player = player | |
| @memoizer = { } | |
| end | |
| def best_move(game) | |
| score_moves = Hash[game.valid_moves.map { |mv| [score_game(game.move(mv)), mv]}] | |
| score_moves[score_moves.keys.max] | |
| end | |
| def score_game(game) | |
| @memoizer[game.board] ||= if game.winner? | |
| (@player == game.next_turn ? -1 : 1) * 10 | |
| elsif game.game_over? | |
| 0 | |
| else | |
| scores = game.valid_moves.collect { |m| score_game(game.move(m)) } | |
| game.next_turn == @player ? scores.max : scores.min | |
| end | |
| end | |
| end | |
| def tests | |
| def assert(phrase, statement) | |
| if statement | |
| puts "#{phrase}: Passed" | |
| else | |
| puts "##### #{phrase}: Failed" | |
| end | |
| end | |
| ttt = TicTacToe.new | |
| assert "has empty state", ttt.prev_ttt.nil? | |
| ttt = ttt.move 4 | |
| assert "moving adds position to state", ttt.move_pos == 4 | |
| ttt = ttt.move 5 | |
| assert "moving adds second position to state after first", ttt.move_pos == 5 && ttt.prev_ttt.move_pos == 4 | |
| assert "valid_moves returns all posible moves", ttt.valid_moves == ((0..8).to_a - [4,5]) | |
| assert "valid_move? returns true if position isn't in state", ttt.valid_move?(6) | |
| assert "valid_move? returns false if position is in state", !ttt.valid_move?(5) | |
| assert "valid_move? returns false if position is < 0", !ttt.valid_move?(-1) | |
| assert "valid_move? returns false if position is > 8", !ttt.valid_move?(9) | |
| assert "pos return X if position is taken and its turn is odd", ttt.board[4] == "O" | |
| assert "pos return O if position is taken and its turn is even", ttt.board[5] == "X" | |
| assert "pos returns nil if position is not taken", ttt.board[7].nil? | |
| assert "winner return X for X win", TicTacToe.from_array([0,3,1,4,6,5]).winner? == "X" | |
| assert "winner return O for O win", TicTacToe.from_array([0,3,1,4,2]).winner? == "O" | |
| assert "winner return O for diagonal O win", TicTacToe.from_array([0,3,4,2,8]).winner? == "O" | |
| assert "winner return false for no win", TicTacToe.from_array([0,3,4,2,7]).winner? == false | |
| end | |
| tests() | |
| Game.new.start |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment