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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8f24b07ee968e8a121f825d7c64e615649b8379e
4
- data.tar.gz: 647b951b33b4394a657ddb1a749b8677ad6497c7
2
+ SHA256:
3
+ metadata.gz: '024418870e37d715d73c1ce299b3db4c59b231d8fea65b64870e280b9f439387'
4
+ data.tar.gz: 460d7dbde08c15c6aec3e067dcd50c11d1b2dfac31cd708a97826a3b7b585ad1
5
5
  SHA512:
6
- metadata.gz: 365788568b669d24207f3e52c604cf218f5c4e6228ec1316b5847a8e9ceadfd25f2f03807cfba0fc5793c79e88fbceffc3559b471e69c9762c43a7382ca401bb
7
- data.tar.gz: b0c4e34d1cf9482e68322dcc3e9f6bc7b47ae1a6952baa4c7ac47b047599082ccddd05e677f3ec4fd62aa12bf76a4f52e21a8dbbd3b390fd59f80fecc475bf43
6
+ metadata.gz: b2871316ce937e2df59f6ec8b912b51bb20cc1b37c3d257e3ddb6be0067f559d7a18feb6d8e292955257d910cfacb36aab945794912f61438d036c0d561269b3
7
+ data.tar.gz: bba3a81eac11819591ff71683b413a9679d06e15fc3334b3e874e415ec25886a600272e783f98495a578722ad8fc4c4514089e954cd2bdf73d6046c8e3836555
data/README.md CHANGED
@@ -1,16 +1,30 @@
1
- Kata for the Game of Go
2
- =======================
3
-
4
- Objective
5
- ---------
6
- Build a go game object with all functionality and uses TDD:
7
- * Can print the board
8
- * Can place a black/white stone on board
9
- * Cannot place a stone on top of another stone
10
- * Captures group of stones with no liberties
11
- * Keeps track of captures
12
- * Can undo all moves
13
- * Can pass
14
- * Keeps track of passes in a row (I.e. passes reset to 0 if a stone is played and subtract by 1 if it is undo)
15
- * Cannot place a suicide stone
16
- * Cannot capture a ko
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.
@@ -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"
@@ -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
@@ -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,11 @@
1
+ module RubyGo
2
+ class Liberty
3
+ def empty?
4
+ true
5
+ end
6
+
7
+ def color
8
+ :empty
9
+ end
10
+ end
11
+ 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