├── app
├── .gitignore
├── libs
│ ├── hex-ai.jar
│ ├── hex-core.jar
│ ├── hex-net.jar
│ └── gson-2.2.4.jar
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable-hdpi
│ │ │ ├── icon.png
│ │ │ ├── play.png
│ │ │ ├── store.png
│ │ │ ├── history.png
│ │ │ ├── settings.png
│ │ │ ├── donate_gold.png
│ │ │ ├── file_icon.png
│ │ │ ├── howtoplay.png
│ │ │ ├── achievements.png
│ │ │ ├── directory_up.png
│ │ │ ├── donate_bronze.png
│ │ │ ├── donate_hollow.png
│ │ │ ├── donate_silver.png
│ │ │ ├── file_hex_icon.png
│ │ │ ├── history_icon.png
│ │ │ ├── directory_icon.png
│ │ │ ├── history_background_black.9.png
│ │ │ ├── history_background_blue.9.png
│ │ │ └── history_background_red.9.png
│ │ ├── drawable-mdpi
│ │ │ ├── icon.png
│ │ │ ├── donate_gold.png
│ │ │ ├── donate_bronze.png
│ │ │ ├── donate_hollow.png
│ │ │ └── donate_silver.png
│ │ ├── drawable-xhdpi
│ │ │ ├── exit.png
│ │ │ ├── home.png
│ │ │ ├── icon.png
│ │ │ ├── undo.png
│ │ │ ├── restart.png
│ │ │ ├── play_again.png
│ │ │ ├── donate_gold.png
│ │ │ ├── donate_bronze.png
│ │ │ ├── donate_bronze_d.png
│ │ │ ├── donate_gold_d.png
│ │ │ ├── donate_hollow.png
│ │ │ ├── donate_silver.png
│ │ │ └── donate_silver_d.png
│ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ ├── dialog_view_game_over_icon.xml
│ │ │ ├── fragment_game_selection.xml
│ │ │ ├── fragment_online_selection.xml
│ │ │ ├── fragment_history.xml
│ │ │ ├── preferences_timer.xml
│ │ │ ├── preferences.xml
│ │ │ ├── view_history_item.xml
│ │ │ ├── dialog_view_donate.xml
│ │ │ ├── fragment_instructions.xml
│ │ │ ├── fragment_game.xml
│ │ │ ├── dialog_view_game_over.xml
│ │ │ └── fragment_main.xml
│ │ ├── values
│ │ │ ├── integer.xml
│ │ │ ├── constants.xml
│ │ │ ├── styles.xml
│ │ │ ├── arrays.xml
│ │ │ ├── colors.xml
│ │ │ └── strings.xml
│ │ ├── drawable
│ │ │ └── background_drawable.xml
│ │ ├── values-nl
│ │ │ ├── arrays.xml
│ │ │ └── strings.xml
│ │ ├── layout-sw600dp
│ │ │ ├── dialog_view_donate.xml
│ │ │ ├── dialog_view_game_over.xml
│ │ │ └── fragment_game.xml
│ │ ├── xml
│ │ │ └── preferences_general.xml
│ │ ├── layout-sw400dp
│ │ │ └── fragment_game.xml
│ │ └── values-de
│ │ │ └── strings.xml
│ │ ├── java
│ │ └── com
│ │ │ ├── google
│ │ │ └── android
│ │ │ │ └── gms
│ │ │ │ └── games
│ │ │ │ ├── GameCompat.java
│ │ │ │ ├── InvitationsClient.java
│ │ │ │ ├── PlayerCompat.java
│ │ │ │ ├── multiplayer
│ │ │ │ ├── Participant.java
│ │ │ │ ├── Invitation.java
│ │ │ │ ├── ParticipantResult.java
│ │ │ │ ├── turnbased
│ │ │ │ │ ├── TurnBasedMatchUpdateCallback.java
│ │ │ │ │ ├── TurnBasedMatch.java
│ │ │ │ │ └── TurnBasedMatchConfig.java
│ │ │ │ ├── Multiplayer.java
│ │ │ │ └── realtime
│ │ │ │ │ └── RoomConfig.java
│ │ │ │ ├── GamesCompat.java
│ │ │ │ └── TurnBasedMultiplayerClient.java
│ │ │ └── xlythe
│ │ │ └── hex
│ │ │ ├── PermissionUtils.java
│ │ │ ├── fragment
│ │ │ ├── InstructionsFragment.java
│ │ │ ├── OnlineSelectionFragment.java
│ │ │ ├── GameSelectionFragment.java
│ │ │ ├── HexFragment.java
│ │ │ ├── HistoryFragment.java
│ │ │ ├── PreferencesFragment.java
│ │ │ └── MainFragment.java
│ │ │ ├── compat
│ │ │ ├── GameOptions.java
│ │ │ ├── Game.java
│ │ │ └── NetworkPlayer.java
│ │ │ ├── FileUtil.java
│ │ │ ├── Stats.java
│ │ │ ├── view
│ │ │ ├── HexDialog.java
│ │ │ ├── DonateDialog.java
│ │ │ ├── GameOverDialog.java
│ │ │ └── SelectorLayout.java
│ │ │ ├── Settings.java
│ │ │ ├── BaseGameActivity.java
│ │ │ ├── PreferencesActivity.java
│ │ │ ├── MainActivity.java
│ │ │ └── NetActivity.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle.properties
├── ic_launcher-web.png
├── .gitignore
├── LICENSE.txt
└── README.txt
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | *.apk
3 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 | android.enableJetifier=true
3 |
--------------------------------------------------------------------------------
/app/libs/hex-ai.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/libs/hex-ai.jar
--------------------------------------------------------------------------------
/app/libs/hex-core.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/libs/hex-core.jar
--------------------------------------------------------------------------------
/app/libs/hex-net.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/libs/hex-net.jar
--------------------------------------------------------------------------------
/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/libs/gson-2.2.4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/libs/gson-2.2.4.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/play.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/store.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-mdpi/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/exit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/home.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/undo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/history.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/settings.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/restart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/restart.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/donate_gold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/donate_gold.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/file_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/file_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/howtoplay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/howtoplay.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/donate_gold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-mdpi/donate_gold.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/play_again.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/play_again.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/achievements.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/achievements.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/directory_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/directory_up.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/donate_bronze.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/donate_bronze.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/donate_hollow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/donate_hollow.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/donate_silver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/donate_silver.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/file_hex_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/file_hex_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/history_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/history_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/donate_bronze.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-mdpi/donate_bronze.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/donate_hollow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-mdpi/donate_hollow.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/donate_silver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-mdpi/donate_silver.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/donate_gold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/donate_gold.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/directory_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/directory_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/donate_bronze.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/donate_bronze.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/donate_bronze_d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/donate_bronze_d.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/donate_gold_d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/donate_gold_d.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/donate_hollow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/donate_hollow.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/donate_silver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/donate_silver.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/donate_silver_d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-xhdpi/donate_silver_d.png
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/GameCompat.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games;
2 |
3 | public interface GameCompat {
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/InvitationsClient.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games;
2 |
3 | public class InvitationsClient {
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/history_background_black.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/history_background_black.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/history_background_blue.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/history_background_blue.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/history_background_red.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Xlythe/Hex/HEAD/app/src/main/res/drawable-hdpi/history_background_red.9.png
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/PlayerCompat.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games;
2 |
3 | public interface PlayerCompat {
4 | String getPlayerId();
5 | String getDisplayName();
6 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/Participant.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer;
2 |
3 | import com.google.android.gms.games.PlayerCompat;
4 |
5 | public interface Participant {
6 | PlayerCompat getPlayer();
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_view_game_over_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_game_selection.xml:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_online_selection.xml:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/integer.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0xffcc5c57
4 | 0xff4ba5e2
5 | 0
6 | 1
7 | 2
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/Invitation.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer;
2 |
3 | import com.google.android.gms.games.GameCompat;
4 |
5 | public interface Invitation {
6 | int INVITATION_TYPE_TURN_BASED = 1;
7 | GameCompat getGame();
8 | String getInvitationId();
9 | int getInvitationType();
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/ParticipantResult.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer;
2 |
3 | public class ParticipantResult {
4 | public static final int MATCH_RESULT_WIN = 0;
5 | public static final int MATCH_RESULT_LOSS = 1;
6 |
7 | public ParticipantResult(String participantId, int result, int placing) {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/turnbased/TurnBasedMatchUpdateCallback.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer.turnbased;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | public interface TurnBasedMatchUpdateCallback {
6 | void onTurnBasedMatchReceived(@NonNull TurnBasedMatch turnBasedMatch);
7 | void onTurnBasedMatchRemoved(@NonNull String matchId);
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/Multiplayer.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer;
2 |
3 | public interface Multiplayer {
4 | String EXTRA_EXCLUSIVE_BIT_MASK = "exclusive_bit_mask";
5 | String EXTRA_INVITATION = "invitation";
6 | String EXTRA_TURN_BASED_MATCH = "turn_based_match";
7 | String EXTRA_MIN_AUTOMATCH_PLAYERS = "min_automatch_players";
8 | String EXTRA_MAX_AUTOMATCH_PLAYERS = "max_automatch_players";
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/GamesCompat.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games;
2 |
3 | import android.content.Context;
4 |
5 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
6 |
7 | public class GamesCompat {
8 | public static TurnBasedMultiplayerClient getTurnBasedMultiplayerClient(Context context, GoogleSignInAccount account) {
9 | return new TurnBasedMultiplayerClient();
10 | }
11 |
12 | public static InvitationsClient getInvitationsClient(Context context, GoogleSignInAccount account) {
13 | return new InvitationsClient();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-nl/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - 7x7
5 | - 9x9
6 | - 11x11
7 | - Aangepast
8 |
9 |
10 | - Makkelijk
11 | - Gemiddeld
12 | - Moeilijk
13 |
14 |
15 | - Geen timer
16 | - Per beurt
17 | - Hele spel
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Can ignore specific files
2 | .DS_Store
3 | Thumbs.db
4 |
5 | # Use wildcards as well
6 | *~
7 | *.swp
8 | *.~
9 |
10 | # Can also ignore all directories and files in a directory.
11 | tmp/**/*
12 |
13 | #ignore the binary
14 | bin/*
15 |
16 | #eclips stuff
17 | .project
18 | .classpath
19 | proguard-project.txt
20 | project.properties
21 | default.properties
22 |
23 | .~lock.designDoc.odt#
24 |
25 | *.class
26 |
27 | scr
28 |
29 | gen/*
30 |
31 | .gradle/*
32 |
33 | gradle/*
34 |
35 | .idea/*
36 |
37 | build/*
38 |
39 | *.iml
40 |
41 | local.properties
42 |
43 | gradlew
44 |
45 | gradlew.bat
46 | .gradle/
47 | .idea/
48 | gradle/
49 | app/release/output-metadata.json
50 | *.dm
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/PermissionUtils.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.content.Context;
4 | import android.content.pm.PackageManager;
5 |
6 | import androidx.core.content.ContextCompat;
7 |
8 | public class PermissionUtils {
9 | /**
10 | * Returns true if all given permissions are available
11 | */
12 | public static boolean hasPermissions(Context context, String... permissions) {
13 | for (String permission : permissions) {
14 | if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
15 | return false;
16 | }
17 | }
18 | return true;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/constants.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @integer/COLOR_RED
4 | @integer/COLOR_BLUE
5 | Player1
6 | Guest
7 | 1
8 | 7
9 | - true
10 | 0
11 | 0
12 | - true
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/turnbased/TurnBasedMatch.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer.turnbased;
2 |
3 | import com.google.android.gms.games.GameCompat;
4 | import com.google.android.gms.games.multiplayer.Participant;
5 |
6 | import java.util.ArrayList;
7 |
8 | public interface TurnBasedMatch {
9 | int MATCH_TURN_STATUS_MY_TURN = 1;
10 | int MATCH_TURN_STATUS_THEIR_TURN = 2;
11 |
12 | GameCompat getGame();
13 | String getMatchId();
14 | int getTurnStatus();
15 | byte[] getData();
16 | int getVersion();
17 | String getRematchId();
18 | ArrayList getParticipantIds();
19 | String getParticipantId(String playerId);
20 | Participant getParticipant(String participantId);
21 | }
--------------------------------------------------------------------------------
/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 C:\Users\Xlyth\AppData\Local\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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/turnbased/TurnBasedMatchConfig.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer.turnbased;
2 |
3 | import android.os.Bundle;
4 |
5 | import java.util.ArrayList;
6 |
7 | public abstract class TurnBasedMatchConfig {
8 | public static Builder builder() {
9 | return new Builder();
10 | }
11 |
12 | public static final class Builder {
13 | private Builder() {}
14 |
15 | public Builder addInvitedPlayers(ArrayList playerIds) {
16 | return this;
17 | }
18 |
19 | public Builder setAutoMatchCriteria(Bundle autoMatchCriteria) {
20 | return this;
21 | }
22 |
23 | public TurnBasedMatchConfig build() {
24 | return null;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/multiplayer/realtime/RoomConfig.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games.multiplayer.realtime;
2 |
3 | import android.os.Bundle;
4 |
5 | import com.google.android.gms.games.multiplayer.Multiplayer;
6 |
7 | public abstract class RoomConfig {
8 | public static final class Builder {
9 | public RoomConfig build() {
10 | return null;
11 | }
12 | }
13 |
14 | public static Bundle createAutoMatchCriteria(int minAutoMatchPlayers,
15 | int maxAutoMatchPlayers, long exclusiveBitMask) {
16 | Bundle autoMatchData = new Bundle();
17 | autoMatchData.putInt(Multiplayer.EXTRA_MIN_AUTOMATCH_PLAYERS, minAutoMatchPlayers);
18 | autoMatchData.putInt(Multiplayer.EXTRA_MAX_AUTOMATCH_PLAYERS, maxAutoMatchPlayers);
19 | autoMatchData.putLong(Multiplayer.EXTRA_EXCLUSIVE_BIT_MASK, exclusiveBitMask);
20 | return autoMatchData;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_history.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
16 |
17 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/preferences_timer.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
20 |
21 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - 7x7
5 | - 9x9
6 | - 11x11
7 | - Custom
8 |
9 |
10 | - 7
11 | - 9
12 | - 11
13 | - 0
14 |
15 |
16 | - Easy
17 | - Medium
18 | - Hard
19 |
20 |
21 | - 0
22 | - 1
23 | - 2
24 |
25 |
26 | - No timer
27 | - Per move
28 | - Entire match
29 |
30 |
31 | - 0
32 | - 1
33 | - 2
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 | #FFFFFF
5 | #00000000
6 | #FFCCCCCC
7 | #00000000
8 | #FFFFFFFF
9 | #00000000
10 | #D2D2D2
11 | #333333
12 |
13 | #cc5c57
14 | #5f6ec2
15 | #f9db00
16 | #b7cf47
17 | #f48935
18 | #4ba5e2
19 |
20 | #b7cf47
21 | #4ba5e2
22 | #cc5c57
23 |
24 | #f9db00
25 | #5f6ec2
26 | #f48935
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/fragment/InstructionsFragment.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.fragment;
2 |
3 | import android.os.Bundle;
4 | import android.text.method.LinkMovementMethod;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.TextView;
9 |
10 | import com.xlythe.hex.R;
11 |
12 | import androidx.annotation.NonNull;
13 |
14 | /**
15 | * @author Will Harmon
16 | **/
17 | public class InstructionsFragment extends HexFragment {
18 | @Override
19 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
20 | super.onCreateView(inflater, container, savedInstanceState);
21 | View v = inflater.inflate(R.layout.fragment_instructions, container, false);
22 |
23 | TextView title = v.findViewById(R.id.title);
24 | title.setText(R.string.activity_title_instructions);
25 |
26 | TextView rules = v.findViewById(R.id.rules);
27 | rules.setMovementMethod(LinkMovementMethod.getInstance());
28 |
29 | TextView privacy = v.findViewById(R.id.privacy);
30 | privacy.setMovementMethod(LinkMovementMethod.getInstance());
31 |
32 | return v;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/preferences.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
20 |
21 |
22 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_history_item.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
22 |
23 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_view_donate.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
13 |
14 |
18 |
19 |
25 |
26 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-sw600dp/dialog_view_donate.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
13 |
14 |
18 |
19 |
25 |
26 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_instructions.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
18 |
19 |
27 |
28 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/compat/GameOptions.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.compat;
2 |
3 | import com.hex.core.Timer;
4 |
5 | public class GameOptions extends com.hex.core.Game.GameOptions {
6 | private GameOptions() {}
7 |
8 | public static class Builder {
9 | private final GameOptions gameOptions = new GameOptions();
10 |
11 | public Builder() {
12 | setNoTimer();
13 | }
14 |
15 | public Builder setGridSize(int gridSize) {
16 | gameOptions.gridSize = gridSize;
17 | return this;
18 | }
19 |
20 | public Builder setSwapEnabled(boolean enabled) {
21 | gameOptions.swap = enabled;
22 | return this;
23 | }
24 |
25 | public Builder setTimer(Timer timer) {
26 | gameOptions.timer = timer;
27 | return this;
28 | }
29 |
30 | public Builder setTimerPerMove(long totalTimeMinutes, long additionalTimePerMoveSeconds) {
31 | return setTimer(new Timer(totalTimeMinutes, additionalTimePerMoveSeconds, Timer.PER_MOVE));
32 | }
33 |
34 | public Builder setTimerForEntireMatch(long totalTimeMinutes) {
35 | return setTimer(new Timer(totalTimeMinutes, 0, Timer.ENTIRE_MATCH));
36 | }
37 |
38 | public Builder setNoTimer() {
39 | return setTimer(new Timer(0, 0, Timer.NO_TIMER));
40 | }
41 |
42 | public GameOptions build() {
43 | return gameOptions;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preferences_general.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
6 |
7 |
14 |
15 |
20 |
21 |
25 |
26 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | Properties local = new Properties()
4 | local.load(project.rootProject.file('local.properties').newDataInputStream())
5 |
6 | android {
7 | compileSdkVersion 36
8 | defaultConfig {
9 | applicationId "com.sam.hex"
10 | minSdkVersion 21
11 | targetSdkVersion 36
12 | versionCode 31
13 | versionName "5.1.2"
14 | multiDexEnabled true
15 | }
16 | signingConfigs {
17 | release {
18 | storeFile file(local.getProperty("keystoreDir"))
19 | storePassword local.getProperty("keystorePassword")
20 | keyAlias local.getProperty("keystoreAlias")
21 | keyPassword local.getProperty("keystoreAliasPassword")
22 | }
23 | }
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
28 | signingConfig signingConfigs.release
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 | namespace 'com.xlythe.hex'
36 | lint {
37 | abortOnError false
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation fileTree(include: ['*.jar'], dir: 'libs')
43 | implementation 'androidx.appcompat:appcompat:+'
44 | implementation 'com.google.android.material:material:+'
45 | implementation 'com.google.android.gms:play-services-auth:+'
46 | implementation 'com.google.android.gms:play-services-games:+'
47 | implementation 'androidx.multidex:multidex:+'
48 | implementation 'com.xlythe:play-billing:3.1.2'
49 | testImplementation 'junit:junit:4.13.2'
50 | }
51 |
52 | configurations.all {
53 | exclude group: 'com.android.support'
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_game.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
10 |
11 |
22 |
23 |
35 |
36 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/android/gms/games/TurnBasedMultiplayerClient.java:
--------------------------------------------------------------------------------
1 | package com.google.android.gms.games;
2 |
3 | import android.content.Intent;
4 |
5 | import com.google.android.gms.games.multiplayer.ParticipantResult;
6 | import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatch;
7 | import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatchConfig;
8 | import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatchUpdateCallback;
9 | import com.google.android.gms.tasks.Task;
10 | import com.google.android.gms.tasks.Tasks;
11 |
12 | public class TurnBasedMultiplayerClient {
13 | public Task rematch(String matchId) {
14 | return Tasks.forException(new Exception("Stub!"));
15 | }
16 |
17 | public Task acceptInvitation(String invitationId) {
18 | return Tasks.forException(new Exception("Stub!"));
19 | }
20 |
21 | public Task createMatch(TurnBasedMatchConfig config) {
22 | return Tasks.forException(new Exception("Stub!"));
23 | }
24 |
25 | public Task getSelectOpponentsIntent(int min, int max, boolean allowAutomatch) {
26 | return Tasks.forException(new Exception("Stub!"));
27 | }
28 |
29 | public Task getInboxIntent() {
30 | return Tasks.forException(new Exception("Stub!"));
31 | }
32 |
33 | public Task registerTurnBasedMatchUpdateCallback(TurnBasedMatchUpdateCallback callback) {
34 | return Tasks.forException(new Exception("Stub!"));
35 | }
36 |
37 | public Task unregisterTurnBasedMatchUpdateCallback(TurnBasedMatchUpdateCallback callback) {
38 | return Tasks.forException(new Exception("Stub!"));
39 | }
40 |
41 | public Task takeTurn(String matchId, byte[] data, String participantId) {
42 | return Tasks.forException(new Exception("Stub!"));
43 | }
44 |
45 | public Task finishMatch(String matchId, byte[] data, ParticipantResult... players) {
46 | return Tasks.forException(new Exception("Stub!"));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-sw600dp/dialog_view_game_over.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
10 |
11 |
19 |
20 |
28 |
29 |
30 |
31 |
36 |
37 |
44 |
45 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_view_game_over.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
10 |
11 |
19 |
20 |
28 |
29 |
30 |
31 |
36 |
37 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/fragment/OnlineSelectionFragment.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.fragment;
2 |
3 | import android.os.Bundle;
4 | import android.view.LayoutInflater;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 |
8 | import com.xlythe.hex.R;
9 | import com.xlythe.hex.view.SelectorLayout;
10 |
11 | import androidx.annotation.NonNull;
12 | import androidx.annotation.Nullable;
13 |
14 | /**
15 | * @author Will Harmon
16 | **/
17 | public class OnlineSelectionFragment extends HexFragment {
18 | private SelectorLayout mSelectorLayout;
19 |
20 | @Override
21 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState) {
22 | super.onCreateView(inflater, container, savedInstanceState);
23 | View v = inflater.inflate(R.layout.fragment_online_selection, container, false);
24 |
25 | mSelectorLayout = v.findViewById(R.id.buttons);
26 |
27 | SelectorLayout.Button quickGameButton = mSelectorLayout.getButtons()[0];
28 | quickGameButton.setColor(getResources().getColor(R.color.select_quick_game));
29 | quickGameButton.setText(R.string.online_selection_button_quick);
30 | quickGameButton.setOnClickListener(this::startQuickGame);
31 |
32 | SelectorLayout.Button inviteButton = mSelectorLayout.getButtons()[1];
33 | inviteButton.setColor(getResources().getColor(R.color.select_friends));
34 | inviteButton.setText(R.string.online_selection_button_invite);
35 | inviteButton.setOnClickListener(this::inviteFriends);
36 |
37 | SelectorLayout.Button pendingButton = mSelectorLayout.getButtons()[2];
38 | pendingButton.setColor(getResources().getColor(R.color.select_pending_invites));
39 | pendingButton.setText(R.string.online_selection_button_pending);
40 | pendingButton.setOnClickListener(this::checkInvites);
41 |
42 | return v;
43 | }
44 |
45 | @Override
46 | public void onResume() {
47 | super.onResume();
48 | mSelectorLayout.reset();
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout-sw600dp/fragment_game.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
10 |
11 |
16 |
17 |
27 |
28 |
38 |
39 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-sw400dp/fragment_game.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
10 |
11 |
16 |
17 |
27 |
28 |
38 |
39 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 SLSW All rights reserved.
2 |
3 | Developed by: Xlythe
4 | University of Illinois, University of Waterloo
5 | https://play.google.com/store/apps/details?id=com.sam.hex
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8 |
9 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers.
10 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution.
11 | Neither the names of Xlythe, University of Illinois, University of Waterloo, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission.
12 |
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
15 |
16 | ---------------------------------
17 |
18 | None of the images are released. If you want to publicly distribute your own version of Hex based off our code, please provide your own images.
19 |
20 | The following code is not ours:
21 |
22 | src/com/sam/hex/replay/FileExplore.java
23 | A modified version of https://github.com/mburman/Android-File-Explore (Released under the Apache 2.0 Open Source license)
24 |
25 | src/com/sam/hex/ai/bee/Bee.java
26 | A modified AI originally written by Konstantin Lopyrev. (No official license. Special permission granted for use)
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | Welcome to Hex, the hexagon connection game.
2 | Help us make the game better!
3 |
4 | If you'd rather write your own board game based off ours, email me at xlythe@gmail.com and I'll try to help with any questions.
5 |
6 | ----------LICENSE------------
7 |
8 | Copyright (c) 2018 Xlythe LLC All rights reserved.
9 |
10 | Developed by: Xlythe
11 | University of Illinois, University of Waterloo
12 | https://play.google.com/store/apps/details?id=com.sam.hex
13 |
14 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
15 |
16 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers.
17 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution.
18 | Neither the names of Xlythe, University of Illinois, University of Waterloo, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission.
19 |
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
22 |
23 | ---------------------------------
24 |
25 | None of the images are released. If you want to publicly distribute your own version of Hex based off our code, please provide your own images.
26 |
27 | The following code is not ours:
28 |
29 | src/com/sam/hex/replay/FileExplore.java
30 | A modified version of https://github.com/mburman/Android-File-Explore (Released under the Apache 2.0 Open Source license)
31 |
32 | src/com/sam/hex/ai/bee/Bee.java
33 | A modified AI originally written by Konstantin Lopyrev. (No official license. Special permission granted for use)
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/FileUtil.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.BufferedWriter;
8 | import java.io.File;
9 | import java.io.FileReader;
10 | import java.io.FileWriter;
11 | import java.io.IOException;
12 | import java.util.Locale;
13 |
14 | import androidx.annotation.NonNull;
15 |
16 | import static com.xlythe.hex.Settings.TAG;
17 |
18 | /**
19 | * @author Will Harmon
20 | **/
21 | public class FileUtil {
22 | private static final String FOLDER = "Hex";
23 |
24 | public static String loadGameAsString(Context context, @NonNull String fileName) throws IOException {
25 | String parentFolder = context.getFilesDir() + File.separator + FOLDER + File.separator;
26 | if (!fileName.startsWith(parentFolder)) {
27 | fileName = parentFolder + fileName;
28 | }
29 | File file = new File(fileName);
30 | StringBuilder text = new StringBuilder();
31 | BufferedReader br = new BufferedReader(new FileReader(file));
32 | String line;
33 |
34 | while ((line = br.readLine()) != null) {
35 | text.append(line);
36 | text.append('\n');
37 | }
38 |
39 | br.close();
40 | return text.toString();
41 | }
42 |
43 | public static void autoSaveGame(Context context, @NonNull String fileName, String gameState) throws IOException {
44 | if (!fileName.toLowerCase(Locale.getDefault()).endsWith(".rhex")) {
45 | fileName = fileName + ".rhex";
46 | }
47 | fileName = context.getFilesDir() + File.separator + FOLDER + File.separator + fileName;
48 | FileUtil.createDirIfNoneExists(context, FOLDER);
49 |
50 | File saveFile = new File(fileName);
51 | Log.d(TAG, "Attempting to create file " + fileName);
52 | if (!saveFile.exists()) {
53 | if (!saveFile.createNewFile()) {
54 | Log.w(TAG, "Failed to create file " + fileName);
55 | }
56 | }
57 |
58 | BufferedWriter buf = new BufferedWriter(new FileWriter(saveFile, true));
59 | buf.append(gameState);
60 | buf.close();
61 | Log.d(TAG, "Successfully created file " + fileName);
62 | }
63 |
64 | private static void createDirIfNoneExists(Context context, @NonNull String path) {
65 | File file = new File(context.getFilesDir(), path);
66 | if (!file.exists()) {
67 | if (!file.mkdirs()) {
68 | Log.w(TAG, "Failed to create directory for " + path);
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_main.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
19 |
20 |
26 |
27 |
33 |
34 |
40 |
41 |
47 |
48 |
49 |
50 |
55 |
56 |
60 |
61 |
67 |
68 |
69 |
70 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/Stats.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.content.Context;
4 | import android.preference.PreferenceManager;
5 |
6 | /**
7 | * @author Will Harmon
8 | **/
9 | public class Stats {
10 | private static final String KEY_TIME_PLAYED = "time_played";
11 | private static final String KEY_GAMES_PLAYED = "games_played";
12 | private static final String KEY_GAMES_WON = "games_won";
13 | private static final String KEY_DONATION_AMOUNT = "donation_amount";
14 |
15 | public static long getTimePlayed(Context context) {
16 | return PreferenceManager.getDefaultSharedPreferences(context).getLong(KEY_TIME_PLAYED, 0);
17 | }
18 |
19 | public static void incrementTimePlayed(Context context, long time) {
20 | time = Math.max(time, 0);
21 | PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(KEY_TIME_PLAYED, getTimePlayed(context) + time).apply();
22 | }
23 |
24 | public static void setTimePlayed(Context context, long time) {
25 | PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(KEY_TIME_PLAYED, time).apply();
26 | }
27 |
28 | public static long getGamesPlayed(Context context) {
29 | return PreferenceManager.getDefaultSharedPreferences(context).getLong(KEY_GAMES_PLAYED, 0);
30 | }
31 |
32 | public static void incrementGamesPlayed(Context context) {
33 | PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(KEY_GAMES_PLAYED, getGamesPlayed(context) + 1).apply();
34 | }
35 |
36 | public static void setGamesPlayed(Context context, long games) {
37 | PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(KEY_GAMES_PLAYED, games).apply();
38 | }
39 |
40 | public static long getGamesWon(Context context) {
41 | return PreferenceManager.getDefaultSharedPreferences(context).getLong(KEY_GAMES_WON, 0);
42 | }
43 |
44 | public static void incrementGamesWon(Context context) {
45 | PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(KEY_GAMES_WON, getGamesWon(context) + 1).apply();
46 | }
47 |
48 | public static void setGamesWon(Context context, long games) {
49 | PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(KEY_GAMES_WON, games).apply();
50 | }
51 |
52 | public static int getDonationRank(Context context) {
53 | return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_DONATION_AMOUNT, 0);
54 | }
55 |
56 | public static void incrementDonationRank(Context context, int amount) {
57 | PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_DONATION_AMOUNT, getDonationRank(context) + amount).apply();
58 | }
59 |
60 | public static void setDonationRank(Context context, int amount) {
61 | PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_DONATION_AMOUNT, amount).apply();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/fragment/GameSelectionFragment.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.fragment;
2 |
3 | import android.os.Bundle;
4 | import android.view.LayoutInflater;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 |
8 | import com.hex.core.Player;
9 | import com.xlythe.hex.R;
10 | import com.xlythe.hex.view.SelectorLayout;
11 |
12 | import androidx.annotation.NonNull;
13 |
14 | /**
15 | * @author Will Harmon
16 | **/
17 | public class GameSelectionFragment extends HexFragment {
18 | private SelectorLayout mSelectorLayout;
19 |
20 | @Override
21 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
22 | super.onCreateView(inflater, container, savedInstanceState);
23 | View v = inflater.inflate(R.layout.fragment_game_selection, container, false);
24 |
25 | mSelectorLayout = v.findViewById(R.id.buttons);
26 |
27 | SelectorLayout.Button computerButton = mSelectorLayout.getButtons()[0];
28 | computerButton.setColor(getResources().getColor(R.color.select_computer));
29 | computerButton.setText(R.string.game_selection_button_computer);
30 | computerButton.setOnClickListener(() -> {
31 | GameFragment gameFragment = new GameFragment();
32 | if (Math.random() > 0.5) {
33 | gameFragment.setPlayer1Type(Player.Human);
34 | gameFragment.setPlayer2Type(Player.AI);
35 | } else {
36 | gameFragment.setPlayer1Type(Player.AI);
37 | gameFragment.setPlayer2Type(Player.Human);
38 | }
39 | swapFragment(gameFragment);
40 | });
41 |
42 | SelectorLayout.Button hotseatButton = mSelectorLayout.getButtons()[1];
43 | hotseatButton.setColor(getResources().getColor(R.color.select_pass_to_play));
44 | hotseatButton.setText(R.string.game_selection_button_pass);
45 | hotseatButton.setOnClickListener(() -> {
46 | GameFragment gameFragment = new GameFragment();
47 | gameFragment.setPlayer1Type(Player.Human);
48 | gameFragment.setPlayer2Type(Player.Human);
49 | swapFragment(gameFragment);
50 | });
51 |
52 | SelectorLayout.Button netButton = mSelectorLayout.getButtons()[2];
53 | netButton.setColor(getResources().getColor(R.color.select_online));
54 | netButton.setText(R.string.game_selection_button_net);
55 | netButton.setOnClickListener(() -> {
56 | if (getMainActivity().isSignedIn()) {
57 | swapFragment(new OnlineSelectionFragment());
58 | } else {
59 | getMainActivity().setOpenOnlineSelectionFragment(true);
60 | signIn();
61 | }
62 | });
63 |
64 | return v;
65 | }
66 |
67 | @Override
68 | public void onResume() {
69 | super.onResume();
70 | mSelectorLayout.reset();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
22 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
60 |
73 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/compat/Game.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.compat;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.JsonObject;
5 | import com.google.gson.JsonParser;
6 | import com.hex.core.MoveList;
7 | import com.hex.core.PlayerObject;
8 | import com.hex.core.PlayingEntity;
9 | import com.hex.core.Timer;
10 |
11 | import java.lang.reflect.Field;
12 |
13 | public class Game extends com.hex.core.Game {
14 | private boolean hasStarted = false;
15 |
16 | public Game(GameOptions gameOptions, PlayingEntity player1, PlayingEntity player2) {
17 | super(gameOptions, player1, player2);
18 | }
19 |
20 | public int getGridSize() {
21 | return gameOptions.gridSize;
22 | }
23 |
24 | public boolean isFirstMoveSwapEnabled() {
25 | return gameOptions.swap;
26 | }
27 |
28 | public boolean hasTimer() {
29 | return gameOptions.timer.type != Timer.NO_TIMER;
30 | }
31 |
32 | public void startTimer() {
33 | gameOptions.timer.start(this);
34 | }
35 |
36 | public synchronized boolean hasStarted() {
37 | return hasStarted;
38 | }
39 |
40 | @Override
41 | public synchronized void start() {
42 | if (hasStarted) {
43 | return;
44 | }
45 |
46 | hasStarted = true;
47 | super.start();
48 | }
49 |
50 | @Override
51 | public synchronized void stop() {
52 | if (!hasStarted) {
53 | return;
54 | }
55 |
56 | super.stop();
57 | hasStarted = false;
58 | }
59 |
60 | public static Game load(String state) {
61 | return load(state, new PlayerObject(1), new PlayerObject(2));
62 | }
63 |
64 | public static Game load(String state, PlayingEntity player1, PlayingEntity player2) {
65 | JsonObject object = new JsonParser().parse(state).getAsJsonObject();
66 |
67 | Gson gson = new Gson();
68 | Game.GameOptions options = gson.fromJson(object.get("gameOptions"), Game.GameOptions.class);
69 | MoveList moves = gson.fromJson(object.get("moveList"), MoveList.class);
70 |
71 | player1.setColor(object.get("player1").getAsJsonObject().get("color").getAsInt());
72 | player1.setName(object.get("player1").getAsJsonObject().get("name").getAsString());
73 | player2.setColor(object.get("player2").getAsJsonObject().get("color").getAsInt());
74 | player2.setName(object.get("player2").getAsJsonObject().get("name").getAsString());
75 |
76 | Game game = new Game(options, player1, player2);
77 | game.setCurrentPlayer(object.get("currentPlayer").getAsInt());
78 | game.setStartTime(object.get("gameStart").getAsLong());
79 | game.setEndTime(object.get("gameEnd").getAsLong());
80 | game.setMoveList(moves);
81 | return game;
82 | }
83 |
84 | private Class getSuperClass() {
85 | return com.hex.core.Game.class;
86 | }
87 |
88 | private void setCurrentPlayer(int player) {
89 | try {
90 | Field field = getSuperClass().getDeclaredField("currentPlayer");
91 | field.setAccessible(true);
92 | field.setInt(this, player);
93 | } catch (Exception e) {
94 | throw new RuntimeException(e);
95 | }
96 | }
97 |
98 | private void setStartTime(long startTime) {
99 | try {
100 | Field field = getSuperClass().getDeclaredField("gameStart");
101 | field.setAccessible(true);
102 | field.setLong(this, startTime);
103 | } catch (Exception e) {
104 | throw new RuntimeException(e);
105 | }
106 | }
107 |
108 | private void setEndTime(long endTime) {
109 | try {
110 | Field field = getSuperClass().getDeclaredField("gameEnd");
111 | field.setAccessible(true);
112 | field.setLong(this, endTime);
113 | } catch (Exception e) {
114 | throw new RuntimeException(e);
115 | }
116 | }
117 |
118 | private void setMoveList(MoveList moveList) {
119 | try {
120 | Field field = getSuperClass().getDeclaredField("moveList");
121 | field.setAccessible(true);
122 | field.set(this, moveList);
123 | } catch (Exception e) {
124 | throw new RuntimeException(e);
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/view/HexDialog.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.view;
2 |
3 | import android.os.Bundle;
4 | import android.view.View;
5 | import android.view.Window;
6 | import android.view.WindowManager;
7 |
8 | import com.android.vending.billing.util.PurchaseActivity;
9 |
10 | import java.util.Arrays;
11 | import java.util.List;
12 |
13 | import androidx.annotation.NonNull;
14 | import androidx.annotation.Nullable;
15 |
16 | /**
17 | * @author Will Harmon
18 | **/
19 | public abstract class HexDialog extends PurchaseActivity {
20 | private static final String KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZd0Iz6EbKEtVhrQuuQzv7023TVoZp5V/sHtKTgObbA8wrkBKpKX2W6kOu2BSCv5Lv5oMQu+fBreIXy06IoXPEFrIGZDz79UOSWra34Gc+0e3rz+73kw0ga8imWpuo5KRyO/tbeT4oLsCM44BIC8I23toMJECGiyZmwpI9qdHoci+cc/oBC6N58mtVNyqDZpAJzxCOL5AKqmSGqNRiCN0c34MXPMtP5HjC3S7G6r+bsTGii81hNThcI8qb9VrjptCJQz1gQe4TjMoSkXDQcy1d3H8AuKHosHtsuLuEI+0F+1eF7A+KkofUhOzh+Ur6dNaPE1dEKrku2zOHK7DubJ9QIDAQAB";
21 | public static final String ITEM_SKU_BASIC = "bronze_donation";
22 | public static final String ITEM_SKU_INTERMEDIATE = "silver_donation";
23 | public static final String ITEM_SKU_ADVANCED = "gold_donation";
24 |
25 | private HexDialogView view;
26 |
27 | public HexDialog() {
28 | super();
29 | }
30 |
31 | @NonNull
32 | @Override
33 | protected String getKey() {
34 | return KEY;
35 | }
36 |
37 | @NonNull
38 | @Override
39 | protected List getProductIds() {
40 | return Arrays.asList(ITEM_SKU_BASIC, ITEM_SKU_INTERMEDIATE, ITEM_SKU_ADVANCED);
41 | }
42 |
43 | @Override
44 | protected void onCreate(@Nullable Bundle savedInstanceState) {
45 | super.onCreate(savedInstanceState);
46 |
47 | if (savedInstanceState != null) {
48 | dismiss();
49 | return;
50 | }
51 |
52 | requestWindowFeature(Window.FEATURE_NO_TITLE);
53 | getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
54 |
55 | view = new HexDialogView(this, this);
56 | view.setBackgroundResource(android.R.color.transparent);
57 |
58 | setContentView(view);
59 |
60 | HexDialogView.Button positive = view.getButtons()[2];
61 | HexDialogView.Button negative = view.getButtons()[0];
62 | HexDialogView.Button neutral = view.getButtons()[1];
63 |
64 | positive.setView(getPositiveView());
65 | negative.setView(getNegativeView());
66 | neutral.setView(getNeutralView());
67 |
68 | positive.setCenterXPercent(getPositiveXPercent());
69 | positive.setCenterYPercent(getPositiveYPercent());
70 | negative.setCenterXPercent(getNegativeXPercent());
71 | negative.setCenterYPercent(getNegativeYPercent());
72 | neutral.setCenterXPercent(getNeutralXPercent());
73 | neutral.setCenterYPercent(getNeutralYPercent());
74 |
75 | positive.setSideLengthPercent(getPositiveSideLengthPercent());
76 | negative.setSideLengthPercent(getNegativeSideLengthPercent());
77 | neutral.setSideLengthPercent(getNeutralSideLengthPercent());
78 |
79 | positive.setOnClickListener(getPositiveOnClickListener());
80 | negative.setOnClickListener(getNegativeOnClickListener());
81 | neutral.setOnClickListener(getNeutralOnClickListener());
82 | }
83 |
84 | public abstract View getPositiveView();
85 |
86 | public abstract HexDialogView.Button.OnClickListener getPositiveOnClickListener();
87 |
88 | public abstract float getPositiveXPercent();
89 |
90 | public abstract float getPositiveYPercent();
91 |
92 | public abstract float getPositiveSideLengthPercent();
93 |
94 | public abstract View getNegativeView();
95 |
96 | public abstract HexDialogView.Button.OnClickListener getNegativeOnClickListener();
97 |
98 | public abstract float getNegativeXPercent();
99 |
100 | public abstract float getNegativeYPercent();
101 |
102 | public abstract float getNegativeSideLengthPercent();
103 |
104 | public abstract View getNeutralView();
105 |
106 | public abstract HexDialogView.Button.OnClickListener getNeutralOnClickListener();
107 |
108 | public abstract float getNeutralXPercent();
109 |
110 | public abstract float getNeutralYPercent();
111 |
112 | public abstract float getNeutralSideLengthPercent();
113 |
114 | public void dismiss() {
115 | finish();
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/view/DonateDialog.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.view;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.view.View;
6 | import android.widget.ImageView;
7 | import android.widget.TextView;
8 |
9 | import com.android.billingclient.api.Purchase;
10 | import com.xlythe.hex.R;
11 | import com.xlythe.hex.Stats;
12 | import com.xlythe.hex.view.HexDialogView.Button.OnClickListener;
13 |
14 | import androidx.annotation.NonNull;
15 |
16 | /**
17 | * @author Will Harmon
18 | **/
19 | public class DonateDialog extends HexDialog {
20 |
21 | public static class Builder {
22 | private final Context context;
23 |
24 | public Builder(Context context) {
25 | this.context = context;
26 | }
27 |
28 | public void show() {
29 | context.startActivity(new Intent(context, DonateDialog.class));
30 | }
31 | }
32 |
33 | @Override
34 | public View getPositiveView() {
35 | View v = View.inflate(this, R.layout.dialog_view_donate, null);
36 |
37 | ImageView iv = v.findViewById(R.id.image);
38 | iv.setImageResource(R.drawable.donate_gold_d);
39 |
40 | TextView tv = v.findViewById(R.id.text);
41 | tv.setText(R.string.donate_gold);
42 |
43 | TextView price = v.findViewById(R.id.price);
44 | price.setText(R.string.donate_gold_price);
45 |
46 | return v;
47 | }
48 |
49 | @Override
50 | public View getNegativeView() {
51 | View v = View.inflate(this, R.layout.dialog_view_donate, null);
52 |
53 | ImageView iv = v.findViewById(R.id.image);
54 | iv.setImageResource(R.drawable.donate_bronze_d);
55 |
56 | TextView tv = v.findViewById(R.id.text);
57 | tv.setText(R.string.donate_bronze);
58 |
59 | TextView price = v.findViewById(R.id.price);
60 | price.setText(R.string.donate_bronze_price);
61 |
62 | return v;
63 | }
64 |
65 | @Override
66 | public View getNeutralView() {
67 | View v = View.inflate(this, R.layout.dialog_view_donate, null);
68 |
69 | ImageView iv = v.findViewById(R.id.image);
70 | iv.setImageResource(R.drawable.donate_silver_d);
71 |
72 | TextView tv = v.findViewById(R.id.text);
73 | tv.setText(R.string.donate_silver);
74 |
75 | TextView price = v.findViewById(R.id.price);
76 | price.setText(R.string.donate_silver_price);
77 |
78 | return v;
79 | }
80 |
81 | @NonNull
82 | @Override
83 | public OnClickListener getPositiveOnClickListener() {
84 | return () -> {
85 | purchaseItem(ITEM_SKU_ADVANCED);
86 | dismiss();
87 | };
88 | }
89 |
90 | @NonNull
91 | @Override
92 | public OnClickListener getNegativeOnClickListener() {
93 | return () -> {
94 | purchaseItem(ITEM_SKU_BASIC);
95 | dismiss();
96 | };
97 | }
98 |
99 | @NonNull
100 | @Override
101 | public OnClickListener getNeutralOnClickListener() {
102 | return () -> {
103 | purchaseItem(ITEM_SKU_INTERMEDIATE);
104 | dismiss();
105 | };
106 | }
107 |
108 | @Override
109 | public void onPurchaseFound(String sku, Purchase purchase) {
110 | int amount = 0;
111 | switch (sku) {
112 | case ITEM_SKU_BASIC:
113 | amount = 1;
114 | break;
115 | case ITEM_SKU_INTERMEDIATE:
116 | amount = 3;
117 | break;
118 | case ITEM_SKU_ADVANCED:
119 | amount = 5;
120 | break;
121 | }
122 | Stats.incrementDonationRank(this, amount);
123 |
124 | dismiss();
125 | }
126 |
127 | @Override
128 | public float getPositiveXPercent() {
129 | return 0.45f;
130 | }
131 |
132 | @Override
133 | public float getPositiveYPercent() {
134 | return 0.30f;
135 | }
136 |
137 | @Override
138 | public float getPositiveSideLengthPercent() {
139 | return 0.15f;
140 | }
141 |
142 | @Override
143 | public float getNegativeXPercent() {
144 | return 0.20f;
145 | }
146 |
147 | @Override
148 | public float getNegativeYPercent() {
149 | return 0.70f;
150 | }
151 |
152 | @Override
153 | public float getNegativeSideLengthPercent() {
154 | return 0.13f;
155 | }
156 |
157 | @Override
158 | public float getNeutralXPercent() {
159 | return 0.77f;
160 | }
161 |
162 | @Override
163 | public float getNeutralYPercent() {
164 | return 0.65f;
165 | }
166 |
167 | @Override
168 | public float getNeutralSideLengthPercent() {
169 | return 0.14f;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/Settings.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.preference.PreferenceManager;
6 |
7 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
8 | import com.hex.core.Timer;
9 |
10 | import androidx.annotation.ColorInt;
11 | import androidx.annotation.NonNull;
12 | import androidx.annotation.Nullable;
13 |
14 | /**
15 | * @author Will Harmon
16 | **/
17 | public class Settings {
18 | public static final String TAG = "Hex";
19 |
20 | public static final int MAX_BOARD_SIZE = 30;
21 | public static final int MIN_BOARD_SIZE = 4;
22 |
23 | private static final String NUM_TIMES_OPENED = "num_times_app_opened_review";
24 | public static final String GAME_SIZE = "gameSizePref";
25 | public static final String CUSTOM_GAME_SIZE = "customGameSizePref";
26 | private static final String SWAP = "swapPref";
27 | private static final String AUTOSAVE = "autosavePref";
28 | public static final String TIMER_TYPE = "timerTypePref";
29 | public static final String TIMER = "timerPref";
30 | public static final String TIMER_OPTIONS = "timerOptionsPref";
31 | public static final String DIFFICULTY = "comDifficulty";
32 |
33 | public static int getNumTimesOpened(@NonNull Context context) {
34 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
35 | return prefs.getInt(NUM_TIMES_OPENED, 0);
36 | }
37 |
38 | public static void incrementNumTimesOpened(@NonNull Context context) {
39 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
40 | int numTimesOpened = prefs.getInt(NUM_TIMES_OPENED, 0);
41 | prefs.edit().putInt(NUM_TIMES_OPENED, numTimesOpened + 1).apply();
42 | }
43 |
44 | public static void setTimesOpened(@NonNull Context context, int times) {
45 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
46 | prefs.edit().putInt(NUM_TIMES_OPENED, times).apply();
47 | }
48 |
49 | public static int getGridSize(@NonNull Context context) {
50 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
51 |
52 | int gridSize = Integer.parseInt(prefs.getString(GAME_SIZE, Integer.toString(context.getResources().getInteger(R.integer.DEFAULT_BOARD_SIZE))));
53 | if (gridSize == 0) {
54 | gridSize = Integer.parseInt(prefs.getString(CUSTOM_GAME_SIZE, Integer.toString(context.getResources().getInteger(R.integer.DEFAULT_BOARD_SIZE))));
55 | }
56 |
57 | // We don't want 0x0 games
58 | if (gridSize <= 0) gridSize = 1;
59 | return gridSize;
60 | }
61 |
62 | public static boolean getSwap(@NonNull Context context) {
63 | return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SWAP, context.getResources().getBoolean(R.bool.DEFAULT_SWAP_ENABLED));
64 | }
65 |
66 | public static boolean getAutosave(@NonNull Context context) {
67 | return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(AUTOSAVE,
68 | context.getResources().getBoolean(R.bool.DEFAULT_AUTOSAVE_ENABLED));
69 | }
70 |
71 | public static int getTimerType(Context context) {
72 | return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(TIMER_TYPE, String.valueOf(Timer.NO_TIMER)));
73 | }
74 |
75 | public static int getTimeAmount(Context context) {
76 | return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(TIMER, "0"));
77 | }
78 |
79 | public static String getPlayer1Name(@NonNull Context context, @Nullable GoogleSignInAccount googleSignInAccount) {
80 | if (googleSignInAccount != null && googleSignInAccount.getDisplayName() != null) {
81 | return googleSignInAccount.getDisplayName().split(" ")[0];
82 | }
83 | return context.getString(R.string.DEFAULT_P1_NAME);
84 | }
85 |
86 | public static String getPlayer2Name(@NonNull Context context) {
87 | return context.getString(R.string.DEFAULT_P2_NAME);
88 | }
89 |
90 | @ColorInt
91 | public static int getPlayer1Color(@NonNull Context context) {
92 | return context.getResources().getInteger(R.integer.DEFAULT_P1_COLOR);
93 | }
94 |
95 | @ColorInt
96 | public static int getPlayer2Color(@NonNull Context context) {
97 | return context.getResources().getInteger(R.integer.DEFAULT_P2_COLOR);
98 | }
99 |
100 | public static int getComputerDifficulty(@NonNull Context context) {
101 | return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(DIFFICULTY,
102 | String.valueOf(context.getResources().getInteger(R.integer.DEFAULT_AI_DIFFICULTY))));
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/view/GameOverDialog.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.view;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.view.View;
6 | import android.widget.ImageView;
7 | import android.widget.TextView;
8 |
9 | import com.hex.core.Game;
10 | import com.hex.core.Player;
11 | import com.hex.core.PlayingEntity;
12 | import com.xlythe.hex.R;
13 | import com.xlythe.hex.fragment.GameFragment;
14 | import com.xlythe.hex.view.HexDialogView.Button.OnClickListener;
15 |
16 | import androidx.annotation.NonNull;
17 | import androidx.annotation.Nullable;
18 |
19 | /**
20 | * @author Will Harmon
21 | **/
22 | public class GameOverDialog extends HexDialog {
23 | private static GameFragment gameFragment;
24 | private static PlayingEntity winner;
25 |
26 | public static class Builder {
27 | private final Context context;
28 |
29 | public Builder(Context context) {
30 | this.context = context;
31 | }
32 |
33 | public Builder setGameFragment(GameFragment fragment) {
34 | gameFragment = fragment;
35 | return this;
36 | }
37 |
38 | public Builder setWinner(PlayingEntity playingEntity) {
39 | winner = playingEntity;
40 | return this;
41 | }
42 |
43 | public void show() {
44 | context.startActivity(new Intent(context, GameOverDialog.class));
45 | }
46 | }
47 |
48 | @Override
49 | protected void onDestroy() {
50 | gameFragment = null;
51 | winner = null;
52 |
53 | super.onDestroy();
54 | }
55 |
56 | @Override
57 | public View getPositiveView() {
58 | View v = View.inflate(this, R.layout.dialog_view_game_over_icon, null);
59 |
60 | ImageView iv = v.findViewById(R.id.image);
61 | iv.setImageResource(R.drawable.play_again);
62 |
63 | return v;
64 | }
65 |
66 | @Override
67 | public View getNegativeView() {
68 | View v = View.inflate(this, R.layout.dialog_view_game_over_icon, null);
69 |
70 | ImageView iv = v.findViewById(R.id.image);
71 | iv.setImageResource(R.drawable.home);
72 |
73 | return v;
74 | }
75 |
76 | @Override
77 | public View getNeutralView() {
78 | View v = View.inflate(this, R.layout.dialog_view_game_over, null);
79 |
80 | TextView action = v.findViewById(R.id.action);
81 | TextView time = v.findViewById(R.id.time);
82 |
83 | Game game = gameFragment.getGame();
84 | PlayingEntity winner = this.winner;
85 |
86 | String actionText = winner.getType().equals(Player.Human) ? getString(R.string.game_over_won) : getString(R.string.game_over_lose);
87 | long hours = game.getGameLength() / (60 * 60 * 1000);
88 | long minutes = game.getGameLength() / (60 * 1000) - hours * 60;
89 | long seconds = game.getGameLength() / (1000) - minutes * 60 - hours * 60 * 60;
90 |
91 | action.setText(getString(R.string.game_over_action, actionText));
92 | time.setText(getString(R.string.game_over_length, hours, minutes, seconds));
93 |
94 | if (game.getPlayer1().getType().equals(Player.Human) && game.getPlayer2().getType().equals(Player.Human)) {
95 | TextView player = v.findViewById(R.id.player);
96 | player.setText(winner.getName());
97 | }
98 |
99 | return v;
100 | }
101 |
102 | @NonNull
103 | @Override
104 | public OnClickListener getPositiveOnClickListener() {
105 | return () -> {
106 | gameFragment.startNewGame();
107 | dismiss();
108 | };
109 | }
110 |
111 | @NonNull
112 | @Override
113 | public OnClickListener getNegativeOnClickListener() {
114 | return () -> {
115 | gameFragment.setGoHome(true);
116 | dismiss();
117 | };
118 | }
119 |
120 | @Nullable
121 | @Override
122 | public OnClickListener getNeutralOnClickListener() {
123 | return null;
124 | }
125 |
126 | @Override
127 | public float getPositiveXPercent() {
128 | return 0.7234375f;
129 | }
130 |
131 | @Override
132 | public float getPositiveYPercent() {
133 | return 0.70442708f;
134 | }
135 |
136 | @Override
137 | public float getPositiveSideLengthPercent() {
138 | return 0.090625f;
139 | }
140 |
141 | @Override
142 | public float getNegativeXPercent() {
143 | return 0.18359375f;
144 | }
145 |
146 | @Override
147 | public float getNegativeYPercent() {
148 | return 0.47005208f;
149 | }
150 |
151 | @Override
152 | public float getNegativeSideLengthPercent() {
153 | return 0.074609375f;
154 | }
155 |
156 | @Override
157 | public float getNeutralXPercent() {
158 | return 0.4921875f;
159 | }
160 |
161 | @Override
162 | public float getNeutralYPercent() {
163 | return 0.3125f;
164 | }
165 |
166 | @Override
167 | public float getNeutralSideLengthPercent() {
168 | return 0.178125f;
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/BaseGameActivity.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.util.Log;
6 |
7 | import com.google.android.gms.auth.api.signin.GoogleSignIn;
8 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
9 | import com.google.android.gms.auth.api.signin.GoogleSignInClient;
10 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
11 | import com.google.android.gms.games.AchievementsClient;
12 | import com.google.android.gms.games.Games;
13 | import com.google.android.gms.games.GamesClient;
14 | import com.google.android.gms.games.GamesCompat;
15 | import com.google.android.gms.games.InvitationsClient;
16 | import com.google.android.gms.games.PlayersClient;
17 | import com.google.android.gms.games.TurnBasedMultiplayerClient;
18 |
19 | import androidx.annotation.Nullable;
20 | import androidx.appcompat.app.AppCompatActivity;
21 |
22 | import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
23 | import static com.xlythe.hex.Settings.TAG;
24 |
25 | public abstract class BaseGameActivity extends AppCompatActivity {
26 |
27 | private static final int REQUEST_CODE_SIGN_IN = 1001;
28 |
29 | private GoogleSignInClient mGoogleSignInClient;
30 |
31 | private GoogleSignInAccount mGoogleSignInAccount;
32 |
33 | private GamesClient mGamesClient;
34 | private TurnBasedMultiplayerClient mTurnBasedMultiplayerClient;
35 | private PlayersClient mPlayersClient;
36 | private AchievementsClient mAchievementsClient;
37 | private InvitationsClient mInvitationsClient;
38 |
39 | @Override
40 | public void onCreate(Bundle savedInstanceState) {
41 | super.onCreate(savedInstanceState);
42 | mGoogleSignInClient = GoogleSignIn.getClient(this, new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
43 | .requestProfile()
44 | .build());
45 | }
46 |
47 | @Override
48 | protected void onStart() {
49 | super.onStart();
50 | mGoogleSignInClient.silentSignIn()
51 | .addOnSuccessListener(this::onSignInSucceeded)
52 | .addOnFailureListener(this::onSignInFailed);
53 | }
54 |
55 | @Override
56 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
57 | if (requestCode == REQUEST_CODE_SIGN_IN) {
58 | GoogleSignIn.getSignedInAccountFromIntent(data)
59 | .addOnSuccessListener(this::onSignInSucceeded)
60 | .addOnFailureListener(this::onSignInFailed);
61 | } else {
62 | super.onActivityResult(requestCode, resultCode, data);
63 | }
64 | }
65 |
66 | public GamesClient getGamesClient() {
67 | return mGamesClient;
68 | }
69 |
70 | public TurnBasedMultiplayerClient getTurnBasedMultiplayerClient() {
71 | return mTurnBasedMultiplayerClient;
72 | }
73 |
74 | public PlayersClient getPlayersClient() {
75 | return mPlayersClient;
76 | }
77 |
78 | public AchievementsClient getAchievementsClient() {
79 | return mAchievementsClient;
80 | }
81 |
82 | public InvitationsClient getInvitationsClient() {
83 | return mInvitationsClient;
84 | }
85 |
86 | public boolean isSignedIn() {
87 | return GoogleSignIn.getLastSignedInAccount(this) != null;
88 | }
89 |
90 | public void signIn() {
91 | Log.v(TAG, "User initiated sign in");
92 | startActivityForResult(mGoogleSignInClient.getSignInIntent(), REQUEST_CODE_SIGN_IN);
93 | }
94 |
95 | public void signOut() {
96 | mGoogleSignInClient.signOut();
97 | onSignInFailed();
98 | }
99 |
100 | @Nullable
101 | public GoogleSignInAccount getGoogleSignInAccount() {
102 | return mGoogleSignInAccount;
103 | }
104 |
105 | public void onSignInSucceeded(GoogleSignInAccount googleSignInAccount) {
106 | Log.d(TAG, "User successfully signed in: " + googleSignInAccount.getDisplayName());
107 | mGoogleSignInAccount = googleSignInAccount;
108 | mGamesClient = Games.getGamesClient(this, googleSignInAccount);
109 | mTurnBasedMultiplayerClient = GamesCompat.getTurnBasedMultiplayerClient(this, googleSignInAccount);
110 | mPlayersClient = Games.getPlayersClient(this, googleSignInAccount);
111 | mAchievementsClient = Games.getAchievementsClient(this, googleSignInAccount);
112 | mInvitationsClient = GamesCompat.getInvitationsClient(this, googleSignInAccount);
113 | }
114 |
115 | public void onSignInFailed() {
116 | onSignInFailed(null);
117 | }
118 |
119 | public void onSignInFailed(@Nullable Throwable reason) {
120 | Log.e(TAG, "Failed to sign in", reason);
121 | mGoogleSignInAccount = null;
122 | }
123 |
124 | public void keepScreenOn(boolean screenOn) {
125 | if (screenOn) {
126 | getWindow().addFlags(FLAG_KEEP_SCREEN_ON);
127 | } else {
128 | getWindow().clearFlags(FLAG_KEEP_SCREEN_ON);
129 | }
130 | }
131 |
132 | public abstract void startQuickGame();
133 |
134 | public abstract void inviteFriends();
135 |
136 | public abstract void checkInvites();
137 |
138 | public abstract void openAchievements();
139 | }
140 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/fragment/HexFragment.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.fragment;
2 |
3 | import android.os.Bundle;
4 | import android.util.Log;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 |
9 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
10 | import com.google.android.gms.games.AchievementsClient;
11 | import com.google.android.gms.games.GamesClient;
12 | import com.google.android.gms.games.PlayersClient;
13 | import com.google.android.gms.games.TurnBasedMultiplayerClient;
14 | import com.xlythe.hex.compat.Game;
15 | import com.xlythe.hex.MainActivity;
16 |
17 | import androidx.annotation.AnimRes;
18 | import androidx.annotation.NonNull;
19 | import androidx.annotation.Nullable;
20 | import androidx.fragment.app.Fragment;
21 |
22 | import static com.xlythe.hex.Settings.TAG;
23 |
24 | /**
25 | * @author Will Harmon
26 | **/
27 | public class HexFragment extends Fragment {
28 |
29 | private ViewGroup mContainer;
30 |
31 | @Nullable
32 | @Override
33 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
34 | View v = super.onCreateView(inflater, container, savedInstanceState);
35 | mContainer = container;
36 | return v;
37 | }
38 |
39 | @Override
40 | public void onStart() {
41 | super.onStart();
42 | mContainer.requestFocus();
43 | }
44 |
45 | protected MainActivity getMainActivity() {
46 | return (MainActivity) getActivity();
47 | }
48 |
49 | protected void keepScreenOn(boolean screenOn) {
50 | MainActivity activity = getMainActivity();
51 | if (activity == null || isDetached()) {
52 | Log.w(TAG, "Unable to change screen on state because fragment is detached.");
53 | return;
54 | }
55 |
56 | activity.keepScreenOn(screenOn);
57 | }
58 |
59 | protected void overridePendingTransition(@AnimRes int enterAnim, @AnimRes int exitAnim) {
60 | getMainActivity().overridePendingTransition(enterAnim, exitAnim);
61 | }
62 |
63 | protected void runOnUiThread(Runnable r) {
64 | MainActivity activity = getMainActivity();
65 | if (activity == null || isDetached()) {
66 | Log.w(TAG, "Unable to run runnable on ui thread because fragment is detached.");
67 | return;
68 | }
69 |
70 | activity.runOnUiThread(r);
71 | }
72 |
73 | protected void swapFragment(Fragment fragment) {
74 | MainActivity activity = getMainActivity();
75 | if (activity == null || isDetached()) {
76 | Log.w(TAG, "Unable to swap fragment because current fragment is detached.");
77 | return;
78 | }
79 |
80 | activity.swapFragment(fragment);
81 | }
82 |
83 | protected GamesClient getGamesClient() {
84 | return getMainActivity().getGamesClient();
85 | }
86 |
87 | protected TurnBasedMultiplayerClient getTurnBasedMultiplayerClient() {
88 | return getMainActivity().getTurnBasedMultiplayerClient();
89 | }
90 |
91 | protected PlayersClient getPlayersClient() {
92 | return getMainActivity().getPlayersClient();
93 | }
94 |
95 | protected AchievementsClient getAchievementsClient() {
96 | return getMainActivity().getAchievementsClient();
97 | }
98 |
99 | @Nullable
100 | protected GoogleSignInAccount getGoogleSignInAccount() {
101 | MainActivity activity = getMainActivity();
102 | if (activity == null || isDetached()) {
103 | return null;
104 | }
105 |
106 | return activity.getGoogleSignInAccount();
107 | }
108 |
109 | protected boolean isSignedIn() {
110 | MainActivity activity = getMainActivity();
111 | if (activity == null || isDetached()) {
112 | return false;
113 | }
114 |
115 | return activity.isSignedIn();
116 | }
117 |
118 | protected void signIn() {
119 | MainActivity activity = getMainActivity();
120 | if (activity == null || isDetached()) {
121 | Log.w(TAG, "Unable to sign in because current fragment is detached.");
122 | return;
123 | }
124 |
125 | activity.signIn();
126 | }
127 |
128 | protected void signOut() {
129 | MainActivity activity = getMainActivity();
130 | if (activity == null || isDetached()) {
131 | Log.w(TAG, "Unable to sign out because current fragment is detached.");
132 | return;
133 | }
134 |
135 | activity.signOut();
136 | }
137 |
138 | protected void returnHome() {
139 | MainActivity activity = getMainActivity();
140 | if (activity == null || isDetached()) {
141 | Log.w(TAG, "Unable to return home because current fragment is detached.");
142 | return;
143 | }
144 |
145 | activity.returnHome();
146 | }
147 |
148 | protected void startQuickGame() {
149 | MainActivity activity = getMainActivity();
150 | if (activity == null || isDetached()) {
151 | Log.w(TAG, "Unable to start a quick game because current fragment is detached.");
152 | return;
153 | }
154 |
155 | activity.startQuickGame();
156 | }
157 |
158 | protected void inviteFriends() {
159 | MainActivity activity = getMainActivity();
160 | if (activity == null || isDetached()) {
161 | Log.w(TAG, "Unable to invite friends because current fragment is detached.");
162 | return;
163 | }
164 |
165 | activity.inviteFriends();
166 | }
167 |
168 | protected void checkInvites() {
169 | MainActivity activity = getMainActivity();
170 | if (activity == null || isDetached()) {
171 | Log.w(TAG, "Unable to check invites because current fragment is detached.");
172 | return;
173 | }
174 |
175 | activity.checkInvites();
176 | }
177 |
178 | protected void openAchievements() {
179 | MainActivity activity = getMainActivity();
180 | if (activity == null || isDetached()) {
181 | Log.w(TAG, "Unable to open achievements because current fragment is detached.");
182 | return;
183 | }
184 |
185 | activity.openAchievements();
186 | }
187 |
188 | protected void switchToGame(Game game) {
189 | MainActivity activity = getMainActivity();
190 | if (activity == null || isDetached()) {
191 | Log.w(TAG, "Unable to switch to game because current fragment is detached.");
192 | return;
193 | }
194 |
195 | activity.switchToGame(game);
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hex
7 | 307536683764
8 | CgkI9MX11PkIEAIQCg
9 | CgkI9MX11PkIEAIQEw
10 | CgkI9MX11PkIEAIQCw
11 | CgkI9MX11PkIEAIQFQ
12 | CgkI9MX11PkIEAIQFg
13 | CgkI9MX11PkIEAIQDA
14 | CgkI9MX11PkIEAIQDQ
15 | CgkI9MX11PkIEAIQDg
16 | CgkI9MX11PkIEAIQFA
17 | CgkI9MX11PkIEAIQGQ
18 |
19 |
20 | How to Play
21 | Settings
22 |
23 |
24 | %s\'s stats
25 | time played %02d:%02d:%02d
26 | games played %d
27 | games won %d
28 |
29 | play
30 | settings
31 | donate
32 | history
33 | how to play
34 | achievements
35 |
36 | Sign Out
37 |
38 |
39 | computer
40 | pass to play
41 | online
42 |
43 |
44 | Quick Game
45 | Invite Friends
46 | Pending Invites
47 |
48 |
49 | Board Size
50 | Pick a size for the gameboard (Current: %sx%s)
51 | Enter a grid size (Ex. 9)
52 | Swap
53 | Allow the second player to swap places during their first move
54 | Timer
55 | Limit the amount of time a player has (in minutes)
56 | Autosave
57 | Save a replay at the end of each game
58 | Computer Difficulty
59 |
60 |
61 | Undo
62 | Restart
63 | Replay
64 | Save
65 | Settings
66 | Exit
67 |
68 |
69 | %s\'s
70 | TURN
71 | Time Left
72 | Saved!
73 | Failed
74 | Claim Victory
75 | %s %s vs %s
76 | %s vs %s
77 | You
78 | %s!
79 | Won
80 | Lost
81 | Time
82 | %02d:%02d:%02d
83 |
84 |
85 | Hex is played by two players, who take turns placing pieces on the board. The players have different colours, say red and blue. (The players themselves are sometimes referred to as Red and Blue) The four edges of the board are coloured with the same colours, in such a way that parallel edges have the same colour. Red wins if he can build a continuous chain between the two red edges, and blue wins if he can build such a chain between the blue edges.\n\n
86 | (wikipedia.org)
87 | Privacy policy
88 |
89 |
90 | Are you sure you want to exit?
91 | Do you want to start a new game?
92 |
93 |
94 | Yes
95 | No
96 | Enter
97 | OK
98 | Cancel
99 | Submit
100 | Rename
101 | Delete
102 | Enter a filename
103 |
104 |
105 | bronze
106 | $.99
107 | silver
108 | $2.99
109 | gold
110 | $4.99
111 |
112 |
113 | Game version error. One of the players has an out of date version of the game. The game has been aborted!
114 |
115 |
116 | UNKNOWN
117 |
118 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hex
6 | 307536683764
7 | CgkI9MX11PkIEAIQCg
8 | CgkI9MX11PkIEAIQEw
9 | CgkI9MX11PkIEAIQCw
10 | CgkI9MX11PkIEAIQFQ
11 | CgkI9MX11PkIEAIQFg
12 | CgkI9MX11PkIEAIQDA
13 | CgkI9MX11PkIEAIQDQ
14 | CgkI9MX11PkIEAIQDg
15 | CgkI9MX11PkIEAIQFA
16 | CgkI9MX11PkIEAIQGQ
17 |
18 |
19 | Anleitung
20 | Einstellungen
21 |
22 | %s\s Statistik
23 | Spielzeit %02d:%02d:%02d
24 | Spiele gespielt %d
25 | Siege %d
26 | Spielen
27 | Einstellungen
28 | Spenden
29 | Verlauf
30 | Anleitung
31 | Errungenschaften
32 |
33 | Ausloggen
34 |
35 | KI
36 | Weiter zum Spiel
37 | Online
38 |
39 | Schnelles Spiel
40 | Freunde einladen
41 | Ausstehende Einladungen
42 |
43 | Feldgröße
44 | Eine Spielfeldgröße wählen (Aktuell: %sx%s)
45 | Eine Feldgröße eingeben (Bsp. 9)
46 | Tauschen
47 | Dem zweiten Spieler erlauben, im ersten Zug ein Feld zu tauschen
48 | Timer
49 | Die Zeit für einen Zug limitieren (in Minuten)
50 | Autospeichern
51 | Die Aufzeichnung am Ende jedes Spiels speichern
52 | Schwierigkeitsgrad
53 |
54 | Rückgängig
55 | Neustarten
56 | Wiedergeben
57 | Speichern
58 | Einstellungen
59 | Verlassen
60 |
61 | %s\s
62 | ZUG
63 | Zeit verbleibend
64 | Gespeichert!
65 | Fehlgeschlagen
66 | Sieg beanspruchen
67 | %s %s vs %s
68 | %s vs %s
69 | Sie
70 | %s!
71 | Gewonnen
72 | Verloren
73 | Zeit
74 | %02d:%02d:%02d
75 | %s fordert dich heraus.
76 | %s bietet ein neues Spiel an.
77 | %s bittet darum, Zug %d rückgängig zu machen.
78 | Akzeptieren
79 | Ablehnen
80 |
81 | Hex wird von zwei Spielern gespielt, die rundenweise Felder auf dem Spielfeld besetzen. Die Spieler haben unterschiedliche Farben, und zwar rot und blau. Die vier Kanten des Spielfelds besitzen ebenfalls diese Farben, und zwar so, dass jeweils die gegenüberliegenden Kanten die gleiche Farbe haben. Ein Spieler gewinnt, falls er einen durchgängigen Weg zwischen den beiden Kanten seiner Farbe erzeugen kann.\n\n
82 | (Nach hexwiki.org)
83 |
84 | Sicher, dass Sie das Spiel verlassen möchten?
85 | Ein neues Spiel beginnen?
86 |
87 | Ja
88 | Nein
89 | Enter
90 | OK
91 | Abbrechen
92 | Absenden
93 | Umbenennen
94 | Löschen
95 | Einen Dateinamen eingeben
96 |
97 | Wie die App?
98 | Hex im Play Store bewerten
99 | Nein, danke
100 | Sicher
101 |
102 | Bronze
103 | $0,99
104 | Silber
105 | $2,99
106 | Gold
107 | $4,99
108 |
109 | Fehler der Spielversion. Einer der Spieler hat eine veraltete Spielversion. Das Spiel wurde abgebrochen!
110 |
111 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/fragment/HistoryFragment.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.fragment;
2 |
3 | import android.Manifest;
4 | import android.content.Context;
5 | import android.os.Bundle;
6 | import android.os.Environment;
7 | import android.os.Handler;
8 | import android.util.Log;
9 | import android.view.LayoutInflater;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 | import android.widget.BaseAdapter;
13 | import android.widget.GridView;
14 | import android.widget.TextView;
15 | import android.widget.Toast;
16 |
17 | import com.google.gson.JsonSyntaxException;
18 | import com.hex.core.Game;
19 | import com.xlythe.hex.FileUtil;
20 | import com.xlythe.hex.R;
21 |
22 | import java.io.File;
23 | import java.io.FilenameFilter;
24 | import java.io.IOException;
25 | import java.text.DateFormat;
26 | import java.text.SimpleDateFormat;
27 | import java.util.Arrays;
28 | import java.util.Date;
29 | import java.util.Locale;
30 |
31 | import androidx.annotation.NonNull;
32 | import androidx.annotation.Nullable;
33 | import androidx.annotation.UiThread;
34 |
35 | import static com.xlythe.hex.PermissionUtils.hasPermissions;
36 | import static com.xlythe.hex.Settings.TAG;
37 |
38 | /**
39 | * @author Will Harmon
40 | **/
41 | public class HistoryFragment extends HexFragment {
42 | private static final DateFormat DATE_FORMAT = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault());
43 |
44 | @Override
45 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
46 | super.onCreateView(inflater, container, savedInstanceState);
47 | View view = inflater.inflate(R.layout.fragment_history, container, false);
48 |
49 | GridView games = view.findViewById(R.id.games);
50 | Item[] fileList = loadFileList();
51 | games.setAdapter(new HistoryAdapter(getMainActivity(), fileList));
52 | games.setOnItemClickListener((parent, v, position, id) -> openFile(fileList[position].file));
53 |
54 | return view;
55 | }
56 |
57 | @NonNull
58 | private Item[] loadFileList() {
59 | File folder = new File(requireContext().getFilesDir() + File.separator + "Hex" + File.separator);
60 |
61 | // Create the path if able.
62 | if (folder.mkdirs()) {
63 | Log.d(TAG, "Successfully made the directory for history");
64 | }
65 |
66 | Item[] items = new Item[0];
67 | if (folder.exists()) {
68 | FilenameFilter filter = (dir, filename) -> {
69 | File sel = new File(dir, filename);
70 | // Filters based on whether the file is hidden or not
71 | return (sel.isFile() || sel.isDirectory()) && !sel.isHidden();
72 | };
73 |
74 | String[] files = folder.list(filter);
75 | if (files != null) {
76 | items = new Item[files.length];
77 | for (int i = 0; i < files.length; i++) {
78 | items[i] = new Item(files[i]);
79 | }
80 | }
81 | }
82 |
83 | Arrays.sort(items, (f1, f2) -> f2.file.compareTo(f1.file));
84 | return items;
85 | }
86 |
87 | private static class Item {
88 | @NonNull
89 | final String file;
90 |
91 | @Nullable
92 | String title;
93 | @Nullable
94 | String date;
95 | int team;
96 |
97 | @Nullable
98 | Game game;
99 |
100 | Item(@NonNull String file) {
101 | this.file = file;
102 | }
103 |
104 | @UiThread
105 | void initialize(Context context, Runnable onLoadedCallback) {
106 | final Handler h = new Handler();
107 | new Thread(() -> {
108 | Game game;
109 | try {
110 | game = Game.load(FileUtil.loadGameAsString(context, file));
111 | } catch (IOException | JsonSyntaxException e) {
112 | e.printStackTrace();
113 | return;
114 | }
115 |
116 | // The last player to make the move is considered the winner.
117 | int team = game.getMoveList().getMove().getTeam();
118 | String title = context.getString(R.string.auto_saved_title, game.getPlayer1().getName(), game.getPlayer2().getName());
119 | String date = DATE_FORMAT.format(new Date(game.getGameStart()));
120 |
121 | h.post(() -> {
122 | this.game = game;
123 | this.team = team;
124 | this.title = title;
125 | this.date = date;
126 | onLoadedCallback.run();
127 | });
128 | }).start();
129 | }
130 | }
131 |
132 | private void openFile(String fileName) {
133 | try {
134 | Bundle b = new Bundle();
135 | b.putString(GameFragment.GAME, FileUtil.loadGameAsString(requireContext(), fileName));
136 | b.putBoolean(GameFragment.REPLAY, true);
137 |
138 | GameFragment gameFragment = new GameFragment();
139 | gameFragment.setArguments(b);
140 | swapFragment(gameFragment);
141 | } catch (IOException e) {
142 | e.printStackTrace();
143 | Toast.makeText(getMainActivity(), R.string.game_toast_failed, Toast.LENGTH_SHORT).show();
144 | }
145 | }
146 |
147 | static class HistoryAdapter extends BaseAdapter {
148 | private final Context context;
149 | private final Item[] files;
150 |
151 | HistoryAdapter(Context context, @NonNull Item[] files) {
152 | this.context = context;
153 | this.files = files;
154 | }
155 |
156 | @Nullable
157 | public View getView(int position, @Nullable View convertView, ViewGroup parent) {
158 | final Item i = files[position];
159 | final View v = convertView != null ? convertView : View.inflate(context, R.layout.view_history_item, null);
160 |
161 | // Load up the game so we can get information
162 | if (i.title == null || i.date == null || i.team == 0) {
163 | i.initialize(context, this::notifyDataSetChanged);
164 | }
165 |
166 | TextView title = v.findViewById(R.id.title);
167 | TextView date = v.findViewById(R.id.date);
168 |
169 | title.setText(i.title);
170 | date.setText(i.date);
171 |
172 | if (i.team == 1) {
173 | v.setBackgroundResource(R.drawable.history_background_red);
174 | } else if (i.team == 2) {
175 | v.setBackgroundResource(R.drawable.history_background_blue);
176 | } else {
177 | v.setBackgroundResource(R.drawable.history_background_black);
178 | }
179 |
180 | return v;
181 | }
182 |
183 | @Override
184 | public int getCount() {
185 | return files.length;
186 | }
187 |
188 | @Override
189 | public Object getItem(int position) {
190 | return files[position];
191 | }
192 |
193 | @Override
194 | public long getItemId(int position) {
195 | return position;
196 | }
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/app/src/main/res/values-nl/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hex
7 | 307536683764
8 | CgkI9MX11PkIEAIQCg
9 | CgkI9MX11PkIEAIQEw
10 | CgkI9MX11PkIEAIQCw
11 | CgkI9MX11PkIEAIQFQ
12 | CgkI9MX11PkIEAIQFg
13 | CgkI9MX11PkIEAIQDA
14 | CgkI9MX11PkIEAIQDQ
15 | CgkI9MX11PkIEAIQDg
16 | CgkI9MX11PkIEAIQFA
17 |
18 |
19 | Uitleg
20 | Instellingen
21 |
22 |
23 | %s\'s statistieken
24 | Tijd gespeeld: %02d:%02d:%02d
25 | Spellen gespeeld: %d
26 | Spellen gewonnen: %d
27 |
28 | start spel
29 | instellingen
30 | doneer
31 | geschiedenis
32 | uitleg
33 | prestaties
34 |
35 | Afmelden
36 |
37 |
38 | computer
39 | offline multiplayer
40 | online multiplayer
41 |
42 |
43 | snel spel
44 | vrienden uitnodigen
45 | openstaande uitnodigingen
46 |
47 |
48 | Speelbord grootte
49 | Kies een grootte voor het speelbord (huidig: %sx%s)
50 | Vul de rastergrootte in (bijv. 9)
51 | Van plaats wisselen
52 | Sta de tweede speler toe om met je van plaats te wisselen tijdens de eerste zet
53 | Timer
54 | Stel een tijdslimiet in (in minuten)
55 | Automatisch opslaan
56 | Sla automatisch een bestand op in de geschiedenis aan het eind van elk spel
57 | Moeilijkheidsgraad computer
58 |
59 |
60 | Ongedaan\nmaken
61 | Opnieuw\nstarten
62 | Terugkijken
63 | Opslaan
64 | Instellingen
65 | Stop
66 |
67 |
68 | %s\'s
69 | BEURT
70 | Tijd over
71 | Opgeslagen!
72 | Misukt
73 | Claim Overwinning
74 | %s %s vs %s
75 | %s vs %s
76 | Jij
77 | %s!
78 | hebt gewonnen
79 | hebt verloren
80 | Tijd
81 | %02d:%02d:%02d
82 | %s daagt je uit voor een spel.
83 | %s wil een nieuw spel starten.
84 | %s wil graag zet %d ongedaan maken.
85 | Accepteren
86 | Weigeren
87 |
88 |
89 | Hex is een spel voor 2 spelers, die om de beurt zeshoeken op het bord leggen. De spelers hebben verschillende kleuren, rood en blauw. De vier hoeken van het bord zijn ook rood en blauw, de hoeken die parallel tegenover elkaar staan hebben dezelfde kleur. Degene die een ononderbroken pad van zeshoeken van de ene kant naar de andere kant van het bord kan aanleggen, wint.\n\n
90 | (Van: hexwiki.org)
91 |
92 |
93 | Weet je zeker dat je wilt stoppen?
94 | Wil je een nieuw spel starten?
95 |
96 |
97 | Ja
98 | Nee
99 | Ga
100 | OK
101 | Annuleer
102 | Invoeren
103 | Hernoemen
104 | Verwijderen
105 | Voer een bestandsnaam in
106 |
107 |
108 | Vind jij deze app leuk?
109 | Waardeer Hex in de Play Store!
110 | Nee, bedankt
111 | Natuurlijk
112 |
113 |
114 | brons
115 | $0.99
116 | zilver
117 | $2.99
118 | goud
119 | $4.99
120 |
121 |
122 | Error! Eén van de spelers heeft een oude versie van dit spel. Het spel is afgebroken!
123 |
124 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/fragment/PreferencesFragment.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.fragment;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.AlertDialog;
5 | import android.content.Context;
6 | import android.content.SharedPreferences;
7 | import android.os.Bundle;
8 | import android.preference.Preference;
9 | import android.preference.Preference.OnPreferenceChangeListener;
10 | import android.preference.Preference.OnPreferenceClickListener;
11 | import android.preference.PreferenceFragment;
12 | import android.preference.PreferenceManager;
13 | import android.text.InputType;
14 | import android.view.LayoutInflater;
15 | import android.view.View;
16 | import android.view.inputmethod.EditorInfo;
17 | import android.widget.AdapterView;
18 | import android.widget.AdapterView.OnItemSelectedListener;
19 | import android.widget.EditText;
20 | import android.widget.Spinner;
21 |
22 | import com.xlythe.hex.R;
23 | import com.xlythe.hex.Settings;
24 |
25 | import androidx.annotation.NonNull;
26 | import androidx.annotation.Nullable;
27 |
28 | /**
29 | * @author Will Harmon
30 | **/
31 | @SuppressLint("NewApi")
32 | public class PreferencesFragment extends PreferenceFragment {
33 | SharedPreferences settings;
34 | Preference gridPref;
35 | Preference timerPref;
36 |
37 | @Override
38 | public void onCreate(@Nullable Bundle savedInstanceState) {
39 | super.onCreate(savedInstanceState);
40 | settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
41 | loadPreferences();
42 | }
43 |
44 | @Override
45 | public void onResume() {
46 | super.onResume();
47 | setListeners();
48 | }
49 |
50 | private class DifficultyListener implements OnPreferenceChangeListener {
51 | @Override
52 | public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) {
53 | preference.setSummary(getResources().getStringArray(R.array.comDifficultyArray)[Integer.parseInt(newValue.toString())]);
54 | return true;
55 | }
56 | }
57 |
58 | private class GridListener implements OnPreferenceChangeListener {
59 | @Override
60 | public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) {
61 | if (newValue.toString().equals("0")) {
62 | // Custom value needed
63 | showInputDialog(getString(R.string.preferences_summary_custom_game_size));
64 | return false;
65 | } else {
66 | preference.setSummary(String.format(getString(R.string.preferences_summary_game_size), newValue, newValue));
67 | return true;
68 | }
69 | }
70 | }
71 |
72 | private class TimerListener implements OnPreferenceClickListener {
73 | @Override
74 | public boolean onPreferenceClick(Preference pref) {
75 | LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
76 | View dialoglayout = inflater.inflate(R.layout.preferences_timer, null);
77 | final Spinner timerType = dialoglayout.findViewById(R.id.timerType);
78 | final EditText timer = dialoglayout.findViewById(R.id.timer);
79 | timer.setText(settings.getString(Settings.TIMER, Integer.toString(getResources().getInteger(R.integer.DEFAULT_TIMER_TIME))));
80 | timerType.setSelection(Integer.valueOf(settings.getString(Settings.TIMER_TYPE, Integer.toString(getResources().getInteger(R.integer.DEFAULT_TIMER_TYPE)))));
81 | timerType.setOnItemSelectedListener(new OnItemSelectedListener() {
82 | @Override
83 | public void onItemSelected(AdapterView> adapterView, View view, int arg2, long arg3) {
84 | if (arg2 > 0) {
85 | timer.setVisibility(View.VISIBLE);
86 | } else {
87 | timer.setVisibility(View.GONE);
88 | }
89 | }
90 |
91 | @Override
92 | public void onNothingSelected(AdapterView> arg0) {
93 | timer.setVisibility(View.GONE);
94 | }
95 | });
96 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
97 | builder.setView(dialoglayout).setPositiveButton(getString(R.string.okay), (dialog, which) -> {
98 | String timerTime = timer.getText().toString();
99 | if (timerTime.isEmpty()) timerTime = "0";
100 | settings.edit()
101 | .putString(Settings.TIMER_TYPE, getResources().getStringArray(R.array.timerTypeValues)[timerType.getSelectedItemPosition()])
102 | .putString(Settings.TIMER, timerTime)
103 | .apply();
104 | }).setNegativeButton(getString(R.string.cancel), null).show();
105 | return true;
106 | }
107 | }
108 |
109 | private void setListeners() {
110 | // Allow for custom grid sizes
111 | gridPref = findPreference(Settings.GAME_SIZE);
112 | if (gridPref != null) {
113 | String boardSize = String.valueOf(Settings.getGridSize(getActivity()));
114 | gridPref.setSummary(String.format(getString(R.string.preferences_summary_game_size), boardSize, boardSize));
115 | gridPref.setOnPreferenceChangeListener(new GridListener());
116 | }
117 |
118 | // Give a custom popup for timers
119 | timerPref = findPreference(Settings.TIMER_OPTIONS);
120 | if (timerPref != null) {
121 | timerPref.setOnPreferenceClickListener(new TimerListener());
122 | }
123 |
124 | Preference comDifficultyPref = findPreference(Settings.DIFFICULTY);
125 | if (comDifficultyPref != null) {
126 | comDifficultyPref.setOnPreferenceChangeListener(new DifficultyListener());
127 | comDifficultyPref.setSummary(getResources().getStringArray(R.array.comDifficultyArray)[Settings.getComputerDifficulty(getActivity())]);
128 | }
129 | }
130 |
131 | private void loadPreferences() {
132 | addPreferencesFromResource(R.xml.preferences_general);
133 | }
134 |
135 | /**
136 | * Popup for custom grid sizes
137 | */
138 | private void showInputDialog(String message) {
139 | final EditText editText = new EditText(getActivity());
140 | editText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
141 | editText.setInputType(InputType.TYPE_CLASS_NUMBER);
142 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
143 | builder.setTitle(message).setView(editText).setPositiveButton(getString(R.string.okay), (dialog, which) -> {
144 | if (!editText.getText().toString().equals("")) {
145 | int input = Integer.decode(editText.getText().toString());
146 | if (input > Settings.MAX_BOARD_SIZE) {
147 | input = Settings.MAX_BOARD_SIZE;
148 | } else if (input < Settings.MIN_BOARD_SIZE) {
149 | input = Settings.MIN_BOARD_SIZE;
150 | }
151 | settings.edit().putString(Settings.CUSTOM_GAME_SIZE, String.valueOf(input)).apply();
152 | settings.edit().putString(Settings.GAME_SIZE, String.valueOf(0)).apply();
153 | String boardSize = settings.getString(Settings.CUSTOM_GAME_SIZE, Integer.toString(getResources().getInteger(R.integer.DEFAULT_BOARD_SIZE)));
154 | gridPref.setSummary(String.format(getString(R.string.preferences_summary_game_size), boardSize, boardSize));
155 | }
156 | }).setNegativeButton(getString(R.string.cancel), null).show();
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/PreferencesActivity.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.app.AlertDialog;
4 | import android.content.Context;
5 | import android.content.SharedPreferences;
6 | import android.os.Bundle;
7 | import android.preference.Preference;
8 | import android.preference.Preference.OnPreferenceChangeListener;
9 | import android.preference.Preference.OnPreferenceClickListener;
10 | import android.preference.PreferenceActivity;
11 | import android.text.InputType;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.widget.AdapterView;
15 | import android.widget.AdapterView.OnItemSelectedListener;
16 | import android.widget.EditText;
17 | import android.widget.Spinner;
18 | import android.widget.TextView;
19 |
20 | import com.xlythe.hex.fragment.PreferencesFragment;
21 |
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.Nullable;
24 |
25 | /**
26 | * @author Will Harmon
27 | **/
28 | public class PreferencesActivity extends PreferenceActivity {
29 | SharedPreferences settings;
30 | Preference gridPref;
31 | Preference timerPref;
32 |
33 | @Override
34 | public void onCreate(@Nullable Bundle savedInstanceState) {
35 | super.onCreate(savedInstanceState);
36 | setContentView(R.layout.preferences);
37 | TextView title = findViewById(R.id.title);
38 | title.setText(R.string.activity_title_preferences);
39 | if (savedInstanceState == null) {
40 | PreferencesFragment preferences = new PreferencesFragment();
41 | getFragmentManager().beginTransaction().add(R.id.content, preferences).commit();
42 | }
43 | }
44 |
45 | @Override
46 | public void finish() {
47 | super.finish();
48 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
49 | }
50 |
51 | @Override
52 | public void onResume() {
53 | super.onResume();
54 | setListeners();
55 | }
56 |
57 | private class DifficultyListener implements OnPreferenceChangeListener {
58 | @Override
59 | public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) {
60 | preference.setSummary(getResources().getStringArray(R.array.comDifficultyArray)[Integer.valueOf(newValue.toString())]);
61 | return true;
62 | }
63 | }
64 |
65 | private class GridListener implements OnPreferenceChangeListener {
66 | @Override
67 | public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) {
68 | if (newValue.toString().equals("0")) {
69 | // Custom value needed
70 | showInputDialog(getString(R.string.preferences_summary_custom_game_size));
71 | return false;
72 | } else {
73 | preference.setSummary(String.format(getString(R.string.preferences_summary_game_size), newValue, newValue));
74 | return true;
75 | }
76 | }
77 | }
78 |
79 | private class TimerListener implements OnPreferenceClickListener {
80 | @Override
81 | public boolean onPreferenceClick(Preference pref) {
82 | LayoutInflater inflater = (LayoutInflater) PreferencesActivity.this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
83 | View dialoglayout = inflater.inflate(R.layout.preferences_timer, null);
84 | final Spinner timerType = dialoglayout.findViewById(R.id.timerType);
85 | final EditText timer = dialoglayout.findViewById(R.id.timer);
86 | timer.setText(settings.getString(Settings.TIMER, Integer.toString(getResources().getInteger(R.integer.DEFAULT_TIMER_TIME))));
87 | timerType.setSelection(Integer.valueOf(settings.getString(Settings.TIMER_TYPE, Integer.toString(getResources().getInteger(R.integer.DEFAULT_TIMER_TYPE)))));
88 | timerType.setOnItemSelectedListener(new OnItemSelectedListener() {
89 | @Override
90 | public void onItemSelected(AdapterView> adapterView, View view, int arg2, long arg3) {
91 | if (arg2 > 0) {
92 | timer.setVisibility(View.VISIBLE);
93 | } else {
94 | timer.setVisibility(View.GONE);
95 | }
96 | }
97 |
98 | @Override
99 | public void onNothingSelected(AdapterView> arg0) {
100 | timer.setVisibility(View.GONE);
101 | }
102 | });
103 | AlertDialog.Builder builder = new AlertDialog.Builder(PreferencesActivity.this);
104 | builder.setView(dialoglayout).setPositiveButton(getString(R.string.okay), (dialog, which) -> {
105 | String timerTime = timer.getText().toString();
106 | if (timerTime.isEmpty()) timerTime = "0";
107 | settings.edit()
108 | .putString(Settings.TIMER_TYPE, getResources().getStringArray(R.array.timerTypeValues)[timerType.getSelectedItemPosition()])
109 | .putString(Settings.TIMER, timerTime)
110 | .apply();
111 | }).setNegativeButton(getString(R.string.cancel), null).show();
112 | return true;
113 | }
114 | }
115 |
116 | private void setListeners() {
117 | // Allow for custom grid sizes
118 | gridPref = findPreference(Settings.GAME_SIZE);
119 | if (gridPref != null) {
120 | String boardSize = String.valueOf(Settings.getGridSize(this));
121 | gridPref.setSummary(String.format(getString(R.string.preferences_summary_game_size), boardSize, boardSize));
122 | gridPref.setOnPreferenceChangeListener(new GridListener());
123 | }
124 |
125 | // Give that custom popup for timers
126 | timerPref = findPreference(Settings.TIMER_OPTIONS);
127 | if (timerPref != null) {
128 | timerPref.setOnPreferenceClickListener(new TimerListener());
129 | }
130 |
131 | Preference comDifficultyPref = findPreference(Settings.DIFFICULTY);
132 | if (comDifficultyPref != null) {
133 | comDifficultyPref.setOnPreferenceChangeListener(new DifficultyListener());
134 | comDifficultyPref.setSummary(getResources().getStringArray(R.array.comDifficultyArray)[Settings.getComputerDifficulty(this)]);
135 | }
136 | }
137 |
138 | /**
139 | * Popup for custom grid sizes
140 | */
141 | private void showInputDialog(String message) {
142 | final EditText editText = new EditText(this);
143 | editText.setInputType(InputType.TYPE_CLASS_NUMBER);
144 | AlertDialog.Builder builder = new AlertDialog.Builder(PreferencesActivity.this);
145 | builder.setTitle(message).setView(editText).setPositiveButton(getString(R.string.okay), (dialog, which) -> {
146 | if (!editText.getText().toString().equals("")) {
147 | int input = Integer.decode(editText.getText().toString());
148 | if (input > Settings.MAX_BOARD_SIZE) {
149 | input = Settings.MAX_BOARD_SIZE;
150 | } else if (input < Settings.MIN_BOARD_SIZE) {
151 | input = Settings.MIN_BOARD_SIZE;
152 | }
153 | settings.edit()
154 | .putString(Settings.CUSTOM_GAME_SIZE, String.valueOf(input))
155 | .putString(Settings.GAME_SIZE, String.valueOf(0))
156 | .apply();
157 | String boardSize = settings.getString(Settings.CUSTOM_GAME_SIZE, Integer.toString(getResources().getInteger(R.integer.DEFAULT_BOARD_SIZE)));
158 | gridPref.setSummary(String.format(getString(R.string.preferences_summary_game_size), boardSize, boardSize));
159 | }
160 | }).setNegativeButton(getString(R.string.cancel), null).show();
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.Manifest;
4 | import android.os.Build;
5 | import android.os.Bundle;
6 | import android.util.Log;
7 |
8 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
9 | import com.xlythe.hex.compat.Game;
10 | import com.xlythe.hex.fragment.GameFragment;
11 | import com.xlythe.hex.fragment.GameSelectionFragment;
12 | import com.xlythe.hex.fragment.HistoryFragment;
13 | import com.xlythe.hex.fragment.InstructionsFragment;
14 | import com.xlythe.hex.fragment.MainFragment;
15 | import com.xlythe.hex.fragment.OnlineSelectionFragment;
16 |
17 | import androidx.annotation.NonNull;
18 | import androidx.annotation.Nullable;
19 | import androidx.fragment.app.Fragment;
20 | import androidx.fragment.app.FragmentManager;
21 |
22 | import static com.xlythe.hex.Settings.TAG;
23 | import static com.xlythe.hex.PermissionUtils.hasPermissions;
24 |
25 | /**
26 | * @author Will Harmon
27 | **/
28 | public class MainActivity extends NetActivity {
29 | private static final String[] REQUIRED_PERMISSIONS = new String[] {
30 | Manifest.permission.INTERNET,
31 | Manifest.permission.ACCESS_NETWORK_STATE
32 | };
33 |
34 | private static final int REQUEST_CODE_REQUIRED_PERMISSIONS = 3;
35 |
36 | // Play variables
37 | private boolean mOpenAchievements = false;
38 | private boolean mOpenOnlineSelectionFragment = false;
39 |
40 | // Fragments
41 | private MainFragment mMainFragment;
42 | private GameFragment mGameFragment;
43 | private GameSelectionFragment mGameSelectionFragment;
44 | private HistoryFragment mHistoryFragment;
45 | private InstructionsFragment mInstructionsFragment;
46 | private OnlineSelectionFragment mOnlineSelectionFragment;
47 |
48 | /**
49 | * Called when the activity is first created.
50 | */
51 | @Override
52 | public void onCreate(@Nullable Bundle savedInstanceState) {
53 | super.onCreate(savedInstanceState);
54 | setContentView(R.layout.activity_main);
55 |
56 | if (savedInstanceState == null) {
57 | mMainFragment = new MainFragment();
58 | mMainFragment.setInitialRotation(-120f);
59 | mMainFragment.setInitialSpin(50f);
60 | getSupportFragmentManager().beginTransaction().add(R.id.content, mMainFragment).commit();
61 | } else {
62 | Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.content);
63 | invalidateFragmentState(fragment);
64 | }
65 |
66 | if (!hasPermissions(this, REQUIRED_PERMISSIONS)) {
67 | requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
68 | }
69 | }
70 |
71 | @Override
72 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
73 | if (requestCode == REQUEST_CODE_REQUIRED_PERMISSIONS) {
74 | if (!hasPermissions(this, REQUIRED_PERMISSIONS)) {
75 | finish();
76 | }
77 | }
78 | }
79 |
80 | public void returnHome() {
81 | if (mMainFragment == null) mMainFragment = new MainFragment();
82 | getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
83 | }
84 |
85 | @Override
86 | public void onSignInSucceeded(GoogleSignInAccount googleSignInAccount) {
87 | super.onSignInSucceeded(googleSignInAccount);
88 |
89 | if (mMainFragment != null) mMainFragment.setSignedIn(true);
90 |
91 | if (mOpenAchievements) {
92 | mOpenAchievements = false;
93 | openAchievements();
94 | }
95 |
96 | if (mOpenOnlineSelectionFragment) {
97 | mOpenOnlineSelectionFragment = false;
98 | swapFragment(new OnlineSelectionFragment());
99 | }
100 | }
101 |
102 | @Override
103 | public void onSignInFailed(Throwable throwable) {
104 | super.onSignInFailed(throwable);
105 | if (mMainFragment != null) mMainFragment.setSignedIn(false);
106 | }
107 |
108 | public void swapFragment(Fragment newFragment) {
109 | // Bugfix for starting a new game on top of an existing game. Remove the existing fragment from
110 | // the backstack by popping it.
111 | if (getSupportFragmentManager().findFragmentById(R.id.content).getClass() == newFragment.getClass()) {
112 | try {
113 | getSupportFragmentManager().popBackStack();
114 | } catch (IllegalStateException e) {
115 | e.printStackTrace();
116 | }
117 | }
118 |
119 | invalidateFragmentState(newFragment);
120 | getSupportFragmentManager()
121 | .beginTransaction()
122 | .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out)
123 | .replace(R.id.content, newFragment)
124 | .addToBackStack(null)
125 | .commitAllowingStateLoss();
126 | }
127 |
128 | private void invalidateFragmentState(Fragment fragment) {
129 | if (fragment != null) {
130 | if (fragment instanceof MainFragment) {
131 | mMainFragment = (MainFragment) fragment;
132 | } else if (fragment instanceof GameFragment) {
133 | mGameFragment = (GameFragment) fragment;
134 | } else if (fragment instanceof GameSelectionFragment) {
135 | mGameSelectionFragment = (GameSelectionFragment) fragment;
136 | } else if (fragment instanceof HistoryFragment) {
137 | mHistoryFragment = (HistoryFragment) fragment;
138 | } else if (fragment instanceof InstructionsFragment) {
139 | mInstructionsFragment = (InstructionsFragment) fragment;
140 | } else if (fragment instanceof OnlineSelectionFragment) {
141 | mOnlineSelectionFragment = (OnlineSelectionFragment) fragment;
142 | } else {
143 | Log.w(TAG, "Unknown fragment " + fragment + " attached.");
144 | }
145 | }
146 | }
147 |
148 | public void setOpenAchievements(boolean open) {
149 | this.mOpenAchievements = open;
150 | }
151 |
152 | public void setOpenOnlineSelectionFragment(boolean open) {
153 | this.mOpenOnlineSelectionFragment = open;
154 | }
155 |
156 | @Override
157 | public void switchToGame(@NonNull Game game) {
158 | Bundle b = new Bundle();
159 | b.putBoolean(GameFragment.PRELOADED_GAME, true);
160 |
161 | mGameFragment = new GameFragment();
162 | mGameFragment.setGame(game);
163 | mGameFragment.setPlayer1Type(game.getPlayer1().getType());
164 | mGameFragment.setPlayer2Type(game.getPlayer2().getType());
165 | mGameFragment.setArguments(b);
166 |
167 | swapFragment(mGameFragment);
168 | }
169 |
170 | public static class Stat {
171 | private long timePlayed;
172 | private long gamesWon;
173 | private long gamesPlayed;
174 | private int donationRank;
175 |
176 | public long getTimePlayed() {
177 | return timePlayed;
178 | }
179 |
180 | public void setTimePlayed(long timePlayed) {
181 | this.timePlayed = timePlayed;
182 | }
183 |
184 | public long getGamesWon() {
185 | return gamesWon;
186 | }
187 |
188 | public void setGamesWon(long gamesWon) {
189 | this.gamesWon = gamesWon;
190 | }
191 |
192 | public long getGamesPlayed() {
193 | return gamesPlayed;
194 | }
195 |
196 | public void setGamesPlayed(long gamesPlayed) {
197 | this.gamesPlayed = gamesPlayed;
198 | }
199 |
200 | public int getDonationRank() {
201 | return donationRank;
202 | }
203 |
204 | public void setDonationRank(int donationRank) {
205 | this.donationRank = donationRank;
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/fragment/MainFragment.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.fragment;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.util.TypedValue;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 | import android.widget.Button;
10 | import android.widget.TextView;
11 |
12 | import com.google.android.gms.common.SignInButton;
13 | import com.xlythe.hex.PreferencesActivity;
14 | import com.xlythe.hex.R;
15 | import com.xlythe.hex.Settings;
16 | import com.xlythe.hex.Stats;
17 | import com.xlythe.hex.view.DonateDialog;
18 | import com.xlythe.hex.view.HexagonLayout;
19 |
20 | import androidx.annotation.DrawableRes;
21 | import androidx.annotation.NonNull;
22 | import androidx.annotation.Nullable;
23 |
24 | /**
25 | * @author Will Harmon
26 | **/
27 | public class MainFragment extends HexFragment {
28 | // Hexagon variables
29 | private HexagonLayout mHexagonLayout;
30 | private float mInitialSpin;
31 | private float mInitialRotation;
32 |
33 | // Stat variables
34 | private TextView mTitleTextView;
35 | private TextView mTimePlayedTextView;
36 | private TextView mGamesPlayedTextView;
37 | private TextView mGamesWonTextView;
38 |
39 | // Play variables
40 | private SignInButton mSignInButton;
41 | private Button mSignOutButton;
42 |
43 | /**
44 | * Called when the activity is first created.
45 | */
46 | @Override
47 | public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState) {
48 | super.onCreateView(inflater, container, savedInstanceState);
49 | keepScreenOn(false);
50 | View view = inflater.inflate(R.layout.fragment_main, container, false);
51 |
52 | mHexagonLayout = view.findViewById(R.id.hexagonButtons);
53 | HexagonLayout.Button settingsButton = mHexagonLayout.getButtons()[0];
54 | HexagonLayout.Button donateButton = mHexagonLayout.getButtons()[1];
55 | HexagonLayout.Button historyButton = mHexagonLayout.getButtons()[2];
56 | HexagonLayout.Button instructionsButton = mHexagonLayout.getButtons()[3];
57 | HexagonLayout.Button achievementsButton = mHexagonLayout.getButtons()[4];
58 | HexagonLayout.Button playButton = mHexagonLayout.getButtons()[5];
59 |
60 | mHexagonLayout.setTopMargin(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()));
61 | mHexagonLayout.setText(R.string.app_name);
62 | mHexagonLayout.setInitialRotation(mInitialRotation);
63 | mInitialRotation = 0f;
64 | mHexagonLayout.setInitialSpin(mInitialSpin);
65 | mInitialSpin = 0f;
66 |
67 | mTitleTextView = view.findViewById(R.id.title);
68 | mTimePlayedTextView = view.findViewById(R.id.timePlayed);
69 | mGamesPlayedTextView = view.findViewById(R.id.gamesPlayed);
70 | mGamesWonTextView = view.findViewById(R.id.gamesWon);
71 |
72 | mSignInButton = view.findViewById(R.id.signInButton);
73 | mSignOutButton = view.findViewById(R.id.signOutButton);
74 |
75 | settingsButton.setText(R.string.main_button_settings);
76 | settingsButton.setColor(getResources().getColor(R.color.main_settings));
77 | settingsButton.setDrawableResource(R.drawable.settings);
78 | settingsButton.setOnClickListener(() -> {
79 | startActivity(new Intent(getMainActivity(), PreferencesActivity.class));
80 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
81 | });
82 |
83 | donateButton.setText(R.string.main_button_donate);
84 | donateButton.setColor(getResources().getColor(R.color.main_donate));
85 | donateButton.setDrawableResource(R.drawable.store);
86 | donateButton.setOnClickListener(() -> new DonateDialog.Builder(getMainActivity()).show());
87 |
88 | historyButton.setText(R.string.main_button_history);
89 | historyButton.setColor(getResources().getColor(R.color.main_history));
90 | historyButton.setDrawableResource(R.drawable.history);
91 | historyButton.setOnClickListener(() -> swapFragment(new HistoryFragment()));
92 |
93 | instructionsButton.setText(R.string.main_button_instructions);
94 | instructionsButton.setColor(getResources().getColor(R.color.main_instructions));
95 | instructionsButton.setDrawableResource(R.drawable.howtoplay);
96 | instructionsButton.setOnClickListener(() -> swapFragment(new InstructionsFragment()));
97 |
98 | achievementsButton.setText(R.string.main_button_achievements);
99 | achievementsButton.setColor(getResources().getColor(R.color.main_achievements));
100 | achievementsButton.setDrawableResource(R.drawable.achievements);
101 | achievementsButton.setOnClickListener(() -> {
102 | if (isSignedIn()) {
103 | openAchievements();
104 | } else {
105 | getMainActivity().setOpenAchievements(true);
106 | signIn();
107 | }
108 | });
109 |
110 | playButton.setText(R.string.main_button_play);
111 | playButton.setColor(getResources().getColor(R.color.main_play));
112 | playButton.setDrawableResource(R.drawable.play);
113 | playButton.setOnClickListener(() -> swapFragment(new GameSelectionFragment()));
114 |
115 | mSignInButton.setOnClickListener(v -> signIn());
116 |
117 | mSignOutButton.setOnClickListener(v -> {
118 | signOut();
119 | refreshPlayerInformation();
120 | });
121 | refreshPlayerInformation();
122 |
123 | return view;
124 | }
125 |
126 | @Override
127 | public void onResume() {
128 | super.onResume();
129 |
130 | showStats();
131 | showDonationStar();
132 | }
133 |
134 | private void showStats() {
135 | long timePlayedInMillis = Stats.getTimePlayed(getMainActivity());
136 | long timePlayedInHours = timePlayedInMillis / (1000 * 60 * 60);
137 | long timePlayedInMintues = (timePlayedInMillis - timePlayedInHours * (1000 * 60 * 60)) / (1000 * 60);
138 | long timePlayedInSeconds = (timePlayedInMillis - timePlayedInHours * (1000 * 60 * 60) - timePlayedInMintues * (1000 * 60)) / (1000);
139 | mTimePlayedTextView.setText(getString(R.string.main_stats_time_played, timePlayedInHours, timePlayedInMintues, timePlayedInSeconds));
140 | mGamesPlayedTextView.setText(getString(R.string.main_stats_games_played, Stats.getGamesPlayed(getMainActivity())));
141 | mGamesWonTextView.setText(getString(R.string.main_stats_games_won, Stats.getGamesWon(getMainActivity())));
142 | }
143 |
144 | private void refreshPlayerInformation() {
145 | try {
146 | // Network is async, no promise that we won't lose connectivity
147 | if (getMainActivity() == null) return;
148 | if (mSignOutButton != null)
149 | mSignOutButton.setVisibility(isSignedIn() ? View.VISIBLE : View.GONE);
150 | if (mSignInButton != null)
151 | mSignInButton.setVisibility(isSignedIn() ? View.GONE : View.VISIBLE);
152 | if (mTitleTextView != null)
153 | mTitleTextView.setText(getString(R.string.main_title,
154 | Settings.getPlayer1Name(getMainActivity(), getGoogleSignInAccount())));
155 | if (mHexagonLayout != null) mHexagonLayout.invalidate();
156 | if (mTimePlayedTextView != null && mGamesPlayedTextView != null && mGamesWonTextView != null)
157 | showStats();
158 | } catch (IllegalStateException e) {
159 | e.printStackTrace();
160 | }
161 | }
162 |
163 | public void setSignedIn(boolean isSignedIn) {
164 | refreshPlayerInformation();
165 | }
166 |
167 | public void setInitialSpin(float initialSpin) {
168 | mInitialSpin = initialSpin;
169 | }
170 |
171 | public void setInitialRotation(float initialRotation) {
172 | mInitialRotation = initialRotation;
173 | }
174 |
175 | private void showDonationStar() {
176 | int donationAmount = Stats.getDonationRank(getMainActivity());
177 | @DrawableRes int resource = R.drawable.donate_hollow;
178 |
179 | if (donationAmount >= 5) {
180 | resource = R.drawable.donate_gold;
181 | } else if (donationAmount >= 3) {
182 | resource = R.drawable.donate_silver;
183 | } else if (donationAmount >= 1) {
184 | resource = R.drawable.donate_bronze;
185 | }
186 |
187 | mTitleTextView.setCompoundDrawablesWithIntrinsicBounds(resource, 0, 0, 0);
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/compat/NetworkPlayer.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.compat;
2 |
3 | import android.util.Log;
4 |
5 | import com.google.android.gms.games.TurnBasedMultiplayerClient;
6 | import com.google.android.gms.games.multiplayer.ParticipantResult;
7 | import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatch;
8 | import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatchUpdateCallback;
9 | import com.google.android.gms.tasks.Tasks;
10 | import com.hex.core.Game;
11 | import com.hex.core.GameAction;
12 | import com.hex.core.Move;
13 | import com.hex.core.MoveList;
14 | import com.hex.core.Player;
15 | import com.hex.core.PlayingEntity;
16 | import com.hex.core.Point;
17 |
18 | import java.io.Serializable;
19 | import java.util.concurrent.LinkedBlockingQueue;
20 |
21 | import androidx.annotation.ColorInt;
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.Nullable;
24 |
25 | import static com.xlythe.hex.Settings.TAG;
26 |
27 | /** A NetworkPlayer that relies on Google Play Games. */
28 | public class NetworkPlayer implements PlayingEntity {
29 | private static final Point END_MOVE = new Point(-1, -1);
30 |
31 | // Our local participant id. Looks like p_1 or p_2. Never null because we only hear about the
32 | // match once we're inside it.
33 | private final String localParticipantId;
34 |
35 | // The remote participant id. Also looks like p_1 or p_2. This may be null for the first turn
36 | // of a automatched game. Otherwise, once there are 2 players in the room, it's no longer null.
37 | // Note that a Google Play Games Player object is not necessarily available even if the remote
38 | // participant id is.
39 | @Nullable
40 | private String remoteParticipantId;
41 |
42 | // The most recent match state given from the server. By listening to this, we can know when
43 | // players join the game, make a move, or request a rematch.
44 | private TurnBasedMatch match;
45 |
46 | // A callback for handling rematches. Shows the user a dialog, starts a new game, etc.
47 | // Things that are out of scope for a PlayingEntity inside a game.
48 | private final Rematcher rematcher;
49 |
50 | // The Play Services client, that allows us to listen to and send updates to the remote device.
51 | private final TurnBasedMultiplayerClient turnBasedMultiplayerClient;
52 |
53 | // The team we're on. Either 1 or 2.
54 | private final int team;
55 | // The name of this player.
56 | private String name;
57 | // The color of this player.
58 | @ColorInt
59 | private int color;
60 |
61 | // How much time this player has left to make their move.
62 | private long timeLeft;
63 |
64 | // If true, this player has forfeited the match.
65 | private boolean hasForfeited = false;
66 |
67 | // Update this queue with the current move when the remote side has reported where they want to place their piece.
68 | private final LinkedBlockingQueue currentMove = new LinkedBlockingQueue<>();
69 |
70 | // A listener that's called whenever the TurnBasedMatch is updated.
71 | private final TurnBasedMatchUpdateCallback turnBasedMatchUpdateCallback = new TurnBasedMatchUpdateCallback() {
72 | @Override
73 | public void onTurnBasedMatchReceived(@NonNull TurnBasedMatch turnBasedMatch) {
74 | // Ignore matches without any data. That means no moves have been made yet.
75 | if (turnBasedMatch.getData() == null) {
76 | Log.w(TAG, "Ignoring match because there was no data inside. Expected game state.");
77 | return;
78 | }
79 |
80 | // Ignore data from matches other than the one we're currently in.
81 | if (!match.getMatchId().equals(turnBasedMatch.getMatchId())) {
82 | Log.w(TAG, String.format("Ignoring match because it's for a different match id. Got %s when expecting %s.", turnBasedMatch.getMatchId(), match.getMatchId()));
83 | return;
84 | }
85 |
86 | // Invalidate our match state.
87 | setMatch(turnBasedMatch);
88 |
89 | // Check to see if the remote side has challenged us to a rematch. Note that this means the game is over.
90 | if (turnBasedMatch.getRematchId() != null) {
91 | Log.d(TAG, "A rematch was requested. Players can use the Play Games notification to join it.");
92 | }
93 |
94 | // Load the game state from the remote side and attempt to make the same move on this side.
95 | Game game = Game.load(new String(turnBasedMatch.getData()));
96 | Move lastMove = game.getMoveList().getMove();
97 | if (lastMove.getTeam() != team) {
98 | Log.w(TAG, "Ignoring match update because it wasn't my move");
99 | return;
100 | }
101 |
102 | currentMove.add(new Point(lastMove.getX(), lastMove.getY()));
103 | Log.d(TAG, String.format("Received (%d,%d) from the remote side.", lastMove.getX(), lastMove.getY()));
104 | }
105 |
106 | @Override
107 | public void onTurnBasedMatchRemoved(@NonNull String matchId) {
108 | // Ignore data from matches other than the one we're currently in.
109 | if (!match.getMatchId().equals(matchId)) {
110 | Log.w(TAG, String.format("Ignoring match because it's for a different match id. Got %s when expecting %s.", matchId, match.getMatchId()));
111 | return;
112 | }
113 |
114 | Log.w(TAG, String.format("Match %s has been removed. Considering it forfeited.", matchId));
115 | hasForfeited = true;
116 | endMove();
117 | }
118 | };
119 |
120 | public NetworkPlayer(
121 | int team,
122 | String localParticipantId,
123 | @Nullable String remoteParticipantId,
124 | TurnBasedMatch match,
125 | Rematcher rematcher,
126 | TurnBasedMultiplayerClient turnBasedMultiplayerClient) {
127 | this.team = team;
128 | this.localParticipantId = localParticipantId;
129 | this.remoteParticipantId = remoteParticipantId;
130 | this.match = match;
131 | this.rematcher = rematcher;
132 | this.turnBasedMultiplayerClient = turnBasedMultiplayerClient;
133 | }
134 |
135 | private void setMatch(TurnBasedMatch match) {
136 | this.match = match;
137 |
138 | // Attempt to find the remote participant. Can be null in automatch games.
139 | if (remoteParticipantId == null) {
140 | for (String participantId : match.getParticipantIds()) {
141 | if (localParticipantId.equals(participantId)) {
142 | continue;
143 | }
144 |
145 | remoteParticipantId = participantId;
146 | Log.d(TAG, "Remote participant id updated to " + participantId);
147 | break;
148 | }
149 | }
150 | }
151 |
152 | /** The game has now started. State can be initialized here. */
153 | @Override
154 | public void startGame() {
155 | turnBasedMultiplayerClient.registerTurnBasedMatchUpdateCallback(turnBasedMatchUpdateCallback);
156 | }
157 |
158 | /**
159 | * It's our turn to make a move. We should block until we've determined which move to make.
160 | * When we've decided on our move, call GameAction.makeMove(PlayingEntity, Point, Game).
161 | * If GameAction.makeMove is not called when this method resolves, the player's turn is
162 | * considered skipped.
163 | */
164 | @Override
165 | public void getPlayerTurn(Game game) {
166 | // Ignore replays. We'll wait for the replay to finish before continuing to make moves.
167 | if (game.replayRunning) {
168 | return;
169 | }
170 |
171 | // As long as this wasn't the very first move, send the local move to the remote side before waiting for their response.
172 | MoveList moveList = game.getMoveList();
173 | if (moveList.size() > 0 && match.getTurnStatus() == TurnBasedMatch.MATCH_TURN_STATUS_MY_TURN) {
174 | try {
175 | match = Tasks.await(turnBasedMultiplayerClient.takeTurn(match.getMatchId(), game.save().getBytes(), remoteParticipantId));
176 | Log.d(TAG, String.format("Successfully told the remote side my move (%d,%d).", moveList.getMove().getX(), moveList.getMove().getY()));
177 | } catch (Exception e) {
178 | Log.e(TAG, "Failed to tell the remote side my move", e);
179 | }
180 | }
181 |
182 | currentMove.clear();
183 | while(true) {
184 | Point p;
185 | try {
186 | p = currentMove.take();
187 | } catch(InterruptedException e) {
188 | Thread.currentThread().interrupt();
189 | p = END_MOVE;
190 | }
191 |
192 | if (p.equals(END_MOVE)) {
193 | break;
194 | }
195 |
196 | if (GameAction.makeMove(this, p, game)) {
197 | break;
198 | }
199 | }
200 | }
201 |
202 | /** The player has passed their turn. This should interrupt getPlayerTurn(). */
203 | @Override
204 | public void endMove() {
205 | currentMove.add(END_MOVE);
206 | }
207 |
208 | /** The game has ended. Clean up state. */
209 | @Override
210 | public void quit() {
211 | endMove();
212 | turnBasedMultiplayerClient.unregisterTurnBasedMatchUpdateCallback(turnBasedMatchUpdateCallback);
213 | }
214 |
215 | /** Return true if the player has forfeited the match. */
216 | @Override
217 | public boolean giveUp() {
218 | return hasForfeited;
219 | }
220 |
221 | /** Called if this player has won the game. */
222 | @Override
223 | public void win() {
224 | // The remote side will report this match as finished.
225 | }
226 |
227 | /** Called if this player has lost the game. */
228 | @Override
229 | public void lose(Game game) {
230 | turnBasedMultiplayerClient.finishMatch(
231 | match.getMatchId(),
232 | game.save().getBytes(),
233 | new ParticipantResult(localParticipantId, ParticipantResult.MATCH_RESULT_WIN, 1),
234 | new ParticipantResult(remoteParticipantId, ParticipantResult.MATCH_RESULT_LOSS, 2))
235 | .addOnSuccessListener(match -> Log.d(TAG, "Informed the remote side that they lost the match."));
236 | }
237 |
238 | /** True if rematches are allowed in this game. */
239 | @Override
240 | public boolean supportsNewgame() {
241 | return false;
242 | }
243 |
244 | /** 'New Game' was called. */
245 | @Override
246 | public void newgameCalled() {
247 | rematcher.requestRematch(match.getMatchId());
248 | }
249 |
250 | /** True if undo is allowed in this game. */
251 | @Override
252 | public boolean supportsUndo(Game game) {
253 | return false;
254 | }
255 |
256 | /** 'Undo' was called. Update local state. */
257 | @Override
258 | public void undoCalled() {}
259 |
260 | @Override
261 | public boolean supportsSave() {
262 | return false;
263 | }
264 |
265 | @Override
266 | public Serializable getSaveState() {
267 | return null;
268 | }
269 |
270 | @Override
271 | public void setSaveState(Serializable state) {}
272 |
273 | /** Sets the player's name. Only allowed to be called once. */
274 | @Override
275 | public synchronized void setName(String name) {
276 | this.name = name;
277 | }
278 |
279 | /** Returns the player's name. */
280 | @Override
281 | public synchronized String getName() {
282 | return name;
283 | }
284 |
285 | /** Sets the player's color. */
286 | @Override
287 | public synchronized void setColor(int color) {
288 | this.color = color;
289 | }
290 |
291 | /** Returns the player's color. */
292 | @Override
293 | public synchronized int getColor() {
294 | return color;
295 | }
296 |
297 | @Override
298 | public synchronized void setTime(long time) {
299 | this.timeLeft = time;
300 | }
301 |
302 | @Override
303 | public synchronized long getTime() {
304 | return timeLeft;
305 | }
306 |
307 | @Override
308 | public byte getTeam() {
309 | return (byte) team;
310 | }
311 |
312 | @Override
313 | public Player getType() {
314 | return Player.Net;
315 | }
316 |
317 | public interface Rematcher {
318 | // The local side would like to request a rematch to the remote side.
319 | void requestRematch(String matchId);
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/NetActivity.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.util.Log;
8 |
9 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
10 | import com.google.android.gms.games.Games;
11 | import com.google.android.gms.games.PlayerCompat;
12 | import com.google.android.gms.games.multiplayer.Invitation;
13 | import com.google.android.gms.games.multiplayer.Multiplayer;
14 | import com.google.android.gms.games.multiplayer.realtime.RoomConfig;
15 | import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatch;
16 | import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatchConfig;
17 | import com.hex.core.PlayerObject;
18 | import com.hex.core.PlayingEntity;
19 | import com.xlythe.hex.compat.Game;
20 | import com.xlythe.hex.compat.GameOptions;
21 | import com.xlythe.hex.compat.NetworkPlayer;
22 |
23 | import java.util.ArrayList;
24 | import java.util.List;
25 |
26 | import androidx.annotation.NonNull;
27 | import androidx.annotation.Nullable;
28 |
29 | import static com.xlythe.hex.Settings.TAG;
30 |
31 | public abstract class NetActivity extends BaseGameActivity {
32 | private final static int MIN_OPPONENTS = 1, MAX_OPPONENTS = 1;
33 |
34 | private static final int REQUEST_CODE_SELECT_OPPONENT = 10001;
35 | private static final int REQUEST_CODE_INBOX = 10002;
36 | private static final int REQUEST_CODE_ACHIEVEMENTS = 10003;
37 |
38 | // The local player id for ourselves. Null if not signed in.
39 | private String mPlayerId;
40 |
41 | // Switches to a new game for a rematch.
42 | private final NetworkPlayer.Rematcher rematcher = matchId -> {
43 | getTurnBasedMultiplayerClient().rematch(matchId)
44 | .addOnSuccessListener(this::startGame)
45 | .addOnFailureListener(e -> {
46 | Log.e(TAG, "Failed to request rematch", e);
47 | checkInvites();
48 | });
49 | };
50 |
51 | public abstract void switchToGame(Game game);
52 |
53 | @Override
54 | public void onSignInSucceeded(GoogleSignInAccount googleSignInAccount) {
55 | super.onSignInSucceeded(googleSignInAccount);
56 | getPlayersClient().getCurrentPlayer().addOnSuccessListener(player -> mPlayerId = player.getPlayerId());
57 | getGamesClient().getActivationHint().addOnSuccessListener(bundle -> {
58 | if (bundle == null) {
59 | return;
60 | }
61 |
62 | if (bundle.containsKey(Multiplayer.EXTRA_INVITATION)) {
63 | Invitation invitation = bundle.getParcelable(Multiplayer.EXTRA_INVITATION);
64 | if (invitation.getInvitationType() == Invitation.INVITATION_TYPE_TURN_BASED) {
65 | getTurnBasedMultiplayerClient().acceptInvitation(invitation.getInvitationId()).addOnSuccessListener(this::startGame);
66 | return;
67 | }
68 | }
69 |
70 | if (bundle.containsKey(Multiplayer.EXTRA_TURN_BASED_MATCH)) {
71 | TurnBasedMatch match = bundle.getParcelable(Multiplayer.EXTRA_TURN_BASED_MATCH);
72 | startGame(match);
73 | return;
74 | }
75 | });
76 | }
77 |
78 | @Override
79 | public void startQuickGame() {
80 | Bundle autoMatchCriteria = RoomConfig.createAutoMatchCriteria(MIN_OPPONENTS, MAX_OPPONENTS, 0);
81 |
82 | TurnBasedMatchConfig config = TurnBasedMatchConfig.builder()
83 | .setAutoMatchCriteria(autoMatchCriteria)
84 | .build();
85 |
86 | // Start the match
87 | getTurnBasedMultiplayerClient().createMatch(config).addOnSuccessListener(this::startGame);
88 | }
89 |
90 | @Override
91 | public void inviteFriends() {
92 | getTurnBasedMultiplayerClient().getSelectOpponentsIntent(MIN_OPPONENTS, MAX_OPPONENTS, true).addOnSuccessListener(intent -> {
93 | startActivityForResult(intent, REQUEST_CODE_SELECT_OPPONENT);
94 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
95 | });
96 | }
97 |
98 | @Override
99 | public void checkInvites() {
100 | getTurnBasedMultiplayerClient().getInboxIntent().addOnSuccessListener(intent -> {
101 | startActivityForResult(intent, REQUEST_CODE_INBOX);
102 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
103 | });
104 | }
105 |
106 | @Override
107 | public void openAchievements() {
108 | getAchievementsClient().getAchievementsIntent().addOnSuccessListener(intent -> {
109 | // Note: Must call startActivityForResult or the activity won't launch.
110 | startActivityForResult(intent, REQUEST_CODE_ACHIEVEMENTS);
111 | overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
112 | });
113 | }
114 |
115 | @Override
116 | protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
117 | switch (requestCode) {
118 | case REQUEST_CODE_SELECT_OPPONENT:
119 | onOpponentSelected(resultCode, intent);
120 | break;
121 | case REQUEST_CODE_INBOX:
122 | onInboxSelected(resultCode, intent);
123 | break;
124 | default:
125 | super.onActivityResult(requestCode, resultCode, intent);
126 | break;
127 | }
128 | }
129 |
130 | private void onOpponentSelected(int resultCode, Intent intent) {
131 | if (resultCode != Activity.RESULT_OK) {
132 | Log.e(TAG, "Failed to select an opponent");
133 | return;
134 | }
135 |
136 | ArrayList participants = intent.getStringArrayListExtra(Games.EXTRA_PLAYER_IDS);
137 | TurnBasedMatchConfig config = TurnBasedMatchConfig.builder()
138 | .addInvitedPlayers(participants)
139 | .setAutoMatchCriteria(RoomConfig.createAutoMatchCriteria(MIN_OPPONENTS, MAX_OPPONENTS, 0))
140 | .build();
141 |
142 | // Start the game
143 | getTurnBasedMultiplayerClient().createMatch(config)
144 | .addOnSuccessListener(this::startGame)
145 | .addOnFailureListener(e -> Log.e(TAG, "Failed to create a match", e));
146 | }
147 |
148 | private void onInboxSelected(int resultCode, Intent intent) {
149 | if (resultCode != Activity.RESULT_OK) {
150 | Log.e(TAG, "Failed to select a match from the inbox");
151 | return;
152 | }
153 |
154 | TurnBasedMatch match = intent.getParcelableExtra(Multiplayer.EXTRA_TURN_BASED_MATCH);
155 | if (match == null) {
156 | Log.e(TAG, "A match was selected from the inbox, but the match was null");
157 | return;
158 | }
159 |
160 | startGame(match);
161 | }
162 |
163 | private void startGame(@NonNull TurnBasedMatch match) {
164 | PlayingEntity[] players = getPlayers(match);
165 |
166 | Game game;
167 | if (match.getData() != null) {
168 | game = Game.load(new String(match.getData()), players[0], players[1]);
169 | Log.d(TAG, String.format("Resuming match %s.", match.getMatchId()));
170 | } else {
171 | GameOptions gameOptions = new GameOptions.Builder()
172 | .setGridSize(Settings.getGridSize(this))
173 | .setSwapEnabled(Settings.getSwap(this))
174 | .setNoTimer()
175 | .build();
176 |
177 | game = new Game(gameOptions, players[0], players[1]);
178 | Log.d(TAG, String.format("New match %s.", match.getMatchId()));
179 | }
180 |
181 | // Set names / colors here. Note that loading a game from a remote side will flip the names,
182 | // so this must occur after Game.load.
183 | String localPlayerName = getLocalPlayerName(match, mPlayerId);
184 | String remotePlayerName = getRemotePlayerName(this, match, mPlayerId);
185 | if (isLocalPlayer1(match)) {
186 | players[0].setName(localPlayerName);
187 | players[1].setName(remotePlayerName);
188 | } else {
189 | players[0].setName(remotePlayerName);
190 | players[1].setName(localPlayerName);
191 | }
192 | players[0].setColor(getResources().getInteger(R.integer.DEFAULT_P1_COLOR));
193 | players[1].setColor(getResources().getInteger(R.integer.DEFAULT_P2_COLOR));
194 |
195 | Log.d(TAG, String.format("%s vs %s in match %s.", players[0].getName(), players[1].getName(), match.getMatchId()));
196 |
197 | switch (match.getTurnStatus()) {
198 | case TurnBasedMatch.MATCH_TURN_STATUS_MY_TURN:
199 | Log.d(TAG, "It's my turn");
200 | break;
201 | case TurnBasedMatch.MATCH_TURN_STATUS_THEIR_TURN:
202 | Log.d(TAG, "It's their turn");
203 | break;
204 | }
205 |
206 | switchToGame(game);
207 | }
208 |
209 | private PlayingEntity[] getPlayers(TurnBasedMatch match) {
210 | String localParticipantId = getLocalParticipantId(match, mPlayerId);
211 | String remoteParticipantId = getRemoteParticipantId(match, mPlayerId);
212 | Log.d(TAG, String.format("Local id %s, remote id %s in match %s.", localParticipantId, remoteParticipantId, match.getMatchId()));
213 |
214 | PlayingEntity[] players = new PlayingEntity[2];
215 | if (isLocalPlayer1(match)) {
216 | players[0] = new PlayerObject(1);
217 | players[1] = new NetworkPlayer(
218 | 2,
219 | localParticipantId,
220 | remoteParticipantId,
221 | match,
222 | rematcher,
223 | getTurnBasedMultiplayerClient());
224 | } else {
225 | players[0] = new NetworkPlayer(
226 | 1,
227 | localParticipantId,
228 | remoteParticipantId,
229 | match,
230 | rematcher,
231 | getTurnBasedMultiplayerClient());
232 | players[1] = new PlayerObject(2);
233 | }
234 |
235 | return players;
236 | }
237 |
238 | private static List getPlayerList(TurnBasedMatch match) {
239 | List players = new ArrayList<>(match.getParticipantIds().size());
240 | for (String participantId : match.getParticipantIds()) {
241 | @Nullable PlayerCompat player = match.getParticipant(participantId).getPlayer();
242 | if (player == null) {
243 | continue;
244 | }
245 |
246 | players.add(player);
247 | }
248 | return players;
249 | }
250 |
251 | private static String getLocalPlayerName(TurnBasedMatch match, String playerId) {
252 | return getShortName(getLocalPlayer(playerId, getPlayerList(match)));
253 | }
254 |
255 | private static String getRemotePlayerName(Context context, TurnBasedMatch match, String playerId) {
256 | @Nullable PlayerCompat remotePlayer = getRemotePlayer(playerId, getPlayerList(match));
257 | if (remotePlayer == null) {
258 | Log.d(TAG, "Unable to get remote player name. No player found.");
259 | return context.getString(R.string.player_automatch);
260 | }
261 | return getShortName(remotePlayer);
262 | }
263 |
264 | private static String getLocalParticipantId(TurnBasedMatch match, String playerId) {
265 | return match.getParticipantId(getLocalPlayer(playerId, getPlayerList(match)).getPlayerId());
266 | }
267 |
268 | // May return null in automatched games.
269 | @Nullable
270 | private static String getRemoteParticipantId(TurnBasedMatch match, String playerId) {
271 | // For automatch games, the remote player is null. Therefore, we need to look up our
272 | // participant id, and then find out who the remote participant is.
273 | String localParticipantId = getLocalParticipantId(match, playerId);
274 | for (String participantId : match.getParticipantIds()) {
275 | if (localParticipantId.equals(participantId)) {
276 | continue;
277 | }
278 |
279 | return participantId;
280 | }
281 |
282 | return null;
283 | }
284 |
285 | @NonNull
286 | private static PlayerCompat getLocalPlayer(String localPlayerId, List playerList) {
287 | for (PlayerCompat player : playerList) {
288 | if (player.getPlayerId().equals(localPlayerId)) {
289 | return player;
290 | }
291 | }
292 | throw new IllegalStateException(String.format("Local player %s was not found within the list of players: %s", localPlayerId, playerList));
293 | }
294 |
295 | @Nullable
296 | private static PlayerCompat getRemotePlayer(String localPlayerId, List playerList) {
297 | for (PlayerCompat player : playerList) {
298 | if (player.getPlayerId().equals(localPlayerId)) {
299 | continue;
300 | }
301 |
302 | return player;
303 | }
304 | return null;
305 | }
306 |
307 | private static String getShortName(PlayerCompat player) {
308 | String name = player.getDisplayName().split(" ")[0];
309 | if (name.length() > 10) {
310 | return name.substring(0, 10);
311 | }
312 | return name;
313 | }
314 |
315 | // Returns true if the local device is player 1.
316 | private static boolean isLocalPlayer1(TurnBasedMatch match) {
317 | // If there's no game state yet, then whoever's turn it is is player 1.
318 | if (match.getData() == null) {
319 | return isMyMove(match);
320 | }
321 |
322 | // Otherwise, if there is already game state, open up a local copy of the game to decide
323 | // if its player1's move or player2's move. If it's player1's turn and it's our turn,
324 | // we're player1. If it's player2's turn and it's our turn, we're player2.
325 | Game game = Game.load(new String(match.getData()));
326 | switch (game.getCurrentPlayer().getTeam()) {
327 | case 1:
328 | return isMyMove(match);
329 | case 2:
330 | return !isMyMove(match);
331 | default:
332 | throw new IllegalStateException("Cannot parse game. Current player has team " + game.getCurrentPlayer().getTeam());
333 | }
334 | }
335 |
336 | private static boolean isMyMove(TurnBasedMatch match) {
337 | return match.getTurnStatus() == TurnBasedMatch.MATCH_TURN_STATUS_MY_TURN;
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xlythe/hex/view/SelectorLayout.java:
--------------------------------------------------------------------------------
1 | package com.xlythe.hex.view;
2 |
3 | import android.animation.Animator;
4 | import android.animation.ValueAnimator;
5 | import android.content.Context;
6 | import android.graphics.Canvas;
7 | import android.graphics.Color;
8 | import android.graphics.Matrix;
9 | import android.graphics.Paint;
10 | import android.graphics.Path;
11 | import android.graphics.Rect;
12 | import android.graphics.drawable.ShapeDrawable;
13 | import android.graphics.drawable.shapes.PathShape;
14 | import android.os.Build;
15 | import android.util.AttributeSet;
16 | import android.util.DisplayMetrics;
17 | import android.util.TypedValue;
18 | import android.view.MotionEvent;
19 | import android.view.View;
20 | import android.view.View.OnTouchListener;
21 | import android.view.animation.AccelerateInterpolator;
22 |
23 | import com.hex.core.Point;
24 |
25 | import androidx.annotation.NonNull;
26 |
27 | /**
28 | * @author Will Harmon
29 | **/
30 | public class SelectorLayout extends View implements OnTouchListener {
31 | private SelectorLayout.Button[] mButtons;
32 | private Paint mButtonTextPaint;
33 | private int mDisabledColor;
34 | private int mFocusedButton = -1;
35 |
36 | private int mWidth;
37 | private int mIndentHeight;
38 | private int mMargin;
39 | private float mRotation;
40 | private Rect[] mOldRect;
41 | private Rect[] mOldMirrorRect;
42 | private Point[] mOldTextPos;
43 | private boolean mIsLaidOut = false;
44 |
45 | public SelectorLayout(Context context) {
46 | super(context);
47 | setUp();
48 | }
49 |
50 | public SelectorLayout(Context context, AttributeSet attrs) {
51 | super(context, attrs);
52 | setUp();
53 | }
54 |
55 | public SelectorLayout(Context context, AttributeSet attrs, int defStyle) {
56 | super(context, attrs, defStyle);
57 | setUp();
58 | }
59 |
60 | public void setUp() {
61 | DisplayMetrics dm = getResources().getDisplayMetrics();
62 | setOnTouchListener(this);
63 | mButtons = new Button[3];
64 | for (int i = 0; i < mButtons.length; i++) {
65 | mButtons[i] = new Button(getContext());
66 | }
67 | mDisabledColor = getDarkerColor(Color.LTGRAY);
68 | mButtonTextPaint = new Paint();
69 | mButtonTextPaint.setColor(Color.WHITE);
70 | mButtonTextPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 24, dm));
71 |
72 | mWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 90, dm);
73 | mIndentHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, dm);
74 | mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, dm);
75 | mRotation = 45f;
76 | setOnClickListener(v -> {
77 | for (final Button b : mButtons) {
78 | if (b.isSelected() || b.isPressed()) {
79 | final float initialTextX = b.textX;
80 | final Rect initialButtonBounds = b.buttonDrawable.copyBounds();
81 | final Rect initialMirrorButtonBounds = b.mirrorButtonDrawable.copyBounds();
82 |
83 | ValueAnimator animator = ValueAnimator.ofInt(0, 3 * getHeight() / 2);
84 | animator.setInterpolator(new AccelerateInterpolator());
85 | animator.addUpdateListener((valueAnimator) -> {
86 | int value = (Integer) valueAnimator.getAnimatedValue();
87 | b.buttonDrawable.setBounds(initialButtonBounds.left, initialButtonBounds.top - value,
88 | initialButtonBounds.right, initialButtonBounds.bottom - value);
89 | b.mirrorButtonDrawable.setBounds(initialMirrorButtonBounds.left, initialMirrorButtonBounds.top + value,
90 | initialMirrorButtonBounds.right, initialMirrorButtonBounds.bottom + value);
91 | b.textX = initialTextX + value;
92 | invalidate();
93 | });
94 | animator.addListener(new Animator.AnimatorListener() {
95 | @Override
96 | public void onAnimationStart(@NonNull Animator animator) {}
97 |
98 | @Override
99 | public void onAnimationEnd(@NonNull Animator animator) {
100 | b.performClick();
101 | }
102 |
103 | @Override
104 | public void onAnimationCancel(@NonNull Animator animator) {}
105 |
106 | @Override
107 | public void onAnimationRepeat(@NonNull Animator animator) {}
108 | });
109 | animator.start();
110 | }
111 | }
112 | invalidate();
113 | });
114 | setFocusable(true);
115 | setOnFocusChangeListener((v, hasFocus) -> {
116 | if (hasFocus) {
117 | mFocusedButton = 0;
118 | mButtons[0].setSelected(true);
119 | invalidate();
120 | } else {
121 | if (mFocusedButton != -1) {
122 | mButtons[mFocusedButton].setSelected(false);
123 | invalidate();
124 | }
125 | }
126 | });
127 | }
128 |
129 | @Override
130 | public View focusSearch(int direction) {
131 | mButtons[mFocusedButton].setSelected(false);
132 | switch (direction) {
133 | case View.FOCUS_RIGHT:
134 | switch (mFocusedButton) {
135 | case 0:
136 | mFocusedButton = 1;
137 | mButtons[mFocusedButton].setSelected(true);
138 | invalidate();
139 | return this;
140 | case 1:
141 | mFocusedButton = 2;
142 | mButtons[mFocusedButton].setSelected(true);
143 | invalidate();
144 | return this;
145 | }
146 | break;
147 | case View.FOCUS_LEFT:
148 | switch (mFocusedButton) {
149 | case 1:
150 | mFocusedButton = 0;
151 | mButtons[mFocusedButton].setSelected(true);
152 | invalidate();
153 | return this;
154 | case 2:
155 | mFocusedButton = 1;
156 | mButtons[mFocusedButton].setSelected(true);
157 | invalidate();
158 | return this;
159 | }
160 | break;
161 | case View.FOCUS_UP:
162 | break;
163 | case View.FOCUS_DOWN:
164 | break;
165 | case View.FOCUS_FORWARD:
166 | break;
167 | case View.FOCUS_BACKWARD:
168 | break;
169 | }
170 | return super.focusSearch(direction);
171 | }
172 |
173 | @Override
174 | public boolean isLaidOut() {
175 | if (Build.VERSION.SDK_INT < 19) {
176 | return mIsLaidOut;
177 | }
178 | return super.isLaidOut();
179 | }
180 |
181 | @Override
182 | protected void onDraw(@NonNull Canvas canvas) {
183 | if (!isLaidOut()) {
184 | return;
185 | }
186 |
187 | canvas.rotate(mRotation);
188 | for (Button button : mButtons) {
189 | if (!button.isEnabled()) {
190 | button.buttonDrawable.getPaint().setColor(mDisabledColor);
191 | button.mirrorButtonDrawable.getPaint().setColor(mDisabledColor);
192 | } else if (button.isPressed()) {
193 | button.buttonDrawable.getPaint().setColor(getDarkerColor(button.getColor()));
194 | button.mirrorButtonDrawable.getPaint().setColor(getDarkerColor(button.getColor()));
195 | } else if (button.isSelected()) {
196 | button.buttonDrawable.getPaint().setColor(getDarkerColor(button.getColor()));
197 | button.mirrorButtonDrawable.getPaint().setColor(getDarkerColor(button.getColor()));
198 | } else {
199 | button.buttonDrawable.getPaint().setColor(button.getColor());
200 | button.mirrorButtonDrawable.getPaint().setColor(button.getColor());
201 | }
202 |
203 | button.buttonDrawable.draw(canvas);
204 | button.mirrorButtonDrawable.draw(canvas);
205 | canvas.save();
206 | canvas.rotate(-90f, button.getHexagon().b.x, button.getHexagon().d.y / 2f);
207 | canvas.drawText(button.getText(), button.textX, button.textY, mButtonTextPaint);
208 | canvas.restore();
209 | }
210 | }
211 |
212 | @Override
213 | public void onSizeChanged(int w, int h, int oldw, int oldh) {
214 | int diagonal = (int) Math.sqrt(w * w + h * h);
215 | int margin = (w - mWidth * 3) / 4;
216 | int offset = margin;
217 | // Create the buttons
218 | mOldRect = new Rect[mButtons.length];
219 | mOldMirrorRect = new Rect[mButtons.length];
220 | mOldTextPos = new Point[mButtons.length];
221 |
222 | for (int i = 0; i < mButtons.length; i++) {
223 | Hexagon hex = new Hexagon(new Point(offset, mIndentHeight - 3 * offset), new Point(mWidth / 2 + offset, -3 * offset), new Point(mWidth + offset,
224 | mIndentHeight - 3 * offset), new Point(mWidth + offset, h - offset), new Point(mWidth / 2 + offset, h - mIndentHeight - offset), new Point(
225 | offset, h - offset));
226 |
227 | // Shape of a pressed state
228 | Path buttonPath = new Path();
229 | buttonPath.moveTo(hex.a.x, Math.max(hex.a.y, -diagonal));
230 | buttonPath.lineTo(hex.b.x, Math.max(hex.b.y, -diagonal));
231 | buttonPath.lineTo(hex.c.x, Math.max(hex.c.y, -diagonal));
232 | buttonPath.lineTo(hex.d.x, hex.d.y);
233 | buttonPath.lineTo(hex.e.x, hex.e.y);
234 | buttonPath.lineTo(hex.f.x, hex.f.y);
235 | buttonPath.close();
236 |
237 | Hexagon mirrorHex = new Hexagon(new Point(offset, mIndentHeight - offset), new Point(mWidth / 2 + offset, -offset), new Point(mWidth + offset,
238 | mIndentHeight - offset), new Point(mWidth + offset, h - offset), new Point(mWidth / 2 + offset, h - mIndentHeight - offset), new Point(
239 | offset, h - offset));
240 |
241 | // Shape of a pressed state
242 | Path mirrorButtonPath = new Path();
243 | mirrorButtonPath.moveTo(mirrorHex.a.x, mirrorHex.a.y);
244 | mirrorButtonPath.lineTo(mirrorHex.b.x, mirrorHex.b.y);
245 | mirrorButtonPath.lineTo(mirrorHex.c.x, mirrorHex.c.y);
246 | mirrorButtonPath.lineTo(mirrorHex.d.x, mirrorHex.d.y);
247 | mirrorButtonPath.lineTo(mirrorHex.e.x, mirrorHex.e.y);
248 | mirrorButtonPath.lineTo(mirrorHex.f.x, mirrorHex.f.y);
249 | mirrorButtonPath.close();
250 |
251 | int heightOffset = (int) (3.6 * mIndentHeight);
252 | mButtons[i].buttonDrawable = new ShapeDrawable(new PathShape(buttonPath, w, h));
253 | mButtons[i].buttonDrawable.setBounds(0, -heightOffset + h / 2, w, h - heightOffset + h / 2);
254 | mButtons[i].mirrorButtonDrawable = new ShapeDrawable(new PathShape(mirrorButtonPath, w, h));
255 | mButtons[i].mirrorButtonDrawable.setBounds(0, (h - mIndentHeight) + mMargin - heightOffset + h / 2, w, (2 * h - mIndentHeight) + mMargin - heightOffset + h
256 | / 2);
257 |
258 | mButtons[i].setHexagon(hex);
259 |
260 | mButtons[i].textX = mButtons[i].getHexagon().b.x * 2 - mButtonTextPaint.measureText(mButtons[i].getText()) / 2 - (int) (1.7 * i * margin);
261 | mButtons[i].textY = mButtons[i].getHexagon().d.y / 2f + mButtonTextPaint.getTextSize() / 4;
262 |
263 | mOldRect[i] = mButtons[i].buttonDrawable.copyBounds();
264 | mOldMirrorRect[i] = mButtons[i].mirrorButtonDrawable.copyBounds();
265 | mOldTextPos[i] = new Point((int) mButtons[i].textX, (int) mButtons[i].textY);
266 |
267 | offset += margin + mWidth;
268 | }
269 |
270 | mIsLaidOut = true;
271 | }
272 |
273 | @Override
274 | public boolean onTouch(View v, @NonNull MotionEvent event) {
275 | if (event.getAction() == MotionEvent.ACTION_DOWN) {
276 | for (Button b : mButtons) {
277 | if (b.getHexagon().contains(new Point((int) event.getX(), (int) event.getY()))) {
278 | b.setPressed(b.isEnabled());
279 | } else {
280 | b.setPressed(false);
281 | }
282 | }
283 | } else if (event.getAction() == MotionEvent.ACTION_UP) {
284 | for (Button b : mButtons) {
285 | if (b.isPressed()) {
286 | performClick();
287 | }
288 | b.setPressed(false);
289 | }
290 | } else {
291 | for (Button b : mButtons) {
292 | if (b.isPressed()) {
293 | if (!b.getHexagon().contains(new Point((int) event.getX(), (int) event.getY()))) {
294 | b.setPressed(false);
295 | }
296 | }
297 | }
298 | }
299 |
300 | invalidate();
301 | return true;
302 | }
303 |
304 | private int getDarkerColor(int color) {
305 | float[] hsv = new float[3];
306 | Color.colorToHSV(color, hsv);
307 | hsv[2] *= 0.8f;
308 | return Color.HSVToColor(hsv);
309 | }
310 |
311 | public Button[] getButtons() {
312 | return mButtons;
313 | }
314 |
315 | public void reset() {
316 | if (mOldRect != null) {
317 | for (int i = 0; i < mButtons.length; i++) {
318 | mButtons[i].buttonDrawable.setBounds(mOldRect[i]);
319 | mButtons[i].mirrorButtonDrawable.setBounds(mOldMirrorRect[i]);
320 | mButtons[i].textX = mOldTextPos[i].x;
321 | mButtons[i].textY = mOldTextPos[i].y;
322 | }
323 | invalidate();
324 | }
325 | }
326 |
327 | private class Hexagon {
328 | private final Point a, b, c, d, e, f;
329 | @NonNull
330 | private final Matrix m;
331 | @NonNull
332 | private final float[] points;
333 |
334 | private Hexagon(Point a, Point b, Point c, Point d, Point e, Point f) {
335 | this.a = a;
336 | this.b = b;
337 | this.c = c;
338 | this.d = d;
339 | this.e = e;
340 | this.f = f;
341 | points = new float[2];
342 | m = new Matrix();
343 | m.postRotate(-mRotation);
344 | }
345 |
346 | public boolean contains(@NonNull Point p) {
347 | points[0] = p.x;
348 | points[1] = p.y;
349 | m.mapPoints(points);
350 | p.x = (int) points[0];
351 | p.y = (int) points[1];
352 |
353 | return p.x > a.x && p.x < c.x;
354 | }
355 | }
356 |
357 | public static class Button {
358 | private final Context context;
359 | private SelectorLayout.Button.OnClickListener onClickListener;
360 | private String text;
361 | private int color;
362 | private Hexagon hexagon;
363 | private boolean pressed = false;
364 | private boolean enabled = true;
365 | private float textX;
366 | private float textY;
367 | private boolean selected;
368 | private ShapeDrawable buttonDrawable;
369 | private ShapeDrawable mirrorButtonDrawable;
370 |
371 | public Button(Context context) {
372 | this.context = context;
373 | }
374 |
375 | public interface OnClickListener {
376 | void onClick();
377 | }
378 |
379 | public void setOnClickListener(SelectorLayout.Button.OnClickListener onClickListener) {
380 | this.onClickListener = onClickListener;
381 | }
382 |
383 | public OnClickListener getOnClickListener() {
384 | return onClickListener;
385 | }
386 |
387 | public void setText(String text) {
388 | this.text = text;
389 | }
390 |
391 | public void setText(int resId) {
392 | setText(context.getString(resId));
393 | }
394 |
395 | public String getText() {
396 | return text;
397 | }
398 |
399 | public void setColor(int color) {
400 | this.color = color;
401 | }
402 |
403 | public int getColor() {
404 | return color;
405 | }
406 |
407 | private void setHexagon(Hexagon hexagon) {
408 | this.hexagon = hexagon;
409 | }
410 |
411 | private Hexagon getHexagon() {
412 | return hexagon;
413 | }
414 |
415 | public void performClick() {
416 | if (onClickListener != null) onClickListener.onClick();
417 | }
418 |
419 | protected boolean isPressed() {
420 | return pressed;
421 | }
422 |
423 | protected void setPressed(boolean pressed) {
424 | this.pressed = pressed;
425 | }
426 |
427 | protected boolean isSelected() {
428 | return selected;
429 | }
430 |
431 | protected void setSelected(boolean selected) {
432 | this.selected = selected;
433 | }
434 |
435 | public boolean isEnabled() {
436 | return enabled;
437 | }
438 |
439 | public void setEnabled(boolean enabled) {
440 | this.enabled = enabled;
441 | }
442 | }
443 | }
444 |
--------------------------------------------------------------------------------