DIR : /home/kozerus/public_html/dgs/tournaments/include/tournament_pool.php

/home/kozerus/public_html/dgs/tournaments/include

<?php
/*
Dragon Go Server
Copyright (C) 2001-  Erik Ouchterlony, 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/>.
*/

 /* Author: Jens-Uwe Gaspar */

$TranslateGroups[] = "Tournament";

require_once 'include/db_classes.php';
require_once 'include/std_classes.php';
require_once 'include/classlib_user.php';
require_once 'tournaments/include/tournament_globals.php';
require_once 'tournaments/include/tournament_helper.php';
require_once 'tournaments/include/tournament_log_helper.php';
require_once 'tournaments/include/tournament_pool_classes.php';
require_once 'tournaments/include/tournament_round.php';
require_once 'tournaments/include/tournament_utils.php';

 /*!
  * \file tournament_pool.php
  *
  * \brief Functions for handling tournament pools (for assigning users)
  *        for round-robin tournaments: tables TournamentPool
  */


 /*!
  * \class TournamentPool
  *
  * \brief Class to manage TournamentPool-table with pooling-related tournament-settings
  *        for round-robin tournaments
  */

define('TPOOL_LOADOPT_USER',    0x01 ); // load Players-stuff
define('TPOOL_LOADOPT_TP_ID',   0x02 ); // load TournamentParticipant.ID (for rid)
define('TPOOL_LOADOPT_TRATING', 0x04 ); // load TournamentParticipant.Rating
define('TPOOL_LOADOPT_REGTIME', 0x08 ); // load TournamentParticipant.Created (=register-time)
define('TPOOL_LOADOPT_TP_LASTMOVED', 0x10 ); // load TournamentParticipant.Lastmoved
define('TPOOL_LOADOPT_ONLY_RATING', 0x20 ); // only load Rating2 from Players-table
define('TPOOL_LOADOPT_UROW_RATING', 0x40 ); // additionally set TPool->User->urow['Rating2']
define('TPOOL_LOADOPT_USER_HANDLE', 0x80 ); // load Players.Handle

global $ENTITY_TOURNAMENT_POOL; //PHP5
$ENTITY_TOURNAMENT_POOL = new Entity( 'TournamentPool',
   FTYPE_PKEY,  'ID',
   FTYPE_AUTO,  'ID',
   FTYPE_INT,   'ID', 'tid', 'Round', 'Tier', 'Pool', 'uid', 'Rank', 'Flags'
   );

class TournamentPool
{
   public $ID;
   public $tid;
   public $Round;
   public $Tier;
   public $Pool;
   public $uid;
   public $Rank;
   public $Flags;

   // non-DB fields

   public $User; // User-object
   public $PoolGames = array(); // PoolGame-object array
   public $Points = 0;
   public $Wins = 0;
   public $Losses = 0;
   public $SODOS = 0;
   public $CalcRank = 0;

   /*! \brief Constructs TournamentPool-object with specified arguments. */
   private function __construct( $id=0, $tid=0, $round=1, $tier=1, $pool=1, $uid=0, $rank=TPOOLRK_NO_RANK, $flags=0 )
   {
      $this->ID = (int)$id;
      $this->tid = (int)$tid;
      $this->Round = (int)$round;
      $this->Tier = (int)$tier;
      $this->Pool = (int)$pool;
      $this->uid = (int)$uid;
      $this->Rank = (int)$rank;
      $this->Flags = (int)$flags;

      // non-DB fields
      if ( $uid instanceof User )
      {
         $this->User = $uid;
         $this->uid = $this->User->ID;
      }
      else
         $this->User = new User( $this->uid );
   }

   public function to_string()
   {
      return print_r( $this, true );
   }

   public function build_result_info( $tourney_type )
   {
      $rank_str = $this->formatRank(false, T_('unset#tpool'));
      $pool_label = PoolViewer::format_pool_label( $tourney_type, $this->Tier, $this->Pool );
      $tier_pool = PoolViewer::format_tier_pool( $tourney_type, $this->Tier, $this->Pool );
      $def_ranktxt = ( $tourney_type == TOURNEY_TYPE_ROUND_ROBIN )
         ? T_('No Pool winner#tourney')
         : echo_image_tourney_relegation($this->Flags, true);
      return
         echo_image_info( "tournaments/roundrobin/view_pools.php?tid={$this->tid}".URI_AMP."round={$this->Round}#$pool_label",
            T_('Tournament Pool') )
         . MINI_SPACING
         . span('bold', T_('Tournament Pool'), '%s: ')
         . sprintf( T_('%s, Rank [%s]#tourney'), $tier_pool, $rank_str )
         . ',' . MED_SPACING . $this->echoRankImage($tourney_type, $def_ranktxt);
   }

   public function get_cmp_rank()
   {
      // NOTE: 127 = lowest prio
      if ( $this->Rank == TPOOLRK_WITHDRAW || $this->Rank == TPOOLRK_NO_RANK )
         return $this->CalcRank;
      else
         return abs($this->Rank);
   }

   public function formatRank( $incl_calc_rank=false, $unset_rank='', $withdraw_rank=false )
   {
      if ( $this->Rank == TPOOLRK_WITHDRAW )
         $s = NO_VALUE . ( $withdraw_rank ? sprintf(' (%s)', T_('withdrawing#tpool')) : '' );
      elseif ( $this->Rank > TPOOLRK_RANK_ZONE )
         $s = abs($this->Rank);
      else // rank unset
         $s = $unset_rank;

      if ( $incl_calc_rank && $this->CalcRank > 0 && $this->CalcRank != abs($this->Rank) )
         $s .= ' ' . span('Calc', sprintf( '(%s)', $this->CalcRank ));
      return trim($s);
   }//formatRank

   public function formatRankText()
   {
      return $this->formatRank( false, T_('unset#tpool'), true );
   }

   public function echoRankImage( $tourney_type, $default='' )
   {
      if ( $tourney_type == TOURNEY_TYPE_ROUND_ROBIN )
         return ( $this->Rank > 0 ) ? echo_image_tourney_pool_winner() : $default;
      else //if ( $tourney_type == TOURNEY_TYPE_LEAGUE )
         return ( $this->Rank > 0 ) ? echo_image_tourney_relegation( $this->Flags ) : $default;
   }

   /*! \brief Inserts or updates tournament-pool in database. */
   public function persist()
   {
      if ( $this->ID > 0 )
         $success = $this->update();
      else
         $success = $this->insert();
      return $success;
   }

   public function insert()
   {
      $entityData = $this->fillEntityData();
      $result = $entityData->insert( "TournamentPool.insert(%s)" );
      if ( $result )
         $this->ID = mysql_insert_id();
      return $result;
   }

   public function update()
   {
      $entityData = $this->fillEntityData();
      return $entityData->update( "TournamentPool.update(%s)" );
   }

   public function delete()
   {
      $entityData = $this->fillEntityData();
      return $entityData->delete( "TournamentPool.delete(%s)" );
   }

   public function fillEntityData( EntityData $data = null )
   {
      if ( is_null($data) )
         $data = $GLOBALS['ENTITY_TOURNAMENT_POOL']->newEntityData();
      $data->set_value( 'ID', $this->ID );
      $data->set_value( 'tid', $this->tid );
      $data->set_value( 'Round', $this->Round );
      $data->set_value( 'Tier', $this->Tier );
      $data->set_value( 'Pool', $this->Pool );
      $data->set_value( 'uid', $this->uid );
      $data->set_value( 'Rank', $this->Rank );
      $data->set_value( 'Flags', $this->Flags );
      return $data;
   }


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

