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,237 @@
|
|
1
|
+
##
|
2
|
+
# RubyShogi, a Shogi Engine
|
3
|
+
# Author: Toni Helminen
|
4
|
+
# License: GPLv3
|
5
|
+
##
|
6
|
+
|
7
|
+
module RubyShogi
|
8
|
+
|
9
|
+
class MgenWhite < RubyShogi::Mgen
|
10
|
+
def initialize(board)
|
11
|
+
@board = board
|
12
|
+
end
|
13
|
+
|
14
|
+
def push_move(me, to, promo)
|
15
|
+
board2 = @board
|
16
|
+
copy = @board.copy_me
|
17
|
+
copy.from = @from_gen
|
18
|
+
copy.to = to
|
19
|
+
copy.promo = promo
|
20
|
+
copy.r50 += 1
|
21
|
+
copy.fullmoves += 1
|
22
|
+
copy.r50 = 0 if [1, 3, 5].include?(me)
|
23
|
+
copy.eat = copy.brd[to]
|
24
|
+
copy.white_pocket.push(eaten_piece(-copy.eat)) if copy.eat != 0
|
25
|
+
copy.wtm = !copy.wtm
|
26
|
+
copy.brd[@from_gen] = 0
|
27
|
+
copy.brd[to] = me
|
28
|
+
copy.wking = to if me == 14
|
29
|
+
#fail if copy.find_white_king != copy.wking
|
30
|
+
@board = copy
|
31
|
+
@moves.push << copy if !checks_b?
|
32
|
+
@board = board2
|
33
|
+
end
|
34
|
+
|
35
|
+
def pawn_drop_checkmate?(to)
|
36
|
+
@board.brd[to + 9] == -14 && !checks_b?(to + 9) ? true : false
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_new_drop_move(me, to)
|
40
|
+
return if me == 3 && to / 9 == 8
|
41
|
+
return if me == 5 && to / 9 >= 7
|
42
|
+
board2 = @board
|
43
|
+
copy = @board.copy_me
|
44
|
+
copy.from = @from_gen
|
45
|
+
copy.to = to
|
46
|
+
copy.drop = me
|
47
|
+
copy.eat = 0
|
48
|
+
copy.r50 += 1
|
49
|
+
copy.fullmoves += 1
|
50
|
+
copy.wtm = ! copy.wtm
|
51
|
+
copy.brd[@from_gen] = 0
|
52
|
+
copy.brd[to] = me
|
53
|
+
copy.white_pocket = remove_from_array(copy.white_pocket, me)
|
54
|
+
#fail if copy.find_white_king != copy.wking
|
55
|
+
@board = copy
|
56
|
+
if !checks_b? && !(me == -1 && pawn_drop_checkmate?(to))
|
57
|
+
@moves.push << copy
|
58
|
+
end
|
59
|
+
@board = board2
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_capture(copy, eat)
|
63
|
+
piece = case eat
|
64
|
+
when 1..2 then 1
|
65
|
+
when 3..4 then 3
|
66
|
+
when 5..6 then 5
|
67
|
+
when 7..8 then 7
|
68
|
+
when 9 then 9
|
69
|
+
when 10..11 then 10
|
70
|
+
when 12..13 then 12
|
71
|
+
end
|
72
|
+
copy.white_pocket.push(piece)
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_new_move(me, to, promo = 0)
|
76
|
+
return if handle_promotion?(me, to)
|
77
|
+
push_move(me, to, promo)
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_promotion?(me, to)
|
81
|
+
return true if must_promote?(me, to)
|
82
|
+
return false if to / 9 <= 5 && @from_gen / 9 <= 5
|
83
|
+
case me
|
84
|
+
when 1
|
85
|
+
push_move(1, to, PROMO_STAY)
|
86
|
+
push_move(2, to, PROMO_YES)
|
87
|
+
return true
|
88
|
+
when 3
|
89
|
+
push_move(3, to, PROMO_STAY)
|
90
|
+
push_move(4, to, PROMO_YES)
|
91
|
+
return true
|
92
|
+
when 5
|
93
|
+
push_move(5, to, PROMO_STAY)
|
94
|
+
push_move(6, to, PROMO_YES)
|
95
|
+
return true
|
96
|
+
when 7
|
97
|
+
push_move(7, to, PROMO_STAY)
|
98
|
+
push_move(8, to, PROMO_YES)
|
99
|
+
return true
|
100
|
+
when 10
|
101
|
+
push_move(10, to, PROMO_STAY)
|
102
|
+
push_move(11, to, PROMO_YES)
|
103
|
+
return true
|
104
|
+
when 12
|
105
|
+
push_move(12, to, PROMO_STAY)
|
106
|
+
push_move(13, to, PROMO_YES)
|
107
|
+
return true
|
108
|
+
end
|
109
|
+
false
|
110
|
+
end
|
111
|
+
|
112
|
+
def must_promote?(me, to)
|
113
|
+
if me == 5 && to / 9 >= 7
|
114
|
+
push_move(6, to, PROMO_YES)
|
115
|
+
return true
|
116
|
+
end
|
117
|
+
return false if to / 9 != 8
|
118
|
+
case me
|
119
|
+
when 1
|
120
|
+
push_move(2, to, PROMO_YES)
|
121
|
+
return true
|
122
|
+
when 3
|
123
|
+
push_move(4, to, PROMO_YES)
|
124
|
+
return true
|
125
|
+
end
|
126
|
+
false
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_new_pawn_move(to)
|
130
|
+
case to / 9
|
131
|
+
when 6..7
|
132
|
+
push_move(1, to, PROMO_STAY)
|
133
|
+
push_move(2, to, PROMO_YES)
|
134
|
+
when 8 then push_move(2, to, PROMO_YES)
|
135
|
+
else
|
136
|
+
push_move(1, to, PROMO_NO)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def generate_pawn_moves
|
141
|
+
to = @x_gen + (@y_gen + 1) * 9
|
142
|
+
add_new_pawn_move(to) if (to < 81 && @board.walkable_w?(to))
|
143
|
+
end
|
144
|
+
|
145
|
+
def generate_jump_moves(jumps, me)
|
146
|
+
jumps.each do |jmp|
|
147
|
+
px, py = @x_gen + jmp[0], @y_gen + jmp[1]
|
148
|
+
to = px + 9 * py
|
149
|
+
add_new_move(me, to) if is_on_board?(px, py) && @board.walkable_w?(to)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_slider_moves(slider, me)
|
154
|
+
slider.each do |jmp|
|
155
|
+
px, py = @x_gen, @y_gen
|
156
|
+
loop do
|
157
|
+
px, py = px + jmp[0], py + jmp[1]
|
158
|
+
break if !is_on_board?(px, py)
|
159
|
+
to = px + 9 * py
|
160
|
+
add_new_move(me, to) if @board.walkable_w?(to)
|
161
|
+
break if !@board.empty?(to)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def pawn_on_column?(c)
|
167
|
+
ret = false
|
168
|
+
9.times do |i|
|
169
|
+
to = 9 * i + c
|
170
|
+
if to != @from_gen && @board.brd[to] == 1
|
171
|
+
ret = true
|
172
|
+
break
|
173
|
+
end
|
174
|
+
end
|
175
|
+
ret
|
176
|
+
end
|
177
|
+
|
178
|
+
def put_pawn_drops
|
179
|
+
(9*8).times do |i|
|
180
|
+
@x_gen, @y_gen, @from_gen = i % 9, i / 9, i
|
181
|
+
add_new_drop_move(1, i) if !pawn_on_column?(i % 9 ) && @board.brd[i] == 0
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def put_drops(piece)
|
186
|
+
81.times do |i|
|
187
|
+
@x_gen, @y_gen, @from_gen = i % 9, i / 9, i
|
188
|
+
add_new_drop_move(piece, i) if @board.brd[i] == 0
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def generate_drops
|
193
|
+
@board.white_pocket.each do |piece|
|
194
|
+
case piece
|
195
|
+
when 1 then put_pawn_drops
|
196
|
+
when 3 then put_drops(3)
|
197
|
+
when 5 then put_drops(5)
|
198
|
+
when 7 then put_drops(7)
|
199
|
+
when 9 then put_drops(9)
|
200
|
+
when 10 then put_drops(10)
|
201
|
+
when 11 then put_drops(11)
|
202
|
+
when 12 then put_drops(12)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
@moves
|
206
|
+
end
|
207
|
+
|
208
|
+
def generate_moves
|
209
|
+
@moves = []
|
210
|
+
81.times do |i|
|
211
|
+
@x_gen, @y_gen, @from_gen = i % 9, i / 9, i
|
212
|
+
case @board.brd[i]
|
213
|
+
when 1 then generate_pawn_moves
|
214
|
+
when 2 then generate_jump_moves(WHITE_GOLD_GENERAL_MOVES, 2)
|
215
|
+
when 3 then generate_slider_moves(WHITE_LANCE_MOVES, 3)
|
216
|
+
when 4 then generate_jump_moves(WHITE_GOLD_GENERAL_MOVES, 4)
|
217
|
+
when 5 then generate_jump_moves(WHITE_KNIGHT_MOVES, 5)
|
218
|
+
when 6 then generate_jump_moves(WHITE_GOLD_GENERAL_MOVES, 6)
|
219
|
+
when 7 then generate_jump_moves(WHITE_SILVER_GENERAL_MOVES, 7)
|
220
|
+
when 8 then generate_jump_moves(WHITE_GOLD_GENERAL_MOVES, 8)
|
221
|
+
when 9 then generate_jump_moves(WHITE_GOLD_GENERAL_MOVES, 9)
|
222
|
+
when 10 then generate_slider_moves(BISHOP_MOVES, 10)
|
223
|
+
when 11
|
224
|
+
generate_slider_moves(BISHOP_MOVES, 11)
|
225
|
+
generate_jump_moves(PROMOTED_BISHOP_MOVES, 11)
|
226
|
+
when 12 then generate_slider_moves(ROOK_MOVES, 12)
|
227
|
+
when 13
|
228
|
+
generate_slider_moves(ROOK_MOVES, 13)
|
229
|
+
generate_jump_moves(PROMOTED_ROOK_MOVES, 13)
|
230
|
+
when 14 then generate_jump_moves(KING_MOVES, 14)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
generate_drops
|
234
|
+
end
|
235
|
+
end # class MgenWhite
|
236
|
+
|
237
|
+
end # module RubyShogi
|
@@ -0,0 +1,87 @@
|
|
1
|
+
##
|
2
|
+
# RubyShogi, a Shogi Engine
|
3
|
+
# Author: Toni Helminen
|
4
|
+
# License: GPLv3
|
5
|
+
##
|
6
|
+
|
7
|
+
module RubyShogi
|
8
|
+
|
9
|
+
class Perft
|
10
|
+
attr_accessor :board
|
11
|
+
|
12
|
+
NUMS = { # https://groups.google.com/forum/#!topic/shogi-l/U7hmtThbk1k
|
13
|
+
0 => 1,
|
14
|
+
1 => 30,
|
15
|
+
2 => 900,
|
16
|
+
3 => 25470,
|
17
|
+
4 => 719731,
|
18
|
+
5 => 19861490,
|
19
|
+
6 => 547581517
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize(fen = nil)
|
23
|
+
@board = RubyShogi::Board.new(fen)
|
24
|
+
end
|
25
|
+
|
26
|
+
def perft_number(depth)
|
27
|
+
return 1 if depth == 0
|
28
|
+
board = @board
|
29
|
+
mgen = @board.mgen_generator
|
30
|
+
n, moves = 0, mgen.generate_moves
|
31
|
+
return moves.length if depth <= 1
|
32
|
+
moves.each do |move|
|
33
|
+
@board = move
|
34
|
+
n += perft_number(depth - 1)
|
35
|
+
end
|
36
|
+
@board = board
|
37
|
+
n
|
38
|
+
end
|
39
|
+
|
40
|
+
def randperft(depth)
|
41
|
+
@board = @board.randpos
|
42
|
+
perft(depth)
|
43
|
+
end
|
44
|
+
|
45
|
+
def perft(depth)
|
46
|
+
puts "~~~ perft( #{depth} ) ~~~"
|
47
|
+
puts "[ fen: #{@board.pos2fen} ]"
|
48
|
+
total_time = 0
|
49
|
+
total_nodes = 0
|
50
|
+
copy = @board
|
51
|
+
(depth+1).times do |i|
|
52
|
+
start = Time.now
|
53
|
+
@board = copy
|
54
|
+
n = perft_number(i)
|
55
|
+
diff = Time.now - start
|
56
|
+
total_time += diff
|
57
|
+
total_nodes += n
|
58
|
+
nps = (diff == 0 or n == 1) ? n : (n / diff).to_i
|
59
|
+
puts "#{i}: #{n} | #{diff.round(3)}s | #{nps} nps"
|
60
|
+
end
|
61
|
+
total_time = 1 if total_time == 0
|
62
|
+
puts "= #{total_nodes} | #{total_time.round(3)}s | #{(total_nodes/total_time).to_i} nps"
|
63
|
+
end
|
64
|
+
|
65
|
+
def suite(depth)
|
66
|
+
puts "~~~ suite( #{depth} ) ~~~"
|
67
|
+
total_time = 0
|
68
|
+
total_nodes = 0
|
69
|
+
copy = @board
|
70
|
+
(depth+1).times do |i|
|
71
|
+
start = Time.now
|
72
|
+
@board = copy
|
73
|
+
n = perft_number(i)
|
74
|
+
diff = Time.now - start
|
75
|
+
total_time += diff
|
76
|
+
total_nodes += n
|
77
|
+
nps = (diff == 0 or n == 1) ? n : (n / diff).to_i
|
78
|
+
error = ["ok", "error"][NUMS[i] - n == 0 ? 0 : 1]
|
79
|
+
break if i >= NUMS.length - 1
|
80
|
+
puts "#{i}: #{n} | #{diff.round(3)}s | #{nps} nps | #{error}"
|
81
|
+
end
|
82
|
+
total_time = 1 if total_time == 0
|
83
|
+
puts "= #{total_nodes} | #{total_time.round(3)}s | #{(total_nodes/total_time).to_i} nps"
|
84
|
+
end
|
85
|
+
end # class Perft
|
86
|
+
|
87
|
+
end # module ShurikenShogi
|
@@ -0,0 +1,66 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# RubyShogi, a Shogi Engine
|
4
|
+
# Copyright (C) 2019 Toni Helminen ( kalleankka1@gmail.com )
|
5
|
+
#
|
6
|
+
# RubyShogi is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# RubyShogi is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
#
|
18
|
+
##
|
19
|
+
|
20
|
+
require_relative "./engine"
|
21
|
+
require_relative "./zobrist"
|
22
|
+
require_relative "./history"
|
23
|
+
require_relative "./cmd"
|
24
|
+
require_relative "./xboard"
|
25
|
+
require_relative "./eval"
|
26
|
+
require_relative "./utils"
|
27
|
+
require_relative "./bench"
|
28
|
+
require_relative "./tokens"
|
29
|
+
require_relative "./board"
|
30
|
+
require_relative "./mgen"
|
31
|
+
require_relative "./mgen_white"
|
32
|
+
require_relative "./mgen_black"
|
33
|
+
require_relative "./perft"
|
34
|
+
require_relative "./tactics"
|
35
|
+
|
36
|
+
$stdout.sync = true
|
37
|
+
$stderr.sync = true
|
38
|
+
Thread.abort_on_exception = true
|
39
|
+
$stderr.reopen("ruby_shogi-error.txt", "a+")
|
40
|
+
|
41
|
+
module RubyShogi
|
42
|
+
NAME = "RubyShogi"
|
43
|
+
VERSION = "0.1"
|
44
|
+
AUTHOR = "Toni Helminen"
|
45
|
+
|
46
|
+
def RubyShogi.init
|
47
|
+
RubyShogi::Eval.init
|
48
|
+
RubyShogi::Zobrist.init
|
49
|
+
end
|
50
|
+
|
51
|
+
# Start RubyShogi
|
52
|
+
#
|
53
|
+
# Example:
|
54
|
+
# >> ruby_shogi -xboard
|
55
|
+
# => enter xboard mode
|
56
|
+
def RubyShogi.go
|
57
|
+
cmd = RubyShogi::Cmd.new
|
58
|
+
cmd.args
|
59
|
+
end
|
60
|
+
end # module RubyShogi
|
61
|
+
|
62
|
+
RubyShogi.init # init just once
|
63
|
+
|
64
|
+
if __FILE__ == $0
|
65
|
+
RubyShogi.go
|
66
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
##
|
2
|
+
# RubyShogi, a Shogi Engine
|
3
|
+
# Author: Toni Helminen
|
4
|
+
# License: GPLv3
|
5
|
+
##
|
6
|
+
|
7
|
+
module RubyShogi
|
8
|
+
|
9
|
+
module Tactics
|
10
|
+
TACTICS = [
|
11
|
+
["3k5/2P1P4/3K5/9/1N7/9/9/9/9[-] w 0 1", "b5c7="], # mate in 1
|
12
|
+
["4k4/9/4K4/9/9/9/9/9/9[G] w 0 1", "G@e8"], # mate in 1
|
13
|
+
["9/9/9/9/9/9/4K4/R4R3/6k2[-] w 0 1", "a2a1"] # mate in 1
|
14
|
+
]
|
15
|
+
|
16
|
+
def Tactics.run
|
17
|
+
puts "~~~ Tactics ~~~"
|
18
|
+
score, total = 0, 0
|
19
|
+
TACTICS.each do |tactic|
|
20
|
+
engine = RubyShogi::Engine.new
|
21
|
+
engine.printinfo = false
|
22
|
+
engine.board.fen(tactic[0])
|
23
|
+
engine.time = 50
|
24
|
+
result = engine.think
|
25
|
+
total += 1
|
26
|
+
score += 1 if tactic[1] == result
|
27
|
+
puts "#{total}. move #{result} | " + (tactic[1] == result ? "ok" : "error")
|
28
|
+
end
|
29
|
+
puts "= #{score} / #{total}"
|
30
|
+
end
|
31
|
+
end # module Tactics
|
32
|
+
|
33
|
+
end # module RubyShogi
|
@@ -0,0 +1,42 @@
|
|
1
|
+
##
|
2
|
+
# RubyShogi, a Shogi Engine
|
3
|
+
# Author: Toni Helminen
|
4
|
+
# License: GPLv3
|
5
|
+
##
|
6
|
+
|
7
|
+
module RubyShogi
|
8
|
+
|
9
|
+
class Tokens
|
10
|
+
def initialize(tokens)
|
11
|
+
@tokens = tokens
|
12
|
+
@token_i = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def peek(n)
|
16
|
+
return nil if @token_i + n < 0 || @token_i + n >= @tokens.length
|
17
|
+
@tokens[@token_i + n]
|
18
|
+
end
|
19
|
+
|
20
|
+
def ok?
|
21
|
+
return @token_i < @tokens.length ? true : false
|
22
|
+
end
|
23
|
+
|
24
|
+
def go_next
|
25
|
+
v = nil
|
26
|
+
if @token_i < @tokens.length
|
27
|
+
v = @tokens[@token_i]
|
28
|
+
@token_i += 1
|
29
|
+
end
|
30
|
+
return v
|
31
|
+
end
|
32
|
+
|
33
|
+
def forward
|
34
|
+
@token_i += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def cur
|
38
|
+
@tokens[@token_i]
|
39
|
+
end
|
40
|
+
end # class Tokens
|
41
|
+
|
42
|
+
end # module RubyShogi
|