DIR : /home/kozerus/public_html/dgs/include/game_functions.php

/home/kozerus/public_html/dgs/include

<?php
/*
Dragon Go Server
Copyright (C) 2001-  Erik Ouchterlony, Rod Ival, Jens-Uwe Gaspar

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

$TranslateGroups[] = "Game";

require_once 'include/globals.php';
require_once 'include/db/games.php';
require_once 'include/db/game_invitation.php';
require_once 'include/db/move_sequence.php';
require_once 'include/coords.php';
require_once 'include/board.php';
require_once 'include/rulesets.php';
require_once 'include/rating.php';
require_once 'include/classlib_profile.php';
require_once 'include/classlib_user.php';
require_once 'include/utilities.php';
require_once 'include/std_classes.php';
require_once 'include/std_functions.php';
require_once 'include/time_functions.php';
require_once 'include/message_functions.php';
require_once 'include/game_texts.php';
require_once 'include/error_codes.php';
require_once 'include/dgs_cache.php';
require_once 'tournaments/include/tournament_games.php';


define('MAX_ADD_DAYS', 14); // max. amount of days that can be added to game by user

define('MAX_GAME_PLAYERS', 16);

// GamePlayers.Flags
define('GPFLAG_MASTER',      0x0001); // game-master
define('GPFLAG_JOINED',      0x0002); // joined game, user set
define('GPFLAG_RESERVED',    0x0004); // 1 = WR or INV started, waiting for join; 0 = user joined if WR or INV set
define('GPFLAG_WAITINGROOM', 0x0008); // join user via waiting-room
define('GPFLAG_INVITATION',  0x0010); // join user via invitation
define('GPFLAGS_SLOT_TAKEN',  (GPFLAG_JOINED|GPFLAG_RESERVED) );
define('GPFLAGS_RESERVED_WAITINGROOM', (GPFLAG_RESERVED|GPFLAG_WAITINGROOM) );
define('GPFLAGS_RESERVED_INVITATION', (GPFLAG_RESERVED|GPFLAG_INVITATION) );

// GamePlayers.GroupColor
define('GPCOL_B',  'B');
define('GPCOL_W',  'W');
define('GPCOL_G1', 'G1');
define('GPCOL_G2', 'G2');
define('GPCOL_BW', 'BW');
define('GPCOL_DEFAULT', GPCOL_BW);

// multi-player-game message-type
define('MPGMSG_STARTGAME', 1);
define('MPGMSG_RESIGN', 2);
define('MPGMSG_INVITE', 3);

// for GameNotify
define('ACTBY_PLAYER', 0);
define('ACTBY_ADMIN',  1);
define('ACTBY_CRON',   2);
define('ACTBY_GAMEMASTER', 3);
global $MAP_ACTBY_SUBJECT; //PHP5;
$MAP_ACTBY_SUBJECT = array( // subject-action-by-part + sprintf-format for deletion/finalize-game-text (English only)
   ACTBY_PLAYER      => array( 'player', 'player %s' ),
   ACTBY_ADMIN       => array( 'ADMIN',  'ADMIN %s' ),
   ACTBY_CRON        => array( 'CRON',   'CRON' ),
   ACTBY_GAMEMASTER  => array( 'game-master', 'game-master %s' ),
);

// enum Waitingroom.JigoMode
define('JIGOMODE_KEEP_KOMI',  'KEEP_KOMI');
define('JIGOMODE_ALLOW_JIGO', 'ALLOW_JIGO');
define('JIGOMODE_NO_JIGO',    'NO_JIGO');
define('CHECK_JIGOMODE', 'KEEP_KOMI|ALLOW_JIGO|NO_JIGO');

define('SAMEOPP_ONLY_NEW', -127);
define('SAMEOPP_TOTAL_STARTED', -100); // base-value for total-started-games Waitingroom.SameOpponent-check

// handicap-types, see Waitingroom.Handicaptype in specs/db/table-Waitingroom.txt
define('HTYPE_CONV',    'conv'); // conventional handicap
define('HTYPE_PROPER',  'proper'); // proper handicap
define('HTYPE_NIGIRI',  'nigiri'); // manual, color nigiri
define('HTYPE_DOUBLE',  'double'); // manual, color double (=black and white)
define('HTYPE_BLACK',   'black'); // manual, color black
define('HTYPE_WHITE',   'white'); // manual, color white
define('HTYPE_ALTERNATE', 'alternate'); // manual, alternate colors (only for round-robin-tournament)
define('HTYPE_AUCTION_SECRET', 'auko_sec'); // fair-komi: secret auction komi
define('HTYPE_AUCTION_OPEN',   'auko_opn'); // fair-komi: open auction komi
define('HTYPE_YOU_KOMI_I_COLOR', 'div_ykic'); // fair-komi: You cut (choose komi), I choose (color)
define('HTYPE_I_KOMI_YOU_COLOR', 'div_ikyc'); // fair-komi: I cut (choose komi), You choose (color)
define('HTYPEMP_MANUAL', 'manual'); // manual, only used for multi-player-game in waiting-room
define('DEFAULT_HTYPE_FAIRKOMI', HTYPE_AUCTION_OPEN);
define('CHECK_HTYPES_FAIRKOMI', 'auko_sec|auko_opn|div_ykic|div_ikyc');

// handicap-type categories
define('CAT_HTYPE_CONV', HTYPE_CONV); // conventional handicap-type
define('CAT_HTYPE_PROPER', HTYPE_PROPER); // proper handicap-type
define('CAT_HTYPE_MANUAL', 'manual'); // manual game setting
define('CAT_HTYPE_FAIR_KOMI', 'fairkomi'); // fair-komi setting

define('GS_SEP', ':'); // separator for game-setup


/**
 * \class GameAddTime
 *
 * \brief Class to handle adding of time for a game.
 *
 * \note class with splitted add_time_opponent()-method to support unit-tests.
 */
class GameAddTime
{
   /*! \brief User GIVING time to his opponent. */
   public $uid;
   /*! \brief user-id of tournament-director, which is adding-time (TD needs admin-right TD_FLAG_GAME_ADD_TIME). */
   private $td_uid;
   public $game_row;
   public $game_query = '';
   private $reset_byoyomi;

   public function __construct( array &$game_row, $uid, $td_uid=0 )
   {
      $this->uid = (int)$uid;
      $this->td_uid = ( (int)$td_uid > GUESTS_ID_MAX ) ? (int)$td_uid : 0;
      $this->game_row = $game_row;
   }

   /*!
    * \brief Adds time to given player updating game-row without data-persisting.
    * \param $add_hours amount of hours to add to maintime of opponent,
    *        allowed range is 0 hour .. MAX_ADD_DAYS
    * \param $reset_byo if true, also byo-yomi will be fully resetted (for JAP/CAN)
    * \return number of hours added (may be 0), if time has been successfully added
    *        for opponent of player (uid) in specified game (gid).
    *        -1|-2 if only byo-yomi has been resetted (-1=full-reset, -2=part-reset).
    *        Otherwise error-string is returned.
    *
    * \note reset_byoyomi is resetted if needed and stored in $this->reset_byoyomi.
    */
   public function add_time( $add_hours, $reset_byo=false )
   {
      $this->reset_byoyomi = $reset_byo;
      if ( !is_numeric($add_hours) || $add_hours < 0
            || $add_hours > time_convert_to_hours( MAX_ADD_DAYS, 'days'))
         return sprintf( 'Invalid value for add_hours [%s]', $add_hours);

      // reset_byo: 0=no-reset, 1=full-byo-yomi-reset, 2=reset-byo-time-only
      $reset_byo = ( $reset_byo ) ? 1 : 0;
      if ( !$reset_byo && $add_hours == 0 )
         return 0; // nothing to do (0 hours added, no error)

      if ( is_numeric($this->game_row) )
      {
         $gid = $this->game_row;
         $this->game_row = mysql_single_fetch( 'add_time_opponent',
            "SELECT G.* from Games AS G WHERE G.ID=$gid");
         if ( !$this->game_row )
            error('unknown_game',"add_time_opponent($gid)");
      }
      else
         $gid = $this->game_row['ID'];

      if ( !GameAddTime::allow_add_time_opponent( $this->game_row, $this->uid, $this->td_uid ) )
         return sprintf( T_('Conditions are not met to allow to add time by user [%s] for game [%s]'),
                         ($this->tdir ? T_('Tournament director') : $this->uid), $gid );

      // get opponents columns to update
      $oppcolor = ( $this->game_row['Black_ID'] == $this->uid ) ? 'White' : 'Black';

      if ( !isset($this->game_row["{$oppcolor}_Maintime"])
            || !isset($this->game_row["{$oppcolor}_Byotime"])
            || !isset($this->game_row["{$oppcolor}_Byoperiods"])
            || !isset($this->game_row['Byotype'])
            || !isset($this->game_row['Byotime'])
            || !isset($this->game_row['Byoperiods']) )
         error('internal_error',"add_time_opponent.incomplete_game_row($gid)");

      // special byo-yomi resetting
      $byotype = $this->game_row['Byotype'];
      if ( !$reset_byo )
      {
         // reset current byo-yomi period for JAP|CAN if in byo-yomi
         if ( $byotype == BYOTYPE_CANADIAN )
            $reset_byo = 1; // CAN-time difficult to handle without reset (see specs/time.txt)
         if ( $byotype == BYOTYPE_JAPANESE && $this->game_row["{$oppcolor}_Byoperiods"] >= 0 )
            $reset_byo = 2; // reset current byo-period for JAP-time
      }
      if ( $reset_byo )
      {
         if ( $byotype == BYOTYPE_FISCHER )
            $reset_byo = 0;
         elseif ( $this->game_row["{$oppcolor}_Byoperiods"] < 0 )
            $reset_byo = 0;
         elseif ( $this->game_row['Byotime'] <= 0 || $this->game_row['Byoperiods'] < 0 )
            $reset_byo = 0; // no byo-yomi-reset if absolute-time
      }
      $this->reset_byoyomi = $reset_byo;

      if ( !$reset_byo && $add_hours <= 0 )
         return 0; // nothing to do (0 hours added, no error)

      // add maintime and eventually reset byo-time for opponent
      $this->game_query = '';
      if ( $add_hours > 0 )
      {
         $this->game_query .= ",{$oppcolor}_Maintime={$oppcolor}_Maintime+$add_hours";
         $this->game_row["{$oppcolor}_Maintime"] += $add_hours;
      }

      if ( $reset_byo == 2 ) // special reset for JAP-time
      {
         $this->game_query .= ",{$oppcolor}_Byotime=" . $this->game_row['Byotime'];
         $this->game_row["{$oppcolor}_Byotime"] = $this->game_row['Byotime'];
      }
      elseif ( $reset_byo == 1 ) // full byo-yomi reset
      {
         if ( $this->game_row["{$oppcolor}_Maintime"] <= 0 )
         {
            $new_byotime = $this->game_row['Byotime'];
            $new_byoper  = $this->game_row['Byoperiods'];
         }
         else
         {
            $new_byotime = 0;
            $new_byoper  = -1;
         }
         $this->game_query .= ",{$oppcolor}_Byotime=$new_byotime";
         $this->game_query .= ",{$oppcolor}_Byoperiods=$new_byoper";
         $this->game_row["{$oppcolor}_Byotime"] = $new_byotime;
         $this->game_row["{$oppcolor}_Byoperiods"] = $new_byoper;
      }

      if ( !$this->game_query )
         return 0; //nothing to do (shouldn't happen here)

      return ( $add_hours > 0 ) ? $add_hours : -$reset_byo;
   }//add_time


   // ------------ static functions ----------------------------

   /*!
    * \brief returns array( days => arr_days, byo_reset => bool ) for add-time-form
    * \param $game_row expect fields: Byotype, Byotime, (Black|White)_Byoperiods
    * \param $color_to_move BLACK | WHITE, used to check if byo-yomi-reset needed for current to-move
    */
   public static function make_add_time_info( array $game_row, $color_to_move )
   {
      $pfx = ($color_to_move == BLACK) ? 'Black' : 'White';

      $allow_reset = false;
      if ( $game_row['Byotype'] == BYOTYPE_CANADIAN )
         $allow_reset = true;
      elseif ( $game_row['Byotype'] == BYOTYPE_JAPANESE && $game_row["{$pfx}_Byoperiods"] >= 0 )
         $allow_reset = true;
      if ( $game_row['Byotime'] <= 0 ) // absolute-time
         $allow_reset = false;

      $arr_days = array();
      $startidx = ($allow_reset) ? 0 : 1;
      for ( $i=$startidx; $i <= MAX_ADD_DAYS; $i++)
         $arr_days[$i] = $i . ' ' . (($i>1) ? T_('days') : T_('day'));

      return array( 'days' => $arr_days, 'byo_reset' => $allow_reset );
   }//make_add_time_info

   /*!
    * \brief returns true, if user (uid) (or TD) is allowed to add additional
    *        time for uid's opponent in game specified by $game_row.
    * \param $game_row expect fields: Status, (Black|White)_(ID|Maintime), tid, Flags
    * \param $is_tdir true, if tournament-director with TD_FLAG_GAME_ADD_TIME-right
    */
   public static function allow_add_time_opponent( array $game_row, $uid, $is_tdir=false )
   {
      // must be a running-game (not allowed for fair-komi)
      if ( !isRunningGame($game_row['Status']) )
         return false;

      // must be one of my games (to give time to my opponent)
      // NOTE: that means adding-time for MPGs is only possible, if user is on turn stored in Black_ID or White_ID
      if ( $game_row['White_ID'] != $uid && $game_row['Black_ID'] != $uid )
         return false;

      // must not be a tournament-game except TD-allowed-to-add-time or game is detached from tournament
      if ( !$is_tdir && $game_row['tid'] != 0 && !($game_row['Flags'] & GAMEFLAGS_TG_DETACHED) )
         return false;

      // get opponents columns
      if ( $game_row['Black_ID'] == $uid )
         $oppcolor = 'White';
      else
         $oppcolor = 'Black';
      // don't exceed 365 days maintime
      if ( $game_row["{$oppcolor}_Maintime"]
            + time_convert_to_hours(MAX_ADD_DAYS,'days') > time_convert_to_hours(360,'days') )
         return false;

      return true;
   }//allow_add_time_opponent

   /*!
    * \brief Adds time to given player updating game-row and save into database.
    * \param $game_row pre-loaded game_row to add time for
    *        or numeric game-id to load the game from database.
    *
    *        Need the following fields set in game_row,
    *        and the fields needed for NextGameOrder::make_timeout_date():
    *           ID, tid, Status, Maintime, Byotype, Byotime, Byoperiods, ToMove_ID,
    *           LastTicks, Moves, ClockUsed,
    *           (Black|White)_(ID|OnVacation|Maintime|Byotime|Byoperiods)
    *
    * \param $uid user giving time to his opponent
    * \param $add_hours amount of hours to add to maintime of opponent,
    *        allowed range is 0 hour .. MAX_ADD_DAYS
    * \param $reset_byo if true, also byo-yomi will be resetted (for JAP/CAN)
    * \param $by_td_uid user-id of tournament-director, which adds time
    * \return number of hours added (may be 0), if time has been successfully added
    *        for opponent of player (uid) in specified game (gid).
    *        -1|-2 if only byo-yomi has been resetted (-1=full-reset, -2=part-reset).
    *        Otherwise error-string is returned.
    *
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public static function add_time_opponent( array &$game_row, $uid, $add_hours, $reset_byo=false, $by_td_uid=0 )
   {
      $game_addtime = new GameAddTime( $game_row, $uid, $by_td_uid );
      $add_hours = $game_addtime->add_time( $add_hours, $reset_byo );
      $reset_byo = $game_addtime->reset_byoyomi;

      if ( !is_numeric($add_hours) || ($add_hours == 0 && !$reset_byo) ) // error or nothing to do
         return $add_hours;

      // handle Games.TimeOutDate if opponent-to-move
      $to_move_id = $game_row['ToMove_ID'];
      if ( $to_move_id != $uid )
      {
         $col_to_move = ( $to_move_id == $game_row['Black_ID'] ) ? BLACK : WHITE;
         $timeout_date = NextGameOrder::make_timeout_date( $game_row, $col_to_move, $game_row['ClockUsed'], $game_row['LastTicks'] );

         $game_addtime->game_query .= ",TimeOutDate=$timeout_date";
         $game_row['TimeOutDate'] = $timeout_date;
      }


      // NOTE: HOT-SECTION to avoid double-clicks must be done by caller
      $gid = $game_row['ID'];
      $game_query = "UPDATE Games SET ".substr($game_addtime->game_query,1)
                  . " WHERE ID=$gid AND Status" . IS_RUNNING_GAME . " LIMIT 1";

      // insert entry in Moves-table
      $Moves = $game_row['Moves'];
      $Stone = ( $game_row['Black_ID'] == $uid ) ? BLACK : WHITE;
      $save_hours = ($add_hours < 0) ? 0 : $add_hours;
      $pos_y = ($reset_byo ? 1 : 0) | ($game_addtime->td_uid ? 2 : 0);
      $move_query = "INSERT INTO Moves (gid, MoveNr, Stone, PosX, PosY, Hours) VALUES "
         . "($gid, $Moves, $Stone, ".POSX_ADDTIME.", $pos_y, $save_hours)";

      // see also confirm.php
      db_query( "GameAddTime:add_time_opponent.update($gid)", $game_query );
      if ( mysql_affected_rows() != 1 ) //0 if it had done nothing
         error('mysql_update_game',"GameAddTime:add_time_opponent.update($gid)");

      db_query( "GameAddTime:add_time_opponent.insert_move($gid)", $move_query );
      if ( mysql_affected_rows() != 1 )
         error('mysql_insert_move',"GameAddTime:add_time_opponent.insert_move($gid)");

      // clear caches
      Board::delete_cache_game_moves( "GameAddTime:add_time_opponent($gid)", $gid );
      GameHelper::delete_cache_game_row( "GameAddTime:add_time_opponent($gid)", $gid );
      clear_cache_quick_status( $to_move_id, QST_CACHE_GAMES );
      GameHelper::delete_cache_status_games( "GameAddTime:add_time_opponent($gid)", $to_move_id );

      return $add_hours; // success (no-error)
   } //add_time_opponent

} //end 'GameAddTime'




/**
 * \class MultiPlayerGame
 *
 * \brief Class to handle multi-player-game.
 */
class MultiPlayerGame
{
   // ------------ static functions ----------------------------

   /*!
    * \brief Returns GAMETYPE for game-players or number of required players (if $count=true);
    *        null (0 if $count set) on wrong format or bad number of players.
    */
   public static function determine_game_type( $game_players, $count=false )
   {
      if ( trim($game_players) == '' || $game_players == '1:1' || $game_players == '2' )
         return ($count) ? 2 : GAMETYPE_GO;
      elseif ( is_numeric($game_players) )
      {
         if ( $game_players > 2 && $game_players <= MAX_GAME_PLAYERS && ($game_players & 1) ) // odd
            return ($count) ? $game_players : GAMETYPE_ZEN_GO;
      }
      elseif ( preg_match("/^\d+:\d+$/", $game_players) )
      {
         $arr = explode(':', $game_players);
         $cnt_players = $arr[0] + $arr[1];
         if ( $cnt_players > 2 && $cnt_players <= MAX_GAME_PLAYERS )
            return ($count) ? $cnt_players : GAMETYPE_TEAM_GO;
      }
      return ($count) ? 0 : null;
   }//determine_game_type

   /*! \brief Returns number of required players. */
   public static function determine_player_count( $game_players )
   {
      return self::determine_game_type( $game_players, true );
   }

   /*! \brief Returns max number of required players in all groups. */
   public static function determine_groups_player_count( $game_players )
   {
      $arr = explode(':', $game_players);
      return array( min($arr), max($arr) );
   }

   /*!
    * \brief Returns max number of required players for all groups per user.
    * \return array( uid => max-count of group with that user, ... )
    */
   public static function count_player_groups( array $arr_game_players )
   {
      $gpcnt = array();
      foreach ( $arr_game_players as $gp )
      {
         if ( isset($gpcnt[$gp->GroupColor]) )
            $gpcnt[$gp->GroupColor]++;
         else
            $gpcnt[$gp->GroupColor] = 1;
      }

      $group_max_players = array();
      foreach ( $arr_game_players as $gp )
         $group_max_players[$gp->uid] = $gpcnt[$gp->GroupColor];

      return $group_max_players;
   }//count_player_groups

   public static function is_single_player( $game_players, $is_black )
   {
      $arr = explode(':', $game_players);
      if ( count($arr) == 2 ) // TeamGo
         return ( $is_black ) ? ( $arr[0] == 1 ) : ( $arr[1] == 1 );
      else
         return false; // ZenGo
   }

   public static function build_game_type_filter_array( $prefix='' )
   {
      return array(
         T_('All') => '',
         GameTexts::get_game_type(GAMETYPE_GO)      => $prefix."GameType='".GAMETYPE_GO."'",
         GameTexts::get_game_type(GAMETYPE_TEAM_GO) => $prefix."GameType='".GAMETYPE_TEAM_GO."'",
         GameTexts::get_game_type(GAMETYPE_ZEN_GO)  => $prefix."GameType='".GAMETYPE_ZEN_GO."'",
         T_('Rengo#gametype') => $prefix."GameType='".GAMETYPE_TEAM_GO."' AND {$prefix}GamePlayers='2:2'",
         T_('Non-Std#gametype') => $prefix."GameType IN ('".GAMETYPE_TEAM_GO."','".GAMETYPE_ZEN_GO."')",
      );
   }//build_game_type_filter_array

   /*!
    * \brief "Deletes" (by updating) specified number of game-players for given game-id and flag-typed placeholder.
    * \param $gp_flag GPFLAG_WAITINGROOM | GPFLAG_INVITATION
    * \return true if slots removed; false if nothing updated
    */
   public static function revoke_offer_game_players( $gid, $del_count, $gp_flag )
   {
      if ( is_numeric($gid) && $gid > 0 && $del_count > 0 && $gp_flag > 0 )
      {
         // remove join-reservation for waiting-room or invitation
         $gpf_check = GPFLAG_RESERVED | $gp_flag;
         db_query( "MultiPlayerGame:revoke_offer_game_players.update_gp($gid,$del_count,$gp_flag)",
            "UPDATE GamePlayers SET Flags=Flags & ~".(GPFLAG_RESERVED|GPFLAG_WAITINGROOM|GPFLAG_INVITATION)
               . " WHERE gid=$gid AND uid=0 AND (Flags & $gpf_check) = $gpf_check LIMIT $del_count" );
         return true;
      }
      else
         return false;
   }//revoke_offer_game_players

   /*! \brief Returns number of game-players for given game-id. */
   public static function count_game_players( $gid )
   {
      $row = mysql_single_fetch( "MultiPlayerGame:count_game_players($gid)",
            "SELECT COUNT(*) AS X_Count FROM GamePlayers WHERE gid=$gid" );
      return ($row) ? (int)@$row['X_Count'] : 0;
   }

   /*! \brief Returns true if uid is game-player for given game-id. */
   public static function is_game_player( $gid, $uid )
   {
      $row = mysql_single_fetch( "MultiPlayerGame:is_game_player($gid,$uid)",
            "SELECT ID FROM GamePlayers WHERE gid=".((int)$gid)." AND uid=".((int)$uid)." LIMIT 1" );
      return ($row) ? 1 : 0;
   }

   /*!
    * \brief Returns diff to required number of players that joined MP-game.
    * \return 0 (=required number reached), <0 (missing players), >0 (too much players)
    */
   public static function check_count_game_players( $gid, $expected_player_count )
   {
      $player_count = self::count_game_players( $gid );
      return ( $player_count - $expected_player_count );
   }

   /*!
    * \brief Inserts game-players entries for given game-id and game-master $uid.
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public static function init_multi_player_game( $dbgmsg, $gid, $uid, $gp_count )
   {
      if ( $gp_count <= 2 )
         error('invalid_args', "$dbgmsg.init_multi_player_game.check.gp_count($gid,$uid,$gp_count)");

      $query = "INSERT GamePlayers (gid,uid,Flags) VALUES ";
      $query .= "($gid,$uid,".(GPFLAG_MASTER|GPFLAG_JOINED).")";
      for ( $i=2; $i <= $gp_count; $i++ )
         $query .= ", ($gid,0,0)";

      db_query( "$dbgmsg.init_multi_player_game.insert_gp($gid,$uid,$gp_count)",
         $query, 'mysql_start_game' );

      self::change_player_mpg_count( "$dbgmsg.init_multi_player_game", $gid, $uid, 1 );
      self::change_joined_players( $dbgmsg, $gid, 1 );

      clear_cache_quick_status( $uid, QST_CACHE_MPG );
   }//init_multi_player_game

   /*!
    * \brief Joins waiting-room MP-game for given user and check for race-conditions.
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section if used with other db-writes!!
    */
   public static function join_waitingroom_mp_game( $dbgmsg, $gid, $uid )
   {
      // check race-condition if other joined first
      db_query( "$dbgmsg.join_waitingroom_mp_game.update_game_players($gid,$uid)",
         "UPDATE GamePlayers SET uid=$uid, " .
            "Flags=(Flags & ~".GPFLAG_RESERVED.") | ".GPFLAG_JOINED." " .
         "WHERE gid=$gid AND uid=0 AND (Flags & ".(GPFLAG_RESERVED|GPFLAG_WAITINGROOM).") LIMIT 1",
         'waitingroom_join_error' );
      if ( mysql_affected_rows() != 1)
         error('waitingroom_join_too_late', "$dbgmsg.join_waitingroom_mp_game($gid,$uid)");

      self::change_player_mpg_count( "$dbgmsg.join_waitingroom_mp_game", $gid, $uid, 1 );
      self::change_joined_players( $dbgmsg, $gid, 1 );

      $master_uid = self::load_master_uid( "$dbgmsg.join_waitingroom_mp_game", $gid );
      if ( $master_uid > 0 )
         clear_cache_quick_status( $master_uid, QST_CACHE_MPG );
   }//join_waitingroom_mp_game

   /*! \brief Returns master-uid for given game-id; or else 0 if nothing found. */
   public static function load_master_uid( $dbgmsg, $gid )
   {
      $grow = mysql_single_fetch( "$dbgmsg.load_master_uid($gid)",
            "SELECT ToMove_ID FROM Games WHERE ID=$gid LIMIT 1");
      return ( $grow ) ? (int)@$grow['ToMove_ID'] : 0;
   }//load_master_uid

   /*! \brief Changes number of joined players of MP-game (store in Games.Moves). */
   public static function change_joined_players( $dbgmsg, $gid, $diff )
   {
      $diff = (int)$diff;
      db_query( "$dbgmsg.change_joined_players.upd_games($gid,$diff)",
         "UPDATE Games SET Moves=Moves+($diff) WHERE ID=$gid AND Status='".GAME_STATUS_SETUP."' LIMIT 1" );
   }//change_joined_players

   /*!
    * \brief Returns arr( GroupColor, GroupOrder, MoveColor ) to identify current game-player to move.
    * \param $game_players Games.GamePlayers = game-players-info
    * \param $game_moves Games.Moves = move-counter starting at 0
    * \param $handicap Games.Handicap = number of handicap-stones.
    * \param $add_moves how many moves in the future (setting-handicap = 1 move); can be <0
    */
   public static function calc_game_player_for_move( $game_players, $game_moves, $handicap, $add_moves=0 )
   {
      if ( $handicap > 0 )
         $moves = ( $game_moves < $handicap ) ? 0 : $game_moves - $handicap + 1;
      else
         $moves = $game_moves;
      $moves += $add_moves;
      if ( $moves < 0 )
         $moves = 0;
      $movecol = ($moves & 1) ? GPCOL_W : GPCOL_B;

      $arr = explode(':', $game_players);
      if ( count($arr) == 2 ) // Team-Go
      {
         if ( $moves & 1 ) // odd = WHITE
            return array( GPCOL_W, ( ( ($moves - 1) >> 1 ) % (int)$arr[1] ) + 1, $movecol );
         else // even = BLACK
            return array( GPCOL_B, ( ( $moves >> 1 ) % (int)$arr[0] ) + 1, $movecol );
      }
      else // Zen-Go
         return array( GPCOL_BW, ($moves % (int)$game_players) + 1, $movecol );
   }//calc_game_player_for_move

   public static function change_player_mpg_count( $dbgmsg, $gid, $uid, $diff )
   {
      db_query( "$dbgmsg.change_player_mpg_count($gid,$uid,$diff)",
         "UPDATE Players SET GamesMPG=GamesMPG+($diff) WHERE ID=$uid LIMIT 1" );
   }//change_player_mpg_count

   /*! \brief Starts MP-game for all participating (joined) players. */
   public static function update_players_start_mpgame( $gid )
   {
      // update Players for all game-players: Running++, GamesMPG--
      db_query( "MultiPlayerGame:update_players_start_mpgame($gid)",
         "UPDATE Players AS P INNER JOIN GamePlayers AS GP ON GP.uid=P.ID "
            . "SET P.Running=P.Running+1, P.GamesMPG=P.GamesMPG-1 WHERE GP.gid=$gid" );
   }//update_players_start_mpgame

   /*!
    * \brief Updates player on end of MP-game for given game-id.
    * \param $game_in_setup_mode true, if Players.GamesMPG must be synchronized because game in setup-mode
    * \param $del_game true=game is deleted; false=game is finished/ended normally
    */
   public static function update_players_end_mpgame( $gid, $game_in_setup_mode, $del_game )
   {
      $arr_set = ( $del_game )
         ? array()
         : array( 'P.Running=P.Running-1', 'P.Finished=P.Finished+1' ); // end-game

      // update Players for all game-players: GamesMPG-- if game in SETUP-mode (for joined or reserved-invitation)
      if ( $game_in_setup_mode )
         $arr_set[] = "P.GamesMPG=P.GamesMPG - IF( ( (GP.Flags & ".GPFLAG_JOINED.") OR " .
               "((GP.Flags & ".GPFLAGS_RESERVED_INVITATION.")=".GPFLAGS_RESERVED_INVITATION.") ), 1, 0)";

      // update Players for all game-players: Running--, Finished++;
      db_query( "MultiPlayerGame:update_players_end_mpgame($gid,$game_in_setup_mode)",
         "UPDATE Players AS P INNER JOIN GamePlayers AS GP ON GP.uid=P.ID "
            . "SET " . implode(', ', $arr_set) . " WHERE GP.gid=$gid" );
   }//update_players_end_mpgame