   /*!
    * \brief Returns db-fields to be used for query of TournamentPool-objects for given tournament-id.
    * \param $tier_pools 0 = load all tiers/pool; otherwise arr( tier-pool-key, ... ) to load specific tier/pools-combinations
    */
   public static function build_query_sql( $tid=0, $round=0, $tier_pools=0 )
   {
      $tid = (int)$tid;
      $round = (int)$round;

      $qsql = $GLOBALS['ENTITY_TOURNAMENT_POOL']->newQuerySQL('TPOOL');
      if ( $tid > 0 )
         $qsql->add_part( SQLP_WHERE, "TPOOL.tid='$tid'" );
      if ( $round > 0 )
         $qsql->add_part( SQLP_WHERE, "TPOOL.Round='$round'" );
      if ( is_array($tier_pools) )
         TournamentUtils::add_qpart_with_tier_pools( $qsql, 'TPOOL', $tier_pools );
      return $qsql;
   }//build_query_sql

   /*! \brief Returns TournamentPool-object created from specified (db-)row. */
   public static function new_from_row( array $row )
   {
      $tpool = new TournamentPool(
            // from TournamentPool
            @$row['ID'],
            @$row['tid'],
            @$row['Round'],
            @$row['Tier'],
            @$row['Pool'],
            @$row['uid'],
            @$row['Rank'],
            @$row['Flags']
         );
      return $tpool;
   }

   /*!
    * \brief Checks, if TournamentPool-entry exists in db; false if no entry found.
    * \param $tier 0 = all tiers; >0 = specific tier
    * \param $pool -2 = check for all pools; -1 = all "real" pools (Pool>0); 0 = special pool #0 for unassigned users
    */
   public static function exists_tournament_tier_pool( $tid, $round, $tier=0, $pool=-2, $uid=null )
   {
      $tid = (int)$tid;
      $round = (int)$round;
      $tier = (int)$tier;
      $pool = (int)$pool;

      $query = "SELECT 1 FROM TournamentPool WHERE tid=$tid AND Round=$round";
      if ( $tier > 0 )
         $query .= " AND Tier=$tier";

      if ( $pool >= 0 )
         $query .= " AND Pool=$pool";
      elseif ( $pool == -1 )
         $query .= " AND Pool>0";

      if ( !is_null($uid) && is_numeric($uid) )
         $query .= " AND uid=$uid";

      $row = mysql_single_fetch( "TournamentPool:exists_tournament_pool.find($tid,$round,$pool,$uid)",
         "$query LIMIT 1" );
      return (bool)$row;
   }//exists_tournament_tier_pool

   /*!
    * \brief Returns array( pool-entries, distinct-tier/pool-count, distinct-user-count ) for given tournament-id
    *        and round (for all tiers & pools, including unassigned-pool).
    */
   public static function count_tournament_tiered_pools( $tid, $round, $count_uid=false )
   {
      $tid = (int)$tid;
      $round = (int)$round;

      $query = 'SELECT SQL_SMALL_RESULT COUNT(*) AS X_CountAll, COUNT(DISTINCT Tier, Pool) AS X_CountPools, '
         . ( $count_uid ? 'COUNT(DISTINCT uid)' : '0' ) . ' AS X_CountUsers '
         . "FROM TournamentPool WHERE tid=$tid AND Round=$round";

      $row = mysql_single_fetch( "TournamentPool:count_tournament_pool($tid,$round,$count_uid)", $query );
      return ($row) ? array( $row['X_CountAll'], $row['X_CountPools'], $row['X_CountUsers'] ) : array( 0, 0, 0 );
   }//count_tournament_tiered_pools

   /*!
    * \brief Returns array( tier-pool-key => user-count ) for given tournament-id and round.
    * \param $rank null=count-all, TPOOLRK_NO_RANK = count-all-unset-rank, other value = count-all with Rank>value
    */
   public static function count_tournament_tiered_pool_users( $tid, $round, $rank=null )
   {
      $tid = (int)$tid;
      $round = (int)$round;

      if ( !is_null($rank) && is_numeric($rank) )
         $where_rank = ( $rank == TPOOLRK_NO_RANK ) ? "AND Rank <= $rank" : "AND Rank > $rank";
      else
         $where_rank = '';
      $result = db_query( "TournamentPool:count_tournament_tiered_pool_users($tid,$round,$rank)",
         "SELECT SQL_SMALL_RESULT Tier, Pool, COUNT(*) AS X_Count FROM TournamentPool " .
         "WHERE tid=$tid AND Round=$round $where_rank GROUP BY Tier, Pool" );

      $arr = array();
      while ( $row = mysql_fetch_assoc($result) )
      {
         $tier_pool_key = TournamentUtils::encode_tier_pool_key( $row['Tier'], $row['Pool'] );
         $arr[$tier_pool_key] = $row['X_Count'];
      }
      mysql_free_result($result);

      return $arr;
   }//count_tournament_tiered_pool_users

   public static function count_tournament_pool_users_by_id( $tpool_id )
   {
      $arr = self::count_tournament_pools_users_by_id( array( $tpool_id ) );
      return (int)@$arr[$tpool_id];
   }//count_tournament_pool_users_by_id

   /*! \brief Returns arr( TPool.ID => pool-user-count, ... ). */
   public static function count_tournament_pools_users_by_id( array $tpool_ids )
   {
      $dbgmsg = sprintf( "TournamentPool:count_tournament_pool_users_by_id([%s])", join( ',', $tpool_ids ) );
      $result = db_query( $dbgmsg,
         "SELECT SQL_SMALL_RESULT TPOOL.ID, COUNT(TPOOL_CNT.ID) AS X_Count " .
         "FROM TournamentPool AS TPOOL " .
            "INNER JOIN TournamentPool AS TPOOL_CNT ON TPOOL_CNT.tid=TPOOL.tid AND TPOOL_CNT.Tier=TPOOL.Tier AND TPOOL_CNT.Pool=TPOOL.Pool " .
         "WHERE " . build_query_in_clause( 'TPOOL.ID', $tpool_ids, false ) .
         "GROUP BY TPOOL.ID" );
      $arr = array();
      while ( $row = mysql_fetch_assoc($result) )
         $arr[$row['ID']] = (int)@$row['X_Count'];
      mysql_free_result($result);

      return $arr;
   }//count_tournament_pools_users_by_id

   /*! \brief Returns array( rank => count ) for given tournament-id and round. */
   public static function count_tournament_pool_ranks( $tid, $round )
   {
      $tid = (int)$tid;
      $round = (int)$round;

      $result = db_query( "TournamentPool:count_tournament_pool_ranks($tid,$round)",
         "SELECT SQL_SMALL_RESULT Rank, COUNT(*) AS X_Count FROM TournamentPool " .
         "WHERE tid=$tid AND Round=$round GROUP BY Rank" );

      $arr = array();
      while ( $row = mysql_fetch_assoc($result) )
         $arr[$row['Rank']] = $row['X_Count'];
      mysql_free_result($result);

      return $arr;
   }//count_tournament_pool_ranks

   /*! \brief Returns array( Rank => arr( TPOOL_FLAG_PROMOTE|DEMOTE|0(=same-tier) => count, .. ) ) for given tournament-id. */
   public static function count_tournament_pool_relegations( $tid )
   {
      $tid = (int)$tid;

      $result = db_query( "TournamentPool:count_tournament_pool_relegations($tid)",
         "SELECT SQL_SMALL_RESULT Rank, (Flags & ".TPOOL_FLAGS_RELEGATIONS.") AS X_Flags, COUNT(*) AS X_Count " .
         "FROM TournamentPool " .
         "WHERE tid=$tid AND Round=1 AND Rank > 0 AND (Flags & ".TPOOL_FLAGS_RELEGATIONS.") " .
         "GROUP BY Rank, Flags" );

      $arr = array();
      while ( $row = mysql_fetch_assoc($result) )
         $arr[$row['Rank']][$row['X_Flags']] = $row['X_Count'];
      mysql_free_result($result);

      return $arr;
   }//count_tournament_pool_relegations

