/*************************************************************************** * __________ __ ___. * 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 #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_WIDTHrand()%(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;ylcd_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_Xsnprintf(buf, 31, "S: %d", ctx->score); rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED); rb->lcd_setfont(FONT_SYSFIXED); if(w+SCORE_Xsnprintf(buf, 31, "%d", ctx->score); rb->font_getstringsize(buf, &w, &h, FONT_UI); rb->lcd_setfont(FONT_UI); if(w+SCORE_Xsnprintf(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_Xsnprintf(buf, 31, "B: %d", best_score); rb->font_getstringsize(buf, &w, &h, FONT_SYSFIXED); rb->lcd_setfont(FONT_SYSFIXED); if(w+BEST_SCORE_Xsnprintf(buf, 31, "%d", best_score); rb->font_getstringsize(buf, &w, &h, FONT_UI); rb->lcd_setfont(FONT_UI); if(w+BEST_SCORE_Xsnprintf(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;ygrid[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;ygrid[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;iscore=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;xcksum+=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;xgrid[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;xgrid[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(); #ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(false); /* back to idle */ #endif 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;igrid, 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;igrid, 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;igrid, 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;igrid, 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; }