2014-07-23 13:41:36 +00:00
|
|
|
/*
|
|
|
|
* File: game.c
|
|
|
|
* Author: Jeremy Rand
|
|
|
|
* Date: July 23, 2014
|
|
|
|
*
|
|
|
|
* This file contains the implementation of the game logic.
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2014-07-23 14:45:10 +00:00
|
|
|
#include <assert.h>
|
2014-07-24 05:31:37 +00:00
|
|
|
#include <stddef.h>
|
2014-07-24 19:21:28 +00:00
|
|
|
#include <stdio.h>
|
2014-07-23 14:45:10 +00:00
|
|
|
#include <stdlib.h>
|
2014-07-23 13:41:36 +00:00
|
|
|
|
2014-07-24 05:31:37 +00:00
|
|
|
#include "game.h"
|
2014-07-23 13:41:36 +00:00
|
|
|
|
2014-07-23 14:10:39 +00:00
|
|
|
|
2014-07-23 14:45:10 +00:00
|
|
|
#define POS_IN_DIR(pos, dir) ((pos) + (dir))
|
2014-07-23 14:10:39 +00:00
|
|
|
|
2014-07-24 19:21:28 +00:00
|
|
|
// The maximum which the game supports in any one tile is 2^26 because that is
|
|
|
|
// the largest number which fits in a 8 character tile. Once this is reached,
|
2014-07-23 14:10:39 +00:00
|
|
|
// the game is won. Note this is a very big number so not a huge restriction.
|
2014-07-24 19:21:28 +00:00
|
|
|
#define MAX_TILE_VALUE 26
|
2014-07-23 14:10:39 +00:00
|
|
|
|
|
|
|
#define BLOCKED_TILE_VALUE -1
|
|
|
|
|
|
|
|
// A tile value is stored as an exponent of base 2. So:
|
|
|
|
// - 0 is an empty tile
|
|
|
|
// - 1 is a 2 (2^1)
|
|
|
|
// - 2 is a 4 (2^2)
|
|
|
|
// - 3 is a 8 (2^3)
|
|
|
|
// ... etc ...
|
|
|
|
// Also, the special value -1 is used to create tiles around the edge of the
|
|
|
|
// board which ensures tiles don't go beyond the bounds of the board.
|
|
|
|
typedef int8_t tTileValue;
|
|
|
|
|
|
|
|
|
|
|
|
static tTileValue gTileValues[NUM_TILES];
|
|
|
|
|
|
|
|
static char *gValueStrings[MAX_TILE_VALUE + 1] = {
|
|
|
|
" ", // 0 or empty tile
|
|
|
|
" 2 ",
|
|
|
|
" 4 ",
|
|
|
|
" 8 ",
|
|
|
|
" 16 ",
|
|
|
|
" 32 ",
|
|
|
|
" 64 ",
|
|
|
|
" 128 ",
|
|
|
|
" 256 ",
|
|
|
|
" 512 ",
|
|
|
|
" 1024 ",
|
|
|
|
" 2048 ",
|
|
|
|
" 4096 ",
|
|
|
|
" 8192 ",
|
|
|
|
" 16384 ",
|
|
|
|
" 32768 ",
|
|
|
|
" 65536 ",
|
|
|
|
" 131072 ",
|
|
|
|
" 262144 ",
|
2014-07-24 19:21:28 +00:00
|
|
|
" 524288 ",
|
|
|
|
"1048576 ",
|
|
|
|
"2097152 ",
|
|
|
|
"4194304 ",
|
|
|
|
"8388608 ",
|
|
|
|
"16777216",
|
|
|
|
"33554432",
|
|
|
|
"67108864"
|
2014-07-23 14:10:39 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
static tScore gValueScores[MAX_TILE_VALUE + 1] = {
|
|
|
|
0l, 2l, 4l, 8l, 16l, 32l, 64l, 128l, 256l, 512l, 1024l, 2048l, 4096l,
|
2014-07-24 19:21:28 +00:00
|
|
|
8192l, 16384l, 32768l, 65536l, 131072l, 262144l, 524288l, 1048576l,
|
|
|
|
2097152l, 4194304l, 8388608l, 16777216l, 33554432l, 67108864l };
|
|
|
|
|
|
|
|
|
|
|
|
struct tScoreRecord {
|
|
|
|
tScore currentScore;
|
|
|
|
tScore highScore;
|
|
|
|
tTileValue highestTile;
|
|
|
|
} gScoreRecord;
|
2014-07-23 14:10:39 +00:00
|
|
|
|
|
|
|
static uint8_t gNumEmptyTiles;
|
2014-07-24 19:21:28 +00:00
|
|
|
static bool gIsGameWon;
|
2014-07-23 14:10:39 +00:00
|
|
|
|
|
|
|
|
2014-07-25 19:55:41 +00:00
|
|
|
static void addRandomTile(void);
|
2014-07-23 16:28:10 +00:00
|
|
|
|
|
|
|
|
2014-07-24 05:31:37 +00:00
|
|
|
static tTileMoveCallback gTileMoveCallback = NULL;
|
|
|
|
static tNewTileCallback gNewTileCallback = NULL;
|
|
|
|
|
|
|
|
|
|
|
|
void initGameEngine(tTileMoveCallback tileMoveCallback,
|
|
|
|
tNewTileCallback newTileCallback)
|
|
|
|
{
|
2014-07-24 19:21:28 +00:00
|
|
|
FILE *scoreFile;
|
|
|
|
|
2014-07-24 05:31:37 +00:00
|
|
|
gTileMoveCallback = tileMoveCallback;
|
|
|
|
gNewTileCallback = newTileCallback;
|
2014-07-24 19:21:28 +00:00
|
|
|
|
|
|
|
gScoreRecord.highScore = 0;
|
|
|
|
gScoreRecord.highestTile = 0;
|
|
|
|
|
|
|
|
scoreFile = fopen("a2048score", "rb");
|
|
|
|
if (scoreFile != NULL) {
|
|
|
|
fread(&gScoreRecord, sizeof(gScoreRecord), 1, scoreFile);
|
|
|
|
fclose(scoreFile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void shutdownGameEngine(void)
|
|
|
|
{
|
|
|
|
FILE *scoreFile;
|
|
|
|
scoreFile = fopen("a2048score", "wb");
|
|
|
|
if (scoreFile != NULL) {
|
|
|
|
fwrite(&gScoreRecord, sizeof(gScoreRecord), 1, scoreFile);
|
|
|
|
fclose(scoreFile);
|
|
|
|
}
|
2014-07-24 05:31:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void newGame(void)
|
2014-07-23 13:41:36 +00:00
|
|
|
{
|
2014-07-23 14:10:39 +00:00
|
|
|
tPos pos;
|
|
|
|
tPos x;
|
|
|
|
tPos y;
|
|
|
|
|
2014-07-24 19:21:28 +00:00
|
|
|
gScoreRecord.currentScore = 0;
|
2014-07-23 14:10:39 +00:00
|
|
|
gNumEmptyTiles = 0;
|
2014-07-24 19:21:28 +00:00
|
|
|
gIsGameWon = false;
|
2014-07-23 14:10:39 +00:00
|
|
|
|
|
|
|
for (pos = 0; pos < NUM_TILES; pos++) {
|
|
|
|
x = POS_TO_X(pos);
|
|
|
|
y = POS_TO_Y(pos);
|
|
|
|
|
|
|
|
if ((x == 0) ||
|
|
|
|
(x > BOARD_SIZE) ||
|
|
|
|
(y == 0) ||
|
|
|
|
(y > BOARD_SIZE)) {
|
|
|
|
gTileValues[pos] = BLOCKED_TILE_VALUE;
|
|
|
|
} else {
|
|
|
|
gTileValues[pos] = 0;
|
|
|
|
gNumEmptyTiles++;
|
|
|
|
}
|
|
|
|
}
|
2014-07-23 14:45:10 +00:00
|
|
|
|
|
|
|
addRandomTile();
|
|
|
|
addRandomTile();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-07-25 19:55:41 +00:00
|
|
|
static tPos nextPosInDir(tPos pos, tDir dir)
|
2014-07-23 14:45:10 +00:00
|
|
|
{
|
|
|
|
tPos result = pos;
|
|
|
|
tPos nextPos;
|
|
|
|
tTileValue tileValue = gTileValues[pos];
|
|
|
|
tTileValue nextValue;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
nextPos = POS_IN_DIR(result, dir);
|
|
|
|
nextValue = gTileValues[nextPos];
|
|
|
|
|
|
|
|
if ((nextValue != 0) &&
|
|
|
|
(nextValue != tileValue))
|
|
|
|
break;
|
|
|
|
|
|
|
|
result = nextPos;
|
|
|
|
if (nextValue != 0)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return result;
|
2014-07-23 13:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-07-25 19:55:41 +00:00
|
|
|
static void increaseScore(tScore value)
|
2014-07-24 19:21:28 +00:00
|
|
|
{
|
|
|
|
gScoreRecord.currentScore += value;
|
|
|
|
if (gScoreRecord.currentScore > gScoreRecord.highScore)
|
|
|
|
gScoreRecord.highScore = gScoreRecord.currentScore;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-07-25 19:55:41 +00:00
|
|
|
static void updateMaxTile(tTileValue tileValue)
|
2014-07-24 19:21:28 +00:00
|
|
|
{
|
|
|
|
if (gScoreRecord.highestTile < tileValue)
|
|
|
|
gScoreRecord.highestTile = tileValue;
|
|
|
|
|
|
|
|
if (tileValue >= MAX_TILE_VALUE)
|
|
|
|
gIsGameWon = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-07-23 13:41:36 +00:00
|
|
|
void slideInDirection(tDir dir)
|
|
|
|
{
|
2014-07-23 14:45:10 +00:00
|
|
|
tPos pos;
|
|
|
|
tPos destPos;
|
|
|
|
int8_t incr;
|
2014-07-23 16:28:10 +00:00
|
|
|
tTileValue tileValue;
|
|
|
|
bool addNewTile = false;
|
2014-07-23 14:45:10 +00:00
|
|
|
|
2014-07-23 16:28:10 +00:00
|
|
|
if (dir < 0) {
|
2014-07-23 14:45:10 +00:00
|
|
|
pos = 0;
|
|
|
|
incr = 1;
|
|
|
|
} else {
|
|
|
|
pos = NUM_TILES - 1;
|
|
|
|
incr = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
for ( ; ((pos >= 0) && (pos < NUM_TILES)); pos += incr) {
|
2014-07-23 21:14:11 +00:00
|
|
|
if (gTileValues[pos] <= 0)
|
2014-07-23 14:45:10 +00:00
|
|
|
continue;
|
|
|
|
destPos = nextPosInDir(pos, dir);
|
|
|
|
if (destPos == pos)
|
|
|
|
continue;
|
|
|
|
|
2014-07-23 16:28:10 +00:00
|
|
|
addNewTile = true;
|
|
|
|
|
|
|
|
tileValue = gTileValues[destPos];
|
|
|
|
if (tileValue > 0) {
|
|
|
|
tileValue++;
|
2014-07-23 14:45:10 +00:00
|
|
|
gTileValues[destPos]++;
|
|
|
|
gNumEmptyTiles++;
|
2014-07-24 19:21:28 +00:00
|
|
|
increaseScore(gValueScores[tileValue]);
|
|
|
|
updateMaxTile(tileValue);
|
2014-07-23 16:28:10 +00:00
|
|
|
|
|
|
|
// This is a hack to prevent multiple merges from happening to
|
|
|
|
// the same tile in a single turn. We set the value to a high
|
|
|
|
// negative (< -1) and then flip the sign bit later.
|
|
|
|
gTileValues[destPos] = -tileValue;
|
2014-07-23 14:45:10 +00:00
|
|
|
} else {
|
|
|
|
gTileValues[destPos] = gTileValues[pos];
|
|
|
|
}
|
2014-07-24 05:31:37 +00:00
|
|
|
gTileMoveCallback(pos, destPos, gValueStrings[gTileValues[pos]]);
|
2014-07-23 14:45:10 +00:00
|
|
|
gTileValues[pos] = 0; // Empty the old position
|
|
|
|
}
|
2014-07-23 16:28:10 +00:00
|
|
|
|
|
|
|
for (pos = 0; pos < NUM_TILES; pos++) {
|
|
|
|
tileValue = gTileValues[pos];
|
|
|
|
if (tileValue < BLOCKED_TILE_VALUE) {
|
|
|
|
gTileValues[pos] = -tileValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (addNewTile)
|
|
|
|
addRandomTile();
|
2014-07-23 14:45:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-07-25 19:55:41 +00:00
|
|
|
static void addRandomTile(void)
|
2014-07-23 14:45:10 +00:00
|
|
|
{
|
|
|
|
int8_t randTile;
|
|
|
|
tPos pos;
|
|
|
|
|
|
|
|
if (gNumEmptyTiles == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
randTile = (rand() % gNumEmptyTiles);
|
|
|
|
|
|
|
|
for (pos = 0; pos < NUM_TILES; pos++) {
|
|
|
|
if (gTileValues[pos] == 0) {
|
|
|
|
if (randTile == 0) {
|
|
|
|
if (rand() < (RAND_MAX / 10))
|
|
|
|
gTileValues[pos] = 2; // This creates a 4
|
|
|
|
else
|
|
|
|
gTileValues[pos] = 1; // This creates a 2
|
|
|
|
gNumEmptyTiles--;
|
2014-07-24 05:31:37 +00:00
|
|
|
gNewTileCallback(pos, gValueStrings[gTileValues[pos]]);
|
2014-07-23 14:45:10 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
randTile--;
|
|
|
|
}
|
|
|
|
}
|
2014-07-23 13:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tScore currentScore(void)
|
|
|
|
{
|
2014-07-24 19:21:28 +00:00
|
|
|
return gScoreRecord.currentScore;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tScore highScore(void)
|
|
|
|
{
|
|
|
|
return gScoreRecord.highScore;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tScore highestTarget(void)
|
|
|
|
{
|
|
|
|
if (gScoreRecord.highestTile < 11)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
return gValueScores[gScoreRecord.highestTile];
|
2014-07-23 13:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tScore nextTarget(void)
|
|
|
|
{
|
2014-07-25 18:51:16 +00:00
|
|
|
tTileValue value = gScoreRecord.highestTile + 1;
|
2014-07-24 19:21:28 +00:00
|
|
|
|
|
|
|
if (value < 11)
|
|
|
|
value = 11;
|
|
|
|
|
|
|
|
return gValueScores[value];
|
2014-07-23 13:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool isGameWon(void)
|
|
|
|
{
|
2014-07-24 19:21:28 +00:00
|
|
|
return gIsGameWon;
|
2014-07-23 13:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool isGameLost(void)
|
|
|
|
{
|
2014-07-23 14:45:10 +00:00
|
|
|
tPos x;
|
|
|
|
tPos y;
|
|
|
|
tPos pos;
|
|
|
|
tTileValue tileValue;
|
|
|
|
|
|
|
|
if (gNumEmptyTiles > 0)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
for (x = 1; x <= BOARD_SIZE; x++) {
|
|
|
|
for (y = 1; y <= BOARD_SIZE; y++) {
|
|
|
|
pos = X_Y_TO_POS(x, y);
|
|
|
|
tileValue = gTileValues[pos];
|
|
|
|
|
|
|
|
assert(tileValue > 0);
|
|
|
|
|
|
|
|
// If a tile value matches another adjacent tile value, then there
|
|
|
|
// are still moves.
|
|
|
|
if (tileValue == gTileValues[POS_IN_DIR(pos, DIR_DOWN)])
|
|
|
|
return false;
|
|
|
|
if (tileValue == gTileValues[POS_IN_DIR(pos, DIR_RIGHT)])
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Because we iterate over all tiles, we only need to check two of
|
|
|
|
// the four directions. That will compare all possible pairs. It
|
|
|
|
// will also check the edge against the blocking tiles but whatever.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we get to here, there were no matching tiles so there are no available
|
|
|
|
// moves.
|
|
|
|
return true;
|
2014-07-23 13:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
char *tileStringForPos(tPos x, tPos y)
|
|
|
|
{
|
2014-07-24 05:31:37 +00:00
|
|
|
tTileValue value = gTileValues[X_Y_TO_POS(x, y)];
|
|
|
|
|
|
|
|
if (value < 0)
|
|
|
|
value *= -1;
|
|
|
|
|
|
|
|
return gValueStrings[value];
|
2014-07-23 13:41:36 +00:00
|
|
|
}
|