RubyShogi 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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