-
Notifications
You must be signed in to change notification settings - Fork 8
Connect four #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Connect four #23
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require 'percival/connect_four/plugin' | ||
require 'percival/connect_four/connect_four' | ||
require 'percival/connect_four/board_score' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
|
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
require 'debugger' | ||
|
||
class ConnectFour | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can probably have some tests for this class in isolation of the plugin functionality. That might give us some opportunity to refactor this so that we have some better MVC in here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried writing this TDD style but it was tough and I gave up and did it the old way. That being said I spent 4 hours or so trying to figure out why it was the worst player ever and had no idea how to track down the bug. It was a minus sign that should have been a plus. I think I'm getting more comfortable with tests now though. I'm actually doing what looks to me like real TDD in the blackjack plugin and the LPMC Casino api plugin. However I'm still not sure how to test this. By it's nature it's so open ended I don't really know what to test and how. Do I test that it chooses the right move? I don't even know what the right move is. I can test that the board scoring is working but there are thousands of possible boards and I don't really know what boards to test that would provide enough coverage. (those aren't really specific questions I'm asking. They're just illustrative examples of the kind of issues I'm having with the concept of testing) At the very least we can provide tests for some of the smaller helper methods though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rule #1 of TDD, if it's hard to test, you've probably got a problem in the code. Rule #2 of TDD, most code is hard to test. Lemma #1 : Most code has problems (follows from Rule #2 and Rule #1). I haven't looked at this code thoroughly yet, my guess, however, is that you The idea of the SRP is that each class should have precisely one responsibility To play Connect Four, I first need a board, a board has many positions, So, we can try to parse that, and read out the actors of the system -- an Actor To play Connect Four, I first need a board, a board has many positions, So we can see that the Actors of the system are:
So far, I'm counting 4 or 5 (I'm not sure how I want to model Fillable positions At this point, We can go back and look at the relationships between models, I
After looking at the relationships, we can start to think about responsibilities A position is a bit different, players will be manipulating them often, we will The players are our "primary" actors, most of the interaction with the program Finally, the Arbitrator is really just encapsulating the "win" condition, he So, already we can see there is a little more apparent complexity (inasmuch as # in board_spec.rb
describe Board do
it { should respond_to :positions }
it { should respond_to :fillable_positions }
it { should respond_to :players }
end
#in position_spec.rb
describe Position do
it { should repsond_to :filled? }
it { should respond_to :empty? }
it { should respond_to :owner }
it { should respond_to :fillable }
describe "a filled/taken position" do
#intentionally blank
end
describe "an empty position" do
#intentionally blank
end
end
#etc These Now we can start speccing. Let's look at the position spec #in position_spec.rb
describe Position do
it { should repsond_to :filled? }
it { should respond_to :empty? }
it { should respond_to :owner }
it { should respond_to :fillable? }
let (:position) { Position.new }
let (:player) { double("Player 1") }
let (:board) { double("Board") }
subject { position }
describe "a filled/taken position" do
before do
position.stub(:owner).and_return(player)
end
it { should_not be_empty }
it { should_not be_fillable }
it { should be_filled }
end
describe "an empty position" do
it { should be_empty }
its(:owner) { should be_nil }
context "an empty, unfillable position" do
before do
#appropriate mocking to make the board specify this position as
#not-fillable
end
it { should_not be_fillable }
end
context "an empty, fillable position" do
before do
#appropriate mocking to make the board specify this position as
#fillable
end
it { should be_fillable }
end
end
end Now hopefully you can see how easy it is to spec that model, and further, the Hopefully that helps elucidate a bit, I want to add the disclaimer that I /Joe [1] This is a pretty common technique, in Haskell they call it the "Maybe" |
||
INF = 10000 | ||
DEPTH = 5 | ||
attr_accessor :board | ||
|
||
def initialize width, height | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parens |
||
@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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does that hash cause the rest of the line to be a comment? I guess it must not, ruby is so clever. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to extract the minimax algorithm here into it's own class/module? |
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can lose this -- that's what pry is for! Just use binding.pry where you would use debugger. plop right into a running ruby interpreter! :)