xwords/xwords4/common/model.c
ehouse 0b868f5b53 Base the number of bytes used to store tiles on the number in the
current dictionary.  This allows us to continue to open games saved
with older code using older dictionaries while still supporting the
new format for up to 64 tiles.  Old versions may crash when opening
games created by new versions, but that's probably ok.
2006-08-10 01:21:31 +00:00

1765 lines
51 KiB
C

/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/*
* Copyright 2000 by Eric House (xwords@eehouse.org). All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
/* #include <assert.h> */
#include "comtypes.h"
#include "modelp.h"
#include "xwstream.h"
#include "util.h"
#include "pool.h"
#include "memstream.h"
#include "strutils.h"
#include "LocalizedStrIncludes.h"
#ifdef CPLUS
extern "C" {
#endif
#define mEND 0x6d454e44
#define MAX_PASSES 2 /* how many times can all players pass? */
/****************************** prototypes ******************************/
typedef void (*MovePrintFuncPre)(ModelCtxt*, XP_U16, StackEntry*, void*);
typedef void (*MovePrintFuncPost)(ModelCtxt*, XP_U16, StackEntry*, XP_S16,
void*);
static void incrPendingTileCountAt( ModelCtxt* model, XP_U16 col,
XP_U16 row );
static void decrPendingTileCountAt( ModelCtxt* model, XP_U16 col,
XP_U16 row );
static void notifyBoardListeners( ModelCtxt* model, XP_U16 turn,
XP_U16 col, XP_U16 row, XP_Bool added );
static void notifyTrayListeners( ModelCtxt* model, XP_U16 turn, TileBit bits);
static CellTile getModelTileRaw( ModelCtxt* model, XP_U16 col, XP_U16 row );
static void setModelTileRaw( ModelCtxt* model, XP_U16 col, XP_U16 row,
CellTile tile );
static void assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
TrayTileSet* tiles );
static void makeTileTrade( ModelCtxt* model, XP_S16 player,
TrayTileSet* oldTiles, TrayTileSet* newTiles );
static XP_S16 commitTurn( ModelCtxt* model, XP_S16 turn,
TrayTileSet* newTiles, XWStreamCtxt* stream,
XP_Bool useStack );
static void buildModelFromStack( ModelCtxt* model, StackCtxt* stack,
XWStreamCtxt* stream,
MovePrintFuncPre mpfpr,
MovePrintFuncPost mpfpo,
void* closure );
static void setPendingCounts( ModelCtxt* model, XP_S16 turn );
static void loadPlayerCtxt( XWStreamCtxt* stream, PlayerCtxt* pc );
static void writePlayerCtxt( XWStreamCtxt* stream, PlayerCtxt* pc );
static XP_U16 model_getRecentPassCount( ModelCtxt* model );
/*****************************************************************************
*
****************************************************************************/
ModelCtxt*
model_make( MPFORMAL DictionaryCtxt* dict, XW_UtilCtxt* util, XP_U16 nCols,
XP_U16 nRows )
{
ModelCtxt* result = (ModelCtxt*)XP_MALLOC( mpool, sizeof( *result ) );
if ( result != NULL ) {
XP_MEMSET( result, 0, sizeof(*result) );
MPASSIGN(result->vol.mpool, mpool);
result->vol.util = util;
model_init( result, nCols, nRows );
XP_ASSERT( !!util->gameInfo );
result->vol.gi = util->gameInfo;
model_setDictionary( result, dict );
}
return result;
} /* model_make */
ModelCtxt*
model_makeFromStream( MPFORMAL XWStreamCtxt* stream, DictionaryCtxt* dict,
XW_UtilCtxt* util )
{
ModelCtxt* model;
DictionaryCtxt* savedDict = (DictionaryCtxt*)NULL;
XP_U16 nCols, nRows;
short i;
XP_Bool hasDict;
XP_U16 nPlayers;
nCols = (XP_U16)stream_getBits( stream, NUMCOLS_NBITS );
nRows = (XP_U16)stream_getBits( stream, NUMCOLS_NBITS );
hasDict = stream_getBits( stream, 1 );
nPlayers = (XP_U16)stream_getBits( stream, NPLAYERS_NBITS );
if ( hasDict ) {
savedDict = util_makeEmptyDict( util );
dict_loadFromStream( savedDict, stream );
if ( !!dict ) {
XP_ASSERT( dict_tilesAreSame( savedDict, dict ) );
dict_destroy( savedDict );
savedDict = dict;
}
}
model = model_make( MPPARM(mpool) savedDict, util, nCols, nRows );
model->nPlayers = nPlayers;
stack_loadFromStream( model->vol.stack, stream );
buildModelFromStack( model, model->vol.stack, (XWStreamCtxt*)NULL,
(MovePrintFuncPre)NULL,
(MovePrintFuncPost)NULL, NULL );
for ( i = 0; i < model->nPlayers; ++i ) {
loadPlayerCtxt( stream, &model->players[i] );
setPendingCounts( model, i );
invalidateScore( model, i );
}
XP_ASSERT( stream_getU32( stream ) == mEND );
return model;
} /* model_makeFromStream */
void
model_writeToStream( ModelCtxt* model, XWStreamCtxt* stream )
{
short i;
DictionaryCtxt* dict;
stream_putBits( stream, NUMCOLS_NBITS, model->nCols );
stream_putBits( stream, NUMCOLS_NBITS, model->nRows );
dict = model_getDictionary( model );
stream_putBits( stream, 1, dict != NULL );
/* we have two bits for nPlayers, so range must be 0..3, not 1..4 */
stream_putBits( stream, NPLAYERS_NBITS, model->nPlayers );
if ( dict != NULL ) {
dict_writeToStream( model_getDictionary( model ), stream );
}
stack_writeToStream( model->vol.stack, stream );
for ( i = 0; i < model->nPlayers; ++i ) {
writePlayerCtxt( stream, &model->players[i] );
}
#ifdef DEBUG
stream_putU32( stream, mEND );
#endif
} /* model_writeToStream */
void
model_init( ModelCtxt* model, XP_U16 nCols, XP_U16 nRows )
{
ModelVolatiles vol = model->vol;
XP_ASSERT( model != NULL );
XP_MEMSET( model, 0, sizeof( *model ) );
XP_MEMSET( &model->tiles, TILE_EMPTY_BIT, sizeof(model->tiles) );
model->nCols = nCols;
model->nRows = nRows;
model->vol = vol;
if ( !!model->vol.stack ) {
stack_init( model->vol.stack );
} else {
model->vol.stack = stack_make( MPPARM(model->vol.mpool)
util_getVTManager(model->vol.util));
}
} /* model_init */
void
model_destroy( ModelCtxt* model )
{
stack_destroy( model->vol.stack );
/* is this it!? */
XP_FREE( model->vol.mpool, model );
} /* model_destroy */
static void
buildModelFromStack( ModelCtxt* model, StackCtxt* stack,
XWStreamCtxt* stream,
MovePrintFuncPre mpf_pre, MovePrintFuncPost mpf_post,
void* closure )
{
StackEntry entry;
XP_U16 i;
XP_S16 moveScore = 0; /* keep compiler happy */
for ( i = 0; stack_getNthEntry( stack, i, &entry ); ++i ) {
if ( !!mpf_pre ) {
(*mpf_pre)( model, i, &entry, closure );
}
switch ( entry.moveType ) {
case MOVE_TYPE:
model_makeTurnFromMoveInfo( model, entry.playerNum,
&entry.u.move.moveInfo);
moveScore = commitTurn( model, entry.playerNum,
&entry.u.move.newTiles, stream, XP_FALSE);
break;
case TRADE_TYPE:
makeTileTrade( model, entry.playerNum, &entry.u.trade.oldTiles,
&entry.u.trade.newTiles );
break;
case ASSIGN_TYPE:
assignPlayerTiles( model, entry.playerNum,
&entry.u.assign.tiles );
break;
case PHONY_TYPE: /* nothing to add */
model_makeTurnFromMoveInfo( model, entry.playerNum,
&entry.u.phony.moveInfo);
/* do something here to cause it to print */
(void)getCurrentMoveScoreIfLegal( model, entry.playerNum, stream,
&moveScore );
moveScore = 0;
model_resetCurrentTurn( model, entry.playerNum );
break;
default:
XP_ASSERT(0);
}
if ( !!mpf_post ) {
(*mpf_post)( model, i, &entry, moveScore, closure );
}
}
} /* buildModelFromStack */
void
model_setNPlayers( ModelCtxt* model, XP_U16 nPlayers )
{
model->nPlayers = nPlayers;
} /* model_setNPlayers */
void
model_setDictionary( ModelCtxt* model, DictionaryCtxt* dict )
{
model->vol.dict = dict;
if ( !!dict ) {
XP_U16 nFaces = dict_numTileFaces( dict );
XP_ASSERT( !!model->vol.stack );
stack_setBitsPerTile( model->vol.stack, nFaces <= 32? 5 : 6 );
}
} /* model_setDictionary */
DictionaryCtxt*
model_getDictionary( ModelCtxt* model )
{
return model->vol.dict;
} /* model_getDictionary */
static XP_Bool
getPendingTileFor( ModelCtxt* model, XP_U16 turn, XP_U16 col, XP_U16 row,
CellTile* cellTile )
{
XP_Bool found = XP_FALSE;
PlayerCtxt* player;
PendingTile* pendings;
XP_U16 i;
player = &model->players[turn];
pendings = player->pendingTiles;
for ( i = 0; i < player->nPending; ++i ) {
if ( (pendings->col == col) && (pendings->row == row) ) {
*cellTile = pendings->tile;
found = XP_TRUE;
XP_ASSERT ( (*cellTile & TILE_EMPTY_BIT) == 0 );
break;
}
++pendings;
}
return found;
} /* getPendingTileFor */
XP_Bool
model_getTile( ModelCtxt* model, XP_U16 col, XP_U16 row, XP_Bool getPending,
XP_S16 turn, Tile* tileP, XP_Bool* isBlank,
XP_Bool* pendingP, XP_Bool* recentP )
{
CellTile cellTile = getModelTileRaw( model, col, row );
XP_Bool pending = XP_FALSE;
if ( (cellTile & TILE_PENDING_BIT) != 0 ) {
if ( getPending
&& getPendingTileFor( model, turn, col, row, &cellTile ) ) {
/* it's pending, but caller doesn't want to see it */
pending = XP_TRUE;
} else {
cellTile = EMPTY_TILE;
}
}
/* this needs to happen after the above b/c cellTile gets changed */
if ( (cellTile & TILE_EMPTY_BIT) != 0 ) {
return XP_FALSE;
}
*tileP = cellTile & TILE_VALUE_MASK;
*isBlank = IS_BLANK(cellTile);
*pendingP = pending;
if ( !!recentP ) {
*recentP = (cellTile & PREV_MOVE_BIT) != 0;
}
return XP_TRUE;
} /* model_getTile */
void
model_listPlacedBlanks( ModelCtxt* model, XP_U16 turn,
XP_Bool includePending, BlankQueue* bcp )
{
XP_U16 nCols = model_numCols( model );
XP_U16 nRows = model_numRows( model );
XP_U16 col, row;
XP_U16 nBlanks = 0;
for ( row = 0; row < nRows; ++row ) {
for ( col = 0; col < nCols; ++col ) {
CellTile cellTile = getModelTileRaw( model, col, row );
if ( (cellTile & TILE_PENDING_BIT) != 0 ) {
if ( !includePending ||
!getPendingTileFor( model, turn, col, row, &cellTile ) ) {
continue;
}
}
if ( (cellTile & TILE_BLANK_BIT) != 0 ) {
bcp->col[nBlanks] = (XP_U8)col;
bcp->row[nBlanks] = (XP_U8)row;
++nBlanks;
}
}
}
bcp->nBlanks = nBlanks;
} /* model_listPlacedBlanks */
void
model_foreachPrevCell( ModelCtxt* model, BoardListener bl, void* closure )
{
XP_U16 col, row;
for ( col = 0; col < model->nCols; ++col ) {
for ( row = 0; row < model->nRows; ++row) {
CellTile tile = getModelTileRaw( model, col, row );
if ( (tile & PREV_MOVE_BIT) != 0 ) {
(*bl)( closure, (XP_U16)CELL_OWNER(tile), col, row, XP_FALSE );
}
}
}
} /* model_foreachPrevCell */
static void
clearAndNotify( void* closure, XP_U16 turn, XP_U16 col, XP_U16 row,
XP_Bool added )
{
ModelCtxt* model = (ModelCtxt*)closure;
CellTile tile = getModelTileRaw( model, col, row );
setModelTileRaw( model, col, row, (CellTile)(tile & ~PREV_MOVE_BIT) );
notifyBoardListeners( model, (XP_U16)CELL_OWNER(tile), col, row,
XP_FALSE );
} /* clearAndNotify */
static void
clearLastMoveInfo( ModelCtxt* model )
{
model_foreachPrevCell( model, clearAndNotify, model );
} /* clearLastMoveInfo */
static void
invalLastMove( ModelCtxt* model )
{
if ( !!model->vol.boardListenerFunc ) {
model_foreachPrevCell( model, model->vol.boardListenerFunc,
model->vol.boardListenerData );
}
} /* invalLastMove */
void
model_foreachPendingCell( ModelCtxt* model, XP_S16 turn,
BoardListener bl, void* closure )
{
PendingTile* pt;
PlayerCtxt* player;
XP_S16 count;
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
count = player->nPending;
for ( pt = player->pendingTiles; count--; ++pt ) {
XP_U16 col, row;
col = pt->col;
row = pt->row;
(*bl)( closure, turn, pt->col, pt->row, XP_FALSE );
}
} /* model_invalPendingCells */
XP_U16
model_getCellOwner( ModelCtxt* model, XP_U16 col, XP_U16 row )
{
CellTile tile;
XP_U16 result;
tile = getModelTileRaw( model, col, row );
result = CELL_OWNER(tile);
return result;
} /* model_getCellOwner */
static void
setModelTileRaw( ModelCtxt* model, XP_U16 col, XP_U16 row, CellTile tile )
{
XP_ASSERT( col < MAX_COLS );
XP_ASSERT( row < MAX_ROWS );
model->tiles[col][row] = tile;
} /* model_setTile */
static CellTile
getModelTileRaw( ModelCtxt* model, XP_U16 col, XP_U16 row )
{
XP_ASSERT( col < MAX_COLS );
XP_ASSERT( row < MAX_ROWS );
return model->tiles[col][row];
} /* getModelTileRaw */
static void
undoFromMoveInfo( ModelCtxt* model, XP_U16 turn, Tile blankTile, MoveInfo* mi )
{
XP_U16 col, row, i;
XP_U16* other;
MoveInfoTile* tinfo;
col = row = mi->commonCoord;
other = mi->isHorizontal? &col: &row;
for ( tinfo = mi->tiles, i = 0; i < mi->nTiles; ++tinfo, ++i ) {
Tile tile;
*other = tinfo->varCoord;
tile = tinfo->tile;
setModelTileRaw( model, col, row, EMPTY_TILE );
notifyBoardListeners( model, turn, col, row, XP_FALSE );
if ( IS_BLANK(tile) ) {
tile = blankTile;
}
model_addPlayerTile( model, turn, -1, tile );
}
adjustScoreForUndone( model, mi, turn );
} /* undoFromMoveInfo */
/* Remove tiles in a set from tray and put them back in the pool.
*/
static void
replaceNewTiles( ModelCtxt* model, PoolContext* pool, XP_U16 turn,
TrayTileSet* tileSet )
{
Tile* t;
XP_U16 i, nTiles;
for ( t = tileSet->tiles, i = 0, nTiles = tileSet->nTiles;
i < nTiles; ++i ) {
XP_S16 index;
Tile tile = *t++;
index = model_trayContains( model, turn, tile );
XP_ASSERT( index >= 0 );
model_removePlayerTile( model, turn, index );
}
if ( !!pool ) {
pool_replaceTiles( pool, tileSet);
}
} /* replaceNewTiles */
/* Turn the most recent move into a phony.
*/
void
model_rejectPreviousMove( ModelCtxt* model, PoolContext* pool, XP_U16* turn )
{
StackCtxt* stack = model->vol.stack;
StackEntry entry;
Tile blankTile = dict_getBlankTile( model_getDictionary(model) );
stack_popEntry( stack, &entry );
XP_ASSERT( entry.moveType == MOVE_TYPE );
replaceNewTiles( model, pool, entry.playerNum, &entry.u.move.newTiles );
undoFromMoveInfo( model, entry.playerNum, blankTile,
&entry.u.move.moveInfo );
stack_addPhony( stack, entry.playerNum, &entry.u.phony.moveInfo );
*turn = entry.playerNum;
} /* model_rejectPreviousMove */
/* Undo a move, but only if it's the move we're expecting to undo (as
* indicated by *moveNumP, if >= 0).
*/
XP_Bool
model_undoLatestMoves( ModelCtxt* model, PoolContext* pool,
XP_U16 nMovesSought, XP_U16* turnP, XP_S16* moveNumP )
{
StackCtxt* stack = model->vol.stack;
StackEntry entry;
XP_U16 turn = 0;
Tile blankTile = dict_getBlankTile( model_getDictionary(model) );
XP_Bool success = XP_TRUE;
XP_S16 moveSought = *moveNumP;
XP_U16 nMovesUndone;
XP_U16 nStackEntries;
nStackEntries = stack_getNEntries( stack );
if ( nStackEntries < nMovesSought ) {
return XP_FALSE;
} else if ( nStackEntries <= model->nPlayers ) {
return XP_FALSE;
}
for ( nMovesUndone = 0; success && nMovesUndone < nMovesSought; ) {
success = stack_popEntry( stack, &entry );
if ( success ) {
++nMovesUndone;
if ( moveSought < 0 ) {
moveSought = entry.moveNum - 1;
} else if ( moveSought-- != entry.moveNum ) {
success = XP_FALSE;
break;
}
turn = entry.playerNum;
model_resetCurrentTurn( model, turn );
if ( entry.moveType == MOVE_TYPE ) {
/* get the tiles out of player's tray and back into the
pool */
replaceNewTiles( model, pool, turn, &entry.u.move.newTiles);
undoFromMoveInfo( model, turn, blankTile,
&entry.u.move.moveInfo );
} else if ( entry.moveType == TRADE_TYPE ) {
if ( pool != NULL ) {
/* If there's no pool, assume we're doing this for
scoring purposes only. */
replaceNewTiles( model, pool, turn,
&entry.u.trade.newTiles );
pool_removeTiles( pool, &entry.u.trade.oldTiles );
assignPlayerTiles( model, turn, &entry.u.trade.oldTiles );
}
} else if ( entry.moveType == PHONY_TYPE ) {
/* nothing to do, since nothing happened */
} else {
XP_ASSERT( entry.moveType == ASSIGN_TYPE );
success = XP_FALSE;
break;
}
}
}
/* Find the first MOVE still on the stack and highlight its tiles since
they're now the most recent move. Trades and lost turns ignored. */
nStackEntries = stack_getNEntries( stack );
for ( ; ; ) {
StackEntry entry;
if ( nStackEntries == 0 ||
!stack_getNthEntry( stack, nStackEntries - 1, &entry ) ) {
break;
}
if ( entry.moveType == MOVE_TYPE ) {
XP_U16 nTiles = entry.u.move.moveInfo.nTiles;
XP_U16 col, row;
XP_U16* varies;
if ( entry.u.move.moveInfo.isHorizontal ) {
row = entry.u.move.moveInfo.commonCoord;
varies = &col;
} else {
col = entry.u.move.moveInfo.commonCoord;
varies = &row;
}
while ( nTiles-- ) {
CellTile tile;
*varies = entry.u.move.moveInfo.tiles[nTiles].varCoord;
tile = getModelTileRaw( model, col, row );
setModelTileRaw( model, col, row,
(CellTile)(tile | PREV_MOVE_BIT) );
notifyBoardListeners( model, entry.playerNum, col, row,
XP_FALSE );
}
break;
} else if ( entry.moveType == ASSIGN_TYPE ) {
break;
} else {
--nStackEntries; /* look at the next one */
}
}
if ( nMovesUndone != nMovesSought ) {
success = XP_FALSE;
}
if ( success ) {
*turnP = turn;
*moveNumP = entry.moveNum;
} else {
while ( nMovesUndone-- ) {
stack_redo( stack );
}
}
return success;
} /* model_undoLatestMoves */
void
model_trayToStream( ModelCtxt* model, XP_S16 turn, XWStreamCtxt* stream )
{
PlayerCtxt* player = &model->players[turn];
traySetToStream( stream, &player->trayTiles );
} /* model_trayToStream */
void
model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream )
{
PlayerCtxt* player = &model->players[turn];
XP_S16 numTiles = player->nPending;
stream_putBits( stream, NTILES_NBITS, numTiles );
while ( numTiles-- ) {
Tile tile;
XP_U16 col, row;
XP_Bool isBlank;
model_getCurrentMoveTile( model, turn, &numTiles, &tile,
&col, &row, &isBlank );
XP_ASSERT( numTiles >= 0 );
stream_putBits( stream, TILE_NBITS, tile );
stream_putBits( stream, NUMCOLS_NBITS, col );
stream_putBits( stream, NUMCOLS_NBITS, row );
stream_putBits( stream, 1, isBlank );
}
} /* model_turnToStream */
/* Take stream as the source of info about what tiles to move from tray to
* board. Undo any current move first -- a player on this device might be
* using the board as scratch during another player's turn. For each tile,
* assert that it's in the tray, remove it from the tray, and place it on the
* board.
*/
void
model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream )
{
XP_U16 numTiles;
Tile blank = dict_getBlankTile( model->vol.dict );
model_resetCurrentTurn( model, playerNum );
numTiles = (XP_U16)stream_getBits( stream, NTILES_NBITS );
XP_STATUSF( "model_makeTurnFromStream: numTiles=%d\n", numTiles );
while ( numTiles-- ) {
XP_S16 foundAt;
Tile moveTile;
Tile tileFace = (Tile)stream_getBits( stream, TILE_NBITS );
XP_U16 col = (XP_U16)stream_getBits( stream, NUMCOLS_NBITS );
XP_U16 row = (XP_U16)stream_getBits( stream, NUMCOLS_NBITS );
XP_Bool isBlank = stream_getBits( stream, 1 );
/* This code gets called both for the server, which has all the
tiles in its tray, and for a client, which has "EMPTY" tiles
only. If it's the empty case, we stuff a real tile into the
tray before falling through to the normal case */
if ( isBlank ) {
moveTile = blank;
} else {
moveTile = tileFace;
}
foundAt = model_trayContains( model, playerNum, moveTile );
if ( foundAt == -1 ) {
XP_ASSERT( EMPTY_TILE==model_getPlayerTile(model, playerNum, 0));
(void)model_removePlayerTile( model, playerNum, -1 );
model_addPlayerTile( model, playerNum, -1, moveTile );
}
model_moveTrayToBoard( model, playerNum, col, row, foundAt, tileFace);
}
} /* model_makeMoveFromStream */
void
model_makeTurnFromMoveInfo( ModelCtxt* model, XP_U16 playerNum,
MoveInfo* newMove )
{
XP_U16 col, row, i;
XP_U16* other;
MoveInfoTile* tinfo;
Tile blank;
XP_U16 numTiles;
blank = dict_getBlankTile( model->vol.dict );
numTiles = newMove->nTiles;
col = row = newMove->commonCoord; /* just assign both */
other = newMove->isHorizontal? &col: &row;
for ( tinfo = newMove->tiles, i = 0; i < numTiles; ++i, ++tinfo ) {
XP_S16 tileIndex;
Tile tile = tinfo->tile;
if ( IS_BLANK(tile) ) {
tile = blank;
}
tileIndex = model_trayContains( model, playerNum, tile );
XP_ASSERT( tileIndex >= 0 );
*other = tinfo->varCoord;
model_moveTrayToBoard( model, (XP_S16)playerNum, col, row, tileIndex,
(Tile)(tinfo->tile & TILE_VALUE_MASK) );
}
} /* model_makeTurnFromMoveInfo */
void
model_countAllTrayTiles( ModelCtxt* model, XP_U16* counts,
XP_S16 excludePlayer )
{
PlayerCtxt* player;
XP_S16 nPlayers = model->nPlayers;
XP_S16 i;
Tile blank;
XP_ASSERT( !!model->vol.dict );
blank = dict_getBlankTile( model->vol.dict );
for ( i = 0, player = model->players; i < nPlayers; ++i, ++player ) {
if ( i != excludePlayer ) {
XP_U16 nTiles = player->nPending;
PendingTile* pt;
Tile* tiles;
/* first the pending tiles */
for ( pt = player->pendingTiles; nTiles--; ++pt ) {
Tile tile = pt->tile;
if ( IS_BLANK(tile) ) {
tile = blank;
} else {
tile &= TILE_VALUE_MASK;
}
XP_ASSERT( tile <= MAX_UNIQUE_TILES );
++counts[tile];
}
/* then the tiles still in the tray */
nTiles = player->trayTiles.nTiles;
tiles = player->trayTiles.tiles;
while ( nTiles-- ) {
++counts[*tiles++];
}
}
}
} /* model_countAllTrayTiles */
XP_S16
model_trayContains( ModelCtxt* model, XP_S16 turn, Tile tile )
{
PlayerCtxt* player;
XP_S16 i;
XP_S16 result = -1;
XP_ASSERT( turn >= 0 );
XP_ASSERT( turn < model->nPlayers );
player = &model->players[turn];
/* search from top down so don't pull out of below divider */
for ( i = player->trayTiles.nTiles - 1; i >= 0 ; --i ) {
Tile playerTile = player->trayTiles.tiles[i];
if ( playerTile == tile ) {
result = i;
break;
}
}
return result;
} /* model_trayContains */
XP_U16
model_getCurrentMoveCount( ModelCtxt* model, XP_S16 turn )
{
PlayerCtxt* player;
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
return player->nPending;
} /* model_getCurrentMoveCount */
void
model_getCurrentMoveTile( ModelCtxt* model, XP_S16 turn, XP_S16* index,
Tile* tile, XP_U16* col, XP_U16* row,
XP_Bool* isBlank )
{
PlayerCtxt* player;
PendingTile* pt;
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
XP_ASSERT( *index < player->nPending );
if ( *index < 0 ) {
*index = player->nPending - 1;
}
pt = &player->pendingTiles[*index];
*col = pt->col;
*row = pt->row;
*isBlank = (pt->tile & TILE_BLANK_BIT) != 0;
*tile = pt->tile & TILE_VALUE_MASK;
} /* model_getCurrentMoveTile */
Tile
model_removePlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index )
{
PlayerCtxt* player = &model->players[turn];
Tile tile;
short i;
TileBit bits = 0;
if ( index < 0 ) {
index = player->trayTiles.nTiles - 1;
} else {
XP_ASSERT( index < player->trayTiles.nTiles );
}
tile = player->trayTiles.tiles[index];
bits = 1 << index;
--player->trayTiles.nTiles;
for ( i = index; i < player->trayTiles.nTiles; ++i ) {
player->trayTiles.tiles[i] = player->trayTiles.tiles[i+1];
bits |= 3 << i;
}
notifyTrayListeners( model, turn, bits );
return tile;
} /* model_removePlayerTile */
void
model_packTilesUtil( ModelCtxt* model, PoolContext* pool,
XP_Bool includeBlank,
XP_U16* nUsed, XP_UCHAR4* texts,
Tile* tiles )
{
DictionaryCtxt* dict = model->vol.dict;
XP_U16 nFaces = dict_numTileFaces( dict );
Tile blankFace = dict_getBlankTile( dict );
Tile tile;
XP_U16 nFacesAvail = 0;
XP_ASSERT( nFaces <= *nUsed );
for ( tile = 0; tile < nFaces; ++tile ) {
XP_U16 nChars;
if ( includeBlank ) {
XP_ASSERT( !!pool );
if ( pool_getNTilesLeftFor( pool, tile ) == 0 ) {
continue;
}
} else if ( tile == blankFace ) {
continue;
}
tiles[nFacesAvail] = tile;
nChars = dict_tilesToString( dict, &tile, 1,
(XP_UCHAR*)&texts[nFacesAvail],
sizeof(texts[0]) );
XP_ASSERT( nChars < sizeof(texts[0]) );
++nFacesAvail;
}
*nUsed = nFacesAvail;
} /* model_packTilesUtil */
static Tile
askBlankTile( ModelCtxt* model, XP_U16 turn )
{
XP_U16 nUsed = MAX_UNIQUE_TILES;
XP_S16 chosen;
XP_UCHAR4 tfaces[MAX_UNIQUE_TILES];
Tile tiles[MAX_UNIQUE_TILES];
PickInfo pi;
pi.why = PICK_FOR_BLANK;
pi.nTotal = 1;
pi.thisPick = 1;
model_packTilesUtil( model, NULL, XP_FALSE,
&nUsed, tfaces, tiles );
chosen = util_userPickTile( model->vol.util, &pi,
turn, (const XP_UCHAR4*)tfaces, nUsed );
if ( chosen < 0 ) {
chosen = 0;
}
return tiles[chosen];
} /* askBlankTile */
void
model_moveTrayToBoard( ModelCtxt* model, XP_S16 turn, XP_U16 col, XP_U16 row,
XP_S16 tileIndex, Tile blankFace )
{
PlayerCtxt* player;
PendingTile* pt;
Tile tile = model_removePlayerTile( model, turn, tileIndex );
if ( tile == dict_getBlankTile(model->vol.dict) ) {
if ( blankFace != EMPTY_TILE ) {
tile = blankFace;
} else {
XP_ASSERT( turn >= 0 );
tile = askBlankTile( model, (XP_U16)turn );
}
tile |= TILE_BLANK_BIT;
}
player = &model->players[turn];
if ( player->nPending == 0 ) {
invalLastMove( model );
}
pt = &player->pendingTiles[player->nPending++];
XP_ASSERT( player->nPending <= MAX_TRAY_TILES );
pt->tile = tile;
pt->col = (XP_U8)col;
pt->row = (XP_U8)row;
invalidateScore( model, turn );
incrPendingTileCountAt( model, col, row );
notifyBoardListeners( model, turn, col, row, XP_TRUE );
} /* model_moveTrayToBoard */
void
model_moveBoardToTray( ModelCtxt* model, XP_S16 turn, XP_S16 index )
{
PlayerCtxt* player;
short i;
PendingTile* pt;
Tile tile;
player = &model->players[turn];
if ( index < 0 ) {
index = player->nPending - 1;
}
pt = &player->pendingTiles[index];
decrPendingTileCountAt( model, pt->col, pt->row );
notifyBoardListeners( model, turn, pt->col, pt->row, XP_FALSE );
tile = pt->tile;
if ( (tile & TILE_BLANK_BIT) != 0 ) {
tile = dict_getBlankTile( model->vol.dict );
}
model_addPlayerTile( model, turn, -1, tile );
--player->nPending;
for ( i = index; i < player->nPending; ++i ) {
player->pendingTiles[i] = player->pendingTiles[i+1];
}
if ( player->nPending == 0 ) {
invalLastMove( model );
}
invalidateScore( model, turn );
} /* model_moveBoardToTray */
void
model_resetCurrentTurn( ModelCtxt* model, XP_S16 whose )
{
PlayerCtxt* player;
XP_ASSERT( whose >= 0 && whose < model->nPlayers );
player = &model->players[whose];
while ( player->nPending > 0 ) {
model_moveBoardToTray( model, whose, -1 );
}
} /* model_resetCurrentTurn */
static void
incrPendingTileCountAt( ModelCtxt* model, XP_U16 col, XP_U16 row )
{
XP_U16 val = getModelTileRaw( model, col, row );
if ( TILE_IS_EMPTY(val) ) {
val = 0;
} else {
XP_ASSERT( (val & TILE_PENDING_BIT) != 0 );
XP_ASSERT( (val & TILE_VALUE_MASK) > 0 );
}
++val;
XP_ASSERT( (val & TILE_VALUE_MASK) > 0 &&
(val & TILE_VALUE_MASK) <= MAX_NUM_PLAYERS );
setModelTileRaw( model, col, row, (CellTile)(val | TILE_PENDING_BIT) );
} /* incrPendingTileCountAt */
static void
setPendingCounts( ModelCtxt* model, XP_S16 turn )
{
PlayerCtxt* player = &model->players[turn];
PendingTile* pending = player->pendingTiles;
XP_U16 nPending;
for ( nPending = player->nPending; nPending--; ) {
incrPendingTileCountAt( model, pending->col, pending->row );
++pending;
}
} /* setPendingCounts */
static void
decrPendingTileCountAt( ModelCtxt* model, XP_U16 col, XP_U16 row )
{
XP_U16 val = getModelTileRaw( model, col, row );
/* for pending tiles, the value is defined in the players array, so what
we keep here is a refcount of how many players have put tiles there. */
val &= TILE_VALUE_MASK; /* the refcount */
XP_ASSERT( val <= MAX_NUM_PLAYERS && val > 0 );
if ( --val > 0 ) {
val |= TILE_PENDING_BIT;
} else {
val = EMPTY_TILE;
}
setModelTileRaw( model, col, row, val );
} /* decrPendingTileCountAt */
static void
putBackOtherPlayersTiles( ModelCtxt* model, XP_U16 notMyTurn,
XP_U16 col, XP_U16 row )
{
XP_S16 turn, j;
for ( turn = 0; turn < model->nPlayers; ++turn ) {
PlayerCtxt* player;
if ( turn == notMyTurn ) {
continue;
}
player = &model->players[turn];
for ( j = player->nPending-1; j >= 0; --j ) { /* backwards in case
removed */
PendingTile* pt = &player->pendingTiles[j];
if ( pt->col == col && pt->row == row ) {
/* this one needs to be put back */
model_moveBoardToTray( model, turn, j );
break; /* a player can have only one tile on a square */
}
}
}
} /* putBackOtherPlayersTiles */
/* Make those tiles placed by 'turn' a permanent part of the board. If any
* other players have placed pending tiles on those same squares, replace them
* in their trays.
*/
static XP_S16
commitTurn( ModelCtxt* model, XP_S16 turn, TrayTileSet* newTiles,
XWStreamCtxt* stream, XP_Bool useStack )
{
short i;
PlayerCtxt* player;
PendingTile* pt;
XP_S16 score;
XP_Bool inLine, isHorizontal;
Tile* newTilesP;
XP_U16 nTiles;
nTiles = newTiles->nTiles;
#ifdef DEBUG
XP_ASSERT( getCurrentMoveScoreIfLegal( model, turn, (XWStreamCtxt*)NULL,
&score ) );
invalidateScore( model, turn );
#endif
XP_ASSERT( turn >= 0 && turn < MAX_NUM_PLAYERS);
clearLastMoveInfo( model );
player = &model->players[turn];
if ( useStack ) {
MoveInfo moveInfo;
inLine = tilesInLine( model, turn, &isHorizontal );
XP_ASSERT( inLine );
normalizeMoves( model, turn, isHorizontal, &moveInfo );
stack_addMove( model->vol.stack, turn, &moveInfo, newTiles );
}
for ( i = 0, pt=player->pendingTiles; i < player->nPending; ++i, ++pt ) {
XP_U16 col, row;
CellTile tile;
XP_U16 val;
col = pt->col;
row = pt->row;
tile = getModelTileRaw( model, col, row );
XP_ASSERT( tile & TILE_PENDING_BIT );
val = tile & TILE_VALUE_MASK;
if ( val > 1 ) { /* somebody else is using this square too! */
putBackOtherPlayersTiles( model, turn, col, row );
}
tile = pt->tile;
tile |= PREV_MOVE_BIT;
tile |= turn << CELL_OWNER_OFFSET;
setModelTileRaw( model, col, row, tile );
notifyBoardListeners( model, turn, col, row, XP_FALSE );
}
(void)getCurrentMoveScoreIfLegal( model, turn, stream, &score );
XP_ASSERT( score >= 0 );
player->score += score;
/* Why is this next loop necessary? */
for ( i = 0; i < model->nPlayers; ++i ) {
invalidateScore( model, i );
}
player->nPending = 0;
newTilesP = newTiles->tiles;
while ( nTiles-- ) {
model_addPlayerTile( model, turn, -1, *newTilesP++ );
}
return score;
} /* commitTurn */
void
model_commitTurn( ModelCtxt* model, XP_S16 turn, TrayTileSet* newTiles )
{
(void)commitTurn( model, turn, newTiles, (XWStreamCtxt*)NULL, XP_TRUE );
} /* model_commitTurn */
/* Given a rack of new tiles and of old, remove all the old from the tray and
* replace them with new. Replace in the same place so that user sees an
* in-place change.
*/
static void
makeTileTrade( ModelCtxt* model, XP_S16 player, TrayTileSet* oldTiles,
TrayTileSet* newTiles )
{
XP_U16 i;
XP_U16 nTiles;
XP_ASSERT( newTiles->nTiles == oldTiles->nTiles );
for ( nTiles = newTiles->nTiles, i = 0; i < nTiles; ++i ) {
Tile oldTile = oldTiles->tiles[i];
XP_S16 tileIndex = model_trayContains( model, player, oldTile );
XP_ASSERT( tileIndex >= 0 );
model_removePlayerTile( model, player, tileIndex );
model_addPlayerTile( model, player, tileIndex, newTiles->tiles[i] );
}
} /* makeTileTrade */
void
model_makeTileTrade( ModelCtxt* model, XP_S16 player,
TrayTileSet* oldTiles, TrayTileSet* newTiles )
{
stack_addTrade( model->vol.stack, player, oldTiles, newTiles );
makeTileTrade( model, player, oldTiles, newTiles );
} /* model_makeTileTrade */
Tile
model_getPlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index )
{
PlayerCtxt* player = &model->players[turn];
if ( index < 0 ) {
index = player->trayTiles.nTiles-1;
}
XP_ASSERT( index < player->trayTiles.nTiles );
return player->trayTiles.tiles[index];
} /* model_getPlayerTile */
const TrayTileSet*
model_getPlayerTiles( ModelCtxt* model, XP_S16 turn )
{
PlayerCtxt* player = &model->players[turn];
return (const TrayTileSet*)&player->trayTiles;
} /* model_getPlayerTile */
void
model_addPlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index, Tile tile )
{
PlayerCtxt* player = &model->players[turn];
short i;
TileBit bits = 0;
XP_ASSERT( player->trayTiles.nTiles < MAX_TRAY_TILES );
if ( index < 0 ) {
index = player->trayTiles.nTiles;
}
/* move tiles up to make room */
for ( i = player->trayTiles.nTiles; i > index; --i ) {
player->trayTiles.tiles[i] = player->trayTiles.tiles[i-1];
bits |= (3 << (i-2));
}
++player->trayTiles.nTiles;
player->trayTiles.tiles[index] = tile;
bits |= (1 << index);
notifyTrayListeners( model, turn, bits );
} /* model_addPlayerTile */
static void
assignPlayerTiles( ModelCtxt* model, XP_S16 turn, TrayTileSet* tiles )
{
Tile* tilep = tiles->tiles;
XP_U16 nTiles = tiles->nTiles;
while ( nTiles-- ) {
model_addPlayerTile( model, turn, -1, *tilep++ );
}
} /* model_addPlayerTiles */
void
model_assignPlayerTiles( ModelCtxt* model, XP_S16 turn, TrayTileSet* tiles )
{
stack_addAssign( model->vol.stack, turn, tiles );
assignPlayerTiles( model, turn, tiles );
} /* model_addPlayerTiles */
XP_U16
model_getNumTilesInTray( ModelCtxt* model, XP_S16 turn )
{
PlayerCtxt* player;
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
return player->trayTiles.nTiles;
} /* model_getNumPlayerTiles */
XP_U16
model_getNumTilesTotal( ModelCtxt* model, XP_S16 turn )
{
PlayerCtxt* player = &model->players[turn];
return player->trayTiles.nTiles + player->nPending;
} /* model_getNumTilesTotal */
XP_U16
model_numRows( ModelCtxt* model )
{
return model->nRows;
} /* model_numRows */
XP_U16
model_numCols( ModelCtxt* model )
{
return model->nCols;
} /* model_numCols */
void
model_setBoardListener( ModelCtxt* model, BoardListener bl, void* data )
{
model->vol.boardListenerFunc = bl;
model->vol.boardListenerData = data;
} /* model_setBoardListener */
void
model_setTrayListener( ModelCtxt* model, TrayListener tl, void* data )
{
model->vol.trayListenerFunc = tl;
model->vol.trayListenerData = data;
} /* model_setBoardListener */
static void
notifyBoardListeners( ModelCtxt* model, XP_U16 turn, XP_U16 col, XP_U16 row,
XP_Bool added )
{
if ( model->vol.boardListenerFunc != NULL ) {
(*model->vol.boardListenerFunc)( model->vol.boardListenerData, turn,
col, row, added );
}
} /* notifyBoardListeners */
static void
notifyTrayListeners( ModelCtxt* model, XP_U16 turn, TileBit bits )
{
if ( model->vol.trayListenerFunc != NULL ) {
(*model->vol.trayListenerFunc)( model->vol.trayListenerData, turn,
bits );
}
} /* notifyTrayListeners */
static void
printString( XWStreamCtxt* stream, XP_UCHAR* str )
{
stream_putString( stream, str );
} /* printString */
static XP_UCHAR*
formatTray( const TrayTileSet* tiles, DictionaryCtxt* dict, XP_UCHAR* buf,
XP_U16 bufSize, XP_Bool keepHidden )
{
if ( keepHidden ) {
XP_U16 i;
for ( i = 0; i < tiles->nTiles; ++i ) {
buf[i] = '?';
}
buf[i] = '\0';
} else {
dict_tilesToString( dict, (Tile*)tiles->tiles, tiles->nTiles,
buf, bufSize );
}
return buf;
} /* formatTray */
typedef struct MovePrintClosure {
XWStreamCtxt* stream;
DictionaryCtxt* dict;
XP_U16 nPrinted;
XP_Bool keepHidden;
} MovePrintClosure;
static void
printMovePre( ModelCtxt* model, XP_U16 moveN, StackEntry* entry,
void* p_closure )
{
XWStreamCtxt* stream;
XP_UCHAR* format;
XP_UCHAR buf[32];
XP_UCHAR traybuf[MAX_TRAY_TILES+1];
MovePrintClosure* closure = (MovePrintClosure*)p_closure;
if ( entry->moveType == ASSIGN_TYPE ) {
return;
}
stream = closure->stream;
XP_SNPRINTF( buf, sizeof(buf), (XP_UCHAR*)"%d:%d ", ++closure->nPrinted,
entry->playerNum+1 );
printString( stream, (XP_UCHAR*)buf );
if ( entry->moveType == TRADE_TYPE ) {
} else {
XP_UCHAR letter[2] = {'\0','\0'};
XP_Bool isHorizontal = entry->u.move.moveInfo.isHorizontal;
XP_U16 col, row;
MoveInfo* mi;
XP_Bool isPass = XP_FALSE;
if ( entry->moveType == PHONY_TYPE ) {
mi = &entry->u.phony.moveInfo;
} else {
mi = &entry->u.move.moveInfo;
if ( mi->nTiles == 0 ) {
isPass = XP_TRUE;
}
}
if ( isPass ) {
format = util_getUserString( model->vol.util, STR_PASS );
XP_SNPRINTF( buf, sizeof(buf), format );
} else {
if ( isHorizontal ) {
format = util_getUserString( model->vol.util, STRS_MOVE_ACROSS );
} else {
format = util_getUserString( model->vol.util, STRS_MOVE_DOWN );
}
row = mi->commonCoord;
col = mi->tiles[0].varCoord;
if ( !isHorizontal ) {
XP_U16 tmp = col; col = row; row = tmp;
}
letter[0] = 'A' + col;
XP_SNPRINTF( traybuf, sizeof(traybuf), (XP_UCHAR *)"%s%d",
letter, row + 1 );
XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
}
printString( stream, (XP_UCHAR*)buf );
}
if ( !closure->keepHidden ) {
format = util_getUserString( model->vol.util, STRS_TRAY_AT_START );
formatTray( model_getPlayerTiles( model, entry->playerNum ),
closure->dict, (XP_UCHAR*)traybuf, sizeof(traybuf),
XP_FALSE );
XP_SNPRINTF( buf, sizeof(buf), format, traybuf );
printString( stream, buf );
}
} /* printMovePre */
static void
printMovePost( ModelCtxt* model, XP_U16 moveN, StackEntry* entry,
XP_S16 score, void* p_closure )
{
MovePrintClosure* closure = (MovePrintClosure*)p_closure;
XWStreamCtxt* stream = closure->stream;
DictionaryCtxt* dict = closure->dict;
XP_UCHAR* format;
XP_U16 nTiles;
XP_S16 totalScore;
XP_UCHAR buf[100];
XP_UCHAR traybuf1[MAX_TRAY_TILES+1];
XP_UCHAR traybuf2[MAX_TRAY_TILES+1];
MoveInfo* mi;
if ( entry->moveType == ASSIGN_TYPE ) {
return;
}
totalScore = model_getPlayerScore( model, entry->playerNum );
switch( entry->moveType ) {
case TRADE_TYPE:
formatTray( (const TrayTileSet*)&entry->u.trade.oldTiles,
dict, traybuf1, sizeof(traybuf1), closure->keepHidden );
formatTray( (const TrayTileSet*) &entry->u.trade.newTiles,
dict, traybuf2, sizeof(traybuf2), closure->keepHidden );
format = util_getUserString( model->vol.util, STRSS_TRADED_FOR );
XP_SNPRINTF( buf, sizeof(buf), format, traybuf1, traybuf2 );
printString( stream, buf );
printString( stream, (XP_UCHAR*)XP_CR );
break;
case PHONY_TYPE:
format = util_getUserString( model->vol.util, STR_PHONY_REJECTED );
printString( stream, format );
case MOVE_TYPE:
format = util_getUserString( model->vol.util, STRD_CUMULATIVE_SCORE );
XP_SNPRINTF( buf, sizeof(buf), format, totalScore );
printString( stream, buf );
if ( entry->moveType == PHONY_TYPE ) {
mi = &entry->u.phony.moveInfo;
} else {
mi = &entry->u.move.moveInfo;
}
nTiles = mi->nTiles;
if ( nTiles > 0 ) {
if ( entry->moveType == PHONY_TYPE ) {
/* printString( stream, (XP_UCHAR*)"phony rejected " ); */
} else if ( !closure->keepHidden ) {
format = util_getUserString(model->vol.util, STRS_NEW_TILES);
XP_SNPRINTF( buf, sizeof(buf), format,
formatTray( &entry->u.move.newTiles, dict,
traybuf1, sizeof(traybuf1),
XP_FALSE ) );
printString( stream, buf );
}
}
break;
}
printString( stream, (XP_UCHAR*)XP_CR );
} /* printMovePost */
static void
copyStack( ModelCtxt* model, StackCtxt* destStack, const StackCtxt* srcStack )
{
XWStreamCtxt* stream = mem_stream_make( MPPARM(model->vol.mpool)
util_getVTManager(model->vol.util),
NULL, 0, NULL );
stack_writeToStream( (StackCtxt*)srcStack, stream );
stack_loadFromStream( destStack, stream );
stream_destroy( stream );
} /* copyStack */
static ModelCtxt*
makeTmpModel( ModelCtxt* model, XWStreamCtxt* stream,
MovePrintFuncPre mpf_pre, MovePrintFuncPost mpf_post,
void* closure )
{
ModelCtxt* tmpModel = model_make( MPPARM(model->vol.mpool)
model_getDictionary(model),
model->vol.util, model_numCols(model),
model_numRows(model));
model_setNPlayers( tmpModel, model->nPlayers );
buildModelFromStack( tmpModel, model->vol.stack, stream,
mpf_pre, mpf_post, closure );
return tmpModel;
} /* makeTmpModel */
void
model_writeGameHistory( ModelCtxt* model, XWStreamCtxt* stream,
ServerCtxt* server, XP_Bool gameOver )
{
ModelCtxt* tmpModel;
MovePrintClosure closure;
closure.stream = stream;
closure.dict = model_getDictionary( model );
closure.keepHidden = !gameOver;
closure.nPrinted = 0;
tmpModel = makeTmpModel( model, stream, printMovePre, printMovePost,
&closure );
model_destroy( tmpModel );
if ( gameOver ) {
/* if the game's over, it shouldn't matter which model I pass to this
method */
server_writeFinalScores( server, stream );
}
} /* model_writeGameHistory */
static void
scoreLastMove( ModelCtxt* model, MoveInfo* moveInfo, XP_U16 howMany,
XP_UCHAR* buf, XP_U16* bufLen )
{
if ( moveInfo->nTiles == 0 ) {
XP_UCHAR* str = util_getUserString( model->vol.util, STR_PASSED );
XP_U16 len = XP_STRLEN( str );
*bufLen = len;
XP_MEMCPY( buf, str, len+1 ); /* no XP_STRCPY yet */
} else {
XP_U16 score;
XP_UCHAR wordBuf[MAX_ROWS+1];
XP_UCHAR* format;
ModelCtxt* tmpModel = makeTmpModel( model, NULL, NULL, NULL, NULL );
XP_U16 turn;
XP_S16 moveNum = -1;
copyStack( model, tmpModel->vol.stack, model->vol.stack );
if ( !model_undoLatestMoves( tmpModel, NULL, howMany, &turn,
&moveNum ) ) {
XP_ASSERT( 0 );
}
score = figureMoveScore( tmpModel, moveInfo, (EngineCtxt*)NULL,
(XWStreamCtxt*)NULL, XP_TRUE,
(WordNotifierInfo*)NULL, wordBuf );
model_destroy( tmpModel );
format = util_getUserString( model->vol.util, STRSD_SUMMARYSCORED );
*bufLen = XP_SNPRINTF( buf, *bufLen, format, wordBuf, score );
}
} /* scoreLastMove */
static XP_U16
model_getRecentPassCount( ModelCtxt* model )
{
StackCtxt* stack = model->vol.stack;
XP_U16 nPasses = 0;
XP_S16 nEntries, which;
StackEntry entry;
XP_ASSERT( !!stack );
nEntries = stack_getNEntries( stack );
for ( which = nEntries - 1; which >= 0; --which ) {
if ( stack_getNthEntry( stack, which, &entry ) ) {
if ( entry.moveType == MOVE_TYPE
&& entry.u.move.moveInfo.nTiles == 0 ) {
++nPasses;
} else {
break;
}
} else {
break;
}
}
return nPasses;
} /* model_getRecentPassCount */
XP_Bool
model_recentPassCountOk( ModelCtxt* model )
{
XP_U16 count = model_getRecentPassCount( model );
XP_U16 okCount = model->nPlayers * MAX_PASSES;
XP_ASSERT( count <= okCount ); /* should never be more than 1 over */
return count < okCount;
}
XP_Bool
model_getPlayersLastScore( ModelCtxt* model, XP_S16 player,
XP_UCHAR* expl, XP_U16* explLen )
{
StackCtxt* stack = model->vol.stack;
XP_S16 nEntries, which;
StackEntry entry;
XP_Bool found = XP_FALSE;
XP_ASSERT( !!stack );
XP_ASSERT( player >= 0 );
nEntries = stack_getNEntries( stack );
for ( which = nEntries; which >= 0; ) {
if ( stack_getNthEntry( stack, --which, &entry ) ) {
if ( entry.playerNum == player ) {
found = XP_TRUE;
break;
}
}
}
if ( found ) { /* success? */
XP_UCHAR* format;
XP_U16 nTiles;
switch ( entry.moveType ) {
case MOVE_TYPE:
scoreLastMove( model, &entry.u.move.moveInfo,
nEntries - which - 1, expl, explLen );
break;
case TRADE_TYPE:
nTiles = entry.u.trade.oldTiles.nTiles;
format = util_getUserString( model->vol.util, STRD_TRADED );
*explLen = XP_SNPRINTF( expl, *explLen, format, nTiles );
break;
case PHONY_TYPE:
format = util_getUserString( model->vol.util, STR_LOSTTURN );
*explLen = XP_STRLEN( format );
XP_STRCAT( expl, format );
break;
case ASSIGN_TYPE:
found = XP_FALSE;
break;
}
}
return found;
} /* model_getPlayersLastScore */
static void
loadPlayerCtxt( XWStreamCtxt* stream, PlayerCtxt* pc )
{
XP_U16 i;
pc->curMoveValid = stream_getBits( stream, 1 );
traySetFromStream( stream, &pc->trayTiles );
pc->nPending = (XP_U8)stream_getBits( stream, NTILES_NBITS );
for ( i = 0; i < pc->nPending; ++i ) {
PendingTile* pt = &pc->pendingTiles[i];
pt->col = (XP_U8)stream_getBits( stream, NUMCOLS_NBITS );
pt->row = (XP_U8)stream_getBits( stream, NUMCOLS_NBITS );
pt->tile = (Tile)stream_getBits( stream, 6 );
}
} /* loadPlayerCtxt */
static void
writePlayerCtxt( XWStreamCtxt* stream, PlayerCtxt* pc )
{
XP_U16 i;
stream_putBits( stream, 1, pc->curMoveValid );
traySetToStream( stream, &pc->trayTiles );
stream_putBits( stream, NTILES_NBITS, pc->nPending );
for ( i = 0; i < pc->nPending; ++i ) {
PendingTile* pt = &pc->pendingTiles[i];
stream_putBits( stream, NUMCOLS_NBITS, pt->col );
stream_putBits( stream, NUMCOLS_NBITS, pt->row );
stream_putBits( stream, 6, pt->tile );
}
} /* loadPlayerCtxt */
#ifdef CPLUS
}
#endif