mirror of https://gitlab.com/nakst/essence
402 lines
12 KiB
C++
402 lines
12 KiB
C++
// This file is part of the Essence operating system.
|
|
// It is released under the terms of the MIT license -- see LICENSE.md.
|
|
// Written by: nakst.
|
|
|
|
#include <essence.h>
|
|
#include <shared/strings.cpp>
|
|
|
|
#define TILE_COUNT (4)
|
|
#define TILE_SIZE ((int32_t) (75 * scale))
|
|
#define TILE_GAP ((int32_t) (10 * scale))
|
|
#define CELL_FILL (0xFFEEEEEE)
|
|
#define TILE_TEXT_COLOR (0xFFFFFF)
|
|
#define TILE_TEXT_GLOW (0x000000)
|
|
#define MAIN_AREA_SIZE() (TILE_SIZE * TILE_COUNT + TILE_GAP * (TILE_COUNT + 1))
|
|
#define MAIN_AREA_FILL (0xFFFFFFFF)
|
|
#define MAIN_AREA_BORDER (0xFFCCCCCC)
|
|
#define ANIMATION_TIME (100)
|
|
|
|
#define TILE_BOUNDS(x, y) ES_RECT_4PD(mainArea.l + TILE_GAP * (x + 1) + TILE_SIZE * x, mainArea.t + TILE_GAP * (y + 1) + TILE_SIZE * y, TILE_SIZE, TILE_SIZE)
|
|
|
|
#define SETTINGS_FILE "|Settings:/Default.dat"
|
|
|
|
struct AnimatingTile {
|
|
float sourceOpacity, targetOpacity;
|
|
uint8_t sourceX, targetX;
|
|
uint8_t sourceY, targetY;
|
|
uint8_t sourceNumber;
|
|
};
|
|
|
|
const uint32_t tileColors[] = {
|
|
0x000000, 0x7CB5E2, 0x4495D4, 0x2F6895,
|
|
0xF5BD70, 0xF2A032, 0xE48709, 0xE37051,
|
|
0xDE5833, 0xBD4A2B, 0x5454DA, 0x3B3C99,
|
|
0xFFD700,
|
|
};
|
|
|
|
const uint32_t tileTextSizes[] = {
|
|
0, 18, 18, 18, 18, 18, 18, 18,
|
|
18, 18, 16, 16, 16, 16, 14, 14,
|
|
14, 12, 12, 12, 10, 10, 10, 10,
|
|
8, 8, 8, 6, 6, 6, 6, 6,
|
|
};
|
|
|
|
AnimatingTile animatingTiles[TILE_COUNT * TILE_COUNT + 1];
|
|
size_t animatingTileCount;
|
|
float animationTimeMs;
|
|
|
|
uint8_t grid[TILE_COUNT][TILE_COUNT];
|
|
int32_t score, highScore;
|
|
|
|
EsInstance *instance;
|
|
EsElement *gameArea;
|
|
EsTextDisplay *scoreDisplay, *highScoreDisplay;
|
|
|
|
void SaveConfiguration() {
|
|
EsBuffer buffer = {};
|
|
buffer.canGrow = true;
|
|
EsBufferWriteInt32Endian(&buffer, highScore);
|
|
EsBufferWriteInt32Endian(&buffer, score);
|
|
EsBufferWrite(&buffer, grid, sizeof(grid));
|
|
EsFileWriteAll(EsLiteral(SETTINGS_FILE), buffer.out, buffer.position);
|
|
EsHeapFree(buffer.out);
|
|
}
|
|
|
|
bool MoveTiles(intptr_t dx, intptr_t dy, bool speculative) {
|
|
uint8_t undo[TILE_COUNT][TILE_COUNT];
|
|
EsMemoryCopy(undo, grid, sizeof(undo));
|
|
|
|
bool validMove = false;
|
|
|
|
for (uintptr_t p = 0; p < TILE_COUNT; p++) {
|
|
bool doneMerge = false;
|
|
|
|
for (uintptr_t q = 0; q < TILE_COUNT; q++) {
|
|
// The tile being moved.
|
|
intptr_t x = dx ? q : p;
|
|
intptr_t y = dx ? p : q;
|
|
if (dx > 0) x = TILE_COUNT - 1 - x;
|
|
if (dy > 0) y = TILE_COUNT - 1 - y;
|
|
|
|
// Ignore empty spaces.
|
|
if (!grid[x][y]) continue;
|
|
|
|
// Setup the animation.
|
|
if (!speculative) {
|
|
AnimatingTile *animation = &animatingTiles[animatingTileCount];
|
|
animation->sourceOpacity = 1;
|
|
animation->targetOpacity = 1;
|
|
animation->sourceX = x;
|
|
animation->sourceY = y;
|
|
animation->sourceNumber = grid[x][y];
|
|
}
|
|
|
|
while (true) {
|
|
// The position to move the tile to.
|
|
intptr_t nx = x + dx;
|
|
intptr_t ny = y + dy;
|
|
|
|
// If the next position is outside the grid, stop.
|
|
if (nx < 0 || nx >= TILE_COUNT) break;
|
|
if (ny < 0 || ny >= TILE_COUNT) break;
|
|
|
|
if (grid[nx][ny]) {
|
|
// If tiles are different, we can't merge; stop.
|
|
if (grid[nx][ny] != grid[x][y]) break;
|
|
|
|
// If there's already been a merge this band, stop.
|
|
if (doneMerge) break;
|
|
|
|
// Merge the tiles.
|
|
grid[nx][ny]++;
|
|
grid[x][y] = 0;
|
|
doneMerge = true;
|
|
|
|
// Add the score.
|
|
if (!speculative) score += 1 << grid[nx][ny];
|
|
} else {
|
|
// Slide the tile.
|
|
grid[nx][ny] = grid[x][y];
|
|
grid[x][y] = 0;
|
|
}
|
|
|
|
// Update the position.
|
|
x = nx;
|
|
y = ny;
|
|
validMove = true;
|
|
}
|
|
|
|
// Set the animation target.
|
|
if (!speculative) {
|
|
AnimatingTile *animation = &animatingTiles[animatingTileCount];
|
|
animation->targetX = x;
|
|
animation->targetY = y;
|
|
animatingTileCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (speculative) {
|
|
EsMemoryCopy(grid, undo, sizeof(undo));
|
|
}
|
|
|
|
return validMove;
|
|
}
|
|
|
|
void SpawnTile() {
|
|
bool full = true;
|
|
|
|
for (uintptr_t i = 0; i < TILE_COUNT; i++) {
|
|
for (uintptr_t j = 0; j < TILE_COUNT; j++) {
|
|
if (!grid[i][j]) {
|
|
full = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (full) {
|
|
// The grid is full.
|
|
return;
|
|
}
|
|
|
|
while (true) {
|
|
uintptr_t x = EsRandomU64() % TILE_COUNT;
|
|
uintptr_t y = EsRandomU64() % TILE_COUNT;
|
|
|
|
if (!grid[x][y]) {
|
|
grid[x][y] = EsRandomU8() < 25 ? 2 : 1;
|
|
|
|
// Setup the animation.
|
|
AnimatingTile *animation = &animatingTiles[animatingTileCount];
|
|
animation->sourceOpacity = 0;
|
|
animation->targetOpacity = 1;
|
|
animation->sourceX = x;
|
|
animation->targetX = x;
|
|
animation->sourceY = y;
|
|
animation->targetY = y;
|
|
animation->sourceNumber = grid[x][y];
|
|
animatingTileCount++;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!MoveTiles(-1, 0, true) && !MoveTiles(1, 0, true) && !MoveTiles(0, -1, true) && !MoveTiles(0, 1, true)) {
|
|
// No moves are possible.
|
|
if (highScore < score) {
|
|
EsDialogShow(instance->window, INTERFACE_STRING(Game2048GameOver), INTERFACE_STRING(Game2048NewHighScore),
|
|
ES_ICON_DIALOG_INFORMATION, ES_DIALOG_ALERT_OK_BUTTON);
|
|
} else {
|
|
EsDialogShow(instance->window, INTERFACE_STRING(Game2048GameOver), INTERFACE_STRING(Game2048GameOverExplanation),
|
|
ES_ICON_DIALOG_INFORMATION, ES_DIALOG_ALERT_OK_BUTTON);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Update(intptr_t dx, intptr_t dy) {
|
|
animatingTileCount = 0;
|
|
animationTimeMs = 0;
|
|
|
|
if (dx || dy) {
|
|
if (!MoveTiles(dx, dy, false)) {
|
|
return;
|
|
}
|
|
|
|
SpawnTile();
|
|
}
|
|
|
|
EsElementStartAnimating(gameArea);
|
|
|
|
if (score > highScore) {
|
|
highScore = score;
|
|
}
|
|
|
|
char buffer[64];
|
|
size_t bytes = EsStringFormat(buffer, sizeof(buffer), "%d", score);
|
|
EsTextDisplaySetContents(scoreDisplay, buffer, bytes);
|
|
bytes = EsStringFormat(buffer, sizeof(buffer), interfaceString_Game2048HighScore, highScore);
|
|
EsTextDisplaySetContents(highScoreDisplay, buffer, bytes);
|
|
}
|
|
|
|
void DrawTileText(EsPainter *painter, EsElement *element, EsRectangle bounds, float opacity, uint8_t number) {
|
|
char buffer[64];
|
|
size_t bytes = EsStringFormat(buffer, sizeof(buffer), "%d", 1 << (uint32_t) number);
|
|
uint32_t alpha = ((uint32_t) (255 * opacity) << 24);
|
|
EsTextStyle style = { .font = { .family = ES_FONT_SANS, .weight = ES_FONT_SEMIBOLD }, .size = (uint16_t) tileTextSizes[number] };
|
|
|
|
if (number >= 12) {
|
|
style.color = TILE_TEXT_GLOW | alpha;
|
|
style.blur = 3;
|
|
EsDrawTextSimple(painter, element, bounds, buffer, bytes, style, ES_TEXT_H_CENTER | ES_TEXT_V_CENTER);
|
|
}
|
|
|
|
style.color = TILE_TEXT_COLOR | alpha;
|
|
style.blur = 0;
|
|
EsDrawTextSimple(painter, element, bounds, buffer, bytes, style, ES_TEXT_H_CENTER | ES_TEXT_V_CENTER);
|
|
}
|
|
|
|
void DrawTile(EsPainter *painter, EsElement *element, uint8_t sourceNumber, uint8_t targetNumber,
|
|
EsRectangle bounds, float opacity, EsCornerRadii cornerRadii, float progress) {
|
|
size_t tileColorCount = sizeof(tileColors) / sizeof(tileColors[0]);
|
|
uint32_t sourceColor = sourceNumber >= tileColorCount ? tileColors[tileColorCount - 1] : tileColors[sourceNumber];
|
|
uint32_t targetColor = targetNumber >= tileColorCount ? tileColors[tileColorCount - 1] : tileColors[targetNumber];
|
|
uint32_t fill = EsColorInterpolate(sourceColor, targetColor, progress) | ((uint32_t) (255 * opacity) << 24);
|
|
float scale = EsElementGetScaleFactor(element);
|
|
|
|
EsDrawRoundedRectangle(painter, bounds, fill, EsColorBlend(fill, 0x20000000, true), ES_RECT_4(0, 0, 0, 3 * scale), cornerRadii);
|
|
|
|
if (sourceNumber == targetNumber) {
|
|
progress = 1.0f;
|
|
}
|
|
|
|
DrawTileText(painter, element, bounds, progress, targetNumber);
|
|
|
|
if (sourceNumber != targetNumber && targetNumber) {
|
|
DrawTileText(painter, element, bounds, 1.0f - progress, sourceNumber);
|
|
}
|
|
}
|
|
|
|
int GameAreaMessage(EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsPainter *painter = message->painter;
|
|
float scale = EsElementGetScaleFactor(element);
|
|
|
|
EsCornerRadii cornerRadii = { (uint32_t) (3 * scale), (uint32_t) (3 * scale), (uint32_t) (3 * scale), (uint32_t) (3 * scale) };
|
|
|
|
EsRectangle bounds = EsPainterBoundsInset(painter);
|
|
EsRectangle mainArea = EsRectangleFit(bounds, ES_RECT_1S(MAIN_AREA_SIZE()), false);
|
|
EsDrawRoundedRectangle(painter, mainArea, MAIN_AREA_FILL, MAIN_AREA_BORDER, ES_RECT_1(scale * 1), cornerRadii);
|
|
|
|
float progress = animationTimeMs / ANIMATION_TIME;
|
|
bool animationComplete = progress == 1.0;
|
|
|
|
progress -= 1.0;
|
|
progress = 1 + progress * progress * progress;
|
|
|
|
for (uintptr_t j = 0; j < TILE_COUNT; j++) {
|
|
for (uintptr_t i = 0; i < TILE_COUNT; i++) {
|
|
if (grid[i][j] && animationComplete) {
|
|
DrawTile(painter, element, grid[i][j], grid[i][j], TILE_BOUNDS(i, j), 1.0f, cornerRadii, 1.0f);
|
|
} else {
|
|
EsDrawRoundedRectangle(painter, TILE_BOUNDS(i, j), CELL_FILL, 0, ES_RECT_1(0), cornerRadii);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!animationComplete) {
|
|
for (uintptr_t i = 0; i < animatingTileCount; i++) {
|
|
AnimatingTile *tile = &animatingTiles[i];
|
|
EsRectangle bounds = EsRectangleLinearInterpolate(TILE_BOUNDS(tile->sourceX, tile->sourceY), TILE_BOUNDS(tile->targetX, tile->targetY), progress);
|
|
float opacity = (tile->targetOpacity - tile->sourceOpacity) * progress + tile->sourceOpacity;
|
|
DrawTile(painter, element, tile->sourceNumber, grid[tile->targetX][tile->targetY], bounds, opacity, cornerRadii, progress);
|
|
}
|
|
}
|
|
} else if (message->type == ES_MSG_ANIMATE) {
|
|
animationTimeMs += message->animate.deltaMs;
|
|
|
|
if (animationTimeMs > ANIMATION_TIME) {
|
|
animationTimeMs = ANIMATION_TIME;
|
|
message->animate.complete = true;
|
|
} else {
|
|
message->animate.complete = false;
|
|
}
|
|
|
|
EsElementRepaint(element);
|
|
} else if (message->type == ES_MSG_KEY_TYPED) {
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_ARROW) {
|
|
Update(-1, 0);
|
|
} else if (message->keyboard.scancode == ES_SCANCODE_RIGHT_ARROW) {
|
|
Update(1, 0);
|
|
} else if (message->keyboard.scancode == ES_SCANCODE_UP_ARROW) {
|
|
Update(0, -1);
|
|
} else if (message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW) {
|
|
Update(0, 1);
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (message->type == ES_MSG_GET_WIDTH || message->type == ES_MSG_GET_HEIGHT) {
|
|
float scale = EsElementGetScaleFactor(element);
|
|
message->measure.width = message->measure.height = MAIN_AREA_SIZE();
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
int InfoPanelMessage(EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_GET_WIDTH || message->type == ES_MSG_GET_HEIGHT) {
|
|
float scale = EsElementGetScaleFactor(element);
|
|
message->measure.width = MAIN_AREA_SIZE() / 2;
|
|
message->measure.height = MAIN_AREA_SIZE();
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
void NewGameCommand(EsInstance *, EsElement *, EsCommand *) {
|
|
SaveConfiguration();
|
|
EsElementStartTransition(gameArea, ES_TRANSITION_SLIDE_UP);
|
|
EsMemoryZero(grid, sizeof(grid));
|
|
score = 0;
|
|
Update(0, 0);
|
|
SpawnTile();
|
|
EsElementFocus(gameArea);
|
|
}
|
|
|
|
void ProcessApplicationMessage(EsMessage *message) {
|
|
if (message->type == ES_MSG_INSTANCE_CREATE) {
|
|
// Create the instance.
|
|
instance = EsInstanceCreate(message, EsLiteral("2048"));
|
|
EsWindowSetIcon(instance->window, ES_ICON_APPLICATIONS_OTHER);
|
|
|
|
// Main horizontal stack.
|
|
EsPanel *panel = EsPanelCreate(instance->window, ES_CELL_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_WINDOW_BACKGROUND);
|
|
EsSpacerCreate(panel, ES_CELL_FILL);
|
|
gameArea = EsCustomElementCreate(panel, ES_ELEMENT_FOCUSABLE);
|
|
gameArea->messageUser = GameAreaMessage;
|
|
EsSpacerCreate(panel, ES_FLAGS_DEFAULT, 0, 30, 0);
|
|
EsPanel *info = EsPanelCreate(panel);
|
|
EsSpacerCreate(panel, ES_CELL_FILL);
|
|
|
|
// Info panel.
|
|
info->messageUser = InfoPanelMessage;
|
|
EsTextDisplayCreate(info, ES_CELL_H_FILL, ES_STYLE_TEXT_LABEL_SECONDARY, INTERFACE_STRING(Game2048Score));
|
|
scoreDisplay = EsTextDisplayCreate(info, ES_CELL_H_FILL, ES_STYLE_TEXT_HEADING0);
|
|
EsSpacerCreate(info, ES_FLAGS_DEFAULT, 0, 0, 10);
|
|
highScoreDisplay = EsTextDisplayCreate(info, ES_CELL_H_FILL | ES_TEXT_DISPLAY_RICH_TEXT, ES_STYLE_TEXT_LABEL_SECONDARY);
|
|
EsSpacerCreate(info, ES_CELL_FILL);
|
|
EsTextDisplayCreate(info, ES_CELL_H_FILL | ES_TEXT_DISPLAY_RICH_TEXT, ES_STYLE_TEXT_PARAGRAPH_SECONDARY, INTERFACE_STRING(Game2048Instructions));
|
|
EsSpacerCreate(info, ES_FLAGS_DEFAULT, 0, 0, 10);
|
|
EsButton *newGame = EsButtonCreate(info, ES_CELL_H_LEFT | ES_BUTTON_NOT_FOCUSABLE, 0, INTERFACE_STRING(Game2048NewGame));
|
|
newGame->accessKey = 'N';
|
|
EsButtonOnCommand(newGame, NewGameCommand);
|
|
|
|
// Start the game!
|
|
EsElementFocus(gameArea);
|
|
Update(0, 0);
|
|
animationTimeMs = ANIMATION_TIME;
|
|
} else if (message->type == ES_MSG_APPLICATION_EXIT) {
|
|
SaveConfiguration();
|
|
}
|
|
}
|
|
|
|
void _start() {
|
|
_init();
|
|
|
|
EsBuffer buffer = {};
|
|
uint8_t *settings = (uint8_t *) EsFileReadAll(EsLiteral(SETTINGS_FILE), &buffer.bytes);
|
|
buffer.in = settings;
|
|
highScore = EsBufferReadInt32Endian(&buffer, 0);
|
|
score = EsBufferReadInt32Endian(&buffer, 0);
|
|
EsBufferReadInto(&buffer, grid, sizeof(grid));
|
|
EsHeapFree(settings);
|
|
if (!settings) SpawnTile();
|
|
|
|
while (true) {
|
|
ProcessApplicationMessage(EsMessageReceive());
|
|
}
|
|
}
|