mobisocial.socialkit.musubi.multiplayer.TurnBasedApp.java Source code

Java tutorial

Introduction

Here is the source code for mobisocial.socialkit.musubi.multiplayer.TurnBasedApp.java

Source

/*
 * Copyright (C) 2011 The Stanford MobiSocial Laboratory
 *
 * 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.
 */

package mobisocial.socialkit.musubi.multiplayer;

import java.util.List;

import mobisocial.socialkit.Obj;
import mobisocial.socialkit.User;
import mobisocial.socialkit.musubi.DbFeed;
import mobisocial.socialkit.musubi.DbIdentity;
import mobisocial.socialkit.musubi.DbObj;
import mobisocial.socialkit.musubi.Musubi;
import mobisocial.socialkit.obj.MemObj;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;

/**
 * Manages the state machine associated with a turn-based multiplayer application.
 */
public abstract class TurnBasedApp extends Multiplayer {
    protected boolean DBG = false;
    protected static final int NO_TURN = -1;

    static final String OBJ_STATE = "state";
    static final String OBJ_MEMBER_CURSOR = "member_cursor";

    /**
     * AppState objects provide state updates to an application instance.
     */
    static final String TYPE_APP_STATE = "appstate";

    /**
     * Turn interrupts are attempts to update the app's state when a user doesn't
     * own the lock on that app's state.
     */
    static final String TYPE_INTERRUPT_REQUEST = "interrupt";

    private final Musubi mMusubi;
    private final ContentObserver mObserver;
    private final DbObj mObjContext;
    private final DbFeed mDbFeed;
    private final String mLocalMember;
    private boolean mObservingUpdates;

    private JSONObject mLatestState;
    private String[] mMembers;
    private int mLocalMemberIndex;
    private int mGlobalMemberCursor;
    private int mLastTurn = NO_TURN;

    /**
     * Prepares a new TurnBasedApp object that can be inserted into a feed.
     * Once inserted, the object can be used to create a new TurnBasedApp
     * instance via {@link TurnBasedApp#TurnBasedApp(DbObj)}.
     */
    public static Obj newInstance(String type, List<DbIdentity> participants, JSONObject initialState) {
        JSONObject json = new JSONObject();
        JSONArray members = new JSONArray();
        for (DbIdentity id : participants) {
            members.put(id.getId());
        }
        try {
            json.put(OBJ_STATE, initialState);
            json.put(OBJ_MEMBERSHIP, members);
            json.put(OBJ_MEMBER_CURSOR, 0);
            json.put(Obj.FIELD_RENDER_TYPE, Obj.RENDER_LATEST);
        } catch (JSONException e) {
            throw new IllegalArgumentException(e);
        }
        return new MemObj(type, json, null, 0);
    }

    public TurnBasedApp(Musubi musubi, DbObj objContext) {
        if (musubi == null || objContext == null) {
            throw new NullPointerException("ObjContext is null");
        }

        mMusubi = musubi;
        mObjContext = objContext;
        mDbFeed = mObjContext.getSubfeed();
        mObserver = new TurnObserver(new Handler(mMusubi.getContext().getMainLooper()));
        mLocalMember = mDbFeed.getLocalUser().getId();
    }

    DbObj fetchLatestState() {
        String[] projection = null;
        String selection = "type = ?";
        String[] args = new String[] { TYPE_APP_STATE };
        String sortOrder = DbObj.COL_INT_KEY + " desc LIMIT 1";
        Cursor cursor = mDbFeed.query(projection, selection, args, sortOrder);
        if (cursor != null && cursor.moveToFirst()) {
            try {
                return mMusubi.objForCursor(cursor);
            } finally {
                cursor.close();
            }
        }
        return null;
    }

    DbObj fetchLatestInterrupt() {
        String[] projection = null;
        String selection = "type = ?";
        String[] args = new String[] { TYPE_INTERRUPT_REQUEST };
        String sortOrder = DbObj.COL_INT_KEY + " desc LIMIT 1";
        Cursor cursor = mDbFeed.query(projection, selection, args, sortOrder);
        if (cursor != null && cursor.moveToFirst()) {
            try {
                return mMusubi.objForCursor(cursor);
            } finally {
                cursor.close();
            }
        }
        return null;
    }

    /**
     * Often called in an activity's onResume method.
     */
    public void enableStateUpdates() {
        Uri uri = mDbFeed.getUri();
        mMusubi.getContext().getContentResolver().registerContentObserver(uri, false, mObserver);
        mObserver.onChange(false); // keep getLatestState() synchronized
        mObservingUpdates = true;
    }

