This is the code for the Tic Tac Toe multiplayer game android app. A user can play with the app logic as a single player or with another user. Any user is created using authentication by email and password. For each user, there's a count of wins and losses and available open games.
It uses Android Navigation Component, with a single activity and three fragments:
-
The DashboardFragment is the home screen. If a user is not logged in, it will navigate to the LoginFragment.
-
The floating button in the dashboard creates a dialog that asks which type of game to create and passes that information to the GameFragment (using SafeArgs).
-
Appropriate listeners and game play logic are added in GameFragment
-
Pressing the back button in the GameFragment opens a dialog that confirms if the user wants to forfeit the game.
-
A "log out" action bar menu is shown on both the dashboard and the game fragments. Clicking it should log the user out and show the LoginFragment. This click is handled in the MainActivity.
- For player 2, there's no warning for waiting for their turn when it's not their turn
- The app crashes when player who creates a two-player game forfeits the game and when the other user clicks back
- Use the APK to install on different users.
- To make the signin/register user page as the initial fragment, I changed the start destination to
loginFragment
innav_graph.xml
:
app:startDestination="@id/loginFragment">
- Firebase Authentication is used for registering and signing in a user. In
LoginFragment.java
: InonCreate()
, a FirebaseAuth instance is created:
mAuth = FirebaseAuth.getInstance();
-
On clicking the register/sign in button the
createUserWithEmailAndPassword
andsignInWithEmailAndPassword()
are used for signup and signin respectively. -
On other fragments, the user can log out by clicking the options menu. In
MainActivity.java
following code logs out user
FirebaseAuth.getInstance().signOut();
NavController mNavController = Navigation.findNavController(this, R.id.nav_host_fragment);
mNavController.navigate(R.id.loginFragment);
- In
LoginFragment.java
, a new user is added to database on sign up:
// add player
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference userRef = database.getReference("users");
assert currentUser != null;
Player player = new Player(currentUser.getUid(),0,0, currentUser.getEmail());
userRef.child(currentUser.getUid()).setValue(player);
- There's a
Game
class that stores various parameters like, the game uuid, the current board, if it's singleplayer, etc ->
private String uuid;
private boolean open;
private boolean singlePlayer;
private String player1, player2;
private Integer turn;
private String winner;
private String status;
private List<String> tictactoe;
-
GameLogic.java
implements the logic for checking who has won, i.e. it checks all the 8 possible combinations of winning situations, and if not won, it is a tie. -
The user starts first and by default their move is "X"
-
In
GameFragment.java
, the player plays against the computer i.e. the app logic, where the computer sets the next available button on tictactoe as "O". -
On winning, the number of wins and losses are updated in the database the dashboard for the user.
-
Game uuid is also stored in the database, and it is fetched through safe args :
gameUUID = args.getGameID();
- Then it is checked if it's empty, that is the user who creates it will be player1 and vice-versa
if(gameUUID.isEmpty()) {
Game game = createGame();
mLogic = new GameLogic(game);
mLogic.setTurn = 1;
gameRef.child(game.getUUID()).setValue(game);
Log.d(TAG, "Player1: " + currentUser.getEmail() + " has created a " + args.getGameType() + " game : " + gameUUID);
} else {
mLogic = new GameLogic(gameUUID, singlePlayer);
mLogic.setTurn = 2;
setTwoPlayerMatch(gameUUID);
Log.d(TAG, "Player2: " + currentUser.getEmail() + " has joined game : " + gameUUID);
Toast.makeText(requireContext(),"Joined game as Player 2", Toast.LENGTH_SHORT).show();
}
private Game createGame() {
boolean open = !singlePlayer;
String player1 = currentUser.getUid();
String player2 = singlePlayer ? getString(R.string.computer) : getString(R.string.waiting);
String status = singlePlayer ? getString(R.string.status_started) : getString(R.string.status_waiting);
String winner = getString(R.string.undecided);
Integer turn = 1;
return new Game(open, singlePlayer, player1,player2,status,winner,turn);
}
- Once the other user has joined, a listener was needed to check for when a user joined an ongoing game, and to check when the data was being changed on database.
private void notifyPlayer1ThatPlayer2Joined() {
gameRef.child(mLogic.gameUUID).child("player2").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if(snapshot.getValue() == null) return;
updateFieldsOnPlayerJoin(snapshot.getValue().toString());
updateUI();
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
Log.w(TAG, "onCancelled", error.toException());
}
});
}
- For checking who has won, I've checked all the possible combinations in which players can win
- The database has 2 paths, one for game and other for users
- The game model stores gameID, isOpen, player1ID, player2ID, isSinglePlayer, status, board's status, whose turn, and who won
- The user model stores userID, wins, losses and their email (for displaying "Welcome, ")
- Manually tested by different users.
- For monkey stress testing : Ran the monkey tool successfully for 10000 iterations using the command
adb -s a470ca26 shell monkey -p androidsamples.java.tictactoe -v 10000
It was fairly smooth to navigate througout the application, where the assitant talks everything on the screen aloud, like the wins, losses, whether to create a single-player or multi-player game.
It gives suggestions like Multiple Descriptions
, Text contrast
for "OK", "ONE-PLAYER" in dialog box, Image contrast
for + button
Took around 48 hours to complete this project.