   /*!
    * \brief Returns default-templates for messages for MP-games with subject and body-text.
    * \param $mpg_arr array with additional keys:
    *    for mpg_type=MPGMSG_RESIGN:
    *       'move' = move for mpg_type=MPGMSG_RESIGN
    *    for mpg_type=MPGMSG_INVITE:
    *       'from_handle' = user-id (=handle) of game-master
    *       'game_type' = "$GameType ($GamePlayers)"
    *       'shape_id' = shape-id
    */
   public static function get_message_defaults( $mpg_type, $mpg_gid, array $mpg_arr )
   {
      switch ( (int)$mpg_type )
      {
         case MPGMSG_RESIGN:
            $move = (int)$mpg_arr['move'];
            return array(
               T_('May I resign?#mpg'),
               "<game_ $mpg_gid,$move>\n"
            );

         case MPGMSG_INVITE:
            $from_handle = @$mpg_arr['from_handle'];
            $game_type   = @$mpg_arr['game_type'];
            $shape_id    = @$mpg_arr['shape_id'];
            return array(
               // subject
               sprintf( T_('Invitation to multi-player-game from [%s]#mpg'), $from_handle ),
               // body
               sprintf( T_('Game-master %s invites you to a %s multi-player-game.#mpg'), "<user =$from_handle>", $game_type ) . "\n" .
               ( $shape_id > 0 ? sprintf( T_('This game is a shape-game (Shape #%s)#mpg'), $shape_id ) . "\n" : '' ) . "\n" .
               sprintf( T_('You can accept or reject the invitation on setup-page of game: %s#mpg'), "<game_ $mpg_gid>" ) . "\n\n" .
               T_('To reject the invitation, please inform the game-master by replying to this message.#mpg') . "\n" .
               T_('You may also want to discuss what team, color or playing order you prefer in the game (see FAQ for more details).#mpg') . "\n\n"
            );

         case MPGMSG_STARTGAME:
         default:
            return array(
               T_('Everybody ready to start the game?#mpg'),
               "<game_ $mpg_gid>\n"
            );
      }
   }//get_message_defaults

   /*! \brief Returns array of GamePlayer-objects (filled with User-object) for given game-id. */
   public static function load_game_players( $gid )
   {
      $result = db_query( "MultiPlayerGame:load_game_players($gid)",
         "SELECT GP.*, " .
            "P.Name, P.Handle, P.Rating2, P.RatingStatus, P.Country, P.OnVacation, " .
            "UNIX_TIMESTAMP(P.Lastaccess) AS X_Lastaccess " .
         "FROM GamePlayers AS GP LEFT JOIN Players AS P ON P.ID=GP.uid " .
         "WHERE gid=$gid ORDER BY GroupColor ASC, GroupOrder ASC" );

      $arr_gp = array();
      while ( $row = mysql_fetch_assoc($result) )
      {
         $uid = (int)$row['uid'];
         if ( $uid > 0 )
         {
            $user = new User( $uid, $row['Name'], $row['Handle'], 0, $row['X_Lastaccess'],
               $row['Country'], $row['Rating2'], $row['RatingStatus'] );
            $user->urow = array(
                  'OnVacation' => $row['OnVacation'],
               );
         }
         else
            $user = null;

         $gp = GamePlayer::build_game_player( $row['ID'], $row['gid'], $row['GroupColor'],
            $row['GroupOrder'], $row['Flags'], $uid, $user );
         $arr_gp[] = $gp;
      }
      mysql_free_result($result);

      return $arr_gp;
   }//load_game_players

   /*!
    * \brief Returns calculated average for groups of MP-game.
    * \param $gamedata game-id, or array of GamePlayer-objects with loaded User-object (with RatingStatus + Rating2)
    *        \see MultiPlayerGame.load_game_players()
    * \param $rating_update if true, return in format for rating-update: $arr_rating['b/wRating'] = rating
    * \return $arr_ratings[$group_color] = average-group-rating, or $arr_rating['b/wRating'] if $rating_udpate set
    */
   public static function calc_average_group_ratings( $gamedata, $rating_update=false )
   {
      $calc_ratings = array();
      if ( is_array($gamedata) )
      {
         foreach ( $gamedata as $gp )
         {
            if ( !is_null($gp->user) && $gp->user->hasRating() )
               $calc_ratings[$gp->GroupColor][] = $gp->user->Rating;
         }
      }
      elseif ( !is_numeric($gamedata) || $gamedata <= 0 )
         error('invalid_args', "MultiPlayerGame:calc_average_group_ratings.check.gid($gamedata)");
      else
      {
         $result = db_query( "MultiPlayerGame:calc_average_group_ratings.find($gamedata)",
            "SELECT GP.GroupColor, P.Rating2, P.RatingStatus " .
            "FROM GamePlayers AS GP INNER JOIN Players AS P ON P.ID=GP.uid " .
            "WHERE gid=$gamedata" );

         while ( $row = mysql_fetch_assoc($result) )
         {
            if ( $row['RatingStatus'] == RATING_INIT || $row['RatingStatus'] == RATING_RATED ) // user has rating
            {
               if ( is_valid_rating($row['Rating2']) )
                  $calc_ratings[$row['GroupColor']][] = $row['Rating2'];
            }
         }
         mysql_free_result($result);
      }

      // calc average rating for groups B/W | BW
      $arr_ratings = array();
      foreach ( $calc_ratings as $gr_col => $arr )
      {
         $cnt = count($arr);
         if ( $cnt )
            $arr_ratings[$gr_col] = array_sum($arr) / $cnt;
      }

      if ( $rating_update ) // convert result into format needed for rating-update
      {
         $upd_ratings = array();
         if ( isset($arr_ratings[GPCOL_BW]) )
            $upd_ratings['bRating'] = $upd_ratings['wRating'] = $arr_ratings[GPCOL_BW];
         else
         {
            $upd_ratings['bRating'] = $arr_ratings[GPCOL_B];
            $upd_ratings['wRating'] = $arr_ratings[GPCOL_W];
         }
         return $upd_ratings;
      }
      else
         return $arr_ratings;
   }//calc_average_group_ratings

} //end 'MultiPlayerGame'



/*!
 * \class GamePlayer
 *
 * \brief Class to model GamePlayers-entity.
 */
class GamePlayer
{
   public $id;
   public $gid;
   public $GroupColor;
   public $GroupOrder;
   public $Flags;
   public $uid;

   public $user = null;

   public function __construct( $id, $gid, $group_color='BW', $group_order=0, $flags=0, $uid=0 )
   {
      $this->id = (int)$id;
      $this->gid = (int)$gid;
      $this->setGroupColor( $group_color );
      $this->GroupOrder = (int)$group_order;
      $this->Flags = (int)$flags;
      $this->uid = (int)$uid;
   }

   public function setGroupColor( $group_color )
   {
      if ( !preg_match("/^(BW|B|W|G[12])$/", $group_color) )
         error('invalid_args', "GamePlayer.setGroupColor($group_color)");
      $this->GroupColor = $group_color;
   }

   public function getGroupColorOrder()
   {
      return self::get_group_color_order( $this->GroupColor );
   }


   // ------------ static functions ----------------------------

   public static function build_game_player( $id, $gid, $group_color, $group_order, $flags, $uid, User $user = null )
   {
      $gp = new GamePlayer( $id, $gid, $group_color, $group_order, $flags, $uid );
      $gp->user = $user;
      return $gp;
   }

   /*! \brief Returns GamePlayer-object for given game-id, group-color and group-order. */
   public static function load_game_player( $gid, $group_color, $group_order )
   {
      $sql_gr_col = mysql_addslashes($group_color);
      $sql_gr_order = (int)$group_order;
      $row = mysql_single_fetch( "GamePlayer:load_game_player($gid,$group_color,$group_order)",
            "SELECT * FROM GamePlayers " .
            "WHERE gid=$gid AND GroupColor='$sql_gr_col' AND GroupOrder=$sql_gr_order LIMIT 1" );
      if ( !$row )
         error('internal_error', "GamePlayer:load_game_player($gid,$group_color,$group_order)");
      return new GamePlayer( $row['ID'], $row['gid'], $row['GroupColor'], $row['GroupOrder'], $row['Flags'], $row['uid'] );
   }//load_game_player

   /*! \brief Returns GamePlayer-object for given game-id and user-id. */
   public static function load_game_player_by_uid( $gid, $uid )
   {
      $row = mysql_single_fetch( "GamePlayer:load_game_player_by_uid($gid,$uid)",
            "SELECT * FROM GamePlayers WHERE gid=$gid AND uid=$uid LIMIT 1" );
      return ( $row )
         ? new GamePlayer( $row['ID'], $row['gid'], $row['GroupColor'], $row['GroupOrder'], $row['Flags'], $row['uid'] )
         : null;
   }//load_game_player_by_uid

   /*! \brief Returns Players.ID for given game-id, group-color and group-order. */
   public static function load_uid_for_move( $gid, $group_color, $group_order )
   {
      $sql_gr_col = mysql_addslashes($group_color);
      $sql_gr_order = (int)$group_order;
      $row = mysql_single_fetch( "GamePlayer:load_uid_for_move($gid,$group_color,$group_order)",
            "SELECT uid FROM GamePlayers " .
            "WHERE gid=$gid AND GroupColor='$sql_gr_col' AND GroupOrder=$sql_gr_order LIMIT 1" );
      if ( !$row )
         error('internal_error', "GamePlayer:load_uid_for_move($gid,$group_color,$group_order)");
      return (int)@$row['uid'];
   }//load_uid_for_move

   /*! \brief Returns true, if GamePlayer-entry exists for given game-id and user-id (and flags). */
   public static function exists_game_player( $gid, $uid, $flags=0 )
   {
      $row = mysql_single_fetch( "GamePlayer:exists_game_player($gid,$uid,$flags)",
            "SELECT Flags FROM GamePlayers WHERE gid=$gid AND uid=$uid LIMIT 1" );
      return ( $row && $flags > 0 )
         ? ($row['Flags'] & $flags)
         : (bool) $row;
   }//exists_game_player

   /*!
    * \brief Returns array of Players.Handle for given game-id (and group-color).
    * \param $arr_users return-only, will be overwritten
    *        clear it, and save as arr_users["$group_color:$group_order"]
    *        = ( GroupColor/GroupOrder/uid/Handle/Name/Rating2/Sessioncode/Sessionexpire => values, ... )
    * \return [ Players.ID => Players.Handle, ... ]
    */
   public static function load_users_for_mpgame( $gid, $group_color = '', $skip_myself = false, array &$arr_users = null )
   {
      global $player_row;

      if ( !is_numeric($gid) || $gid <= 0 )
         error('invalid_args', "GamePlayers:load_users_for_mpgame.check.gid($gid,$group_color)");
      if ( (string)$group_color != '' && !preg_match("/^(BW|B|W|G[12])$/", $group_color) )
         error('invalid_args', "GamePlayers:load_users_for_mpgame.check.grcol($gid,$group_color)");

      $qpart_grcol = ($group_color) ? " AND GP.GroupColor='$group_color'" : '';
      $result = db_query( "GamePlayer:load_users_for_mpgame.find($gid,$group_color)",
            "SELECT GP.GroupColor, GP.GroupOrder, GP.uid, " .
               "P.Handle, P.Name, P.Rating2, P.Sessioncode, " .
               "UNIX_TIMESTAMP(P.Sessionexpire) AS X_Sessionexpire " .
            "FROM GamePlayers AS GP INNER JOIN Players AS P ON P.ID=GP.uid " .
            "WHERE GP.gid=$gid $qpart_grcol" );
      $out = array(); # uid => Handle
      $arr_users = array(); // clear
      while ( $row = mysql_fetch_array( $result ) )
      {
         if ( $skip_myself && $player_row['Handle'] == $row['Handle'] )
            continue;
         $out[$row['uid']] = $row['Handle'];
         $arr_users["{$row['GroupColor']}:{$row['GroupOrder']}"] = $row;
      }
      mysql_free_result($result);
      return $out;
   }//load_users_for_mpgame

   /*!
    * \brief Finds and return MPG-user with specific uid (if >0) or handle (if !='' and uid==0).
    * \note Used to check if specifc player (e.g. logged-in user) participates as MPG-player.
    * \param $mpg_users array filled as last arg in GamePlayer::load_users_for_mpgame()-func
    * \param $uid if > 0 search for that uid in $mpg_users
    * \param $handle if $uid==0 and non-empty search for that handle in $mpg_users
    * \return entry of $mpg_users (see GamePlayer::load_users_for_mpgame()-func) if match found; null otherwise
    */
   public static function find_mpg_user( array $mpg_users, $uid, $handle='' )
   {
      foreach ( $mpg_users as $key => $arr )
      {
         if ( ($uid > 0 && $arr['uid'] == $uid ) || ($uid == 0 && $arr['Handle'] == $handle ) )
            return $arr;
      }
      return null;
   }//find_mpg_user

   /*! \brief Returns user-map ( uid/Handle/etc => vals ) as created in arr_users by load_users_for_mpgame()-method. */
   public static function get_user_info( array $arr_users, $group_color, $group_order )
   {
      return @$arr_users["{$group_color}:{$group_order}"];
   }//get_user_info

   public static function build_image_group_color( $group_color )
   {
      static $arr_col_images = null;
      if ( is_null($arr_col_images) )
      {
         global $base_path;

         $arr_col_images = array(
            'BW'  => image( $base_path.'17/y.gif', T_('Black/White or unset#gpcol'), null ),
            'B'   => image( $base_path.'17/b.gif', T_('Black#gpcol'), null ),
            'W'   => image( $base_path.'17/w.gif', T_('White#gpcol'), null ),
            'G1'  => image( $base_path.'17/bw.gif', T_('Group #1#gpcol'), null ),
            'G2'  => image( $base_path.'17/wb.gif', T_('Group #2#gpcol'), null ),
         );
      }
      return @$arr_col_images[$group_color];
   }//build_image_group_color

   public static function get_group_color_text( $group_color=null )
   {
      static $arr_group_cols = null;
      if ( is_null($arr_group_cols) )
      {
         $arr_group_cols = array(
            'BW'  => T_('BW#gpcol'),
            'B'   => T_('Black#gpcol'),
            'W'   => T_('White#gpcol'),
            'G1'  => T_('Group #1#gpcol'),
            'G2'  => T_('Group #2#gpcol'),
         );
      }
      return (is_null($group_color)) ? $arr_group_cols : @$arr_group_cols[$group_color];
   }//get_group_color_text

   public static function get_group_color_order( $group_color )
   {
      static $arr_grcol_order = null;
      if ( is_null($arr_grcol_order) )
      {
         $arr_grcol_order = array(
            'B'   => 0,
            'W'   => 1,
            'G1'  => 2,
            'G2'  => 3,
            'BW'  => 4,
         );
      }
      return $arr_grcol_order[$group_color];
   }//get_group_color_order

   /*!
    * \brief Deletes reserved invitation for MP-game.
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public static function delete_reserved_invitation( $gid, $uid )
   {
      db_query( "GamePlayer:delete_reserved_invitation.gp_upd($gid,$uid)",
         "UPDATE GamePlayers SET uid=0, GroupColor='".GPCOL_DEFAULT."', GroupOrder=0, Flags=0 " .
         "WHERE gid=$gid AND uid=$uid AND " .
            "(Flags & ".GPFLAGS_RESERVED_INVITATION.") = ".GPFLAGS_RESERVED_INVITATION." " .
         "LIMIT 1" );

      MultiPlayerGame::change_player_mpg_count( "$dbgmsg.delete_reserved_invitation", $gid, $uid, -1 );
   }//delete_reserved_invitation

   /*!
    * \brief Deletes joined game-player of MP-game.
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public static function delete_joined_player( $gid, $uid )
   {
      $dbgmsg = "GamePlayer:delete_joined_player($gid,$uid)";
      db_query( "$dbgmsg.gp_upd",
         "UPDATE GamePlayers SET uid=0, GroupColor='".GPCOL_DEFAULT."', GroupOrder=0, Flags=0 " .
         "WHERE gid=$gid AND uid=$uid AND (Flags & ".GPFLAG_JOINED.") > 0 " .
         "LIMIT 1" );

      MultiPlayerGame::change_player_mpg_count( "$dbgmsg.delete_joined_player", $gid, $uid, -1 );
   }//delete_joined_player

} // end 'GamePlayer'




/**
 * \class GameHelper
 *
 * \brief Class to handle general game-actions.
 */
class GameHelper
{

   /*!
    * \brief Deletes non-tourney non-detached (=non-annulled) game and all related tables for given game-id,
    *       if not finished yet.
    * \param $upd_players if true, also decrease Players.Running & GameStatus.Running
    * \return true on success, false on invalid args or invalid game to delete (finished, tourney)
    */
   public static function delete_running_game( $gid, $upd_players=true )
   {
      if ( !is_numeric($gid) && $gid <= 0 )
         return false;

      $grow = mysql_single_fetch( "GameHelper:delete_running_game.check.gid($gid)",
            "SELECT Status, tid, Flags, DoubleGame_ID, GameType, Black_ID, White_ID, ToMove_ID " .
            "FROM Games WHERE ID=$gid LIMIT 1");
      if ( !$grow || (int)@$grow['tid'] > 0 || ((int)@$grow['Flags'] & GAMEFLAGS_TG_DETACHED) )
         return false;
      elseif ( @$grow['Status'] == GAME_STATUS_FINISHED ) // must not be finished game, allow for setup/fair-komi
      {
         error('invalid_args', "GameHelper:delete_running_game.check.status($gid,{$grow['Status']})");
         return false; // if passing through on error()
      }

      ta_begin();
      {//HOT-section to delete game table-entries for running game
         self::remove_double_game_reference( $gid, (int)@$grow['DoubleGame_ID'] );

         // delete potentially assigned waiting-room-entry (max. 1)
         if ( $grow['GameType'] != GAMETYPE_GO )
            db_query( "GameHelper:delete_running_game.del_wroom($gid)",
               "DELETE FROM Waitingroom WHERE gid=$gid LIMIT 1" );

         $Black_ID = (int)@$grow['Black_ID'];
         $White_ID = (int)@$grow['White_ID'];
         if ( $upd_players )
         {
            if ( $grow['GameType'] == GAMETYPE_GO )
            {
               db_query( "GameHelper:delete_running_game.upd_players($gid,$Black_ID,$White_ID)",
                  "UPDATE Players SET Running=Running-1 WHERE ID IN ($Black_ID,$White_ID) LIMIT 2" );
               db_query( "GameHelper:delete_running_game.upd_gamestats($gid,$Black_ID,$White_ID)",
                  // only UPDATE as entries exist for started game
                  "UPDATE GameStats SET Running=Running-1 " .
                  "WHERE uid=IF($Black_ID<$White_ID,$Black_ID,$White_ID) AND oid=IF($Black_ID<$White_ID,$White_ID,$Black_ID) LIMIT 1" );
            }
            else
               MultiPlayerGame::update_players_end_mpgame( $gid, ($grow['Status'] == GAME_STATUS_SETUP), /*del*/true );
         }

         self::_delete_base_game_tables( $gid );

         GameInvitation::delete_game_invitations( $gid );

         // clear caches
         $tomove_id = (int)@$grow['ToMove_ID'];
         clear_cache_quick_status( $tomove_id, QST_CACHE_GAMES );
         self::delete_cache_status_games( "GameHelper:delete_running_game($gid)", $tomove_id );
      }
      ta_end();

      return true;
   }//delete_running_game

   /*!
    * \brief Deletes finished non-tourney non-detached (=non-annulled) unrated game and all related tables
    *       for given game-id.
    * \note Players-table will be updated (decrease Players.Finished & GameStats.Finished)
    * \return true on success, false on invalid args or invalid game to delete (not finished, tourney,
    *       annulled, rated game)
    */
   public static function delete_finished_unrated_game( $gid )
   {
      if ( !is_numeric($gid) && $gid <= 0 )
         return false;

      $grow = mysql_single_fetch( "GameHelper:delete_finished_unrated_game.check.gid($gid)",
         "SELECT Status, tid, Flags, DoubleGame_ID, GameType, Black_ID, White_ID, Rated " .
         "FROM Games WHERE ID=$gid LIMIT 1");
      if ( is_null($grow) )
         return false;
      elseif ( @$grow['Status'] != GAME_STATUS_FINISHED ) // must be finished game
      {
         error('invalid_args', "GameHelper:delete_finished_unrated_game.check.status($gid,{$grow['Status']})");
         return false; // if passing through on error()
      }
      elseif ( (int)@$grow['tid'] > 0 || ((int)@$grow['Flags'] & GAMEFLAGS_TG_DETACHED) )
         return false;
      elseif ( @$grow['Rated'] != 'N' )
         return false;

      ta_begin();
      {//HOT-section to delete game table-entries for finished game
         self::remove_double_game_reference( $gid, (int)@$grow['DoubleGame_ID'] );

         if ( $grow['GameType'] == GAMETYPE_GO )
         {
            $Black_ID = (int)@$grow['Black_ID'];
            $White_ID = (int)@$grow['White_ID'];
            db_query( "GameHelper:delete_finished_unrated_game.upd_players($gid,$Black_ID,$White_ID)",
               "UPDATE Players SET Finished=Finished-1 WHERE ID IN ($Black_ID,$White_ID) LIMIT 2" );

            db_query( "GameHelper:delete_finished_unrated_game.upd_gamestats($gid,$Black_ID,$White_ID)",
               // only UPDATE as entries exist for finished game
               "UPDATE GameStats SET Finished=Finished-1 " .
               "WHERE uid=IF($Black_ID<$White_ID,$Black_ID,$White_ID) AND oid=IF($Black_ID<$White_ID,$White_ID,$Black_ID) LIMIT 1" );
         }
         else
         {
            db_query( "GameHelper:delete_finished_unrated_game.upd_mgplayers($gid)",
               "UPDATE Players AS P INNER JOIN GamePlayers AS GP ON GP.uid=P.ID "
                  . "SET P.Finished=P.Finished-1 WHERE GP.gid=$gid" );
         }

         self::_delete_base_game_tables( $gid );
      }
      ta_end();

      return true;
   }//delete_finished_unrated_game

   /*! \brief Marks reference in other double-game to indicate referring game has vanished. */
   public static function remove_double_game_reference( $gid, $double_gid )
   {
      $double_gid = (int)$double_gid;
      if ( $double_gid > 0 )
         db_query( "GameHelper:remove_double_game_reference.doublegame($gid)",
            "UPDATE Games SET DoubleGame_ID=-ABS(DoubleGame_ID) WHERE ID=$double_gid LIMIT 1" );
   }//remove_double_game_reference

   /*!
    * \brief Deletes basic game-related table-entries for given game-id.
    * \internal
    */
   private static function _delete_base_game_tables( $gid )
   {
      $dbgmsg = "GameHelper:_delete_base_game_tables($gid)";

      //HOT-section to avoid potential race-condition as DGS does not have real transactions,
      //    game-ending while this is called leads to corrupted data!
      db_lock( $dbgmsg,
         "GamesPriority WRITE, Observers WRITE, GameSgf WRITE, GamesNotes WRITE, MoveMessages WRITE, " .
         ( ALLOW_CONDITIONAL_MOVES ? "MoveSequence WRITE, " : '' ) .
         "Moves WRITE, GamePlayers WRITE, Games WRITE " );
      {//LOCK Games-related tables
         NextGameOrder::delete_game_priorities($gid);
         db_query( "$dbgmsg.observers", "DELETE FROM Observers WHERE ID=$gid" );
         db_query( "$dbgmsg.game_sgf", "DELETE FROM GameSgf WHERE gid=$gid" );
         db_query( "$dbgmsg.notes", "DELETE FROM GamesNotes WHERE gid=$gid" );
         db_query( "$dbgmsg.movemsg", "DELETE FROM MoveMessages WHERE gid=$gid" );
         if ( ALLOW_CONDITIONAL_MOVES )
            db_query( "$dbgmsg.cond_moves", "DELETE FROM MoveSequence WHERE gid=$gid" );
         db_query( "$dbgmsg.moves", "DELETE FROM Moves WHERE gid=$gid" );
         db_query( "$dbgmsg.gameplayers", "DELETE FROM GamePlayers WHERE gid=$gid" );
         db_query( "$dbgmsg.games", "DELETE FROM Games WHERE ID=$gid LIMIT 1" );
      }
      db_unlock();

      // clear big-cache-entries to free cache-space
      DgsCache::delete_group( "$dbgmsg.observers",
         CACHE_GRP_GAME_OBSERVERS, "Observers.$gid" );
      if ( ALLOW_CONDITIONAL_MOVES )
         MoveSequence::delete_cache_move_sequence( "$dbgmsg.cond_moves", $gid );
      Board::delete_cache_game_moves( "$dbgmsg.moves", $gid );
      Board::delete_cache_game_move_messages( "$dbgmsg.movemsg", $gid );
      self::delete_cache_game_row( "$dbgmsg.games", $gid );
   }//_delete_base_game_tables

   /*!
    * \brief Deletes game-invitation game (on INVITED-status).
    * \return true for success; false on failure.
    *
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public static function delete_invitation_game( $dbgmsg, $gid, $uid1, $uid2 )
   {
      db_query( "GameHelper:delete_invitation_game.delgame.$dbgmsg($gid,$uid1,$uid2)",
         "DELETE FROM Games WHERE ID=$gid AND Status='".GAME_STATUS_INVITED."' " .
            "AND ( Black_ID=$uid1 OR White_ID=$uid1 ) " .
            "AND ( Black_ID=$uid2 OR White_ID=$uid2 ) " .
            "LIMIT 1" );
      if ( mysql_affected_rows() != 1)
      {
         error('game_delete_invitation', "GameHelper:delete_invitation_game.delres.$dbgmsg($gid,$uid1,$uid2)");
         return false;
      }

      GameInvitation::delete_game_invitations( $gid );

      return true;
   }//delete_invitation_game

   public static function update_players_start_game( $dbgmsg, $uid1, $uid2, $game_count, $rated_game )
   {
      $dbgmsg = "GameHelper:update_players_start_game($uid1,$uid2,$game_count,$rated_game).$dbgmsg";
      if ( !is_numeric($uid1) || !is_numeric($uid2) || !is_numeric($game_count) || $game_count <= 0 )
         error('invalid_args', "$dbgmsg.check.uids_gcnt");

      $upd_players = new UpdateQuery('Players');
      $upd_players->upd_raw('Running', "Running + $game_count" );
      if ( $rated_game )
         $upd_players->upd_txt('RatingStatus', RATING_RATED);
      db_query( "$dbgmsg.update",
         "UPDATE Players SET " . $upd_players->get_query() . " WHERE ID IN ($uid1,$uid2) LIMIT 2" );

      db_query( "$dbgmsg.update",
         "INSERT INTO GameStats (uid,oid,Running) " .
         "VALUES (IF($uid1<$uid2,$uid1,$uid2),IF($uid1<$uid2,$uid2,$uid1),$game_count) " .
         "ON DUPLICATE KEY UPDATE Running=Running + $game_count" );
   }//update_players_start_game

   /*!
    * \brief Updates game-stats in Players-table for simple or multi-player game when game is finished.
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public static function update_players_end_game( $dbgmsg, $gid, $game_type, $rated_status, $score,
         $black_id, $white_id, $black_start_rating, $white_start_rating )
   {
      if ( !is_numeric($gid) && $gid <= 0 )
         return;

      if ( $game_type == GAMETYPE_GO )
      {
         db_query( $dbgmsg."GameHelper:update_players_end_game.W($gid,$white_id)",
            "UPDATE Players SET Running=Running-1, Finished=Finished+1"
            .($rated_status ? '' : ", RatedGames=RatedGames+1"
               .($score > 0 ? ", Won=Won+1" : ($score < 0 ? ", Lost=Lost+1 " : ""))
             ). " WHERE ID=$white_id LIMIT 1" );

         db_query( $dbgmsg."GameHelper:update_players_end_game.B($gid,$black_id)",
            "UPDATE Players SET Running=Running-1, Finished=Finished+1"
            .($rated_status ? '' : ", RatedGames=RatedGames+1"
               .($score < 0 ? ", Won=Won+1" : ($score > 0 ? ", Lost=Lost+1 " : ""))
             ). " WHERE ID=$black_id LIMIT 1" );

         db_query( $dbgmsg."GameHelper:update_players_end_game.upd_gamestats($gid,$black_id,$white_id)",
            // only UPDATE as entries exist for started game
            "UPDATE GameStats SET Running=Running-1, Finished=Finished+1 " .
            "WHERE uid=IF($black_id<$white_id,$black_id,$white_id) AND oid=IF($black_id<$white_id,$white_id,$black_id) LIMIT 1" );

         // track game-count with weaker player
         $hero_uid = self::determine_finished_game_hero_uid( $black_id, $white_id, $black_start_rating, $white_start_rating );
         if ( $hero_uid > 0 )
         {
            db_query( $dbgmsg."GameHelper:update_players_end_game.C($gid,$hero_uid)",
               "UPDATE Players SET GamesWeaker=GamesWeaker+1 WHERE ID=$hero_uid LIMIT 1" );
         }
      }
      else // multi-player-go
         MultiPlayerGame::update_players_end_mpgame( $gid, /*setup*/false, /*del*/false );
   }//update_players_end_game

   /*! \brief Returns uid for hero-awarding (playing with weaker player); or 0 if no user eligible. */
   public static function determine_finished_game_hero_uid( $black_id, $white_id, $b_start_rating, $w_start_rating )
   {
      if ( $b_start_rating >= MIN_RATING
            && ( $w_start_rating <= -OUT_OF_RATING || $b_start_rating >= $w_start_rating + MIN_RATDIFF_HERO ) )
         $hero_uid = $black_id;
      elseif ( $w_start_rating >= MIN_RATING
            && ( $b_start_rating <= -OUT_OF_RATING || $w_start_rating >= $b_start_rating + MIN_RATDIFF_HERO ) )
         $hero_uid = $white_id;
      else
         $hero_uid = 0;

      return $hero_uid;
   }//determine_finished_game_hero_uid

   /*!
    * \brief Fills result-map( prefix.'Running|Finished' => count ) with started & finished games for a pair of players.
    * \param $uid if 0 fills result-map with 0 values for expected keys
    */
   public static function count_games_with_opponent( $uid, $opp, array &$result = null, $prefix = '' )
   {
      $uid = (int)$uid;
      $opp = (int)$opp;
      $result[$prefix.'Running'] = $result[$prefix.'Finished'] = 0;
      if ( $uid == 0 )
         return null;

      $row = User::load_game_stats_for_users( 'GameHelper:count_games_with_opponent', $uid, $opp );
      if ( $row && is_array($result) )
      {
         $result[$prefix.'Running'] = $row['Running'];
         $result[$prefix.'Finished'] = $row['Finished'];
      }
      return $row;
   }//count_games_with_opponent