    /**
     * Often called in an activity's onPause method.
     */
    public void disableStateUpdates() {
        mMusubi.getContext().getContentResolver().unregisterContentObserver(mObserver);
        mObservingUpdates = false;
    }

    @Override
    public User getLocalUser() {
        return mDbFeed.getLocalUser();
    }

    /**
     * Returns a view suitable for display in a feed.
     */
    protected abstract FeedRenderable getFeedView(JSONObject state);

    private void setMembershipFromJson(JSONArray memberArr) {
        mMembers = new String[memberArr.length()];
        for (int i = 0; i < memberArr.length(); i++) {
            mMembers[i] = memberArr.optString(i);
            if (mMembers[i].equals(mLocalMember)) {
                mLocalMemberIndex = i;
            }
        }
    }

    /**
     * Returns the index within the membership list that represents the
     * local user.
     */
    public int getLocalMemberIndex() {
        return mLocalMemberIndex;
    }

    /**
     * Returns a cursor within the membership list that points to
     * the user with control of the state machine.
     */
    public int getGlobalMemberCursor() {
        return mGlobalMemberCursor;
    }

    /**
     * Returns true if the local member index equals the membership cursor.
     * In other words, its the local user's turn.
     */
    public boolean isMyTurn() {
        if (mGlobalMemberCursor < 0 || mGlobalMemberCursor >= mMembers.length) {
            throw new IllegalStateException("Invalid global member cursor");
        }
        DbIdentity potential = mDbFeed.userForGlobalId(mMembers[mGlobalMemberCursor]);
        if (potential == null) {
            throw new IllegalStateException("app member not a feed member " + "#" + mGlobalMemberCursor + "="
                    + mMembers[mGlobalMemberCursor]);
        }
        return potential.isOwned();
    }

    /**
     * Attempts to take a turn despite it not being my turn.
     * Send a UpdateOutOfOrderObj with n+1 as int field.
     * 
     * If it's my turn, I listen for interrupt requests and,
     * if I see one that is agreeable, I allow it by rebroadcasting
     * as a state update.
     *
     * @hide
     */
    public void takeTurnOutOfOrder(JSONArray members, int nextPlayer, JSONObject state) {
        getLatestState(); // force update
        JSONObject out = new JSONObject();
        try {
            out.put(OBJ_MEMBER_CURSOR, nextPlayer);
            out.put(OBJ_MEMBERSHIP, members);
            out.put(OBJ_STATE, state);

            if (DBG)
                Log.d(TAG, "Attempted interrupt #" + mLastTurn);
            mDbFeed.postObj(new MemObj(TYPE_INTERRUPT_REQUEST, out, null, mLastTurn));
        } catch (JSONException e) {
            Log.e(TAG, "Failed to update cursor.", e);
        }
    }

    public boolean takeTurn(JSONArray members, int nextPlayer, JSONObject state) {
        getLatestState(); // force update
        if (!isMyTurn()) {
            return false;
        }
        JSONObject out = new JSONObject();
        try {
            mGlobalMemberCursor = nextPlayer;
            out.put(OBJ_MEMBER_CURSOR, mGlobalMemberCursor);
            out.put(OBJ_MEMBERSHIP, members);
            out.put(OBJ_STATE, state);
            mLatestState = state;

            postAppStateRenderable(out, getFeedView(state));
            if (DBG)
                Log.d(TAG, "Sent cursor " + out.optInt(OBJ_MEMBER_CURSOR));
            return true;
        } catch (JSONException e) {
            Log.e(TAG, "Failed to update cursor.", e);
            return false;
        }
    }

    /**
     * Updates the state machine with the user's move, passing control
     * to nextPlayer. The state machine is only updated if it is the
     * local user's turn.
     * @return true if a turn was taken.
     */
    public boolean takeTurn(int nextPlayer, JSONObject state) {
        return takeTurn(membersJsonArray(), nextPlayer, state);
    }

    /**
     * Updates the state machine with the user's move, passing control to
     * the next player in {@link #getMembers()}. The state machine
     * is only updated if it is the local user's turn.
     * @return true if a turn was taken.
     */
    public boolean takeTurn(JSONObject state) {
        int next = (mGlobalMemberCursor + 1) % mMembers.length;
        return takeTurn(next, state);
    }

    /**
     * Takes the last turn in this turn-based game.
     *
     * @hide
     */
    public void takeFinalTurn(GameResult result, FeedRenderable display) {
        postAppStateRenderable(result.getJson(), display);
    }

