toward better recovery from out-of-sync move and undo: include a hash

of the move stack with every turn (version permitting), and drop
incoming moves created against a move stack different from ours.
This commit is contained in:
Eric House 2012-05-01 07:58:11 -07:00
parent 0e07fab731
commit c52db8a8f0
3 changed files with 141 additions and 112 deletions

View file

@ -936,6 +936,12 @@ model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
nColsNBits = NUMCOLS_NBITS_4;
#endif
#ifdef STREAM_VERS_BIGBOARD
if ( STREAM_VERS_BIGBOARD <= stream_getVersion( stream ) ) {
stream_putU32( stream, stack_getHash(model->vol.stack) );
}
#endif
XP_ASSERT( turn >= 0 );
player = &model->players[turn];
numTiles = player->nPending;
@ -963,10 +969,11 @@ model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
* assert that it's in the tray, remove it from the tray, and place it on the
* board.
*/
void
XP_Bool
model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream )
{
XP_Bool success = XP_TRUE;
XP_U16 numTiles, ii;
Tile blank = dict_getBlankTile( model_getDictionary(model) );
XP_U16 nColsNBits;
@ -978,40 +985,50 @@ model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
#endif
model_resetCurrentTurn( model, playerNum );
numTiles = (XP_U16)stream_getBits( stream, NTILES_NBITS );
XP_LOGF( "%s: numTiles=%d", __func__, numTiles );
for ( ii = 0; ii < numTiles; ++ii ) {
XP_S16 foundAt;
Tile moveTile;
Tile tileFace = (Tile)stream_getBits( stream, TILE_NBITS );
XP_U16 col = (XP_U16)stream_getBits( stream, nColsNBits );
XP_U16 row = (XP_U16)stream_getBits( stream, nColsNBits );
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 );
#ifdef STREAM_VERS_BIGBOARD
if ( STREAM_VERS_BIGBOARD <= stream_getVersion( stream ) ) {
XP_U32 hash = stream_getU32( stream );
success = hash == stack_getHash( model->vol.stack );
}
#endif
if ( success ) {
numTiles = (XP_U16)stream_getBits( stream, NTILES_NBITS );
XP_LOGF( "%s: numTiles=%d", __func__, numTiles );
for ( ii = 0; ii < numTiles; ++ii ) {
XP_S16 foundAt;
Tile moveTile;
Tile tileFace = (Tile)stream_getBits( stream, TILE_NBITS );
XP_U16 col = (XP_U16)stream_getBits( stream, nColsNBits );
XP_U16 row = (XP_U16)stream_getBits( stream, nColsNBits );
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 );
}
} else {
XP_LOGF( "%s: dropping move on hash mismatch", __func__ );
}
return success;
} /* model_makeTurnFromStream */
void

View file

@ -198,8 +198,8 @@ void model_trayToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream );
void model_currentMoveToStream( ModelCtxt* model, XP_S16 turn,
XWStreamCtxt* stream);
void model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream );
XP_Bool model_makeTurnFromStream( ModelCtxt* model, XP_U16 playerNum,
XWStreamCtxt* stream );
void model_makeTurnFromMoveInfo( ModelCtxt* model, XP_U16 playerNum,
const MoveInfo* newMove );

View file

