4 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
6 ## This program 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 2 of the License, or
9 ## (at your option) any later version.
11 ## This program 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.
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 Max_Write_Queue_Size = 1000
21 Max_Identifier_Length = 32
22 Default_Timeout = 60 # for single socket operation
24 Default_Game_Name = "default-1500-0"
27 Least_Time_Per_Move = 1
28 Login_Time = 300 # time for LOGIN
30 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
31 Release.concat("-") if (Release == "")
32 Revision = "$Revision$".gsub(/[^\.\d]/, '')
45 TCPSocket.do_not_reverse_lookup = true
46 Thread.abort_on_exception = true
50 def gets_timeout(t = Default_Timeout)
61 def gets_safe(t = nil)
82 return self.write(str)
95 @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") )
97 attr_accessor :players, :games, :event
100 self.load(player) if player.id
101 @players[player.name] = player
105 @players.delete(player.name)
108 def get_player(status, game_name, sente, searcher=nil)
109 @players.each do |name, player|
110 if ((player.status == status) &&
111 (player.game_name == game_name) &&
112 ((sente == nil) || (player.sente == nil) || (player.sente == sente)) &&
113 ((searcher == nil) || (player != searcher)))
121 hash = search(player.id)
124 player.name = hash['name']
125 player.rate = hash['rate']
126 player.modified_at = hash['last_modified']
140 @db.transaction(true) do
141 @db.roots.each do |id|
145 return players.collect do |id|
156 # Idetifier of the player in the rating system
162 # Password of the player, which does not include a trip
163 attr_accessor :password
165 # Score in the rating sysem
168 # Last timestamp when the rate was modified
169 attr_accessor :modified_at
179 @modified_at || Time.now
185 @modified_at = Time.now
195 simple_name = @name.gsub(/@.*?$/, '')
196 "%s+%s" % [simple_name, @trip[0..8]]
203 # Parses str in the LOGIN command, sets up @id and @trip
205 def set_password(str)
206 if str && !str.empty?
207 @password = str.strip
208 @id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
210 @id = @password = nil
217 def Login.good_login?(str)
219 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
220 (tokens[0] == "LOGIN") &&
221 (good_identifier?(tokens[1])))
228 def Login.good_game_name?(str)
229 if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1)))
236 def Login.good_identifier?(str)
237 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
244 def Login.factory(str, player)
245 (login, player.name, password, ext) = str.chomp.split
246 player.set_password(password)
248 return Loginx1.new(player)
250 return LoginCSA.new(player)
254 attr_reader :player, :csa_1st_str
256 def initialize(player)
262 @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name))
265 def incorrect_duplicated_player(str)
266 @player.write_safe("LOGIN:incorrect\n")
267 @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4)
268 sleep 3 # wait for sending the above messages.
269 @palyer.name = "%s [duplicated]" % [@player.name]
274 class LoginCSA < Login
277 def initialize(player)
279 @player.protocol = PROTOCOL
284 log_message(sprintf("user %s run in CSA mode", @player.name))
285 if (self.class.good_game_name?(@player.password))
286 @csa_1st_str = "%%GAME #{@player.password} *"
288 @csa_1st_str = "%%GAME #{Default_Game_Name} *"
293 class Loginx1 < Login
296 def initialize(player)
298 @player.protocol = PROTOCOL
303 log_message(sprintf("user %s run in %s mode", @player.name, PROTOCOL))
304 @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL))
309 class Player < BasicPlayer
310 def initialize(str, socket)
313 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
315 @protocol = nil # CSA or x1
316 @eol = "\m" # favorite eol code
319 @mytime = 0 # set in start method also
321 @write_queue = Queue::new
322 @main_thread = Thread::current
323 @writer_thread = Thread::start do
329 attr_accessor :socket, :status
330 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
331 attr_accessor :main_thread, :writer_thread, :write_queue
334 log_message(sprintf("user %s killed", @name))
339 Thread::kill(@main_thread) if @main_thread
343 if (@status != "finished")
345 log_message(sprintf("user %s finish", @name))
346 # TODO you should confirm that there is no message in the queue.
347 Thread::kill(@writer_thread) if @writer_thread
349 @socket.close if (! @socket.closed?)
351 log_message(sprintf("user %s finish failed", @name))
357 @write_queue.push(str.gsub(/[\r\n]+/, @eol))
361 while (str = @write_queue.pop)
362 @socket.write_safe(str)
367 if ((status == "game_waiting") ||
368 (status == "start_waiting") ||
369 (status == "agree_waiting") ||
372 return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
373 elsif (@sente == false)
374 return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
375 elsif (@sente == nil)
376 return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
379 return sprintf("%s %s %s", @name, @protocol, @status)
384 @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
387 def run(csa_1st_str=nil)
388 while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
395 if (@write_queue.size > Max_Write_Queue_Size)
396 log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
400 if (@status == "finished")
403 str.chomp! if (str.class == String)
406 if (@status == "game")
407 array_str = str.split(",")
408 move = array_str.shift
409 additional = array_str.shift
410 if /^'(.*)/ =~ additional
411 comment = array_str.unshift("'*#{$1}")
413 s = @game.handle_one_move(move, self)
414 @game.fh.print("#{comment}\n") if (comment && !s)
415 return if (s && @protocol == LoginCSA::PROTOCOL)
417 when /^%[^%]/, :timeout
418 if (@status == "game")
419 s = @game.handle_one_move(str, self)
420 return if (s && @protocol == LoginCSA::PROTOCOL)
423 if (@status == "agree_waiting")
425 return if (@protocol == LoginCSA::PROTOCOL)
427 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
430 if (@status == "agree_waiting")
431 @status = "start_waiting"
432 if ((@game.sente.status == "start_waiting") &&
433 (@game.gote.status == "start_waiting"))
435 @game.sente.status = "game"
436 @game.gote.status = "game"
439 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
441 when /^%%SHOW\s+(\S+)/
443 if (LEAGUE.games[game_id])
444 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
446 write_safe("##[SHOW] +OK\n")
447 when /^%%MONITORON\s+(\S+)/
449 if (LEAGUE.games[game_id])
450 LEAGUE.games[game_id].monitoron(self)
451 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
452 write_safe("##[MONITOR][#{game_id}] +OK\n")
454 when /^%%MONITOROFF\s+(\S+)/
456 if (LEAGUE.games[game_id])
457 LEAGUE.games[game_id].monitoroff(self)
462 players = LEAGUE.rated_players
463 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
464 write_safe("##[RATING] %s \t %4d @%s\n" %
465 [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
467 write_safe("##[RATING] +OK\n")
469 write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
470 write_safe("##[VERSION] +OK\n")
472 if ((@status == "connected") || (@status == "game_waiting"))
473 @status = "connected"
476 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
478 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
482 if (! Login::good_game_name?(game_name))
483 write_safe(sprintf("##[ERROR] bad game name\n"))
485 elsif ((@status == "connected") || (@status == "game_waiting"))
488 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
491 if ((my_sente_str == "*") ||
492 (my_sente_str == "+") ||
493 (my_sente_str == "-"))
496 write_safe(sprintf("##[ERROR] bad game option\n"))
500 if (my_sente_str == "*")
501 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
502 elsif (my_sente_str == "+")
503 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
504 elsif (my_sente_str == "-")
505 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
510 @game_name = game_name
511 if ((my_sente_str == "*") && (rival.sente == nil))
519 elsif (rival.sente == true) # rival has higher priority
521 elsif (rival.sente == false)
523 elsif (my_sente_str == "+")
526 elsif (my_sente_str == "-")
532 Game::new(@game_name, self, rival)
533 self.status = "agree_waiting"
534 rival.status = "agree_waiting"
535 else # rival not found
536 if (command_name == "GAME")
537 @status = "game_waiting"
538 @game_name = game_name
539 if (my_sente_str == "+")
541 elsif (my_sente_str == "-")
547 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
548 @status = "connected"
553 when /^%%CHAT\s+(.+)/
555 LEAGUE.players.each do |name, player|
556 if (player.protocol != LoginCSA::PROTOCOL)
557 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
562 LEAGUE.games.each do |id, game|
563 buf.push(sprintf("##[LIST] %s\n", id))
565 buf.push("##[LIST] +OK\n")
569 LEAGUE.players.each do |name, player|
570 buf.push(sprintf("##[WHO] %s\n", player.to_s))
572 buf.push("##[WHO] +OK\n")
575 @status = "connected"
576 write_safe("LOGOUT:completed\n")
579 ## ignore null string
581 write_safe(sprintf("##[ERROR] unknown command %s\n", str))
591 PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
592 def initialize(board, x, y, sente, promoted=false)
599 if ((x == 0) || (y == 0))
601 hands = board.sente_hands
603 hands = board.gote_hands
610 @board.array[x][y] = self
613 attr_accessor :promoted, :sente, :x, :y, :board
615 def room_of_head?(x, y, name)
620 return adjacent_movable_grids + far_movable_grids
623 def far_movable_grids
628 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
629 if ((@board.array[x][y] == nil) || # dst is empty
630 (@board.array[x][y].sente != @sente)) # dst is enemy
638 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
639 if (@board.array[x][y] == nil) # dst is empty?
646 def adjacent_movable_grids
649 moves = @promoted_moves
651 moves = @normal_moves
653 moves.each do |(dx, dy)|
660 if (jump_to?(cand_x, cand_y))
661 grids.push([cand_x, cand_y])
667 def move_to?(x, y, name)
668 return false if (! room_of_head?(x, y, name))
669 return false if ((name != @name) && (name != @promoted_name))
670 return false if (@promoted && (name != @promoted_name)) # can't un-promote
673 return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
675 return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
677 return false if ((6 >= @y) && (6 >= y) && (name != @name))
681 if ((@x == 0) || (@y == 0))
682 return jump_to?(x, y)
684 return movable_grids.include?([x, y])
689 if ((@x == 0) || (@y == 0))
691 @board.sente_hands.delete(self)
693 @board.gote_hands.delete(self)
695 @board.array[x][y] = self
696 elsif ((x == 0) || (y == 0))
697 @promoted = false # clear promoted flag before moving to hands
699 @board.sente_hands.push(self)
701 @board.gote_hands.push(self)
703 @board.array[@x][@y] = nil
705 @board.array[@x][@y] = nil
706 @board.array[x][y] = self
739 class PieceFU < Piece
742 @normal_moves = [[0, +1]]
743 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
745 @promoted_name = "TO"
748 def room_of_head?(x, y, name)
751 return false if (y == 1)
753 return false if (y == 9)
759 if ((iy != @y) && # not source position
760 @board.array[x][iy] &&
761 (@board.array[x][iy].sente == @sente) && # mine
762 (@board.array[x][iy].name == "FU") &&
763 (@board.array[x][iy].promoted == false))
773 class PieceKY < Piece
777 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
779 @promoted_name = "NY"
782 def room_of_head?(x, y, name)
785 return false if (y == 1)
787 return false if (y == 9)
792 def far_movable_grids
800 while (jump_to?(cand_x, cand_y))
801 grids.push([cand_x, cand_y])
802 break if (! put_to?(cand_x, cand_y))
808 while (jump_to?(cand_x, cand_y))
809 grids.push([cand_x, cand_y])
810 break if (! put_to?(cand_x, cand_y))
818 class PieceKE < Piece
821 @normal_moves = [[+1, +2], [-1, +2]]
822 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
824 @promoted_name = "NK"
827 def room_of_head?(x, y, name)
830 return false if ((y == 1) || (y == 2))
832 return false if ((y == 9) || (y == 8))
838 class PieceGI < Piece
841 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
842 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
844 @promoted_name = "NG"
848 class PieceKI < Piece
851 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
858 class PieceKA < Piece
862 @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
864 @promoted_name = "UM"
867 def far_movable_grids
872 while (jump_to?(cand_x, cand_y))
873 grids.push([cand_x, cand_y])
874 break if (! put_to?(cand_x, cand_y))
881 while (jump_to?(cand_x, cand_y))
882 grids.push([cand_x, cand_y])
883 break if (! put_to?(cand_x, cand_y))
890 while (jump_to?(cand_x, cand_y))
891 grids.push([cand_x, cand_y])
892 break if (! put_to?(cand_x, cand_y))
899 while (jump_to?(cand_x, cand_y))
900 grids.push([cand_x, cand_y])
901 break if (! put_to?(cand_x, cand_y))
908 class PieceHI < Piece
912 @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
914 @promoted_name = "RY"
917 def far_movable_grids
922 while (jump_to?(cand_x, cand_y))
923 grids.push([cand_x, cand_y])
924 break if (! put_to?(cand_x, cand_y))
930 while (jump_to?(cand_x, cand_y))
931 grids.push([cand_x, cand_y])
932 break if (! put_to?(cand_x, cand_y))
938 while (jump_to?(cand_x, cand_y))
939 grids.push([cand_x, cand_y])
940 break if (! put_to?(cand_x, cand_y))
946 while (jump_to?(cand_x, cand_y))
947 grids.push([cand_x, cand_y])
948 break if (! put_to?(cand_x, cand_y))
954 class PieceOU < Piece
957 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
967 @sente_hands = Array::new
968 @gote_hands = Array::new
970 @sente_history = Hash::new
971 @gote_history = Hash::new
972 @array = [[], [], [], [], [], [], [], [], [], []]
975 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
976 attr_reader :move_count
979 PieceKY::new(self, 1, 1, false)
980 PieceKE::new(self, 2, 1, false)
981 PieceGI::new(self, 3, 1, false)
982 PieceKI::new(self, 4, 1, false)
983 PieceOU::new(self, 5, 1, false)
984 PieceKI::new(self, 6, 1, false)
985 PieceGI::new(self, 7, 1, false)
986 PieceKE::new(self, 8, 1, false)
987 PieceKY::new(self, 9, 1, false)
988 PieceKA::new(self, 2, 2, false)
989 PieceHI::new(self, 8, 2, false)
990 PieceFU::new(self, 1, 3, false)
991 PieceFU::new(self, 2, 3, false)
992 PieceFU::new(self, 3, 3, false)
993 PieceFU::new(self, 4, 3, false)
994 PieceFU::new(self, 5, 3, false)
995 PieceFU::new(self, 6, 3, false)
996 PieceFU::new(self, 7, 3, false)
997 PieceFU::new(self, 8, 3, false)
998 PieceFU::new(self, 9, 3, false)
1000 PieceKY::new(self, 1, 9, true)
1001 PieceKE::new(self, 2, 9, true)
1002 PieceGI::new(self, 3, 9, true)
1003 PieceKI::new(self, 4, 9, true)
1004 PieceOU::new(self, 5, 9, true)
1005 PieceKI::new(self, 6, 9, true)
1006 PieceGI::new(self, 7, 9, true)
1007 PieceKE::new(self, 8, 9, true)
1008 PieceKY::new(self, 9, 9, true)
1009 PieceKA::new(self, 8, 8, true)
1010 PieceHI::new(self, 2, 8, true)
1011 PieceFU::new(self, 1, 7, true)
1012 PieceFU::new(self, 2, 7, true)
1013 PieceFU::new(self, 3, 7, true)
1014 PieceFU::new(self, 4, 7, true)
1015 PieceFU::new(self, 5, 7, true)
1016 PieceFU::new(self, 6, 7, true)
1017 PieceFU::new(self, 7, 7, true)
1018 PieceFU::new(self, 8, 7, true)
1019 PieceFU::new(self, 9, 7, true)
1022 def have_piece?(hands, name)
1023 piece = hands.find { |i|
1029 def move_to(x0, y0, x1, y1, name, sente)
1031 hands = @sente_hands
1036 if ((x0 == 0) || (y0 == 0))
1037 piece = have_piece?(hands, name)
1038 return :illegal if (! piece.move_to?(x1, y1, name))
1039 piece.move_to(x1, y1)
1041 return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
1042 if (@array[x0][y0].name != name) # promoted ?
1043 @array[x0][y0].promoted = true
1046 if (@array[x1][y1].name == "OU")
1047 return :outori # return board update
1049 @array[x1][y1].sente = @array[x0][y0].sente
1050 @array[x1][y1].move_to(0, 0)
1055 @array[x0][y0].move_to(x1, y1)
1061 def look_for_ou(sente)
1067 (@array[x][y].name == "OU") &&
1068 (@array[x][y].sente == sente))
1075 raise "can't find ou"
1078 def checkmated?(sente) # sente is loosing
1079 ou = look_for_ou(sente)
1085 (@array[x][y].sente != sente))
1086 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1097 def uchifuzume?(sente)
1098 rival_ou = look_for_ou(! sente) # rival's ou
1099 if (sente) # rival is gote
1100 if ((rival_ou.y != 9) &&
1101 (@array[rival_ou.x][rival_ou.y + 1]) &&
1102 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1103 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1105 fu_y = rival_ou.y + 1
1110 if ((rival_ou.y != 0) &&
1111 (@array[rival_ou.x][rival_ou.y - 1]) &&
1112 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1113 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1115 fu_y = rival_ou.y - 1
1121 ## case: rival_ou is moving
1123 rival_ou.movable_grids.each do |(cand_x, cand_y)|
1124 tmp_board = Marshal.load(Marshal.dump(self))
1125 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1126 raise "internal error" if (s != true)
1127 if (! tmp_board.checkmated?(! sente)) # good move
1132 ## case: rival is capturing fu
1138 (@array[x][y].sente != sente) &&
1139 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1140 if (@array[x][y].promoted)
1141 name = @array[x][y].promoted_name
1143 name = @array[x][y].name
1145 tmp_board = Marshal.load(Marshal.dump(self))
1146 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1147 raise "internal error" if (s != true)
1148 if (! tmp_board.checkmated?(! sente)) # good move
1159 def oute_sennichite?(sente)
1160 if (checkmated?(! sente))
1163 if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1167 if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1175 def sennichite?(sente)
1177 if (@history[str] && (@history[str] >= 3)) # already 3 times
1183 def good_kachi?(sente)
1184 if (checkmated?(sente))
1185 puts "'NG: Checkmating." if $DEBUG
1189 ou = look_for_ou(sente)
1190 if (sente && (ou.y >= 4))
1191 puts "'NG: Black's OU does not enter yet." if $DEBUG
1194 if (! sente && (ou.y <= 6))
1195 puts "'NG: White's OU does not enter yet." if $DEBUG
1203 hands = @sente_hands
1213 (@array[x][y].sente == sente) &&
1214 (@array[x][y].point > 0))
1215 point = point + @array[x][y].point
1221 hands.each do |piece|
1222 point = point + piece.point
1226 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1231 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1236 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1241 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1245 def handle_one_move(str, sente=nil)
1246 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1253 elsif (str =~ /^%KACHI/)
1254 raise ArgumentError, "sente is null", caller if sente == nil
1255 if (good_kachi?(sente))
1260 elsif (str =~ /^%TORYO/)
1266 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1267 ((x0 != 0) || (y0 != 0)))
1269 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1275 hands = @sente_hands
1282 if ((x0 == 0) && (y0 == 0))
1283 return :illegal if (! have_piece?(hands, name))
1284 elsif (! @array[x0][y0])
1285 return :illegal # no piece
1286 elsif (@array[x0][y0].sente != sente)
1287 return :illegal # this is not mine
1288 elsif (@array[x0][y0].name != name)
1289 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1292 ## destination check
1293 if (@array[x1][y1] &&
1294 (@array[x1][y1].sente == sente)) # can't capture mine
1296 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1297 return :illegal # can't put on existing piece
1300 tmp_board = Marshal.load(Marshal.dump(self))
1301 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1302 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1303 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1304 return :sennichite if tmp_board.sennichite?(sente)
1306 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1310 move_to(x0, y0, x1, y1, name, sente)
1313 if (checkmated?(! sente))
1315 @sente_history[str] = (@sente_history[str] || 0) + 1
1317 @gote_history[str] = (@gote_history[str] || 0) + 1
1321 @sente_history.clear
1326 @history[str] = (@history[str] || 0) + 1
1334 a.push(sprintf("P%d", y))
1337 piece = @array[x][y]
1346 a.push(sprintf("\n"))
1349 if (! sente_hands.empty?)
1351 sente_hands.each do |p|
1352 a.push("00" + p.name)
1356 if (! gote_hands.empty?)
1358 gote_hands.each do |p|
1359 a.push("00" + p.name)
1369 attr_reader :players, :black, :white
1371 def initialize(p1, p2)
1375 if p1.sente && !p2.sente
1376 @black, @white = p1, p2
1377 elsif !p1.sente && p2.sente
1378 @black, @white = p2, p1
1380 raise "Never reached!"
1385 class GameResultWin < GameResult
1386 attr_reader :winner, :loser
1388 def initialize(winner, loser)
1390 @winner, @loser = winner, loser
1394 black_name = @black.id || @black.name
1395 white_name = @white.id || @white.name
1396 "%s:%s" % [black_name, white_name]
1400 class GameResultDraw < GameResult
1408 def initialize(game_name, player0, player1)
1409 @monitors = Array::new
1410 @game_name = game_name
1411 if (@game_name =~ /-(\d+)-(\d+)$/)
1412 @total_time = $1.to_i
1423 @current_player = @sente
1424 @next_player = @gote
1432 @sente.status = "agree_waiting"
1433 @gote.status = "agree_waiting"
1435 @id = sprintf("%s+%s+%s+%s+%s",
1436 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1437 @logfile = @id + ".csa"
1439 LEAGUE.games[@id] = self
1441 log_message(sprintf("game created %s", @id))
1451 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1452 attr_accessor :last_move, :current_turn
1456 @sente.rated? && @gote.rated?
1459 def monitoron(monitor)
1460 @monitors.delete(monitor)
1461 @monitors.push(monitor)
1464 def monitoroff(monitor)
1465 @monitors.delete(monitor)
1468 def reject(rejector)
1469 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1470 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1475 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1477 elsif (@current_player == killer)
1484 log_message(sprintf("game finished %s", @id))
1485 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1490 @sente.status = "connected"
1491 @gote.status = "connected"
1493 if (@current_player.protocol == LoginCSA::PROTOCOL)
1494 @current_player.finish
1496 if (@next_player.protocol == LoginCSA::PROTOCOL)
1499 @monitors = Array::new
1502 @current_player = nil
1504 LEAGUE.games.delete(@id)
1507 def handle_one_move(str, player)
1509 if (@current_player == player)
1510 @end_time = Time::new
1511 t = (@end_time - @start_time).floor
1512 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1515 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1517 elsif (str == :timeout)
1518 return false # time isn't expired. players aren't swapped. continue game
1520 @current_player.mytime = @current_player.mytime - t
1521 if (@current_player.mytime < 0)
1522 @current_player.mytime = 0
1526 move_status = @board.handle_one_move(str, @sente == @current_player)
1528 # log_error("handle_one_move raise exception for #{str}")
1529 # move_status = :illegal
1532 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1533 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1535 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1536 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1537 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1538 @fh.printf("%s\nT%d\n", str, t)
1539 @last_move = sprintf("%s,T%d", str, t)
1540 @current_turn = @current_turn + 1
1543 @monitors.each do |monitor|
1544 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1545 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1550 if (@next_player.status != "game") # rival is logout or disconnected
1552 elsif (status == :timeout)
1554 elsif (move_status == :illegal)
1556 elsif (move_status == :kachi_win)
1558 elsif (move_status == :kachi_lose)
1560 elsif (move_status == :toryo)
1562 elsif (move_status == :outori)
1564 elsif (move_status == :sennichite)
1566 elsif (move_status == :oute_sennichite)
1567 oute_sennichite_lose()
1568 elsif (move_status == :uchifuzume)
1570 elsif (move_status == :oute_kaihimore)
1571 oute_kaihimore_lose()
1575 finish() if finish_flag
1576 (@current_player, @next_player) = [@next_player, @current_player]
1577 @start_time = Time::new
1583 @current_player.status = "connected"
1584 @next_player.status = "connected"
1585 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1586 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1587 @fh.printf("%%TORYO\n")
1588 @fh.print(@board.to_s.gsub(/^/, "\'"))
1589 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1590 @result = GameResultWin.new(@current_player, @next_player)
1591 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1592 @monitors.each do |monitor|
1593 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1598 @current_player.status = "connected"
1599 @next_player.status = "connected"
1600 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1601 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1602 @fh.printf("%%TORYO\n")
1603 @fh.print(@board.to_s.gsub(/^/, "\'"))
1604 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1605 @result = GameResultWin.new(@next_player, @current_player)
1606 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1607 @monitors.each do |monitor|
1608 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1613 @current_player.status = "connected"
1614 @next_player.status = "connected"
1615 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1616 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1617 @fh.print(@board.to_s.gsub(/^/, "\'"))
1618 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1619 @result = GameResultDraw.new(@current_player, @next_player)
1620 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1621 @monitors.each do |monitor|
1622 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1626 def oute_sennichite_lose
1627 @current_player.status = "connected"
1628 @next_player.status = "connected"
1629 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1630 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1631 @fh.print(@board.to_s.gsub(/^/, "\'"))
1632 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1633 @result = GameResultWin.new(@next_player, @current_player)
1634 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1635 @monitors.each do |monitor|
1636 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1641 @current_player.status = "connected"
1642 @next_player.status = "connected"
1643 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1644 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1645 @fh.print(@board.to_s.gsub(/^/, "\'"))
1646 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1647 @result = GameResultWin.new(@next_player, @current_player)
1648 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1649 @monitors.each do |monitor|
1650 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1655 @current_player.status = "connected"
1656 @next_player.status = "connected"
1657 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1658 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1659 @fh.print(@board.to_s.gsub(/^/, "\'"))
1660 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1661 @result = GameResultWin.new(@next_player, @current_player)
1662 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1663 @monitors.each do |monitor|
1664 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1668 def oute_kaihimore_lose
1669 @current_player.status = "connected"
1670 @next_player.status = "connected"
1671 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1672 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1673 @fh.print(@board.to_s.gsub(/^/, "\'"))
1674 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1675 @result = GameResultWin.new(@next_player, @current_player)
1676 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1677 @monitors.each do |monitor|
1678 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1683 @current_player.status = "connected"
1684 @next_player.status = "connected"
1685 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1686 @next_player.write_safe("#TIME_UP\n#WIN\n")
1687 @fh.print(@board.to_s.gsub(/^/, "\'"))
1688 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1689 @result = GameResultWin.new(@next_player, @current_player)
1690 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1691 @monitors.each do |monitor|
1692 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1697 @current_player.status = "connected"
1698 @next_player.status = "connected"
1699 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1700 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1701 @fh.printf("%%KACHI\n")
1702 @fh.print(@board.to_s.gsub(/^/, "\'"))
1703 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1704 @result = GameResultWin.new(@current_player, @next_player)
1705 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1706 @monitors.each do |monitor|
1707 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1712 @current_player.status = "connected"
1713 @next_player.status = "connected"
1714 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1715 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1716 @fh.printf("%%KACHI\n")
1717 @fh.print(@board.to_s.gsub(/^/, "\'"))
1718 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1719 @result = GameResultWin.new(@next_player, @current_player)
1720 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1721 @monitors.each do |monitor|
1722 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1727 @current_player.status = "connected"
1728 @next_player.status = "connected"
1729 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1730 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1731 @fh.printf("%%TORYO\n")
1732 @fh.print(@board.to_s.gsub(/^/, "\'"))
1733 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1734 @result = GameResultWin.new(@next_player, @current_player)
1735 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1736 @monitors.each do |monitor|
1737 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1742 @current_player.status = "connected"
1743 @next_player.status = "connected"
1744 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1745 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1746 @fh.print(@board.to_s.gsub(/^/, "\'"))
1747 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1748 @result = GameResultWin.new(@current_player, @next_player)
1749 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1750 @monitors.each do |monitor|
1751 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1756 log_message(sprintf("game started %s", @id))
1757 @sente.write_safe(sprintf("START:%s\n", @id))
1758 @gote.write_safe(sprintf("START:%s\n", @id))
1759 @sente.mytime = @total_time
1760 @gote.mytime = @total_time
1761 @start_time = Time::new
1766 @fh = open(@logfile, "w")
1770 @fh.printf("N+%s\n", @sente.name)
1771 @fh.printf("N-%s\n", @gote.name)
1772 @fh.printf("$EVENT:%s\n", @id)
1774 @sente.write_safe(propose_message("+"))
1775 @gote.write_safe(propose_message("-"))
1777 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1779 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1780 P2 * -HI * * * * * -KA *
1781 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1782 P4 * * * * * * * * *
1783 P5 * * * * * * * * *
1784 P6 * * * * * * * * *
1785 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1786 P8 * +KA * * * * * +HI *
1787 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1796 Protocol_Version:1.1
1797 Protocol_Mode:Server
1799 Declaration:Jishogi 1.1
1801 Name+:#{@sente.name}
1807 Total_Time:#{@total_time}
1809 Least_Time_Per_Move:#{Least_Time_Per_Move}
1810 Remaining_Time+:#{@sente.mytime}
1811 Remaining_Time-:#{@gote.mytime}
1812 Last_Move:#{@last_move}
1813 Current_Turn:#{@current_turn}
1823 return str0 + @board.to_s + str1
1826 def propose_message(sg_flag)
1829 Protocol_Version:1.1
1830 Protocol_Mode:Server
1832 Declaration:Jishogi 1.1
1834 Name+:#{@sente.name}
1836 Your_Turn:#{sg_flag}
1841 Total_Time:#{@total_time}
1843 Least_Time_Per_Move:#{Least_Time_Per_Move}
1846 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1847 P2 * -HI * * * * * -KA *
1848 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1849 P4 * * * * * * * * *
1850 P5 * * * * * * * * *
1851 P6 * * * * * * * * *
1852 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1853 P8 * +KA * * * * * +HI *
1854 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1866 def issue_current_time
1867 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1868 @@mutex.synchronize do
1869 while time <= @@time do
1877 #################################################
1884 shogi-server - server for CSA server protocol
1887 shogi-server event_name port_number
1890 server for CSA server protocol
1894 specify filename for logging process ID
1897 this file is distributed under GPL version2 and might be compiled by Exerb
1909 def log_message(str)
1910 printf("%s message: %s\n", Time::new.to_s, str)
1913 def log_warning(str)
1914 printf("%s warning: %s\n", Time::new.to_s, str)
1918 printf("%s error: %s\n", Time::new.to_s, str)
1922 def parse_command_line
1924 parser = GetoptLong.new
1925 parser.ordering = GetoptLong::REQUIRE_ORDER
1927 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1931 parser.each_option do |name, arg|
1932 name.sub!(/^--/, '')
1933 options[name] = arg.dup
1937 raise parser.error_message
1942 def write_pid_file(file)
1943 open(file, "w") do |fh|
1944 fh.print Process::pid, "\n"
1948 def mutex_watchdog(mutex, sec)
1960 log_error("mutex watchdog timeout")
1970 mutex_watchdog($mutex, 10)
1973 $options = parse_command_line
1974 if (ARGV.length != 2)
1979 LEAGUE.event = ARGV.shift
1982 write_pid_file($options["pid-file"]) if ($options["pid-file"])
1984 server = TCPserver.open(port)
1985 log_message("server started")
1988 Thread::start(server.accept) do |client|
1993 while (str = client.gets_timeout(Login_Time))
1998 if (Login::good_login?(str))
1999 player = Player::new(str, client)
2001 login = Login::factory(str, player)
2002 if (LEAGUE.players[player.name])
2003 if ((LEAGUE.players[player.name].password == player.password) &&
2004 (LEAGUE.players[player.name].status != "game"))
2005 log_message(sprintf("user %s login forcely", player.name))
2006 LEAGUE.players[player.name].kill
2008 login.incorrect_duplicated_player(str)
2016 client.write_safe("LOGIN:incorrect" + eol)
2017 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2028 log_message(sprintf("user %s login", player.name))
2030 player.run(login.csa_1st_str)
2034 player.game.kill(player)
2036 player.finish # socket has been closed
2037 LEAGUE.delete(player)
2038 log_message(sprintf("user %s logout", player.name))
2047 LEAGUE = League::new