reversi 0.0.1 → 0.1.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 +4 -4
- data/.gitignore +1 -0
- data/Gemfile +15 -1
- data/Guardfile +9 -0
- data/README.md +80 -1
- data/Rakefile +5 -2
- data/images/reversi.gif +0 -0
- data/lib/reversi/board.rb +215 -0
- data/lib/reversi/configuration.rb +36 -0
- data/lib/reversi/game.rb +99 -0
- data/lib/reversi/player/base_player.rb +76 -0
- data/lib/reversi/player/human.rb +49 -0
- data/lib/reversi/player/negamax_ai.rb +42 -0
- data/lib/reversi/player/random_ai.rb +9 -0
- data/lib/reversi/player.rb +7 -0
- data/lib/reversi/version.rb +1 -1
- data/lib/reversi.rb +8 -1
- data/reversi.gemspec +9 -5
- data/spec/base_player_spec.rb +78 -0
- data/spec/board_spec.rb +136 -0
- data/spec/game_spec.rb +84 -0
- metadata +26 -26
- data/.travis.yml +0 -3
- data/spec/reversi_spec.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dde2e7d7a4f73ef89fbdc626ff5f06d22e0451fa
|
|
4
|
+
data.tar.gz: d3d2a7f4eea6769ea366c8cd3a1a059238cbf0f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96c2d82cfb35399f3951b62c08a18d024b564a146d588311ce3ef1790d512be2d9d77e1def514be5fd23dae97509a979cfb5308c9b03a2e92d55365a5375fefa
|
|
7
|
+
data.tar.gz: 32d24821205506520d37fec291679fa21ca5e30369bdafa1ed5f7a8772008f9b207189864ce286f9a2dd16b23030b2af11ddafa4d680ec7287bd6580e4536ba2
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
gem 'rake'
|
|
4
|
+
gem 'yard'
|
|
5
|
+
|
|
6
|
+
group :development do
|
|
7
|
+
gem 'pry'
|
|
8
|
+
gem 'pry-doc'
|
|
9
|
+
gem 'pry-stack_explorer'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
group :test do
|
|
13
|
+
gem 'rspec', '>= 3.1'
|
|
14
|
+
gem 'guard-rspec', '4.5.0'
|
|
15
|
+
gem 'libnotify', '0.9.1'
|
|
16
|
+
end
|
|
17
|
+
|
|
4
18
|
gemspec
|
data/Guardfile
ADDED
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Reversi
|
|
2
2
|
|
|
3
|
+
[](http://badge.fury.io/rb/reversi)
|
|
4
|
+
A Ruby Gem to play reversi game. You can enjoy a game on the command line or easily make your original reversi game programs.
|
|
5
|
+
|
|
6
|
+

|
|
3
7
|
|
|
4
8
|
## Installation
|
|
5
9
|
|
|
@@ -19,10 +23,85 @@ Or install it yourself as:
|
|
|
19
23
|
|
|
20
24
|
## Usage
|
|
21
25
|
|
|
26
|
+
Run a demo program in this introduction.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require 'reversi'
|
|
30
|
+
|
|
31
|
+
Reversi.configure do |config|
|
|
32
|
+
config.disk_color_b = 'cyan'
|
|
33
|
+
config.disk_b = "O"
|
|
34
|
+
config.disk_w = "O"
|
|
35
|
+
config.progress = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
game = Reversi::Game.new
|
|
39
|
+
game.start
|
|
40
|
+
puts "black #{game.board.status[:black].size}"
|
|
41
|
+
puts "white #{game.board.status[:white].size}"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Configuration
|
|
45
|
+
|
|
46
|
+
Use `Reversi.configure` to configure setting for a reversi game.
|
|
47
|
+
|
|
48
|
+
`name` description... (default value)
|
|
49
|
+
|
|
50
|
+
* `player_b` A player having the first move uses this class object. (Reversi::Player::RandomAI)
|
|
51
|
+
* `player_w` A player having the passive move uses this class object. (Reversi::Player::RandomAI)
|
|
52
|
+
* `disk_b` A string of the black disks. ('b')
|
|
53
|
+
* `disk_w` A string of the black disks. ('w')
|
|
54
|
+
* `disk_color_b` A color of the black disks. (0)
|
|
55
|
+
* `disk_color_w` A color of the black disks. (0)
|
|
56
|
+
* `progress` Whether or not the progress of the game is displayed. (false)
|
|
57
|
+
* `stack_limit` The upper limit number of times of use `Reversi::Board#undo!` . (3)
|
|
58
|
+
|
|
59
|
+
A string and a color of the disks are reflected on `game.board.to_s` .
|
|
60
|
+
You can choose from 9 colors, black, red, green, yellow, blue, magenda, cyan, white and gray.
|
|
61
|
+
|
|
62
|
+
### Human vs Computer
|
|
63
|
+
|
|
64
|
+
Set `Reversi::Player::Human` to player_b or player_w, and run. Please input your move (for example: d3). This program is terminated when this game is over or when you input `q` or `exit`.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
Reversi.configure do |config|
|
|
68
|
+
config.player_b = Reversi::Player::Human
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
game = Reversi::Game.new
|
|
72
|
+
game.start
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### My AI
|
|
76
|
+
|
|
77
|
+
You can make your original player class by inheriting `Reversi::Player::BasePlayer` and defining `move` method.
|
|
78
|
+
|
|
79
|
+
`next_moves` method returns an array of the next moves information. A player places a supplied color's disk on specified position, and flips the opponent's disks by using `put_disk` method. You can get the current game board state from a `board` variable.
|
|
80
|
+
|
|
81
|
+
* Example of Random AI
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class MyAI < Reversi::Player::BasePlayer
|
|
85
|
+
def move(board)
|
|
86
|
+
moves = next_moves.map{ |v| v[:move] }
|
|
87
|
+
put_disk(*moves.sample) unless moves.empty?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Reversi.configure do |config|
|
|
92
|
+
config.player_b = MyAI
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
game = Reversi::Game.new
|
|
96
|
+
game.start
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
* Example of Negamax Algorithm
|
|
100
|
+
Please see `Reversi::Player::NegamaxAI` .
|
|
22
101
|
|
|
23
102
|
## Contributing
|
|
24
103
|
|
|
25
|
-
1. Fork it
|
|
104
|
+
1. Fork it
|
|
26
105
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
27
106
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
28
107
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
require "bundler/gem_tasks"
|
|
2
|
-
require "rspec/core/rake_task"
|
|
3
2
|
|
|
3
|
+
require "rspec/core/rake_task"
|
|
4
4
|
RSpec::Core::RakeTask.new(:spec)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
require 'yard'
|
|
7
|
+
YARD::Rake::YardocTask.new do |t|
|
|
8
|
+
t.files = ['lib/**/*.rb', 'lib/*.rb']
|
|
9
|
+
end
|
|
7
10
|
|
data/images/reversi.gif
ADDED
|
Binary file
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
module Reversi
|
|
2
|
+
class Board
|
|
3
|
+
attr_reader :options, :columns, :stack
|
|
4
|
+
|
|
5
|
+
COORDINATES = (:a..:h).map{ |x| (1..8).map{ |y| [x, y] } }.flatten(1).freeze
|
|
6
|
+
|
|
7
|
+
DISK = {
|
|
8
|
+
:none => 0,
|
|
9
|
+
:wall => 2,
|
|
10
|
+
:black => -1,
|
|
11
|
+
:white => 1
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
DISK_COLOR = {
|
|
15
|
+
:black => 30,
|
|
16
|
+
:red => 31,
|
|
17
|
+
:green => 32,
|
|
18
|
+
:yellow => 33,
|
|
19
|
+
:blue => 34,
|
|
20
|
+
:magenda => 35,
|
|
21
|
+
:cyan => 36,
|
|
22
|
+
:white => 37,
|
|
23
|
+
:gray => 90
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# Initializes a new Board object.
|
|
27
|
+
#
|
|
28
|
+
# @see Reversi::Game
|
|
29
|
+
# @see Reversi::Configration
|
|
30
|
+
# @return [Reversi::Board]
|
|
31
|
+
def initialize(options = {})
|
|
32
|
+
@options = options
|
|
33
|
+
@stack = []
|
|
34
|
+
[:disk_color_b, :disk_color_w].each do |color|
|
|
35
|
+
if options[color].is_a?(Symbol) || options[color].is_a?(String)
|
|
36
|
+
options[color] = DISK_COLOR[options[color].to_sym].to_i
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
@columns = (0..9).map{ (0..9).map{ |_| DISK[:none] } }
|
|
40
|
+
put_disk(4, 4, :white); put_disk(5, 5, :white)
|
|
41
|
+
put_disk(4, 5, :black); put_disk(5, 4, :black)
|
|
42
|
+
@columns.each do |col|
|
|
43
|
+
col[0] = 2; col[-1] = 2
|
|
44
|
+
end.tap do |cols|
|
|
45
|
+
cols[0].fill(2); cols[-1].fill(2)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns a string of the game board in human-readable form.
|
|
50
|
+
#
|
|
51
|
+
# @return [String]
|
|
52
|
+
def to_s
|
|
53
|
+
" #{(:a..:h).to_a.map(&:to_s).join(" ")}\n" <<
|
|
54
|
+
" #{"+---"*8}+\n" <<
|
|
55
|
+
@columns[1][1..-2].zip(*@columns[2..8].map{ |col| col[1..-2] })
|
|
56
|
+
.map{ |row| row.map do |e|
|
|
57
|
+
case e
|
|
58
|
+
when 0 then " "
|
|
59
|
+
when -1 then "\e[#{@options[:disk_color_b]}m#{@options[:disk_b]}\e[0m"
|
|
60
|
+
when 1 then "\e[#{@options[:disk_color_w]}m#{@options[:disk_w]}\e[0m"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
.map{ |e| "| #{e} |" }.join }.map{ |e| e.gsub(/\|\|/, "|") }
|
|
64
|
+
.tap{ |rows| break (0..7).to_a.map{ |i| " #{i+1} " << rows[i] } }
|
|
65
|
+
.join("\n #{"+---"*8}+\n") <<
|
|
66
|
+
"\n #{"+---"*8}+\n"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Pushes an array of the game board onto a stack.
|
|
70
|
+
# The stack size limit is 3(default).
|
|
71
|
+
def push_stack
|
|
72
|
+
@stack.push(Marshal.load(Marshal.dump(@columns)))
|
|
73
|
+
@stack.shift if @stack.size > @options[:stack_limit]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Pops an array of the game board off of the stack,
|
|
77
|
+
# and that is stored in the instance variable.(`@columns`)
|
|
78
|
+
#
|
|
79
|
+
# @param num [Integer] Be popped from the stack by the supplied number of times.
|
|
80
|
+
def undo!(num = 1)
|
|
81
|
+
num.times{ @columns = @stack.pop }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns a hash containing the coordinates of each color.
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash{Symbol => Array<Symbol, Integer>}]
|
|
87
|
+
def status
|
|
88
|
+
Hash[*[:none, :black, :white].map do |key|
|
|
89
|
+
[key, COORDINATES.map{ |x, y| [x, y] if key == at(x, y) }.compact]
|
|
90
|
+
end.flatten(1)]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the openness of the coordinates.
|
|
94
|
+
#
|
|
95
|
+
# @param x [Symbol, Integer] the column number
|
|
96
|
+
# @param y [Integer] the row number
|
|
97
|
+
# @return [Integer] the openness
|
|
98
|
+
def openness(x, y)
|
|
99
|
+
x = (:a..:h).to_a.index(x) + 1 if x.is_a? Symbol
|
|
100
|
+
([-1,0,1].product([-1,0,1]) - [[0, 0]]).inject(0) do |sum, (dx, dy)|
|
|
101
|
+
sum + (@columns[x + dx][y + dy] == 0 ? 1 : 0)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the color of supplied coordinates.
|
|
106
|
+
#
|
|
107
|
+
# @param x [Symbol, Integer] the column number
|
|
108
|
+
# @param y [Integer] the row number
|
|
109
|
+
# @return [Symbol] the color or `:none`
|
|
110
|
+
def at(x, y)
|
|
111
|
+
x = (:a..:h).to_a.index(x) + 1 if x.is_a? Symbol
|
|
112
|
+
DISK.key(@columns[x][y])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Counts the number of the supplied color's disks.
|
|
116
|
+
#
|
|
117
|
+
# @param color [Symbol]
|
|
118
|
+
# @return [Integer] the sum of the counted disks
|
|
119
|
+
def count_disks(color)
|
|
120
|
+
@columns.flatten.inject(0) do |sum, e|
|
|
121
|
+
sum + (e == DISK[color] ? 1 : 0)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns an array of the next moves.
|
|
126
|
+
#
|
|
127
|
+
# @param color [Symbol]
|
|
128
|
+
# @return [Array<Array<Symbol, Integer>>]
|
|
129
|
+
def next_moves(color)
|
|
130
|
+
@columns[1..8].map{ |col| col[1..-2] }
|
|
131
|
+
.flatten.each_with_index.inject([]) do |list, (_, i)|
|
|
132
|
+
list << (puttable?(*COORDINATES[i], color) ? COORDINATES[i] : nil)
|
|
133
|
+
end.compact
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Places a supplied color's disk on specified position.
|
|
137
|
+
#
|
|
138
|
+
# @param x [Symbol, Integer] the column number
|
|
139
|
+
# @param y [Integer] the row number
|
|
140
|
+
# @param color [Symbol]
|
|
141
|
+
def put_disk(x, y, color)
|
|
142
|
+
x = (:a..:h).to_a.index(x) + 1 if x.is_a? Symbol
|
|
143
|
+
@columns[x][y] = DISK[color.to_sym]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Flips the opponent's disks between a new disk and another disk of my color.
|
|
147
|
+
# the invalid move has no effect.
|
|
148
|
+
#
|
|
149
|
+
# @param x [Symbol, Integer] the column number
|
|
150
|
+
# @param y [Integer] the row number
|
|
151
|
+
# @param color [Symbol]
|
|
152
|
+
def flip_disks(x, y, color)
|
|
153
|
+
x = (:a..:h).to_a.index(x) + 1 if x.is_a? Symbol
|
|
154
|
+
[-1,0,1].product([-1,0,1]).each do |dx, dy|
|
|
155
|
+
next if dx == 0 && dy == 0
|
|
156
|
+
# 隣接石が異色であったらflippable?でひっくり返せるか(挟まれているか)確認
|
|
157
|
+
if @columns[x + dx][y + dy] == DISK[color]*(-1)
|
|
158
|
+
flip_disk(x, y, dx, dy, color) if flippable?(x, y, dx, dy, color)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# Flips the opponent's disks on one of these straight lines
|
|
166
|
+
# between a new disk and another disk of my color.
|
|
167
|
+
#
|
|
168
|
+
# @param x [Symbol, Integer] the column number
|
|
169
|
+
# @param y [Integer] the row number
|
|
170
|
+
# @param dx [Integer] a horizontal difference
|
|
171
|
+
# @param dy [Integer] a verticaldistance
|
|
172
|
+
# @param color [Symbol]
|
|
173
|
+
def flip_disk(x, y, dx, dy, color)
|
|
174
|
+
return if [DISK[:wall], DISK[:none], DISK[color]].include?(@columns[x+dx][y+dy])
|
|
175
|
+
@columns[x+dx][y+dy] = DISK[color]
|
|
176
|
+
flip_disk(x+dx, y+dy, dx, dy, color)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Whether or not a player can place a new disk on specified position.
|
|
180
|
+
# Returns true if the move is valid.
|
|
181
|
+
#
|
|
182
|
+
# @param x [Symbol, Integer] the column number
|
|
183
|
+
# @param y [Integer] the row number
|
|
184
|
+
# @param color [Symbol]
|
|
185
|
+
# @return [Boolean]
|
|
186
|
+
def puttable?(x, y, color)
|
|
187
|
+
x = (:a..:h).to_a.index(x) + 1 if x.is_a? Symbol
|
|
188
|
+
return false if @columns[x][y] != 0
|
|
189
|
+
[-1,0,1].product([-1,0,1]).each do |dx, dy|
|
|
190
|
+
next if dx == 0 && dy == 0
|
|
191
|
+
if @columns[x + dx][y + dy] == DISK[color]*(-1)
|
|
192
|
+
return true if flippable?(x, y, dx, dy, color)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Whether or not a player can flip the opponent's disks.
|
|
199
|
+
#
|
|
200
|
+
# @param x [Symbol, Integer] the column number
|
|
201
|
+
# @param y [Integer] the row number
|
|
202
|
+
# @param dx [Integer] a horizontal difference
|
|
203
|
+
# @param dy [Integer] a verticaldistance
|
|
204
|
+
# @param color [Symbol]
|
|
205
|
+
# @return [Boolean]
|
|
206
|
+
def flippable?(x, y, dx, dy, color)
|
|
207
|
+
loop do
|
|
208
|
+
x += dx; y += dy
|
|
209
|
+
return true if @columns[x][y] == DISK[color]
|
|
210
|
+
break if [DISK[:wall] ,DISK[:none]].include?(@columns[x][y])
|
|
211
|
+
end
|
|
212
|
+
false
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Reversi
|
|
2
|
+
module Configuration
|
|
3
|
+
|
|
4
|
+
OPTIONS_KEYS = [
|
|
5
|
+
:player_b,
|
|
6
|
+
:player_w,
|
|
7
|
+
:disk_b,
|
|
8
|
+
:disk_w,
|
|
9
|
+
:disk_color_b,
|
|
10
|
+
:disk_color_w,
|
|
11
|
+
:progress,
|
|
12
|
+
:stack_limit
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
attr_accessor *OPTIONS_KEYS
|
|
16
|
+
|
|
17
|
+
def configure
|
|
18
|
+
yield self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def options
|
|
22
|
+
Hash[*OPTIONS_KEYS.map{|key| [key, send(key)]}.flatten]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_defaults
|
|
26
|
+
self.player_b ||= Reversi::Player::RandomAI
|
|
27
|
+
self.player_w ||= Reversi::Player::RandomAI
|
|
28
|
+
self.disk_b ||= "b"
|
|
29
|
+
self.disk_w ||= "w"
|
|
30
|
+
self.disk_color_b ||= 0
|
|
31
|
+
self.disk_color_w ||= 0
|
|
32
|
+
self.progress ||= false
|
|
33
|
+
self.stack_limit ||= 3
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/reversi/game.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Reversi
|
|
2
|
+
class Game
|
|
3
|
+
attr_accessor *Configuration::OPTIONS_KEYS
|
|
4
|
+
attr_reader :options, :board, :vs_human, :status
|
|
5
|
+
|
|
6
|
+
# Initializes a new Board object.
|
|
7
|
+
def initialize(options = {})
|
|
8
|
+
reset(options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Set the option values and start a game.
|
|
12
|
+
#
|
|
13
|
+
# @see #run
|
|
14
|
+
def start(options = {})
|
|
15
|
+
reset(options)
|
|
16
|
+
mode = {:progress => @progress, :vs_human => @vs_human}
|
|
17
|
+
run(mode)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Reset the instance variable, `@board`.
|
|
23
|
+
def reset(options = {})
|
|
24
|
+
load_config(options)
|
|
25
|
+
@board = Board.new(@options)
|
|
26
|
+
@player_class_b = @player_b
|
|
27
|
+
@player_class_w = @player_w
|
|
28
|
+
@player_b = @player_class_b.new(:black, @board)
|
|
29
|
+
@player_w = @player_class_w.new(:white, @board)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Load the configuration.
|
|
33
|
+
def load_config(options = {})
|
|
34
|
+
Reversi.set_defaults
|
|
35
|
+
@options = Reversi.options.merge(options)
|
|
36
|
+
Configuration::OPTIONS_KEYS.each do |key|
|
|
37
|
+
send("#{key}=".to_sym, @options[key])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if [@player_b, @player_w].include? Reversi::Player::Human
|
|
41
|
+
@vs_human = true
|
|
42
|
+
@progress = false
|
|
43
|
+
end
|
|
44
|
+
@vs_human ||= false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Execute a game and run until the end of the game.
|
|
48
|
+
#
|
|
49
|
+
# @option mode [Boolean] :progress Display the progress of the game.
|
|
50
|
+
# @option mode [Boolean] :vs_human
|
|
51
|
+
def run(mode = {})
|
|
52
|
+
show_board if mode[:progress]
|
|
53
|
+
loop do
|
|
54
|
+
break if game_over?
|
|
55
|
+
@status = @board.status
|
|
56
|
+
@player_b.move(@board); check_move(:black)
|
|
57
|
+
show_board if mode[:progress]
|
|
58
|
+
|
|
59
|
+
@status = @board.status
|
|
60
|
+
@player_w.move(@board); check_move(:white)
|
|
61
|
+
show_board if mode[:progress]
|
|
62
|
+
end
|
|
63
|
+
puts @board.to_s if mode[:progress] || mode[:vs_human]
|
|
64
|
+
rescue Interrupt
|
|
65
|
+
exit 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Show the current state of this game board.
|
|
69
|
+
def show_board
|
|
70
|
+
puts @board.to_s
|
|
71
|
+
printf "\e[#{18}A"; STDOUT.flush; sleep 0.1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Checks a move to make sure it is valid.
|
|
75
|
+
def check_move(color)
|
|
76
|
+
blank_diff = @status[:none].size - @board.count_disks(:none)
|
|
77
|
+
|
|
78
|
+
case blank_diff
|
|
79
|
+
when 1
|
|
80
|
+
if (@board.count_disks(color) - @status[color].size) < 2
|
|
81
|
+
raise MoveError, "A player must flip at least one or more opponent's disks."
|
|
82
|
+
end
|
|
83
|
+
when 0
|
|
84
|
+
unless (@board.count_disks(:black) - @status[:black].size) == 0 &&
|
|
85
|
+
(@board.count_disks(:white) - @status[:white].size) == 0
|
|
86
|
+
raise MoveError, "When a player can't make a valid move, you must not place a new disk."
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
raise MoveError, "A player must place a new disk on the board."
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether or not this game is over.
|
|
94
|
+
# Both players can't find a next move, that's the end of the game.
|
|
95
|
+
def game_over?
|
|
96
|
+
@player_w.next_moves.empty? && @player_b.next_moves.empty?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Reversi::Player
|
|
2
|
+
class BasePlayer
|
|
3
|
+
attr_reader :my_color, :opponent_color, :board
|
|
4
|
+
|
|
5
|
+
# Initializes a new BasePlayer object.
|
|
6
|
+
def initialize(color, board)
|
|
7
|
+
@my_color = color
|
|
8
|
+
@opponent_color = (@my_color == :white ? :black : :white)
|
|
9
|
+
@board = board
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Override this method in your original subclass.
|
|
13
|
+
def move(board)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Places a supplied color's disk on specified position,
|
|
17
|
+
# and flips the opponent's disks.
|
|
18
|
+
#
|
|
19
|
+
# @param x [Symbol, Integer] the column number
|
|
20
|
+
# @param y [Integer] the row number
|
|
21
|
+
# @param my_color [Boolean] my color or opponent's color
|
|
22
|
+
def put_disk(x, y, my_color = true)
|
|
23
|
+
@board.push_stack
|
|
24
|
+
color = my_color ? @my_color : @opponent_color
|
|
25
|
+
x = (:a..:h).to_a.index(x) + 1 if x.is_a? Symbol
|
|
26
|
+
diff = flip_disks(x, y, color)
|
|
27
|
+
if diff.empty?
|
|
28
|
+
raise Reversi::MoveError, "A player must flip at least one or more opponent's disks."
|
|
29
|
+
end
|
|
30
|
+
@board.put_disk(x, y, color)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns an array of the next moves.
|
|
34
|
+
#
|
|
35
|
+
# @param my_color [Boolean] my color or opponent's color
|
|
36
|
+
# @return [Hash] the next moves
|
|
37
|
+
def next_moves(my_color = true)
|
|
38
|
+
color = my_color ? @my_color : @opponent_color
|
|
39
|
+
@board.next_moves(color).map do |move|
|
|
40
|
+
diff = flip_disks(*move, color)
|
|
41
|
+
openness = diff.inject(0){ |sum, (x, y)| sum + @board.openness(x, y) }
|
|
42
|
+
# undo the flipped disks
|
|
43
|
+
diff.each{ |x, y| @board.put_disk(x, y, my_color ? @opponent_color : @my_color) }
|
|
44
|
+
{:move => move, :openness => openness, :result => diff}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns a number of the supplied color's disks.
|
|
49
|
+
#
|
|
50
|
+
# @param my_color [Boolean] my color or opponent's color
|
|
51
|
+
# @return [Integer] a number of the supplied color's disks
|
|
52
|
+
def count_disks(my_color = true)
|
|
53
|
+
@board.count_disks(my_color ? @my_color : @opponent_color)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns a hash containing the coordinates of each color.
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash{Symbol => Array<Symbol, Integer>}]
|
|
59
|
+
def status
|
|
60
|
+
convert = {
|
|
61
|
+
:black => @my_color == :black ? :mine : :opponent,
|
|
62
|
+
:white => @my_color == :white ? :mine : :opponent,
|
|
63
|
+
:none => :none }
|
|
64
|
+
Hash[*@board.status.map{ |k, v| [convert[k], v] }.flatten(1)]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def flip_disks(x, y, color)
|
|
70
|
+
before = @board.status[color]
|
|
71
|
+
@board.flip_disks(x, y, color)
|
|
72
|
+
after = @board.status[color]
|
|
73
|
+
after - before
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Reversi::Player
|
|
2
|
+
class Human < BasePlayer
|
|
3
|
+
|
|
4
|
+
def move(board)
|
|
5
|
+
return if next_moves.empty?
|
|
6
|
+
puts board.to_s
|
|
7
|
+
input_move
|
|
8
|
+
printf "\e[#{18}A"; STDOUT.flush
|
|
9
|
+
puts board.to_s; sleep 0.5
|
|
10
|
+
printf "\e[#{18}A"; STDOUT.flush
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def input_move
|
|
16
|
+
loop do
|
|
17
|
+
print "#{@my_color}: "
|
|
18
|
+
@input_move = gets.chomp.split("")
|
|
19
|
+
exit if [['q'], ['e','x','i','t']].include? @input_move
|
|
20
|
+
redo if check_size == :redo
|
|
21
|
+
@input_move[0] = @input_move[0].to_sym
|
|
22
|
+
@input_move[1] = @input_move[1].to_i
|
|
23
|
+
printf "\e[#{1}A"; STDOUT.flush
|
|
24
|
+
print "#{" "*9}"
|
|
25
|
+
printf "\e[#{9}D"; STDOUT.flush
|
|
26
|
+
redo if check_valid == :redo
|
|
27
|
+
put_disk(*@input_move)
|
|
28
|
+
break
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check_size
|
|
33
|
+
unless @input_move.size == 2
|
|
34
|
+
printf "\e[#{1}A"; STDOUT.flush
|
|
35
|
+
print "#{" "*(@input_move.join.size + 7)}"
|
|
36
|
+
printf "\e[#{@input_move.join.size + 7}D"; STDOUT.flush
|
|
37
|
+
return :redo
|
|
38
|
+
end
|
|
39
|
+
:valid
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def check_valid
|
|
43
|
+
unless next_moves.map{ |v| v[:move] }.include?(@input_move)
|
|
44
|
+
return :redo
|
|
45
|
+
end
|
|
46
|
+
:valid
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Reversi::Player
|
|
2
|
+
class NegamaxAI < Reversi::Player::BasePlayer
|
|
3
|
+
|
|
4
|
+
POINT = {
|
|
5
|
+
[:a, 1] => 100, [:b, 1] => -10, [:c, 1] => 0, [:d, 1] => -1, [:e, 1] => -1, [:f, 1] => 0, [:g, 1] => -10, [:h, 1] => 100,
|
|
6
|
+
[:a, 2] => -10, [:b, 2] => -30, [:c, 2] => -5, [:d, 2] => -5, [:e, 2] => -5, [:f, 2] => -5, [:g, 2] => -30, [:h, 2] => -10,
|
|
7
|
+
[:a, 3] => 0, [:b, 3] => -5, [:c, 3] => 0, [:d, 3] => -1, [:e, 3] => -1, [:f, 3] => 0, [:g, 3] => -5, [:h, 3] => 0,
|
|
8
|
+
[:a, 4] => -1, [:b, 4] => -5, [:c, 4] => -1, [:d, 4] => -1, [:e, 4] => -1, [:f, 4] => -1, [:g, 4] => -5, [:h, 4] => -1,
|
|
9
|
+
[:a, 5] => -1, [:b, 5] => -5, [:c, 5] => -1, [:d, 5] => -1, [:e, 5] => -1, [:f, 5] => -1, [:g, 5] => -5, [:h, 5] => -1,
|
|
10
|
+
[:a, 6] => 0, [:b, 6] => -5, [:c, 6] => 0, [:d, 6] => -1, [:e, 6] => -1, [:f, 6] => 0, [:g, 6] => -5, [:h, 6] => 0,
|
|
11
|
+
[:a, 7] => -10, [:b, 7] => -30, [:c, 7] => -5, [:d, 7] => -5, [:e, 7] => -5, [:f, 7] => -5, [:g, 7] => -30, [:h, 7] => -10,
|
|
12
|
+
[:a, 8] => 100, [:b, 8] => -10, [:c, 8] => 0, [:d, 8] => -1, [:e, 8] => -1, [:f, 8] => 0, [:g, 8] => -12, [:h, 8] => 100
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def move(board)
|
|
16
|
+
moves = next_moves.map{ |v| v[:move] }
|
|
17
|
+
return if moves.empty?
|
|
18
|
+
|
|
19
|
+
next_move = moves.map do |move|
|
|
20
|
+
{ :move => move, :point => evaluate(move, board, 1, true) }
|
|
21
|
+
end
|
|
22
|
+
.max_by{ |v| v[:point] }[:move]
|
|
23
|
+
put_disk(*next_move)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def evaluate(move, board, depth, color)
|
|
27
|
+
put_disk(*move, color)
|
|
28
|
+
moves = next_moves(!color).map{ |v| v[:move] }
|
|
29
|
+
|
|
30
|
+
if depth == 3
|
|
31
|
+
status[:mine].inject(0){ |sum, xy| sum + POINT[xy] }
|
|
32
|
+
elsif moves.empty?
|
|
33
|
+
-100
|
|
34
|
+
else
|
|
35
|
+
-( moves.map{ |move| evaluate(move, board, depth + 1, !color) }.max )
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ensure
|
|
39
|
+
board.undo!
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/reversi/version.rb
CHANGED
data/lib/reversi.rb
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
require "reversi/version"
|
|
2
|
+
require "reversi/player"
|
|
3
|
+
require "reversi/player/base_player"
|
|
4
|
+
require "reversi/board"
|
|
5
|
+
require "reversi/configuration"
|
|
6
|
+
require "reversi/game"
|
|
2
7
|
|
|
3
8
|
module Reversi
|
|
4
|
-
|
|
9
|
+
extend Configuration
|
|
10
|
+
|
|
11
|
+
class MoveError < StandardError; end
|
|
5
12
|
end
|
data/reversi.gemspec
CHANGED
|
@@ -7,17 +7,21 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.name = "reversi"
|
|
8
8
|
spec.version = Reversi::VERSION
|
|
9
9
|
spec.authors = ["seinosuke"]
|
|
10
|
-
spec.
|
|
11
|
-
spec.
|
|
12
|
-
spec.description = %q{A reversi game program.}
|
|
13
|
-
spec.homepage = ""
|
|
10
|
+
spec.summary = %q{A Ruby Gem to play reversi game.}
|
|
11
|
+
spec.homepage = "https://github.com/seinosuke/reversi"
|
|
14
12
|
spec.license = "MIT"
|
|
15
13
|
|
|
14
|
+
spec.required_ruby_version = '>= 1.9.3'
|
|
15
|
+
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
19
|
spec.require_paths = ["lib"]
|
|
20
20
|
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.7"
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
spec.description = <<-END
|
|
24
|
+
A Ruby Gem to play reversi game. You can enjoy a game on the command line
|
|
25
|
+
or easily make your original reversi game programs.
|
|
26
|
+
END
|
|
23
27
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Reversi::Player::BasePlayer do
|
|
4
|
+
let(:game) { Reversi::Game.new }
|
|
5
|
+
let(:player) { game.player_b }
|
|
6
|
+
let(:board) { game.board }
|
|
7
|
+
|
|
8
|
+
describe "#put_disk" do
|
|
9
|
+
context "when a player make a valid move" do
|
|
10
|
+
it "flips the opponent's disks between a new disk and my disk" do
|
|
11
|
+
expect{ player.put_disk(:d, 3) }.to change{ board.status[:black].size }.by(2)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
context "when a player make an invalid move" do
|
|
16
|
+
it "Reversi::MoveError raised" do
|
|
17
|
+
expect{ player.put_disk(:a, 1) }.to raise_error Reversi::MoveError
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context "when the third argument is `false`" do
|
|
22
|
+
it "makes a opponent's move" do
|
|
23
|
+
expect{ player.put_disk(:e, 3, false) }.to change{ board.status[:white].size }.by(2)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#next_moves" do
|
|
29
|
+
context "when the first argument is omitted" do
|
|
30
|
+
it do
|
|
31
|
+
ans = [[:c, 4], [:d, 3], [:e, 6], [:f, 5]]
|
|
32
|
+
expect(player.next_moves.map{ |move| move[:move] }).to eq ans
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it do
|
|
36
|
+
player.put_disk(:d, 3)
|
|
37
|
+
player.put_disk(:e, 3, false)
|
|
38
|
+
expect(player.next_moves[1][:openness]).to eq 8
|
|
39
|
+
expect(player.next_moves[1][:result]).to eq [[:e, 3], [:e, 4]]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "when the first argument is `false`" do
|
|
44
|
+
it do
|
|
45
|
+
ans = [[:c, 5], [:d, 6], [:e, 3], [:f, 4]]
|
|
46
|
+
expect(player.next_moves(false).map{ |move| move[:move] }).to eq ans
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it do
|
|
50
|
+
player.put_disk(:d, 3)
|
|
51
|
+
player.put_disk(:e, 3, false)
|
|
52
|
+
expect(player.next_moves(false)[0][:openness]).to eq 5
|
|
53
|
+
expect(player.next_moves(false)[0][:result]).to eq [[:d, 3]]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "#count_disks" do
|
|
59
|
+
context "when the first argument is omitted" do
|
|
60
|
+
it do
|
|
61
|
+
board.put_disk(:a, 1, :black)
|
|
62
|
+
expect(player.count_disks).to eq 3
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context "when the first argument is `false`" do
|
|
67
|
+
it do
|
|
68
|
+
board.put_disk(:a, 1, :black)
|
|
69
|
+
expect(player.count_disks(false)).to eq 2
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "#status" do
|
|
75
|
+
it { expect(player.status[:mine]).to eq [[:d, 5], [:e, 4]] }
|
|
76
|
+
it { expect(player.status[:opponent]).to eq [[:d, 4], [:e, 5]] }
|
|
77
|
+
end
|
|
78
|
+
end
|
data/spec/board_spec.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Reversi::Board do
|
|
4
|
+
let(:options) do
|
|
5
|
+
{:player_b => Reversi::Player::RandomAI,
|
|
6
|
+
:player_w => Reversi::Player::RandomAI,
|
|
7
|
+
:disk_b => "b",
|
|
8
|
+
:disk_w => "w",
|
|
9
|
+
:disk_color_b => disk_color_b,
|
|
10
|
+
:disk_color_w => disk_color_w,
|
|
11
|
+
:progress => false,
|
|
12
|
+
:stack_limit => 3}
|
|
13
|
+
end
|
|
14
|
+
let(:board) { Reversi::Board.new(options) }
|
|
15
|
+
|
|
16
|
+
describe "sets each disk color" do
|
|
17
|
+
context "when a color name is valid" do
|
|
18
|
+
let(:disk_color_b) { "cyan" }
|
|
19
|
+
let(:disk_color_w) { :red }
|
|
20
|
+
it "sets the valid color value" do
|
|
21
|
+
expect(board.options[:disk_color_b]).to eq 36
|
|
22
|
+
expect(board.options[:disk_color_w]).to eq 31
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context "when a color name is invalid" do
|
|
27
|
+
let(:disk_color_b) { "hoge" }
|
|
28
|
+
let(:disk_color_w) { :hoge }
|
|
29
|
+
it "sets 0" do
|
|
30
|
+
expect(board.options[:disk_color_b]).to eq 0
|
|
31
|
+
expect(board.options[:disk_color_w]).to eq 0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "#push_stack" do
|
|
37
|
+
let(:disk_color_b) { 0 }
|
|
38
|
+
let(:disk_color_w) { 0 }
|
|
39
|
+
|
|
40
|
+
it "the stack size limit is 3(default)" do
|
|
41
|
+
4.times { board.push_stack }
|
|
42
|
+
expect(board.stack.size).to eq 3
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "the deep copy operation is used" do
|
|
46
|
+
board.push_stack
|
|
47
|
+
board.put_disk(:a, 1, :black)
|
|
48
|
+
board.push_stack
|
|
49
|
+
expect(board.stack[0]).not_to eq board.stack[1]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "#undo!" do
|
|
54
|
+
let(:disk_color_b) { 0 }
|
|
55
|
+
let(:disk_color_w) { 0 }
|
|
56
|
+
|
|
57
|
+
context "when the first argument is omitted" do
|
|
58
|
+
it "the default value is 1" do
|
|
59
|
+
3.times { board.push_stack }
|
|
60
|
+
expect{ board.undo! }.to change{ board.stack.size }.by(-1)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context "when the first argument is supplied" do
|
|
65
|
+
it "the number is used" do
|
|
66
|
+
3.times { board.push_stack }
|
|
67
|
+
expect{ board.undo! 2 }.to change{ board.stack.size }.by(-2)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "#status" do
|
|
73
|
+
let(:disk_color_b) { 0 }
|
|
74
|
+
let(:disk_color_w) { 0 }
|
|
75
|
+
it "returns a hash containing the coordinates of each color" do
|
|
76
|
+
expect{ board.put_disk(:a, 1, :black) }.to change{ board.status[:black].size }.by(1)
|
|
77
|
+
expect{ board.put_disk(:b, 1, :black) }.to change{ board.status[:none].size }.by(-1)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "#openness" do
|
|
82
|
+
let(:disk_color_b) { 0 }
|
|
83
|
+
let(:disk_color_w) { 0 }
|
|
84
|
+
it "returns the openness of the coordinates" do
|
|
85
|
+
expect(board.openness(:a, 1)).to eq 3
|
|
86
|
+
expect(board.openness(:b, 2)).to eq 8
|
|
87
|
+
expect(board.openness(:d, 4)).to eq 5
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe "#at" do
|
|
92
|
+
let(:disk_color_b) { 0 }
|
|
93
|
+
let(:disk_color_w) { 0 }
|
|
94
|
+
|
|
95
|
+
context "when the first argument is a number" do
|
|
96
|
+
it do
|
|
97
|
+
board.put_disk(:c, 3, :white)
|
|
98
|
+
expect(board.at(3, 3)).to eq :white
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context "when the first argument is a symbol" do
|
|
103
|
+
it do
|
|
104
|
+
board.put_disk(7, 8, :black)
|
|
105
|
+
expect(board.at(:g, 8)).to eq :black
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "#count_disks" do
|
|
111
|
+
let(:disk_color_b) { 0 }
|
|
112
|
+
let(:disk_color_w) { 0 }
|
|
113
|
+
it { expect(board.count_disks(:black)).to eq 2 }
|
|
114
|
+
it { expect(board.count_disks(:white)).to eq 2 }
|
|
115
|
+
it { expect(board.count_disks(:none)).to eq 60 }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "#next_moves" do
|
|
119
|
+
let(:disk_color_b) { 0 }
|
|
120
|
+
let(:disk_color_w) { 0 }
|
|
121
|
+
it { expect(board.next_moves(:black)).to eq [[:c, 4], [:d, 3], [:e, 6], [:f, 5]] }
|
|
122
|
+
it { expect(board.next_moves(:white)).to eq [[:c, 5], [:d, 6], [:e, 3], [:f, 4]] }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "#put_disk, #flip_disks" do
|
|
126
|
+
let(:disk_color_b) { 0 }
|
|
127
|
+
let(:disk_color_w) { 0 }
|
|
128
|
+
it "flips the opponent's disks between a new disk and another disk of my color" do
|
|
129
|
+
board.put_disk(:d, 6, :white)
|
|
130
|
+
board.put_disk(:e, 3, :white)
|
|
131
|
+
board.put_disk(:f, 3, :black)
|
|
132
|
+
board.put_disk(:d, 3, :black)
|
|
133
|
+
expect{ board.flip_disks(:d, 3, :black) }.to change{ board.status[:black].size }.by(2)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/spec/game_spec.rb
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Reversi::Game do
|
|
4
|
+
describe "next moves" do
|
|
5
|
+
before do
|
|
6
|
+
@game = Reversi::Game.new
|
|
7
|
+
@game.board.put_disk(6, 3, :black)
|
|
8
|
+
@game.board.put_disk(6, 5, :black)
|
|
9
|
+
@game.board.put_disk(6, 6, :black)
|
|
10
|
+
@game.board.put_disk(6, 7, :white)
|
|
11
|
+
@game.board.put_disk(7, 4, :black)
|
|
12
|
+
@game.board.put_disk(8, 4, :black)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
context "before `player_w` places a piece on position [f4]" do
|
|
16
|
+
it do
|
|
17
|
+
ans = [[:c, 5], [:d, 6], [:e, 3], [:f, 4], [:g, 5], [:g, 7]]
|
|
18
|
+
expect(@game.player_w.next_moves.map{ |move| move[:move] }).to eq ans
|
|
19
|
+
end
|
|
20
|
+
it { expect(@game.player_w.count_disks).to eq 3 }
|
|
21
|
+
it { expect(@game.player_b.count_disks).to eq 7 }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context "after `player_w` places a piece on position [f4]" do
|
|
25
|
+
before do
|
|
26
|
+
@game.player_w.put_disk(:f, 4)
|
|
27
|
+
end
|
|
28
|
+
it do
|
|
29
|
+
ans = [[:c, 5], [:c, 6], [:d, 6], [:f, 2], [:g, 2], [:h, 3]]
|
|
30
|
+
expect(@game.player_w.next_moves.map{ |move| move[:move] }).to eq ans
|
|
31
|
+
end
|
|
32
|
+
it { expect(@game.player_w.count_disks).to eq 7 }
|
|
33
|
+
it { expect(@game.player_b.count_disks).to eq 4 }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe "from a record of a reversi game" do
|
|
38
|
+
game = Reversi::Game.new
|
|
39
|
+
game.player_b.put_disk(:c, 4); game.player_w.put_disk(:e, 3)
|
|
40
|
+
game.player_b.put_disk(:f, 6); game.player_w.put_disk(:e, 6)
|
|
41
|
+
game.player_b.put_disk(:f, 5); game.player_w.put_disk(:c, 5)
|
|
42
|
+
game.player_b.put_disk(:f, 4); game.player_w.put_disk(:g, 6)
|
|
43
|
+
game.player_b.put_disk(:f, 7); game.player_w.put_disk(:d, 3)
|
|
44
|
+
game.player_b.put_disk(:f, 3); game.player_w.put_disk(:g, 5)
|
|
45
|
+
game.player_b.put_disk(:g, 4); game.player_w.put_disk(:e, 7)
|
|
46
|
+
game.player_b.put_disk(:d, 6); game.player_w.put_disk(:h, 3)
|
|
47
|
+
game.player_b.put_disk(:f, 8); game.player_w.put_disk(:g, 3)
|
|
48
|
+
game.player_b.put_disk(:c, 6); game.player_w.put_disk(:c, 3)
|
|
49
|
+
game.player_b.put_disk(:c, 2); game.player_w.put_disk(:d, 7)
|
|
50
|
+
game.player_b.put_disk(:e, 8); game.player_w.put_disk(:c, 8)
|
|
51
|
+
game.player_b.put_disk(:h, 4); game.player_w.put_disk(:h, 5)
|
|
52
|
+
game.player_b.put_disk(:d, 2); game.player_w.put_disk(:d, 8)
|
|
53
|
+
game.player_b.put_disk(:b, 8); game.player_w.put_disk(:b, 3)
|
|
54
|
+
game.player_b.put_disk(:c, 7); game.player_w.put_disk(:e, 2)
|
|
55
|
+
game.player_b.put_disk(:a, 4); game.player_w.put_disk(:d, 1)
|
|
56
|
+
game.player_b.put_disk(:c, 1); game.player_w.put_disk(:f, 2)
|
|
57
|
+
game.player_b.put_disk(:e, 1); game.player_w.put_disk(:f, 1)
|
|
58
|
+
game.player_b.put_disk(:g, 1); game.player_w.put_disk(:a, 3)
|
|
59
|
+
game.player_b.put_disk(:b, 5); game.player_w.put_disk(:b, 4)
|
|
60
|
+
game.player_b.put_disk(:a, 5); game.player_w.put_disk(:a, 6)
|
|
61
|
+
game.player_b.put_disk(:b, 6); game.player_w.put_disk(:a, 7)
|
|
62
|
+
game.player_b.put_disk(:g, 2); game.player_w.put_disk(:h, 1)
|
|
63
|
+
game.player_b.put_disk(:b, 2); game.player_w.put_disk(:b, 7)
|
|
64
|
+
game.player_b.put_disk(:h, 2); game.player_w.put_disk(:g, 7)
|
|
65
|
+
game.player_b.put_disk(:a, 8); game.player_w.put_disk(:a, 2)
|
|
66
|
+
game.player_b.put_disk(:h, 7); game.player_w.put_disk(:h, 6)
|
|
67
|
+
game.player_b.put_disk(:a, 1); game.player_w.put_disk(:b, 1)
|
|
68
|
+
game.player_w.put_disk(:g, 8)
|
|
69
|
+
game.player_b.put_disk(:h, 8)
|
|
70
|
+
ans = [
|
|
71
|
+
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
|
|
72
|
+
[2, -1, -1, -1, -1, -1, -1, -1, -1, 2],
|
|
73
|
+
[2, 1, 1, 1, -1, -1, 1, -1, -1, 2],
|
|
74
|
+
[2, 1, 1, -1, 1, 1, -1, -1, -1, 2],
|
|
75
|
+
[2, 1, -1, 1, 1, -1, -1, -1, -1, 2],
|
|
76
|
+
[2, 1, -1, -1, 1, -1, 1, -1, -1, 2],
|
|
77
|
+
[2, 1, -1, -1, -1, 1, -1, 1, -1, 2],
|
|
78
|
+
[2, 1, -1, -1, 1, 1, 1, -1, -1, 2],
|
|
79
|
+
[2, 1, -1, 1, 1, 1, 1, -1, -1, 2],
|
|
80
|
+
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
|
|
81
|
+
]
|
|
82
|
+
it { expect(game.board.columns).to eq ans }
|
|
83
|
+
end
|
|
84
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: reversi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- seinosuke
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2015-
|
|
11
|
+
date: 2015-03-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -24,40 +24,38 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.7'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- - "~>"
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '10.0'
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - "~>"
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '10.0'
|
|
41
|
-
description: A reversi game program.
|
|
42
|
-
email:
|
|
43
|
-
- seinosuke.3606@gmail.com
|
|
27
|
+
description: |
|
|
28
|
+
A Ruby Gem to play reversi game. You can enjoy a game on the command line
|
|
29
|
+
or easily make your original reversi game programs.
|
|
30
|
+
email:
|
|
44
31
|
executables: []
|
|
45
32
|
extensions: []
|
|
46
33
|
extra_rdoc_files: []
|
|
47
34
|
files:
|
|
48
35
|
- ".gitignore"
|
|
49
36
|
- ".rspec"
|
|
50
|
-
- ".travis.yml"
|
|
51
37
|
- Gemfile
|
|
38
|
+
- Guardfile
|
|
52
39
|
- LICENSE.txt
|
|
53
40
|
- README.md
|
|
54
41
|
- Rakefile
|
|
42
|
+
- images/reversi.gif
|
|
55
43
|
- lib/reversi.rb
|
|
44
|
+
- lib/reversi/board.rb
|
|
45
|
+
- lib/reversi/configuration.rb
|
|
46
|
+
- lib/reversi/game.rb
|
|
47
|
+
- lib/reversi/player.rb
|
|
48
|
+
- lib/reversi/player/base_player.rb
|
|
49
|
+
- lib/reversi/player/human.rb
|
|
50
|
+
- lib/reversi/player/negamax_ai.rb
|
|
51
|
+
- lib/reversi/player/random_ai.rb
|
|
56
52
|
- lib/reversi/version.rb
|
|
57
53
|
- reversi.gemspec
|
|
58
|
-
- spec/
|
|
54
|
+
- spec/base_player_spec.rb
|
|
55
|
+
- spec/board_spec.rb
|
|
56
|
+
- spec/game_spec.rb
|
|
59
57
|
- spec/spec_helper.rb
|
|
60
|
-
homepage:
|
|
58
|
+
homepage: https://github.com/seinosuke/reversi
|
|
61
59
|
licenses:
|
|
62
60
|
- MIT
|
|
63
61
|
metadata: {}
|
|
@@ -69,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
69
67
|
requirements:
|
|
70
68
|
- - ">="
|
|
71
69
|
- !ruby/object:Gem::Version
|
|
72
|
-
version:
|
|
70
|
+
version: 1.9.3
|
|
73
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
72
|
requirements:
|
|
75
73
|
- - ">="
|
|
@@ -77,11 +75,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
75
|
version: '0'
|
|
78
76
|
requirements: []
|
|
79
77
|
rubyforge_project:
|
|
80
|
-
rubygems_version: 2.
|
|
78
|
+
rubygems_version: 2.2.2
|
|
81
79
|
signing_key:
|
|
82
80
|
specification_version: 4
|
|
83
|
-
summary:
|
|
81
|
+
summary: A Ruby Gem to play reversi game.
|
|
84
82
|
test_files:
|
|
85
|
-
- spec/
|
|
83
|
+
- spec/base_player_spec.rb
|
|
84
|
+
- spec/board_spec.rb
|
|
85
|
+
- spec/game_spec.rb
|
|
86
86
|
- spec/spec_helper.rb
|
|
87
87
|
has_rdoc:
|
data/.travis.yml
DELETED