From 6e5973c55ba6c79813aa5f55af91ab99fc9092c6 Mon Sep 17 00:00:00 2001 From: Eric House Date: Thu, 9 Mar 2017 20:36:14 -0800 Subject: [PATCH] toward making tile picking work through rotations Make face-up tile picker util method return void and add mechanism for passing in selected tiles asynchronously, as has been done recently with the rest of the once-blocking util callbacks. Works perfectly in the gtk case. Likely crashes in curses (if picking face-up option is on.) In java all the callbacks are there but rather than put up a UI we pretend the user says "pick 'em for me" each time. Putting up a UI is next. --- .../eehouse/android/xw4/BoardDelegate.java | 32 +++- .../eehouse/android/xw4/jni/JNIThread.java | 11 +- .../org/eehouse/android/xw4/jni/UtilCtxt.java | 5 +- .../eehouse/android/xw4/jni/UtilCtxtImpl.java | 7 +- .../org/eehouse/android/xw4/jni/XwJNI.java | 5 +- xwords4/android/jni/drawwrapper.c | 2 +- xwords4/android/jni/utilwrapper.c | 37 +++-- xwords4/android/jni/xwjni.c | 40 ++++- xwords4/common/board.c | 44 +++-- xwords4/common/board.h | 2 +- xwords4/common/server.c | 152 +++++++++++++++--- xwords4/common/server.h | 10 +- xwords4/common/tray.c | 2 +- xwords4/common/util.h | 16 +- xwords4/linux/cursesmain.c | 34 ++-- xwords4/linux/gtkboard.c | 96 +++++++++-- xwords4/linux/gtkletterask.c | 13 +- xwords4/linux/gtkletterask.h | 5 +- xwords4/linux/main.h | 3 + 19 files changed, 385 insertions(+), 131 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java index 09f4989fb..c7b348f12 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java @@ -1869,16 +1869,32 @@ public class BoardDelegate extends DelegateBase } @Override - public int userPickTileTray( int playerNum, String[] texts, - String[] curTiles, int nPicked ) + public void informNeedPickTiles( final boolean isInitial, + final int playerNum, int nToPick, + String[] texts, int[] counts ) { - String curTilesStr = TextUtils.join( ", ", curTiles ); - boolean canUndoTiles = 0 < nPicked; - waitBlockingDialog( DlgID.PICK_TILE_REQUESTTRAY_BLK, - UtilCtxt.PICKER_PICKALL, texts, curTilesStr, - canUndoTiles ); - return m_resultCode; + post( new Runnable() { + @Override + public void run() { + int[] noNewTiles = new int[0]; + if ( isInitial ) { + handleViaThread( JNICmd.CMD_TILES_PICKED, playerNum, noNewTiles ); + } else { + handleViaThread( JNICmd.CMD_COMMIT, true, true, noNewTiles ); + } + } + } ); } + // public int userPickTileTray( int playerNum, String[] texts, + // String[] curTiles, int nPicked ) + // { + // String curTilesStr = TextUtils.join( ", ", curTiles ); + // boolean canUndoTiles = 0 < nPicked; + // waitBlockingDialog( DlgID.PICK_TILE_REQUESTTRAY_BLK, + // UtilCtxt.PICKER_PICKALL, texts, curTilesStr, + // canUndoTiles ); + // return m_resultCode; + // } @Override public void informNeedPassword( int player, String name ) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java index c2741cbb7..41b8c8951 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/JNIThread.java @@ -72,6 +72,7 @@ public class JNIThread extends Thread { CMD_KEYUP, CMD_TIMER_FIRED, CMD_COMMIT, + CMD_TILES_PICKED, CMD_JUGGLE, CMD_FLIP, CMD_TOGGLE_TRAY, @@ -552,9 +553,17 @@ public class JNIThread extends Thread { ? false : (Boolean)args[0]; boolean turnConfirmed = args.length < 2 ? false : (Boolean)args[1]; + int[] newTiles = args.length < 3 ? null : (int[])args[2]; draw = XwJNI.board_commitTurn( m_jniGamePtr, phoniesConfirmed, - turnConfirmed ); + turnConfirmed, newTiles ); break; + + case CMD_TILES_PICKED: + int playerNum = (Integer)args[0]; + int[] tiles = (int[])args[1]; + XwJNI.server_tilesPicked( m_jniGamePtr, playerNum, tiles ); + break; + case CMD_JUGGLE: draw = XwJNI.board_juggleTray( m_jniGamePtr ); break; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java index 28732ffb0..235b32831 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxt.java @@ -38,8 +38,9 @@ public interface UtilCtxt { public static final int PICKER_BACKUP = -2; void notifyPickTileBlank( int playerNum, int col, int row, String[] texts ); - int userPickTileTray( int playerNum, String[] tiles, - String[] curTiles, int nPicked ); + + void informNeedPickTiles( boolean isInitial, int playerNum, int nToPick, + String[] texts, int[] counts ); void informNeedPassword( int player, String name ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java index cdae748a7..df519cf09 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/UtilCtxtImpl.java @@ -54,11 +54,10 @@ public class UtilCtxtImpl implements UtilCtxt { subclassOverride( "userPickTileBlank" ); } - public int userPickTileTray( int playerNum, String[] texts, - String[] curTiles, int nPicked ) + public void informNeedPickTiles( boolean isInitial, int playerNum, int nToPick, + String[] texts, int[] counts ) { - subclassOverride( "userPickTileTray" ); - return 0; + subclassOverride( "informNeedPickTiles" ); } public void informNeedPassword( int player, String name ) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java index 55f0401c4..4548e84cb 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java @@ -300,7 +300,8 @@ public class XwJNI { public static native boolean board_toggle_showValues( GamePtr gamePtr ); public static native boolean board_commitTurn( GamePtr gamePtr, boolean phoniesConfirmed, - boolean turnConfirmed ); + boolean turnConfirmed, + int[] newTiles ); public static native boolean board_flip( GamePtr gamePtr ); public static native boolean board_replaceTiles( GamePtr gamePtr ); @@ -358,6 +359,8 @@ public class XwJNI { public static native void server_reset( GamePtr gamePtr ); public static native void server_handleUndo( GamePtr gamePtr ); public static native boolean server_do( GamePtr gamePtr ); + public static native void server_tilesPicked( GamePtr gamePtr, int player, int[] tiles ); + public static native String server_formatDictCounts( GamePtr gamePtr, int nCols ); public static native boolean server_getGameIsOver( GamePtr gamePtr ); public static native String server_writeFinalScores( GamePtr gamePtr ); diff --git a/xwords4/android/jni/drawwrapper.c b/xwords4/android/jni/drawwrapper.c index a7fc11ab9..b383ae742 100644 --- a/xwords4/android/jni/drawwrapper.c +++ b/xwords4/android/jni/drawwrapper.c @@ -1,4 +1,4 @@ -/* -*-mode: C; compile-command: "cd ..; ../scripts/ndkbuild.sh -j3"; -*- */ +/* -*-mode: C; compile-command: "find-and-gradle.sh insXwdDeb"; -*- */ /* * Copyright 2001-2010 by Eric House (xwords@eehouse.org). All rights * reserved. diff --git a/xwords4/android/jni/utilwrapper.c b/xwords4/android/jni/utilwrapper.c index f7125aa9c..0ebc2c574 100644 --- a/xwords4/android/jni/utilwrapper.c +++ b/xwords4/android/jni/utilwrapper.c @@ -1,6 +1,6 @@ -/* -*- compile-command: "find-and-ant.sh debug install"; -*- */ +/* -*- compile-command: "find-and-gradle.sh insXw4Deb"; -*- */ /* - * Copyright 2001-2014 by Eric House (xwords@eehouse.org). All rights + * Copyright 2001 - 2017 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or @@ -162,24 +162,23 @@ and_util_notifyPickTileBlank( XW_UtilCtxt* uc, XP_U16 playerNum, UTIL_CBK_TAIL(); } -static XP_S16 -and_util_userPickTileTray( XW_UtilCtxt* uc, const PickInfo* pi, - XP_U16 playerNum, const XP_UCHAR** tileFaces, - XP_U16 nTiles ) +static void +and_util_informNeedPickTiles( XW_UtilCtxt* uc, XP_Bool isInitial, + XP_U16 player, XP_U16 nToPick, + XP_U16 nFaces, const XP_UCHAR** faces, + const XP_U16* counts ) { - XP_S16 result = -1; - UTIL_CBK_HEADER("userPickTileTray", - "(I[Ljava/lang/String;[Ljava/lang/String;I)I" ); - jobject jtexts = makeStringArray( env, nTiles, tileFaces ); - jobject jcurtiles = makeStringArray( env, pi->nCurTiles, pi->curTiles ); - result = (*env)->CallIntMethod( env, util->jutil, mid, - playerNum, jtexts, jcurtiles, - pi->thisPick ); - deleteLocalRefs( env, jtexts, jcurtiles, DELETE_NO_REF ); - + UTIL_CBK_HEADER("informNeedPickTiles", + "(ZII[Ljava/lang/String;[I)V" ); + jobject jtexts = makeStringArray( env, nFaces, faces ); + jobject jcounts = makeIntArray( env, nFaces, counts ); + + (*env)->CallVoidMethod( env, util->jutil, mid, isInitial, player, + nToPick, jtexts, jcounts ); + + deleteLocalRefs( env, jtexts, jcounts, DELETE_NO_REF ); UTIL_CBK_TAIL(); - return result; -} /* and_util_userPickTile */ +} /* and_util_informNeedPickTiles */ static void and_util_informNeedPassword( XW_UtilCtxt* uc, XP_U16 player, @@ -707,7 +706,7 @@ makeUtil( MPFORMAL EnvThreadInfo* ti, jobject jutil, CurGameInfo* gi, SET_PROC(notifyMove); SET_PROC(notifyTrade); SET_PROC(notifyPickTileBlank); - SET_PROC(userPickTileTray); + SET_PROC(informNeedPickTiles); SET_PROC(informNeedPassword); SET_PROC(trayHiddenChange); SET_PROC(yOffsetChange); diff --git a/xwords4/android/jni/xwjni.c b/xwords4/android/jni/xwjni.c index 985a91325..afb64c10f 100644 --- a/xwords4/android/jni/xwjni.c +++ b/xwords4/android/jni/xwjni.c @@ -220,6 +220,21 @@ envForMe( EnvThreadInfo* ti, const char* caller ) return result; } +static void +tilesArrayToTileSet( JNIEnv* env, jintArray jtiles, TrayTileSet* tset ) +{ + if ( jtiles != NULL ) { + jsize nTiles = (*env)->GetArrayLength( env, jtiles ); + int tmp[MAX_TRAY_TILES]; + getIntsFromArray( env, tmp, jtiles, nTiles, XP_FALSE ); + + tset->nTiles = nTiles; + for ( int ii = 0; ii < nTiles; ++ii ) { + tset->tiles[ii] = tmp[ii]; + } + } +} + #ifdef GAMEPTR_IS_OBJECT static JNIState* getState( JNIEnv* env, GamePtrType gamePtr ) @@ -1282,13 +1297,21 @@ Java_org_eehouse_android_xw4_jni_XwJNI_board_1toggle_1showValues JNIEXPORT jboolean JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_board_1commitTurn -(JNIEnv* env, jclass C, GamePtrType gamePtr, jboolean phoniesConfirmed, - jboolean turnConfirmed) +( JNIEnv* env, jclass C, GamePtrType gamePtr, jboolean phoniesConfirmed, + jboolean turnConfirmed, jintArray jNewTiles ) { jboolean result; XWJNI_START(); + TrayTileSet* newTilesP = NULL; + TrayTileSet newTiles; + + if ( jNewTiles != NULL ) { + tilesArrayToTileSet( env, jNewTiles, &newTiles ); + newTilesP = &newTiles; + } + result = board_commitTurn( state->game.board, phoniesConfirmed, - turnConfirmed ); + turnConfirmed, newTilesP ); XWJNI_END(); return result; } @@ -1356,6 +1379,17 @@ Java_org_eehouse_android_xw4_jni_XwJNI_server_1do return result; } +JNIEXPORT void JNICALL +Java_org_eehouse_android_xw4_jni_XwJNI_server_1tilesPicked +( JNIEnv* env, jclass C, GamePtrType gamePtr, jint player, jintArray jNewTiles ) +{ + XWJNI_START(); + TrayTileSet newTiles; + tilesArrayToTileSet( env, jNewTiles, &newTiles ); + server_tilesPicked( state->game.server, player, &newTiles ); + XWJNI_END(); +} + JNIEXPORT void JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_board_1resetEngine (JNIEnv* env, jclass C, GamePtrType gamePtr ) diff --git a/xwords4/common/board.c b/xwords4/common/board.c index ba8f8f8fe..fd498af4f 100644 --- a/xwords4/common/board.c +++ b/xwords4/common/board.c @@ -1026,17 +1026,19 @@ boardNotifyTrade( BoardCtxt* board, const TrayTileSet* tiles ) XP_Bool board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed, - XP_Bool turnConfirmed /* includes trade */ ) + XP_Bool turnConfirmed /* includes trade */, + TrayTileSet* newTiles ) { XP_Bool result = XP_FALSE; const XP_S16 turn = server_getCurrentTurn( board->server, NULL ); PerTurnInfo* pti = board->pti + turn; + ModelCtxt* model = board->model; if ( board->gameOver || turn < 0 ) { /* do nothing */ } else if ( turn != board->selPlayer ) { util_userError( board->util, ERR_NOT_YOUR_TURN ); - } else if ( 0 == model_getNumTilesTotal( board->model, turn ) ) { + } else if ( 0 == model_getNumTilesTotal( model, turn ) ) { /* game's over but still undoable so turn hasn't changed; do nothing */ } else if ( phoniesConfirmed || turnConfirmed || checkRevealTray( board ) ) { @@ -1051,11 +1053,15 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed, TrayTileSet selTiles; getSelTiles( board, traySelBits, &selTiles ); if ( turnConfirmed ) { - /* server_commitTrade() changes selPlayer, so board_endTrade - must be called first() */ - (void)board_endTrade( board ); + if ( !server_askPickTiles( board->server, turn, newTiles, + selTiles.nTiles ) ) { + /* server_commitTrade() changes selPlayer, so board_endTrade + must be called first() */ + (void)board_endTrade( board ); - (void)server_commitTrade( board->server, &selTiles ); + (void)server_commitTrade( board->server, &selTiles, + newTiles ); + } } else { boardNotifyTrade( board, &selTiles ); } @@ -1081,13 +1087,13 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed, info.proc = saveBadWords; info.closure = &bwl; } - legal = model_checkMoveLegal( board->model, turn, stream, + legal = model_checkMoveLegal( model, turn, stream, warn? &info:(WordNotifierInfo*)NULL); } if ( 0 < bwl.bwi.nWords && !phoniesConfirmed ) { bwl.bwi.dictName = - dict_getShortName( model_getPlayerDict( board->model, turn ) ); + dict_getShortName( model_getPlayerDict( model, turn ) ); util_notifyIllegalWords( board->util, &bwl.bwi, turn, XP_FALSE ); } else { /* Hide the tray so no peeking. Leave it hidden even if user @@ -1099,14 +1105,20 @@ board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed, } if ( board->skipCommitConfirm || turnConfirmed ) { - result = server_commitMove( board->server ) || result; - /* invalidate all tiles in case we'll be drawing this tray - again rather than some other -- as is the case in a - two-player game where one's a robot. We really only - need the selected tiles and the rightmost (in case it's - showing points-this-turn), but this is easier. */ - board_invalTrayTiles( board, ALLTILES ); - pti->traySelBits = 0x00; + XP_U16 nToPick = MAX_TRAY_TILES - + model_getNumTilesInTray( model, turn ); + if ( !server_askPickTiles( board->server, turn, newTiles, + nToPick ) ) { + result = server_commitMove( board->server, newTiles ) + || result; + /* invalidate all tiles in case we'll be drawing this tray + again rather than some other -- as is the case in a + two-player game where one's a robot. We really only + need the selected tiles and the rightmost (in case it's + showing points-this-turn), but this is easier. */ + board_invalTrayTiles( board, ALLTILES ); + pti->traySelBits = 0x00; + } } else { util_notifyMove( board->util, stream ); } diff --git a/xwords4/common/board.h b/xwords4/common/board.h index 83bf397d3..476c5af89 100644 --- a/xwords4/common/board.h +++ b/xwords4/common/board.h @@ -177,7 +177,7 @@ XP_Bool board_setBlankValue( BoardCtxt* board, XP_U16 XP_UNUSED(player), void board_resetEngine( BoardCtxt* board ); XP_Bool board_commitTurn( BoardCtxt* board, XP_Bool phoniesConfirmed, - XP_Bool turnConfirmed ); + XP_Bool turnConfirmed, TrayTileSet* newTiles ); void board_pushTimerSave( BoardCtxt* board ); void board_popTimerSave( BoardCtxt* board ); diff --git a/xwords4/common/server.c b/xwords4/common/server.c index 87f8cf02a..98b66a1e7 100644 --- a/xwords4/common/server.c +++ b/xwords4/common/server.c @@ -34,6 +34,7 @@ #include "pool.h" #include "engine.h" #include "strutils.h" +#include "dbgutil.h" #include "LocalizedStrIncludes.h" @@ -132,6 +133,8 @@ struct ServerCtxt { /******************************* prototypes *******************************/ static void assignTilesToAll( ServerCtxt* server ); +static void makePoolOnce( ServerCtxt* server ); + static void resetEngines( ServerCtxt* server ); static void nextTurn( ServerCtxt* server, XP_S16 nxtTurn ); @@ -142,6 +145,9 @@ static void badWordMoveUndoAndTellUser( ServerCtxt* server, static XP_Bool tileCountsOk( const ServerCtxt* server ); static void setTurn( ServerCtxt* server, XP_S16 turn ); static XWStreamCtxt* mkServerStream( ServerCtxt* server ); +static void fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch, + const TrayTileSet* tradedTiles, + TrayTileSet* resultTiles ); #ifndef XWFEATURE_STANDALONE_ONLY static XWStreamCtxt* messageStreamWithHeader( ServerCtxt* server, @@ -900,7 +906,7 @@ makeRobotMove( ServerCtxt* server ) if ( trade ) { TrayTileSet oldTiles = *model_getPlayerTiles( model, turn ); XP_LOGF( "%s: robot trading %d tiles", __func__, oldTiles.nTiles ); - result = server_commitTrade( server, &oldTiles ); + result = server_commitTrade( server, &oldTiles, NULL ); /* Quick hack to fix gremlin bug where all-robot game seen none able to trade for tiles to move and blowing the undo stack. @@ -933,7 +939,7 @@ makeRobotMove( ServerCtxt* server ) server->nv.prevMoveStream = stream; server->nv.prevWordsStream = wordsStream; } - result = server_commitMove( server ); + result = server_commitMove( server, NULL ); } else { result = XP_FALSE; } @@ -1047,6 +1053,64 @@ showPrevScore( ServerCtxt* server ) SETSTATE( server, server->nv.stateAfterShow ); } /* showPrevScore */ +void +server_tilesPicked( ServerCtxt* server, XP_U16 player, + const TrayTileSet* newTilesP ) +{ + TrayTileSet newTiles = *newTilesP; + pool_removeTiles( server->pool, &newTiles ); + + fetchTiles( server, player, MAX_TRAY_TILES, NULL, &newTiles ); + model_assignPlayerTiles( server->vol.model, player, &newTiles ); + + util_requestTime( server->vol.util ); +} + +static void +informNeedPickTiles( ServerCtxt* server, XP_Bool initial, XP_U16 turn, + XP_U16 nToPick ) +{ + ModelCtxt* model = server->vol.model; + DictionaryCtxt* dict = model_getDictionary(model); + XP_U16 nFaces = dict_numTileFaces( dict ); + XP_U16 counts[MAX_UNIQUE_TILES]; + const XP_UCHAR* faces[MAX_UNIQUE_TILES]; + + XP_U16 nLeft = pool_getNTilesLeft( server->pool ); + if ( nLeft < nToPick ) { + nToPick = nLeft; + } + + for ( Tile tile = 0; tile < nFaces; ++tile ) { + faces[tile] = dict_getTileString( dict, tile ); + counts[tile] = pool_getNTilesLeftFor( server->pool, tile ); + } + util_informNeedPickTiles( server->vol.util, initial, turn, + nToPick, nFaces, faces, counts ); +} + +static XP_Bool +askedForTiles( ServerCtxt* server ) +{ + XP_Bool asked = XP_FALSE; + CurGameInfo* gi = server->vol.gi; + if ( gi->serverRole == SERVER_STANDALONE && gi->allowPickTiles ) { + XP_U16 nPlayers = gi->nPlayers; + ModelCtxt* model = server->vol.model; + makePoolOnce( server ); + for ( XP_U16 turn = 0; !asked && turn < nPlayers; ++turn ) { + LocalPlayer* player = &gi->players[turn]; + XP_U16 nTiles = model_getNumTilesInTray( model, turn ); + if ( nTiles == 0 && !LP_IS_ROBOT(player) ) { + informNeedPickTiles( server, XP_TRUE, turn, MAX_TRAY_TILES ); + asked = XP_TRUE; + } + } + } + LOG_RETURNF( "%s", boolToStr(asked)); + return asked; +} + XP_Bool server_do( ServerCtxt* server ) { @@ -1062,10 +1126,12 @@ server_do( ServerCtxt* server ) case XWSTATE_BEGIN: if ( server->nv.pendingRegistrations == 0 ) { /* all players on device */ - assignTilesToAll( server ); - SETSTATE( server, XWSTATE_INTURN ); - setTurn( server, 0 ); - moreToDo = XP_TRUE; + if ( !askedForTiles( server ) ) { + assignTilesToAll( server ); + SETSTATE( server, XWSTATE_INTURN ); + setTurn( server, 0 ); + moreToDo = XP_TRUE; + } } break; @@ -1359,8 +1425,8 @@ client_readInitialMessage( ServerCtxt* server, XWStreamCtxt* stream ) dict_unref( newDict ); /* new owner will have ref'd */ XP_ASSERT( !server->pool ); - pool = server->pool = pool_make( MPPARM_NOCOMMA(server->mpool) ); - pool_initFromDict( server->pool, model_getDictionary(model)); + makePoolOnce( server ); + pool = server->pool; /* now read the assigned tiles for each player from the stream, and remove them from the newly-created local pool. */ @@ -1755,6 +1821,23 @@ curTrayAsTexts( ServerCtxt* server, XP_U16 turn, const TrayTileSet* notInTray, *nUsedP = nUsed; } /* curTrayAsTexts */ +/** + * Return true (after calling util_informPickTiles()) IFF allowPickTiles is + * TRUE and the tile set passed in is NULL. If it doesn't contain as many + * tiles as are needed that's cool: server code will later interpret that as + * meaning the remainder should be assigned randomly as usual. + */ +XP_Bool +server_askPickTiles( ServerCtxt* server, XP_U16 turn, TrayTileSet* newTiles, + XP_U16 nToPick ) +{ + XP_Bool asked = newTiles == NULL && server->vol.gi->allowPickTiles; + if ( asked ) { + informNeedPickTiles( server, XP_FALSE, turn, nToPick ); + } + return asked; +} + /* Get tiles for one user. If picking is available, let user pick until * cancels. Otherwise, and after cancel, pick for 'im. */ @@ -1763,7 +1846,7 @@ fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch, const TrayTileSet* tradedTiles, TrayTileSet* resultTiles ) { XP_Bool ask; - XP_U16 nSoFar = 0; + XP_U16 nSoFar = resultTiles->nTiles; XP_U16 nLeft; PoolContext* pool = server->pool; TrayTileSet oneTile; @@ -1796,17 +1879,17 @@ fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch, #ifdef FEATURE_TRAY_EDIT /* good compiler would note ask==0, but... */ /* First ask until cancelled */ - for ( nSoFar = 0; ask && nSoFar < nToFetch; ) { + for ( ; ask && nSoFar < nToFetch; ) { const XP_UCHAR* texts[MAX_UNIQUE_TILES]; Tile tiles[MAX_UNIQUE_TILES]; XP_S16 chosen; XP_U16 nUsed = MAX_UNIQUE_TILES; - + // XP_ASSERT(0); /* should no longer happen!!! */ model_packTilesUtil( server->vol.model, pool, XP_TRUE, &nUsed, texts, tiles ); - chosen = util_userPickTileTray( server->vol.util, &pi, playerNum, - texts, nUsed ); + chosen = PICKER_PICKALL; /*util_userPickTileTray( server->vol.util, + &pi, playerNum, texts, nUsed );*/ if ( chosen == PICKER_PICKALL ) { ask = XP_FALSE; @@ -1846,6 +1929,18 @@ fetchTiles( ServerCtxt* server, XP_U16 playerNum, XP_U16 nToFetch, resultTiles->nTiles = (XP_U8)nSoFar; } /* fetchTiles */ +static void +makePoolOnce( ServerCtxt* server ) +{ + ModelCtxt* model = server->vol.model; + XP_ASSERT( model_getDictionary(model) != NULL ); + if ( server->pool == NULL ) { + server->pool = pool_make( MPPARM_NOCOMMA(server->mpool) ); + XP_STATUSF( "initing pool" ); + pool_initFromDict( server->pool, model_getDictionary(model)); + } +} + static void assignTilesToAll( ServerCtxt* server ) { @@ -1855,12 +1950,7 @@ assignTilesToAll( ServerCtxt* server ) XP_U16 nPlayers = server->vol.gi->nPlayers; XP_ASSERT( server->vol.gi->serverRole != SERVER_ISCLIENT ); - XP_ASSERT( model_getDictionary(model) != NULL ); - if ( server->pool == NULL ) { - server->pool = pool_make( MPPARM_NOCOMMA(server->mpool) ); - XP_STATUSF( "initing pool" ); - pool_initFromDict( server->pool, model_getDictionary(model)); - } + makePoolOnce( server ); XP_STATUSF( "assignTilesToAll" ); @@ -1871,9 +1961,11 @@ assignTilesToAll( ServerCtxt* server ) numAssigned = MAX_TRAY_TILES; } for ( ii = 0; ii < nPlayers; ++ii ) { - TrayTileSet newTiles; - fetchTiles( server, ii, numAssigned, NULL, &newTiles ); - model_assignPlayerTiles( model, ii, &newTiles ); + if ( 0 == model_getNumTilesInTray( model, ii ) ) { + TrayTileSet newTiles = {0}; + fetchTiles( server, ii, numAssigned, NULL, &newTiles ); + model_assignPlayerTiles( model, ii, &newTiles ); + } sortTilesIf( server, ii ); } @@ -2345,16 +2437,20 @@ reflectMove( ServerCtxt* server, XWStreamCtxt* stream ) * if it was legal -- but only if DISALLOW is set. */ XP_Bool -server_commitMove( ServerCtxt* server ) +server_commitMove( ServerCtxt* server, TrayTileSet* newTilesP ) { XP_S16 turn = server->nv.currentTurn; ModelCtxt* model = server->vol.model; CurGameInfo* gi = server->vol.gi; - TrayTileSet newTiles; + TrayTileSet newTiles = {0}; XP_U16 nTilesMoved; XP_Bool isLegalMove = XP_TRUE; XP_Bool isClient = gi->serverRole == SERVER_ISCLIENT; + if ( !!newTilesP ) { + newTiles = *newTilesP; + } + #ifdef DEBUG if ( LP_IS_ROBOT( &gi->players[turn] ) ) { XP_ASSERT( model_checkMoveLegal( model, turn, (XWStreamCtxt*)NULL, @@ -2408,9 +2504,13 @@ server_commitMove( ServerCtxt* server ) } /* server_commitMove */ XP_Bool -server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles ) +server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles, + TrayTileSet* newTilesP ) { - TrayTileSet newTiles; + TrayTileSet newTiles = {0}; + if ( !!newTilesP ) { + newTiles = *newTilesP; + } XP_U16 turn = server->nv.currentTurn; fetchTiles( server, turn, oldTiles->nTiles, oldTiles, &newTiles ); diff --git a/xwords4/common/server.h b/xwords4/common/server.h index 931dd9c8f..0ca3a6a0f 100644 --- a/xwords4/common/server.h +++ b/xwords4/common/server.h @@ -102,12 +102,18 @@ XP_U32 server_getLastMoveTime( const ServerCtxt* server ); /* Signed in case no dictionary available */ XP_S16 server_countTilesInPool( ServerCtxt* server ); +XP_Bool server_askPickTiles( ServerCtxt* server, XP_U16 player, + TrayTileSet* newTiles, XP_U16 nToPick ); +void server_tilesPicked( ServerCtxt* server, XP_U16 player, + const TrayTileSet* newTiles ); + XP_U16 server_getPendingRegs( const ServerCtxt* server ); XP_Bool server_do( ServerCtxt* server ); -XP_Bool server_commitMove( ServerCtxt* server ); -XP_Bool server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles ); +XP_Bool server_commitMove( ServerCtxt* server, TrayTileSet* newTiles ); +XP_Bool server_commitTrade( ServerCtxt* server, const TrayTileSet* oldTiles, + TrayTileSet* newTiles ); /* call this when user wants to end the game */ void server_endGame( ServerCtxt* server ); diff --git a/xwords4/common/tray.c b/xwords4/common/tray.c index 64b518c37..1eb926c07 100644 --- a/xwords4/common/tray.c +++ b/xwords4/common/tray.c @@ -460,7 +460,7 @@ handleActionInTray( BoardCtxt* board, XP_S16 index, XP_Bool onDivider ) } #endif } else if ( index == -(MAX_TRAY_TILES) ) { /* pending score tile */ - result = board_commitTurn( board, XP_FALSE, XP_FALSE ); + result = board_commitTurn( board, XP_FALSE, XP_FALSE, NULL ); #if defined XWFEATURE_TRAYUNDO_ALL } else if ( index < 0 ) { /* other empty area */ /* it better be true */ diff --git a/xwords4/common/util.h b/xwords4/common/util.h index abfaff6b5..ea4888d79 100644 --- a/xwords4/common/util.h +++ b/xwords4/common/util.h @@ -101,14 +101,14 @@ typedef struct UtilVtable { void (*m_util_notifyMove)( XW_UtilCtxt* uc, XWStreamCtxt* stream ); void (*m_util_notifyTrade)( XW_UtilCtxt* uc, const XP_UCHAR** tiles, XP_U16 nTiles ); - /* return of < 0 means computer should pick */ void (*m_util_notifyPickTileBlank)( XW_UtilCtxt* uc, XP_U16 playerNum, XP_U16 col, XP_U16 row, const XP_UCHAR** tileFaces, XP_U16 nTiles ); - XP_S16 (*m_util_userPickTileTray)( XW_UtilCtxt* uc, const PickInfo* pi, - XP_U16 playerNum, - const XP_UCHAR** texts, XP_U16 nTiles ); + void (*m_util_informNeedPickTiles)( XW_UtilCtxt* uc, XP_Bool isInitial, + XP_U16 player, XP_U16 nToPick, + XP_U16 nFaces, const XP_UCHAR** faces, + const XP_U16* counts ); void (*m_util_informNeedPassword)( XW_UtilCtxt* uc, XP_U16 playerNum, const XP_UCHAR* name ); @@ -236,9 +236,11 @@ struct XW_UtilCtxt { (uc)->vtable->m_util_notifyTrade((uc), (tx), (nt)) #define util_notifyPickTileBlank( uc, c, r, n, tx, nt ) \ - (uc)->vtable->m_util_notifyPickTileBlank( (uc), (c), (r), (n), (tx), (nt) ) -#define util_userPickTileTray( uc, w, n, tx, nt ) \ - (uc)->vtable->m_util_userPickTileTray( (uc), (w), (n), (tx), (nt) ) + (uc)->vtable->m_util_notifyPickTileBlank( (uc), (c), (r), (n), (tx), (nt) ) + +#define util_informNeedPickTiles( uc, ii, pl, np, nt, fc, cn ) \ + (uc)->vtable->m_util_informNeedPickTiles( (uc), (ii), (pl), (np), (nt), (fc), (cn) ) + #define util_informNeedPassword( uc, pn, n ) \ (uc)->vtable->m_util_informNeedPassword( (uc), (pn), (n) ) diff --git a/xwords4/linux/cursesmain.c b/xwords4/linux/cursesmain.c index 9432f9080..9416e6b23 100644 --- a/xwords4/linux/cursesmain.c +++ b/xwords4/linux/cursesmain.c @@ -243,22 +243,26 @@ curses_util_notifyPickTileBlank( XW_UtilCtxt* uc, XP_U16 playerNum, // return index; } /* util_userPickTile */ -static XP_S16 -curses_util_userPickTileTray( XW_UtilCtxt* uc, const PickInfo* XP_UNUSED(pi), - XP_U16 playerNum, const XP_UCHAR** texts, - XP_U16 nTiles ) +static void +curses_util_informNeedPickTiles( XW_UtilCtxt* XP_UNUSED(uc), + XP_Bool XP_UNUSED(isInitial), + XP_U16 XP_UNUSED(player), + XP_U16 XP_UNUSED(nToPick), + XP_U16 XP_UNUSED(nFaces), + const XP_UCHAR** XP_UNUSED(faces), + const XP_U16* XP_UNUSED(counts) ) { - CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; - char query[128]; - XP_S16 index; - char* playerName = globals->cGlobals.gi->players[playerNum].name; + /* CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure; */ + /* char query[128]; */ + /* XP_S16 index; */ + /* char* playerName = globals->cGlobals.gi->players[playerNum].name; */ - snprintf( query, sizeof(query), - "Pick tile for %s! (Tab or type letter to select " - "then hit .)", playerName ); + /* snprintf( query, sizeof(query), */ + /* "Pick tile for %s! (Tab or type letter to select " */ + /* "then hit .)", playerName ); */ - index = curses_askLetter( globals, query, texts, nTiles ); - return index; + /* index = curses_askLetter( globals, query, texts, nTiles ); */ + /* return index; */ } /* util_userPickTile */ static void @@ -620,7 +624,7 @@ static XP_Bool handleCommit( CursesAppGlobals* globals ) { globals->doDraw = board_commitTurn( globals->cGlobals.game.board, - XP_FALSE, XP_FALSE ); + XP_FALSE, XP_FALSE, NULL ); return XP_TRUE; } /* handleCommit */ @@ -1456,7 +1460,7 @@ setupCursesUtilCallbacks( CursesAppGlobals* globals, XW_UtilCtxt* util ) util->vtable->m_util_notifyMove = curses_util_notifyMove; util->vtable->m_util_notifyTrade = curses_util_notifyTrade; util->vtable->m_util_notifyPickTileBlank = curses_util_notifyPickTileBlank; - util->vtable->m_util_userPickTileTray = curses_util_userPickTileTray; + util->vtable->m_util_informNeedPickTiles = curses_util_informNeedPickTiles; util->vtable->m_util_trayHiddenChange = curses_util_trayHiddenChange; util->vtable->m_util_informMove = curses_util_informMove; util->vtable->m_util_informUndo = curses_util_informUndo; diff --git a/xwords4/linux/gtkboard.c b/xwords4/linux/gtkboard.c index ecb5f6385..13b3b8fed 100644 --- a/xwords4/linux/gtkboard.c +++ b/xwords4/linux/gtkboard.c @@ -1445,7 +1445,8 @@ handle_trade_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) static void handle_done_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) { - if ( board_commitTurn( globals->cGlobals.game.board, XP_FALSE, XP_FALSE ) ) { + if ( board_commitTurn( globals->cGlobals.game.board, XP_FALSE, + XP_FALSE, NULL ) ) { board_draw( globals->cGlobals.game.board ); disenable_buttons( globals ); } @@ -1543,7 +1544,8 @@ handle_hide_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) static void handle_commit_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) { - if ( board_commitTurn( globals->cGlobals.game.board, XP_FALSE, XP_FALSE ) ) { + if ( board_commitTurn( globals->cGlobals.game.board, XP_FALSE, + XP_FALSE, NULL ) ) { board_draw( globals->cGlobals.game.board ); } } /* handle_commit_button */ @@ -1693,7 +1695,7 @@ ask_blank( gpointer data ) XP_UCHAR* name = globals->cGlobals.gi->players[cGlobals->selPlayer].name; XP_S16 result = gtkletterask( NULL, XP_FALSE, name, - cGlobals->nTiles, cGlobals->tiles ); + cGlobals->nTiles, cGlobals->tiles, NULL ); for ( int ii = 0; ii < cGlobals->nTiles; ++ii ) { g_free( (gpointer)cGlobals->tiles[ii] ); @@ -1725,18 +1727,78 @@ gtk_util_notifyPickTileBlank( XW_UtilCtxt* uc, XP_U16 playerNum, XP_U16 col, (void)g_idle_add( ask_blank, globals ); } -static XP_S16 -gtk_util_userPickTileTray( XW_UtilCtxt* uc, const PickInfo* pi, - XP_U16 playerNum, const XP_UCHAR** texts, - XP_U16 nTiles ) +static gint +ask_tiles( gpointer data ) { - XP_S16 chosen; - GtkGameGlobals* globals = (GtkGameGlobals*)uc->closure; - XP_UCHAR* name = globals->cGlobals.gi->players[playerNum].name; + GtkGameGlobals* globals = (GtkGameGlobals*)data; + CommonGlobals* cGlobals = &globals->cGlobals; - chosen = gtkletterask( pi, XP_TRUE, name, nTiles, texts ); - return chosen; -} /* gtk_util_userPickTile */ + TrayTileSet newTiles = {0}; + XP_UCHAR* name = cGlobals->gi->players[cGlobals->selPlayer].name; + for ( XP_Bool done = XP_FALSE; !done; ) { + XP_S16 picked = gtkletterask( &newTiles, XP_TRUE, name, + cGlobals->nTiles, cGlobals->tiles, + cGlobals->tileCounts ); + switch ( picked ) { + case PICKER_PICKALL: + done = XP_TRUE; + break; + case PICKER_BACKUP: + if ( newTiles.nTiles > 0 ) { + Tile backed = newTiles.tiles[--newTiles.nTiles]; + ++cGlobals->tileCounts[backed]; + } + break; + default: + XP_ASSERT( picked >= 0 && picked < cGlobals->nTiles ); + --cGlobals->tileCounts[picked]; + newTiles.tiles[newTiles.nTiles++] = picked; + done = newTiles.nTiles == cGlobals->nToPick; + break; + } + } + + for ( int ii = 0; ii < cGlobals->nTiles; ++ii ) { + g_free( (gpointer)cGlobals->tiles[ii] ); + } + + BoardCtxt* board = cGlobals->game.board; + XP_Bool draw = XP_TRUE; + if ( cGlobals->pickIsInitial ) { + server_tilesPicked( cGlobals->game.server, cGlobals->selPlayer, + &newTiles ); + } else { + draw = board_commitTurn( cGlobals->game.board, XP_TRUE, XP_TRUE, + &newTiles ); + } + + if ( draw ) { + board_draw( board ); + } + + return 0; +} + +static void +gtk_util_informNeedPickTiles( XW_UtilCtxt* uc, XP_Bool isInitial, + XP_U16 player, XP_U16 nToPick, + XP_U16 nFaces, const XP_UCHAR** faces, + const XP_U16* counts ) +{ + GtkGameGlobals* globals = (GtkGameGlobals*)uc->closure; + CommonGlobals* cGlobals = &globals->cGlobals; + cGlobals->selPlayer = player; + cGlobals->pickIsInitial = isInitial; + + cGlobals->nToPick = nToPick; + cGlobals->nTiles = nFaces; + for ( int ii = 0; ii < nFaces; ++ii ) { + cGlobals->tiles[ii] = g_strdup( faces[ii] ); + cGlobals->tileCounts[ii] = counts[ii]; + } + + (void)g_idle_add( ask_tiles, globals ); +} /* gtk_util_informNeedPickTiles */ static gint ask_password( gpointer data ) @@ -2125,7 +2187,7 @@ ask_bad_words( gpointer data ) if ( GTK_RESPONSE_YES == gtkask( globals->window, cGlobals->question, GTK_BUTTONS_YES_NO, NULL ) ) { - board_commitTurn( cGlobals->game.board, XP_TRUE, XP_FALSE ); + board_commitTurn( cGlobals->game.board, XP_TRUE, XP_FALSE, NULL ); } return 0; } @@ -2275,7 +2337,7 @@ ask_move( gpointer data ) gint chosen = gtkask( globals->window, cGlobals->question, buttons, NULL ); if ( GTK_RESPONSE_OK == chosen || chosen == GTK_RESPONSE_YES ) { BoardCtxt* board = cGlobals->game.board; - if ( board_commitTurn( board, XP_TRUE, XP_TRUE ) ) { + if ( board_commitTurn( board, XP_TRUE, XP_TRUE, NULL ) ) { board_draw( board ); } } @@ -2314,7 +2376,7 @@ ask_trade( gpointer data ) cGlobals->question, GTK_BUTTONS_YES_NO, NULL ) ) { BoardCtxt* board = cGlobals->game.board; - if ( board_commitTurn( board, XP_TRUE, XP_TRUE ) ) { + if ( board_commitTurn( board, XP_TRUE, XP_TRUE, NULL ) ) { board_draw( board ); } } @@ -2466,7 +2528,7 @@ setupGtkUtilCallbacks( GtkGameGlobals* globals, XW_UtilCtxt* util ) util->vtable->m_util_notifyTrade = gtk_util_notifyTrade; util->vtable->m_util_getVTManager = gtk_util_getVTManager; util->vtable->m_util_notifyPickTileBlank = gtk_util_notifyPickTileBlank; - util->vtable->m_util_userPickTileTray = gtk_util_userPickTileTray; + util->vtable->m_util_informNeedPickTiles = gtk_util_informNeedPickTiles; util->vtable->m_util_informNeedPassword = gtk_util_informNeedPassword; util->vtable->m_util_trayHiddenChange = gtk_util_trayHiddenChange; util->vtable->m_util_yOffsetChange = gtk_util_yOffsetChange; diff --git a/xwords4/linux/gtkletterask.c b/xwords4/linux/gtkletterask.c index 70546412c..c2d66cbdf 100644 --- a/xwords4/linux/gtkletterask.c +++ b/xwords4/linux/gtkletterask.c @@ -42,8 +42,8 @@ abort_button_event( GtkWidget* XP_UNUSED(widget), gpointer XP_UNUSED(closure) ) #define BUTTONS_PER_ROW 13 XP_S16 -gtkletterask( const PickInfo* pi, XP_Bool forTray, const XP_UCHAR* name, - XP_U16 nTiles, const XP_UCHAR** texts ) +gtkletterask( const TrayTileSet* curPick, XP_Bool forTray, const XP_UCHAR* name, + XP_U16 nTiles, const XP_UCHAR** texts, const XP_U16* counts ) { GtkWidget* dialog; GtkWidget* label; @@ -70,7 +70,9 @@ gtkletterask( const PickInfo* pi, XP_Bool forTray, const XP_UCHAR* name, gtk_box_pack_start( GTK_BOX(hbox), button, FALSE, TRUE, 0 ); g_signal_connect( button, "clicked", G_CALLBACK(set_bool_and_quit), &results[ii] ); - gtk_widget_show( button ); + + /* disable if we don't have any tiles! */ + gtk_widget_set_sensitive( button, !counts || counts[ii] > 0 ); if ( ii+1 == nTiles || (ii % BUTTONS_PER_ROW == 0) ) { gtk_widget_show( hbox ); @@ -118,9 +120,10 @@ gtkletterask( const PickInfo* pi, XP_Bool forTray, const XP_UCHAR* name, char curTilesBuf[64]; int len = snprintf( curTilesBuf, sizeof(curTilesBuf), "%s", "Tiles so far: " ); - for ( ii = 0; ii < pi->nCurTiles; ++ii ) { + for ( ii = 0; ii < curPick->nTiles; ++ii ) { + Tile tile = curPick->tiles[ii]; len += snprintf( &curTilesBuf[len], sizeof(curTilesBuf) - len, "%s ", - pi->curTiles[ii] ); + texts[tile] ); } GtkWidget* curTilesLabel = gtk_label_new( curTilesBuf ); diff --git a/xwords4/linux/gtkletterask.h b/xwords4/linux/gtkletterask.h index 474279a97..a875f8f7e 100644 --- a/xwords4/linux/gtkletterask.h +++ b/xwords4/linux/gtkletterask.h @@ -26,9 +26,10 @@ #include "gtkboard.h" -XP_S16 gtkletterask( const PickInfo* pi, XP_Bool forTray, +XP_S16 gtkletterask( const TrayTileSet* curPick, XP_Bool forTray, const XP_UCHAR* name, - XP_U16 nTiles, const XP_UCHAR** texts ); + XP_U16 nTiles, const XP_UCHAR** texts, + const XP_U16* counts ); #endif /* _GTKLETTERASK_H_ */ diff --git a/xwords4/linux/main.h b/xwords4/linux/main.h index 9168c0c6b..93605dc93 100644 --- a/xwords4/linux/main.h +++ b/xwords4/linux/main.h @@ -220,10 +220,13 @@ struct CommonGlobals { char question[256*4]; const XP_UCHAR* askPassName; XP_U16 nTiles; + XP_U16 nToPick; XP_U16 blankCol; XP_U16 blankRow; + XP_Bool pickIsInitial; const XP_UCHAR* tiles[MAX_UNIQUE_TILES]; + XP_U16 tileCounts[MAX_UNIQUE_TILES]; #ifdef XWFEATURE_RELAY int relaySocket; /* tcp connection to relay */