   /*!
    * \brief Updates clock-values for game.
    * \param $grow game-row with mandatory fields:
    *        Maintime, Byotime, ClockUsed, LastTicks, WeekendClock,
    *        Black_Byoperiods, Black_Byotime, Black_Maintime, Black_OnVacation,
    *        White_Byoperiods, White_Byotime, White_Maintime, White_OnVacation,
    * \return array( hours, UpdateQuery )
    */
   public static function update_clock( $dbgmsg, array $grow, $to_move, $next_to_move, $do_ticks=true )
   {
      $upd_query = new UpdateQuery('Games');
      $hours = 0;
      if ( $grow['Maintime'] > 0 || $grow['Byotime'] > 0)
      {
         if ( $do_ticks )
         {
            // LastTicks may handle -(time spend) at the moment of the start of vacations
            // time since start of move in the reference of the ClockUsed by the game
            $clock_ticks = get_clock_ticks( $dbgmsg.'.GH.update_clock', $grow['ClockUsed'] );
            $hours = ticks_to_hours( $clock_ticks - $grow['LastTicks'] );

            if ( $to_move == BLACK )
            {
               time_remaining( $hours,
                  $grow['Black_Maintime'], $grow['Black_Byotime'], $grow['Black_Byoperiods'],
                  $grow['Maintime'], $grow['Byotype'], $grow['Byotime'], $grow['Byoperiods'], true);
               $upd_query->upd_num('Black_Maintime', $grow['Black_Maintime']);
               $upd_query->upd_num('Black_Byotime', $grow['Black_Byotime']);
               $upd_query->upd_num('Black_Byoperiods', $grow['Black_Byoperiods']);
            }
            else // WHITE
            {
               time_remaining( $hours,
                  $grow['White_Maintime'], $grow['White_Byotime'], $grow['White_Byoperiods'],
                  $grow['Maintime'], $grow['Byotype'], $grow['Byotime'], $grow['Byoperiods'], true);
               $upd_query->upd_num('White_Maintime', $grow['White_Maintime']);
               $upd_query->upd_num('White_Byotime', $grow['White_Byotime']);
               $upd_query->upd_num('White_Byoperiods', $grow['White_Byoperiods']);
            }
         }//do_ticks

         // determine clock-used, last-ticks, timeout-date
         $pfx = ( $next_to_move == BLACK ) ? 'Black' : 'White';
         $next_player_on_vacation = $grow["{$pfx}_OnVacation"] > 0;
         $next_clockused = get_game_clock( $grow['WeekendClock'], $next_player_on_vacation );
         $next_lastticks = get_clock_ticks( $dbgmsg.'.GH.update_clock2', $next_clockused );

         $timeout_date = NextGameOrder::make_timeout_date( $grow, $next_to_move, $next_clockused, $next_lastticks );

         $upd_query->upd_num('LastTicks', $next_lastticks);
         $upd_query->upd_num('ClockUsed', $next_clockused);
         $upd_query->upd_num('TimeOutDate', $timeout_date);
      }
      return array( $hours, $upd_query );
   }//update_clock

   public static function load_game_row( $dbgmsg, $gid, $add_fields=false )
   {
      $addfields_query = ( $add_fields )
         ? "black.Name AS Blackname, " .
           "black.Rank AS Blackrank, " .
           "black.Rating2 AS Blackrating, " .
           "black.RatingStatus AS Blackratingstatus, " .
           "UNIX_TIMESTAMP(black.Lastaccess) AS Black_Lastaccess, " .
           "white.Name AS Whitename, " .
           "white.Rank AS Whiterank, " .
           "white.Rating2 AS Whiterating, " .
           "white.RatingStatus AS Whiteratingstatus, " .
           "UNIX_TIMESTAMP(white.Lastaccess) AS White_Lastaccess, "
         : '';
      $grow = mysql_single_fetch( "GameHelper.load_game_row($gid).$dbgmsg",
            "SELECT G.*, " .
            $addfields_query .
            "black.OnVacation AS Black_OnVacation, " . // for GameHelper::update_clock()
            "white.OnVacation AS White_OnVacation," .
            "black.Handle AS Blackhandle, " .
            "white.Handle AS Whitehandle " .
            "FROM Games AS G " .
                "INNER JOIN Players AS black ON black.ID=G.Black_ID " .
                "INNER JOIN Players AS white ON white.ID=G.White_ID " .
            "WHERE G.ID=$gid LIMIT 1" )
         or error('unknown_game', "GameHelper.load_game_row2($gid).$dbgmsg");
      return $grow;
   }//load_game_row

   public static function load_cache_game_row( $dbgmsg, $gid )
   {
      $dbgmsg = "GameHelper:load_cache_game_row($gid).$dbgmsg";
      $key = "Games.$gid";

      $row = DgsCache::fetch( $dbgmsg, CACHE_GRP_GAMES, $key );
      if ( is_null($row) )
      {
         $row = self::load_game_row( $dbgmsg, $gid, /*add-fields*/true );
         if ( $row && isStartedGame(@$row['Status']) )
            DgsCache::store( $dbgmsg, CACHE_GRP_GAMES, $key, $row, 10*SECS_PER_MIN );
      }

      return $row;
   }//load_cache_game_row

   public static function delete_cache_game_row( $dbgmsg, $gid )
   {
      DgsCache::delete( $dbgmsg, CACHE_GRP_GAMES, "Games.$gid" );
   }

   public static function get_quick_game_action( $game_status, $handicap, $moves, $fk )
   {
      if ( $handicap > 0 && $moves < $handicap && $game_status == GAME_STATUS_PLAY )
         return 1; // set handicap-stones
      elseif ( $game_status == GAME_STATUS_PLAY || $game_status == GAME_STATUS_PASS )
         return 2; // move
      elseif ( $game_status == GAME_STATUS_SCORE || $game_status == GAME_STATUS_SCORE2 )
         return 3; // scoring
      elseif ( $game_status == GAME_STATUS_KOMI && !is_null($fk) )
         return $fk->get_quick_fair_komi_game_action();
      else
         return 0; // unsupported
   }//get_quick_game_action

   public static function extend_query_with_game_notes( QuerySQL &$qsql, $uid, $tablename='Games' )
   {
      $qsql->add_part( SQLP_FIELDS, "GN.Notes AS X_Note" );
      $qsql->add_part( SQLP_FROM, "LEFT JOIN GamesNotes AS GN ON GN.gid=$tablename.ID AND GN.uid=$uid" );
   }

   public static function extend_query_with_game_prio( QuerySQL &$qsql, $uid, $load_prio, $tablename='Games' )
   {
      if ( $load_prio )
      {
         $qsql->add_part( SQLP_FIELDS, "COALESCE(GPRIO.Priority,0) AS X_Priority" );
         $qsql->add_part( SQLP_FROM, "LEFT JOIN GamesPriority AS GPRIO ON GPRIO.gid=$tablename.ID AND GPRIO.uid=$uid" );
      }
      else
         $qsql->add_part( SQLP_FIELDS, '0 AS X_Priority');
   }//extend_query_with_game_prio

   public static function load_cache_game_notes( $dbgmsg, $gid, $uid )
   {
      $dbgmsg = "GameHelper:load_cache_game_notes($gid,$uid).$dbgmsg";
      $key = "GameNotes.$gid.$uid";

      $row = DgsCache::fetch( $dbgmsg, CACHE_GRP_GAME_NOTES, $key );
      if ( is_null($row) )
      {
         $row = mysql_single_fetch( $dbgmsg,
            "SELECT Hidden, Notes FROM GamesNotes WHERE gid=$gid AND uid=$uid LIMIT 1" );

         DgsCache::store( $dbgmsg, CACHE_GRP_GAME_NOTES, $key, $row, 30*SECS_PER_MIN );
      }

      return $row;
   }//load_cache_game_notes

   /*!
    * \brief Updates GamesNotes for given game-id and user, but only if user is player of game.
    * \throws error 'not_game_player' if user not a player of the game
    */
   public static function update_game_notes( $dbgmsg, $gid, $uid, $hidden, $notes )
   {
      $gid = (int)$gid;
      $uid = (int)$uid;
      $dbgmsg = "GameHelper:update_game_notes($gid,$uid,$hidden).$dbgmsg";

      $row = mysql_single_fetch( "$dbgmsg.chk_my_game",
         "SELECT IF(ISNULL(GP.uid),IF(G.Black_ID=$uid OR G.White_ID=$uid,1,0),2) AS X_MyGame " .
         "FROM Games AS G LEFT JOIN GamePlayers AS GP ON GP.gid=G.ID and GP.uid=$uid where G.ID=$gid LIMIT 1" );
      if ( (int)@$row['X_MyGame'] == 0 )
         error('not_game_player', $dbgmsg);

      if ( $hidden != 'Y' )
         $hidden = 'N'; // use default (not hidden) for invalid args

      ta_begin();
      {//HOT-section to update private game-notes
         // note: GamesNotes needs PRIMARY KEY (gid,player):
         db_query( $dbgmsg,
            "REPLACE INTO GamesNotes (gid,uid,Hidden,Notes) " .
            "VALUES ($gid,$uid,'$hidden','" . mysql_addslashes($notes) . "')" );

         DgsCache::store( $dbgmsg, CACHE_GRP_GAME_NOTES, "GameNotes.$gid.$uid",
            array( 'Hidden' => $hidden, 'Notes' => $notes ), 30*SECS_PER_MIN );
      }
      ta_end();
   }//update_game_notes

   public static function game_need_mark_dead( $game_status )
   {
      return ( $game_status == GAME_STATUS_SCORE || $game_status == GAME_STATUS_SCORE2 || $game_status == GAME_STATUS_FINISHED );
   }

   /*!
    * \brief Loads status-games rows-array from cache (if stored).
    * \param $next_game_order NGO_...; normally from $player_row['NextGameOrder']
    * \param $field_lastaccess fieldname of $player_row to check for last-access within minute (if longer ago
    *        no need to cache anything); leave empty '' to always store in cache.
    * \return non-null array with rows from db-query
    */
   public static function load_cache_status_games( $dbgmsg, $next_game_order, $field_lastaccess='X_Lastaccess',
         $load_prio=false, $load_notes=false )
   {
      global $player_row, $NOW;

      $uid = (int)@$player_row['ID'];
      $dbgmsg = $dbgmsg.".GameHelper:load_cache_status_games($uid)";
      $key = "StatusGames.$uid";

      // build status-query (including next-game-order)
      $qsql = NextGameOrder::build_status_games_query(
         $uid, IS_STARTED_GAME, $next_game_order, /*ticks*/true, /*tourney*/true, $load_prio, $load_notes );
      $query = $qsql->get_select();
      $chksum_query = crc32($query); // compare with stored checksum in cache

      $result = DgsCache::fetch( $dbgmsg, CACHE_GRP_GAMELIST_STATUS, $key );
      if ( is_array($result) )
      {
         // check if db-query matches stored db-query by comparing check-sum
         if ( count($result) > 0 )
         {
            $stored_chksum_query = array_shift($result); // remove checksum for final result
            if ( $stored_chksum_query != $chksum_query )
               $result = null; // different query -> reload
         }
         else
            $result = null; // invalid store-data -> reload
      }
      if ( is_null($result) )
      {
         $result = array( $chksum_query );
         $db_result = db_query( $dbgmsg.'.find_games', $query );
         while ( $row = mysql_fetch_assoc($db_result) )
            $result[] = $row;
         mysql_free_result($db_result);

         // store in cache only if last-access within one min (otherwise it's expired immediately) thereby saving cache-space
         $last_access = ($field_lastaccess ) ? (int)@$player_row[$field_lastaccess] : 0;
         $store_cache = ( $last_access >= $NOW - 1*SECS_PER_MIN );
         if ( $store_cache )
            DgsCache::store( $dbgmsg, CACHE_GRP_GAMELIST_STATUS, $key, $result, 1*SECS_PER_MIN );
         array_shift($result); // remove checksum for final result (only needed for cache)
      }

      return $result;
   }//load_cache_status_games

   public static function delete_cache_status_games( $dbgmsg, $uid, $oid=0 )
   {
      DgsCache::delete( $dbgmsg, CACHE_GRP_GAMELIST_STATUS, "StatusGames.$uid" );
      if ( $oid > 0 )
         DgsCache::delete( $dbgmsg, CACHE_GRP_GAMELIST_STATUS, "StatusGames.$oid" );
   }

} // end 'GameHelper'




/**
 * \class FairKomiNegotiation
 *
 * \brief Class to handle fair-komi.
 */
class FairKomiNegotiation
{
   private $gid;
   public $game_setup; // GameSetup-obj
   private $game_status; // GAME_STATUS_...
   private $black_id;
   private $white_id;
   private $tomove_id;

   private $black_handle;
   private $white_handle;

   public function __construct( GameSetup $game_setup, array $game_row )
   {
      $this->game_setup = $game_setup;
      $this->read_from_game_row( $game_row );
   }

   /*! \brief Reads fields from row: ID, Status, Black_ID, White_ID, ToMove_ID [, Black/Whitehandle ]. */
   private function read_from_game_row( array $grow )
   {
      $this->gid = (int)$grow['ID'];
      $this->game_status = $grow['Status'];
      $this->black_id = (int)$grow['Black_ID'];
      $this->white_id = (int)$grow['White_ID'];
      $this->tomove_id = (int)$grow['ToMove_ID'];

      $this->black_handle = @$grow['Blackhandle'];
      $this->white_handle = @$grow['Whitehandle'];
   }//read_from_game_row

   public function has_both_komibids()
   {
      return !is_null($this->game_setup->Komi) && !is_null($this->game_setup->OppKomi);
   }

   public function get_komibid( $uid, $opponent=false, $non_empty=false )
   {
      if ( $non_empty )
         $komi = (is_null($this->game_setup->Komi)) ? $this->game_setup->OppKomi : $this->game_setup->Komi;
      else
      {
         $mine = ( $uid == $this->game_setup->uid );
         if ( $opponent )
            $mine = !$mine;

         $komi = ( $mine ) ? $this->game_setup->Komi : $this->game_setup->OppKomi;
      }
      return $komi;
   }//get_komibid

   public function set_komibid( $uid, $komibid )
   {
      if ( $uid != $this->black_id && $uid != $this->white_id )
         error('invalid_args', "FKN.set_komibid({$this->gid},$uid)");
      if ( $uid == $this->game_setup->uid )
         $this->game_setup->Komi = $komibid;
      else
         $this->game_setup->OppKomi = $komibid;
   }//set_komibid

   public function get_uid_highest_bid()
   {
      if ( !$this->has_both_komibids() )
         return 0;
      if ( $this->game_setup->Komi > $this->game_setup->OppKomi )
         return $this->game_setup->uid;
      elseif ( $this->game_setup->Komi < $this->game_setup->OppKomi )
         return ( $this->game_setup->uid == $this->black_id ) ? $this->white_id : $this->black_id;
      else // nigiri on identical komi-bids
      {
         mt_srand ((double) microtime() * 1000000);
         return ( mt_rand(0,1) ) ? $this->black_id : $this->white_id;
      }
   }//get_uid_highest_bid

   public function is_choose_color( $player_id )
   {
      $htype = $this->game_setup->Handicaptype;
      if ( is_htype_divide_choose($htype) )
      {
         $gs_uid = $this->game_setup->uid;
         if ( $this->game_status == GAME_STATUS_KOMI ) // negotiation
            $choose_color = ( $player_id == $this->white_id );
         else // game started
            $choose_color = ( $htype == HTYPE_YOU_KOMI_I_COLOR && $gs_uid == $player_id )
                         || ( $htype == HTYPE_I_KOMI_YOU_COLOR && $gs_uid != $player_id );
      }
      else
         $choose_color = false;

      return $choose_color;
   }//is_choose_color

   public function get_quick_fair_komi_game_action()
   {
      global $player_row;
      $my_id = $player_row['ID'];

      if ( $this->game_status != GAME_STATUS_KOMI ) // shouldn't happen
         error('invalid_args', "FKN.get_quick_fair_komi_game_action({$this->game_status})");
      if ( $my_id != $this->tomove_id )
         return 13; // wait

      if ( $this->is_choose_color($my_id) )
         return 12; // choose color

      $komibid = $this->get_komibid($my_id);
      if ( !is_null($komibid) && $this->game_setup->Handicaptype == HTYPE_AUCTION_OPEN )
         return 11; // bid komi or accept last komi-bid

      return 10; // bid komi
   }//get_quick_fair_komi_game_action

   /*! \brief Show text for komi-bid of player $player_id to viewer $viewer_uid. */
   public function get_view_komibid( $viewer_uid, $player_id, Form $form = null, $komibid_input = null )
   {
      global $base_path;
      if ( $player_id != $this->black_id && $player_id != $this->white_id )
         error('invalid_args', "FKN.get_view_komibid.check.player({$this->gid},$viewer_uid,$player_id)");

      $htype = $this->game_setup->Handicaptype;
      $is_negotiation = ( $this->game_status == GAME_STATUS_KOMI );
      $komibid = $this->get_komibid( $player_id );
      $secret_text = T_('Top Secret#fairkomi');

      $gs_uid = $this->game_setup->uid;
      $choose_color = $this->is_choose_color( $player_id );

      if ( !$is_negotiation || $viewer_uid != $player_id || $player_id != $this->tomove_id || is_null($form) ) // VIEW-only
      {
         if ( !$is_negotiation && $choose_color )
         {
            $is_black = ( $gs_uid == $this->black_id );
            if ( $gs_uid != $player_id )
               $is_black = !$is_black;
            $result = ($is_black) ? T_('Black chosen#fairkomi') : T_('White chosen#fairkomi');
         }
         elseif ( is_null($komibid) )
            $result = ( $choose_color ) ? T_('Choose color#fairkomi') : sprintf( '(%s)', T_('No bid yet#fairkomi') );
         elseif ( $is_negotiation && ($htype == HTYPE_AUCTION_SECRET) && $viewer_uid != $player_id )
            $result = $secret_text; // hide komi-bid
         else
            $result = ( $choose_color ) ? T_('Choose color#fairkomi') : $komibid;

         if ( $is_negotiation && $this->tomove_id == $player_id )
            $result .= MED_SPACING . image( $base_path.'images/prev.gif', T_('Player to move'), null, 'class=InTextImage' );
      }
      else // EDIT: viewer is player and to-move -> show input-elements
      {
         if ( $choose_color )
         {
            $result =
               $form->print_insert_radio_buttonsx('komibid', array( 'B' => T_('I choose Black#fairkomi') ), false)
               . "<br>\n" .
               $form->print_insert_radio_buttonsx('komibid', array( 'W' => T_('I choose White#fairkomi') ), false);
         }
         else
         {
            $curr_bid = ( is_null($komibid) )
               ? sprintf( '(%s)', T_('No bid yet#fairkomi') )
               : sprintf( '%s (%s)', $komibid, T_('Current bid#fairkomi') );

            if ( is_null($komibid_input) )
               $komibid_input = $komibid;
            $result = $form->print_insert_text_input('komibid', 8, 8, $komibid_input) . SMALL_SPACING . $curr_bid;
         }
      }

      return $result;
   }//get_view_komibid

   public function echo_fairkomi_table( Form $form, array $user, $show_bid, $my_id )
   {
      $htype = $this->game_setup->Handicaptype;

      $arr_actions = ($my_id == $this->tomove_id) ? $this->build_form_actions($form, $my_id) : null;
      $form_actions = (is_null($arr_actions)) ? '' : implode(span('BigSpace'), $arr_actions);

      $uhandles = $this->get_htype_user_handles();
      $fk_type = GameTexts::get_fair_komi_types( $htype, null, $uhandles[0], $uhandles[1] );

      echo "<table id=\"fairkomiTable\" class=\"Infos\">",
         "<tr><td class=Caption colspan=2>",
            span('Caption', $fk_type), "<br>\n",
            '(', GameTexts::get_jigo_modes($this->game_setup->JigoMode), ')',
         "</td></tr>\n",
         "<tr class=Header><td>", T_('Player'), "</td><td>", T_('Komi Bid#fairkomi'), "</td></tr>\n",
         "<tr><td>", $user[0], "</td><td>", $show_bid[0], "</td></tr>\n",
         "<tr><td>", $user[1], "</td><td>", $show_bid[1], "</td></tr>\n",
         "</table>\n",
         span('Center', $form_actions), "<br><br>\n";
   }//echo_fairkomi_table

   public function get_htype_user_handles()
   {
      return ( $this->game_setup->uid == $this->black_id )
         ? array( $this->black_handle, $this->white_handle )
         : array( $this->white_handle, $this->black_handle );
   }

   public function build_form_actions( Form $form, $my_id )
   {
      $htype = $this->game_setup->Handicaptype;
      $choose_color = $this->is_choose_color($my_id);
      $actions = array();

      if ( $htype == HTYPE_AUCTION_SECRET || $htype == HTYPE_AUCTION_OPEN || !$choose_color )
         $actions[] = $form->print_insert_submit_button('komi_save', T_('Save Komi Bid') );

      if ( $htype == HTYPE_AUCTION_OPEN || $choose_color )
      {
         $actions[] = $form->print_insert_submit_buttonx('fk_start', T_('Start Game#fairkomi'),
            ( $this->allow_start_game($my_id) ? '' : 'disabled=1' ) );
      }

      return $actions;
   }//build_form_actions

   public function build_notes_fair_komi()
   {
      $htype = $this->game_setup->Handicaptype;
      $notes = array();

      if ( $htype == HTYPE_AUCTION_SECRET )
      {
         $notes[] = T_('Players secretly give their respective komi-bid (once per player).#fairkomi');
         $notes[] = T_('Komi-bids are hidden from the other player.#fairkomi');
         $notes[] = array(
            T_('After the 2nd komi-bid, the game will be started:#fairkomi'),
            T_('The player with the highest bid takes Black and is giving that number of komi to White.#fairkomi'),
            T_('If the komi-bids are equal, player color is determined by Nigiri.#fairkomi'), );
      }
      elseif ( $htype == HTYPE_AUCTION_OPEN )
      {
         $notes[] = T_('Players openly give their komi-bids.#fairkomi');
         $notes[] = T_('Komi-bids are shown to the other player.#fairkomi');
         $notes[] = array(
            T_('Komi-bids are given in turn till one player starts the game:#fairkomi'),
            T_('Each komi-bid must be higher than the last bid of the opponent.#fairkomi'),
            T_('The player with the highest bid takes Black and is giving that number of komi to White.#fairkomi'), );
      }
      elseif ( is_htype_divide_choose($htype) )
      {
         $notes[] = T_('One player chooses the komi for White.#fairkomi');
         $notes[] = T_('Then the other player chooses the color to play and the game starts.#fairkomi');
      }

      $notes[] = null;
      $notes[] = T_('During fair-komi negotiation the clock is running.#fairkomi');

      return $notes;
   }//build_notes_fair_komi


   /*! \brief Returns error-list checking komi-bid for fair-komi; empty on success. */
   public function check_komibid( $komibid, $my_id )
   {
      $komibid = trim($komibid);
      $choose_color = $this->is_choose_color($my_id);

      $errors = array();
      if ( strlen($komibid) == 0 )
         $errors[] = ($choose_color) ? T_('Missing color choice#fairkomi') : T_('Missing komi-bid#fairkomi');
      elseif ( $choose_color && $komibid != 'B' && $komibid != 'W' )
         $errors[] = T_('Invalid color choice#fairkomi');
      elseif ( !$choose_color && !is_numeric($komibid) )
         $errors[] = T_('Komi-bid must be a numeric value#fairkomi');
      else
      {
         $htype = $this->game_setup->Handicaptype;
         $jigomode = $this->game_setup->JigoMode;

         if ( floor(2 * $komibid) != 2 * $komibid ) // check for x.0|x.5
            $errors[] = ErrorCode::get_error_text('komi_bad_fraction');

         $is_fractional = floor(2 * abs($komibid)) & 1;
         if ( $jigomode == JIGOMODE_ALLOW_JIGO && $is_fractional )
            $errors[] = T_('Jigo is enforced, so komi-bid must not be fractional#fairkomi');
         elseif ( $jigomode == JIGOMODE_NO_JIGO && !$is_fractional )
            $errors[] = T_('Jigo is forbidden, so komi-bid must be fractional#fairkomi');

         if ( $htype == HTYPE_AUCTION_OPEN )
         {
            // komi-bid must be increasing
            $opp_komibid = $this->get_komibid($my_id, /*opp*/true);
            if ( !is_null($opp_komibid) && $komibid <= $opp_komibid )
               $errors[] = T_('Your komi-bid must be higher than that of the opponent.#fairkomi');
         }
      }

      return $errors;
   }//check_komibid

   /*! \brief Returns true if game can be started (with "start game" button). */
   public function allow_start_game( $my_id )
   {
      $htype = $this->game_setup->Handicaptype;
      $allow_start = false;

      if ( $htype == HTYPE_AUCTION_OPEN || is_htype_divide_choose($htype) )
      {
         // start-game allowed, if there is a komi-bid from opponent to accept
         $opp_komibid = $this->get_komibid($my_id, /*opp*/true);
         if ( !is_null($opp_komibid) )
            $allow_start = true;
      }

      return $allow_start;
   }//allow_start_game

   /*!
    * \brief Saves and process komi-bid dependent on handi-type.
    * \brief $komibid komi, or else color B|W for divide&choose
    * \return array( result, next_to_move_id )
    *    result: 0=saved-komi, 1=saved+started-game; otherwise text-error-code
    *    next_to_move_id: user-id of player that has to do something on the game (bid, play)
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public function save_komi( array $game_row, $komibid, $is_start_game = false )
   {
      $to_move = ( $this->tomove_id == $this->black_id ) ? BLACK : WHITE;
      $next_to_move = BLACK + WHITE - $to_move;
      $next_tomove_id = ( $this->tomove_id == $this->black_id ) ? $this->white_id : $this->black_id;
      $my_id = $this->tomove_id;
      $opp_id = ( $this->black_id == $my_id ) ? $this->white_id : $this->black_id;

      list( $hours, $upd_game ) = GameHelper::update_clock( "FKN.save_komi({$this->gid})", $game_row, $to_move, $next_to_move );
      $upd_game->upd_num('ToMove_ID', $next_tomove_id );

      // eventually determine komi/colors + start-game
      $start_game_new_black = 0;
      $htype = $this->game_setup->Handicaptype;
      if ( $htype == HTYPE_AUCTION_SECRET )
      {
         if ( $is_start_game ) // shouldn't happen
            error('invalid_args', "FKN.save_komi.check.auct_secret.start({$this->gid},$htype,$my_id)");

         // save komi-bid for player to-move
         $this->set_komibid( $this->tomove_id, $komibid );
         $upd_game->upd_txt('GameSetup', $this->game_setup->encode_game_setup());

         if ( $this->has_both_komibids() )
            $start_game_new_black = $this->get_uid_highest_bid();
      }
      elseif ( $htype == HTYPE_AUCTION_OPEN )
      {
         if ( $is_start_game )
         {
            if ( !$this->allow_start_game($my_id) ) // shouldn't happen
               error('invalid_args', "FKN.save_komi.check.auct_open.start({$this->gid},$htype,$my_id)");

            $start_game_new_black = $opp_id; // accept higher komi-bid of opponent (opp=Black, me=White)
         }
         else
         {
            // save komi-bid for player to-move
            $this->set_komibid( $this->tomove_id, $komibid );
            $upd_game->upd_txt('GameSetup', $this->game_setup->encode_game_setup());
         }
      }
      elseif ( is_htype_divide_choose($htype) )
      {
         if ( $is_start_game )
         {
            if ( $komibid != 'B' && $komibid != 'W' )
               error('invalid_args', "FKN.save_komi.check.divchoose.check.komibid({$this->gid},$htype,$my_id,$komibid)");
            if ( !$this->allow_start_game($my_id) ) // shouldn't happen
               error('invalid_args', "FKN.save_komi.check.divchoose.start({$this->gid},$htype,$my_id)");

            $start_game_new_black = ( $komibid == 'B' ) ? $my_id : $opp_id; // accept my color choice (komi-bid)
         }
         else
         {
            if ( $this->is_choose_color($my_id) ) // shouldn't happen
               error('invalid_args', "FKN.save_komi.check.divchoose.savekomi({$this->gid},$htype,$my_id)");

            // save komi-bid for player to-move (only once for black)
            $this->set_komibid( $this->tomove_id, $komibid );
            $upd_game->upd_txt('GameSetup', $this->game_setup->encode_game_setup());
         }
      }

      // first update clock-stuff, next to-move-id
      $upd_game->upd_time('Lastchanged');
      db_query( "FKN.save_komi.update({$this->gid},{$this->tomove_id},$komibid)",
         "UPDATE Games SET " . $upd_game->get_query()
         . " WHERE ID={$this->gid} AND Status='".GAME_STATUS_KOMI."' AND ToMove_ID={$this->tomove_id} LIMIT 1" );

      global $ActivityMax, $ActivityForMove;
      $upd_player = new UpdateQuery('Players');
      $upd_player->upd_raw('Activity', "LEAST($ActivityMax,$ActivityForMove+Activity)");
      $upd_player->upd_time('LastMove');
      db_query( "FKN.save_komi.upd_activity({$this->gid},$my_id)",
         "UPDATE Players SET " . $upd_player->get_query() . " WHERE ID=$my_id LIMIT 1" );

      $result_next_to_move_uid = $next_tomove_id;
      if ( $start_game_new_black > 0 )
      {
         $result_next_to_move_uid = $this->start_fairkomi_game( $start_game_new_black );
         $result_bid_saved = 1; // komi-bid saved + started game
      }
      else
         $result_bid_saved = 0; // komi-bid saved

      // clear caches
      clear_cache_quick_status( array( $this->black_id, $this->white_id ), QST_CACHE_GAMES );
      GameHelper::delete_cache_status_games( "FKN.save_komi.update2({$this->gid})", $this->black_id, $this->white_id );
      GameHelper::delete_cache_game_row( "FKN.save_komi.update3({$this->gid})", $this->gid );

      return array( $result_bid_saved, $result_next_to_move_uid );
   }//save_komi

   /*!
    * \brief Starts fairkomi-game by updating existing game on KOMI-status.
    * \return next_to_move_id (uid)
    *
    * \see also create_game()-func in "include/make_game.php"
    * \note clearing game-caches is done outside -> see save_komi()
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    */
   public function start_fairkomi_game( $new_black_id )
   {
      // re-read Games in order to switch values if B/W-players changed
      $gid = $this->gid;
      $dbgmsg = "FKN.start_fairkomi_game($gid,$new_black_id)";
      $grow = GameHelper::load_game_row( $dbgmsg, $gid );
      $this->read_from_game_row( $grow );

      // checks
      if ( $this->game_status != GAME_STATUS_KOMI )
         error('internal_error', "$dbgmsg.check.status({$this->game_status})");
      if ( $new_black_id != $this->black_id && $new_black_id != $this->white_id )
         error('wrong_players', "$dbgmsg.check.players");

      $new_white_id = ( $new_black_id == $this->black_id ) ? $this->white_id : $this->black_id;

      // handle shape-game (need to determine color to start playing)
      $black_first = true; // fair-komi has NO handicap
      $shape_id = (int)$grow['ShapeID'];
      if ( $shape_id > 0 )
      {
         $shape_snapshot = $grow['ShapeSnapshot'];
         $arr_shape = GameSnapshot::parse_check_extended_snapshot($shape_snapshot);
         if ( !is_array($arr_shape) ) // overwrite with defaults
            error('invalid_snapshot', "$dbgmsg.check.shape($shape_id,$shape_snapshot)");
         if ( ! (bool)@$arr_shape['PlayColorB'] ) // W-first
            $black_first = false;
      }
      $next_tomove_id = ($black_first) ? $new_black_id : $new_white_id;

      // update game

      $upd_game = new UpdateQuery('Games');
      $upd_game->upd_num('ToMove_ID', $next_tomove_id);
      $upd_game->upd_txt('Status', GAME_STATUS_PLAY);

      $use_opp_komi = is_htype_divide_choose($this->game_setup->Handicaptype);
      $upd_game->upd_num('Komi', $this->get_komibid($new_black_id, false, $use_opp_komi) );

      if ( $new_black_id != $this->black_id ) // switch attributes of B/W-player
      {
         $upd_game->upd_num('Black_ID', $new_black_id);
         $upd_game->upd_num('White_ID', $new_white_id);
         $upd_game->upd_num('Black_Start_Rating', $grow['White_Start_Rating']);
         $upd_game->upd_num('White_Start_Rating', $grow['Black_Start_Rating']);

         $upd_game->upd_num('Black_Maintime', $grow['White_Maintime']);
         $upd_game->upd_num('White_Maintime', $grow['Black_Maintime']);
         $upd_game->upd_num('Black_Byotime', $grow['White_Byotime']);
         $upd_game->upd_num('White_Byotime', $grow['Black_Byotime']);
         $upd_game->upd_num('Black_Byoperiods', $grow['White_Byoperiods']);
         $upd_game->upd_num('White_Byoperiods', $grow['Black_Byoperiods']);

         $to_move = ( $new_black_id == $next_tomove_id ) ? BLACK : WHITE;
         $next_to_move = BLACK + WHITE - $to_move;
         list( $hours, $upd_clock ) = // NOTE: clock-ticks already updated (so $ticks=false)
            GameHelper::update_clock( "FKN.start_fkg($gid)", $grow, $to_move, $next_to_move, /*ticks*/false );
         $upd_game->merge( $upd_clock );
      }

      db_query( "$dbgmsg.update",
         "UPDATE Games SET " . $upd_game->get_query() . " WHERE ID=$gid AND Status='{$this->game_status}' LIMIT 1" );

      return $next_tomove_id;
   }//start_fairkomi_game

} // end 'FairKomiNegotiation'