   /*! \brief Returns expected sum of games for all tiered pools for given tournament and round. */
   public static function count_tournament_tiered_pool_games( $tid, $round )
   {
      $tid = (int)$tid;
      $round = (int)$round;

      $result = db_query( "TournamentPool:count_tournament_tiered_pool_games($tid,$round)",
         "SELECT SQL_SMALL_RESULT Tier, Pool, COUNT(*) AS X_Count FROM TournamentPool " .
         "WHERE tid=$tid AND Round=$round GROUP BY Tier, Pool" );

      $games_factor = TournamentHelper::determine_games_factor( $tid );

      $count = 0;
      while ( $row = mysql_fetch_assoc($result) )
         $count += TournamentUtils::calc_pool_games( $row['X_Count'], $games_factor );
      mysql_free_result($result);

      return $count;
   }//count_tournament_tiered_pool_games

   /*!
    * \brief Returns array( pool-entries, pool-count, distinct-user-count ) for given tournament-id
    *        and round (and pool-id if given).
    */
   public static function load_tournament_pool_bad_user( $tid, $round )
   {
      $tid = (int)$tid;
      $round = (int)$round;

      $result = db_query( "TournamentPool:load_tournament_pool_bad_user($tid,$round)",
         "SELECT uid FROM TournamentPool WHERE tid=$tid AND Round=$round GROUP BY uid HAVING COUNT(*) > 1" );

      $arr_uids = array();
      while ( $row = mysql_fetch_assoc($result) )
         $arr_uids[] = $row['uid'];
      mysql_free_result($result);

      return $arr_uids;
   }//load_tournament_pool_bad_user

   /*!
    * \brief Loads single TournamentPool of pool-user with same args and results
    *        like load_tournament_pools()-func.
    * \return null if no matching pool-user found, otherwise TournamentPool-object
    */
   public static function load_tournament_pool_user( $tid, $round, $uid, $load_opts=0 )
   {
      $uid = (int)$uid;
      $tpool_user_iterator = new ListIterator( 'TournamentPool:load_tournament_pool_user',
         new QuerySQL( SQLP_WHERE, "TPOOL.uid=$uid") );
      $tpool_user_iterator = self::load_tournament_pools( $tpool_user_iterator, $tid, $round, 0, $load_opts );

      if ( $tpool_user_iterator->getItemCount() == 1 )
      {
         list(,$arr_item) = $tpool_user_iterator->getListIterator();
         list( $tpool_user, $orow ) = $arr_item;
         return $tpool_user;
      }
      else
         return null;
   }//load_tournament_pool_user

   /*!
    * \brief Returns enhanced (passed) ListIterator with TournamentPool-objects for given tournament-id.
    * \param $tier_pools 0 = load all tiers/pool; otherwise arr( tier-pool-key, ... ) to load specific tier/pools-combinations
    * \param $load_opts see options TPOOL_LOADOPT_...
    * \return uid-indexed iterator with items: TournamentPool + .User + .User.urow[TP_X_RegisterTime|TP_Rating]
    *
    * \note IMPORTANT NOTE: keep in sync with TournamentCache::load_cache_tournament_pools()
    */
   public static function load_tournament_pools( ListIterator $iterator, $tid, $round, $tier_pools=0, $load_opts=0 )
   {
      $needs_tp = ( $load_opts & (TPOOL_LOADOPT_TP_ID|TPOOL_LOADOPT_TRATING|TPOOL_LOADOPT_TP_LASTMOVED|TPOOL_LOADOPT_REGTIME) );
      $needs_tp_rating = ( $load_opts & TPOOL_LOADOPT_TRATING );

      $qsql = self::build_query_sql( $tid, $round, $tier_pools );
      if ( $load_opts & TPOOL_LOADOPT_USER )
      {
         if ( !($load_opts & TPOOL_LOADOPT_ONLY_RATING) )
         {
            $qsql->add_part( SQLP_FIELDS,
               'TPU.Name AS TPU_Name',
               'TPU.Handle AS TPU_Handle',
               'TPU.Country AS TPU_Country' );
         }
         $qsql->add_part( SQLP_FIELDS,
            'TPU.ID AS TPU_ID', // MUST have to overwrite TP.ID with Players.ID in User-obj and User->urow(!)
            'TPU.Rating2 AS TPU_Rating2',
            'TPU.RatingStatus AS TPU_RatingStatus',
            'UNIX_TIMESTAMP(TPU.Lastaccess) AS TPU_X_Lastaccess',
            'TPU.OnVacation AS TPU_OnVacation' );
         $qsql->add_part( SQLP_FROM,
            'INNER JOIN Players AS TPU ON TPU.ID=TPOOL.uid' );

         if ( !$needs_tp_rating )
            $qsql->add_part( SQLP_FIELDS, 'TPU.Rating2 AS TP_Rating' );
         if ( !($load_opts & TPOOL_LOADOPT_REGTIME) )
            $qsql->add_part( SQLP_FIELDS, '0 AS TP_X_RegisterTime' );
      }
      elseif ( $load_opts & TPOOL_LOADOPT_USER_HANDLE )
      {
         $qsql->add_part( SQLP_FIELDS,
            'TPU.Handle AS TPU_Handle' );
         $qsql->add_part( SQLP_FROM,
            'INNER JOIN Players AS TPU ON TPU.ID=TPOOL.uid' );
      }

      if ( $needs_tp )
      {
         if ( $needs_tp_rating )
            $qsql->add_part( SQLP_FIELDS, 'TP.Rating AS TP_Rating' );
         if ( $load_opts & TPOOL_LOADOPT_TP_LASTMOVED )
            $qsql->add_part( SQLP_FIELDS, 'UNIX_TIMESTAMP(TP.Lastmoved) AS TP_X_Lastmoved' );
         if ( $load_opts & TPOOL_LOADOPT_REGTIME )
            $qsql->add_part( SQLP_FIELDS, 'UNIX_TIMESTAMP(TP.Created) AS TP_X_RegisterTime' );

         $qsql->add_part( SQLP_FIELDS, 'TP.ID AS TP_ID' );
         $qsql->add_part( SQLP_FROM,
            "INNER JOIN TournamentParticipant AS TP ON TP.tid=$tid AND TP.uid=TPOOL.uid" );
      }

      $iterator->setQuerySQL( $qsql );
      $iterator->addIndex( 'uid' );
      $query = $iterator->buildQuery();
      $result = db_query( "TournamentPool:load_tournament_pools", $query );
      $iterator->setResultRows( mysql_num_rows($result) );

      $iterator->clearItems();
      while ( $row = mysql_fetch_array( $result ) )
      {
         $tpool = self::new_tournament_pool_from_cache_row( $row, $load_opts );
         $iterator->addItem( $tpool, $row );
      }
      mysql_free_result($result);

      return $iterator;
   }//load_tournament_pools

   /*!
    * \brief Creates TournamentPool-object from row loaded by load_tournament_pools() to be placed in ListIterator.
    * \note only used internally for load_tournament_pools() and TournamentCache::load_cache_tournament_pools().
    */
   public static function new_tournament_pool_from_cache_row( array $row, $load_opts )
   {
      $has_user_data = ( $load_opts & (TPOOL_LOADOPT_USER
         | TPOOL_LOADOPT_TP_ID|TPOOL_LOADOPT_TRATING|TPOOL_LOADOPT_TP_LASTMOVED|TPOOL_LOADOPT_REGTIME) );

      $tpool = self::new_from_row( $row );
      if ( $has_user_data )
      {
         $user = User::new_from_row( $row, 'TPU_', /*urow-strip-prefix*/true );
         $user->urow['TP_ID'] = (int)@$row['TP_ID'];
         $user->urow['TP_X_RegisterTime'] = (int)@$row['TP_X_RegisterTime'];
         $user->urow['TP_X_Lastmoved'] = (int)@$row['TP_X_Lastmoved'];
         $user->urow['TP_Rating'] = (float)@$row['TP_Rating'];
         if ( $load_opts & TPOOL_LOADOPT_UROW_RATING ) // User- or TP-rating dependent on load-opts TPOOL_LOADOPT_TRATING
            $user->urow['Rating2'] = $user->urow['TP_Rating'];
         $tpool->User = $user;
      }
      elseif ( $load_opts & TPOOL_LOADOPT_USER_HANDLE )
      {
         $tpool->User = new User( $row['uid'], '', @$row['TPU_Handle'] );
      }
      return $tpool;
   }//new_tournament_pool_from_cache_row

