ruby-go 0.0.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +30 -16
- data/bin/rubygo +56 -0
- data/lib/ruby-go.rb +9 -4
- data/lib/ruby-go/board.rb +66 -0
- data/lib/ruby-go/game.rb +135 -0
- data/lib/ruby-go/liberty.rb +11 -0
- data/lib/ruby-go/moves.rb +63 -0
- data/lib/ruby-go/printers/html.rb +76 -0
- data/lib/ruby-go/printers/text.rb +40 -0
- data/lib/ruby-go/stone.rb +50 -0
- data/lib/ruby-go/version.rb +3 -0
- data/test/go_test.rb +158 -136
- data/test/sgf_test.rb +43 -42
- data/test/test_helper.rb +2 -1
- metadata +15 -11
- data/bin/ruby-go.rb +0 -45
- data/lib/board.rb +0 -82
- data/lib/game.rb +0 -116
- data/lib/stone.rb +0 -101
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '024418870e37d715d73c1ce299b3db4c59b231d8fea65b64870e280b9f439387'
|
4
|
+
data.tar.gz: 460d7dbde08c15c6aec3e067dcd50c11d1b2dfac31cd708a97826a3b7b585ad1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2871316ce937e2df59f6ec8b912b51bb20cc1b37c3d257e3ddb6be0067f559d7a18feb6d8e292955257d910cfacb36aab945794912f61438d036c0d561269b3
|
7
|
+
data.tar.gz: bba3a81eac11819591ff71683b413a9679d06e15fc3334b3e874e415ec25886a600272e783f98495a578722ad8fc4c4514089e954cd2bdf73d6046c8e3836555
|
data/README.md
CHANGED
@@ -1,16 +1,30 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
# Ruby Go
|
2
|
+
|
3
|
+
A gem that implements the rules of go (igo, weiqi, baduk).
|
4
|
+
|
5
|
+
## Use
|
6
|
+
|
7
|
+
```
|
8
|
+
game = RubyGo::Game(board: 19)
|
9
|
+
game.place_black(3,3)
|
10
|
+
game.place_white(15,15)
|
11
|
+
|
12
|
+
# ...
|
13
|
+
|
14
|
+
game.pass_black
|
15
|
+
game.pass_white
|
16
|
+
```
|
17
|
+
|
18
|
+
### Printers
|
19
|
+
|
20
|
+
There are a few printers included in the gem, including `TextPrinter` and `HTMLPrinter`.
|
21
|
+
|
22
|
+
The printer takes an `IO` like object, and can print the game.
|
23
|
+
```
|
24
|
+
printer = RubyGo::TextPrinter.new($stdout)
|
25
|
+
printer.print_game(game)
|
26
|
+
```
|
27
|
+
|
28
|
+
### rubygo Command Line Interface
|
29
|
+
|
30
|
+
The rubygo executable included in the gem is CLI that plays the game of go.
|
data/bin/rubygo
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'ruby-go'
|
4
|
+
require 'ruby-go/printers/text'
|
5
|
+
|
6
|
+
game = RubyGo::Game.new
|
7
|
+
printer = RubyGo::TextPrinter.new($stdout)
|
8
|
+
|
9
|
+
Signal.trap("INTERUPT") do
|
10
|
+
puts "\nGoodbye"
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def prompt_for_turn(game)
|
15
|
+
loop do
|
16
|
+
print 'Enter "Pass" or enter coordinates x, y for move (e.g. "4, 4"): '
|
17
|
+
ans = gets.chomp.downcase.gsub(/\s/, '')
|
18
|
+
|
19
|
+
return if ans =~ /pass/
|
20
|
+
|
21
|
+
return ans.split(',').collect {|c| Integer(c)} if ans =~/\d+,\d+/
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_terminal(printer, game)
|
26
|
+
system "clear"
|
27
|
+
printer.print_game(game)
|
28
|
+
end
|
29
|
+
|
30
|
+
update_terminal(printer, game)
|
31
|
+
|
32
|
+
turn = 0
|
33
|
+
until game.passes >= 2
|
34
|
+
begin
|
35
|
+
if turn % 2 == 0
|
36
|
+
#black's turn
|
37
|
+
puts "Black's turn"
|
38
|
+
move = prompt_for_turn(game)
|
39
|
+
move ? game.place_black(*move) : game.black_pass
|
40
|
+
else
|
41
|
+
#white's turn
|
42
|
+
puts "White's turn"
|
43
|
+
move = prompt_for_turn(game)
|
44
|
+
move ? game.place_white(*move) : game.white_pass
|
45
|
+
end
|
46
|
+
|
47
|
+
update_terminal(printer, game)
|
48
|
+
|
49
|
+
turn += 1
|
50
|
+
rescue RubyGo::Game::IllegalMove => e
|
51
|
+
puts e.message
|
52
|
+
next
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "Game over"
|
data/lib/ruby-go.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
-
require_relative 'board'
|
2
|
-
require_relative 'game'
|
3
|
-
require_relative 'stone'
|
4
|
-
|
5
1
|
require 'sgf'
|
2
|
+
|
3
|
+
require_relative 'ruby-go/liberty'
|
4
|
+
require_relative 'ruby-go/stone'
|
5
|
+
require_relative 'ruby-go/board'
|
6
|
+
require_relative 'ruby-go/moves'
|
7
|
+
require_relative 'ruby-go/game'
|
8
|
+
|
9
|
+
module RubyGo
|
10
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module RubyGo
|
2
|
+
class Board
|
3
|
+
attr_reader :rows, :size
|
4
|
+
|
5
|
+
def initialize(size)
|
6
|
+
@rows = Array.new(size) { Array.new(size) { Liberty.new } }
|
7
|
+
@size = size
|
8
|
+
end
|
9
|
+
|
10
|
+
def empty?
|
11
|
+
rows.flatten.all?(&:empty?)
|
12
|
+
end
|
13
|
+
|
14
|
+
def at(x, y)
|
15
|
+
rows[y][x]
|
16
|
+
end
|
17
|
+
|
18
|
+
def around(x, y)
|
19
|
+
intersections = []
|
20
|
+
|
21
|
+
intersections << at(x-1, y) unless x == 0
|
22
|
+
intersections << at(x+1, y) unless x == (size - 1)
|
23
|
+
intersections << at(x, y-1) unless y == 0
|
24
|
+
intersections << at(x, y+1) unless y == (size - 1)
|
25
|
+
intersections
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove(stone)
|
29
|
+
return if stone.empty?
|
30
|
+
|
31
|
+
x, y = stone.to_coord
|
32
|
+
|
33
|
+
rows[y][x] = Liberty.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def place(stone)
|
37
|
+
x, y = stone.to_coord
|
38
|
+
|
39
|
+
rows[y][x] = stone
|
40
|
+
end
|
41
|
+
|
42
|
+
def liberties(stone)
|
43
|
+
libs = []
|
44
|
+
|
45
|
+
group_of(stone).each do |stn|
|
46
|
+
libs += around(*stn.to_coord).select(&:empty?)
|
47
|
+
end
|
48
|
+
|
49
|
+
libs.uniq.length
|
50
|
+
end
|
51
|
+
|
52
|
+
def group_of(stone, stones = [])
|
53
|
+
return stones if stones.include?(stone)
|
54
|
+
|
55
|
+
stones << stone
|
56
|
+
|
57
|
+
around(*stone.to_coord).each do |intersection|
|
58
|
+
next if intersection.empty?
|
59
|
+
|
60
|
+
group_of(intersection, stones) if intersection.color == stone.color
|
61
|
+
end
|
62
|
+
|
63
|
+
stones
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/ruby-go/game.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
module RubyGo
|
2
|
+
class Game
|
3
|
+
attr_reader :board, :moves
|
4
|
+
|
5
|
+
def initialize(board: 19)
|
6
|
+
@board = Board.new(board)
|
7
|
+
@moves = Moves.new
|
8
|
+
end
|
9
|
+
|
10
|
+
private :moves
|
11
|
+
|
12
|
+
def save(name="my_go_game")
|
13
|
+
tree = SGF::Parser.new.parse(to_sgf)
|
14
|
+
tree.save(name + '.sgf')
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_sgf
|
18
|
+
sgf = "(;GM[1]FF[4]CA[UTF-8]AP[jphager2]SZ[#{board.size}]PW[White]PB[Black]"
|
19
|
+
|
20
|
+
moves.each do |move|
|
21
|
+
sgf << move.played.to_sgf
|
22
|
+
end
|
23
|
+
|
24
|
+
sgf << ')'
|
25
|
+
end
|
26
|
+
|
27
|
+
def place_black(x, y)
|
28
|
+
play(Stone.new(x, y, :black))
|
29
|
+
end
|
30
|
+
|
31
|
+
def place_white(x, y)
|
32
|
+
play(Stone.new(x, y, :white))
|
33
|
+
end
|
34
|
+
|
35
|
+
def black_pass
|
36
|
+
pass(:black)
|
37
|
+
end
|
38
|
+
|
39
|
+
def white_pass
|
40
|
+
pass(:white)
|
41
|
+
end
|
42
|
+
|
43
|
+
def undo
|
44
|
+
move = moves.pop
|
45
|
+
|
46
|
+
board.remove(move.played)
|
47
|
+
move.captures.each do |stone|
|
48
|
+
board.place(stone)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def passes
|
53
|
+
moves.pass_count
|
54
|
+
end
|
55
|
+
|
56
|
+
def captures
|
57
|
+
moves.capture_count
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def pass(color)
|
63
|
+
moves.pass(NullStone.new(color))
|
64
|
+
end
|
65
|
+
|
66
|
+
def play(stone)
|
67
|
+
check_illegal_placement!(stone)
|
68
|
+
|
69
|
+
board.place(stone)
|
70
|
+
moves.play(stone)
|
71
|
+
record_captures!(stone)
|
72
|
+
|
73
|
+
check_illegal_suicide!(stone)
|
74
|
+
check_illegal_ko!(stone)
|
75
|
+
end
|
76
|
+
|
77
|
+
def check_illegal_placement!(stone)
|
78
|
+
coord = stone.to_coord
|
79
|
+
|
80
|
+
if coord.any? { |i| i < 0 || i >= board.size }
|
81
|
+
raise(
|
82
|
+
Game::IllegalMove,
|
83
|
+
"You cannot place a stone off the board."
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
intersection = board.at(*coord)
|
88
|
+
|
89
|
+
unless intersection.empty?
|
90
|
+
raise(
|
91
|
+
Game::IllegalMove,
|
92
|
+
"You cannot place a stone on top of another stone."
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def check_illegal_ko!(stone)
|
98
|
+
last_move = moves.prev
|
99
|
+
|
100
|
+
return unless last_move
|
101
|
+
|
102
|
+
if last_move.captures == [stone] && moves.current.captures.one?
|
103
|
+
undo
|
104
|
+
raise IllegalMove,
|
105
|
+
"You cannot capture the ko, play a ko threat first"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def check_illegal_suicide!(stone)
|
110
|
+
if board.liberties(stone).zero?
|
111
|
+
undo
|
112
|
+
raise IllegalMove, "You cannot play a suicide."
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def record_captures!(stone)
|
117
|
+
stones_around = board.around(*stone.to_coord).reject(&:empty?)
|
118
|
+
|
119
|
+
captures = stones_around
|
120
|
+
.reject {| stn| stn.color == stone.color }
|
121
|
+
.select { |stn| @board.liberties(stn).zero? }
|
122
|
+
|
123
|
+
captures.map {|stone| @board.group_of(stone)}
|
124
|
+
.flatten.uniq.each {|stone| capture_stone(stone)}
|
125
|
+
end
|
126
|
+
|
127
|
+
def capture_stone(stone)
|
128
|
+
moves.capture(stone)
|
129
|
+
board.remove(stone)
|
130
|
+
end
|
131
|
+
|
132
|
+
class IllegalMove < StandardError
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module RubyGo
|
2
|
+
class Moves
|
3
|
+
attr_reader :internal_moves, :capture_count, :pass_count
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@internal_moves = []
|
7
|
+
@pass_count = 0
|
8
|
+
@capture_count = { black: 0, white: 0 }
|
9
|
+
end
|
10
|
+
|
11
|
+
def play(played)
|
12
|
+
@pass_count += 0
|
13
|
+
internal_moves << Move.new(played)
|
14
|
+
end
|
15
|
+
|
16
|
+
def pass(pass)
|
17
|
+
@pass_count += 1
|
18
|
+
internal_moves << Move.new(pass)
|
19
|
+
end
|
20
|
+
|
21
|
+
def each(&block)
|
22
|
+
internal_moves.each(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def prev
|
26
|
+
internal_moves[-2]
|
27
|
+
end
|
28
|
+
|
29
|
+
def current
|
30
|
+
internal_moves[-1]
|
31
|
+
end
|
32
|
+
|
33
|
+
def pop
|
34
|
+
move = internal_moves.pop
|
35
|
+
|
36
|
+
move.captures.each do |stone|
|
37
|
+
capture_count[stone.color] -= 1
|
38
|
+
end
|
39
|
+
|
40
|
+
@pass_count -= 1 if move.empty?
|
41
|
+
|
42
|
+
move
|
43
|
+
end
|
44
|
+
|
45
|
+
def capture(stone)
|
46
|
+
current.captures << stone
|
47
|
+
capture_count[stone.color] += 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Move
|
52
|
+
attr_reader :played, :captures
|
53
|
+
|
54
|
+
def initialize(played)
|
55
|
+
@played = played
|
56
|
+
@captures = []
|
57
|
+
end
|
58
|
+
|
59
|
+
def empty?
|
60
|
+
played.empty?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module RubyGo
|
4
|
+
class HTMLPrinter
|
5
|
+
LETTERS = ('A'..'Z').to_a.freeze
|
6
|
+
COLORS = { black: 'black stone', white: 'white stone', empty: 'liberty' }.freeze
|
7
|
+
TEMPLATE = ERB.new(<<~ERB).freeze
|
8
|
+
<style>
|
9
|
+
.board { max-width: 100%; padding: 16px; }
|
10
|
+
.row { display: flex; margin: 0; }
|
11
|
+
.intersection { display: inline-block; height: 32px; width: 32px; border-top: 2px solid black; border-left: 2px solid black; }
|
12
|
+
.intersection:last-child { border-top-width: 0px; height: 34px; }
|
13
|
+
.row:last-child .intersection { border-left-width: 0px; width: 34px; }
|
14
|
+
.row:last-child .intersection:last-child { width: 2px; height: 2px; background: black; }
|
15
|
+
.stone, .liberty { display: inline-block; width: 30px; height: 30px; border-radius: 100%; margin: 0; position: relative; top: -18px; left: -18px; }
|
16
|
+
.stone { border: 2px solid black; }
|
17
|
+
.black.stone { background: black; }
|
18
|
+
.white.stone { background: white; }
|
19
|
+
.liberty:hover { border: 2px solid red; }
|
20
|
+
.row-num, .column-num { display: inline-block; width: 34px; height: 34px; margin: 0; position: relative; }
|
21
|
+
.row-num { top: -0.5em; }
|
22
|
+
.column-num { left: -0.3em; }
|
23
|
+
</style>
|
24
|
+
<div class="game">
|
25
|
+
<div class="board">
|
26
|
+
<div class="row">
|
27
|
+
<div class="column-num row-num"></div>
|
28
|
+
<% size.times do |i| %>
|
29
|
+
<div class="column-num"><%= letters[i] %></div>
|
30
|
+
<% end %>
|
31
|
+
</div>
|
32
|
+
<% rows.each_with_index do |row, i| %>
|
33
|
+
<div class="row">
|
34
|
+
<div class="row-num"><%= letters[i] %></div>
|
35
|
+
<% row.each do |stn| %>
|
36
|
+
<div class="intersection">
|
37
|
+
<div class="<%= colors[stn.color] %>"></div>
|
38
|
+
</div>
|
39
|
+
<% end %>
|
40
|
+
</div>
|
41
|
+
<% end %>
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<div class="info">
|
45
|
+
<table>
|
46
|
+
<tbody>
|
47
|
+
<tr>
|
48
|
+
<th>Prisoners</th>
|
49
|
+
<th>White</th>
|
50
|
+
<td><%= captures[:black] %></td>
|
51
|
+
<th>Black</th>
|
52
|
+
<td><%= captures[:white] %></td>
|
53
|
+
</tr>
|
54
|
+
</tbody>
|
55
|
+
</table>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
ERB
|
59
|
+
|
60
|
+
attr_reader :io
|
61
|
+
|
62
|
+
def initialize(io)
|
63
|
+
@io = io
|
64
|
+
end
|
65
|
+
|
66
|
+
def print_game(game)
|
67
|
+
html = TEMPLATE.result_with_hash(
|
68
|
+
size: game.board.size,
|
69
|
+
captures: game.captures,
|
70
|
+
rows: game.board.rows,
|
71
|
+
colors: COLORS, letters: LETTERS
|
72
|
+
)
|
73
|
+
io.write(html)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|