ruby-go 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|