diff --git a/Gemfile b/Gemfile index 30369fe..9880d49 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source 'http://rubygems.org' gem 'rspec' gem 'pry' +gem 'debugger' # Specify your gem's dependencies in percival.gemspec gemspec diff --git a/Rakefile b/Rakefile index debb76b..84bb1c0 100644 --- a/Rakefile +++ b/Rakefile @@ -35,9 +35,9 @@ task :start do c.plugins.plugins = [ClockPlugin, LoggerPlugin, ChannelChangerPlugin, - NameChangerPlugin] + NameChangerPlugin, + ConnectFourPlugin] end end - bot.start end diff --git a/lib/percival.rb b/lib/percival.rb index cecec07..bddcbed 100644 --- a/lib/percival.rb +++ b/lib/percival.rb @@ -5,6 +5,7 @@ require 'percival/logger' require 'percival/channel_changer' require 'percival/name_changer' +require 'percival/connect_four' PERCIVAL_ROOT = File.dirname(File.dirname(__FILE__)) diff --git a/lib/percival/connect_four.rb b/lib/percival/connect_four.rb new file mode 100644 index 0000000..f2a1be4 --- /dev/null +++ b/lib/percival/connect_four.rb @@ -0,0 +1,3 @@ +require 'percival/connect_four/plugin' +require 'percival/connect_four/connect_four' +require 'percival/connect_four/board_score' diff --git a/lib/percival/connect_four/board_score.rb b/lib/percival/connect_four/board_score.rb new file mode 100644 index 0000000..41af8f1 --- /dev/null +++ b/lib/percival/connect_four/board_score.rb @@ -0,0 +1,109 @@ +class BoardScore + + WIN_VALUE= 10000 + DEPTH = 4 + attr_accessor :score, :win + + def initialize board + @board = board + @win = false + @score = 0 + score_board + end + + def terminal + return true if @win + end + + + def score_board + @board.each_with_index do |column,i| + column.each_with_index do |color, j| + # up + count = counter(color) do |k| + @board[i][j+k] + end + return if won? count, color + @score += color * (count ** 2) + + #up diag + count = counter(color) do |k| + i+k < @board.size ? @board[i+k][j+k] : nil + end + return if won? count, color + @score += color * (count ** 2) + + #right + count = counter(color) do |k| + i+k < @board.size ? @board[i+k][j] : nil + end + return if won? count, color + + @score += color * (count ** 2) + + #down right diag + count = counter(color) do |k| + j-k >= 0 and i+k < @board.size ? @board[i+k][j-k] : nil + end + return if won? count, color + @score += color * (count ** 2) + + #down + count = counter(color) do |k| + j-k >= 0 ? @board[i][j-k] : nil + end + return if won? count, color + @score += color * (count ** 2) + + #down left diag + count = counter(color) do |k| + j-k >= 0 and i-k >= 0 ? @board[i-k][j-k] : nil + end + return if won? count, color + @score += color * (count ** 2) + + # left + count = counter(color) do |k| + i-k >= 0 ? @board[i-k][j] : nil + end + return if won? count, color + @score += color * (count ** 2) + + #up left diag + count = counter(color) do |k| + i-k >= 0 ? @board[i-k][j+k] : nil + end + return if won? count, color + @score += color * (count ** 2) + + end + end + end + + def counter e_color + count = 0 + (0..3).each do |k| + color = yield k + if color == - e_color + count = 0 + break + end + count += 1 if color == e_color + end + count + end + + def won? count, color + if count == 4 + @win = color + @score = @win * WIN_VALUE + return true + end + false + end +end + + + + + diff --git a/lib/percival/connect_four/connect_four.rb b/lib/percival/connect_four/connect_four.rb new file mode 100644 index 0000000..91bf889 --- /dev/null +++ b/lib/percival/connect_four/connect_four.rb @@ -0,0 +1,104 @@ +require 'debugger' + +class ConnectFour + INF = 10000 + DEPTH = 5 + attr_accessor :board + + def initialize width, height + @width, @height = width, height + @my_board = [] + @your_board = [] + @width.times { @my_board.push [] } + end + + def your_move move + @your_board = new_board @my_board, move, -1 + rendering = " +You dropped a piece at #{move + 1} +#{board_to_string(@your_board)}" + + bs = BoardScore.new(@your_board) + if bs.win + return "You win" + rendering + end + return rendering + end + + def my_move + my_move = get_move(@your_board) + @my_board = new_board(@your_board, my_move, 1) + rendering = " +I dropped a piece at #{my_move + 1} +#{board_to_string(@my_board)}" + + bs = BoardScore.new(@my_board) + if bs.win + return "I win!!\n\n" + rendering + end + return rendering + end + + def you_won + "You Won!!! #can't happen" + end + + def i_won + "I Won!!!" + end + + def get_move board + idx = 0 + min = INF + (0..board.size - 1).each do |i| + nb = new_board board, i, 1 + val = negamax nb, DEPTH, -INF, INF, -1 + + if val < min + min = val + idx = i + end + end + return idx + end + + #negamax alpha beta pruning http://en.wikipedia.org/wiki/Negamax + #much better than my mickey mouse implementation of minimax + def negamax board, depth, alpha, beta, color + bs = BoardScore.new board + if depth == 0 or bs.terminal + return color * bs.score + end + (0..(board.size - 1)).each do |i| + nb = new_board board, i, color + val = - negamax( nb, depth - 1, - beta, - alpha, - color) + return val if val >= beta + alpha = val if val >= alpha + end + return alpha + end + + def new_board board, move, color + nb = Marshal.load(Marshal.dump(board)) + nb[move].push color + nb + end + + def board_to_string b + l = [] + (0..@height-1).to_a.reverse.each do |i| + e = [] + (0..@width - 1).each do |j| + if b[j][i].nil? + e << ' ' + elsif b[j][i] > 0 + e << 'X' + elsif b[j][i] < 0 + e << '0' + end + end + l << e.join(' ') + end + return '|' + l.join("|\n|") + "|\n|1 2 3 4 5 6 7|" + end +end diff --git a/lib/percival/connect_four/plugin.rb b/lib/percival/connect_four/plugin.rb new file mode 100644 index 0000000..666bbbe --- /dev/null +++ b/lib/percival/connect_four/plugin.rb @@ -0,0 +1,23 @@ +class ConnectFourPlugin + include Cinch::Plugin + + def initialize *args + super + @games = { } + end + + match /cf\s+(\S+)?/, :method => :connect_four + + def connect_four m, command + if /new/.match command + @games[m.user.name] = ConnectFour.new 7,6 + m.reply "Awaiting your move sir" + elsif /[1-7]/.match command + @games[m.user.name] ||= ConnectFour.new 7,6 + m.reply @games[m.user.name].your_move(command.to_i - 1) + m.reply @games[m.user.name].my_move + else + m.reply "command must be in [new|[1-7]]" + end + end +end