/**
 * \brief Class to handle game-deletion and game-finishing.
 */
class GameFinalizer
{
   private $action_by; // ACTBY_...
   private $my_id; // for game-notify: <0=cron(timeout), 0=admin
   private $gid;
   private $tid;
   private $Status;
   private $GameType;
   private $GamePlayers;
   private $GameFlags;
   private $Black_id;
   private $White_id;
   private $Moves;
   private $is_rated;
   private $made_unrated = false;
   private $skip_game_query = false;
   private $Black_Start_Rating;
   private $White_Start_Rating;

   public function __construct( $action_by, $my_id, $gid, $tid, $game_status, $game_type, $game_players, $game_flags,
         $black_id, $white_id, $moves, $is_rated, $b_start_rat, $w_start_rat )
   {
      $this->action_by = $action_by;
      $this->my_id = (int)$my_id;
      $this->gid = (int)$gid;
      $this->tid = (int)$tid;
      $this->Status = $game_status;
      $this->GameType = $game_type;
      $this->GamePlayers = $game_players;
      $this->GameFlags = (int)$game_flags;
      $this->Black_ID = (int)$black_id;
      $this->White_ID = (int)$white_id;
      $this->Moves = (int)$moves;
      $this->is_rated = (bool)$is_rated;
      $this->Black_Start_Rating = (float)$b_start_rat;
      $this->White_Start_Rating = (float)$w_start_rat;
   }//__construct

   public function skip_game_query()
   {
      $this->skip_game_query = true;
   }

   public function is_made_unrated()
   {
      return $this->made_unrated;
   }

   /*!
    * \brief Finishes or deletes game.
    * \param $do_delete true=delete-running-game, false=end-running-game
    * \param $upd_game null=build default query; otherwise UpdateQuery with SQL-Games-update-query
    * \param $game_score score to end game with
    * \param $message message added in game-notify to players
    *
    * \note IMPORTANT NOTE: caller needs to open TA with HOT-section!!
    * \note game is made unrated if game ended by no-result by game-admin (this->action_by=ACTBY_ADMIN);
    *       sets this->made_unrated if game changed to unrated
    */
   public function finish_game( $dbgmsg, $do_delete, $game_score, UpdateQuery $upd_game = null, $message = '' )
   {
      global $NOW, $player_row;
      $gid = $this->gid;
      $dbgmsg = "GameFinalizer:finish_game($gid).$dbgmsg";

      // update Games-entry
      $timeout_rejected = $game_forfeited = $game_no_result = false;
      if ( !$this->skip_game_query && !$do_delete )
      {
         if ( $this->action_by == ACTBY_ADMIN )
         {
            $this->GameFlags |= GAMEFLAGS_ADMIN_RESULT;

            if ( abs($game_score) == SCORE_FORFEIT )
               $game_forfeited = true;
            elseif ( $this->GameFlags & GAMEFLAGS_NO_RESULT )
               $game_no_result = true;
         }

         // make game unrated if criteria matches and opponent rejects-win-by-timeout
         $timeout_rejected = $this->should_reject_win_by_timeout($game_score);

         if ( is_null($upd_game) )
         {
            $upd_game = new UpdateQuery('Games');
            $upd_game->upd_txt('Status', GAME_STATUS_FINISHED );
            $upd_game->upd_num('Last_X', self::convert_score_to_posx($game_score, $this->GameFlags) );
            $upd_game->upd_num('ToMove_ID', 0 );
            $upd_game->upd_num('Flags', $this->GameFlags );
            $upd_game->upd_num('Score', $game_score );
            $upd_game->upd_time('Lastchanged');
         }
         if ( $game_no_result || $timeout_rejected ) // make game unrated on NO-RESULT score
         {
            $upd_game->upd_txt('Rated', 'N');
            $this->made_unrated = true;
         }

         $game_updquery = "UPDATE Games SET " . $upd_game->get_query() .
            " WHERE ID=$gid AND Status".IS_STARTED_GAME." AND Moves={$this->Moves} LIMIT 1";
         $result = db_query( "$dbgmsg.upd_game", $game_updquery );
         if ( mysql_affected_rows() != 1 )
            error('mysql_update_game', "$dbgmsg.upd_game2");
      }

      // signal game-end for tournament
      if ( $this->tid > 0 )
         TournamentGames::update_tournament_game_end( "$dbgmsg.tourney_game_end",
            $this->tid, $gid, $this->Black_ID, $game_score, $this->GameFlags );

      // send message to my opponent / all-players / observers about the result
      $game_notify = new GameNotify( $gid, $this->my_id, $this->Status, $this->GameType, $this->GamePlayers,
         $this->GameFlags, $this->Black_ID, $this->White_ID, $game_score, $game_forfeited, $game_no_result,
         $timeout_rejected, $message );

      if ( $do_delete )
      {
         GameHelper::delete_running_game( $gid );
         list( $Subject, $Text ) = $game_notify->get_text_game_deleted( $this->action_by );
      }
      else
      {
         if ( $this->GameType != GAMETYPE_GO ) // MP-game
         {
            $arr_ratings = MultiPlayerGame::calc_average_group_ratings( $gid, /*rating-upd*/true );
            $rated_status = update_rating2($gid, true, /*simul*/false, $arr_ratings);
         }
         else
            $rated_status = update_rating2($gid);
         GameHelper::update_players_end_game( $dbgmsg,
            $gid, $this->GameType, $rated_status, $game_score, $this->Black_ID, $this->White_ID,
            $this->Black_Start_Rating, $this->White_Start_Rating );

         list( $Subject, $Text, $observerText ) = $game_notify->get_text_game_result( $this->action_by,
            ($this->my_id > 0 ? $player_row : null) );

         if ( ALLOW_CONDITIONAL_MOVES )
            MoveSequence::deactivate_move_sequences( $dbgmsg, $gid );

         // GamesPriority-entries are kept for running games only, delete for finished games too
         NextGameOrder::delete_game_priorities( $gid );

         delete_all_observers($gid, ($rated_status != RATEDSTATUS_DELETABLE), $observerText);
         GameInvitation::delete_game_invitations( $gid );
      }

      // Send a message to the opponent
      send_message( "$dbgmsg.msg", $Text, $Subject,
         $game_notify->get_recipients(), '',
         /*notify*/true, /*system-msg*/0, MSGTYPE_RESULT, $gid );

      // clear caches
      clear_cache_quick_status( array( $this->Black_ID, $this->White_ID ), QST_CACHE_GAMES );
      GameHelper::delete_cache_status_games( "GameFinalizer.finish_game.del_cache($gid)", $this->Black_ID, $this->White_ID );
      GameHelper::delete_cache_game_row( "GameFinalizer.finish_game.del_cache($gid)", $gid );
      User::delete_cache_user_handle( "GameFinalizer.finish_game.del_cache($gid)",
         $game_notify->get_user_handle($this->Black_ID), $game_notify->get_user_handle($this->White_ID) );
   }//finish_game

   /*! \brief Returns true, if opponent rejects-win-by-timout and game should be made unrated. */
   public function should_reject_win_by_timeout( $game_score )
   {
      global $NOW;

      // must be scored by timeout, must be rated game, must be GO-type game
      if ( abs($game_score) == SCORE_TIME && $this->is_rated && $this->GameType == GAMETYPE_GO )
      {
         if ( $game_score == -SCORE_TIME )
         {
            $uid_winner = $this->Black_ID;
            $uid_loser  = $this->White_ID;
         }
         else
         {
            $uid_winner = $this->White_ID;
            $uid_loser  = $this->Black_ID;
         }

         $urow_winner = mysql_single_fetch("GameFinalizer.should_reject_win_by_timeout.find_winner({$this->gid},$uid_winner)",
               "SELECT RejectTimeoutWin FROM Players WHERE ID=$uid_winner LIMIT 1" )
            or error('unknown_user', "GameFinalizer.should_reject_win_by_timeout.find_winner2({$this->gid},$uid_winner)");

         // check if reject-timeout enabled for winner (-1 = disabled, 0 = always reject, >0 days chk on losers lastmove)
         $winner_reject_timeout_days = (int)$urow_winner['RejectTimeoutWin'];
         if ( $winner_reject_timeout_days == 0 )
            return true; // reject timeout
         elseif ( $winner_reject_timeout_days > 0 ) // reject-timeout must be enabled for winner
         {
            $urow_loser = mysql_single_fetch("GameFinalizer.should_reject_win_by_timeout.find_loser({$this->gid},$uid_loser)",
                  "SELECT UNIX_TIMESTAMP(LastMove) AS X_LastMove FROM Players WHERE ID=$uid_loser LIMIT 1" )
               or error('unknown_user', "GameFinalizer.should_reject_win_by_timeout.find_loser2({$this->gid},$uid_loser)");

            $loser_last_moved = (int)$urow_loser['X_LastMove'];

            if ( $loser_last_moved <= ($NOW - $winner_reject_timeout_days * SECS_PER_DAY) )
               return true; // reject timeout
         }
      }

      return false; // no reject, perform normal timeout-handling
   }//should_reject_win_by_timeout


   // ------------ static functions ----------------------------

   private static function convert_score_to_posx( $score, $game_flags )
   {
      if ( abs($score) == SCORE_RESIGN )
         return POSX_RESIGN;
      elseif ( abs($score) == SCORE_TIME )
         return POSX_TIME;
      elseif ( abs($score) == SCORE_FORFEIT )
         return POSX_FORFEIT;
      else //=score|0
      {
         if ( $score == 0 && ($game_flags & GAMEFLAGS_NO_RESULT) )
            return POSX_NO_RESULT;
         else
            return POSX_SCORE;
      }
   }//convert_score_to_posx

} // end 'GameFinalizer





/*!
 * \class GameNotify
 *
 * \brief Class to build game-related notifications.
 */
class GameNotify
{
   private $gid;
   private $uid; // can be 0 for admin (<> players)
   private $game_status;
   private $game_type;
   private $game_players;
   private $game_flags;
   private $black_id;
   private $white_id;
   private $score;
   private $game_forfeited;
   private $game_no_result;
   private $timeout_rejected;
   private $message;

   private $players; // [ uid => [ ID/Name/Handle => ...], ... ]
   private $black_name;
   private $white_name;
   private $players_text;

   /*! \brief Constructs GameNotify also loading player-info from db. */
   public function __construct( $gid, $uid, $game_status, $game_type, $game_players, $game_flags,
         $black_id, $white_id, $score, $game_forfeited, $game_no_result, $timeout_rejected, $message )
   {
      $this->gid = (int)$gid;
      $this->uid = (int)$uid;
      $this->game_status = $game_status;
      $this->game_type = $game_type;
      $this->game_players = $game_players;
      $this->game_flags = (int)$game_flags;
      $this->black_id = (int)$black_id;
      $this->white_id = (int)$white_id;
      $this->score = $score;
      $this->game_forfeited = (bool)$game_forfeited;
      $this->game_no_result = (bool)$game_no_result;
      $this->timeout_rejected = (bool)$timeout_rejected;
      $this->message = $message;

      $this->_load_players();
      $this->black_name = @$this->players[$black_id]['Name'];
      $this->white_name = @$this->players[$white_id]['Name'];
      $this->players_text = $this->_build_text_players();
   }//__construct

   /*! \brief Loads players (for simple-game B|W, for multi-player-game all players). */
   private function _load_players()
   {
      $this->players = array();

      if ( $this->game_type == GAMETYPE_GO )
      {
         $result = db_query( "GameNotify.find_players({$this->gid})",
            "SELECT ID, Handle, Name FROM Players WHERE ID IN ({$this->black_id},{$this->white_id}) LIMIT 2" );
      }
      else // mp-game
      {
         $result = db_query( "GameNotify.find_mpg_players({$this->gid})",
            "SELECT P.ID, P.Handle, P.Name " .
            "FROM GamePlayers AS GP INNER JOIN Players AS P ON P.ID=GP.uid " .
            "WHERE GP.gid={$this->gid}" );
      }

      while ( $row = mysql_fetch_array( $result ) )
         $this->players[$row['ID']] = $row;
      mysql_free_result($result);
   }//_load_players

   private function _build_text_players()
   {
      // NOTE: server messages does not allow a reply, so add an *in message* reference to players
      $arr = array();
      foreach ( $this->players as $user_row )
         $arr[] = send_reference( REF_LINK, 1, '', $user_row );
      return "<p>Send a message to:<center>" . implode('<br>', $arr) . "</center>";
   }//_build_text_players

   private function build_game_ref_array()
   {
      return array(
         'Blackname' => $this->black_name, 'Whitename' => $this->white_name,
         'GameType' => $this->game_type, 'GamePlayers' => $this->game_players,
         'Status' => $this->game_status,
         );
   }//build_game_ref_array

   public function get_user_handle( $uid )
   {
      return @$this->players[$uid]['Handle'];
   }

   /*!
    * \brief Returns (untranslated) subject and text for message to players if game got deleted.
    * \param $action_by ACTBY_...
    */
   public function get_text_game_deleted( $action_by )
   {
      global $player_row, $MAP_ACTBY_SUBJECT;
      $act_user_text = $MAP_ACTBY_SUBJECT[$action_by][0];

      $subject = 'Game deleted';
      if ( $action_by == ACTBY_ADMIN || $action_by == ACTBY_CRON || $action_by == ACTBY_GAMEMASTER )
         $subject .= sprintf(' (by %s)', $act_user_text );
      elseif ( $action_by != ACTBY_PLAYER )
         $action_by = ACTBY_PLAYER;

      if ( $this->game_status == GAME_STATUS_FINISHED )
         $gstatus = 'finished ';
      elseif ( $this->game_status == GAME_STATUS_SETUP )
         $gstatus = 'setup-';
      elseif ( $this->game_status == GAME_STATUS_KOMI )
         $gstatus = 'started ';
      else
         $gstatus = 'running ';

      $text = "The {$gstatus}game:<center>"
            // game will be deleted => can't use <game>
            . game_reference( REF_LINK, 1, '', $this->gid, 0, $this->build_game_ref_array() )
            . "</center>has been deleted by $act_user_text:<center>"
            . send_reference( REF_LINK, 1, '', $player_row )
            . "</center>"
            . $this->players_text;

      if ( $this->message )
         $text .= "<p>The $act_user_text wrote:<p></p>" . $this->message;

      return array( $subject, $text );
   }//get_text_game_deleted

   /*!
    * \brief Returns (untranslated) subject and text (and observer-text) for message to players/observers
    *       with normal game-result.
    * \param $action_by ACTBY_...
    */
   public function get_text_game_result( $action_by, array $user_row = null )
   {
      global $MAP_ACTBY_SUBJECT;
      if ( is_null($this->score) )
         error('invalid_args', "GameNotify.get_text_game_result.check.score({$this->gid})");

      $subject = 'Game result';
      if ( $action_by == ACTBY_ADMIN )
         $subject .= sprintf(' (by %s)', $MAP_ACTBY_SUBJECT[$action_by][0]);
      elseif ( $action_by != ACTBY_PLAYER && $action_by != ACTBY_CRON )
         $action_by = ACTBY_PLAYER;

      $text = "The result in the game:<center>"
            . game_reference( REF_LINK, 1, '', $this->gid, 0, $this->build_game_ref_array() )
            . "</center>was:<center>"
            . score2text($this->score, $this->game_flags, /*verbose*/true, /*engl*/true)
            . "</center>";

      $info_text = ( $this->game_flags & GAMEFLAGS_HIDDEN_MSG )
         ? "<p><b>Info:</b> The game has hidden comments!"
         : '';
      $obs_info_text = $info_text;

      if ( $this->game_flags & GAMEFLAGS_SECRET_MSG )
         $info_text .= "<p><b>Info:</b> The game has secret comments!";

      if ( $action_by == ACTBY_ADMIN && $this->game_no_result )
         $info_text .= "<p><b>Info:</b> The game was changed to unrated, " .
            "because it was ended with NO-RESULT by a game admin!";
      if ( $this->timeout_rejected )
         $info_text .= "<p><b>Info:</b> Based on the winner's profile preference, " .
            "the win by timeout was automatically rejected and the game was changed to unrated!";

      $act_user_text = ( is_array($user_row) )
         ? sprintf( $MAP_ACTBY_SUBJECT[$action_by][1],
                    user_reference(REF_LINK, 1, '', $user_row['ID'], $user_row['Handle'], '') )
         : $MAP_ACTBY_SUBJECT[$action_by][0];

      $player_text = $this->players_text;
      $msg_text = ( $this->message ) ? "<p>The $act_user_text wrote:<p></p>" . $this->message : '';

      return array( $subject,
                    $text . $info_text . $this->players_text . $msg_text, // text for players
                    $text . $obs_info_text . $this->players_text, // text for observers
            );
   }//get_text_game_result

   /*!
    * \brief Returns list of Players.IDs to which message should be sent:
    *       to all users for time-out & forfeit & no-result (=void);
    *       otherwise only for others than current-user for resign/result/delete.
    */
   public function get_recipients()
   {
      $arr = array_keys( $this->players );
      if ( $this->uid > 0 && abs($this->score) != SCORE_TIME && abs($this->score) != SCORE_FORFEIT
            && ($this->game_flags & GAMEFLAGS_NO_RESULT) == 0 )
         unset($arr[$this->uid]);
      return $arr;
   }//get_recipients

} // end 'GameNotify'




/*!
 * \class MaxGamesCheck
 *
 * \brief Class to handle checks on max-games for users.
 */
class MaxGamesCheck
{
   public $count_games; // number of started/running-games

   /*! \param $urow int count-games, or arr( Running|GamesMPG => count ). */
   public function __construct( $urow=null )
   {
      if ( is_numeric($urow) )
         $this->count_games = (int)$urow;
      else
      {
         global $player_row;
         if ( !is_array($urow) )
            $urow = $player_row;
         $this->count_games = (int)@$urow['Running'] + (int)@$urow['GamesMPG'];
      }
   }//__construct

   /*! \brief Returns true, if new game is allowed to start regarding max-games-limit. */
   public function allow_game_start()
   {
      return ( MAX_GAMESRUN <= 0 || $this->count_games < MAX_GAMESRUN );
   }

   /*! \brief Returns true, if tourney-registration is allowed (with expected/future new games) regarding max-games-limit. */
   public function allow_tournament_registration()
   {
      return ( MAX_GAMESRUN <= 0 || $this->count_games < self::get_max_gamesrun_treg() );
   }

   /*! \brief Returns amount of allowed games to start from given number. */
   public function get_allowed_games( $num )
   {
      if ( MAX_GAMESRUN <= 0 ) // unlimited
         return $num;
      else
      {
         $cnt = MAX_GAMESRUN - $this->count_games;
         return ($cnt < 0) ? 0 : min($num, $cnt);
      }
   }//get_allowed_games

   public function get_error_text( $html=true )
   {
      $msg = sprintf( T_('Sorry, you are not allowed to start more than %s games!'), MAX_GAMESRUN );
      return ($html) ? span('ErrMsgMaxGames', $msg) : $msg;
   }

   /*! \brief Returns true, if warning-threshold reached. */
   public function need_warning()
   {
      if ( MAX_GAMESRUN <= 0 ) // unlimited
         return false;

      $warn_threshold = round(MAX_GAMESRUN * 75/100); // warning-threshold 75% of MAX_GAMESRUN
      if ( ALLOW_TOURNAMENTS && MAX_GAMESRUN_TREG > 0 )
         $warn_threshold = min( $warn_threshold, round(MAX_GAMESRUN_TREG * 75/100) ); // 75% for tourney-reg

      return ( $this->count_games >= max(1, $warn_threshold) );
   }//need_warning

   public function get_warn_text( $html=true )
   {
      if ( !$this->need_warning() )
         return '';

      $msg = sprintf( T_('You already started %s of max. %s games.'), $this->count_games, MAX_GAMESRUN );
      if ( ALLOW_TOURNAMENTS )
         $msg .= ' ' . sprintf( T_('Tournament registration only allowed for <%s games.'),
            self::get_max_gamesrun_treg() );

      $class = ($this->count_games >= MAX_GAMESRUN) ? 'ErrMsg' : 'WarnMsg';
      return ($html) ? span($class, $msg) . "<br><br>\n" : $msg;
   }//get_warn_text

   public static function is_limited()
   {
      return (MAX_GAMESRUN > 0);
   }

   // internal
   private static function get_max_gamesrun_treg()
   {
      return (MAX_GAMESRUN_TREG > 0) ? MAX_GAMESRUN_TREG : MAX_GAMESRUN;
   }

} // end 'MaxGamesCheck'



define('GSENC_FULL_GAME', 0x01); // GameSetup includes game-part + invite-part

/*!
 * \class GameSetup
 *
 * \brief Class to handle game-setup, stored in Games.GameSetup.
 * \note To support: 1. handle fair-komi negotiation, 2. rematch with same settings, 3. better invitation-handling
 */
class GameSetup
{
   // fields stored in encoded form in Games.GameSetup
   // (data required for rematch and komi-negotiation, though not neccessarily for playing game except Handicap/Komi)
   public $uid;
   public $Handicaptype; // HTYPE_...
   public $Handicap;
   public $AdjHandicap;
   public $MinHandicap;
   public $MaxHandicap;
   public $Komi; // null=no-komi set yet (used for fair-komi)
   public $AdjKomi;
   public $JigoMode; // JIGOMODE_...
   public $MustBeRated; // bool
   public $RatingMin;
   public $RatingMax;
   public $MinRatedGames;
   public $MinHeroRatio;
   public $SameOpponent;
   public $Message;

   // fields for fair-komi negotiation
   public $OppKomi; // komi-bid of opponent: null=no-komi set yet (used for fair-komi)

   // additional games-fields solely needed to PLAY game (and are therefore already stored in Games-table)
   // - fields that are set once
   public $tid;
   public $ShapeID;
   public $ShapeSnapshot;
   public $GameType; // GAMETYPE_...
   public $GamePlayers;
   // - fields that are changeable
   public $Ruleset; // RULESET_...
   public $Size;
   public $Rated; // bool
   public $StdHandicap; // bool
   public $Maintime;
   public $Byotype; // BYOTYPE_...
   public $Byotime;
   public $Byoperiods;
   public $WeekendClock; // bool

   // additional fields, only used for save-template of new-game views
   public $NumGames;
   public $ViewMode;

   private static $MAP_GAME_SETUP = array(
         // map Handicap-types -> encoded GameSetup-handi-type
         HTYPE_CONV     => 'T1',
         HTYPE_PROPER   => 'T2',
         HTYPE_NIGIRI   => 'T3',
         HTYPE_DOUBLE   => 'T4',
         HTYPE_BLACK    => 'T5',
         HTYPE_WHITE    => 'T6',
         HTYPE_AUCTION_SECRET    => 'T7',
         HTYPE_AUCTION_OPEN      => 'T8',
         HTYPE_YOU_KOMI_I_COLOR  => 'T9',
         HTYPE_I_KOMI_YOU_COLOR  => 'T10',
         HTYPE_ALTERNATE => 'T11',

         // map GameSetup-handi-type -> Handicap-types
         'T1'  => HTYPE_CONV,
         'T2'  => HTYPE_PROPER,
         'T3'  => HTYPE_NIGIRI,
         'T4'  => HTYPE_DOUBLE,
         'T5'  => HTYPE_BLACK,
         'T6'  => HTYPE_WHITE,
         'T7'  => HTYPE_AUCTION_SECRET,
         'T8'  => HTYPE_AUCTION_OPEN,
         'T9'  => HTYPE_YOU_KOMI_I_COLOR,
         'T10' => HTYPE_I_KOMI_YOU_COLOR,
         'T11' => HTYPE_ALTERNATE,

         // map JigoMode -> encoded GameSetup-jigo-mode
         JIGOMODE_KEEP_KOMI   => 'J0',
         JIGOMODE_ALLOW_JIGO  => 'J1',
         JIGOMODE_NO_JIGO     => 'J2',

         // map GameSetup-jigo-mode -> JigoMode
         'J0' => JIGOMODE_KEEP_KOMI,
         'J1' => JIGOMODE_ALLOW_JIGO,
         'J2' => JIGOMODE_NO_JIGO,

         // map Ruleset -> encoded GameSetup-ruleset
         RULESET_JAPANESE => 'r1',
         RULESET_CHINESE  => 'r2',

         'r1' => RULESET_JAPANESE,
         'r2' => RULESET_CHINESE,

         // map Byotype -> encoded GameSetup-byotype
         BYOTYPE_JAPANESE => 'tJ',
         BYOTYPE_CANADIAN => 'tC',
         BYOTYPE_FISCHER => 'tF',

         'tJ' => BYOTYPE_JAPANESE,
         'tC' => BYOTYPE_CANADIAN,
         'tF' => BYOTYPE_FISCHER,
      );//MAP_GAME_SETUP


   public function __construct( $uid )
   {
      $this->set_defaults();
      $this->uid = $uid;
   }

   private function set_defaults()
   {
      // defaults
      $this->uid = 0;
      $this->Handicaptype = HTYPE_NIGIRI;
      $this->Handicap = 0;
      $this->AdjHandicap = 0;
      $this->MinHandicap = 0;
      $this->MaxHandicap = DEFAULT_MAX_HANDICAP;
      $this->Komi = 0.0;
      $this->AdjKomi = 0.0;
      $this->JigoMode = JIGOMODE_KEEP_KOMI;
      $this->MustBeRated = true;
      $this->RatingMin = MIN_RATING;
      $this->RatingMax = RATING_9DAN;
      $this->MinRatedGames = 0;
      $this->MinHeroRatio = 0;
      $this->SameOpponent = 0;
      $this->Message = '';

      // other fields
      $this->OppKomi = null;
      $this->tid = 0;
      $this->ShapeID = 0;
      $this->ShapeSnapshot = '';
      $this->GameType = GAMETYPE_GO;
      $this->GamePlayers = '';

      $this->Ruleset = Ruleset::get_default_ruleset();
      $this->Size = 19;
      $this->Rated = true;
      $this->StdHandicap = true;
      $this->Maintime = time_convert_to_hours( 14, 'days' );
      $this->Byotype = BYOTYPE_FISCHER;
      $this->Byotime = time_convert_to_hours( 1, 'days' );
      $this->Byoperiods = 0;
      $this->WeekendClock = true;

      $this->NumGames = 1;
      $this->ViewMode = null;
   }//set_defaults

   public function init_opponent_handicaptype( $opp_id )
   {
      $this->uid = $opp_id;
      $this->Handicaptype = self::swap_htype_black_white( $this->Handicaptype );
      $this->Message = '';
      $this->OppKomi = null;
   }

   public function is_fairkomi()
   {
      $cat_htype = get_category_handicaptype($this->Handicaptype);
      return ( $cat_htype == CAT_HTYPE_FAIR_KOMI );
   }

   /*!
    * \brief Encodes GameSetup-object into Games.GameSetup encoding/"compressed" game-setup.
    * \param $enc_flags flags, that can be enabled to return different formats using/or'ing
    *       0 = default return only 1st game-part,
    *       GSENC_FULL_GAME = include game-part + invite-part with full game-data
    * \see GameSetup-field in 'specs/db/table-Games.txt'
    */
   public function encode_game_setup( $enc_flags=0 )
   {
      if ( !isset(self::$MAP_GAME_SETUP[$this->Handicaptype]) )
         error('invalid_args', "GameSetup.encode_game_setup.htype({$this->uid},{$this->Handicaptype})");
      if ( !isset(self::$MAP_GAME_SETUP[$this->JigoMode]) )
         error('invalid_args', "GameSetup.encode_game_setup.jigomode({$this->uid},{$this->JigoMode})");
      if ( $enc_flags & GSENC_FULL_GAME )
      {
         if ( !isset(self::$MAP_GAME_SETUP[$this->Ruleset]) )
            error('invalid_args', "GameSetup.encode_game_setup.ruleset({$this->uid},{$this->Ruleset})");
         if ( !isset(self::$MAP_GAME_SETUP[$this->Byotype]) )
            error('invalid_args', "GameSetup.encode_game_setup.byotype({$this->uid},{$this->Byotype})");
      }

      $out = array();
      $out[] = self::$MAP_GAME_SETUP[$this->Handicaptype]; // type: T10
      $out[] = 'U'.(int)$this->uid; // user: U123

      // handicap-stuff: H21:-21:10:21
      array_push($out,
         'H'.(int)$this->Handicap,
         (int)$this->AdjHandicap,
         (int)$this->MinHandicap,
         (int)$this->MaxHandicap );

      // komi-stuff: K-199.5:-199.5:J2:FK[7]
      $out[] = (is_null($this->Komi)) ? 'K' : sprintf('K%.1f', (float)$this->Komi);
      $out[] = sprintf('%.1f', (float)$this->AdjKomi);
      $out[] = self::$MAP_GAME_SETUP[$this->JigoMode]; // J<x>
      $out[] = (is_null($this->OppKomi)) ? 'FK' : sprintf('FK%.1f', (float)$this->OppKomi);

      // restriction-stuff: R1:-900:2900:999:H%50-:-103
      array_push($out,
         'R' . ( $this->MustBeRated ? 1 : 0 ),
         (int)$this->RatingMin,
         (int)$this->RatingMax,
         (int)$this->MinRatedGames,
         'H%' . ( $this->MinHeroRatio <= 0 ? '' : $this->MinHeroRatio . '-' ),
         (int)$this->SameOpponent );

      if ( $enc_flags & GSENC_FULL_GAME )
      {
         // NOTE: include also all Games-table-fields (used for profile-templates w/o Games-table-entry)
         $out[] = 'C'; // message (empty)

         $out[] = 'I'.(int)$this->Size; // I=invitation-data
         $out[] = ( $this->Rated ? 1 : 0 );
         $out[] = self::$MAP_GAME_SETUP[$this->Ruleset]; // ruleset: r1
         $out[] = ( $this->StdHandicap ? 1 : 0 );

         // time-stuff: tJ:72:24:5:1
         $out[] = self::$MAP_GAME_SETUP[$this->Byotype]; // byoType: tF
         $out[] = (int)$this->Maintime;
         $out[] = (int)$this->Byotime;
         $out[] = (int)$this->Byoperiods;
         $out[] = ( $this->WeekendClock ? 1 : 0 );
      }
      else
         $out[] = 'C'.$this->Message; // message: Cslow game

      return implode(GS_SEP, $out);
   }//encode_game_setup

