Java tutorial
/* Copyright (C) 2013 Google Inc. * * 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 com.sdspikes.fireworks; import android.app.ActionBar; import android.app.Activity; import android.app.FragmentManager; import android.content.Intent; import android.os.Bundle; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.games.Games; import com.google.android.gms.games.GamesStatusCodes; import com.google.android.gms.games.GamesActivityResultCodes; import com.google.android.gms.games.multiplayer.Invitation; import com.google.android.gms.games.multiplayer.Multiplayer; import com.google.android.gms.games.multiplayer.OnInvitationReceivedListener; import com.google.android.gms.games.multiplayer.Participant; import com.google.android.gms.games.multiplayer.realtime.RealTimeMessage; import com.google.android.gms.games.multiplayer.realtime.RealTimeMessageReceivedListener; import com.google.android.gms.games.multiplayer.realtime.RealTimeMultiplayer; import com.google.android.gms.games.multiplayer.realtime.Room; import com.google.android.gms.games.multiplayer.realtime.RoomConfig; import com.google.android.gms.games.multiplayer.realtime.RoomStatusUpdateListener; import com.google.android.gms.games.multiplayer.realtime.RoomUpdateListener; import com.google.android.gms.plus.Plus; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; /** * Button Clicker 2000. A minimalistic game showing the multiplayer features of * the Google Play game services API. The objective of this game is clicking a * button. Whoever clicks the button the most times within a 20 second interval * wins. It's that simple. This game can be played with 2, 3 or 4 players. The * code is organized in sections in order to make understanding as clear as * possible. We start with the integration section where we show how the game * is integrated with the Google Play game services API, then move on to * game-specific UI and logic. * <p/> * INSTRUCTIONS: To run this sample, please set up * a project in the Developer Console. Then, place your app ID on * res/values/ids.xml. Also, change the package name to the package name you * used to create the client ID in Developer Console. Make sure you sign the * APK with the certificate whose fingerprint you entered in Developer Console * when creating your Client Id. * * @author Bruno Oliveira (btco), 2013-04-26 */ public class FireworksActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, View.OnClickListener, RealTimeMessageReceivedListener, RoomStatusUpdateListener, RoomUpdateListener, OnInvitationReceivedListener, HandFragment.OnFragmentInteractionListener { /* * API INTEGRATION SECTION. This section contains the code that integrates * the game with the Google Play game services API. */ public static final String TAG = "FireworksActivity"; // Request codes for the UIs that we show with startActivityForResult: final static int RC_SELECT_PLAYERS = 10000; final static int RC_INVITATION_INBOX = 10001; final static int RC_WAITING_ROOM = 10002; public static final int MIN_NUM_PLAYERS = 2; public static final int MAX_NUM_PLAYERS = 5; // Request code used to invoke sign in user interactions. private static final int RC_SIGN_IN = 9001; public static final int MAX_CARD_WIDTH = 40; public static final int MIN_CARD_WIDTH = 20; // Client used to interact with Google APIs. private GoogleApiClient mGoogleApiClient; // Are we currently resolving a connection failure? private boolean mResolvingConnectionFailure = false; // Has the user clicked the sign-in button? private boolean mSignInClicked = false; // Set to true to automatically start the sign in flow when the Activity starts. // Set to false to require the user to click the button in order to sign in. private boolean mAutoStartSignInFlow = true; // Room ID where the currently active game is taking place; null if we're // not playing. private String mRoomId = null; private Room mRoom = null; // Are we playing in multiplayer mode? private boolean mMultiplayer = false; // Is the game done? private boolean mGameComplete = false; // The participants in the currently active game private List<Participant> mParticipants = null; // If non-null, this is the id of the invitation we received via the // invitation listener private String mIncomingInvitationId = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fireworks); // Create the Google Api Client with access to Plus and Games mGoogleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(this) .addOnConnectionFailedListener(this).addApi(Plus.API).addScope(Plus.SCOPE_PLUS_LOGIN) .addApi(Games.API).addScope(Games.SCOPE_GAMES).build(); // set up a click listener for everything we care about for (int id : CLICKABLES) { findViewById(id).setOnClickListener(this); } } @Override public void onClick(View v) { Intent intent; switch (v.getId()) { case R.id.button_single_player: case R.id.button_single_player_2: // play a single-player game resetGameVars(); startGame(false); break; case R.id.button_sign_in: // user wants to sign in // Check to see the developer who's running this sample code read the instructions :-) // NOTE: this check is here only because this is a sample! Don't include this // check in your actual production app. if (!BaseGameUtils.verifySampleSetup(this, R.string.app_id)) { Log.w(TAG, "*** Warning: setup problems detected. Sign in may not work!"); } // start the sign-in flow Log.d(TAG, "Sign-in button clicked"); mSignInClicked = true; mGoogleApiClient.connect(); break; case R.id.button_sign_out: // user wants to sign out // sign out. Log.d(TAG, "Sign-out button clicked"); mSignInClicked = false; Games.signOut(mGoogleApiClient); mGoogleApiClient.disconnect(); switchToScreen(R.id.screen_sign_in); break; case R.id.button_invite_players: // show list of invitable players intent = Games.RealTimeMultiplayer.getSelectOpponentsIntent(mGoogleApiClient, 1, 3); switchToScreen(R.id.screen_wait); startActivityForResult(intent, RC_SELECT_PLAYERS); break; case R.id.button_see_invitations: // show list of pending invitations intent = Games.Invitations.getInvitationInboxIntent(mGoogleApiClient); switchToScreen(R.id.screen_wait); startActivityForResult(intent, RC_INVITATION_INBOX); break; case R.id.button_accept_popup_invitation: // user wants to accept the invitation shown on the invitation popup // (the one we got through the OnInvitationReceivedListener). acceptInviteToRoom(mIncomingInvitationId); mIncomingInvitationId = null; break; case R.id.button_quick_game: // user wants to play against a random opponent right now startQuickGame(); break; } } void startQuickGame() { // quick-start a game with 1 randomly selected opponent final int MIN_OPPONENTS = 1, MAX_OPPONENTS = 4; Bundle autoMatchCriteria = RoomConfig.createAutoMatchCriteria(MIN_OPPONENTS, MAX_OPPONENTS, 0); RoomConfig.Builder rtmConfigBuilder = RoomConfig.builder(this); rtmConfigBuilder.setMessageReceivedListener(this); rtmConfigBuilder.setRoomStatusUpdateListener(this); rtmConfigBuilder.setAutoMatchCriteria(autoMatchCriteria); switchToScreen(R.id.screen_wait); keepScreenOn(); resetGameVars(); Games.RealTimeMultiplayer.create(mGoogleApiClient, rtmConfigBuilder.build()); } @Override public void onActivityResult(int requestCode, int responseCode, Intent intent) { super.onActivityResult(requestCode, responseCode, intent); switch (requestCode) { case RC_SELECT_PLAYERS: // we got the result from the "select players" UI -- ready to create the room handleSelectPlayersResult(responseCode, intent); break; case RC_INVITATION_INBOX: // we got the result from the "select invitation" UI (invitation inbox). We're // ready to accept the selected invitation: handleInvitationInboxResult(responseCode, intent); break; case RC_WAITING_ROOM: // we got the result from the "waiting room" UI. if (responseCode == Activity.RESULT_OK) { // ready to start playing Log.d(TAG, "Starting game (waiting room returned OK)."); startGame(true); } else if (responseCode == GamesActivityResultCodes.RESULT_LEFT_ROOM) { // player indicated that they want to leave the room leaveRoom(); } else if (responseCode == Activity.RESULT_CANCELED) { // Dialog was cancelled (user pressed back key, for instance). In our game, // this means leaving the room too. In more elaborate games, this could mean // something else (like minimizing the waiting room UI). leaveRoom(); } break; case RC_SIGN_IN: Log.d(TAG, "onActivityResult with requestCode == RC_SIGN_IN, responseCode=" + responseCode + ", intent=" + intent); mSignInClicked = false; mResolvingConnectionFailure = false; if (responseCode == RESULT_OK) { mGoogleApiClient.connect(); } else { BaseGameUtils.showActivityResultError(this, requestCode, responseCode, R.string.signin_other_error); } break; } super.onActivityResult(requestCode, responseCode, intent); } // Handle the result of the "Select players UI" we launched when the user clicked the // "Invite friends" button. We react by creating a room with those players. private void handleSelectPlayersResult(int response, Intent data) { if (response != Activity.RESULT_OK) { Log.w(TAG, "*** select players UI cancelled, " + response); switchToMainScreen(); return; } Log.d(TAG, "Select players UI succeeded."); // get the invitee list final ArrayList<String> invitees = data.getStringArrayListExtra(Games.EXTRA_PLAYER_IDS); Log.d(TAG, "Invitee count: " + invitees.size()); // get the automatch criteria Bundle autoMatchCriteria = null; int minAutoMatchPlayers = data.getIntExtra(Multiplayer.EXTRA_MIN_AUTOMATCH_PLAYERS, 0); int maxAutoMatchPlayers = data.getIntExtra(Multiplayer.EXTRA_MAX_AUTOMATCH_PLAYERS, 0); if (minAutoMatchPlayers > 0 || maxAutoMatchPlayers > 0) { autoMatchCriteria = RoomConfig.createAutoMatchCriteria(minAutoMatchPlayers, maxAutoMatchPlayers, 0); Log.d(TAG, "Automatch criteria: " + autoMatchCriteria); } // create the room Log.d(TAG, "Creating room..."); RoomConfig.Builder rtmConfigBuilder = RoomConfig.builder(this); rtmConfigBuilder.addPlayersToInvite(invitees); rtmConfigBuilder.setMessageReceivedListener(this); rtmConfigBuilder.setRoomStatusUpdateListener(this); if (autoMatchCriteria != null) { rtmConfigBuilder.setAutoMatchCriteria(autoMatchCriteria); } switchToScreen(R.id.screen_wait); keepScreenOn(); resetGameVars(); Games.RealTimeMultiplayer.create(mGoogleApiClient, rtmConfigBuilder.build()); Log.d(TAG, "Room created, waiting for it to be ready..."); } // Handle the result of the invitation inbox UI, where the player can pick an invitation // to accept. We react by accepting the selected invitation, if any. private void handleInvitationInboxResult(int response, Intent data) { if (response != Activity.RESULT_OK) { Log.w(TAG, "*** invitation inbox UI cancelled, " + response); switchToMainScreen(); return; } Log.d(TAG, "Invitation inbox UI succeeded."); Invitation inv = data.getExtras().getParcelable(Multiplayer.EXTRA_INVITATION); // accept invitation acceptInviteToRoom(inv.getInvitationId()); } // Accept the given invitation. void acceptInviteToRoom(String invId) { // accept the invitation Log.d(TAG, "Accepting invitation: " + invId); RoomConfig.Builder roomConfigBuilder = RoomConfig.builder(this); roomConfigBuilder.setInvitationIdToAccept(invId).setMessageReceivedListener(this) .setRoomStatusUpdateListener(this); switchToScreen(R.id.screen_wait); keepScreenOn(); resetGameVars(); Games.RealTimeMultiplayer.join(mGoogleApiClient, roomConfigBuilder.build()); } // Activity is going to the background. We have to leave the current room. @Override public void onStop() { Log.d(TAG, "**** got onStop"); // if we're in a room, leave it. leaveRoom(); // stop trying to keep the screen on stopKeepingScreenOn(); if (mGoogleApiClient == null || !mGoogleApiClient.isConnected()) { switchToScreen(R.id.screen_sign_in); } else { switchToScreen(R.id.screen_wait); } super.onStop(); } // Activity just got to the foreground. We switch to the wait screen because we will now // go through the sign-in flow (remember that, yes, every time the Activity comes back to the // foreground we go through the sign-in flow -- but if the user is already authenticated, // this flow simply succeeds and is imperceptible). @Override public void onStart() { switchToScreen(R.id.screen_wait); if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) { Log.w(TAG, "GameHelper: client was already connected on onStart()"); } else { Log.d(TAG, "Connecting client."); mGoogleApiClient.connect(); } super.onStart(); } // Handle back key to make sure we cleanly leave a game if we are in the middle of one @Override public boolean onKeyDown(int keyCode, KeyEvent e) { if (keyCode == KeyEvent.KEYCODE_BACK && mCurScreen == R.id.screen_game) { leaveRoom(); return true; } return super.onKeyDown(keyCode, e); } // Leave the room. void leaveRoom() { Log.d(TAG, "Leaving room."); stopKeepingScreenOn(); if (mRoomId != null) { Games.RealTimeMultiplayer.leave(mGoogleApiClient, this, mRoomId); mRoomId = null; switchToScreen(R.id.screen_wait); } else { switchToMainScreen(); } } // Show the waiting room UI to track the progress of other players as they enter the // room and get connected. void showWaitingRoom(Room room) { // minimum number of players required for our game // For simplicity, we require everyone to join the game before we start it // (this is signaled by Integer.MAX_VALUE). final int MIN_PLAYERS = Integer.MAX_VALUE; Intent i = Games.RealTimeMultiplayer.getWaitingRoomIntent(mGoogleApiClient, room, MIN_PLAYERS); // show waiting room UI startActivityForResult(i, RC_WAITING_ROOM); } // Called when we get an invitation to play a game. We react by showing that to the user. @Override public void onInvitationReceived(Invitation invitation) { // We got an invitation to play a game! So, store it in // mIncomingInvitationId // and show the popup on the screen. mIncomingInvitationId = invitation.getInvitationId(); ((TextView) findViewById(R.id.incoming_invitation_text)) .setText(invitation.getInviter().getDisplayName() + " " + getString(R.string.is_inviting_you)); switchToScreen(mCurScreen); // This will show the invitation popup } @Override public void onInvitationRemoved(String invitationId) { if (mIncomingInvitationId.equals(invitationId)) { mIncomingInvitationId = null; switchToScreen(mCurScreen); // This will hide the invitation popup } } /* * CALLBACKS SECTION. This section shows how we implement the several games * API callbacks. */ @Override public void onConnected(Bundle connectionHint) { Log.d(TAG, "onConnected() called. Sign in successful!"); Log.d(TAG, "Sign-in succeeded."); // register listener so we are notified if we receive an invitation to play // while we are in the game Games.Invitations.registerInvitationListener(mGoogleApiClient, this); if (connectionHint != null) { Log.d(TAG, "onConnected: connection hint provided. Checking for invite."); Invitation inv = connectionHint.getParcelable(Multiplayer.EXTRA_INVITATION); if (inv != null && inv.getInvitationId() != null) { // retrieve and cache the invitation ID Log.d(TAG, "onConnected: connection hint has a room invite!"); acceptInviteToRoom(inv.getInvitationId()); return; } } switchToMainScreen(); } @Override public void onConnectionSuspended(int i) { Log.d(TAG, "onConnectionSuspended() called. Trying to reconnect."); mGoogleApiClient.connect(); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { Log.d(TAG, "onConnectionFailed() called, result: " + connectionResult); if (mResolvingConnectionFailure) { Log.d(TAG, "onConnectionFailed() ignoring connection failure; already resolving."); return; } if (mSignInClicked || mAutoStartSignInFlow) { mAutoStartSignInFlow = false; mSignInClicked = false; mResolvingConnectionFailure = BaseGameUtils.resolveConnectionFailure(this, mGoogleApiClient, connectionResult, RC_SIGN_IN, getString(R.string.signin_other_error)); } switchToScreen(R.id.screen_sign_in); } // Called when we are connected to the room. We're not ready to play yet! (maybe not everybody // is connected yet). @Override public void onConnectedToRoom(Room room) { Log.d(TAG, "onConnectedToRoom."); // get room ID, participants and my ID: mRoomId = room.getRoomId(); mRoom = room; mMyId = room.getParticipantId(Games.Players.getCurrentPlayerId(mGoogleApiClient)); // print out the list of participants (for debug purposes) Log.d(TAG, "Room ID: " + mRoomId); Log.d(TAG, "My ID " + mMyId); Log.d(TAG, "<< CONNECTED TO ROOM>>"); } // Called when we've successfully left the room (this happens a result of voluntarily leaving // via a call to leaveRoom(). If we get disconnected, we get onDisconnectedFromRoom()). @Override public void onLeftRoom(int statusCode, String roomId) { // we have left the room; return to main screen. Log.d(TAG, "onLeftRoom, code " + statusCode); switchToMainScreen(); } // Called when we get disconnected from the room. We return to the main screen. @Override public void onDisconnectedFromRoom(Room room) { mRoomId = null; showGameError(); } // Show error message about game being cancelled and return to main screen. void showGameError() { BaseGameUtils.makeSimpleDialog(this, getString(R.string.game_problem)); switchToMainScreen(); } // Called when room has been created @Override public void onRoomCreated(int statusCode, Room room) { Log.d(TAG, "onRoomCreated(" + statusCode + ", " + room + ")"); if (statusCode != GamesStatusCodes.STATUS_OK) { Log.e(TAG, "*** Error: onRoomCreated, status " + statusCode); showGameError(); return; } // show the waiting room UI showWaitingRoom(room); } // Called when room is fully connected. @Override public void onRoomConnected(int statusCode, Room room) { Log.d(TAG, "onRoomConnected(" + statusCode + ", " + room + ")"); if (statusCode != GamesStatusCodes.STATUS_OK) { Log.e(TAG, "*** Error: onRoomConnected, status " + statusCode); showGameError(); return; } updateRoom(room); } @Override public void onJoinedRoom(int statusCode, Room room) { Log.d(TAG, "onJoinedRoom(" + statusCode + ", " + room + ")"); if (statusCode != GamesStatusCodes.STATUS_OK) { Log.e(TAG, "*** Error: onRoomConnected, status " + statusCode); showGameError(); return; } // show the waiting room UI showWaitingRoom(room); } // We treat most of the room update callbacks in the same way: we update our list of // participants and update the display. In a real game we would also have to check if that // change requires some action like removing the corresponding player avatar from the screen, // etc. @Override public void onPeerDeclined(Room room, List<String> arg1) { updateRoom(room); } @Override public void onPeerInvitedToRoom(Room room, List<String> arg1) { updateRoom(room); } @Override public void onP2PDisconnected(String participant) { } @Override public void onP2PConnected(String participant) { } @Override public void onPeerJoined(Room room, List<String> arg1) { updateRoom(room); } @Override public void onPeerLeft(Room room, List<String> peersWhoLeft) { updateRoom(room); } @Override public void onRoomAutoMatching(Room room) { updateRoom(room); } @Override public void onRoomConnecting(Room room) { updateRoom(room); } @Override public void onPeersConnected(Room room, List<String> peers) { updateRoom(room); } @Override public void onPeersDisconnected(Room room, List<String> peers) { updateRoom(room); } void updateRoom(Room room) { if (room != null && mRoom != room) { mRoom = room; mRoomId = room.getRoomId(); Log.d(TAG, "updating room: " + mRoomId); mParticipants = room.getParticipants(); if (mParticipants != null) { resetGameVars(); setUpMap(); } } else { Log.d(TAG, "tried to update room"); } } /* * GAME LOGIC SECTION. Methods that implement the game's rules. */ // Current state of the game: // My participant ID in the currently active game private String mMyId = null; private SortedMap<String, String> mIdToName; private FireworksTurn mTurnData = null; private boolean mHandSelectionMode = false; private boolean mDiscardMode = false; private boolean mPlayMode = false; private boolean mPlayerSelectionMode = false; private Map<String, HandFragment> fragments = null; private int mDiscardWidthR1 = 0; private int mDiscardWidthR2 = 0; private List<String> actionLog = new ArrayList<>(); private String mRecipientPlayer = null; // Reset game variables in preparation for a new game. void resetGameVars() { mMyId = null; mTurnData = null; mIdToName = null; mHandSelectionMode = false; mDiscardMode = false; mPlayMode = false; fragments = null; mDiscardWidthR1 = 0; mDiscardWidthR2 = 0; actionLog = new ArrayList<>(); mPlayerSelectionMode = false; mRecipientPlayer = null; togglePlayOptionsVisible(PlayOptions.play); } // Assumes mRoom is set up private void setUpMap() { if (mIdToName == null) { mParticipants = mRoom.getParticipants(); mMyId = mRoom.getParticipantId(Games.Players.getCurrentPlayerId(mGoogleApiClient)); mIdToName = new TreeMap<>(); for (Participant p : mParticipants) { mIdToName.put(p.getParticipantId(), p.getDisplayName()); } } } public void startGame(boolean multiplayer) { mMultiplayer = multiplayer; if (!multiplayer) { // TODO(sdspikes): create a game with some AIs, give them names? // mTurnData.state = new GameState(3); } else { setUpMap(); if (mMyId.equals(mIdToName.keySet().iterator().next())) { try { setUpGame(); } catch (JSONException e) { e.printStackTrace(); } // Log.d(TAG, mParticipants.get(0).getParticipantId() + " is the first id and will choose who is first player"); // //TODO(sdspikes): actually send the message // int rand = new Random().nextInt(mParticipants.size()); // String firstPlayerId = mParticipants.get(rand).getParticipantId(); // JSONObject firstPlayer = new JSONObject(); // try { // if (firstPlayerId.equals(mMyId)) { // // If it's me, don't bother sending messages, just set up the game and send // // the message about the first state // setUpGame(); // } else { // // otherwise, set up a message letting everyone know who the first player is // // so that player can set up the game // firstPlayer.put("firstPlayer", firstPlayerId); // broadcastGameInfo(firstPlayer); // } // } catch (JSONException e) { // e.printStackTrace(); // } togglePlayOptionsVisible(PlayOptions.play); } else { Log.d(TAG, mMyId + " is not the first id and should get a message w/ game state"); togglePlayOptionsVisible(PlayOptions.turnMessage); } } } public interface LogItem { public static final String SENDER_ID = "senderId"; public static final String TYPE = "type"; public JSONObject getJSONObject(); public String toString(); } private String getPlayerString(String id, boolean beginning) { if (id.equals(mMyId)) { if (beginning) return "You"; return "you"; } return mIdToName.get(id); } public class InfoLogItem implements LogItem { public static final String RECIPIENT_ID = "recipientId"; public static final String INFO = "info"; public static final String POSITIONS = "positions"; final String senderId; final String recipientId; // This will be a rank or color (e.g. two, red). final String info; final int[] positions; public InfoLogItem(String senderId, String recipientId, String info, int[] positions) { this.senderId = senderId; this.recipientId = recipientId; this.info = info; this.positions = positions; } public InfoLogItem(JSONObject obj) throws JSONException { senderId = obj.getString(SENDER_ID); recipientId = obj.getString(RECIPIENT_ID); info = obj.getString(INFO); positions = GameState.decodeIntArray(obj.getJSONArray(POSITIONS)); } @Override public JSONObject getJSONObject() { JSONObject obj = new JSONObject(); try { obj.put(SENDER_ID, senderId); obj.put(RECIPIENT_ID, recipientId); obj.put(INFO, info); obj.put(POSITIONS, GameState.encodeIntArray(positions)); obj.put(TYPE, INFO); } catch (JSONException e) { e.printStackTrace(); } return obj; } @Override public String toString() { String s = (positions.length > 1) ? "s" : ""; return getPlayerString(senderId, true) + " told " + getPlayerString(recipientId, false) + " about the " + info + s + " at the following position" + s + ": " + Arrays.toString(positions); } } public class ActionLogItem implements LogItem { public static final String CARD = "card"; public static final String ACTION = "action"; final String senderId; final GameState.Card card; // "discarded" or "played" or "drew" final String action; public ActionLogItem(String senderId, GameState.Card card, String action) { this.senderId = senderId; this.card = card; this.action = action; } public ActionLogItem(JSONObject obj) throws JSONException { senderId = obj.getString(SENDER_ID); card = new GameState.Card(obj.getJSONObject(CARD)); action = obj.getString(ACTION); } @Override public JSONObject getJSONObject() { JSONObject obj = new JSONObject(); try { obj.put(SENDER_ID, senderId); obj.put(CARD, card.encodeCard()); obj.put(ACTION, action); obj.put(TYPE, ACTION); } catch (JSONException e) { e.printStackTrace(); } return obj; } @Override public String toString() { String cardString = card.toString(); if (mMyId.equals(senderId) && action.equals("drew")) { cardString = "card"; } return getPlayerString(senderId, true) + " " + action + " a " + cardString; } } public LogItem parseLogItem(JSONObject obj) throws JSONException { String type = obj.getString(LogItem.TYPE); if (type.equals("action")) { return new ActionLogItem(obj); } else if (type.equals("info")) { return new InfoLogItem(obj); } return null; } private void createInitialLog() { String currentId = mMyId; while (true) { GameState.HandNode handNode = mTurnData.state.hands.get(currentId); for (GameState.Card card : handNode.hand) { actionLog.add(new ActionLogItem(currentId, card, "drew").toString()); } currentId = handNode.nextPlayerId; if (mMyId.equals(currentId)) { break; } } } private void displayInitialState() { switchToScreen(R.id.screen_game); GameState.HandNode currentNode = mTurnData.state.hands.get(mMyId); FragmentManager fm = getFragmentManager(); fragments = new HashMap<>(); if (fm.findFragmentById(R.id.my_hand) == null) { if (currentNode != null) { addHandFragment(fm, currentNode, mMyId, mIdToName.get(mMyId), R.id.my_hand); } else { Log.d(TAG, mMyId); } } if (fm.findFragmentById(R.id.other_hands) == null) { while (true) { String currentId = currentNode.nextPlayerId; if (currentId.equals(mMyId)) { break; } currentNode = mTurnData.state.hands.get(currentId); addHandFragment(fm, currentNode, currentId, mIdToName.get(currentId), R.id.other_hands); } } LinearLayout played = (LinearLayout) findViewById(R.id.played_pile); played.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { LinearLayout played = (LinearLayout) findViewById(R.id.played_pile); mDiscardWidthR2 = played.getMeasuredWidth(); if (mDiscardWidthR2 != 0) { int usableWidth = mDiscardWidthR2 - findViewById(R.id.played_label).getMeasuredWidth(); mDiscardWidthR1 = mDiscardWidthR2 - findViewById(R.id.discarded_label).getMeasuredWidth(); for (int i = 1; i < played.getChildCount(); i++) { ViewGroup.LayoutParams params = played.getChildAt(i).getLayoutParams(); params.width = usableWidth / 5; played.getChildAt(i).setLayoutParams(params); } played.getViewTreeObserver().removeOnGlobalLayoutListener(this); LinearLayout chooseAttribute = (LinearLayout) findViewById(R.id.chooseAttribute); for (int i = 1; i <= 5; i++) { chooseAttribute.addView(makeAttributeTextView(i, null)); } for (GameState.CardColor color : GameState.CardColor.values()) { chooseAttribute.addView(makeAttributeTextView(-1, color)); } // In case all the data is ready already and was just waiting on this. updateDisplay(); } } }); createInitialLog(); ((TextView) findViewById(R.id.log)).setMovementMethod(new ScrollingMovementMethod()); } private void addHandFragment(FragmentManager fm, GameState.HandNode node, String playerId, String playerName, int id) { try { Log.d(TAG, node.encodeNode().toString()); } catch (JSONException e) { e.printStackTrace(); } HandFragment handFragment = HandFragment.newInstance(node.hand, playerId, playerName, playerId == mMyId); fragments.put(playerId, handFragment); // TODO(sdspikes): previously I just had .commit here and got // java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState fm.beginTransaction().add(id, handFragment).commitAllowingStateLoss(); } private enum PlayOptions { play, chooseCard, turnMessage, choosePlayer, chooseAttribute } private void togglePlayOptionsVisible(PlayOptions option) { findViewById(R.id.my_turn_buttons).setVisibility(option == PlayOptions.play ? View.VISIBLE : View.GONE); TextView message = (TextView) findViewById(R.id.player_message); message.setVisibility(View.GONE); if (option == PlayOptions.turnMessage) { if (mTurnData != null) message.setText(mIdToName.get(mTurnData.state.currentPlayerId) + "'s turn"); message.setVisibility(View.VISIBLE); } else if (option == PlayOptions.choosePlayer) { message.setText("Choose a player."); message.setVisibility(View.VISIBLE); } else if (option == PlayOptions.chooseCard) { message.setText("Choose a card from your hand."); message.setVisibility(View.VISIBLE); } LinearLayout chooseAttribute = (LinearLayout) findViewById(R.id.chooseAttribute); chooseAttribute.setVisibility(option == PlayOptions.chooseAttribute ? View.VISIBLE : View.GONE); } private TextView makeAttributeTextView(final int rank, final GameState.CardColor color) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); int oneButtonWidth = mDiscardWidthR2 / 10; int marginWidth = oneButtonWidth / 10; params.setMargins(marginWidth, 5, marginWidth, 5); params.width = oneButtonWidth - marginWidth * 2; params.height = params.width; TextView textView = new TextView(this); textView.setLayoutParams(params); textView.setText(String.valueOf(rank)); if (rank == -1) textView.setText(" "); textView.setGravity(Gravity.CENTER); textView.setBackgroundResource(HandFragment.cardColorToBGColor.get(color)); textView.setTextColor(getResources().getColor(HandFragment.cardColorToTextColor(color))); textView.setVisibility(View.VISIBLE); textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d(TAG, "clicked on an attribute button: " + GameState.Card.cardColorToString(color) + " " + rank); List<GameState.Card> hand = mTurnData.state.hands.get(mRecipientPlayer).hand; List<Integer> locations = new ArrayList<Integer>(); String info = ""; if (rank == -1) { info = GameState.Card.cardColorToString(color); for (int i = 0; i < hand.size(); i++) { if (hand.get(i).color == color) locations.add(i); } } else { info = HandFragment.rankToString(rank); for (int i = 0; i < hand.size(); i++) { if (hand.get(i).rank == rank) locations.add(i); } } if (locations.size() == 0) { Toast.makeText(FireworksActivity.this, mIdToName.get(mRecipientPlayer) + " does not have any " + info, Toast.LENGTH_SHORT); } else { int[] positions = new int[locations.size()]; for (int i = 0; i < positions.length; i++) { // Make the positions 1-indexed positions[i] = locations.get(i) + 1; } LogItem item = new InfoLogItem(mMyId, mRecipientPlayer, info, positions); actionLog.add(item.toString()); mTurnData.state.hintsRemaining--; mTurnData.state.currentPlayerId = mTurnData.state.hands .get(mTurnData.state.currentPlayerId).nextPlayerId; togglePlayOptionsVisible(PlayOptions.turnMessage); broadcastGameInfo(item.getJSONObject()); updateAllPlayers(mTurnData.getJSONObject()); mRecipientPlayer = null; } } }); return textView; } /* * COMMUNICATIONS SECTION. Methods that implement the game's network * protocol. */ // Called when we receive a real-time message from the network. @Override public void onRealTimeMessageReceived(RealTimeMessage rtm) { byte[] buf = rtm.getMessageData(); String sender = rtm.getSenderParticipantId(); try { JSONObject obj = unpersist(buf); // First, try seeing if it's a gamestate since that's the most common message FireworksTurn newTurn = FireworksTurn.unpersist(obj); if (newTurn != null) { mTurnData = newTurn; Log.d(TAG, "got turn data!"); if (obj.has("firstTime")) { // Set up the id map if necessary setUpMap(); displayInitialState(); } else { updateDisplay(); } togglePlayOptionsVisible( mMyId.equals(mTurnData.state.currentPlayerId) ? PlayOptions.play : PlayOptions.turnMessage); } else if (obj.has(LogItem.TYPE)) { actionLog.add(parseLogItem(obj).toString()); updateDisplay(); } else if (obj.has("handUpdate")) { // TODO(sdspikes): is it possible that the hand could be updated (different cards) // before this? mTurnData.state.hands.get(sender).setHand(obj.getJSONArray("handUpdate")); } else if (obj.has("firstPlayer")) { String firstPlayerId = obj.getString("firstPlayer"); Log.d(TAG, "first player id: " + firstPlayerId + ", mine: " + mMyId); if (firstPlayerId == mMyId) { setUpGame(); } } } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private void setUpGame() throws JSONException { // set up initial turn data mTurnData = new FireworksTurn(); mTurnData.state = new GameState(mParticipants); // TODO(sdspikes): maybe pass this to GameState constructor? mTurnData.state.currentPlayerId = mMyId; JSONObject jsonTurn = mTurnData.getJSONObject(); jsonTurn.put("firstTime", mMyId); broadcastGameInfo(jsonTurn); displayInitialState(); } private JSONObject unpersist(byte[] buf) throws JSONException { Log.d(TAG, "Message received: " + buf); if (buf == null) { Log.d(TAG, "Empty message---possible bug."); return null; } String st = null; try { st = new String(buf, "UTF-8"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); return null; } Log.d(TAG, "====UNPERSIST \n" + st); return new JSONObject(st); } public byte[] persist(JSONObject obj) { String st = obj.toString(); Log.d(TAG, "==== PERSISTING\n" + st); return st.getBytes(Charset.forName("UTF-8")); } // Broadcast my score to everybody else. private void broadcastGameInfo(JSONObject obj) { byte[] buf = persist(obj); if (!mMultiplayer) return; // playing single-player mode, no need to inform anyone of anything // Send to every other participant. for (Participant p : mParticipants) { Log.d(TAG, "about to attempt to send to " + p.getParticipantId()); if (p.getParticipantId().equals(mMyId)) continue; if (p.getStatus() != Participant.STATUS_JOINED) continue; if (mGameComplete) { Log.d(TAG, "sending reliable to " + p.getParticipantId()); // final score notification must be sent via reliable message int result = Games.RealTimeMultiplayer.sendReliableMessage(mGoogleApiClient, null, buf, mRoomId, p.getParticipantId()); } else { sendUnreliable(buf, p.getParticipantId()); } } } private void sendUnreliable(byte[] buf, String participantId) { Log.d(TAG, "sending unreliable to " + participantId); // it's an interim score notification, so we can use unreliable int result = Games.RealTimeMultiplayer.sendUnreliableMessage(mGoogleApiClient, buf, mRoomId, participantId); if (result == RealTimeMultiplayer.REAL_TIME_MESSAGE_FAILED) { Log.d(TAG, "failed to send message."); if (buf.length > Multiplayer.MAX_UNRELIABLE_MESSAGE_LEN) { Log.d(TAG, "It's too long by " + (buf.length - Multiplayer.MAX_UNRELIABLE_MESSAGE_LEN)); } } if (result != RealTimeMultiplayer.REAL_TIME_MESSAGE_FAILED) Log.d(TAG, "success!"); } /* * UI SECTION. Methods that implement the game's UI. */ // This array lists everything that's clickable, so we can install click // event handlers. final static int[] CLICKABLES = { R.id.button_accept_popup_invitation, R.id.button_invite_players, R.id.button_quick_game, R.id.button_see_invitations, R.id.button_sign_in, R.id.button_sign_out, R.id.button_single_player, R.id.button_single_player_2 }; // This array lists all the individual screens our game has. final static int[] SCREENS = { R.id.screen_game, R.id.screen_main, R.id.screen_sign_in, R.id.screen_wait }; int mCurScreen = -1; void switchToScreen(int screenId) { // make the requested screen visible; hide all others. for (int id : SCREENS) { findViewById(id).setVisibility(screenId == id ? View.VISIBLE : View.GONE); } mCurScreen = screenId; // should we show the invitation popup? boolean showInvPopup; if (mIncomingInvitationId == null) { // no invitation, so no popup showInvPopup = false; } else if (mMultiplayer) { // if in multiplayer, only show invitation on main screen showInvPopup = (mCurScreen == R.id.screen_main); } else { // single-player: show on main screen and gameplay screen showInvPopup = (mCurScreen == R.id.screen_main || mCurScreen == R.id.screen_game); } findViewById(R.id.invitation_popup).setVisibility(showInvPopup ? View.VISIBLE : View.GONE); } void switchToMainScreen() { if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) { switchToScreen(R.id.screen_main); } else { switchToScreen(R.id.screen_sign_in); } } // updates the label that shows my score void updateAllPlayers(JSONObject obj) { updateDisplay(); Log.d(TAG, "Updating players about something"); broadcastGameInfo(obj); } private void updateDisplay() { // In case it's called too early if (mTurnData == null || fragments == null) { return; } for (Map.Entry<String, GameState.HandNode> entry : mTurnData.state.hands.entrySet()) { fragments.get(entry.getKey()).updateHand(entry.getValue().hand); } LinearLayout played = (LinearLayout) findViewById(R.id.played_pile); for (int i = 1; i < played.getChildCount(); i++) { ((TextView) played.getChildAt(i)).setText(String.valueOf(mTurnData.state.played[i - 1])); } ((TextView) findViewById(R.id.deck)).setText(String.valueOf(mTurnData.state.deck.size())); ((TextView) findViewById(R.id.hints)).setText(String.valueOf(mTurnData.state.hintsRemaining)); ((TextView) findViewById(R.id.misplays)).setText(String.valueOf(mTurnData.state.explosionsRemaining)); if (mDiscardWidthR1 != 0) { displayDiscardPiles(); } // TODO(sdspikes): add log String logString = ""; for (String log : actionLog) { logString += log + "\n"; } ((TextView) findViewById(R.id.log)).setText(logString); } private void displayDiscardPiles() { LinearLayout currRow = ((LinearLayout) findViewById(R.id.discard_pile_row_1)); LinearLayout row2 = ((LinearLayout) findViewById(R.id.discard_pile_row_2)); currRow.removeAllViews(); row2.removeAllViews(); int maxInTopRow = mDiscardWidthR1 / MIN_CARD_WIDTH; int rowDivision = mTurnData.state.discarded.length; int totalDiscarded = 0; int topRow = 0; for (int i = 0; i < mTurnData.state.discarded.length; i++) { totalDiscarded += mTurnData.state.discarded[i].size(); if (totalDiscarded <= maxInTopRow) { topRow = totalDiscarded; } if (totalDiscarded > maxInTopRow && rowDivision == mTurnData.state.discarded.length) { rowDivision = i; } } if (rowDivision == mTurnData.state.discarded.length) { row2.setVisibility(View.GONE); } if (totalDiscarded == 0) { return; } int width = mDiscardWidthR1 / topRow; for (int i = 0; i < mTurnData.state.discarded.length; i++) { for (int j = 0; j < mTurnData.state.discarded[i].size(); j++) { if (i == rowDivision) { currRow = row2; row2.setVisibility(View.VISIBLE); width = mDiscardWidthR2 / (totalDiscarded - topRow); } currRow.addView(makeNewCardTextView(width, mTurnData.state.discarded[i].get(j))); } } } private TextView makeNewCardTextView(int width, GameState.Card card) { width = Math.min(width, MAX_CARD_WIDTH); width = Math.max(width, MIN_CARD_WIDTH); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT); TextView textView = new TextView(this); textView.setLayoutParams(params); textView.setGravity(Gravity.CENTER); textView.setText(String.valueOf(card.rank)); textView.setBackgroundResource(HandFragment.cardColorToBGColor.get(card.color)); textView.setTextColor(getResources().getColor(HandFragment.cardColorToTextColor(card.color))); return textView; } /* * MISC SECTION. Miscellaneous methods. */ // Sets the flag to keep this screen on. It's recommended to do that during // the // handshake when setting up a game, because if the screen turns off, the // game will be // cancelled. void keepScreenOn() { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } // Clears the flag that keeps the screen on. void stopKeepingScreenOn() { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } // In-game controls // Cancel the game. Should possibly wait until the game is canceled before // giving up on the view. public void onDiscardClicked(View view) { // allow user to click their cards, listener will deal with actually doing the work togglePlayOptionsVisible(PlayOptions.chooseCard); mHandSelectionMode = true; mDiscardMode = true; } // Leave the game during your turn. Note that there is a separate // Games.TurnBasedMultiplayer.leaveMatch() if you want to leave NOT on your turn. public void onPlayClicked(View view) { // allow user to click their cards, listener will deal with actually doing the work togglePlayOptionsVisible(PlayOptions.chooseCard); mHandSelectionMode = true; mPlayMode = true; } // Upload your new gamestate, then take a turn, and pass it on to the next // player. public void onGiveHintClicked(View view) { mTurnData.turnCounter += 1; // TODO(sdspikes): choose character to give info // TODO(sdspikes): choose attribute // TODO(sdspikes): notify all players updateAllPlayers(mTurnData.getJSONObject()); togglePlayOptionsVisible(PlayOptions.choosePlayer); mPlayerSelectionMode = true; } @Override public void onFragmentSelected(String playerId, int index) { if (mHandSelectionMode && playerId.equals(mMyId)) { String action = ""; GameState.HandNode node = mTurnData.state.hands.get(mMyId); GameState.Card removedCard = node.hand.remove(index); GameState.Card drawnCard = mTurnData.state.deck.remove(); node.hand.add(drawnCard); if (mPlayMode) { action = "played"; if (mTurnData.state.played[removedCard.color.ordinal()] == removedCard.rank - 1) { mTurnData.state.played[removedCard.color.ordinal()]++; } else { mTurnData.state.explosionsRemaining--; } } if (mDiscardMode) { action = "discarded"; mTurnData.state.discarded[removedCard.color.ordinal()].add(removedCard); } mTurnData.state.currentPlayerId = mTurnData.state.hands .get(mTurnData.state.currentPlayerId).nextPlayerId; Log.d(TAG, "updated currentId : " + mTurnData.state.currentPlayerId); mPlayMode = false; mDiscardMode = false; mHandSelectionMode = false; updateAllPlayers(mTurnData.getJSONObject()); ActionLogItem item = new ActionLogItem(mMyId, removedCard, action); actionLog.add(item.toString()); broadcastGameInfo(item.getJSONObject()); item = new ActionLogItem(mMyId, drawnCard, "drew"); actionLog.add(item.toString()); broadcastGameInfo(item.getJSONObject()); updateDisplay(); togglePlayOptionsVisible(PlayOptions.turnMessage); } else if (mPlayerSelectionMode && playerId != mMyId) { mRecipientPlayer = playerId; togglePlayOptionsVisible(PlayOptions.chooseAttribute); } } }