shurikenengine 0.31
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/shuriken +5 -0
- data/lib/shuriken.rb +1 -0
- data/lib/shuriken/bench.rb +118 -0
- data/lib/shuriken/board.rb +70 -0
- data/lib/shuriken/board_caparandom.rb +422 -0
- data/lib/shuriken/cmd.rb +185 -0
- data/lib/shuriken/engine.rb +53 -0
- data/lib/shuriken/engine_caparandom.rb +226 -0
- data/lib/shuriken/eval_caparandom.rb +110 -0
- data/lib/shuriken/falcon_moves.rb +40 -0
- data/lib/shuriken/fen.rb +52 -0
- data/lib/shuriken/history.rb +60 -0
- data/lib/shuriken/mgen_caparandom.rb +181 -0
- data/lib/shuriken/mgen_caparandom_black.rb +222 -0
- data/lib/shuriken/mgen_caparandom_white.rb +225 -0
- data/lib/shuriken/perft_capablanca_caparandom.rb +26 -0
- data/lib/shuriken/perft_caparandom.rb +77 -0
- data/lib/shuriken/perft_falcon_caparandom.rb +20 -0
- data/lib/shuriken/perft_gothic_caparandom.rb +26 -0
- data/lib/shuriken/shuriken.rb +75 -0
- data/lib/shuriken/tactics_caparandom.rb +72 -0
- data/lib/shuriken/tokens.rb +42 -0
- data/lib/shuriken/utils.rb +25 -0
- data/lib/shuriken/xboard.rb +134 -0
- data/lib/shuriken/zobrist.rb +25 -0
- metadata +69 -0
data/lib/shuriken/cmd.rb
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Shuriken, a Ruby chess variant engine
|
|
3
|
+
# Author: Toni Helminen
|
|
4
|
+
# License: GPLv3
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
module Shuriken
|
|
8
|
+
|
|
9
|
+
class Cmd
|
|
10
|
+
attr_accessor :engine, :random_mode
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@variant = "caparandom" # default
|
|
14
|
+
@random_mode = false
|
|
15
|
+
@tokens = Tokens.new(ARGV)
|
|
16
|
+
@fen = nil#"rnbqckabnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNBQCKABNR w KQkq - 0 1"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
puts "#{Shuriken::NAME} v#{Shuriken::VERSION} by #{Shuriken::AUTHOR}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run_suite(depth = 4)
|
|
24
|
+
case @variant
|
|
25
|
+
when "gothic"
|
|
26
|
+
Shuriken::PerftGothicCaparandom.new
|
|
27
|
+
when "falcon"
|
|
28
|
+
Shuriken::PerftFalconCaparandom.new
|
|
29
|
+
else
|
|
30
|
+
Shuriken::PerftCapablancaCaparandom.new
|
|
31
|
+
end.suite([0, depth].max)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def randommode
|
|
35
|
+
@random_mode = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mbench
|
|
39
|
+
depth = 4
|
|
40
|
+
val = @tokens.peek(1)
|
|
41
|
+
if val != nil && val.match(/\d+/)
|
|
42
|
+
@tokens.forward
|
|
43
|
+
depth = @tokens.cur.to_i
|
|
44
|
+
end
|
|
45
|
+
run_suite(depth)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def perft
|
|
49
|
+
depth = 3
|
|
50
|
+
val = @tokens.peek(1)
|
|
51
|
+
if val != nil && val.match(/\d+/)
|
|
52
|
+
@tokens.forward
|
|
53
|
+
depth = @tokens.cur.to_i
|
|
54
|
+
end
|
|
55
|
+
p = PerftCaparandom.new(@variant, @fen)
|
|
56
|
+
p.perft(depth)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def bench
|
|
60
|
+
e = Shuriken::EngineCaparandom.new(@variant, random_mode: @random_mode)
|
|
61
|
+
e.bench
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stats
|
|
65
|
+
n = 100
|
|
66
|
+
val = @tokens.peek(1)
|
|
67
|
+
if val != nil && val.match(/\d+/)
|
|
68
|
+
@tokens.forward
|
|
69
|
+
n = val.to_i
|
|
70
|
+
end
|
|
71
|
+
e = Shuriken::EngineCaparandom.new("falcon", random_mode: @random_mode)
|
|
72
|
+
e.board.use_fen(@fen) if @fen != nil
|
|
73
|
+
e.stats(n)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def tactics
|
|
77
|
+
Shuriken::TacticsCaparandom.run
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fen
|
|
81
|
+
@tokens.go_next
|
|
82
|
+
@fen = @tokens.cur
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def variant
|
|
86
|
+
@tokens.go_next
|
|
87
|
+
fail "Bad Input" unless @tokens.ok?
|
|
88
|
+
@variant = @tokens.cur
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def list
|
|
92
|
+
board = Shuriken::BoardCaparandom.new(@variant)
|
|
93
|
+
board.use_fen(@fen)
|
|
94
|
+
mgen = board.mgen_generator
|
|
95
|
+
moves = mgen.generate_moves
|
|
96
|
+
i = 0
|
|
97
|
+
moves.each do |b|
|
|
98
|
+
puts "> #{i}: #{b.move_str}"
|
|
99
|
+
i += 1
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test
|
|
104
|
+
# ...
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def rubybench
|
|
108
|
+
Shuriken::Bench.go
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def xboard
|
|
112
|
+
xboard = Shuriken::Xboard.new(@variant, @random_mode)
|
|
113
|
+
xboard.go
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def profile
|
|
117
|
+
require 'ruby-prof'
|
|
118
|
+
result = RubyProf.profile do
|
|
119
|
+
e = Shuriken::EngineCaparandom.new("gothic", random_mode: @random_mode)
|
|
120
|
+
e.bench
|
|
121
|
+
end
|
|
122
|
+
printer = RubyProf::FlatPrinter.new(result)
|
|
123
|
+
printer.print(STDOUT)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def help
|
|
127
|
+
puts "Usage: ruby shuriken.rb [OPTION]... [PARAMS]..."
|
|
128
|
+
puts "-help: This Help"
|
|
129
|
+
puts "-xboard: Enter Xboard Mode"
|
|
130
|
+
puts "-tactics: Run Tactics"
|
|
131
|
+
puts "-name: Print Name Tactics"
|
|
132
|
+
puts "-rubybench: Benchmark Ruby"
|
|
133
|
+
puts "-bench: Benchmark Shuriken Engine"
|
|
134
|
+
puts "-mbench: Benchmark Shuriken Movegen"
|
|
135
|
+
puts "-profile: Profile Shuriken"
|
|
136
|
+
puts "-variant [NAME]: Set Variant (gothic / caparandom / falcon / capablanca)"
|
|
137
|
+
puts "-randommode: Activate Random Mode"
|
|
138
|
+
puts "-fen [FEN]: Set Fen"
|
|
139
|
+
puts "-stats [NUM]: Statistical Analysis"
|
|
140
|
+
puts "-list: List Moves"
|
|
141
|
+
puts "-perft [NUM]: Run Perft"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def args
|
|
145
|
+
help && return if ARGV.length < 1
|
|
146
|
+
while @tokens.ok?
|
|
147
|
+
case @tokens.cur
|
|
148
|
+
when "-xboard" then # enter xboard mode
|
|
149
|
+
xboard and return
|
|
150
|
+
when "-mbench" then
|
|
151
|
+
mbench
|
|
152
|
+
when "-rubybench" then
|
|
153
|
+
rubybench
|
|
154
|
+
when "-bench" then
|
|
155
|
+
bench
|
|
156
|
+
when "-stats" then
|
|
157
|
+
stats
|
|
158
|
+
when "-variant" then
|
|
159
|
+
variant
|
|
160
|
+
when "-randommode" then
|
|
161
|
+
randommode
|
|
162
|
+
when "-tactics" then
|
|
163
|
+
tactics
|
|
164
|
+
when "-test" then
|
|
165
|
+
test
|
|
166
|
+
when "-name" then
|
|
167
|
+
name
|
|
168
|
+
when "-fen" then
|
|
169
|
+
fen
|
|
170
|
+
when "-profile" then
|
|
171
|
+
profile
|
|
172
|
+
when "-list" then
|
|
173
|
+
list
|
|
174
|
+
when "-help" then
|
|
175
|
+
help
|
|
176
|
+
else
|
|
177
|
+
puts "Shuriken Error: Unknown Command: '#{@tokens.cur}'"
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
@tokens.forward
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end # class Cmd
|
|
184
|
+
|
|
185
|
+
end # module Shuriken
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Shuriken, a Ruby chess variant engine
|
|
3
|
+
# Author: Toni Helminen
|
|
4
|
+
# License: GPLv3
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
module Shuriken
|
|
8
|
+
|
|
9
|
+
class Engine
|
|
10
|
+
RESULT_DRAW = 1
|
|
11
|
+
RESULT_BLACK_WIN = 2
|
|
12
|
+
RESULT_WHITE_WIN = 4
|
|
13
|
+
|
|
14
|
+
def init_mate_bonus
|
|
15
|
+
@mate_bonus = [1] * 100
|
|
16
|
+
(0..20).each { |i| @mate_bonus[i] += 20 - i }
|
|
17
|
+
@mate_bonus[0] = 50
|
|
18
|
+
@mate_bonus[1] = 40
|
|
19
|
+
@mate_bonus[2] = 30
|
|
20
|
+
@mate_bonus[3] = 25
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def history_reset
|
|
24
|
+
@history.reset
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def history_remove
|
|
28
|
+
@board = @history.remove
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def history_undo
|
|
32
|
+
@board = @history.undo
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def print_move_list(moves)
|
|
36
|
+
i = 0
|
|
37
|
+
moves.each do |board|
|
|
38
|
+
i += 1
|
|
39
|
+
puts "#{i} / #{board.move_str} / #{board.score}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def move_list
|
|
44
|
+
mgen = @board.mgen_generator
|
|
45
|
+
moves, i = mgen.generate_moves, 0
|
|
46
|
+
moves.each do |board|
|
|
47
|
+
i += 1
|
|
48
|
+
puts "#{i} / #{board.move_str} / #{board.score}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end # class Engine
|
|
52
|
+
|
|
53
|
+
end # module Shuriken
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Shuriken, a Ruby chess variant engine
|
|
3
|
+
# Author: Toni Helminen
|
|
4
|
+
# License: GPLv3
|
|
5
|
+
##
|
|
6
|
+
|
|
7
|
+
module Shuriken
|
|
8
|
+
|
|
9
|
+
class EngineCaparandom < Shuriken::Engine
|
|
10
|
+
attr_accessor :board, :random_mode, :gameover, :move_now, :debug, :time, :movestogo, :printinfo
|
|
11
|
+
|
|
12
|
+
INF = 1000
|
|
13
|
+
MATERIAL_SCALE = 0.01
|
|
14
|
+
|
|
15
|
+
def initialize(variant, random_mode: false)
|
|
16
|
+
init_mate_bonus
|
|
17
|
+
@board = Shuriken::BoardCaparandom.new(variant)
|
|
18
|
+
@random_mode = random_mode
|
|
19
|
+
@history = Shuriken::History.new
|
|
20
|
+
@board.startpos(variant)
|
|
21
|
+
@printinfo = true
|
|
22
|
+
@time = 10 # seconds
|
|
23
|
+
@movestogo = 40
|
|
24
|
+
@stop_time = 0
|
|
25
|
+
@stop_search = false
|
|
26
|
+
@nodes = 0
|
|
27
|
+
@move_now = false
|
|
28
|
+
@debug = false
|
|
29
|
+
@gameover = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def make_move?(move)
|
|
33
|
+
mgen = @board.mgen_generator
|
|
34
|
+
moves = mgen.generate_moves
|
|
35
|
+
moves.each do |board|
|
|
36
|
+
if board.move_str == move
|
|
37
|
+
@history.add(board)
|
|
38
|
+
@board = board
|
|
39
|
+
return true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
puts "illegal move: #{move}"
|
|
43
|
+
false
|
|
44
|
+
#fail "Shuriken Error: Illegal Move: '#{move}'"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def print_score(moves, depth, started)
|
|
48
|
+
return unless @printinfo
|
|
49
|
+
moves = moves.sort_by(&:score).reverse
|
|
50
|
+
best = moves[0]
|
|
51
|
+
n = (100 * (Time.now - started)).to_i
|
|
52
|
+
puts " #{depth} #{(best.score).to_i} #{n} #{@nodes} #{best.move_str}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def search_moves_w(cur, depth, total = 0)
|
|
56
|
+
@nodes += 1
|
|
57
|
+
@stop_search = (Time.now > @stop_time || total > 90)
|
|
58
|
+
return 0 if @stop_search
|
|
59
|
+
return MATERIAL_SCALE * cur.material if depth < 1
|
|
60
|
+
mgen = Shuriken::MgenCaparandomWhite.new(cur)
|
|
61
|
+
moves = mgen.generate_moves
|
|
62
|
+
if moves.length == 0 # assume mate
|
|
63
|
+
return 0.1 * @mate_bonus[total] * -INF
|
|
64
|
+
end
|
|
65
|
+
search_moves_b(moves.sample, depth - 1, total + 1)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def search_moves_b(cur, depth, total = 0)
|
|
69
|
+
@nodes += 1
|
|
70
|
+
@stop_search = (Time.now > @stop_time || total > 90)
|
|
71
|
+
return 0 if @stop_search
|
|
72
|
+
return MATERIAL_SCALE * cur.material if depth < 1
|
|
73
|
+
mgen = Shuriken::MgenCaparandomBlack.new(cur)
|
|
74
|
+
moves = mgen.generate_moves
|
|
75
|
+
if moves.length == 0 # assume mate
|
|
76
|
+
return 0.1 * @mate_bonus[total] * INF
|
|
77
|
+
end
|
|
78
|
+
search_moves_w(moves.sample, depth - 1, total + 1)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def search(moves)
|
|
82
|
+
now = Time.now
|
|
83
|
+
time4print = 0.5
|
|
84
|
+
#@stop_time = now + (@time / ((@movestogo < 1 ? 30 : @movestogo) + 2)) # no time losses
|
|
85
|
+
divv = @movestogo < 10 ? 20 : 30
|
|
86
|
+
@stop_time = now + (@time / divv)
|
|
87
|
+
depth = 2
|
|
88
|
+
while true
|
|
89
|
+
moves.each do | board |
|
|
90
|
+
puts "> #{@nodes} / #{board.move_str}" if @debug
|
|
91
|
+
next if board.nodetype == 2
|
|
92
|
+
depth = 3 + rand(20)
|
|
93
|
+
board.score += board.wtm ? search_moves_w(board, depth, 0) : search_moves_b(board, depth, 0)
|
|
94
|
+
if Time.now > @stop_time || @move_now
|
|
95
|
+
print_score(moves, depth, now)
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
if Time.now - now > time4print
|
|
100
|
+
now = Time.now
|
|
101
|
+
print_score(moves, depth, now)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def draw_moves(moves)
|
|
107
|
+
moves.each do | board |
|
|
108
|
+
if @history.is_draw?(board)
|
|
109
|
+
board.nodetype, board.score = 2, 0
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def hash_moves(moves)
|
|
115
|
+
moves.each { |board| board.create_hash }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def game_status(mgen, moves)
|
|
119
|
+
if moves.length == 0
|
|
120
|
+
if @board.wtm && mgen.checks_b?(@board.find_white_king)
|
|
121
|
+
return Shuriken::Engine::RESULT_BLACK_WIN
|
|
122
|
+
elsif !@board.wtm && mgen.checks_w?(@board.find_black_king)
|
|
123
|
+
return Shuriken::Engine::RESULT_WHITE_WIN
|
|
124
|
+
end
|
|
125
|
+
return Shuriken::Engine::RESULT_DRAW
|
|
126
|
+
end
|
|
127
|
+
@board.create_hash
|
|
128
|
+
if @history.is_draw?(@board, 3) || @board.material_draw?
|
|
129
|
+
return Shuriken::Engine::RESULT_DRAW
|
|
130
|
+
end
|
|
131
|
+
0
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def is_gameover?(mgen, moves)
|
|
135
|
+
@board.create_hash
|
|
136
|
+
if @history.is_draw?(@board, 3)
|
|
137
|
+
puts "1/2-1/2 {Draw by repetition}"
|
|
138
|
+
return true
|
|
139
|
+
end
|
|
140
|
+
if moves.length == 0
|
|
141
|
+
if @board.wtm && mgen.checks_b?(@board.find_white_king)
|
|
142
|
+
puts "0-1 {Black mates}"
|
|
143
|
+
elsif ! @board.wtm && mgen.checks_w?(@board.find_black_king)
|
|
144
|
+
puts "1-0 {White mates}"
|
|
145
|
+
end
|
|
146
|
+
puts "1/2-1/2 {Stalemate}"
|
|
147
|
+
return true
|
|
148
|
+
end
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def bench
|
|
153
|
+
t = Time.now
|
|
154
|
+
@time = 500
|
|
155
|
+
think
|
|
156
|
+
diff = Time.now - t
|
|
157
|
+
puts "= #{@nodes} nodes | #{diff.round(3)} s | #{(@nodes/diff).to_i} nps"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def think
|
|
161
|
+
@nodes = 0
|
|
162
|
+
@move_now = false
|
|
163
|
+
@history.reset
|
|
164
|
+
board = @board
|
|
165
|
+
mgen = @board.mgen_generator
|
|
166
|
+
moves = mgen.generate_moves
|
|
167
|
+
hash_moves(moves)
|
|
168
|
+
draw_moves(moves)
|
|
169
|
+
func = -> { board.wtm ? moves.sort_by(&:score).reverse : moves.sort_by(&:score) }
|
|
170
|
+
@gameover = is_gameover?(mgen, moves)
|
|
171
|
+
return if @gameover
|
|
172
|
+
if @random_mode
|
|
173
|
+
@board = moves.sample
|
|
174
|
+
else
|
|
175
|
+
search(moves)
|
|
176
|
+
moves = func.call
|
|
177
|
+
@board = moves[0]
|
|
178
|
+
end
|
|
179
|
+
print_move_list(moves) if @debug
|
|
180
|
+
@history.add(@board)
|
|
181
|
+
@board.move_str
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def print_score_stats(results)
|
|
185
|
+
wscore = results[Shuriken::Engine::RESULT_WHITE_WIN]
|
|
186
|
+
bscore = results[Shuriken::Engine::RESULT_BLACK_WIN]
|
|
187
|
+
draws = results[Shuriken::Engine::RESULT_DRAW]
|
|
188
|
+
total = wscore + bscore + draws
|
|
189
|
+
printf("[ Score: %i - %i - %i [%.2f] %i ]\n", wscore, bscore, draws, (wscore + 0.5 * draws) / total, total)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def stats(rounds = 10)
|
|
193
|
+
rounds = 6
|
|
194
|
+
@nodes = 0
|
|
195
|
+
@move_now = false
|
|
196
|
+
board = @board
|
|
197
|
+
results = [0] * 5
|
|
198
|
+
puts "Running stats ..."
|
|
199
|
+
rounds.times do |n|
|
|
200
|
+
@board = board
|
|
201
|
+
@history.reset
|
|
202
|
+
lastboard = board
|
|
203
|
+
while true
|
|
204
|
+
lastboard = board
|
|
205
|
+
#@board.print_board
|
|
206
|
+
mgen = @board.mgen_generator
|
|
207
|
+
moves = mgen.generate_moves
|
|
208
|
+
hash_moves(moves)
|
|
209
|
+
draw_moves(moves)
|
|
210
|
+
#@board = moves.length == 0 ? lastboard : @board
|
|
211
|
+
status = game_status(mgen, moves)
|
|
212
|
+
@board = moves.sample
|
|
213
|
+
@history.add(@board)
|
|
214
|
+
if status != 0
|
|
215
|
+
results[status] += 1
|
|
216
|
+
break
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
print_score_stats(results) if (n + 1) % 2 == 0 && n + 1 < rounds
|
|
220
|
+
end
|
|
221
|
+
puts "="
|
|
222
|
+
print_score_stats(results)
|
|
223
|
+
end
|
|
224
|
+
end # class EngineCaparandom
|
|
225
|
+
|
|
226
|
+
end # module Shuriken
|