   /*! \brief Encodes this GameSetup into GameSetup used for profile-template for invitation and new-game. */
   public function encode_profile_template( $prof_type )
   {
      if ( $prof_type == PROFTYPE_TMPL_INVITE || $prof_type == PROFTYPE_TMPL_NEWGAME )
         return $this->encode_game_setup( GSENC_FULL_GAME );
      else
         error('invalid_args', "GameSetup.encode_profile_template.check($prof_type)");
   }//encode_profile_template

   /*!
    * \brief Parses remaining game-fields (that are not required for setup a game) into this GameSetup.
    * \param $grow parsed fields:
    *        ShapeID, ShapeSnapshot, GameType, GamePlayers, tid, Ruleset, Size, Rated,
    *        StdHandicap, Maintime, Byotype, Byotime, Byoperiods, WeekendClock
    * \see GameSetup::new_from_waitingroom_game_row()
    */
   public function read_waitingroom_fields( array $grow )
   {
      if ( isset($grow['tid']) )
         $this->tid = (int)$grow['tid'];

      if ( isset($grow['ShapeID']) )
         $this->ShapeID = (int)$grow['ShapeID'];
      if ( isset($grow['ShapeSnapshot']) )
         $this->ShapeSnapshot = $grow['ShapeSnapshot'];
      if ( isset($grow['GameType']) )
         $this->GameType = $grow['GameType'];
      if ( isset($grow['GamePlayers']) )
         $this->GamePlayers = $grow['GamePlayers'];
      if ( isset($grow['Ruleset']) )
         $this->Ruleset = $grow['Ruleset'];
      if ( isset($grow['Size']) )
         $this->Size = (int)@$grow['Size'];
      if ( isset($grow['Rated']) )
         $this->Rated = ( $grow['Rated'] == 'Y' );
      if ( isset($grow['StdHandicap']) )
         $this->StdHandicap = ( $grow['StdHandicap'] == 'Y' );

      if ( isset($grow['Maintime']) )
         $this->Maintime = (int)$grow['Maintime'];
      if ( isset($grow['Byotype']) )
         $this->Byotype = $grow['Byotype'];
      if ( isset($grow['Byotime']) )
         $this->Byotime = (int)$grow['Byotime'];
      if ( isset($grow['Byoperiods']) )
         $this->Byoperiods = (int)$grow['Byoperiods'];
      if ( isset($grow['WeekendClock']) )
         $this->WeekendClock = ( $grow['WeekendClock'] == 'Y' );
   }//read_waitingroom_fields

   /*! \brief Builds GameInvitation-object from this GameSetup-object. */
   public function build_game_invitation( $gid=0 )
   {
      global $NOW;
      return new GameInvitation( $gid, $this->uid, $NOW,
         $this->Ruleset, $this->Size, $this->Komi, $this->Handicap, $this->Handicaptype,
         $this->AdjKomi, $this->JigoMode, $this->AdjHandicap, $this->MinHandicap, $this->MaxHandicap,
         $this->Maintime, $this->Byotype, $this->Byotime, $this->Byoperiods, $this->WeekendClock,
         $this->Rated, $this->StdHandicap );
   }//build_game_invitation

   public function to_string( $invitation=false )
   {
      $result = "GameSetup: U={$this->uid} T={$this->Handicaptype} " .
         "H={$this->Handicap}/{$this->AdjHandicap}/{$this->MinHandicap}..{$this->MaxHandicap} " .
         "K={$this->Komi}/{$this->AdjKomi} J={$this->JigoMode} FK={$this->OppKomi} " .
         "MBR=" . yesno($this->MustBeRated) . " Rating={$this->RatingMin}..{$this->RatingMax} " .
         "MRG={$this->MinRatedGames} HERO={$this->MinHeroRatio}- SO={$this->SameOpponent} M=[{$this->Message}]";
      if ( $invitation )
         $result .= "; S={$this->Size} Rules={$this->Ruleset} Rated=" . yesno($this->Rated) . " StdH=" . yesno($this->StdHandicap) .
            " Time={$this->Byotype}:{$this->Maintime}/{$this->Byotime}/{$this->Byoperiods}:" . yesno($this->WeekendClock) .
            " tid={$this->tid} shape={$this->ShapeID}/[{$this->ShapeSnapshot}] " .
            "gtype={$this->GameType}/{$this->GamePlayers}";
      $result .= "; #G={$this->NumGames} view={$this->ViewMode}";
      return $result;
   }//to_string

   /*!
    * \brief Returns handicap-type seen from point of given user-id.
    * \note The handicap-types HTYPE_BLACK, HTYPE_WHITE, HTYPE_YOU_KOMI_I_COLOR, HTYPE_I_KOMI_YOU_COLOR
    *       need to be treated from the perspective of a certain user. If the pivot-user 123 uses
    *       handicap-type HTYPE_BLACK, from his opponent's 456 point of view, user 456 would get
    *       HTYPE_WHITE! So this method aligns the "view" of the handicap-type to the given user.
    */
   public function get_user_view_handicaptype( $uid )
   {
      if ( $this->uid == $uid )
         return $this->Handicaptype;
      else
         return self::swap_htype_black_white( $this->Handicaptype );
   }

   public function get_htype_swapped()
   {
      return self::swap_htype_black_white( $this->Handicaptype );
   }

   public function format_handicap_type( $handicaptype, $pivot_handle, $opp_handle )
   {
      if ( is_null($handicaptype) )
         $handicaptype = $this->Handicaptype;

      $cat_htype = get_category_handicaptype($handicaptype);
      switch ( (string)$cat_htype )
      {
         case CAT_HTYPE_CONV:
            return T_('Conventional handicap');

         case CAT_HTYPE_PROPER:
            return T_('Proper handicap');

         case CAT_HTYPE_FAIR_KOMI:
            $fk_type = ( is_htype_divide_choose($handicaptype) )
               ? GameTexts::get_fair_komi_types($handicaptype, null, $pivot_handle, $opp_handle )
               : GameTexts::get_fair_komi_types($handicaptype);
            return sprintf( T_('Fair Komi of Type [%s], Jigo mode [%s]'),
                            $fk_type, GameTexts::get_jigo_modes($this->JigoMode) );

         case CAT_HTYPE_MANUAL:
         default:
            return sprintf( T_('Manual setting with My Color [%s], Handicap %s, Komi %s'),
                            GameTexts::get_manual_handicap_types($handicaptype), $this->Handicap, $this->Komi );
      }
   }//format_handicap_type

   public function format_time()
   {
      return TimeFormat::echo_time_limit( $this->Maintime, $this->Byotype, $this->Byotime, $this->Byoperiods,
         TIMEFMT_SHORT|TIMEFMT_ADDTYPE);
   }//format_time

   public function format_std_handicap()
   {
      return ($this->StdHandicap) ? T_('Standard-Handicap#inv_diff') : T_('Free-Handicap#inv_diff');
   }


   // ------------ static functions ----------------------------

   /*! \brief Decodes (=parse) Games.GameSetup value with game-setup into GameSetup-object. */
   public static function new_from_game_setup( $game_setup, $invitation=false, $null_on_empty=false )
   {
      static $RX_KOMI = "-?\\d+(\\.[05])?";
      static $RX_KOMI2 = "(-?\\d+(\\.[05])?)?"; // can be empty

      $gs = new GameSetup( 0 );
      $game_setup = trim($game_setup);
      if ( (string)$game_setup == '' )
         return ($null_on_empty) ? null : $gs;

      // Standard:
      //   "T<htype>:U<uid>:H<handicap>:<adjH>:<minH>:<maxH>:K<komi>:<adjK>:J<jigomode>:R<mustBeRated>:<ratingMin>:<ratingMax>:<minRG>:H%<heroRange>:<sameOpp>:C<msg>"
      // Invitation (standard-part shows example):
      //   "T1:U2:H0:0:0:-1:K6.5:0.0:J2:R0:0:0:0:0:C:I<size>:<rated>:r<ruleset>:<stdH>:t<byoType>:<mainT>:<byoTime>:<byoPeriods>:<wkclock>"
      // NOTE: when adding new game-settings -> adjust regex also matching "old" syntaxes
      $rx_inv = ($invitation) ? ":I\\d+:[01]:r\\d+:[01]:t[JCF]:\\d+:\\d+:\\d+:[01]\$" : '';
      $rx_gs = "/^T\\d+:U\\d+:H\\d+:-?\\d+:\\d+:-?\\d+:K$RX_KOMI2:$RX_KOMI:J[012]:FK$RX_KOMI2:R[01]:-?\\d+:-?\\d+:\\d+:H%(\\d+-)?:-?\\d+:C$rx_inv/";
      if ( !preg_match($rx_gs, $game_setup) )
         error('invalid_args', "GameSetup:new_from_game_setup.check_gs($invitation,$game_setup)");
      $arr = explode(GS_SEP, $game_setup);
      //error_log("new_from_game_setup($invitation,$game_setup): [". implode('] [', $arr)."]"); //TEST

      $gs->Handicaptype = self::$MAP_GAME_SETUP[ array_shift($arr) ];
      $gs->uid = (int)substr( array_shift($arr), 1);

      $gs->Handicap = (int)substr( array_shift($arr), 1);
      $gs->AdjHandicap = (int)array_shift($arr);
      $gs->MinHandicap = (int)array_shift($arr);
      $gs->MaxHandicap = (int)array_shift($arr);

      $komi = substr( array_shift($arr), 1);
      $gs->Komi = ( strlen($komi) > 0 ) ? (float)$komi : null;
      $gs->AdjKomi = (float)array_shift($arr);
      $gs->JigoMode = self::$MAP_GAME_SETUP[ array_shift($arr) ];
      $komi = substr( array_shift($arr), 2);
      $gs->OppKomi = ( strlen($komi) > 0 ) ? (float)$komi : null;

      $gs->MustBeRated = ( (int)substr( array_shift($arr), 1) != 0 );
      $gs->RatingMin = (int)array_shift($arr);
      $gs->RatingMax = (int)array_shift($arr);
      $gs->MinRatedGames = (int)array_shift($arr);
      $arr_hero = explode('-', substr( array_shift($arr), 2 ) ); // H% | H%99-
      $gs->MinHeroRatio = ( count($arr_hero) > 0 ) ? (int)$arr_hero[0] : 0;
      $gs->SameOpponent = (int)array_shift($arr);

      if ( $invitation )
      {
         $gs->Message = substr( array_shift($arr), 1);

         $gs->Size = (int)substr( array_shift($arr), 1);
         $gs->Rated = ( (int)array_shift($arr) != 0 );
         $gs->Ruleset = self::$MAP_GAME_SETUP[ array_shift($arr) ];
         $gs->StdHandicap = ( (int)array_shift($arr) != 0 );

         $gs->Byotype = @self::$MAP_GAME_SETUP[ array_shift($arr) ];
         $gs->Maintime = (int)array_shift($arr);
         $gs->Byotime = (int)array_shift($arr);
         $gs->Byoperiods = (int)array_shift($arr);
         $gs->WeekendClock = ( (int)array_shift($arr) != 0 );
      }
      else
         $gs->Message = substr( implode(GS_SEP, $arr), 1 );

      //error_log("new_from_game_setup.2: ".$gs->to_string(true)); //TEST
      return $gs;
   }//new_from_game_setup

   /*!
    * \brief Creates GameSetup parsing fields from games-row (for waiting-room).
    * \param $grow array with keys: uid, Handicaptype, Handicap, Adj/Min/MaxHandicap, Komi, AdjKomi, JigoMode,
    *        MustBeRated, RatingMin/Max, MinRatedGames, MinHeroRatio, SameOpponent, Message|Comment
    * \see GameSetup.read_waitingroom_fields()
    */
   public static function new_from_waitingroom_game_row( array $grow )
   {
      $gs = new GameSetup( (int)@$grow['uid'] );
      $gs->Handicaptype = $grow['Handicaptype'];
      $gs->Handicap = (int)$grow['Handicap'];
      $gs->AdjHandicap = (int)$grow['AdjHandicap'];
      $gs->MinHandicap = (int)$grow['MinHandicap'];
      $gs->MaxHandicap = (int)$grow['MaxHandicap'];
      $gs->Komi = (float)$grow['Komi'];
      $gs->AdjKomi = (float)$grow['AdjKomi'];
      $gs->JigoMode = $grow['JigoMode'];
      $gs->MustBeRated = ( $grow['MustBeRated'] == 'Y' );
      $gs->RatingMin = (int)$grow['RatingMin'];
      $gs->RatingMax = (int)$grow['RatingMax'];
      $gs->MinRatedGames = (int)$grow['MinRatedGames'];
      $gs->MinHeroRatio = (int)$grow['MinHeroRatio'];
      $gs->SameOpponent = (int)$grow['SameOpponent'];
      if ( isset($grow['Message']) )
         $gs->Message = $grow['Message'];
      elseif ( isset($grow['Comment']) )
         $gs->Message = $grow['Comment'];
      return $gs;
   }//new_from_waitingroom_game_row

   /*!
    * \brief Builds GameSetup-objects from given game- and game-invitation data with pivot-user's first.
    * \return array( GameSetup of pivot-user, GameSetup of pivot-opponent )
    */
   public static function build_game_setup_from_game_invitations( $pivot_uid, array $game_row, array $game_invitations )
   {
      if ( count($game_invitations) != 2 )
         error('invite_bad_gamesetup', "GameSetup:build_game_setup_from_game_invitations.check_cnt_game_inv($pivot_uid)");

      if ( $game_invitations[0]->uid == $pivot_uid )
         $idx = 0;
      elseif ( $game_invitations[1]->uid == $pivot_uid )
         $idx = 1;
      else
         error('invite_bad_gamesetup', "GameSetup:build_game_setup_from_game_invitations.miss_game_inv($pivot_uid)");

      $pivot_gs = self::new_from_game_invitation( $game_row, $game_invitations[$idx] );
      $opp_gs = self::new_from_game_invitation( $game_row, $game_invitations[1-$idx] );

      return array( $pivot_gs, $opp_gs );
   }//build_game_setup_from_game_invitations

   /*! \brief Builds GameSetup-object from game-row and GameInvitation-object. */
   public static function new_from_game_invitation( array $grow, GameInvitation $game_inv )
   {
      $gs = new GameSetup( $game_inv->uid );

      // unchangeable fields set once for invitation from Games-table
      $gs->ShapeID = (int)$grow['ShapeID'];
      $gs->ShapeSnapshot = $grow['ShapeSnapshot'];

      // fields that are changeable for invitation by both players
      $gs->Handicaptype = $game_inv->Handicaptype;
      $gs->Handicap = $game_inv->Handicap;
      $gs->AdjHandicap = $game_inv->AdjHandicap;
      $gs->MinHandicap = $game_inv->MinHandicap;
      $gs->MaxHandicap = $game_inv->MaxHandicap;
      $gs->Komi = $game_inv->Komi;
      $gs->AdjKomi = $game_inv->AdjKomi;
      $gs->JigoMode = $game_inv->JigoMode;
      $gs->Ruleset = $game_inv->Ruleset;
      $gs->Size = $game_inv->Size;
      $gs->Rated = $game_inv->Rated;
      $gs->StdHandicap = $game_inv->StdHandicap;
      $gs->Maintime = $game_inv->Maintime;
      $gs->Byotype = $game_inv->Byotype;
      $gs->Byotime = $game_inv->Byotime;
      $gs->Byoperiods = $game_inv->Byoperiods;
      $gs->WeekendClock = $game_inv->WeekendClock;
      return $gs;
   }//new_from_game_invitation

   /*!
    * \briefs Builds diffs between two GameSetup-objects regarding invitation-game-settings.
    * \return array with diff-arrays: [ setting-title, old-setting, new-setting [, long-diff=0|1]  ]
    */
   public static function build_invitation_diffs( GameSetup $gs_old, GameSetup $gs_new, $my_handle='', $opp_handle='' )
   {
      $out = array();

      if ( $gs_old->Ruleset !== $gs_new->Ruleset )
         $out[] = array( T_('Ruleset'), Ruleset::getRulesetText($gs_old->Ruleset), Ruleset::getRulesetText($gs_new->Ruleset) );
      if ( $gs_old->Size !== $gs_new->Size )
         $out[] = array( T_('Board Size'), $gs_old->Size, $gs_new->Size );

      // handicap-type
      $htype_new = self::swap_htype_black_white($gs_new->Handicaptype);
      $htype_old_text = $gs_old->format_handicap_type( null, $my_handle, $opp_handle );
      $htype_new_text = $gs_new->format_handicap_type( $htype_new, $my_handle, $opp_handle );
      if ( $htype_old_text !== $htype_new_text )
         $out[] = array( T_('Handicap Type#inv_diff'), $htype_old_text, $htype_new_text, 1 );

      $old_adj_handi = GameSettings::build_adjust_handicap(
         $gs_old->Size, $gs_old->AdjHandicap, $gs_old->MinHandicap, $gs_old->MaxHandicap );
      $new_adj_handi = GameSettings::build_adjust_handicap(
         $gs_new->Size, $gs_new->AdjHandicap, $gs_new->MinHandicap, $gs_new->MaxHandicap );
      if ( $old_adj_handi !== $new_adj_handi )
         $out[] = array( T_('Adjust Handicap#inv_diff'), $old_adj_handi, $new_adj_handi );

      if ( $gs_old->StdHandicap !== $gs_new->StdHandicap )
         $out[] = array( T_('Handicap stones placement#inv_diff'), $gs_old->format_std_handicap(), $gs_new->format_std_handicap() );

      $old_adj_komi = GameSettings::build_adjust_komi( $gs_old->AdjKomi, $gs_old->JigoMode );
      $new_adj_komi = GameSettings::build_adjust_komi( $gs_new->AdjKomi, $gs_new->JigoMode );
      if ( $old_adj_komi !== $new_adj_komi )
         $out[] = array( T_('Adjust Komi#inv_diff'), $old_adj_komi, $new_adj_komi );

      // time
      $old_time = $gs_old->format_time();
      $new_time = $gs_new->format_time();
      if ( $old_time !== $new_time )
         $out[] = array( T_('Time#inv_diff'), $old_time, $new_time );

      if ( $gs_old->WeekendClock !== $gs_new->WeekendClock )
         $out[] = array( T_('Clock runs on weekends'), yesno($gs_old->WeekendClock), yesno($gs_new->WeekendClock) );
      if ( $gs_old->Rated !== $gs_new->Rated )
         $out[] = array( T_('Rated game'), yesno($gs_old->Rated), yesno($gs_new->Rated) );

      return $out;
   }//build_invitation_diffs

   public static function create_opponent_game_setup( $game_setup, $opp_id )
   {
      $opp_gs = clone $game_setup;
      $opp_gs->init_opponent_handicaptype( $opp_id );
      return $opp_gs;
   }//create_opponent_game_setup

   public static function swap_htype_black_white( $handicaptype )
   {
      if ( $handicaptype == HTYPE_BLACK )
         return HTYPE_WHITE;
      elseif ( $handicaptype == HTYPE_WHITE )
         return HTYPE_BLACK;
      elseif ( $handicaptype == HTYPE_YOU_KOMI_I_COLOR )
         return HTYPE_I_KOMI_YOU_COLOR;
      elseif ( $handicaptype == HTYPE_I_KOMI_YOU_COLOR )
         return HTYPE_YOU_KOMI_I_COLOR;
      else
         return $handicaptype;
   }//swap_htype_black_white

} //end 'GameSetup'



define('GSC_VIEW_INVITE', -1); // invite/dispute
define('GSC_VIEW_TRULES', -2); // tournament-rules (behaves similar to GSETVIEW_STANDARD)

/*!
 * \class GameSetupChecker
 *
 * \brief Class to check values of input-fields for invitation/dispute and new-game.
 */
class GameSetupChecker
{
   private $view; //GSETVIEW_... for new-game, GSC_VIEW_INVITE for invite/dispute, GSC_VIEW_TRULES for tournament-rules
   public $errors = array();
   private $error_fields = array(); // field => 1

   public function __construct( $view )
   {
      $this->view = (int)$view;
   }

   public function has_errors()
   {
      return count($this->errors);
   }

   public function get_errors()
   {
      return $this->errors;
   }

   public function add_error( $error )
   {
      $this->errors[] = $error;
   }

   private function add_default_values_info()
   {
      $this->errors[] = T_('Note: Some invalid values may have been replaced with default-values!');
   }

   public function is_error_field( $field )
   {
      return @$this->error_fields[$field];
   }

   public function get_class_error_field( $field, $as_arr=false )
   {
      if ( $this->is_error_field($field) )
         return ( $as_arr ) ? array( 'class' => 'GSError' ) : 'class="GSError"';
      else
         return ( $as_arr ) ? array() : '';
   }

   private function check_game_rated()
   {
      if ( $this->view == GSETVIEW_MPGAME || $this->view == GSC_VIEW_TRULES )
         return;

      $rated = ( @$_REQUEST['rated'] == 'Y' );
      if ( $rated )
      {
         if ( !user_has_rating() )
            $this->errors[] = T_('User has no rating');
      }
   }//check_game_rated

   /*!
    * \brief Returns false if combination of game-settings (calculated handicap-type/rated-game)
    *       conflicts with user without rating; true otherwise.
    */
   public function check_opponent_rating( $opp_handle )
   {
      if ( (string)trim($opp_handle) == '' )
         return true;
      if ( $this->view != GSC_VIEW_INVITE )
         return;

      $is_rated_game = ( @$_REQUEST['rated'] == 'Y' );
      $is_calc_htype = is_htype_calculated( @$_REQUEST['cat_htype'], /*cat-htype*/true );
      if ( $is_rated_game || $is_calc_htype )
      {
         $opp_user = User::load_user_by_handle( $opp_handle );
         if ( !is_null($opp_user) && !$opp_user->hasRating() )
         {
            if ( $is_calc_htype )
               $this->add_error( T_('Opponent needs a rating to be able to use conventional or proper handicap-type.') );
            if ( $is_rated_game )
               $this->add_error( T_('Opponent needs a rating to be able to play a rated game.') );
         }
      }
   }//check_opponent_rating

   private function check_komi()
   {
      // komi-check only for: invite, std new-game (& tournament-rules)
      if ( $this->view != GSC_VIEW_INVITE && $this->view != GSETVIEW_STANDARD && $this->view != GSC_VIEW_TRULES )
         return;

      $has_err = false;
      $komi = trim(@$_REQUEST['komi_m']);
      if ( (string)$komi == '' || !is_numeric($komi) )
         $this->errors[] = $has_err = sprintf( T_('Invalid value for komi [%s].'), $komi );

      $komi = (float)$komi;

      if ( abs($komi) > MAX_KOMI_RANGE )
         $this->errors[] = $has_err = ErrorCode::get_error_text('komi_range');
      if ( floor(2 * $komi) != 2 * $komi ) // check for x.0|x.5
         $this->errors[] = $has_err = ErrorCode::get_error_text('komi_bad_fraction');

      if ( $has_err )
         $this->error_fields['komi_m'] = 1;
   }//check_komi

   private function check_time()
   {
      static $arr_check_fields = array( 'timevalue', // maintime
            'byotimevalue_jap', 'byoperiods_jap', // JAP
            'byotimevalue_can', 'byoperiods_can', // CAN
            'byotimevalue_fis', // FIS
         );

      $has_err = false;
      $ferrors = array();
      foreach ( $arr_check_fields as $field )
      {
         $val = trim(@$_REQUEST[$field]);
         if ( (string)$val == '' || !is_numeric($val) || (int)$val != $val || $val < 0 )
         {
            $ferrors[] = $val;
            $this->error_fields[$field] = $has_err = 1;
         }
      }
      if ( count($ferrors) )
         $this->errors[] = sprintf(T_('Invalid time values [%s], must be integer and >= 0.'), implode('][', $ferrors));

      $byoyomitype = @$_REQUEST['byoyomitype'];
      $timevalue = (int)@$_REQUEST['timevalue'];
      $timeunit = @$_REQUEST['timeunit'];

      $byotimevalue_jap = (int)@$_REQUEST['byotimevalue_jap'];
      $timeunit_jap = @$_REQUEST['timeunit_jap'];
      $byoperiods_jap = (int)@$_REQUEST['byoperiods_jap'];

      $byotimevalue_can = (int)@$_REQUEST['byotimevalue_can'];
      $timeunit_can = @$_REQUEST['timeunit_can'];
      $byoperiods_can = (int)@$_REQUEST['byoperiods_can'];

      if ( $byoyomitype == BYOTYPE_JAPANESE && $byoperiods_jap <= 0 )
      {
         $this->errors[] = T_('Invalid extra periods for Japanese byo-yomi');
         $this->error_fields['byoperiods_jap'] = $has_err = 1;
      }
      elseif ( $byoyomitype == BYOTYPE_CANADIAN && $byoperiods_can <= 0 )
      {
         $this->errors[] = T_('Invalid number of stones for Canadian byo-yomi');
         $this->error_fields['byoperiods_can'] = $has_err = 1;
      }

      $byotimevalue_fis = (int)@$_REQUEST['byotimevalue_fis'];
      $timeunit_fis = @$_REQUEST['timeunit_fis'];

      list( $hours, $byohours, $byoperiods, $limit_errors, $limit_errfields ) =
         interpret_time_limit_forms($byoyomitype, $timevalue, $timeunit,
                                    $byotimevalue_jap, $timeunit_jap, $byoperiods_jap,
                                    $byotimevalue_can, $timeunit_can, $byoperiods_can,
                                    $byotimevalue_fis, $timeunit_fis);
      if ( count($limit_errors) )
      {
         $this->errors = array_merge( $this->errors, $limit_errors );
         $this->error_fields = array_merge( $this->error_fields, $limit_errfields );
         $has_err = 1;
      }

      if ( $hours < 1 && ($byohours < 1 || $byoyomitype == BYOTYPE_FISCHER) )
      {
         $this->errors[] = ErrorCode::get_error_text('time_limit_too_small');
         if ( !$has_err )
         {
            foreach ( $arr_check_fields as $err_field )
               $this->error_fields[$err_field] = 1;
         }
      }
   }//check_time

   private function check_adjust_komi()
   {
      // komi-check only for: invite, std new-game (incl. tournament-rules)
      if ( $this->view != GSC_VIEW_INVITE && $this->view != GSETVIEW_STANDARD && $this->view != GSC_VIEW_TRULES )
         return;

      $has_err = false;
      $adj_komi = @$_REQUEST['adj_komi'];
      if ( (string)$adj_komi == '' || !is_numeric($adj_komi) )
         $this->errors[] = $has_err = sprintf( T_('Invalid value for komi-adjustment [%s].'), $adj_komi );

      if ( abs($adj_komi) > MAX_KOMI_RANGE )
         $this->errors[] = $has_err = T_('Adjust Komi') . ': ' . ErrorCode::get_error_text('komi_range');
      if ( floor(2 * $adj_komi) != 2 * $adj_komi ) // check for x.0|x.5
         $this->errors[] = $has_err = T_('Adjust Komi') . ': ' . ErrorCode::get_error_text('komi_bad_fraction');

      if ( $has_err )
         $this->error_fields['adj_komi'] = 1;
   }//check_adjust_komi

   private function check_min_rated_games()
   {
      // min-rated-games only for: std/fair-komi new-game (incl. tournament-rules)
      if ( $this->view != GSETVIEW_STANDARD && $this->view != GSC_VIEW_TRULES && $this->view != GSETVIEW_FAIRKOMI )
         return;

      $min_rgames = @$_REQUEST['min_rated_games'];
      if ( (string)$min_rgames == '' )
         return;

      $has_err = false;
      if ( !is_numeric($min_rgames) || (int)$min_rgames != $min_rgames || $min_rgames < 0 )
         $this->errors[] = $has_err = sprintf( T_('Invalid value for min. rated games [%s].'), $min_rgames );

      $min_rgames = (int)$min_rgames;
      if ( $min_rgames > 999 )
         $this->errors[] = $has_err = sprintf( T_('Value for min. rated games is out of range.'), $min_rgames );

      if ( $has_err )
         $this->error_fields['min_rated_games'] = 1;
   }//check_min_rated_games

   private function check_hero_ratio()
   {
      // min-hero-ratio only for: std/fair-komi new-game
      if ( $this->view != GSETVIEW_STANDARD && $this->view != GSETVIEW_FAIRKOMI )
         return;

      $min_hero_ratio = @$_REQUEST['min_hero_ratio'];
      if ( (string)$min_hero_ratio == '' )
         $min_hero_ratio = 0;

      $has_min_err = false;
      if ( !is_numeric($min_hero_ratio) || (int)$min_hero_ratio != $min_hero_ratio || $min_hero_ratio < 0 || $min_hero_ratio > 100 )
         $this->errors[] = $has_min_err = sprintf( T_('Invalid value for min. hero ratio [%s] (only integer in range 0..100).'), $min_hero_ratio );

      if ( $has_min_err )
         $this->error_fields['min_hero_ratio'] = 1;
   }//check_hero_ratio

   private function check_game_players()
   {
      // game-players only for: MPG new-game
      if ( $this->view != GSETVIEW_MPGAME )
         return;

      $has_err = false;
      $game_players = @$_REQUEST['game_players'];
      $game_type = MultiPlayerGame::determine_game_type($game_players);
      if ( is_null($game_type) )
         $this->errors[] = $has_err = sprintf( T_('Invalid value for game-players [%s].'), $game_players );

      if ( $has_err )
         $this->error_fields['game_players'] = 1;
   }//check_game_players


   // ------------ static functions ----------------------------

   public static function check_fields( $view )
   {
      $gsc = new GameSetupChecker( $view );
      $gsc->check_game_rated();
      $gsc->check_komi();
      $gsc->check_time();
      $gsc->check_adjust_komi();
      $gsc->check_min_rated_games();
      $gsc->check_hero_ratio();
      $gsc->check_game_players();

      if ( $gsc->has_errors() )
         $gsc->add_default_values_info();
      return $gsc;
   }//check_fields