@ -1914,34 +1914,42 @@ sendMoveTo( ServerCtxt* server, XP_U16 devIndex, XP_U16 turn,
stream_destroy( stream );
} /* sendMoveTo */
static void
static XP_Bool
readMoveInfo( ServerCtxt* server, XWStreamCtxt* stream,
XP_U16* whoMovedP, XP_Bool* isTradeP,
TrayTileSet* newTiles, TrayTileSet* tradedTiles,
XP_Bool* legalP )
{
XP_Bool success;
XP_U16 whoMoved = stream_getBits( stream, PLAYERNUM_NBITS );
XP_Bool legalMove = XP_TRUE;
XP_Bool isTrade;
traySetFromStream( stream, newTiles );
isTrade = stream_getBits( stream, 1 );
success = pool_containsTiles( server->pool, newTiles );
if ( success ) {
isTrade = stream_getBits( stream, 1 );
if ( isTrade ) {
traySetFromStream( stream, tradedTiles );
XP_LOGF( "%s: got trade of %d tiles", __func__, tradedTiles->nTiles );
} else {
legalMove = stream_getBits( stream, 1 );
model_makeTurnFromStream( server->vol.model, whoMoved, stream );
if ( isTrade ) {
traySetFromStream( stream, tradedTiles );
XP_LOGF( "%s: got trade of %d tiles", __func__,
tradedTiles->nTiles );
} else {
legalMove = stream_getBits( stream, 1 );
success = model_makeTurnFromStream( server->vol.model,
whoMoved, stream );
getPlayerTime( server, stream, whoMoved );
}
getPlayerTime( server, stream, whoMoved );
if ( success ) {
pool_removeTiles( server->pool, newTiles );
*whoMovedP = whoMoved;
*isTradeP = isTrade;
*legalP = legalMove;
}
}
pool_removeTiles( server->pool, newTiles );
*whoMovedP = whoMoved;
*isTradeP = isTrade;
*legalP = legalMove;
return success;
} /* readMoveInfo */
static void
@ -1997,6 +2005,7 @@ makeMoveReportIf( ServerCtxt* server, XWStreamCtxt** wordsStream )
static XP_Bool
reflectMoveAndInform( ServerCtxt* server, XWStreamCtxt* stream )
{
XP_Bool success = XP_TRUE;
ModelCtxt* model = server->vol.model;
XP_U16 whoMoved;
XP_U16 nTilesMoved = 0; /* trade case */
@ -2013,76 +2022,78 @@ reflectMoveAndInform( ServerCtxt* server, XWStreamCtxt* stream )
XP_ASSERT( gi->serverRole == SERVER_ISSERVER );
readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
&tradedTiles, &isLegalMove ); /* modifies model */
XP_ASSERT( isLegalMove ); /* client should always report as true */
success = readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
&tradedTiles, &isLegalMove ); /* modifies model */
XP_ASSERT( !success || isLegalMove ); /* client should always report as true */
isLegalMove = XP_TRUE;
if ( isTrade ) {
if ( success ) {
if ( isTrade ) {
sendMoveToClientsExcept( server, whoMoved, XP_TRUE, &newTiles,
&tradedTiles, sourceClientIndex );
sendMoveToClientsExcept( server, whoMoved, XP_TRUE, &newTiles,
&tradedTiles, sourceClientIndex );
model_makeTileTrade( model, whoMoved,
&tradedTiles, &newTiles );
pool_replaceTiles( server->pool, &tradedTiles );
model_makeTileTrade( model, whoMoved,
&tradedTiles, &newTiles );
pool_replaceTiles( server->pool, &tradedTiles );
server->vol.showPrevMove = XP_TRUE;
mvStream = makeTradeReportIf( server, &tradedTiles );
server->vol.showPrevMove = XP_TRUE;
mvStream = makeTradeReportIf( server, &tradedTiles );
} else {
nTilesMoved = model_getCurrentMoveCount( model, whoMoved );
isLegalMove = (nTilesMoved == 0)
|| checkMoveAllowed( server, whoMoved );
/* I don't think this will work if there are more than two devices in
a palm game; need to change state and get out of here before
returning to send additional messages. PENDING(ehouse) */
sendMoveToClientsExcept( server, whoMoved, isLegalMove, &newTiles,
(TrayTileSet*)NULL, sourceClientIndex );
server->vol.showPrevMove = XP_TRUE;
mvStream = makeMoveReportIf( server, &wordsStream );
model_commitTurn( model, whoMoved, &newTiles );
resetEngines( server );
}
if ( isLegalMove ) {
XP_U16 nTilesLeft = model_getNumTilesTotal( model, whoMoved );
if ( (gi->phoniesAction == PHONIES_DISALLOW) && (nTilesMoved > 0) ) {
server->lastMoveSource = sourceClientIndex;
SETSTATE( server, XWSTATE_MOVE_CONFIRM_MUSTSEND );
doRequest = XP_TRUE;
} else if ( nTilesLeft > 0 ) {
nextTurn( server, PICK_NEXT );
} else {
SETSTATE(server, XWSTATE_NEEDSEND_ENDGAME );
nTilesMoved = model_getCurrentMoveCount( model, whoMoved );
isLegalMove = (nTilesMoved == 0)
|| checkMoveAllowed( server, whoMoved );
/* I don't think this will work if there are more than two devices in
a palm game; need to change state and get out of here before
returning to send additional messages. PENDING(ehouse) */
sendMoveToClientsExcept( server, whoMoved, isLegalMove, &newTiles,
(TrayTileSet*)NULL, sourceClientIndex );
server->vol.showPrevMove = XP_TRUE;
mvStream = makeMoveReportIf( server, &wordsStream );
model_commitTurn( model, whoMoved, &newTiles );
resetEngines( server );
}
if ( isLegalMove ) {
XP_U16 nTilesLeft = model_getNumTilesTotal( model, whoMoved );
if ( (gi->phoniesAction == PHONIES_DISALLOW) && (nTilesMoved > 0) ) {
server->lastMoveSource = sourceClientIndex;
SETSTATE( server, XWSTATE_MOVE_CONFIRM_MUSTSEND );
doRequest = XP_TRUE;
} else if ( nTilesLeft > 0 ) {
nextTurn( server, PICK_NEXT );
} else {
SETSTATE(server, XWSTATE_NEEDSEND_ENDGAME );
doRequest = XP_TRUE;
}
if ( !!mvStream ) {
XP_ASSERT( !server->nv.prevMoveStream );
server->nv.prevMoveStream = mvStream;
XP_ASSERT( !server->nv.prevWordsStream );
server->nv.prevWordsStream = wordsStream;
}
} else {
/* The client from which the move came still needs to be told. But we
can't send a message now since we're burried in a message handler.
(Palm, at least, won't manage.) So set up state to tell that
client again in a minute. */
SETSTATE( server, XWSTATE_NEEDSEND_BADWORD_INFO );
server->lastMoveSource = sourceClientIndex;
doRequest = XP_TRUE;
}
if ( !!mvStream ) {
XP_ASSERT( !server->nv.prevMoveStream );
server->nv.prevMoveStream = mvStream;
XP_ASSERT( !server->nv.prevWordsStream );
server->nv.prevWordsStream = wordsStream;
if ( doRequest ) {
util_requestTime( server->vol.util );
}
} else {
/* The client from which the move came still needs to be told. But we
can't send a message now since we're burried in a message handler.
(Palm, at least, won't manage.) So set up state to tell that
client again in a minute. */
SETSTATE( server, XWSTATE_NEEDSEND_BADWORD_INFO );
server->lastMoveSource = sourceClientIndex;
doRequest = XP_TRUE;
}
if ( doRequest ) {
util_requestTime( server->vol.util );
}
return XP_TRUE;
LOG_RETURNF( "%d", success );
return success;
} /* reflectMoveAndInform */
static XP_Bool
@ -2101,9 +2112,10 @@ reflectMove( ServerCtxt* server, XWStreamCtxt* stream )
moveOk = XWSTATE_INTURN == server->nv.gameState;
XP_ASSERT( moveOk ); /* message permanently lost if dropped here! */
if ( moveOk ) {
readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
&tradedTiles, &isLegal ); /* modifies model */
moveOk = readMoveInfo( server, stream, &whoMoved, &isTrade, &newTiles,
&tradedTiles, &isLegal ); /* modifies model */
}
if ( moveOk ) {
if ( isTrade ) {
model_makeTileTrade( model, whoMoved, &tradedTiles, &newTiles );
pool_replaceTiles( server->pool, &tradedTiles );