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 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