   /*!
    * \brief Returns ListIterator of tournament-participants with NextRound=$round with array-items:
    *       incomplete TPool(tid,round,tier,pool) + orow.
    * \return orow: array( 'uid' => X, 'Tier' => T, 'Pool' => Y, 'X_HasPool' => 0|1 (0=TP without pool-entry) ).
    * \note TPool returned is incomplete, so don't update() from that object
    */
   private static function load_tournament_participants_with_pools( ListIterator $iterator, $tid, $round, $only_pools = false )
   {
      if ( $only_pools )
      {
         $qsql = new QuerySQL(
            SQLP_FIELDS, 'uid', 'ID', 'Tier', 'Pool', '1 AS X_HasPool',
            SQLP_FROM,   'TournamentPool',
            SQLP_WHERE,  "tid=$tid", "Round=$round" );
      }
      else
      {
         $qsql = new QuerySQL(
            SQLP_FIELDS,
               'TP.uid',
               'TPOOL.ID', 'TPOOL.Tier', 'TPOOL.Pool', 'IFNULL(TPOOL.ID,0) AS X_HasPool',
            SQLP_FROM,
               'TournamentParticipant AS TP',
               "LEFT JOIN TournamentPool AS TPOOL ON TPOOL.uid=TP.uid "
                  . "AND TPOOL.tid=$tid AND TPOOL.Round=$round", // must not be in main-WHERE
            SQLP_WHERE,
               "TP.tid=$tid",
               "TP.Status='".TP_STATUS_REGISTER."'",
               "TP.NextRound=$round" // load only TPs for given round
            );
      }

      $iterator->setQuerySQL( $qsql );
      $iterator->addIndex( 'uid' );
      $query = $iterator->buildQuery();
      $result = db_query( "TournamentPool:load_tournament_participants_with_pools", $query );
      $iterator->setResultRows( mysql_num_rows($result) );

      $iterator->clearItems();
      while ( $row = mysql_fetch_array( $result ) )
      {
         $tpool = ( $row['X_HasPool'] )
            ? new TournamentPool( $row['ID'], $tid, $round, $row['Tier'], $row['Pool'], $row['uid'] )
            : null;
         $iterator->addItem( $tpool, $row );
      }
      mysql_free_result($result);

      return $iterator;
   }//load_tournament_participants_with_pools

   /*! \brief Delete all pools for given tournament-id and round. */
   public static function delete_pools( $tlog_type, $tid, $round )
   {
      if ( !is_numeric($tid) || $tid <= 0 )
         error('invalid_args', "TournamentPool:delete_pools.check.tid($tid,$round)");
      if ( !is_numeric($round) || $round < 1 )
         error('invalid_args', "TournamentPool:delete_pools.check.round($tid,$round)");

      $query = "DELETE FROM TournamentPool WHERE tid=$tid AND Round=$round";
      $result = db_query( "TournamentPool:delete_pools($tid,$round)", $query );
      TournamentLogHelper::log_delete_pools( $tid, $tlog_type, $round, mysql_affected_rows(), $result );
      self::delete_cache_tournament_pools( "TournamentPool:delete_pools($tid,$round)", $tid, $round );
      return $result;
   }//delete_pools

   /*! \brief Seeds pools with all registered TPs for round. */
   public static function seed_pools( $tlog_type, $tid, $t_type, TournamentProperties $tprops, TournamentRound $tround,
                                      $seed_order, $slice_mode )
   {
      if ( !is_numeric($tid) || $tid <= 0 )
         error('unknown_tournament', "TournamentPool:seed_pools.check_tid($tid)");

      $round = $tround->Round;
      $dbgmsg = "TournamentPool:seed_pools($tid,$round,$seed_order,$slice_mode)";

      list( $def, $arr_seed_order ) = $tprops->build_seed_order();
      if ( !isset($arr_seed_order[$seed_order]) )
         error('invalid_args', "$dbgmsg.check_seed_order");

      list( $def, $arr_slice_mode ) = self::get_slice_modes();
      if ( !isset($arr_slice_mode[$slice_mode]) )
         error('invalid_args', "$dbgmsg.check_slice_mode");

      // load already joined pool-users
      $tpool_iterator = new ListIterator( "$dbgmsg.load_pools" );
      $tpool_iterator->addIndex( 'uid' );
      $tpool_iterator = self::load_tournament_pools( $tpool_iterator, $tid, $round );

      // find all registered TPs (optimized)
      $arr_TPs = TournamentParticipant::load_registered_users_in_seedorder( $tid, $round, $seed_order );

      // add all TPs to pools
      $entity_tpool = $GLOBALS['ENTITY_TOURNAMENT_POOL']->newEntityData();
      $arr_inserts = array();
      $slicer = new TierSlicer( $t_type, $tround, $slice_mode, count($arr_TPs) );
      foreach ( $arr_TPs as $row )
      {
         $uid = $row['uid'];

         // handle slice-mode for user-distribution on tiers & pools
         list( $tier, $pool ) = $slicer->next_tier_pool();

         $tpool = $tpool_iterator->getIndexValue( 'uid', $uid, 0 );
         if ( is_null($tpool) ) // user not joined yet
            $tpool = new TournamentPool( 0, $tid, $round, $tier, $pool, $uid );
         else // user already joined
         {
            $tpool->Tier = $tier;
            $tpool->Pool = $pool;
         }

         $data_tpool = $tpool->fillEntityData( $entity_tpool );
         $arr_inserts[] = $data_tpool->build_sql_insert_values(false, /*with-PK*/true);
      }
      unset($arr_TPs);
      unset($tpool_iterator);

      list( $max_tier, $cnt_pools, $cnt_unassigned_pool ) = $slicer->get_slicer_counts();

      // insert all registered TPs to pools
      $cnt = count($arr_inserts);
      $seed_query = $entity_tpool->build_sql_insert_values(true, /*with-PK*/true) . implode(',', $arr_inserts)
         . " ON DUPLICATE KEY UPDATE Tier=VALUES(Tier), Pool=VALUES(Pool) ";

      ta_begin();
      {//HOT-section for seeding-pools
         db_lock( $dbgmsg, "TournamentPool WRITE, TournamentRound WRITE" );
         {//LOCK TournamentPool
            $result = db_query( "$dbgmsg.insert(#$cnt)", $seed_query );
            self::delete_cache_tournament_pools( $dbgmsg, $tid, $round );

            if ( $t_type == TOURNEY_TYPE_LEAGUE )
            {
               $tround->Pools = (int)$cnt_pools;
               $tround->update();
               TournamentRound::delete_cache_tournament_round( $dbgmsg, $tid, $round );
            }
         }
         db_unlock();

         TournamentLogHelper::log_seed_pools( $tid, $tlog_type, $round,
            "seed_order=$seed_order, slice_mode=$slice_mode",
            $cnt, $max_tier, $cnt_pools, $cnt_unassigned_pool, $result );
      }
      ta_end();

      return $result;
   }//seed_pools

