add test for rematch (once for now)

This commit is contained in:
Eric House 2023-11-29 17:11:51 -08:00
parent be9159fc35
commit d021bb4029
4 changed files with 199 additions and 40 deletions

View file

@ -135,6 +135,7 @@ static void relay_requestJoin_curses( void* closure, const XP_UCHAR* devID,
XP_U16 nPlayersTotal, XP_U16 seed, XP_U16 lang );
#endif
static XP_Bool rematch_and_save( CursesBoardGlobals* bGlobals, XP_U32* newGameID );
static void disposeBoard( CursesBoardGlobals* bGlobals );
static void initCP( CommonGlobals* cGlobals );
static CursesBoardGlobals* commonInit( CursesBoardState* cbState,
@ -595,6 +596,20 @@ enableDraw( CursesBoardGlobals* bGlobals, const cb_dims* dims )
setupBoard( bGlobals );
}
static CursesBoardGlobals*
findOrOpenForGameID( CursesBoardState* cbState, XP_U32 gameID,
const CurGameInfo* gi, const CommsAddrRec* returnAddr )
{
CursesBoardGlobals* result = NULL;
sqlite3_int64 rowids[1];
int nRowIDs = VSIZE(rowids);
gdb_getRowsForGameID( cbState->params->pDb, gameID, rowids, &nRowIDs );
if ( 1 == nRowIDs ) {
result = findOrOpen( cbState, rowids[0], gi, returnAddr );
}
return result;
}
static CursesBoardGlobals*
findOrOpen( CursesBoardState* cbState, sqlite3_int64 rowid,
const CurGameInfo* gi, const CommsAddrRec* returnAddr )
@ -657,12 +672,7 @@ void
cb_addInvite( CursesBoardState* cbState, XP_U32 gameID, XP_U16 forceChannel,
const CommsAddrRec* destAddr )
{
sqlite3_int64 rowids[1];
int nRowIDs = VSIZE(rowids);
gdb_getRowsForGameID( cbState->params->pDb, gameID, rowids, &nRowIDs );
XP_ASSERT( 1 == nRowIDs );
CursesBoardGlobals* bGlobals = findOrOpen( cbState, rowids[0], NULL, NULL );
CursesBoardGlobals* bGlobals = findOrOpenForGameID( cbState, gameID, NULL, NULL );
CommonGlobals* cGlobals = &bGlobals->cGlobals;
CommsCtxt* comms = cGlobals->game.comms;
@ -676,6 +686,40 @@ cb_addInvite( CursesBoardState* cbState, XP_U32 gameID, XP_U16 forceChannel,
comms_invite( comms, NULL_XWE, &nli, destAddr, XP_TRUE );
}
XP_Bool
cb_makeRematch( CursesBoardState* cbState, XP_U32 gameID, XP_U32* newGameID )
{
CursesBoardGlobals* bGlobals = findOrOpenForGameID( cbState, gameID, NULL, NULL );
XP_Bool success = rematch_and_save( bGlobals, newGameID );
return success;
}
XP_Bool
cb_makeMoveIf( CursesBoardState* cbState, XP_U32 gameID )
{
CursesBoardGlobals* bGlobals =
findOrOpenForGameID( cbState, gameID, NULL, NULL );
XP_Bool success = !!bGlobals;
if ( success ) {
CommonGlobals* cGlobals = &bGlobals->cGlobals;
BoardCtxt* board = cGlobals->game.board;
success = board_canHint( board );
if ( success ) {
XP_Bool ignored;
success = board_requestHint( cGlobals->game.board, NULL_XWE,
#ifdef XWFEATURE_SEARCHLIMIT
XP_FALSE,
#endif
XP_FALSE, &ignored );
if ( success ) {
success = board_commitTurn( board, NULL_XWE, XP_TRUE, XP_TRUE, NULL );
}
}
}
LOG_RETURNF( "%s", boolToStr(success) );
return success;
}
static void
kill_board( gpointer data )
{
@ -938,7 +982,6 @@ rematch_and_save_once( CursesBoardGlobals* bGlobals )
{
LOG_FUNC();
CommonGlobals* cGlobals = &bGlobals->cGlobals;
CursesBoardState* cbState = bGlobals->cbState;
int32_t alreadyDone;
gchar key[128];
@ -947,21 +990,37 @@ rematch_and_save_once( CursesBoardGlobals* bGlobals )
&& 0 != alreadyDone ) {
XP_LOGFF( "already rematched game %X", cGlobals->gi->gameID );
} else {
CursesBoardGlobals* bGlobalsNew = commonInit( cbState, -1, NULL );
XP_Bool success = game_makeRematch( &bGlobals->cGlobals.game, NULL_XWE,
bGlobalsNew->cGlobals.util,
&cGlobals->cp, &bGlobalsNew->cGlobals.procs,
&bGlobalsNew->cGlobals.game, "newName" );
if ( success ) {
linuxSaveGame( &bGlobalsNew->cGlobals );
if ( rematch_and_save( bGlobals, NULL ) ) {
gdb_storeInt( cGlobals->params->pDb, key, 1 );
}
disposeBoard( bGlobalsNew );
}
LOG_RETURN_VOID();
}
static XP_Bool
rematch_and_save( CursesBoardGlobals* bGlobals, XP_U32* newGameID )
{
LOG_FUNC();
CommonGlobals* cGlobals = &bGlobals->cGlobals;
CursesBoardState* cbState = bGlobals->cbState;
CursesBoardGlobals* bGlobalsNew = commonInit( cbState, -1, NULL );
XP_Bool success = game_makeRematch( &bGlobals->cGlobals.game, NULL_XWE,
bGlobalsNew->cGlobals.util,
&cGlobals->cp, &bGlobalsNew->cGlobals.procs,
&bGlobalsNew->cGlobals.game, "newName" );
if ( success ) {
if ( !!newGameID ) {
*newGameID = bGlobalsNew->cGlobals.gi->gameID;
}
linuxSaveGame( &bGlobalsNew->cGlobals );
}
disposeBoard( bGlobalsNew );
LOG_RETURNF( "%s", boolToStr(success) );
return success;
}
static void
curses_util_notifyGameOver( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), XP_S16 quitter )
{

View file

@ -52,6 +52,9 @@ void cb_feedGame( CursesBoardState* cbState, XP_U32 gameID,
const XP_U8* buf, XP_U16 len, const CommsAddrRec* from );
void cb_addInvite( CursesBoardState* cbState, XP_U32 gameID, XP_U16 forceChannel,
const CommsAddrRec* destAddr );
XP_Bool cb_makeRematch( CursesBoardState* cbState, XP_U32 gameID, XP_U32* newGameID );
XP_Bool cb_makeMoveIf( CursesBoardState* cbState, XP_U32 gameID );
void cb_closeAll( CursesBoardState* cbState );
#endif

View file

@ -1447,6 +1447,30 @@ onGameSaved( CursesAppGlobals* aGlobals, sqlite3_int64 rowid, bool isNew )
cgl_refreshOne( aGlobals->gameList, rowid, isNew );
}
static XP_U32
castGid( cJSON* obj )
{
XP_U32 gameID;
sscanf( obj->valuestring, "%X", &gameID );
return gameID;
}
static XP_U32
gidFromObject( cJSON* obj )
{
cJSON* tmp = cJSON_GetObjectItem( obj, "gid" );
XP_ASSERT( !!tmp );
return castGid( tmp );
}
static void
addGIDToObject( cJSON* obj, XP_U32 gid, const char* key )
{
char buf[16];
sprintf( buf, "%08X", gid );
cJSON_AddStringToObject( obj, key, buf );
}
static XP_Bool
makeGameFromArgs( CursesAppGlobals* aGlobals, cJSON* args )
{
@ -1457,11 +1481,9 @@ makeGameFromArgs( CursesAppGlobals* aGlobals, cJSON* args )
gi.boardSize = 15;
gi.traySize = 7;
cJSON* tmp = cJSON_GetObjectItem( args, "gid" );
XP_ASSERT( !!tmp );
sscanf( tmp->valuestring, "%X", &gi.gameID );
gi.gameID = gidFromObject( args );
tmp = cJSON_GetObjectItem( args, "nPlayers" );
cJSON* tmp = cJSON_GetObjectItem( args, "nPlayers" );
XP_ASSERT( !!tmp );
gi.nPlayers = tmp->valueint;
@ -1506,18 +1528,13 @@ inviteFromArgs( CursesAppGlobals* aGlobals, cJSON* args )
/* XP_LOGFF( "(%s)", buf ); */
/* } */
XP_U32 gameID;
cJSON* tmp = cJSON_GetObjectItem( args, "gid" );
XP_ASSERT( !!tmp );
sscanf( tmp->valuestring, "%X", &gameID );
XP_U32 gameID = gidFromObject( args );
tmp = cJSON_GetObjectItem( args, "channel" );
cJSON* tmp = cJSON_GetObjectItem( args, "channel" );
XP_ASSERT( !!tmp );
XP_U16 channel = tmp->valueint;
XP_LOGFF( "read channel: %X", channel );
/* CursesBoardState* cbState, XP_U32 gameID, XP_U16 forceChannel, */
/* const CommsAddrRec* destAddr */
CommsAddrRec destAddr = {0};
cJSON* addr = cJSON_GetObjectItem( args, "addr" );
XP_ASSERT( !!addr );
@ -1541,6 +1558,28 @@ inviteFromArgs( CursesAppGlobals* aGlobals, cJSON* args )
return XP_TRUE;
}
static XP_Bool
moveifFromArgs( CursesAppGlobals* aGlobals, cJSON* args )
{
XP_U32 gameID = gidFromObject( args );
return cb_makeMoveIf( aGlobals->cbState, gameID );
}
/* Return 'gid' of new game */
static XP_U32
rematchFromArgs( CursesAppGlobals* aGlobals, cJSON* args )
{
XP_U32 result = 0;
XP_U32 gameID = gidFromObject( args );
XP_U32 newGameID = 0;
if ( cb_makeRematch( aGlobals->cbState, gameID, &newGameID ) ) {
result = newGameID;
}
return result;
}
static cJSON*
getGamesStateForArgs( CursesAppGlobals* aGlobals, cJSON* args )
{
@ -1549,14 +1588,12 @@ getGamesStateForArgs( CursesAppGlobals* aGlobals, cJSON* args )
LaunchParams* params = aGlobals->cag.params;
cJSON* gids = cJSON_GetObjectItem(args, "gids" );
for ( int ii = 0 ; ii < cJSON_GetArraySize(gids) ; ++ii ) {
XP_U32 gameID;
cJSON* gid = cJSON_GetArrayItem( gids, ii );
sscanf( gid->valuestring, "%X", &gameID );
XP_U32 gameID = castGid( cJSON_GetArrayItem( gids, ii ) );
GameInfo gib;
if ( gdb_getGameInfoForGID( params->pDb, gameID, &gib ) ) {
cJSON* item = cJSON_CreateObject();
cJSON_AddStringToObject( item, "gid", gid->valuestring );
addGIDToObject( item, gameID, "gid" );
cJSON_AddBoolToObject( item, "gameOver", gib.gameOver );
cJSON_AddNumberToObject( item, "nPending", gib.nPending );
cJSON_AddNumberToObject( item, "nMoves", gib.nMoves );
@ -1631,6 +1668,15 @@ on_incoming_signal( GSocketService* XP_UNUSED(service),
} else if ( 0 == strcmp( cmdStr, "invite" ) ) {
XP_Bool success = inviteFromArgs( aGlobals, args );
response = makeBoolObj( "success", success );
} else if ( 0 == strcmp( cmdStr, "moveIf" ) ) {
XP_Bool success = moveifFromArgs( aGlobals, args );
response = makeBoolObj( "success", success );
} else if ( 0 == strcmp( cmdStr, "rematch" ) ) {
XP_U32 newGameID = rematchFromArgs( aGlobals, args );
if ( 0 != newGameID ) {
response = cJSON_CreateObject();
addGIDToObject( response, newGameID, "newGid" );
}
} else if ( 0 == strcmp( cmdStr, "gamesState" ) ) {
response = getGamesStateForArgs( aGlobals, args );
} else {

View file

@ -37,10 +37,11 @@ class GuestGameInfo():
# Should be subclass of GuestGameInfo
class HostedInfo():
def __init__(self, guests):
def __init__(self, guests, gid=None, invitesSent=False):
self.guestNames = guests
self.gid = '{:08X}'.format(random.randint(0, 0x7FFFFFFF))
self.invitesSent = False
self.gid = gid and gid or '{:08X}'.format(random.randint(1, 0x7FFFFFFF))
assert len(self.gid) == 8
self.invitesSent = invitesSent
def __str__(self):
return 'gid: {}, guests: {}'.format(self.gid, self.guestNames)
@ -165,6 +166,16 @@ class Device():
while not self.endTime or not os.path.exists(self.cmdSocketName):
time.sleep(0.2)
def moveOne(self):
moved = False
gids = [game.gid for game in self._allGames() if not self.gameOver(game.gid)]
random.shuffle(gids)
for gid in gids:
response = self._sendWaitReply('moveIf', gid=gid)
moved = response.get('success', False)
if moved: break
return moved
def _sendWaitReply(self, cmd, **kwargs):
self.launchIfNot()
@ -229,15 +240,28 @@ class Device():
if not stepped:
if not self.endTime:
self.launchIfNot()
elif datetime.datetime.now() > self.endTime:
self.quit()
elif self.moveOne():
pass
else:
now = datetime.datetime.now()
if now > self.endTime:
self.quit()
else:
# self._log('sleeping with {} to go'.format(self.endTime-now))
time.sleep(0.5)
# self._log('sleeping with {} to go'.format(self.endTime-now))
time.sleep(0.5)
stepped = True;
# I may be a guest or a host in this game. Rematch works either
# way. But how I figure out the other players differs.
def rematch(self, gid):
newGid = self._sendWaitReply('rematch', gid=gid).get('newGid')
if newGid:
guests = Device.playersIn(gid)
guests.remove(self.host)
self._log('rematch: new host: {}; new guest[s]: {}, gid: {}'.format(self.host, guests, newGid))
self.hostedGames.append(HostedInfo(guests, newGid, True))
for guest in guests:
Device.getFor(guest).expectInvite(newGid)
def invite(self, game):
failed = False
for ii in range(len(game.guestNames)):
@ -365,6 +389,10 @@ class Device():
Device._devs[host] = dev
return dev
@staticmethod
def playersIn(gid):
return [dev.host for dev in Device.devsWith(gid)]
@staticmethod
# return all devices (up to 4 of them) that are host or guest in a
# game with <gid>"""
@ -376,6 +404,16 @@ class Device():
def getAll():
return [dev for dev in Device._devs.values()]
@staticmethod
def getFor(player):
result = None
for dev in Device.getAll():
if dev.host == player:
result = dev
break;
assert result
return result
def addGameWith(self, guests):
# self._log('addGameWith({})'.format(guests))
hosted = HostedInfo(guests)
@ -407,11 +445,22 @@ def openOnExit(args):
subprocess.Popen([str(arg) for arg in appargs], stdout = subprocess.DEVNULL,
stderr = subprocess.DEVNULL, universal_newlines = True)
# Pick a game that's joined -- all invites accepted -- and call
# rematch on it. Return True if successful
def testRematch():
for dev in Device.getAll():
for gid, status in dev.gameStates.items():
if 2 < status.get('nMoves', 0):
dev.rematch(gid)
return True
return False
def mainLoop(args, devs):
startCount = len(devs)
startTime = datetime.datetime.now()
nextStallCheck = startTime + datetime.timedelta(seconds = 20)
rematchTested = False
while 0 < len(devs):
if gDone:
@ -424,6 +473,8 @@ def mainLoop(args, devs):
devs.remove(dev)
log(args, 'removed dev for {}; {} devs left'.format(dev.host, len(devs)))
if not rematchTested: rematchTested = testRematch()
now = datetime.datetime.now()
if devs and now > nextStallCheck:
nextStallCheck = now + datetime.timedelta(seconds = 10)