ruby-go 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 091a2ca59d5d27ee3f245c028892c85e397ad95c
4
+ data.tar.gz: 89446d73ed6b27a4ca938e287d51cda3e7b2489c
5
+ SHA512:
6
+ metadata.gz: 8f69ca47e570e43b03f3079c4058f2dc37e8a201417d4ba47cd497216e43619312e3d5aa7fd60286740668a30ccde51d252358336db8b2434e2214385649ca9f
7
+ data.tar.gz: fc3d835f40595d18b13d06852daddd3c819d89437aa495cc999636e2669b1a956eaae542cefa7556c215f0976182f6530433fe2d2fd7f7f8efbe3a139be36046
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 John Hager
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,16 @@
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
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rake'
2
+
3
+ task :default do
4
+ Dir.glob('test/*_test.rb').each {|test| load test}
5
+ end
data/bin/ruby-go.rb ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/go'
4
+
5
+ game = Game.new
6
+
7
+ def game.prompt_for_turn
8
+ print "Pass or enter coordinates x, y for move (e.g. 4, 4): "
9
+ ans = gets.chomp.downcase.gsub(/\s/, '')
10
+
11
+ if ans =~ /pass/
12
+ self.pass
13
+ nil
14
+ else
15
+ ans.split(',').collect {|c| Integer(c)}
16
+ end
17
+ end
18
+
19
+ system "clear"
20
+ game.view
21
+ turn = 0
22
+ until game.passes >= 2
23
+ begin
24
+ if turn % 2 == 0
25
+ #black's turn
26
+ puts "Black's turn"
27
+ ans = game.prompt_for_turn
28
+ game.black(*ans) if ans
29
+ else
30
+ #white's turn
31
+ puts "white's turn"
32
+ ans = game.prompt_for_turn
33
+ game.white(*ans) if ans
34
+ end
35
+
36
+ system "clear"
37
+ game.view
38
+
39
+ turn += 1
40
+ rescue Game::IllegalMove
41
+ next
42
+ end
43
+ end
44
+
45
+ system "clear"
data/lib/board.rb ADDED
@@ -0,0 +1,82 @@
1
+ class Board
2
+ Colors = {black: 'x', white: 'o', empty: '_'}
3
+
4
+ attr_accessor :board
5
+ def initialize(size)
6
+ @board = []
7
+ size -= 1
8
+
9
+ 0.upto(size) do |y|
10
+ row = []
11
+ 0.upto(size) do |x|
12
+ row << Liberty.new(x, y)
13
+ end
14
+ @board << row
15
+ end
16
+ end
17
+
18
+ def size
19
+ @board.length
20
+ end
21
+
22
+ def empty?
23
+ !@board.flatten.any? do |s|
24
+ !s.empty?
25
+ end
26
+ end
27
+
28
+ def at(x, y)
29
+ @board[y][x]
30
+ end
31
+
32
+ def around(x, y = :y_not_given)
33
+ x, y = x.to_coord if x.kind_of?(Stone)
34
+ stones = []
35
+ stones << at(x-1, y) unless x == 0
36
+ stones << at(x+1, y) unless x == (@board.length - 1)
37
+ stones << at(x, y-1) unless y == 0
38
+ stones << at(x, y+1) unless y == (@board.length - 1)
39
+ stones
40
+ end
41
+
42
+ def remove(stone)
43
+ stone.remove_from_board(self)
44
+ end
45
+
46
+ def place(stone)
47
+ stone.place_on_board(self)
48
+ end
49
+
50
+ def liberties(stone)
51
+ group_of(stone).inject(0) do |libs, stn|
52
+ libs + stn.liberties(self).count
53
+ end
54
+ end
55
+
56
+ def group_of(stone)
57
+ stone.group(self)
58
+ end
59
+
60
+ def to_str
61
+ out = ""
62
+ if @board.length < 11
63
+ out << "\s\s\s#{(0..@board.length - 1).to_a.join(' ')}\n"
64
+ else
65
+ out <<
66
+ "\s\s\s#{(0..10).to_a.join(' ')}" <<
67
+ "#{(11..@board.length - 1).to_a.join('')}\n"
68
+ end
69
+
70
+ @board.each_with_index do |row, i|
71
+ i = "\s#{i}" if i < 10
72
+ out << "#{i}\s#{(row.collect {|stn| stn.to_s}).join(' ')}\n"
73
+ end
74
+
75
+ out
76
+ end
77
+
78
+ def to_s
79
+ to_str
80
+ end
81
+ end
82
+
data/lib/game.rb ADDED
@@ -0,0 +1,116 @@
1
+ class Game
2
+
3
+ LETTERS = ('a'..'z').to_a
4
+
5
+ attr_reader :board
6
+ def initialize(board: 19)
7
+ @board = Board.new(board)
8
+ @moves = []
9
+ end
10
+
11
+ def save(name="my_go_game")
12
+ tree = SGF::Parser.new.parse(to_sgf)
13
+ tree.save(name + '.sgf')
14
+ end
15
+
16
+ def to_sgf
17
+ sgf = "(;GM[1]FF[4]CA[UTF-8]AP[jphager2]SZ[19]PW[White]PB[Black]"
18
+
19
+ @moves.each do |move|
20
+ sgf << move[:stone].to_sgf
21
+ end
22
+
23
+ sgf << ')'
24
+ end
25
+
26
+ def view
27
+ puts @board.to_s
28
+ puts " " + "_"*(@board.size * 2)
29
+ print " Prisoners || White: #{captures[:black]} |"
30
+ puts " Black: #{captures[:white]}"
31
+ puts " " + "-"*(@board.size * 2)
32
+ end
33
+
34
+ def black(x, y)
35
+ play(BlackStone.new(x,y))
36
+ end
37
+
38
+ def white(x, y)
39
+ play(WhiteStone.new(x,y))
40
+ end
41
+
42
+ def pass
43
+ @moves << {stone: NullStone.new(), captures: [], pass: true}
44
+ end
45
+
46
+ def undo
47
+ move = @moves.pop
48
+ @board.remove(move[:stone])
49
+ move[:captures].each {|stone| @board.place(stone)}
50
+ end
51
+
52
+ def passes
53
+ @moves.inject(0) {|total, move| move[:pass] ? total + 1 : 0}
54
+ end
55
+
56
+ def captures
57
+ @moves.each_with_object({black: 0, white: 0}) do |move, total|
58
+ move[:captures].each do |capture|
59
+ total[capture.color] += 1
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+ def play(stone)
66
+ @board.place(stone)
67
+ @moves << {stone: stone, captures: [], pass: false}
68
+
69
+ capture; suicide; ko
70
+ end
71
+
72
+ def ko
73
+ return if @moves.length < 2 or !@moves[-2][:captures]
74
+
75
+ captures = @moves[-2][:captures]
76
+ stone = @moves.last[:stone]
77
+
78
+ if captures.include?(stone)
79
+ undo
80
+ raise IllegalMove,
81
+ "You cannot capture the ko, play a ko threat first"
82
+ end
83
+ end
84
+
85
+ def suicide
86
+ stone = @moves.last[:stone]
87
+ unless @board.liberties(stone) > 0
88
+ undo
89
+ raise IllegalMove, "You cannot play a suicide."
90
+ end
91
+ end
92
+
93
+ def capture
94
+ stone = @moves.last[:stone]
95
+ stones_around = @board.around(stone)
96
+
97
+ captures = stones_around.select {|stn| @board.liberties(stn) == 0}
98
+
99
+ captures.each {|stone| capture_group(stone)}
100
+ end
101
+
102
+ def capture_group(stone)
103
+ @board.group_of(stone).each {|stone| capture_stone(stone)}
104
+ end
105
+
106
+ def capture_stone(stone)
107
+ @moves.last[:captures] << stone
108
+ @board.remove(stone)
109
+ end
110
+
111
+ public
112
+
113
+ class IllegalMove < Exception
114
+ end
115
+ end
116
+
data/lib/go.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative 'board'
2
+ require_relative 'game'
3
+ require_relative 'stone'
4
+
5
+ require 'sgf'
data/lib/stone.rb ADDED
@@ -0,0 +1,101 @@
1
+ class Stone
2
+
3
+ attr_reader :color
4
+ def initialize(x, y)
5
+ @x, @y = x, y
6
+ @color = :none
7
+ end
8
+
9
+ def to_sgf
10
+ x = Game::LETTERS[@x]
11
+ y = Game::LETTERS[@y]
12
+ ";#{color.to_s[0].upcase}[#{x+y}]"
13
+ end
14
+
15
+ def empty?
16
+ @color == :empty
17
+ end
18
+
19
+ def place_on_board(board)
20
+ unless board.at(@x, @y).empty?
21
+ raise Game::IllegalMove,
22
+ "You cannot place a stone on top of another stone."
23
+ end
24
+ board.board[@y][@x] = self
25
+ end
26
+
27
+ def remove_from_board(board)
28
+ board.board[@y][@x] = Liberty.new(*self.to_coord)
29
+ end
30
+
31
+ def liberties(board)
32
+ board.around(self).select {|stone| stone.empty?}
33
+ end
34
+
35
+ def group(board, stones = [])
36
+ return stones if stones.any? {|stone| stone.eql?(self)}
37
+ stones << self
38
+
39
+ board.around(self).each do |stone|
40
+ if stone.color == @color
41
+ stone.group(board, stones)
42
+ end
43
+ end
44
+
45
+ stones
46
+ end
47
+
48
+ def to_coord
49
+ [@x, @y]
50
+ end
51
+
52
+ def to_str
53
+ Board::Colors[@color]
54
+ end
55
+
56
+ def to_s
57
+ to_str
58
+ end
59
+
60
+ def ==(other)
61
+ (self.color == other.color) and (self.to_coord == other.to_coord)
62
+ end
63
+ end
64
+
65
+ class Liberty < Stone
66
+ def initialize(x, y)
67
+ super
68
+ @color = :empty
69
+ end
70
+
71
+ def liberties(board)
72
+ [self]
73
+ end
74
+ end
75
+
76
+ class BlackStone < Stone
77
+ def initialize(x, y)
78
+ super
79
+ @color = :black
80
+ end
81
+ end
82
+
83
+ class WhiteStone < Stone
84
+ def initialize(x, y)
85
+ super
86
+ @color = :white
87
+ end
88
+ end
89
+
90
+ class NullStone < Stone
91
+ def initialize(*args)
92
+ @x, @y = nil, nil
93
+ end
94
+
95
+ def remove_from_board(board)
96
+ end
97
+
98
+ def to_sfg
99
+ ""
100
+ end
101
+ end
data/test/go_test.rb ADDED
@@ -0,0 +1,135 @@
1
+ require_relative 'test_helper'
2
+ require 'stringio'
3
+
4
+ class GoTest < Minitest::Test
5
+ def setup
6
+ b,w = Board::Colors[:black], Board::Colors[:white]
7
+ e = Board::Colors[:empty]
8
+
9
+ @row_9_str = "_ _ _ _ _ _ _ _ _\n"
10
+
11
+ @game = Game.new(board: 9)
12
+ end
13
+
14
+ def test_game_has_blank_board_when_initialized
15
+ assert @game.board.empty?
16
+ end
17
+
18
+ def test_empty_board_looks_like_empty_board
19
+ assert_includes @game.board.to_s, @row_9_str
20
+ end
21
+
22
+ def test_game_can_print_the_board
23
+ out = StringIO.new
24
+ temp = $stdout
25
+ $stdout = out
26
+ @game.view
27
+ $stdout = temp
28
+ out.rewind
29
+ assert_includes out.read, @row_9_str
30
+ end
31
+
32
+ def test_game_can_place_a_stone
33
+ game = Game.new(board: 9)
34
+ game.black(2,2)
35
+ assert_equal BlackStone.new(2,2), game.board.at(2,2)
36
+ end
37
+
38
+ def test_cannot_place_a_stone_on_top_of_another_stone
39
+ game = Game.new(board: 9)
40
+ game.black(2,2)
41
+ assert_raises(Game::IllegalMove) do
42
+ game.white(2,2)
43
+ end
44
+ end
45
+
46
+ def test_can_capture_one_stone
47
+ game = Game.new(board: 9)
48
+ game.black(2,2)
49
+ game.white(2,1)
50
+ game.white(2,3)
51
+ game.white(1,2)
52
+ game.white(3,2)
53
+ assert_equal 1, game.captures[:black]
54
+ end
55
+
56
+ def test_capture_removes_stone
57
+ game = Game.new(board: 9)
58
+ game.black(2,2)
59
+ game.white(2,1)
60
+ game.white(2,3)
61
+ game.white(1,2)
62
+ game.white(3,2)
63
+ assert_equal Liberty.new(2,2), game.board.at(2,2)
64
+ end
65
+
66
+ def test_capture_three_stones
67
+ game = Game.new(board: 9)
68
+ game.black(2,1)
69
+ game.black(2,2)
70
+ game.black(2,3)
71
+ game.white(2,0)
72
+ game.white(2,4)
73
+ game.white(1,1)
74
+ game.white(1,2)
75
+ game.white(1,3)
76
+ game.white(3,1)
77
+ game.white(3,2)
78
+ game.white(3,3)
79
+ assert_equal 3, game.captures[:black]
80
+ end
81
+
82
+ def test_can_undo
83
+ game = Game.new()
84
+ game.black(2,2)
85
+ game.white(2,3)
86
+ game.undo
87
+ assert_equal Liberty.new(2,3), game.board.at(2,3)
88
+ end
89
+
90
+ def test_can_undo_until_beginning
91
+ game = Game.new()
92
+ game.black(2,2)
93
+ game.white(3,2)
94
+ game.black(3,3)
95
+ 3.times {game.undo}
96
+ assert_equal Liberty.new(2,2), game.board.at(2,2)
97
+ end
98
+
99
+ def test_can_pass
100
+ game = Game.new
101
+ game.pass
102
+ game.pass
103
+ assert_equal 2, game.passes
104
+ end
105
+
106
+ def test_cannot_play_a_suicide
107
+ game = Game.new
108
+ game.black(3,3)
109
+ game.white(2,3)
110
+ game.white(4,3)
111
+ game.white(3,4)
112
+ game.white(3,2)
113
+ assert_raises(Game::IllegalMove) do
114
+ game.black(3,3)
115
+ end
116
+ end
117
+
118
+ def test_cannot_play_a_ko
119
+ game = Game.new
120
+ game.black(3,3)
121
+ game.white(2,3)
122
+ game.white(4,3)
123
+ game.white(3,4)
124
+ game.white(3,2)
125
+ game.black(4,2)
126
+ game.black(4,4)
127
+ game.black(5,3)
128
+ game.black(3,3)
129
+ assert_raises(Game::IllegalMove) do
130
+ game.white(4,3)
131
+ end
132
+ end
133
+ end
134
+
135
+
data/test/sgf_test.rb ADDED
@@ -0,0 +1,51 @@
1
+ require_relative 'test_helper'
2
+
3
+ class SGFTest < Minitest::Test
4
+
5
+ def test_game_converts_to_sgf
6
+ game = Game.new
7
+ assert_equal "(;GM[1]FF[4]CA[UTF-8]AP[jphager2]SZ[19]PW[White]PB[Black])",
8
+ game.to_sgf
9
+ end
10
+
11
+ def test_game_converts_to_sgf_with_moves
12
+ game = Game.new
13
+ game.black(15,3)
14
+ game.white(3,3)
15
+ game.black(3,15)
16
+ game.white(15,15)
17
+ assert_equal "(;GM[1]FF[4]CA[UTF-8]AP[jphager2]SZ[19]PW[White]PB[Black];B[pd];W[dd];B[dp];W[pp])",
18
+ game.to_sgf
19
+ end
20
+
21
+ def test_game_converts_correctly_with_capture
22
+ game = Game.new
23
+ game.black(15,3)
24
+ game.white(16,3)
25
+ game.black(16,2)
26
+ game.white(15,2)
27
+ game.black(14,2)
28
+ game.white(14,3)
29
+ game.black(17,3)
30
+ game.white(15,4)
31
+ game.black(16,4)
32
+ game.white(15,3)
33
+
34
+ assert_equal "(;GM[1]FF[4]CA[UTF-8]AP[jphager2]SZ[19]PW[White]PB[Black];B[pd];W[qd];B[qc];W[pc];B[oc];W[od];B[rd];W[pe];B[qe];W[pd])",
35
+ game.to_sgf
36
+ end
37
+
38
+ def test_saves_sgf
39
+ file = File.expand_path('../../my_go_game.sgf', __FILE__)
40
+ path = File.expand_path('../../*.sgf', __FILE__)
41
+ File.delete(file) if File.exist?(file)
42
+ game = Game.new
43
+ game.black(15,3)
44
+ game.white(16,3)
45
+ game.black(16,2)
46
+ game.white(15,2)
47
+ game.save
48
+ assert_includes Dir.glob(path), file
49
+ end
50
+
51
+ end
@@ -0,0 +1,3 @@
1
+ require_relative '../lib/go.rb'
2
+
3
+ require 'minitest/autorun'
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-go
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - jphager2
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A gem to play go and save games as sgf
14
+ email: jphager2@gmail.com
15
+ executables:
16
+ - ruby-go.rb
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - Rakefile
23
+ - bin/ruby-go.rb
24
+ - lib/board.rb
25
+ - lib/game.rb
26
+ - lib/go.rb
27
+ - lib/stone.rb
28
+ - test/go_test.rb
29
+ - test/sgf_test.rb
30
+ - test/test_helper.rb
31
+ homepage: https://github.com/jphager2/ruby-go
32
+ licenses:
33
+ - MIT
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.2.2
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: The game of Go, writen in Ruby
55
+ test_files: []