]> hydra-www.ietfng.org Git - acmetensortoys-ctfws-android/commitdiff
Improve encapsulation of game state
authorNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Sat, 11 Nov 2017 06:39:17 +0000 (01:39 -0500)
committerNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Sat, 11 Nov 2017 06:40:11 +0000 (01:40 -0500)
And deduplication of messages, I hope.

lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameStateManager.java [moved from lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java with 55% similarity]
mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java
mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java
mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java
mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.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 96bf7354bbd42823fa9008fafda4139c848b78b7..389afd9fc46c641ffc0be460a271a2ad20b767b0 100644 (file)
@@ -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<Msg> {
         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<Msg> 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<Msg> msgs);
+        void onCtFwSMessage(CtFwSGameStateManager game, List<Msg> msgs);
     }
     final private Set<Observer> 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()));
             }
index 94ccc9838777abeb3b0a17b25082d8d97712bb5d..4f976f1eb4c39ce8460d09c6dbe5205b591a96f6 100644 (file)
@@ -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;
     }
 
index 81c0356a34af0b7dfa286cc6794ed950262d2981..7a7385997e5b3840e9a4c06929600eabfb73fe62 100644 (file)
@@ -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<CtFwSGameState.Msg> msgs) {
+    public void onCtFwSMessage(CtFwSGameStateManager gs, List<CtFwSGameStateManager.Msg> 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;
 
index 9affb092edd0f732f2ae683f4a33214cb2245826..b7d6ad70e17062bd428d16bf6513179a72b896d3 100644 (file)
@@ -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;
         }
 
index 47b896fd385868ea9afd0ff4bdc440a2e355ed5f..83903b0d40ceafaf32bedf519e36eb6dadcb34dd 100644 (file)
@@ -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<CtFwSGameState.Msg> msgs) {
+            public void onCtFwSMessage(CtFwSGameStateManager game, List<CtFwSGameStateManager.Msg> msgs) {
                 // Only do anything if we aren't clearing the message list
                 int s = msgs.size();
                 if (s != 0) {