package com.magnet.demo.mmx.rpsls; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.widget.Toast; import com.magnet.max.android.ApiCallback; import com.magnet.max.android.ApiError; import com.magnet.max.android.User; import com.magnet.mmx.client.api.ListResult; import com.magnet.mmx.client.api.MMXChannel; import com.magnet.mmx.client.api.MMXMessage; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; /** * Contains the main RPSLS game logic. */ public class RPSLS { /** * Enumeration of the possible outcomes */ public enum Outcome {WIN, LOSS, DRAW} /** * Enumeration of how a win is accomplished */ public enum How {CUTS, COVERS, CRUSHES, POISONS, SMASHES, DECAPITATES, EATS, DISPROVES, VAPORIZES} /** * Enumeration of the possible choices for each user */ public enum Choice { ROCK(R.drawable.rock_300), PAPER(R.drawable.paper_300), SCISSORS(R.drawable.scissors_300), LIZARD(R.drawable.lizard_300), SPOCK(R.drawable.spock_300); private int mResourceId; Choice(int resourceId) { mResourceId = resourceId; } public int getResourceId() { return mResourceId; } } /** * Statically initialized table of what each choice beats and how */ private static final Map<Choice, BeatsHow[]> sBeatsTable; private static final BeatsHow[] ROCK_BEATS = {new BeatsHow(Choice.SCISSORS, How.CRUSHES, R.drawable.rock_vs_scissiors_450), new BeatsHow(Choice.LIZARD, How.CRUSHES, R.drawable.rock_vs_lizard_450)}; private static final BeatsHow[] PAPER_BEATS = {new BeatsHow(Choice.ROCK, How.COVERS, R.drawable.paper_vs_rock_450), new BeatsHow(Choice.SPOCK, How.DISPROVES, R.drawable.paper_vs_spock_450)}; private static final BeatsHow[] SCISSORS_BEATS = {new BeatsHow(Choice.PAPER, How.CUTS, R.drawable.scissors_vs_paper_450), new BeatsHow(Choice.LIZARD, How.DECAPITATES, R.drawable.scissors_vs_lizard_450)}; private static final BeatsHow[] LIZARD_BEATS = {new BeatsHow(Choice.PAPER, How.EATS, R.drawable.lizard_vs_paper_450), new BeatsHow(Choice.SPOCK, How.POISONS, R.drawable.lizard_vs_spock_450)}; private static final BeatsHow[] SPOCK_BEATS = {new BeatsHow(Choice.ROCK, How.VAPORIZES, R.drawable.spock_vs_rock_450), new BeatsHow(Choice.SCISSORS, How.SMASHES, R.drawable.spock_vs_scissors_450)}; static { HashMap<Choice, BeatsHow[]> table = new HashMap<Choice, BeatsHow[]>(); table.put(Choice.ROCK, ROCK_BEATS); table.put(Choice.PAPER, PAPER_BEATS); table.put(Choice.SCISSORS, SCISSORS_BEATS); table.put(Choice.LIZARD, LIZARD_BEATS); table.put(Choice.SPOCK, SPOCK_BEATS); sBeatsTable = Collections.unmodifiableMap(table); } /** * The choice that is beaten and how it's accomplished */ static final class BeatsHow { private final Choice mBeats; private final How mHow; private final int mResourceId; private BeatsHow(Choice beats, How how, int resourceId) { mBeats = beats; mHow = how; mResourceId = resourceId; } public final Choice getChoice() { return mBeats; } public final How getHow() { return mHow; } public final int getResourceId() { return mResourceId; } } /** * The main logic to determine how an attacker beats the defender. Will * return null of the attacker does not win. * @param attacker the attacker's choice * @param defender the defender's choice * @return how the defender is beaten */ public static BeatsHow getHowAttackerWins(Choice attacker, Choice defender) { BeatsHow[] beatsHow = sBeatsTable.get(attacker); for (BeatsHow current : beatsHow) { if (current.getChoice().equals(defender)) { return current; } } return null; } /** * Contains all of the constants for the messaging that is used for this application. */ public static final class MessageConstants { /** * The topic name that availability messages are published against */ public static final String AVAILABILITY_TOPIC_NAME = "availableplayers"; /** * Message type used to announce availability (or unavailability) */ public static final String TYPE_AVAILABILITY = "AVAILABILITY"; /** * An invitation message */ public static final String TYPE_INVITATION = "INVITATION"; /** * The acceptance of an invitation to play */ public static final String TYPE_ACCEPTANCE = "ACCEPTANCE"; /** * The message type used for an actual choice by a player */ public static final String TYPE_CHOICE = "CHOICE"; public static final String KEY_TYPE = "type"; public static final String KEY_USERNAME = "username"; public static final String KEY_IS_AVAILABLE = "isAvailable"; public static final String KEY_IS_ACCEPT = "isAccept"; public static final String KEY_WINS = "wins"; public static final String KEY_LOSSES = "losses"; public static final String KEY_DRAWS = "ties"; public static final String KEY_TIMESTAMP = "timestamp"; public static final String KEY_GAMEID = "gameId"; public static final String KEY_CHOICE = "choice"; public static final String CHOICE_ROCK = "ROCK"; public static final String CHOICE_PAPER = "PAPER"; public static final String CHOICE_SCISSORS = "SCISSORS"; public static final String CHOICE_LIZARD = "LIZARD"; public static final String CHOICE_SPOCK = "SPOCK"; public static final Map<String, Choice> CHOICE_MAP; static { HashMap<String, Choice> choiceMap = new HashMap<>(); choiceMap.put(CHOICE_ROCK, Choice.ROCK); choiceMap.put(CHOICE_PAPER, Choice.PAPER); choiceMap.put(CHOICE_SCISSORS, Choice.SCISSORS); choiceMap.put(CHOICE_LIZARD, Choice.LIZARD); choiceMap.put(CHOICE_SPOCK, Choice.SPOCK); CHOICE_MAP = Collections.unmodifiableMap(choiceMap); } public static final Map<Choice, String> CHOICE_REVERSE_MAP; static { HashMap<Choice, String> choiceMap = new HashMap<>(); choiceMap.put(Choice.ROCK, CHOICE_ROCK); choiceMap.put(Choice.PAPER, CHOICE_PAPER); choiceMap.put(Choice.SCISSORS, CHOICE_SCISSORS); choiceMap.put(Choice.LIZARD, CHOICE_LIZARD); choiceMap.put(Choice.SPOCK, CHOICE_SPOCK); CHOICE_REVERSE_MAP = Collections.unmodifiableMap(choiceMap); } /** * The amount of time an inviter waits for a response from an invitee * before the invitees are considered to have "rejected" the invitation */ public static final long OPPONENT_ACCEPTANCE_WAIT_TIME = 1 * 60 * 1000l; // 1 minute /** * The amount of time to wait for an opponent's choice after the current player makes * a choice. */ public static final long OPPONENT_CHOICE_WAIT_TIME = 10 * 1000l; //10 seconds /** * The amount of time to look back into the player availability messages to build the available player's list. */ public static final long AVAILABLE_PLAYERS_SINCE_DURATION = 30 * 60 * 1000l; //30 minutes /** * How long an invitation is valid */ public static final long INVITATION_VALIDITY_DURATION = OPPONENT_ACCEPTANCE_WAIT_TIME; // 1 minute } /** * This Util class contains the main Magnet Message logic and navigation logic for RPSLS. */ public static class Util { private static final String TAG = Util.class.getSimpleName(); public static final String EXTRA_GAME_ID = "gameId"; private static final String[] ROBOT_NAMES = {"Rosie", "C-3PO", "R2-D2", "Data", "David", "IronGiant", "Johnny5", "OptimusPrime", "Wall-E"}; private static final char DELIMITER = '-'; private static final Random RANDOM = new Random(); private static final LinkedList<UserProfile> sAvailablePlayers = new LinkedList<>(); private static final DateFormat mDateFormatter = DateFormat.getDateTimeInstance(); private static final HashMap<String, Game> sPendingGames = new HashMap<>(); //gameId:game private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); static { //These AI players can be selected as opponents by the current player //sAvailablePlayers.add(new UserProfile("HAL 9000", new UserProfile.Stats(0,0,0,0,0,0,0,0), new Date(-51494400000l), true)); //May 15, 1968 //sAvailablePlayers.add(new UserProfile("WOPR/Joshua", new UserProfile.Stats(0,0,0,0,0,0,0,0), new Date(423446400000l), true)); //June 3, 1983 //sAvailablePlayers.add(new UserProfile("Skynet", new UserProfile.Stats(0,0,0,0,0,0,0,0), new Date(467596800000l), true)); //October 26, 1984 //sAvailablePlayers.add(new UserProfile("V'Ger", new UserProfile.Stats(0,0,0,0,0,0,0,0), new Date(313372800000l), true)); //December 7, 1979 //sAvailablePlayers.add(new UserProfile("Gort", new UserProfile.Stats(0,0,0,0,0,0,0,0), new Date(-576288000000l), true)); //September 28, 1951 } /** * Generates a username * @return a generated username */ public static String generateUsername() { int randomIndex = RANDOM.nextInt(ROBOT_NAMES.length); return ROBOT_NAMES[randomIndex] + DELIMITER + RANDOM.nextInt(Integer.MAX_VALUE) + DELIMITER + (System.currentTimeMillis() / 1000); } /** * Generates a password * @return a generated password */ public static byte[] generatePassword() { byte[] password = String.valueOf(RANDOM.nextLong()).getBytes(); try { MessageDigest digester = MessageDigest.getInstance("SHA-256"); digester.update(password); password = digester.digest(); } catch (NoSuchAlgorithmException e) { Log.e(TAG, "generatePassword(): Unable to hash the password.", e); } return password; } /** * Generates a UUID for the game. * @return a generated UUID */ public static String generateGameId() { return UUID.randomUUID().toString(); } /** * The current list of available players * @return available players */ public static List<UserProfile> getAvailablePlayers() { synchronized (sAvailablePlayers) { return new ArrayList<>(sAvailablePlayers); } } /** * Sets up the messaging for this application. This is meant to be called * after a connection is already made. It will create the availability channel (if necessary), * subscribe to the channel, and publish the current user as available. * * @param context the context */ public static void setupGameMessaging(final Context context) { MMXChannel.getPublicChannel(MessageConstants.AVAILABILITY_TOPIC_NAME, new MMXChannel.OnFinishedListener<MMXChannel>() { @Override public void onSuccess(MMXChannel mmxChannel) { sAvailabilityChannel = mmxChannel; sAvailabilityChannel.subscribe(new MMXChannel.OnFinishedListener<String>() { public void onSuccess(String subId) { Log.d(TAG, "setupGameMessaging(): subscribed successfully to topic: " + sAvailabilityChannel.getName() + ", subId=" + subId); } public void onFailure(MMXChannel.FailureCode failureCode, Throwable throwable) { Log.e(TAG, "setupGameMessaging(): unable to subscribe to availability topic", throwable); } }); fetchAvailablePlayers(context); } @Override public void onFailure(MMXChannel.FailureCode failureCode, Throwable throwable) { Log.e(TAG, "setupGameMessaging(): unable to find availability channel: " + MessageConstants.AVAILABILITY_TOPIC_NAME + ". " + failureCode, throwable); } }); String[] botNames = {"player_bot"}; User.getUsersByUserNames(Arrays.asList(botNames), new ApiCallback<List<User>>() { public void success(List<User> users) { Log.d(TAG, "setupGameMessaging(): attempt to find player_bot found " + users.size() + " users."); if (users.size() > 0) { sAvailablePlayers.add(new UserProfile(users.get(0).getUserName(), users.get(0), new UserProfile.Stats(0, 0, 0, 0, 0, 0, 0, 0), new Date(System.currentTimeMillis()), false)); //September 28, 1951 } } public void failure(ApiError apiError) { Log.e(TAG, "setupGameMessage(): unable to resolve player_bot: " + apiError, apiError.getCause()); } }); } public static void fetchAvailablePlayers(final Context context) { sMainHandler.post(new Runnable() { public void run() { if (sAvailabilityChannel == null) { sMainHandler.postDelayed(this, 2000); return; } sAvailabilityChannel.getMessages(new Date(System.currentTimeMillis() - (MessageConstants.AVAILABLE_PLAYERS_SINCE_DURATION)), null, 100, 0, false, new MMXChannel.OnFinishedListener<ListResult<MMXMessage>>() { public void onSuccess(ListResult<MMXMessage> mmxMessages) { Log.d(TAG, "fetchAvailablePlayers(): found " + mmxMessages.totalCount + " availability items published in the last 30 minutes"); for (int i = mmxMessages.totalCount; --i >= 0; ) { //start with the last (oldest message); handleAvailabilityMessage(context, mmxMessages.items.get(i)); } } public void onFailure(MMXChannel.FailureCode failureCode, Throwable throwable) { Log.e(TAG, "fetchAvailablePlayers(): caught exception while finding the latest available players", throwable); } }); } }); } /** * Publishes the availability for the current user * * @param context the context * @param isAvailable whether or not the current user is available */ public static void publishAvailability(final Context context, final boolean isAvailable) { sMainHandler.post(new Runnable() { public void run() { if (sAvailabilityChannel == null) { sMainHandler.postDelayed(this, 2000); return; } HashMap<String, String> messageContent = new HashMap<>(); MyProfile profile = MyProfile.getInstance(context); setProfileToMessage(profile, messageContent); messageContent.put(MessageConstants.KEY_TYPE, MessageConstants.TYPE_AVAILABILITY); messageContent.put(MessageConstants.KEY_IS_AVAILABLE, String.valueOf(isAvailable)); messageContent.put(MessageConstants.KEY_TIMESTAMP, String.valueOf(System.currentTimeMillis())); sAvailabilityChannel.publish(messageContent, new MMXChannel.OnFinishedListener<String>() { public void onSuccess(String messageId) { Log.d(TAG, "publishAvailability(): successfully published availability: " + messageId); } public void onFailure(MMXChannel.FailureCode failureCode, Throwable throwable) { Log.e(TAG, "publishAvailability(): unable to publish availability: " + failureCode, throwable); } }); } }); } /** * Handles an availability message. This is a published availability message * * @param context the context * @param message the availability message */ private static boolean handleAvailabilityMessage(Context context, MMXMessage message) { Map<String, String> messageContent = message.getContent(); UserProfile profileFromPayload = parseUserProfileFromMessage(message.getSender(), messageContent); String username = profileFromPayload.getUsername(); Log.d(TAG, "handleAvailabilityMessage(): handling availability message sent by " + username + " at " + mDateFormatter.format(message.getTimestamp())); if (username == null || username.equals(MyProfile.getInstance(context).getUsername())) { Log.d(TAG, "handleAvailabilityMessage(): ignoring my own availability"); return false; } boolean isAvailable = Boolean.parseBoolean(messageContent.get(MessageConstants.KEY_IS_AVAILABLE)); synchronized (sAvailablePlayers) { for (int i = sAvailablePlayers.size(); --i>=0;) { final UserProfile profile = sAvailablePlayers.get(i); if (profile.getUsername().equals(username)) { //found an existing profile: sAvailablePlayers.remove(i); break; } } if (isAvailable) { //add profile here sAvailablePlayers.add(0, profileFromPayload); } } return true; } /** * Marshaller method to pull a UserProfile object from a payload * * @param messageContent the messageContent * @return the UserProfile object */ private static UserProfile parseUserProfileFromMessage(User fromUser, Map<String,String> messageContent) { String username = messageContent.get(MessageConstants.KEY_USERNAME); String winStr = messageContent.get(MessageConstants.KEY_WINS); String lossStr = messageContent.get(MessageConstants.KEY_LOSSES); String tieStr = messageContent.get(MessageConstants.KEY_DRAWS); int wins = winStr == null ? 0 : Integer.parseInt(winStr); int losses = lossStr == null ? 0 : Integer.parseInt(lossStr); int ties = tieStr == null ? 0 : Integer.parseInt(tieStr); return new UserProfile(username, fromUser, new UserProfile.Stats(wins, losses, ties, 0, 0, 0, 0, 0), null, false); } /** * Marshaller method to put a UserProfile into a payload * * @param profile the UserProfile * @param messageContent the message content */ private static void setProfileToMessage(UserProfile profile, Map<String,String> messageContent) { Map<Outcome,Integer> outcomeCounts = profile.getStats().getOutcomeCounts(); messageContent.put(MessageConstants.KEY_USERNAME, profile.getUsername()); messageContent.put(MessageConstants.KEY_WINS, String.valueOf(outcomeCounts.get(Outcome.WIN))); messageContent.put(MessageConstants.KEY_LOSSES, String.valueOf(outcomeCounts.get(Outcome.LOSS))); messageContent.put(MessageConstants.KEY_DRAWS, String.valueOf(outcomeCounts.get(Outcome.DRAW))); } /** * Handles an incoming message, including messages from onMessageReceived and onPubsubItemReceived() * * @param context the context * @param message the incoming message * @return true if the message was processed, false otherwise */ public static boolean handleIncomingMessage(final Context context, final MMXMessage message) { Map<String,String> messageContent = message.getContent(); final String type = messageContent.get(MessageConstants.KEY_TYPE); boolean returnVal = false; if (MessageConstants.TYPE_INVITATION.equals(type)) { returnVal = handleInvitation(context, message); } else if (MessageConstants.TYPE_ACCEPTANCE.equals(type)) { returnVal = handleInvitationAcceptance(context, message); } else if (MessageConstants.TYPE_CHOICE.equals(type)) { returnVal = handleOpponentChoice(message); } else if (MessageConstants.TYPE_AVAILABILITY.equals(type)) { returnVal = handleAvailabilityMessage(context, message); } else { Log.d(TAG, "handleIncomingMessage(): unsupported message type: " + type); } return returnVal; } private static Map<String,String> buildAcceptPayload(Context context, String gameId, boolean isAccept) { HashMap<String,String> messageContent = new HashMap<>(); messageContent.put(MessageConstants.KEY_TYPE, MessageConstants.TYPE_ACCEPTANCE); MyProfile myProfile = MyProfile.getInstance(context); messageContent.put(MessageConstants.KEY_GAMEID, gameId); messageContent.put(MessageConstants.KEY_IS_ACCEPT, String.valueOf(isAccept)); messageContent.put(MessageConstants.KEY_TIMESTAMP, String.valueOf(System.currentTimeMillis())); setProfileToMessage(myProfile, messageContent); return messageContent; } private static boolean handleInvitation(final Context context, MMXMessage message) { Log.d(TAG, "handleInvitation(): Message is an invitation"); Map<String,String> messageContent = message.getContent(); final String gameId = messageContent.get(MessageConstants.KEY_GAMEID); String timestamp = messageContent.get(MessageConstants.KEY_TIMESTAMP); final UserProfile profile = parseUserProfileFromMessage(message.getSender(), messageContent); if (gameId == null || profile.getUsername() == null) { //can't continue Log.w(TAG, "handleInvitation(): Invitation is invalid"); return false; } else if (timestamp != null && (System.currentTimeMillis() - Long.parseLong(timestamp)) > (MessageConstants.INVITATION_VALIDITY_DURATION)){ //if the invitation is too old, drop it Log.d(TAG, "handleInvitation(): Invitation is outdated"); return false; } else { //show a UI whether or not to accept the invitation AlertDialog.Builder builder = new AlertDialog.Builder(context) .setTitle(context.getString(R.string.invitation_from, profile.getUsername())) .setMessage(R.string.accept_invitation_prompt) .setPositiveButton(R.string.btn_accept, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { synchronized (sPendingGames) { sPendingGames.put(gameId, new Game(gameId, profile)); HashSet<User> recipients = new HashSet<>(); recipients.add(profile.getUser()); MMXMessage message = new MMXMessage.Builder() .content(buildAcceptPayload(context, gameId, true)) .recipients(recipients) .build(); message.send(new MMXMessage.OnFinishedListener<String>() { public void onSuccess(String messageId) { Log.d(TAG, "handleInvitation(): sent acceptance message: " + messageId); launchGameActivity(context, gameId); } public void onFailure(final MMXMessage.FailureCode failureCode, final Throwable throwable) { Log.e(TAG, "handleInvitation(): unable to send acceptance message", throwable); Toast.makeText(context, "Unable to accept: " + failureCode + ", " + throwable, Toast.LENGTH_LONG).show(); } }); dialog.dismiss(); } } }) .setNegativeButton(R.string.btn_reject, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { HashSet<User> recipients = new HashSet<>(); recipients.add(profile.getUser()); MMXMessage message = new MMXMessage.Builder() .content(buildAcceptPayload(context, gameId, false)) .recipients(recipients) .build(); message.send(new MMXMessage.OnFinishedListener<String>() { public void onSuccess(String messageId) { Log.d(TAG, "handleInvitation(): sent rejection message: " + messageId); } public void onFailure(final MMXMessage.FailureCode failureCode, final Throwable throwable) { Log.e(TAG, "handleInvitation(): unable to send rejection message", throwable); Toast.makeText(context, "Unable to reject: " + failureCode + ", " + throwable, Toast.LENGTH_LONG).show(); } }); dialog.dismiss(); } }); builder.show(); return true; } } private static boolean handleInvitationAcceptance(final Context context, MMXMessage message) { //start the game Map<String,String> messageContent = message.getContent(); UserProfile opponent = parseUserProfileFromMessage(message.getSender(), messageContent); final String gameId = messageContent.get(MessageConstants.KEY_GAMEID); String timestamp = messageContent.get(MessageConstants.KEY_TIMESTAMP); boolean isAccept = Boolean.parseBoolean(messageContent.get(MessageConstants.KEY_IS_ACCEPT)); if (gameId == null) { Log.d(TAG, "handleInvitationAcceptance(): gameId was null"); return false; } else if (timestamp != null && (System.currentTimeMillis() - Long.parseLong(timestamp)) > (MessageConstants.INVITATION_VALIDITY_DURATION)){ //if the invitation is too old, drop it Log.d(TAG, "handleInvitationAcceptance(): Invitation is outdated"); return false; } Game game = getGame(gameId); if (game != null) { if (game.getSelectedOpponent() == null) { if (isAccept) { game.setSelectedOpponent(opponent.getUsername()); checkStartGame(context, game); } else { //game was rejected game.removeInvitee(opponent.getUsername()); if (!game.hasRealInvitee()) { if (game.setRandomAIOpponent()) { checkStartGame(context, game); } else { //no more opponents. remove this game removeGame(game.getGameId()); } } } } else { Log.d(TAG, "handleInvitationAcceptance(): Game has already been accepted. ignoring acceptance."); } } return true; } private static boolean handleOpponentChoice(final MMXMessage message) { Map<String,String> messageContent = message.getContent(); String choiceStr = messageContent.get(MessageConstants.KEY_CHOICE); String gameId = messageContent.get(MessageConstants.KEY_GAMEID); Choice choice = MessageConstants.CHOICE_MAP.get(choiceStr); Log.d(TAG, "handleOpponentChoice(): gameId=" + gameId + ", choice=" + choiceStr); if (gameId == null || choiceStr == null) { Log.e(TAG, "handleOpponentChoice(): gameId or choiceStr was null. Can't continue"); return false; } else if (choice == null) { Log.e(TAG, "handleOpponentChoice(): unrecognized choice string: " + choiceStr); return false; } Game game = getGame(gameId); game.setOpponentChoice(MessageConstants.CHOICE_MAP.get(choiceStr)); return true; } private static void checkStartGame(final Context context, final Game game) { final UserProfile opponent = game.getSelectedOpponent(); if (opponent != null) { AlertDialog.Builder builder = new AlertDialog.Builder(context) .setTitle(R.string.invitation_accepted) .setMessage(context.getString(R.string.starting_game, opponent.getUsername())) .setPositiveButton(R.string.btn_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { launchGameActivity(context, game.mGameId); dialog.dismiss(); } }); builder.show(); } } public static void launchGameActivity(Context context, String gameId) { Intent intent = new Intent(context, GameActivity.class); intent.putExtra(EXTRA_GAME_ID, gameId); context.startActivity(intent); } public static boolean sendInvitations(Context context, List<UserProfile> invitees) { boolean result = false; if (invitees != null && invitees.size() > 0) { String gameId = generateGameId(); HashMap<String,String> messageContent = new HashMap<>(); messageContent.put(MessageConstants.KEY_TYPE, MessageConstants.TYPE_INVITATION); MyProfile myProfile = MyProfile.getInstance(context); setProfileToMessage(myProfile, messageContent); messageContent.put(MessageConstants.KEY_GAMEID, gameId); messageContent.put(MessageConstants.KEY_TIMESTAMP, String.valueOf(System.currentTimeMillis())); final HashSet<User> recipients = new HashSet<>(); ArrayList<UserProfile> aiPlayers = new ArrayList<>(); for (UserProfile invitee : invitees) { if (!invitee.isArtificialIntelligence()) { recipients.add(invitee.getUser()); } else { aiPlayers.add(invitee); } } Game game = new Game(gameId, invitees); synchronized (sPendingGames) { sPendingGames.put(gameId, game); } if (recipients.size() > 0) { MMXMessage message = new MMXMessage.Builder() .recipients(recipients) .content(messageContent) .build(); message.send(new MMXMessage.OnFinishedListener<String>() { public void onSuccess(String messageId) { Log.d(TAG, "sendInvitations(): sent invitation to " + recipients.size() + " users. messageId=" + messageId); } public void onFailure(MMXMessage.FailureCode failureCode, Throwable throwable) { Log.e(TAG, "sendInvitations(): unable to send invitation to recipient: " + failureCode + ", " + throwable, throwable); } }); game.waitForOpponent(context, MessageConstants.OPPONENT_ACCEPTANCE_WAIT_TIME); result = true; } else if (aiPlayers.size() > 0) { game.setRandomAIOpponent(); checkStartGame(context, game); result = true; } } return result; } private static MMXChannel sAvailabilityChannel = null; public static Game getGame(String gameId) { synchronized (sPendingGames) { return sPendingGames.get(gameId); } } public static Game removeGame(String gameId) { synchronized (sPendingGames) { return sPendingGames.remove(gameId); } } } /** * Representation of an actual game. Each time the current user invites players to play, * a new Game object is created in memory and the invitees responses are in references to the * specific game instance. */ public static class Game { private static final String TAG = Game.class.getSimpleName(); private String mGameId = null; private List<UserProfile> mInvitees = null; private UserProfile mSelectedOpponent = null; private Choice mOpponentChoice = null; private Runnable mWaitRunnable = null; /** * Called by the inviter * * @param gameId the game id * @param invitees a list of invitees */ private Game(String gameId, List<UserProfile> invitees) { mGameId = gameId; mInvitees = invitees; } /** * Called by the person accepting the invitation * * @param gameId the game id * @param selectedOpponent the selected opponent */ private Game(String gameId, UserProfile selectedOpponent) { mGameId = gameId; mSelectedOpponent = selectedOpponent; } public final String getGameId() { return mGameId; } private synchronized void removeInvitee(String username) { for (int i=mInvitees.size(); --i>=0;) { if (mInvitees.get(i).getUsername().equals(username)) { mInvitees.remove(i); break; } } } private synchronized boolean hasRealInvitee() { for (UserProfile invitee : mInvitees) { if (!invitee.isArtificialIntelligence()) { return true; } } return false; } private synchronized boolean setSelectedOpponent(String username) { if (mInvitees == null) { throw new IllegalStateException("This method should be called by the original inviter"); } if (username == null) { throw new IllegalArgumentException("Username cannot be null"); } if (mSelectedOpponent != null) { //opponent already selected return false; } for (UserProfile curProfile : mInvitees) { if (curProfile.getUsername().equals(username)) { mSelectedOpponent = curProfile; return true; } } throw new IllegalArgumentException("Specified username was not an invitee: " + username); } private synchronized boolean setRandomAIOpponent() { if (mInvitees == null) { throw new IllegalStateException("This method should be called by the original inviter"); } ArrayList<UserProfile> ai = new ArrayList<UserProfile>(); for (UserProfile curProfile : mInvitees) { if (curProfile.isArtificialIntelligence()) { ai.add(curProfile); } } int aiCount = ai.size(); if (aiCount == 0) { Log.d(TAG, "setRandomAIOpponent(): no ai opponents were invited"); return false; } else { mSelectedOpponent = ai.get(Util.RANDOM.nextInt(aiCount)); return true; } } public synchronized UserProfile getSelectedOpponent() { return mSelectedOpponent; } private void setOpponentChoice(Choice choice) { synchronized (this) { mOpponentChoice = choice; this.notifyAll(); } } private void waitForOpponent(final Context context, long waitTime) { mWaitRunnable = new Runnable() { public void run() { //if this runs, it means that it wasn't cancelled, which means that there was no response from a real player synchronized (Game.this) { if (mSelectedOpponent == null) { //select an opponent setRandomAIOpponent(); Util.checkStartGame(context, Game.this); } } } }; Handler handler = new Handler(); handler.postDelayed(mWaitRunnable, waitTime); } /** * Called when the current player makes a choice. This will block until the opponent makes their * choice selection or until timeout specified in the MessageConstants value. * * @param context the context * @param choice the current user's choice * @see com.magnet.demo.mmx.rpsls.RPSLS.MessageConstants#OPPONENT_CHOICE_WAIT_TIME * @return the result of this game */ public Result getResult(Context context, Choice choice) { if (!mSelectedOpponent.isArtificialIntelligence()) { HashMap<String, String> messageContent = new HashMap<>(); messageContent.put(MessageConstants.KEY_TYPE, MessageConstants.TYPE_CHOICE); MyProfile profile = MyProfile.getInstance(context); RPSLS.Util.setProfileToMessage(profile, messageContent); messageContent.put(MessageConstants.KEY_GAMEID, mGameId); messageContent.put(MessageConstants.KEY_CHOICE, MessageConstants.CHOICE_REVERSE_MAP.get(choice)); messageContent.put(MessageConstants.KEY_TIMESTAMP, String.valueOf(System.currentTimeMillis())); HashSet<User> recipients = new HashSet<>(); recipients.add(mSelectedOpponent.getUser()); MMXMessage message = new MMXMessage.Builder() .recipients(recipients) .content(messageContent) .build(); message.send(new MMXMessage.OnFinishedListener<String>() { public void onSuccess(String messageId) { Log.d(TAG, "getResult(): sent choice to opponent. messageId=" + messageId); } public void onFailure(MMXMessage.FailureCode failureCode, Throwable throwable) { Log.e(TAG, "getresult(): unable to send choice to opponent.", throwable); } }); synchronized (this) { if (mOpponentChoice == null) { try { this.wait(MessageConstants.OPPONENT_CHOICE_WAIT_TIME); } catch (InterruptedException e) { Log.e(TAG, "getResult(): caught exception", e); } } } } else { //AI. randomly make a choice and return the result mOpponentChoice = Choice.values()[Util.RANDOM.nextInt(Choice.values().length)]; } if (mOpponentChoice != null) { BeatsHow iWinHow = RPSLS.getHowAttackerWins(choice, mOpponentChoice); BeatsHow theyWinHow = RPSLS.getHowAttackerWins(mOpponentChoice, choice); BeatsHow how = null; Outcome outcome; if (iWinHow != null) { how = iWinHow; outcome = Outcome.WIN; } else if (theyWinHow != null) { how = theyWinHow; outcome = Outcome.LOSS; } else { outcome = Outcome.DRAW; } MyProfile.getInstance(context).incrementCount(choice, outcome); return new Result(outcome, how != null ? how.getHow() : null, choice, mOpponentChoice, how != null ? how.getResourceId() : R.drawable.draw); } else { return null; } } /** * Represents the results of the current game. * * @see com.magnet.demo.mmx.rpsls.RPSLS.Game#getResult(Context, Choice) */ public static class Result { public final Outcome outcome; public final How how; public final Choice myChoice; public final Choice opponentChoice; public final int resourceId; private Result(Outcome outcome, How how, Choice myChoice, Choice opponentChoice, int resourceId) { this.outcome = outcome; this.how = how; this.myChoice = myChoice; this.opponentChoice = opponentChoice; this.resourceId = resourceId; } } } }