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 module ShogiServer # for a namespace
22 Max_Write_Queue_Size = 1000
23 Max_Identifier_Length = 32
24 Default_Timeout = 60 # for single socket operation
26 Default_Game_Name = "default-1500-0"
29 Least_Time_Per_Move = 1
30 Login_Time = 300 # time for LOGIN
32 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
33 Release.concat("-") if (Release == "")
34 Revision = "$Revision$".gsub(/[^\.\d]/, '')
47 def gets_timeout(t = Default_Timeout)
58 def gets_safe(t = nil)
79 return self.write(str)
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 def handle_one_move(str, sente=nil)
1275 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1282 elsif (str =~ /^%KACHI/)
1283 raise ArgumentError, "sente is null", caller if sente == nil
1284 if (good_kachi?(sente))
1289 elsif (str =~ /^%TORYO/)
1295 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1296 ((x0 != 0) || (y0 != 0)))
1298 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1304 hands = @sente_hands
1311 if ((x0 == 0) && (y0 == 0))
1312 return :illegal if (! have_piece?(hands, name))
1313 elsif (! @array[x0][y0])
1314 return :illegal # no piece
1315 elsif (@array[x0][y0].sente != sente)
1316 return :illegal # this is not mine
1317 elsif (@array[x0][y0].name != name)
1318 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1321 ## destination check
1322 if (@array[x1][y1] &&
1323 (@array[x1][y1].sente == sente)) # can't capture mine
1325 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1326 return :illegal # can't put on existing piece
1329 tmp_board = Marshal.load(Marshal.dump(self))
1330 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1331 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1332 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1333 return :sennichite if tmp_board.sennichite?(sente)
1335 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1339 move_to(x0, y0, x1, y1, name, sente)
1342 if (checkmated?(! sente))
1344 @sente_history[str] = (@sente_history[str] || 0) + 1
1346 @gote_history[str] = (@gote_history[str] || 0) + 1
1350 @sente_history.clear
1355 @history[str] = (@history[str] || 0) + 1
1363 a.push(sprintf("P%d", y))
1366 piece = @array[x][y]
1375 a.push(sprintf("\n"))
1378 if (! sente_hands.empty?)
1380 sente_hands.each do |p|
1381 a.push("00" + p.name)
1385 if (! gote_hands.empty?)
1387 gote_hands.each do |p|
1388 a.push("00" + p.name)
1398 attr_reader :players, :black, :white
1400 def initialize(p1, p2)
1404 if p1.sente && !p2.sente
1405 @black, @white = p1, p2
1406 elsif !p1.sente && p2.sente
1407 @black, @white = p2, p1
1409 raise "Never reached!"
1414 class GameResultWin < GameResult
1415 attr_reader :winner, :loser
1417 def initialize(winner, loser)
1419 @winner, @loser = winner, loser
1423 black_name = @black.id || @black.name
1424 white_name = @white.id || @white.name
1425 "%s:%s" % [black_name, white_name]
1429 class GameResultDraw < GameResult
1437 def initialize(game_name, player0, player1)
1438 @monitors = Array::new
1439 @game_name = game_name
1440 if (@game_name =~ /-(\d+)-(\d+)$/)
1441 @total_time = $1.to_i
1452 @current_player = @sente
1453 @next_player = @gote
1461 @sente.status = "agree_waiting"
1462 @gote.status = "agree_waiting"
1464 @id = sprintf("%s+%s+%s+%s+%s",
1465 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1466 @logfile = @id + ".csa"
1468 LEAGUE.games[@id] = self
1470 log_message(sprintf("game created %s", @id))
1480 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1481 attr_accessor :last_move, :current_turn
1485 @sente.rated? && @gote.rated?
1488 def monitoron(monitor)
1489 @monitors.delete(monitor)
1490 @monitors.push(monitor)
1493 def monitoroff(monitor)
1494 @monitors.delete(monitor)
1497 def reject(rejector)
1498 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1499 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1504 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1506 elsif (@current_player == killer)
1513 log_message(sprintf("game finished %s", @id))
1514 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1519 @sente.status = "connected"
1520 @gote.status = "connected"
1522 if (@current_player.protocol == LoginCSA::PROTOCOL)
1523 @current_player.finish
1525 if (@next_player.protocol == LoginCSA::PROTOCOL)
1528 @monitors = Array::new
1531 @current_player = nil
1533 LEAGUE.games.delete(@id)
1536 def handle_one_move(str, player)
1538 if (@current_player == player)
1539 @end_time = Time::new
1540 t = (@end_time - @start_time).floor
1541 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1544 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1546 elsif (str == :timeout)
1547 return false # time isn't expired. players aren't swapped. continue game
1549 @current_player.mytime = @current_player.mytime - t
1550 if (@current_player.mytime < 0)
1551 @current_player.mytime = 0
1555 move_status = @board.handle_one_move(str, @sente == @current_player)
1557 # log_error("handle_one_move raise exception for #{str}")
1558 # move_status = :illegal
1561 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1562 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1564 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1565 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1566 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1567 @fh.printf("%s\nT%d\n", str, t)
1568 @last_move = sprintf("%s,T%d", str, t)
1569 @current_turn = @current_turn + 1
1572 @monitors.each do |monitor|
1573 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1574 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1579 if (@next_player.status != "game") # rival is logout or disconnected
1581 elsif (status == :timeout)
1583 elsif (move_status == :illegal)
1585 elsif (move_status == :kachi_win)
1587 elsif (move_status == :kachi_lose)
1589 elsif (move_status == :toryo)
1591 elsif (move_status == :outori)
1593 elsif (move_status == :sennichite)
1595 elsif (move_status == :oute_sennichite)
1596 oute_sennichite_lose()
1597 elsif (move_status == :uchifuzume)
1599 elsif (move_status == :oute_kaihimore)
1600 oute_kaihimore_lose()
1604 finish() if finish_flag
1605 (@current_player, @next_player) = [@next_player, @current_player]
1606 @start_time = Time::new
1612 @current_player.status = "connected"
1613 @next_player.status = "connected"
1614 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1615 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1616 @fh.printf("%%TORYO\n")
1617 @fh.print(@board.to_s.gsub(/^/, "\'"))
1618 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1619 @result = GameResultWin.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] %%TORYO\n", @id))
1627 @current_player.status = "connected"
1628 @next_player.status = "connected"
1629 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1630 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1631 @fh.printf("%%TORYO\n")
1632 @fh.print(@board.to_s.gsub(/^/, "\'"))
1633 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1634 @result = GameResultWin.new(@next_player, @current_player)
1635 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1636 @monitors.each do |monitor|
1637 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1642 @current_player.status = "connected"
1643 @next_player.status = "connected"
1644 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1645 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1646 @fh.print(@board.to_s.gsub(/^/, "\'"))
1647 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1648 @result = GameResultDraw.new(@current_player, @next_player)
1649 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1650 @monitors.each do |monitor|
1651 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1655 def oute_sennichite_lose
1656 @current_player.status = "connected"
1657 @next_player.status = "connected"
1658 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1659 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1660 @fh.print(@board.to_s.gsub(/^/, "\'"))
1661 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1662 @result = GameResultWin.new(@next_player, @current_player)
1663 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1664 @monitors.each do |monitor|
1665 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1670 @current_player.status = "connected"
1671 @next_player.status = "connected"
1672 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1673 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1674 @fh.print(@board.to_s.gsub(/^/, "\'"))
1675 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1676 @result = GameResultWin.new(@next_player, @current_player)
1677 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1678 @monitors.each do |monitor|
1679 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1684 @current_player.status = "connected"
1685 @next_player.status = "connected"
1686 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1687 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1688 @fh.print(@board.to_s.gsub(/^/, "\'"))
1689 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1690 @result = GameResultWin.new(@next_player, @current_player)
1691 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1692 @monitors.each do |monitor|
1693 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1697 def oute_kaihimore_lose
1698 @current_player.status = "connected"
1699 @next_player.status = "connected"
1700 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1701 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1702 @fh.print(@board.to_s.gsub(/^/, "\'"))
1703 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1704 @result = GameResultWin.new(@next_player, @current_player)
1705 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1706 @monitors.each do |monitor|
1707 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1712 @current_player.status = "connected"
1713 @next_player.status = "connected"
1714 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1715 @next_player.write_safe("#TIME_UP\n#WIN\n")
1716 @fh.print(@board.to_s.gsub(/^/, "\'"))
1717 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1718 @result = GameResultWin.new(@next_player, @current_player)
1719 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1720 @monitors.each do |monitor|
1721 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1726 @current_player.status = "connected"
1727 @next_player.status = "connected"
1728 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1729 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1730 @fh.printf("%%KACHI\n")
1731 @fh.print(@board.to_s.gsub(/^/, "\'"))
1732 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1733 @result = GameResultWin.new(@current_player, @next_player)
1734 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1735 @monitors.each do |monitor|
1736 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1741 @current_player.status = "connected"
1742 @next_player.status = "connected"
1743 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1744 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1745 @fh.printf("%%KACHI\n")
1746 @fh.print(@board.to_s.gsub(/^/, "\'"))
1747 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1748 @result = GameResultWin.new(@next_player, @current_player)
1749 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1750 @monitors.each do |monitor|
1751 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1756 @current_player.status = "connected"
1757 @next_player.status = "connected"
1758 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1759 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1760 @fh.printf("%%TORYO\n")
1761 @fh.print(@board.to_s.gsub(/^/, "\'"))
1762 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1763 @result = GameResultWin.new(@next_player, @current_player)
1764 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1765 @monitors.each do |monitor|
1766 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1771 @current_player.status = "connected"
1772 @next_player.status = "connected"
1773 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1774 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1775 @fh.print(@board.to_s.gsub(/^/, "\'"))
1776 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1777 @result = GameResultWin.new(@current_player, @next_player)
1778 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1779 @monitors.each do |monitor|
1780 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1785 log_message(sprintf("game started %s", @id))
1786 @sente.write_safe(sprintf("START:%s\n", @id))
1787 @gote.write_safe(sprintf("START:%s\n", @id))
1788 @sente.mytime = @total_time
1789 @gote.mytime = @total_time
1790 @start_time = Time::new
1795 @fh = open(@logfile, "w")
1799 @fh.printf("N+%s\n", @sente.name)
1800 @fh.printf("N-%s\n", @gote.name)
1801 @fh.printf("$EVENT:%s\n", @id)
1803 @sente.write_safe(propose_message("+"))
1804 @gote.write_safe(propose_message("-"))
1806 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1808 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1809 P2 * -HI * * * * * -KA *
1810 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1811 P4 * * * * * * * * *
1812 P5 * * * * * * * * *
1813 P6 * * * * * * * * *
1814 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1815 P8 * +KA * * * * * +HI *
1816 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1825 Protocol_Version:1.1
1826 Protocol_Mode:Server
1828 Declaration:Jishogi 1.1
1830 Name+:#{@sente.name}
1836 Total_Time:#{@total_time}
1838 Least_Time_Per_Move:#{Least_Time_Per_Move}
1839 Remaining_Time+:#{@sente.mytime}
1840 Remaining_Time-:#{@gote.mytime}
1841 Last_Move:#{@last_move}
1842 Current_Turn:#{@current_turn}
1852 return str0 + @board.to_s + str1
1855 def propose_message(sg_flag)
1858 Protocol_Version:1.1
1859 Protocol_Mode:Server
1861 Declaration:Jishogi 1.1
1863 Name+:#{@sente.name}
1865 Your_Turn:#{sg_flag}
1870 Total_Time:#{@total_time}
1872 Least_Time_Per_Move:#{Least_Time_Per_Move}
1875 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1876 P2 * -HI * * * * * -KA *
1877 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1878 P4 * * * * * * * * *
1879 P5 * * * * * * * * *
1880 P6 * * * * * * * * *
1881 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1882 P8 * +KA * * * * * +HI *
1883 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1895 def issue_current_time
1896 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1897 @@mutex.synchronize do
1898 while time <= @@time do
1906 #################################################
1913 shogi-server - server for CSA server protocol
1916 shogi-server event_name port_number
1919 server for CSA server protocol
1923 specify filename for logging process ID
1926 this file is distributed under GPL version2 and might be compiled by Exerb
1938 def log_message(str)
1939 printf("%s message: %s\n", Time::new.to_s, str)
1942 def log_warning(str)
1943 printf("%s warning: %s\n", Time::new.to_s, str)
1947 printf("%s error: %s\n", Time::new.to_s, str)
1951 def parse_command_line
1953 parser = GetoptLong.new
1954 parser.ordering = GetoptLong::REQUIRE_ORDER
1956 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1960 parser.each_option do |name, arg|
1961 name.sub!(/^--/, '')
1962 options[name] = arg.dup
1966 raise parser.error_message
1971 def write_pid_file(file)
1972 open(file, "w") do |fh|
1973 fh.print Process::pid, "\n"
1977 def mutex_watchdog(mutex, sec)
1989 log_error("mutex watchdog timeout")
2000 mutex_watchdog($mutex, 10)
2003 $options = parse_command_line
2004 if (ARGV.length != 2)
2009 LEAGUE.event = ARGV.shift
2012 write_pid_file($options["pid-file"]) if ($options["pid-file"])
2014 server = TCPserver.open(port)
2015 log_message("server started")
2018 Thread::start(server.accept) do |client|
2023 while (str = client.gets_timeout(Login_Time))
2028 if (Login::good_login?(str))
2029 player = Player::new(str, client)
2031 login = Login::factory(str, player)
2032 if (LEAGUE.players[player.name])
2033 if ((LEAGUE.players[player.name].password == player.password) &&
2034 (LEAGUE.players[player.name].status != "game"))
2035 log_message(sprintf("user %s login forcely", player.name))
2036 LEAGUE.players[player.name].kill
2038 login.incorrect_duplicated_player(str)
2046 client.write_safe("LOGIN:incorrect" + eol)
2047 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2058 log_message(sprintf("user %s login", player.name))
2060 player.run(login.csa_1st_str)
2064 player.game.kill(player)
2066 player.finish # socket has been closed
2067 LEAGUE.delete(player)
2068 log_message(sprintf("user %s logout", player.name))
2075 module_function :main
2077 end # module ShogiServer
2082 TCPSocket.do_not_reverse_lookup = true
2083 Thread.abort_on_exception = true
2085 LEAGUE = ShogiServer::League::new