new feature to remember acceptable phonies

Present to user option to remember "phonies", words played but not
in the current wordlist. They're stored by language (not by wordlist)
and not available for hints or the robot to use (as that would require
incorporating them into a wordlist, a much larger change.)
This commit is contained in:
Eric House 2024-04-05 11:17:12 -07:00
parent 182ce9b7f0
commit 8be9d18287
30 changed files with 860 additions and 126 deletions

View file

@ -39,6 +39,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
@ -315,15 +316,25 @@ public class BoardDelegate extends DelegateBase
}
break;
case NOTIFY_BADWORDS: {
case ASK_BADWORDS: {
LinearLayout rpLayout =
(LinearLayout)inflate( R.layout.phonies_found );
TextView tv = (TextView)rpLayout.findViewById( R.id.message );
tv.setText( (String)params[0] );
final int badWordsKey = (int)params[1];
lstnr = new OnClickListener() {
@Override
public void onClick( DialogInterface dlg, int bx ) {
handleViaThread( JNICmd.CMD_COMMIT, true, false );
CheckBox cb = (CheckBox)rpLayout
.findViewById(R.id.remember_phonies_check);
handleViaThread( JNICmd.CMD_COMMIT, true, false,
cb.isChecked() ? badWordsKey : 0 );
}
};
dialog = ab.setTitle( R.string.phonies_found_title )
.setMessage( (String)params[0] )
.setView( rpLayout )
.setPositiveButton( R.string.button_yes, lstnr )
.setNegativeButton( android.R.string.cancel, null )
.create();
@ -2187,7 +2198,7 @@ public class BoardDelegate extends DelegateBase
@Override
public void notifyIllegalWords( String dict, String[] words, int turn,
boolean turnLost )
boolean turnLost, int badWordsKey )
{
String wordsString = TextUtils.join( ", ", words );
String message =
@ -2198,7 +2209,7 @@ public class BoardDelegate extends DelegateBase
message + getString( R.string.badwords_lost ) );
} else {
String msg = message + getString( R.string.badwords_accept );
showDialogFragment( DlgID.NOTIFY_BADWORDS, msg );
showDialogFragment( DlgID.ASK_BADWORDS, msg, badWordsKey );
}
}

View file

@ -55,7 +55,7 @@ public enum DlgID {
WARN_NODICT_INVITED, // when responding to invitation
WARN_NODICT_SUBST, // when a substitution will be possible/suggested
DLG_BADWORDS,
NOTIFY_BADWORDS,
ASK_BADWORDS,
QUERY_MOVE,
QUERY_TRADE,
ASK_PASSWORD,

View file

@ -1817,6 +1817,9 @@ public class GamesListDelegate extends ListDelegateBase
enable = nothingSelected && Utils.isGooglePlayApp( m_activity );
Utils.setItemVisible( menu, R.id.games_menu_rateme, enable );
enable = BuildConfig.NON_RELEASE && XwJNI.dvc_haveLegalPhonies();
Utils.setItemVisible( menu, R.id.games_submenu_legalPhonies, enable );
enable = nothingSelected && XWPrefs.getStudyEnabled( m_activity );
Utils.setItemVisible( menu, R.id.games_menu_study, enable );
@ -1906,6 +1909,15 @@ public class GamesListDelegate extends ListDelegateBase
}
break;
case R.id.games_menu_clearLPs:
int nDeleted = XwJNI.dvc_clearLegalPhonies();
Utils.showToast( m_activity, R.string.cleared_lps_fmt, nDeleted );
break;
case R.id.games_menu_listLPs:
String txt = XwJNI.dvc_listLegalPhonies();
makeOkOnlyBuilder( txt ).show();
break;
case R.id.games_menu_study:
StudyListDelegate.launchOrAlert( getDelegator(), this );
break;

View file

@ -573,13 +573,22 @@ public class JNIThread extends Thread implements AutoCloseable {
break;
case CMD_COMMIT:
boolean phoniesConfirmed = args.length < 1
? false : (Boolean)args[0];
boolean turnConfirmed = args.length < 2
? false : (Boolean)args[1];
int[] newTiles = args.length < 3 ? null : (int[])args[2];
boolean phoniesConfirmed =
args.length >= 1 ? (Boolean)args[0] : false;
boolean turnConfirmed =
args.length >= 2 ? (Boolean)args[1] : false;
int[] newTiles = null;
int badWordsKey = 0;
if ( args.length >= 3 ) {
Object obj = args[2];
if ( obj instanceof Integer ) {
badWordsKey = (Integer)obj;
} else if ( obj instanceof int[] ) {
newTiles = (int[])obj;
}
}
draw = XwJNI.board_commitTurn( m_jniGamePtr, phoniesConfirmed,
turnConfirmed, newTiles );
badWordsKey, turnConfirmed, newTiles );
break;
case CMD_TILES_PICKED:

View file

@ -117,7 +117,7 @@ public interface UtilCtxt {
//void yOffsetChange( int maxOffset, int oldOffset, int newOffset );
void notifyIllegalWords( String dict, String[] words, int turn,
boolean turnLost );
boolean turnLost, int badWordsKey );
void showChat( String msg, int fromPlayer, int tsSeconds );

View file

@ -244,7 +244,7 @@ public class UtilCtxtImpl implements UtilCtxt {
@Override
public void notifyIllegalWords( String dict, String[] words, int turn,
boolean turnLost )
boolean turnLost, int badWordsKey )
{
subclassOverride( "notifyIllegalWords" );
}

View file

