summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFranklin Wei <frankhwei536@gmail.com>2014-06-29 14:49:07 -0400
committerMichael Giacomelli <giac2000@hotmail.com>2014-08-12 00:01:24 +0200
commitb61553c2b18d37566b6561e413ef1b49de39c0a7 (patch)
tree69c93171d9cc7675b05f66925c7cbd42ff73c9d9
parent9a3400a4a667e32d1dd0b50364b083787ff63320 (diff)
downloadrockbox-b61553c.tar.gz
rockbox-b61553c.tar.bz2
rockbox-b61553c.zip
Added 2048 game
Change-Id: I4012dca4f93ca0db386a454635534f648ba906e9 Reviewed-on: http://gerrit.rockbox.org/888 Reviewed-by: Michael Giacomelli <giac2000@hotmail.com> Tested: Michael Giacomelli <giac2000@hotmail.com>
-rw-r--r--apps/plugins/2048.c916
-rw-r--r--apps/plugins/CATEGORIES1
-rw-r--r--apps/plugins/SOURCES2
-rw-r--r--apps/plugins/bitmaps/native/SOURCES22
-rw-r--r--apps/plugins/bitmaps/native/_2048_background.103x103x24.bmpbin0 -> 32258 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_background.121x121x24.bmpbin0 -> 44166 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_background.168x168x24.bmpbin0 -> 84794 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_background.224x224x24.bmpbin0 -> 150650 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_background.56x56x24.bmpbin0 -> 9530 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_tiles.12x12x24.bmpbin0 -> 7898 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_tiles.22x22x24.bmpbin0 -> 26258 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_tiles.26x26x24.bmpbin0 -> 36626 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_tiles.36x36x24.bmpbin0 -> 70106 bytes
-rw-r--r--apps/plugins/bitmaps/native/_2048_tiles.48x48x24.bmpbin0 -> 124538 bytes
-rw-r--r--docs/CREDITS1
-rw-r--r--manual/plugins/2048.tex16
-rw-r--r--manual/plugins/main.tex2
17 files changed, 960 insertions, 0 deletions
diff --git a/apps/plugins/2048.c b/apps/plugins/2048.c
new file mode 100644
index 0000000000..9a74b0ff38
--- /dev/null
+++ b/apps/plugins/2048.c
@@ -0,0 +1,916 @@
+/***************************************************************************
+ * __________ __ ___.
+ * Open \______ \ ____ ____ | | _\_ |__ _______ ___
+ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
+ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
+ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
+ * \/ \/ \/ \/ \/
+ * $Id$
+ *
+ * Copyright (C) 2014 Franklin Wei
+ *
+ * Clone of 2048 by Gabriele Cirulli
+ *
+ * Thanks to [Saint], saratoga, and gevaerts for answering all my n00b
+ * questions :)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ***************************************************************************/
+
+/* TODO
+ * Sounds!
+ * Better animations!
+ */
+
+/* includes */
+
+#include <plugin.h>
+
+#include "lib/display_text.h"
+
+#include "lib/helper.h"
+#include "lib/highscore.h"
+#include "lib/playback_control.h"
+#include "lib/pluginlib_actions.h"
+#include "lib/pluginlib_exit.h"
+
+#ifdef HAVE_LCD_BITMAP
+#include "pluginbitmaps/_2048_background.h"
+#include "pluginbitmaps/_2048_tiles.h"
+#endif
+
+/* defines */
+
+#define ANIM_SLEEPTIME (HZ/20)
+#define GRID_SIZE 4
+#define HISCORES_FILE PLUGIN_GAMES_DATA_DIR "/2048.score"
+#define NUM_SCORES 5
+#define NUM_STARTING_TILES 2
+#define RESUME_FILE PLUGIN_GAMES_DATA_DIR "/2048.save"
+#define WHAT_FONT FONT_UI
+#define SPACES (GRID_SIZE*GRID_SIZE)
+#define MIN_SPACE (BMPHEIGHT__2048_tiles*0.134) /* space between tiles */
+#define MAX_UNDOS 64
+#define VERT_SPACING 4
+#define WINNING_TILE 2048
+
+/* screen-specific configuration */
+
+#if LCD_WIDTH<LCD_HEIGHT
+/* tall screens */
+#define TITLE_X 0
+#define TITLE_Y 0
+#define BASE_Y (BMPHEIGHT__2048_tiles*1.5)
+#define BASE_X (BMPHEIGHT__2048_tiles*.5-MIN_SPACE)
+#define SCORE_X 0
+#define SCORE_Y (max_numeral_height)
+#define BEST_SCORE_X 0
+#define BEST_SCORE_Y (2*max_numeral_height)
+#else
+/* wide screens or square screens*/
+#define TITLE_X 0
+#define TITLE_Y 0
+#define BASE_X (LCD_WIDTH-(GRID_SIZE*BMPHEIGHT__2048_tiles)-(((GRID_SIZE+1)*MIN_SPACE)))
+#define BASE_Y (BMPHEIGHT__2048_tiles*.5-MIN_SPACE)
+#define SCORE_X 0
+#define SCORE_Y (max_numeral_height)
+#define BEST_SCORE_X 0
+#define BEST_SCORE_Y (2*max_numeral_height)
+#endif
+
+#define BACKGROUND_X (BASE_X-MIN_SPACE)
+#define BACKGROUND_Y (BASE_Y-MIN_SPACE)
+
+/* key mappings */
+
+#define KEY_UP PLA_UP
+#define KEY_DOWN PLA_DOWN
+#define KEY_LEFT PLA_LEFT
+#define KEY_RIGHT PLA_RIGHT
+#define KEY_EXIT PLA_CANCEL
+#define KEY_UNDO PLA_SELECT
+
+/* colors */
+
+#define BACKGROUND (LCD_RGBPACK(0xfa, 0xf8, 0xef))
+#define BOARD_BACKGROUND (LCD_RGBPACK(0xbb, 0xad, 0xa0))
+#define TEXT_COLOR (LCD_RGBPACK(0x77, 0x6e, 0x65))
+
+static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
+
+/* game data */
+struct game_ctx_t {
+ int grid[GRID_SIZE][GRID_SIZE];
+ int score;
+ int cksum; /* sum of grid, XORed by score */
+ bool already_won;
+};
+
+static struct game_ctx_t ctx_data;
+/* use a pointer to make save/load easier */
+static struct game_ctx_t *ctx=&ctx_data;
+
+/* temporary data */
+static bool merged_grid[GRID_SIZE][GRID_SIZE];
+static int old_grid[GRID_SIZE][GRID_SIZE];
+static int max_numeral_height=-1;
+static bool loaded=false;
+
+/* first init_game will set this, when it is exceeded, it will be updated in the slide functions */
+static int best_score;
+
+static bool abnormal_exit=true;
+static struct highscore highscores[NUM_SCORES];
+
+/* returns a random int between min and max */
+static inline int rand_range(int min, int max)
+{
+ return rb->rand()%(max-min+1)+min;
+}
+
+/* prepares to exit */
+static void cleanup(void)
+{
+ backlight_use_settings();
+}
+
+/* returns 2 or 4 */
+static inline int rand_2_or_4(void)
+{
+ /* 1 in 10 chance of a four */
+ if(rb->rand()%10==0)
+ return 4;
+ else
+ return 2;
+}
+
+/* display the help text */
+static bool do_help(void)
+{
+#ifdef HAVE_LCD_COLOR
+ rb->lcd_set_foreground(LCD_WHITE);
+ rb->lcd_set_background(LCD_BLACK);
+#endif
+ rb->lcd_setfont(FONT_UI);
+ char* help_text[]= {"2048", "", "Aim",
+ "", "Join", "the", "numbers", "to", "get", "to", "the", "2048", "tile!", "", "",
+ "How", "to", "Play", "",
+ "", "Use", "the", "directional", "keys", "to", "move", "the", "tiles.", "When",
+ "two", "tiles", "with", "the", "same", "number", "touch,", "they", "merge", "into", "one!"};
+ struct style_text style[]={
+ {0, TEXT_CENTER|TEXT_UNDERLINE},
+ {2, C_RED},
+ {15, C_RED}, {16, C_RED}, {17,C_RED},
+ LAST_STYLE_ITEM
+ };
+ return display_text(ARRAYLEN(help_text), help_text, style,NULL,true);
+}
+
+/*** the logic for sliding ***/
+
+/* this is the helper function that does the actual tile moving */
+
+static inline void slide_internal(int startx, int starty,
+ int stopx, int stopy,
+ int dx, int dy,
+ int lookx, int looky)
+{
+ int best_score_before=best_score;
+ for(int y=starty;y!=stopy;y+=dy)
+ {
+ for(int x=startx;x!=stopx;x+=dx)
+ {
+ if(ctx->grid[x+lookx][y+looky]==ctx->grid[x][y] && ctx->grid[x][y] && !merged_grid[x+lookx][y+looky] && !merged_grid[x][y]) /* Slide into */
+ {
+ /* Each merged tile cannot be merged again */
+ merged_grid[x+lookx][y+looky]=true;
+ ctx->grid[x+lookx][y+looky]=2*ctx->grid[x][y];
+ ctx->score+=ctx->grid[x+lookx][y+looky];
+ ctx->grid[x][y]=0;
+ }
+ else if(ctx->grid[x+lookx][y+looky]==0) /* Empty! */
+ {
+ ctx->grid[x+lookx][y+looky]=ctx->grid[x][y];
+ ctx->grid[x][y]=0;
+ }
+ }
+ }
+ if(ctx->score>best_score_before)
+ best_score=ctx->score;
+}
+
+/* these functions move each tile 1 space in the direction specified via calls to slide_internal */
+
+/* Up
+ 0
+ 1 ^ ^ ^ ^
+ 2 ^ ^ ^ ^
+ 3 ^ ^ ^ ^
+ 0 1 2 3
+*/
+static void up(void)
+{
+ slide_internal(0, 1, /* start values */
+ GRID_SIZE, GRID_SIZE, /* stop values */
+ 1, 1, /* delta values */
+ 0, -1); /* lookahead values */
+}
+/* Down
+ 0 v v v v
+ 1 v v v v
+ 2 v v v v
+ 3
+ 0 1 2 3
+*/
+static void down(void)
+{
+ slide_internal(0, GRID_SIZE-2,
+ GRID_SIZE, -1,
+ 1, -1,
+ 0, 1);
+}
+/* Left
+ 0 < < <
+ 1 < < <
+ 2 < < <
+ 3 < < <
+ 0 1 2 3
+*/
+static void left(void)
+{
+ slide_internal(1, 0,
+ GRID_SIZE, GRID_SIZE,
+ 1, 1,
+ -1, 0);
+}
+/* Right
+ 0 > > >
+ 1 > > >
+ 2 > > >
+ 3 > > >
+ 0 1 2 3
+*/
+static void right(void)
+{
+ slide_internal(GRID_SIZE-2, 0, /* start */
+ -1, GRID_SIZE, /* stop */
+ -1, 1, /* delta */
+ 1, 0); /* lookahead */
+}
+
+/* slightly modified version of base 2 log, returns 1 when given zero, and log2(n)+1 for anything else */
+
+static inline int ilog2(int n)
+{
+ if(n==0)
+ return 1;
+ int log=0;
+ while(n>1)
+ {
+ n>>=1;
+ ++log;
+ }
+ return log+1;
+}
+static void draw(void)
+{
+#ifdef HAVE_LCD_COLOR
+ rb->lcd_set_background(BACKGROUND);
+#endif
+ rb->lcd_clear_display();
+
+ /* draw the background */
+
+ rb->lcd_bitmap(_2048_background, BACKGROUND_X, BACKGROUND_Y, BMPWIDTH__2048_background, BMPWIDTH__2048_background);
+
+ /*
+ grey_gray_bitmap(_2048_background, BACKGROUND_X, BACKGROUND_Y, BMPWIDTH__2048_background, BMPHEIGHT__2048_background);
+ */
+
+ /* draw the grid */
+
+ for(int y=0;y<GRID_SIZE;++y)
+ {
+ for(int x=0;x<GRID_SIZE;++x)
+ {
+ rb->lcd_bitmap_part(_2048_tiles, /* source */
+ BMPWIDTH__2048_tiles-BMPHEIGHT__2048_tiles*ilog2(ctx->grid[x][y]), 0, /* source upper left corner */
+ STRIDE(SCREEN_MAIN, BMPWIDTH__2048_tiles, BMPHEIGHT__2048_tiles), /* stride */
+ (BMPHEIGHT__2048_tiles+MIN_SPACE)*x+BASE_X, (BMPHEIGHT__2048_tiles+MIN_SPACE)*y+BASE_Y, /* dest upper-left corner */
+ BMPHEIGHT__2048_tiles, BMPHEIGHT__2048_tiles); /* size of the cut section */
+ }
+ }
+ /* draw the title */
+ char buf[32];
+#ifdef HAVE_LCD_COLOR
+ rb->lcd_set_foreground(TEXT_COLOR);
+#endif
+ rb->snprintf(buf, 31, "%d", WINNING_TILE);
+
+ /* check if the title will go into the grid */
+ int w, h;
+ rb->lcd_setfont(FONT_UI);
+ rb->font_getstringsize(buf, &w, &h, FONT_UI);
+ if(w+TITLE_X>=BACKGROUND_X && h+TITLE_Y>=BACKGROUND_Y)
+ {
+ /* if it goes into the grid, use the system font, which should be smaller */
+ rb->lcd_setfont(FONT_SYSFIXED);
+ }
+ rb->lcd_putsxy(TITLE_X, TITLE_Y, buf);
+
+ /* draw the score */
+ rb->snprintf(buf, 31, "Score: %d", ctx->score);
+#ifdef HAVE_LCD_COLOR
+ rb->lcd_set_foreground(LCD_WHITE);
+ rb->lcd_set_background(BOARD_BACKGROUND);
+#endif
+ rb->lcd_setfont(FONT_UI);
+ rb->font_getstringsize(buf, &w, &h, FONT_UI);
+ if(w+SCORE_X>=BACKGROUND_X && h+SCORE_Y>=BACKGROUND_Y)
+ {
+ /* score overflows */
+ /* first see if it fits with Score: and FONT_SYSFIXED */
+ rb->lcd_setfont(FONT_SYSFIXED);
+ rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
+ if(w+SCORE_X < BACKGROUND_X)
+ /* it fits, go and draw it */
+ goto draw_lbl;
+
+ /* now try with S: and FONT_UI */
+ rb->snprintf(buf, 31, "S: %d", ctx->score);
+ rb->font_getstringsize(buf, &w, &h, FONT_UI);
+ rb->lcd_setfont(FONT_UI);
+ if(w+SCORE_X<BACKGROUND_X)
+ goto draw_lbl;
+
+ /* now try with S: and FONT_SYSFIXED */
+ rb->snprintf(buf, 31, "S: %d", ctx->score);
+ rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
+ rb->lcd_setfont(FONT_SYSFIXED);
+ if(w+SCORE_X<BACKGROUND_X)
+ goto draw_lbl;
+
+ /* then try without Score: and FONT_UI */
+ rb->snprintf(buf, 31, "%d", ctx->score);
+ rb->font_getstringsize(buf, &w, &h, FONT_UI);
+ rb->lcd_setfont(FONT_UI);
+ if(w+SCORE_X<BACKGROUND_X)
+ goto draw_lbl;
+
+ /* as a last resort, don't use Score: and use the system font */
+ rb->snprintf(buf, 31, "%d", ctx->score);
+ rb->lcd_setfont(FONT_SYSFIXED);
+ }
+draw_lbl:
+ rb->lcd_putsxy(SCORE_X, SCORE_Y, buf);
+
+ /* draw the best score */
+
+ rb->snprintf(buf, 31, "Best: %d", best_score);
+#ifdef HAVE_LCD_COLOR
+ rb->lcd_set_foreground(LCD_WHITE);
+ rb->lcd_set_background(BOARD_BACKGROUND);
+#endif
+ rb->lcd_setfont(FONT_UI);
+ rb->font_getstringsize(buf, &w, &h, FONT_UI);
+ if(w+BEST_SCORE_X>=BACKGROUND_X && h+BEST_SCORE_Y>=BACKGROUND_Y)
+ {
+ /* score overflows */
+ /* first see if it fits with Score: and FONT_SYSFIXED */
+ rb->lcd_setfont(FONT_SYSFIXED);
+ rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
+ if(w+BEST_SCORE_X < BACKGROUND_X)
+ /* it fits, go and draw it */
+ goto draw_best;
+
+ /* now try with S: and FONT_UI */
+ rb->snprintf(buf, 31, "B: %d", best_score);
+ rb->font_getstringsize(buf, &w, &h, FONT_UI);
+ rb->lcd_setfont(FONT_UI);
+ if(w+BEST_SCORE_X<BACKGROUND_X)
+ goto draw_best;
+
+ /* now try with S: and FONT_SYSFIXED */
+ rb->snprintf(buf, 31, "B: %d", best_score);
+ rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED);
+ rb->lcd_setfont(FONT_SYSFIXED);
+ if(w+BEST_SCORE_X<BACKGROUND_X)
+ goto draw_best;
+
+ /* then try without Score: and FONT_UI */
+ rb->snprintf(buf, 31, "%d", best_score);
+ rb->font_getstringsize(buf, &w, &h, FONT_UI);
+ rb->lcd_setfont(FONT_UI);
+ if(w+BEST_SCORE_X<BACKGROUND_X)
+ goto draw_best;
+
+ /* as a last resort, don't use Score: and use the system font */
+ rb->snprintf(buf, 31, "%d", best_score);
+ rb->lcd_setfont(FONT_SYSFIXED);
+ }
+draw_best:
+ rb->lcd_putsxy(BEST_SCORE_X, BEST_SCORE_Y, buf);
+
+ rb->lcd_update();
+ /* revert the font back */
+ rb->lcd_setfont(WHAT_FONT);
+}
+
+/* place a 2 or 4 in a random empty space */
+static void place_random(void)
+{
+ int xpos[SPACES],ypos[SPACES];
+ int back=0;
+ /* get the indexes of empty spaces */
+ for(int y=0;y<GRID_SIZE;++y)
+ for(int x=0;x<GRID_SIZE;++x)
+ {
+ if(!ctx->grid[x][y])
+ {
+ xpos[back]=x;
+ ypos[back]=y;
+ ++back;
+ }
+ }
+ if(!back)
+ /* no empty spaces */
+ return;
+ int idx=rand_range(0,back-1);
+ ctx->grid[xpos[idx]][ypos[idx]]=rand_2_or_4();
+}
+
+/* copies old_grid to ctx->grid */
+static void restore_old_grid(void)
+{
+ memcpy(&ctx->grid, &old_grid, sizeof(int)*SPACES);
+}
+
+/* checks for a win or loss */
+static bool check_gameover(void)
+{
+ int numempty=0;
+ for(int y=0;y<GRID_SIZE;++y)
+ {
+ for(int x=0;x<GRID_SIZE;++x)
+ {
+ if(ctx->grid[x][y]==0)
+ ++numempty;
+ if(ctx->grid[x][y]==WINNING_TILE && !ctx->already_won)
+ {
+ /* Let the user see the tile in its full glory... */
+ draw();
+ ctx->already_won=true;
+ rb->splash(HZ*2,"You win!");
+ const struct text_message prompt={(const char*[]){"Keep going?"}, 1};
+ enum yesno_res keepgoing=rb->gui_syncyesno_run(&prompt, NULL, NULL);
+ if(keepgoing==YESNO_NO)
+ return true;
+ else
+ return false;
+ }
+ }
+ }
+ if(!numempty)
+ {
+ /* No empty spaces, check for valid moves */
+ /* Then, get the current score */
+ int oldscore=ctx->score;
+ memset(&merged_grid,0,SPACES*sizeof(bool));
+ up();
+ if(memcmp(&old_grid, &ctx->grid, sizeof(int)*SPACES))
+ {
+ restore_old_grid();
+ ctx->score=oldscore;
+ return false;
+ }
+ restore_old_grid();
+ memset(&merged_grid,0,SPACES*sizeof(bool));
+ down();
+ if(memcmp(&old_grid, &ctx->grid, sizeof(int)*SPACES))
+ {
+ restore_old_grid();
+ ctx->score=oldscore;
+ return false;
+ }
+ restore_old_grid();
+ memset(&merged_grid,0,SPACES*sizeof(bool));
+ left();
+ if(memcmp(&old_grid, &ctx->grid, sizeof(int)*SPACES))
+ {
+ restore_old_grid();
+ ctx->score=oldscore;
+ return false;
+ }
+ restore_old_grid();
+ memset(&merged_grid,0,SPACES*sizeof(bool));
+ right();
+ if(memcmp(&old_grid, &ctx->grid, sizeof(int)*SPACES))
+ {
+ restore_old_grid();
+ ctx->score=oldscore;
+ return false;
+ }
+ /* no more legal moves */
+ ctx->score=oldscore;
+ draw(); /* Shame the player :) */
+ rb->splash(HZ*2, "Game Over!");
+ return true;
+ }
+ return false;
+}
+
+/* loads highscores from disk */
+/* creates an empty structure if the file does not exist */
+static void load_hs(void)
+{
+ if(rb->file_exists(HISCORES_FILE))
+ highscore_load(HISCORES_FILE, highscores, NUM_SCORES);
+ else
+ memset(highscores, 0, sizeof(struct highscore)*NUM_SCORES);
+}
+
+/* initialize the data structures */
+static void init_game(bool newgame)
+{
+ best_score=highscores[0].score;
+ if(newgame)
+ {
+ /* initialize the game context */
+ memset(ctx->grid, 0, sizeof(int)*SPACES);
+ for(int i=0;i<NUM_STARTING_TILES;++i)
+ {
+ place_random();
+ }
+ ctx->score=0;
+ ctx->already_won=false;
+ }
+ /* using the menu resets the font */
+ /* set it again here */
+ rb->lcd_setfont(WHAT_FONT);
+ /* Now calculate font sizes */
+ /* Now get the height of the font */
+ rb->font_getstringsize("0123456789", NULL, &max_numeral_height,WHAT_FONT);
+ max_numeral_height+=VERT_SPACING;
+ backlight_ignore_timeout();
+ rb->lcd_clear_display();
+ draw();
+}
+
+/* save the current game state */
+static void save_game(void)
+{
+ rb->splash(0, "Saving...");
+ int fd=rb->open(RESUME_FILE,O_WRONLY|O_CREAT, 0666);
+ if(fd<0)
+ {
+ return;
+ }
+ ctx->cksum=0;
+ for(int x=0;x<GRID_SIZE;++x)
+ for(int y=0;y<GRID_SIZE;++y)
+ ctx->cksum+=ctx->grid[x][y];
+ ctx->cksum^=ctx->score;
+ rb->write(fd, ctx,sizeof(struct game_ctx_t));
+ rb->close(fd);
+ rb->lcd_update();
+}
+
+/* loads a saved game, returns true on success */
+static bool load_game(void)
+{
+ int success=0;
+ int fd=rb->open(RESUME_FILE, O_RDONLY);
+ if(fd<0)
+ {
+ rb->remove(RESUME_FILE);
+ return false;
+ }
+ int numread=rb->read(fd, ctx, sizeof(struct game_ctx_t));
+ int calc=0;
+ for(int x=0;x<GRID_SIZE;++x)
+ for(int y=0;y<GRID_SIZE;++y)
+ calc+=ctx->grid[x][y];
+ calc^=ctx->score;
+ if(numread==sizeof(struct game_ctx_t) && calc==ctx->cksum)
+ ++success;
+ rb->close(fd);
+ rb->remove(RESUME_FILE);
+ return (success==1);
+}
+
+/* update the highscores with ctx->score */
+static void hs_check_update(bool noshow)
+{
+ /* first, find the biggest tile to show as the level */
+ int biggest=0;
+ for(int x=0;x<GRID_SIZE;++x)
+ {
+ for(int y=0;y<GRID_SIZE;++y)
+ {
+ if(ctx->grid[x][y]>biggest)
+ biggest=ctx->grid[x][y];
+ }
+ }
+ int hs_idx=highscore_update(ctx->score,biggest, "", highscores,NUM_SCORES);
+ if(!noshow)
+ {
+ /* show the scores if there is a new high score */
+ if(hs_idx>=0)
+ {
+ rb->splashf(HZ*2,"New High Score: %d", ctx->score);
+ rb->lcd_clear_display();
+ highscore_show(hs_idx,highscores,NUM_SCORES,true);
+ }
+ }
+ highscore_save(HISCORES_FILE,highscores,NUM_SCORES);
+}
+
+/* asks the user if they wish to quit */
+static bool confirm_quit(void)
+{
+ const struct text_message prompt={(const char*[]){"Are you sure?", "This will clear your current game."}, 2};
+ enum yesno_res response=rb->gui_syncyesno_run(&prompt, NULL, NULL);
+ if(response==YESNO_NO)
+ return false;
+ else
+ return true;
+}
+
+/* show the pause menu */
+static int do_2048_pause_menu(void)
+{
+ int sel=0;
+ MENUITEM_STRINGLIST(menu,"2048 Menu", NULL,
+ "Resume Game",
+ "Start New Game",
+ "High Scores",
+ "Playback Control",
+ "Help",
+ "Quit without Saving",
+ "Quit");
+ bool quit=false;
+ while(!quit)
+ {
+ switch(rb->do_menu(&menu, &sel, NULL, false))
+ {
+ case 0:
+ draw();
+ return 0;
+ case 1:
+ {
+ if(!confirm_quit())
+ break;
+ else
+ {
+ hs_check_update(false);
+ return 1;
+ }
+ }
+ case 2:
+ highscore_show(-1,highscores, NUM_SCORES, true);
+ break;
+ case 3:
+ playback_control(NULL);
+ break;
+ case 4:
+ do_help();
+ break;
+ case 5: /* quit w/o saving */
+ {
+ if(!confirm_quit())
+ break;
+ else
+ {
+ return 2;
+ }
+ }
+ case 6:
+ return 3;
+ }
+ }
+ return 0;
+}
+
+static void exit_handler(void)
+{
+ cleanup();
+ if(abnormal_exit)
+ save_game();
+ return;
+}
+static bool check_hs;
+/* main game loop */
+static enum plugin_status do_game(bool newgame)
+{
+ init_game(newgame);
+ rb_atexit(&exit_handler);
+ int made_move=0;
+ while(1)
+ {
+#ifdef HAVE_ADJUSTABLE_CPU_FREQ
+ rb->cpu_boost(false); /* Save battery when idling */
+#endif
+ /* Wait for a button press */
+ int button=pluginlib_getaction(-1, plugin_contexts, ARRAYLEN(plugin_contexts));
+ made_move=0;
+ memset(&merged_grid,0,SPACES*sizeof(bool));
+ memcpy(&old_grid, &ctx->grid, sizeof(int)*SPACES);
+ int grid_before_anim_step[GRID_SIZE][GRID_SIZE];
+#ifdef HAVE_ADJUSTABLE_CPU_FREQ
+ rb->cpu_boost(true); /* doing work now... */
+#endif
+ switch(button)
+ {
+ case KEY_UP:
+ for(int i=0;i<GRID_SIZE-1;++i)
+ {
+ memcpy(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES);
+ up();
+ if(memcmp(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES))
+ {
+ rb->sleep(ANIM_SLEEPTIME);
+ draw();
+ }
+ }
+ made_move=1;
+ break;
+ case KEY_DOWN:
+ for(int i=0;i<GRID_SIZE-1;++i)
+ {
+ memcpy(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES);
+ down();
+ if(memcmp(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES))
+ {
+ rb->sleep(ANIM_SLEEPTIME);
+ draw();
+ }
+ }
+ made_move=1;
+ break;
+ case KEY_LEFT:
+ for(int i=0;i<GRID_SIZE-1;++i)
+ {
+ memcpy(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES);
+ left();
+ if(memcmp(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES))
+ {
+ rb->sleep(ANIM_SLEEPTIME);
+ draw();
+ }
+ }
+ made_move=1;
+ break;
+ case KEY_RIGHT:
+ for(int i=0;i<GRID_SIZE-1;++i)
+ {
+ memcpy(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES);
+ right();
+ if(memcmp(grid_before_anim_step, ctx->grid, sizeof(int)*SPACES))
+ {
+ rb->sleep(ANIM_SLEEPTIME);
+ draw();
+ }
+ }
+ made_move=1;
+ break;
+ case KEY_EXIT:
+ switch(do_2048_pause_menu())
+ {
+ case 0: /* resume */
+ break;
+ case 1: /* new game */
+ init_game(true);
+ made_move=1;
+ continue;
+ break;
+ case 2: /* quit without saving */
+ check_hs=true;
+ rb->remove(RESUME_FILE);
+ return PLUGIN_ERROR;
+ case 3: /* save and quit */
+ check_hs=false;
+ save_game();
+ return PLUGIN_ERROR;
+ }
+ break;
+ default:
+ {
+ exit_on_usb(button); /* handles poweroff and USB events */
+ break;
+ }
+ }
+ if(made_move)
+ {
+ /* Check if we actually moved, then add random */
+ if(memcmp(&old_grid, ctx->grid, sizeof(int)*SPACES))
+ {
+ place_random();
+ }
+ memcpy(&old_grid, ctx->grid, sizeof(int)*SPACES);
+ if(check_gameover())
+ return PLUGIN_OK;
+ draw();
+ }
+#ifdef HAVE_ADJUSTABLE_CPU_FREQ
+ rb->cpu_boost(false); /* back to idle */
+#endif
+ rb->yield();
+ }
+}
+
+/* decide if this_item should be shown in the main menu */
+/* used to hide resume option when there is no save */
+static int mainmenu_cb(int action, const struct menu_item_ex *this_item)
+{
+ int idx=((intptr_t)this_item);
+ if(action==ACTION_REQUEST_MENUITEM && !loaded && (idx==0 || idx==5))
+ return ACTION_EXIT_MENUITEM;
+ return action;
+}
+
+/* show the main menu */
+static enum plugin_status do_2048_menu(void)
+{
+ int sel=0;
+ loaded=load_game();
+ MENUITEM_STRINGLIST(menu,"2048 Menu", mainmenu_cb, "Resume Game", "Start New Game","High Scores","Playback Control", "Help", "Quit without Saving", "Quit");
+ bool quit=false;
+ while(!quit)
+ {
+ int item;
+ switch(item=rb->do_menu(&menu,&sel,NULL,false))
+ {
+ case 0: /* Start new game or resume a game */
+ case 1:
+ {
+ if(item==1 && loaded)
+ {
+ if(!confirm_quit())
+ break;
+ }
+ enum plugin_status ret=do_game(item==1);
+ switch(ret)
+ {
+ case PLUGIN_OK:
+ {
+ loaded=false;
+ rb->remove(RESUME_FILE);
+ hs_check_update(false);
+ break;
+ }
+ case PLUGIN_USB_CONNECTED:
+ save_game();
+ /* Don't bother showing the high scores... */
+ return ret;
+ case PLUGIN_ERROR: /* exit without menu */
+ if(check_hs)
+ hs_check_update(false);
+ return PLUGIN_OK;
+ default:
+ break;
+ }
+ break;
+ }
+ case 2:
+ highscore_show(-1,highscores, NUM_SCORES, true);
+ break;
+ case 3:
+ playback_control(NULL);
+ break;
+ case 4:
+ do_help();
+ break;
+ case 5:
+ if(confirm_quit())
+ return PLUGIN_OK;
+ case 6:
+ if(loaded)
+ save_game();
+ return PLUGIN_OK;
+ default:
+ break;
+ }
+ }
+ return PLUGIN_OK;
+}
+enum plugin_status plugin_start(const void* param)
+{
+ (void)param;
+ rb->srand(*rb->current_tick);
+ load_hs();
+ rb->lcd_setfont(WHAT_FONT);
+
+ /* now start the game menu */
+ enum plugin_status ret=do_2048_menu();
+ highscore_save(HISCORES_FILE,highscores,NUM_SCORES);
+ cleanup();
+ abnormal_exit=false;
+ return ret;
+}
diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES
index 8a849aafc2..626dca0d32 100644
--- a/apps/plugins/CATEGORIES
+++ b/apps/plugins/CATEGORIES
@@ -1,3 +1,4 @@
+2048,games
alpine_cdc,apps
alarmclock,apps
autostart,apps
diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES
index c56cd5eb46..9b7436ac20 100644
--- a/apps/plugins/SOURCES
+++ b/apps/plugins/SOURCES
@@ -213,6 +213,8 @@ nim.c
#if LCD_DEPTH > 1 /* non-mono bitmap targets */
+/* 2048 runs on 1-bit LCDs, but it is unplayable */
+2048.c
matrix.c
#if (LCD_WIDTH > 138)
diff --git a/apps/plugins/bitmaps/native/SOURCES b/apps/plugins/bitmaps/native/SOURCES
index 4b7f0e2670..d608062335 100644
--- a/apps/plugins/bitmaps/native/SOURCES
+++ b/apps/plugins/bitmaps/native/SOURCES
@@ -1,5 +1,27 @@
#ifdef HAVE_LCD_BITMAP
+/* 2048 */
+
+#define MIN(x,y) ((x<y)?x:y)
+#if MIN(LCD_WIDTH, LCD_HEIGHT)>=240
+_2048_tiles.48x48x24.bmp
+_2048_background.224x224x24.bmp
+#elif MIN(LCD_WIDTH, LCD_HEIGHT)>=176
+_2048_tiles.36x36x24.bmp
+_2048_background.168x168x24.bmp
+#elif MIN(LCD_WIDTH, LCD_HEIGHT)>=132 || MIN(LCD_WIDTH, LCD_HEIGHT)>=128
+_2048_tiles.26x26x24.bmp
+_2048_background.121x121x24.bmp
+#elif MIN(LCD_WIDTH, LCD_HEIGHT)>=110
+_2048_tiles.22x22x24.bmp
+_2048_background.103x103x24.bmp
+#else
+/* default to smallest bitmaps */
+_2048_tiles.12x12x24.bmp
+_2048_background.56x56x24.bmp
+#endif
+#undef MIN
+
/* Brickmania */
#ifdef HAVE_LCD_COLOR
#if LCD_WIDTH >= 112
diff --git a/apps/plugins/bitmaps/native/_2048_background.103x103x24.bmp b/apps/plugins/bitmaps/native/_2048_background.103x103x24.bmp
new file mode 100644
index 0000000000..153126bc99
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_background.103x103x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_background.121x121x24.bmp b/apps/plugins/bitmaps/native/_2048_background.121x121x24.bmp
new file mode 100644
index 0000000000..1e6e3270d0
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_background.121x121x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_background.168x168x24.bmp b/apps/plugins/bitmaps/native/_2048_background.168x168x24.bmp
new file mode 100644
index 0000000000..c47b5d26d5
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_background.168x168x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_background.224x224x24.bmp b/apps/plugins/bitmaps/native/_2048_background.224x224x24.bmp
new file mode 100644
index 0000000000..531d5ed541
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_background.224x224x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_background.56x56x24.bmp b/apps/plugins/bitmaps/native/_2048_background.56x56x24.bmp
new file mode 100644
index 0000000000..c3008f8eae
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_background.56x56x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_tiles.12x12x24.bmp b/apps/plugins/bitmaps/native/_2048_tiles.12x12x24.bmp
new file mode 100644
index 0000000000..e531c2b6e2
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_tiles.12x12x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_tiles.22x22x24.bmp b/apps/plugins/bitmaps/native/_2048_tiles.22x22x24.bmp
new file mode 100644
index 0000000000..186ba8bc95
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_tiles.22x22x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_tiles.26x26x24.bmp b/apps/plugins/bitmaps/native/_2048_tiles.26x26x24.bmp
new file mode 100644
index 0000000000..f7df5ba5a4
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_tiles.26x26x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_tiles.36x36x24.bmp b/apps/plugins/bitmaps/native/_2048_tiles.36x36x24.bmp
new file mode 100644
index 0000000000..e27ada133d
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_tiles.36x36x24.bmp
Binary files differ
diff --git a/apps/plugins/bitmaps/native/_2048_tiles.48x48x24.bmp b/apps/plugins/bitmaps/native/_2048_tiles.48x48x24.bmp
new file mode 100644
index 0000000000..09165203ed
--- /dev/null
+++ b/apps/plugins/bitmaps/native/_2048_tiles.48x48x24.bmp
Binary files differ
diff --git a/docs/CREDITS b/docs/CREDITS
index 0329738b1b..9d6e984964 100644
--- a/docs/CREDITS
+++ b/docs/CREDITS
@@ -637,6 +637,7 @@ Dmitry Gamza
Sebastian Leonhardt
Avi Eisenberg
Richard Burke
+Franklin Wei
The libmad team
The wavpack team
diff --git a/manual/plugins/2048.tex b/manual/plugins/2048.tex
new file mode 100644
index 0000000000..863eebb961
--- /dev/null
+++ b/manual/plugins/2048.tex
@@ -0,0 +1,16 @@
+\subsection{2048}
+%\screenshot{plugins/images/ss-2048}{2048}{fig:2048}
+
+2048 is a simple, addictive puzzle game played by moving tiles in around on a 4x4 grid. Tiles slide as far as possible in the direction chosen by the player each turn until they are stopped by either another tile or the edge of the grid. If two tiles of the same number collide while moving, they merge into a tile with the total value of the two tiles that collided. The resulting tile cannot merge with another the same move. After each move, a tile with the value of 2 or 4 is created in an empty spot on the grid.
+
+The game is won when a tile with a value of 2048 is created, and the player loses when there are no more possible moves.
+
+\begin{btnmap}
+ \PluginUp, \PluginDown, \PluginLeft, \PluginRight
+ \opt{HAVEREMOTEKEYMAP}{& \PluginRCUp, \PluginRCDown, \PluginRCLeft, \PluginRCRight}
+ & Slide tiles\\
+
+ \PluginCancel
+ \opt{HAVEREMOTEKEYMAP}{& \PluginRCCancel}
+ & Go to menu\\
+\end{btnmap}
diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex
index 856af129e7..7369d35c03 100644
--- a/manual/plugins/main.tex
+++ b/manual/plugins/main.tex
@@ -23,6 +23,8 @@ text files%
\opt{archosrecorder,archosfmrecorder,iriverh100,iaudiom5,lcd_color}
{and Rockboy in \reference{ref:Rockboy}}.}
+\opt{lcd_bitmap}{\input{plugins/2048.tex}}
+
\opt{lcd_bitmap}{\input{plugins/blackjack.tex}}
\opt{lcd_bitmap}{\input{plugins/brickmania.tex}}