Java tutorial
/* * 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 mobisocial.socialkit.Obj; import mobisocial.socialkit.User; import mobisocial.socialkit.musubi.DbFeed; import mobisocial.socialkit.musubi.DbObj; import mobisocial.socialkit.musubi.DbIdentity; import mobisocial.socialkit.musubi.FeedObserver; import mobisocial.socialkit.obj.MemObj; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.util.Log; /** * Manages the state machine associated with a turn-based multiplayer application. * @see TurnBasedApp * * @deprecated */ public abstract class TurnBasedMultiplayer extends Multiplayer { static final String OBJ_MEMBER_CURSOR = "member_cursor"; static final String TYPE_APP_STATE = "appstate"; static final String TYPE_INTERRUPT_REQUEST = "interrupt"; private final DbObj mObjContext; private final DbFeed mDbFeed; private final String mLocalMember; private JSONObject mLatestState; private String[] mMembers; private int mLocalMemberIndex; private int mGlobalMemberCursor; private Integer mLastTurn; public TurnBasedMultiplayer(DbObj objContext) { if (objContext == null) { throw new NullPointerException("ObjContext is null"); } mObjContext = objContext; mDbFeed = mObjContext.getSubfeed(); String[] projection = null; String selection = "type = ? OR type = ?"; String[] selectionArgs = new String[] { TYPE_APP_STATE, TYPE_INTERRUPT_REQUEST }; String sortOrder = DbObj.COL_INT_KEY + " desc"; mDbFeed.setQueryArgs(projection, selection, selectionArgs, sortOrder); mDbFeed.registerStateObserver(mInternalStateObserver); Obj obj = mDbFeed.getLatestObj(TYPE_APP_STATE); //Log.d(TAG, "The latest obj has " + obj.getIntKey()); mLocalMember = mDbFeed.getLocalUser().getId(); JSONArray membership = null; if (obj == null) { // No turn taken yet. Log.e(TAG, "App state is null."); try { membership = objContext.getJson().getJSONArray(OBJ_MEMBERSHIP); } catch (JSONException e) { Log.w(TAG, "No membership for obj context"); membership = new JSONArray(); membership.put(mLocalMember); } mLastTurn = 0; setMembershipFromJson(membership); if (objContext.getSender().getId().equals(mLocalMember)) { // Set the initial state that all members will see mLatestState = getInitialState(); Log.d(TAG, "set initial state " + mLatestState); if (mLatestState != null) { takeTurn(membership, 0, mLatestState); } } return; } // At least one turn has been taken. JSONObject json = obj.getJson(); if (json == null || !json.has(OBJ_MEMBERSHIP)) { Log.e(TAG, "App state has no membership."); mMembers = null; mLocalMemberIndex = -1; return; } JSONArray memberArr = json.optJSONArray(OBJ_MEMBERSHIP); setMembershipFromJson(memberArr); mLastTurn = (obj.getIntKey() == null) ? 0 : obj.getIntKey(); Log.d(TAG, "Read last turn " + mLastTurn); mGlobalMemberCursor = (json.has(OBJ_MEMBER_CURSOR)) ? json.optInt(OBJ_MEMBER_CURSOR) : 0; } @Override public User getLocalUser() { return mDbFeed.getLocalUser(); } /** * Returns the initial state for this turn-based game. */ protected abstract JSONObject getInitialState(); /** * 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() { return mLocalMemberIndex == mGlobalMemberCursor; } /** * 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. */ public void takeTurnOutOfOrder(JSONArray members, int nextPlayer, JSONObject state) { JSONObject out = new JSONObject(); try { out.put(OBJ_MEMBER_CURSOR, nextPlayer); out.put(OBJ_MEMBERSHIP, members); out.put("state", state); } catch (JSONException e) { Log.e(TAG, "Failed to update cursor.", e); } if (DBG) Log.d(TAG, "Attempted interrupt #" + mLastTurn); mDbFeed.postObj(new MemObj(TYPE_INTERRUPT_REQUEST, out, null, mLastTurn)); } public boolean takeTurn(JSONArray members, int nextPlayer, JSONObject state) { if (!isMyTurn()) { return false; } JSONObject out = new JSONObject(); try { mGlobalMemberCursor = nextPlayer; out.put(OBJ_MEMBER_CURSOR, mGlobalMemberCursor); out.put(OBJ_MEMBERSHIP, members); out.put("state", state); mLatestState = state; } catch (JSONException e) { Log.e(TAG, "Failed to update cursor.", e); return false; } postAppStateRenderable(out, getFeedView(state)); if (DBG) Log.d(TAG, "Sent cursor " + out.optInt(OBJ_MEMBER_CURSOR)); return true; } /** * 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. */ 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 (mLatestState == null) { Obj obj = mDbFeed.getLatestObj(TYPE_APP_STATE); if (obj != null && obj.getJson() != null && obj.getJson().has("state")) { mLatestState = obj.getJson().optJSONObject("state"); Log.d(TAG, "returning latest state " + mLatestState + "; " + obj.getIntKey()); } } return mLatestState; } /** * Handles newly received state updates. */ 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. */ 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 != mLastTurn) { 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("state")); postAppStateRenderable(out, thumb); } private final FeedObserver mInternalStateObserver = new FeedObserver() { @Override public void onUpdate(DbObj obj) { if (DBG) Log.d(TAG, "Update with " + obj.getType()); Integer turnTaken = obj.getIntKey(); if (turnTaken == null) { Log.w(TAG, "no turn taken."); return; } if (TYPE_INTERRUPT_REQUEST.equals(obj.getType())) { handleInterrupt(turnTaken, obj); return; } if (turnTaken <= mLastTurn) { Log.w(TAG, "Turn " + turnTaken + " is at/before known turn " + mLastTurn); return; } mLastTurn = turnTaken; JSONObject newState = obj.getJson(); if (newState == null || !newState.has("state")) return; try { if (newState.has(OBJ_MEMBERSHIP)) { setMembershipFromJson(newState.getJSONArray(OBJ_MEMBERSHIP)); } mLatestState = newState.optJSONObject("state"); mGlobalMemberCursor = newState.getInt(OBJ_MEMBER_CURSOR); if (DBG) Log.d(TAG, "Updated cursor to " + mGlobalMemberCursor); } catch (JSONException e) { Log.e(TAG, "Failed to get member_cursor from " + newState); } onStateUpdate(mLatestState); } }; /** * 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) { } } public interface StateObserver { public void onUpdate(JSONObject state); } }