   /*! \brief Add missing registered users to pool #0 (very similar to seed_pools()-func). */
   public static function add_missing_registered_users( $tlog_type, $tid, TournamentRound $tround )
   {
      if ( !is_numeric($tid) || $tid <= 0 )
         error('unknown_tournament', "TournamentPool:add_missing_registered_users.check_tid($tid)");

      $round = $tround->Round;

      // load already joined pool-users
      $tpool_iterator = new ListIterator( "TournamentPool:add_missing_registered_users.load_pools($tid,$round)" );
      $tpool_iterator->addIndex( 'uid' );
      $tpool_iterator = self::load_tournament_pools( $tpool_iterator, $tid, $round );

      // find all registered TPs (optimized = no seed-order)
      $arr_TPs = TournamentParticipant::load_registered_users_in_seedorder( $tid, $round, TOURNEY_SEEDORDER_NONE );

      // add all missing TPs to pools
      $NOW = $GLOBALS['NOW'];
      $entity_tpool = $GLOBALS['ENTITY_TOURNAMENT_POOL']->newEntityData();
      $arr_inserts = array();

      $tier = 1;
      $pool = 0; // like for TROUND_SLICE_MANUAL
      foreach ( $arr_TPs as $row )
      {
         $uid = $row['uid'];

         $tpool = $tpool_iterator->getIndexValue( 'uid', $uid, 0 );
         if ( !is_null($tpool) ) // user already joined
            continue;

         $tpool = new TournamentPool( 0, $tid, $round, $tier, $pool, $uid );
         $data_tpool = $tpool->fillEntityData( $entity_tpool );
         $arr_inserts[] = $data_tpool->build_sql_insert_values(false, /*with-PK*/true);
      }
      unset($arr_TPs);
      unset($tpool_iterator);

      // insert all missing registered TPs to pool #0
      $cnt = count($arr_inserts);
      $seed_query = $entity_tpool->build_sql_insert_values(true, /*with-PK*/true) . implode(',', $arr_inserts)
         . " ON DUPLICATE KEY UPDATE Pool=VALUES(Pool) ";

      db_lock( "TournamentPool:add_missing_registered_users($tid,$round)",
         "TournamentPool WRITE" );
      {//LOCK TournamentPool
         $result = db_query( "TournamentPool:add_missing_registered_users.insert($tid,$round,#$cnt)", $seed_query );
      }
      db_unlock();

      TournamentLogHelper::log_seed_pools_add_missing_users( $tid, $tlog_type, $round, $cnt, $result );

      return $result;
   }//add_missing_registered_users

   /*!
    * \brief Assigns list of users to given tournament-round and tier/pool.
    * \note parameters $tier & $pool are not checked for valid values!
    */
   public static function assign_pool( $tlog_type, TournamentRound $tround, $tier, $pool, array $arr_uid )
   {
      $tid = (int)$tround->tid;
      $round = (int)$tround->Round;
      $dbg = "TournamentPool:assign_pool($tid,$round,$tier,$pool)";

      $cnt = count($arr_uid);
      if ( $cnt == 0 )
         return true;

      // find old-pool-state for T-logging
      $uid_where = implode(',', $arr_uid);
      $result = db_query( "$dbg.find_old",
         "SELECT Tier, Pool, uid FROM TournamentPool " .
         "WHERE tid=$tid AND Round=$round AND uid IN ($uid_where) LIMIT $cnt" );
      $arr_old_pools = array(); // arr( tier/pool -> [uid, ...] )
      $pn_log = new PoolNameFormatter( 'T%t(num)P%p(num)' );
      while ( $row = mysql_fetch_assoc($result) )
      {
         $key = $pn_log->format( $row['Tier'], $row['Pool'] );
         $arr_old_pools[$key][] = $row['uid'];
      }
      mysql_free_result($result);

      // assign new pool for given users
      $result = db_query( "$dbg.update",
         "UPDATE TournamentPool SET Tier=$tier, Pool=$pool " .
         "WHERE tid=$tid AND Round=$round AND uid IN ($uid_where) LIMIT $cnt" );
      $upd_count = mysql_affected_rows();

      TournamentLogHelper::log_assign_tournament_pool( $tid, $tlog_type, $tround, $arr_old_pools, $arr_uid,
         $pn_log->format($tier, $pool), $pool );

      if ( $upd_count > 0 )
         self::delete_cache_tournament_pools( $dbg, $tid, $round );

      return $upd_count;
   }//assign_pool

   /*!
    * \brief checks pool integrity and return list of errors, and fills in number of started-game-counts
    *       in PoolSummary-array; empty list if all ok.
    * \param $tround TournamentRound-object
    * \param $tourney_type Tournament.Type
    * \return array( errors, pool_summary ) with pool_summary := [ tier-pool-key => PoolSummaryEvent, ... ]
    */
   public static function check_pools( TournamentRound $tround, $tourney_type, $only_summary=false )
   {
      $tid = $tround->tid;
      $round = $tround->Round;
      $errors = array();

      $games_factor = TournamentHelper::determine_games_factor( $tid );

      // load tourney-participants and pool data
      $tpool_iterator = new ListIterator( 'TournamentPool.check_pools.load_tp_pools' );
      $tpool_iterator = self::load_tournament_participants_with_pools( $tpool_iterator, $tid, $round, $only_summary );
      $poolTables = new PoolTables( $tpool_iterator );

      $pool_summary = $poolTables->calc_pool_summary( $games_factor );
      if ( $only_summary ) // load only summary
      {
         // check game-counts integrity for all pools
         $arr_game_counts = self::check_pools_game_integrity( $tround );
         $cnt_miss_games = $cnt_bad_games = 0;
         foreach ( $pool_summary as $tier_pool_key => $pse )
         {
            list( $tier, $pool ) = TournamentUtils::decode_tier_pool_key( $tier_pool_key );

            $cnt_expected_games = $pse->count_games;
            if ( isset($arr_game_counts[$tier_pool_key]) )
            {
               list( $cnt_tgames, $cnt_games ) = $arr_game_counts[$tier_pool_key];
               if ( $cnt_tgames != $cnt_games || $cnt_expected_games != $cnt_tgames )
               {
                  if ( $cnt_expected_games != $cnt_tgames )
                     ++$cnt_miss_games;
                  if ( $cnt_tgames != $cnt_games )
                     ++$cnt_bad_games;

                  $pse->errors[] =
                     sprintf( T_('Inconsistency: expected %s games, but have: %s TGames & %s Games#tourney'),
                        $cnt_expected_games, $cnt_tgames, $cnt_games );
               }
               $pse->count_started_games += $cnt_tgames;
            }
         }
         if ( $cnt_miss_games )
            $errors[] = T_('Found inconsistencies requiring partial restart to create missing tournament games.#tourney');
         if ( $cnt_bad_games )
            $errors[] = T_('Found game inconsistencies (TGames!=Games). Contact tournament-admin for assistance!#tourney');

         return array( $errors, $pool_summary );
      }//only-summary


      $arr_user_counts = array(); // [ tier-pool-key => #users, ... ]
      foreach ( $pool_summary as $tier_pool_key => $pse )
         $arr_user_counts[$tier_pool_key] = $pse->count_users;

      $pool0_key = TournamentUtils::encode_tier_pool_key( 1, 0 );
      $cnt_pool0 = (int)@$arr_user_counts[$pool0_key];
      $cnt_real_pools = count($arr_user_counts) - ( $cnt_pool0 > 0 ? 1 : 0 ); // pool-count without 0-pool
      list( $cnt_entries, $cnt_pools, $cnt_users ) = // $cnt_pools can include 0-pool
         self::count_tournament_tiered_pools( $tid, $round, /*count_uid*/true );

      // ---------- check pool integrity ----------

      // check that uids are distinct in all pools
      if ( $cnt_entries != $cnt_users )
      {
         $arr_bad_users = self::load_tournament_pool_bad_user( $tid, $round );
         $errors[] = sprintf( T_('Fatal error: Please contact an admin to fix multiple user-entries [%s] for tournament #%d, round %d.'),
            implode(',',$arr_bad_users), $tid, $round );
      }

      // check that there are some pools
      if ( $cnt_pools == 0 )
         $errors[] = T_('Expecting at least one pool.');

      // check that count of pools matches the expected TRound.Pools-count
      if ( $cnt_real_pools != $tround->Pools )
         $errors[] = sprintf( T_('Expected %s pools, but currently there are %s pools.'),
            $tround->Pools, $cnt_real_pools );

      // check for correct use of tier/pools
      $invalid_pools = array();
      $p_parser = new PoolParser( $tourney_type, $tround );
      foreach ( $pool_summary as $tier_pool_key => $pse )
      {
         list( $tier, $pool ) = TournamentUtils::decode_tier_pool_key( $tier_pool_key );
         if ( $tier == 1 && $pool == 0 ) // skip unassigned-pool
            continue;
         if ( !$p_parser->is_valid_tier_pool( $tier, $pool ) )
         {
            $pse->errors[] = T_('Invalid tier/pool combination');
            $invalid_pools[] = PoolViewer::format_tier_pool( $tourney_type, $tier, $pool, true );
         }
      }
      if ( count($invalid_pools) )
         $errors[] = sprintf( T_('Found invalid tier/pool combinations [%s]. Contact tournament-admin for assistance!'), join(', ', $invalid_pools) );

      // check that all registered users joined somewhere in the pools
      $arr_missing_users = array();
      $tpool_iterator->resetListIterator();
      $cnt_missing_users = 0;
      while ( list(,$arr_item) = $tpool_iterator->getListIterator() )
      {
         list(,$orow ) = $arr_item;
         if ( !$orow['X_HasPool'] )
            $cnt_missing_users++;
      }
      if ( $cnt_missing_users > 0 )
         $errors[] = sprintf( T_('There are %s registered users, that are not appearing in the pools.'),
            $cnt_missing_users );

      // check that there are no unassigned users (with Pool=0)
      if ( $cnt_pool0 > 0 )
      {
         $errors[] = sprintf( T_('There are %s unassigned users. Please assign them to a pool!'), $cnt_pool0 );
         $pool_summary[$pool0_key]->errors[] = T_('Unassigned users#tpool');
      }

      // check that the user-count of each pool is in valid range of min/max-pool-size
      $cnt_violate_poolsize = 0;
      foreach ( $arr_user_counts as $tier_pool_key => $pool_usercount )
      {
         list( $tier, $pool ) = TournamentUtils::decode_tier_pool_key( $tier_pool_key );
         if ( $pool == 0 )
            continue;
         if ( $pool_usercount < $tround->MinPoolSize || $pool_usercount > $tround->MaxPoolSize )
            $cnt_violate_poolsize++;
         if ( $pool_usercount < $tround->MinPoolSize )
            $pool_summary[$tier_pool_key]->errors[] = T_('Pool-Size too small');
         if ( $pool_usercount > $tround->MaxPoolSize )
            $pool_summary[$tier_pool_key]->errors[] = T_('Pool-Size too big');
      }
      if ( $cnt_violate_poolsize > 0 )
         $errors[] = sprintf( T_('There are %s pools violating the valid pool-size range %s.'),
            $cnt_violate_poolsize,
            build_range_text($tround->MinPoolSize, $tround->MaxPoolSize) );

      // check that there are no empty pools
      $cnt_empty = 0;
      foreach ( $arr_user_counts as $tier_pool_key => $pool_usercount )
      {
         if ( $pool_usercount == 0 )
         {
            $cnt_empty++;
            $pool_summary[$tier_pool_key]->errors[] = T_('Pool empty');
         }
      }
      if ( $cnt_empty > 0 )
         $errors[] = sprintf( T_('There are %s empty pools. Please fill or remove them!'), $cnt_empty );

      return array( $errors, $pool_summary );
   }//check_pools