   /*! \brief Checks max. count of existing waiting-room-entries. */
   public static function check_wroom_count( $view, $uid, array &$errors )
   {
      if ( WROOM_MAX_ENTRIES <= 0 )
         return;

      if ( $view == GSETVIEW_STANDARD || $view == GSETVIEW_FAIRKOMI || $view == GSETVIEW_MPGAME )
      {
         $row = mysql_single_fetch( "GameSetupChecker:check_wroom_count.count_wr($uid,$view)",
               "SELECT COUNT(*) AS X_Count FROM Waitingroom WHERE uid=$uid" );
         if ( $row && (int)@$row['X_Count'] >= WROOM_MAX_ENTRIES )
         {
            $errors[] = sprintf( T_('Max. number of own waiting-room entries [%s] has been reached.'), WROOM_MAX_ENTRIES ) . "\n" .
               T_('You can save the form as template for later, delete one waiting-room offer or wait till they are joined.') . "\n" .
               ( $view != GSETVIEW_MPGAME ?
                  T_('Number of games for same offer can be set with \'Number of games to add\'.') : '' );
         }
      }
   }//check_wroom_count

} //end 'GameSetupChecker'




/*!
 * \class GameSetupBuilder
 *
 * \brief Class to pre-seed form-fields for invites and game-setup for rematch and templates.
 */
class GameSetupBuilder
{
   private $my_id;
   private $game_setup;
   private $game;
   private $is_mpg;
   private $is_template;

   /*!
    * \brief Constructs GameSetupBuilder.
    * \param $game Games-object, null, or GameSetup-object (as it has the same field-interface) for fields:
    *           GamePlayers, ShapeID, ShapeSnapshot, Ruleset, Size, StdHandicap, Handicap, Komi,
    *           Maintime, Byotype, Byotime, Byoperiods, WeekendClock;
    *        The following fields do not have common semantics, therefore are treated differently using $is_template:
    *           DoubleGame_ID, Black_ID, White_ID, Rated
    */
   public function __construct( $my_id = 0, $game_setup = null, $game = null, $is_mpg = false, $is_template = false )
   {
      $this->my_id = $my_id;
      $this->game_setup = $game_setup;
      $this->game = $game;
      $this->is_mpg = $is_mpg;
      $this->is_template = $is_template;
   }

   public function fill_invite_from_game( array &$url )
   {
      $this->build_url_invite_to( $url );
      $this->build_url_game_basics( $url );
      $this->build_url_cat_htype_manual( $url, CAT_HTYPE_MANUAL, null );
      $this->build_url_handi_komi_rated( $url );
   }//fill_invite_from_game

   public function fill_invite_from_game_setup( array &$url )
   {
      $cat_htype = get_category_handicaptype( $this->game_setup->Handicaptype );

      $this->build_url_invite_to( $url );
      $this->build_url_game_basics( $url );
      $this->build_url_cat_htype_manual( $url, $cat_htype, $this->game_setup->Handicaptype );
      $this->build_url_handi_komi_rated( $url );
      $this->build_url_adjustments( $url, $cat_htype );

      if ( !$this->is_template )
         $url['message'] = $this->game_setup->Message;
   }//fill_invite_from_game_setup


   public function fill_new_game_from_game( array &$url )
   {
      $url['view'] = ( $this->is_mpg ) ? GSETVIEW_MPGAME : GSETVIEW_STANDARD;

      $this->build_url_game_basics( $url );
      $url['game_players'] = $this->game->GamePlayers;

      if ( !$this->is_mpg )
      {
         $this->build_url_cat_htype_manual( $url, CAT_HTYPE_MANUAL, null );
         $this->build_url_handi_komi_rated( $url );
      }
   }//fill_new_game_from_game

   public function fill_new_game_from_game_setup( array &$url )
   {
      $cat_htype = get_category_handicaptype( $this->game_setup->Handicaptype );

      if ( $this->is_template )
      {
         $url['nrGames'] = $this->game_setup->NumGames;
         $url['view'] = $this->game_setup->ViewMode;
      }
      else
         $url['view'] = ($cat_htype === CAT_HTYPE_FAIR_KOMI) ? GSETVIEW_FAIRKOMI : GSETVIEW_STANDARD;

      $this->build_url_game_basics( $url );
      $url['game_players'] = $this->game->GamePlayers;

      $this->build_url_cat_htype_manual( $url, $cat_htype, $this->game_setup->Handicaptype );
      $this->build_url_handi_komi_rated( $url );
      $this->build_url_adjustments( $url, $cat_htype );

      $url['mb_rated'] = bool_YN( $this->game_setup->MustBeRated );
      if ( $this->game_setup->RatingMin < OUT_OF_RATING )
         $url['rat1'] = $this->game_setup->RatingMin;
      if ( $this->game_setup->RatingMax < OUT_OF_RATING )
         $url['rat2'] = $this->game_setup->RatingMax;
      $url['min_rg'] = $this->game_setup->MinRatedGames;
      $url['min_hero_ratio'] = $this->game_setup->MinHeroRatio;
      $url['same_opp'] = $this->game_setup->SameOpponent;
      $url['comment'] = $this->game_setup->Message;
   }//fill_new_game_from_game_setup


   public function build_url_game_basics( array &$url )
   {
      if ( $this->game->ShapeID > 0 )
      {
         $url['shape'] = $this->game->ShapeID;
         $url['snapshot'] = $this->game->ShapeSnapshot;
      }

      $url['ruleset'] = $this->game->Ruleset;
      $url['size'] = $this->game->Size;
      $url['stdhandicap'] = bool_YN( $this->game->StdHandicap );

      $this->build_url_time( $url );
   }//build_url_game_basics

   public function build_url_time( array &$url )
   {
      $MaintimeUnit = 'hours';
      $Maintime = $this->game->Maintime;
      time_convert_to_longer_unit($Maintime, $MaintimeUnit);
      $url['timeunit'] = $MaintimeUnit;
      $url['timevalue'] = $Maintime;

      $url['byoyomitype'] = $this->game->Byotype;
      $ByotimeUnit = 'hours';
      $Byotime = $this->game->Byotime;
      time_convert_to_longer_unit($Byotime, $ByotimeUnit);
      $url['byotimevalue_jap'] = $url['byotimevalue_can'] = $url['byotimevalue_fis'] = $Byotime;
      $url['timeunit_jap'] = $url['timeunit_can'] = $url['timeunit_fis'] = $ByotimeUnit;

      if ( $this->game->Byoperiods > 0 )
         $url['byoperiods_jap'] = $url['byoperiods_can'] = $this->game->Byoperiods;

      $url['weekendclock'] = bool_YN( $this->game->WeekendClock );
   }//build_url_time

   public function build_url_cat_htype_manual( array &$url, $cat_htype, $gs_htype )
   {
      $url['cat_htype'] = $cat_htype;
      if ( !is_null($gs_htype) && $cat_htype === CAT_HTYPE_MANUAL )
         $url['color_m'] = ($gs_htype == HTYPE_ALTERNATE) ? HTYPE_NIGIRI : $gs_htype;
      elseif ( !$this->is_template && $this->game->DoubleGame_ID != 0 )
         $url['color_m'] = HTYPE_DOUBLE;
      elseif ( !$this->is_template && $this->my_id == $this->game->Black_ID )
         $url['color_m'] = HTYPE_BLACK;
      elseif ( !$this->is_template && $this->my_id == $this->game->White_ID )
         $url['color_m'] = HTYPE_WHITE;
      else
         $url['color_m'] = HTYPE_NIGIRI; // default
   }//build_url_cat_htype_manual

   public function build_url_handi_komi_rated( array &$url )
   {
      if ( is_null($this->game_setup) )
      {
         $url['handicap_m'] = $this->game->Handicap;
         $url['komi_m'] = $this->game->Komi;
      }
      else
      {
         $url['handicap_m'] = $this->game_setup->Handicap;
         $url['komi_m'] = $this->game_setup->Komi;
      }

      if ( $this->is_template )
         $url['rated'] = bool_YN( $this->game->Rated );
      else
         $url['rated'] = ( $this->game->Rated == 'N' ) ? 'N' : 'Y';
   }//build_url_handi_komi_rated

   public function build_url_adjustments( array &$url, $cat_htype )
   {
      $url['fk_htype'] = ($cat_htype === CAT_HTYPE_FAIR_KOMI) ? $this->game_setup->Handicaptype : DEFAULT_HTYPE_FAIRKOMI;
      $url['adj_komi'] = $this->game_setup->AdjKomi;
      $url['jigo_mode'] = $this->game_setup->JigoMode;
      $url['adj_handicap'] = $this->game_setup->AdjHandicap;
      $url['min_handicap'] = $this->game_setup->MinHandicap;
      $url['max_handicap'] = $this->game_setup->MaxHandicap;
   }//build_url_adjustments

   public function build_url_invite_to( array &$url )
   {
      if ( $this->is_template ) // skip for template
         return;

      if ( $this->my_id == $this->game->Black_ID )
         $opp_id = $this->game->White_ID;
      elseif ( $this->my_id == $this->game->White_ID )
         $opp_id = $this->game->Black_ID;
      else
         $opp_id = 0;

      $opp_to = '';
      if ( $opp_id > 0 )
      {
         $users = User::load_quick_userinfo( array( $opp_id ) );
         if ( isset($users[$opp_id]) )
            $opp_to = $users[$opp_id]['Handle'];
      }
      $url['to'] = $opp_to;
   }//build_url_invite_to

} //end 'GameSetupBuilder'



// see also specs/quick_suite.txt, section (3c) message "info"-command, calc_*-fields
define('GSC_COL_DOUBLE', 'double');
define('GSC_COL_FAIRKOMI', 'fairkomi');
define('GSC_COL_NIGIRI', 'nigiri');
define('GSC_COL_BLACK', 'black');
define('GSC_COL_WHITE', 'white');

/*!
 * \class GameSettingsCalculator
 *
 * \brief Helper-class to calculate probable game-setting.
 */
class GameSettingsCalculator
{
   private $game_setup;
   private $game_settings;
   private $pl_rating;
   private $opp_rating;
   private $is_calculated;
   private $is_tourney;

   // calculated results
   public $calc_type = 0; // 1=probable, 2=fix/calculated
   public $calc_color = ''; // double, fairkomi, nigiri, black, white
   public $calc_handicap = 0;
   public $calc_komi = 0;

   /*!
    * \brief Constructs GameSettingsCalculator.
    * \param $game_setup GameSetup-object with expected fields: Handicaptype, Size, Komi,
    *        AdjHandicap, MinHandicap, MaxHandicap, AdjKomi, JigoMode
    * \param $is_calculated null if should be calculated here; must be given for ladder-tourney
    */
   public function __construct( $game_setup, $player_rating, $opp_rating, $is_calculated=null )
   {
      $this->game_setup = $game_setup;
      $this->game_settings = GameSettings::get_game_settings_from_gamesetup( $game_setup );
      $this->pl_rating = $player_rating;
      $this->opp_rating = $opp_rating;
      if ( is_null($is_calculated) )
      {
         $htype = $game_setup->Handicaptype;
         $this->is_calculated = is_htype_calculated( $htype );
      }
      else
         $this->is_calculated = $is_calculated;
   }//__construct

   public function calculate_settings( $pivot_uid )
   {
      $htype = $this->game_setup->get_user_view_handicaptype( $pivot_uid );
      $CategoryHandiType = get_category_handicaptype($htype);
      $is_fairkomi = ( $CategoryHandiType === CAT_HTYPE_FAIR_KOMI );

      $is_nigiri = false; // true, if nigiri needed (because of same rating)
      if ( $CategoryHandiType == CAT_HTYPE_PROPER )
      {
         list( $infoHandicap, $infoKomi, $info_i_am_black, $is_nigiri ) =
            $this->game_settings->suggest_proper( $this->pl_rating, $this->opp_rating );
      }
      elseif ( $CategoryHandiType == CAT_HTYPE_CONV )
      {
         list( $infoHandicap, $infoKomi, $info_i_am_black, $is_nigiri ) =
            $this->game_settings->suggest_conventional( $this->pl_rating, $this->opp_rating );
      }
      elseif ( $is_fairkomi )
      {
         $infoHandicap = $this->game_setup->Handicap;
         $infoKomi = 0;
         //$info_i_am_black unknown yet
      }
      else //if ( $CategoryHandiType == CAT_HTYPE_MANUAL )
      {
         $infoHandicap = $this->game_setup->Handicap;
         $infoKomi = $this->game_setup->Komi;
         $info_i_am_black = ($htype == HTYPE_BLACK); // game-offerer (pivot-user) wants BLACK, so challenger gets WHITE
      }

      // determine color
      if ( $htype == HTYPE_DOUBLE )
         $color = GSC_COL_DOUBLE;
      elseif ( $is_fairkomi )
         $color = GSC_COL_FAIRKOMI;
      elseif ( $htype == HTYPE_NIGIRI || $is_nigiri )
         $color = GSC_COL_NIGIRI;
      else
         $color = ( $info_i_am_black ) ? GSC_COL_BLACK : GSC_COL_WHITE;

      $this->calc_type = ($this->is_calculated) ? 2 : 1;
      $this->calc_color = $color;
      $this->calc_handicap = $infoHandicap;
      $this->calc_komi = ( $is_fairkomi ) ? '' : $infoKomi;
   }//calculate_settings

} //end 'GameSettingsCalculator'



/*!
 * \class GameSettings
 *
 * \brief Class to store parameters for calculation of game-settings for conventional and proper handicap type.
 */
class GameSettings
{
   private $Size;
   private $Ruleset;
   private $AdjHandicap;
   private $MinHandicap;
   private $MaxHandicap;
   private $AdjKomi;
   private $JigoMode;

   public function __construct( $size, $ruleset, $adj_handicap, $min_handicap, $max_handicap, $adj_komi, $jigomode )
   {
      $this->Size = (int)$size;
      $this->Ruleset = $ruleset;
      $this->AdjHandicap = (int)$adj_handicap;
      $this->MinHandicap = (int)$min_handicap;
      $this->MaxHandicap = (int)$max_handicap;
      $this->AdjKomi = (float)$adj_komi;
      $this->JigoMode = $jigomode;
   }

   public function to_string()
   {
      return "GS({$this->Size},{$this->Ruleset};"
         . "H{$this->AdjHandicap}[{$this->MinHandicap},{$this->MaxHandicap}];"
         . "K{$this->AdjKomi}[{$this->JigoMode}])";
   }

   // (handi,komi,iamblack,is_nigiri) = suggest_proper(my_rating, $opp_rating)
   // NOTE: iamblack/is_nigiri is <>''
   public function suggest_proper( $rating_W, $rating_B, $positive_komi=false )
   {
      $H = abs($rating_W - $rating_B) / 100.0;

      // Handicap value is about proportional to number of moves
      $H *= handicapfactor( $this->Size );

      $H += 0.5; // advantage for playing first;
      $handicap = ( $positive_komi ? ceil($H) : round($H) );

      // temporary, there is no 0 handicap stone game in this calculus. An equal
      // game is a 1 stone game where black play his handicap stone where he want.
      if ( $handicap < 1 ) $handicap = 1;

      $komi = round( 2.0 * STONE_VALUE * ( $handicap - $H ) ) / 2.0;
      $komi += Ruleset::getRulesetDefaultKomi($this->Ruleset) - STONE_VALUE / 2.0;

      // adjust handicap & komi
      $handicap = self::adjust_handicap( $this->Size, $handicap,
         $this->AdjHandicap, $this->MinHandicap, $this->MaxHandicap, /*round-H*/false );
      $komi = self::adjust_komi( $komi, $this->AdjKomi, $this->JigoMode );

      if ( $handicap == 1 ) $handicap = 0; //back to the 0 handicap habit

      // NOTE: calculate nigiri/i-am-black on original rating-diff even after handicap/komi-adjustment
      $is_nigiri = ( $rating_B == $rating_W );
      if ( $is_nigiri )
         $iamblack = mt_rand(0,1); // nigiri on same rating
      else
         $iamblack = ( $rating_B > $rating_W );

      return array( $handicap, $komi, ($iamblack ? 1:0), ($is_nigiri ? 1:0) );
   }//suggest_proper

   // (handi,komi,iamblack,is_nigiri) = suggest_conventional(my_rating, $opp_rating)
   // NOTE: iamblack/is_nigiri is <>''
   public function suggest_conventional( $rating_W, $rating_B, $positive_komi=false )
   {
      $H = abs($rating_W - $rating_B) / 100.0;

      // Handicap value is about proportional to number of moves
      $H *= handicapfactor( $this->Size );
      $handicap = round($H);

      $handicap = self::adjust_handicap( $this->Size, $handicap,
         $this->AdjHandicap, $this->MinHandicap, $this->MaxHandicap, /*round-H*/false );

      if ( $handicap == 0 ) // even-game
      {
         $komi = Ruleset::getRulesetDefaultKomi($this->Ruleset);
         $is_nigiri = true;
         $iamblack = mt_rand(0,1); // nigiri on even-game
      }
      else // handicap-game
      {
         if ( $handicap == 1 ) $handicap = 0;
         $komi = 0.5;
         $is_nigiri = false;
         $iamblack = ( $rating_B > $rating_W );
      }

      $komi = self::adjust_komi( $komi, $this->AdjKomi, $this->JigoMode );

      return array( $handicap, $komi, ($iamblack ? 1:0), ($is_nigiri ? 1:0) );
   }//suggest_conventional


   // ------------ static functions ----------------------------

   public static function get_game_settings_from_gamesetup( $game_setup )
   {
      return new GameSettings( $game_setup->Size, $game_setup->Ruleset,
         $game_setup->AdjHandicap, $game_setup->MinHandicap, $game_setup->MaxHandicap,
         $game_setup->AdjKomi, $game_setup->JigoMode );
   }

   // $use_defaults : true = adjust-handicap/komi fields are not available in $grow (use defaults)
   public static function get_game_settings_from_gamerow( array $grow, $use_defaults )
   {
      if ( $use_defaults )
      {
         $gs = new GameSetup(0);
         return new GameSettings( $grow['Size'], $grow['Ruleset'],
            $gs->AdjHandicap, $gs->MinHandicap, $gs->MaxHandicap, $gs->AdjKomi, $gs->JigoMode );
      }
      else
      {
         return new GameSettings( $grow['Size'], $grow['Ruleset'],
            $grow['AdjHandicap'], $grow['MinHandicap'], $grow['MaxHandicap'], $grow['AdjKomi'], $grow['JigoMode'] );
      }
   }

   /*! \brief Returns arr( minH, maxH ) to check for limits on handicap stones. */
   private static function get_handicap_limits( $size, $min_handicap, $max_handicap )
   {
      // assure valid limits
      $min_handicap = min( MAX_HANDICAP, max( 0, $min_handicap ));
      $max_handicap = DefaultMaxHandicap::limit_max_handicap( $max_handicap );

      if ( $max_handicap == DEFAULT_MAX_HANDICAP )
      {
         $chk_max_handicap = DefaultMaxHandicap::calc_def_max_handicap( $size );
         if ( $min_handicap > $chk_max_handicap )
            $min_handicap = $chk_max_handicap;
      }
      else
      {
         if ( $min_handicap > $max_handicap )
            swap( $min_handicap, $max_handicap );
         $chk_max_handicap = $max_handicap;
      }

      return array( $min_handicap, $chk_max_handicap );
   }//get_handicap_limits

   // returns adjusted handicap within limits, also checking for valid limits
   public static function adjust_handicap( $size, $handicap, $adj_handicap, $min_handicap, $max_handicap, $round_handi1=true )
   {
      list( $chk_min_handicap, $chk_max_handicap ) = self::get_handicap_limits( $size, $min_handicap, $max_handicap );

      // adjust
      if ( $adj_handicap )
         $handicap += $adj_handicap;

      if ( $handicap < $chk_min_handicap )
         $handicap = $chk_min_handicap;
      elseif ( $handicap > $chk_max_handicap )
         $handicap = $chk_max_handicap;

      if ( $round_handi1 && $handicap == 1 )
         $handicap = 0;

      return (int)$handicap;
   }//adjust_handicap

   // returns adjusted komi within limits, also checking for valid limits
   public static function adjust_komi( $komi, $adj_komi, $jigo_mode )
   {
      // adjust
      if ( $adj_komi )
         $komi += $adj_komi;

      // assure valid limits up to the limits
      if ( $komi < -MAX_KOMI_RANGE )
         $komi = -MAX_KOMI_RANGE;
      elseif ( $komi > MAX_KOMI_RANGE )
         $komi = MAX_KOMI_RANGE;

      if ( $jigo_mode == JIGOMODE_ALLOW_JIGO && floor($komi) != $komi )
         $komi = (($komi < 0) ? -1 : 1) * floor(abs($komi));
      elseif ( $jigo_mode == JIGOMODE_NO_JIGO && floor($komi) == $komi )
         $komi += ($komi < 0) ? -0.5 : 0.5;

      // assure valid limits after applying jigo-mode
      $komi = (float) round( 2 * $komi ) / 2;
      if ( $komi < -MAX_KOMI_RANGE )
         $komi += 1.0;
      elseif ( $komi > MAX_KOMI_RANGE )
         $komi -= 1.0;

      if ( (string)$komi == '-0' ) // strange effect
         $komi = 0;
      return (float)$komi;
   }//adjust_komi

   // output (with optional parts): +/-adj [min,[D]max]
   // returns '' if no handicap; caller must format empty to NO_VALUE for example
   public static function build_adjust_handicap( $size, $adj_handicap, $min_handicap, $max_handicap, $short=false )
   {
      $out = array();
      if ( $adj_handicap )
         $out[] = ($adj_handicap > 0 ? '+' : '') . $adj_handicap;
      if ( $max_handicap == DEFAULT_MAX_HANDICAP )
      {
         $def_max_handicap = DefaultMaxHandicap::calc_def_max_handicap($size);
         if ( $short )
            $out[] = sprintf( "[%d,D%d]", $min_handicap, $def_max_handicap );
         else
            $out[] = sprintf( "[%d,%s %d]", $min_handicap, T_('Default'), $def_max_handicap );
      }
      elseif ( $min_handicap > 0 || $max_handicap < MAX_HANDICAP )
         $out[] = sprintf( "[%d,%d]", $min_handicap, min( MAX_HANDICAP, $max_handicap) );

      return ( count($out) ) ? implode(' ',$out) : '';
   }//build_adjust_handicap

   // output (with optional parts): +/-adj [jigomode]
   // returns '' if no komi-adjustment; caller must format "empty" value
   public static function build_adjust_komi( $adj_komi, $jigo_mode, $short=false )
   {
      $out = array();
      if ( (float)$adj_komi != 0.0 )
         $out[] = ($adj_komi > 0 ? '+' : '') . (float)$adj_komi;
      if ( $jigo_mode != JIGOMODE_KEEP_KOMI )
      {
         $jigo_str = '';
         if ( $jigo_mode == JIGOMODE_ALLOW_JIGO )
            $jigo_str = ($short) ? T_('.0#wroomshort') : T_('Allow Jigo#wroom');
         elseif ( $jigo_mode == JIGOMODE_NO_JIGO )
            $jigo_str = ($short) ? T_('.5#wroomshort') : T_('No Jigo#wroom');
         if ( $jigo_str )
            $out[] = sprintf( '[%s]', $jigo_str );
      }

      return ( count($out) ) ? implode(' ',$out) : '';
   }//build_adjust_komi

} // end 'GameSettings'



define('MAX_PROFILE_TEMPLATES', 30);
define('MAX_PROFILE_TEMPLATES_DATA', 10000); // max byte-len for template

/**
 * \class ProfileTemplate
 *
 * \brief Class to handle templates for send-message and game-setup (for invitation and new-game)
 *        stored in Profiles-table.
 */
class ProfileTemplate
{
   public $TemplateType; //PROFTYPE_TMPL_...

   public $GameSetup;
   private $Subject;
   private $Text;

   /*!
    * \brief Constructs template with template-type.
    * \param $template_type one of PROFTYPE_TMPL_...
    */
   public function __construct( $template_type )
   {
      if ( !self::is_valid_type($template_type) )
         error('invalid_args', "ProfileTemplate.new($template_type)");
      $this->TemplateType = (int)$template_type;
   }

   /*! \brief Encodes template into blob-value stored in Profiles-table. */
   public function encode_template()
   {
      if ( $this->TemplateType == PROFTYPE_TMPL_SENDMSG )
         $result = "{$this->Subject}\n{$this->Text}";
      elseif ( $this->TemplateType == PROFTYPE_TMPL_INVITE )
      {
         $extra = sprintf('SH%s:%s', $this->GameSetup->ShapeID, $this->GameSetup->ShapeSnapshot );
         $result = $this->GameSetup->encode_profile_template( $this->TemplateType )
            . "\n$extra\n{$this->Subject}\n{$this->Text}";
      }
      elseif ( $this->TemplateType == PROFTYPE_TMPL_NEWGAME )
      {
         $extra = sprintf('V%s G%s GP%s SH%s:%s',
            $this->GameSetup->ViewMode, $this->GameSetup->NumGames, $this->GameSetup->GamePlayers,
            $this->GameSetup->ShapeID, $this->GameSetup->ShapeSnapshot );
         $result = $this->GameSetup->encode_profile_template( $this->TemplateType )
            . "\n$extra\n{$this->Subject}\n{$this->Text}";
      }
      else
         error('invalid_args', "ProfileTemplate.encode.check.type({$this->TemplateType})");

      return trim($result);
   }//encode_template

   public function build_profile()
   {
      global $player_row;

      $profile = self::new_default_profile( $player_row['ID'], $this->TemplateType );
      $profile->set_text( $this->encode_template() );
      return $profile;
   }//build_profile

   public function fill( array &$url, $use_type=null )
   {
      if ( is_null($use_type) )
         $use_type = $this->TemplateType;

      if ( $use_type == PROFTYPE_TMPL_SENDMSG )
         $this->fill_message( $url );
      elseif ( $use_type == PROFTYPE_TMPL_INVITE )
      {
         $gs_builder = new GameSetupBuilder( 0, $this->GameSetup, /*game*/$this->GameSetup, /*mpg*/false, /*tmpl*/true );
         $gs_builder->fill_invite_from_game_setup( $url );
         $this->fill_message( $url );
      }
      elseif ( $use_type == PROFTYPE_TMPL_NEWGAME )
      {
         $gs_builder = new GameSetupBuilder( 0, $this->GameSetup, /*game*/$this->GameSetup, /*mpg*/false, /*tmpl*/true );
         $gs_builder->fill_new_game_from_game_setup( $url );
         $url['comment'] = $this->Subject;
      }
      else
         error('invalid_args', "ProfileTemplate.fill({$this->TemplateType},$use_type)");
   }//fill

   /*! \brief Fills new-game form-values with invite-template-type data. */
   public function fill_new_game_with_invite( array &$url, $use_type )
   {
      if ( $use_type == PROFTYPE_TMPL_NEWGAME && $this->TemplateType == PROFTYPE_TMPL_INVITE )
      {
         list( $line, $tmp ) = self::eat_line( $this->Text ); // take 1st line
         $url['comment'] = ( strlen($line) > 40 ) ? substr($line,0,40) : $line;
      }
   }//fill_new_game_with_invite

   public function fill_invite_with_new_game( array &$url, $use_type )
   {
      if ( $use_type == PROFTYPE_TMPL_INVITE && $this->TemplateType == PROFTYPE_TMPL_NEWGAME )
         $url['message'] = $this->Subject;
   }

   public function fill_message( array &$url )
   {
      $url['subject'] = $this->Subject;
      $url['message'] = $this->Text;
   }

   /*!
    * \brief Returns true if current decoded new-game-template can be used as template for invite.
    * \see is_valid_template_raw_check()
    */
   public function is_valid_new_game_template_for_invite()
   {
      if ( $this->TemplateType == PROFTYPE_TMPL_NEWGAME && !is_null($this->GameSetup) )
      {
         if ( $this->GameSetup->ViewMode == GSETVIEW_MPGAME )
            return false;
      }
      return true;
   }//is_valid_new_game_template_for_invite

   public function to_string()
   {
      $gs_str = (is_null($this->GameSetup)) ? '-' : $this->GameSetup->to_string(/*inv*/true);
      return "ProfileTemplate({$this->TemplateType}): GameSetup=[$gs_str] Subject=[{$this->Subject}] Text=[$this->Text]";
   }


   // ------------ static functions ----------------------------

   public static function new_template_send_message( $subject, $text )
   {
      $tmpl = new ProfileTemplate( PROFTYPE_TMPL_SENDMSG );
      $tmpl->Subject = trim($subject);
      $tmpl->Text = trim($text);
      return $tmpl;
   }

   public static function new_template_game_setup_invite( $subject, $text )
   {
      $tmpl = new ProfileTemplate( PROFTYPE_TMPL_INVITE );
      $tmpl->Subject = trim($subject);
      $tmpl->Text = trim($text);
      return $tmpl;
   }

   public static function new_template_game_setup_newgame( $comment )
   {
      $tmpl = new ProfileTemplate( PROFTYPE_TMPL_NEWGAME );
      $tmpl->Subject = trim($comment);
      $tmpl->Text = '';
      return $tmpl;
   }

   /*!
    * \brief Returns false, if value from Profile.Text of given template-type can be used for $use_type.
    * \see #is_valid_new_game_template_for_invite()
    */
   public static function is_valid_template_raw_check( $template_type, $use_type, $value )
   {
      if ( $template_type == PROFTYPE_TMPL_NEWGAME && $use_type == PROFTYPE_TMPL_INVITE )
      {
         // MPG not supported for invite
         if ( preg_match("/^[^\\n]+\\nV".GSETVIEW_MPGAME."\\b/", $value) )
            return false;
      }
      return true;
   }//is_valid_template_raw_check

   /*! \brief Parses profile-raw-value into game-setup and other fields dependent on template-type. */
   public static function decode( $template_type, $value )
   {
      $tmpl = new ProfileTemplate( $template_type );

      if ( $template_type == PROFTYPE_TMPL_SENDMSG )
      {
         list( $tmpl->Subject, $tmpl->Text ) = self::eat_line( $value );
      }
      elseif ( $template_type == PROFTYPE_TMPL_INVITE || $template_type == PROFTYPE_TMPL_NEWGAME )
      {
         list( $gs_line, $rem1 ) = self::eat_line( $value );
         list( $extra_line, $rem2 ) = self::eat_line( $rem1 );
         list( $tmpl->Subject, $tmpl->Text ) = self::eat_line( $rem2 );

         $tmpl->GameSetup = GameSetup::new_from_game_setup( $gs_line, /*inv*/true, /*0OnEmpty*/false );

         // parse extra-format for types -> see 'specs/db/table-Profiles.txt'
         $pline = $extra_line;
         while ( (string)$pline != '' )
         {
            if ( preg_match("/^SH\\d+:.*$/", $pline) ) // new-game + invite
            {
               list( $shape_id, $shape_snapshot ) = self::eat_line( $pline, ':' );
               $shape_id = (int)substr($shape_id, 2);
               if ( $shape_id <= 0 )
               {
                  $shape_id = 0;
                  $shape_snapshot = '';
               }
               $tmpl->GameSetup->ShapeID = $shape_id;
               $tmpl->GameSetup->ShapeSnapshot = $shape_snapshot;

               $pline = '';
               break; // shape must be last group
            }

            $input = $pline; // note: must use copied value for list(..) =
            list( $val, $pline ) = self::eat_line( $input, ' ' ); // eat next group

            if ( preg_match("/^V\\d+$/", $val) ) // new-game: viewmode
               $tmpl->GameSetup->ViewMode = (int)substr($val, 1);
            elseif ( preg_match("/^G\\d+$/", $val) ) // new-game: num-games
               $tmpl->GameSetup->NumGames = (int)substr($val, 1);
            elseif ( preg_match("/^GP(\\d+|\\d+:\\d+)?$/", $val) ) // new-game: game-players
               $tmpl->GameSetup->GamePlayers = trim( substr($val, 2) );
            else
               error('invalid_args', "ProfileTemplate.decode.parse.extra($template_type,[$val],[$pline],[$extra_line])");
         }//while
      }//invite/new-game

      return $tmpl;
   }//decode