    public JSONArray membersJsonArray() {
        JSONArray r = new JSONArray();
        for (String m : mMembers) {
            r.put(m);
        }
        return r;
    }

    /**
     * Returns the latest application state.
     */
    public JSONObject getLatestState() {
        if (!mObservingUpdates) {
            mObserver.onChange(false);
        }
        return mLatestState;
    }

    public int getLastTurnNumber() {
        if (!mObservingUpdates) {
            mObserver.onChange(false);
        }
        return mLastTurn;
    }

    /**
     * Override this method to handle state updates to this turn-basd app.
     */
    protected abstract void onStateUpdate(JSONObject json);

    /**
     * Handles an interrupt request. It may be useful to override this method
     * to customize for your needs, be mindful of concurrency issues.
     *
     * @hide
     */
    protected void handleInterrupt(int turnRequested, DbObj obj) {
        if (DBG)
            Log.d(TAG, "Incoming interrupt!");
        if (!isMyTurn()) {
            if (DBG)
                Log.d(TAG, "not my turn.");
            return;
        }

        if (turnRequested != getLastTurnNumber()) {
            if (DBG)
                Log.d(TAG, "stale state.");
            return;
        }
        if (DBG)
            Log.d(TAG, "interrupting with " + obj);
        JSONObject out = obj.getJson();
        FeedRenderable thumb = getFeedView(out.optJSONObject(OBJ_STATE));
        postAppStateRenderable(out, thumb);
    }

    /**
     * Returns the array of member identifiers.
     */
    public String[] getMembers() {
        return mMembers;
    }

    public DbIdentity getUser(int memberIndex) {
        return mDbFeed.userForGlobalId(mMembers[memberIndex]);
    }

    private void postAppStateRenderable(JSONObject state, FeedRenderable thumbnail) {
        try {
            JSONObject b = new JSONObject(state.toString());
            if (thumbnail != null) {
                thumbnail.addToJson(b);
            }
            mDbFeed.postObj(new MemObj(TYPE_APP_STATE, b, null, mLastTurn + 1));
        } catch (JSONException e) {
        }
    }

    /**
     * Monitors this turn-based app's subfeed for state updates and populates related
     * state member variables.
     */
    class TurnObserver extends ContentObserver {
        public TurnObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            updateState(selfChange);
            attemptInterrupt();
        }

        void updateState(boolean explicitRequest) {
            DbObj obj = fetchLatestState();
            if (DBG)
                Log.e(TAG, "fetched " + obj);
            if (obj == null) {
                obj = mObjContext;
            }

            JSONObject json = obj.getJson();
            if (json == null || !json.has(OBJ_MEMBERSHIP)) {
                if (DBG)
                    Log.e(TAG, "App state has no membership.");
                mMembers = null;
                mLocalMemberIndex = -1;
                mGlobalMemberCursor = 0;
                return;
            }
            try {
                setMembershipFromJson(json.getJSONArray(OBJ_MEMBERSHIP));
            } catch (JSONException e) {
                Log.e(TAG, "Error parsing membership", e);
                return;
            }

            Integer turnTaken = obj.getIntKey();
            if (turnTaken == null) {
                if (DBG)
                    Log.e(TAG, "no turn taken.");
                return;
            }

            if (mLastTurn != NO_TURN && turnTaken <= mLastTurn) {
                if (DBG)
                    Log.d(TAG, "Turn " + turnTaken + " is at/before known turn " + mLastTurn);
                return;
            }
            mLastTurn = turnTaken;
            JSONObject newState = obj.getJson();
            if (newState == null || !newState.has(OBJ_STATE)) {
                if (DBG)
                    Log.w(TAG, "No state for update " + obj);
                return;
            }
            try {
                mLatestState = newState.optJSONObject(OBJ_STATE);
                mGlobalMemberCursor = newState.getInt(OBJ_MEMBER_CURSOR);
                if (DBG)
                    Log.i(TAG, "Updated cursor to " + mGlobalMemberCursor);
            } catch (JSONException e) {
                Log.e(TAG, "Failed to get member_cursor from " + newState);
            }

            if (!explicitRequest) {
                onStateUpdate(mLatestState);
            }
        }

        void attemptInterrupt() {
            DbObj interrupt = fetchLatestInterrupt();
            if (interrupt == null) {
                return;
            }
            Integer turnRequested = interrupt.getIntKey();
            if (turnRequested == null || turnRequested < mLastTurn) {
                return;
            }
            handleInterrupt(turnRequested, interrupt);
        }
    }
}