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
30 def gets_timeout(t = Default_Timeout)
41 def gets_safe(t = nil)
62 return self.write(str)
70 module ShogiServer # for a namespace
72 Max_Write_Queue_Size = 1000
73 Max_Identifier_Length = 32
74 Default_Timeout = 60 # for single socket operation
76 Default_Game_Name = "default-1500-0"
79 Least_Time_Per_Move = 1
80 Login_Time = 300 # time for LOGIN
82 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
83 Release.concat("-") if (Release == "")
84 Revision = "$Revision$".gsub(/[^\.\d]/, '')
92 @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") )
94 attr_accessor :players, :games, :event
97 self.load(player) if player.id
98 @players[player.name] = player
102 @players.delete(player.name)
105 def get_player(status, game_name, sente, searcher=nil)
106 @players.each do |name, player|
107 if ((player.status == status) &&
108 (player.game_name == game_name) &&
109 ((sente == nil) || (player.sente == nil) || (player.sente == sente)) &&
110 ((searcher == nil) || (player != searcher)))
118 hash = search(player.id)
121 player.name = hash['name']
122 player.rate = hash['rate']
123 player.modified_at = hash['last_modified']
124 player.rating_group = hash['rating_group']
131 @db["players"].each do |group, players|
141 @db.transaction(true) do
142 @db["players"].each do |group, players_hash|
143 players << players_hash.keys
146 return players.flatten.collect do |id|
156 ######################################################
157 # Processes the LOGIN command.
160 def Login.good_login?(str)
162 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
163 (tokens[0] == "LOGIN") &&
164 (good_identifier?(tokens[1])))
171 def Login.good_game_name?(str)
172 if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1)))
179 def Login.good_identifier?(str)
180 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
187 def Login.factory(str, player)
188 (login, player.name, password, ext) = str.chomp.split
190 return Loginx1.new(player, password)
192 return LoginCSA.new(player, password)
198 # the first command that will be executed just after LOGIN.
199 # If it is nil, the default process will be started.
200 attr_reader :csa_1st_str
202 def initialize(player, password)
205 parse_password(password)
209 @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name))
210 log_message(sprintf("user %s run in %s mode", @player.name, @player.protocol))
213 def incorrect_duplicated_player(str)
214 @player.write_safe("LOGIN:incorrect\n")
215 @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4)
216 sleep 3 # wait for sending the above messages.
217 @player.name = "%s [duplicated]" % [@player.name]
222 ######################################################
223 # Processes LOGIN for the CSA standard mode.
225 class LoginCSA < Login
228 def initialize(player, password)
231 @player.protocol = PROTOCOL
234 def parse_password(password)
235 if Login.good_game_name?(password)
237 @player.set_password(nil)
238 elsif password.split(",").size > 1
239 @gamename, *trip = password.split(",")
240 @player.set_password(trip.join(","))
242 @player.set_password(password)
243 @gamename = Default_Game_Name
245 @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name
250 @csa_1st_str = "%%GAME #{@gamename} *"
254 ######################################################
255 # Processes LOGIN for the extented mode.
257 class Loginx1 < Login
260 def initialize(player, password)
262 @player.protocol = PROTOCOL
265 def parse_password(password)
266 @player.set_password(password)
271 @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL))
277 # Idetifier of the player in the rating system
283 # Password of the player, which does not include a trip
284 attr_accessor :password
286 # Score in the rating sysem
289 # Group in the rating system
290 attr_accessor :rating_group
292 # Last timestamp when the rate was modified
293 attr_accessor :modified_at
301 @modified_at || Time.now
307 @modified_at = Time.now
317 simple_name = @name.gsub(/@.*?$/, '')
318 "%s+%s" % [simple_name, @trip[0..8]]
325 # Parses str in the LOGIN command, sets up @id and @trip
327 def set_password(str)
328 if str && !str.empty?
329 @password = str.strip
330 @id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
332 @id = @password = nil
338 class Player < BasicPlayer
339 def initialize(str, socket)
342 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
344 @protocol = nil # CSA or x1
345 @eol = "\m" # favorite eol code
348 @mytime = 0 # set in start method also
350 @write_queue = Queue::new
351 @main_thread = Thread::current
352 @writer_thread = Thread::start do
358 attr_accessor :socket, :status
359 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
360 attr_accessor :main_thread, :writer_thread, :write_queue
363 log_message(sprintf("user %s killed", @name))
368 Thread::kill(@main_thread) if @main_thread
372 if (@status != "finished")
374 log_message(sprintf("user %s finish", @name))
375 # TODO you should confirm that there is no message in the queue.
376 Thread::kill(@writer_thread) if @writer_thread
378 @socket.close if (! @socket.closed?)
380 log_message(sprintf("user %s finish failed", @name))
386 @write_queue.push(str.gsub(/[\r\n]+/, @eol))
390 while (str = @write_queue.pop)
391 @socket.write_safe(str)
396 if ((status == "game_waiting") ||
397 (status == "start_waiting") ||
398 (status == "agree_waiting") ||
401 return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
402 elsif (@sente == false)
403 return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
404 elsif (@sente == nil)
405 return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
408 return sprintf("%s %s %s", @name, @protocol, @status)
413 @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
416 def run(csa_1st_str=nil)
417 while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
424 if (@write_queue.size > Max_Write_Queue_Size)
425 log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
429 if (@status == "finished")
432 str.chomp! if (str.class == String)
435 if (@status == "game")
436 array_str = str.split(",")
437 move = array_str.shift
438 additional = array_str.shift
439 if /^'(.*)/ =~ additional
440 comment = array_str.unshift("'*#{$1}")
442 s = @game.handle_one_move(move, self)
443 @game.fh.print("#{comment}\n") if (comment && !s)
444 return if (s && @protocol == LoginCSA::PROTOCOL)
446 when /^%[^%]/, :timeout
447 if (@status == "game")
448 s = @game.handle_one_move(str, self)
449 return if (s && @protocol == LoginCSA::PROTOCOL)
452 if (@status == "agree_waiting")
454 return if (@protocol == LoginCSA::PROTOCOL)
456 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
459 if (@status == "agree_waiting")
460 @status = "start_waiting"
461 if ((@game.sente.status == "start_waiting") &&
462 (@game.gote.status == "start_waiting"))
464 @game.sente.status = "game"
465 @game.gote.status = "game"
468 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
470 when /^%%SHOW\s+(\S+)/
472 if (LEAGUE.games[game_id])
473 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
475 write_safe("##[SHOW] +OK\n")
476 when /^%%MONITORON\s+(\S+)/
478 if (LEAGUE.games[game_id])
479 LEAGUE.games[game_id].monitoron(self)
480 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
481 write_safe("##[MONITOR][#{game_id}] +OK\n")
483 when /^%%MONITOROFF\s+(\S+)/
485 if (LEAGUE.games[game_id])
486 LEAGUE.games[game_id].monitoroff(self)
491 players = LEAGUE.rated_players
492 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
493 write_safe("##[RATING] %s \t %4d @%s\n" %
494 [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
496 write_safe("##[RATING] +OK\n")
498 write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
499 write_safe("##[VERSION] +OK\n")
501 if ((@status == "connected") || (@status == "game_waiting"))
502 @status = "connected"
505 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
507 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
511 if (! Login::good_game_name?(game_name))
512 write_safe(sprintf("##[ERROR] bad game name\n"))
514 elsif ((@status == "connected") || (@status == "game_waiting"))
517 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
520 if ((my_sente_str == "*") ||
521 (my_sente_str == "+") ||
522 (my_sente_str == "-"))
525 write_safe(sprintf("##[ERROR] bad game option\n"))
529 if (my_sente_str == "*")
530 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
531 elsif (my_sente_str == "+")
532 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
533 elsif (my_sente_str == "-")
534 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
539 @game_name = game_name
540 if ((my_sente_str == "*") && (rival.sente == nil))
548 elsif (rival.sente == true) # rival has higher priority
550 elsif (rival.sente == false)
552 elsif (my_sente_str == "+")
555 elsif (my_sente_str == "-")
561 Game::new(@game_name, self, rival)
562 self.status = "agree_waiting"
563 rival.status = "agree_waiting"
564 else # rival not found
565 if (command_name == "GAME")
566 @status = "game_waiting"
567 @game_name = game_name
568 if (my_sente_str == "+")
570 elsif (my_sente_str == "-")
576 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
577 @status = "connected"
582 when /^%%CHAT\s+(.+)/
584 LEAGUE.players.each do |name, player|
585 if (player.protocol != LoginCSA::PROTOCOL)
586 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
591 LEAGUE.games.each do |id, game|
592 buf.push(sprintf("##[LIST] %s\n", id))
594 buf.push("##[LIST] +OK\n")
598 LEAGUE.players.each do |name, player|
599 buf.push(sprintf("##[WHO] %s\n", player.to_s))
601 buf.push("##[WHO] +OK\n")
604 @status = "connected"
605 write_safe("LOGOUT:completed\n")
608 ## ignore null string
610 write_safe(sprintf("##[ERROR] unknown command %s\n", str))
620 PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
621 def initialize(board, x, y, sente, promoted=false)
628 if ((x == 0) || (y == 0))
630 hands = board.sente_hands
632 hands = board.gote_hands
639 @board.array[x][y] = self
642 attr_accessor :promoted, :sente, :x, :y, :board
644 def room_of_head?(x, y, name)
649 return adjacent_movable_grids + far_movable_grids
652 def far_movable_grids
657 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
658 if ((@board.array[x][y] == nil) || # dst is empty
659 (@board.array[x][y].sente != @sente)) # dst is enemy
667 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
668 if (@board.array[x][y] == nil) # dst is empty?
675 def adjacent_movable_grids
678 moves = @promoted_moves
680 moves = @normal_moves
682 moves.each do |(dx, dy)|
689 if (jump_to?(cand_x, cand_y))
690 grids.push([cand_x, cand_y])
696 def move_to?(x, y, name)
697 return false if (! room_of_head?(x, y, name))
698 return false if ((name != @name) && (name != @promoted_name))
699 return false if (@promoted && (name != @promoted_name)) # can't un-promote
702 return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
704 return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
706 return false if ((6 >= @y) && (6 >= y) && (name != @name))
710 if ((@x == 0) || (@y == 0))
711 return jump_to?(x, y)
713 return movable_grids.include?([x, y])
718 if ((@x == 0) || (@y == 0))
720 @board.sente_hands.delete(self)
722 @board.gote_hands.delete(self)
724 @board.array[x][y] = self
725 elsif ((x == 0) || (y == 0))
726 @promoted = false # clear promoted flag before moving to hands
728 @board.sente_hands.push(self)
730 @board.gote_hands.push(self)
732 @board.array[@x][@y] = nil
734 @board.array[@x][@y] = nil
735 @board.array[x][y] = self
768 class PieceFU < Piece
771 @normal_moves = [[0, +1]]
772 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
774 @promoted_name = "TO"
777 def room_of_head?(x, y, name)
780 return false if (y == 1)
782 return false if (y == 9)
788 if ((iy != @y) && # not source position
789 @board.array[x][iy] &&
790 (@board.array[x][iy].sente == @sente) && # mine
791 (@board.array[x][iy].name == "FU") &&
792 (@board.array[x][iy].promoted == false))
802 class PieceKY < Piece
806 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
808 @promoted_name = "NY"
811 def room_of_head?(x, y, name)
814 return false if (y == 1)
816 return false if (y == 9)
821 def far_movable_grids
829 while (jump_to?(cand_x, cand_y))
830 grids.push([cand_x, cand_y])
831 break if (! put_to?(cand_x, cand_y))
837 while (jump_to?(cand_x, cand_y))
838 grids.push([cand_x, cand_y])
839 break if (! put_to?(cand_x, cand_y))
847 class PieceKE < Piece
850 @normal_moves = [[+1, +2], [-1, +2]]
851 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
853 @promoted_name = "NK"
856 def room_of_head?(x, y, name)
859 return false if ((y == 1) || (y == 2))
861 return false if ((y == 9) || (y == 8))
867 class PieceGI < Piece
870 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
871 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
873 @promoted_name = "NG"
877 class PieceKI < Piece
880 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
887 class PieceKA < Piece
891 @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
893 @promoted_name = "UM"
896 def far_movable_grids
901 while (jump_to?(cand_x, cand_y))
902 grids.push([cand_x, cand_y])
903 break if (! put_to?(cand_x, cand_y))
910 while (jump_to?(cand_x, cand_y))
911 grids.push([cand_x, cand_y])
912 break if (! put_to?(cand_x, cand_y))
919 while (jump_to?(cand_x, cand_y))
920 grids.push([cand_x, cand_y])
921 break if (! put_to?(cand_x, cand_y))
928 while (jump_to?(cand_x, cand_y))
929 grids.push([cand_x, cand_y])
930 break if (! put_to?(cand_x, cand_y))
937 class PieceHI < Piece
941 @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
943 @promoted_name = "RY"
946 def far_movable_grids
951 while (jump_to?(cand_x, cand_y))
952 grids.push([cand_x, cand_y])
953 break if (! put_to?(cand_x, cand_y))
959 while (jump_to?(cand_x, cand_y))
960 grids.push([cand_x, cand_y])
961 break if (! put_to?(cand_x, cand_y))
967 while (jump_to?(cand_x, cand_y))
968 grids.push([cand_x, cand_y])
969 break if (! put_to?(cand_x, cand_y))
975 while (jump_to?(cand_x, cand_y))
976 grids.push([cand_x, cand_y])
977 break if (! put_to?(cand_x, cand_y))
983 class PieceOU < Piece
986 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
996 @sente_hands = Array::new
997 @gote_hands = Array::new
999 @sente_history = Hash::new
1000 @gote_history = Hash::new
1001 @array = [[], [], [], [], [], [], [], [], [], []]
1004 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
1005 attr_reader :move_count
1008 PieceKY::new(self, 1, 1, false)
1009 PieceKE::new(self, 2, 1, false)
1010 PieceGI::new(self, 3, 1, false)
1011 PieceKI::new(self, 4, 1, false)
1012 PieceOU::new(self, 5, 1, false)
1013 PieceKI::new(self, 6, 1, false)
1014 PieceGI::new(self, 7, 1, false)
1015 PieceKE::new(self, 8, 1, false)
1016 PieceKY::new(self, 9, 1, false)
1017 PieceKA::new(self, 2, 2, false)
1018 PieceHI::new(self, 8, 2, false)
1019 PieceFU::new(self, 1, 3, false)
1020 PieceFU::new(self, 2, 3, false)
1021 PieceFU::new(self, 3, 3, false)
1022 PieceFU::new(self, 4, 3, false)
1023 PieceFU::new(self, 5, 3, false)
1024 PieceFU::new(self, 6, 3, false)
1025 PieceFU::new(self, 7, 3, false)
1026 PieceFU::new(self, 8, 3, false)
1027 PieceFU::new(self, 9, 3, false)
1029 PieceKY::new(self, 1, 9, true)
1030 PieceKE::new(self, 2, 9, true)
1031 PieceGI::new(self, 3, 9, true)
1032 PieceKI::new(self, 4, 9, true)
1033 PieceOU::new(self, 5, 9, true)
1034 PieceKI::new(self, 6, 9, true)
1035 PieceGI::new(self, 7, 9, true)
1036 PieceKE::new(self, 8, 9, true)
1037 PieceKY::new(self, 9, 9, true)
1038 PieceKA::new(self, 8, 8, true)
1039 PieceHI::new(self, 2, 8, true)
1040 PieceFU::new(self, 1, 7, true)
1041 PieceFU::new(self, 2, 7, true)
1042 PieceFU::new(self, 3, 7, true)
1043 PieceFU::new(self, 4, 7, true)
1044 PieceFU::new(self, 5, 7, true)
1045 PieceFU::new(self, 6, 7, true)
1046 PieceFU::new(self, 7, 7, true)
1047 PieceFU::new(self, 8, 7, true)
1048 PieceFU::new(self, 9, 7, true)
1051 def have_piece?(hands, name)
1052 piece = hands.find { |i|
1058 def move_to(x0, y0, x1, y1, name, sente)
1060 hands = @sente_hands
1065 if ((x0 == 0) || (y0 == 0))
1066 piece = have_piece?(hands, name)
1067 return :illegal if (! piece.move_to?(x1, y1, name))
1068 piece.move_to(x1, y1)
1070 return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
1071 if (@array[x0][y0].name != name) # promoted ?
1072 @array[x0][y0].promoted = true
1075 if (@array[x1][y1].name == "OU")
1076 return :outori # return board update
1078 @array[x1][y1].sente = @array[x0][y0].sente
1079 @array[x1][y1].move_to(0, 0)
1084 @array[x0][y0].move_to(x1, y1)
1090 def look_for_ou(sente)
1096 (@array[x][y].name == "OU") &&
1097 (@array[x][y].sente == sente))
1104 raise "can't find ou"
1107 def checkmated?(sente) # sente is loosing
1108 ou = look_for_ou(sente)
1114 (@array[x][y].sente != sente))
1115 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1126 def uchifuzume?(sente)
1127 rival_ou = look_for_ou(! sente) # rival's ou
1128 if (sente) # rival is gote
1129 if ((rival_ou.y != 9) &&
1130 (@array[rival_ou.x][rival_ou.y + 1]) &&
1131 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1132 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1134 fu_y = rival_ou.y + 1
1139 if ((rival_ou.y != 0) &&
1140 (@array[rival_ou.x][rival_ou.y - 1]) &&
1141 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1142 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1144 fu_y = rival_ou.y - 1
1150 ## case: rival_ou is moving
1152 rival_ou.movable_grids.each do |(cand_x, cand_y)|
1153 tmp_board = Marshal.load(Marshal.dump(self))
1154 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1155 raise "internal error" if (s != true)
1156 if (! tmp_board.checkmated?(! sente)) # good move
1161 ## case: rival is capturing fu
1167 (@array[x][y].sente != sente) &&
1168 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1169 if (@array[x][y].promoted)
1170 name = @array[x][y].promoted_name
1172 name = @array[x][y].name
1174 tmp_board = Marshal.load(Marshal.dump(self))
1175 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1176 raise "internal error" if (s != true)
1177 if (! tmp_board.checkmated?(! sente)) # good move
1188 def oute_sennichite?(sente)
1189 if (checkmated?(! sente))
1192 if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1196 if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1204 def sennichite?(sente)
1206 if (@history[str] && (@history[str] >= 3)) # already 3 times
1212 def good_kachi?(sente)
1213 if (checkmated?(sente))
1214 puts "'NG: Checkmating." if $DEBUG
1218 ou = look_for_ou(sente)
1219 if (sente && (ou.y >= 4))
1220 puts "'NG: Black's OU does not enter yet." if $DEBUG
1223 if (! sente && (ou.y <= 6))
1224 puts "'NG: White's OU does not enter yet." if $DEBUG
1232 hands = @sente_hands
1242 (@array[x][y].sente == sente) &&
1243 (@array[x][y].point > 0))
1244 point = point + @array[x][y].point
1250 hands.each do |piece|
1251 point = point + piece.point
1255 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1260 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1265 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1270 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1274 # sente is nil only if tests in test_board run
1275 def handle_one_move(str, sente=nil)
1276 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1283 elsif (str =~ /^%KACHI/)
1284 raise ArgumentError, "sente is null", caller if sente == nil
1285 if (good_kachi?(sente))
1290 elsif (str =~ /^%TORYO/)
1296 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1297 ((x0 != 0) || (y0 != 0)))
1299 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1305 sente = true if sente == nil # deprecated
1306 return :illegal unless sente == true # black player's move must be black
1307 hands = @sente_hands
1309 sente = false if sente == nil # deprecated
1310 return :illegal unless sente == false # white player's move must be white
1315 if ((x0 == 0) && (y0 == 0))
1316 return :illegal if (! have_piece?(hands, name))
1317 elsif (! @array[x0][y0])
1318 return :illegal # no piece
1319 elsif (@array[x0][y0].sente != sente)
1320 return :illegal # this is not mine
1321 elsif (@array[x0][y0].name != name)
1322 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1325 ## destination check
1326 if (@array[x1][y1] &&
1327 (@array[x1][y1].sente == sente)) # can't capture mine
1329 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1330 return :illegal # can't put on existing piece
1333 tmp_board = Marshal.load(Marshal.dump(self))
1334 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1335 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1336 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1337 return :sennichite if tmp_board.sennichite?(sente)
1339 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1343 move_to(x0, y0, x1, y1, name, sente)
1346 if (checkmated?(! sente))
1348 @sente_history[str] = (@sente_history[str] || 0) + 1
1350 @gote_history[str] = (@gote_history[str] || 0) + 1
1354 @sente_history.clear
1359 @history[str] = (@history[str] || 0) + 1
1367 a.push(sprintf("P%d", y))
1370 piece = @array[x][y]
1379 a.push(sprintf("\n"))
1382 if (! sente_hands.empty?)
1384 sente_hands.each do |p|
1385 a.push("00" + p.name)
1389 if (! gote_hands.empty?)
1391 gote_hands.each do |p|
1392 a.push("00" + p.name)
1402 attr_reader :players, :black, :white
1404 def initialize(p1, p2)
1408 if p1.sente && !p2.sente
1409 @black, @white = p1, p2
1410 elsif !p1.sente && p2.sente
1411 @black, @white = p2, p1
1413 raise "Never reached!"
1418 class GameResultWin < GameResult
1419 attr_reader :winner, :loser
1421 def initialize(winner, loser)
1423 @winner, @loser = winner, loser
1427 black_name = @black.id || @black.name
1428 white_name = @white.id || @white.name
1429 "%s:%s" % [black_name, white_name]
1433 class GameResultDraw < GameResult
1441 def initialize(game_name, player0, player1)
1442 @monitors = Array::new
1443 @game_name = game_name
1444 if (@game_name =~ /-(\d+)-(\d+)$/)
1445 @total_time = $1.to_i
1456 @current_player = @sente
1457 @next_player = @gote
1465 @sente.status = "agree_waiting"
1466 @gote.status = "agree_waiting"
1468 @id = sprintf("%s+%s+%s+%s+%s",
1469 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1470 @logfile = @id + ".csa"
1472 LEAGUE.games[@id] = self
1474 log_message(sprintf("game created %s", @id))
1484 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1485 attr_accessor :last_move, :current_turn
1489 @sente.rated? && @gote.rated?
1492 def monitoron(monitor)
1493 @monitors.delete(monitor)
1494 @monitors.push(monitor)
1497 def monitoroff(monitor)
1498 @monitors.delete(monitor)
1501 def reject(rejector)
1502 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1503 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1508 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1510 elsif (@current_player == killer)
1517 log_message(sprintf("game finished %s", @id))
1518 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1523 @sente.status = "connected"
1524 @gote.status = "connected"
1526 if (@current_player.protocol == LoginCSA::PROTOCOL)
1527 @current_player.finish
1529 if (@next_player.protocol == LoginCSA::PROTOCOL)
1532 @monitors = Array::new
1535 @current_player = nil
1537 LEAGUE.games.delete(@id)
1540 def handle_one_move(str, player)
1542 if (@current_player == player)
1543 @end_time = Time::new
1544 t = (@end_time - @start_time).floor
1545 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1548 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1550 elsif (str == :timeout)
1551 return false # time isn't expired. players aren't swapped. continue game
1553 @current_player.mytime = @current_player.mytime - t
1554 if (@current_player.mytime < 0)
1555 @current_player.mytime = 0
1559 move_status = @board.handle_one_move(str, @sente == @current_player)
1561 # log_error("handle_one_move raise exception for #{str}")
1562 # move_status = :illegal
1565 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1566 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1568 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1569 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1570 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1571 @fh.printf("%s\nT%d\n", str, t)
1572 @last_move = sprintf("%s,T%d", str, t)
1573 @current_turn = @current_turn + 1
1576 @monitors.each do |monitor|
1577 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1578 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1583 if (@next_player.status != "game") # rival is logout or disconnected
1585 elsif (status == :timeout)
1587 elsif (move_status == :illegal)
1589 elsif (move_status == :kachi_win)
1591 elsif (move_status == :kachi_lose)
1593 elsif (move_status == :toryo)
1595 elsif (move_status == :outori)
1597 elsif (move_status == :sennichite)
1599 elsif (move_status == :oute_sennichite)
1600 oute_sennichite_lose()
1601 elsif (move_status == :uchifuzume)
1603 elsif (move_status == :oute_kaihimore)
1604 oute_kaihimore_lose()
1608 finish() if finish_flag
1609 (@current_player, @next_player) = [@next_player, @current_player]
1610 @start_time = Time::new
1616 @current_player.status = "connected"
1617 @next_player.status = "connected"
1618 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1619 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1620 @fh.printf("%%TORYO\n")
1621 @fh.print(@board.to_s.gsub(/^/, "\'"))
1622 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1623 @result = GameResultWin.new(@current_player, @next_player)
1624 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1625 @monitors.each do |monitor|
1626 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1631 @current_player.status = "connected"
1632 @next_player.status = "connected"
1633 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1634 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1635 @fh.printf("%%TORYO\n")
1636 @fh.print(@board.to_s.gsub(/^/, "\'"))
1637 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1638 @result = GameResultWin.new(@next_player, @current_player)
1639 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1640 @monitors.each do |monitor|
1641 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1646 @current_player.status = "connected"
1647 @next_player.status = "connected"
1648 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1649 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1650 @fh.print(@board.to_s.gsub(/^/, "\'"))
1651 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1652 @result = GameResultDraw.new(@current_player, @next_player)
1653 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1654 @monitors.each do |monitor|
1655 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1659 def oute_sennichite_lose
1660 @current_player.status = "connected"
1661 @next_player.status = "connected"
1662 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1663 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1664 @fh.print(@board.to_s.gsub(/^/, "\'"))
1665 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1666 @result = GameResultWin.new(@next_player, @current_player)
1667 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1668 @monitors.each do |monitor|
1669 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1674 @current_player.status = "connected"
1675 @next_player.status = "connected"
1676 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1677 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1678 @fh.print(@board.to_s.gsub(/^/, "\'"))
1679 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1680 @result = GameResultWin.new(@next_player, @current_player)
1681 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1682 @monitors.each do |monitor|
1683 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1688 @current_player.status = "connected"
1689 @next_player.status = "connected"
1690 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1691 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1692 @fh.print(@board.to_s.gsub(/^/, "\'"))
1693 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1694 @result = GameResultWin.new(@next_player, @current_player)
1695 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1696 @monitors.each do |monitor|
1697 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1701 def oute_kaihimore_lose
1702 @current_player.status = "connected"
1703 @next_player.status = "connected"
1704 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1705 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1706 @fh.print(@board.to_s.gsub(/^/, "\'"))
1707 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1708 @result = GameResultWin.new(@next_player, @current_player)
1709 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1710 @monitors.each do |monitor|
1711 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1716 @current_player.status = "connected"
1717 @next_player.status = "connected"
1718 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1719 @next_player.write_safe("#TIME_UP\n#WIN\n")
1720 @fh.print(@board.to_s.gsub(/^/, "\'"))
1721 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1722 @result = GameResultWin.new(@next_player, @current_player)
1723 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1724 @monitors.each do |monitor|
1725 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1730 @current_player.status = "connected"
1731 @next_player.status = "connected"
1732 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1733 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1734 @fh.printf("%%KACHI\n")
1735 @fh.print(@board.to_s.gsub(/^/, "\'"))
1736 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1737 @result = GameResultWin.new(@current_player, @next_player)
1738 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1739 @monitors.each do |monitor|
1740 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1745 @current_player.status = "connected"
1746 @next_player.status = "connected"
1747 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1748 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1749 @fh.printf("%%KACHI\n")
1750 @fh.print(@board.to_s.gsub(/^/, "\'"))
1751 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1752 @result = GameResultWin.new(@next_player, @current_player)
1753 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1754 @monitors.each do |monitor|
1755 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1760 @current_player.status = "connected"
1761 @next_player.status = "connected"
1762 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1763 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1764 @fh.printf("%%TORYO\n")
1765 @fh.print(@board.to_s.gsub(/^/, "\'"))
1766 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1767 @result = GameResultWin.new(@next_player, @current_player)
1768 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1769 @monitors.each do |monitor|
1770 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1775 @current_player.status = "connected"
1776 @next_player.status = "connected"
1777 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1778 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1779 @fh.print(@board.to_s.gsub(/^/, "\'"))
1780 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1781 @result = GameResultWin.new(@current_player, @next_player)
1782 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1783 @monitors.each do |monitor|
1784 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1789 log_message(sprintf("game started %s", @id))
1790 @sente.write_safe(sprintf("START:%s\n", @id))
1791 @gote.write_safe(sprintf("START:%s\n", @id))
1792 @sente.mytime = @total_time
1793 @gote.mytime = @total_time
1794 @start_time = Time::new
1799 @fh = open(@logfile, "w")
1803 @fh.printf("N+%s\n", @sente.name)
1804 @fh.printf("N-%s\n", @gote.name)
1805 @fh.printf("$EVENT:%s\n", @id)
1807 @sente.write_safe(propose_message("+"))
1808 @gote.write_safe(propose_message("-"))
1810 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1812 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1813 P2 * -HI * * * * * -KA *
1814 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1815 P4 * * * * * * * * *
1816 P5 * * * * * * * * *
1817 P6 * * * * * * * * *
1818 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1819 P8 * +KA * * * * * +HI *
1820 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1829 Protocol_Version:1.1
1830 Protocol_Mode:Server
1832 Declaration:Jishogi 1.1
1834 Name+:#{@sente.name}
1840 Total_Time:#{@total_time}
1842 Least_Time_Per_Move:#{Least_Time_Per_Move}
1843 Remaining_Time+:#{@sente.mytime}
1844 Remaining_Time-:#{@gote.mytime}
1845 Last_Move:#{@last_move}
1846 Current_Turn:#{@current_turn}
1856 return str0 + @board.to_s + str1
1859 def propose_message(sg_flag)
1862 Protocol_Version:1.1
1863 Protocol_Mode:Server
1865 Declaration:Jishogi 1.1
1867 Name+:#{@sente.name}
1869 Your_Turn:#{sg_flag}
1874 Total_Time:#{@total_time}
1876 Least_Time_Per_Move:#{Least_Time_Per_Move}
1879 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1880 P2 * -HI * * * * * -KA *
1881 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1882 P4 * * * * * * * * *
1883 P5 * * * * * * * * *
1884 P6 * * * * * * * * *
1885 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1886 P8 * +KA * * * * * +HI *
1887 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1899 def issue_current_time
1900 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1901 @@mutex.synchronize do
1902 while time <= @@time do
1909 end # module ShogiServer
1911 #################################################
1918 shogi-server - server for CSA server protocol
1921 shogi-server event_name port_number
1924 server for CSA server protocol
1928 specify filename for logging process ID
1931 this file is distributed under GPL version2 and might be compiled by Exerb
1943 def log_message(str)
1944 printf("%s message: %s\n", Time::new.to_s, str)
1947 def log_warning(str)
1948 printf("%s warning: %s\n", Time::new.to_s, str)
1952 printf("%s error: %s\n", Time::new.to_s, str)
1956 def parse_command_line
1958 parser = GetoptLong.new
1959 parser.ordering = GetoptLong::REQUIRE_ORDER
1961 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1965 parser.each_option do |name, arg|
1966 name.sub!(/^--/, '')
1967 options[name] = arg.dup
1971 raise parser.error_message
1976 def write_pid_file(file)
1977 open(file, "w") do |fh|
1978 fh.print Process::pid, "\n"
1982 def mutex_watchdog(mutex, sec)
1994 log_error("mutex watchdog timeout")
2005 mutex_watchdog($mutex, 10)
2008 $options = parse_command_line
2009 if (ARGV.length != 2)
2014 LEAGUE.event = ARGV.shift
2017 write_pid_file($options["pid-file"]) if ($options["pid-file"])
2019 server = TCPserver.open(port)
2020 log_message("server started")
2023 Thread::start(server.accept) do |client|
2028 while (str = client.gets_timeout(ShogiServer::Login_Time))
2033 if (ShogiServer::Login::good_login?(str))
2034 player = ShogiServer::Player::new(str, client)
2036 login = ShogiServer::Login::factory(str, player)
2037 if (LEAGUE.players[player.name])
2038 if ((LEAGUE.players[player.name].password == player.password) &&
2039 (LEAGUE.players[player.name].status != "game"))
2040 log_message(sprintf("user %s login forcely", player.name))
2041 LEAGUE.players[player.name].kill
2043 login.incorrect_duplicated_player(str)
2051 client.write_safe("LOGIN:incorrect" + eol)
2052 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2063 log_message(sprintf("user %s login", player.name))
2065 player.run(login.csa_1st_str)
2069 player.game.kill(player)
2071 player.finish # socket has been closed
2072 LEAGUE.delete(player)
2073 log_message(sprintf("user %s logout", player.name))
2085 TCPSocket.do_not_reverse_lookup = true
2086 Thread.abort_on_exception = true
2088 LEAGUE = ShogiServer::League::new