diff --git a/xwords4/common/board.c b/xwords4/common/board.c index 43ffc8c2f..52680aade 100644 --- a/xwords4/common/board.c +++ b/xwords4/common/board.c @@ -124,7 +124,6 @@ static XP_Bool board_moveArrow( BoardCtxt* board, XWEnv xwe, XP_Key cursorKey ); static XP_Bool board_setXOffset( BoardCtxt* board, XP_U16 offset ); static XP_Bool preflight( BoardCtxt* board, XWEnv xwe, XP_Bool reveal ); -static XP_U16 MIN_TRADE_TILES( const BoardCtxt* board ); #ifdef KEY_SUPPORT static XP_Bool moveKeyTileToBoard( BoardCtxt* board, XWEnv xwe, @@ -866,7 +865,7 @@ board_canTrade( BoardCtxt* board, XWEnv xwe ) { XP_Bool result = preflight( board, xwe, XP_FALSE ) && !board->gi->inDuplicateMode - && MIN_TRADE_TILES(board) <= server_countTilesInPool( board->server ); + && MIN_TRADE_TILES(board->gi) <= server_countTilesInPool( board->server ); return result; } @@ -1230,7 +1229,7 @@ selectPlayerImpl( BoardCtxt* board, XWEnv xwe, XP_U16 newPlayer, XP_Bool reveal, /* Just in case somebody started a trade when it wasn't his turn and there were plenty of tiles but now there aren't. */ if ( newInfo->tradeInProgress && - server_countTilesInPool(board->server) < MIN_TRADE_TILES(board) ) { + server_countTilesInPool(board->server) < MIN_TRADE_TILES(board->gi) ) { newInfo->tradeInProgress = XP_FALSE; newInfo->traySelBits = 0x00; /* clear any selected */ } @@ -2100,15 +2099,6 @@ preflight( BoardCtxt* board, XWEnv xwe, XP_Bool reveal ) && !TRADE_IN_PROGRESS(board); } /* preflight */ -static XP_U16 -MIN_TRADE_TILES( const BoardCtxt* board ) -{ - const DictionaryCtxt* dict = model_getDictionary( board->model ); - const XP_UCHAR* isoCode = dict_getISOCode( dict ); - /* In Spanish, I'm told, you can trade until there are no tiles left.) */ - return 0 == XP_STRCMP( "es", isoCode ) ? 1 : MIN_TRAY_TILES; -} - /* Refuse with error message if any tiles are currently on board in this turn. * Then call the engine, and display the first move. Return true if there's * any redrawing to be done. @@ -2566,7 +2556,7 @@ board_beginTrade( BoardCtxt* board, XWEnv xwe ) result = preflight( board, xwe, XP_TRUE ); if ( result ) { XP_S16 tilesLeft = server_countTilesInPool(board->server); - if ( tilesLeft < MIN_TRADE_TILES( board ) ) { + if ( tilesLeft < MIN_TRADE_TILES( board->gi ) ) { util_userError( board->util, xwe, ERR_TOO_FEW_TILES_LEFT_TO_TRADE ); } else { model_resetCurrentTurn( board->model, xwe, board->selPlayer ); diff --git a/xwords4/common/comtypes.h b/xwords4/common/comtypes.h index e269852ab..2008f5c10 100644 --- a/xwords4/common/comtypes.h +++ b/xwords4/common/comtypes.h @@ -48,6 +48,7 @@ #define MAX_COLS MAX_ROWS #define MIN_COLS 11 +#define STREAM_VERS_SUBSEVEN 0x26 #define STREAM_VERS_REMATCHORDER 0x25 #define STREAM_VERS_REMATCHADDRS 0x24 #define STREAM_VERS_MSGSTREAMVERS 0x23 @@ -100,7 +101,7 @@ #define STREAM_VERS_405 0x01 /* search for FIX_NEXT_VERSION_CHANGE next time this is changed */ -#define CUR_STREAM_VERS STREAM_VERS_REMATCHORDER +#define CUR_STREAM_VERS STREAM_VERS_SUBSEVEN typedef struct XP_Rect { XP_S16 left; diff --git a/xwords4/common/game.c b/xwords4/common/game.c index 06cf85232..93f9eada2 100644 --- a/xwords4/common/game.c +++ b/xwords4/common/game.c @@ -635,6 +635,7 @@ gi_copy( MPFORMAL CurGameInfo* destGI, const CurGameInfo* srcGI ) destGI->inDuplicateMode = srcGI->inDuplicateMode; XP_LOGFF( "copied forceChannel: %d; inDuplicateMode: %d", destGI->forceChannel, destGI->inDuplicateMode ); + destGI->tradeSubSeven = srcGI->tradeSubSeven; const LocalPlayer* srcPl; LocalPlayer* destPl; @@ -725,6 +726,9 @@ gi_equal( const CurGameInfo* gi1, const CurGameInfo* gi2 ) equal = strEq( gi1->isoCodeStr, gi2->isoCodeStr ); break; case 17: + equal = gi1->tradeSubSeven == gi2->tradeSubSeven; + break; + case 18: for ( int jj = 0; equal && jj < gi1->nPlayers; ++jj ) { const LocalPlayer* lp1 = &gi1->players[jj]; const LocalPlayer* lp2 = &gi2->players[jj]; @@ -865,6 +869,9 @@ gi_readFromStream( MPFORMAL XWStreamCtxt* stream, CurGameInfo* gi ) gi->inDuplicateMode = strVersion >= STREAM_VERS_DUPLICATE ? stream_getBits( stream, 1 ) : XP_FALSE; + gi->tradeSubSeven = strVersion >= STREAM_VERS_SUBSEVEN + ? stream_getBits( stream, 1 ) + : XP_FALSE; if ( strVersion >= STREAM_VERS_41B4 ) { gi->allowPickTiles = stream_getBits( stream, 1 ); gi->allowHintRect = stream_getBits( stream, 1 ); @@ -954,6 +961,9 @@ gi_writeToStream( XWStreamCtxt* stream, const CurGameInfo* gi ) stream_putBits( stream, 2, gi->phoniesAction ); stream_putBits( stream, 1, gi->timerEnabled ); stream_putBits( stream, 1, gi->inDuplicateMode ); + if ( strVersion >= STREAM_VERS_SUBSEVEN ) { + stream_putBits( stream, 1, gi->tradeSubSeven ); + } stream_putBits( stream, 1, gi->allowPickTiles ); stream_putBits( stream, 1, gi->allowHintRect ); stream_putBits( stream, 1, gi->confirmBTConnect ); @@ -1056,6 +1066,7 @@ game_logGI( const CurGameInfo* gi, const char* msg, const char* func, int line ) XP_LOGF( " serverRole: %d", gi->serverRole ); XP_LOGF( " dictName: %s", gi->dictName ); XP_LOGF( " isoCode: %s", gi->isoCodeStr ); + XP_LOGF( " tradeSubSeven: %s", boolToStr(gi->tradeSubSeven) ); } } #endif diff --git a/xwords4/common/gameinfo.h b/xwords4/common/gameinfo.h index 514b5bcd7..00d3faf35 100644 --- a/xwords4/common/gameinfo.h +++ b/xwords4/common/gameinfo.h @@ -61,10 +61,13 @@ typedef struct CurGameInfo { XP_Bool allowPickTiles; XP_Bool allowHintRect; XP_Bool inDuplicateMode; + XP_Bool tradeSubSeven; XWPhoniesChoice phoniesAction; XP_Bool confirmBTConnect; /* only used for BT */ } CurGameInfo; +#define MIN_TRADE_TILES(GI) ((GI)->tradeSubSeven ? 1 : (GI)->traySize) + #ifdef DEBUG # define LOGGI( gip, msg ) game_logGI( (gip), (msg), __func__, __LINE__ ) void game_logGI( const CurGameInfo* gi, const char* msg, diff --git a/xwords4/common/nwgamest.c b/xwords4/common/nwgamest.c index 5d41c3ce2..98dd0cab1 100644 --- a/xwords4/common/nwgamest.c +++ b/xwords4/common/nwgamest.c @@ -52,6 +52,7 @@ struct NewGameCtx { XP_TriEnable juggleEnabled; XP_TriEnable settingsEnabled; XP_Bool duplicateEnabled; + XP_Bool sub7Enabled; MPSLOT }; @@ -150,6 +151,10 @@ newg_load( NewGameCtx* ngc, XWEnv xwe, const CurGameInfo* gi ) value.ng_bool = ngc->duplicateEnabled; (*ngc->setAttrProc)( closure, NG_ATTR_DUPLICATE, value ); + ngc->sub7Enabled = gi->tradeSubSeven; + value.ng_bool = ngc->sub7Enabled; + (*ngc->setAttrProc)( closure, NG_ATTR_SUB7, value ); + ngc->timerSeconds = gi->gameSeconds; value.ng_u16 = ngc->timerSeconds; (*ngc->setAttrProc)( closure, NG_ATTR_TIMER, value ); @@ -229,6 +234,7 @@ newg_store( NewGameCtx* ngc, XWEnv xwe, CurGameInfo* gi, XP_Bool warn ) gi->timerEnabled = gi->gameSeconds > 0; gi->inDuplicateMode = ngc->duplicateEnabled; + gi->tradeSubSeven = ngc->sub7Enabled; gi->gameSeconds = ngc->timerSeconds; gi->timerEnabled = gi->gameSeconds > 0; @@ -274,6 +280,10 @@ newg_attrChanged( NewGameCtx* ngc, XWEnv xwe, case NG_ATTR_DUPLICATE: ngc->duplicateEnabled = value.ng_bool; break; + case NG_ATTR_SUB7: + ngc->sub7Enabled = value.ng_bool; + break; + default: XP_ASSERT( 0 ); } diff --git a/xwords4/common/nwgamest.h b/xwords4/common/nwgamest.h index 6129b63fe..47ce2303c 100644 --- a/xwords4/common/nwgamest.h +++ b/xwords4/common/nwgamest.h @@ -58,6 +58,7 @@ typedef enum { NG_ATTR_CANJUGGLE, NG_ATTR_TIMER, NG_ATTR_DUPLICATE, + NG_ATTR_SUB7, } NewGameAttr; typedef union NGValue { diff --git a/xwords4/linux/cursesboard.c b/xwords4/linux/cursesboard.c index a288fb71b..4aed2d70d 100644 --- a/xwords4/linux/cursesboard.c +++ b/xwords4/linux/cursesboard.c @@ -742,6 +742,11 @@ cb_makeMoveIf( CursesBoardState* cbState, XP_U32 gameID, XP_Bool tryTrade ) ModelCtxt* model = cGlobals->game.model; TrayTileSet oldTiles = *model_getPlayerTiles( model, turn ); + XP_S16 nTiles = server_countTilesInPool( server ); + XP_ASSERT( 0 <= nTiles ); + if ( nTiles < oldTiles.nTiles ) { + oldTiles.nTiles = nTiles; + } success = server_commitTrade( server, NULL_XWE, &oldTiles, NULL ); } else { XP_Bool ignored; diff --git a/xwords4/linux/cursesmain.c b/xwords4/linux/cursesmain.c index e67ddc70a..f150b8532 100644 --- a/xwords4/linux/cursesmain.c +++ b/xwords4/linux/cursesmain.c @@ -1521,6 +1521,9 @@ makeGameFromArgs( CursesAppGlobals* aGlobals, cJSON* args ) gi.traySize = tmp->valueint; } + tmp = cJSON_GetObjectItem( args, "allowSub7" ); + gi.tradeSubSeven = !!tmp && cJSON_IsTrue( tmp ); + tmp = cJSON_GetObjectItem( args, "isSolo" ); XP_ASSERT( !!tmp ); XP_Bool isSolo = cJSON_IsTrue( tmp ); diff --git a/xwords4/linux/gtknewgame.c b/xwords4/linux/gtknewgame.c index 9c8219e59..3d626d979 100644 --- a/xwords4/linux/gtknewgame.c +++ b/xwords4/linux/gtknewgame.c @@ -65,6 +65,7 @@ typedef struct GtkNewGameState { GtkWidget* juggleButton; GtkWidget* timerField; GtkWidget* duplicateCheck; + GtkWidget* sub7Check; } GtkNewGameState; static void @@ -275,6 +276,13 @@ addTimerWidget( GtkNewGameState* state, GtkWidget* parent ) gtk_box_pack_start( GTK_BOX(hbox), state->timerField, FALSE, TRUE, 0 ); } +static void +handle_sub7_toggled( GtkWidget* item, GtkNewGameState* state ) +{ + NGValue value = { .ng_bool = gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON(item) ) }; + newg_attrChanged( state->newGameCtxt, NULL_XWE, NG_ATTR_SUB7, value ); +} + static void handle_duplicate_toggled( GtkWidget* item, GtkNewGameState* state ) { @@ -282,6 +290,17 @@ handle_duplicate_toggled( GtkWidget* item, GtkNewGameState* state ) newg_attrChanged( state->newGameCtxt, NULL_XWE, NG_ATTR_DUPLICATE, value ); } +static void +addTradeSub7Checkbox( GtkNewGameState* state, GtkWidget* parent ) +{ + GtkWidget* sub7Check = state->sub7Check = + gtk_check_button_new_with_label( "Allow sub-7 trades" ); + g_signal_connect( sub7Check, "toggled", + (GCallback)handle_sub7_toggled, state ); + gtk_widget_show( sub7Check ); + gtk_box_pack_start( GTK_BOX(parent), sub7Check, FALSE, TRUE, 0 ); +} + static void addDuplicateCheckbox( GtkNewGameState* state, GtkWidget* parent ) { @@ -505,6 +524,7 @@ makeNewGameDialog( GtkNewGameState* state ) gtk_box_pack_start( GTK_BOX(vbox), hbox, FALSE, TRUE, 0 ); addTimerWidget( state, vbox ); + addTradeSub7Checkbox( state, vbox ); addDuplicateCheckbox( state, vbox ); /* buttons at the bottom */ @@ -691,6 +711,10 @@ gtk_newgame_attr_set( void* closure, NewGameAttr attr, NGValue value ) gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(state->duplicateCheck), value.ng_bool ); break; + case NG_ATTR_SUB7: + gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(state->sub7Check), + value.ng_bool ); + break; default: XP_ASSERT(0); break; diff --git a/xwords4/linux/scripts/netGamesTest.py b/xwords4/linux/scripts/netGamesTest.py index be24ed796..84284f9b8 100755 --- a/xwords4/linux/scripts/netGamesTest.py +++ b/xwords4/linux/scripts/netGamesTest.py @@ -334,10 +334,10 @@ class Device(): hostPosn = random.randint(0, nPlayers-1) traySize = 0 == args.TRAY_SIZE and random.randint(7, 9) or args.TRAY_SIZE boardSize = random.choice(range(args.BOARD_SIZE_MIN, args.BOARD_SIZE_MAX+1, 2)) - + allowSub7 = random.randint(0, 99) < self.args.SUB7_TRADES_PCT response = self._sendWaitReply('makeGame', nPlayers=nPlayers, hostPosn=hostPosn, dict=args.DICTS[0], boardSize=boardSize, - traySize=traySize, isSolo=isSolo) + traySize=traySize, isSolo=isSolo, allowSub7=allowSub7) newGid = response.get('newGid') if newGid: game.setGid(newGid) @@ -788,6 +788,7 @@ def mkParser(): # parser.add_argument('--undo-pct', dest = 'UNDO_PCT', default = 0, type = int) parser.add_argument('--trade-pct', dest = 'TRADE_PCT', default = 10, type = int) + parser.add_argument('--sub7-trades-pct', dest = 'SUB7_TRADES_PCT', default = 10, type=int) parser.add_argument('--with-sms', dest = 'WITH_SMS', action = 'store_true') parser.add_argument('--without-sms', dest = 'WITH_SMS', default = False, action = 'store_false')