From: Nathaniel Wesley Filardo Date: Sat, 11 Nov 2017 06:39:17 +0000 (-0500) Subject: Improve encapsulation of game state X-Git-Tag: release-1.2~22 X-Git-Url: https://hydra-www.ietfng.org/gitweb/?a=commitdiff_plain;h=992ec2a9076f3cfa2e0f1853bdcef60f2489a0fe;p=acmetensortoys-ctfws-android Improve encapsulation of game state And deduplication of messages, I hope. --- diff --git a/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java b/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameStateManager.java similarity index 55% rename from lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java rename to lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameStateManager.java index 96bf735..389afd9 100644 --- a/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java +++ b/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameStateManager.java @@ -8,61 +8,85 @@ import java.util.NoSuchElementException; import java.util.Scanner; import java.util.Set; -public class CtFwSGameState { +public class CtFwSGameStateManager { private final TimerProvider mT; - public CtFwSGameState (TimerProvider t) { + public CtFwSGameStateManager(TimerProvider t) { mT = t; } - // Game time - private boolean configured = false; - private long startT; // POSIX seconds for game start - private int setupD; - private int rounds; - private int roundD; - private int gameIx; - private long endT = 0; // POSIX seconds for game end (if >= startT) + private class Game { + // Game time + private boolean configured = false; + private long startT; // POSIX seconds for game start + private int setupD; + private int rounds; + private int roundD; + private int gameIx; + private long endT = 0; // POSIX seconds for game end (if >= startT) + + public int flagsTotal; + + public boolean equals(Game g) { + return (this.configured == g.configured) + && (this.startT == g.startT) + && (this.setupD == g.setupD) + && (this.rounds == g.rounds) + && (this.roundD == g.roundD) + && (this.gameIx == g.gameIx) + && (this.endT == g.endT) + && (this.flagsTotal == g.flagsTotal); + } + }; + private Game curstate = new Game(); public synchronized void fromMqttConfigMessage(String st) { + Game g = new Game(); + String tm = st.trim(); switch (tm) { case "none": - this.configured = false; + g.configured = false; break; default: try { Scanner s = new Scanner(tm); - this.startT = s.nextLong(); - this.setupD = s.nextInt(); - this.rounds = s.nextInt(); - this.roundD = s.nextInt(); - this.flagsTotal = s.nextInt(); - this.gameIx = s.nextInt(); - this.configured = true; + g.startT = s.nextLong(); + g.setupD = s.nextInt(); + g.rounds = s.nextInt(); + g.roundD = s.nextInt(); + g.flagsTotal = s.nextInt(); + g.gameIx = s.nextInt(); + g.configured = true; } catch (NoSuchElementException e) { - this.configured = false; + g.configured = false; } break; } - notifyConfigEtAl(); + if (!curstate.equals(g)) { + curstate = g; + notifyConfigEtAl(); + } } public synchronized String toMqttConfigMessage() { - if (!configured) { + if (!curstate.configured) { return "none"; } return String.format(Locale.ROOT, "%d %d %d %d %d", - startT, setupD, rounds, roundD, flagsTotal); + curstate.startT, curstate.setupD, curstate.rounds, + curstate.roundD, curstate.flagsTotal); } public synchronized void deconfigure() { - this.configured = false; + curstate.configured = false; notifyConfigEtAl(); } public synchronized void setEndT(long endT) { - this.endT = endT; - notifyConfigEtAl(); + if (endT != curstate.endT) { + curstate.endT = endT; + notifyConfigEtAl(); + } } public class Now { @@ -81,37 +105,37 @@ public class CtFwSGameState { long now = wallMS/1000; synchronized (this) { - if (!configured) { + if (!curstate.configured) { res.rationale = "Game not configured!"; res.stop = true; - } else if (endT >= startT) { + } else if (curstate.endT >= curstate.startT) { res.rationale = "Game declared over!"; res.stop = true; res.past = true; - } else if (now < startT) { + } else if (now < curstate.startT) { res.rationale = "Start time in the future!"; - res.roundStart = res.roundEnd = startT; + res.roundStart = res.roundEnd = curstate.startT; } if (res.rationale != null) { return res; } - long elapsed = now - startT; - if (elapsed < setupD) { + long elapsed = now - curstate.startT; + if (elapsed < curstate.setupD) { res.round = 0; - res.roundStart = startT; - res.roundEnd = startT + setupD; + res.roundStart = curstate.startT; + res.roundEnd = curstate.startT + curstate.setupD; return res; } - elapsed -= setupD; - res.round = (int) (elapsed / roundD); - if (res.round >= rounds) { + elapsed -= curstate.setupD; + res.round = (int) (elapsed / curstate.roundD); + if (res.round >= curstate.rounds) { res.rationale = "Game time up!"; res.stop = true; res.past = true; return res; } - res.roundStart = startT + setupD + (res.round * roundD); - res.roundEnd = res.roundStart + roundD; + res.roundStart = curstate.startT + curstate.setupD + (res.round * curstate.roundD); + res.roundEnd = res.roundStart + curstate.roundD; res.round += 1; return res; } @@ -119,53 +143,70 @@ public class CtFwSGameState { // Callbacks used for observers, which run in synchronized(this) context. Generally // unwise to call these outside such a context. - public boolean isConfigured(){ return configured; } - public int getGameIx() { return gameIx; } - public long getStartT() { return startT; } - public long getFirstRoundStartT() { return startT + setupD; } - public int getRounds() { return rounds; } - public int getComputedGameDuration() { return rounds * roundD ; } + public boolean isConfigured(){ return curstate.configured; } + public int getGameIx() { return curstate.gameIx; } + public long getStartT() { return curstate.startT; } + public long getFirstRoundStartT() { return curstate.startT + curstate.setupD; } + public int getRounds() { return curstate.rounds; } + public int getComputedGameDuration() { return curstate.rounds * curstate.roundD ; } + public int getFlagsTotal() { return curstate.flagsTotal; } // Leaves off the natural endT comparison so that messages can be posted after the // game ends and still count as part of this one (i.e. still be displayed). private boolean isMessageTimeWithin(long time) { - return !configured || time >= startT; + return !curstate.configured || time >= curstate.startT; } // Game score - public int flagsTotal; - public boolean flagsVisible = false; - public int flagsRed = 0; - public int flagsYel = 0; + private class Flags { + public boolean flagsVisible = false; + public int flagsRed = 0; + public int flagsYel = 0; + + public boolean equals(Flags f) { + return (this.flagsVisible == f.flagsVisible) + && (this.flagsRed == f.flagsRed) + && (this.flagsYel == f.flagsYel); + } + } + private Flags curflags = new Flags(); public synchronized void fromMqttFlagsMessage(String st) { + Flags f = new Flags(); String tm = st.trim(); switch(tm) { case "?": - flagsVisible = false; + f.flagsVisible = false; break; default: Scanner s = new Scanner(tm); try { - flagsVisible = true; + f.flagsVisible = true; int red = s.nextInt(); int yel = s.nextInt(); - flagsRed = red; - flagsYel = yel; + f.flagsRed = red; + f.flagsYel = yel; } catch (NumberFormatException e) { - flagsVisible = false; + f.flagsVisible = false; } } - notifyFlags(); + if (!curflags.equals(f)) { + curflags = f; + notifyFlags(); + } } public synchronized String toMqttFlagsMessage() { - if (!configured || !flagsVisible) { + if (!curstate.configured || !curflags.flagsVisible) { return "?"; } - return String.format(Locale.ROOT, "%d %d", flagsRed, flagsYel); + return String.format(Locale.ROOT, "%d %d", curflags.flagsRed, curflags.flagsYel); } + public boolean getFlagsVisible() { return curflags.flagsVisible; } + // Only sensible if flags visible + public int getFlagsRed() { return curflags.flagsRed; } + public int getFlagsYel() { return curflags.flagsYel; } // Informative messages handling - public class Msg { + public class Msg implements Comparable { public final long when; public final String msg; @@ -173,36 +214,48 @@ public class CtFwSGameState { this.when = when; this.msg = msg; } + + public int compareTo(Msg m) { + if (this.when == m.when) { + return this.msg.compareTo(m.msg); + } + return (Long.valueOf(when).compareTo(Long.valueOf(m.when))); + } } private final List msgs = new ArrayList<>(); private long lastMsgTimestamp; public void onNewMessage(String str) { + Msg m = null; + Scanner s = new Scanner(str); - long t; + long t = 0; try { t = s.nextLong(); } catch (NoSuchElementException nse) { - // Maybe they forgot a time stamp. That's not ideal, but... fake it? + // Maybe they forgot a time stamp; use round start. + // That's not ideal, but... fake it? // XXX Back off a bit, for time sync reasons synchronized (this) { - lastMsgTimestamp = mT.wallMS() / 1000 - 30; - msgs.add(new Msg(lastMsgTimestamp, str)); - notifyMessages(); - return; + lastMsgTimestamp = 0; + m = new Msg(lastMsgTimestamp, str); } } - s.useDelimiter("\\z"); - String msg = s.next().trim(); + if (m == null) { + s.useDelimiter("\\z"); + new Msg(t, s.next().trim()); + } synchronized (this) { // If there is no configuration, assume the message is new enough // If there *is* a configuration, check the time. if (isMessageTimeWithin(t) && (lastMsgTimestamp <= t)) { lastMsgTimestamp = t; - msgs.add(new Msg(t, msg)); - notifyMessages(); + if (!msgs.contains(m)) { + msgs.add(m); + notifyMessages(); + } } } } @@ -210,20 +263,20 @@ public class CtFwSGameState { // Observer interface public interface Observer { // Called when the game configuration parameters change - void onCtFwSConfigure(CtFwSGameState game); + void onCtFwSConfigure(CtFwSGameStateManager game); // Called when the game parameters change and at round boundaries during the game; // this is an excellent thing to hook for UI update (including updating one's own, // more finely-grained timers). The Now argument captures the state of the game // immediately prior to the dispatch of callbacks. - void onCtFwSNow(CtFwSGameState game, Now now); + void onCtFwSNow(CtFwSGameStateManager game, Now now); // Called when a flag message arrives. - void onCtFwSFlags(CtFwSGameState game); + void onCtFwSFlags(CtFwSGameStateManager game); // Called when a human-readable message arrives or when a new game starts (to // empty the list), and is given the entire set of messages received this game // (or since the last), even though usually one only cares about the most recent // entry on the list. We reserve the right to trim this list in the future, but // at the moment we do not. Callees should not alter the list in any way. - void onCtFwSMessage(CtFwSGameState game, List msgs); + void onCtFwSMessage(CtFwSGameStateManager game, List msgs); } final private Set mObsvs = new HashSet<>(); private synchronized void notifyFlags() { @@ -260,7 +313,7 @@ public class CtFwSGameState { // Synchronize observer with game state as of right now. d.onCtFwSConfigure(this); d.onCtFwSMessage(this, msgs); - if (configured) { + if (curstate.configured) { d.onCtFwSFlags(this); d.onCtFwSNow(this, getNow(mT.wallMS())); } diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java index 94ccc98..4f976f1 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java @@ -2,15 +2,15 @@ package com.acmetensortoys.ctfwstimer; import android.util.Log; -import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState; +import com.acmetensortoys.ctfwstimer.lib.CtFwSGameStateManager; import org.eclipse.paho.client.mqttv3.IMqttMessageListener; import org.eclipse.paho.client.mqttv3.MqttMessage; class CtFwSCallbacksMQTT { - private CtFwSGameState mCgs; + private CtFwSGameStateManager mCgs; - CtFwSCallbacksMQTT(CtFwSGameState cgs) { + CtFwSCallbacksMQTT(CtFwSGameStateManager cgs) { mCgs = cgs; } diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java index 81c0356..7a73859 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java @@ -8,19 +8,18 @@ import android.os.SystemClock; import android.text.format.DateUtils; import android.util.Log; import android.view.View; -import android.widget.Button; import android.widget.Chronometer; import android.widget.ProgressBar; import android.widget.TextView; -import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState; +import com.acmetensortoys.ctfwstimer.lib.CtFwSGameStateManager; import java.util.List; import static android.view.View.INVISIBLE; // TODO nwf is bad at UI design; someone who isn't him should improve this -class CtFwSDisplayLocal implements CtFwSGameState.Observer { +class CtFwSDisplayLocal implements CtFwSGameStateManager.Observer { final private Activity mAct; String gameStateLabelText; @@ -63,7 +62,7 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { if(es.length > 1) { resumeTimer(stun_long, es[1]); } } - private void doSetGameStateLabelText(final CtFwSGameState gs, String rationale) { + private void doSetGameStateLabelText(final CtFwSGameStateManager gs, String rationale) { int gameIndex = gs.getGameIx(); String pfx = @@ -91,12 +90,12 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } @Override - public void onCtFwSConfigure(final CtFwSGameState gs) { + public void onCtFwSConfigure(final CtFwSGameStateManager gs) { doSetGameStateLabelText(gs, null); } @Override - public void onCtFwSNow(final CtFwSGameState gs, final CtFwSGameState.Now now) { + public void onCtFwSNow(final CtFwSGameStateManager gs, final CtFwSGameStateManager.Now now) { // time base correction factor ("when we booted"-ish) final long tbcf = System.currentTimeMillis() - SystemClock.elapsedRealtime(); @@ -219,7 +218,9 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { @Override public void run() { tv_flags.setText(mAct.getResources() - .getQuantityString(R.plurals.ctfws_flags,gs.flagsTotal,gs.flagsTotal)); + .getQuantityString(R.plurals.ctfws_flags, + gs.getFlagsTotal(), + gs.getFlagsTotal())); } }); } @@ -272,16 +273,16 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } @Override - public void onCtFwSFlags(CtFwSGameState gs) { + public void onCtFwSFlags(CtFwSGameStateManager gs) { // TODO: This stinks final StringBuffer sb = new StringBuffer(); if (gs.isConfigured()) { - if (gs.flagsVisible) { + if (gs.getFlagsVisible()) { sb.append("r="); - sb.append(gs.flagsRed); + sb.append(gs.getFlagsRed()); sb.append(" y="); - sb.append(gs.flagsYel); + sb.append(gs.getFlagsYel()); } else { sb.append("r=? y=?"); } @@ -297,7 +298,7 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } @Override - public void onCtFwSMessage(CtFwSGameState gs, List msgs) { + public void onCtFwSMessage(CtFwSGameStateManager gs, List msgs) { final TextView msgstv = (TextView) (mAct.findViewById(R.id.msgs)); int s = msgs.size(); @@ -309,7 +310,7 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } }); } else { - CtFwSGameState.Msg m = msgs.get(s - 1); + CtFwSGameStateManager.Msg m = msgs.get(s - 1); long td = (m.when == 0) ? 0 : (gs.isConfigured()) ? m.when - gs.getStartT() : 0; diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java index 9affb09..b7d6ad7 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java @@ -10,7 +10,7 @@ import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.util.Log; -import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState; +import com.acmetensortoys.ctfwstimer.lib.CtFwSGameStateManager; import com.acmetensortoys.ctfwstimer.lib.TimerProvider; import org.eclipse.paho.android.service.MqttAndroidClient; @@ -34,8 +34,8 @@ public class MainService extends Service { private Handler mHandler; // set in onCreate // The reason we're here! - private final CtFwSGameState mCgs - = new CtFwSGameState(new TimerProvider() { + private final CtFwSGameStateManager mCgs + = new CtFwSGameStateManager(new TimerProvider() { @Override public long wallMS() { return System.currentTimeMillis(); @@ -297,7 +297,7 @@ public class MainService extends Service { } public class LocalBinder extends Binder { - CtFwSGameState getGameState() { + CtFwSGameStateManager getGameState() { return mCgs; } diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java index 47b896f..83903b0 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java @@ -13,7 +13,7 @@ import android.os.IBinder; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; -import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState; +import com.acmetensortoys.ctfwstimer.lib.CtFwSGameStateManager; import java.util.List; @@ -27,7 +27,7 @@ class MainServiceNotification { private enum LastContentTextSource { NONE, FLAG, MESG } private LastContentTextSource lastContextTextSource = LastContentTextSource.NONE; - MainServiceNotification(MainService ms, CtFwSGameState game){ + MainServiceNotification(MainService ms, CtFwSGameStateManager game){ mService = ms; Intent ni = new Intent(ms, MainActivity.class); @@ -40,12 +40,12 @@ class MainServiceNotification { .setSmallIcon(R.drawable.shield1) .setContentIntent(PendingIntent.getActivity(ms, 0, ni, 0)); - game.registerObserver(new CtFwSGameState.Observer() { + game.registerObserver(new CtFwSGameStateManager.Observer() { @Override - public void onCtFwSConfigure(CtFwSGameState game) { } + public void onCtFwSConfigure(CtFwSGameStateManager game) { } @Override - public void onCtFwSNow(CtFwSGameState game, CtFwSGameState.Now now) { + public void onCtFwSNow(CtFwSGameStateManager game, CtFwSGameStateManager.Now now) { if (now.rationale == null || !now.stop) { // game is afoot or in the future! @@ -89,24 +89,24 @@ class MainServiceNotification { } @Override - public void onCtFwSFlags(CtFwSGameState game) { + public void onCtFwSFlags(CtFwSGameStateManager game) { // If flags are hidden or there aren't any captured (e.g. this is a notification // of a reset to 0), don't do anything, unless the flags were the last thing // asserted, in which case, we allow a correction. - if (game.flagsVisible + if (game.getFlagsVisible() && ((lastContextTextSource == LastContentTextSource.FLAG) - || (game.flagsRed + game.flagsYel > 0))) { + || (game.getFlagsRed() + game.getFlagsYel() > 0))) { notifyUserSomehow(NotificationSource.FLAG); lastContextTextSource = LastContentTextSource.FLAG; userNoteBuilder.setContentText( String.format(mService.getResources().getString(R.string.notify_flags), - game.flagsRed, game.flagsYel)); + game.getFlagsRed(), game.getFlagsYel())); refreshNotification(); } } @Override - public void onCtFwSMessage(CtFwSGameState game, List msgs) { + public void onCtFwSMessage(CtFwSGameStateManager game, List msgs) { // Only do anything if we aren't clearing the message list int s = msgs.size(); if (s != 0) {