   /*!
    * \brief checks game integrity for (tiered) pools of given tournament-round.
    * \return count of TournamentGames- and respective Games-entries (which should be the same) for each pool in array:
    *       array( tier-pool-key => array( TournamentGames-count, Games-count ), ... )
    */
   private static function check_pools_game_integrity( TournamentRound $tround )
   {
      $tid = $tround->tid;
      $round_id = $tround->ID;

      $qsql = new QuerySQL(
         SQLP_OPTS, 'SQL_SMALL_RESULT',
         SQLP_FIELDS, 'Tier', 'Pool', 'COUNT(*) AS X_Count_TGames', 'SUM(IF(ISNULL(G.ID),0,1)) AS X_Count_Games',
         SQLP_FROM, 'TournamentGames AS TG', 'LEFT JOIN Games as G ON G.ID=TG.gid',
         SQLP_WHERE, "TG.tid=$tid", "TG.Round_ID=$round_id",
         SQLP_GROUP, 'Tier', 'Pool' );

      $result = db_query( "TournamentPool:check_pools_game_integrity($tid,$round_id)", $qsql->get_select() );
      $arr = array();
      while ( $row = mysql_fetch_array( $result ) )
      {
         $tier_pool_key = TournamentUtils::encode_tier_pool_key( $row['Tier'], $row['Pool'] );
         $arr[$tier_pool_key] = array( $row['X_Count_TGames'], $row['X_Count_Games'] );
      }
      mysql_free_result($result);

      return $arr;
   }//check_pools_game_integrity

   /*!
    * \brief Updates TournamentPool.Rank with given rank for specified TournamentPool.IDs.
    * \param $tpool_ids array with TournamentPool-IDs to update rank
    * \param $rank Rank in range of 1..$tround->PoolSize to be set for pool-user
    * \param $fix_rank if true, no update-restriction on Rank;
    *        otherwise expect Rank<TPOOLRK_RANK_ZONE to auto-fill rank
    * \return number of updated entries
    *
    * \note tournament-pool cache for cache-group CACHE_GRP_TPOOLS is NOT invalidated! must be done at calling side.
    */
   public static function update_tournament_pool_ranks( TournamentRound $tround, $tlog_type, $tlog_ref,
                                                        array $tpool_ids, $rank, $fix_rank = false )
   {
      $tid = $tround->tid;
      $rank = (int)$rank;

      $cnt = count( $tpool_ids );
      if ( $cnt <= 0 )
         return 0;
      $dbgmsg = sprintf( "TournamentPool:update_tournament_pool_ranks.check.rank($tid,[%s]#$cnt,$rank,$fix_rank)",
         join(',', $tpool_ids));

      if ( abs($rank) > $tround->PoolSize )
         error('invalid_args', $dbgmsg.".check.rank.round_poolsize({$tround->PoolSize})");
      $count_poolusers = self::count_tournament_pools_users_by_id( $tpool_ids );
      foreach ($count_poolusers as $tpool_id => $count)
      {
         if ( abs($rank) > $count )
            error('invalid_args', $dbgmsg.".check.rank.pool_size($tpool_id,$count)");
      }

      $qpart_rank = ($fix_rank) ? '' : ' AND Rank<'.TPOOLRK_RANK_ZONE;
      $qpart_tpools = build_query_in_clause( 'ID', $tpool_ids, false );
      $result = db_query( $dbgmsg.".update",
         "UPDATE TournamentPool SET Rank=$rank WHERE $qpart_tpools $qpart_rank LIMIT $cnt" );
      $upd_count = mysql_affected_rows();

      TournamentLogHelper::log_set_tournament_pool_ranks( $tid, $tlog_type, $tlog_ref, $tpool_ids, $rank, $fix_rank, $upd_count );

      return $upd_count;
   }//update_tournament_pool_ranks

   /*!
    * \brief Updates TournamentPool.Rank setting pool-winners determined by tround->PoolWinnerRanks (if >0).
    * \param $tround TournamentRound-object
    * \return number of affected rows
    *
    * \note expecting TournamentPool.Rank already set, either manually by TD or TournamentRoundHelper::fill_ranks_tournament_pool()
    * \note pool-entries on WITHDRAWN-rank are not touched
    * \note already set pool-winners are not touched
    */
   public static function update_tournament_pool_set_pool_winners( TournamentRound $tround )
   {
      $tid = (int)$tround->tid;
      $round = (int)$tround->Round;
      $poolwinner_ranks = (int)$tround->PoolWinnerRanks;
      if ( $poolwinner_ranks == 0 )
         return 0;

      $result = db_query( "TournamentPool:update_tournament_pool_set_pool_winners.update($tid,{$tround->ID},$round,$poolwinner_ranks)",
         "UPDATE TournamentPool SET Rank=ABS(Rank) " .
         "WHERE tid=$tid AND Round=$round AND Rank < 0 AND Rank >= ".TPOOLRK_RANK_ZONE . // rank safety-checks
            " AND Rank >= -$poolwinner_ranks" );
      $upd_count = mysql_affected_rows();

      if ( $upd_count > 0 )
         self::delete_cache_tournament_pools( "TournamentPool:update_tournament_pool_set_pool_winners($tid,$round)",
            $tid, $round );

      return $upd_count;
   }//update_tournament_pool_set_pool_winners

