mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2024-12-28 09:58:30 +01:00
4a57b76817
It's only needed when there's a close proc, and that's rare.
2966 lines
91 KiB
C
2966 lines
91 KiB
C
/* -*- compile-command: "cd ../linux && make -j3 MEMDEBUG=TRUE"; -*- */
|
|
/*
|
|
* Copyright 2000 - 2022 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 "game.h"
|
|
#include "dbgutil.h"
|
|
#include "memstream.h"
|
|
#include "strutils.h"
|
|
#include "LocalizedStrIncludes.h"
|
|
|
|
#ifdef CPLUS
|
|
extern "C" {
|
|
#endif
|
|
|
|
#define MAX_PASSES 2 /* how many times can all players pass? */
|
|
|
|
/****************************** prototypes ******************************/
|
|
typedef void (*MovePrintFuncPre)(ModelCtxt*, XWEnv, XP_U16, const StackEntry*, void*);
|
|
typedef void (*MovePrintFuncPost)(ModelCtxt*, XWEnv, XP_U16, const 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, XWEnv xwe, XP_U16 turn,
|
|
XP_U16 col, XP_U16 row, XP_Bool added );
|
|
static void notifyTrayListeners( ModelCtxt* model, XP_U16 turn,
|
|
XP_S16 index1, XP_S16 index2 );
|
|
static void notifyDictListeners( ModelCtxt* model, XWEnv xwe, XP_S16 playerNum,
|
|
const DictionaryCtxt* oldDict,
|
|
const DictionaryCtxt* newDict );
|
|
static void model_unrefDicts( ModelCtxt* model, XWEnv xwe );
|
|
|
|
static CellTile getModelTileRaw( const ModelCtxt* model, XP_U16 col,
|
|
XP_U16 row );
|
|
static void setModelTileRaw( ModelCtxt* model, XP_U16 col, XP_U16 row,
|
|
CellTile tile );
|
|
static void makeTileTrade( ModelCtxt* model, XP_S16 player,
|
|
const TrayTileSet* oldTiles,
|
|
const TrayTileSet* newTiles );
|
|
static XP_S16 commitTurn( ModelCtxt* model, XWEnv xwe, XP_S16 turn,
|
|
const TrayTileSet* newTiles, XWStreamCtxt* stream,
|
|
WordNotifierInfo* wni, XP_Bool useStack );
|
|
static void buildModelFromStack( ModelCtxt* model, XWEnv xwe, StackCtxt* stack,
|
|
XP_Bool useStack, XP_U16 initial,
|
|
XWStreamCtxt* stream, WordNotifierInfo* wni,
|
|
MovePrintFuncPre mpfpr,
|
|
MovePrintFuncPost mpfpo, void* closure );
|
|
static void setPendingCounts( ModelCtxt* model, XP_S16 turn );
|
|
static XP_S16 setContains( const TrayTileSet* tiles, Tile tile );
|
|
static void loadPlayerCtxt( const ModelCtxt* model, XWStreamCtxt* stream,
|
|
XP_U16 version, PlayerCtxt* pc );
|
|
static void writePlayerCtxt( const ModelCtxt* model, XWStreamCtxt* stream,
|
|
const PlayerCtxt* pc );
|
|
static XP_U16 model_getRecentPassCount( ModelCtxt* model );
|
|
static void recordWord( const WNParams* wnp, void *closure );
|
|
#ifdef DEBUG
|
|
typedef struct _DiffTurnState {
|
|
XP_S16 lastPlayerNum;
|
|
XP_S16 lastMoveNum;
|
|
} DiffTurnState;
|
|
static void assertDiffTurn( ModelCtxt* model, XWEnv xwe, XP_U16 turn,
|
|
const StackEntry* entry, void* closure);
|
|
#endif
|
|
|
|
/*****************************************************************************
|
|
*
|
|
****************************************************************************/
|
|
ModelCtxt*
|
|
model_make( MPFORMAL XWEnv xwe, const DictionaryCtxt* dict,
|
|
const PlayerDicts* dicts, XW_UtilCtxt* util, XP_U16 nCols )
|
|
{
|
|
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;
|
|
result->vol.dutil = util_getDevUtilCtxt( util, xwe );
|
|
result->vol.wni.proc = recordWord;
|
|
result->vol.wni.closure = &result->vol.rwi;
|
|
|
|
XP_ASSERT( !!util->gameInfo );
|
|
result->vol.gi = util->gameInfo;
|
|
|
|
model_setSize( result, nCols );
|
|
|
|
model_setDictionary( result, xwe, dict );
|
|
model_setPlayerDicts( result, xwe, dicts );
|
|
}
|
|
|
|
return result;
|
|
} /* model_make */
|
|
|
|
ModelCtxt*
|
|
model_makeFromStream( MPFORMAL XWEnv xwe, XWStreamCtxt* stream,
|
|
const DictionaryCtxt* dict, const PlayerDicts* dicts,
|
|
XW_UtilCtxt* util )
|
|
{
|
|
ModelCtxt* model;
|
|
XP_U16 nCols;
|
|
XP_U16 version = stream_getVersion( stream );
|
|
|
|
XP_ASSERT( !!dict || !!dicts );
|
|
|
|
if ( 0 ) {
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
} else if ( STREAM_VERS_BIGBOARD <= version ) {
|
|
nCols = (XP_U16)stream_getBits( stream, NUMCOLS_NBITS_5 );
|
|
#endif
|
|
} else {
|
|
nCols = (XP_U16)stream_getBits( stream, NUMCOLS_NBITS_4 );
|
|
(void)stream_getBits( stream, NUMCOLS_NBITS_4 );
|
|
}
|
|
XP_ASSERT( MAX_COLS >= nCols );
|
|
|
|
XP_U16 nPlayers = (XP_U16)stream_getBits( stream, NPLAYERS_NBITS );
|
|
|
|
model = model_make( MPPARM(mpool) xwe, dict, dicts, util, nCols );
|
|
model->nPlayers = nPlayers;
|
|
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
if ( STREAM_VERS_BIGBOARD <= version ) {
|
|
model->vol.nBonuses = stream_getBits( stream, 7 );
|
|
if ( 0 < model->vol.nBonuses ) {
|
|
model->vol.bonuses =
|
|
XP_MALLOC( model->vol.mpool,
|
|
model->vol.nBonuses * sizeof( model->vol.bonuses[0] ) );
|
|
for ( int ii = 0; ii < model->vol.nBonuses; ++ii ) {
|
|
model->vol.bonuses[ii] = stream_getBits( stream, 4 );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
stack_loadFromStream( model->vol.stack, stream );
|
|
|
|
MovePrintFuncPre pre = NULL;
|
|
void* closure = NULL;
|
|
#ifdef DEBUG
|
|
pre = assertDiffTurn;
|
|
DiffTurnState state = { -1, -1 };
|
|
closure = &state;
|
|
#endif
|
|
|
|
buildModelFromStack( model, xwe, model->vol.stack, XP_FALSE, 0,
|
|
(XWStreamCtxt*)NULL, (WordNotifierInfo*)NULL,
|
|
pre, (MovePrintFuncPost)NULL, closure );
|
|
|
|
for ( int ii = 0; ii < model->nPlayers; ++ii ) {
|
|
loadPlayerCtxt( model, stream, version, &model->players[ii] );
|
|
setPendingCounts( model, ii );
|
|
invalidateScore( model, ii );
|
|
}
|
|
|
|
return model;
|
|
} /* model_makeFromStream */
|
|
|
|
void
|
|
model_writeToStream( const ModelCtxt* model, XWStreamCtxt* stream )
|
|
{
|
|
XP_U16 ii;
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
XP_ASSERT( STREAM_VERS_BIGBOARD <= stream_getVersion( stream ) );
|
|
stream_putBits( stream, NUMCOLS_NBITS_5, model->nCols );
|
|
#else
|
|
stream_putBits( stream, NUMCOLS_NBITS_4, model->nCols );
|
|
stream_putBits( stream, NUMCOLS_NBITS_4, model->nRows );
|
|
#endif
|
|
|
|
/* we have two bits for nPlayers, so range must be 0..3, not 1..4 */
|
|
stream_putBits( stream, NPLAYERS_NBITS, model->nPlayers );
|
|
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
stream_putBits( stream, 7, model->vol.nBonuses );
|
|
for ( ii = 0; ii < model->vol.nBonuses; ++ii ) {
|
|
stream_putBits( stream, 4, model->vol.bonuses[ii] );
|
|
}
|
|
#endif
|
|
|
|
stack_writeToStream( model->vol.stack, stream );
|
|
|
|
for ( ii = 0; ii < model->nPlayers; ++ii ) {
|
|
writePlayerCtxt( model, stream, &model->players[ii] );
|
|
}
|
|
} /* model_writeToStream */
|
|
|
|
#ifdef TEXT_MODEL
|
|
void
|
|
model_writeToTextStream( const ModelCtxt* model, XWStreamCtxt* stream )
|
|
{
|
|
const DictionaryCtxt* dict = model_getDictionary( model );
|
|
int width = dict_getMaxWidth( dict );
|
|
XP_UCHAR empty[4] = {0};
|
|
XP_U16 ii;
|
|
XP_U16 col, row;
|
|
|
|
for ( ii = 0; ii < width; ++ii ) {
|
|
empty[ii] = '.';
|
|
}
|
|
|
|
for ( row = 0; row < model->nRows; ++row ) {
|
|
XP_UCHAR buf[256];
|
|
XP_U16 len = 0;
|
|
for ( col = 0; col < model->nCols; ++col ) {
|
|
XP_Bool isBlank;
|
|
Tile tile;
|
|
if ( model_getTile( model, col, row, XP_FALSE, -1, &tile,
|
|
&isBlank, NULL, NULL ) ) {
|
|
const XP_UCHAR* face = dict_getTileString( dict, tile );
|
|
XP_UCHAR lower[1+XP_STRLEN(face)];
|
|
XP_STRNCPY( lower, face, sizeof(lower) );
|
|
if ( isBlank ) {
|
|
XP_LOWERSTR( lower );
|
|
}
|
|
len += snprintf( &buf[len], sizeof(buf) - len, "%*s",
|
|
width, lower );
|
|
} else {
|
|
len += snprintf( &buf[len], sizeof(buf) - len, "%s", empty );
|
|
}
|
|
}
|
|
buf[len++] = '\n';
|
|
buf[len] = '\0';
|
|
stream_catString( stream, buf );
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void
|
|
model_setSize( ModelCtxt* model, XP_U16 nCols )
|
|
{
|
|
ModelVolatiles saveVol = model->vol; /* save vol so we don't wipe it out */
|
|
XP_U16 oldSize = model->nCols; /* zero when called from model_make() */
|
|
|
|
XP_ASSERT( MAX_COLS >= nCols );
|
|
XP_ASSERT( model != NULL );
|
|
XP_MEMSET( model, 0, sizeof( *model ) );
|
|
|
|
model->nCols = nCols;
|
|
model->nRows = nCols;
|
|
model->vol = saveVol;
|
|
|
|
ModelVolatiles* vol = &model->vol;
|
|
if ( oldSize != nCols ) {
|
|
if ( !!vol->tiles ) {
|
|
XP_FREE( vol->mpool, vol->tiles );
|
|
}
|
|
vol->tiles = XP_MALLOC( vol->mpool, TILES_SIZE(model, nCols) );
|
|
}
|
|
XP_MEMSET( vol->tiles, TILE_EMPTY_BIT, TILES_SIZE(model, nCols) );
|
|
|
|
if ( !!vol->stack ) {
|
|
stack_init( vol->stack, vol->gi->nPlayers, vol->gi->inDuplicateMode );
|
|
} else {
|
|
vol->stack = stack_make( MPPARM(vol->mpool)
|
|
dutil_getVTManager(vol->dutil),
|
|
vol->gi->nPlayers, vol->gi->inDuplicateMode );
|
|
}
|
|
} /* model_setSize */
|
|
|
|
void
|
|
model_forceStack7Tiles( ModelCtxt* model )
|
|
{
|
|
stack_set7Tiles( model->vol.stack );
|
|
}
|
|
|
|
void
|
|
model_destroy( ModelCtxt* model, XWEnv xwe )
|
|
{
|
|
model_unrefDicts( model, xwe );
|
|
stack_destroy( model->vol.stack );
|
|
/* is this it!? */
|
|
if ( !!model->vol.bonuses ) {
|
|
XP_FREE( model->vol.mpool, model->vol.bonuses );
|
|
}
|
|
XP_FREE( model->vol.mpool, model->vol.tiles );
|
|
XP_FREE( model->vol.mpool, model );
|
|
} /* model_destroy */
|
|
|
|
XP_U32
|
|
model_getHash( const ModelCtxt* model )
|
|
{
|
|
#ifndef STREAM_VERS_HASHSTREAM
|
|
XP_USE(version);
|
|
#endif
|
|
StackCtxt* stack = model->vol.stack;
|
|
XP_ASSERT( !!stack );
|
|
return stack_getHash( stack );
|
|
}
|
|
|
|
XP_Bool
|
|
model_hashMatches( const ModelCtxt* model, const XP_U32 hash )
|
|
{
|
|
StackCtxt* stack = model->vol.stack;
|
|
XP_Bool matches = hash == stack_getHash( stack );
|
|
XP_LOGFF( "(hash=%X) => %s", hash, boolToStr(matches) );
|
|
return matches;
|
|
}
|
|
|
|
XP_Bool
|
|
model_popToHash( ModelCtxt* model, XWEnv xwe, const XP_U32 hash, PoolContext* pool )
|
|
{
|
|
LOG_FUNC();
|
|
XP_U16 nPopped = 0;
|
|
StackCtxt* stack = model->vol.stack;
|
|
const XP_U16 nEntries = stack_getNEntries( stack );
|
|
StackEntry entries[nEntries];
|
|
XP_S16 foundAt = -1;
|
|
|
|
for ( XP_U16 ii = 0; ii < nEntries; ++ii ) {
|
|
XP_U32 hash1 = stack_getHash( stack );
|
|
XP_LOGFF( "comparing %X with entry #%d %X", hash, nEntries - ii, hash1 );
|
|
if ( hash == hash1 ) {
|
|
foundAt = ii;
|
|
break;
|
|
}
|
|
|
|
if ( ! stack_popEntry( stack, &entries[ii] ) ) {
|
|
XP_LOGFF( "stack_popEntry(%d) failed", ii );
|
|
XP_ASSERT(0);
|
|
break;
|
|
}
|
|
++nPopped;
|
|
}
|
|
|
|
for ( XP_S16 ii = nPopped - 1; ii >= 0; --ii ) {
|
|
stack_redo( stack, &entries[ii] );
|
|
stack_freeEntry( stack, &entries[ii] );
|
|
}
|
|
|
|
XP_Bool found = -1 != foundAt;
|
|
if ( found ) {
|
|
if ( 0 < foundAt ) { /* if 0, nothing to do */
|
|
XP_LOGFF( "undoing %d turns to match hash %X",
|
|
foundAt, hash );
|
|
#ifdef DEBUG
|
|
XP_Bool success =
|
|
#endif
|
|
model_undoLatestMoves( model, xwe, pool, foundAt, NULL, NULL );
|
|
XP_ASSERT( success );
|
|
}
|
|
/* Assert not needed for long */
|
|
XP_ASSERT( hash == stack_getHash( model->vol.stack ) );
|
|
} else {
|
|
XP_ASSERT( nEntries == stack_getNEntries(stack) );
|
|
}
|
|
|
|
LOG_RETURNF( "%s (hash=%X, nEntries=%d)", boolToStr(found), hash, nEntries );
|
|
return found;
|
|
} /* model_popToHash */
|
|
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
void
|
|
model_setSquareBonuses( ModelCtxt* XP_UNUSED(model), XWBonusType* XP_UNUSED(bonuses),
|
|
XP_U16 XP_UNUSED(nBonuses) )
|
|
{
|
|
XP_LOGFF( "doing nothing" );
|
|
}
|
|
#endif
|
|
|
|
#define EM BONUS_NONE
|
|
#define DL BONUS_DOUBLE_LETTER
|
|
#define DW BONUS_DOUBLE_WORD
|
|
#define TL BONUS_TRIPLE_LETTER
|
|
#define TW BONUS_TRIPLE_WORD
|
|
#define QL BONUS_QUAD_LETTER
|
|
#define QW BONUS_QUAD_WORD
|
|
|
|
static XWBonusType sTwentyOne[] = {
|
|
QW,
|
|
EM, DW,
|
|
EM, EM, DW,
|
|
DL, EM, EM, TW,
|
|
EM, TL, EM, EM, DW,
|
|
EM, EM, QL, EM, EM, DW,
|
|
EM, EM, EM, DL, EM, EM, DW,
|
|
TW, EM, EM, EM, EM, EM, EM, DW,
|
|
EM, DW, EM, EM, TL, EM, EM, EM, TL,
|
|
EM, EM, DW, EM, EM, DL, EM, EM, EM, DL,
|
|
DL, EM, EM, TW, EM, EM, DL, EM, EM, EM, DW,
|
|
}; /* sTwentyOne */
|
|
|
|
static XWBonusType
|
|
getSquareBonus( XP_U16 nCols, XP_U16 col, XP_U16 row )
|
|
{
|
|
if ( col > (nCols/2) ) {
|
|
col = nCols - 1 - col;
|
|
}
|
|
if ( row > (nCols/2) ) {
|
|
row = nCols - 1 - row;
|
|
}
|
|
if ( col > row ) {
|
|
XP_U16 tmp = col;
|
|
col = row;
|
|
row = tmp;
|
|
}
|
|
|
|
/* For a smaller board, skip the outer "rings." For larger,
|
|
outer rings are empty */
|
|
XWBonusType result = BONUS_NONE;
|
|
XP_U16 adj = (21 - nCols) / 2;
|
|
if ( 0 <= adj ) {
|
|
col += adj;
|
|
row += adj;
|
|
|
|
if ( col <= 21 && row <= 21 ) {
|
|
XP_U16 index = col;
|
|
for ( XP_U16 ii = 1; ii <= row; ++ii ) {
|
|
index += ii;
|
|
}
|
|
|
|
if ( index < VSIZE(sTwentyOne)) {
|
|
result = sTwentyOne[index];
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
XWBonusType
|
|
model_getSquareBonus( const ModelCtxt* model, XP_U16 col, XP_U16 row )
|
|
{
|
|
XWBonusType result = BONUS_NONE;
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
const ModelCtxt* bonusOwner = model->loaner? model->loaner : model;
|
|
#endif
|
|
|
|
if ( 0 ) {
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
} else if ( !!bonusOwner->vol.bonuses ) {
|
|
XP_U16 nCols = model_numCols( model );
|
|
XP_U16 ii;
|
|
if ( col > (nCols/2) ) {
|
|
col = nCols - 1 - col;
|
|
}
|
|
if ( row > (nCols/2) ) {
|
|
row = nCols - 1 - row;
|
|
}
|
|
if ( col > row ) {
|
|
XP_U16 tmp = col;
|
|
col = row;
|
|
row = tmp;
|
|
}
|
|
for ( ii = 1; ii <= row; ++ii ) {
|
|
col += ii;
|
|
}
|
|
|
|
if ( col < bonusOwner->vol.nBonuses ) {
|
|
result = bonusOwner->vol.bonuses[col];
|
|
}
|
|
#endif
|
|
} else {
|
|
result = getSquareBonus( model_numRows(model), col, row );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static XP_U16
|
|
makeAndCommit( ModelCtxt* model, XWEnv xwe, XP_U16 turn, const MoveInfo* mi,
|
|
const TrayTileSet* tiles, XWStreamCtxt* stream,
|
|
XP_Bool useStack, WordNotifierInfo* wni )
|
|
{
|
|
model_makeTurnFromMoveInfo( model, xwe, turn, mi );
|
|
XP_U16 moveScore = commitTurn( model, xwe, turn, tiles,
|
|
stream, wni, useStack );
|
|
return moveScore;
|
|
}
|
|
|
|
static void
|
|
dupe_adjustScores( ModelCtxt* model, XP_Bool add, XP_U16 nScores, const XP_U16* scores )
|
|
{
|
|
XP_S16 mult = add ? 1 : -1;
|
|
for ( XP_U16 ii = 0; ii < nScores; ++ii ) {
|
|
model->players[ii].score += mult * scores[ii];
|
|
}
|
|
}
|
|
|
|
void
|
|
model_cloneDupeTrays( ModelCtxt* model, XWEnv xwe )
|
|
{
|
|
XP_ASSERT( model->vol.gi->inDuplicateMode );
|
|
XP_U16 nTiles = model->players[DUP_PLAYER].trayTiles.nTiles;
|
|
for ( XP_U16 ii = 0; ii < model->nPlayers; ++ii ) {
|
|
if ( ii != DUP_PLAYER ) {
|
|
model_resetCurrentTurn( model, xwe, ii );
|
|
model->players[ii].trayTiles = model->players[DUP_PLAYER].trayTiles;
|
|
notifyTrayListeners( model, ii, 0, nTiles );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
modelAddEntry( ModelCtxt* model, XWEnv xwe, XP_U16 indx, const StackEntry* entry,
|
|
XP_Bool useStack, XWStreamCtxt* stream,
|
|
WordNotifierInfo* wni, MovePrintFuncPre mpf_pre,
|
|
MovePrintFuncPost mpf_post, void* closure )
|
|
{
|
|
XP_S16 moveScore = 0; /* keep compiler happy */
|
|
if ( !!mpf_pre ) {
|
|
(*mpf_pre)( model, xwe, indx, entry, closure );
|
|
}
|
|
|
|
switch ( entry->moveType ) {
|
|
case MOVE_TYPE:
|
|
moveScore = makeAndCommit( model, xwe, entry->playerNum, &entry->u.move.moveInfo,
|
|
&entry->u.move.newTiles, stream, useStack, wni );
|
|
if ( model->vol.gi->inDuplicateMode ) {
|
|
XP_ASSERT( DUP_PLAYER == entry->playerNum );
|
|
dupe_adjustScores( model, XP_TRUE, entry->u.move.dup.nScores,
|
|
entry->u.move.dup.scores );
|
|
model_cloneDupeTrays( model, xwe );
|
|
}
|
|
break;
|
|
case TRADE_TYPE:
|
|
makeTileTrade( model, entry->playerNum, &entry->u.trade.oldTiles,
|
|
&entry->u.trade.newTiles );
|
|
if ( model->vol.gi->inDuplicateMode ) {
|
|
XP_ASSERT( DUP_PLAYER == entry->playerNum );
|
|
model_cloneDupeTrays( model, xwe );
|
|
}
|
|
break;
|
|
case ASSIGN_TYPE:
|
|
model_addNewTiles( model, entry->playerNum, &entry->u.assign.tiles );
|
|
if ( model->vol.gi->inDuplicateMode ) {
|
|
XP_ASSERT( DUP_PLAYER == entry->playerNum );
|
|
model_cloneDupeTrays( model, xwe );
|
|
}
|
|
break;
|
|
case PHONY_TYPE: /* nothing to add */
|
|
model_makeTurnFromMoveInfo( model, xwe, entry->playerNum,
|
|
&entry->u.phony.moveInfo);
|
|
/* do something here to cause it to print */
|
|
(void)getCurrentMoveScoreIfLegal( model, xwe, entry->playerNum, stream,
|
|
wni, &moveScore );
|
|
moveScore = 0;
|
|
model_resetCurrentTurn( model, xwe, entry->playerNum );
|
|
|
|
break;
|
|
case PAUSE_TYPE:
|
|
// XP_LOGF( "%s(): nothing to do with PAUSE_TYPE", __func__ );
|
|
break;
|
|
default:
|
|
XP_ASSERT(0);
|
|
}
|
|
|
|
if ( !!mpf_post ) {
|
|
(*mpf_post)( model, xwe, indx, entry, moveScore, closure );
|
|
}
|
|
} /* modelAddEntry */
|
|
|
|
static void
|
|
buildModelFromStack( ModelCtxt* model, XWEnv xwe, StackCtxt* stack, XP_Bool useStack,
|
|
XP_U16 initial, XWStreamCtxt* stream,
|
|
WordNotifierInfo* wni, MovePrintFuncPre mpf_pre,
|
|
MovePrintFuncPost mpf_post, void* closure )
|
|
{
|
|
StackEntry entry;
|
|
for ( XP_U16 ii = initial; stack_getNthEntry( stack, ii, &entry ); ++ii ) {
|
|
modelAddEntry( model, xwe, ii, &entry, useStack, stream, wni,
|
|
mpf_pre, mpf_post, closure );
|
|
stack_freeEntry( stack, &entry );
|
|
}
|
|
} /* buildModelFromStack */
|
|
|
|
void
|
|
model_setNPlayers( ModelCtxt* model, XP_U16 nPlayers )
|
|
{
|
|
model->nPlayers = nPlayers;
|
|
} /* model_setNPlayers */
|
|
|
|
XP_U16
|
|
model_getNPlayers( const ModelCtxt* model )
|
|
{
|
|
return model->nPlayers;
|
|
}
|
|
|
|
static void
|
|
setStackBits( ModelCtxt* model, const DictionaryCtxt* dict )
|
|
{
|
|
if ( NULL != dict ) {
|
|
XP_U16 nFaces = dict_numTileFaces( dict );
|
|
XP_ASSERT( !!model->vol.stack );
|
|
stack_setBitsPerTile( model->vol.stack, nFaces <= 32? 5 : 6 );
|
|
}
|
|
}
|
|
|
|
void
|
|
model_setDictionary( ModelCtxt* model, XWEnv xwe, const DictionaryCtxt* dict )
|
|
{
|
|
const DictionaryCtxt* oldDict = model->vol.dict;
|
|
model->vol.dict = dict_ref( dict, xwe );
|
|
|
|
if ( !!dict ) {
|
|
setStackBits( model, dict );
|
|
}
|
|
|
|
notifyDictListeners( model, xwe, -1, oldDict, dict );
|
|
dict_unref( oldDict, xwe );
|
|
} /* model_setDictionary */
|
|
|
|
void
|
|
model_setPlayerDicts( ModelCtxt* model, XWEnv xwe, const PlayerDicts* dicts )
|
|
{
|
|
if ( !!dicts ) {
|
|
XP_U16 ii;
|
|
#ifdef DEBUG
|
|
const DictionaryCtxt* gameDict = model_getDictionary( model );
|
|
#endif
|
|
for ( ii = 0; ii < VSIZE(dicts->dicts); ++ii ) {
|
|
const DictionaryCtxt* oldDict = model->vol.dicts.dicts[ii];
|
|
const DictionaryCtxt* newDict = dicts->dicts[ii];
|
|
if ( oldDict != newDict ) {
|
|
XP_ASSERT( NULL == newDict || NULL == gameDict
|
|
|| dict_tilesAreSame( gameDict, newDict ) );
|
|
model->vol.dicts.dicts[ii] = dict_ref( newDict, xwe );
|
|
|
|
notifyDictListeners( model, xwe, ii, oldDict, newDict );
|
|
setStackBits( model, newDict );
|
|
|
|
dict_unref( oldDict, xwe );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const DictionaryCtxt*
|
|
model_getDictionary( const ModelCtxt* model )
|
|
{
|
|
XP_U16 ii;
|
|
const DictionaryCtxt* result = model->vol.dict;
|
|
for ( ii = 0; !result && ii < VSIZE(model->vol.dicts.dicts); ++ii ) {
|
|
result = model->vol.dicts.dicts[ii];
|
|
}
|
|
return result;
|
|
} /* model_getDictionary */
|
|
|
|
const DictionaryCtxt*
|
|
model_getPlayerDict( const ModelCtxt* model, XP_S16 playerNum )
|
|
{
|
|
const DictionaryCtxt* dict = NULL;
|
|
if ( 0 <= playerNum && playerNum < VSIZE(model->vol.dicts.dicts) ) {
|
|
dict = model->vol.dicts.dicts[playerNum];
|
|
}
|
|
if ( NULL == dict ) {
|
|
dict = model->vol.dict;
|
|
}
|
|
XP_ASSERT( !!dict );
|
|
return dict;
|
|
}
|
|
|
|
static void
|
|
model_unrefDicts( ModelCtxt* model, XWEnv xwe )
|
|
{
|
|
XP_U16 ii;
|
|
for ( ii = 0; ii < VSIZE(model->vol.dicts.dicts); ++ii ) {
|
|
dict_unref( model->vol.dicts.dicts[ii], xwe );
|
|
model->vol.dicts.dicts[ii] = NULL;
|
|
}
|
|
dict_unref( model->vol.dict, xwe );
|
|
model->vol.dict = NULL;
|
|
}
|
|
|
|
static XP_Bool
|
|
getPendingTileFor( const ModelCtxt* model, XP_U16 turn, XP_U16 col, XP_U16 row,
|
|
CellTile* cellTile )
|
|
{
|
|
XP_Bool found = XP_FALSE;
|
|
const PlayerCtxt* player;
|
|
const PendingTile* pendings;
|
|
XP_U16 ii;
|
|
|
|
XP_ASSERT( turn < VSIZE(model->players) );
|
|
|
|
player = &model->players[turn];
|
|
pendings = player->pendingTiles;
|
|
for ( ii = 0; ii < player->nPending; ++ii ) {
|
|
|
|
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( const 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;
|
|
XP_Bool inUse = XP_TRUE;
|
|
|
|
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 */
|
|
inUse = 0 == (cellTile & TILE_EMPTY_BIT);
|
|
if ( inUse ) {
|
|
if ( NULL != tileP ) {
|
|
*tileP = cellTile & TILE_VALUE_MASK;
|
|
}
|
|
if ( NULL != isBlank ) {
|
|
*isBlank = IS_BLANK(cellTile);
|
|
}
|
|
if ( NULL != pendingP ) {
|
|
*pendingP = pending;
|
|
}
|
|
if ( !!recentP ) {
|
|
*recentP = (cellTile & PREV_MOVE_BIT) != 0;
|
|
}
|
|
}
|
|
return inUse;
|
|
} /* 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, XWEnv xwe, 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)( xwe, closure, (XP_U16)CELL_OWNER(tile), col, row, XP_FALSE );
|
|
}
|
|
}
|
|
}
|
|
} /* model_foreachPrevCell */
|
|
|
|
static void
|
|
clearAndNotify( XWEnv xwe, void* closure, XP_U16 XP_UNUSED(turn),
|
|
XP_U16 col, XP_U16 row, XP_Bool XP_UNUSED(added) )
|
|
{
|
|
ModelCtxt* model = (ModelCtxt*)closure;
|
|
CellTile tile = getModelTileRaw( model, col, row );
|
|
setModelTileRaw( model, col, row, (CellTile)(tile & ~PREV_MOVE_BIT) );
|
|
|
|
notifyBoardListeners( model, xwe, (XP_U16)CELL_OWNER(tile), col, row,
|
|
XP_FALSE );
|
|
} /* clearAndNotify */
|
|
|
|
static void
|
|
clearLastMoveInfo( ModelCtxt* model, XWEnv xwe )
|
|
{
|
|
model_foreachPrevCell( model, xwe, clearAndNotify, model );
|
|
} /* clearLastMoveInfo */
|
|
|
|
static void
|
|
invalLastMove( ModelCtxt* model, XWEnv xwe )
|
|
{
|
|
if ( !!model->vol.boardListenerFunc ) {
|
|
model_foreachPrevCell( model, xwe, model->vol.boardListenerFunc,
|
|
model->vol.boardListenerData );
|
|
}
|
|
} /* invalLastMove */
|
|
|
|
void
|
|
model_foreachPendingCell( ModelCtxt* model, XWEnv xwe, 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 ) {
|
|
(*bl)( xwe, closure, turn, pt->col, pt->row, XP_FALSE );
|
|
}
|
|
} /* model_foreachPendingCell */
|
|
|
|
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 < model->nCols );
|
|
XP_ASSERT( row < model->nRows );
|
|
model->vol.tiles[(row*model->nCols) + col] = tile;
|
|
} /* model_setTile */
|
|
|
|
static CellTile
|
|
getModelTileRaw( const ModelCtxt* model, XP_U16 col, XP_U16 row )
|
|
{
|
|
XP_U16 nCols = model->nCols;
|
|
XP_ASSERT( model->nRows == nCols );
|
|
XP_ASSERT( col < nCols );
|
|
XP_ASSERT( row < nCols );
|
|
return model->vol.tiles[(row*nCols) + col];
|
|
} /* getModelTileRaw */
|
|
|
|
static void
|
|
undoFromMove( ModelCtxt* model, XWEnv xwe, XP_U16 turn, Tile blankTile, MoveRec* move )
|
|
{
|
|
const MoveInfo* mi = &move->moveInfo;
|
|
|
|
XP_U16 col, row;
|
|
col = row = mi->commonCoord;
|
|
XP_U16* other = mi->isHorizontal? &col: &row;
|
|
|
|
const MoveInfoTile* tinfo;
|
|
XP_U16 ii;
|
|
for ( tinfo = mi->tiles, ii = 0; ii < mi->nTiles; ++tinfo, ++ii ) {
|
|
Tile tile = tinfo->tile;
|
|
*other = tinfo->varCoord;
|
|
|
|
setModelTileRaw( model, col, row, EMPTY_TILE );
|
|
notifyBoardListeners( model, xwe, turn, col, row, XP_FALSE );
|
|
--model->vol.nTilesOnBoard;
|
|
|
|
if ( IS_BLANK(tile) ) {
|
|
tile = blankTile;
|
|
}
|
|
model_addPlayerTile( model, turn, -1, tile );
|
|
}
|
|
|
|
if ( model->vol.gi->inDuplicateMode ) {
|
|
dupe_adjustScores( model, XP_FALSE, move->dup.nScores, move->dup.scores );
|
|
} else {
|
|
adjustScoreForUndone( model, xwe, mi, turn );
|
|
}
|
|
} /* undoFromMove */
|
|
|
|
/* 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 ii, nTiles;
|
|
|
|
for ( t = tileSet->tiles, ii = 0, nTiles = tileSet->nTiles;
|
|
ii < nTiles; ++ii ) {
|
|
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, XWEnv xwe,
|
|
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 );
|
|
|
|
model_resetCurrentTurn( model, xwe, entry.playerNum );
|
|
|
|
replaceNewTiles( model, pool, entry.playerNum, &entry.u.move.newTiles );
|
|
XP_ASSERT( !model->vol.gi->inDuplicateMode );
|
|
undoFromMove( model, xwe, entry.playerNum, blankTile, &entry.u.move );
|
|
|
|
stack_addPhony( stack, entry.playerNum, &entry.u.phony.moveInfo );
|
|
|
|
*turn = entry.playerNum;
|
|
stack_freeEntry( stack, &entry );
|
|
} /* model_rejectPreviousMove */
|
|
|
|
XP_Bool
|
|
model_canUndo( const ModelCtxt* model )
|
|
{
|
|
XP_Bool inDuplicateMode = model->vol.gi->inDuplicateMode;
|
|
/* I'm turning off undo for duplicate mode for now to avoid
|
|
crashes. Ideally a duplicate mode player could change his mind until
|
|
the timer fires, so try to fix this. PENDING*/
|
|
XP_Bool result = !inDuplicateMode;
|
|
if ( result ) {
|
|
const StackCtxt* stack = model->vol.stack;
|
|
XP_U16 nStackEntries = stack_getNEntries( stack );
|
|
|
|
/* More than just tile assignment? */
|
|
XP_U16 assignCount = inDuplicateMode ? 1 : model->nPlayers;
|
|
result = nStackEntries > assignCount;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* 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, XWEnv xwe, PoolContext* pool,
|
|
XP_U16 nMovesSought, XP_U16* turnP, XP_S16* moveNumP )
|
|
{
|
|
XP_ASSERT( 0 < nMovesSought ); /* this case isn't handled correctly */
|
|
StackCtxt* stack = model->vol.stack;
|
|
XP_Bool success;
|
|
XP_S16 moveSought = !!moveNumP ? *moveNumP : -1;
|
|
XP_U16 nStackEntries = stack_getNEntries( stack );
|
|
const XP_U16 assignCount = model->vol.gi->inDuplicateMode
|
|
? 1 : model->nPlayers;
|
|
|
|
if ( 0 <= moveSought && moveSought >= nStackEntries ) {
|
|
XP_LOGFF( "BAD: moveSought (%d) >= nStackEntries (%d)", moveSought,
|
|
nStackEntries );
|
|
success = XP_FALSE;
|
|
} else if ( nStackEntries < nMovesSought ) {
|
|
XP_LOGFF( "BAD: nStackEntries (%d) < nMovesSought (%d)", nStackEntries,
|
|
nMovesSought );
|
|
success = XP_FALSE;
|
|
} else if ( nStackEntries <= assignCount ) {
|
|
XP_LOGFF( "BAD: nStackEntries (%d) <= assignCount (%d)", nStackEntries,
|
|
assignCount );
|
|
success = XP_FALSE;
|
|
} else {
|
|
XP_U16 nMovesUndone = 0;
|
|
XP_U16 turn;
|
|
StackEntry entry;
|
|
Tile blankTile = dict_getBlankTile( model_getDictionary(model) );
|
|
|
|
for ( ; ; ) {
|
|
success = stack_popEntry( stack, &entry );
|
|
if ( !success ) {
|
|
break;
|
|
}
|
|
++nMovesUndone;
|
|
|
|
turn = entry.playerNum;
|
|
model_resetCurrentTurn( model, xwe, 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 );
|
|
|
|
undoFromMove( model, xwe, turn, blankTile, &entry.u.move );
|
|
model_sortTiles( model, turn );
|
|
|
|
if ( model->vol.gi->inDuplicateMode ) {
|
|
#ifdef DEBUG
|
|
if ( DUP_PLAYER != turn ) {
|
|
XP_LOGFF( "turn: %d", turn );
|
|
XP_ASSERT( 0 );
|
|
}
|
|
#endif
|
|
model_cloneDupeTrays( model, xwe );
|
|
}
|
|
} else if ( entry.moveType == TRADE_TYPE ) {
|
|
replaceNewTiles( model, pool, turn,
|
|
&entry.u.trade.newTiles );
|
|
if ( pool != NULL ) {
|
|
pool_removeTiles( pool, &entry.u.trade.oldTiles );
|
|
}
|
|
model_addNewTiles( 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;
|
|
}
|
|
|
|
/* exit if we've undone what we're supposed to. If the sought
|
|
stack height has been provided, use that. Otherwise go by move
|
|
count. */
|
|
if ( 0 <= moveSought ) {
|
|
if ( moveSought == entry.moveNum ) {
|
|
break;
|
|
}
|
|
} else if ( nMovesSought == nMovesUndone ) {
|
|
break;
|
|
}
|
|
stack_freeEntry( stack, &entry );
|
|
}
|
|
|
|
/* 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. */
|
|
for ( nStackEntries = stack_getNEntries( stack );
|
|
0 < nStackEntries; --nStackEntries ) {
|
|
StackEntry entry;
|
|
if ( !stack_getNthEntry( stack, nStackEntries - 1, &entry ) ) {
|
|
break;
|
|
}
|
|
if ( entry.moveType == ASSIGN_TYPE ) {
|
|
break;
|
|
} else 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, xwe, entry.playerNum, col, row,
|
|
XP_FALSE );
|
|
}
|
|
break;
|
|
}
|
|
stack_freeEntry( stack, &entry );
|
|
}
|
|
|
|
/* We fail if we didn't undo as many as requested UNLESS the lower
|
|
limit is the trumping target/test. */
|
|
if ( 0 <= moveSought ) {
|
|
if ( moveSought != entry.moveNum ) {
|
|
success = XP_FALSE;
|
|
}
|
|
} else {
|
|
if ( nMovesUndone != nMovesSought ) {
|
|
success = XP_FALSE;
|
|
}
|
|
}
|
|
|
|
if ( success ) {
|
|
if ( !!turnP ) {
|
|
*turnP = turn;
|
|
}
|
|
if ( !!moveNumP ) {
|
|
*moveNumP = entry.moveNum;
|
|
}
|
|
} else {
|
|
while ( nMovesUndone-- ) {
|
|
/* redo isn't enough here: pool's got tiles in it!! */
|
|
XP_ASSERT( 0 );
|
|
(void)stack_redo( stack, NULL );
|
|
}
|
|
}
|
|
}
|
|
|
|
return success;
|
|
} /* model_undoLatestMoves */
|
|
|
|
void
|
|
model_trayToStream( const ModelCtxt* model, XP_S16 turn, XWStreamCtxt* stream )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
const PlayerCtxt* player = &model->players[turn];
|
|
|
|
traySetToStream( stream, &player->trayTiles );
|
|
} /* model_trayToStream */
|
|
|
|
void
|
|
model_currentMoveToMoveInfo( const ModelCtxt* model, XP_S16 turn,
|
|
MoveInfo* moveInfo )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
const XP_S16 numTiles = model->players[turn].nPending;
|
|
moveInfo->nTiles = numTiles;
|
|
|
|
XP_U16 cols[MAX_TRAY_TILES];
|
|
XP_U16 rows[MAX_TRAY_TILES];
|
|
for ( XP_S16 ii = 0; ii < numTiles; ++ii ) {
|
|
XP_Bool isBlank;
|
|
Tile tile;
|
|
model_getCurrentMoveTile( model, turn, &ii, &tile,
|
|
&cols[ii], &rows[ii], &isBlank );
|
|
if ( isBlank ) {
|
|
tile |= TILE_BLANK_BIT;
|
|
}
|
|
moveInfo->tiles[ii].tile = tile;
|
|
}
|
|
|
|
XP_Bool isHorizontal = XP_TRUE;
|
|
if ( 1 == numTiles ) { /* horizonal/vertical makes no sense */
|
|
moveInfo->tiles[0].varCoord = cols[0];
|
|
moveInfo->commonCoord = rows[0];
|
|
} else if ( 1 < numTiles ) {
|
|
isHorizontal = rows[0] == rows[1];
|
|
moveInfo->commonCoord = isHorizontal ? rows[0] : cols[0];
|
|
for ( XP_U16 ii = 0; ii < numTiles; ++ii ) {
|
|
moveInfo->tiles[ii].varCoord =
|
|
isHorizontal ? cols[ii] : rows[ii];
|
|
/* MoveInfo assumes legal moves! Check here */
|
|
if ( isHorizontal ) {
|
|
XP_ASSERT( rows[ii] == rows[0] );
|
|
} else {
|
|
XP_ASSERT( cols[ii] == cols[0] );
|
|
}
|
|
}
|
|
}
|
|
moveInfo->isHorizontal = isHorizontal;
|
|
|
|
normalizeMI( moveInfo, moveInfo );
|
|
}
|
|
|
|
void
|
|
model_currentMoveToStream( const ModelCtxt* model, XP_S16 turn,
|
|
XWStreamCtxt* stream )
|
|
{
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
XP_U16 nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
|
|
: NUMCOLS_NBITS_4;
|
|
#else
|
|
XP_U16 nColsNBits = NUMCOLS_NBITS_4;
|
|
#endif
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
XP_S16 numTiles = model->players[turn].nPending;
|
|
|
|
stream_putBits( stream, tilesNBits(stream), 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, nColsNBits, col );
|
|
stream_putBits( stream, nColsNBits, row );
|
|
stream_putBits( stream, 1, isBlank );
|
|
}
|
|
} /* model_currentMoveToStream */
|
|
|
|
/* 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.
|
|
*/
|
|
XP_Bool
|
|
model_makeTurnFromStream( ModelCtxt* model, XWEnv xwe, XP_U16 playerNum,
|
|
XWStreamCtxt* stream )
|
|
{
|
|
Tile blank = dict_getBlankTile( model_getDictionary(model) );
|
|
XP_U16 nColsNBits =
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
16 <= model_numCols( model ) ? NUMCOLS_NBITS_5 : NUMCOLS_NBITS_4
|
|
#else
|
|
NUMCOLS_NBITS_4
|
|
#endif
|
|
;
|
|
|
|
model_resetCurrentTurn( model, xwe, playerNum );
|
|
|
|
XP_U16 numTiles = (XP_U16)stream_getBits( stream, tilesNBits(stream) );
|
|
XP_LOGFF( "numTiles=%d", numTiles );
|
|
|
|
Tile tileFaces[numTiles];
|
|
XP_U16 cols[numTiles];
|
|
XP_U16 rows[numTiles];
|
|
XP_Bool isBlanks[numTiles];
|
|
Tile moveTiles[numTiles];
|
|
TrayTileSet curTiles = *model_getPlayerTiles( model, playerNum );
|
|
|
|
XP_Bool success = XP_TRUE;
|
|
for ( XP_U16 ii = 0; success && ii < numTiles; ++ii ) {
|
|
tileFaces[ii] = (Tile)stream_getBits( stream, TILE_NBITS );
|
|
cols[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
|
|
rows[ii] = (XP_U16)stream_getBits( stream, nColsNBits );
|
|
isBlanks[ii] = stream_getBits( stream, 1 );
|
|
|
|
if ( isBlanks[ii] ) {
|
|
moveTiles[ii] = blank;
|
|
} else {
|
|
moveTiles[ii] = tileFaces[ii];
|
|
}
|
|
|
|
XP_S16 index = setContains( &curTiles, moveTiles[ii] );
|
|
if ( 0 <= index ) {
|
|
removeTile( &curTiles, index );
|
|
} else {
|
|
success = XP_FALSE;
|
|
}
|
|
}
|
|
|
|
if ( success ) {
|
|
for ( XP_U16 ii = 0; ii < numTiles; ++ii ) {
|
|
XP_S16 foundAt = model_trayContains( model, playerNum, moveTiles[ii] );
|
|
if ( foundAt == -1 ) {
|
|
XP_ASSERT( EMPTY_TILE == model_getPlayerTile(model, playerNum,
|
|
0));
|
|
/* Does this ever happen? */
|
|
XP_LOGFF( "found empty tile and it's ok" );
|
|
|
|
(void)model_removePlayerTile( model, playerNum, -1 );
|
|
model_addPlayerTile( model, playerNum, -1, moveTiles[ii] );
|
|
}
|
|
|
|
model_moveTrayToBoard( model, xwe, playerNum, cols[ii], rows[ii], foundAt,
|
|
tileFaces[ii] );
|
|
}
|
|
}
|
|
return success;
|
|
} /* model_makeTurnFromStream */
|
|
|
|
#ifdef DEBUG
|
|
void
|
|
juggleMoveIfDebug( MoveInfo* move )
|
|
{
|
|
XP_U16 nTiles = move->nTiles;
|
|
// XP_LOGF( "%s(): move len: %d", __func__, nTiles );
|
|
MoveInfoTile tiles[MAX_TRAY_TILES];
|
|
XP_MEMCPY( tiles, move->tiles, sizeof(tiles) );
|
|
|
|
for ( int ii = 0; ii < nTiles; ++ii ) {
|
|
int last = nTiles - ii;
|
|
int choice = XP_RANDOM() % last;
|
|
move->tiles[ii] = tiles[choice];
|
|
// XP_LOGF( "%s(): setting %d to %d", __func__, ii, choice );
|
|
if ( choice != --last ) {
|
|
tiles[choice] = tiles[last];
|
|
// XP_LOGF( "%s(): replacing %d with %d", __func__, choice, last );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Reverse the *letters on* the tiles */
|
|
#ifdef XWFEATURE_ROBOTPHONIES
|
|
void
|
|
reverseTiles( MoveInfo* move )
|
|
{
|
|
MoveInfoTile* start = &move->tiles[0];
|
|
MoveInfoTile* end = start + move->nTiles - 1;
|
|
while ( start < end ) {
|
|
Tile tmp = start->tile;
|
|
start->tile = end->tile;
|
|
end->tile = tmp;
|
|
--end; ++start;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void
|
|
model_makeTurnFromMoveInfo( ModelCtxt* model, XWEnv xwe, XP_U16 playerNum,
|
|
const MoveInfo* newMove )
|
|
{
|
|
XP_U16 col, row, ii;
|
|
XP_U16* other;
|
|
const MoveInfoTile* tinfo;
|
|
Tile blank = dict_getBlankTile( model_getDictionary( model ) );
|
|
XP_U16 numTiles = newMove->nTiles;
|
|
|
|
col = row = newMove->commonCoord; /* just assign both */
|
|
other = newMove->isHorizontal? &col: &row;
|
|
|
|
for ( tinfo = newMove->tiles, ii = 0; ii < numTiles; ++ii, ++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, xwe, (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 ii;
|
|
Tile blank;
|
|
|
|
blank = dict_getBlankTile( model_getDictionary(model) );
|
|
|
|
for ( ii = 0, player = model->players; ii < nPlayers; ++ii, ++player ) {
|
|
if ( ii != 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 )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
XP_ASSERT( turn < model->nPlayers );
|
|
|
|
const TrayTileSet* tiles = model_getPlayerTiles( model, turn );
|
|
return setContains( tiles, tile );
|
|
} /* model_trayContains */
|
|
|
|
XP_U16
|
|
model_getCurrentMoveCount( const ModelCtxt* model, XP_S16 turn )
|
|
{
|
|
const PlayerCtxt* player;
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
return player->nPending;
|
|
} /* model_getCurrentMoveCount */
|
|
|
|
XP_Bool
|
|
model_getCurrentMoveIsVertical( const ModelCtxt* model, XP_S16 turn,
|
|
XP_Bool* isVertical )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
const PlayerCtxt* player = &model->players[turn];
|
|
XP_U16 nPending = player->nPending;
|
|
XP_Bool known = 2 <= nPending;
|
|
if ( known ) {
|
|
--nPending;
|
|
if ( player->pendingTiles[nPending].col
|
|
== player->pendingTiles[nPending-1].col ) {
|
|
*isVertical = XP_TRUE;
|
|
} else if ( player->pendingTiles[nPending].row
|
|
== player->pendingTiles[nPending-1].row ) {
|
|
*isVertical = XP_FALSE;
|
|
} else {
|
|
known = XP_FALSE;
|
|
}
|
|
}
|
|
return known;
|
|
}
|
|
|
|
void
|
|
model_getCurrentMoveTile( const ModelCtxt* model, XP_S16 turn, XP_S16* index,
|
|
Tile* tile, XP_U16* col, XP_U16* row,
|
|
XP_Bool* isBlank )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
|
|
const PlayerCtxt* player = &model->players[turn];
|
|
XP_ASSERT( *index < player->nPending );
|
|
|
|
if ( *index < 0 ) {
|
|
*index = player->nPending - 1;
|
|
}
|
|
|
|
const PendingTile* 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 */
|
|
|
|
static Tile
|
|
removePlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index )
|
|
{
|
|
PlayerCtxt* player;
|
|
Tile tile;
|
|
short ii;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
|
|
XP_ASSERT( index < player->trayTiles.nTiles );
|
|
|
|
tile = player->trayTiles.tiles[index];
|
|
|
|
--player->trayTiles.nTiles;
|
|
for ( ii = index; ii < player->trayTiles.nTiles; ++ii ) {
|
|
player->trayTiles.tiles[ii] = player->trayTiles.tiles[ii+1];
|
|
}
|
|
|
|
return tile;
|
|
} /* removePlayerTile */
|
|
|
|
Tile
|
|
model_removePlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index )
|
|
{
|
|
Tile tile;
|
|
PlayerCtxt* player;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
if ( index < 0 ) {
|
|
index = player->trayTiles.nTiles - 1;
|
|
}
|
|
tile = removePlayerTile( model, turn, index );
|
|
notifyTrayListeners( model, turn, index, player->trayTiles.nTiles );
|
|
return tile;
|
|
} /* model_removePlayerTile */
|
|
|
|
void
|
|
model_removePlayerTiles( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
PlayerCtxt* player = &model->players[turn];
|
|
for ( XP_U16 ii = 0; ii < tiles->nTiles; ++ii ) {
|
|
Tile tile = tiles->tiles[ii];
|
|
XP_S16 index = -1;
|
|
for ( XP_U16 jj = 0; index < 0 && jj < player->trayTiles.nTiles; ++jj ) {
|
|
if ( tile == player->trayTiles.tiles[jj] ) {
|
|
index = jj;
|
|
}
|
|
}
|
|
XP_ASSERT( index >= 0 );
|
|
model_removePlayerTile( model, turn, index );
|
|
}
|
|
}
|
|
|
|
void
|
|
model_packTilesUtil( ModelCtxt* model, PoolContext* pool,
|
|
XP_Bool includeBlank,
|
|
XP_U16* nUsed, const XP_UCHAR** texts,
|
|
Tile* tiles )
|
|
{
|
|
const DictionaryCtxt* dict = model_getDictionary(model);
|
|
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 ) {
|
|
if ( includeBlank ) {
|
|
XP_ASSERT( !!pool );
|
|
if ( pool_getNTilesLeftFor( pool, tile ) == 0 ) {
|
|
continue;
|
|
}
|
|
} else if ( tile == blankFace ) {
|
|
continue;
|
|
}
|
|
|
|
tiles[nFacesAvail] = tile;
|
|
texts[nFacesAvail] = dict_getTileString( dict, tile );
|
|
++nFacesAvail;
|
|
}
|
|
|
|
*nUsed = nFacesAvail;
|
|
|
|
} /* model_packTilesUtil */
|
|
|
|
/* setup async query for blank value, but while at it return a reasonable
|
|
default. */
|
|
Tile
|
|
model_askBlankTile( ModelCtxt* model, XWEnv xwe, XP_U16 turn, XP_U16 col, XP_U16 row )
|
|
{
|
|
XP_U16 nUsed = MAX_UNIQUE_TILES;
|
|
const XP_UCHAR* tfaces[MAX_UNIQUE_TILES];
|
|
Tile tiles[MAX_UNIQUE_TILES];
|
|
|
|
model_packTilesUtil( model, NULL, XP_FALSE,
|
|
&nUsed, tfaces, tiles );
|
|
|
|
util_notifyPickTileBlank( model->vol.util, xwe, turn, col, row,
|
|
tfaces, nUsed );
|
|
return tiles[0];
|
|
} /* model_askBlankTile */
|
|
|
|
void
|
|
model_moveTrayToBoard( ModelCtxt* model, XWEnv xwe, 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_getDictionary(model)) ) {
|
|
if ( blankFace != EMPTY_TILE ) {
|
|
tile = blankFace;
|
|
} else {
|
|
XP_ASSERT( turn >= 0 );
|
|
tile = TILE_BLANK_BIT
|
|
| model_askBlankTile( model, xwe, (XP_U16)turn, col, row );
|
|
}
|
|
tile |= TILE_BLANK_BIT;
|
|
}
|
|
|
|
player = &model->players[turn];
|
|
|
|
if ( player->nPending == 0 ) {
|
|
invalLastMove( model, xwe );
|
|
}
|
|
|
|
player->nUndone = 0;
|
|
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, xwe, turn, col, row, XP_TRUE );
|
|
} /* model_moveTrayToBoard */
|
|
|
|
XP_Bool
|
|
model_setBlankValue( ModelCtxt* model, XP_U16 turn,
|
|
XP_U16 col, XP_U16 row, XP_U16 newIndex )
|
|
{
|
|
XP_Bool found = XP_FALSE;
|
|
PlayerCtxt* player = &model->players[turn];
|
|
for ( int ii = 0; ii < player->nPending; ++ii ) {
|
|
PendingTile* pt = &player->pendingTiles[ii];
|
|
found = pt->col == col && pt->row == row;
|
|
if ( found ) {
|
|
XP_ASSERT( (pt->tile & TILE_BLANK_BIT) != 0 );
|
|
if ( (pt->tile & TILE_BLANK_BIT) != 0 ) {
|
|
XP_U16 nUsed = MAX_UNIQUE_TILES;
|
|
const XP_UCHAR* tfaces[MAX_UNIQUE_TILES];
|
|
Tile tiles[MAX_UNIQUE_TILES];
|
|
model_packTilesUtil( model, NULL, XP_FALSE,
|
|
&nUsed, tfaces, tiles );
|
|
|
|
pt->tile = tiles[newIndex] | TILE_BLANK_BIT;
|
|
|
|
/* force a recalc in case phonies==PHONIES_BLOCK */
|
|
invalidateScore( model, turn );
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
XP_Bool
|
|
model_redoPendingTiles( ModelCtxt* model, XWEnv xwe, XP_S16 turn )
|
|
{
|
|
XP_U16 actualCnt = 0;
|
|
PlayerCtxt* player;
|
|
XP_U16 nUndone;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
|
|
player = &model->players[turn];
|
|
nUndone = player->nUndone;
|
|
if ( nUndone > 0 ) {
|
|
PendingTile pendingTiles[nUndone];
|
|
PendingTile* pt = pendingTiles;
|
|
|
|
XP_MEMCPY( pendingTiles, &player->pendingTiles[player->nPending],
|
|
nUndone * sizeof(pendingTiles[0]) );
|
|
|
|
/* Now we have info about each tile, but don't know where in the
|
|
tray they are. So find 'em. */
|
|
for ( pt = pendingTiles; nUndone-- > 0; ++pt ) {
|
|
Tile tile = pt->tile;
|
|
XP_Bool isBlank = 0 != (tile & TILE_BLANK_BIT);
|
|
XP_S16 foundAt;
|
|
|
|
if ( isBlank ) {
|
|
tile = dict_getBlankTile( model_getDictionary(model) );
|
|
}
|
|
foundAt = model_trayContains( model, turn, tile );
|
|
XP_ASSERT( foundAt >= 0 );
|
|
|
|
if ( !model_getTile( model, pt->col, pt->row, XP_FALSE, turn,
|
|
NULL, NULL, NULL, NULL ) ) {
|
|
model_moveTrayToBoard( model, xwe, turn, pt->col, pt->row,
|
|
foundAt, pt->tile & ~TILE_BLANK_BIT );
|
|
++actualCnt;
|
|
}
|
|
}
|
|
}
|
|
return actualCnt > 0;
|
|
}
|
|
|
|
void
|
|
model_moveBoardToTray( ModelCtxt* model, XWEnv xwe, XP_S16 turn,
|
|
XP_U16 col, XP_U16 row, XP_U16 trayOffset )
|
|
{
|
|
XP_S16 index;
|
|
PlayerCtxt* player;
|
|
short ii;
|
|
PendingTile* pt;
|
|
Tile tile;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
for ( pt = player->pendingTiles, index = 0;
|
|
index < player->nPending;
|
|
++index, ++pt ) {
|
|
if ( pt->col == col && pt->row == row ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* if we're called from putBackOtherPlayersTiles there may be nothing
|
|
here */
|
|
if ( index < player->nPending ) {
|
|
PendingTile tmpPending;
|
|
decrPendingTileCountAt( model, col, row );
|
|
notifyBoardListeners( model, xwe, turn, col, row, XP_FALSE );
|
|
|
|
tile = pt->tile;
|
|
if ( (tile & TILE_BLANK_BIT) != 0 ) {
|
|
tile = dict_getBlankTile( model_getDictionary(model) );
|
|
}
|
|
|
|
model_addPlayerTile( model, turn, trayOffset, tile );
|
|
|
|
--player->nPending;
|
|
tmpPending = player->pendingTiles[index];
|
|
for ( ii = index; ii < player->nPending; ++ii ) {
|
|
player->pendingTiles[ii] = player->pendingTiles[ii+1];
|
|
}
|
|
player->pendingTiles[player->nPending] = tmpPending;
|
|
|
|
++player->nUndone;
|
|
//XP_LOGF( "%s: nUndone(%d): %d", __func__, turn, player->nUndone );
|
|
|
|
if ( player->nPending == 0 ) {
|
|
invalLastMove( model, xwe );
|
|
}
|
|
|
|
invalidateScore( model, turn );
|
|
}
|
|
} /* model_moveBoardToTray */
|
|
|
|
XP_Bool
|
|
model_moveTileOnBoard( ModelCtxt* model, XWEnv xwe, XP_S16 turn, XP_U16 colCur,
|
|
XP_U16 rowCur, XP_U16 colNew, XP_U16 rowNew )
|
|
{
|
|
XP_Bool found = XP_FALSE;
|
|
PlayerCtxt* player;
|
|
XP_S16 index;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
|
|
player = &model->players[turn];
|
|
index = player->nPending;
|
|
|
|
while ( index-- ) {
|
|
Tile tile;
|
|
XP_U16 tcol, trow;
|
|
XP_Bool isBlank;
|
|
model_getCurrentMoveTile( model, turn, &index, &tile, &tcol, &trow,
|
|
&isBlank );
|
|
if ( colCur == tcol && rowCur == trow ) {
|
|
PendingTile* pt = &player->pendingTiles[index];
|
|
pt->col = colNew;
|
|
pt->row = rowNew;
|
|
if ( isBlank ) {
|
|
(void)model_askBlankTile( model, xwe, turn, colNew, rowNew );
|
|
}
|
|
|
|
decrPendingTileCountAt( model, colCur, rowCur );
|
|
incrPendingTileCountAt( model, colNew, rowNew );
|
|
|
|
invalidateScore( model, turn );
|
|
found = XP_TRUE;
|
|
break;
|
|
}
|
|
}
|
|
return found;
|
|
} /* model_moveTileOnBoard */
|
|
|
|
void
|
|
model_resetCurrentTurn( ModelCtxt* model, XWEnv xwe, XP_S16 whose )
|
|
{
|
|
PlayerCtxt* player;
|
|
|
|
XP_ASSERT( whose >= 0 && whose < model->nPlayers );
|
|
player = &model->players[whose];
|
|
|
|
while ( player->nPending > 0 ) {
|
|
model_moveBoardToTray( model, xwe, whose,
|
|
player->pendingTiles[0].col,
|
|
player->pendingTiles[0].row,
|
|
-1 );
|
|
}
|
|
} /* model_resetCurrentTurn */
|
|
|
|
XP_S16
|
|
model_getNMoves( const ModelCtxt* model )
|
|
{
|
|
XP_U16 nAssigns = model->vol.gi->inDuplicateMode ? 1 : model->nPlayers;
|
|
XP_U16 result = stack_getNEntries( model->vol.stack ) - nAssigns;
|
|
return result;
|
|
}
|
|
|
|
XP_U16
|
|
model_visTileCount( const ModelCtxt* model, XP_U16 turn, XP_Bool trayVisible )
|
|
{
|
|
XP_U16 count = model->vol.nTilesOnBoard;
|
|
if ( trayVisible ) {
|
|
count += model_getCurrentMoveCount( model, turn );
|
|
}
|
|
return count;
|
|
}
|
|
|
|
XP_Bool
|
|
model_canShuffle( const ModelCtxt* model, XP_U16 turn, XP_Bool trayVisible )
|
|
{
|
|
return trayVisible
|
|
&& model_getPlayerTiles( model, turn )->nTiles > 1;
|
|
}
|
|
|
|
XP_Bool
|
|
model_canTogglePending( const ModelCtxt* model, XP_U16 turn )
|
|
{
|
|
const PlayerCtxt* player = &model->players[turn];
|
|
return 0 < player->nPending || 0 < player->nUndone;
|
|
}
|
|
|
|
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;
|
|
PendingTile* pending;
|
|
XP_U16 nPending;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
pending = player->pendingTiles;
|
|
|
|
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, XWEnv xwe, XP_U16 notMyTurn,
|
|
XP_U16 col, XP_U16 row )
|
|
{
|
|
XP_U16 turn;
|
|
|
|
for ( turn = 0; turn < model->nPlayers; ++turn ) {
|
|
if ( turn == notMyTurn ) {
|
|
continue;
|
|
}
|
|
model_moveBoardToTray( model, xwe, turn, col, row, -1 );
|
|
}
|
|
} /* putBackOtherPlayersTiles */
|
|
|
|
static void
|
|
invalidateScores( ModelCtxt* model )
|
|
{
|
|
for ( int ii = 0; ii < model->nPlayers; ++ii ) {
|
|
invalidateScore( model, ii );
|
|
}
|
|
}
|
|
|
|
/* 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, XWEnv xwe, XP_S16 turn, const TrayTileSet* newTiles,
|
|
XWStreamCtxt* stream, WordNotifierInfo* wni, XP_Bool useStack )
|
|
{
|
|
XP_S16 score = -1;
|
|
|
|
#ifdef DEBUG
|
|
XP_ASSERT( getCurrentMoveScoreIfLegal( model, xwe, turn, (XWStreamCtxt*)NULL,
|
|
(WordNotifierInfo*)NULL, &score ) );
|
|
invalidateScore( model, turn );
|
|
#endif
|
|
|
|
XP_ASSERT( turn >= 0 && turn < MAX_NUM_PLAYERS);
|
|
|
|
clearLastMoveInfo( model, xwe );
|
|
|
|
PlayerCtxt* player = &model->players[turn];
|
|
|
|
if ( useStack ) {
|
|
XP_Bool isHorizontal;
|
|
#ifdef DEBUG
|
|
XP_Bool inLine =
|
|
#endif
|
|
tilesInLine( model, turn, &isHorizontal );
|
|
XP_ASSERT( inLine );
|
|
MoveInfo moveInfo = {0};
|
|
normalizeMoves( model, turn, isHorizontal, &moveInfo );
|
|
|
|
stack_addMove( model->vol.stack, turn, &moveInfo, newTiles );
|
|
}
|
|
|
|
/* Where's it removed from tray? Need to assert there! */
|
|
for ( int ii = 0; ii < player->nPending; ++ii ) {
|
|
const PendingTile* pt = &player->pendingTiles[ii];
|
|
XP_U16 col = pt->col;
|
|
XP_U16 row = pt->row;
|
|
CellTile tile = getModelTileRaw( model, col, row );
|
|
|
|
XP_ASSERT( (tile & TILE_PENDING_BIT) != 0 );
|
|
|
|
XP_U16 val = tile & TILE_VALUE_MASK;
|
|
if ( val > 1 ) { /* somebody else is using this square too! */
|
|
putBackOtherPlayersTiles( model, xwe, turn, col, row );
|
|
}
|
|
|
|
tile = pt->tile;
|
|
tile |= PREV_MOVE_BIT;
|
|
tile |= turn << CELL_OWNER_OFFSET;
|
|
|
|
setModelTileRaw( model, col, row, tile );
|
|
|
|
notifyBoardListeners( model, xwe, turn, col, row, XP_FALSE );
|
|
|
|
++model->vol.nTilesOnBoard;
|
|
}
|
|
|
|
(void)getCurrentMoveScoreIfLegal( model, xwe, turn, stream, wni, &score );
|
|
XP_ASSERT( score >= 0 );
|
|
if ( ! model->vol.gi->inDuplicateMode ) {
|
|
player->score += score;
|
|
}
|
|
|
|
invalidateScores( model );
|
|
|
|
player->nPending = 0;
|
|
player->nUndone = 0;
|
|
|
|
/* Move new tiles into tray */
|
|
for ( int ii = newTiles->nTiles - 1; ii >= 0; --ii ) {
|
|
model_addPlayerTile( model, turn, -1, newTiles->tiles[ii] );
|
|
}
|
|
|
|
return score;
|
|
} /* commitTurn */
|
|
|
|
XP_Bool
|
|
model_commitTurn( ModelCtxt* model, XWEnv xwe, XP_S16 turn, TrayTileSet* newTiles )
|
|
{
|
|
XP_S16 score = commitTurn( model, xwe, turn, newTiles, NULL, NULL, XP_TRUE );
|
|
return 0 <= score;
|
|
} /* model_commitTurn */
|
|
|
|
void
|
|
model_commitDupeTurn( ModelCtxt* model, XWEnv xwe, const MoveInfo* moveInfo,
|
|
XP_U16 nScores, XP_U16* scores, TrayTileSet* newTiles )
|
|
{
|
|
model_resetCurrentTurn( model, xwe, DUP_PLAYER );
|
|
model_makeTurnFromMoveInfo( model, xwe, DUP_PLAYER, moveInfo );
|
|
(void)commitTurn( model, xwe, DUP_PLAYER, newTiles, NULL, NULL, XP_FALSE );
|
|
dupe_adjustScores( model, XP_TRUE, nScores, scores );
|
|
invalidateScores( model );
|
|
|
|
stack_addDupMove( model->vol.stack, moveInfo, nScores, scores, newTiles );
|
|
}
|
|
|
|
void
|
|
model_commitDupeTrade( ModelCtxt* model, const TrayTileSet* oldTiles,
|
|
const TrayTileSet* newTiles )
|
|
{
|
|
stack_addDupTrade( model->vol.stack, oldTiles, newTiles );
|
|
}
|
|
|
|
void
|
|
model_noteDupePause( ModelCtxt* model, XWEnv xwe, DupPauseType typ, XP_S16 turn,
|
|
const XP_UCHAR* msg )
|
|
{
|
|
XP_U32 when = dutil_getCurSeconds( model->vol.dutil, xwe );
|
|
stack_addPause( model->vol.stack, typ, turn, when, msg );
|
|
}
|
|
|
|
/* 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, const TrayTileSet* oldTiles,
|
|
const TrayTileSet* newTiles )
|
|
{
|
|
XP_ASSERT( newTiles->nTiles == oldTiles->nTiles );
|
|
XP_ASSERT( oldTiles != &model->players[player].trayTiles );
|
|
|
|
const XP_U16 nTiles = newTiles->nTiles;
|
|
for ( XP_U16 ii = 0; ii < nTiles; ++ii ) {
|
|
Tile oldTile = oldTiles->tiles[ii];
|
|
|
|
XP_S16 tileIndex = model_trayContains( model, player, oldTile );
|
|
XP_ASSERT( tileIndex >= 0 );
|
|
model_removePlayerTile( model, player, tileIndex );
|
|
model_addPlayerTile( model, player, tileIndex, newTiles->tiles[ii] );
|
|
}
|
|
} /* makeTileTrade */
|
|
|
|
void
|
|
model_makeTileTrade( ModelCtxt* model, XP_S16 player,
|
|
const TrayTileSet* oldTiles, const TrayTileSet* newTiles )
|
|
{
|
|
stack_addTrade( model->vol.stack, player, oldTiles, newTiles );
|
|
|
|
makeTileTrade( model, player, oldTiles, newTiles );
|
|
} /* model_makeTileTrade */
|
|
|
|
Tile
|
|
model_getPlayerTile( const ModelCtxt* model, XP_S16 turn, XP_S16 index )
|
|
{
|
|
const PlayerCtxt* player;
|
|
XP_ASSERT( turn >= 0 );
|
|
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( const ModelCtxt* model, XP_S16 turn )
|
|
{
|
|
const PlayerCtxt* player;
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
|
|
return (const TrayTileSet*)&player->trayTiles;
|
|
} /* model_getPlayerTile */
|
|
|
|
#ifdef DEBUG
|
|
XP_UCHAR*
|
|
formatTileSet( const TrayTileSet* tiles, XP_UCHAR* buf, XP_U16 len )
|
|
{
|
|
XP_U16 ii, used;
|
|
for ( ii = 0, used = 0; ii < tiles->nTiles && used < len; ++ii ) {
|
|
used += XP_SNPRINTF( &buf[used], len - used, "%d,", tiles->tiles[ii] );
|
|
}
|
|
if ( used > len ) {
|
|
buf[len-1] = '\0';
|
|
}
|
|
return buf;
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
addPlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index, const Tile tile )
|
|
{
|
|
PlayerCtxt* player;
|
|
short ii;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
|
|
XP_ASSERT( player->trayTiles.nTiles < MAX_TRAY_TILES );
|
|
XP_ASSERT( index >= 0 );
|
|
|
|
/* move tiles up to make room */
|
|
for ( ii = player->trayTiles.nTiles; ii > index; --ii ) {
|
|
player->trayTiles.tiles[ii] = player->trayTiles.tiles[ii-1];
|
|
}
|
|
++player->trayTiles.nTiles;
|
|
player->trayTiles.tiles[index] = tile;
|
|
} /* addPlayerTile */
|
|
|
|
void
|
|
model_addPlayerTile( ModelCtxt* model, XP_S16 turn, XP_S16 index, Tile tile )
|
|
{
|
|
PlayerCtxt* player;
|
|
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
if ( index < 0 ) {
|
|
index = player->trayTiles.nTiles;
|
|
}
|
|
addPlayerTile( model, turn, index, tile );
|
|
|
|
notifyTrayListeners( model, turn, index, player->trayTiles.nTiles );
|
|
} /* model_addPlayerTile */
|
|
|
|
void
|
|
model_moveTileOnTray( ModelCtxt* model, XP_S16 turn, XP_S16 indexCur,
|
|
XP_S16 indexNew )
|
|
{
|
|
Tile tile = removePlayerTile( model, turn, indexCur );
|
|
addPlayerTile( model, turn, indexNew, tile );
|
|
notifyTrayListeners( model, turn, indexCur, indexNew );
|
|
} /* model_moveTileOnTray */
|
|
|
|
XP_U16
|
|
model_getDividerLoc( const ModelCtxt* model, XP_S16 turn )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
const PlayerCtxt* player = &model->players[turn];
|
|
return player->dividerLoc;
|
|
}
|
|
|
|
void
|
|
model_setDividerLoc( ModelCtxt* model, XP_S16 turn, XP_U16 loc )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
PlayerCtxt* player = &model->players[turn];
|
|
XP_ASSERT( loc < 0xFF );
|
|
player->dividerLoc = (XP_U8)loc;
|
|
}
|
|
|
|
void
|
|
model_addNewTiles( ModelCtxt* model, XP_S16 turn, const TrayTileSet* tiles )
|
|
{
|
|
const Tile* tilep = tiles->tiles;
|
|
XP_U16 nTiles = tiles->nTiles;
|
|
while ( nTiles-- ) {
|
|
model_addPlayerTile( model, turn, -1, *tilep++ );
|
|
}
|
|
} /* assignPlayerTiles */
|
|
|
|
void
|
|
model_assignPlayerTiles( ModelCtxt* model, XP_S16 turn,
|
|
const TrayTileSet* tiles )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
XP_ASSERT( turn == DUP_PLAYER || !model->vol.gi->inDuplicateMode );
|
|
TrayTileSet sorted;
|
|
sortTiles( &sorted, tiles, 0 );
|
|
stack_addAssign( model->vol.stack, turn, &sorted );
|
|
|
|
model_addNewTiles( model, turn, &sorted );
|
|
} /* model_assignPlayerTiles */
|
|
|
|
XP_S16
|
|
model_getNextTurn( const ModelCtxt* model )
|
|
{
|
|
XP_S16 result = stack_getNextTurn( model->vol.stack );
|
|
// LOG_RETURNF( "%d", result );
|
|
return result;
|
|
}
|
|
|
|
void
|
|
model_assignDupeTiles( ModelCtxt* model, XWEnv xwe, const TrayTileSet* tiles )
|
|
{
|
|
model_assignPlayerTiles( model, DUP_PLAYER, tiles );
|
|
model_cloneDupeTrays( model, xwe );
|
|
}
|
|
|
|
void
|
|
model_sortTiles( ModelCtxt* model, XP_S16 turn )
|
|
{
|
|
XP_U16 dividerLoc = model_getDividerLoc( model, turn );
|
|
const TrayTileSet* curTiles = model_getPlayerTiles( model, turn );
|
|
if ( curTiles->nTiles >= dividerLoc) { /* any to sort? */
|
|
TrayTileSet sorted;
|
|
sortTiles( &sorted, curTiles, dividerLoc );
|
|
|
|
for ( XP_S16 nTiles = sorted.nTiles; nTiles > 0; ) {
|
|
removePlayerTile( model, turn, --nTiles );
|
|
}
|
|
|
|
model_addNewTiles( model, turn, &sorted );
|
|
}
|
|
} /* model_sortTiles */
|
|
|
|
XP_U16
|
|
model_getNumTilesInTray( ModelCtxt* model, XP_S16 turn )
|
|
{
|
|
PlayerCtxt* player;
|
|
XP_ASSERT( turn >= 0 );
|
|
player = &model->players[turn];
|
|
XP_U16 result = player->trayTiles.nTiles;
|
|
// XP_LOGFF( "(turn=%d) => %d", turn, result );
|
|
return result;
|
|
} /* model_getNumTilesInTray */
|
|
|
|
XP_U16
|
|
model_getNumTilesTotal( ModelCtxt* model, XP_S16 turn )
|
|
{
|
|
XP_ASSERT( turn >= 0 );
|
|
PlayerCtxt* player = &model->players[turn];
|
|
XP_U16 result = player->trayTiles.nTiles + player->nPending;
|
|
return result;
|
|
} /* model_getNumTilesTotal */
|
|
|
|
XP_U16
|
|
model_numRows( const ModelCtxt* model )
|
|
{
|
|
return model->nRows;
|
|
} /* model_numRows */
|
|
|
|
XP_U16
|
|
model_numCols( const 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_setTrayListener */
|
|
|
|
void
|
|
model_setDictListener( ModelCtxt* model, DictListener dl, void* data )
|
|
{
|
|
model->vol.dictListenerFunc = dl;
|
|
model->vol.dictListenerData = data;
|
|
} /* model_setDictListener */
|
|
|
|
static void
|
|
notifyBoardListeners( ModelCtxt* model, XWEnv xwe, XP_U16 turn, XP_U16 col,
|
|
XP_U16 row, XP_Bool added )
|
|
{
|
|
if ( model->vol.boardListenerFunc != NULL ) {
|
|
(*model->vol.boardListenerFunc)( xwe, model->vol.boardListenerData,
|
|
turn, col, row, added );
|
|
}
|
|
} /* notifyBoardListeners */
|
|
|
|
static void
|
|
notifyTrayListeners( ModelCtxt* model, XP_U16 turn, XP_S16 index1,
|
|
XP_S16 index2 )
|
|
{
|
|
if ( model->vol.trayListenerFunc != NULL ) {
|
|
(*model->vol.trayListenerFunc)( model->vol.trayListenerData, turn,
|
|
index1, index2 );
|
|
}
|
|
} /* notifyTrayListeners */
|
|
|
|
static void
|
|
notifyDictListeners( ModelCtxt* model, XWEnv xwe, XP_S16 playerNum,
|
|
const DictionaryCtxt* oldDict, const DictionaryCtxt* newDict )
|
|
{
|
|
if ( model->vol.dictListenerFunc != NULL ) {
|
|
(*model->vol.dictListenerFunc)( model->vol.dictListenerData, xwe,
|
|
playerNum, oldDict, newDict );
|
|
}
|
|
} /* notifyDictListeners */
|
|
|
|
static void
|
|
printString( XWStreamCtxt* stream, const XP_UCHAR* str )
|
|
{
|
|
stream_catString( stream, str );
|
|
} /* printString */
|
|
|
|
static XP_UCHAR*
|
|
formatTray( const TrayTileSet* tiles, const DictionaryCtxt* dict,
|
|
XP_UCHAR* buf, XP_U16 bufSize, XP_Bool keepHidden )
|
|
{
|
|
if ( keepHidden ) {
|
|
XP_U16 ii;
|
|
for ( ii = 0; ii < tiles->nTiles; ++ii ) {
|
|
buf[ii] = '?';
|
|
}
|
|
buf[ii] = '\0';
|
|
} else {
|
|
dict_tilesToString( dict, (Tile*)tiles->tiles, tiles->nTiles,
|
|
buf, bufSize, NULL );
|
|
}
|
|
|
|
return buf;
|
|
} /* formatTray */
|
|
|
|
typedef struct MovePrintClosure {
|
|
XWStreamCtxt* stream;
|
|
const DictionaryCtxt* dict;
|
|
XP_U16 nPrinted;
|
|
XP_Bool keepHidden;
|
|
XP_U32 lastPauseWhen;
|
|
} MovePrintClosure;
|
|
|
|
static void
|
|
printMovePre( ModelCtxt* model, XWEnv xwe, XP_U16 XP_UNUSED(moveN),
|
|
const StackEntry* entry, void* p_closure )
|
|
{
|
|
if ( entry->moveType != ASSIGN_TYPE ) {
|
|
const XP_UCHAR* format;
|
|
XP_UCHAR buf[64];
|
|
XP_UCHAR traybuf[MAX_TRAY_TILES+1];
|
|
MovePrintClosure* closure = (MovePrintClosure*)p_closure;
|
|
XWStreamCtxt* stream = closure->stream;
|
|
|
|
XP_SNPRINTF( buf, sizeof(buf), (XP_UCHAR*)"%d:%d ", ++closure->nPrinted,
|
|
entry->playerNum+1 );
|
|
printString( stream, (XP_UCHAR*)buf );
|
|
|
|
switch ( entry->moveType ) {
|
|
case TRADE_TYPE:
|
|
case PAUSE_TYPE:
|
|
break;
|
|
default: {
|
|
XP_UCHAR letter[2] = {'\0','\0'};
|
|
XP_Bool isHorizontal = entry->u.move.moveInfo.isHorizontal;
|
|
XP_U16 col, row;
|
|
const 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 = dutil_getUserString( model->vol.dutil, xwe, STR_PASS );
|
|
XP_SNPRINTF( buf, VSIZE(buf), "%s", format );
|
|
} else {
|
|
if ( isHorizontal ) {
|
|
format = dutil_getUserString( model->vol.dutil, xwe, STRS_MOVE_ACROSS );
|
|
} else {
|
|
format = dutil_getUserString( model->vol.dutil, xwe, 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 = dutil_getUserString( model->vol.dutil, xwe, 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 );
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} /* printMovePre */
|
|
|
|
static void
|
|
printMovePost( ModelCtxt* model, XWEnv xwe, XP_U16 XP_UNUSED(moveN),
|
|
const StackEntry* entry, XP_S16 XP_UNUSED(score),
|
|
void* p_closure )
|
|
{
|
|
if ( entry->moveType != ASSIGN_TYPE ) {
|
|
MovePrintClosure* closure = (MovePrintClosure*)p_closure;
|
|
XWStreamCtxt* stream = closure->stream;
|
|
const DictionaryCtxt* dict = closure->dict;
|
|
const XP_UCHAR* format;
|
|
XP_U16 nTiles;
|
|
|
|
XP_UCHAR buf[100];
|
|
XP_UCHAR traybuf1[MAX_TRAY_TILES+1];
|
|
XP_UCHAR traybuf2[MAX_TRAY_TILES+1];
|
|
const MoveInfo* mi;
|
|
XP_S16 totalScore = model_getPlayerScore( model, entry->playerNum );
|
|
XP_Bool addCR = XP_FALSE;
|
|
|
|
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 = dutil_getUserString( model->vol.dutil, xwe, STRSS_TRADED_FOR );
|
|
XP_SNPRINTF( buf, sizeof(buf), format, traybuf1, traybuf2 );
|
|
printString( stream, buf );
|
|
addCR = XP_TRUE;
|
|
break;
|
|
|
|
case PHONY_TYPE:
|
|
format = dutil_getUserString( model->vol.dutil, xwe, STR_PHONY_REJECTED );
|
|
printString( stream, format );
|
|
/* FALLTHRU */
|
|
case MOVE_TYPE:
|
|
/* Duplicate case */
|
|
if ( model->vol.gi->inDuplicateMode ) {
|
|
XP_U16 offset = 0; // XP_SNPRINTF( buf, VSIZE(buf), "%s", format );
|
|
for ( XP_U16 ii = 0; ii < entry->u.move.dup.nScores; ++ii ) {
|
|
offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset, "%d,",
|
|
entry->u.move.dup.scores[ii] );
|
|
}
|
|
buf[offset-1] = '\0'; /* replace last ',' */
|
|
|
|
XP_UCHAR buf2[256];
|
|
format = dutil_getUserString( model->vol.dutil, xwe,
|
|
STRS_DUP_ALLSCORES );
|
|
XP_SNPRINTF( buf2, sizeof(buf2), format, buf );
|
|
XP_STRCAT( buf2, "\n" );
|
|
printString( stream, buf2 );
|
|
}
|
|
|
|
/* This is wrong */
|
|
format = dutil_getUserString( model->vol.dutil, xwe, 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 = dutil_getUserString( model->vol.dutil, xwe, STRS_NEW_TILES );
|
|
XP_SNPRINTF( buf, sizeof(buf), format,
|
|
formatTray( &entry->u.move.newTiles, dict,
|
|
traybuf1, sizeof(traybuf1),
|
|
XP_FALSE ) );
|
|
printString( stream, buf );
|
|
addCR = XP_TRUE;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case PAUSE_TYPE:
|
|
util_formatPauseHistory( model->vol.util, xwe, stream, entry->u.pause.pauseType,
|
|
entry->playerNum, closure->lastPauseWhen,
|
|
entry->u.pause.when, entry->u.pause.msg );
|
|
closure->lastPauseWhen = entry->u.pause.when;
|
|
addCR = XP_TRUE;
|
|
break;
|
|
|
|
default:
|
|
XP_ASSERT( 0 );
|
|
}
|
|
|
|
if ( addCR ) {
|
|
printString( stream, (XP_UCHAR*)XP_CR );
|
|
}
|
|
|
|
printString( stream, (XP_UCHAR*)XP_CR );
|
|
}
|
|
} /* printMovePost */
|
|
|
|
static void
|
|
copyStack( const ModelCtxt* model, StackCtxt* destStack,
|
|
const StackCtxt* srcStack )
|
|
{
|
|
XWStreamCtxt* stream =
|
|
mem_stream_make_raw( MPPARM(model->vol.mpool)
|
|
dutil_getVTManager(model->vol.dutil) );
|
|
|
|
stream_setVersion( stream, stack_getVersion(srcStack) );
|
|
stack_writeToStream( srcStack, stream );
|
|
stack_loadFromStream( destStack, stream );
|
|
XP_ASSERT( stack_getVersion(destStack) == stack_getVersion( srcStack ) );
|
|
|
|
stream_destroy( stream );
|
|
} /* copyStack */
|
|
|
|
static ModelCtxt*
|
|
makeTmpModel( const ModelCtxt* model, XWEnv xwe, XWStreamCtxt* stream,
|
|
MovePrintFuncPre mpf_pre, MovePrintFuncPost mpf_post,
|
|
void* closure )
|
|
{
|
|
ModelCtxt* tmpModel = model_make( MPPARM(model->vol.mpool)
|
|
xwe, model_getDictionary(model), NULL,
|
|
model->vol.util, model_numCols(model) );
|
|
tmpModel->loaner = model;
|
|
model_setNPlayers( tmpModel, model->nPlayers );
|
|
|
|
buildModelFromStack( tmpModel, xwe, model->vol.stack, XP_FALSE, 0, stream,
|
|
(WordNotifierInfo*)NULL, mpf_pre, mpf_post, closure );
|
|
|
|
return tmpModel;
|
|
} /* makeTmpModel */
|
|
|
|
void
|
|
model_writeGameHistory( ModelCtxt* model, XWEnv xwe, XWStreamCtxt* stream,
|
|
ServerCtxt* server, XP_Bool gameOver )
|
|
{
|
|
MovePrintClosure closure = {
|
|
.stream = stream,
|
|
.dict = model_getDictionary( model ),
|
|
.keepHidden = !gameOver && !model->vol.gi->inDuplicateMode,
|
|
.nPrinted = 0
|
|
};
|
|
|
|
ModelCtxt* tmpModel = makeTmpModel( model, xwe, stream, printMovePre,
|
|
printMovePost, &closure );
|
|
model_destroy( tmpModel, xwe );
|
|
|
|
if ( gameOver ) {
|
|
/* if the game's over, it shouldn't matter which model I pass to this
|
|
method */
|
|
server_writeFinalScores( server, xwe, stream );
|
|
}
|
|
} /* model_writeGameHistory */
|
|
|
|
typedef struct _FirstWordData {
|
|
XP_UCHAR word[32];
|
|
} FirstWordData;
|
|
|
|
static void
|
|
getFirstWord( const WNParams* wnp, void* closure )
|
|
{
|
|
FirstWordData* data = (FirstWordData*)closure;
|
|
if ( '\0' == data->word[0] && '\0' != wnp->word[0] ) {
|
|
XP_STRCAT( data->word, wnp->word );
|
|
}
|
|
}
|
|
|
|
static void
|
|
scoreLastMove( ModelCtxt* model, XWEnv xwe, MoveInfo* moveInfo, XP_U16 howMany,
|
|
LastMoveInfo* lmi )
|
|
{
|
|
XP_U16 score;
|
|
WordNotifierInfo notifyInfo;
|
|
FirstWordData data;
|
|
|
|
ModelCtxt* tmpModel = makeTmpModel( model, xwe, NULL, NULL, NULL, NULL );
|
|
XP_U16 turn;
|
|
XP_S16 moveNum = -1;
|
|
|
|
copyStack( model, tmpModel->vol.stack, model->vol.stack );
|
|
|
|
if ( !model_undoLatestMoves( tmpModel, xwe, NULL, howMany, &turn,
|
|
&moveNum ) ) {
|
|
XP_ASSERT( 0 );
|
|
}
|
|
|
|
data.word[0] = '\0';
|
|
notifyInfo.proc = getFirstWord;
|
|
notifyInfo.closure = &data;
|
|
score = figureMoveScore( tmpModel, xwe, turn, moveInfo, (EngineCtxt*)NULL,
|
|
(XWStreamCtxt*)NULL, ¬ifyInfo );
|
|
|
|
model_destroy( tmpModel, xwe );
|
|
|
|
lmi->score = score;
|
|
XP_SNPRINTF( lmi->word, VSIZE(lmi->word), "%s", data.word );
|
|
} /* scoreLastMove */
|
|
|
|
static XP_U16
|
|
model_getRecentPassCount( ModelCtxt* model )
|
|
{
|
|
StackCtxt* stack = model->vol.stack;
|
|
XP_U16 nPasses = 0;
|
|
|
|
XP_ASSERT( !!stack );
|
|
|
|
XP_S16 nEntries = stack_getNEntries( stack );
|
|
for ( XP_S16 which = nEntries - 1; which >= 0; --which ) {
|
|
StackEntry entry;
|
|
if ( !stack_getNthEntry( stack, which, &entry ) ) {
|
|
break;
|
|
}
|
|
switch ( entry.moveType ) {
|
|
case MOVE_TYPE:
|
|
if ( entry.u.move.moveInfo.nTiles == 0 ) {
|
|
++nPasses;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
stack_freeEntry( stack, &entry );
|
|
}
|
|
return nPasses;
|
|
} /* model_getRecentPassCount */
|
|
|
|
XP_Bool
|
|
model_recentPassCountOk( ModelCtxt* model )
|
|
{
|
|
XP_U16 count = model_getRecentPassCount( model );
|
|
XP_U16 okCount = MAX_PASSES;
|
|
if ( !model->vol.gi->inDuplicateMode ) {
|
|
okCount *= model->nPlayers;
|
|
}
|
|
XP_ASSERT( count <= okCount ); /* should never be more than 1 over */
|
|
return count < okCount;
|
|
}
|
|
|
|
static void
|
|
appendWithCR( XWStreamCtxt* stream, const XP_UCHAR* word, XP_U16* counter )
|
|
{
|
|
if ( 0 < (*counter)++ ) {
|
|
stream_putU8( stream, '\n' );
|
|
}
|
|
stream_catString( stream, word );
|
|
}
|
|
|
|
static void
|
|
recordWord( const WNParams* wnp, void* closure )
|
|
{
|
|
RecordWordsInfo* info = (RecordWordsInfo*)closure;
|
|
appendWithCR( info->stream, wnp->word, &info->nWords );
|
|
}
|
|
|
|
WordNotifierInfo*
|
|
model_initWordCounter( ModelCtxt* model, XWStreamCtxt* stream )
|
|
{
|
|
XP_ASSERT( model->vol.wni.proc == recordWord );
|
|
XP_ASSERT( model->vol.wni.closure == &model->vol.rwi );
|
|
model->vol.rwi.stream = stream;
|
|
model->vol.rwi.nWords = 0;
|
|
return &model->vol.wni;
|
|
}
|
|
|
|
#ifdef XWFEATURE_BOARDWORDS
|
|
typedef struct _ListWordsThroughInfo {
|
|
XWStreamCtxt* stream;
|
|
XP_U16 col, row;
|
|
XP_U16 nWords;
|
|
} ListWordsThroughInfo;
|
|
|
|
static void
|
|
listWordsThrough( const WNParams* wnp, void* closure )
|
|
{
|
|
ListWordsThroughInfo* info = (ListWordsThroughInfo*)closure;
|
|
const MoveInfo* movei = wnp->movei;
|
|
|
|
XP_Bool contained = XP_FALSE;
|
|
if ( movei->isHorizontal && movei->commonCoord == info->row ) {
|
|
contained = wnp->start <= info->col && wnp->end >= info->col;
|
|
} else if ( !movei->isHorizontal && movei->commonCoord == info->col ) {
|
|
contained = wnp->start <= info->row && wnp->end >= info->row;
|
|
}
|
|
|
|
if ( contained ) {
|
|
appendWithCR( info->stream, wnp->word, &info->nWords );
|
|
}
|
|
}
|
|
|
|
/* List every word played that includes the tile on {col,row}.
|
|
*
|
|
* How? Undo backwards until we find the move that placed that tile.*/
|
|
XP_Bool
|
|
model_listWordsThrough( ModelCtxt* model, XWEnv xwe, XP_U16 col, XP_U16 row,
|
|
XP_S16 turn, XWStreamCtxt* stream )
|
|
{
|
|
XP_Bool found = XP_FALSE;
|
|
ModelCtxt* tmpModel = makeTmpModel( model, xwe, NULL, NULL, NULL, NULL );
|
|
copyStack( model, tmpModel->vol.stack, model->vol.stack );
|
|
|
|
XP_Bool isHorizontal;
|
|
if ( tilesInLine( model, turn, &isHorizontal ) ) {
|
|
MoveInfo moveInfo = {0};
|
|
normalizeMoves( model, turn, isHorizontal, &moveInfo );
|
|
model_makeTurnFromMoveInfo( tmpModel, xwe, turn, &moveInfo );
|
|
|
|
/* Might not be a legal move. If isn't, don't add it! */
|
|
if ( getCurrentMoveScoreIfLegal( tmpModel, xwe, turn, (XWStreamCtxt*)NULL,
|
|
(WordNotifierInfo*)NULL, NULL ) ) {
|
|
TrayTileSet newTiles = {.nTiles = 0};
|
|
commitTurn( tmpModel, xwe, turn, &newTiles, NULL, NULL, XP_TRUE );
|
|
} else {
|
|
model_resetCurrentTurn( tmpModel, xwe, turn );
|
|
}
|
|
}
|
|
|
|
XP_ASSERT( !!stream );
|
|
StackCtxt* stack = tmpModel->vol.stack;
|
|
XP_U16 nEntriesBefore = stack_getNEntries( stack );
|
|
XP_U16 nEntriesAfter;
|
|
|
|
/* Loop until we undo the move that placed the tile. */
|
|
while ( model_undoLatestMoves( tmpModel, xwe, NULL, 1, NULL, NULL ) ) {
|
|
if ( 0 != (TILE_EMPTY_BIT & getModelTileRaw( tmpModel, col, row ) ) ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
nEntriesAfter = stack_getNEntries( stack );
|
|
XP_ASSERT( nEntriesAfter < nEntriesBefore );
|
|
if ( nEntriesAfter < nEntriesBefore ) {
|
|
ListWordsThroughInfo lwtInfo = { .stream = stream, .col = col,
|
|
.row = row, .nWords = 0,
|
|
};
|
|
WordNotifierInfo ni = { .proc = listWordsThrough, .closure = &lwtInfo };
|
|
/* Now push the undone moves back into the model one at a time.
|
|
recordWord() will add each played word to the stream as it's
|
|
scored */
|
|
while ( nEntriesAfter < nEntriesBefore ) {
|
|
StackEntry entry;
|
|
if ( ! stack_redo( stack, &entry ) ) {
|
|
XP_ASSERT( 0 );
|
|
break;
|
|
}
|
|
modelAddEntry( tmpModel, xwe, nEntriesAfter++, &entry, XP_FALSE, NULL, &ni,
|
|
NULL, NULL, NULL );
|
|
}
|
|
XP_LOGFF( "nWords: %d", lwtInfo.nWords );
|
|
found = 0 < lwtInfo.nWords;
|
|
}
|
|
stream_putU8( stream, '\0' ); /* null-terminate for good luck */
|
|
|
|
model_destroy( tmpModel, xwe );
|
|
return found;
|
|
} /* model_listWordsThrough */
|
|
#endif
|
|
|
|
/* Set array of 1-4 (>1 in case of tie) with highest scores' owners */
|
|
static void
|
|
listHighestScores( const ModelCtxt* model, LastMoveInfo* lmi, MoveRec* move )
|
|
{
|
|
/* find highest */
|
|
XP_U16 max = 0;
|
|
lmi->nWinners = 0;
|
|
for ( XP_U16 ii = 0; ii < move->dup.nScores; ++ii ) {
|
|
XP_U16 score = move->dup.scores[ii];
|
|
if ( 0 == score || score < max ) {
|
|
continue;
|
|
} else if ( score > max ) {
|
|
max = score;
|
|
lmi->nWinners = 0;
|
|
lmi->score = score;
|
|
}
|
|
lmi->names[lmi->nWinners++] = model->vol.gi->players[ii].name;
|
|
}
|
|
}
|
|
|
|
XP_Bool
|
|
model_getPlayersLastScore( ModelCtxt* model, XWEnv xwe,
|
|
XP_S16 player, LastMoveInfo* lmi )
|
|
{
|
|
StackCtxt* stack = model->vol.stack;
|
|
XP_S16 nEntries, which;
|
|
StackEntry entry;
|
|
XP_Bool found = XP_FALSE;
|
|
XP_Bool inDuplicateMode = model->vol.gi->inDuplicateMode;
|
|
XP_MEMSET( lmi, 0, sizeof(*lmi) );
|
|
|
|
XP_ASSERT( !!stack );
|
|
|
|
nEntries = stack_getNEntries( stack );
|
|
|
|
for ( which = nEntries; which >= 0; ) {
|
|
if ( stack_getNthEntry( stack, --which, &entry ) ) {
|
|
if ( -1 == player || inDuplicateMode || entry.playerNum == player ) {
|
|
found = XP_TRUE;
|
|
break;
|
|
}
|
|
}
|
|
stack_freeEntry( stack, &entry );
|
|
}
|
|
|
|
if ( found ) { /* success? */
|
|
XP_ASSERT( -1 == player || inDuplicateMode || player == entry.playerNum );
|
|
|
|
|
|
XP_LOGFF( "found move %d", which );
|
|
lmi->names[0] = model->vol.gi->players[entry.playerNum].name;
|
|
lmi->nWinners = 1;
|
|
lmi->moveType = entry.moveType;
|
|
lmi->inDuplicateMode = inDuplicateMode;
|
|
|
|
switch ( entry.moveType ) {
|
|
case MOVE_TYPE:
|
|
XP_ASSERT( !inDuplicateMode || entry.playerNum == DUP_PLAYER );
|
|
lmi->nTiles = entry.u.move.moveInfo.nTiles;
|
|
if ( 0 < entry.u.move.moveInfo.nTiles ) {
|
|
scoreLastMove( model, xwe, &entry.u.move.moveInfo,
|
|
nEntries - which, lmi );
|
|
if ( inDuplicateMode ) {
|
|
listHighestScores( model, lmi, &entry.u.move );
|
|
}
|
|
}
|
|
break;
|
|
case TRADE_TYPE:
|
|
XP_ASSERT( !inDuplicateMode || entry.playerNum == DUP_PLAYER );
|
|
lmi->nTiles = entry.u.trade.oldTiles.nTiles;
|
|
break;
|
|
case PHONY_TYPE:
|
|
case ASSIGN_TYPE:
|
|
case PAUSE_TYPE:
|
|
break;
|
|
default:
|
|
XP_ASSERT( 0 );
|
|
}
|
|
}
|
|
|
|
return found;
|
|
} /* model_getPlayersLastScore */
|
|
|
|
static void
|
|
loadPlayerCtxt( const ModelCtxt* model, XWStreamCtxt* stream, XP_U16 version,
|
|
PlayerCtxt* pc )
|
|
{
|
|
XP_U16 nColsNBits;
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
|
|
: NUMCOLS_NBITS_4;
|
|
#else
|
|
XP_USE(model);
|
|
nColsNBits = NUMCOLS_NBITS_4;
|
|
#endif
|
|
|
|
pc->curMoveValid = stream_getBits( stream, 1 );
|
|
|
|
traySetFromStream( stream, &pc->trayTiles );
|
|
|
|
const XP_U16 nTileBits = tilesNBits(stream);
|
|
pc->nPending = (XP_U8)stream_getBits( stream, nTileBits );
|
|
if ( STREAM_VERS_NUNDONE <= version ) {
|
|
pc->nUndone = (XP_U8)stream_getBits( stream, nTileBits );
|
|
} else {
|
|
XP_ASSERT( 0 == pc->nUndone );
|
|
}
|
|
XP_ASSERT( 0 == pc->dividerLoc );
|
|
if ( STREAM_VERS_MODELDIVIDER <= version ) {
|
|
pc->dividerLoc = stream_getBits( stream, nTileBits );
|
|
}
|
|
|
|
XP_U16 nTiles = pc->nPending + pc->nUndone;
|
|
for ( PendingTile* pt = pc->pendingTiles; nTiles-- > 0; ++pt ) {
|
|
pt->col = (XP_U8)stream_getBits( stream, nColsNBits );
|
|
pt->row = (XP_U8)stream_getBits( stream, nColsNBits );
|
|
|
|
XP_U16 nBits = (version <= STREAM_VERS_RELAY) ? 6 : 7;
|
|
pt->tile = (Tile)stream_getBits( stream, nBits );
|
|
}
|
|
|
|
} /* loadPlayerCtxt */
|
|
|
|
static void
|
|
writePlayerCtxt( const ModelCtxt* model, XWStreamCtxt* stream,
|
|
const PlayerCtxt* pc )
|
|
{
|
|
XP_U16 nTiles;
|
|
const PendingTile* pt;
|
|
XP_U16 nColsNBits;
|
|
#ifdef STREAM_VERS_BIGBOARD
|
|
nColsNBits = 16 <= model_numCols( model ) ? NUMCOLS_NBITS_5
|
|
: NUMCOLS_NBITS_4;
|
|
#else
|
|
XP_USE(model);
|
|
nColsNBits = NUMCOLS_NBITS_4;
|
|
#endif
|
|
|
|
stream_putBits( stream, 1, pc->curMoveValid );
|
|
|
|
traySetToStream( stream, &pc->trayTiles );
|
|
|
|
XP_U16 nBits = tilesNBits( stream );
|
|
stream_putBits( stream, nBits, pc->nPending );
|
|
stream_putBits( stream, nBits, pc->nUndone );
|
|
stream_putBits( stream, nBits, pc->dividerLoc );
|
|
|
|
nTiles = pc->nPending + pc->nUndone;
|
|
for ( pt = pc->pendingTiles; nTiles-- > 0; ++pt ) {
|
|
stream_putBits( stream, nColsNBits, pt->col );
|
|
stream_putBits( stream, nColsNBits, pt->row );
|
|
stream_putBits( stream, 7, pt->tile );
|
|
}
|
|
} /* writePlayerCtxt */
|
|
|
|
static XP_S16
|
|
setContains( const TrayTileSet* tiles, Tile tile )
|
|
{
|
|
XP_S16 result = -1;
|
|
XP_S16 ii;
|
|
/* search from top down so don't pull out of below divider */
|
|
for ( ii = tiles->nTiles - 1; ii >= 0 ; --ii ) {
|
|
Tile playerTile = tiles->tiles[ii];
|
|
if ( playerTile == tile ) {
|
|
result = ii;
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
static void
|
|
assertDiffTurn( ModelCtxt* model, XWEnv XP_UNUSED(xwe),
|
|
XP_U16 XP_UNUSED(turn), const StackEntry* entry,
|
|
void* closure )
|
|
{
|
|
if ( 1 < model->nPlayers && ! model->vol.gi->inDuplicateMode ) {
|
|
DiffTurnState* state = (DiffTurnState*)closure;
|
|
if ( -1 != state->lastPlayerNum ) {
|
|
XP_ASSERT( state->lastPlayerNum != entry->playerNum );
|
|
XP_ASSERT( state->lastMoveNum + 1 == entry->moveNum );
|
|
}
|
|
state->lastPlayerNum = entry->playerNum;
|
|
state->lastMoveNum = entry->moveNum;
|
|
}
|
|
}
|
|
|
|
void
|
|
model_printTrays( const ModelCtxt* model )
|
|
{
|
|
for ( XP_U16 ii = 0; ii < model->nPlayers; ++ii ) {
|
|
const PlayerCtxt* player = &model->players[ii];
|
|
XP_UCHAR buf[128];
|
|
XP_LOGFF( "player %d: %s", ii,
|
|
formatTileSet( &player->trayTiles, buf, VSIZE(buf) ) );
|
|
}
|
|
}
|
|
|
|
void
|
|
model_dumpSelf( const ModelCtxt* model, const XP_UCHAR* msg )
|
|
{
|
|
XP_LOGFF( "(msg=%s)", msg );
|
|
|
|
XP_UCHAR buf[256];
|
|
XP_U16 offset = 0;
|
|
|
|
for ( XP_U16 col = 0; col < model_numCols( model ); ++col ) {
|
|
offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset,
|
|
"%.2d ", col );
|
|
}
|
|
XP_LOGF( " %s", buf );
|
|
|
|
for ( XP_U16 row = 0; row < model_numRows( model ); ++row ) {
|
|
XP_UCHAR buf[256];
|
|
XP_U16 offset = 0;
|
|
|
|
for ( XP_U16 col = 0; col < model_numCols( model ); ++col ) {
|
|
Tile tile = getModelTileRaw( model, col, row );
|
|
offset += XP_SNPRINTF( &buf[offset], VSIZE(buf) - offset,
|
|
"%.2x ", tile );
|
|
}
|
|
XP_LOGF( "%.2d: %s", row, buf );
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef CPLUS
|
|
}
|
|
#endif
|