RubyShogi 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/ruby_shogi +11 -0
- data/lib/ruby_shogi.rb +7 -0
- data/lib/ruby_shogi/bench.rb +127 -0
- data/lib/ruby_shogi/board.rb +451 -0
- data/lib/ruby_shogi/cmd.rb +188 -0
- data/lib/ruby_shogi/engine.rb +276 -0
- data/lib/ruby_shogi/eval.rb +102 -0
- data/lib/ruby_shogi/history.rb +63 -0
- data/lib/ruby_shogi/mgen.rb +169 -0
- data/lib/ruby_shogi/mgen_black.rb +238 -0
- data/lib/ruby_shogi/mgen_white.rb +237 -0
- data/lib/ruby_shogi/perft.rb +87 -0
- data/lib/ruby_shogi/ruby_shogi.rb +66 -0
- data/lib/ruby_shogi/tactics.rb +33 -0
- data/lib/ruby_shogi/tokens.rb +42 -0
- data/lib/ruby_shogi/utils.rb +15 -0
- data/lib/ruby_shogi/xboard.rb +117 -0
- data/lib/ruby_shogi/zobrist.rb +25 -0
- metadata +62 -0
@@ -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
|