   /*!
    * \brief Updates TournamentPool.Rank setting relegations determined by tround->PromoteRanks & tround->DemoteStartRank.
    * \param $tround TournamentRound-object
    * \return number of affected rows
    *
    * \note expecting TournamentPool.Rank already set, either manually by TD or TournamentRoundHelper::fill_ranks_tournament_pool()
    * \note pool-entries on WITHDRAWN-rank are not touched
    * \note already set relegations are not touched (if TPOOL_FLAG_TD_MANUAL set in db)
    */
   public static function update_tournament_pool_set_relegations( TournamentRound $tround )
   {
      $tid = (int)$tround->tid;
      $promote_ranks = (int)$tround->PromoteRanks;
      $demote_start_rank = (int)$tround->DemoteStartRank;
      if ( $promote_ranks < 1 || $demote_start_rank < 1 )
         return 0;

      $sql_relegate_flags = "CASE WHEN (Rank BETWEEN 1 AND $promote_ranks) THEN ".TPOOL_FLAG_PROMOTE." " .
              "WHEN (Rank >= $demote_start_rank) THEN ".TPOOL_FLAG_DEMOTE." " .
              "ELSE 0 END";
      $result = db_query( "TournamentPool:update_tournament_pool_set_relegations.update($tid,{$tround->ID})",
         "UPDATE TournamentPool SET Flags=Flags & ~".TPOOL_FLAGS_RELEGATIONS." | ($sql_relegate_flags) " .
         "WHERE tid=$tid AND Round=1 AND Rank > 0 AND (Flags & ".TPOOL_FLAG_TD_MANUAL.") = 0" );
      $upd_count = mysql_affected_rows();

      if ( $upd_count > 0 )
         self::delete_cache_tournament_pools( "TournamentPool:update_tournament_pool_set_relegations($tid)", $tid, 1 );

      return $upd_count;
   }//update_tournament_pool_set_relegations

   /*!
    * \brief Executes rank-actions on TournamentPool.Rank for specified tournament-round.
    * \param $tourney Tournament-object to get ID & CurrentRound
    * \param $action one of RKACT_... to change TournamentPool.Rank+Flags with:
    *     for round-robin & league tournaments:
    *        RKACT_WITHDRAW       = sets Rank=0, withdrawing pool-user from next-round and as pool-winner, clears relegation-flags
    *        RKACT_REMOVE_RANKS   = sets Rank=TPOOLRK_NO_RANK, clears relegation-flags to allow filling ranks anew
    *     only for round-robin-tournaments:
    *        RKACT_SET_POOL_WIN   = sets pool-winner = ABS(Rank) for Ranks < 0
    *        RKACT_CLEAR_POOL_WIN = clears pool-winner = -ABS(Rank) for Ranks > 0
    *     only for league-tournaments:
    *        RKACT_PROMOTE        = sets Flags(mask) = TPOOL_FLAG_PROMOTE if Rank > 0
    *        RKACT_DEMOTE         = sets Flags(mask) = TPOOL_FLAG_DEMOTE  if Rank > 0
    *        RKACT_CLEAR_RELEGATION = clears Flags(mask) = TPOOL_FLAGS_RELEGATIONS
    * \param $uid 0=all users matching rank-criteriat; otherwise specific user
    * \param $rank_from ''=all ranks, otherwise numeric rank
    * \param $rank_to ''=same as rank_from (single rank), otherwise numeric rank
    * \param $tier_pool_key null=all tiers/pool, otherwise specific tier/pool
    * \return number of updated entries
    */
   public static function execute_rank_action( $tlog_type, Tournament $tourney, TournamentRound $tround, $action, $uid,
                                               $rank_from = null, $rank_to = null, $tier_pool_key = '' )
   {
      static $ALLOWED_ACTION = array(
            TOURNEY_TYPE_ROUND_ROBIN => array( RKACT_SET_POOL_WIN, RKACT_CLEAR_POOL_WIN, RKACT_WITHDRAW, RKACT_REMOVE_RANKS ),
            TOURNEY_TYPE_LEAGUE => array( RKACT_PROMOTE, RKACT_DEMOTE, RKACT_CLEAR_RELEGATION, RKACT_WITHDRAW, RKACT_REMOVE_RANKS ),
         );
      $tid = $tourney->ID;
      $round = $tourney->CurrentRound;
      $tier = $pool = '';

      // check args
      if ( !is_numeric($action) || !in_array( $action, $ALLOWED_ACTION[$tourney->Type] ) )
         error('invalid_args', "TournamentPool:execute_rank_action.check.action($tid,{$tourney->Type},$round,$action)");
      if ( !is_numeric($uid) || ($uid != 0 && $uid <= GUESTS_ID_MAX) )
         error('invalid_args', "TournamentPool:execute_rank_action.check.uid($tid,$round,$uid)");
      if ( $uid == 0 )
      {
         if ( (string)$rank_from != '' && ( !is_numeric($rank_from) || $rank_from < 0 ))
            error('invalid_args', "TournamentPool:execute_rank_action.check.rank_from($tid,$round,$rank_from)");
         if ( (string)$rank_to != '' && ( !is_numeric($rank_to) || $rank_to < 0 ))
            error('invalid_args', "TournamentPool:execute_rank_action.check.rank_to($tid,$round,$rank_to)");
         if ( (string)$tier_pool_key != '' )
         {
            if ( !is_numeric($tier_pool_key) )
               error('invalid_args', "TournamentPool:execute_rank_action.check.tpk($tid,$round,$tier_pool_key)");
            list( $tier, $pool ) = TournamentUtils::decode_tier_pool_key( $tier_pool_key );
            $p_parser = new PoolParser( $tourney->Type, $tround );
            if ( !$p_parser->is_valid_tier_pool( $tier, $pool ) )
               error('invalid_args', "TournamentPool:execute_rank_action.check.tpk_valid($tid,{$tourney->Type},$round,$tier_pool_key,t$tier,p$pool)");
         }
      }

      list( $qpart_rank, $mod_rank_from, $mod_rank_to ) = self::build_qpart_rank( $action, $uid, $rank_from, $rank_to );

      $change_unset = false; // unset-ranks are not allowed to be changed normally
      $set_flags = $clear_flags = 0;
      if ( $action == RKACT_SET_POOL_WIN )
         $rankval = 'ABS(Rank)'; // where Rank < 0
      elseif ( $action == RKACT_CLEAR_POOL_WIN )
         $rankval = '-ABS(Rank)'; // where Rank > 0
      elseif ( $action == RKACT_WITHDRAW )
      {
         $rankval = '0';
         $change_unset = true; // allow overwriting unset-ranks for withdrawing (as it's a manual TD-operation)
         $clear_flags = TPOOL_FLAGS_RELEGATIONS | TPOOL_FLAG_TD_MANUAL;
      }
      elseif ( $action == RKACT_PROMOTE )
      {
         $rankval = null;
         $clear_flags = TPOOL_FLAG_DEMOTE;
         $set_flags = TPOOL_FLAG_PROMOTE;
      }
      elseif ( $action == RKACT_DEMOTE )
      {
         $rankval = null;
         $clear_flags = TPOOL_FLAG_PROMOTE;
         $set_flags = TPOOL_FLAG_DEMOTE;
      }
      elseif ( $action == RKACT_CLEAR_RELEGATION )
      {
         $rankval = null;
         $clear_flags = TPOOL_FLAGS_RELEGATIONS | TPOOL_FLAG_TD_MANUAL;
      }
      else //if ( $action == RKACT_REMOVE_RANKS )
      {
         $rankval = TPOOLRK_NO_RANK;
         $clear_flags = TPOOL_FLAGS_RELEGATIONS | TPOOL_FLAG_TD_MANUAL;
      }

      // update
      $qset_parts = array();
      if ( !is_null($rankval) )
         $qset_parts[] = "Rank=$rankval";
      if ( $set_flags || $clear_flags )
      {
         if ( $set_flags )
            $set_flags |= TPOOL_FLAG_TD_MANUAL;
         $qset_parts[] = "Flags=Flags & ~$clear_flags | $set_flags";
      }
      $query = "UPDATE TournamentPool SET " . join(', ', $qset_parts)
         . " WHERE tid=$tid AND Round=$round "
         . ( is_numeric($tier) && is_numeric($pool) ? " AND Tier=$tier AND Pool=$pool" : '' )
         . ( $change_unset ? '' : " AND Rank >".TPOOLRK_RANK_ZONE ) // don't touch UNSET-ranks
         . $qpart_rank
         . ( $uid ? " AND uid=$uid LIMIT 1" : '' );
      $criteria_ranks = "$rank_from..$rank_to>[$mod_rank_from..$mod_rank_to]";
      $result = db_query( "TournamentPool:execute_rank_action.update($tid,$round,a$action,u$uid,$criteria_ranks"
         . ",t$tier,p$pool:r=$rankval,f=-$clear_flags|$set_flags", $query );
      $upd_count = mysql_affected_rows();

      $pn_log = new PoolNameFormatter( 'T%t(num)P%p(num)' );
      TournamentLogHelper::log_execute_tournament_pool_rank_action( $tid, $tlog_type, $round, $action, $uid,
         $criteria_ranks, $pn_log->format($tier, $pool), $upd_count );

      if ( $upd_count > 0 )
         self::delete_cache_tournament_pools( "TournamentPool:execute_rank_action($tid,$round)", $tid, $round );

      return $upd_count;
   }//execute_rank_action

   /*!
    * \brief Returns query-part with restriction on TournamentPool.Rank to allow given $action
    *     for execute_rank_action()-method + modified rank_from/to.
    */
   private static function build_qpart_rank( $action, $uid, $rank_from, $rank_to )
   {
      $is_set_relegation_action = ( $action == RKACT_PROMOTE || $action == RKACT_DEMOTE );

      $qpart_rank = ''; // where-clause
      if ( $uid ) // has uid
      {
         if ( $action == RKACT_SET_POOL_WIN )
            $qpart_rank = " AND Rank < 0";
         elseif ( $action == RKACT_CLEAR_POOL_WIN || $is_set_relegation_action )
            $qpart_rank = " AND Rank > 0";
      }
      else // no uid
      {
         if ( is_numeric($rank_from) && !is_numeric($rank_to) )
            $rank_to = $rank_from;
         $rank_min = min( $rank_from, $rank_to );
         $rank_max = max( $rank_from, $rank_to );

         if ( $action == RKACT_SET_POOL_WIN )
         {
            if ( !is_numeric($rank_from) )
               $qpart_rank = " AND Rank < 0";
            else
            {
               $rank_from = -$rank_from;
               $rank_to = -$rank_to;
            }
         }
         elseif ( ($action == RKACT_CLEAR_POOL_WIN || $is_set_relegation_action) && !is_numeric($rank_from) )
            $qpart_rank = " AND Rank > 0";
         elseif ( ($action == RKACT_WITHDRAW || $action == RKACT_REMOVE_RANKS) && is_numeric($rank_from) )
         {
            if ( $rank_from == $rank_to )
               $qpart_rank = " AND Rank IN (-$rank_from,$rank_from)";
            else
            {
               $qpart_rank = ' AND ' . build_query_in_clause('Rank',
                  array_merge( range($rank_min,$rank_max), range(-$rank_max,-$rank_min) ),
                  /*str*/false );
            }
         }

         if ( !$qpart_rank && is_numeric($rank_from) )
         {
            if ( $rank_from == $rank_to )
               $qpart_rank = " AND Rank=$rank_from";
            else
               $qpart_rank = " AND Rank BETWEEN $rank_min AND $rank_max";
         }
      }//no uid

      return array( $qpart_rank, $rank_from, $rank_to );
   }//build_qpart_rank

   /*!
    * \brief Returns number of user marked as next-rounders/finalists for given tournament-id and round.
    * \note only used by round-robin-tournaments!
    */
   public static function count_tournament_pool_next_rounders( $tid, $round )
   {
      $tid = (int)$tid;
      $round = (int)$round;

      $row = mysql_single_fetch( "TournamentPool:count_tournament_pool_next_rounders($tid,$round)",
         "SELECT COUNT(*) AS X_Count FROM TournamentPool WHERE tid=$tid AND Round=$round AND Rank>0" );
      return ($row) ? (int)@$row['X_Count'] : 0;
   }//count_tournament_pool_next_rounders

   /*!
    * \brief Returns number of users marked as next-rounders without set TP.NextRound for next-round.
    * \note only used by round-robin-tournaments!
    */
   public static function count_tournament_pool_missing_next_rounders( $tid, $round )
   {
      $tid = (int)$tid;
      $round = (int)$round; // current-round
      $next_round = $round + 1;

      $row = mysql_single_fetch( "TournamentPool:count_tournament_pool_missing_next_rounders($tid,$round)",
         "SELECT COUNT(*) AS X_Count " .
         "FROM TournamentParticipant AS TP " .
            "INNER JOIN TournamentPool AS TPOOL ON TPOOL.uid=TP.uid " .
         "WHERE TPOOL.tid=$tid AND TPOOL.Round=$round AND TPOOL.Rank>0 " .
            "AND TP.tid=$tid AND TP.NextRound < $next_round" );
      return ($row) ? (int)@$row['X_Count'] : 0;
   }//count_tournament_pool_missing_next_rounders

   /*!
    * \brief Sets TournamentParticipant.NextRound to next-round (round+1) for missing users marked
    *    as finalists/next-rounders in current round.
    * \note only used by round-robin-tournaments!
    */
   public static function mark_next_round_participation( $tid, $round )
   {
      global $NOW, $player_row;
      $tid = (int)$tid;
      $round = (int)$round; // current-round
      $next_round = $round + 1;
      $changed_by = ( (string)@$player_row['Handle'] != '' ) ? @$player_row['Handle'] : UNKNOWN_VALUE;

      return db_query( "TournamentPool:mark_next_round_participation.TP.update($tid,$round)",
         "UPDATE TournamentParticipant AS TP " .
            "INNER JOIN TournamentPool AS TPOOL ON TPOOL.uid=TP.uid " .
         "SET TP.NextRound=$next_round, TP.Lastchanged=$NOW, " .
            "TP.ChangedBy=RTRIM(CONCAT('[".mysql_addslashes($changed_by)."] ',TP.ChangedBy)) " .
         "WHERE TPOOL.tid=$tid AND TPOOL.Round=$round AND TPOOL.Rank>0 " .
            "AND TP.tid=$tid AND TP.NextRound <= $round" );
   }//mark_next_round_participation

   /*! \brief Returns array with default and slice-mode array for round-robin-tournament. */
   public static function get_slice_modes()
   {
      static $ARR_SLICE_MODES = null;

      if ( is_null($ARR_SLICE_MODES) ) // lazy-init
      {
         $ARR_SLICE_MODES = array(
            /*default*/TROUND_SLICE_SNAKE,
            array(
               TROUND_SLICE_SNAKE         => T_('Snake Seeding#trd_slicemode'),
               TROUND_SLICE_ROUND_ROBIN   => T_('Round-Robin#trd_slicemode'),
               TROUND_SLICE_FILLUP_POOLS  => T_('Filling up pools#trd_slicemode'),
               TROUND_SLICE_MANUAL        => T_('Manual#trd_slicemode'),
            ));
      }

      return $ARR_SLICE_MODES;
   }//get_slice_modes

   public static function get_edit_tournament_status()
   {
      static $statuslist = array( TOURNEY_STATUS_PAIR );
      return $statuslist;
   }

   public static function delete_cache_tournament_pools( $dbgmsg, $tid, $round )
   {
      DgsCache::delete_group( $dbgmsg, CACHE_GRP_TPOOLS, "TPools.$tid.$round" );
   }

} // end of 'TournamentPool'
?>
//


koh5_pano



Your browser does not support the HTML5 canvas element.


Drag mouse to navigate.

Navigation





17.Aug.2010, Martin Wengenmayer