DIR : /home/kozerus/public_html/dgs/include/board.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/>.
*/
require_once 'include/classlib_userconfig.php';
require_once 'include/coords.php';
require_once 'include/game_functions.php';
if ( !defined('EDGE_SIZE') )
define('EDGE_SIZE', 10);
// for Board::load_from_db()
define('BOARDOPT_MARK_DEAD', 0x01);
define('BOARDOPT_LOAD_LAST_MSG', 0x02);
define('BOARDOPT_STOP_ON_FIX', 0x04); // if set: stop and return FALSE if corrupted game found
define('BOARDOPT_USE_CACHE', 0x08);
define('BOARDOPT_LOAD_ALL_MSG', 0x10); // mutual-exclusive with BOARDOPT_LOAD_LAST_MSG
define('JS_CHILDREN', '_children');
class Board implements StoneReader
{
public $gid;
public $size;
public $max_moves;
public $curr_move; // move game-board has been loaded up to
private $coord_borders = -1;
private $board_flags = 0;
private $stone_size = 25;
private $woodcolor = 1;
public $array = null; //2D array: [$PosX][$PosY] => $Stone
public $moves = array(); //array: [$MoveNr] => array($Stone,$PosX,$PosY); first MoveNr=MOVE_SETUP ('S') for shape-game
private $marks = null; //array: [$sgf_coord] => $mark
private $captures = null; //array: [$MoveNr] => array($Stone,$PosX,$PosY,$mark)
private $js_moves = array(); //array($MoveNr,$Stone,$PosX,$PosY) to build moves for JS-game-editor
private $moves_captures = null; //array: [$MoveNr] => array(sgfcoord-prisoners, ...)
private $last_captures = array(); //array: [sgfcoord] => 1 (if pos should be marked)
public $shape_arr_xy = array();
public $prisoners = array( BLACK => 0, WHITE => 0 ); //array: [BLACK|WHITE] => count
private $cond_moves = null; // array: B|W[sgf_coord], e.g. 'Baa', 'W' (=pass), 'Wee'; no tree, but flat variation
private $cond_moves_errpos = -1; // pos in $this->cond_moves of move-error
//Last move shown ($movemrkx<0 if PASS, RESIGN or SCORE)
private $movemrkx = -1;
private $movemrky = -1;
public $movecol = DAME;
public $movemsg = ''; // string '' or last-move-msg; or array( MoveNr => move-message, ...) for all messages
public $infos = array(); //extra-infos collected
// directions arrays
private static $dirx = array( -1,0,1,0 );
private static $diry = array( 0,-1,0,1 );
public $visited_points = null; // visited coords arr[x][y] set after run of has_liberty_check()-call
/*! \brief Constructor. Create a new board array and initialize it. */
public function __construct( $gid=0, $size=19, $max_moves=0 )
{
$this->gid = $gid;
$this->size = $size;
$this->max_moves = $max_moves;
}
public function init_board()
{
$this->array = array();
$this->moves = array();
$this->marks = array();
$this->captures = array();
$this->movemrkx = $this->movemrky = -1; // don't use as last move coord
$this->movecol = DAME;
$this->movemsg = '';
$this->infos = array();
$this->js_moves = array();
$this->moves_captures = array();
$this->shape_arr_xy = array();
$this->prisoners = array( BLACK => 0, WHITE => 0 );
$this->cond_moves = null;
}//init_board
private function get_last_move_mark_type()
{
/* Halloween 2015 (animated) */
return ( 0 && ( $this->stone_size == 25 ) )
? 'msa' // special last-move-marker (2015 animated)
: 'm'; // normal last-move-marker
/* Halloween 2014
return ( 0 && ( $this->stone_size == 21 || $this->stone_size == 25 ) )
? 'ms' // special last-move-marker (2014)
: 'm'; // normal last-move-marker
*/
}
public function debug_array( $sep=':' )
{
$arr = array();
foreach ( $this->array as $x => $y_arr )
{
foreach ( $y_arr as $y => $v )
$arr[] = number2board_coords( $x, $y, $this->size ) . $sep . $v;
}
return implode(' ', $arr);
}
public function set_conditional_moves( array $moves = null )
{
$this->cond_moves = ( is_array($moves) && count($moves) ) ? $moves : null;
}
public function has_conditional_moves()
{
return !is_null($this->cond_moves);
}
public function set_conditional_moves_errpos( $pos )
{
$this->cond_moves_errpos = $pos;
}
/*!
* \brief Loads move and messages into Board for given game-info and move-number.
* \param $game_row need fields: ID, Size, Moves, ShapeSnapshot
* \param $move move-number, 0=last-move, MOVE_SETUP=S=initial-setup-for-shape-game
* \param $board_opts BOARDOPT_... specifying extras to do or not do:
* BOARDOPT_MARK_DEAD, BOARDOPT_LOAD_LAST_MSG xor BOARDOPT_LOAD_ALL_MSG, BOARDOPT_STOP_ON_FIX, BOARDOPT_USE_CACHE
* \param $cache_ttl 0 = use default (see load_cache_game_moves-func); otherwise TTL in secs for caching game-moves
*
* \note fills $this->array with positions where the stones are (incl. handling of shape-game)
* \note fills $this->moves with moves and coordinates.
* \note fills $this->shape_arr_xy with parsed shape-setup as returned by GameSnapshot::parse_stones_snapshot().
* \note fills $this->prisoners with prisoner-count up to move $move
* \note fills $this->curr_move with selected move-number
* \note fills $this->movemsg with '' or string last-move-msg (for BOARDOPT_LOAD_LAST_MSG), or
* array( MoveNr => raw-comment, ...) with all move-messages for BOARDOPT_LOAD_ALL_MSG
* \note fills $this->movecol/movemrkx/movemrky with last move for last-move-marker
* \note keep the coords, color and message of the move $move.
*/
public function load_from_db( array $game_row, $move=0, $board_opts=0, $cache_ttl=0 )
{
$this->init_board();
$this->curr_move = ( $move === MOVE_SETUP ) ? 0 : $move;
$gid = $game_row['ID'];
if ( $gid <= 0 )
return FALSE;
$this->gid = $gid;
$this->size = $game_row['Size'];
$this->max_moves = $game_row['Moves'];
$shape_snapshot = @$game_row['ShapeSnapshot'];
$is_shape = (bool)$shape_snapshot;
if ( !$is_shape && $this->max_moves <= 0 )
return TRUE;
// handle goto-move (incl. shape-game)
if ( !$is_shape && $move === MOVE_SETUP )
error('invalid_args', "board.load_from_db.check.move.no_shape($gid,$move)");
if ( $is_shape && $move === MOVE_SETUP )
{
$move = 0;
$show_move_setup = true;
}
else
$show_move_setup = false;
// correct illegal move & default move=0 (-> last-move); exception for showing shape-setup of shape-game
if ( $move < 0 || ($move == 0 && !$show_move_setup) || $move > $this->max_moves )
$move = $this->max_moves;
// load moves
$use_cache = (bool)( $board_opts & BOARDOPT_USE_CACHE );
$arr_moves = self::load_cache_game_moves( 'load_from_db', $gid, $use_cache, $use_cache, $cache_ttl );
if ( $arr_moves === false )
return FALSE;
$cnt_moves = count($arr_moves);
if ( !$is_shape && $cnt_moves <= 0 )
return FALSE;
$marked_dead = array();
$removed_dead = FALSE;
// parse init-board from shape-snapshot
if ( $shape_snapshot )
{
$arr_xy = GameSnapshot::parse_stones_snapshot( $this->size, $shape_snapshot, BLACK, WHITE );
$this->shape_arr_xy = $arr_xy;
if ( count($arr_xy) )
{
$this->moves[MOVE_SETUP] = array( BLACK, POSX_SETUP, 0 );
foreach ( $arr_xy as $arr_setup )
{
list( $Stone, $PosX, $PosY ) = $arr_setup;
$this->array[$PosX][$PosY] = $Stone;
$this->js_moves[] = array( 0, $Stone, $PosX, $PosY );
}
}
}
// load moves
$fix_stop = ( $board_opts & BOARDOPT_STOP_ON_FIX );
foreach ( $arr_moves as $row )
{
extract($row); //$MoveNr, $Stone, $PosX, $PosY, $Hours
if ( $PosX <= POSX_ADDTIME ) //configuration actions
{
if ( $PosX == POSX_ADDTIME )
{
$added_by_td = ($PosY & 2);
$time_from = ($added_by_td) ? STONE_TD_ADDTIME : $Stone;
$this->infos[] = array(POSX_ADDTIME, $MoveNr, $time_from, $Stone, $Hours, ($PosY & 1));
}
continue;
}
if ( $Stone == BLACK || $Stone == WHITE )
{
$this->moves[$MoveNr] = array( $Stone, $PosX, $PosY);
$this->js_moves[] = array( $MoveNr, $Stone, $PosX, $PosY );
$this->prisoners[$Stone] += count( @$this->moves_captures[$MoveNr] );
}
if ( $MoveNr > $move )
{
if ( $MoveNr > $this->max_moves )
{
if ( $fix_stop )
return FALSE;
else
self::fix_corrupted_move_table( $gid);
break;
}
continue;
}
if ( $Stone == NONE && $PosX >= 0 ) // prisoners
$this->moves_captures[$MoveNr][] = number2sgf_coords( $PosX, $PosY, $this->size );
if ( $Stone <= WHITE ) //including NONE (prisoners)
{
if ( $PosX < 0 )
continue; //excluding PASS, RESIGN and SCORE, TIMEOUT, ADDTIME
$this->array[$PosX][$PosY] = $Stone; //including DAME (prisoners)
$removed_dead = FALSE; // restart removal
}
else if ( $Stone == MARKED_BY_WHITE || $Stone == MARKED_BY_BLACK)
{
if ( !$removed_dead )
{
$marked_dead = array(); // restart removal
$removed_dead = TRUE;
}
$marked_dead[] = array( $PosX, $PosY);
$this->js_moves[] = array( $MoveNr, $Stone, $PosX, $PosY );
}
}//scan moves
unset($arr_moves);
if ( ($board_opts & BOARDOPT_MARK_DEAD) && $removed_dead )
{
foreach ( $marked_dead as $sub )
{
list( $x, $y) = $sub;
@$this->array[$x][$y] ^= OFFSET_MARKED;
}
}
// load game-move-messages
if ( $board_opts & BOARDOPT_LOAD_ALL_MSG )
{
$move_texts = self::load_cache_game_move_message( 'load_from_db.all',
$gid, null, $use_cache, $use_cache, $cache_ttl );
if ( $move_texts !== false )
$this->movemsg = $move_texts; // array
}
else
$move_texts = null; // not loaded yet
if ( !$show_move_setup ) // move does not show shape-setup (without last-marker or move-msg)
{
if ( isset($this->moves[$move]) )
list( $this->movecol, $this->movemrkx, $this->movemrky ) = $this->moves[$move];
if ( $board_opts & BOARDOPT_LOAD_LAST_MSG )
{
// don't reload move-texts if already loaded, but then $this->movemsg is array|''
if ( is_null($move_texts) )
{
$move_text = self::load_cache_game_move_message( 'load_from_db.last',
$gid, $move, $use_cache, $use_cache, $cache_ttl );
if ( $move_text !== false )
$this->movemsg = $move_text; // string
}
}
}
return TRUE;
}//load_from_db
public static function load_cache_game_moves( $dbgmsg, $gid, $fetch_cache, $store_cache, $cache_ttl=0 )
{
$dbgmsg = "Board:load_cache_game_moves($gid,$fetch_cache,$store_cache).$dbgmsg";
$key = "Game.moves.$gid";
$arr_moves = ( $fetch_cache && DgsCache::is_persistent(CACHE_GRP_GAME_MOVES) )
? DgsCache::fetch( $dbgmsg, CACHE_GRP_GAME_MOVES, $key )
: null;
if ( is_null($arr_moves) )
{
$db_result = db_query( $dbgmsg,
"SELECT MoveNr, Stone, PosX, PosY, Hours FROM Moves AS MV WHERE MV.gid=$gid ORDER BY MoveNr" );
if ( $db_result )
{
$arr_moves = array();
while ( $row = mysql_fetch_assoc($db_result) )
$arr_moves[] = $row;
}
else
$arr_moves = false;
mysql_free_result($db_result);
if ( $store_cache )
{
$ttl = ( is_numeric($cache_ttl) && $cache_ttl > 0 ) ? $cache_ttl : 10*SECS_PER_MIN;
DgsCache::store( $dbgmsg, CACHE_GRP_GAME_MOVES, $key, $arr_moves, $ttl );
}
}
return $arr_moves;
}//load_cache_game_moves
public static function delete_cache_game_moves( $dbgmsg, $gid )
{
DgsCache::delete( $dbgmsg, CACHE_GRP_GAME_MOVES, "Game.moves.$gid" );
}
/*!
* \brief Loads game move-messages.
* \param $move null = load all moves, otherwise MoveNr for text
* \return false = nothing found; Text (if $move>0 and entry found); arr( MoveNr => Text, ...) otherwise (arr can be empty)
*/
public static function load_cache_game_move_message( $dbgmsg, $gid, $move, $fetch_cache, $store_cache, $cache_ttl=0 )
{
$dbgmsg = "board.load_cache_game_move_message($gid,$move,$fetch_cache,$store_cache).$dbgmsg";
$key = "Game.movemsg.$gid";
$query = false;
if ( $fetch_cache && DgsCache::is_persistent(CACHE_GRP_GAME_MOVEMSG) )
{
$result = DgsCache::fetch( $dbgmsg, CACHE_GRP_GAME_MOVEMSG, $key );
if ( is_null($result) )
$query = "SELECT MoveNr, Text FROM MoveMessages WHERE gid=$gid";
}
elseif ( is_numeric($move) )
$query = "SELECT MoveNr, Text FROM MoveMessages WHERE gid=$gid AND MoveNr=$move LIMIT 1";
else
$query = "SELECT MoveNr, Text FROM MoveMessages WHERE gid=$gid";
if ( $query )
{
$db_result = db_query( $dbgmsg, $query ); // single or all
if ( $db_result )
{
$result = array();
while ( $row = mysql_fetch_assoc($db_result) )
$result[$row['MoveNr']] = $row['Text'];
}
else
$result = false;
mysql_free_result($db_result);
if ( $store_cache )
{
$ttl = ( is_numeric($cache_ttl) && $cache_ttl > 0 ) ? $cache_ttl : 10*SECS_PER_MIN;
DgsCache::store( $dbgmsg, CACHE_GRP_GAME_MOVEMSG, $key, $result, $ttl );
}
}
if ( is_numeric($move) )
{
if ( isset($result[$move]) )
$result = $result[$move]; // Text-value for single-move
else
$result = false;
}
return $result;
}//load_cache_game_move_message
public static function delete_cache_game_move_messages( $dbgmsg, $gid )
{
DgsCache::delete( $dbgmsg, CACHE_GRP_GAME_MOVEMSG, "Game.movemsg.$gid" );
}
/*!
* \brief Loads game-, shape-setup- and moves-data from DGS-database to compare with SGF-parsed-data
* in verify_game_sgf()-function.
* \param $game pre-loaded Games-object, or Games-row with at least fields (ID, Size, Moves, Handicap, ShapeSnapshot)
* \param $max_moves <0 = load all moves, else restrict loaded moves by given amount
* \param $skip_pass false = include PASSes as 'B' and 'W' in resulting db-sgf-moves-arr; true = do not include PASS-moves
* \param $board pre-loaded Board-object, null = Board with moves will be loaded
* \return arr( count-moves-to-check, db-shape-setup-arr, db-sgf-moves-arr )
* - db-shape-setup-arr = arr( SGF-coord => BLACK|WHITE, ... )
* - db-sgf-moves-arr = arr( 'Baa', 'Wbb', ... ); // <COLOR><SGF_COORD>; only B/W-moves;
* PASS-moves are only returned if $skip_pass=false; captured stones are not returned
*/
public static function prepare_verify_game_sgf( $game, $max_moves, $skip_pass, Board $board = null )
{
if ( is_array($game) )
$game_row = $game;
else
{
$game_row = array(
'ID' => $game->ID,
'Size' => $game->Size,
'Moves' => $game->Moves,
'Handicap' => $game->Handicap,
'ShapeSnapshot' => $game->ShapeSnapshot,
);
}
$gid = $game_row['ID'];
$size = $game_row['Size'];
// load some of the current game-moves and shape-setup from DB to compare with SGF
$chk_cnt_moves = ( $max_moves < 0 ) ? $game_row['Moves'] : min( $max_moves, $game_row['Moves'] );
if ( is_null($board) )
{
$board = new Board();
if ( !$board->load_from_db( $game_row, $chk_cnt_moves, BOARDOPT_USE_CACHE ) )
error('internal_error', "GameSgfControl:prepare_verify_game_sgf.check.moves($gid,$chk_cnt_moves,$skip_pass)");
}
// prepare shape-setup from db
$db_shape_setup = array(); // arr( SGF-coord => BLACK|WHITE, ... )
foreach ( $board->shape_arr_xy as $arr )
{
list( $Stone, $PosX, $PosY) = $arr;
$sgf_coord = number2sgf_coords( $PosX, $PosY, $size, $size );
$db_shape_setup[$sgf_coord] = $Stone;
}
// prepare to verify some moves: read some db-moves
$db_sgf_moves = array(); // Baa, Wbb, B (=PASS) ...
$count_handicap = $game_row['Handicap'];
$check_posx = ( $skip_pass ) ? 0 : POSX_PASS;
foreach ( $board->moves as $arr ) // arr: Stone,PosX,PosY
{
list( $Stone, $PosX, $PosY) = $arr;
if ( $PosX < $check_posx ) // check for valid move (or PASS, if PASS shouldn't be skipped)
continue;
if ( $Stone == BLACK )
$color = 'B';
elseif ( $Stone == WHITE )
$color = 'W';
else
continue;
$sgf_coord = ( $PosX >= 0 ) ? number2sgf_coords( $PosX, $PosY, $size, $size ) : ''; // ''=PASS
if ( $count_handicap > 0 ) // put handicap-stones into shape-setup-arr
{
$count_handicap--;
if ( $sgf_coord )
$db_shape_setup[$sgf_coord] = $Stone;
}
else
$db_sgf_moves[] = $color . $sgf_coord;
}
return array( $chk_cnt_moves, $db_shape_setup, $db_sgf_moves );
}//prepare_verify_game_sgf
/*!
* \brief Fills $array with positions where the stones are (incl. handling of shape-game).
* \note only used for shape-checking, not to control board
*/
public function build_from_shape_snapshot( $size, $shape_snapshot )
{
$this->init_board();
$this->size = $size;
// parse init-board from shape-snapshot
if ( $shape_snapshot )
{
$arr_xy = GameSnapshot::parse_stones_snapshot( $this->size, $shape_snapshot, BLACK, WHITE );
foreach ( $arr_xy as $arr_setup )
{
list( $Stone, $PosX, $PosY ) = $arr_setup;
$this->array[$PosX][$PosY] = $Stone;
}
}
}//build_from_shape_snapshot
/*!
* \brief Adds handicap stones to board-array.
* \param $coords list with coordinates-array: [ (x,y), ... ]
*/
public function add_handicap_stones( array $coords )
{
foreach ( $coords as $coord )
{
list( $x, $y ) = $coord;
if ( !isset($x) || !isset($y) || @$this->array[$x][$y] != NONE )
error('illegal_position', "board.add_handicap_stones({$this->gid},$x,$y)");
$this->array[$x][$y] = BLACK;
}
}//add_handicap_stones
public function set_move_mark( $x=-1, $y=-1)
{
$this->movemrkx= $x;
$this->movemrky= $y;
}
public function move_marks( $start, $end, $mark=0, $next_move_marked=false )
{
if ( $this->cond_moves ) // conditional-moves-preview overwrites move-numbering
{
$moves = $this->cond_moves;
$is_relative = true;
$is_reverse = false;
$mark = 0;
$start = 0;
$end = ( $this->cond_moves_errpos < 0 ) ? count($moves) - 1 : $this->cond_moves_errpos;
}
else
{
$moves = $this->moves;
$is_relative = ($this->coord_borders & COORD_RELATIVE_MOVENUM);
$is_reverse = ($this->coord_borders & COORD_REVERSE_MOVENUM);
$start = max( $start, 1 );
}
if ( is_string($mark) )
$mod = 0;
elseif ( is_numeric($mark) )
{
if ( $mark > 0 )
$mod = $mark;
else
$mod = MAX_MOVENUMBERS;
$mod = min( $mod, MAX_MOVENUMBERS);
if ( $mod <= 1 )
return;
$mark = '';
}
else
return;
if ( $next_move_marked )
{
$start--;
$delta = 1;
}
else
$delta = 0;
$movenums = count($moves);
if ( !$this->cond_moves && isset($moves[MOVE_SETUP]) ) // shape-game-setup
$movenums--;
for ( $n=$end; $n>=$start; $n-- )
{
$move_idx = $n + $delta;
if ( isset($moves[$move_idx]) )
{
list( $s, $x, $y) = $moves[$move_idx];
if ( $x == POSX_PASS )
continue;
//if ( $s != BLACK && $s != WHITE ) continue;
$sgfc = number2sgf_coords( $x, $y, $this->size);
if ( $sgfc )
{
if ( $mod > 1 )
{
$movelabel = $n;
if ( $is_reverse )
$movelabel = ($is_relative ? $end : $movenums) - $movelabel;
elseif ( $is_relative )
$movelabel = $movelabel - $start + 1;
$mrk = (($movelabel-1) % $mod)+1;
}
else
$mrk = $mark;
if ( !isset($this->marks[$sgfc]) )
{
$b = @$this->array[$x][$y];
if ( ($b % OFFSET_MARKED) == $s ) // || $s==($b^OFFSET_MARKED)
{
if ( $mrk > '' )
$this->marks[$sgfc] = $mrk;
continue;
}
if ( $b==NONE )
$this->marks[$sgfc] = 'x';
}
$this->captures[$move_idx] = array( $s, $x, $y, $mrk);
}
}
}
}//move_marks
/*! \brief Returns true, if scoring is allowed. */
public function is_scoring_step( $move, $game_status )
{
// last move and on scoring game-status
if ( $move == $this->max_moves && ($game_status == GAME_STATUS_SCORE || $game_status == GAME_STATUS_SCORE2) )
return true;
// current move is SCORE-step
$curr_move = @$this->moves[$move][1];
if ( $curr_move == POSX_SCORE )
return true;
return false;
}//is_scoring_step
public function draw_captures_box( $caption)
{
if ( !is_array($this->captures) || $this->cond_moves_errpos >= 0 )
return false;
$n= count($this->captures);
if ( $n < 1 )
return false;
$stone_size = $this->stone_size;
$size = $this->size;
$numover = $this->coord_borders & NUMBER_OVER ;
$stone_attb = 'class=capt';
$wcap= array();
$bcap= array();
foreach ( $this->captures as $n => $sub )
{
list( $s, $x, $y, $mrk) = $sub;
if ( $numover )
{
$tit= (string)$n;
if ( is_numeric($mrk) )
$mrk= ''; //no mark if $numover
}
else
$tit= '';
$brdc = number2board_coords( $x, $y, $size);
if ( $s == BLACK )
{
array_unshift( $bcap,
image( "$stone_size/b$mrk.gif", "X$n", $tit, $stone_attb)
. " : $brdc" );
}
else if ( $s == WHITE )
{
array_unshift( $wcap,
image( "$stone_size/w$mrk.gif", "O$n", $tit, $stone_attb)
. " : $brdc" );
}
}
echo "<table class=Captures>\n";
echo "<tr>\n<th colspan=2>$caption</th>\n </tr>\n";
echo "<tr>\n<td class=b>\n";
if ( count($bcap)>0 )
{
echo '<dl><dt>'.implode("</dt\n><dt>", $bcap)."</dt\n></dl>";
//echo implode("<br>\n", $bcap);
}
echo "</td>\n<td class=w>\n";
if ( count($wcap)>0 )
{
echo '<dl><dt>'.implode("</dt\n><dt>", $wcap)."</dt\n></dl>";
//echo implode("<br>\n", $wcap);
}
echo "</td>\n</tr>\n";
echo "</table>\n";
return true;
}//draw_captures_box
public function mark_last_captures( $move )
{
$this->last_captures = array();
if ( is_array($this->moves_captures) && isset($this->moves_captures[$move]) )
{
foreach ( $this->moves_captures[$move] as $sgfc )
$this->last_captures[$sgfc] = 1;
}
return count($this->last_captures);
}//mark_last_captures
public function draw_last_captures_box( $caption )
{
if ( !is_array($this->last_captures) )
return false;
$cnt_captures = count($this->last_captures);
$arr = array();
foreach ( $this->last_captures as $sgfc => $val )
{
list( $x, $y ) = sgf2number_coords( $sgfc, $this->size );
$arr[] = number2board_coords( $x, $y, $this->size );
}
if ( $cnt_captures == 0 )
$arr[] = NO_VALUE;
$capture_str = wordwrap( implode(', ', $arr), 20, "<br>\n", false );
echo "<table class=Captures>\n",
"<tr>\n<th>$caption</th>\n </tr>\n",
"<tr><td>",
sprintf('#%d: %s', $cnt_captures, $capture_str),
"</td></tr>\n",
"</table>\n";
return true;
}//draw_last_captures_box
public function set_style( ConfigBoard $cfg_board )
{
$board_coords = $cfg_board->get_board_coords();
if ( is_numeric($board_coords) )
$this->coord_borders = $board_coords;
else
$this->coord_borders = -1;
$this->board_flags = $cfg_board->get_board_flags();
$this->stone_size = $cfg_board->get_stone_size();
$this->woodcolor = $cfg_board->get_wood_color();
}//set_style
// keep in sync with GobanHandlerGfxBoard
public function style_string()
{
$stone_size = $this->stone_size;
$coord_width = floor($stone_size*31/25);
$tmp = "img.%s{ width:%dpx; height:%dpx;}\n";
$str = sprintf( $tmp, 'brdx', $stone_size, $stone_size) //board stones
. sprintf( $tmp, 'brdl', $stone_size, $stone_size) //letter coords
. sprintf( $tmp, 'brdn', $coord_width, $stone_size) //num coords
. sprintf( $tmp, 'capt', $stone_size, $stone_size) //capture box
;
$tmp = "td.%s{ width:%dpx; height:%dpx;}\n";
$str.= sprintf( $tmp, 'brdx', $stone_size, $stone_size) //board stones
. sprintf( $tmp, 'brdl', $stone_size, $stone_size) //letter coords
. sprintf( $tmp, 'brdn', $coord_width, $stone_size) //num coords
;
return $str;
}//style_string
// keep in sync with GobanHandlerGfxBoard
private function draw_coord_row( $coord_start_letter, $coord_alt, $coord_end, $coord_left, $coord_right )
{
echo "<tr>\n";
if ( $coord_left )
echo $coord_left;
$colnr = 1;
$letter = 'a';
while ( $colnr <= $this->size )
{
echo $coord_start_letter . $letter . $coord_alt . $letter . $coord_end;
$colnr++;
$letter++; if ( $letter == 'i' ) $letter++;
}
if ( $coord_right )
echo $coord_right;
echo "</tr>\n";
}//draw_coord_row
// keep in sync with GobanHandlerGfxBoard
private function draw_edge_row( $edge_start, $edge_coord, $border_start, $border_imgs, $border_rem )
{
echo "<tr>\n";
if ( $this->coord_borders & COORD_LEFT )
echo $edge_coord;
echo '<td>' . $edge_start . 'l.gif" width='.EDGE_SIZE.'>' . "</td>\n";
echo '<td colspan=' . $this->size . ' width=' . $this->size*$this->stone_size . '>';
if ( $border_imgs >= 0 )
echo $edge_start . '.gif" width=' . $border_start . '>';
for ($i=0; $i<$border_imgs; $i++ )
echo $edge_start . '.gif" width=150>';
echo $edge_start . '.gif" width=' . $border_rem . '>';
echo "</td>\n" . '<td>' . $edge_start . 'r.gif" width='.EDGE_SIZE.'>' . "</td>\n";
if ( $this->coord_borders & COORD_RIGHT )
echo $edge_coord;
echo "</tr>\n";
}//draw_edge_row
// keep in sync with GobanHandlerGfxBoard
// board: img.alt-attr mapping: B>X W>O, last-move B># W>@, dead B>x W>o, terr B>+ W>-, dame>. seki-dame>s hoshi>, else>.
// \param $action GAMEACT_SET_HANDICAP, 'remove', or other text
// \param $draw_move_msg array( move-title, move-message ) to show above board in message-box, null=no move-msg
public function draw_board( $may_play=false, $action='', $stonestring='', array $draw_move_msg=null )
{
global $woodbgcolors;
if ( ($gid=$this->gid) <= 0 )
$may_play= false;
$has_move_msg = ( is_array($draw_move_msg) && count($draw_move_msg) == 2 );
$show_movemsg_above = ($this->board_flags & BOARDFLAG_MOVEMSG_ABOVE_BOARD);
$stone_size = $this->stone_size;
$coord_width = floor($stone_size*31/25);
$mark_last_capture = ($this->board_flags & BOARDFLAG_MARK_LAST_CAPTURE );
$smooth_edge = ( ($this->coord_borders & SMOOTH_EDGE) && ($this->woodcolor < 10) );
if ( $smooth_edge )
{
$border_start = 140 - ( $this->coord_borders & COORD_LEFT ? $coord_width : 0 );
$border_imgs = ceil( ($this->size * $stone_size - $border_start) / 150 ) - 1;
$border_rem = $this->size * $stone_size - $border_start - 150 * $border_imgs;
if ( $border_imgs < 0 )
$border_rem = $this->size * $stone_size;
$edge_coord = '<td><img alt=" " height='.EDGE_SIZE.' src="images/blank.gif" width=' . $coord_width . "></td>\n";
$edge_start = '<img alt=" " height='.EDGE_SIZE.' src="images/wood' . $this->woodcolor . '_' ;
$edge_vert = '<img alt=" " height=' . $stone_size . ' width='.EDGE_SIZE.' src="images/wood' . $this->woodcolor . '_' ;
}
$coord_alt = '.gif" alt="';
$coord_end = "\"></td>\n";
if ( $this->coord_borders & (COORD_LEFT | COORD_RIGHT) )
{
$coord_start_number = "<td class=brdn><img class=brdn src=\"$stone_size/c";
}
if ( $this->coord_borders & (COORD_UP | COORD_DOWN) )
{
$coord_start_letter = "<td class=brdl><img class=brdl src=\"$stone_size/c";
$s = ($this->coord_borders & COORD_LEFT ? 1 : 0 ) + ( $smooth_edge ? 1 : 0 );
if ( $s )
$coord_left = "<td colspan=$s><img src=\"images/blank.gif\" width=" .
( ( $this->coord_borders & COORD_LEFT ? $coord_width : 0 )
+ ( $smooth_edge ? EDGE_SIZE : 0 ) ) .
" height=$stone_size alt=\" \"></td>\n";
else
$coord_left = '';
$s = ($this->coord_borders & COORD_RIGHT ? 1 : 0 ) + ( $smooth_edge ? 1 : 0 );
if ( $s )
$coord_right = "<td colspan=$s><img src=\"images/blank.gif\" width=" .
( ( $this->coord_borders & COORD_RIGHT ? $coord_width : 0 )
+ ( $smooth_edge ? EDGE_SIZE : 0 ) ) .
" height=$stone_size alt=\" \"></td>\n";
else
$coord_right = '';
}
$nomove_start = "<td id=%s class=brdx><img class=brdx alt=\"";
$nomove_src = "\" src=\"$stone_size/";
$nomove_end = ".gif\"></td>\n";
if ( $may_play )
{
switch ( (string)$action )
{
case GAMEACT_SET_HANDICAP:
$on_not_empty = false;
$on_empty = true;
$move_start = "<td id=%s class=brdx><a href=\"game.php?g=$gid".URI_AMP."a=".GAMEACT_SET_HANDICAP.URI_AMP."c=";
$move_alt = "\"><img class=brdx alt=\"";
if ( $stonestring )
$move_alt = URI_AMP."s=$stonestring".$move_alt;
break;
case 'remove':
$on_not_empty = true;
if ( MAX_SEKI_MARK>0 )
$on_empty = true;
else
$on_empty = false;
$move_start = "<td id=%s class=brdx><a href=\"game.php?g=$gid".URI_AMP."a=remove".URI_AMP."c=";
$move_alt = "\"><img class=brdx alt=\"";
if ( $stonestring )
$move_alt = URI_AMP."s=$stonestring".$move_alt;
break;
default:
$on_not_empty = false;
$on_empty = true;
$move_start = "<td id=%s class=brdx><a href=\"game.php?g=$gid".URI_AMP."a=domove".URI_AMP."c=";
$move_alt = "\"><img class=brdx alt=\"";
break;
}
$move_src = "\" src=\"$stone_size/";
$move_end = ".gif\"></a></td>\n";
}
if ( $show_movemsg_above && $has_move_msg )
$this->draw_move_message( $draw_move_msg[1], $draw_move_msg[0] );
{ // draw goban
/**
* style="background-image..." is not understood by old browsers like Netscape Navigator 4.0
* meanwhile background="..." is not W3C compliant
* so, for those old browsers, use the bgcolor="..." option
**/
if ( $this->woodcolor > 10 )
$woodstring = ' bgcolor="' . $woodbgcolors[$this->woodcolor - 10] . '"';
else
$woodstring = ' style="background-image:url(images/wood' . $this->woodcolor . '.gif);"';
/**
* Some simple browsers (like Pocket PC IE or PALM ones) poorly
* manage the CSS commands related to cellspacing and cellpadding.
* Most of the time, this results in a 1 or 2 pixels added to the
* cells size and is not so disturbing into normal tables.
* But this is really annoying for the board cells.
* So we keep the HTML commands here, even if deprecated.
**/
$cell_size_fix = ' border=0 cellspacing=0 cellpadding=0';
echo '<table id=Goban class=Goban' . $woodstring . $cell_size_fix . '>';
echo '<tbody>';
if ( $this->coord_borders & COORD_UP )
$this->draw_coord_row( $coord_start_letter, $coord_alt, $coord_end,
$coord_left, $coord_right );
if ( $smooth_edge )
$this->draw_edge_row( $edge_start.'u', $edge_coord,
$border_start, $border_imgs, $border_rem );
$letter_r = 'a';
for ($rownr = $this->size; $rownr > 0; $rownr-- )
{
echo '<tr>';
if ( $this->coord_borders & COORD_LEFT )
echo $coord_start_number . $rownr . $coord_alt . $rownr .$coord_end;
if ( $smooth_edge )
echo '<td>' . $edge_vert . "l.gif\"></td>\n";
$letter = 'a';
$letter_c = 'a';
for ($colnr = 0; $colnr < $this->size; $colnr++ )
{
$stone = (int)@$this->array[$colnr][$this->size-$rownr];
$empty = false;
$marked = false;
if ( $stone & FLAG_NOCLICK ) {
$stone &= ~FLAG_NOCLICK;
$no_click = true;
}
else
$no_click = false;
if ( $stone == BLACK )
{
$type = 'b';
$alt = 'X';
}
else if ( $stone == WHITE )
{
$type = 'w';
$alt = 'O';
}
else if ( $stone == BLACK_DEAD )
{
$type = 'bw';
$alt = 'x';
$marked = true;
}
else if ( $stone == WHITE_DEAD )
{
$type = 'wb';
$alt = 'o';
$marked = true;
}
else
{
$empty = true;
$type = 'e';
$alt = '.';
if ( $rownr == 1 ) $type = 'd';
else if ( $rownr == $this->size ) $type = 'u';
if ( $colnr == 0 ) $type .= 'l';
else if ( $colnr == $this->size-1 ) $type .= 'r';
if ( $stone == BLACK_TERRITORY )
{
$type .= 'b';
$alt = '+';
$marked = true;
}
else if ( $stone == WHITE_TERRITORY )
{
$type .= 'w';
$alt = '-';
$marked = true;
}
else if ( $stone == DAME )
{
$type .= 'd';
$alt = '.';
$marked = true;
}
else if ( $stone == MARKED_DAME )
{
$type .= 'g';
$alt = 's'; //for seki
$marked = true;
}
if ( $type=='e' )
{
if ( is_hoshi($colnr, $this->size-$rownr, $this->size) )
{
$type = 'h';
$alt = ',';
}
}
}
$sgfc = number2sgf_coords($colnr, $this->size-$rownr, $this->size);
$mrk = '';
$numover = false;
if ( is_array($this->marks) && $sgfc && @$this->marks[$sgfc] )
{
$mrk = $this->marks[$sgfc];
if ( is_numeric($mrk) && ($this->coord_borders & NUMBER_OVER) )
$numover = true;
}
if ( !$mrk && $sgfc && $mark_last_capture && @$this->last_captures[$sgfc] )
$mrk = 'x';
$img_id = '';
if ( !$marked )
{
if ( !$empty && ( $stone == BLACK || $stone == WHITE )
&& $this->movemrkx == $colnr
&& $this->movemrky == $this->size-$rownr )
{ //last move mark
$type .= $this->get_last_move_mark_type();
$alt = ( $stone == BLACK ? '#' : '@' );
$img_id = 'lastMove';
$marked = true;
}
elseif ( $mrk )
{
//$alt .= $mrk;
if ( !$numover )
{
$type .= $mrk;
$marked = true;
} //else no mark if $numover
}
}
$tit = '';
if ( $numover )
$tit = $mrk;
if ( $this->coord_borders & COORD_OVER )
//strtoupper? -> change capturebox too
$tit = ( $tit ? $tit.' - ' : '' ) . $letter.$rownr;
if ( $this->coord_borders & COORD_SGFOVER )
$tit = ( $tit ? $tit.' - ' : '' ) . $sgfc;
if ( $tit )
$alt.= "\" title=\"$tit";
if ( $img_id )
$img_id = "\" id=\"$img_id";
if ( $may_play && !$no_click &&
( ($empty && $on_empty) || (!$empty && $on_not_empty) ) )
echo sprintf($move_start, $sgfc) . "$letter_c$letter_r$move_alt$alt$img_id$move_src$type$move_end";
else
echo sprintf($nomove_start, $sgfc) . "$alt$img_id$nomove_src$type$nomove_end";
$letter_c++;
$letter++; if ( $letter == 'i' ) $letter++;
}//colnr
if ( $smooth_edge )
echo '<td>' . $edge_vert . "r.gif\"></td>\n";
if ( $this->coord_borders & COORD_RIGHT )
echo $coord_start_number . $rownr . $coord_alt . $rownr .$coord_end;
echo "</tr>\n";
$letter_r++;
}//rownr
if ( $smooth_edge )
$this->draw_edge_row( $edge_start.'d', $edge_coord,
$border_start, $border_imgs, $border_rem );
if ( $this->coord_borders & COORD_DOWN )
$this->draw_coord_row( $coord_start_letter, $coord_alt, $coord_end,
$coord_left, $coord_right );
echo '</tbody>';
echo "</table>\n";
} //goban
if ( !$show_movemsg_above && $has_move_msg )
$this->draw_move_message( $draw_move_msg[1], $draw_move_msg[0] );
}//draw_board
//$coord_borders and $movemsg stay local.
public function draw_ascii_board( $movemsg='', $coord_borders=COORD_MASK)
{
$out = "\n";
if ( (string)$movemsg != '' )
$out .= wordwrap("Message: $movemsg", 47) . "\n\n";
if ( $coord_borders & COORD_UP )
{
$out .= ' ';
if ( $coord_borders & COORD_LEFT )
$out .= ' ';
$colnr = 1;
$letter = 'a';
while ( $colnr <= $this->size )
{
$out .= " $letter";
$colnr++;
$letter++; if ( $letter == 'i' ) $letter++;
}
$out .= "\n";
}
$letter_r = 'a';
for ($rownr = $this->size; $rownr > 0; $rownr-- )
{
$out .= ' ';
if ( $coord_borders & COORD_LEFT )
$out .= str_pad($rownr, 2, ' ', STR_PAD_LEFT);
$pre_mark = false;
$letter_c = 'a';
for ($colnr = 0; $colnr < $this->size; $colnr++ )
{
$stone = (int)@$this->array[$colnr][$this->size-$rownr];
$empty = false;
if ( $stone == BLACK )
$type = 'X';
else if ( $stone == WHITE )
$type = 'O';
else if ( $stone == BLACK_DEAD )
$type = 'x';
else if ( $stone == WHITE_DEAD )
$type = 'o';
else
{
$empty = true;
$type = '.';
if ( $stone == BLACK_TERRITORY )
$type .= '+';
else if ( $stone == WHITE_TERRITORY )
$type .= '-';
else if ( $stone == DAME )
$type .= '.';
else if ( $stone == MARKED_DAME )
$type .= 's'; //for seki
if ( $type=='.' )
{
if ( is_hoshi($colnr, $this->size-$rownr, $this->size) )
$type = ',';
}
}
if ( $pre_mark )
{
$out .= ")$type";
$pre_mark = false;
}
else if ( !$empty && ( $stone == BLACK || $stone == WHITE )
&& $this->movemrkx == $colnr
&& $this->movemrky == $this->size-$rownr )
{
$out .= "($type";
$pre_mark = true;
}
else
$out .= " $type";
$letter_c++;
}
$out .= ( $pre_mark ? ')' : ' ' );
if ( $coord_borders & COORD_RIGHT )
$out .= str_pad($rownr, 2, ' ', STR_PAD_RIGHT);
$letter_r++;
$out .= "\n";
}
if ( $coord_borders & COORD_DOWN )
{
$out .= ' ';
if ( $coord_borders & COORD_LEFT )
$out .= ' ';
$colnr = 1;
$letter = 'a';
while ( $colnr <= $this->size )
{
$out .= " $letter";
$colnr++;
$letter++; if ( $letter == 'i' ) $letter++;
}
$out .= "\n";
}
return $out;
}//draw_ascii_board
public function draw_move_message( $msg, $title=null )
{
if ( (string)$msg != '' )
{
$width = $this->stone_size * 19;
$title_row = ($title) ? "<tr><th width=\"$width\">$title</th></tr>" : '';
echo "<table class=GameMessageBox>{$title_row}<tr><td width=\"$width\">$msg</td></tr></table>\n";
}
}
/*!
* \brief Returns true if group at position (x,y) has at least one liberty.
* \param $start_x/y board-position to check, 0..n
* \param $prisoners if $remove is set, pass-back extended prisoners-array if group at x,y has no liberty
* \param $remove if true, remove captured stones
* \note fill this.visited_points if set as an array
*/
public function has_liberty_check( $start_x, $start_y, array &$prisoners, $remove )
{
$color = @$this->array[$start_x][$start_y]; // Color of this stone
$arr_xy = array( $start_x, $start_y );
$stack = array( $arr_xy );
$visited = array(); // potential prisoners and marker if point already checked
$visited[$start_x][$start_y] = $arr_xy;
$fill_visit = !is_null($this->visited_points);
if ( $fill_visit )
$this->visited_points[$start_x][$start_y] = 1;
// scanning all directions starting at start-x/y building up a stack of adjacent points to check
while ( $arr_xy = array_shift($stack) )
{
list( $x, $y ) = $arr_xy;
for ( $dir=0; $dir < 4; $dir++) { // scan all directions: W N E S
$new_x = $x + self::$dirx[$dir];
$new_y = $y + self::$diry[$dir];
if ( ($new_x >= 0 && $new_x < $this->size) && ($new_y >= 0 && $new_y < $this->size) )
{
$new_color = @$this->array[$new_x][$new_y];
if ( !$new_color || $new_color == NONE )
return true; // found liberty
elseif ( $new_color == $color && !@$visited[$new_x][$new_y] )
{
$arr_xy = array( $new_x, $new_y );
$stack[] = $arr_xy;
$visited[$new_x][$new_y] = $arr_xy;
if ( $fill_visit )
$this->visited_points[$new_x][$new_y] = 1;
}
}
}
}
if ( $remove )
{
foreach ( $visited as $x => $arr_y )
{
foreach ( $arr_y as $y => $arr_xy )
{
$prisoners[] = $arr_xy;
unset($this->array[$x][$y]);
}
}
}
return false;
}//has_liberty_check
/*!
* \brief Checks if stone/move at [colnr/rownr] captures some stone(s).
* \return true, if some stones captured (in which case $prisoners-arg contains the x/y-coords of the prisoners)
*/
public function check_prisoners( $colnr, $rownr, $col, array &$prisoners )
{
$some = false;
for ($i=0; $i<4; $i++) // determine captured stones for ALL directions
{
$x = $colnr + self::$dirx[$i];
$y = $rownr + self::$diry[$i];
if ( @$this->array[$x][$y] == $col )
{
if ( !$this->has_liberty_check( $x, $y, $prisoners, true) ) // change $prisoners
$some = true; // can't exit before all prisoners collected
}
}
return $some;
}//check_prisoners
/*!
* \brief Returns number of marked territory points at position (x,y).
* \param $start_x/y board-position to check, 0..n
*/
public function mark_territory( $start_x, $start_y )
{
$stack = array( array( $start_x, $start_y ) );
$visited = array(); // marker if point already checked
// current point
$visited[$start_x][$start_y] = 1;
$point_count = 1; // number of checked points, may be > count of visited points
// scanning all directions starting at start-x/y building up a stack of adjacent points to check
$color = -1; // color of territory
while ( $arr_xy = array_shift($stack) )
{
list( $x, $y ) = $arr_xy;
for ( $dir=0; $dir < 4; $dir++) { // scan all directions: W N E S
$new_x = $x + self::$dirx[$dir];
$new_y = $y + self::$diry[$dir];
if ( ($new_x >= 0 && $new_x < $this->size) && ($new_y >= 0 && $new_y < $this->size) && !@$visited[$new_x][$new_y] )
{
$new_color = @$this->array[$new_x][$new_y];
if ( !$new_color || $new_color == NONE || $new_color >= BLACK_DEAD )
{
$stack[] = array( $new_x, $new_y );
$visited[$new_x][$new_y] = 1;
++$point_count;
}
else //remains BLACK/WHITE/DAME/BLACK_TERRITORY/WHITE_TERRITORY and MARKED_DAME
{
if ( $new_color == MARKED_DAME )
$color = NONE; // This area will become dame
else if ( $color == -1 )
$color = $new_color;
else if ( $color == (WHITE + BLACK - $new_color) )
$color = NONE; // This area has both colors as boundary
}
}
}
}
if ( $color == -1 )
$color = DAME ;
else
$color |= OFFSET_TERRITORY;
if ( $color == DAME || $point_count > MAX_SEKI_MARK )
$color |= FLAG_NOCLICK;
foreach ( $visited as $x => $arr_y )
{
foreach ( $arr_y as $y => $val )
{
//keep all marks unchanged and reversible
if ( @$this->array[$x][$y] < MARKED_DAME )
$this->array[$x][$y] = $color;
}
}
//error_log("Board.mark_territory(".number2board_coords($start_x,$start_y,$this->size).") = $point_count");
return $point_count;
}//mark_territory
/*!
* \brief Calculates game-score-data.
* \param $with_board_status enriches GameScore-object with board-status (used for quick-scoring)
* \return if $with_coords is false, return null; otherwise returns
* [ territory-array, prisoners-array ] with [ coords => SGF-prop, ...] for SGF-download
*/
public function fill_game_score( GameScore &$game_score, $with_coords=false, $with_board_status=false )
{
// mark territory
for ( $x=0; $x<$this->size; $x++)
{
for ( $y=0; $y<$this->size; $y++)
{
if ( !@$this->array[$x][$y] || $this->array[$x][$y] == NONE )
$this->mark_territory( $x, $y);
}
}
// count
$counts = array(
DAME => 0,
BLACK => 0, BLACK_DEAD => 0, BLACK_TERRITORY => 0,
WHITE => 0, WHITE_DEAD => 0, WHITE_TERRITORY => 0,
);
if ( $with_board_status )
{
$counts[MARKED_DAME] = 0; // =neutral, only for board-status
$bs = new BoardStatus();
$game_score->set_board_status( $bs );
}
else
$bs = null;
$territory = array();
$prisoners = array();
for ( $x=0; $x<$this->size; $x++)
{
for ( $y=0; $y<$this->size; $y++)
{
if ( $bs || $with_coords )
$coord = chr($x + ord('a')) . chr($y + ord('a'));
# 0=NONE, 1=BLACK, 2=WHITE, 4=DAME, 5=BLACK_TERRITORY, 6=WHITE_TERRITORY, 9=BLACK_DEAD, 10=WHITE_DEAD
$mark = ( @$this->array[$x][$y] & ~FLAG_NOCLICK );
if ( isset($counts[$mark]) )
{
$counts[$mark]++;
if ( $bs )
$bs->add_coord( $mark, $coord );
}
if ( $with_coords )
{
switch ( (int)$mark )
{
case WHITE_DEAD:
$prisoners[$coord] = 'AE';
case BLACK_TERRITORY:
$territory[$coord] = 'TB';
break;
case BLACK_DEAD:
$prisoners[$coord] = 'AE';
case WHITE_TERRITORY:
$territory[$coord] = 'TW';
break;
}
}
}
}
// fill
$game_score->set_stones_all( $counts[BLACK], $counts[WHITE] );
$game_score->set_dead_stones_all( $counts[BLACK_DEAD], $counts[WHITE_DEAD] );
$game_score->set_territory_all( $counts[BLACK_TERRITORY], $counts[WHITE_TERRITORY] );
$game_score->set_dame( $counts[DAME] );
//error_log($game_score->to_string());
return ( $with_coords ) ? array( $territory, $prisoners ) : null;
}//fill_game_score
/*!
* \brief Toggles marked area for scoring starting at position (x,y).
* \param $start_x/y board-position to check, 0..n
* \param $marked pass-back marked x/y-points
* \param $toggled array[x][y] with toggled points to avoid re-toggling (used for unique-toggle-mode of quick-suite)
*/
public function toggle_marked_area( $start_x, $start_y, array &$marked, array &$toggled = null, $companion_groups = true )
{
$color = @$this->array[$start_x][$start_y]; // Color of this stone
/***
* Actually, $opposite_dead force an already marked dead neighbour group from the
* opposite color to reverse to not dead, but this does not work properly if
* $companion_groups is not true, as both groups may be not touching themself.
***/
if ( $companion_groups && ($color == BLACK || $color == WHITE) )
$opposite_dead = WHITE + BLACK_DEAD - $color;
else
$opposite_dead = -1 ;
$arr_xy = array( $start_x, $start_y );
$stack = array( $arr_xy );
$visited = array(); // marker if point already checked
$visited[$start_x][$start_y] = $arr_xy;
$has_toggle = is_array($toggled);
if ( $has_toggle )
self::visit_xy($toggled, $start_x, $start_y);
// scanning all directions starting at start-x/y building up a stack of adjacent points to check
while ( $arr_xy = array_shift($stack) )
{
list( $x, $y ) = $arr_xy;
for ( $dir=0; $dir < 4; $dir++) { // scan all directions: W N E S
$new_x = $x + self::$dirx[$dir];
$new_y = $y + self::$diry[$dir];
if ( ($new_x >= 0 && $new_x < $this->size) && ($new_y >= 0 && $new_y < $this->size) && !@$visited[$new_x][$new_y] )
{
$new_color = @$this->array[$new_x][$new_y];
if ( $new_color == $color || ( $companion_groups && $new_color == NONE ) )
{
$stack[] = array( $new_x, $new_y, $color );
$visited[$new_x][$new_y] = array( $new_x, $new_y );
if ( $has_toggle )
self::visit_xy($toggled, $new_x, $new_y);
}
elseif ( $new_color == $opposite_dead )
{
$this->toggle_marked_area( $new_x, $new_y, $marked, $toggled, $companion_groups);
}
}
}
}
foreach ( $visited as $x => $arr_y )
{
foreach ( $arr_y as $y => $arr_xy )
{
if ( $color == @$this->array[$x][$y] ) {
if ( !$has_toggle || @$toggled[$x][$y] == 1 )
{
$marked[] = $arr_xy;
self::toggle_mask_xy($this->array, $x, $y, OFFSET_MARKED);
}
}
}
}
}//toggle_marked_area
private static function visit_xy( array &$arr, $x, $y )
{
if ( isset($arr[$x][$y]) )
$arr[$x][$y]++;
else
$arr[$x][$y] = 1;
}
private static function toggle_mask_xy( array &$arr, $x, $y, $mask )
{
if ( isset($arr[$x][$y]) )
$arr[$x][$y] ^= $mask;
else
$arr[$x][$y] = $mask;
}
// If move update was interupted between two mysql queries, there may
// be extra entries in the Moves and MoveMessages tables.
// \internal
private static function fix_corrupted_move_table( $gid)
{
$row= mysql_single_fetch( "board.fix_corrupted_move_table.moves: $gid",
"SELECT Moves FROM Games WHERE ID=$gid" );
if ( !$row )
error('internal_error', "board.fix_corrupted_move_table.moves($gid)");
$Moves = $row['Moves'];
$row = mysql_single_fetch( "board.fix_corrupted_move_table.max: $gid",
"SELECT MAX(MoveNr) AS max FROM Moves WHERE gid=$gid" );
if ( !$row )
error('internal_error', "board.fix_corrupted_move_table.max($gid)");
$max_movenr= $row['max'];
if ($max_movenr == $Moves)
return;
if ( $max_movenr != $Moves+1 )
error('database_corrupted', "board.fix_corrupted_move_table.unfixable($gid)"); // Can't handle this type of problem
ta_begin();
{//HOT-section to fix Moves-table
db_query( "board.fix_corrupted_move_table.delete_moves($gid)",
"DELETE FROM Moves WHERE gid=$gid AND MoveNr=$max_movenr" );
db_query( "board.fix_corrupted_move_table.delete_move_mess($gid)",
"DELETE FROM MoveMessages WHERE gid=$gid AND MoveNr=$max_movenr" );
self::delete_cache_game_moves( "board.fix_corrupted_move_table.delete_moves($gid)", $gid );
self::delete_cache_game_move_messages( "board.fix_corrupted_move_table.delete_move_mess($gid)", $gid );
}
ta_end();
}//fix_corrupted_move_table
/*!
* \brief StoneReader-interface-method on Board-class for GameSnapshot-class.
* \return 0/1/2/3 for given board-pos x/y=0..size-1, 0=empty, 1=black, 2=white, 3=dead
* \see GameSnapshot#make_game_snapshot()
*/
public function read_stone_value( $x, $y, $with_dead=true )
{
static $VAL_MAP = array(
BLACK => 1, // 01 (black)
WHITE => 2, // 10 (white)
BLACK_DEAD => 3, // 11 (dead-stone)
WHITE_DEAD => 3, // 11 (dead-stone)
);
$stone = (int)@$this->array[$x][$y];
$result = (int)@$VAL_MAP[$stone]; // undef = 00 (empty-field)
return ( !$with_dead && $result == 3 ) ? 0 : $result;
}//read_stone_value
/*!
* \brief Returns JS-String with game-tree as required for 'js/game-editor.js'.
* \return JSON { _children: [..], B|W: sgf-move|'', AB|AW: [ shape-sgf ],
* d_tp: [ coords ], d_capt: [ sgf-captures ], d_mn: dgs-move-number }
* \note d_tp = toggled points (into dead-/alive stone or into territory-/neutral-point depending on board-context/state)
*
* \note game-move-comments not included, because those are already displayed on game-page in msg-box
*/
public function make_js_game_tree()
{
static $MAP_PROPS = array( // map Stone -> game-tree-property
BLACK => 'B',
WHITE => 'W',
MARKED_BY_WHITE => 'd_tp',
MARKED_BY_BLACK => 'd_tp',
);
// format := [ var-num, node+, var-num, node+, ... ];
// node := { prop: val|arr, prop2:...} || { prop:.., _vars: [var-num, ...] }; // _vars is only present if there are variations
$root_node = new JS_GameNode();
$var_num = 0;
$out = array( $var_num ); // variation #0
$out[] = $root_node;
$shape = array( BLACK => array(), WHITE => array() );
$territory = array();
foreach ( $this->js_moves as $arr )
{
//error_log("make_js_game_tree().js_moves.arr = [".dgs_json_encode($arr)."]");
list( $move_nr, $stone, $x, $y ) = $arr;
if ( $x < POSX_SCORE ) // RESIGN|TIME|SETUP|ADDTIME
continue;
if ( !( $prop = @$MAP_PROPS[$stone] ) )
continue; // stone=NONE, x=POSX_RESIGN/TIMEOUT
$val = ( $x == POSX_PASS || $x == POSX_SCORE ) ? '' : number2sgf_coords($x,$y, $this->size);
if ( $move_nr == 0 )
$shape[$stone][] = $val;
else if ( $stone == MARKED_BY_BLACK || $stone == MARKED_BY_WHITE )
$territory[] = $val; // collect toggle-points for final POSX_SCORE with same move-nr
else
{
$curr_node = new JS_GameNode();
$curr_node->d_mn = $move_nr;
if ( $x == POSX_SCORE )
{
if ( count($territory) )
{
$curr_node->d_tp = $territory;
$territory = array();
}
}
else
$curr_node->{$prop} = $val;
if ( count(@$this->moves_captures[$move_nr]) )
$curr_node->d_capt = $this->moves_captures[$move_nr]; // captures
$out[] = $curr_node;
}
}
$root_node->d_mn = 0;
if ( count($shape[BLACK]) )
$root_node->AB = $shape[BLACK];
if ( count($shape[WHITE]) )
$root_node->AW = $shape[WHITE];
$result = dgs_json_encode( $out );
//error_log("make_js_game_tree({$this->size}) = [$result]");
return $result;
}//make_js_game_tree
} //class Board
/*! \brief Class used to create JavaScript-data using json-encode to be passed to 'js/game-editor.js'. */
class JS_GameNode
{
// attributes are set dynamically
}
?>
//
koh5_pano
Drag mouse to navigate.
Navigation
- Left/Right Mouse drag: Changes camera heading.
- Up/Sown Mouse drag: Changes camera pitch.
- Scroll wheel: Changes camera field of view.
- I-Key: Displays Info panel with canvas size, image size and FPS.
17.Aug.2010, Martin Wengenmayer