diff --git a/xwords4/android/XWords4/jni/LocalizedStrIncludes.h b/xwords4/android/XWords4/jni/LocalizedStrIncludes.h
index a4f86f4b5..b9afeee58 100644
--- a/xwords4/android/XWords4/jni/LocalizedStrIncludes.h
+++ b/xwords4/android/XWords4/jni/LocalizedStrIncludes.h
@@ -28,6 +28,8 @@
# define STRD_TURN_SCORE 22
# define STRD_REMAINS_HEADER 23
# define STRD_REMAINS_EXPL 24
+# define STR_RESIGNED 25
+# define STR_WINNER 26
-# define N_AND_USER_STRINGS 24
+# define N_AND_USER_STRINGS 26
#endif
diff --git a/xwords4/android/XWords4/res/menu/board_menu.xml b/xwords4/android/XWords4/res/menu/board_menu.xml
index eb888921e..0b75e22b0 100644
--- a/xwords4/android/XWords4/res/menu/board_menu.xml
+++ b/xwords4/android/XWords4/res/menu/board_menu.xml
@@ -29,8 +29,8 @@
android:title="@string/board_menu_game_left" />
-
+
diff --git a/xwords4/android/XWords4/res/values/strings.xml b/xwords4/android/XWords4/res/values/strings.xml
index 61769251d..c2f0f13fc 100644
--- a/xwords4/android/XWords4/res/values/strings.xml
+++ b/xwords4/android/XWords4/res/values/strings.xml
@@ -693,6 +693,7 @@
is not yet over, gives you a choice whether to end it now,
and if you decline does nothing. -->
Final scores
+ Resign
- Are you sure you want to end the game now?
+ Are you sure you want to resign?
@@ -2110,5 +2111,9 @@
New version of %s
Tap to download
+
+ Resigned
+
+ Winner
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/BoardActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/BoardActivity.java
index 36671ff0e..47c462c96 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/BoardActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/BoardActivity.java
@@ -731,7 +731,7 @@ public class BoardActivity extends XWActivity
R.string.history_title );
break;
- case R.id.board_menu_game_final:
+ case R.id.board_menu_game_resign:
m_jniThread.handle( JNIThread.JNICmd.CMD_FINAL,
R.string.history_title );
break;
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java
index 8e975a334..06a518e4f 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java
@@ -84,6 +84,8 @@ public interface UtilCtxt {
static final int STRD_TURN_SCORE = 22;
static final int STRD_REMAINS_HEADER = 23;
static final int STRD_REMAINS_EXPL = 24;
+ static final int STR_RESIGNED = 25;
+ static final int STR_WINNER = 26;
String getUserString( int stringCode );
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
index ddbed884c..8fb99b467 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
@@ -181,6 +181,12 @@ public class UtilCtxtImpl implements UtilCtxt {
case UtilCtxt.STRD_REMAINS_EXPL:
id = R.string.strd_remains_expl;
break;
+ case UtilCtxt.STR_RESIGNED:
+ id = R.string.str_resigned;
+ break;
+ case UtilCtxt.STR_WINNER:
+ id = R.string.str_winner;
+ break;
default:
DbgUtils.logf( "no such stringCode: %d", stringCode );
diff --git a/xwords4/common/server.c b/xwords4/common/server.c
index 0ddb63a11..7bf529ee0 100644
--- a/xwords4/common/server.c
+++ b/xwords4/common/server.c
@@ -85,6 +85,7 @@ typedef struct ServerNonvolatiles {
XW_State gameState;
XW_State stateAfterShow;
XP_S8 currentTurn; /* invalid when game is over */
+ XP_S8 quitter; /* -1 unless somebody resigned */
XP_U8 pendingRegistrations;
XP_Bool showRobotScores;
XP_Bool sortNewTiles;
@@ -134,8 +135,8 @@ static void assignTilesToAll( ServerCtxt* server );
static void resetEngines( ServerCtxt* server );
static void nextTurn( ServerCtxt* server, XP_S16 nxtTurn );
-static void doEndGame( ServerCtxt* server );
-static void endGameInternal( ServerCtxt* server, GameEndReason why );
+static void doEndGame( ServerCtxt* server, XP_S16 quitter );
+static void endGameInternal( ServerCtxt* server, GameEndReason why, XP_S16 quitter );
static void badWordMoveUndoAndTellUser( ServerCtxt* server,
BadWordInfo* bwi );
static XP_Bool tileCountsOk( const ServerCtxt* server );
@@ -249,6 +250,7 @@ initServer( ServerCtxt* server )
#ifdef STREAM_VERS_BIGBOARD
server->nv.streamVersion = STREAM_SAVE_PREVWORDS; /* default to old */
#endif
+ server->nv.quitter = -1;
} /* initServer */
ServerCtxt*
@@ -298,6 +300,9 @@ getNV( XWStreamCtxt* stream, ServerNonvolatiles* nv, XP_U16 nPlayers )
}
nv->currentTurn = (XP_S8)stream_getBits( stream, NPLAYERS_NBITS ) - 1;
+ if ( STREAM_VERS_DICTNAME <= version ) {
+ nv->quitter = (XP_S8)stream_getBits( stream, NPLAYERS_NBITS ) - 1;
+ }
nv->pendingRegistrations = (XP_U8)stream_getBits( stream, NPLAYERS_NBITS );
for ( ii = 0; ii < nPlayers; ++ii ) {
@@ -332,6 +337,7 @@ putNV( XWStreamCtxt* stream, const ServerNonvolatiles* nv, XP_U16 nPlayers )
/* +1: make -1 (NOTURN) into a positive number */
stream_putBits( stream, NPLAYERS_NBITS, nv->currentTurn+1 );
+ stream_putBits( stream, NPLAYERS_NBITS, nv->quitter+1 );
stream_putBits( stream, NPLAYERS_NBITS, nv->pendingRegistrations );
for ( ii = 0; ii < nPlayers; ++ii ) {
@@ -1054,7 +1060,7 @@ server_do( ServerCtxt* server )
#endif /* XWFEATURE_STANDALONE_ONLY */
case XWSTATE_NEEDSEND_ENDGAME:
- endGameInternal( server, END_REASON_OUT_OF_TILES );
+ endGameInternal( server, END_REASON_OUT_OF_TILES, -1 );
break;
case XWSTATE_NEED_SHOWSCORE:
@@ -2399,14 +2405,30 @@ server_getLastMoveTime( const ServerCtxt* server )
}
static void
-doEndGame( ServerCtxt* server )
+doEndGame( ServerCtxt* server, XP_S16 quitter )
{
SETSTATE( server, XWSTATE_GAMEOVER );
setTurn( server, -1 );
+ server->nv.quitter = quitter;
(*server->vol.gameOverListener)( server->vol.gameOverData );
} /* doEndGame */
+static void
+putQuitter( const ServerCtxt* server, XWStreamCtxt* stream, XP_S16 quitter )
+{
+ if ( STREAM_VERS_DICTNAME <= server->nv.streamVersion ) {
+ stream_putU8( stream, quitter );
+ }
+}
+
+static void
+getQuitter( const ServerCtxt* server, XWStreamCtxt* stream, XP_S16* quitter )
+{
+ *quitter = STREAM_VERS_DICTNAME <= server->nv.streamVersion
+ ? stream_getU8( stream ) : -1;
+}
+
/* Somebody wants to end the game.
*
* If I'm the server, I send a END_GAME message to all clients. If I'm a
@@ -2415,7 +2437,7 @@ doEndGame( ServerCtxt* server )
* GAME_OVER message to all clients including the one that requested it.
*/
static void
-endGameInternal( ServerCtxt* server, GameEndReason XP_UNUSED(why) )
+endGameInternal( ServerCtxt* server, GameEndReason XP_UNUSED(why), XP_S16 quitter )
{
XP_ASSERT( server->nv.gameState != XWSTATE_GAMEOVER );
@@ -2427,10 +2449,11 @@ endGameInternal( ServerCtxt* server, GameEndReason XP_UNUSED(why) )
XWStreamCtxt* stream;
stream = messageStreamWithHeader( server, devIndex,
XWPROTO_END_GAME );
+ putQuitter( server, stream, quitter );
stream_destroy( stream );
}
#endif
- doEndGame( server );
+ doEndGame( server, quitter );
#ifndef XWFEATURE_STANDALONE_ONLY
} else {
@@ -2449,7 +2472,7 @@ server_endGame( ServerCtxt* server )
{
XW_State gameState = server->nv.gameState;
if ( gameState < XWSTATE_GAMEOVER && gameState >= XWSTATE_INTURN ) {
- endGameInternal( server, END_REASON_USER_REQUEST );
+ endGameInternal( server, END_REASON_USER_REQUEST, server->nv.currentTurn );
}
} /* server_endGame */
@@ -2711,7 +2734,7 @@ server_receiveMessage( ServerCtxt* server, XWStreamCtxt* incoming )
XP_FREE( server->mpool, msg );
#endif
} else if ( readStreamHeader( server, incoming ) ) {
-
+ XP_S16 quitter;
switch( code ) {
/* case XWPROTO_MOVEMADE_INFO: */
/* accepted = client_reflectMoveMade( server, incoming ); */
@@ -2763,11 +2786,13 @@ server_receiveMessage( ServerCtxt* server, XWStreamCtxt* incoming )
break;
case XWPROTO_CLIENT_REQ_END_GAME:
- endGameInternal( server, END_REASON_USER_REQUEST );
+ getQuitter( server, incoming, &quitter );
+ endGameInternal( server, END_REASON_USER_REQUEST, quitter );
accepted = XP_TRUE;
break;
case XWPROTO_END_GAME:
- doEndGame( server );
+ getQuitter( server, incoming, &quitter );
+ doEndGame( server, quitter );
accepted = XP_TRUE;
break;
default:
@@ -2927,21 +2952,63 @@ server_figureFinishBonus( const ServerCtxt* server, XP_U16 turn )
#endif
#define IMPOSSIBLY_LOW_SCORE -1000
+#if 0
+static void
+printPlayer( const ServerCtxt* server, XWStreamCtxt* stream, XP_U16 index,
+ const XP_UCHAR* placeBuf, ScoresArray* scores,
+ ScoresArray* tilePenalties, XP_U16 place )
+{
+ XP_UCHAR buf[128];
+ CurGameInfo* gi = server->vol.gi;
+ ModelCtxt* model = server->vol.model;
+ XP_Bool firstDone = model_getNumTilesTotal( model, index ) == 0;
+ XP_UCHAR tmpbuf[48];
+ XP_U16 addSubKey = firstDone? STRD_REMAINING_TILES_ADD : STRD_UNUSED_TILES_SUB;
+ const XP_UCHAR* addSubString = util_getUserString( server->vol.util, addSubKey );
+ XP_UCHAR* timeStr = (XP_UCHAR*)"";
+ XP_UCHAR timeBuf[16];
+ if ( gi->timerEnabled ) {
+ XP_U16 penalty = player_timePenalty( gi, index );
+ if ( penalty > 0 ) {
+ XP_SNPRINTF( timeBuf, sizeof(timeBuf),
+ util_getUserString( server->vol.util,
+ STRD_TIME_PENALTY_SUB ),
+ penalty ); /* positive for formatting */
+ timeStr = timeBuf;
+ }
+ }
+
+ XP_SNPRINTF( tmpbuf, sizeof(tmpbuf), addSubString,
+ firstDone?
+ tilePenalties->arr[index]:
+ -tilePenalties->arr[index] );
+
+ XP_SNPRINTF( buf, sizeof(buf),
+ (XP_UCHAR*)"[%s] %s: %d" XP_CR " (%d %s%s)",
+ placeBuf, emptyStringIfNull(gi->players[index].name),
+ scores->arr[index], model_getPlayerScore( model, index ),
+ tmpbuf, timeStr );
+ if ( 0 < place ) {
+ stream_catString( stream, XP_CR );
+ }
+ stream_catString( stream, buf );
+} /* printPlayer */
+#endif
+
void
server_writeFinalScores( ServerCtxt* server, XWStreamCtxt* stream )
{
ScoresArray scores;
ScoresArray tilePenalties;
- XP_S16 highestIndex;
- XP_S16 highestScore;
- XP_U16 place, nPlayers, ii;
- XP_S16 curScore;
+ XP_U16 place, nPlayers;
+ XP_S16 quitter = server->nv.quitter;
+ XP_Bool quitterDone = XP_FALSE;
+ XP_USE(quitter);
ModelCtxt* model = server->vol.model;
const XP_UCHAR* addString = util_getUserString( server->vol.util,
STRD_REMAINING_TILES_ADD );
const XP_UCHAR* subString = util_getUserString( server->vol.util,
STRD_UNUSED_TILES_SUB );
- XP_UCHAR timeBuf[16];
XP_UCHAR* timeStr;
CurGameInfo* gi = server->vol.gi;
@@ -2951,29 +3018,46 @@ server_writeFinalScores( ServerCtxt* server, XWStreamCtxt* stream )
nPlayers = gi->nPlayers;
- for ( place = 1; ; ++place ) {
+ for ( place = 1; !quitterDone; ++place ) {
+ XP_UCHAR timeBuf[16];
+ XP_UCHAR buf[128];
+ XP_S16 highestScore = IMPOSSIBLY_LOW_SCORE;
+ XP_S16 highestIndex = -1;
+ const XP_UCHAR* placeStr = NULL;
+ XP_UCHAR placeBuf[32];
XP_UCHAR tmpbuf[48];
- XP_UCHAR buf[128];
+ XP_U16 ii, placeKey = 0;
XP_Bool firstDone;
- highestScore = IMPOSSIBLY_LOW_SCORE;
- highestIndex = -1;
-
+ /* Find the next player we should print */
for ( ii = 0; ii < nPlayers; ++ii ) {
- if ( scores.arr[ii] > highestScore ) {
+ if ( quitter != ii && scores.arr[ii] > highestScore ) {
highestIndex = ii;
highestScore = scores.arr[ii];
}
}
if ( highestIndex == -1 ) {
- break; /* we're done */
- } else if ( place > 1 ) {
- stream_catString( stream, XP_CR );
+ if ( quitter >= 0 ) {
+ XP_ASSERT( !quitterDone );
+ highestIndex = quitter;
+ quitterDone = XP_TRUE;
+ placeKey = STR_RESIGNED;
+ } else {
+ break; /* we're done */
+ }
+ } else if ( place == 1 ) {
+ placeKey = STR_WINNER;
}
- scores.arr[highestIndex] = IMPOSSIBLY_LOW_SCORE;
- curScore = model_getPlayerScore( model, highestIndex );
+ if ( !placeStr ) {
+ if ( 0 < placeKey ) {
+ placeStr = util_getUserString( server->vol.util, placeKey );
+ } else {
+ XP_SNPRINTF( placeBuf, VSIZE(placeBuf), "#%d", place );
+ placeStr = placeBuf;
+ }
+ }
timeStr = (XP_UCHAR*)"";
if ( gi->timerEnabled ) {
@@ -2996,11 +3080,19 @@ server_writeFinalScores( ServerCtxt* server, XWStreamCtxt* stream )
-tilePenalties.arr[highestIndex] );
XP_SNPRINTF( buf, sizeof(buf),
- (XP_UCHAR*)"[%d] %s: %d" XP_CR " (%d %s%s)",
- place,
+ (XP_UCHAR*)"[%s] %s: %d" XP_CR " (%d %s%s)", placeStr,
emptyStringIfNull(gi->players[highestIndex].name),
- highestScore, curScore, tmpbuf, timeStr );
+ scores.arr[highestIndex],
+ model_getPlayerScore( model, highestIndex ),
+ tmpbuf, timeStr );
+
+ if ( 1 < place ) {
+ stream_catString( stream, XP_CR );
+ }
stream_catString( stream, buf );
+
+ /* Don't consider this one next time around */
+ scores.arr[highestIndex] = IMPOSSIBLY_LOW_SCORE;
}
} /* server_writeFinalScores */
diff --git a/xwords4/linux/LocalizedStrIncludes.h b/xwords4/linux/LocalizedStrIncludes.h
index 66637efbe..1f75b4c02 100644
--- a/xwords4/linux/LocalizedStrIncludes.h
+++ b/xwords4/linux/LocalizedStrIncludes.h
@@ -58,6 +58,9 @@ enum {
STRD_REMAINS_HEADER,
STRD_REMAINS_EXPL,
+ STR_RESIGNED,
+ STR_WINNER,
+
STR_LAST
};
diff --git a/xwords4/linux/gtkmain.c b/xwords4/linux/gtkmain.c
index 097bb9c88..b5a0a5de9 100644
--- a/xwords4/linux/gtkmain.c
+++ b/xwords4/linux/gtkmain.c
@@ -765,8 +765,9 @@ final_scores( GtkWidget* XP_UNUSED(widget), GtkAppGlobals* globals )
catFinalScores( &globals->cGlobals );
} else {
if ( gtkask( globals->window,
- "Are you sure everybody wants to end the game now?",
+ "Are you sure you want to resign?",
GTK_BUTTONS_YES_NO ) ) {
+ globals->cGlobals.manualFinal = XP_TRUE;
server_endGame( globals->cGlobals.game.server );
gameOver = TRUE;
}
@@ -1412,7 +1413,8 @@ gtkShowFinalScores( const GtkAppGlobals* globals )
text = strFromStream( stream );
stream_destroy( stream );
- (void)gtkask_timeout( globals->window, text, GTK_BUTTONS_OK, 500 );
+ XP_U16 timeout = cGlobals->manualFinal? 0 : 500;
+ (void)gtkask_timeout( globals->window, text, GTK_BUTTONS_OK, timeout );
free( text );
} /* gtkShowFinalScores */
diff --git a/xwords4/linux/linuxutl.c b/xwords4/linux/linuxutl.c
index 9650b6bb3..94c1cdcf1 100644
--- a/xwords4/linux/linuxutl.c
+++ b/xwords4/linux/linuxutl.c
@@ -335,7 +335,10 @@ linux_util_getUserString( XW_UtilCtxt* XP_UNUSED(uc), XP_U16 code )
return (XP_UCHAR*)"%d tiles left in pool.";
case STRD_REMAINS_EXPL:
return (XP_UCHAR*)"%d tiles left in pool and all tray[s]:\n";
-
+ case STR_RESIGNED:
+ return "Resigned";
+ case STR_WINNER:
+ return "Winner";
default:
return (XP_UCHAR*)"unknown code to linux_util_getUserString";
}
diff --git a/xwords4/linux/main.h b/xwords4/linux/main.h
index e3118d603..fd9b3bf70 100644
--- a/xwords4/linux/main.h
+++ b/xwords4/linux/main.h
@@ -165,6 +165,7 @@ struct CommonGlobals {
XWGame game;
XP_U16 lastNTilesToUse;
XP_U16 lastStreamSize;
+ XP_Bool manualFinal; /* use asked for final scores */
SocketChangedFunc socketChanged;
void* socketChangedClosure;