diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..6db89b9 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.game2048' + compileSdk 33 + + defaultConfig { + applicationId "com.game2048" + minSdk 11 + targetSdk 33 + versionCode 17 + versionName "17" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + externalNativeBuild { + ndkBuild { + path file('src/main/cpp/CMakeLists.mk') + } + } +} + +dependencies { + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b5787b9 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/cpp/2048.cpp b/app/src/main/cpp/2048.cpp new file mode 100644 index 0000000..9d7fdea --- /dev/null +++ b/app/src/main/cpp/2048.cpp @@ -0,0 +1,448 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "android/log.h" +#include "2048.h" + +#include "config.h" + +#if defined(HAVE_UNORDERED_MAP) +#include +typedef std::unordered_map trans_table_t; +#elif defined(HAVE_TR1_UNORDERED_MAP) +#include +typedef std::tr1::unordered_map trans_table_t; +#else + +#include +#include + +typedef std::map trans_table_t; +#endif + +/* MSVC compatibility: undefine max and min macros */ +#if defined(max) +#undef max +#endif + +#if defined(min) +#undef min +#endif + +// Transpose rows/columns in a board: +// 0123 048c +// 4567 --> 159d +// 89ab 26ae +// cdef 37bf +static inline board_t transpose(board_t x) { + board_t a1 = x & 0xF0F00F0FF0F00F0FULL; + board_t a2 = x & 0x0000F0F00000F0F0ULL; + board_t a3 = x & 0x0F0F00000F0F0000ULL; + board_t a = a1 | (a2 << 12) | (a3 >> 12); + board_t b1 = a & 0xFF00FF0000FF00FFULL; + board_t b2 = a & 0x00FF00FF00000000ULL; + board_t b3 = a & 0x00000000FF00FF00ULL; + return b1 | (b2 >> 24) | (b3 << 24); +} + +// Count the number of empty positions (= zero nibbles) in a board. +// Precondition: the board cannot be fully empty. +static int count_empty(board_t x) { + x |= (x >> 2) & 0x3333333333333333ULL; + x |= (x >> 1); + x = ~x & 0x1111111111111111ULL; + // At this point each nibble is: + // 0 if the original nibble was non-zero + // 1 if the original nibble was zero + // Next sum them all + x += x >> 32; + x += x >> 16; + x += x >> 8; + x += x >> 4; // this can overflow to the next nibble if there were 16 empty positions + return x & 0xf; +} + +/* We can perform state lookups one row at a time by using arrays with 65536 entries. */ + +/* Move tables. Each row or compressed column is mapped to (oldrow^newrow) assuming row/col 0. + * + * Thus, the value is 0 if there is no move, and otherwise equals a value that can easily be + * xor'ed into the current board state to update the board. */ +static row_t row_left_table[65536]; +static row_t row_right_table[65536]; +static board_t col_up_table[65536]; +static board_t col_down_table[65536]; +static float heur_score_table[65536]; + +// Heuristic scoring settings +static const float SCORE_LOST_PENALTY = 200000.0f; +static const float SCORE_MONOTONICITY_POWER = 4.0f; +static const float SCORE_MONOTONICITY_WEIGHT = 47.0f; +static const float SCORE_SUM_POWER = 3.5f; +static const float SCORE_SUM_WEIGHT = 11.0f; +static const float SCORE_MERGES_WEIGHT = 700.0f; +static const float SCORE_EMPTY_WEIGHT = 270.0f; +static int maxDepth = 0; + +void init_tables() { + for (unsigned row = 0; row < 65536; ++row) { + unsigned line[4] = { + (row >> 0) & 0xf, + (row >> 4) & 0xf, + (row >> 8) & 0xf, + (row >> 12) & 0xf + }; + + + + // Heuristic score + float sum = 0; + int empty = 0; + int merges = 0; + + int prev = 0; + int counter = 0; + for (int i = 0; i < 4; ++i) { + int rank = line[i]; + sum += pow(rank, SCORE_SUM_POWER); + if (rank == 0) { + empty++; + } else { + if (prev == rank) { + counter++; + } else if (counter > 0) { + merges += 1 + counter; + counter = 0; + } + prev = rank; + } + } + if (counter > 0) { + merges += 1 + counter; + } + + float monotonicity_left = 0; + float monotonicity_right = 0; + for (int i = 1; i < 4; ++i) { + if (line[i - 1] > line[i]) { + monotonicity_left += pow(line[i - 1], SCORE_MONOTONICITY_POWER) - + pow(line[i], SCORE_MONOTONICITY_POWER); + } else { + monotonicity_right += pow(line[i], SCORE_MONOTONICITY_POWER) - + pow(line[i - 1], SCORE_MONOTONICITY_POWER); + } + } + + heur_score_table[row] = SCORE_LOST_PENALTY + + SCORE_EMPTY_WEIGHT * empty + + SCORE_MERGES_WEIGHT * merges - + SCORE_MONOTONICITY_WEIGHT * + std::min(monotonicity_left, monotonicity_right) - + SCORE_SUM_WEIGHT * sum; + + // execute a move to the left + for (int i = 0; i < 3; ++i) { + int j; + for (j = i + 1; j < 4; ++j) { + if (line[j] != 0) break; + } + if (j == 4) break; // no more tiles to the right + + if (line[i] == 0) { + line[i] = line[j]; + line[j] = 0; + i--; // retry this entry + } else if (line[i] == line[j]) { + if (line[i] != 0xf) { + /* Pretend that 32768 + 32768 = 32768 (representational limit). */ + line[i]++; + } + line[j] = 0; + } + } + + row_t result = (line[0] << 0) | + (line[1] << 4) | + (line[2] << 8) | + (line[3] << 12); + row_t rev_result = reverse_row(result); + unsigned rev_row = reverse_row(row); + + row_left_table[row] = row ^ result; + row_right_table[rev_row] = rev_row ^ rev_result; + col_up_table[row] = unpack_col(row) ^ unpack_col(result); + col_down_table[rev_row] = unpack_col(rev_row) ^ unpack_col(rev_result); + } +} + +static inline board_t execute_move_0(board_t board) { + board_t ret = board; + board_t t = transpose(board); + ret ^= col_up_table[(t >> 0) & ROW_MASK] << 0; + ret ^= col_up_table[(t >> 16) & ROW_MASK] << 4; + ret ^= col_up_table[(t >> 32) & ROW_MASK] << 8; + ret ^= col_up_table[(t >> 48) & ROW_MASK] << 12; + return ret; +} + +static inline board_t execute_move_1(board_t board) { + board_t ret = board; + board_t t = transpose(board); + ret ^= col_down_table[(t >> 0) & ROW_MASK] << 0; + ret ^= col_down_table[(t >> 16) & ROW_MASK] << 4; + ret ^= col_down_table[(t >> 32) & ROW_MASK] << 8; + ret ^= col_down_table[(t >> 48) & ROW_MASK] << 12; + return ret; +} + +static inline board_t execute_move_2(board_t board) { + board_t ret = board; + ret ^= board_t(row_left_table[(board >> 0) & ROW_MASK]) << 0; + ret ^= board_t(row_left_table[(board >> 16) & ROW_MASK]) << 16; + ret ^= board_t(row_left_table[(board >> 32) & ROW_MASK]) << 32; + ret ^= board_t(row_left_table[(board >> 48) & ROW_MASK]) << 48; + return ret; +} + +static inline board_t execute_move_3(board_t board) { + board_t ret = board; + ret ^= board_t(row_right_table[(board >> 0) & ROW_MASK]) << 0; + ret ^= board_t(row_right_table[(board >> 16) & ROW_MASK]) << 16; + ret ^= board_t(row_right_table[(board >> 32) & ROW_MASK]) << 32; + ret ^= board_t(row_right_table[(board >> 48) & ROW_MASK]) << 48; + return ret; +} + +/* Execute a move. */ +static inline board_t execute_move(int move, board_t board) { + switch (move) { + case 0: // up + return execute_move_0(board); + case 1: // down + return execute_move_1(board); + case 2: // left + return execute_move_2(board); + case 3: // right + return execute_move_3(board); + default: + return ~0ULL; + } +} + + +static inline int count_distinct_tiles(board_t board) { + uint16_t bitset = 0; + while (board) { + bitset |= 1 << (board & 0xf); + board >>= 4; + } + + // Don't count empty tiles. + bitset >>= 1; + + int count = 0; + while (bitset) { + bitset &= bitset - 1; + count++; + } + return count; +} + +/* Optimizing the game */ + +struct eval_state { + trans_table_t trans_table; // transposition table, to cache previously-seen moves + int maxdepth; + int curdepth; + int cachehits; + unsigned long moves_evaled; + int depth_limit; + + eval_state() : maxdepth(0), curdepth(0), cachehits(0), moves_evaled(0), depth_limit(0) { + } +}; + +// score a single board heuristically +static float score_heur_board(board_t board); + + +// score over all possible moves +static float score_move_node(eval_state &state, board_t board, float cprob); + +// score over all possible tile choices and placements +static float score_tilechoose_node(eval_state &state, board_t board, float cprob); + + +static float score_helper(board_t board, const float *table) { + return table[(board >> 0) & ROW_MASK] + + table[(board >> 16) & ROW_MASK] + + table[(board >> 32) & ROW_MASK] + + table[(board >> 48) & ROW_MASK]; +} + +static float score_heur_board(board_t board) { + return score_helper(board, heur_score_table) + + score_helper(transpose(board), heur_score_table); +} + +// Statistics and controls +// cprob: cumulative probability +// don't recurse into a node with a cprob less than this threshold +static const float CPROB_THRESH_BASE = 0.0001f; +static const int CACHE_DEPTH_LIMIT = 15; + +static float score_tilechoose_node(eval_state &state, board_t board, float cprob) { + if (cprob < CPROB_THRESH_BASE || state.curdepth >= state.depth_limit) { + state.maxdepth = std::max(state.curdepth, state.maxdepth); + return score_heur_board(board); + } + if (state.curdepth < CACHE_DEPTH_LIMIT) { + const trans_table_t::iterator &i = state.trans_table.find(board); + if (i != state.trans_table.end()) { + trans_table_entry_t entry = i->second; + /* + return heuristic from transposition table only if it means that + the node will have been evaluated to a minimum depth of state.depth_limit. + This will result in slightly fewer cache hits, but should not impact the + strength of the ai negatively. + */ + if (entry.depth <= state.curdepth) { + state.cachehits++; + return entry.heuristic; + } + } + } + + int num_open = count_empty(board); + cprob /= num_open; + + float res = 0.0f; + board_t tmp = board; + board_t tile_2 = 1; + while (tile_2) { + if ((tmp & 0xf) == 0) { + res += score_move_node(state, board | tile_2, cprob * 0.9f) * 0.9f; + res += score_move_node(state, board | (tile_2 << 1), cprob * 0.1f) * 0.1f; + } + tmp >>= 4; + tile_2 <<= 4; + } + res = res / num_open; + + if (state.curdepth < CACHE_DEPTH_LIMIT) { + trans_table_entry_t entry = {static_cast(state.curdepth), res}; + state.trans_table[board] = entry; + } + + return res; +} + +static float score_move_node(eval_state &state, board_t board, float cprob) { + float best = 0.0f; + state.curdepth++; + for (int move = 0; move < 4; ++move) { + board_t newboard = execute_move(move, board); + state.moves_evaled++; + + if (board != newboard) { + best = std::max(best, score_tilechoose_node(state, newboard, cprob)); + } + } + state.curdepth--; + + return best; +} + +static float _score_toplevel_move(eval_state &state, board_t board, int move) { + //int maxrank = get_max_rank(board); + board_t newboard = execute_move(move, board); + + if (board == newboard) + return 0; + + return score_tilechoose_node(state, newboard, 1.0f) + 1e-6; +} + +float score_toplevel_move(board_t board, int move) { + float res; + + eval_state state; + state.depth_limit = std::max(3, maxDepth == 0 ? count_distinct_tiles(board) - 2 : std::min(maxDepth, count_distinct_tiles(board) - 2)); + res = _score_toplevel_move(state, board, move); + + return res; +} + +/* Find the best move for a given board. */ +int find_best_move(board_t board) { + int move; + float best = 0; + int bestmove = -1; + + + for (move = 0; move < 4; move++) { + float res = score_toplevel_move(board, move); + + if (res > best) { + best = res; + bestmove = move; + } + } + + return bestmove; +} + + +__attribute__((constructor)) +int main() { + init_tables(); +} + +int log2int(int n) { + if (n == 0) return 0; + return 31 - __builtin_clz(n); +} + + +extern "C" +JNIEXPORT jint JNICALL +Java_com_game2048_MainGame_nativeGetBestMove(JNIEnv *env, jclass clazz, jobjectArray grid) { + board_t board = 0; + + for (int i = 0; i < 4; i++) { + jintArray intArray = (jintArray) env->GetObjectArrayElement(grid, i); + jint *elements = env->GetIntArrayElements(intArray, NULL); + for (int j = 0; j < 4; j++) { + int powerVal = log2int(elements[j]); + board |= (uint64_t) (powerVal & 0xf) << ((i * 4 + j) * 4); + } + env->ReleaseIntArrayElements(intArray, elements, JNI_ABORT); + env->DeleteLocalRef(intArray); + + } + switch (find_best_move(board)) { + case 0: + return 3; + case 1: + return 1; + case 2: + return 0; + case 3: + return 2; + } + + return rand() % 4; +} + + + +extern "C" +JNIEXPORT void JNICALL +Java_com_game2048_MainGame_nativeSetMaxDepth(JNIEnv *env, jclass clazz, jint depth) { + maxDepth = depth; +} \ No newline at end of file diff --git a/app/src/main/cpp/2048.h b/app/src/main/cpp/2048.h new file mode 100644 index 0000000..7461306 --- /dev/null +++ b/app/src/main/cpp/2048.h @@ -0,0 +1,50 @@ +#include +#include "platdefs.h" + +/* The fundamental trick: the 4x4 board is represented as a 64-bit word, + * with each board square packed into a single 4-bit nibble. + * + * The maximum possible board value that can be supported is 32768 (2^15), but + * this is a minor limitation as achieving 65536 is highly unlikely under normal circumstances. + * + * The space and computation savings from using this representation should be significant. + * + * The nibble shift can be computed as (r,c) -> shift (4*r + c). That is, (0,0) is the LSB. + */ + +typedef uint64_t board_t; +typedef uint16_t row_t; + +//store the depth at which the heuristic was recorded as well as the actual heuristic +struct trans_table_entry_t{ + uint8_t depth; + float heuristic; +}; + +static const board_t ROW_MASK = 0xFFFFULL; +static const board_t COL_MASK = 0x000F000F000F000FULL; + + +static inline board_t unpack_col(row_t row) { + board_t tmp = row; + return (tmp | (tmp << 12ULL) | (tmp << 24ULL) | (tmp << 36ULL)) & COL_MASK; +} + +static inline row_t reverse_row(row_t row) { + return (row >> 12) | ((row >> 4) & 0x00F0) | ((row << 4) & 0x0F00) | (row << 12); +} + +/* Functions */ +#ifdef __cplusplus +extern "C" { +#endif + +DLL_PUBLIC void init_tables(); + +typedef int (*get_move_func_t)(board_t); +DLL_PUBLIC float score_toplevel_move(board_t board, int move); +DLL_PUBLIC int find_best_move(board_t board); + +#ifdef __cplusplus +} +#endif diff --git a/app/src/main/cpp/CMakeLists.mk b/app/src/main/cpp/CMakeLists.mk new file mode 100644 index 0000000..bc49227 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.mk @@ -0,0 +1,34 @@ +LOCAL_PATH := $(call my-dir) +MKDIR_P := $(shell mkdir -p $(LOCAL_PATH)/bin) + +#include $(CLEAR_VARS) +# +#LOCAL_MODULE := 2048 +#LOCAL_SRC_FILES := Main.cpp +#LOCAL_CPPFLAGS := +#LOCAL_CFLAGS := -g -O2 +#LOCAL_LDFLAGS := +#LOCAL_LDLIBS := +# +#LOCAL_MODULE_PATH := $(LOCAL_PATH)/bin +#LOCAL_MODULE_SUFFIX := $(EXEEXT) +# +#include $(BUILD_EXECUTABLE) + +include $(CLEAR_VARS) + +LOCAL_MODULE := 2048 +LOCAL_SRC_FILES := 2048.cpp +LOCAL_CPPFLAGS := +LOCAL_CFLAGS := -g -O2 -fPIC +LOCAL_LDFLAGS := +LOCAL_LDLIBS += +#LOCAL_LDLIBS += -llog + +LOCAL_MODULE_PATH := $(LOCAL_PATH)/bin +LOCAL_MODULE_SUFFIX := .so + +include $(BUILD_SHARED_LIBRARY) + +clean: + rm -rf $(LOCAL_PATH)/bin/* diff --git a/app/src/main/cpp/config.h b/app/src/main/cpp/config.h new file mode 100644 index 0000000..408de26 --- /dev/null +++ b/app/src/main/cpp/config.h @@ -0,0 +1,96 @@ +/* config.h. Default, conservative configuration for configure-less platforms like MSVC on Windows. */ +/* config.h.in. Generated from configure.ac by autoheader. */ + +/* Define to 1 if you have the `arc4random_uniform' function. */ +#undef HAVE_ARC4RANDOM_UNIFORM + +/* define if the compiler supports basic C++11 syntax */ +#undef HAVE_CXX11 + +/* Define to 1 if you have the `drand48' function. */ +#undef HAVE_DRAND48 + +/* Define to 1 if you have the header file. */ +#undef HAVE_FCNTL_H + +/* Define to 1 if you have the `gettimeofday' function. */ +#undef HAVE_GETTIMEOFDAY + +/* Define to 1 if you have the header file. */ +#undef HAVE_INTTYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_MEMORY_H + +/* Define to 1 if you have the header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the `strchr' function. */ +#define HAVE_STRCHR 1 + +/* Define to 1 if you have the header file. */ +#undef HAVE_STRINGS_H + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_STAT_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_TIME_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_TYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_TR1_UNORDERED_MAP + +/* Define to 1 if you have the header file. */ +#undef HAVE_UNISTD_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_UNORDERED_MAP + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "https://github.com/nneonneo/2048-ai/issues" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "2048 AI" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "2048 AI 1.0" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "2048-ai" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "https://github.com/nneonneo/2048-ai" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "1.0" + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* Define for Solaris 2.5.1 so the uint64_t typedef from , + , or is not used. If the typedef were allowed, the + #define below would cause a syntax error. */ +/* #undef _UINT64_T */ + +/* Define to `__inline__' or `__inline' if that's what the C compiler + calls it, or to nothing if 'inline' is not supported under any name. */ +#ifndef __cplusplus +/* #undef inline */ +#endif + +/* Define to the type of an unsigned integer type of width exactly 16 bits if + such a type exists and the standard includes do not define it. */ +/* #undef uint16_t */ + +/* Define to the type of an unsigned integer type of width exactly 64 bits if + such a type exists and the standard includes do not define it. */ +/* #undef uint64_t */ diff --git a/app/src/main/cpp/config.h.in b/app/src/main/cpp/config.h.in new file mode 100644 index 0000000..39f8145 --- /dev/null +++ b/app/src/main/cpp/config.h.in @@ -0,0 +1,95 @@ +/* config.h.in. Generated from configure.ac by autoheader. */ + +/* Define to 1 if you have the `arc4random_uniform' function. */ +#undef HAVE_ARC4RANDOM_UNIFORM + +/* define if the compiler supports basic C++11 syntax */ +#undef HAVE_CXX11 + +/* Define to 1 if you have the `drand48' function. */ +#undef HAVE_DRAND48 + +/* Define to 1 if you have the header file. */ +#undef HAVE_FCNTL_H + +/* Define to 1 if you have the `gettimeofday' function. */ +#undef HAVE_GETTIMEOFDAY + +/* Define to 1 if you have the header file. */ +#undef HAVE_INTTYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_MEMORY_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STDINT_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STDLIB_H + +/* Define to 1 if you have the `strchr' function. */ +#undef HAVE_STRCHR + +/* Define to 1 if you have the header file. */ +#undef HAVE_STRINGS_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_STRING_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_STAT_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_TIME_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_TYPES_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_TR1_UNORDERED_MAP + +/* Define to 1 if you have the header file. */ +#undef HAVE_UNISTD_H + +/* Define to 1 if you have the header file. */ +#undef HAVE_UNORDERED_MAP + +/* Define to the address where bug reports for this package should be sent. */ +#undef PACKAGE_BUGREPORT + +/* Define to the full name of this package. */ +#undef PACKAGE_NAME + +/* Define to the full name and version of this package. */ +#undef PACKAGE_STRING + +/* Define to the one symbol short name of this package. */ +#undef PACKAGE_TARNAME + +/* Define to the home page for this package. */ +#undef PACKAGE_URL + +/* Define to the version of this package. */ +#undef PACKAGE_VERSION + +/* Define to 1 if you have the ANSI C header files. */ +#undef STDC_HEADERS + +/* Define for Solaris 2.5.1 so the uint64_t typedef from , + , or is not used. If the typedef were allowed, the + #define below would cause a syntax error. */ +#undef _UINT64_T + +/* Define to `__inline__' or `__inline' if that's what the C compiler + calls it, or to nothing if 'inline' is not supported under any name. */ +#ifndef __cplusplus +#undef inline +#endif + +/* Define to the type of an unsigned integer type of width exactly 16 bits if + such a type exists and the standard includes do not define it. */ +#undef uint16_t + +/* Define to the type of an unsigned integer type of width exactly 64 bits if + such a type exists and the standard includes do not define it. */ +#undef uint64_t diff --git a/app/src/main/cpp/platdefs.h b/app/src/main/cpp/platdefs.h new file mode 100644 index 0000000..85fedac --- /dev/null +++ b/app/src/main/cpp/platdefs.h @@ -0,0 +1,123 @@ +#ifndef PLATDEFS_H +#define PLATDEFS_H + +#include "config.h" + +#include + +/** unif_random */ +/* unif_random is defined as a random number generator returning a value in [0..n-1]. */ +#if defined(HAVE_ARC4RANDOM_UNIFORM) +static inline unsigned unif_random(unsigned n) { + return arc4random_uniform(n); +} +#elif defined(HAVE_DRAND48) +// Warning: This is a slightly biased RNG. +#include +#include +#include +static inline unsigned unif_random(unsigned n) { + static int seeded = 0; + + if(!seeded) { + int fd = open("/dev/urandom", O_RDONLY); + unsigned short seed[3]; + if(fd < 0 || read(fd, seed, sizeof(seed)) < (int)sizeof(seed)) { + srand48(time(NULL)); + } else { + seed48(seed); + } + if(fd >= 0) + close(fd); + + seeded = 1; + } + + return (int)(drand48() * n); +} +#else +// Warning: This is a slightly biased RNG. +#include +static inline unsigned unif_random(unsigned n) { + static int seeded = 0; + + if(!seeded) { + srand(time(NULL)); + seeded = 1; + } + + return rand() % n; +} +#endif + +/** DLL_PUBLIC */ +/* DLL_PUBLIC definition from http://gcc.gnu.org/wiki/Visibility */ +#if defined _WIN32 || defined __CYGWIN__ + #if defined(_WINDLL) + #define BUILDING_DLL + #endif + #ifdef BUILDING_DLL + #ifdef __GNUC__ + #define DLL_PUBLIC __attribute__ ((dllexport)) + #else + #define DLL_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax. + #endif + #else + #ifdef __GNUC__ + #define DLL_PUBLIC __attribute__ ((dllimport)) + #else + #define DLL_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax. + #endif + #endif +#else + #if __GNUC__ >= 4 + #define DLL_PUBLIC __attribute__ ((visibility ("default"))) + #else + #define DLL_PUBLIC + #endif +#endif + +/** gettimeofday */ +/* Win32 gettimeofday implementation from +http://social.msdn.microsoft.com/Forums/vstudio/en-US/430449b3-f6dd-4e18-84de-eebd26a8d668/gettimeofday +with a missing "0" added to DELTA_EPOCH_IN_MICROSECS */ +#if defined(_WIN32) && (!defined(HAVE_GETTIMEOFDAY) || !defined(HAVE_SYS_TIME_H)) +#include +#include +#if defined(_MSC_VER) || defined(_MSC_EXTENSIONS) +#define DELTA_EPOCH_IN_MICROSECS 116444736000000000Ui64 +#else +#define DELTA_EPOCH_IN_MICROSECS 116444736000000000ULL +#endif + +struct timezone; + +int gettimeofday(struct timeval *tv, struct timezone *tz) +{ + FILETIME ft; + unsigned __int64 tmpres = 0; + + (void)tz; + + if (NULL != tv) + { + GetSystemTimeAsFileTime(&ft); + + tmpres |= ft.dwHighDateTime; + tmpres <<= 32; + tmpres |= ft.dwLowDateTime; + + /*converting file time to unix epoch*/ + tmpres -= DELTA_EPOCH_IN_MICROSECS; + tmpres /= 10; /*convert into microseconds*/ + tv->tv_sec = (long)(tmpres / 1000000UL); + tv->tv_usec = (long)(tmpres % 1000000UL); + } + + return 0; +} +#else +#include +#endif + +#endif /* PLATDEFS_H */ diff --git a/app/src/main/java/com/game2048/AI/AI.java b/app/src/main/java/com/game2048/AI/AI.java new file mode 100644 index 0000000..dc3330c --- /dev/null +++ b/app/src/main/java/com/game2048/AI/AI.java @@ -0,0 +1,179 @@ +package com.game2048.AI; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +/** + * Created by admin on 2018/1/8. + */ +public class AI { + + private final GameState grid; + int smoothWeight, //平滑性权重系数 + monoWeight, //单调性权重系数 + emptyWeight, //空格数权重系数 + maxWeight; //最大数权重系数 + long start; + private final boolean considerFour; + + public AI(int[][] grid, int smoothWeight, int monoWeight, int emptyWeight, int maxWeight, boolean considerFour) { + this.grid = new GameState(grid); + this.smoothWeight = smoothWeight; + this.monoWeight = monoWeight; + this.emptyWeight = emptyWeight; + this.maxWeight = maxWeight; + this.considerFour = considerFour; + } + + /** + * 格局评估函数 + * + * @return 返回当前格局的评估值,用于比较判断格局的好坏 + */ + private SearchResult search(int depth, int alpha, int beta, int positions, int cutoffs) { + + int bestScore; + int bestMove = -1; + SearchResult result = new SearchResult(); + int[] directions = {0, 1, 2, 3}; + if (grid.playerTurn) { // Max 层 + bestScore = alpha; + for (int direction : directions) { // 玩家遍历四个滑动方向,找出一个最好的 + GameState newGrid = new GameState(grid.getCellMatrix()); + if (newGrid.move(direction)) { + positions++; + AI newAI = new AI(newGrid.getCellMatrix(), smoothWeight, monoWeight, emptyWeight, maxWeight, considerFour); + + newAI.grid.playerTurn = false; + + if (depth == 0) { + result.move = direction; +// Log.d("TAG", "search: "+direction); + result.score = newGrid.evaluate(smoothWeight, monoWeight, emptyWeight, maxWeight); + } else { + result = newAI.search(depth - 1, bestScore, beta, positions, cutoffs); + if (result.score > 9900) { + result.score--; + } + positions = result.positions; + cutoffs = result.cutoffs; + } + + + if (result.score > bestScore) { + bestScore = result.score; + bestMove = direction; + } + if (bestScore > beta) { + cutoffs++; //剪枝 + + return new SearchResult(bestMove, beta, positions, cutoffs); + } + } + } + } else { + bestScore = beta; + + List candidates = new ArrayList<>(); + List cells = grid.getAvailableCells(); + + if (considerFour) { + int[] fill = {2, 4}; + List scores_2 = new ArrayList<>(); + List scores_4 = new ArrayList<>(); + for (int value : fill) { + for (int i = 0; i < cells.size(); i++) { + grid.insertTitle(cells.get(i)[0], cells.get(i)[1], value); + if (value == 2) scores_2.add(i, -grid.smoothness() + grid.islands()); + if (value == 4) scores_4.add(i, -grid.smoothness() + grid.islands()); + grid.removeTile(cells.get(i)[0], cells.get(i)[1]); + } + } + + // 找出使格局变得最坏的所有可能操作 + int maxScore = Math.max(Collections.max(scores_2), Collections.max(scores_4)); + List maxIndices = new ArrayList<>(); + for (int value : fill) { + if (value == 2) { + for (int i = 0; i < scores_2.size(); i++) { + if (scores_2.get(i) == maxScore) { + maxIndices.add(i); + } + } + } + if (value == 4) { + for (int i = 0; i < scores_4.size(); i++) { + if (scores_4.get(i) == maxScore) { + maxIndices.add(i); + } + } + } + } + int randomIndex = maxIndices.get((int) (Math.random() * maxIndices.size())); + candidates.add(new Candidate(cells.get(randomIndex)[0], cells.get(randomIndex)[1], fill[randomIndex % 2])); + + } else { + + List scores_2 = new ArrayList<>(); + for (int i = 0; i < cells.size(); i++) { + grid.insertTitle(cells.get(i)[0], cells.get(i)[1], 2); + scores_2.add(i, -grid.smoothness() + grid.islands()); + grid.removeTile(cells.get(i)[0], cells.get(i)[1]); + } + // 找出使格局变得最坏的所有可能操作 + int maxScore = Collections.max(scores_2); + for (Integer fitness : scores_2) { + if (fitness == maxScore) { + int index = scores_2.indexOf(fitness); + candidates.add(new Candidate(cells.get(index)[0], cells.get(index)[1], 2)); + } + } + } + // 然后遍历这些操作,基于这些操作向下搜索,找到使得格局最坏的分支 + for (int i = 0; i < candidates.size(); i++) { + int pos_x = candidates.get(i).x; + int pos_y = candidates.get(i).y; + int value = candidates.get(i).value; + GameState newGrid = new GameState(grid.getCellMatrix()); + // 电脑即对手做出一个可能的对于电脑来说最好的(对于玩家来说最坏的)决策 + newGrid.insertTitle(pos_x, pos_y, value); + positions++; + AI newAI = new AI(newGrid.getCellMatrix(), smoothWeight, monoWeight, emptyWeight, maxWeight, considerFour); + // 向下搜索,下一层为Max层,轮到玩家进行决策 + newAI.grid.playerTurn = true; + // 这里depth没有减1是为了保证搜索到最深的层为Max层 + result = newAI.search(depth, alpha, bestScore, positions, cutoffs); + positions = result.positions; + cutoffs = result.cutoffs; + + if (result.score < bestScore) { + bestScore = result.score; + } + // 如果当前bestScore也即beta= 0 && cnt_x < cellMatrix.length && cnt_y >= 0 && cnt_y < cellMatrix[0].length; + } + + /** + * 测量网格的平滑程度(这些块的值可以形象地解释为海拔)。 + * 相邻两个方块的值差异越小,格局就越平滑(在log空间中,所以它表示在合并之前需要进行的合并的数量)。 + */ + public int smoothness() { + int smoothness = 0; + for (int x = 0; x < cellMatrix.length; x++) { + for (int y = 0; y < cellMatrix[0].length; y++) { + if (cellMatrix[x][y] != 0) { + int value = log2(cellMatrix[x][y]); + // 计算水平方向和垂直方向的平滑性评估值 + for (int direction = 1; direction <= 2; direction++) { + int[] vector = vectors[direction]; + int cnt_x = x, cnt_y = y; + do { + cnt_x += vector[0]; + cnt_y += vector[1]; + } while (isInBounds(cnt_x, cnt_y) && isCellAvailable(cnt_x, cnt_y)); + if (isInBounds(cnt_x, cnt_y)) { + if (cellMatrix[cnt_x][cnt_y] != 0) { + int targetValue = log2(cellMatrix[cnt_x][cnt_y]); + smoothness -= Math.abs(value - targetValue); + } + } + } + } + } + } + return smoothness; + } + + /** + * 测量网格的单调性。 + * 这意味着在向左/向右和向上/向下的方向,方块的值都是严格递增或递减的。 + */ + public int monotonicity() { + // 保存四个方向格局单调性的评估值 + int[] totals = {0, 0, 0, 0}; + + // 左/右 方向 + for (int[] matrix : cellMatrix) { + int current = 0; + int next = current + 1; + while (next < cellMatrix[0].length) { + while (next < cellMatrix[0].length && matrix[next] == 0) next++; + if (next >= cellMatrix[0].length) next--; + int currentValue = log2(matrix[current]); + int nextValue = log2(matrix[next]); + if (currentValue > nextValue) { + totals[0] += nextValue - currentValue; + } else if (nextValue > currentValue) { + totals[1] += currentValue - nextValue; + } + current = next; + next++; + } + } + + // 上/下 方向 + for (int y = 0; y < cellMatrix[0].length; y++) { + int current = 0; + int next = current + 1; + while (next < cellMatrix.length) { + while (next < cellMatrix.length && cellMatrix[next][y] == 0) next++; + if (next >= cellMatrix.length) next--; + int currentValue = log2(cellMatrix[current][y]); + int nextValue = log2(cellMatrix[next][y]); + + if (currentValue > nextValue) { + totals[2] += nextValue - currentValue; + } else if (nextValue > currentValue) { + totals[3] += currentValue - nextValue; + } + current = next; + next++; + } + } + + // 取四个方向中最大的值为当前格局单调性的评估值 + return Math.max(totals[0], totals[1]) + Math.max(totals[2], totals[3]); + } + + /** + * 取最大数,这里取对数是为与前面其它指标的计算保持一致,均在log空间进行 + */ + public int maxValue() { + int max = 0; + for (int[] aMatrix : cellMatrix) + for (int j = 0; j < cellMatrix[0].length; j++) + if (aMatrix[j] > max) max = aMatrix[j]; + + return log2(max); + } + + private void merge(int[] row, int action) { + + int[] mergeRow = new int[row.length]; + System.arraycopy(row, 0, mergeRow, 0, row.length); + + int[] moveRow = new int[row.length]; + if (action == 3 || action == 0) { + //进行合并,如 2 2 4 4,合并后为 4 0 8 0 + for (int i = 0; i < mergeRow.length - 1; i++) { + if (mergeRow[i] == 0) continue; + for (int j = i + 1; j < mergeRow.length; j++) { + if (mergeRow[j] == 0) continue; + if (mergeRow[i] != mergeRow[j]) break; + + mergeRow[i] <<= 1; + mergeRow[j] = 0; + + break; + } + } + int k = 0; + //移动,如 4 0 8 0,移动后为 4 8 0 0 + for (int j : mergeRow) { + if (j != 0) moveRow[k++] = j; + } + } + if (action == 1 || action == 2) { + //进行合并,如 2 2 4 4,合并后为 0 4 0 8 + for (int i = mergeRow.length - 1; i > 0; i--) { + if (mergeRow[i] == 0) continue; + for (int j = i - 1; j >= 0; j--) { + if (mergeRow[j] == 0) continue; + if (mergeRow[i] != mergeRow[j]) break; + + mergeRow[i] <<= 1; + mergeRow[j] = 0; + + break; + } + } + int k = row.length - 1; + //移动,如 0 4 0 8,移动后为 0 0 4 8 + for (int i = k; i >= 0; i--) { + if (mergeRow[i] != 0) moveRow[k--] = mergeRow[i]; + } + } + System.arraycopy(moveRow, 0, row, 0, moveRow.length); + } + + public boolean move(int direction) { + int[][] preMatrix = new int[cellMatrix.length][cellMatrix[0].length]; + for (int xx = 0; xx < cellMatrix.length; xx++) { + System.arraycopy(cellMatrix[xx], 0, preMatrix[xx], 0, cellMatrix[0].length); + } + + + boolean moved = false; + + switch (direction) { + case 0: + antiClockwiseRotate90(cellMatrix); + for (int[] matrix : cellMatrix) merge(matrix, 0); + clockwiseRotate90(cellMatrix); + break; + case 1: + for (int[] matrix : cellMatrix) merge(matrix, 1); + break; + case 2: + antiClockwiseRotate90(cellMatrix); + for (int[] matrix : cellMatrix) merge(matrix, 2); + clockwiseRotate90(cellMatrix); + break; + case 3: + for (int[] matrix : cellMatrix) merge(matrix, 3); + break; + } + + if (!isMatrixEquals(preMatrix, cellMatrix)) { + moved = true; + playerTurn = false; + } + return moved; + } + + + public static void antiClockwiseRotate90(int[][] matrix) { + int[][] newMatrix = new int[matrix[0].length][matrix.length]; + for (int p = matrix[0].length - 1, i = 0; i < matrix[0].length; p--, i++) { + for (int q = 0, j = 0; j < matrix.length; q++, j++) { + newMatrix[p][q] = matrix[j][i]; + } + } + + for (int i = 0; i < newMatrix[0].length; i++) { + System.arraycopy(newMatrix[i], 0, matrix[i], 0, newMatrix[0].length); + } + } + + /** + * 将矩阵顺时针旋转90度 + */ + public static void clockwiseRotate90(int[][] matrix) { + int[][] newMatrix = new int[matrix[0].length][matrix.length]; + for (int p = 0, i = 0; i < matrix[0].length; p++, i++) { + for (int q = matrix.length - 1, j = 0; j < matrix.length; q--, j++) { + newMatrix[p][q] = matrix[j][i]; + } + } + for (int i = 0; i < newMatrix[0].length; i++) { + System.arraycopy(newMatrix[i], 0, matrix[i], 0, newMatrix[0].length); + } + } + + public static boolean isMatrixEquals(int[][] matrix_1, int[][] matrix_2) { + for (int i = 0; i < matrix_1.length; i++) { + for (int j = 0; j < matrix_1[0].length; j++) { + if (matrix_1[i][j] != matrix_2[i][j]) return false; + } + } + return true; + } + + public List getAvailableCells() { + List cells = new ArrayList<>(); + for (int x = 0; x < cellMatrix.length; x++) { + for (int y = 0; y < cellMatrix[0].length; y++) { + if (cellMatrix[x][y] == 0) { + int[] tmp = {x, y}; + cells.add(tmp); + } + } + } + return cells; + } + + public void removeTile(int x, int y) { + cellMatrix[x][y] = 0; + } + + public void insertTitle(int x, int y, int value) { + cellMatrix[x][y] = value; + } + + public int islands() { + int islands = 0; + + marked = new boolean[cellMatrix.length][cellMatrix[0].length]; + for (int x = 0; x < cellMatrix.length; x++) { + for (int y = 0; y < cellMatrix[0].length; y++) { + if (cellMatrix[x][y] != 0) { + marked[x][y] = false; + } + } + } + for (int x = 0; x < cellMatrix.length; x++) { + for (int y = 0; y < cellMatrix[0].length; y++) { + if (cellMatrix[x][y] != 0 && !marked[x][y]) { + islands++; + mark(x, y, cellMatrix[x][y]); + } + } + } + + + return islands; + } + + private void mark(int x, int y, int value) { + if (x >= 0 && x <= cellMatrix.length - 1 && y >= 0 && y <= cellMatrix[0].length - 1 && (cellMatrix[x][y] != 0) + && (cellMatrix[x][y] == value) && (!marked[x][y])) { + marked[x][y] = true; + for (int direction = 0; direction < 4; direction++) { + int[] vector = vectors[direction]; + mark(x + vector[0], y + vector[1], value); + } + } + } +} diff --git a/app/src/main/java/com/game2048/AI/SearchResult.java b/app/src/main/java/com/game2048/AI/SearchResult.java new file mode 100644 index 0000000..f17d3a4 --- /dev/null +++ b/app/src/main/java/com/game2048/AI/SearchResult.java @@ -0,0 +1,26 @@ +package com.game2048.AI; + +/** + * Created by admin on 2018/1/9. + */ +public class SearchResult { + public int move; + public int score; + public int positions; + public int cutoffs; + + public SearchResult() { + } + + public SearchResult(int move, int score) { + this.move = move; + this.score = score; + } + + public SearchResult(int move, int score, int positions, int cutoffs) { + this.move = move; + this.score = score; + this.positions = positions; + this.cutoffs = cutoffs; + } +} diff --git a/app/src/main/java/com/game2048/AnimationCell.java b/app/src/main/java/com/game2048/AnimationCell.java new file mode 100644 index 0000000..ba7911a --- /dev/null +++ b/app/src/main/java/com/game2048/AnimationCell.java @@ -0,0 +1,38 @@ +package com.game2048; + +public class AnimationCell extends Cell { + private final int animationType; + private long timeElapsed; + private final long animationTime; + private final long delayTime; + public int[] extras; + + public AnimationCell (int x, int y, int animationType, long length, long delay, int[] extras) { + super(x, y); + this.animationType = animationType; + animationTime = length; + delayTime = delay; + this.extras = extras; + } + + public int getAnimationType() { + return animationType; + } + + public void tick(long timeElapsed) { + this.timeElapsed = this.timeElapsed + timeElapsed; + } + + public boolean animationDone() { + return animationTime + delayTime < timeElapsed; + } + + public double getPercentageDone() { + return Math.max(0, 1.0 * (timeElapsed - delayTime) / animationTime); + } + + public boolean isActive() { + return (timeElapsed >= delayTime); + } + +} diff --git a/app/src/main/java/com/game2048/AnimationGrid.java b/app/src/main/java/com/game2048/AnimationGrid.java new file mode 100644 index 0000000..dbe917e --- /dev/null +++ b/app/src/main/java/com/game2048/AnimationGrid.java @@ -0,0 +1,104 @@ +package com.game2048; + +import java.util.ArrayList; + +public class AnimationGrid { + public ArrayList[][] field; + int activeAnimations = 0; + boolean oneMoreFrame = false; + public ArrayList globalAnimation = new ArrayList<>(); + + public AnimationGrid(int x, int y) { + field = new ArrayList[x][y]; + + for (int xx = 0; xx < x; xx++) { + for (int yy = 0; yy < y; yy++) { + field[xx][yy] = new ArrayList<>(); + } + } + } + + public void startAnimation(int x, int y, int animationType, long length, long delay, int[] extras) { + try { + AnimationCell animationToAdd = new AnimationCell(x, y, animationType, length, delay, extras); + if (x == -1 && y == -1) { + globalAnimation.add(animationToAdd); + } else { + field[x][y].add(animationToAdd); + } + activeAnimations = activeAnimations + 1; + } catch (ArrayIndexOutOfBoundsException ignored) { + } + } + + public void tickAll(long timeElapsed) { + try { + ArrayList cancelledAnimations = new ArrayList<>(); + for (AnimationCell animation : globalAnimation) { + animation.tick(timeElapsed); + if (animation.animationDone()) { + cancelledAnimations.add(animation); + activeAnimations = activeAnimations - 1; + } + } + + for (ArrayList[] array : field) { + for (ArrayList list : array) { + for (AnimationCell animation : list) { + animation.tick(timeElapsed); + if (animation.animationDone()) { + cancelledAnimations.add(animation); + activeAnimations = activeAnimations - 1; + } + } + } + } + + for (AnimationCell animation : cancelledAnimations) { + cancelAnimation(animation); + } + } catch (Exception ignored) { + } + } + + public boolean isAnimationActive() { + if (activeAnimations != 0) { + oneMoreFrame = true; + return true; + } else if (oneMoreFrame) { + oneMoreFrame = false; + return true; + } else { + return false; + } + } + + public ArrayList getAnimationCell(int x, int y) { + return field[x][y]; + } + + public void cancelAnimations() { + try { + for (ArrayList[] array : field) { + for (ArrayList list : array) { + list.clear(); + } + } + globalAnimation.clear(); + activeAnimations = 0; + } catch (Exception ignored) { + } + } + + public void cancelAnimation(AnimationCell animation) { + try { + if (animation.getX() == -1 && animation.getY() == -1) { + globalAnimation.remove(animation); + } else { + field[animation.getX()][animation.getY()].remove(animation); + } + } catch (Exception ignored) { + } + } + +} diff --git a/app/src/main/java/com/game2048/Cell.java b/app/src/main/java/com/game2048/Cell.java new file mode 100644 index 0000000..8768d2d --- /dev/null +++ b/app/src/main/java/com/game2048/Cell.java @@ -0,0 +1,27 @@ +package com.game2048; + +public class Cell { + private int x; + private int y; + + public Cell(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public void setX(int x) { + this.x = x; + } + + public void setY(int y) { + this.y = y; + } +} diff --git a/app/src/main/java/com/game2048/Grid.java b/app/src/main/java/com/game2048/Grid.java new file mode 100644 index 0000000..77e3483 --- /dev/null +++ b/app/src/main/java/com/game2048/Grid.java @@ -0,0 +1,158 @@ +package com.game2048; + +import java.util.ArrayList; + +public class Grid { + + public Tile[][] field; + public ArrayList undoList = new ArrayList<>(); + private final Tile[][] bufferField; + + public Grid(int sizeX, int sizeY) { + field = new Tile[sizeX][sizeY]; + bufferField = new Tile[sizeX][sizeY]; + clearGrid(); + clearUndoList(); + } + + public int[][] getCellMatrix() { + int[][] tmp = new int[field.length][field[0].length]; + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { + tmp[xx][yy] = field[xx][yy] == null ? 0 : field[xx][yy].getValue(); + } + } + return tmp; + } + + public Cell randomAvailableCell() { + ArrayList availableCells = getAvailableCells(); + if (availableCells.size() >= 1) { + return availableCells.get((int) Math.floor(Math.random() + * availableCells.size())); + } + return null; + } + + public ArrayList getAvailableCells() { + ArrayList availableCells = new ArrayList<>(); + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { + if (field[xx][yy] == null) { + availableCells.add(new Cell(xx, yy)); + } + } + } + return availableCells; + } + + public ArrayList getNotAvailableCells() { + ArrayList notAvailableCells = new ArrayList<>(); + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { + if (field[xx][yy] != null) { + notAvailableCells.add(new Cell(xx, yy)); + } + } + } + return notAvailableCells; + } + + public boolean isCellsAvailable() { + return (getAvailableCells().size() >= 1); + } + + public boolean isCellAvailable(Cell cell) { + return !isCellOccupied(cell); + } + + public boolean isCellOccupied(Cell cell) { + return (getCellContent(cell) != null); + } + + public Tile getCellContent(Cell cell) { + if (cell != null && isCellWithinBounds(cell)) { + return field[cell.getX()][cell.getY()]; + } else { + return null; + } + } + + public Tile getCellContent(int x, int y) { + if (isCellWithinBounds(x, y)) { + return field[x][y]; + } else { + return null; + } + } + + public boolean isCellWithinBounds(Cell cell) { + return 0 <= cell.getX() && cell.getX() < field.length + && 0 <= cell.getY() && cell.getY() < field[0].length; + } + + public boolean isCellWithinBounds(int x, int y) { + return 0 <= x && x < field.length && 0 <= y && y < field[0].length; + } + + public void insertTile(Tile tile) { + field[tile.getX()][tile.getY()] = tile; + } + + public void removeTile(Tile tile) { + field[tile.getX()][tile.getY()] = null; + } + + public void saveTiles() { + Tile[][] tmpField = new Tile[bufferField.length][bufferField[0].length]; + for (int xx = 0; xx < bufferField.length; xx++) { + for (int yy = 0; yy < bufferField[0].length; yy++) { + if (bufferField[xx][yy] == null) { + tmpField[xx][yy] = null; + } else { + tmpField[xx][yy] = new Tile(xx, yy, bufferField[xx][yy].getValue()); + } + } + } + undoList.add(tmpField); + } + + public void prepareSaveTiles() { + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { + if (field[xx][yy] == null) { + bufferField[xx][yy] = null; + } else { + bufferField[xx][yy] = new Tile(xx, yy, + field[xx][yy].getValue()); + } + } + } + } + + public void revertTiles() { + if (undoList.size()<=0) return; + for (int xx = 0; xx < undoList.get(undoList.size()-1).length; xx++) { + for (int yy = 0; yy < undoList.get(undoList.size()-1)[0].length; yy++) { + if (undoList.get(undoList.size()-1)[xx][yy] == null) { + field[xx][yy] = null; + } else { + field[xx][yy] = new Tile(xx, yy, undoList.get(undoList.size()-1)[xx][yy].getValue()); + } + } + } + undoList.remove(undoList.size()-1); + } + + public void clearGrid() { + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { + field[xx][yy] = null; + } + } + } + + public void clearUndoList() { + undoList = new ArrayList<>(); + } +} diff --git a/app/src/main/java/com/game2048/InputListener.java b/app/src/main/java/com/game2048/InputListener.java new file mode 100644 index 0000000..1f7ef0f --- /dev/null +++ b/app/src/main/java/com/game2048/InputListener.java @@ -0,0 +1,176 @@ +package com.game2048; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Toast; + +public class InputListener implements View.OnTouchListener { + + private static final int SWIPE_MIN_DISTANCE = 0; + private static final int SWIPE_THRESHOLD_VELOCITY = 25; + private static final int MOVE_THRESHOLD = 250; + private static final int RESET_STARTING = 10; + + private float x; + private float y; + private float lastdx; + private float lastdy; + private float previousX; + private float previousY; + private float startingX; + private float startingY; + private int previousDirection = 1; + private int veryLastDirection = 1; + private boolean hasMoved = false; + + MainView mView; + + public InputListener(MainView view) { + super(); + this.mView = view; + } + + public boolean onTouch(View view, MotionEvent event) { + switch (event.getAction()) { + + case MotionEvent.ACTION_DOWN: + x = event.getX(); + y = event.getY(); + startingX = x; + startingY = y; + previousX = x; + previousY = y; + lastdx = 0; + lastdy = 0; + hasMoved = false; + return true; + case MotionEvent.ACTION_MOVE: + x = event.getX(); + y = event.getY(); + if (mView.game.isActive()) { + float dx = x - previousX; + if (Math.abs(lastdx + dx) < Math.abs(lastdx) + Math.abs(dx) + && Math.abs(dx) > RESET_STARTING + && Math.abs(x - startingX) > SWIPE_MIN_DISTANCE) { + startingX = x; + startingY = y; + lastdx = dx; + previousDirection = veryLastDirection; + } + if (lastdx == 0) { + lastdx = dx; + } + float dy = y - previousY; + if (Math.abs(lastdy + dy) < Math.abs(lastdy) + Math.abs(dy) + && Math.abs(dy) > RESET_STARTING + && Math.abs(y - startingY) > SWIPE_MIN_DISTANCE) { + startingX = x; + startingY = y; + lastdy = dy; + previousDirection = veryLastDirection; + } + if (lastdy == 0) { + lastdy = dy; + } + if (pathMoved() > SWIPE_MIN_DISTANCE * SWIPE_MIN_DISTANCE) { + boolean moved = false; + if (((dy >= SWIPE_THRESHOLD_VELOCITY && previousDirection == 1) || y + - startingY >= MOVE_THRESHOLD) + && previousDirection % 2 != 0) { + moved = true; + previousDirection = previousDirection * 2; + veryLastDirection = 2; + mView.game.move(2); + } else if (((dy <= -SWIPE_THRESHOLD_VELOCITY && previousDirection == 1) || y + - startingY <= -MOVE_THRESHOLD) + && previousDirection % 3 != 0) { + moved = true; + previousDirection = previousDirection * 3; + veryLastDirection = 3; + mView.game.move(0); + } else if (((dx >= SWIPE_THRESHOLD_VELOCITY && previousDirection == 1) || x + - startingX >= MOVE_THRESHOLD) + && previousDirection % 5 != 0) { + moved = true; + previousDirection = previousDirection * 5; + veryLastDirection = 5; + mView.game.move(1); + } else if (((dx <= -SWIPE_THRESHOLD_VELOCITY && previousDirection == 1) || x + - startingX <= -MOVE_THRESHOLD) + && previousDirection % 7 != 0) { + moved = true; + previousDirection = previousDirection * 7; + veryLastDirection = 7; + mView.game.move(3); + } + if (moved) { + hasMoved = true; + startingX = x; + startingY = y; + } + } + } + previousX = x; + previousY = y; + return true; + case MotionEvent.ACTION_UP: + x = event.getX(); + y = event.getY(); + previousDirection = 1; + veryLastDirection = 1; + // "Menu" inputs + if (!hasMoved) { + if (iconPressed(mView.sXNewGame, mView.sYIcons)) { + mView.game.newGame(); + } else if (iconPressed(mView.sXUndo, mView.sYIcons)) { + mView.game.revertUndoState(); + } else if (iconPressed(mView.sXCheat, mView.sYIcons)) { + mView.game.cheat(); + } else if (iconPressed(mView.sXSetting, mView.sYIcons)) { + mView.game.openSetting((int) x, (int) y); + } else if (iconPressed(mView.sXAI, mView.sYIcons)) { + if (mView.game.numSquaresX == mView.game.numSquaresY) { + mView.game.runAi(); + mView.setOnTouchListener((view1, motionEvent) -> { + mView.game.stopAi(); + return false; + }); + } + } else if (iconPressed(mView.sXSound, mView.sYIcons)) { + switchSound(); + } + } + } + return true; + } + + public void switchSound() { + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mView.getContext()); + boolean soundIsOn = mView.game.soundIsOn; + sp.edit().putBoolean("soundIsOn", !soundIsOn).apply(); + Toast.makeText(mView.getContext(), String.format(mView.getContext().getString(R.string.change_sound), !soundIsOn ? mView.getContext().getString(R.string.open) : mView.getContext().getString(R.string.close)), Toast.LENGTH_SHORT).show(); + + } + + private float pathMoved() { + return (x - startingX) * (x - startingX) + (y - startingY) + * (y - startingY); + } + + private boolean iconPressed(int sx, int sy) { + return isTap() && inRange(sx, x, sx + mView.iconSize) && inRange(sy, y, sy + mView.iconSize); + } + + private boolean inRange(float starting, float check, float ending) { + return (starting <= check && check <= ending); + } + + private boolean isTap() { + return pathMoved() <= mView.iconSize * mView.iconSize; + } + + +} diff --git a/app/src/main/java/com/game2048/MainActivity.java b/app/src/main/java/com/game2048/MainActivity.java new file mode 100644 index 0000000..ba83894 --- /dev/null +++ b/app/src/main/java/com/game2048/MainActivity.java @@ -0,0 +1,270 @@ +package com.game2048; + +import android.animation.Animator; +import android.app.Activity; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsetsController; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +public class MainActivity extends Activity { + + + MainView view; + private boolean isAnimPlayed = false, shouldLoad; + public static final String WIDTH = "width"; + public static final String HEIGHT = "height"; + public static final String SCORE = "score"; + public static final String HIGH_SCORE = "high score temp"; + public static final String UNDO_SCORE = "undo score"; + public static final String GAME_STATE = "game state"; + public static final String UNDO_GAME_STATE = "undo game state"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + boolean isNight = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) == Configuration.UI_MODE_NIGHT_YES; + if (!isNight) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + view = new MainView(this, isNight); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme(isNight ? android.R.style.Theme_DeviceDefault : android.R.style.Theme_DeviceDefault_Light); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false); + WindowInsetsController insetsController = window.getInsetsController(); + if (insetsController != null) { + insetsController.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.displayCutout()); + insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } + } + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); + view.hasSaveState = settings.getBoolean("save_state", false); + shouldLoad = !settings.getBoolean("changeScale", false); + settings.edit().putBoolean("changeScale", false).apply(); + if (savedInstanceState != null && savedInstanceState.getBoolean("hasState")) { + load(); + } + setContentView(view); + + // 创建水波动画 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> { + if (!isAnimPlayed) { + isAnimPlayed = true; + Rect rect = getIntent().getSourceBounds(); + float x, y; + if (null != rect) { + x = (rect.right + rect.left) >> 1; + y = (rect.bottom + rect.top) >> 1; + } else { + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + x = metrics.widthPixels; + y = metrics.heightPixels; + } + + // 创建水波背景视图 + // 获取点击位置相对于屏幕的坐标 + int[] location = new int[2]; + view.getLocationOnScreen(location); + int screenX = (int) (x - location[0]); + int screenY = (int) (y - location[1]); + + // 计算动画半径 + int screenWidth = getResources().getDisplayMetrics().widthPixels; + int screenHeight = getResources().getDisplayMetrics().heightPixels; + double maxRadius = Math.sqrt(Math.pow(screenWidth, 2) + Math.pow(screenHeight, 2)); + int finalRadius = (int) Math.max(maxRadius - x, Math.max(maxRadius - y, Math.max(x, y))); + + // 创建水波动画 + Animator animator = ViewAnimationUtils.createCircularReveal(view, screenX, screenY, 0, finalRadius); + animator.setDuration(500); + animator.setInterpolator(new AccelerateInterpolator()); // 设置插值器 + animator.start(); + + } + + }); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU) { + // Do nothing + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_S) { + view.game.move(2); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_W) { + view.game.move(0); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_A) { + view.game.move(3); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_D) { + view.game.move(1); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putBoolean("hasState", true); + save(); + } + + @Override + protected void onPause() { + super.onPause(); + save(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + save(); + } + + private void save() { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = settings.edit(); + Tile[][] field = view.game.grid.field; + editor.putInt(WIDTH, field.length); + editor.putInt(HEIGHT, field.length); + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { + if (field[xx][yy] != null) { + editor.putInt(xx + " " + yy, field[xx][yy].getValue()); + } else { + editor.putInt(xx + " " + yy, 0); + } + + } + } + editor.putLong(SCORE, view.game.score); + editor.putLong(HIGH_SCORE, view.game.highScore); + editor.putLong(UNDO_SCORE, view.game.score); + editor.putInt(GAME_STATE, view.game.gameState); + editor.putInt(UNDO_GAME_STATE, view.game.lastGameState); + editor.apply(); + } + + @Override + protected void onResume() { + super.onResume(); + load(); + } + + private void load() { + // Stopping all animations + view.game.aGrid.cancelAnimations(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); + if (shouldLoad) { + for (int xx = 0; xx < view.game.grid.field.length; xx++) { + for (int yy = 0; yy < view.game.grid.field[0].length; yy++) { + int value = settings.getInt(xx + " " + yy, -1); + if (value > 0) { + view.game.grid.field[xx][yy] = new Tile(xx, yy, value); + } else if (value == 0) { + view.game.grid.field[xx][yy] = null; + } + } + } + view.game.score = settings.getLong(SCORE, view.game.score); + view.game.lastScore.add(settings.getLong(UNDO_SCORE, 0)); + view.game.lastGameState = settings.getInt(UNDO_GAME_STATE, view.game.lastGameState); + view.game.gameState = settings.getInt(GAME_STATE, view.game.gameState); + } + view.game.highScore = settings.getLong(HIGH_SCORE, view.game.highScore); + + } + + @Override + public void onBackPressed() { + animateFinish(); + } + + public void animateFinish() { + view.game.isAIRunning = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + Rect rect = getIntent().getSourceBounds(); + int x, y; + if (null != rect) { + x = (rect.right + rect.left) >> 1; + y = (rect.bottom + rect.top) >> 1; + } else { + DisplayMetrics metrics1 = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getRealMetrics(metrics1); + x = metrics1.widthPixels; + y = metrics1.heightPixels; + } + // 计算动画半径 + int screenWidth = getResources().getDisplayMetrics().widthPixels; + int screenHeight = getResources().getDisplayMetrics().heightPixels; + double maxRadius = Math.sqrt(Math.pow(screenWidth, 2) + Math.pow(screenHeight, 2)); + int finalRadius = (int) Math.max(maxRadius - x, Math.max(maxRadius - y, Math.max(x, y))); + + // 创建水波动画 + Animator animator = ViewAnimationUtils.createCircularReveal(view, x, y, finalRadius, 0); + animator.setDuration(500); + animator.setInterpolator(new DecelerateInterpolator()); // 设置插值器 + animator.start(); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + + } + + @Override + public void onAnimationEnd(Animator animator) { + view.setVisibility(View.GONE); + finish(); + } + + @Override + public void onAnimationCancel(Animator animator) { + + } + + @Override + public void onAnimationRepeat(Animator animator) { + + } + }); + } else { + + finish(); + } + } +} diff --git a/app/src/main/java/com/game2048/MainGame.java b/app/src/main/java/com/game2048/MainGame.java new file mode 100644 index 0000000..397dc21 --- /dev/null +++ b/app/src/main/java/com/game2048/MainGame.java @@ -0,0 +1,464 @@ +package com.game2048; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.MediaPlayer; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import com.game2048.AI.AI; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class MainGame { + public boolean isAIRunning = false; + public static final int SPAWN_ANIMATION = -1; + public static final int MOVE_ANIMATION = 0; + public static final int MERGE_ANIMATION = 1; + + public static final int FADE_GLOBAL_ANIMATION = 0; + + public static long MOVE_ANIMATION_TIME = MainView.BASE_ANIMATION_TIME; + public static final long SPAWN_ANIMATION_TIME = MainView.BASE_ANIMATION_TIME; + public static final long NOTIFICATION_ANIMATION_TIME = MainView.BASE_ANIMATION_TIME * 5; + public static final long NOTIFICATION_DELAY_TIME = MOVE_ANIMATION_TIME + SPAWN_ANIMATION_TIME; + private static final String HIGH_SCORE = "high score"; + + + public static final int GAME_LOST = -1; + public static final int GAME_NORMAL = 0; + + public Grid grid = null; + public AnimationGrid aGrid; + public int numSquaresX; + public int numSquaresY; + public int startTiles; + + public int gameState = 0; + + public long score = 0; + public long highScore = 0; + + public List lastScore = new ArrayList<>(); + public int lastGameState = 0; + + private long bufferScore = 0; + + public int TimeMoved = 0; + public int poss; + public boolean soundIsOn; + private final Context mContext; + + private final MainView mView; + private int numTilesAddedPerMove; + SharedPreferences settings = null; + private MediaPlayer mediaPlayer; + + static { + System.loadLibrary("2048"); + } + + public MainGame(Context context, MainView view) { + mContext = context; + mView = view; + } + + private static native int nativeGetBestMove(int[][] grid); + + private static native void nativeSetMaxDepth(int depth); + + private boolean considerFour; + private boolean usePowerfulAI = false; + private long lastPowerfulAIMoveTime = 0; + private final Handler gHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + + if (isAIRunning) { + + new Thread(() -> { + if (usePowerfulAI) { + final long stopTime = lastPowerfulAIMoveTime + timeIntervalPerMove; + while (System.nanoTime() < stopTime) ; + final int direction = nativeGetBestMove(grid.getCellMatrix()); + lastPowerfulAIMoveTime = System.nanoTime(); + ((Activity) mContext).runOnUiThread(() -> { + move(direction); + continueRunAi(); + }); + return; + } + final int direction = new AI(grid.getCellMatrix(), smoothWeight, monoWeight, emptyWeight, maxWeight, considerFour).getBestMove(timeIntervalPerMove); + ((Activity) mContext).runOnUiThread(() -> { + move(direction); + continueRunAi(); + }); + }).start(); + + } else { + mView.setOnTouchListener(new InputListener(mView)); + } + } + }; + + + public void newGame() { + if (settings == null) { + settings = PreferenceManager.getDefaultSharedPreferences(mContext); + numSquaresX = settings.getInt("x", 4); + numSquaresY = settings.getInt("y", 4); + } + numTilesAddedPerMove = settings.getInt("time", 1); + startTiles = settings.getInt("start", 2); + poss = settings.getInt("poss", 90); + considerFour = settings.getBoolean("considerFour", true); + soundIsOn = settings.getBoolean("soundIsOn", true); + mediaPlayer = MediaPlayer.create(mContext, R.raw.sound); + if (grid == null) { + grid = new Grid(numSquaresX, numSquaresY); + } else { + prepareUndoState(); + saveUndoState(); + grid.clearGrid(); + grid.clearUndoList(); + } + aGrid = new AnimationGrid(numSquaresX, numSquaresY); + highScore = getHighScore(); + if (score >= highScore) { + highScore = score; + recordHighScore(); + } + score = 0; + gameState = GAME_NORMAL; + addStartTiles(); + mView.refreshLastTime = true; + mView.resyncTime(); + mView.invalidate(); + TimeMoved = 0; + lastScore.clear(); + lastScore = new ArrayList<>(); + } + + + private void addStartTiles() { + for (int xx = 0; xx < startTiles; xx++) { + addRandomTile(); + } + } + + private void addRandomTile() { + if (grid.isCellsAvailable()) { + int value = Math.random() < poss / 100f ? 2 : 4; + Tile tile = new Tile(grid.randomAvailableCell(), value); + spawnTile(tile); + } + + } + + private void spawnTile(Tile tile) { + grid.insertTile(tile); + aGrid.startAnimation(tile.getX(), tile.getY(), SPAWN_ANIMATION, SPAWN_ANIMATION_TIME, MOVE_ANIMATION_TIME, null); // Direction: + // -1 = + // EXPANDING + } + + private void recordHighScore() { + PreferenceManager.getDefaultSharedPreferences(mContext).edit().putLong(HIGH_SCORE, highScore).apply(); + } + + private long getHighScore() { + return PreferenceManager.getDefaultSharedPreferences(mContext).getLong(HIGH_SCORE, -1); + } + + private void prepareTiles() { + for (Tile[] array : grid.field) { + for (Tile tile : array) { + if (grid.isCellOccupied(tile)) { + tile.setMergedFrom(null); + } + } + } + } + + private void moveTile(Tile tile, Cell cell) { + + grid.field[tile.getX()][tile.getY()] = null; + grid.field[cell.getX()][cell.getY()] = tile; + tile.updatePosition(cell); + } + + private void saveUndoState() { + grid.saveTiles(); + lastScore.add(bufferScore); + TimeMoved++; + } + + // cheat remove 2 + public void cheat() { + ArrayList notAvailableCell = grid.getNotAvailableCells(); + Tile tile; + prepareUndoState(); + for (Cell cell : notAvailableCell) { + tile = grid.getCellContent(cell); + if (2 == tile.getValue()) { + grid.removeTile(tile); + gameState = GAME_NORMAL; + } + } + + if (grid.getNotAvailableCells().size() == 0) { + addStartTiles(); + } + saveUndoState(); + mView.resyncTime(); + mView.invalidate(); + } + + private void prepareUndoState() { + grid.prepareSaveTiles(); + bufferScore = score; + } + + public void revertUndoState() { + if (TimeMoved > 0) { + aGrid.cancelAnimations(); + grid.revertTiles(); + TimeMoved--; + score = lastScore.get(lastScore.size() - 1); + lastScore.remove(lastScore.size() - 1); + gameState = lastGameState; + mView.refreshLastTime = true; + mView.invalidate(); + } + } + + public boolean gameLost() { + return (gameState == GAME_LOST); + } + + public boolean isActive() { + return !gameLost(); + } + + public void move(int direction) { + aGrid.cancelAnimations(); + // 0: up, 1: right, 2: down, 3: left + if (!isActive()) { + return; + } + prepareUndoState(); + Cell vector = getVector(direction); + List traversalsX = buildTraversalsX(vector); + List traversalsY = buildTraversalsY(vector); + boolean moved = false; + + prepareTiles(); + + for (int xx : traversalsX) { + for (int yy : traversalsY) { + Cell cell = new Cell(xx, yy); + Tile tile = grid.getCellContent(cell); + + if (tile != null) { + Cell[] positions = findFarthestPosition(cell, vector); + Tile next = grid.getCellContent(positions[1]); + + if (next != null && next.getValue() == tile.getValue() && next.getMergedFrom() == null) { + + Tile merged = new Tile(positions[1], tile.getValue() * 2); + Tile[] temp = {tile, next}; + merged.setMergedFrom(temp); + + grid.insertTile(merged); + grid.removeTile(tile); + + // Converge the two tiles' positions + tile.updatePosition(positions[1]); + + int[] extras = {xx, yy}; + aGrid.startAnimation(merged.getX(), merged.getY(), MOVE_ANIMATION, MOVE_ANIMATION_TIME, 0, extras); // Direction: + // 0 + // = + // MOVING + // MERGED + aGrid.startAnimation(merged.getX(), merged.getY(), MERGE_ANIMATION, SPAWN_ANIMATION_TIME, MOVE_ANIMATION_TIME, null); + + // Update the score + score += merged.getValue(); + highScore = Math.max(score, highScore); + + // The mighty 2048 tile + + } else { + moveTile(tile, positions[0]); + int[] extras = {xx, yy, 0}; + aGrid.startAnimation(positions[0].getX(), positions[0].getY(), MOVE_ANIMATION, MOVE_ANIMATION_TIME, 0, extras); // Direction: 1 + // = MOVING + // NO MERGE + } + + if (!positionsEqual(cell, tile)) { + moved = true; + } + } + } + } + + if (moved) { + if (soundIsOn) { + mediaPlayer.seekTo(0); + mediaPlayer.start(); + } + if (!isAIRunning) saveUndoState(); + for (int i = 0; i < numTilesAddedPerMove; i++) { + addRandomTile(); + } + checkLose(); + } + mView.resyncTime(); + mView.invalidate(); + + } + + private void checkLose() { + if (!movesAvailable()) { + gameState = GAME_LOST; + endGame(); + } + } + + private void endGame() { + aGrid.startAnimation(-1, -1, FADE_GLOBAL_ANIMATION, NOTIFICATION_ANIMATION_TIME, NOTIFICATION_DELAY_TIME, null); + if (score >= highScore) { + highScore = score; + recordHighScore(); + } + } + + private Cell getVector(int direction) { + Cell[] map = {new Cell(0, -1), // up + new Cell(1, 0), // right + new Cell(0, 1), // down + new Cell(-1, 0) // left + }; + return map[direction]; + } + + private List buildTraversalsX(Cell vector) { + List traversals = new ArrayList<>(); + + for (int xx = 0; xx < numSquaresX; xx++) { + traversals.add(xx); + } + if (vector.getX() == 1) { + Collections.reverse(traversals); + } + + return traversals; + } + + private List buildTraversalsY(Cell vector) { + List traversals = new ArrayList<>(); + + for (int xx = 0; xx < numSquaresY; xx++) { + traversals.add(xx); + } + if (vector.getY() == 1) { + Collections.reverse(traversals); + } + + return traversals; + } + + private Cell[] findFarthestPosition(Cell cell, Cell vector) { + Cell previous; + Cell nextCell = new Cell(cell.getX(), cell.getY()); + do { + previous = nextCell; + nextCell = new Cell(previous.getX() + vector.getX(), previous.getY() + vector.getY()); + } while (grid.isCellWithinBounds(nextCell) && grid.isCellAvailable(nextCell)); + + return new Cell[]{previous, nextCell}; + } + + private boolean movesAvailable() { + return grid.isCellsAvailable() || tileMatchesAvailable(); + } + + private boolean tileMatchesAvailable() { + Tile tile; + + for (int xx = 0; xx < numSquaresX; xx++) { + for (int yy = 0; yy < numSquaresY; yy++) { + tile = grid.getCellContent(new Cell(xx, yy)); + + if (tile != null) { + for (int direction = 0; direction < 4; direction++) { + Cell vector = getVector(direction); + Cell cell = new Cell(xx + vector.getX(), yy + vector.getY()); + + Tile other = grid.getCellContent(cell); + + if (other != null && other.getValue() == tile.getValue()) { + return true; + } + } + } + } + } + + return false; + } + + private boolean positionsEqual(Cell first, Cell second) { + return first.getX() == second.getX() && first.getY() == second.getY(); + } + + + public void openSetting(int x, int y) { + mContext.startActivity(new Intent(mContext, SetActivity.class).putExtra("startX", x).putExtra("startY", y)); + } + + long timeIntervalPerMove; + int smoothWeight, monoWeight, emptyWeight, maxWeight; + + public void runAi() { + isAIRunning = true; + Toast.makeText(mContext, R.string.press_anywhere_to_stop, Toast.LENGTH_SHORT).show(); + timeIntervalPerMove = settings.getInt("AItime", 100) * 1000000L; + + usePowerfulAI = (numSquaresY == 4 && numSquaresX == 4); + if (usePowerfulAI) { + nativeSetMaxDepth(settings.getInt("maxDepth", 5)); + lastPowerfulAIMoveTime = System.nanoTime(); + } else { + considerFour = settings.getBoolean("considerFour", true); + smoothWeight = settings.getInt("smooth", 1); //平滑性权重系数 + monoWeight = settings.getInt("mono", 40); //单调性权重系数 + emptyWeight = settings.getInt("empty", 27); //空格数权重系数 + maxWeight = settings.getInt("max", 10); //最大数权重系数 + } + + continueRunAi(); + } + + public void continueRunAi() { + if (gameState == 0) { + gHandler.sendMessage(new Message()); + } else stopAi(); + } + + public void stopAi() { + Toast.makeText(mContext, R.string.stopped, Toast.LENGTH_SHORT).show(); + mView.setOnTouchListener(new InputListener(mView)); + isAIRunning = false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/game2048/MainView.java b/app/src/main/java/com/game2048/MainView.java new file mode 100644 index 0000000..538c977 --- /dev/null +++ b/app/src/main/java/com/game2048/MainView.java @@ -0,0 +1,568 @@ +package com.game2048; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.preference.PreferenceManager; +import android.view.View; + + +import java.util.ArrayList; + +public class MainView extends View { + + + // Internal variables + Paint paint = new Paint(); + public MainGame game; + public boolean hasSaveState = false; + private final int numCellTypes = 18; + public boolean continueButtonEnabled = false; + + // Layout variables + private int cellSize = 0; + private float textSize = 0; + private float cellTextSize = 0; + private int gridWidth = 0; + private final int TEXT_BLACK; + private final int TEXT_WHITE; + private final int TEXT_BROWN; + public int startingX; + public int startingY; + public int endingX; + public int endingY; + private int textPaddingSize; + public int iconPaddingSize; + + // Assets + private final Drawable backgroundRectangle; + private final Drawable[] cellRectangle = new Drawable[numCellTypes]; + private final BitmapDrawable[] bitmapCell = new BitmapDrawable[numCellTypes]; + private final Drawable newGameIcon, undoIcon, cheatIcon, settingIcon, aiIcon, soundIcon; + private final Drawable lightUpRectangle; + private Bitmap background = null; + private BitmapDrawable loseGameOverlay; + + // Text variables + private int sYAll; + private int titleStartYAll; + private int bodyStartYAll; + private int eYAll; + private int titleWidthHighScore; + private int titleWidthScore; + + // Icon variables + public int sYIcons; + public int sXNewGame, sXUndo, sXCheat, iconSize, sXSetting, sXAI, sXSound; + + // Text values + private final String headerText; + private final String highScoreTitle; + private final String scoreTitle; + private final String loseText; + + long lastFPSTime = System.nanoTime(); + long currentTime = System.nanoTime(); + + float titleTextSize; + float bodyTextSize; + float headerTextSize; + float instructionsTextSize; + float gameOverTextSize; + + boolean refreshLastTime = true; + + static final int BASE_ANIMATION_TIME = 100000000; + + static final float MERGING_ACCELERATION = (float) -0.5; + static final float INITIAL_VELOCITY = (1 - MERGING_ACCELERATION) / 4; + + + @Override + public void onDraw(Canvas canvas) { + // Reset the transparency of the screen + canvas.drawBitmap(background, 0, 0, paint); + + drawScoreText(canvas); + + if (!game.isActive() && !game.aGrid.isAnimationActive()) { + drawNewGameButton(canvas, true); + } + + drawCells(canvas); + + if (!game.isActive()) { + drawEndGameState(canvas); + } + + // Refresh the screen if there is still an animation running + if (game.aGrid.isAnimationActive()) { + invalidate(startingX, startingY, endingX, endingY); + tick(); + // Refresh one last time on game end. + } else if (!game.isActive() && refreshLastTime) { + invalidate(); + refreshLastTime = false; + } + } + + @Override + protected void onSizeChanged(int width, int height, int oldw, int oldh) { + super.onSizeChanged(width, height, oldw, oldh); + getLayout(width, height); + createBackgroundBitmap(width, height); + createBitmapCells(); + createOverlays(); + } + + private void drawDrawable(Canvas canvas, Drawable draw, int startingX, + int startingY, int endingX, int endingY) { + draw.setBounds(startingX, startingY, endingX, endingY); + draw.draw(canvas); + } + + private void drawCellText(Canvas canvas, int value) { + int textShiftY = centerText(); + paint.setColor(value >= 8 ? TEXT_WHITE : TEXT_BLACK); + paint.setTextSize(value <= 8192 ? cellTextSize : cellTextSize / 1.2f); + canvas.drawText("" + value, (cellSize >> 1), (cellSize >> 1) + - textShiftY, paint); + } + + private void drawScoreText(Canvas canvas) { + // Drawing the score text: Ver 2 + paint.setTextSize(game.score < 99999 ? bodyTextSize : bodyTextSize / 1.5f); + paint.setTextAlign(Paint.Align.CENTER); + + int bodyWidthHighScore = (int) (paint.measureText("" + game.highScore)); + int bodyWidthScore = (int) (paint.measureText("" + game.score)); + + int textWidthHighScore = Math.max(titleWidthHighScore, + bodyWidthHighScore) + textPaddingSize * 2; + int textWidthScore = Math.max(titleWidthScore, bodyWidthScore) + + textPaddingSize * 2; + + int textMiddleHighScore = textWidthHighScore / 2; + int textMiddleScore = textWidthScore / 2; + + int eXHighScore = endingX; + int sXHighScore = eXHighScore - textWidthHighScore; + + int eXScore = sXHighScore - textPaddingSize; + int sXScore = eXScore - textWidthScore; + + // Outputting high-scores box + backgroundRectangle.setBounds(sXHighScore, sYAll, eXHighScore, eYAll); + backgroundRectangle.draw(canvas); + paint.setTextSize(titleTextSize); + paint.setColor(TEXT_BROWN); + canvas.drawText(highScoreTitle, sXHighScore + textMiddleHighScore, + titleStartYAll, paint); + paint.setTextSize(game.highScore < 99999 ? bodyTextSize : bodyTextSize / 1.5f); + paint.setColor(TEXT_WHITE); + canvas.drawText(String.valueOf(game.highScore), sXHighScore + + textMiddleHighScore, bodyStartYAll, paint); + + // Outputting scores box + backgroundRectangle.setBounds(sXScore, sYAll, eXScore, eYAll); + backgroundRectangle.draw(canvas); + paint.setTextSize(titleTextSize); + paint.setColor(TEXT_BROWN); + canvas.drawText(scoreTitle, sXScore + textMiddleScore, titleStartYAll, + paint); + paint.setTextSize(game.score < 99999 ? bodyTextSize : bodyTextSize / 1.5f); + paint.setColor(TEXT_WHITE); + canvas.drawText(String.valueOf(game.score), sXScore + textMiddleScore, + bodyStartYAll, paint); + } + + private void drawNewGameButton(Canvas canvas, boolean lightUp) { + if (lightUp) { + drawDrawable(canvas, lightUpRectangle, sXNewGame, sYIcons, + sXNewGame + iconSize, sYIcons + iconSize); + } else { + drawDrawable(canvas, backgroundRectangle, sXNewGame, sYIcons, + sXNewGame + iconSize, sYIcons + iconSize); + } + drawDrawable(canvas, newGameIcon, sXNewGame + iconPaddingSize, sYIcons + + iconPaddingSize, sXNewGame + iconSize - iconPaddingSize, + sYIcons + iconSize - iconPaddingSize); + } + + private void drawCheatButton(Canvas canvas) { + drawDrawable(canvas, backgroundRectangle, sXCheat, sYIcons, sXCheat + + iconSize, sYIcons + iconSize); + drawDrawable(canvas, cheatIcon, sXCheat + iconPaddingSize, sYIcons + + iconPaddingSize, sXCheat + iconSize - iconPaddingSize, + sYIcons + iconSize - iconPaddingSize); + } + + private void drawUndoButton(Canvas canvas) { + drawDrawable(canvas, backgroundRectangle, sXUndo, sYIcons, sXUndo + + iconSize, sYIcons + iconSize); + drawDrawable(canvas, undoIcon, sXUndo + iconPaddingSize, sYIcons + + iconPaddingSize, sXUndo + iconSize - iconPaddingSize, sYIcons + + iconSize - iconPaddingSize); + } + + private void drawSettingButton(Canvas canvas) { + drawDrawable(canvas, backgroundRectangle, sXSetting, sYIcons, sXSetting + + iconSize, sYIcons + iconSize); + drawDrawable(canvas, settingIcon, sXSetting + iconPaddingSize, sYIcons + + iconPaddingSize, sXSetting + iconSize - iconPaddingSize, sYIcons + + iconSize - iconPaddingSize); + } + + private void drawAIButton(Canvas canvas) { + drawDrawable(canvas, backgroundRectangle, sXAI, sYIcons, sXAI + + iconSize, sYIcons + iconSize); + drawDrawable(canvas, aiIcon, sXAI + iconPaddingSize, sYIcons + + iconPaddingSize, sXAI + iconSize - iconPaddingSize, sYIcons + + iconSize - iconPaddingSize); + } + + public void drawSoundButton(Canvas canvas) { + drawDrawable(canvas, backgroundRectangle, sXSound, sYIcons, sXSound + + iconSize, sYIcons + iconSize); + drawDrawable(canvas, soundIcon, sXSound + iconPaddingSize, sYIcons + + iconPaddingSize, sXSound + iconSize - iconPaddingSize, sYIcons + + iconSize - iconPaddingSize); + } + + private void drawHeader(Canvas canvas) { + // Drawing the header + paint.setTextSize(headerTextSize); + paint.setColor(TEXT_BLACK); + paint.setTextAlign(Paint.Align.LEFT); + int textShiftY = centerText() * 2; + int headerStartY = sYAll - textShiftY; + canvas.drawText(headerText, startingX, headerStartY, paint); + } + + private void drawBackground(Canvas canvas) { + drawDrawable(canvas, backgroundRectangle, startingX, startingY, + endingX, endingY); + } + + private void drawBackgroundGrid(Canvas canvas) { + // Outputting the game grid + for (int xx = 0; xx < game.numSquaresX; xx++) { + for (int yy = 0; yy < game.numSquaresY; yy++) { + int sX = startingX + gridWidth + (cellSize + gridWidth) * xx; + int eX = sX + cellSize; + int sY = startingY + gridWidth + (cellSize + gridWidth) * yy; + int eY = sY + cellSize; + + drawDrawable(canvas, cellRectangle[0], sX, sY, eX, eY); + } + } + } + + private void drawCells(Canvas canvas) { + try { + + + paint.setTextSize(textSize); + paint.setTextAlign(Paint.Align.CENTER); + // Outputting the individual cells + for (int xx = 0; xx < game.numSquaresX; xx++) { + for (int yy = 0; yy < game.numSquaresY; yy++) { + int sX = startingX + gridWidth + (cellSize + gridWidth) * xx; + int eX = sX + cellSize; + int sY = startingY + gridWidth + (cellSize + gridWidth) * yy; + int eY = sY + cellSize; + + Tile currentTile = game.grid.getCellContent(xx, yy); + if (currentTile != null) { + // Get and represent the value of the tile + int value = currentTile.getValue(); + int index = log2(value); + + // Check for any active animations + ArrayList aArray = game.aGrid.getAnimationCell(xx, yy); + boolean animated = false; + for (int i = aArray.size() - 1; i >= 0; i--) { + AnimationCell aCell = aArray.get(i); + // If this animation is not active, skip it + if (aCell.getAnimationType() == MainGame.SPAWN_ANIMATION) { + animated = true; + } + if (!aCell.isActive()) { + continue; + } + + if (aCell.getAnimationType() == MainGame.SPAWN_ANIMATION) { // Spawning + // animation + double percentDone = aCell.getPercentageDone(); + float textScaleSize = (float) (percentDone); + paint.setTextSize(textSize * textScaleSize); + + float cellScaleSize = (cellSize >> 1) * (1 - textScaleSize); + bitmapCell[index].setBounds( + (int) (sX + cellScaleSize), + (int) (sY + cellScaleSize), + (int) (eX - cellScaleSize), + (int) (eY - cellScaleSize)); + bitmapCell[index].draw(canvas); + } else if (aCell.getAnimationType() == MainGame.MERGE_ANIMATION) { // Merging + // Animation + double percentDone = aCell.getPercentageDone(); + float textScaleSize = (float) (1 + INITIAL_VELOCITY + * percentDone + MERGING_ACCELERATION + * percentDone * percentDone / 2); + paint.setTextSize(textSize * textScaleSize); + + float cellScaleSize = (cellSize >> 1) * (1 - textScaleSize); + bitmapCell[index].setBounds( + (int) (sX + cellScaleSize), + (int) (sY + cellScaleSize), + (int) (eX - cellScaleSize), + (int) (eY - cellScaleSize)); + bitmapCell[index].draw(canvas); + } else if (aCell.getAnimationType() == MainGame.MOVE_ANIMATION) { // Moving + // animation + double percentDone = aCell.getPercentageDone(); + int tempIndex = index; + if (aArray.size() >= 2) { + tempIndex = tempIndex - 1; + } + int previousX = aCell.extras[0]; + int previousY = aCell.extras[1]; + int currentX = currentTile.getX(); + int currentY = currentTile.getY(); + int dX = (int) ((currentX - previousX) + * (cellSize + gridWidth) + * (percentDone - 1) * 1.0); + int dY = (int) ((currentY - previousY) + * (cellSize + gridWidth) + * (percentDone - 1) * 1.0); + bitmapCell[tempIndex].setBounds(sX + dX, sY + dY, + eX + dX, eY + dY); + bitmapCell[tempIndex].draw(canvas); + } + animated = true; + } + + // No active animations? Just draw the cell + if (!animated) { + bitmapCell[index].setBounds(sX, sY, eX, eY); + bitmapCell[index].draw(canvas); + } + } + } + + } + } catch (Exception ignored) { + } + + } + + private void drawEndGameState(Canvas canvas) { + double alphaChange = 1; + continueButtonEnabled = false; + for (AnimationCell animation : game.aGrid.globalAnimation) { + if (animation.getAnimationType() == MainGame.FADE_GLOBAL_ANIMATION) { + alphaChange = animation.getPercentageDone(); + } + } + BitmapDrawable displayOverlay = null; + if (game.gameLost()) { + displayOverlay = loseGameOverlay; + } + + if (displayOverlay != null) { + displayOverlay.setBounds(startingX, startingY, endingX, endingY); + displayOverlay.setAlpha((int) (255 * alphaChange)); + displayOverlay.draw(canvas); + } + } + + private void createEndGameStates(Canvas canvas) { + int width = endingX - startingX; + int length = endingY - startingY; + int middleX = width / 2; + int middleY = length / 2; + lightUpRectangle.setAlpha(127); + drawDrawable(canvas, lightUpRectangle, 0, 0, width, length); + paint.setColor(this.TEXT_BLACK); + paint.setAlpha(255); + paint.setTextSize(this.gameOverTextSize); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(this.loseText, (float) middleX, (float) (middleY - centerText()), this.paint); + } + + + private void createBackgroundBitmap(int width, int height) { + background = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(background); + drawHeader(canvas); + drawCheatButton(canvas); + drawNewGameButton(canvas, false); + drawUndoButton(canvas); + drawBackground(canvas); + drawBackgroundGrid(canvas); + drawSettingButton(canvas); + drawAIButton(canvas); + drawSoundButton(canvas); + } + + + private void createBitmapCells() { + paint.setTextSize(cellTextSize); + paint.setTextAlign(Paint.Align.CENTER); + Resources resources = getResources(); + for (int xx = 0; xx < bitmapCell.length; xx++) { + Bitmap bitmap = Bitmap.createBitmap(cellSize, cellSize, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawDrawable(canvas, cellRectangle[xx], 0, 0, cellSize, cellSize); + drawCellText(canvas, (int) Math.pow(2, xx)); + bitmapCell[xx] = new BitmapDrawable(resources, bitmap); + } + } + + private void createOverlays() { + Resources resources = getResources(); + // Initalize overlays + Bitmap bitmap = Bitmap.createBitmap(endingX - startingX, endingY + - startingY, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + createEndGameStates(canvas); + loseGameOverlay = new BitmapDrawable(resources, bitmap); + } + + private void tick() { + currentTime = System.nanoTime(); + game.aGrid.tickAll(currentTime - lastFPSTime); + lastFPSTime = currentTime; + } + + public void resyncTime() { + lastFPSTime = System.nanoTime(); + } + + private static int log2(int n) { + if (n <= 0) + throw new IllegalArgumentException(); + return 31 - Integer.numberOfLeadingZeros(n); + } + + private void getLayout(int width, int height) { + cellSize = Math.min(width / (game.numSquaresX + 1), height / (game.numSquaresY + 3)); + gridWidth = cellSize / 7; + int screenMiddleX = width / 2; + int screenMiddleY = height / 2; + int boardMiddleY = screenMiddleY + cellSize / 2; + iconSize = cellSize / 2; + + paint.setTextAlign(Paint.Align.CENTER); + paint.setTextSize(cellSize); + textSize = cellSize * cellSize / Math.max(cellSize, paint.measureText("0000")); + cellTextSize = textSize * 0.9f; + titleTextSize = textSize / 3; + bodyTextSize = (int) (textSize / 1.5); + instructionsTextSize = (int) (textSize / 1.8); + headerTextSize = textSize * 2; + gameOverTextSize = textSize * 2; + textPaddingSize = (int) (textSize / 3); + iconPaddingSize = (int) (textSize / 7); + + // Grid Dimensions + double halfNumSquaresX = game.numSquaresX / 2d; + double halfNumSquaresY = game.numSquaresY / 2d; + + startingX = (int) (screenMiddleX - (cellSize + gridWidth) + * halfNumSquaresX - gridWidth / 2); + endingX = (int) (screenMiddleX + (cellSize + gridWidth) + * halfNumSquaresX + gridWidth / 2); + startingY = (int) (boardMiddleY - (cellSize + gridWidth) + * halfNumSquaresY - gridWidth / 2); + endingY = (int) (boardMiddleY + (cellSize + gridWidth) + * halfNumSquaresY + gridWidth / 2); + + paint.setTextSize(titleTextSize); + + int textShiftYAll = centerText(); + // static variables + sYAll = (int) (startingY - cellSize * 1.5); + titleStartYAll = (int) (sYAll + textPaddingSize + titleTextSize / 2 - textShiftYAll); + bodyStartYAll = (int) (titleStartYAll + textPaddingSize + titleTextSize / 2 + bodyTextSize / 2); + + titleWidthHighScore = (int) (paint.measureText(highScoreTitle)); + titleWidthScore = (int) (paint.measureText(scoreTitle)); + paint.setTextSize(bodyTextSize); + textShiftYAll = centerText(); + eYAll = (int) (bodyStartYAll + textShiftYAll + bodyTextSize / 2 + textPaddingSize); + + sYIcons = (startingY + eYAll) / 2 - iconSize / 2; + sXNewGame = (endingX - iconSize); + sXUndo = sXNewGame - iconSize * 3 / 2 - iconPaddingSize; + sXCheat = sXUndo - iconSize * 3 / 2 - iconPaddingSize; + sXSetting = sXCheat - iconSize * 3 / 2 - iconPaddingSize; + sXAI = sXSetting - iconSize * 3 / 2 - iconPaddingSize; + sXSound = sXAI - iconSize * 3 / 2 - iconPaddingSize; + resyncTime(); + } + + private int centerText() { + return (int) ((paint.descent() + paint.ascent()) / 2); + } + + public MainView(Context context, boolean isNightMode) { + super(context); + Resources resources = context.getResources(); + // Loading resources + game = new MainGame(context, this); + // Getting text values + headerText = "2048"; + highScoreTitle = context.getString(R.string.max_score); + scoreTitle = context.getString(R.string.current_score); + loseText = "Game Over!"; + // Getting assets + + boolean soundIsOn = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("soundIsOn", true); + backgroundRectangle = isNightMode ? resources.getDrawable(R.drawable.background_night_rectangle) : resources.getDrawable(R.drawable.background_rectangle); + + cellRectangle[0] = isNightMode ? resources.getDrawable(R.drawable.cell_night_rectangle) : resources.getDrawable(R.drawable.cell_rectangle); + cellRectangle[1] = isNightMode ? resources.getDrawable(R.drawable.cell_rectangle_night_2) : resources.getDrawable(R.drawable.cell_rectangle_2); + cellRectangle[2] = isNightMode ? resources.getDrawable(R.drawable.cell_rectangle_night_4) : resources.getDrawable(R.drawable.cell_rectangle_4); + cellRectangle[3] = resources.getDrawable(R.drawable.cell_rectangle_8); + cellRectangle[4] = resources.getDrawable(R.drawable.cell_rectangle_16); + cellRectangle[5] = resources.getDrawable(R.drawable.cell_rectangle_32); + cellRectangle[6] = resources.getDrawable(R.drawable.cell_rectangle_64); + cellRectangle[7] = resources.getDrawable(R.drawable.cell_rectangle_128); + cellRectangle[8] = resources.getDrawable(R.drawable.cell_rectangle_256); + cellRectangle[9] = resources.getDrawable(R.drawable.cell_rectangle_512); + cellRectangle[10] = resources.getDrawable(R.drawable.cell_rectangle_1024); + cellRectangle[11] = resources.getDrawable(R.drawable.cell_rectangle_2048); + cellRectangle[12] = resources.getDrawable(R.drawable.cell_rectangle_4096); + cellRectangle[13] = resources.getDrawable(R.drawable.cell_rectangle_8192); + cellRectangle[14] = resources.getDrawable(R.drawable.cell_rectangle_16384); + cellRectangle[15] = resources.getDrawable(R.drawable.cell_rectangle_32768); + cellRectangle[16] = resources.getDrawable(R.drawable.cell_rectangle_65536); + cellRectangle[17] = resources.getDrawable(R.drawable.cell_rectangle_131072); + newGameIcon = resources.getDrawable(R.drawable.ic_action_refresh); + undoIcon = resources.getDrawable(R.drawable.ic_action_undo); + cheatIcon = resources.getDrawable(R.drawable.ic_action_cheat); + settingIcon = resources.getDrawable(R.drawable.ic_action_settings); + aiIcon = resources.getDrawable(R.drawable.ic_action_ai); + + soundIcon = resources.getDrawable(soundIsOn ? R.drawable.ic_action_soundon : R.drawable.ic_action_soundoff); + lightUpRectangle = resources.getDrawable(R.drawable.light_up_rectangle); + TEXT_WHITE = resources.getColor(R.color.text_white); + TEXT_BLACK = resources.getColor(R.color.text_black); + TEXT_BROWN = isNightMode ? resources.getColor(R.color.text_brown) : resources.getColor(R.color.text_white); + this.setBackgroundColor(resources.getColor(R.color.background)); + paint.setTypeface(Typeface.DEFAULT_BOLD); + paint.setAntiAlias(true); + setOnTouchListener(new InputListener(this)); + game.newGame(); + if (game.numSquaresX != game.numSquaresY) aiIcon.setAlpha(127); + } + +} diff --git a/app/src/main/java/com/game2048/SetActivity.java b/app/src/main/java/com/game2048/SetActivity.java new file mode 100644 index 0000000..ad68786 --- /dev/null +++ b/app/src/main/java/com/game2048/SetActivity.java @@ -0,0 +1,266 @@ +package com.game2048; + +import android.animation.Animator; +import android.app.Activity; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsetsController; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ScrollView; +import android.widget.SeekBar; +import android.widget.Toast; + +public class SetActivity extends Activity { + private boolean isAnimPlayed = false; + private ScrollView scrollView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + Window window = getWindow(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + boolean isNight = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) == Configuration.UI_MODE_NIGHT_YES; + if (!isNight) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false); + WindowInsetsController insetsController = window.getInsetsController(); + if (insetsController != null) { + insetsController.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.displayCutout()); + insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme(isNight ? android.R.style.Theme_DeviceDefault : android.R.style.Theme_DeviceDefault_Light); + + setContentView(R.layout.set); + scrollView = findViewById(R.id.scrollView); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> { + if (!isAnimPlayed) { + isAnimPlayed = true; + int x, y; + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + x = getIntent().getIntExtra("startX", metrics.widthPixels >> 1); + y = getIntent().getIntExtra("startY", metrics.heightPixels >> 1); + + + scrollView.setBackgroundColor(getResources().getColor(R.color.background)); + // 计算动画半径 + int screenWidth = getResources().getDisplayMetrics().widthPixels; + int screenHeight = getResources().getDisplayMetrics().heightPixels; + double maxRadius = Math.sqrt(Math.pow(screenWidth, 2) + Math.pow(screenHeight, 2)); + int finalRadius = (int) Math.max(maxRadius - x, Math.max(maxRadius - y, Math.max(x, y))); + + // 创建水波动画 + Animator animator = ViewAnimationUtils.createCircularReveal(scrollView, x, y, 0, finalRadius); + animator.setDuration(500); + animator.setInterpolator(new AccelerateInterpolator()); // 设置插值器 + animator.start(); + } + + }); + } + EditText e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11; + CheckBox c1; + SeekBar s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11; + + e1 = findViewById(R.id.e1); + e2 = findViewById(R.id.e2); + e3 = findViewById(R.id.e3); + e4 = findViewById(R.id.e4); + e5 = findViewById(R.id.e5); + e6 = findViewById(R.id.e6); + e7 = findViewById(R.id.e7); + e8 = findViewById(R.id.e8); + e9 = findViewById(R.id.e9); + e10 = findViewById(R.id.e10); + e11 = findViewById(R.id.e11); + c1 = findViewById(R.id.c1); + s1 = findViewById(R.id.s1); + s2 = findViewById(R.id.s2); + s3 = findViewById(R.id.s3); + s4 = findViewById(R.id.s4); + s5 = findViewById(R.id.s5); + s6 = findViewById(R.id.s6); + s7 = findViewById(R.id.s7); + s8 = findViewById(R.id.s8); + s9 = findViewById(R.id.s9); + s10 = findViewById(R.id.s10); + s11 = findViewById(R.id.s11); + EditText[] editTexts = {e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11}; + SeekBar[] seekBars = {s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11}; + int[] defaultValues = {4, 4, 1, 2, 90, 100, 10, 1, 40, 27, 5}; + int[] minValues = {3, 3, 1, 1, 0, 50, 0, 0, 0, 0, 0}; + String[] preferencesKeys = {"x", "y", "time", "start", "poss", "AItime", "max", "smooth", "mono", "empty", "maxDepth"}; + + SharedPreferences set = PreferenceManager.getDefaultSharedPreferences(this); + + for (int i = 0; i < editTexts.length; i++) { + EditText editText = editTexts[i]; + SeekBar seekBar = seekBars[i]; + int defaultValue = defaultValues[i]; + int minValue = minValues[i]; + String preferencesKey = preferencesKeys[i]; + + editText.setText(String.valueOf(set.getInt(preferencesKey, defaultValue))); + seekBar.setProgress(set.getInt(preferencesKey, defaultValue)); + + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (progress < minValue) { + seekBar.setProgress(minValue); + return; + } + editText.setText(String.valueOf(progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + editText.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + String text = editText.getText().toString(); + int value = Integer.parseInt(text.isEmpty() ? "0" : text); + seekBar.setProgress(value); + } + }); + editText.setOnKeyListener((view, i1, keyEvent) -> { + if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER && keyEvent.getAction() == KeyEvent.ACTION_DOWN && editText.getText().length() > 0) { + + String text = editText.getText().toString(); + int value = Integer.parseInt(text.isEmpty() ? "0" : text); + seekBar.setProgress(value); + + } + return false; + }); + } + + c1.setChecked(set.getBoolean("considerFour", true)); + final Button save = findViewById(R.id.save); + save.setOnClickListener(view -> { + for (EditText e : new EditText[]{e1, e2, e3, e4, e5}) { + if (e.getText().length() == 0) { + Toast.makeText(SetActivity.this, R.string.re_input, Toast.LENGTH_SHORT).show(); + return; + } + } + for (EditText e : new EditText[]{e7, e8, e9, e10}) { + if (e.getText().length() == 0) { + Toast.makeText(SetActivity.this, R.string.re_input_ai_weight, Toast.LENGTH_SHORT).show(); + return; + } + } + if (Integer.parseInt(e1.getText().toString()) != set.getInt("x", 4) || Integer.parseInt(e2.getText().toString()) != set.getInt("y", 4)) { + set.edit().putBoolean("changeScale", true).apply(); + } + set.edit().putInt("x", Integer.parseInt(e1.getText().toString())) + .putInt("y", Integer.parseInt(e2.getText().toString())) + .putInt("time", Integer.parseInt(e3.getText().toString())) + .putInt("start", Integer.parseInt(e4.getText().toString())) + .putInt("poss", Integer.parseInt(e5.getText().toString())) + .putInt("AItime", Integer.parseInt(e6.getText().toString())) + .putInt("max", Integer.parseInt(e7.getText().toString())) + .putInt("smooth", Integer.parseInt(e8.getText().toString())) + .putInt("mono", Integer.parseInt(e9.getText().toString())) + .putInt("empty", Integer.parseInt(e10.getText().toString())) + .putInt("maxDepth", Integer.parseInt(e11.getText().toString())) + .putBoolean("considerFour", c1.isChecked()) + .apply(); + Toast.makeText(SetActivity.this, R.string.setting_change_effect, Toast.LENGTH_SHORT).show(); + animateFinish(); + }); + + + } + + public void animateFinish() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + int x, y; + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + x = getIntent().getIntExtra("startX", metrics.widthPixels >> 1); + y = getIntent().getIntExtra("startY", metrics.heightPixels >> 1); + + // 计算动画半径 + int screenWidth = getResources().getDisplayMetrics().widthPixels; + int screenHeight = getResources().getDisplayMetrics().heightPixels; + double maxRadius = Math.sqrt(Math.pow(screenWidth, 2) + Math.pow(screenHeight, 2)); + int finalRadius = (int) Math.max(maxRadius - x, Math.max(maxRadius - y, Math.max(x, y))); + + // 创建水波动画 + Animator animator = ViewAnimationUtils.createCircularReveal(scrollView, x, y, finalRadius, 0); + animator.setDuration(500); + animator.setInterpolator(new DecelerateInterpolator()); // 设置插值器 + animator.start(); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + + } + + @Override + public void onAnimationEnd(Animator animator) { + scrollView.setVisibility(View.GONE); + finish(); + } + + @Override + public void onAnimationCancel(Animator animator) { + + } + + @Override + public void onAnimationRepeat(Animator animator) { + + } + }); + } else { + finish(); + } + } + + public void cancel(View view) { + animateFinish(); + } + + @Override + public void onBackPressed() { + animateFinish(); + } +} diff --git a/app/src/main/java/com/game2048/Tile.java b/app/src/main/java/com/game2048/Tile.java new file mode 100644 index 0000000..975d624 --- /dev/null +++ b/app/src/main/java/com/game2048/Tile.java @@ -0,0 +1,32 @@ +package com.game2048; +public class Tile extends Cell { + private final int value; + private Tile[] mergedFrom = null; + + public Tile(int x, int y, int value) { + super(x, y); + this.value = value; + } + + public Tile(Cell cell, int value) { + super(cell.getX(), cell.getY()); + this.value = value; + } + + public void updatePosition(Cell cell) { + this.setX(cell.getX()); + this.setY(cell.getY()); + } + + public int getValue() { + return this.value; + } + + public Tile[] getMergedFrom() { + return mergedFrom; + } + + public void setMergedFrom(Tile[] tile) { + mergedFrom = tile; + } +} diff --git a/app/src/main/res/drawable-v21/ic_action_ai.xml b/app/src/main/res/drawable-v21/ic_action_ai.xml new file mode 100644 index 0000000..5bc5210 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_action_ai.xml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_action_cheat.xml b/app/src/main/res/drawable-v21/ic_action_cheat.xml new file mode 100644 index 0000000..441dae1 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_action_cheat.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_action_refresh.xml b/app/src/main/res/drawable-v21/ic_action_refresh.xml new file mode 100644 index 0000000..9307b5d --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_action_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_action_settings.xml b/app/src/main/res/drawable-v21/ic_action_settings.xml new file mode 100644 index 0000000..584de10 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_action_settings.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-v21/ic_action_soundoff.xml b/app/src/main/res/drawable-v21/ic_action_soundoff.xml new file mode 100644 index 0000000..93c0013 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_action_soundoff.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_action_soundon.xml b/app/src/main/res/drawable-v21/ic_action_soundon.xml new file mode 100644 index 0000000..81e5aa8 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_action_soundon.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_action_undo.xml b/app/src/main/res/drawable-v21/ic_action_undo.xml new file mode 100644 index 0000000..4ea9e69 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_action_undo.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v26/ic_fore.xml b/app/src/main/res/drawable-v26/ic_fore.xml new file mode 100644 index 0000000..b942658 --- /dev/null +++ b/app/src/main/res/drawable-v26/ic_fore.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_night_rectangle.xml b/app/src/main/res/drawable/background_night_rectangle.xml new file mode 100644 index 0000000..1c21da5 --- /dev/null +++ b/app/src/main/res/drawable/background_night_rectangle.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_rectangle.xml b/app/src/main/res/drawable/background_rectangle.xml new file mode 100644 index 0000000..5f69e1b --- /dev/null +++ b/app/src/main/res/drawable/background_rectangle.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_night_rectangle.xml b/app/src/main/res/drawable/cell_night_rectangle.xml new file mode 100644 index 0000000..a9a9e34 --- /dev/null +++ b/app/src/main/res/drawable/cell_night_rectangle.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle.xml b/app/src/main/res/drawable/cell_rectangle.xml new file mode 100644 index 0000000..f90eef9 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_1024.xml b/app/src/main/res/drawable/cell_rectangle_1024.xml new file mode 100644 index 0000000..1fc9597 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_1024.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_128.xml b/app/src/main/res/drawable/cell_rectangle_128.xml new file mode 100644 index 0000000..e9dc4c9 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_128.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_131072.xml b/app/src/main/res/drawable/cell_rectangle_131072.xml new file mode 100644 index 0000000..08fc745 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_131072.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_16.xml b/app/src/main/res/drawable/cell_rectangle_16.xml new file mode 100644 index 0000000..8f9c010 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_16.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_16384.xml b/app/src/main/res/drawable/cell_rectangle_16384.xml new file mode 100644 index 0000000..911a125 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_16384.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_2.xml b/app/src/main/res/drawable/cell_rectangle_2.xml new file mode 100644 index 0000000..b79ceb0 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_2.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_2048.xml b/app/src/main/res/drawable/cell_rectangle_2048.xml new file mode 100644 index 0000000..d59f8e6 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_2048.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_256.xml b/app/src/main/res/drawable/cell_rectangle_256.xml new file mode 100644 index 0000000..5764a0c --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_256.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_32.xml b/app/src/main/res/drawable/cell_rectangle_32.xml new file mode 100644 index 0000000..478777f --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_32.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_32768.xml b/app/src/main/res/drawable/cell_rectangle_32768.xml new file mode 100644 index 0000000..ef2fe1c --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_32768.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_4.xml b/app/src/main/res/drawable/cell_rectangle_4.xml new file mode 100644 index 0000000..975d64f --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_4.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_4096.xml b/app/src/main/res/drawable/cell_rectangle_4096.xml new file mode 100644 index 0000000..78a6094 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_4096.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_512.xml b/app/src/main/res/drawable/cell_rectangle_512.xml new file mode 100644 index 0000000..ef6a0ea --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_512.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_64.xml b/app/src/main/res/drawable/cell_rectangle_64.xml new file mode 100644 index 0000000..f8b6f96 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_64.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_65536.xml b/app/src/main/res/drawable/cell_rectangle_65536.xml new file mode 100644 index 0000000..4a7bdaa --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_65536.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_8.xml b/app/src/main/res/drawable/cell_rectangle_8.xml new file mode 100644 index 0000000..2f2151c --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_8.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_8192.xml b/app/src/main/res/drawable/cell_rectangle_8192.xml new file mode 100644 index 0000000..85c49f5 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_8192.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_night_2.xml b/app/src/main/res/drawable/cell_rectangle_night_2.xml new file mode 100644 index 0000000..66f15db --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_night_2.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cell_rectangle_night_4.xml b/app/src/main/res/drawable/cell_rectangle_night_4.xml new file mode 100644 index 0000000..ccea6b6 --- /dev/null +++ b/app/src/main/res/drawable/cell_rectangle_night_4.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fade_rectangle.xml b/app/src/main/res/drawable/fade_rectangle.xml new file mode 100644 index 0000000..9e4eebc --- /dev/null +++ b/app/src/main/res/drawable/fade_rectangle.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action_ai.png b/app/src/main/res/drawable/ic_action_ai.png new file mode 100644 index 0000000..fe6fc15 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_ai.png differ diff --git a/app/src/main/res/drawable/ic_action_cheat.png b/app/src/main/res/drawable/ic_action_cheat.png new file mode 100644 index 0000000..41b1e30 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_cheat.png differ diff --git a/app/src/main/res/drawable/ic_action_refresh.png b/app/src/main/res/drawable/ic_action_refresh.png new file mode 100644 index 0000000..139c831 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_refresh.png differ diff --git a/app/src/main/res/drawable/ic_action_settings.png b/app/src/main/res/drawable/ic_action_settings.png new file mode 100644 index 0000000..8db5f58 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_settings.png differ diff --git a/app/src/main/res/drawable/ic_action_soundoff.png b/app/src/main/res/drawable/ic_action_soundoff.png new file mode 100644 index 0000000..6b1abb3 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_soundoff.png differ diff --git a/app/src/main/res/drawable/ic_action_soundon.png b/app/src/main/res/drawable/ic_action_soundon.png new file mode 100644 index 0000000..9f02217 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_soundon.png differ diff --git a/app/src/main/res/drawable/ic_action_undo.png b/app/src/main/res/drawable/ic_action_undo.png new file mode 100644 index 0000000..e562cd0 Binary files /dev/null and b/app/src/main/res/drawable/ic_action_undo.png differ diff --git a/app/src/main/res/drawable/light_up_rectangle.xml b/app/src/main/res/drawable/light_up_rectangle.xml new file mode 100644 index 0000000..fe05ab3 --- /dev/null +++ b/app/src/main/res/drawable/light_up_rectangle.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/set.xml b/app/src/main/res/layout/set.xml new file mode 100644 index 0000000..125f4f7 --- /dev/null +++ b/app/src/main/res/layout/set.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +