RubyShogi 0.1

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.
@@ -0,0 +1,188 @@
1
+ ##
2
+ # RubyShogi, a Shogi Engine
3
+ # Author: Toni Helminen
4
+ # License: GPLv3
5
+ ##
6
+
7
+ module RubyShogi
8
+
9
+ class Cmd
10
+ attr_accessor :engine, :random_mode
11
+
12
+ def initialize
13
+ @random_mode = false
14
+ @tokens = RubyShogi::Tokens.new(ARGV)
15
+ @fen = nil
16
+ end
17
+
18
+ def name
19
+ puts "#{RubyShogi::NAME} v#{RubyShogi::VERSION} by #{RubyShogi::AUTHOR}"
20
+ end
21
+
22
+ def randommode
23
+ @random_mode = true
24
+ end
25
+
26
+ def peek_argint(defval)
27
+ val = @tokens.peek(1)
28
+ if val != nil && val.match(/\d+/)
29
+ @tokens.forward
30
+ defval = @tokens.cur.to_i
31
+ end
32
+ defval
33
+ end
34
+
35
+ def mbench
36
+ suite(peek_argint(4))
37
+ end
38
+
39
+ def perft
40
+ p = RubyShogi::Perft.new(@fen)
41
+ p.perft(peek_argint(5))
42
+ end
43
+
44
+ def randperft
45
+ n = peek_argint(3)
46
+ n.times do |i|
47
+ p = RubyShogi::Perft.new
48
+ puts "#{i} / ..."
49
+ p.randperft(2)
50
+ end
51
+ end
52
+
53
+ def suite
54
+ p = RubyShogi::Perft.new
55
+ p.suite(peek_argint(5))
56
+ end
57
+
58
+ def bench
59
+ e = RubyShogi::Engine.new(random_mode: @random_mode)
60
+ e.bench
61
+ end
62
+
63
+ def stats
64
+ n, val = 100, @tokens.peek(1)
65
+ if val != nil && val.match(/\d+/)
66
+ @tokens.forward
67
+ n = val.to_i
68
+ end
69
+ e = RubyShogi::Engine.new("falcon", random_mode: @random_mode)
70
+ e.board.use_fen(@fen) if @fen != nil
71
+ e.stats(n)
72
+ end
73
+
74
+ def tactics
75
+ RubyShogi::Tactics.run
76
+ end
77
+
78
+ def fen
79
+ @tokens.go_next
80
+ @fen = @tokens.cur
81
+ end
82
+
83
+ def list
84
+ board = RubyShogi::Board.new
85
+ board.fen(@fen)
86
+ mgen = board.mgen_generator
87
+ moves = mgen.generate_moves
88
+ moves.each_with_index { |b, i| puts "> #{i}: #{b.move_str}" }
89
+ end
90
+
91
+ def test
92
+ b = RubyShogi::Board.new
93
+ #b.startpos
94
+ #b.fen("lnsgkgsnl/2r4b1/ppppp+Pp1p/7p1/9/9/PPPPP1PPP/1B5R1/LNSGKGSNL[P] w 1 5")
95
+ #b.fen("9/3k5/7+P1/8P/9/9/6P2/R3K4/1NSG1GSNL[PPPPPPPPPNNBRLLSSGGppppppbl] w 19 100")
96
+ # +P8/9/8L/L3k3R/1KP6/9/3s5/2S6/1+n1g4+B[PPPPPPPPPNBRLLSGpppppppnnsgg] b
97
+ # b.fen("5K3/2+P6/6+P2/9/4k4/9/9/9/9[PPPPPPPPPNNBRLLSSGGpppppppnnbrllssgg] w 67 185")
98
+ #b.fen("8+r/5K3/9/9/9/9/k8/9/9[-] w 24 1")
99
+ b.fen("2+P+P+P4/8P/3k3+P1/1+b2P1p2/3p2Pp1/2p5l/+p3+p3p/1p+l+p5/+p4K2+p[NNRSSGGnnbrllssgg] b 1 ")
100
+ b.print_board
101
+ mgen = b.mgen_generator
102
+ mgen.generate_moves
103
+ mgen.print_move_list
104
+ end
105
+
106
+ def print_numbers
107
+ s = ""
108
+ 9.times do |y|
109
+ 9.times do |x|
110
+ i = (8 - y) * 9 + x
111
+ s << "#{i}"
112
+ s << (i < 10 ? " " : " ")
113
+ end
114
+ s << "\n"
115
+ end
116
+ puts "~~~ Table Numbers ~~~"
117
+ puts s
118
+ end
119
+
120
+ def rubybench
121
+ RubyShogi::Bench.go
122
+ end
123
+
124
+ def xboard
125
+ xboard = RubyShogi::Xboard.new(@random_mode)
126
+ xboard.go
127
+ end
128
+
129
+ def profile
130
+ require 'ruby-prof'
131
+ result = RubyProf.profile do
132
+ e = RubyShogi::Engine.new(random_mode: @random_mode)
133
+ e.bench
134
+ end
135
+ printer = RubyProf::FlatPrinter.new(result)
136
+ printer.print(STDOUT)
137
+ end
138
+
139
+ def help
140
+ puts "Usage: ruby shuriken_ruby.rb [OPTION]... [PARAMS]..."
141
+ puts "###"
142
+ puts "-help: This Help"
143
+ puts "-xboard: Enter Xboard Mode"
144
+ puts "-tactics: Run Tactics"
145
+ puts "-name: Print Name Tactics"
146
+ puts "-rubybench: Benchmark Ruby"
147
+ puts "-bench: Benchmark ShurikenShogi Engine"
148
+ puts "-mbench: Benchmark ShurikenShogi Movegen"
149
+ puts "-perft [NUM]: Run Perft"
150
+ puts "-profile: Profile ShurikenShogi"
151
+ puts "-randommode: Activate Random Mode"
152
+ puts "-fen [FEN]: Set Fen"
153
+ puts "-stats [NUM]: Statistical Analysis"
154
+ puts "-list: List Moves"
155
+ puts "-numbers: Board Numbers"
156
+ end
157
+
158
+ def args
159
+ help && return if ARGV.length < 1
160
+ while @tokens.ok?
161
+ case @tokens.cur
162
+ when "-xboard" then xboard and return # enter xboard mode
163
+ when "-rubybench" then rubybench
164
+ when "-bench" then bench
165
+ when "-mbench" then mbench
166
+ when "-stats" then stats
167
+ when "-variant" then variant
168
+ when "-randommode" then randommode
169
+ when "-tactics" then tactics
170
+ when "-test" then test
171
+ when "-name" then name
172
+ when "-fen" then fen
173
+ when "-profile" then profile
174
+ when "-perft" then perft
175
+ when "-randperft" then randperft
176
+ when "-suite" then suite
177
+ when "-numbers" then print_numbers
178
+ when "-help" then help
179
+ else
180
+ puts "RubyShogi Error: Unknown Command: '#{@tokens.cur}'"
181
+ return
182
+ end
183
+ @tokens.forward
184
+ end
185
+ end
186
+ end # class Cmd
187
+
188
+ end # module RubyShogi
@@ -0,0 +1,276 @@
1
+ ##
2
+ # RubyShogi, a Shogi Engine
3
+ # Author: Toni Helminen
4
+ # License: GPLv3
5
+ ##
6
+
7
+ module RubyShogi
8
+
9
+ class Engine
10
+ attr_accessor :board, :random_mode, :gameover, :move_now, :debug, :time, :movestogo, :printinfo
11
+
12
+ INF = 1000
13
+ MATERIAL_SCALE = 0.01
14
+ RESULT_DRAW = 1
15
+ RESULT_BLACK_WIN = 2
16
+ RESULT_WHITE_WIN = 4
17
+
18
+ def initialize(random_mode: false)
19
+ init_mate_bonus
20
+ @board = RubyShogi::Board.new
21
+ @random_mode = random_mode
22
+ @history = RubyShogi::History.new
23
+ @board.startpos
24
+ @printinfo = true
25
+ @time = 10 # seconds
26
+ @movestogo = 40
27
+ @stop_time = 0
28
+ @stop_search = false
29
+ @nodes = 0
30
+ @move_now = false
31
+ @debug = false
32
+ @gameover = false
33
+ end
34
+
35
+ def init_mate_bonus
36
+ @mate_bonus = [1] * 100
37
+ (0..20).each { |i| @mate_bonus[i] += 20 - i }
38
+ @mate_bonus[0] = 50
39
+ @mate_bonus[1] = 40
40
+ @mate_bonus[2] = 30
41
+ @mate_bonus[3] = 25
42
+ end
43
+
44
+ def history_reset
45
+ @history.reset
46
+ end
47
+
48
+ def history_remove
49
+ @board = @history.remove
50
+ end
51
+
52
+ def history_undo
53
+ @board = @history.undo
54
+ end
55
+
56
+ def print_move_list(moves)
57
+ moves.each_with_index { |board, i| puts "#{i} / #{board.move_str} / #{board.score}" }
58
+ end
59
+
60
+ def move_list
61
+ mgen = @board.mgen_generator
62
+ moves = mgen.generate_moves
63
+ moves.each_with_index { |board, i| puts "#{i} / #{board.move_str} / #{board.score}" }
64
+ end
65
+
66
+ def make_move?(move)
67
+ mgen = @board.mgen_generator
68
+ moves = mgen.generate_moves
69
+ moves.each do |board|
70
+ if board.move_str == move
71
+ @history.add(board)
72
+ @board = board
73
+ return true
74
+ end
75
+ end
76
+ puts "illegal move: #{move}"
77
+ false
78
+ end
79
+
80
+ def print_score(moves, depth, started)
81
+ return unless @printinfo
82
+ moves = moves.sort_by(&:score).reverse
83
+ best = moves[0]
84
+ n = (100 * (Time.now - started)).to_i
85
+ puts " #{depth} #{(best.score).to_i} #{n} #{@nodes} #{best.move_str}"
86
+ end
87
+
88
+ def search_moves_w(cur, depth, total = 0)
89
+ @nodes += 1
90
+ @stop_search = Time.now > @stop_time || total > 90
91
+ return 0 if @stop_search
92
+ return MATERIAL_SCALE * cur.material if depth < 1
93
+ mgen = RubyShogi::MgenWhite.new(cur)
94
+ moves = mgen.generate_moves
95
+ if moves.length == 0 # assume mate
96
+ return mgen.checks_b? ? 0.1 * @mate_bonus[total] * -INF + rand : 1
97
+ end
98
+ search_moves_b(moves.sample, depth - 1, total + 1)
99
+ end
100
+
101
+ def search_moves_b(cur, depth, total = 0)
102
+ @nodes += 1
103
+ @stop_search = Time.now > @stop_time || total > 90
104
+ return 0 if @stop_search
105
+ return MATERIAL_SCALE * cur.material if depth < 1
106
+ mgen = RubyShogi::MgenBlack.new(cur)
107
+ moves = mgen.generate_moves
108
+ if moves.length == 0 # assume mate
109
+ return mgen.checks_w? ? 0.1 * @mate_bonus[total] * INF + rand : 1
110
+ end
111
+ search_moves_w(moves.sample, depth - 1, total + 1)
112
+ end
113
+
114
+ def search(moves)
115
+ now = Time.now
116
+ time4print = 0.5
117
+ divv = @movestogo < 10 ? 20 : 30
118
+ @stop_time = now + (@time / divv)
119
+ depth = 2
120
+ while true
121
+ moves.each do |board|
122
+ puts "> #{@nodes} / #{board.move_str}" if @debug
123
+ next if board.nodetype == 2
124
+ depth = 3 + rand(20)
125
+ board.score += board.wtm ? search_moves_w(board, depth, 0) : search_moves_b(board, depth, 0)
126
+ if Time.now > @stop_time || @move_now
127
+ print_score(moves, depth, now)
128
+ return
129
+ end
130
+ end
131
+ if Time.now - now > time4print
132
+ now = Time.now
133
+ print_score(moves, depth, now)
134
+ end
135
+ end
136
+ end
137
+
138
+ def draw_moves(moves)
139
+ moves.each do | board |
140
+ if @history.is_draw?(board)
141
+ board.nodetype, board.score = 2, 0
142
+ end
143
+ end
144
+ end
145
+
146
+ def hash_moves(moves)
147
+ moves.each { |board| board.create_hash }
148
+ end
149
+
150
+ def game_status(mgen, moves)
151
+ if moves.length == 0
152
+ if @board.wtm && mgen.checks_b?(@board.find_white_king)
153
+ return RubyShogi::Engine::RESULT_BLACK_WIN
154
+ elsif !@board.wtm && mgen.checks_w?(@board.find_black_king)
155
+ return RubyShogi::Engine::RESULT_WHITE_WIN
156
+ end
157
+ return RubyShogi::Engine::RESULT_DRAW
158
+ end
159
+ @board.create_hash
160
+ if @history.is_draw?(@board, 3) || @board.material_draw?
161
+ return RubyShogi::Engine::RESULT_DRAW
162
+ end
163
+ 0
164
+ end
165
+
166
+ def jishogi?
167
+ if @board.jishogi?
168
+ w = @board.count_jishogi_w
169
+ b = @board.count_jishogi_b
170
+ if w >= 24 && b < 24
171
+ puts "1-0 {White wins by Jishogi}"
172
+ return true
173
+ elsif w < 24 && b >= 24
174
+ puts "0-1 {Black wins by Jishogi}"
175
+ return true
176
+ else
177
+ puts "1/2-1/2 {Draw by Impasse}"
178
+ return true
179
+ end
180
+ end
181
+ false
182
+ end
183
+
184
+ def is_gameover?(mgen, moves)
185
+ @board.create_hash
186
+ return true if jishogi?
187
+ if @board.fullmoves > 900
188
+ puts "1/2-1/2 {Draw by Max Moves}"
189
+ return true
190
+ end
191
+ if @history.is_draw?(@board, 3)
192
+ puts "1/2-1/2 {Draw by Sennichite}"
193
+ return true
194
+ end
195
+ if moves.length == 0
196
+ if @board.wtm && mgen.checks_b?(@board.find_white_king)
197
+ puts "0-1 {Black mates}"
198
+ return true
199
+ elsif !@board.wtm && mgen.checks_w?(@board.find_black_king)
200
+ puts "1-0 {White mates}"
201
+ return true
202
+ end
203
+ end
204
+ false
205
+ end
206
+
207
+ def bench
208
+ t = Time.now
209
+ @time = 500
210
+ think
211
+ diff = Time.now - t
212
+ puts "= #{@nodes} nodes | #{diff.round(3)} s | #{(@nodes/diff).to_i} nps"
213
+ end
214
+
215
+ def think
216
+ @nodes = 0
217
+ @move_now = false
218
+ board = @board
219
+ mgen = @board.mgen_generator
220
+ moves = mgen.generate_moves
221
+ hash_moves(moves)
222
+ draw_moves(moves)
223
+ func = -> { board.wtm ? moves.sort_by(&:score).reverse : moves.sort_by(&:score) }
224
+ @gameover = is_gameover?(mgen, moves)
225
+ return if @gameover
226
+ if @random_mode
227
+ @board = moves.sample
228
+ else
229
+ search(moves)
230
+ moves = func.call
231
+ #print_move_list(moves)
232
+ @board = moves[0]
233
+ end
234
+ print_move_list(moves) if @debug
235
+ @history.add(@board)
236
+ @board.move_str
237
+ end
238
+
239
+ def print_score_stats(results)
240
+ wscore = results[RubyShogi::Engine::RESULT_WHITE_WIN]
241
+ bscore = results[RubyShogi::Engine::RESULT_BLACK_WIN]
242
+ draws = results[RubyShogi::Engine::RESULT_DRAW]
243
+ total = wscore + bscore + draws
244
+ printf("[ Score: %i - %i - %i [%.2f] %i ]\n", wscore, bscore, draws, (wscore + 0.5 * draws) / total, total)
245
+ end
246
+
247
+ def stats(rounds = 10)
248
+ @nodes = 0
249
+ @move_now = false
250
+ board = @board
251
+ results = [0] * 5
252
+ puts "Running stats ..."
253
+ rounds.times do |n|
254
+ @board = board
255
+ @history.reset
256
+ while true
257
+ mgen = @board.mgen_generator
258
+ moves = mgen.generate_moves
259
+ hash_moves(moves)
260
+ draw_moves(moves)
261
+ status = game_status(mgen, moves)
262
+ @board = moves.sample
263
+ @history.add(@board)
264
+ if status != 0
265
+ results[status] += 1
266
+ break
267
+ end
268
+ end
269
+ print_score_stats(results) if (n + 1) % 2 == 0 && n + 1 < rounds
270
+ end
271
+ puts "="
272
+ print_score_stats(results)
273
+ end
274
+ end # class Engine
275
+
276
+ end # module RubyShogi