@ -189,6 +189,21 @@ public class XwJNI {
dvc_onWebSendResult( getJNI().m_ptrGlobals, resultKey, succeeded, result );
}
public static boolean dvc_haveLegalPhonies()
{
return dvc_haveLegalPhonies( getJNI().m_ptrGlobals );
}
public static String dvc_listLegalPhonies()
{
return dvc_listLegalPhonies( getJNI().m_ptrGlobals );
}
public static int dvc_clearLegalPhonies()
{
return dvc_clearLegalPhonies( getJNI().m_ptrGlobals );
}
public static boolean hasKnownPlayers()
{
String[] players = kplr_getPlayers();
@ -476,6 +491,7 @@ public class XwJNI {
public static native boolean board_showTray( GamePtr gamePtr );
public static native boolean board_commitTurn( GamePtr gamePtr,
boolean phoniesConfirmed,
int badWordsKey,
boolean turnConfirmed,
int[] newTiles );
@ -802,6 +818,9 @@ public class XwJNI {
private static native void dvc_onWebSendResult( long jniState, int resultKey,
boolean succeeded,
String result );
private static native boolean dvc_haveLegalPhonies( long jniState );
private static native String dvc_listLegalPhonies( long jniState );
private static native int dvc_clearLegalPhonies( long jniState );
private static native String[] kplr_getPlayers( long jniState, boolean byDate );
private static native boolean kplr_renamePlayer( long jniState, String oldName,
String newName );

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
>
<TextView android:id="@+id/message"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
<CheckBox android:id="@+id/remember_phonies_check"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/remember_phonies_label"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
</LinearLayout>

View file

@ -33,6 +33,19 @@
android:icon="@drawable/dict__gen"
android:showAsAction="ifRoom"
/>
<item android:id="@+id/games_submenu_legalPhonies"
android:title="@string/gamel_menu_legalPhonies"
>
<menu>
<item android:id="@+id/games_menu_clearLPs"
android:title="@string/gamel_menu_clearLPs"
/>
<item android:id="@+id/games_menu_listLPs"
android:title="@string/gamel_menu_listLPs"
/>
</menu>
</item>
<item android:id="@+id/games_menu_study"
android:title="@string/gamel_menu_study"
/>

View file

@ -15,4 +15,12 @@
<string name="url_scheme_http">Force http</string>
<string name="url_scheme_https">Force https</string>
<string name="gamel_menu_legalPhonies">Legal phonies</string>
<string name="gamel_menu_clearLPs">Clear all</string>
<string name="gamel_menu_listLPs">Show all</string>
<string name="remember_phonies_label">Accept from now on</string>
<string name="cleared_lps_fmt">Cleared %d legal phonies</string>
</resources>

View file

@ -555,19 +555,22 @@ and_dutil_remove( XW_DUtilCtxt* duc, const XP_UCHAR* keys[] )
#endif
static void
and_util_notifyIllegalWords( XW_UtilCtxt* uc, XWEnv xwe, BadWordInfo* bwi,
XP_U16 turn, XP_Bool turnLost )
and_util_notifyIllegalWords( XW_UtilCtxt* uc, XWEnv xwe,
const BadWordInfo* bwi,
const XP_UCHAR* dictName,
XP_U16 turn, XP_Bool turnLost,
XP_U32 badWordsKey )
{
UTIL_CBK_HEADER("notifyIllegalWords",
"(Ljava/lang/String;[Ljava/lang/String;IZ)V" );
"(Ljava/lang/String;[Ljava/lang/String;IZI)V" );
XP_ASSERT( bwi->nWords > 0 );
if ( bwi->nWords > 0 ) {
jobjectArray jwords = makeStringArray( env, bwi->nWords,
(const XP_UCHAR**)bwi->words );
XP_ASSERT( !!bwi->dictName );
jstring jname = (*env)->NewStringUTF( env, bwi->dictName );
XP_ASSERT( !!dictName );
jstring jname = (*env)->NewStringUTF( env, dictName );
(*env)->CallVoidMethod( env, util->jutil, mid,
jname, jwords, turn, turnLost );
jname, jwords, turn, turnLost, badWordsKey );
deleteLocalRefs( env, jwords, jname, DELETE_NO_REF );
}
UTIL_CBK_TAIL();

View file

@ -794,6 +794,50 @@ Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1onWebSendResult
DVC_HEADER_END();
}
JNIEXPORT jboolean JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1haveLegalPhonies
( JNIEnv* env, jclass C, jlong jniGlobalPtr )
{
jboolean jresult;
DVC_HEADER(jniGlobalPtr);
jresult = dvc_haveLegalPhonies( globalState->dutil, env );
DVC_HEADER_END();
return jresult;
}
#ifdef DEBUG
JNIEXPORT jstring JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1listLegalPhonies
( JNIEnv* env, jclass C, jlong jniGlobalPtr )
{
jstring jresult;
DVC_HEADER(jniGlobalPtr);
#ifdef MEM_DEBUG
MemPoolCtx* mpool = GETMPOOL( globalState );
#endif
XWStreamCtxt* stream = mem_stream_make( MPPARM(mpool) globalState->vtMgr,
NULL, 0, NULL, NULL );
dvc_listLegalPhonies( globalState->dutil, env, stream );
jresult = streamToJString( env, stream );
stream_destroy( stream );
DVC_HEADER_END();
return jresult;
}
#endif
JNIEXPORT jint JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1clearLegalPhonies
( JNIEnv* env, jclass C, jlong jniGlobalPtr )
{
jint result;
DVC_HEADER(jniGlobalPtr);
result = dvc_clearLegalPhonies( globalState->dutil, env );
DVC_HEADER_END();
return result;
}
# ifdef XWFEATURE_KNOWNPLAYERS
JNIEXPORT jobjectArray JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_kplr_1getPlayers
@ -1925,7 +1969,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_board_1setBlankValue
JNIEXPORT jboolean JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_board_1commitTurn
( JNIEnv* env, jclass C, GamePtrType gamePtr, jboolean phoniesConfirmed,
jboolean turnConfirmed, jintArray jNewTiles )
jint badWordsKey, jboolean turnConfirmed, jintArray jNewTiles )
{
jboolean result;
XWJNI_START(gamePtr);
@ -1936,8 +1980,11 @@ Java_org_eehouse_android_xw4_jni_XwJNI_board_1commitTurn
tilesArrayToTileSet( env, jNewTiles, &newTiles );
newTilesP = &newTiles;
}
result = board_commitTurn( state->game.board, env, phoniesConfirmed,
PhoniesConf pc = {.confirmed = phoniesConfirmed,
.key = badWordsKey,
};
result = board_commitTurn( state->game.board, env,
phoniesConfirmed ? &pc : NULL,
turnConfirmed, newTilesP );
XWJNI_END();
return result;

View file

@ -54,6 +54,7 @@
#include "comms.h" /* for CHANNEL_NONE */
#include "dictnry.h"
#include "draw.h"
#include "device.h"
#include "engine.h"
#include "util.h"
#include "mempool.h" /* debug only */
@ -323,7 +324,6 @@ board_getDraw( const BoardCtxt* board )
void
board_writeToStream( const BoardCtxt* board, XWStreamCtxt* stream )
{
XP_U16 nPlayers, ii;
XP_U16 nColsNBits;
#ifdef STREAM_VERS_BIGBOARD
nColsNBits = 16 > model_numCols(board->model) ? NUMCOLS_NBITS_4
@ -347,9 +347,9 @@ board_writeToStream( const BoardCtxt* board, XWStreamCtxt* stream )
#endif
XP_ASSERT( !!board->server );
nPlayers = board->gi->nPlayers;
XP_U16 nPlayers = board->gi->nPlayers;
for ( ii = 0; ii < nPlayers; ++ii ) {
for ( int ii = 0; ii < nPlayers; ++ii ) {
const PerTurnInfo* pti = &board->pti[ii];
const BoardArrow* arrow = &pti->boardArrow;
stream_putBits( stream, nColsNBits, arrow->col );
@ -1043,12 +1043,6 @@ hideMiniWindow( BoardCtxt* board, XP_Bool destroy, MiniWindowType winType )
#endif
#endif
typedef struct _BadWordList {
BadWordInfo bwi;
XP_UCHAR buf[256];
XP_U16 index;
} BadWordList;
static void
saveBadWords( const WNParams* wnp, void* closure )
{
@ -1076,7 +1070,8 @@ boardNotifyTrade( BoardCtxt* board, XWEnv xwe, const TrayTileSet* tiles )
}
XP_Bool
board_commitTurn( BoardCtxt* board, XWEnv xwe, XP_Bool phoniesConfirmed,
board_commitTurn( BoardCtxt* board, XWEnv xwe,
const PhoniesConf* pconf,
XP_Bool turnConfirmed /* includes trade */,
TrayTileSet* newTiles )
{
@ -1084,6 +1079,7 @@ board_commitTurn( BoardCtxt* board, XWEnv xwe, XP_Bool phoniesConfirmed,
const XP_S16 turn = server_getCurrentTurn( board->server, NULL );
const XP_U16 selPlayer = board->selPlayer;
ModelCtxt* model = board->model;
const XP_Bool phoniesConfirmed = !!pconf && pconf->confirmed;
if ( board->gameOver || turn < 0 ) {
/* do nothing */
@ -1093,6 +1089,22 @@ board_commitTurn( BoardCtxt* board, XWEnv xwe, XP_Bool phoniesConfirmed,
/* game's over but still undoable so turn hasn't changed; do
nothing */
} else if ( phoniesConfirmed || turnConfirmed || checkRevealTray( board, xwe ) ) {
const DictionaryCtxt* dict = model_getPlayerDict( model, selPlayer );
BadWordList* bwl = &board->bwl;
if ( phoniesConfirmed && 0 != pconf->key ) {
XP_ASSERT( bwl->key == pconf->key );
if ( bwl->key == pconf->key ) {
const BadWordInfo* bwi = &bwl->bwi;
const XP_UCHAR* isoCode = dict_getISOCode( dict );
for ( int ii = 0; ii < bwi->nWords; ++ii ) {
dvc_addLegalPhony( board->dutil, xwe, isoCode,
bwi->words[ii] );
}
}
}
PerTurnInfo* pti = &board->pti[selPlayer];
if ( pti->tradeInProgress ) {
TileBit traySelBits = pti->traySelBits;
@ -1127,8 +1139,6 @@ board_commitTurn( BoardCtxt* board, XWEnv xwe, XP_Bool phoniesConfirmed,
} else {
XWStreamCtxt* stream = NULL;
XP_Bool legal = turnConfirmed;
BadWordList bwl;
XP_MEMSET( &bwl, 0, sizeof(bwl) );
if ( !legal ) {
stream = mem_stream_make_raw( MPPARM(board->mpool)
@ -1136,23 +1146,26 @@ board_commitTurn( BoardCtxt* board, XWEnv xwe, XP_Bool phoniesConfirmed,
XP_U16 stringCode = board->gi->inDuplicateMode
? STR_SUBMIT_CONFIRM : STR_COMMIT_CONFIRM;
const XP_UCHAR* str = dutil_getUserString( board->dutil, xwe, stringCode );
const XP_UCHAR* str = dutil_getUserString( board->dutil, xwe,
stringCode );
stream_catString( stream, str );
XP_Bool warn = board->util->gameInfo->phoniesAction == PHONIES_WARN;
WordNotifierInfo info;
if ( warn ) {
XP_MEMSET( bwl, 0, sizeof(*bwl) );
bwl->key = 0x7FFFFFFF & XP_RANDOM(); /* clear high bit so can be signed */
info.proc = saveBadWords;
info.closure = &bwl;
info.closure = bwl;
}
legal = model_checkMoveLegal( model, xwe, selPlayer, stream,
warn? &info:(WordNotifierInfo*)NULL);
}
if ( 0 < bwl.bwi.nWords && !phoniesConfirmed ) {
bwl.bwi.dictName =
dict_getShortName( model_getPlayerDict( model, selPlayer ) );
util_notifyIllegalWords( board->util, xwe, &bwl.bwi, selPlayer, XP_FALSE );
if ( 0 < bwl->bwi.nWords && !phoniesConfirmed ) {
const XP_UCHAR* dictName = dict_getShortName( dict );
util_notifyIllegalWords( board->util, xwe, &bwl->bwi, dictName,
selPlayer, XP_FALSE, bwl->key );
} else if ( legal ) {
/* Hide the tray so no peeking. Leave it hidden even if user
cancels as otherwise another player could get around

View file

@ -176,7 +176,11 @@ XP_Bool board_setBlankValue( BoardCtxt* board, XP_U16 XP_UNUSED(player),
void board_resetEngine( BoardCtxt* board );
XP_Bool board_commitTurn( BoardCtxt* board, XWEnv xwe, XP_Bool phoniesConfirmed,
typedef struct _PhoniesConf {
XP_Bool confirmed;
XP_U32 key;
} PhoniesConf;
XP_Bool board_commitTurn( BoardCtxt* board, XWEnv xwe, const PhoniesConf* pc,
XP_Bool turnConfirmed, TrayTileSet* newTiles );
void board_pushTimerSave( BoardCtxt* board, XWEnv xwe );

View file

@ -25,6 +25,7 @@
#include "board.h"
#include "engine.h"
#include "mempool.h" /* debug only */
#include "util.h"
#ifdef CPLUS
extern "C" {
@ -133,6 +134,13 @@ typedef struct _ScrollData {
} ScrollData;
typedef enum { SCROLL_H, SCROLL_V, N_SCROLL_DIMS } SDIndex;
typedef struct _BadWordList {
BadWordInfo bwi;
XP_UCHAR buf[256];
XP_U16 index;
XP_U32 key;
} BadWordList;
struct BoardCtxt {
/* BoardVTable* vtable; */
ModelCtxt* model;
@ -144,6 +152,8 @@ struct BoardCtxt {
struct CurGameInfo* gi;
ScrollData sd[N_SCROLL_DIMS];
BadWordList bwl;
XP_U16 preHideYOffset;
XP_U16 prevYScrollOffset; /* represents where the last draw took place;
used to see if bit scrolling can be used */

View file

@ -256,6 +256,7 @@ typedef enum _TileValueType {
#define SUFFIX_PARTIALS "partials"
#define SUFFIX_NEXTID "nextID"
#define SUFFIX_DEVSTATE "devState"
#define SUFFIX_LEGAL_PHONIES "legalPhonies"
#define SUFFIX_MQTT_DEVID "mqtt_devid_key"
#define SUFFIX_KNOWN_PLAYERS "known_players_key_dev1"
@ -264,6 +265,7 @@ typedef enum _TileValueType {
#define KEY_PARTIALS FULL_KEY(SUFFIX_PARTIALS)
#define KEY_NEXTID FULL_KEY(SUFFIX_NEXTID)
#define KEY_DEVSTATE FULL_KEY(SUFFIX_DEVSTATE)
#define KEY_LEGAL_PHONIES FULL_KEY(SUFFIX_LEGAL_PHONIES)
#define MQTT_DEVID_KEY FULL_KEY(SUFFIX_MQTT_DEVID)
#define KNOWN_PLAYERS_KEY FULL_KEY(SUFFIX_KNOWN_PLAYERS)

View file

@ -39,6 +39,8 @@
#define LAST_REG_KEY FULL_KEY("device_last_reg")
#define KEY_GITREV FULL_KEY("device_gitrev")
#define PD_VERSION_1 2
static XWStreamCtxt*
mkStream( XW_DUtilCtxt* dutil )
{
@ -55,11 +57,25 @@ typedef struct WSData {
WSR code;
} WSData;
typedef struct _PhoniesDataStrs {
DLHead links;
XP_UCHAR* phony;
} PhoniesDataStrs;
typedef struct _PhoniesDataCodes {
DLHead links;
XP_UCHAR* isoCode;
PhoniesDataStrs* head;
} PhoniesDataCodes;
typedef struct _DevCtxt {
XP_U16 devCount;
WSData* webSendData;
XP_U32 mWebSendKey;
pthread_mutex_t webSendMutex;
PhoniesDataCodes* pd;
#ifdef DEBUG
XP_U32 magic;
#endif
@ -706,6 +722,295 @@ dvc_onWebSendResult( XW_DUtilCtxt* dutil, XWEnv xwe, XP_U32 resultKey,
}
}
typedef struct _FindIsoState {
const XP_UCHAR* isoCode;
PhoniesDataCodes* found;
} FindIsoState;
static ForEachAct
findIsoProc( const DLHead* elem, void* closure )
{
ForEachAct result = FEA_OK;
PhoniesDataCodes* pdc = (PhoniesDataCodes*)elem;
FindIsoState* fis = (FindIsoState*)closure;
if ( 0 == XP_STRCMP( fis->isoCode, pdc->isoCode ) ) {
fis->found = pdc;
result = FEA_EXIT;
}
return result;
}
static PhoniesDataCodes*
findForIso( XW_DUtilCtxt* dutil, DevCtxt* dc, const XP_UCHAR* isoCode )
{
FindIsoState fis = {
.isoCode = isoCode,
};
dll_map( &dc->pd->links, findIsoProc, NULL, &fis );
PhoniesDataCodes* pdc = fis.found;
if ( !pdc ) {
pdc = XP_CALLOC( dutil->mpool, sizeof(*pdc) );
pdc->isoCode = copyString( dutil->mpool, isoCode );
dc->pd = (PhoniesDataCodes*)dll_insert( &dc->pd->links, &pdc->links, NULL );
XP_ASSERT( pdc == dc->pd );
}
return pdc;
}
static void
addPhony( XW_DUtilCtxt* dutil, DevCtxt* dc, const XP_UCHAR* isoCode,
const XP_UCHAR* phony )
{
PhoniesDataCodes* pdc = findForIso( dutil, dc, isoCode );
XP_ASSERT( !!pdc );
PhoniesDataStrs* pd = XP_CALLOC( dutil->mpool, sizeof(*pd) );
pd->phony = copyString( dutil->mpool, phony );
pdc->head = (PhoniesDataStrs*)dll_insert( &pdc->head->links, &pd->links, NULL );
}
static ForEachAct
storeStrs( const DLHead* elem, void* closure )
{
const PhoniesDataStrs* pds = (PhoniesDataStrs*)elem;
XWStreamCtxt* stream = (XWStreamCtxt*)closure;
stringToStream( stream, pds->phony );
return FEA_OK;
}
static ForEachAct
storeIso( const DLHead* elem, void* closure )
{
const PhoniesDataCodes* pdc = (PhoniesDataCodes*)elem;
XWStreamCtxt* stream = (XWStreamCtxt*)closure;
stringToStream( stream, pdc->isoCode );
PhoniesDataStrs* pds = pdc->head;
XP_U16 numStrs = dll_length( &pds->links );
XP_ASSERT( 0 < numStrs );
stream_putU32VL( stream, numStrs );
dll_map( &pds->links, storeStrs, NULL, stream );
return FEA_OK;
}
/* Storage format for PD_VERSION_1:
* version (byte)
* isoCount (byte)
** (repeats isoCount times)
** isoCode (string)
** strCount (var len XP_U32)
*** (repeats strCount times)
*** phony (string)
*/
static void
storePhoniesData( XW_DUtilCtxt* dutil, XWEnv xwe, DevCtxt* dc )
{
XWStreamCtxt* stream = mkStream( dutil );
if ( !!dc->pd ) {
stream_putU8( stream, PD_VERSION_1 );
PhoniesDataCodes* pdc = dc->pd;
XP_U16 numIsos = dll_length( &pdc->links );
XP_ASSERT( 0 < numIsos );
stream_putU8( stream, numIsos );
#ifdef DEBUG
PhoniesDataCodes* pdc1 = (PhoniesDataCodes*)
#endif
dll_map( &pdc->links, storeIso, NULL, stream );
XP_ASSERT( pdc1 == pdc );
}
const XP_UCHAR* keys[] = { KEY_LEGAL_PHONIES, NULL };
dutil_storeStream( dutil, xwe, keys, stream );
stream_destroy( stream );
}
static void
loadPhoniesData( XW_DUtilCtxt* dutil, XWEnv xwe, DevCtxt* dc )
{
LOG_FUNC();
XP_ASSERT ( !dc->pd );
XWStreamCtxt* stream = mkStream( dutil );
const XP_UCHAR* keys[] = { KEY_LEGAL_PHONIES, NULL };
dutil_loadStream( dutil, xwe, keys, stream );
XP_U8 flags;
if ( stream_gotU8( stream, &flags ) && PD_VERSION_1 == flags ) {
XP_U8 numIsos;
if ( stream_gotU8( stream, &numIsos ) ) {
for ( int ii = 0; ii < numIsos; ++ii ) {
XP_UCHAR isoCode[32];
stringFromStreamHere( stream, isoCode, VSIZE(isoCode) );
XP_U32 numStrs = stream_getU32VL( stream );
for ( int jj = 0; jj < numStrs; ++jj ) {
XP_UCHAR phony[32];
stringFromStreamHere( stream, phony, VSIZE(phony) );
addPhony( dutil, dc, isoCode, phony );
}
}
}
} else {
XP_LOGFF( "nothing there???" );
}
stream_destroy( stream );
}
void
dvc_addLegalPhony( XW_DUtilCtxt* dutil, XWEnv xwe,
const XP_UCHAR* isoCode,
const XP_UCHAR* phony )
{
if ( ! dvc_isLegalPhony( dutil, xwe, isoCode, phony ) ) {
DevCtxt* dc = load( dutil, xwe );
addPhony( dutil, dc, isoCode, phony );
storePhoniesData( dutil, xwe, dc );
}
}
XP_Bool
dvc_haveLegalPhonies( XW_DUtilCtxt* dutil, XWEnv xwe )
{
DevCtxt* dc = load( dutil, xwe );
XP_Bool result = 0 < dll_length( &dc->pd->links );
LOG_RETURNF( "%s", boolToStr(result) );
return result;
}
static void
freeOnePhony( DLHead* elem, void* closure )
{
XW_DUtilCtxt* dutil = (XW_DUtilCtxt*)closure;
const PhoniesDataStrs* pds = (PhoniesDataStrs*)elem;
XP_FREE( dutil->mpool, pds->phony );
XP_FREE( dutil->mpool, elem );
}
static void
freeOneCode( DLHead* elem, void* closure)
{
XW_DUtilCtxt* dutil = (XW_DUtilCtxt*)closure;
const PhoniesDataCodes* pdc = (PhoniesDataCodes*)elem;
dll_removeAll( &pdc->head->links, freeOnePhony, dutil );
XP_FREE( dutil->mpool, pdc->isoCode );
XP_FREE( dutil->mpool, elem );
}
static DevCtxt*
freePhonyState( XW_DUtilCtxt* dutil, XWEnv xwe )
{
DevCtxt* dc = load( dutil, xwe );
dll_removeAll( &dc->pd->links, freeOneCode, dutil );
dc->pd = NULL;
return dc;
}
XP_U16
dvc_clearLegalPhonies( XW_DUtilCtxt* dutil, XWEnv xwe )
{
DevCtxt* dc = load( dutil, xwe );
XP_U16 len = dll_length( &dc->pd->links );
freePhonyState( dutil, xwe );
storePhoniesData( dutil, xwe, dc );
return len;
}
#ifdef DEBUG
static ForEachAct
listPhoniesProc( const DLHead* elem, void* closure )
{
const PhoniesDataStrs* pds = (PhoniesDataStrs*)elem;
XWStreamCtxt* stream = (XWStreamCtxt*)closure;
stream_catString( stream, "- " );
stream_catString( stream, pds->phony );
stream_catString( stream, "\n" );
return FEA_OK;
}
static ForEachAct
listIsosProc( const DLHead* elem, void* closure )
{
const PhoniesDataCodes* pdc = (PhoniesDataCodes*)elem;
XWStreamCtxt* stream = (XWStreamCtxt*)closure;
stream_catString( stream, pdc->isoCode );
stream_catString( stream, ":\n" );
dll_map( &pdc->head->links, listPhoniesProc, NULL, stream );
return FEA_OK;
}
void
dvc_listLegalPhonies( XW_DUtilCtxt* dutil, XWEnv xwe, XWStreamCtxt* stream )
{
DevCtxt* dc = load( dutil, xwe );
dll_map( &dc->pd->links, listIsosProc, NULL, stream );
stream_putU8( stream, '\0' );
}
#endif
typedef struct _FindPhonyData {
XP_Bool found;
const XP_UCHAR* isoCode;
const XP_UCHAR* phony;
} FindPhonyData;
static ForEachAct
findPhonyProc2( const DLHead* elem, void* closure )
{
ForEachAct result = FEA_OK;
FindPhonyData* fpd = (FindPhonyData*)closure;
const PhoniesDataStrs* pds = (PhoniesDataStrs*)elem;
if ( 0 == XP_STRCMP( fpd->phony, pds->phony ) ) {
fpd->found = XP_TRUE;
result |= FEA_EXIT;
}
return result;
}
static ForEachAct
findPhonyProc1( const DLHead* elem, void* closure )
{
ForEachAct result = FEA_OK;
FindPhonyData* fpd = (FindPhonyData*)closure;
const PhoniesDataCodes* pdc = (PhoniesDataCodes*)elem;
if ( 0 == XP_STRCMP( fpd->isoCode, pdc->isoCode ) ) {
dll_map( &pdc->head->links, findPhonyProc2, NULL, closure );
result |= FEA_EXIT;
}
return result;
}
XP_Bool
dvc_isLegalPhony( XW_DUtilCtxt* dutil, XWEnv xwe,
const XP_UCHAR* isoCode, const XP_UCHAR* phony )
{
DevCtxt* dc = load( dutil, xwe );
FindPhonyData fpd = {
.isoCode = isoCode,
.phony = phony,
.found = XP_FALSE,
};
dll_map( &dc->pd->links, findPhonyProc1, NULL, &fpd );
return fpd.found;
}
static void
registerIf( XW_DUtilCtxt* dutil, XWEnv xwe )
{
@ -768,6 +1073,8 @@ dvc_init( XW_DUtilCtxt* dutil, XWEnv xwe )
dc->mWebSendKey = 0;
pthread_mutex_init( &dc->webSendMutex, NULL );
loadPhoniesData( dutil, xwe, dc );
#ifdef DEBUG
dutil->magic = MAGIC_INITED;
#endif
@ -777,7 +1084,10 @@ dvc_init( XW_DUtilCtxt* dutil, XWEnv xwe )
void
dvc_cleanup( XW_DUtilCtxt* dutil, XWEnv xwe )
{
DevCtxt* dc = load( dutil, xwe );
LOG_FUNC();
DevCtxt* dc = freePhonyState( dutil, xwe );
pthread_mutex_destroy( &dc->webSendMutex );
XP_FREEP( dutil->mpool, &dc );
}

View file

@ -22,6 +22,7 @@
#define _DEVICE_H_
#include "dutil.h"
#include "dictmgr.h"
// void device_load( XW_DUtilCtxt dctxt );
# ifdef XWFEATURE_DEVICE
@ -63,6 +64,16 @@ void dvc_parseMQTTPacket( XW_DUtilCtxt* dutil, XWEnv xwe, const XP_UCHAR* topic,
void dvc_onWebSendResult( XW_DUtilCtxt* dutil, XWEnv xwe, XP_U32 resultKey,
XP_Bool succeeded, const XP_UCHAR* result );
void dvc_addLegalPhony( XW_DUtilCtxt* dutil, XWEnv xwe,
const XP_UCHAR* isoCode, const XP_UCHAR* phony );
XP_Bool dvc_isLegalPhony( XW_DUtilCtxt* dutil, XWEnv xwe,
const XP_UCHAR* isoCode, const XP_UCHAR* phony );
XP_Bool dvc_haveLegalPhonies( XW_DUtilCtxt* dutil, XWEnv xwe );
XP_U16 dvc_clearLegalPhonies( XW_DUtilCtxt* dutil, XWEnv xwe );
#ifdef DEBUG
void dvc_listLegalPhonies( XW_DUtilCtxt* dutil, XWEnv xwe, XWStreamCtxt* stream );
#endif
/* All platforms need to call this shortly after setting up their XW_DUtilCtxt */
void dvc_init( XW_DUtilCtxt* dutil, XWEnv xwe );
void dvc_cleanup( XW_DUtilCtxt* dutil, XWEnv xwe );

View file

@ -126,6 +126,18 @@ dll_map( DLHead* list, DLMapProc mapProc, DLDisposeProc dispProc,
return newHead;
}
static ForEachAct
removeAllProc( const DLHead* XP_UNUSED(elem), void* XP_UNUSED(closure) )
{
return FEA_OK | FEA_REMOVE;
}
void
dll_removeAll( DLHead* list, DLDisposeProc dispProc, void* closure )
{
dll_map( list, removeAllProc, dispProc, closure );
}
DLHead*
dll_sort( DLHead* list, DLCompProc proc )
{

View file

@ -44,6 +44,7 @@ typedef ForEachAct (*DLMapProc)(const DLHead* elem, void* closure);
typedef void (*DLDisposeProc)(DLHead* elem, void* closure);
DLHead* dll_map( DLHead* list, DLMapProc mapProc, DLDisposeProc dispProc,
void* closure );
void dll_removeAll( DLHead* list, DLDisposeProc dispProc, void* closure );
#ifdef CPLUS
}

View file

@ -20,6 +20,7 @@
#include "modelp.h"
#include "util.h"
#include "device.h"
#include "engine.h"
#include "game.h"
#include "strutils.h"
@ -42,7 +43,7 @@ static XP_U16 find_start( const ModelCtxt* model, XP_U16 col, XP_U16 row,
static XP_S16 checkScoreMove( ModelCtxt* model, XWEnv xwe, XP_S16 turn,
EngineCtxt* engine, XWStreamCtxt* stream,
XP_Bool silent, WordNotifierInfo* notifyInfo );
static XP_U16 scoreWord( const ModelCtxt* model, XP_U16 turn,
static XP_U16 scoreWord( const ModelCtxt* model, XWEnv xwe, XP_U16 turn,
const MoveInfo* movei, EngineCtxt* engine,
XWStreamCtxt* stream, WordNotifierInfo* notifyInfo );
@ -571,7 +572,7 @@ figureMoveScore( const ModelCtxt* model, XWEnv xwe, XP_U16 turn,
word_multiplier( model, col, row );
}
oneScore = scoreWord( model, turn, moveInfo, (EngineCtxt*)NULL,
oneScore = scoreWord( model, xwe, turn, moveInfo, (EngineCtxt*)NULL,
stream, notifyInfo );
if ( !!stream ) {
formatWordScore( stream, oneScore, moveMultiplier );
@ -589,7 +590,7 @@ figureMoveScore( const ModelCtxt* model, XWEnv xwe, XP_U16 turn,
tmpMI.commonCoord = tiles->varCoord;
tmpMI.tiles[0].tile = tiles->tile;
oneScore = scoreWord( model, turn, &tmpMI, engine, stream, notifyInfo );
oneScore = scoreWord( model, xwe, turn, &tmpMI, engine, stream, notifyInfo );
if ( !!stream ) {
formatWordScore( stream, oneScore, multipliers[ii] );
}
@ -657,7 +658,7 @@ tile_multiplier( const ModelCtxt* model, XP_U16 col, XP_U16 row )
} /* tile_multiplier */
static XP_U16
scoreWord( const ModelCtxt* model, XP_U16 turn,
scoreWord( const ModelCtxt* model, XWEnv xwe, XP_U16 turn,
const MoveInfo* movei, /* new tiles */
EngineCtxt* engine,/* for crosswise caching */
XWStreamCtxt* stream,
@ -781,7 +782,15 @@ scoreWord( const ModelCtxt* model, XP_U16 turn,
dict_tilesToString( dict, checkWordBuf, len, buf,
sizeof(buf), NULL );
WNParams wnp = { .word = buf, .isLegal = legal, .dict = dict,
if ( !legal && PHONIES_WARN == model->vol.gi->phoniesAction ) {
legal = dvc_isLegalPhony( model->vol.dutil, xwe,
dict_getISOCode(dict), buf );
}
WNParams wnp = {
.word = buf,
.isLegal = legal,
.dict = dict,
#ifdef XWFEATURE_BOARDWORDS
.movei = movei, .start = start, .end = end,
#endif

View file

@ -164,13 +164,19 @@ typedef struct _ServerNonvolatiles {
} ServerNonvolatiles;
typedef struct _BadWordsState {
BadWordInfo bwi;
XP_UCHAR* dictName;
} BadWordsState;
struct ServerCtxt {
ServerVolatiles vol;
ServerNonvolatiles nv;
PoolContext* pool;
BadWordInfo illegalWordInfo;
BadWordsState bws;
XP_U16 lastMoveSource;
ServerPlayer srvPlyrs[MAX_NUM_PLAYERS];
@ -237,7 +243,7 @@ static void doEndGame( ServerCtxt* server, XWEnv xwe, XP_S16 quitter );
static void endGameInternal( ServerCtxt* server, XWEnv xwe,
GameEndReason why, XP_S16 quitter );
static void badWordMoveUndoAndTellUser( ServerCtxt* server, XWEnv xwe,
BadWordInfo* bwi );
const BadWordsState* bws );
static XP_Bool tileCountsOk( const ServerCtxt* server );
static void setTurn( ServerCtxt* server, XWEnv xwe, XP_S16 turn );
static XWStreamCtxt* mkServerStream( const ServerCtxt* server, XP_U8 version );
@ -1997,7 +2003,7 @@ server_do( ServerCtxt* server, XWEnv xwe )
case XWSTATE_NEEDSEND_BADWORD_INFO:
XP_ASSERT( server->vol.gi->serverRole == SERVER_ISHOST );
badWordMoveUndoAndTellUser( server, xwe, &server->illegalWordInfo );
badWordMoveUndoAndTellUser( server, xwe, &server->bws );
sendBadWordMsgs( server, xwe );
nextTurn( server, xwe, PICK_NEXT );
//moreToDo = XP_TRUE; /* why? */
@ -2449,11 +2455,12 @@ sendInitialMessage( ServerCtxt* server, XWEnv xwe )
} /* sendInitialMessage */
static void
freeBWI( MPFORMAL BadWordInfo* bwi )
freeBWS( MPFORMAL BadWordsState* bws )
{
BadWordInfo* bwi = &bws->bwi;
XP_U16 nWords = bwi->nWords;
XP_FREEP( mpool, &bwi->dictName );
XP_FREEP( mpool, &bws->dictName );
while ( nWords-- ) {
XP_FREEP( mpool, &bwi->words[nWords] );
}
@ -2462,35 +2469,35 @@ freeBWI( MPFORMAL BadWordInfo* bwi )
} /* freeBWI */
static void
bwiToStream( XWStreamCtxt* stream, BadWordInfo* bwi )
bwsToStream( XWStreamCtxt* stream, const BadWordsState* bws )
{
XP_U16 nWords = bwi->nWords;
const XP_UCHAR** sp;
const XP_U16 nWords = bws->bwi.nWords;
stream_putBits( stream, 4, nWords );
if ( STREAM_VERS_DICTNAME <= stream_getVersion( stream ) ) {
stringToStream( stream, bwi->dictName );
stringToStream( stream, bws->dictName );
}
for ( sp = bwi->words; nWords > 0; --nWords, ++sp ) {
stringToStream( stream, *sp );
for ( int ii = 0; ii < nWords; ++ii ) {
stringToStream( stream, bws->bwi.words[ii] );
}
} /* bwiToStream */
} /* bwsToStream */
static void
bwiFromStream( MPFORMAL XWStreamCtxt* stream, BadWordInfo* bwi )
bwsFromStream( MPFORMAL XWStreamCtxt* stream, BadWordsState* bws )
{
XP_U16 nWords = stream_getBits( stream, 4 );
XP_ASSERT( nWords < VSIZE(bwi->words) - 1 );
XP_ASSERT( nWords < VSIZE(bws->bwi.words) - 1 );
bwi->nWords = nWords;
bwi->dictName = ( STREAM_VERS_DICTNAME <= stream_getVersion( stream ) )
? stringFromStream( mpool, stream ) : NULL;
for ( int ii = 0; ii < nWords; ++ii ) {
bwi->words[ii] = (const XP_UCHAR*)stringFromStream( mpool, stream );
bws->bwi.nWords = nWords;
if ( STREAM_VERS_DICTNAME <= stream_getVersion( stream ) ) {
bws->dictName = stringFromStream( mpool, stream );
}
bwi->words[nWords] = NULL;
} /* bwiFromStream */
for ( int ii = 0; ii < nWords; ++ii ) {
bws->bwi.words[ii] = (const XP_UCHAR*)stringFromStream( mpool, stream );
}
bws->bwi.words[nWords] = NULL;
} /* bwsFromStream */
#ifdef DEBUG
#define caseStr(s) case s: str = #s; break;
@ -2561,15 +2568,15 @@ messageStreamWithHeader( ServerCtxt* server, XWEnv xwe, XP_U16 devIndex, XW_Prot
static void
sendBadWordMsgs( ServerCtxt* server, XWEnv xwe )
{
XP_ASSERT( server->illegalWordInfo.nWords > 0 );
XP_ASSERT( server->bws.bwi.nWords > 0 );
if ( server->illegalWordInfo.nWords > 0 ) { /* fail gracefully */
if ( server->bws.bwi.nWords > 0 ) { /* fail gracefully */
XWStreamCtxt* stream =
messageStreamWithHeader( server, xwe, server->lastMoveSource,
XWPROTO_BADWORD_INFO );
stream_putBits( stream, PLAYERNUM_NBITS, server->nv.currentTurn );
bwiToStream( stream, &server->illegalWordInfo );
bwsToStream( stream, &server->bws );
/* XP_U32 hash = model_getHash( server->vol.model ); */
/* stream_putU32( stream, hash ); */
@ -2577,13 +2584,14 @@ sendBadWordMsgs( ServerCtxt* server, XWEnv xwe )
stream_destroy( stream );
freeBWI( MPPARM(server->mpool) &server->illegalWordInfo );
freeBWS( MPPARM(server->mpool) &server->bws );
}
SETSTATE( server, XWSTATE_INTURN );
} /* sendBadWordMsgs */
static void
badWordMoveUndoAndTellUser( ServerCtxt* server, XWEnv xwe, BadWordInfo* bwi )
badWordMoveUndoAndTellUser( ServerCtxt* server, XWEnv xwe,
const BadWordsState* bws )
{
XP_U16 turn;
ModelCtxt* model = server->vol.model;
@ -2593,7 +2601,8 @@ badWordMoveUndoAndTellUser( ServerCtxt* server, XWEnv xwe, BadWordInfo* bwi )
model_rejectPreviousMove( model, xwe, server->pool, &turn );
util_notifyIllegalWords( server->vol.util, xwe, bwi, turn, XP_TRUE );
util_notifyIllegalWords( server->vol.util, xwe, &bws->bwi,
bws->dictName, turn, XP_TRUE, 0 );
} /* badWordMoveUndoAndTellUser */
EngineCtxt*
@ -2978,14 +2987,15 @@ storeBadWords( const WNParams* wnp, void* closure )
{
if ( !wnp->isLegal ) {
ServerCtxt* server = (ServerCtxt*)closure;
const XP_UCHAR* name = dict_getShortName( wnp->dict );
const XP_UCHAR* dictName = dict_getShortName( wnp->dict );
XP_LOGFF( "storeBadWords called with \"%s\" (name=%s)", wnp->word, name );
if ( NULL == server->illegalWordInfo.dictName ) {
server->illegalWordInfo.dictName = copyString( server->mpool, name );
XP_LOGFF( "storeBadWords called with \"%s\" (name=%s)", wnp->word,
dictName );
if ( NULL == server->bws.dictName ) {
server->bws.dictName = copyString( server->mpool, dictName );
}
server->illegalWordInfo.words[server->illegalWordInfo.nWords++]
= copyString( server->mpool, wnp->word );
BadWordInfo* bwi = &server->bws.bwi;
bwi->words[bwi->nWords++] = copyString( server->mpool, wnp->word );
}
} /* storeBadWords */
@ -2993,7 +3003,7 @@ static XP_Bool
checkMoveAllowed( ServerCtxt* server, XWEnv xwe, XP_U16 playerNum )
{
CurGameInfo* gi = server->vol.gi;
XP_ASSERT( server->illegalWordInfo.nWords == 0 );
XP_ASSERT( server->bws.bwi.nWords == 0 );
if ( gi->phoniesAction == PHONIES_DISALLOW ) {
WordNotifierInfo info;
@ -3003,7 +3013,7 @@ checkMoveAllowed( ServerCtxt* server, XWEnv xwe, XP_U16 playerNum )
(XWStreamCtxt*)NULL, &info );
}
return server->illegalWordInfo.nWords == 0;
return server->bws.bwi.nWords == 0;
} /* checkMoveAllowed */
static void
@ -3052,9 +3062,9 @@ sendMoveTo( ServerCtxt* server, XWEnv xwe, XP_U16 devIndex, XP_U16 turn,
}
if ( !legal ) {
XP_ASSERT( server->illegalWordInfo.nWords > 0 );
XP_ASSERT( server->bws.bwi.nWords > 0 );
stream_putBits( stream, PLAYERNUM_NBITS, turn );
bwiToStream( stream, &server->illegalWordInfo );
bwsToStream( stream, &server->bws );
}
}
@ -3935,11 +3945,11 @@ finishMove( ServerCtxt* server, XWEnv xwe, TrayTileSet* newTiles, XP_U16 turn )
sortTilesIf( server, turn );
if ( !isLegalMove && !isClient ) {
badWordMoveUndoAndTellUser( server, xwe, &server->illegalWordInfo );
badWordMoveUndoAndTellUser( server, xwe, &server->bws );
/* It's ok to free these guys. I'm the server, and the move was made
here, so I've notified all clients already by setting the flag (and
passing the word) in sendMoveToClientsExcept. */
freeBWI( MPPARM(server->mpool) &server->illegalWordInfo );
freeBWS( MPPARM(server->mpool) &server->bws );
}
if (isClient && (gi->phoniesAction == PHONIES_DISALLOW)
@ -4739,14 +4749,14 @@ tellMoveWasLegal( ServerCtxt* server, XWEnv xwe )
static XP_Bool
handleIllegalWord( ServerCtxt* server, XWEnv xwe, XWStreamCtxt* incoming )
{
BadWordInfo bwi;
BadWordsState bws = {{0}};
(void)stream_getBits( incoming, PLAYERNUM_NBITS );
bwiFromStream( MPPARM(server->mpool) incoming, &bwi );
bwsFromStream( MPPARM(server->mpool) incoming, &bws );
badWordMoveUndoAndTellUser( server, xwe, &bwi );
badWordMoveUndoAndTellUser( server, xwe, &bws );
freeBWI( MPPARM(server->mpool) &bwi );
freeBWS( MPPARM(server->mpool) &bws );
SETSTATE( server, XWSTATE_INTURN );
return XP_TRUE;

View file

@ -458,7 +458,7 @@ handleActionInTray( BoardCtxt* board, XWEnv xwe, XP_S16 index, XP_Bool onDivider
}
#endif
} else if ( index == -(board->gi->traySize) ) { /* pending score tile */
result = board_commitTurn( board, xwe, XP_FALSE, XP_FALSE, NULL );
result = board_commitTurn( board, xwe, NULL, XP_FALSE, NULL );
#if defined XWFEATURE_TRAYUNDO_ALL
} else if ( index < 0 ) { /* other empty area */
/* it better be true */

View file

@ -73,7 +73,6 @@ typedef struct PickInfo {
typedef struct _BadWordInfo {
XP_U16 nWords;
const XP_UCHAR* dictName;
/* Null-terminated array of ptrs */
const XP_UCHAR* words[MAX_TRAY_TILES+2]; /* can form in both directions */
} BadWordInfo;
@ -139,8 +138,11 @@ typedef struct UtilVtable {
XP_Bool (*m_util_altKeyDown)( XW_UtilCtxt* uc, XWEnv xwe );
DictionaryCtxt* (*m_util_makeEmptyDict)( XW_UtilCtxt* uc, XWEnv xwe );
void (*m_util_notifyIllegalWords)( XW_UtilCtxt* uc, XWEnv xwe, BadWordInfo* bwi,
XP_U16 turn, XP_Bool turnLost );
void (*m_util_notifyIllegalWords)( XW_UtilCtxt* uc, XWEnv xwe,
const BadWordInfo* bwi,
const XP_UCHAR* dictName,
XP_U16 turn, XP_Bool turnLost,
XP_U32 badWordsKey );
void (*m_util_remSelected)(XW_UtilCtxt* uc, XWEnv xwe);
@ -279,8 +281,8 @@ struct XW_UtilCtxt {
#define util_makeEmptyDict( uc, e ) \
(uc)->vtable->m_util_makeEmptyDict((uc), (e))
#define util_notifyIllegalWords( uc,e, w, p, b ) \
(uc)->vtable->m_util_notifyIllegalWords((uc), (e),(w),(p),(b))
#define util_notifyIllegalWords( uc,e, w, d, p, b, k ) \
(uc)->vtable->m_util_notifyIllegalWords((uc), (e), (w), (d), (p), (b), (k))
#define util_remSelected( uc,e ) \
(uc)->vtable->m_util_remSelected((uc), (e))

View file

@ -211,6 +211,7 @@ GTK_OBJS = \
$(BUILD_PLAT_DIR)/gtkutils.o \
$(BUILD_PLAT_DIR)/gtkntilesask.o \
$(BUILD_PLAT_DIR)/gtkaskdict.o \
$(BUILD_PLAT_DIR)/gtkaskbad.o \
$(BUILD_PLAT_DIR)/gtkchat.o \
$(BUILD_PLAT_DIR)/gtkkpdlg.o \
$(BUILD_PLAT_DIR)/gtkrmtch.o \

View file

@ -825,10 +825,11 @@ ask_move( gpointer data )
CommonGlobals* cGlobals = &bGlobals->cGlobals;
const char* answers[] = {"Ok", "Cancel", NULL};
if (0 == cursesask(bGlobals->boardWin, cGlobals->question,
VSIZE(answers)-1, answers) ) {
if ( 0 == cursesask( bGlobals->boardWin, cGlobals->question,
VSIZE(answers)-1, answers ) ) {
BoardCtxt* board = cGlobals->game.board;
if ( board_commitTurn( board, NULL_XWE, XP_TRUE, XP_TRUE, NULL ) ) {
PhoniesConf pc = { .confirmed = XP_TRUE };
if ( board_commitTurn( board, NULL_XWE, &pc, XP_TRUE, NULL ) ) {
board_draw( board, NULL_XWE );
linuxSaveGame( &bGlobals->cGlobals );
}
@ -873,12 +874,16 @@ curses_util_turnChanged( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe),
#endif
static void
curses_util_notifyIllegalWords( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), BadWordInfo* bwi,
XP_U16 player, XP_Bool turnLost )
curses_util_notifyIllegalWords( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe),
const BadWordInfo* bwi,
const XP_UCHAR* XP_UNUSED(dictName),
XP_U16 player, XP_Bool turnLost,
XP_U32 bwKey )
{
gchar* strs = g_strjoinv( "\", \"", (gchar**)bwi->words );
gchar* msg = g_strdup_printf( "Player %d played bad word[s]: \"%s\". "
"Turn lost: %s", player, strs, boolToStr(turnLost) );
"Turn lost: %s; key=%d", player, strs,
boolToStr(turnLost), bwKey );
CursesBoardGlobals* bGlobals = (CursesBoardGlobals*)uc->closure;
if ( !!bGlobals->boardWin ) {
@ -913,7 +918,8 @@ ask_trade( gpointer data )
if (0 == cursesask( bGlobals->boardWin, cGlobals->question,
VSIZE(buttons), buttons ) ) {
BoardCtxt* board = cGlobals->game.board;
if ( board_commitTurn( board, NULL_XWE, XP_TRUE, XP_TRUE, NULL ) ) {
PhoniesConf pc = { .confirmed = XP_TRUE };
if ( board_commitTurn( board, NULL_XWE, &pc, XP_TRUE, NULL ) ) {
board_draw( board, NULL_XWE );
linuxSaveGame( cGlobals );
}
@ -1451,7 +1457,7 @@ handleCommit( void* closure, int XP_UNUSED(key) )
{
CursesBoardGlobals* bGlobals = (CursesBoardGlobals*)closure;
CommonGlobals* cGlobals = &bGlobals->cGlobals;
if ( board_commitTurn( cGlobals->game.board, NULL_XWE, XP_FALSE,
if ( board_commitTurn( cGlobals->game.board, NULL_XWE, NULL,
XP_FALSE, NULL ) ) {
board_draw( cGlobals->game.board, NULL_XWE );
}

121
xwords4/linux/gtkaskbad.c Normal file
View file

@ -0,0 +1,121 @@
/* -*- compile-command: "make MEMDEBUG=TRUE -j5"; -*- */
/*
* Copyright 2001-2024 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.
*/
#ifdef PLATFORM_GTK
#include "gtkaskbad.h"
#include "gtkutils.h"
#include "dbgutil.h"
typedef struct _AskBadState {
GtkWidget* check;
GStrv words;
bool skipNext;
const char* dictName;
} AskBadState;
/* static void */
/* handle_response( GtkWidget* item, AskBadState* state ) */
/* { */
/* LOG_FUNC(); */
/* } */
static void
handle_check_toggled( GtkWidget* item, AskBadState* state )
{
XP_ASSERT( item == state->check );
state->skipNext = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(item));
XP_LOGFF( "checked: %s", boolToStr(state->skipNext) );
}
/* static void */
/* handle_ok( GtkWidget* XP_UNUSED(item), AskBadState* state ) */
/* { */
/* state->confirmed = true; */
/* gtk_main_quit(); */
/* } */
/* static void */
/* handle_cancel( GtkWidget* XP_UNUSED(item), AskBadState* state ) */
/* { */
/* state->confirmed = false; */
/* gtk_main_quit(); */
/* } */
static GtkWidget*
buildDialog( AskBadState* state )
{
GtkWidget* dialog = gtk_dialog_new_with_buttons( NULL, NULL, //GtkWindow *parent,
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
"Ok", GTK_RESPONSE_ACCEPT,
"Cancel", GTK_RESPONSE_REJECT,
NULL );
/* GtkWidget* bc = gtk_dialog_get_action_area( GTK_DIALOG(dialog) ); */
/* g_object_set_property( G_OBJECT(bc), "halign", GTK_ALIGN_CENTER ); */
GtkWidget* vbox = gtk_dialog_get_content_area( GTK_DIALOG(dialog) );
gchar* words = g_strjoinv( "\n", state->words );
gchar* msg = g_strdup_printf("The word (or words) below are not in the wordlist %s. "
"\n\n%s\n\n"
"Would you like to accept them anyway?\n",
state->dictName, words );
GtkWidget* label = gtk_label_new ( msg );
g_free( words );
g_free( msg );
gtk_widget_show( label );
gtk_box_pack_start( GTK_BOX(vbox), label, FALSE, TRUE, 0 );
state->check = gtk_check_button_new_with_label( "Always accept" );
g_signal_connect( state->check, "toggled",
(GCallback)handle_check_toggled, state );
gtk_widget_show( state->check );
gtk_box_pack_start( GTK_BOX(vbox), state->check, FALSE, TRUE, 0 );
gtk_widget_show( vbox );
return dialog;
}
/* return true if not cancelled */
bool
gtkAskBad( GtkGameGlobals* globals, GStrv words, const char* dictName,
bool* skipNext )
{
XP_USE( globals );
AskBadState state = {
.words = words,
.dictName = dictName,
};
GtkWidget* dialog = buildDialog( &state );
gtk_window_set_modal( GTK_WINDOW( dialog ), TRUE );
gtk_widget_show_all( dialog );
gint response = gtk_dialog_run( GTK_DIALOG(dialog) );
gtk_widget_destroy( dialog );
*skipNext = state.skipNext;
return GTK_RESPONSE_ACCEPT == response;
}
#endif

32
xwords4/linux/gtkaskbad.h Normal file
View file

@ -0,0 +1,32 @@
/* -*- compile-command: "make MEMDEBUG=TRUE -j5"; -*- */
/*
* Copyright 2001-2024 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.
*/
#ifdef PLATFORM_GTK
#ifndef _GTKASKBAD_H_
#define _GTKASKBAD_H_
#include "xptypes.h"
#include "gtkboard.h"
/* return true if not cancelled */
bool gtkAskBad( GtkGameGlobals* globals, GStrv words, const char* dictName,
bool* skipNext );
#endif
#endif

View file

@ -56,6 +56,7 @@
#include "movestak.h"
#include "strutils.h"
#include "dbgutil.h"
#include "device.h"
#include "gtkask.h"
#include "gtkinvit.h"
#include "gtkaskm.h"
@ -65,6 +66,7 @@
#include "gtkpasswdask.h"
#include "gtkntilesask.h"
#include "gtkaskdict.h"
#include "gtkaskbad.h"
#include "linuxdict.h"
/* #include "undo.h" */
#include "gtkdraw.h"
@ -1240,8 +1242,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, NULL_XWE, XP_FALSE,
XP_FALSE, NULL ) ) {
if ( board_commitTurn( globals->cGlobals.game.board, NULL_XWE,
NULL, XP_FALSE, NULL ) ) {
board_draw( globals->cGlobals.game.board, NULL_XWE );
disenable_buttons( globals );
}
@ -1354,7 +1356,7 @@ static void
handle_commit_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals )
{
if ( board_commitTurn( globals->cGlobals.game.board, NULL_XWE,
XP_FALSE, XP_FALSE, NULL ) ) {
NULL, XP_FALSE, NULL ) ) {
board_draw( globals->cGlobals.game.board, NULL_XWE );
}
} /* handle_commit_button */
@ -1572,8 +1574,9 @@ ask_tiles( gpointer data )
server_tilesPicked( cGlobals->game.server, NULL_XWE,
cGlobals->selPlayer, &newTiles );
} else {
PhoniesConf pc = { .confirmed = XP_TRUE };
draw = board_commitTurn( cGlobals->game.board, NULL_XWE,
XP_TRUE, XP_TRUE, &newTiles );
&pc, XP_TRUE, &newTiles );
}
if ( draw ) {
@ -1856,30 +1859,50 @@ gtk_util_engineProgressCallback( XW_UtilCtxt* XP_UNUSED(uc), XWEnv XP_UNUSED(xwe
#endif
} /* gtk_util_engineProgressCallback */
typedef struct _BadWordsData {
GtkGameGlobals* globals;
XP_U32 bwKey;
GStrv words;
gchar* dictName;
} BadWordsData;
static gint
ask_bad_words( gpointer data )
{
GtkGameGlobals* globals = (GtkGameGlobals*)data;
CommonGlobals* cGlobals = &globals->cGlobals;
BadWordsData* bwd = (BadWordsData*)data;
CommonGlobals* cGlobals = &bwd->globals->cGlobals;
if ( GTK_RESPONSE_YES == gtkask( globals->window, cGlobals->question,
GTK_BUTTONS_YES_NO, NULL ) ) {
board_commitTurn( cGlobals->game.board, NULL_XWE, XP_TRUE, XP_FALSE, NULL );
bool skipNext = false;
if ( gtkAskBad( bwd->globals, bwd->words, bwd->dictName, &skipNext ) ) {
PhoniesConf pc = {
.confirmed = XP_TRUE,
.key = skipNext ? bwd->bwKey : 0,
};
board_commitTurn( cGlobals->game.board, NULL_XWE,
&pc, XP_FALSE, NULL );
}
g_free( bwd->dictName );
g_strfreev( bwd->words );
XP_FREE( cGlobals->util->mpool, bwd );
return 0;
}
static void
gtk_util_notifyIllegalWords( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe),
BadWordInfo* bwi, XP_U16 player,
XP_Bool turnLost )
const BadWordInfo* bwi,
const XP_UCHAR* dictName,
XP_U16 player, XP_Bool turnLost,
XP_U32 bwKey )
{
GtkGameGlobals* globals = (GtkGameGlobals*)uc->closure;
CommonGlobals* cGlobals = &globals->cGlobals;
char buf[300];
gchar* strs = g_strjoinv( "\", \"", (gchar**)bwi->words );
if ( turnLost ) {
XP_ASSERT( 0 == bwKey );
char buf[300];
gchar* strs = g_strjoinv( "\", \"", (gchar**)bwi->words );
XP_UCHAR* name = cGlobals->gi->players[player].name;
XP_ASSERT( !!name );
@ -1891,13 +1914,22 @@ gtk_util_notifyIllegalWords( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe),
} else {
gtkUserError( globals, buf );
}
g_free( strs );
} else {
sprintf( cGlobals->question, "Word[s] \"%s\" not in the current dictionary (%s). "
"Use anyway?", strs, bwi->dictName );
BadWordsData* bwd = XP_MALLOC( cGlobals->util->mpool, sizeof(*bwd) );
bwd->globals = globals;
bwd->dictName = g_strdup( dictName );
bwd->bwKey = bwKey;
(void)g_idle_add( ask_bad_words, globals );
GStrvBuilder* builder = g_strv_builder_new();
for ( const char* const* word = bwi->words; !!*word; ++word ) {
g_strv_builder_add( builder, *word );
}
bwd->words = g_strv_builder_end( builder );
g_strv_builder_unref( builder );
(void)g_idle_add( ask_bad_words, bwd );
}
g_free( strs );
} /* gtk_util_notifyIllegalWords */
static void
@ -2049,7 +2081,8 @@ 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, NULL_XWE, XP_TRUE, XP_TRUE, NULL ) ) {
PhoniesConf pc = { .confirmed = XP_TRUE };
if ( board_commitTurn( board, NULL_XWE, &pc, XP_TRUE, NULL ) ) {
board_draw( board, NULL_XWE );
}
}
@ -2081,7 +2114,8 @@ ask_trade( gpointer data )
cGlobals->question,
GTK_BUTTONS_YES_NO, NULL ) ) {
BoardCtxt* board = cGlobals->game.board;
if ( board_commitTurn( board, NULL_XWE, XP_TRUE, XP_TRUE, NULL ) ) {
PhoniesConf pc = { .confirmed = XP_TRUE };
if ( board_commitTurn( board, NULL_XWE, &pc, XP_TRUE, NULL ) ) {
board_draw( board, NULL_XWE );
}
}

View file

@ -163,8 +163,8 @@ linux_makeMoveIf( CommonGlobals* cGlobals, XP_Bool tryTrade )
} else {
XP_LOGFF( "unable to find hint; so PASSing" );
}
success = board_commitTurn( board, NULL_XWE, XP_TRUE, XP_TRUE,
NULL );
PhoniesConf pc = { .confirmed = XP_TRUE };
success = board_commitTurn( board, NULL_XWE, &pc, XP_TRUE, NULL );
}
}
return success;