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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b74434187306f90d0e807720becda4d613e203d
4
- data.tar.gz: 24ec4031e6efc0b91abb9376f591351410fa11bc
3
+ metadata.gz: dde2e7d7a4f73ef89fbdc626ff5f06d22e0451fa
4
+ data.tar.gz: d3d2a7f4eea6769ea366c8cd3a1a059238cbf0f4
5
5
  SHA512:
6
- metadata.gz: fab9a5fee9f1208adc01bf6a80e40a2392db4f2114510389cbfc4ec1f0ee1accd700daf47818a89003e2dbf61a77123d7b6c3d13061f08e4e5f6f62759527c0b
7
- data.tar.gz: 2b57d30206e350947c5cb406bca275bed3e4eb897dbf7f801813051a67c37452ae036f199a45ea319e9aabec8ab103be5ddf019d55c9905f43e5c37eca3f21bd
6
+ metadata.gz: 96c2d82cfb35399f3951b62c08a18d024b564a146d588311ce3ef1790d512be2d9d77e1def514be5fd23dae97509a979cfb5308c9b03a2e92d55365a5375fefa
7
+ data.tar.gz: 32d24821205506520d37fec291679fa21ca5e30369bdafa1ed5f7a8772008f9b207189864ce286f9a2dd16b23030b2af11ddafa4d680ec7287bd6580e4536ba2
data/.gitignore CHANGED
@@ -7,6 +7,7 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /test/
10
11
  *.bundle
11
12
  *.so
12
13
  *.o
data/Gemfile CHANGED
@@ -1,4 +1,18 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in reversi.gemspec
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
@@ -0,0 +1,9 @@
1
+ # coding: utf-8
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch('spec/spec_helper.rb') { 'spec' }
6
+
7
+ watch(/lib\/(.+)\.rb/) { 'spec' }
8
+ watch(/lib\/reversi\/(.+)\.rb/) { 'spec' }
9
+ end
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Reversi
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/reversi.svg)](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
+ ![reversi](https://github.com/seinosuke/reversi/blob/master/images/reversi.gif)
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 ( https://github.com/[my-github-username]/reversi/fork )
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
- task :default => :spec
6
+ require 'yard'
7
+ YARD::Rake::YardocTask.new do |t|
8
+ t.files = ['lib/**/*.rb', 'lib/*.rb']
9
+ end
7
10
 
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
@@ -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
@@ -0,0 +1,9 @@
1
+ module Reversi::Player
2
+ class RandomAI < BasePlayer
3
+
4
+ def move(board)
5
+ moves = next_moves.map{ |v| v[:move] }
6
+ put_disk(*moves.sample) unless moves.empty?
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Reversi
2
+ module Player
3
+ autoload :RandomAI, "reversi/player/random_ai"
4
+ autoload :NegamaxAI, "reversi/player/negamax_ai"
5
+ autoload :Human, "reversi/player/human"
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Reversi
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
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
- # Your code goes here...
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.email = ["seinosuke.3606@gmail.com"]
11
- spec.summary = %q{Reversi}
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
- spec.add_development_dependency "rake", "~> 10.0"
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
@@ -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.1
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-02-11 00:00:00.000000000 Z
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
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
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/reversi_spec.rb
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: '0'
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.4.5
78
+ rubygems_version: 2.2.2
81
79
  signing_key:
82
80
  specification_version: 4
83
- summary: Reversi
81
+ summary: A Ruby Gem to play reversi game.
84
82
  test_files:
85
- - spec/reversi_spec.rb
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
@@ -1,3 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.2.0
data/spec/reversi_spec.rb DELETED
@@ -1,11 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Reversi do
4
- it 'has a version number' do
5
- expect(Reversi::VERSION).not_to be nil
6
- end
7
-
8
- it 'does something useful' do
9
- expect(false).to eq(true)
10
- end
11
- end