   /*! \brief Splits line at next LF and return it as arr(1st-part, remaining-part). */
   private static function eat_line( $str, $sep="\n" )
   {
      $pos = strpos($str, $sep);
      if ( $pos === false )
         return array( $str, '' );
      else
         return array( trim( substr($str, 0, $pos) ), trim( substr($str, $pos + 1) ) );
   }//eat_line

   public static function add_menu_link( array &$menu, $handle='' )
   {
      $handle = trim($handle);
      if ( (string)$handle != '' && strpos($handle, ' ') === false ) // handle set & no multi-users
      {
         $text = sprintf( T_('Templates with user-id [%s]'), $handle);
         $menu[$text] = "templates.php?to=".urlencode($handle);
      }
      else
         $menu[T_('Templates')] = "templates.php";
   }//add_menu_link

   public static function get_template_type_text( $type )
   {
      if ( $type == PROFTYPE_TMPL_SENDMSG )
         return T_('Message#tmpl');
      elseif ( $type == PROFTYPE_TMPL_INVITE )
         return T_('Invite#tmpl');
      elseif ( $type == PROFTYPE_TMPL_NEWGAME )
         return T_('New Game#tmpl');
      else
         error('invalid_args', "ProfileTemplate.get_template_type_text($type)");
   }//get_template_type_text

   public static function new_default_profile( $uid, $type )
   {
      if ( !self::is_valid_type($type) )
         error('invalid_args', "ProfileTemplate.new_default_profile.check.type($type)");

      return new Profile( 0, $uid, $type, 1, true );
   }

   public static function is_valid_type( $type )
   {
      return ( $type == PROFTYPE_TMPL_SENDMSG
            || $type == PROFTYPE_TMPL_INVITE
            || $type == PROFTYPE_TMPL_NEWGAME );
   }

   public static function known_template_types()
   {
      static $TYPES = array( PROFTYPE_TMPL_SENDMSG, PROFTYPE_TMPL_INVITE, PROFTYPE_TMPL_NEWGAME );
      return $TYPES;
   }

} // end 'ProfileTemplate'



/*!
 * \class GameRematch
 *
 * \brief Class to handle invite-rematch and new-game from existing game/game-setup.
 */
class GameRematch
{

   // ------------ static functions ----------------------------

   public static function add_rematch_links( array &$arr_menu, $gid, $game_status, $game_type, $tid )
   {
      global $base_path;

      $allow_newgame = $allow_invite = false;
      if ( $game_type != GAMETYPE_GO ) // MPG
         $allow_newgame = true;
      elseif ( $tid > 0 ) // tournament
         $allow_invite = $allow_newgame = true;
      elseif ( $game_status != GAME_STATUS_INVITED ) // normal-game
         $allow_invite = $allow_newgame = true;

      if ( $allow_invite )
         $arr_menu[T_('Rematch')] = $base_path."game_rematch.php?mode=" . REMATCH_INVITE .URI_AMP."gid=$gid";
      if ( $allow_newgame )
         $arr_menu[T_('Copy as new game#rematch')] = $base_path."game_rematch.php?mode=" . REMATCH_NEWGAME .URI_AMP."gid=$gid";
   }//add_rematch_links

} //end 'GameRematch'




define('GSMODE_TERRITORY_SCORING', 'TERRITORY_SCORING');
define('GSMODE_AREA_SCORING', 'AREA_SCORING');

define('GSCOL_BLACK', 0);
define('GSCOL_WHITE', 1);

 /*!
  * \class GameScore
  *
  * \brief Container class to help in calculating game-score.
  */
class GameScore
{
   /*! \brief RULESET_JAPANESE, RULESET_CHINESE. */
   private $ruleset;
   /*! \brief Number of handicap stones. */
   private $handicap;
   /*! \brief used komi [float] */
   private $komi;
   /*! \brief Prisoners of BLACK and WHITE stored in array[GSCOL_BLACK|WHITE] */
   private $prisoners = array( 0, 0 ); // [ B, W ]
   /*! \brief Stone-count on board of BLACK and WHITE. */
   private $stones = array( 0, 0 ); // [ B, W ]
   /*! \brief "dead" stones on board of BLACK and WHITE. */
   private $dead_stones = array( 0, 0 ); // [ B, W ]
   /*! \brief Territory of BLACK and WHITE. */
   private $territory = array( 0, 0 ); // [ B, W ]
   /*! \brief dame-count */
   private $dame = 0;

   /*! \brief calculated scoring-information (may be independently set from $this->ruleset). */
   private $scoring_info = null;
   /*! \brief calculated score (may be independently set from $this->ruleset). */
   private $score = null;
   /*! \brief BoardStatus-object (if set), used for scoring with quick-suite. */
   private $board_status = null;


   /*!
    * \brief Constructs GameScore-object.
    * \param $ruleset RULESET_..
    */
   public function __construct( $ruleset, $handicap, $komi )
   {
      self::check_ruleset( $ruleset, 'constructor' );
      $this->ruleset = $ruleset;
      $this->handicap = $handicap;
      $this->komi = $komi;
   }

   /*! \brief Returns prisoners for given color GSCOL_WHITE|BLACK. */
   public function get_prisoners( $gscol )
   {
      self::check_gscol( $gscol, 'get_prisoners' );
      return $this->prisoners[$gscol];
   }

   /*! \brief Sets prisoners for given color GSCOL_WHITE|BLACK. */
   public function set_prisoners( $gscol, $count )
   {
      self::check_gscol( $gscol, 'set_prisoners' );
      $this->prisoners[$gscol] = $count;
   }

   /*! \brief Sets prisoners for black and white color. */
   public function set_prisoners_all( $black_count, $white_count )
   {
      $this->prisoners[GSCOL_BLACK] = $black_count;
      $this->prisoners[GSCOL_WHITE] = $white_count;
   }

   /*! \brief Returns number of stones on the board of given color GSCOL_WHITE|BLACK. */
   public function get_stones( $gscol )
   {
      self::check_gscol( $gscol, 'get_stones' );
      return $this->stones[$gscol];
   }

   /*! \brief Sets number of stones on the board of given color GSCOL_WHITE|BLACK. */
   public function set_stones( $gscol, $count )
   {
      self::check_gscol( $gscol, 'set_stones' );
      $this->stones[$gscol] = $count;
   }

   /*! \brief Sets number of stones on the board for black and white color. */
   public function set_stones_all( $black_count, $white_count )
   {
      $this->stones[GSCOL_BLACK] = $black_count;
      $this->stones[GSCOL_WHITE] = $white_count;
   }

   /*! \brief Returns number of captured stones on the board of given color GSCOL_WHITE|BLACK. */
   public function get_dead_stones( $gscol )
   {
      self::check_gscol( $gscol, 'get_dead_stones' );
      return $this->dead_stones[$gscol];
   }

   /*! \brief Sets number of captured stones on the board of given color GSCOL_WHITE|BLACK. */
   public function set_dead_stones( $gscol, $count )
   {
      self::check_gscol( $gscol, 'set_dead_stones' );
      $this->dead_stones[$gscol] = $count;
   }

   /*! \brief Sets number of captured stones on the board for black and white color. */
   public function set_dead_stones_all( $black_count, $white_count )
   {
      $this->dead_stones[GSCOL_BLACK] = $black_count;
      $this->dead_stones[GSCOL_WHITE] = $white_count;
   }

   /*! \brief Returns number of territory points on the board of given color GSCOL_WHITE|BLACK. */
   public function get_territory( $gscol )
   {
      self::check_gscol( $gscol, 'get_territory' );
      return $this->territory[$gscol];
   }

   /*! \brief Sets number of territory points on the board of given color GSCOL_WHITE|BLACK. */
   public function set_territory( $gscol, $count )
   {
      self::check_gscol( $gscol, 'set_territory' );
      $this->territory[$gscol] = $count;
   }

   /*! \brief Sets number of territory points on the board for black and white color. */
   public function set_territory_all( $black_count, $white_count )
   {
      $this->territory[GSCOL_BLACK] = $black_count;
      $this->territory[GSCOL_WHITE] = $white_count;
   }

   /*! \brief Sets number of neutral (=dame) points. */
   public function set_dame( $count )
   {
      $this->dame = $count;
   }

   /*!
    * \brief Returns map with more detailed information about scoring in textual form,
    * filled in calculate_score-func.
    */
   public function get_scoring_info()
   {
      return $this->scoring_info;
   }

   public function set_board_status( BoardStatus $bs )
   {
      $this->board_status = $bs;
   }

   public function get_board_status()
   {
      return $this->board_status;
   }


   /*!
    * \brief Calculating score for given ruleset (which defines if territory or area scoring is used).
    * \param $ruleset null (use ruleset from constructing this object) or specified RULESET_...
    * \param $fill_scoring_info fill scoring-info map if not-false, 'sgf' for sgf-text scoring-info
    * \return overall score (=score-white - score-black), also set in $this->score
    * NOTE: for format of scoring-info map see code and game.php#draw_score_box()
    */
   public function calculate_score( $ruleset=null, $fill_scoring_info=true )
   {
      // check args
      if ( is_null($ruleset) )
         $ruleset = $this->ruleset;
      else
         self::check_ruleset( $ruleset, 'calculate_score' );
      $gs_mode = Ruleset::getRulesetScoring( $ruleset );

      $handi_compensation = Ruleset::getRulesetHandicapCompensation( $ruleset );
      $handi_diff = ( is_numeric($handi_compensation) && $this->handicap > 0 )
         ? max(0, $this->handicap + $handi_compensation)
         : 0;

      // calculate score
      if ( $gs_mode == GSMODE_TERRITORY_SCORING )
      {
         $score_black = $this->territory[GSCOL_BLACK]
                        + $this->prisoners[GSCOL_BLACK]
                        + 2 * $this->dead_stones[GSCOL_WHITE];
         $score_white = $this->territory[GSCOL_WHITE]
                        + $this->prisoners[GSCOL_WHITE]
                        + 2 * $this->dead_stones[GSCOL_BLACK]
                        + $this->komi;
      }
      elseif ( $gs_mode == GSMODE_AREA_SCORING )
      {
         $score_black = $this->stones[GSCOL_BLACK]
                        + $this->dead_stones[GSCOL_WHITE]
                        + $this->territory[GSCOL_BLACK]
                        - $handi_diff;

         $score_white = $this->stones[GSCOL_WHITE]
                        + $this->dead_stones[GSCOL_BLACK]
                        + $this->territory[GSCOL_WHITE]
                        + $this->komi;
      }
      $this->score = $score_white - $score_black;

      if ( $fill_scoring_info )
      {
         $fill_sgf = ( $fill_scoring_info == 'sgf' );

         $map = array(
            'ruleset' => $ruleset,
            'mode'  => $gs_mode,
            'mode_text' => self::getModeText($gs_mode),
            'dame'  => sprintf( '(%s)', $this->dame ),
            'score' => $this->score,
         );
         $isArea = ( $gs_mode == GSMODE_AREA_SCORING );

         $arr_sgf = array(); // keep texts in english
         if ( $fill_sgf && $isArea )
            $arr_sgf['Dame'] = ($this->dame == 1) ? '1 stone' : "{$this->dame} stones";

         $fmt_dead = ($isArea) ? '' : '2*';
         foreach ( array( GSCOL_BLACK, GSCOL_WHITE ) as $gscol )
         {
            $gscol_opp = ( $gscol == GSCOL_BLACK ) ? GSCOL_WHITE : GSCOL_BLACK;
            $arr = array(
               'stones'      => sprintf( ( $isArea ? '+%s' : '(%s)' ), $this->stones[$gscol] ),
               'dead_stones' => sprintf( "(%s)<br>+{$fmt_dead}%s", $this->dead_stones[$gscol], $this->dead_stones[$gscol_opp] ),
               'prisoners'   => sprintf( ( $isArea ? '(%s)' : '+%s' ), $this->prisoners[$gscol] ),
               'territory'   => sprintf( '+%s', $this->territory[$gscol] ),
            );
            $map[$gscol] = $arr;

            if ( $fill_sgf )
            {
               $sgf_text = sprintf("%d territories + {$fmt_dead}%d dead %s(%s)",
                  $this->territory[$gscol],
                  $this->dead_stones[$gscol_opp], ($this->dead_stones[$gscol_opp] == 1 ? 'stone' : 'stones'),
                     ($gscol_opp == GSCOL_BLACK ? 'B' : 'W') );
               $sgf_komi = ( $gscol == GSCOL_WHITE )
                  ? sprintf( ' %s %s komi', ($this->komi < 0.0 ? '-' : '+'), abs($this->komi) )
                  : '';
               if ( $isArea )
               {
                  $sgf_text .= sprintf(' + %d %s(%s)%s', $this->stones[$gscol],
                     ($this->stones[$gscol] == 1 ? 'stone' : 'stones'), ($gscol == GSCOL_BLACK ? 'B' : 'W'),
                     ( $gscol == GSCOL_WHITE
                        ? $sgf_komi
                        : sprintf( ' - %d handicap %s', $handi_diff, ($handi_diff == 1 ? 'stone' : 'stones') ) ) );
               }
               else
               {
                  $sgf_text .= sprintf(' + %d %s%s',
                     $this->prisoners[$gscol], ($this->prisoners[$gscol] == 1 ? 'prisoner' : 'prisoners'),
                     $sgf_komi );
               }
               $sgf_text .= sprintf(' = %s', ($gscol == GSCOL_BLACK ? $score_black : $score_white) );
               $arr_sgf[($gscol == GSCOL_WHITE ? 'White' : 'Black')] = $sgf_text;
            }//sgf
         }

         $map['skip_dame'] = $map['skip_stones'] = !$isArea;
         $map['skip_prisoners'] = $isArea;

         $map[GSCOL_BLACK]['extra'] = ( $isArea && $handi_diff > 0 )
            ? sprintf( '-%s %s', $handi_diff, T_('(H)#scoring') ) : '';
         if ( $this->komi != 0.0 )
         {
            $fmt_komi = ($this->komi < 0.0) ? '%s %s' : '+%s %s';
            $map[GSCOL_WHITE]['extra'] = sprintf( $fmt_komi, $this->komi, T_('(K)#scoring') );
         }
         else
            $map[GSCOL_WHITE]['extra'] = '';
         $map[GSCOL_BLACK]['score'] = $score_black;
         $map[GSCOL_WHITE]['score'] = $score_white;

         $map['sgf_texts'] = $arr_sgf;

         $this->scoring_info = $map;
      }

      return $this->score;
   }//calculate_score

   /*! \brief Recalculates score if given ruleset different from ruleset of this object. */
   public function recalculate_score( $ruleset, $fill_scoring_info=true )
   {
      if ( strcmp($this->ruleset, $ruleset) != 0 )
         $this->calculate_score($ruleset, $fill_scoring_info);
      return $this->score;
   }

   /*! \brief Returns string-representation of this object (for debugging purposes). */
   public function to_string()
   {
      return "GameScore:"
         . "  ruleset={$this->ruleset}"
         . ", handicap={$this->handicap}"
         . ", komi={$this->komi}"
         . ", stones=[B" . $this->stones[GSCOL_BLACK] . ",W" . $this->stones[GSCOL_WHITE] . "]"
         . ", dead_stones=[B" . $this->dead_stones[GSCOL_BLACK] . ",W" . $this->dead_stones[GSCOL_WHITE] . "]"
         . ", territory=[B" . $this->territory[GSCOL_BLACK] . ",W" . $this->territory[GSCOL_WHITE] . "]"
         . ", prisoners=[B" . $this->prisoners[GSCOL_BLACK] . ",W" . $this->prisoners[GSCOL_WHITE] . "]"
         . ", dame={$this->dame}"
         . ", score=[{$this->score}]"
         . ", scoring_info=[ " . print_r($this->scoring_info, false) . " ]"
         . ", board_status=[ " . print_r($this->board_status, false) . " ]"
         ;
   }//to_string


   // ---------- static funcs ----------------------

   private static function check_ruleset( $ruleset, $method )
   {
      if ( !preg_match( "/^(".ALLOWED_RULESETS.")$/", $ruleset) )
         error('invalid_args', "GameScore:$method($ruleset)");
   }

   private static function check_gscol( $gscol, $method )
   {
      if ( $gscol != GSCOL_BLACK && $gscol != GSCOL_WHITE )
         error('invalid_args', "GameScore:$method($gscol)");
   }

   /*! \brief [GUI] Draws table of given GameScore and ruleset using echo(). */
   public static function draw_score_box( GameScore $game_score, $game_flags, $ruleset )
   {
      $game_score->recalculate_score($ruleset); // recalc if needed
      $score_info = $game_score->get_scoring_info();

      $fmtline3 = "<tr><td class=\"%s\">%s</td><td>%s</td><td>%s</td></tr>\n";
      $fmtline2 = "<tr><td class=\"%s\">%s</td><td colspan=\"2\">%s</td></tr>\n";

      global $base_path;
      $caption = T_('Scoring information#scoring');
      $caption2 = sprintf( T_('%s ruleset'), Ruleset::getRulesetText( $score_info['ruleset'] ) );
      $caption3 = $score_info['mode_text'];
      echo "<table id=\"scoreInfo\" class=\"Scoring NoPrint\">\n",
         "<tr><th colspan=\"3\">$caption<br>$caption2<br>($caption3)</th></tr>\n";
      if ( !$score_info['skip_dame'] )
         echo sprintf( $fmtline2, 'Header', T_('Dame#scoring'), $score_info['dame'] );
      echo sprintf( "<tr class=\"Header\"><td></td><td>%s</td><td>%s</td></tr>\n",
                  image( "{$base_path}17/b.gif", T_('Black'), null ),
                  image( "{$base_path}17/w.gif", T_('White'), null ) ),
         sprintf( $fmtline3, 'Header', T_('Territory#scoring'),
                  $score_info[GSCOL_BLACK]['territory'],
                  $score_info[GSCOL_WHITE]['territory'] ),
         sprintf( $fmtline3, 'Header', T_('Dead stones#scoring'),
                  $score_info[GSCOL_BLACK]['dead_stones'],
                  $score_info[GSCOL_WHITE]['dead_stones'] );
      if ( !$score_info['skip_stones'] )
         echo sprintf( $fmtline3, 'Header', T_('Stones#scoring'),
                  $score_info[GSCOL_BLACK]['stones'],
                  $score_info[GSCOL_WHITE]['stones'] );
      if ( !$score_info['skip_prisoners'] )
         echo sprintf( $fmtline3, 'Header', T_('Prisoners#scoring'),
                  $score_info[GSCOL_BLACK]['prisoners'],
                  $score_info[GSCOL_WHITE]['prisoners'] );
      echo sprintf( $fmtline3, 'Header', T_('Extra#scoring'),
                  $score_info[GSCOL_BLACK]['extra'],
                  $score_info[GSCOL_WHITE]['extra'] ),
         sprintf( $fmtline3, 'HeaderSum', T_('Score#scoring'),
                  $score_info[GSCOL_BLACK]['score'],
                  $score_info[GSCOL_WHITE]['score'] ),
         sprintf( $fmtline2, 'HeaderSum', T_('Result#scoring'),
                  score2text($score_info['score'], $game_flags, /*verbose*/false) ),
         "</table>\n";
   }//draw_score_box

   /*! \brief Returns mode-text or all mode-texts (if arg=null). */
   public static function getModeText( $mode=null )
   {
      static $ARR_GSMODES = null; // gsmode => text

      // lazy-init of texts
      if ( is_null($ARR_GSMODES) )
      {
         $arr = array();
         $arr[GSMODE_TERRITORY_SCORING] = T_('Territory scoring#scoring');
         $arr[GSMODE_AREA_SCORING]      = T_('Area scoring#scoring');
         $ARR_GSMODES = $arr;
      }

      if ( is_null($mode) )
         return $ARR_GSMODES;
      if ( !isset($ARR_GSMODES[$mode]) )
         error('invalid_args', "GameScore:getModeText($mode)");
      return $ARR_GSMODES[$mode];
   }//getModeText

} //end 'GameScore'



 /*!
  * \class BoardStatus
  *
  * \brief Container class to store SGF-coordinates of black/white-territory/dead-stones/alive-stones, neutral & dame.
  */
class BoardStatus
{
   /* \brief Map storing list of board-coordinates for different mark-types: arr[marktype] = [ coord, ... ] */
   private $arr = array(); // keys: DAME, MARKED_DAME(=NEUTRAL), BLACK/WHITE, BLACK/WHITE_DEAD, BLACK/WHITE_TERRITORY

   public function __construct()
   {
      foreach ( array( DAME, MARKED_DAME, BLACK, WHITE, BLACK_DEAD, WHITE_DEAD, BLACK_TERRITORY, WHITE_TERRITORY ) as $key )
         $this->arr[$key] = array();
   }

   public function add_coord( $key, $coord )
   {
      if ( !isset($this->arr[$key]) )
         error('invalid_args', "BoardStatus.add_coord($key,$coord)");
      $this->arr[$key][] = $coord;
   }

   public function get_coords( $key )
   {
      if ( !isset($this->arr[$key]) )
         error('invalid_args', "BoardStatus.get_coords($key)");
      return implode( array_unique( $this->arr[$key] ), '' ); // SGF-format
   }

} //end 'BoardStatus'




 /*!
  * \class NextGameOrder
  *
  * \brief Static helper class to handle table next-game-order for status-games
  *        and GamesPriority-table.
  */

define('NGO_LASTMOVED_OLD_FIRST', 'LASTMOVED_OF');
define('NGO_LASTMOVED_NEW_FIRST', 'LASTMOVED_NF');
define('NGO_MOVES', 'MOVES');
define('NGO_PRIO', 'PRIO');
define('NGO_TIMELEFT', 'TIMELEFT');

class NextGameOrder
{
   // ---------- static funcs ----------------------

   /*! \brief Returns array with orders for status-games-list order-selection. */
   public static function get_next_game_orders_selection()
   {
      return array(
         1 => T_('Last moved (old first)#nextgame'),
         2 => T_('Last moved (new first)#nextgame'),
         3 => T_('Moves#nextgame'),
         4 => T_('Priority#nextgame'),
         5 => T_('Time remaining#nextgame'),
      );
   }

   /*! \brief Builds basic QuerySQL for retrieving status-games. */
   public static function build_status_games_query( $uid, $game_status_op, $next_game_order, $load_ticks=false,
         $load_tournament=false, $load_prio_field=false, $load_notes=false )
   {
      if ( stripos($game_status_op, GAME_STATUS_INVITED) !== false )
         error('invalid_args', "NextGameOrder:build_status_games_query.bad_status_op($uid,$game_status_op)");

      $checked_order = ( is_numeric($next_game_order) )
         ? NextGameOrder::get_next_game_order( $next_game_order ) // int -> enum
         : $next_game_order;
      $next_game_order = ( (string)$checked_order != '' ) // default on invalid order
         ? $checked_order
         : NGO_LASTMOVED_OLD_FIRST;

      $sql_order = NextGameOrder::get_next_game_order( $next_game_order, 'Games', /*orderby*/false ); // enum -> order
      $load_prio = ($next_game_order == NGO_PRIO) || $load_prio_field;

      $qsql = new QuerySQL(
            SQLP_FIELDS,
               'Games.*',
               'UNIX_TIMESTAMP(Games.Starttime) AS X_Starttime',
               'UNIX_TIMESTAMP(Games.Lastchanged) AS X_Lastchanged',
               "IF(Games.Rated='N','N','Y') AS X_Rated",
               "(Games.White_ID=$uid)+0 AS Color",
               //extra bits of X_Color are for sorting purposes
               //b0= White to play, b1= I am White, b4= not my turn, b5= bad or no ToMove info
               "IF(Games.ToMove_ID=$uid,0,0+0x10)+IF(Games.White_ID=$uid,2,0) + " .
                  "IF(Games.White_ID=Games.ToMove_ID,1,IF(Games.Black_ID=Games.ToMove_ID,0,0+0x20)) AS X_Color",
               'opp.ID AS opp_ID', 'opp.Name AS opp_Name', 'opp.Handle AS opp_Handle', 'opp.Rating2 AS opp_Rating',
               'opp.OnVacation AS opp_OnVacation', 'UNIX_TIMESTAMP(opp.Lastaccess) AS opp_Lastaccess',
            SQLP_FROM,
               'Games',
               "INNER JOIN Players AS opp ON opp.ID=(Games.Black_ID+Games.White_ID-$uid)",
            SQLP_WHERE,
               "Games.ToMove_ID=$uid",
               "Games.Status $game_status_op",
            SQLP_ORDER,
               $sql_order
         );

      if ( $load_ticks )
      {
         $qsql->add_part( SQLP_FIELDS,
            "COALESCE(Clock.Ticks,0) AS X_Ticks" ); //always my clock because always my turn (status page)
         $qsql->add_part( SQLP_FROM,
            "LEFT JOIN Clock ON Clock.ID=Games.ClockUsed" );
      }

      if ( ALLOW_TOURNAMENTS && $load_tournament )
      {
         $qsql->add_part( SQLP_FIELDS,
            'T.Title AS T_Title' );
         $qsql->add_part( SQLP_FROM,
            'LEFT JOIN Tournament AS T ON T.ID=Games.tid AND Games.tid>0' );
      }

      GameHelper::extend_query_with_game_prio( $qsql, $uid, $load_prio );

      if ( $load_notes )
         GameHelper::extend_query_with_game_notes( $qsql, $uid );

      return $qsql;
   }//build_status_games_query

   /*!
    * \brief Maps NextGameOrder for status-games.
    * \param $idx if numeric (map selection-index to enum-value (sql_order=false) or to order-string;
    *             if string (map enum-value to selection-index ot to order-string)
    * \param $tablename return SQL 'ORDER BY...' if tablename given;
    *                   return enum for numeric-index if empty
    *                   return quick-order for next-game-order if 'QUICK'
    * \return '' for unknown $idx; otherwise return enum-value or SQL-order-by-clause dependent on $tablename
    *
    * Examples:
    *   get_next_game_order( 3 ) -> NGO_PRIO
    *   get_next_game_order( 'MOVES' ) -> 2
    *   get_next_game_order( 'MOVES', 'G', true ) = get_..( 2, 'G', true ) -> 'ORDER BY G.Moves, ...'
    *   get_next_game_order( 'MOVES', 'Games' ) = 'G.Moves, ...'
    */
   public static function get_next_game_order( $idx, $tablename='', $with_order_by=true )
   {
      // SQL-ordering for Status-game list and "next game" on game-page (%G = Games-table)
      // NOTE: also adjust 'jump_to_next_game(..)' in confirm.php
      static $ARR_NEXT_GAME_ORDER = array(
         // idx => [ Players.NextGameOrder-value, order-string, quick-suite-order ]
         1 => array( NGO_LASTMOVED_OLD_FIRST, '%G.Lastchanged ASC, %G.ID DESC',           'time_lastmove+,id-' ),
         2 => array( NGO_LASTMOVED_NEW_FIRST, '%G.Lastchanged DESC, %G.ID DESC',          'time_lastmove-,id-' ),
         3 => array( NGO_MOVES,     '%G.Moves DESC, %G.Lastchanged ASC, %G.ID DESC',      'move_count-,time_lastmove+,id-' ),
         4 => array( NGO_PRIO,      'X_Priority DESC, %G.Lastchanged ASC, %G.ID DESC',    'prio-,time_lastmove+,id-' ),
         5 => array( NGO_TIMELEFT,  '%G.TimeOutDate ASC, %G.Lastchanged ASC, %G.ID DESC', 'TIMEOUTDATE+,time_lastmove+,id-' ),
         NGO_LASTMOVED_OLD_FIRST => 1,
         NGO_LASTMOVED_NEW_FIRST => 2,
         NGO_MOVES     => 3,
         NGO_PRIO      => 4,
         NGO_TIMELEFT  => 5,
      );

      if ( !isset($ARR_NEXT_GAME_ORDER[$idx]) )
         return '';

      $idx_value = $idx;
      if ( !is_numeric($idx) ) // map enum-val to selection-index or order-string
      {
         $idx_value = $ARR_NEXT_GAME_ORDER[$idx];
         if ( !$tablename )
            return $idx_value;
      }

      if ( $tablename === 'QUICK' )
         return $ARR_NEXT_GAME_ORDER[$idx_value][2];
      elseif ( $tablename )
      {
         $order_fmt = $ARR_NEXT_GAME_ORDER[$idx_value][1];
         return ( $with_order_by ? 'ORDER BY ' : '' ) . str_replace( '%G', $tablename, $order_fmt );
      }
      else
         return $ARR_NEXT_GAME_ORDER[$idx_value][0];
   }//get_next_game_order

   /*! \brief Returns loaded Players.NextGameOrder for given user; or null otherwise. */
   public static function load_user_next_game_order( $uid )
   {
      $uid = (int) $uid;
      $user_row = mysql_single_col( "NextGameOrder:load_user_next_game_order.find($uid)",
         "SELECT NextGameOrder FROM Players WHERE ID=$uid LIMIT 1" );
      return ( $user_row ) ? $user_row[0] : null;
   }//load_user_next_game_order

   /*!
    * \brief Loads priority from GamesPriority-table for given game and user.
    * \return loaded integer-prio or else $defval
    */
   public static function load_game_priority( $gid, $uid, $defval=0 )
   {
      $prio_row = mysql_single_col( "NextGameOrder:load_game_priority.find($gid,$uid)",
         "SELECT Priority FROM GamesPriority WHERE gid=$gid AND uid=$uid LIMIT 1" );
      $prio = ( $prio_row ) ? $prio_row[0] : $defval;
      return $prio;
   }

