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.
- 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
|