├── .idea
├── .name
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── vcs.xml
├── modules.xml
├── runConfigurations.xml
├── compiler.xml
├── gradle.xml
└── misc.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── api_key.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── styles.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── colors.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── fragment_game.xml
│ │ │ │ └── fragment_intro.xml
│ │ │ └── values-w820dp
│ │ │ │ └── dimens.xml
│ │ ├── java
│ │ │ └── me
│ │ │ │ └── mattlogan
│ │ │ │ └── twentyseven
│ │ │ │ ├── OnNearbyApiAvailableEvent.java
│ │ │ │ ├── Plane.java
│ │ │ │ ├── PlaneTracker.java
│ │ │ │ ├── AppModule.java
│ │ │ │ ├── messages
│ │ │ │ ├── MessagePublisher.java
│ │ │ │ └── IncomingMessageRouter.java
│ │ │ │ ├── game
│ │ │ │ ├── Game.java
│ │ │ │ ├── GameFragment.java
│ │ │ │ ├── WinChecker.java
│ │ │ │ └── BoardView.java
│ │ │ │ ├── intro
│ │ │ │ └── IntroFragment.java
│ │ │ │ └── MainActivity.java
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ └── java
│ │ │ └── me
│ │ │ └── mattlogan
│ │ │ └── twentyseven
│ │ │ └── ApplicationTest.java
│ └── test
│ │ └── java
│ │ └── me
│ │ └── mattlogan
│ │ └── twentyseven
│ │ ├── GameTest.java
│ │ └── WinCheckerTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | Twentyseven
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattlogan/Twenty-seven/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattlogan/Twenty-seven/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattlogan/Twenty-seven/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattlogan/Twenty-seven/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattlogan/Twenty-seven/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattlogan/Twenty-seven/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/OnNearbyApiAvailableEvent.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | public final class OnNearbyApiAvailableEvent {
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/api_key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AIzaSyDDk0zt6wHVipPF0a2IsU_X-atebaiquz4
4 |
5 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | https://www.youtube.com/watch?v=8_pHdeu4Rks
2 |
3 | This is my submission for the Android Experiments 2016 contest.
4 |
5 | Twenty-seven is a three-dimensional tic-tac-toe game where the grid is a 3x3x3 cube and a player can win by connecting three spaces in any direction, including diagonals. Each vertical plane is represented by a separate Android device.
6 |
7 | The Android devices communicate via the Nearby Messages API.
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Twenty-seven
3 | Front
4 | Middle
5 | Back
6 | Waiting…
7 | %c\'s turn
8 | %c wins
9 | New game
10 |
11 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/me/mattlogan/twentyseven/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/Plane.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | public enum Plane {
4 | FRONT(0),
5 | MIDDLE(1),
6 | BACK(2);
7 |
8 | private final int zValue;
9 |
10 | Plane(int zValue) {
11 | this.zValue = zValue;
12 | }
13 |
14 | public int zValue() {
15 | return zValue;
16 | }
17 |
18 | public String toDisplayString() {
19 | String uppercase = this.toString();
20 | return uppercase.charAt(0) + uppercase.substring(1).toLowerCase();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/PlaneTracker.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | import timber.log.Timber;
4 |
5 | /**
6 | * Informs the rest of the application which plane this instance of the app represents in the
7 | * 3 dimensional tic tac toe space -- front, middle, or back
8 | */
9 | public class PlaneTracker {
10 |
11 | private Plane plane;
12 |
13 | public void updatePlane(Plane plane) {
14 | Timber.d("updatePlane: %s", plane);
15 | this.plane = plane;
16 | }
17 |
18 | public Plane currentPlane() {
19 | return plane;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #830509
4 | #410000
5 | #50815A
6 | #333333
7 | #E7DC8C
8 | #FFFFFF
9 | #B20B38
10 | #F0AF00
11 | #649500
12 | #195B83
13 | #2F1B60
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/matthewlogan/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
12 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/test/java/me/mattlogan/twentyseven/GameTest.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | import org.junit.Test;
4 |
5 | import me.mattlogan.twentyseven.game.Game;
6 |
7 | import static junit.framework.Assert.assertEquals;
8 |
9 | public class GameTest {
10 |
11 | @Test
12 | public void testGameToMessage() {
13 | Game game = Game.createNewGame();
14 | char[][][] grid = game.grid();
15 | grid[0][0][0] = 'X';
16 | grid[1][0][0] = 'X';
17 | grid[2][0][0] = 'X';
18 |
19 | grid[0][0][2] = 'O';
20 | grid[1][1][2] = 'O';
21 | grid[2][1][2] = 'O';
22 |
23 | assertEquals("XXXEEEEEEEEEEEEEEEOEEEOOEEE_TURN_X", game.toString());
24 | }
25 |
26 | @Test
27 | public void testMessageToGame() {
28 | Game game = Game.createNewGame();
29 | char[][][] grid = game.grid();
30 | grid[0][0][0] = 'X';
31 | grid[1][0][0] = 'X';
32 | grid[2][0][0] = 'X';
33 |
34 | grid[0][0][2] = 'O';
35 | grid[1][1][2] = 'O';
36 | grid[2][1][2] = 'O';
37 |
38 | assertEquals(game, Game.fromString("XXXEEEEEEEEEEEEEEEOEEEOOEEE_TURN_X"));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "23.0.2"
6 |
7 | defaultConfig {
8 | applicationId "me.mattlogan.twentyseven"
9 | minSdkVersion 19
10 | targetSdkVersion 23
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 |
15 | testOptions {
16 | unitTests.all {
17 | testLogging {
18 | events "passed", "skipped", "failed", "standardOut", "standardError"
19 | }
20 | }
21 | }
22 | }
23 |
24 | dependencies {
25 | compile 'com.android.support:appcompat-v7:23.2.1'
26 | compile 'com.google.android.gms:play-services-nearby:8.4.0'
27 | compile 'com.jakewharton.timber:timber:4.1.1'
28 | compile 'com.jakewharton:butterknife:7.0.1'
29 | compile 'com.squareup:otto:1.3.8'
30 | compile 'com.squareup.dagger:dagger:1.2.2'
31 | provided 'com.squareup.dagger:dagger-compiler:1.2.2'
32 | compile 'com.android.support:percent:23.2.1'
33 | compile 'com.android.support:gridlayout-v7:23.2.1'
34 |
35 | testCompile 'junit:junit:4.12'
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/test/java/me/mattlogan/twentyseven/WinCheckerTest.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | import org.junit.Test;
4 |
5 | import me.mattlogan.twentyseven.game.Game;
6 | import me.mattlogan.twentyseven.game.WinChecker;
7 |
8 | import static org.junit.Assert.*;
9 |
10 | /**
11 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
12 | */
13 | public class WinCheckerTest {
14 |
15 | @Test
16 | public void testHorizontalWinInFrontPlane() throws Exception {
17 | char[][][] grid = Game.createNewGame().grid();
18 | grid[0][0][0] = 'X';
19 | grid[0][1][0] = 'X';
20 | grid[0][2][0] = 'X';
21 |
22 | WinChecker.Win win = WinChecker.checkForWinner(grid);
23 |
24 | assertNotNull(win);
25 | assertTrue(win.winner() == 'X');
26 | }
27 |
28 | @Test
29 | public void testCubeDiagonalWin() throws Exception {
30 | char[][][] grid = Game.createNewGame().grid();
31 | grid[0][0][0] = 'O';
32 | grid[1][1][1] = 'O';
33 | grid[2][2][2] = 'O';
34 |
35 | WinChecker.Win win = WinChecker.checkForWinner(grid);
36 |
37 | assertNotNull(win);
38 | assertTrue(win.winner() == 'O');
39 | }
40 |
41 | @Test
42 | public void testNoWinner() throws Exception {
43 | char[][][] grid = Game.createNewGame().grid();
44 |
45 | assertNull(WinChecker.checkForWinner(grid));
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/AppModule.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | import android.content.Context;
4 |
5 | import com.google.android.gms.common.api.GoogleApiClient;
6 | import com.google.android.gms.nearby.Nearby;
7 | import com.squareup.otto.Bus;
8 |
9 | import javax.inject.Singleton;
10 |
11 | import dagger.Module;
12 | import dagger.Provides;
13 | import me.mattlogan.twentyseven.game.GameFragment;
14 | import me.mattlogan.twentyseven.intro.IntroFragment;
15 | import me.mattlogan.twentyseven.messages.IncomingMessageRouter;
16 | import me.mattlogan.twentyseven.messages.MessagePublisher;
17 |
18 | @Module(
19 | injects = {
20 | MainActivity.class,
21 | IntroFragment.class,
22 | GameFragment.class
23 | }
24 | )
25 | public final class AppModule {
26 |
27 | private final Context context;
28 |
29 | public AppModule(Context context) {
30 | this.context = context;
31 | }
32 |
33 | @Provides @Singleton Bus bus() {
34 | return new Bus();
35 | }
36 |
37 | @Provides @Singleton PlaneTracker planeTracker() {
38 | return new PlaneTracker();
39 | }
40 |
41 | @Provides @Singleton GoogleApiClient client() {
42 | return new GoogleApiClient.Builder(context)
43 | .addApi(Nearby.MESSAGES_API)
44 | .build();
45 | }
46 |
47 | @Provides @Singleton IncomingMessageRouter router() {
48 | return new IncomingMessageRouter();
49 | }
50 |
51 | @Provides @Singleton MessagePublisher publisher(GoogleApiClient client) {
52 | return new MessagePublisher(client);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_game.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
30 |
31 |
38 |
39 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/messages/MessagePublisher.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven.messages;
2 |
3 | import android.os.Handler;
4 | import android.os.Looper;
5 | import android.support.annotation.NonNull;
6 |
7 | import com.google.android.gms.common.api.GoogleApiClient;
8 | import com.google.android.gms.common.api.ResultCallback;
9 | import com.google.android.gms.common.api.Status;
10 | import com.google.android.gms.nearby.Nearby;
11 | import com.google.android.gms.nearby.messages.Message;
12 |
13 | import me.mattlogan.twentyseven.Plane;
14 | import me.mattlogan.twentyseven.game.Game;
15 | import timber.log.Timber;
16 |
17 | public final class MessagePublisher {
18 |
19 | static final String PLANE_SELECTED = "PLANE_SELECTED_";
20 | static final String GAME_UPDATED = "GAME_UPDATED_";
21 |
22 | private final GoogleApiClient client;
23 | private final Handler handler;
24 |
25 | public MessagePublisher(GoogleApiClient client) {
26 | this.client = client;
27 | this.handler = new Handler(Looper.getMainLooper());
28 | }
29 |
30 | public void publishPlaneSelectedMessage(Plane plane) {
31 | String message = PLANE_SELECTED + plane;
32 | Timber.d("Publishing plane selected message: %s", message);
33 | publish(message);
34 | }
35 |
36 | public void publishGameUpdateMessage(Game game) {
37 | String message = GAME_UPDATED + game;
38 | Timber.d("Publishing game updated message: %s", message);
39 | publish(message);
40 | }
41 |
42 | private void publish(String messageString) {
43 | final Message message = new Message(messageString.getBytes());
44 | Nearby.Messages.publish(client, message)
45 | .setResultCallback(new ResultCallback() {
46 | @Override public void onResult(@NonNull Status status) {
47 | if (status.isSuccess()) {
48 | Timber.d("Successfully published message");
49 | } else {
50 | Timber.d("Failed to publish message: %s", status.getStatusMessage());
51 | }
52 | }
53 | });
54 |
55 | // Unpublish message after five seconds
56 | handler.postDelayed(new Runnable() {
57 | @Override public void run() {
58 | Nearby.Messages.unpublish(client, message);
59 | Timber.d("Unpublished message");
60 | }
61 | }, 5000);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/messages/IncomingMessageRouter.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven.messages;
2 |
3 | import com.google.android.gms.nearby.messages.Message;
4 | import com.google.android.gms.nearby.messages.MessageListener;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | import me.mattlogan.twentyseven.Plane;
10 | import me.mattlogan.twentyseven.game.Game;
11 | import timber.log.Timber;
12 |
13 | import static me.mattlogan.twentyseven.messages.MessagePublisher.PLANE_SELECTED;
14 | import static me.mattlogan.twentyseven.messages.MessagePublisher.GAME_UPDATED;
15 |
16 | public final class IncomingMessageRouter extends MessageListener {
17 |
18 | public interface RemotePlaneSelectedListener {
19 | /** Plane selected on another device */
20 | void onRemotePlaneSelected(Plane plane);
21 | }
22 |
23 | public interface GameplayListener {
24 | void onGameUpdated(Game game);
25 | }
26 |
27 | private List remotePlaneSelectedListeners = new ArrayList<>();
28 | private List gameplayListeners = new ArrayList<>();
29 |
30 | @Override public void onFound(Message message) {
31 | String s = new String(message.getContent());
32 | Timber.d("onFound: %s", s);
33 | if (s.startsWith(PLANE_SELECTED)) {
34 | Plane selectedPlane = Plane.valueOf(s.substring(PLANE_SELECTED.length()));
35 | for (RemotePlaneSelectedListener listener : remotePlaneSelectedListeners) {
36 | listener.onRemotePlaneSelected(selectedPlane);
37 | }
38 | } else if (s.startsWith(GAME_UPDATED)) {
39 | Game game = Game.fromString(s.substring(GAME_UPDATED.length()));
40 | for (GameplayListener listener : gameplayListeners) {
41 | listener.onGameUpdated(game);
42 | }
43 | }
44 | }
45 |
46 | public void addPlaneSelectedListener(RemotePlaneSelectedListener listener) {
47 | remotePlaneSelectedListeners.add(listener);
48 | }
49 |
50 | public void addGameUpdatedListener(GameplayListener listener) {
51 | gameplayListeners.add(listener);
52 | }
53 |
54 | public void removePlaneSelectedListener(RemotePlaneSelectedListener listener) {
55 | remotePlaneSelectedListeners.remove(listener);
56 | }
57 |
58 | public void removeGameUpdatedListener(GameplayListener listener) {
59 | gameplayListeners.remove(listener);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/game/Game.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven.game;
2 |
3 | import java.util.Arrays;
4 |
5 | /**
6 | * Representation of game state. 'E' is an empty space, 'X' and 'O' should be obvious!
7 | */
8 | public class Game {
9 |
10 | private static final String TURN_PREFIX = "_TURN_";
11 |
12 | private final char[][][] grid;
13 | private char turn;
14 |
15 | private Game(char[][][] grid, char turn) {
16 | this.grid = grid;
17 | this.turn = turn;
18 | }
19 |
20 | public static Game createNewGame() {
21 | return new Game(emptyGrid(), 'X');
22 | }
23 |
24 | private static char[][][] emptyGrid() {
25 | char[][][] grid = new char[3][3][3];
26 | for (int z = 0; z < 3; z++) {
27 | for (int y = 0; y < 3; y++) {
28 | for (int x = 0; x < 3; x++) {
29 | grid[x][y][z] = 'E';
30 | }
31 | }
32 | }
33 | return grid;
34 | }
35 |
36 | public static Game fromString(String str) {
37 | String gridStr = str.substring(0, str.indexOf(TURN_PREFIX));
38 | char[][][] grid = emptyGrid();
39 | for (int strPos = 0; strPos < gridStr.length(); strPos += 1) {
40 | int x = strPos % 3;
41 | int y = (strPos / 3) % 3;
42 | int z = (strPos / 9) % 3;
43 | grid[x][y][z] = gridStr.charAt(strPos);
44 | }
45 |
46 | char turn = str.charAt(str.indexOf(TURN_PREFIX) + TURN_PREFIX.length());
47 |
48 | return new Game(grid, turn);
49 | }
50 |
51 | public char[][][] grid() {
52 | return grid;
53 | }
54 |
55 | public char turn() {
56 | return turn;
57 | }
58 |
59 | public void incrementTurn() {
60 | if (turn == 'X') {
61 | turn = 'O';
62 | } else if (turn == 'O') {
63 | turn = 'X';
64 | }
65 | }
66 |
67 | @Override
68 | public String toString() {
69 | StringBuilder sb = new StringBuilder();
70 | for (int z = 0; z < 3; z++) {
71 | for (int y = 0; y < 3; y++) {
72 | for (int x = 0; x < 3; x++) {
73 | sb.append(grid[x][y][z]);
74 | }
75 | }
76 | }
77 | sb.append(TURN_PREFIX).append(turn);
78 | return sb.toString();
79 | }
80 |
81 | @Override public boolean equals(Object o) {
82 | if (this == o) return true;
83 | if (o == null || getClass() != o.getClass()) return false;
84 |
85 | Game game = (Game) o;
86 |
87 | if (turn != game.turn) return false;
88 | return Arrays.deepEquals(grid, game.grid);
89 | }
90 |
91 | @Override public int hashCode() {
92 | int result = Arrays.deepHashCode(grid);
93 | result = 31 * result + (int) turn;
94 | return result;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_intro.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
18 |
19 |
30 |
31 |
35 |
36 |
47 |
48 |
52 |
53 |
64 |
65 |
69 |
70 |
71 |
72 |
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/game/GameFragment.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven.game;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.app.Fragment;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.Button;
9 | import android.widget.TextView;
10 |
11 | import javax.inject.Inject;
12 |
13 | import butterknife.Bind;
14 | import butterknife.ButterKnife;
15 | import butterknife.OnClick;
16 | import me.mattlogan.twentyseven.MainActivity;
17 | import me.mattlogan.twentyseven.PlaneTracker;
18 | import me.mattlogan.twentyseven.R;
19 | import me.mattlogan.twentyseven.messages.IncomingMessageRouter;
20 | import me.mattlogan.twentyseven.messages.MessagePublisher;
21 | import timber.log.Timber;
22 |
23 | public class GameFragment extends Fragment
24 | implements IncomingMessageRouter.GameplayListener, BoardView.ActionListener {
25 |
26 | @Inject IncomingMessageRouter messageRouter;
27 | @Inject MessagePublisher messagePublisher;
28 | @Inject PlaneTracker planeTracker;
29 |
30 | @Bind(R.id.board_view) BoardView boardView;
31 | @Bind(R.id.game_status_text) TextView statusText;
32 | @Bind(R.id.plane_label) TextView planeLabel;
33 | @Bind(R.id.button_new_game) Button newGameButton;
34 |
35 | private Game game;
36 |
37 | @Override public View onCreateView(LayoutInflater inflater, ViewGroup root, Bundle state) {
38 | ((MainActivity) getActivity()).inject(this);
39 | View view = inflater.inflate(R.layout.fragment_game, root, false);
40 | ButterKnife.bind(this, view);
41 | messageRouter.addGameUpdatedListener(this);
42 | boardView.setActionListener(this);
43 | planeLabel.setText(planeTracker.currentPlane().toDisplayString());
44 | onGameUpdated(Game.createNewGame()); // Start with new game
45 | return view;
46 | }
47 |
48 | @Override public void onDestroyView() {
49 | super.onDestroyView();
50 | messageRouter.removeGameUpdatedListener(this);
51 | }
52 |
53 | @Override public void onGameUpdated(Game game) {
54 | Timber.d("onGameUpdated: %s", game);
55 | this.game = game;
56 | updateViews();
57 | }
58 |
59 | @Override public void onActionTaken(int space, char mark) {
60 | Timber.d("onActionTaken, space: %d, mark %c", space, mark);
61 | char[][][] grid = game.grid();
62 | grid[space % 3][space / 3][planeTracker.currentPlane().zValue()] = mark;
63 | game.incrementTurn();
64 | messagePublisher.publishGameUpdateMessage(game);
65 | updateViews();
66 | }
67 |
68 | private void updateViews() {
69 | WinChecker.Win win = WinChecker.checkForWinner(game.grid());
70 | if (win != null) {
71 | statusText.setText(getString(R.string.x_wins, win.winner()));
72 | boardView.showWin(win.winner(), win.spaces(), planeTracker.currentPlane().zValue());
73 | newGameButton.setVisibility(View.VISIBLE);
74 | } else {
75 | boardView.updateTurn(game.turn());
76 | boardView.updateGrid(game.grid(), planeTracker.currentPlane().zValue());
77 | boardView.clearWin();
78 | statusText.setText(getString(R.string.xs_turn, game.turn()));
79 | newGameButton.setVisibility(View.GONE);
80 | }
81 | }
82 |
83 | @OnClick(R.id.button_new_game)
84 | public void onNewGameClicked() {
85 | Timber.d("onNewGameClicked");
86 | game = Game.createNewGame();
87 | updateViews();
88 | messagePublisher.publishGameUpdateMessage(game);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/intro/IntroFragment.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven.intro;
2 |
3 | import android.os.Bundle;
4 | import android.support.v4.app.Fragment;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.Button;
9 | import android.widget.TextView;
10 |
11 | import com.squareup.otto.Bus;
12 | import com.squareup.otto.Subscribe;
13 |
14 | import javax.inject.Inject;
15 |
16 | import butterknife.Bind;
17 | import butterknife.ButterKnife;
18 | import butterknife.OnClick;
19 | import me.mattlogan.twentyseven.MainActivity;
20 | import me.mattlogan.twentyseven.OnNearbyApiAvailableEvent;
21 | import me.mattlogan.twentyseven.Plane;
22 | import me.mattlogan.twentyseven.PlaneTracker;
23 | import me.mattlogan.twentyseven.R;
24 | import me.mattlogan.twentyseven.game.GameFragment;
25 | import me.mattlogan.twentyseven.messages.IncomingMessageRouter;
26 | import me.mattlogan.twentyseven.messages.MessagePublisher;
27 |
28 | public class IntroFragment extends Fragment
29 | implements IncomingMessageRouter.RemotePlaneSelectedListener {
30 |
31 | @Inject Bus bus;
32 | @Inject PlaneTracker planeTracker;
33 | @Inject MessagePublisher messagePublisher;
34 | @Inject IncomingMessageRouter messageRouter;
35 |
36 | @Bind(R.id.buttons_layout) View buttonsLayout;
37 | @Bind(R.id.button_front) Button frontButton;
38 | @Bind(R.id.button_middle) Button middleButton;
39 | @Bind(R.id.button_back) Button backButton;
40 | @Bind(R.id.waiting_text) TextView waitingText;
41 |
42 | private int numSelectedRemotePlanes;
43 |
44 | @Override public View onCreateView(LayoutInflater inflater, ViewGroup root, Bundle state) {
45 | ((MainActivity) getActivity()).inject(this);
46 | View view = inflater.inflate(R.layout.fragment_intro, root, false);
47 | ButterKnife.bind(this, view);
48 | bus.register(this);
49 | messageRouter.addPlaneSelectedListener(this);
50 | return view;
51 | }
52 |
53 | @Override public void onDestroyView() {
54 | super.onDestroyView();
55 | bus.unregister(this);
56 | messageRouter.removePlaneSelectedListener(this);
57 | }
58 |
59 | @OnClick(R.id.button_front) public void onFrontButtonClicked() {
60 | onLocalPlaneSelected(Plane.FRONT);
61 | }
62 |
63 | @OnClick(R.id.button_middle) public void onMiddleButtonClicked() {
64 | onLocalPlaneSelected(Plane.MIDDLE);
65 | }
66 |
67 | @OnClick(R.id.button_back) public void onBackButtonClicked() {
68 | onLocalPlaneSelected(Plane.BACK);
69 | }
70 |
71 | private void enableButtons() {
72 | frontButton.setEnabled(true);
73 | middleButton.setEnabled(true);
74 | backButton.setEnabled(true);
75 | }
76 |
77 | private void onLocalPlaneSelected(Plane plane) {
78 | messagePublisher.publishPlaneSelectedMessage(plane);
79 | planeTracker.updatePlane(plane);
80 | if (numSelectedRemotePlanes == 2) {
81 | continueToGame();
82 | } else {
83 | showWaiting();
84 | }
85 | }
86 |
87 | @Subscribe public void onNearbyApiAvailable(OnNearbyApiAvailableEvent e) {
88 | enableButtons();
89 | }
90 |
91 | @Override public void onRemotePlaneSelected(Plane plane) {
92 | switch (plane) {
93 | case FRONT:
94 | frontButton.setEnabled(false);
95 | break;
96 | case MIDDLE:
97 | middleButton.setEnabled(false);
98 | break;
99 | case BACK:
100 | backButton.setEnabled(false);
101 | break;
102 | }
103 |
104 | if (++numSelectedRemotePlanes == 2 && planeTracker.currentPlane() != null) {
105 | continueToGame();
106 | }
107 | }
108 |
109 | private void showWaiting() {
110 | buttonsLayout.setVisibility(View.GONE);
111 | waitingText.setVisibility(View.VISIBLE);
112 | }
113 |
114 | private void continueToGame() {
115 | getActivity().getSupportFragmentManager()
116 | .beginTransaction()
117 | .replace(R.id.fragment_container, new GameFragment())
118 | .commit();
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/MainActivity.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven;
2 |
3 | import android.content.Intent;
4 | import android.content.IntentSender;
5 | import android.support.annotation.NonNull;
6 | import android.support.annotation.Nullable;
7 | import android.support.v7.app.AppCompatActivity;
8 | import android.os.Bundle;
9 |
10 | import com.google.android.gms.common.api.GoogleApiClient;
11 | import com.google.android.gms.common.api.ResultCallback;
12 | import com.google.android.gms.common.api.Status;
13 | import com.google.android.gms.nearby.Nearby;
14 | import com.google.android.gms.nearby.messages.Strategy;
15 | import com.google.android.gms.nearby.messages.SubscribeOptions;
16 | import com.squareup.otto.Bus;
17 |
18 | import javax.inject.Inject;
19 |
20 | import dagger.ObjectGraph;
21 | import me.mattlogan.twentyseven.intro.IntroFragment;
22 | import me.mattlogan.twentyseven.messages.IncomingMessageRouter;
23 | import timber.log.Timber;
24 |
25 | /**
26 | * Host of IntroFragment and GameFragment, handles connecting to Nearby API
27 | */
28 | public final class MainActivity extends AppCompatActivity {
29 |
30 | @Inject GoogleApiClient client;
31 | @Inject Bus bus;
32 | @Inject IncomingMessageRouter messageRouter;
33 |
34 | private static final int PERMISSION_REQ_CODE = 123;
35 |
36 | private ObjectGraph graph;
37 |
38 | @Override protected void onCreate(Bundle savedInstanceState) {
39 | super.onCreate(savedInstanceState);
40 | if (Timber.treeCount() == 0) {
41 | Timber.plant(new Timber.DebugTree());
42 | }
43 | graph = ObjectGraph.create(new AppModule(this));
44 | inject(this);
45 | setContentView(R.layout.activity_main);
46 | if (savedInstanceState == null) {
47 | getSupportFragmentManager()
48 | .beginTransaction()
49 | .add(R.id.fragment_container, new IntroFragment())
50 | .commit();
51 | }
52 | }
53 |
54 | public void inject(Object o) {
55 | graph.inject(o);
56 | }
57 |
58 | @Override public void onStart() {
59 | super.onStart();
60 | Timber.d("onStart");
61 | initializeNearbyApi();
62 | }
63 |
64 | private void initializeNearbyApi() {
65 | client.registerConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
66 | @Override public void onConnected(@Nullable Bundle bundle) {
67 | Timber.d("onConnected");
68 | checkPermission();
69 | }
70 |
71 | @Override public void onConnectionSuspended(int i) {
72 | Timber.d("onConnectionSuspended");
73 | }
74 | });
75 |
76 | client.connect();
77 | }
78 |
79 | private void checkPermission() {
80 | Nearby.Messages.getPermissionStatus(client).setResultCallback(new ResultCallback() {
81 | @Override public void onResult(@NonNull Status status) {
82 | if (status.isSuccess()) {
83 | Timber.d("Permission request successful");
84 | onNearbyApiAvailable();
85 | } else if (status.hasResolution()) {
86 | try {
87 | Timber.d("Permission request failed, attempting to resolve");
88 | status.startResolutionForResult(MainActivity.this, PERMISSION_REQ_CODE);
89 | } catch (IntentSender.SendIntentException e) {
90 | Timber.d(e, "Error resolving permission failure");
91 | }
92 | }
93 | }
94 | });
95 | }
96 |
97 | @Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
98 | if (requestCode == PERMISSION_REQ_CODE && resultCode == RESULT_OK) {
99 | onNearbyApiAvailable();
100 | }
101 | }
102 |
103 | private void onNearbyApiAvailable() {
104 | Timber.d("onNearbyApiAvailable");
105 | SubscribeOptions options = new SubscribeOptions.Builder()
106 | .setStrategy(new Strategy.Builder()
107 | .setTtlSeconds(Strategy.TTL_SECONDS_INFINITE)
108 | .build())
109 | .build();
110 | Nearby.Messages.subscribe(client, messageRouter, options);
111 | bus.post(new OnNearbyApiAvailableEvent());
112 | }
113 |
114 | @Override public void onStop() {
115 | super.onStop();
116 | Timber.d("onStop");
117 | if (client.isConnected()) {
118 | Nearby.Messages.unsubscribe(client, messageRouter);
119 | client.disconnect();
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/game/WinChecker.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven.game;
2 |
3 | import android.support.annotation.Nullable;
4 |
5 | public final class WinChecker {
6 |
7 | private WinChecker() {
8 | // No instances
9 | }
10 |
11 | public static class Win {
12 | private final char winner;
13 | private final boolean[][][] spaces;
14 |
15 | public Win(char winner, boolean[][][] spaces) {
16 | this.winner = winner;
17 | this.spaces = spaces;
18 | }
19 |
20 | public char winner() {
21 | return winner;
22 | }
23 |
24 | public boolean[][][] spaces() {
25 | return spaces;
26 | }
27 | }
28 |
29 | /**
30 | * Checks for a winner. Perhaps a more elegant solution exists...
31 | *
32 | * @return a Result object with winner and winning spaces if there is a winner, or null if not
33 | */
34 | @Nullable public static Win checkForWinner(char[][][] grid) {
35 | // iterates through each x-y plane along the z-axis
36 | for (int i = 0; i < 3; i++) {
37 |
38 | // checks for vertical wins in the current x-y plane
39 | for (int j = 0; j < 3; j++) {
40 | char top = grid[j][0][i];
41 | char mid = grid[j][1][i];
42 | char bottom = grid[j][2][i];
43 |
44 | if (top != 'E' && top == mid && top == bottom) {
45 | return new Win(top, winningSpaces(j, 0, i, j, 1, i, j, 2, i));
46 | }
47 | }
48 |
49 | // front plane, checks for horizontal wins in the current x-y plane
50 | for (int j = 0; j < 3; j++) {
51 | char left = grid[0][j][i];
52 | char mid = grid[1][j][i];
53 | char right = grid[2][j][i];
54 |
55 | if (left != 'E' && left == mid && left == right) {
56 | return new Win(left, winningSpaces(0, j, i, 1, j, i, 2, j, i));
57 | }
58 | }
59 |
60 | // front plane, checks for diagonal wins in the current x-y plane
61 | char topLeft = grid[0][0][i];
62 | char topRight = grid[2][0][i];
63 | char mid = grid[1][1][i];
64 | char bottomLeft = grid[0][2][i];
65 | char bottomRight = grid[2][2][i];
66 |
67 | // top-left to bottom-right win in the current x-y plane
68 | if (topLeft != 'E' && topLeft == mid && topLeft == bottomRight) {
69 | return new Win(topLeft, winningSpaces(0, 0, i, 1, 1, i, 2, 2, i));
70 | }
71 |
72 | // top-right to bottom-left win in the current x-y plane
73 | if (topRight != 'E' && topRight == mid && topRight == bottomLeft) {
74 | return new Win(topRight, winningSpaces(2, 0, i, 1, 1, i, 0, 2, i));
75 | }
76 | }
77 |
78 | // check for wins straight along z-axis
79 | for (int i = 0; i < 3; i++) {
80 | for (int j = 0; j < 3; j++) {
81 | char front = grid[i][j][0];
82 | char mid = grid[i][j][1];
83 | char back = grid[i][j][2];
84 |
85 | if (front != 'E' && front == mid && front == back) {
86 | return new Win(front, winningSpaces(i, j, 0, i, j, 1, i, j, 2));
87 | }
88 | }
89 | }
90 |
91 | // iterate through y values, or x-z planes
92 | for (int i = 0; i < 3; i++) {
93 | char frontLeft = grid[0][i][0];
94 | char frontRight = grid[2][i][0];
95 | char mid = grid[1][i][1];
96 | char backLeft = grid[0][i][2];
97 | char backRight = grid[2][i][2];
98 |
99 | // front-left to back-right diagonal in current x-z plane
100 | if (frontLeft != 'E' && frontLeft == mid && frontLeft == backRight) {
101 | return new Win(frontLeft, winningSpaces(0, i, 0, 1, i, 1, 2, i, 2));
102 | }
103 |
104 | // front-right to back-left diagonal in current x-z plane
105 | if (frontRight != 'E' && frontRight == mid && frontRight == backLeft) {
106 | return new Win(frontRight, winningSpaces(2, i, 0, 1, i, 1, 0, i, 2));
107 | }
108 | }
109 |
110 | // iterate through x values, or y-z planes
111 | for (int i = 0; i < 3; i++) {
112 | char frontTop = grid[i][0][0];
113 | char frontBottom = grid[i][2][0];
114 | char mid = grid[i][1][1];
115 | char backTop = grid[i][0][2];
116 | char backBottom = grid[i][2][2];
117 |
118 | // front-top to back-bottom diagonal in current y-z plane
119 | if (frontTop != 'E' && frontTop == mid && frontTop == backBottom) {
120 | return new Win(frontTop, winningSpaces(i, 0, 0, i, 1, 1, i, 2, 2));
121 | }
122 |
123 | // front-bottom to back-top diagonal in current y-z plane
124 | if (frontBottom != 'E' && frontBottom == mid && frontBottom == backTop) {
125 | return new Win(frontBottom, winningSpaces(i, 2, 0, i, 1, 1, i, 0, 2));
126 | }
127 | }
128 |
129 | // check for 3-d diagonals!!!!
130 | char topLeftFront = grid[0][0][0];
131 | char topRightFront = grid[2][0][0];
132 | char bottomLeftFront = grid[0][2][0];
133 | char bottomRightFront = grid[2][2][0];
134 | char mid = grid[1][1][1];
135 | char topLeftBack = grid[0][0][2];
136 | char topRightBack = grid[2][0][2];
137 | char bottomLeftBack = grid[0][2][2];
138 | char bottomRightBack = grid[2][2][2];
139 |
140 | if (topLeftFront != 'E' && topLeftFront == mid && topLeftFront == bottomRightBack) {
141 | return new Win(topLeftFront, winningSpaces(0, 0, 0, 1, 1, 1, 2, 2, 2));
142 | }
143 |
144 | if (topRightFront != 'E' && topRightFront == mid && topRightFront == bottomLeftBack) {
145 | return new Win(topRightFront, winningSpaces(2, 0, 0, 1, 1, 1, 0, 2, 2));
146 | }
147 |
148 | if (bottomLeftFront != 'E' && bottomLeftFront == mid && bottomLeftFront == topRightBack) {
149 | return new Win(bottomLeftFront, winningSpaces(0, 2, 0, 1, 1, 1, 2, 0, 2));
150 | }
151 |
152 | if (bottomRightFront != 'E' && bottomRightFront == mid && bottomRightFront == topLeftBack) {
153 | return new Win(bottomRightFront, winningSpaces(2, 2, 0, 1, 1, 1, 0, 0, 2));
154 | }
155 |
156 | return null;
157 | }
158 |
159 | private static boolean[][][] winningSpaces(int x0, int y0, int z0,
160 | int x1, int y1, int z1,
161 | int x2, int y2, int z2) {
162 | boolean[][][] array = new boolean[3][3][3];
163 | array[x0][y0][z0] = true;
164 | array[x1][y1][z1] = true;
165 | array[x2][y2][z2] = true;
166 | return array;
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/app/src/main/java/me/mattlogan/twentyseven/game/BoardView.java:
--------------------------------------------------------------------------------
1 | package me.mattlogan.twentyseven.game;
2 |
3 | import android.animation.ValueAnimator;
4 | import android.content.Context;
5 | import android.graphics.Canvas;
6 | import android.graphics.Paint;
7 | import android.graphics.RectF;
8 | import android.support.v7.widget.GridLayout;
9 | import android.util.AttributeSet;
10 | import android.view.MotionEvent;
11 | import android.view.animation.LinearInterpolator;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 |
16 | import butterknife.ButterKnife;
17 | import me.mattlogan.twentyseven.R;
18 |
19 | public class BoardView extends GridLayout {
20 |
21 | public interface ActionListener {
22 | /**
23 | * Called when a turn is taken on the provided space (2d to 1d mapped index)
24 | */
25 | void onActionTaken(int space, char mark);
26 | }
27 |
28 | private ActionListener listener;
29 |
30 | private Paint gridPaint;
31 | private Paint oPaint;
32 | private Paint xPaint;
33 |
34 | private RectF gridLeftLine;
35 | private RectF gridRightLine;
36 | private RectF gridTopLine;
37 | private RectF gridBottomLine;
38 |
39 | private float roundingRadius;
40 |
41 | private RectF[] os = new RectF[9];
42 |
43 | private Float[][][] xs = new Float[9][2][4]; // 9 Xs of 2 lines with 2 points (x and y) each
44 |
45 | private char turn;
46 | private char[] planeState = new char[9];
47 |
48 | private final List winningOs = new ArrayList<>();
49 | private final List winningXs = new ArrayList<>();
50 |
51 | private ValueAnimator winAnimator;
52 |
53 | public BoardView(Context context, AttributeSet attrs) {
54 | super(context, attrs);
55 | ButterKnife.bind(this);
56 | gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
57 | gridPaint.setColor(context.getResources().getColor(R.color.dark_gray));
58 | oPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
59 | oPaint.setColor(context.getResources().getColor(R.color.green));
60 | oPaint.setStyle(Paint.Style.STROKE);
61 | xPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
62 | xPaint.setColor(context.getResources().getColor(R.color.red));
63 | xPaint.setStyle(Paint.Style.STROKE);
64 | roundingRadius = 5 * context.getResources().getDisplayMetrics().density;
65 | setWillNotDraw(false);
66 | initAnimator();
67 | }
68 |
69 | private void initAnimator() {
70 | winAnimator = ValueAnimator.ofFloat(0, (float) (2 * Math.PI));
71 | winAnimator.setInterpolator(new LinearInterpolator());
72 | winAnimator.setRepeatCount(ValueAnimator.INFINITE);
73 | winAnimator.setDuration(500);
74 | }
75 |
76 | @Override public void onSizeChanged(int w, int h, int oldw, int oldh) {
77 | super.onSizeChanged(w, h, oldw, oldh);
78 |
79 | float third = w / 3f; // third of grid, or width of one grid space
80 |
81 | float thickness = 10f * getResources().getDisplayMetrics().density;
82 | oPaint.setStrokeWidth(thickness);
83 | xPaint.setStrokeWidth(thickness);
84 |
85 | // The grid
86 | gridTopLine = new RectF(0, third - thickness / 2, w, third + thickness / 2);
87 | gridBottomLine = new RectF(0, 2 * third - thickness / 2, w, 2 * third + thickness / 2);
88 | gridLeftLine = new RectF(third - thickness / 2, 0, third + thickness / 2, h);
89 | gridRightLine = new RectF(2 * third - thickness / 2, 0, 2 * third + thickness / 2, h);
90 |
91 | calculateXsAndOs();
92 | }
93 |
94 | private void calculateXsAndOs() {
95 | float width = getWidth() / 3f; // width of one grid space
96 | // Xs and the oh oh ohs
97 | for (int i = 0; i < 9; i++) {
98 | // Xs
99 | int x = i % 3; // 0, 1, 2, 0, 1, 2, 0, 1, 2
100 | int y = i / 3; // 0, 0, 0, 1, 1, 1, 2, 2, 2
101 |
102 | float offset = width / 4;
103 | float leftX = x * width + offset;
104 | float topY = y * width + offset;
105 | float rightX = x * width + width - offset;
106 | float bottomY = y * width + width - offset;
107 |
108 | Float[] firstLine = xs[i][0];
109 | firstLine[0] = leftX;
110 | firstLine[1] = topY;
111 | firstLine[2] = rightX;
112 | firstLine[3] = bottomY;
113 |
114 | Float[] secondLine = xs[i][1];
115 | secondLine[0] = leftX;
116 | secondLine[1] = bottomY;
117 | secondLine[2] = rightX;
118 | secondLine[3] = topY;
119 |
120 | // Os
121 | float rad = width / 4;
122 | float cx = x * width + width / 2;
123 | float cy = y * width + width / 2;
124 |
125 | os[i] = new RectF(cx - rad, cy - rad, cx + rad, cy + rad);
126 | }
127 | }
128 |
129 | @Override public boolean onTouchEvent(MotionEvent e) {
130 | if (e.getAction() != MotionEvent.ACTION_DOWN || !this.isEnabled()) {
131 | // Only handle action_down touch events when this view is enabled
132 | return false;
133 | }
134 |
135 | float third = getWidth() / 3f;
136 | int x;
137 | int y;
138 |
139 | if (e.getX() <= third) {
140 | x = 0;
141 | } else if (e.getX() <= 2 * third) {
142 | x = 1;
143 | } else {
144 | x = 2;
145 | }
146 |
147 | if (e.getY() <= third) {
148 | y = 0;
149 | } else if (e.getY() <= 2 * third) {
150 | y = 1;
151 | } else {
152 | y = 2;
153 | }
154 |
155 | int index = 3 * y + x;
156 |
157 | planeState[index] = turn;
158 |
159 | if (listener != null) {
160 | listener.onActionTaken(index, turn);
161 | }
162 |
163 | invalidate();
164 |
165 | return true;
166 | }
167 |
168 | public void setActionListener(ActionListener listener) {
169 | this.listener = listener;
170 | }
171 |
172 | public void updateGrid(char[][][] grid, int zIndex) {
173 | for (int y = 0; y < 3; y++) {
174 | for (int x = 0; x < 3; x++) {
175 | planeState[y * 3 + x] = grid[x][y][zIndex];
176 | }
177 | }
178 | invalidate();
179 | }
180 |
181 | public void showWin(char winner, boolean[][][] winningGrid, int zIndex) {
182 | for (int y = 0; y < 3; y++) {
183 | for (int x = 0; x < 3; x++) {
184 | if (winningGrid[x][y][zIndex]) { // Space has win
185 | int index = y * 3 + x;
186 | if (planeState[index] == 'X') {
187 | winningXs.add(xs[index]);
188 | } else if (planeState[index] == 'O') {
189 | winningOs.add(os[index]);
190 | }
191 | }
192 | }
193 | }
194 |
195 | if (winner == 'X') {
196 | winAnimator.addUpdateListener(xUpdateListener);
197 | } else if (winner == 'O') {
198 | winAnimator.addUpdateListener(oUpdateListener);
199 | }
200 | winAnimator.start();
201 |
202 | this.setEnabled(false);
203 | }
204 |
205 | private ValueAnimator.AnimatorUpdateListener xUpdateListener =
206 | new ValueAnimator.AnimatorUpdateListener() {
207 | @Override public void onAnimationUpdate(ValueAnimator animation) {
208 | float theta = (float) animation.getAnimatedValue();
209 | for (Float[][] x : winningXs) {
210 | Float[] line1 = x[0];
211 | Float[] line2 = x[1];
212 | float cx = (line1[0] + line2[2]) / 2f;
213 | float rad = (line1[3] - line1[1]) / 2f;
214 | float newRad = (float) (rad * Math.cos(theta));
215 | float x0 = cx - newRad;
216 | float x1 = cx + newRad;
217 | line1[0] = Math.min(x0, x1);
218 | line1[2] = Math.max(x0, x1);
219 | line2[0] = Math.min(x0, x1);
220 | line2[2] = Math.max(x0, x1);
221 | }
222 | invalidate();
223 | }
224 | };
225 |
226 | private ValueAnimator.AnimatorUpdateListener oUpdateListener =
227 | new ValueAnimator.AnimatorUpdateListener() {
228 | @Override public void onAnimationUpdate(ValueAnimator animation) {
229 | float theta = (float) animation.getAnimatedValue();
230 | for (RectF oval : winningOs) {
231 | float cx = oval.centerX();
232 | float rad = (oval.bottom - oval.top) / 2f;
233 | float newRad = (float) (rad * Math.cos(theta));
234 | float x0 = cx - newRad;
235 | float x1 = cx + newRad;
236 | oval.left = Math.min(x0, x1);
237 | oval.right = Math.max(x0, x1);
238 | }
239 | invalidate();
240 | }
241 | };
242 |
243 | public void clearWin() {
244 | winAnimator.pause();
245 | winAnimator.removeAllUpdateListeners();
246 | winningOs.clear();
247 | winningXs.clear();
248 | calculateXsAndOs();
249 | this.setEnabled(true);
250 | }
251 |
252 | public void updateTurn(char turn) {
253 | this.turn = turn;
254 | }
255 |
256 | @Override public void onDraw(Canvas canvas) {
257 | super.onDraw(canvas);
258 | canvas.drawRoundRect(gridTopLine, roundingRadius, roundingRadius, gridPaint);
259 | canvas.drawRoundRect(gridBottomLine, roundingRadius, roundingRadius, gridPaint);
260 | canvas.drawRoundRect(gridLeftLine, roundingRadius, roundingRadius, gridPaint);
261 | canvas.drawRoundRect(gridRightLine, roundingRadius, roundingRadius, gridPaint);
262 |
263 | for (int i = 0; i < planeState.length; i++) {
264 | char mark = planeState[i];
265 | if (mark == 'X') {
266 | Float[][] lines = xs[i];
267 | Float[] firstLine = lines[0];
268 | Float[] secondLine = lines[1];
269 | canvas.drawLine(firstLine[0], firstLine[1], firstLine[2], firstLine[3], xPaint);
270 | canvas.drawLine(secondLine[0], secondLine[1], secondLine[2], secondLine[3], xPaint);
271 | } else if (mark == 'O') {
272 | canvas.drawOval(os[i], oPaint);
273 | }
274 | }
275 | }
276 | }
277 |
--------------------------------------------------------------------------------