   /*!
    * \brief Saves or deletes GamesPriority-table-entry for given arguments.
    * \param $prio '' to delete entry; integer-value to save it;
    *              non-integer will lead to an error
    */
   public static function persist_game_priority( $gid, $uid, $prio )
   {
      if ( (string)$prio == '' )
      {
         db_query( "NextGameOrder:persist_game_priority.delete($gid,$uid)",
            "DELETE FROM GamesPriority WHERE gid=$gid AND uid=$uid LIMIT 1" );
      }
      else
      {
         if ( !is_numeric($prio) )
            error('invalid_args', "NextGameOrder:persist_game_priority.check.prio.no_int($gid,$uid,$new_prio)");
         $new_prio = (int)$prio;
         if ( $new_prio < -32768 || $new_prio > 32767 )
            error('invalid_args', "NextGameOrder:persist_game_priority.check.prio.range($gid,$uid,$new_prio)");

         db_query( "NextGameOrder:persist_game_priority.update($gid,$uid,$new_prio)",
            "INSERT INTO GamesPriority (gid,uid,Priority) VALUES ($gid,$uid,$new_prio) " .
            "ON DUPLICATE KEY UPDATE Priority=VALUES(Priority)" );
      }

      // clear caches
      clear_cache_quick_status( $uid, QST_CACHE_GAMES );
      GameHelper::delete_cache_status_games( "NextGameOrder:persist_game_priority($gid,$uid)", $uid );
   }//persist_game_priority

   /*! \brief Deletes all GamesPriority-table-entries for given game-id. */
   public static function delete_game_priorities( $gid )
   {
      db_query( "NextGameOrder:delete_game_priorities.delete_all($gid)",
         "DELETE FROM GamesPriority WHERE gid=$gid" ); // no 'LIMIT' because of multi-player-go
   }

   /*!
    * \brief Calculates timeout-date (in ticks) to be stored in Games.TimeOutDate.
    * \note used for status-games-ordering on remaining-time
    * \note ignores weekends as explained in the specs
    *
    * \param $grow Games-table-row to read time-settings for game, expected fields:
    *              Maintime, Byotype, Byotime, Byoperiods,
    *              (White|Black)_(Maintime|Byotime|Byoperiods),
    *              (White|Black)_OnVacation
    * \param $game_clock_used if >=0 then $game_ticks are ticks relative to this clock,
    *        otherwise $game_ticks are the elapsed-ticks
    * \param $game_ticks absolute or relative game-ticks (see $game_clock_used); if <=0 then no game-clock-check done
    * \param $to_move BLACK | WHITE
    * \param $is_new_game if true get time-settings from $grow without prefix
    * \return "absolute" date (in ticks) aligned to Clock[ID=CLOCK_TIMELEFT]
    *
    * \see for full specs and pitfalls, see 'TimeOutDate/ClockUsed/LastTicks'-field in 'specs/db/table-Games.txt'
    */
   public static function make_timeout_date( array $grow, $to_move, $game_clock_used, $game_ticks, $is_new_game=false )
   {
      // determine time-stuff for time-left-calculus
      $pfx = ($to_move == BLACK) ? 'Black' : 'White';
      if ( $is_new_game )
      {
         $tl_Maintime   = $grow['Maintime'];
         $tl_Byotime    = $grow['Byotime'];
         $tl_Byoperiods = $grow['Byoperiods'];
      }
      else
      {
         $tl_Maintime   = $grow["{$pfx}_Maintime"];
         $tl_Byotime    = $grow["{$pfx}_Byotime"];
         $tl_Byoperiods = $grow["{$pfx}_Byoperiods"];
      }

      // calculate elapsed ticks
      if ( $game_ticks <= 0 || $game_clock_used < 0 ) // relative ticks (for new-game or if on-vacation)
         $elapsed_ticks = abs($game_ticks);
      else // absolute ticks (of game-clock), no vacation clock
      {
         $game_clock_ticks = get_clock_ticks( 'NGO.make_timeout_date', $game_clock_used );
         $elapsed_ticks = $game_clock_ticks - $game_ticks;
      }

      $elapsed_hours = ticks_to_hours( $elapsed_ticks );
      time_remaining($elapsed_hours, $tl_Maintime, $tl_Byotime, $tl_Byoperiods,
         $grow['Maintime'], $grow['Byotype'], $grow['Byotime'], $grow['Byoperiods'], false);

      $hours_left = time_remaining_value( $grow['Byotype'], $grow['Byotime'], $grow['Byoperiods'],
            $tl_Maintime, $tl_Byotime, $tl_Byoperiods );

      $tl_vac_days = (float)@$grow["{$pfx}_OnVacation"];
      if ( $tl_vac_days > 0 ) // handle remaining vacation
         $hours_left += round( HOURS_PER_DAY * $tl_vac_days );

      $timeout_date = time_left_ticksdate( $hours_left );

      return $timeout_date;
   }//make_timeout_date

} //end 'NextGameOrder'



 /*!
  * \class GameSnapshot
  *
  * \brief Helper-class to build and parse Games.Snapshot for thumbnails and Shape-games.
  */
class GameSnapshot
{
   private static $BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";


   /*! \brief Returns empty board used to init started game. */
   public static function init_game_snapshot()
   {
      return 'A';
   }

   /*!
    * \brief Returns base64-encoded snapshot of game-positions with B/W- and dead-stones (without color).
    * \param $stone_reader interface with method $stone_reader->read_stone_value(x,y,$with_dead) with x/y=0..size-1
    *        returning 1 for black-stone, 2 for white-stone and 3 for dead-stone (black|white), 0 for empty-intersection
    * \param $with_dead true to include dead-stones, false to omit them (used for shape-game)
    * \return game-snapshot
    * \note one char in output = 3 positions of 2-bit-values (00=empty, 01=B, 10=W, 11=dead)
    */
   public static function make_game_snapshot( $size, StoneReader $stone_reader, $with_dead = true )
   {
      $out = '';
      $enc_val = $enc_cnt = 0;
      for ( $y = 0; $y < $size; $y++ )
      {
         for ( $x = 0; $x < $size; $x++ )
         {
            $stone_val = $stone_reader->read_stone_value( $x, $y, $with_dead );
            $enc_val = ($enc_val << 2) + $stone_val;
            if ( ++$enc_cnt == 3 )
            {
               $out .= self::$BASE64[$enc_val];
               $enc_cnt = $enc_val = 0;
            }
         }
      }

      if ( $enc_cnt > 0 )
      {
         $enc_val <<= (2 * (3 - $enc_cnt));
         $out .= self::$BASE64[$enc_val];
      }

      $out = rtrim($out, 'A');
      if ( (string)$out != '' )
      {
         $out = preg_replace( array(
               "/AAAAAAAAAAAAAAAA/", // *=16xA
               "/AAAAAAAA/", // @=8xA
               "/AAAA/", // #=4xA
               "/AAA/", // %=3xA
               "/AA/", // :=2xA
            ),
            array(
               "*", "@", "#",  "%", ":",
            ), $out );
      }
      else
         $out = 'A';

      return $out;
   }//make_game_snapshot

   /*! \brief Returns array with black/white stones and coordinates: [ $black/$white, x,y ] x/y=0..n */
   public static function parse_stones_snapshot( $size, $snapshot, $black, $white )
   {
      static $SKIPPOS_MAP = array(
            'A' =>  1, // 1xA
            ':' =>  2, // 2xA
            '%' =>  3, // 3xA
            '#' =>  4, // 4xA
            '@' =>  8, // 8xA
            '*' => 16, // 16xA
         );

      // 00=empty, 01=Black, 10=White, 11=Dead-stone
      $out = array();
      $psize = $size * $size;
      $snapshot_len = strlen($snapshot);
      for ( $i=0, $p=0; $p < $psize && $i < $snapshot_len; $i++ )
      {
         $ch = $snapshot[$i];
         if ( $ch == ' ' ) // stop on space (extended syntax)
            break;
         $skip_pos = @$SKIPPOS_MAP[$ch];
         if ( $skip_pos )
            $p += 3 * $skip_pos; // skip empties
         else
         {
            $data = strpos(self::$BASE64, $ch);
            if ( $data === false )
               error('invalid_snapshot_char', "GameSnapshot.parse_stones_snapshot($size,$ch,$p,$snapshot)");
            foreach ( array( ($data >> 4) & 0x3, ($data >> 2) & 0x3, $data & 0x3 ) as $val )
            {
               if ( $val == 1 || $val == 2 ) // 1=Black, 2=White, 3=Dead B|W
                  $out[] = array( ($val == 1 ? $black : $white), $p % $size, (int)($p / $size) );
               $p++;
            }
         }
      }

      return $out;
   }//parse_stones_snapshot

   /*!
    * \brief Returns parsed map from extended snapshot.
    * \note Extended Format: "AAAA S19 (W|B|'')"
    * \return [ Snapshot => snapshot-only, Size => size, PlayColorB => 1(def)|0, Error => 'unknown stuff' ]
    *         Error-key not-empty if unknown item found in format
    */
   public static function parse_extended_snapshot( $ext_snapshot )
   {
      $arr = explode(' ', $ext_snapshot);
      if ( count($arr) )
      {
         $out = array( //defaults
               'Snapshot'     => array_shift($arr),
               'Size'         => 0, // unset
               'PlayColorB'   => 1,
            );
         $err = '';
         foreach ( $arr as $item )
         {
            if ( preg_match("/^S(\d+)$/", $item, $matches) )
               $out['Size'] = (int)@$matches[1];
            elseif ( preg_match("/^([BW])$/", $item, $matches) )
               $out['PlayColorB'] = (@$matches[1] == 'W') ? 0 : 1;
            else
               $err .= " [$err]";
         }
         if ( strlen($err) )
            $out['Error'] = substr($err, 1);
      }
      else
         $out = null;
      return $out;
   }//parse_extended_snapshot

   // Extended Format: "AAAA S19 (W|B|'')"
   public static function build_extended_snapshot( $snapshot, $size, $flags=0 )
   {
      $out = "$snapshot S$size";
      if ( $flags & SHAPE_FLAG_PLAYCOLOR_W )
         $out .= " W";
      return $out;
   }//build_extended_snapshot

   /*! \brief Checks non-extended snapshot for valid chars, returns bad chars (or EMPTY if empty) or '' if valid. */
   public static function check_snapshot( $snapshot )
   {
      if ( (string)$snapshot == '' )
         return T_('EMPTY#shape');
      else
         return preg_replace("/[A-Za-z0-9\\+\\/\\*@#%:]/", "", $snapshot);
   }

   /*!
    * \brief Checks for illegal stone-positions (like suicide).
    * \return empty if no errors found; otherwise comma-separated coordinates of illegal positions
    */
   public static function check_illegal_positions( $size, $snapshot, $suicide_allowed )
   {
      $board = new Board( 0, $size );
      $board->build_from_shape_snapshot( $size, $snapshot );
      $board->visited_points = array(); // [x][y] = 1 if visited already

      $arr_xy = self::parse_stones_snapshot( $size, $snapshot, BLACK, WHITE );
      $arr_cnt = array( BLACK => 0, WHITE => 0 );
      $errpos = array();
      $prisoners = array();
      foreach ( $arr_xy as $point )
      {
         list( $stone, $x, $y ) = $point;
         $arr_cnt[$stone]++;

         // check for illegal positions
         if ( !$suicide_allowed && !@$board->visited_points[$x][$y] )
         {
            if ( !$board->has_liberty_check($x, $y, $prisoners, /*rm*/false) )
               $errpos[] = number2board_coords($x, $y, $size);
         }
      }

      if ( $arr_cnt[BLACK] + $arr_cnt[WHITE] == 0 )
         return T_('EMPTY-BOARD#shape');
      if ( $arr_cnt[BLACK] == $size*$size || $arr_cnt[WHITE] == $size*$size )
         return T_('FULL-BOARD#shape');

      return implode(',', $errpos);
   }//check_illegal_positions

   /*!
    * \brief Parses and strictly checks extended-snapshot.
    * \return output-arr from parse_extended_snapshot()-func, null on error
    */
   public static function parse_check_extended_snapshot( $ext_snapshot )
   {
      $arr = self::parse_extended_snapshot( $ext_snapshot );
      if ( is_null($arr) )
         return null;
      if ( $arr['Size'] < MIN_BOARD_SIZE || $arr['Size'] > MAX_BOARD_SIZE )
         return null;
      if ( strlen(@$arr['Error']) > 0 )
         return null;
      if ( self::check_snapshot($arr['Snapshot']) )
         return null;
      return $arr;
   }//parse_check_extended_snapshot

} //end 'GameSnapshot'





 /*!
  * \class DefaultMaxHandicap
  *
  * \brief Helper-class to check and calculate default max-handicap for game-settings
  *        of new-game, invitations and tournament-rules.
  */
class DefaultMaxHandicap
{

   /*!
    * \brief Calculate default max-handicap for given board-size.
    *
    * \note some tested formulas from DGS-forums: http://www.dragongoserver.net/forum/read.php?forum=5&thread=34914#35577
    * last formula chosen as it's quadratic on board-size and results in acceptable and not too small default-handicaps.
    *   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  Size n
    *  -----------------------------------------------------------------------------------+-------------------------------
    *   2   3   3   4   4   5   5   6   6   7   7   8   8   9   9  10  10  11  11  12  12  floor(n/2)
    *   0   1   1   1   2   2   3   3   4   5   5   6   7   8   9  10  11  12  13  15  16  round(((n-1)/6)^2)
    *   0   0   1   1   2   3   3   4   5   5   6   7   7   8   9   9  10  11  11  12  13  floor((2n-11)/3)
    *   0   0   1   1   1   2   2   3   4   4   5   6   7   8   9  10  11  13  14  16  17  round(9 * (n-3)^2 / 256)
    *   2   2   2   2   3   3   3   4   4   5   6   6   7   8   9  10  11  12  13  14  16  round(7.5 * ((n-3)^2 / 256 + 0.2))
    */
   public static function calc_def_max_handicap( $board_size )
   {
      return round( 7.5 * ( handicapfactor($board_size) + 0.2 ) );
   }

   /*! \brief Limits passed max-handicap to min/max absolute limits for max-handicap. */
   public static function limit_max_handicap( $max_handicap )
   {
      return min( MAX_HANDICAP, max( DEFAULT_MAX_HANDICAP, (int)$max_handicap ));
   }

   /*!
    * \brief Returns checked and limited min- and max-handicap in regard to default-max-handicap.
    * \return arr( $min_h, $max_h )
    */
   public static function limit_min_max_with_def_handicap( $board_size, $min_handicap, $orig_max_handicap )
   {
      $max_handicap = self::limit_max_handicap( $orig_max_handicap );
      $def_max_handicap = self::calc_def_max_handicap( $board_size );

      if ( $max_handicap == DEFAULT_MAX_HANDICAP && $min_handicap > $def_max_handicap )
         $min_handicap = $def_max_handicap;
      elseif ( $max_handicap >= 0 && $min_handicap > $max_handicap )
         swap( $min_handicap, $max_handicap );

      return array( $min_handicap, $max_handicap );
   }//limit_min_max_with_def_handicap

   /*! \brief Returns limitation-text for min/max-handicap stones (used for tournament-rules). */
   public static function build_text_handicap_limits( $board_size, $min_handicap, $max_handicap )
   {
      $use_max_handi = ( $max_handicap == DEFAULT_MAX_HANDICAP )
         ? self::calc_def_max_handicap( $board_size )
         : min( MAX_HANDICAP, $max_handicap );

      if ( $min_handicap > 0 && $use_max_handi < MAX_HANDICAP )
         $lim_handi = sprintf( T_('limited by min. %s and max. %s stones'), $min_handicap, $use_max_handi );
      elseif ( $min_handicap > 0 )
         $lim_handi = sprintf( T_('limited by min. %s stones'), $min_handicap );
      elseif ( $use_max_handi < MAX_HANDICAP )
         $lim_handi = sprintf( T_('limited by max. %s stones'), $use_max_handi );
      else
         $lim_handi = '';

      if ( $lim_handi )
      {
         if ( $max_handicap == DEFAULT_MAX_HANDICAP )
            $lim_handi .= sprintf(' [%s]', T_('Default Max.#handi') );
      }

      return $lim_handi;
   }//build_text_handicap_limits

   public static function build_default_max_handicap_javascript_map()
   {
      $arr = array();
      for ( $size=MIN_BOARD_SIZE; $size <= MAX_BOARD_SIZE; ++$size )
         $arr[] = sprintf( "%s: %s", $size, self::calc_def_max_handicap($size));
      return '{ ' . implode(', ', $arr) . ' }';
   }

} //end 'DefaultMaxHandicap'




/*! \brief Determines who is to move (BLACK|WHITE), expects game-row with fields ID,Black_ID,White_ID,ToMove_ID. */
function get_to_move( array $grow, $errmsg )
{
   $to_move_id = $grow['ToMove_ID'];
   if ( $grow['Black_ID'] == $to_move_id )
      $to_move = BLACK;
   elseif ( $grow['White_ID'] == $to_move_id )
      $to_move = WHITE;
   elseif ( $to_move_id )
      error('database_corrupted', "$errmsg({$grow['ID']})");
   else
      $to_move = -1; // can happen on finished game
   return $to_move;
}

function is_htype_calculated( $htype, $cat_htype=false )
{
   if ( $cat_htype )
      return ( $htype == CAT_HTYPE_CONV || $htype == CAT_HTYPE_PROPER );
   else
      return ( $htype == HTYPE_CONV || $htype == HTYPE_PROPER );
}

function is_htype_divide_choose( $htype )
{
   return ( $htype == HTYPE_YOU_KOMI_I_COLOR || $htype == HTYPE_I_KOMI_YOU_COLOR  );
}

/*!
 * \brief Returns handicap-type category for handicap-type (Waitingroom.)Handicaptype.
 * \return CAT_HTYPE_... if valid handicap-type given; false otherwise
 */
function get_category_handicaptype( $handitype )
{
   // handicap-type => handicaptype-category
   static $ARR_HTYPES = array(
         HTYPE_CONV        => CAT_HTYPE_CONV,
         HTYPE_PROPER      => CAT_HTYPE_PROPER,
         HTYPE_NIGIRI      => CAT_HTYPE_MANUAL,
         HTYPE_DOUBLE      => CAT_HTYPE_MANUAL,
         HTYPE_BLACK       => CAT_HTYPE_MANUAL,
         HTYPE_WHITE       => CAT_HTYPE_MANUAL,
         HTYPE_ALTERNATE   => CAT_HTYPE_MANUAL,
         HTYPE_AUCTION_SECRET   => CAT_HTYPE_FAIR_KOMI,
         HTYPE_AUCTION_OPEN     => CAT_HTYPE_FAIR_KOMI,
         HTYPE_YOU_KOMI_I_COLOR => CAT_HTYPE_FAIR_KOMI,
         HTYPE_I_KOMI_YOU_COLOR => CAT_HTYPE_FAIR_KOMI,
      );
   return @$ARR_HTYPES[$handitype];
}

function build_image_double_game( $with_sep=false, $class='' )
{
   global $base_path;
   return image( $base_path.'17/w.gif', T_('Double game (White)'), null, $class)
          . ( $with_sep ? '&nbsp;+&nbsp;' : '' )
          . image( $base_path.'17/b.gif', T_('Double game (Black)'), null, $class);
}

function build_game_settings_javascript()
{
   if ( is_javascript_enabled() )
   {
      $js = add_js_var('ARR_RULESET_DEF_KOMI', Ruleset::build_ruleset_default_komi_javascript_map(), /*raw*/true );
      $js .= add_js_var('ARR_DEF_MAX_HANDICAP', DefaultMaxHandicap::build_default_max_handicap_javascript_map(), /*raw*/true );
      $js .= add_js_var('T_defaultMaxHandicap', T_('(Def. is %s for size %s)#defmaxhandi') );
   }
   else
      $js = null;
   return $js;
}

function get_gamesettings_viewmode( $viewmode )
{
   static $ARR = null;
   if ( is_null($ARR) )
   {
      $ARR = array(
         GSETVIEW_STANDARD => T_('standard settings#gsview'),
         GSETVIEW_FAIRKOMI => T_('fair-komi settings#gsview'),
         GSETVIEW_MPGAME => T_('multi-player settings#gsview'),
      );
   }
   return @$ARR[$viewmode];
}

// param $adjust true = adjust by +-50, use false when saving template to keep orig-values
// return arr( $must_be_rated, $rating1, $rating2 ) ready for db-insert
// note: multi-player-game requires rated game-players (RatingStatus != NONE),
//       see also append_form_add_waiting_room_game()-func
function parse_waiting_room_rating_range( $is_multi_player_game=false, $adjust=true, $arg_rating1=null, $arg_rating2=null )
{
   if ( !$is_multi_player_game && get_request_arg('must_be_rated') != 'Y' )
   {
      $MustBeRated = 'N';
      //to keep a good column sorting:
      $rating1 = $rating2 = OUT_OF_RATING;
   }
   else
   {
      $MustBeRated = 'Y';
      $rating1 = ( is_null($arg_rating1) ) ? get_request_arg('rating1') : $arg_rating1;
      $rating2 = ( is_null($arg_rating2) ) ? get_request_arg('rating2') : $arg_rating2;
      $rating1 = read_rating( $rating1 );
      $rating2 = read_rating( $rating2 );

      if ( $rating1 == NO_RATING || $rating2 == NO_RATING )
         error('rank_not_rating', "parse_waiting_room_rating_range.check($rating1,$rating2)");

      if ( $rating2 < $rating1 )
         swap( $rating1, $rating2 );

      if ( $adjust )
      {
         $rating1 -= 50;
         $rating2 += 50;
      }
   }

   return array( $MustBeRated, $rating1, $rating2 );
}//parse_waiting_room_rating_range

// add form-elements required for new-game for waiting-room
// INPUT: must_be_rated, rating1, rating2, min_rated_games, comment
function append_form_add_waiting_room_game( Form &$mform, $viewmode, $read_args = false, $gsc = null )
{
   // note: multi-player-game requires rated game-players (RatingStatus != NONE)
   $rating_array = getRatingArray();
   if ( is_null($gsc) )
      $gsc = new GameSetupChecker( $viewmode );

   $rating_min = '30 kyu';
   $rating_max = '9 dan';
   if ( $read_args ) // read init-vals from URL (for rematch / profile-template)
   {
      $must_be_rated = ( @$_REQUEST['mb_rated'] == 'Y' );
      if ( isset($_REQUEST['rat1']) )
      {
         $url_rat_min = limit( (int) $_REQUEST['rat1'], MIN_RATING, RATING_9DAN, MIN_RATING );
         $rating_min = echo_rating( $url_rat_min, /*%*/false, /*gfx-uid*/0, /*engl*/true, /*short*/false );
      }
      if ( isset($_REQUEST['rat2']) )
      {
         $url_rat_max = limit( (int) $_REQUEST['rat2'], MIN_RATING, RATING_9DAN, RATING_9DAN );
         $rating_max = echo_rating( $url_rat_max, /*%*/false, /*gfx-uid*/0, /*engl*/true, /*short*/false );
      }
      $min_rated_games = (int) @$_REQUEST['min_rg'];
      $min_hero_ratio = @$_REQUEST['min_hero_ratio'];
      $same_opponent = (int) @$_REQUEST['same_opp'];
      $comment = trim(@$_REQUEST['comment']);
   }
   else
   {
      $must_be_rated = false;
      $min_rated_games = $comment = '';
      $min_hero_ratio = '';
      $same_opponent = 0;
   }
   if ( $viewmode == GSETVIEW_MPGAME )
      $must_be_rated = $disable_mbrated = true;
   else
      $disable_mbrated = false;

   $rat_attb_arr = ( is_javascript_enabled() ) ? array( 'onclick' => 'this.form.must_be_rated.checked=true;' ) : array();
   $mform->add_row( array( 'DESCRIPTION', T_('Require rated opponent'),
                           'CHECKBOXX', 'must_be_rated', 'Y', "", $must_be_rated, ($disable_mbrated ? 'disabled=1' : ''),
                           'TEXT', sptext(T_('If yes, rating between'),1),
                           'SELECTBOXX', 'rating1', 1, $rating_array, $rating_min, false, $rat_attb_arr,
                           'TEXT', sptext(T_('and')),
                           'SELECTBOXX', 'rating2', 1, $rating_array, $rating_max, false, $rat_attb_arr ) );

   $mform->add_row( array(
         'DESCRIPTION', T_('Min. rated finished games'),
         'TEXTINPUTX', 'min_rated_games', 5, 5, $min_rated_games, $gsc->get_class_error_field('min_rated_games'),
         'TEXT', MINI_SPACING . T_('(optional)'), ));

   $mform->add_row( array(
         'DESCRIPTION', T_('Min. hero percentage'),
         'TEXTINPUTX', 'min_hero_ratio', 3, 3, $min_hero_ratio, $gsc->get_class_error_field('min_hero_ratio'),
         'TEXT', '%' . MED_SPACING . T_('(optional)'), ));

   if ( $viewmode == GSETVIEW_STANDARD || $viewmode == GSETVIEW_FAIRKOMI )
   {
      $same_opp_array = build_accept_same_opponent_array(array( 0, SAMEOPP_ONLY_NEW,
            SAMEOPP_TOTAL_STARTED-1, SAMEOPP_TOTAL_STARTED-2, SAMEOPP_TOTAL_STARTED-3,  -1, -2, -3,  3, 7, 14 ));
      $mform->add_row( array( 'DESCRIPTION', T_('Accept same opponent'),
                              'SELECTBOX', 'same_opp', 1, $same_opp_array, $same_opponent, false, ));
   }

   $mform->add_row( array( 'SPACE' ) );
   $mform->add_row( array( 'DESCRIPTION', T_('Comment'),
                           'TEXTINPUT', 'comment', 40, 40, $comment ) );
}//append_form_add_waiting_room_game

function echo_started_games( $game_count )
{
   if ( $game_count <= 0 )
      return '';

   $fmt = ($game_count > 1)
      ? T_('%s games started#same_opp')
      : T_('%s game started#same_opp');
   return sprintf( $fmt, $game_count );
}

// WaitingRoom.SameOpponent: 0=always, -127=only-if-never-played-before, <-101=(-n-100) total times (run-games),
//                           <0=n times (same game-offer), >0=after n days
// \param $game_row expecting JoinedCount, X_GOPP_Running
function echo_accept_same_opponent( $same_opp, array $game_row = null )
{
   if ( $same_opp == 0 )
      return T_('always#same_opp');
   elseif ( $same_opp == SAMEOPP_ONLY_NEW )
      return T_('accept only if never played before#same_opp');

   if ( $same_opp < SAMEOPP_TOTAL_STARTED )
   {
      if ( $same_opp == SAMEOPP_TOTAL_STARTED-1 )
         $out = T_('1 total time (started games)#same_opp');
      else
         $out = sprintf( T_('%s total times (started games)#same_opp'), -$same_opp + SAMEOPP_TOTAL_STARTED );
      if ( is_array($game_row) && (int)@$game_row['X_GOPP_Running'] > 0 )
         $out .= ' (' . echo_started_games($game_row['X_GOPP_Running']) . ')';
   }
   elseif ( $same_opp < 0 )
   {
      if ( $same_opp == -1 )
         $out = T_('1 time (same offer)#same_opp');
      else //if ( $same_opp < 0 )
         $out = sprintf( T_('%s times (same offer)#same_opp'), -$same_opp );
      if ( is_array($game_row) && (int)@$game_row['JoinedCount'] > 0 )
      {
         $join_fmt = ($game_row['JoinedCount'] > 1)
            ? T_('joined %s games#same_opp')
            : T_('joined %s game#same_opp');
         $out .= sprintf( " ($join_fmt)", $game_row['JoinedCount'] );
      }
   }
   else
   {
      global $NOW;
      if ( $same_opp == 1 )
         $out = T_('after 1 day#same_opp');
      else //if ( $same_opp > 0 )
         $out = sprintf( T_('after %s days#same_opp'), $same_opp );
      if ( is_array($game_row) && isset($game_row['X_ExpireDate']) && ($game_row['X_ExpireDate'] > $NOW) )
      {
         $out .= ' (' . sprintf( T_('wait till %s#same_opp'),
            format_translated_date(DATE_FMT6, $game_row['X_ExpireDate']) ) . ')';
      }
   }
   return $out;
}//echo_accept_same_opponent

function build_accept_same_opponent_array( array $arr )
{
   $out = array();
   foreach ( $arr as $same_opp )
      $out[$same_opp] = echo_accept_same_opponent($same_opp);
   return $out;
}

function build_arr_handicap_stones( $with_default )
{
   $handi_stones = array();
   if ( $with_default )
      $handi_stones[-1] = T_('Default#maxhandi');
   $handi_stones[0] = 0;
   for ( $bs = 2; $bs <= MAX_HANDICAP; $bs++ )
      $handi_stones[$bs] = $bs;
   return $handi_stones;
}

function build_suggestion_shortinfo( array $suggest_result, $mpgame=false )
{
   list( $handi, $komi, $iamblack, $is_nigiri ) = $suggest_result;
   $fmt = ($mpgame)
      ? T_('... your Color is probably %1$s with Handicap %2$s, Komi %3$.1f')
      : T_('... your Color would be %1$s with Handicap %2$s, Komi %3$.1f');
   $info = sprintf( $fmt, get_colortext_probable($iamblack, $is_nigiri), $handi, $komi );
   return $info;
}

function get_colortext_probable( $iamblack, $is_nigiri )
{
   static $color_class = 'class="InTextStone"';
   global $base_path;

   if ( $is_nigiri )
      return image( $base_path.'17/y.gif', T_('Nigiri#color'), T_('Nigiri (You randomly play Black or White)#color'), $color_class );
   elseif ( $iamblack )
      return image( $base_path.'17/b.gif', T_('Black'), null, $color_class);
   else
      return image( $base_path.'17/w.gif', T_('White'), null, $color_class);
}

// hover-texts for colors-column
function get_color_titles()
{
   return array( // %s=user-handle
      'w_w' => T_('[%s] has White, White to move#hover'),
      'w_b' => T_('[%s] has White, Black to move#hover'),
      'b_w' => T_('[%s] has Black, White to move#hover'),
      'b_b' => T_('[%s] has Black, Black to move#hover'),
      'b'   => T_('Black to move#hover'),
      'w'   => T_('White to move#hover'),
      'y'   => T_('Fair Komi Negotiation: [%s] next#fairkomi'),
   );
}


define('MOVESTAT_TIMESLOT', 60); // minutes

/*! \brief increase MoveStats by one for given user. */
function increaseMoveStats( $uid )
{
   //return; // not used currently

   $loctime = get_utc_timeinfo(); // need UTC to equalize all users on time-slot
   $time_min = floor( $loctime['tm_min'] / MOVESTAT_TIMESLOT ) * MOVESTAT_TIMESLOT;
   $slot_time = (int)sprintf('%02d%02d', $loctime['tm_hour'], $time_min );
   $slot_wday = $loctime['tm_wday'];
   $slot_week = floor( $loctime['tm_yday'] / 7 );

   // ignore errors on update
   db_query( "increaseMoveStats.insert($uid)",
      "INSERT INTO MoveStats (uid,SlotTime,SlotWDay,SlotWeek,Counter) " .
      "VALUES ($uid,$slot_time,$slot_wday,$slot_week,1) " .
      "ON DUPLICATE KEY UPDATE Counter=Counter+1" );
}//increaseMoveStats

?>
//


koh5_pano



Your browser does not support the HTML5 canvas element.


Drag mouse to navigate.

Navigation





17.Aug.2010, Martin Wengenmayer