2048: Difference between revisions
m (→{{header|Java}}: fix template) |
m (→{{header|Java}}: small change) |
||
Line 1,673: | Line 1,673: | ||
boolean canMergeWith(Tile other) { |
boolean canMergeWith(Tile other) { |
||
return !merged && other != null && value == other.getValue(); |
return !merged && other != null && !other.merged && value == other.getValue(); |
||
} |
} |
||
Revision as of 02:05, 4 March 2016
Implement a 2D sliding block puzzle game where blocks with numbers are combined to add their values.
The rules are that each turn the player must perform a valid move shifting all tiles in one direction (up, down, left or right). A move is valid when at least one tile can be moved in that direction. When moved against each other tiles with the same number on them combine into one. A new tile with the value of 2 is spawned at the end of each turn if there is an empty spot for it. To win the player must create a tile with the number 2048. The player loses if no valid moves are possible.
The name comes from the popular open-source implementation of this game mechanic, 2048.
Requirements:
- "Non-greedy" movement. The tiles that were created by combining other tiles should not be combined again during the same turn (move). That is to say that moving the tile row of
[2][2][2][2]
to the right should result in
......[4][4]
and not
.........[8]
- "Move direction priority". If more than one variant of combining is possible, move direction shows one that will take effect. For example, moving the tile row of
...[2][2][2]
to the right should result in
......[2][4]
and not
......[4][2]
- Adding a new tile on a blank space. Most of the time new "2" is to be added and occasionally (10% of the time) - "4"
- Check for valid moves. The player shouldn't be able to skip their turn by trying a move that doesn't change the board.
- Win condition.
- Lose condition.
Batch File
<lang dos>::2048 Game Task from RosettaCode.org
- Batch File Implementation
@echo off setlocal enabledelayedexpansion cls
- begin_game
%== Set variables ==% set "score=0" set "won=0" set "SUP_score=0" for /l %%A in (1,1,4) do for /l %%B in (1,1,4) do set /a "X_%%A%%B=0"
call :addtile call :addtile
%== Main game loop ==%
- main_loop
set "changed=0" call :display echo( echo Keys: WASD (Slide Movement^), N (New game^), P (Exit^)
%== Get Keypress ==% set "key=" for /f "delims=" %%? in ('xcopy /w "%~f0" "%~f0" 2^>nul') do if not defined key set "key=%%?" set "key=%key:~-1%"
%== Process keypress ==% if /i "!key!"=="W" ( for /l %%? in (1,1,4) do call :slide X_1%%? X_2%%? X_3%%? X_4%%? ) if /i "!key!"=="A" ( for /l %%? in (1,1,4) do call :slide X_%%?1 X_%%?2 X_%%?3 X_%%?4 ) if /i "!key!"=="S" ( for /l %%? in (1,1,4) do call :slide X_4%%? X_3%%? X_2%%? X_1%%? ) if /i "!key!"=="D" ( for /l %%? in (1,1,4) do call :slide X_%%?4 X_%%?3 X_%%?2 X_%%?1 ) if /i "!key!"=="N" goto :begin_game if /i "!key!"=="P" exit /b
%== Check if the board changed ==% if %changed% neq 0 call :addtile
%== Check if already won ==% if %won% equ 1 ( set "msg=Nice one... You WON^!^!" goto :gameover )
%== Check for lose condition ==% set /a "real_blanks=blank_count-1" if %real_blanks% leq 0 ( for /l %%A in (1,1,4) do for /l %%B in (1,1,4) do set "TRY_%%A%%B=!X_%%A%%B!" set "TRY_changed=%changed%" & set "changed=0" set "SUP_score=1" for /l %%? in (1,1,4) do call :slide TRY_%%?1 TRY_%%?2 TRY_%%?3 TRY_%%?4 for /l %%? in (1,1,4) do call :slide TRY_1%%? TRY_2%%? TRY_3%%? TRY_4%%? if !changed! equ 0 ( set "msg=No moves are possible... Game Over :(" goto :gameover ) else (set "changed=!TRY_changed!" & set "SUP_score=0") ) goto main_loop
- ~~~~~~~~~~~~~~~~~~~~ Sub Procedures ~~~~~~~~~~~~~~~~~~~~::
%== Game Over xD ==%
- gameover
call :display echo( echo(!msg! echo( echo(Keys: N (New game^), P (Exit^)
- key_loop
set "key=" for /f "delims=" %%? in ('xcopy /w "%~f0" "%~f0" 2^>nul') do if not defined key set "key=%%?" set "key=%key:~-1%" if /i "!key!"=="N" goto :begin_game if /i "!key!"=="P" exit /b goto :key_loop
%== The main slider of numbers in tiles ==%
- slide
set "next=" set "slide_1=" set "slide_2=" for %%? in (%*) do if !%%?! neq 0 set "slide_1=!slide_1! !%%?!" for %%? in (!slide_1!) do ( set "scan=%%?" if "!scan!"=="!next!" ( set /a "next*=2" if !SUP_score! equ 0 set /a "score+=!next!" %== WINNING CONDITION!!! ==% if "!next!" equ "2048" set "won=1" set "scan=" ) set "slide_2=!slide_2! !next!" set "next=!scan!" ) set "slide_2=!slide_2! !next!" for /l %%? in (1,1,4) do set "final_%%?=0" set "cnt=0" & for %%? in (!slide_2!) do if !cnt! lss 4 ( set /a "cnt+=1" set "final_!cnt!=%%?" ) if not "!%1!!%2!!%3!!%4!"=="!final_1!!final_2!!final_3!!final_4!" set "changed=1" set "cnt=0" & for %%? in (%*) do ( set /a "cnt+=1" set /a "%%?=final_!cnt!" ) goto :EOF
%== Add number to tile ==%
- addtile
set "blank_list=" set "blank_count=0" for /l %%A in (1,1,4) do for /l %%B in (1,1,4) do ( if !X_%%A%%B! equ 0 ( set "blank_list=!blank_list!X_%%A%%B" set /a blank_count+=1 ) ) set /a "pick_tile=(%random% %% %blank_count%)*4" set /a "rnd=%random%%%10+1" set "tile_new=!blank_list:~%pick_tile%,4!" if %rnd%==5 (set !tile_new!=4) else (set !tile_new!=2) goto :EOF
%== Display the table ==%
- display
cls echo 2048 Game in Batch echo( for /l %%A in (1,1,4) do ( for /l %%B in (1,1,4) do ( set "DX_%%A%%B=!X_%%A%%B!" if !tile_new!==X_%%A%%B (set "DX_%%A%%B= +!X_%%A%%B!") else ( if !X_%%A%%B! lss 1000 set "DX_%%A%%B= !DX_%%A%%B!" if !X_%%A%%B! lss 100 set "DX_%%A%%B= !DX_%%A%%B!" if !X_%%A%%B! lss 10 set "DX_%%A%%B= !DX_%%A%%B!" if !X_%%A%%B! equ 0 set "DX_%%A%%B= " ) ) echo +----+----+----+----+ echo ^|!DX_%%A1!^|!DX_%%A2!^|!DX_%%A3!^|!DX_%%A4!^| ) echo +----+----+----+----+ echo( echo Score: %score% goto :EOF</lang>
- Output:
2048 Game in Batch +----+----+----+----+ | | +2| | | +----+----+----+----+ | 4| | | | +----+----+----+----+ | 4| | | | +----+----+----+----+ | 16| 4| | 2| +----+----+----+----+ Score: 60 Keys: WASD (Slide Movement), N (New game), P (Exit)
C
Supports limited colours through vt100 escape codes. Requires a posix machine for termios.h and unistd.h> headers. Provides simplistic animations when moving and merging blocks. <lang c>
- include <stdio.h>
- include <stdlib.h>
- include <string.h>
- include <termios.h>
- include <time.h>
- include <unistd.h>
- define D_INVALID -1
- define D_UP 1
- define D_DOWN 2
- define D_RIGHT 3
- define D_LEFT 4
const long values[] = {
0, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048
};
const char *colors[] = {
"39", "31", "32", "33", "34", "35", "36", "37", "91", "92", "93", "94"
};
struct gamestate_struct__ {
int grid[4][4]; int have_moved; long total_score; long score_last_move; int blocks_in_play;
} game;
struct termios oldt, newt;
void do_draw(void) {
printf("\033[2J\033[HScore: %ld", game.total_score); if (game.score_last_move) printf(" (+%ld)", game.score_last_move); printf("\n");
for (int i = 0; i < 25; ++i) printf("-"); printf("\n");
for (int y = 0; y < 4; ++y) { printf("|"); for (int x = 0; x < 4; ++x) { if (game.grid[x][y]) printf("\033[7m\033[%sm%*zd \033[0m|", colors[game.grid[x][y]], 4, values[game.grid[x][y]]); else printf("%*s |", 4, ""); } printf("\n"); }
for (int i = 0; i < 25; ++i) { printf("-"); } printf("\n");
}
void do_merge(int d) { /* These macros look pretty scary, but mainly demonstrate some space saving */
- define MERGE_DIRECTION(_v1, _v2, _xs, _xc, _xi, _ys, _yc, _yi, _x, _y) \
do { \ for (int _v1 = _xs; _v1 _xc; _v1 += _xi) { \ for (int _v2 = _ys; _v2 _yc; _v2 += _yi) { \ if (game.grid[x][y] && (game.grid[x][y] == \ game.grid[x + _x][y + _y])) { \ game.grid[x][y] += (game.have_moved = 1); \ game.grid[x + _x][y + _y] = (0 * game.blocks_in_play--);\ game.score_last_move += values[game.grid[x][y]]; \ game.total_score += values[game.grid[x][y]]; \ } \ } \ } \ } while (0)
game.score_last_move = 0;
switch (d) { case D_LEFT: MERGE_DIRECTION(x, y, 0, < 3, 1, 0, < 4, 1, 1, 0); break; case D_RIGHT: MERGE_DIRECTION(x, y, 3, > 0, -1, 0, < 4, 1, -1, 0); break; case D_DOWN: MERGE_DIRECTION(y, x, 3, > 0, -1, 0, < 4, 1, 0, -1); break; case D_UP: MERGE_DIRECTION(y, x, 0, < 3, 1, 0, < 4, 1, 0, 1); break; }
- undef MERGE_DIRECTION
}
void do_gravity(int d) {
- define GRAVITATE_DIRECTION(_v1, _v2, _xs, _xc, _xi, _ys, _yc, _yi, _x, _y) \
do { \ int break_cond = 0; \ while (!break_cond) { \ break_cond = 1; \ for (int _v1 = _xs; _v1 _xc; _v1 += _xi) { \ for (int _v2 = _ys; _v2 _yc; _v2 += _yi) { \ if (!game.grid[x][y] && game.grid[x + _x][y + _y]) { \ game.grid[x][y] = game.grid[x + _x][y + _y]; \ game.grid[x + _x][y + _y] = break_cond = 0; \ } \ } \ } \ do_draw(); usleep(40000); \ } \ } while (0)
switch (d) { case D_LEFT: GRAVITATE_DIRECTION(x, y, 0, < 3, 1, 0, < 4, 1, 1, 0); break; case D_RIGHT: GRAVITATE_DIRECTION(x, y, 3, > 0, -1, 0, < 4, 1, -1, 0); break; case D_DOWN: GRAVITATE_DIRECTION(y, x, 3, > 0, -1, 0, < 4, 1, 0, -1); break; case D_UP: GRAVITATE_DIRECTION(y, x, 0, < 3, 1, 0, < 4, 1, 0, 1); break; }
- undef GRAVITATE_DIRECTION
}
int do_check_end_condition(void) {
int ret = -1; for (int x = 0; x < 4; ++x) { for (int y = 0; y < 4; ++y) { if (values[game.grid[x][y]] == 2048) return 1; if (!game.grid[x][y] || ((x + 1 < 4) && (game.grid[x][y] == game.grid[x + 1][y])) || ((y + 1 < 4) && (game.grid[x][y] == game.grid[x][y + 1]))) ret = 0; } } return ret;
}
int do_tick(int d) {
game.have_moved = 0; do_gravity(d); do_merge(d); do_gravity(d); return game.have_moved;
}
void do_newblock(void) {
if (game.blocks_in_play >= 16) return;
int bn = rand() % (16 - game.blocks_in_play); int pn = 0;
for (int x = 0; x < 4; ++x) { for (int y = 0; y < 4; ++y) { if (game.grid[x][y]) continue;
if (pn == bn){ game.grid[x][y] = rand() % 10 ? 1 : 2; game.blocks_in_play += 1; return; } else { ++pn; } } }
}
int main(void) {
/* Initialize terminal settings */ tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt);
srand(time(NULL)); memset(&game, sizeof(game), 0); do_newblock(); do_newblock(); do_draw();
while (1) { int found_valid_key, direction, value; do { found_valid_key = 1; direction = D_INVALID; value = getchar(); switch (value) { case 'h': case 'a': direction = D_LEFT; break; case 'l': case 'd': direction = D_RIGHT; break; case 'j': case 's': direction = D_DOWN; break; case 'k': case 'w': direction = D_UP; break; case 'q': goto game_quit; break; default: found_valid_key = 0; break; } } while (!found_valid_key);
do_tick(direction); do_newblock(); do_draw();
switch (do_check_end_condition()) { case -1: goto game_lose; case 1: goto game_win; case 0: break; } }
if (0)
game_lose: printf("You lose!\n");
if (0)
game_win: printf("You win!\n");
if (0)
game_quit:
/* Restore terminal settings */ tcsetattr(STDIN_FILENO, TCSANOW, &oldt); return 0;
} </lang>
- Output:
Score: 1100 (+4) ------------------------- | 64 | 32 | 64 | 32 | | 32 | 16 | 2 | 8 | | 16 | 4 | 8 | 4 | | 4 | 2 | 4 | 2 | ------------------------- You lose!
C++
<lang cpp>
- include <time.h>
- include <iostream>
- include <string>
- include <iomanip>
- include <cstdlib>
typedef unsigned int uint; using namespace std; enum movDir { UP, DOWN, LEFT, RIGHT };
class tile { public:
tile() : val( 0 ), blocked( false ) {} uint val; bool blocked;
};
class g2048 { public:
g2048() : done( false ), win( false ), moved( true ), score( 0 ) {} void loop() {
addTile(); while( true ) { if( moved ) addTile(); drawBoard(); if( done ) break; waitKey(); } string s = "Game Over!"; if( win ) s = "You've made it!"; cout << s << endl << endl;
}
private:
void drawBoard() {
system( "cls" ); cout << "SCORE: " << score << endl << endl; for( int y = 0; y < 4; y++ ) { cout << "+------+------+------+------+" << endl << "| "; for( int x = 0; x < 4; x++ ) { if( !board[x][y].val ) cout << setw( 4 ) << " "; else cout << setw( 4 ) << board[x][y].val; cout << " | "; } cout << endl; } cout << "+------+------+------+------+" << endl << endl;
} void waitKey() {
moved = false; char c; cout << "(W)Up (S)Down (A)Left (D)Right "; cin >> c; c &= 0x5F; switch( c ) { case 'W': move( UP );break; case 'A': move( LEFT ); break; case 'S': move( DOWN ); break; case 'D': move( RIGHT ); } for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) board[x][y].blocked = false;
} void addTile() {
for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) if( !board[x][y].val ) { uint a, b; do { a = rand() % 4; b = rand() % 4; } while( board[a][b].val );
int s = rand() % 100; if( s > 89 ) board[a][b].val = 4; else board[a][b].val = 2; if( canMove() ) return; } done = true;
} bool canMove() {
for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) if( !board[x][y].val ) return true;
for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) { if( testAdd( x + 1, y, board[x][y].val ) ) return true; if( testAdd( x - 1, y, board[x][y].val ) ) return true; if( testAdd( x, y + 1, board[x][y].val ) ) return true; if( testAdd( x, y - 1, board[x][y].val ) ) return true; } return false;
} bool testAdd( int x, int y, uint v ) {
if( x < 0 || x > 3 || y < 0 || y > 3 ) return false; return board[x][y].val == v;
} void moveVert( int x, int y, int d ) {
if( board[x][y + d].val && board[x][y + d].val == board[x][y].val && !board[x][y].blocked && !board[x][y + d].blocked ) { board[x][y].val = 0; board[x][y + d].val *= 2; score += board[x][y + d].val; board[x][y + d].blocked = true; moved = true; } else if( !board[x][y + d].val && board[x][y].val ) { board[x][y + d].val = board[x][y].val; board[x][y].val = 0; moved = true; } if( d > 0 ) { if( y + d < 3 ) moveVert( x, y + d, 1 ); } else { if( y + d > 0 ) moveVert( x, y + d, -1 ); }
} void moveHori( int x, int y, int d ) {
if( board[x + d][y].val && board[x + d][y].val == board[x][y].val && !board[x][y].blocked && !board[x + d][y].blocked ) { board[x][y].val = 0; board[x + d][y].val *= 2; score += board[x + d][y].val; board[x + d][y].blocked = true; moved = true; } else if( !board[x + d][y].val && board[x][y].val ) { board[x + d][y].val = board[x][y].val; board[x][y].val = 0; moved = true; } if( d > 0 ) { if( x + d < 3 ) moveHori( x + d, y, 1 ); } else { if( x + d > 0 ) moveHori( x + d, y, -1 ); }
} void move( movDir d ) {
switch( d ) { case UP: for( int x = 0; x < 4; x++ ) { int y = 1; while( y < 4 ) { if( board[x][y].val ) moveVert( x, y, -1 ); y++;} } break; case DOWN: for( int x = 0; x < 4; x++ ) { int y = 2; while( y >= 0 ) { if( board[x][y].val ) moveVert( x, y, 1 ); y--;} } break; case LEFT: for( int y = 0; y < 4; y++ ) { int x = 1; while( x < 4 ) { if( board[x][y].val ) moveHori( x, y, -1 ); x++;} } break; case RIGHT: for( int y = 0; y < 4; y++ ) { int x = 2; while( x >= 0 ) { if( board[x][y].val ) moveHori( x, y, 1 ); x--;} } }
} tile board[4][4]; bool win, done, moved; uint score;
}; int main( int argc, char* argv[] ) {
srand( static_cast<uint>( time( NULL ) ) ); g2048 g; g.loop(); return system( "pause" );
} </lang>
- Output:
SCORE: 2024 +------+------+------+------+ | 2 | 8 | 32 | 256 | +------+------+------+------+ | | | 4 | 32 | +------+------+------+------+ | | | 2 | 8 | +------+------+------+------+ | | | | 2 | +------+------+------+------+ (W)Up (S)Down (A)Left (D)Right
Common Lisp
Depends on Windows msvcrt.dll for _getch. Depends on quicklisp. Use arrow keys to make moves and press "Q" to quit. Tested with SBCL. <lang lisp>(ql:quickload '(cffi alexandria))
(defpackage :2048-lisp
(:use :common-lisp :cffi :alexandria))
(in-package :2048-lisp)
(defvar *lib-loaded* nil)
(unless *lib-loaded*
;; Load msvcrt.dll to access _getch. (define-foreign-library msvcrt (:windows (:default "msvcrt")))
(use-foreign-library msvcrt)
(defcfun "_getch" :int)
(setf *lib-loaded* t))
(defun read-arrow ()
"Get an arrow key from input as UP, DOWN, LEFT, or RIGHT, otherwise
return a char of whatever was pressed."
(let ((first-char (-getch))) (if (= 224 first-char) (case (-getch) (75 'left) (80 'down) (77 'right) (72 'up)) (code-char first-char))))
(defmacro swap (place1 place2)
"Exchange the values of two places." (let ((temp (gensym "TEMP"))) `(cl:let ((,temp ,place1)) (cl:setf ,place1 ,place2) (cl:setf ,place2 ,temp))))
(defun nflip (board &optional (left-right t))
"Flip the elements of a BOARD left and right or optionally up and down." (let* ((y-len (array-dimension board 0)) (x-len (array-dimension board 1)) (y-max (if left-right y-len (truncate y-len 2))) (x-max (if left-right (truncate x-len 2) x-len))) (loop for y from 0 below y-max for y-place = (- y-len 1 y) do (loop for x from 0 below x-max for x-place = (- x-len 1 x) do (if left-right (swap (aref board y x) (aref board y x-place)) (swap (aref board y x) (aref board y-place x))))) board))
(defun flip (board &optional (left-right t))
"Flip the elements of a BOARD left and right or optionally up and down.
Non-destructive version."
(nflip (copy-array board) left-right))
(defun transpose (board)
"Transpose the elements of BOARD into a new array." (let* ((y-len (array-dimension board 0)) (x-len (array-dimension board 1)) (new-board (make-array (reverse (array-dimensions board))))) (loop for y from 0 below y-len do (loop for x from 0 below x-len do (setf (aref new-board x y) (aref board y x)))) new-board))
(defun add-random-piece (board)
"Find a random empty spot on the BOARD to add a new piece.
Return T if successful, NIL otherwise."
(loop for x from 0 below (array-total-size board) unless (row-major-aref board x) count 1 into count and collect x into indices finally (unless (= 0 count) (setf (row-major-aref board (nth (random count) indices)) (if (= 0 (random 10)) 4 2)) (return t))))
(defun squash-line (line)
"Reduce a sequence of numbers from left to right according to
the rules of 2048. Return the score of squashing as well."
(let* ((squashed (reduce (lambda (acc x) (if (equal x (car acc)) (cons (list (* 2 x)) (cdr acc)) (cons x acc))) (nreverse (remove-if #'null line)) :initial-value nil)) (new-line (flatten squashed))) (list (append (make-list (- (length line) (length new-line))) new-line) (reduce #'+ (flatten (remove-if-not #'listp squashed))))))
(defun squash-board (board)
"Reduce each row of a board from left to right according to
the rules of 2048. Return the total score of squashing the board as well."
(let ((y-len (array-dimension board 0)) (x-len (array-dimension board 1)) (total 0)) (list (make-array (array-dimensions board) :initial-contents (loop for y from 0 below y-len for (line score) = (squash-line (make-array x-len :displaced-to board :displaced-index-offset (array-row-major-index board y 0))) collect line do (incf total score))) total)))
(defun make-move (board direction)
"Make a move in the given DIRECTION on a new board." ;; Move by always squashing right, but transforming the board as needed. (destructuring-bind (new-board score) (case direction (up (squash-board (flip (transpose board)))) (down (squash-board (flip (transpose board) nil))) (left (squash-board (nflip (flip board nil)))) (right (squash-board board))) (let ((new-board ;; Reverse the transformation. (case direction (up (transpose (nflip new-board))) (down (transpose (nflip new-board nil))) (left (nflip (nflip new-board nil))) (right new-board)))) (unless (equalp board new-board) (add-random-piece new-board) (list new-board score)))))
(defun winp (board winning-tile)
"Determine if a BOARD is in a winning state." (loop for x from 0 below (array-total-size board) for val = (row-major-aref board x) when (eql val winning-tile) do (return t)))
(defun game-overp (board)
"Determine if a BOARD is in a game over state." ;; If a move is simulated in every direction and nothing changes, ;; then we can assume there are no valid moves left. (notany (lambda (dir) (car (make-move board dir))) '(up down left right)))
(defun print-divider (cells cell-size)
"A print helper function for PRINT-BOARD." (dotimes (_ cells) (princ "+") (dotimes (_ cell-size) (princ "-"))) (princ "+") (terpri))
(defun print-board (board cell-size)
"Pretty print the given BOARD with a particular CELL-SIZE." (let* ((y-len (array-dimension board 0)) (x-len (array-dimension board 1)) (super-size (+ 2 cell-size))) (loop for y from 0 below y-len do (print-divider x-len super-size) (loop for x from 0 below x-len for val = (aref board y x) do (princ "|") (if val (format t " ~VD " cell-size val) (dotimes (_ super-size) (princ " ")))) (princ "|") (terpri)) (print-divider x-len super-size)))
(defun init-board ()
(let ((board (make-array '(4 4) :initial-element nil))) (setf (row-major-aref board (random (array-total-size board))) 2) board))
(defun prompt-input (board score &optional (check t))
(cond ((and check (winp board 2048)) (format t "You win!")) ((and check (game-overp board)) (format t "Game over...")) (t (let ((choice (read-arrow))) (cond ((symbolp choice) (destructuring-bind (&optional move (new-score 0)) (make-move board choice) (if move (prompt move (+ score new-score)) (prompt-input board score)))) ((find choice "qQ") (format t "Quitting.")) (t (prompt-input board score nil)))))))
(defun prompt (&optional (board (init-board)) (score 0))
(format t "~% Score: ~D~%" score) (print-board board 4) (prompt-input board score))</lang>
- Output:
* (2048-lisp::prompt) Score: 0 +------+------+------+------+ | | | | | +------+------+------+------+ | | | | | +------+------+------+------+ | | | 2 | | +------+------+------+------+ | | | | | +------+------+------+------+
Some time later...
Score: 336 +------+------+------+------+ | | 4 | 16 | 32 | +------+------+------+------+ | | | 4 | 16 | +------+------+------+------+ | | | 32 | | +------+------+------+------+ | | 2 | | | +------+------+------+------+
D
<lang d>import std.stdio, std.string, std.random; import core.stdc.stdlib: exit;
struct G2048 {
public void gameLoop() /*@safe @nogc*/ { addTile; while (true) { if (moved) addTile; drawBoard; if (done) break; waitKey; } writeln(win ? "You win!" : "Game Over!"); }
private:
static struct Tile { uint val = 0; bool blocked = false; }
enum moveDir { up, down, left, right } enum uint side = 4;
Tile[side][side] board; bool win = false, done = false, moved = true; uint score = 0;
void drawBoard() const /*@safe @nogc*/ { writeln("SCORE: ", score, "\n"); foreach (immutable y; 0 .. side) { write("+------+------+------+------+\n| "); foreach (immutable x; 0 .. side) { if (board[x][y].val) writef("%4d", board[x][y].val); else writef("%4s", " "); write(" | "); } writeln; } "+------+------+------+------+\n".writeln; }
void waitKey() /*@safe*/ { moved = false; "(W)Up (S)Down (A)Left (D)Right (Q)Quit: ".write; immutable c = readln.strip.toLower;
switch (c) { case "w": move(moveDir.up); break; case "a": move(moveDir.left); break; case "s": move(moveDir.down); break; case "d": move(moveDir.right); break; case "q": endGame; break; default: break; }
foreach (immutable y; 0 .. side) foreach (immutable x; 0 .. side) board[x][y].blocked = false; }
void endGame() const { writeln("Game ended with score: ", score); exit(0); }
void addTile() /*nothrow*/ @safe /*@nogc*/ { foreach (immutable y; 0 .. side) { foreach (immutable x; 0 .. side) { if (!board[x][y].val) { uint a, b; do { a = uniform(0, side); b = uniform(0, side); } while (board[a][b].val);
board[a][b].val = (uniform01 > 0.89) ? side : 2; if (canMove) return; } } } done = true; }
bool canMove() const pure nothrow @safe @nogc { foreach (immutable y; 0 .. side) foreach (immutable x; 0 .. side) if (!board[x][y].val) return true;
foreach (immutable y; 0 .. side) { foreach (immutable x; 0 .. side) { if (testAdd(x + 1, y, board[x][y].val) || testAdd(x - 1, y, board[x][y].val) || testAdd(x, y + 1, board[x][y].val) || testAdd(x, y - 1, board[x][y].val)) return true; } } return false; }
bool testAdd(in uint x, in uint y, in uint v) const pure nothrow @safe @nogc { if (x > 3 || y > 3) return false; return board[x][y].val == v; }
void moveVertically(in uint x, in uint y, in uint d) pure nothrow @safe @nogc { if (board[x][y + d].val && board[x][y + d].val == board[x][y].val && !board[x][y].blocked && !board[x][y + d].blocked) { board[x][y].val = 0; board[x][y + d].val *= 2; score += board[x][y + d].val; board[x][y + d].blocked = true; moved = true; } else if (!board[x][y + d].val && board[x][y].val) { board[x][y + d].val = board[x][y].val; board[x][y].val = 0; moved = true; }
if (d > 0) { if (y + d < 3) moveVertically(x, y + d, 1); } else { if (y + d > 0) moveVertically(x, y + d, -1); } }
void moveHorizontally(in uint x, in uint y, in uint d) pure nothrow @safe @nogc { if (board[x + d][y].val && board[x + d][y].val == board[x][y].val && !board[x][y].blocked && !board[x + d][y].blocked) { board[x][y].val = 0; board[x + d][y].val *= 2; score += board[x + d][y].val; board[x + d][y].blocked = true; moved = true; } else if (!board[x + d][y].val && board[x][y].val) { board[x + d][y].val = board[x][y].val; board[x][y].val = 0; moved = true; }
if (d > 0) { if (x + d < 3) moveHorizontally(x + d, y, 1); } else { if (x + d > 0) moveHorizontally(x + d, y, -1); } }
void move(in moveDir d) pure nothrow @safe @nogc { final switch (d) with(moveDir) { case up: foreach (immutable x; 0 .. side) foreach (immutable y; 1 .. side) if (board[x][y].val) moveVertically(x, y, -1); break; case down: foreach (immutable x; 0 .. side) foreach_reverse (immutable y; 0 .. 3) if (board[x][y].val) moveVertically(x, y, 1); break; case left: foreach (immutable y; 0 .. side) foreach (immutable x; 1 .. side) if (board[x][y].val) moveHorizontally(x, y, -1); break; case right: foreach (immutable y; 0 .. side) foreach_reverse (immutable x; 0 .. 3) if (board[x][y].val) moveHorizontally(x, y, 1); } }
}
void main() /*safe*/ {
G2048 g; g.gameLoop;
}</lang> The output is the same as the C++ version.
Go
<lang Go>package main
import ( "bufio" "fmt" "log" "math/rand" "os" "os/exec" "strconv" "strings" "text/template" "time" "unicode"
"golang.org/x/crypto/ssh/terminal" )
const maxPoints = 2048 const ( fieldSizeX = 4 fieldSizeY = 4 ) const tilesAtStart = 2 const probFor2 = 0.9
type button int
const ( _ button = iota up down right left quit )
var labels = func() map[button]rune { m := make(map[button]rune, 4) m[up] = 'W' m[down] = 'S' m[right] = 'D' m[left] = 'A' return m }() var keybinding = func() map[rune]button { m := make(map[rune]button, 8) for b, r := range labels { m[r] = b if unicode.IsUpper(r) { r = unicode.ToLower(r) } else { r = unicode.ToUpper(r) } m[r] = b } m[0x03] = quit return m }()
var model = struct { Score int Field [fieldSizeY][fieldSizeX]int }{}
var view = func() *template.Template { maxWidth := 1 for i := maxPoints; i >= 10; i /= 10 { maxWidth++ }
w := maxWidth + 3 r := make([]byte, fieldSizeX*w+1) for i := range r { if i%w == 0 { r[i] = '+' } else { r[i] = '-' } } rawBorder := string(r)
v, err := template.New("").Parse(`SCORE: Template:.Score Template:Range .Field ` + rawBorder + ` |Template:Range . Template:If .Template:Printf "%` + strconv.Itoa(maxWidth) + `d" .Template:Else` + strings.Repeat(" ", maxWidth) + `Template:End |Template:EndTemplate:End ` + rawBorder + `
(` + string(labels[up]) + `)Up (` + string(labels[down]) + `)Down (` + string(labels[left]) + `)Left (` + string(labels[right]) + `)Right `) check(err) return v }()
func check(err error) { if err != nil { log.Panicln(err) } }
func clear() { c := exec.Command("clear") c.Stdout = os.Stdout check(c.Run()) }
func draw() { clear() check(view.Execute(os.Stdout, model)) }
func addRandTile() (full bool) { free := make([]*int, 0, fieldSizeX*fieldSizeY)
for x := 0; x < fieldSizeX; x++ { for y := 0; y < fieldSizeY; y++ { if model.Field[y][x] == 0 { free = append(free, &model.Field[y][x]) } } }
val := 4 if rand.Float64() < probFor2 { val = 2 } *free[rand.Intn(len(free))] = val
return len(free) == 1 }
type point struct{ x, y int }
func (p point) get() int { return model.Field[p.y][p.x] } func (p point) set(n int) { model.Field[p.y][p.x] = n } func (p point) inField() bool { return p.x >= 0 && p.y >= 0 && p.x < fieldSizeX && p.y < fieldSizeY } func (p *point) next(n point) { p.x += n.x; p.y += n.y }
func controller(key rune) (gameOver bool) { b := keybinding[key]
if b == 0 { return false } if b == quit { return true }
var starts []point var next point
switch b { case up: next = point{0, 1} starts = make([]point, fieldSizeX) for x := 0; x < fieldSizeX; x++ { starts[x] = point{x, 0} } case down: next = point{0, -1} starts = make([]point, fieldSizeX) for x := 0; x < fieldSizeX; x++ { starts[x] = point{x, fieldSizeY - 1} } case right: next = point{-1, 0} starts = make([]point, fieldSizeY) for y := 0; y < fieldSizeY; y++ { starts[y] = point{fieldSizeX - 1, y} } case left: next = point{1, 0} starts = make([]point, fieldSizeY) for y := 0; y < fieldSizeY; y++ { starts[y] = point{0, y} } }
moved := false winning := false
for _, s := range starts { n := s move := func(set int) { moved = true s.set(set) n.set(0) } for n.next(next); n.inField(); n.next(next) { if s.get() != 0 { if n.get() == s.get() { score := s.get() * 2 model.Score += score winning = score >= maxPoints
move(score) s.next(next) } else if n.get() != 0 { s.next(next) if s.get() == 0 { move(n.get()) } } } else if n.get() != 0 { move(n.get()) } } }
if !moved { return false }
lost := false if addRandTile() { lost = true Out: for x := 0; x < fieldSizeX; x++ { for y := 0; y < fieldSizeY; y++ { if (y > 0 && model.Field[y][x] == model.Field[y-1][x]) || (x > 0 && model.Field[y][x] == model.Field[y][x-1]) { lost = false break Out } } } }
draw()
if winning { fmt.Println("You win!") return true } if lost { fmt.Println("Game Over") return true }
return false }
func main() { oldState, err := terminal.MakeRaw(0) check(err) defer terminal.Restore(0, oldState)
rand.Seed(time.Now().Unix())
for i := tilesAtStart; i > 0; i-- { addRandTile() } draw()
stdin := bufio.NewReader(os.Stdin)
readKey := func() rune { r, _, err := stdin.ReadRune() check(err) return r }
for !controller(readKey()) { } } </lang>
Java
<lang java>import java.awt.*; import java.awt.event.*; import java.util.Random; import javax.swing.*;
public class Game2048 extends JPanel {
enum State { start, won, running, over }
final Color[] colorTable = { new Color(0x701710), new Color(0xFFE4C3), new Color(0xfff4d3), new Color(0xffdac3), new Color(0xe7b08e), new Color(0xe7bf8e), new Color(0xffc4c3), new Color(0xE7948e), new Color(0xbe7e56), new Color(0xbe5e56), new Color(0x9c3931), new Color(0x701710)};
final static int target = 2048;
static int highest; static int score;
private Color gridColor = new Color(0xBBADA0); private Color emptyColor = new Color(0xCDC1B4); private Color startColor = new Color(0xFFEBCD);
private Random rand = new Random();
private Tile[][] tiles; private int side = 4; private State gamestate = State.start; private boolean checkingAvailableMoves;
public Game2048() { setPreferredSize(new Dimension(900, 700)); setBackground(new Color(0xFAF8EF)); setFont(new Font("SansSerif", Font.BOLD, 48)); setFocusable(true);
addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { startGame(); repaint(); } });
addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_UP: moveUp(); break; case KeyEvent.VK_DOWN: moveDown(); break; case KeyEvent.VK_LEFT: moveLeft(); break; case KeyEvent.VK_RIGHT: moveRight(); break; } repaint(); } }); }
@Override public void paintComponent(Graphics gg) { super.paintComponent(gg); Graphics2D g = (Graphics2D) gg; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
drawGrid(g); }
void startGame() { if (gamestate != State.running) { score = 0; highest = 0; gamestate = State.running; tiles = new Tile[side][side]; addRandomTile(); addRandomTile(); } }
void drawGrid(Graphics2D g) { g.setColor(gridColor); g.fillRoundRect(200, 100, 499, 499, 15, 15);
if (gamestate == State.running) {
for (int r = 0; r < side; r++) { for (int c = 0; c < side; c++) { if (tiles[r][c] == null) { g.setColor(emptyColor); g.fillRoundRect(215 + c * 121, 115 + r * 121, 106, 106, 7, 7); } else { drawTile(g, r, c); } } } } else { g.setColor(startColor); g.fillRoundRect(215, 115, 469, 469, 7, 7);
g.setColor(gridColor.darker()); g.setFont(new Font("SansSerif", Font.BOLD, 128)); g.drawString("2048", 310, 270);
g.setFont(new Font("SansSerif", Font.BOLD, 20));
if (gamestate == State.won) { g.drawString("you made it!", 390, 350);
} else if (gamestate == State.over) g.drawString("game over", 400, 350);
g.setColor(gridColor); g.drawString("click to start a new game", 330, 470); g.drawString("(use arrow keys to move tiles)", 310, 530); } }
void drawTile(Graphics2D g, int r, int c) { int value = tiles[r][c].getValue();
g.setColor(colorTable[(int) (Math.log(value) / Math.log(2)) + 1]); g.fillRoundRect(215 + c * 121, 115 + r * 121, 106, 106, 7, 7); String s = String.valueOf(value);
g.setColor(value < 128 ? colorTable[0] : colorTable[1]);
FontMetrics fm = g.getFontMetrics(); int asc = fm.getAscent(); int dec = fm.getDescent();
int x = 215 + c * 121 + (106 - fm.stringWidth(s)) / 2; int y = 115 + r * 121 + (asc + (106 - (asc + dec)) / 2);
g.drawString(s, x, y); }
private void addRandomTile() { int pos = rand.nextInt(side * side); int row, col; do { pos = (pos + 1) % (side * side); row = pos / side; col = pos % side; } while (tiles[row][col] != null);
int val = rand.nextInt(10) == 0 ? 4 : 2; tiles[row][col] = new Tile(val); }
private boolean move(int countDownFrom, int yIncr, int xIncr) { boolean moved = false;
for (int i = 0; i < side * side; i++) { int j = Math.abs(countDownFrom - i);
int r = j / side; int c = j % side;
if (tiles[r][c] == null) continue;
int nextR = r + yIncr; int nextC = c + xIncr;
while (nextR >= 0 && nextR < side && nextC >= 0 && nextC < side) {
Tile next = tiles[nextR][nextC]; Tile curr = tiles[r][c];
if (next == null) {
if (checkingAvailableMoves) return true;
tiles[nextR][nextC] = curr; tiles[r][c] = null; r = nextR; c = nextC; nextR += yIncr; nextC += xIncr; moved = true;
} else if (next.canMergeWith(curr)) {
if (checkingAvailableMoves) return true;
int value = next.mergeWith(curr); if (value > highest) highest = value; score += value; tiles[r][c] = null; moved = true; break; } else break; } }
if (moved) { if (highest < target) { clearMerged(); addRandomTile(); if (!movesAvailable()) { gamestate = State.over; } } else if (highest == target) gamestate = State.won; }
return moved; }
boolean moveUp() { return move(0, -1, 0); }
boolean moveDown() { return move(side * side - 1, 1, 0); }
boolean moveLeft() { return move(0, 0, -1); }
boolean moveRight() { return move(side * side - 1, 0, 1); }
void clearMerged() { for (Tile[] row : tiles) for (Tile tile : row) if (tile != null) tile.setMerged(false); }
boolean movesAvailable() { checkingAvailableMoves = true; boolean hasMoves = moveUp() || moveDown() || moveLeft() || moveRight(); checkingAvailableMoves = false; return hasMoves; }
public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame f = new JFrame(); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setTitle("2048"); f.setResizable(true); f.add(new Game2048(), BorderLayout.CENTER); f.pack(); f.setLocationRelativeTo(null); f.setVisible(true); }); }
}
class Tile {
private boolean merged; private int value;
Tile(int val) { value = val; }
int getValue() { return value; }
void setMerged(boolean m) { merged = m; }
boolean canMergeWith(Tile other) { return !merged && other != null && !other.merged && value == other.getValue(); }
int mergeWith(Tile other) { if (canMergeWith(other)) { value *= 2; merged = true; return value; } return -1; }
}</lang>
Kotlin
Stateless with focus on clarity rather than conciseness.
<lang kotlin> package com.sbg.g2048.stateless
import java.io.BufferedReader import java.io.InputStreamReader
const val positiveGameOverMessage = "So sorry, but you won the game." const val negativeGameOverMessage = "So sorry, but you lost the game."
fun main(args: Array<String>) {
val grid = arrayOf( arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0), arrayOf(0, 0, 0, 0) )
val gameOverMessage = run2048(grid) println(gameOverMessage)
}
fun run2048(grid: Array<Array<Int>>): String {
if (isGridSolved(grid)) return positiveGameOverMessage else if (isGridFull(grid)) return negativeGameOverMessage
val populatedGrid = spawnNumber(grid) display(populatedGrid)
return run2048(manipulateGrid(populatedGrid, waitForValidInput()))
}
fun isGridSolved(grid: Array<Array<Int>>): Boolean = grid.any { row -> row.contains(2048) } fun isGridFull(grid: Array<Array<Int>>): Boolean = grid.all { row -> !row.contains(0) }
fun spawnNumber(grid: Array<Array<Int>>):Array<Array<Int>> {
val coordinates = locateSpawnCoordinates(grid) val number = generateNumber()
return updateGrid(grid, coordinates, number)
}
fun locateSpawnCoordinates(grid: Array<Array<Int>>): Pair<Int, Int> {
val emptyCells = arrayListOf<Pair<Int, Int>>() grid.forEachIndexed { x, row -> row.forEachIndexed { y, cell -> if (cell == 0) emptyCells.add(Pair(x, y)) } }
return emptyCells[(Math.random() * (emptyCells.size()-1)).toInt()]
} fun generateNumber(): Int = if (Math.random() > 0.10) 2 else 4
fun updateGrid(grid: Array<Array<Int>>, at: Pair<Int, Int>, value: Int): Array<Array<Int>> {
val updatedGrid = grid.copyOf() updatedGrid[at.first][at.second] = value return updatedGrid
}
fun waitForValidInput():String {
val input = waitForInput() return if (isValidInput(input)) input else waitForValidInput()
} fun isValidInput(input: String): Boolean = arrayOf("a", "s", "d", "w").contains(input)
fun waitForInput(): String {
val br = BufferedReader(InputStreamReader(System.`in`)); println("Direction? ") return br.readLine()
}
fun manipulateGrid(grid: Array<Array<Int>>, input: String): Array<Array<Int>> = when (input) {
"a" -> shiftCellsLeft(grid) "s" -> shiftCellsDown(grid) "d" -> shiftCellsRight(grid) "w" -> shiftCellsUp(grid) else -> throw IllegalArgumentException("Expected one of [a, s, d, w]")
}
fun shiftCellsLeft(grid: Array<Array<Int>>): Array<Array<Int>> =
grid.map { row -> mergeAndOrganizeCells(row) }.toTypedArray()
fun shiftCellsRight(grid: Array<Array<Int>>): Array<Array<Int>> =
grid.map { row -> mergeAndOrganizeCells(row.reversed().toTypedArray()).reversed().toTypedArray() }.toTypedArray()
fun shiftCellsUp(grid: Array<Array<Int>>): Array<Array<Int>> {
val rows: Array<Array<Int>> = arrayOf( arrayOf(grid[0][0], grid[1][0], grid[2][0], grid[3][0]), arrayOf(grid[0][1], grid[1][1], grid[2][1], grid[3][1]), arrayOf(grid[0][2], grid[1][2], grid[2][2], grid[3][2]), arrayOf(grid[0][3], grid[1][3], grid[2][3], grid[3][3]) )
val updatedGrid = grid.copyOf()
rows.map { row -> mergeAndOrganizeCells(row) }.forEachIndexed { rowIdx, row -> updatedGrid[0][rowIdx] = row[0] updatedGrid[1][rowIdx] = row[1] updatedGrid[2][rowIdx] = row[2] updatedGrid[3][rowIdx] = row[3] }
return updatedGrid
}
fun shiftCellsDown(grid: Array<Array<Int>>): Array<Array<Int>> {
val rows: Array<Array<Int>> = arrayOf( arrayOf(grid[3][0], grid[2][0], grid[1][0], grid[0][0]), arrayOf(grid[3][1], grid[2][1], grid[1][1], grid[0][1]), arrayOf(grid[3][2], grid[2][2], grid[1][2], grid[0][2]), arrayOf(grid[3][3], grid[2][3], grid[1][3], grid[0][3]) )
val updatedGrid = grid.copyOf()
rows.map { row -> mergeAndOrganizeCells(row) }.forEachIndexed { rowIdx, row -> updatedGrid[3][rowIdx] = row[0] updatedGrid[2][rowIdx] = row[1] updatedGrid[1][rowIdx] = row[2] updatedGrid[0][rowIdx] = row[3] }
return updatedGrid
}
fun mergeAndOrganizeCells(row: Array<Int>): Array<Int> = organize(merge(row.copyOf()))
fun merge(row: Array<Int>, idxToMatch: Int = 0, idxToCompare: Int = 1): Array<Int> {
if (idxToMatch >= row.size) return row if (idxToCompare >= row.size) return merge(row, idxToMatch + 1, idxToMatch + 2) if (row[idxToMatch] == 0) return merge(row, idxToMatch + 1, idxToMatch + 2)
if (row[idxToMatch] == row[idxToCompare]) { row[idxToMatch] *= 2 row[idxToCompare] = 0
return merge(row, idxToMatch + 1, idxToMatch + 2) } else { return if (row[idxToCompare] != 0) merge(row, idxToMatch + 1, idxToMatch + 2) else merge(row, idxToMatch, idxToCompare + 1) }
}
fun organize(row: Array<Int>, idxToMatch: Int = 0, idxToCompare: Int = 1): Array<Int> {
if (idxToMatch >= row.size) return row if (idxToCompare >= row.size) return organize(row, idxToMatch + 1, idxToMatch + 2) if (row[idxToMatch] != 0) return organize(row, idxToMatch + 1, idxToMatch + 2)
if (row[idxToCompare] != 0) { row[idxToMatch] = row[idxToCompare] row[idxToCompare] = 0
return organize(row, idxToMatch + 1, idxToMatch + 2) } else { return organize(row, idxToMatch, idxToCompare + 1) }
}
fun display(grid: Array<Array<Int>>) {
val prettyPrintableGrid = grid.map { row -> row.map { cell -> if (cell == 0) " " else java.lang.String.format("%4d", cell) } }
println("New Grid:") prettyPrintableGrid.forEach { row -> println("+----+----+----+----+") row.forEach { print("|$it") } println("|") } println("+----+----+----+----+")
}</lang>
Sample output:
New Grid: +----+----+----+----+ | 2| | | | +----+----+----+----+ | | | | 2| +----+----+----+----+ | 4| 16| | | +----+----+----+----+ | 16| 4| 2| | +----+----+----+----+ Direction?
Perl 6
Uses termios to set the terminal options, so only compatible with POSIX terminals. This version does not include a specific "win" or "lose" condition. (though it would be trivial to add them.) You can continue to play this even after getting a 2048 tile; and if there is no valid move you can make, you can't do anything but quit.
<lang perl6>use Term::termios;
constant $saved = Term::termios.new(fd => 1).getattr; constant $termios = Term::termios.new(fd => 1).getattr;
- raw mode interferes with carriage returns, so
- set flags needed to emulate it manually
$termios.unset_iflags(<BRKINT ICRNL ISTRIP IXON>); $termios.unset_lflags(< ECHO ICANON IEXTEN ISIG>); $termios.setattr(:DRAIN);
- reset terminal to original setting on exit
END { $saved.setattr(:NOW) }
constant n = 4; # board size constant cell = 6; # cell width constant ansi = True; # color!
my @board = ( [ xx n] xx n ); my $save = ; my $score = 0;
constant $top = join '─' x cell, '┌', '┬' xx n-1, '┐'; constant $mid = join '─' x cell, '├', '┼' xx n-1, '┤'; constant $bot = join '─' x cell, '└', '┴' xx n-1, '┘';
my %dir = (
"\e[A" => 'up', "\e[B" => 'down', "\e[C" => 'right', "\e[D" => 'left',
);
my @ANSI = <0 1;97 1;93 1;92 1;96 1;91 1;95 1;94 1;30;47 1;43
1;42 1;46 1;41 1;45 1;44 1;33;43 1;33;42 1;33;41 1;33;44>;
sub row (@row) { '│' ~ (join '│', @row».¢er) ~ '│' }
sub center ($s){
my $c = cell - $s.chars; my $pad = ' ' x ceiling($c/2); my $tile = sprintf "%{cell}s", "$s$pad"; my $idx = $s ?? $s.log(2) !! 0; ansi ?? "\e[{@ANSI[$idx]}m$tile\e[0m" !! $tile;
}
sub draw-board {
run('clear'); print qq:to/END/;
Press direction arrows to move.
Press q to quit.
$top { join "\n\t$mid\n\t", map { .&row }, @board } $bot
Score: $score
END }
sub squash (@c) {
my @t = grep { .chars }, @c; map { combine(@t[$_], @t[$_+1]) if @t[$_] && @t[$_+1] == @t[$_] }, ^@t-1; @t = grep { .chars }, @t; @t.push: while @t < n; @t;
}
sub combine ($v is rw, $w is rw) { $v += $w; $w = ; $score += $v; }
multi sub move('up') {
map { @board[*;$_] = squash @board[*;$_] }, ^n;
}
multi sub move('down') {
map { @board[*;$_] = reverse squash reverse @board[*;$_] }, ^n;
}
multi sub move('left') {
map { @board[$_] = squash @board[$_] }, ^n;
}
multi sub move('right') {
map { @board[$_] = reverse squash reverse @board[$_] }, ^n;
}
sub another {
my @empties; for @board.kv -> $r, @row { @empties.push(($r, $_)) for @row.grep(:k, ); } my ( $x, $y ) = @empties.roll; @board[$x; $y] = (flat 2 xx 9, 4).roll;
}
sub save () { join '|', flat @board».list }
loop {
another if $save ne save(); draw-board; $save = save();
# Read up to 4 bytes from keyboard buffer. # Page navigation keys are 3-4 bytes each. # Specifically, arrow keys are 3. my $key = $*IN.read(4).decode;
move %dir{$key} if so %dir{$key}; last if $key eq 'q'; # (q)uit
}</lang> Sample output:
Press direction arrows to move. Press q to quit. ┌──────┬──────┬──────┬──────┐ │ 4 │ 2 │ │ │ ├──────┼──────┼──────┼──────┤ │ 16 │ 8 │ │ │ ├──────┼──────┼──────┼──────┤ │ 64 │ 32 │ 16 │ │ ├──────┼──────┼──────┼──────┤ │ 128 │ 512 │ 128 │ 64 │ └──────┴──────┴──────┴──────┘ Score: 6392
Phix
Faithful desktop gui (windows only) reproduction of the above link (https://gabrielecirulli.github.io/2048/) Now I just got figure out how to win... <lang Phix>include ..\arwen\arwen.ew include ..\arwen\axtra.ew
constant main = create(Window, "2048", 0, 0, 20, 20, 520, 540, 0),
mainDC = getPrivateDC(main), viewDC = c_func(xCreateCompatibleDC, {NULL})
constant TA_CENTER = 6 {} = c_func(xSetTextAlign,{viewDC,TA_CENTER}) constant hFont40 = createFontForDC(viewDC, "Calibri", 40, Bold) constant hFont32 = createFontForDC(viewDC, "Calibri", 32, Bold)
constant tile_colours = {#B4C0CC, -- blank
#DAE4EE, -- 2 #C8E0ED, -- 4 #79B1F2, -- 8 #6395F5, -- 16 #5F7CF6, -- 32 #3B5EF6, -- 64 #72CFED, -- 128 #61CCED, -- 256 #50C8ED, -- 512 #3FC5ED, -- 1024 #2EC2ED} -- 2048
-- the 4x4 board. -- note that values are [1..12] for [blank,2,4,8,..2048]. -- (merging two eights is not 8+8->16 but 4+1->5, internally) sequence board
integer newgame = 1
procedure add_rand(integer count) -- (nb infinite loop if board is full) integer x, y
while count do x = rand(4) y = rand(4) if board[y][x]=1 then -- blank board[y][x] = 2+(rand(10)=10) count -= 1 end if end while
end procedure
integer valid = 0 integer prev, nxt, bxy
procedure move_x(integer x, integer y, integer d)
bxy = board[x][y] if bxy!=1 then if bxy=prev then board[x][y] = 1 bxy += 1 board[x][nxt] = bxy nxt += d prev = 13 valid = 1 else if prev=1 or y!=nxt then if prev!=1 and prev!=13 then nxt += d end if if y!=nxt then board[x][y] = 1 board[x][nxt] = bxy valid = 1 end if end if prev = bxy end if end if
end procedure
procedure move_y(integer x, integer y, integer d)
bxy = board[x][y] if bxy!=1 then if bxy=prev then board[x][y] = 1 bxy += 1 board[nxt][y] = bxy nxt += d prev = 13 valid = 1 else if prev=1 or x!=nxt then if prev!=1 and prev!=13 then nxt += d end if if x!=nxt then board[x][y] = 1 board[nxt][y] = bxy valid = 1 end if end if prev = bxy end if end if
end procedure
function move(integer key) -- a non-zero result means it changed something.
valid = 0 if key=VK_LEFT then for x=1 to 4 do prev = 13 nxt = 1 for y=1 to 4 do move_x(x,y,+1) end for end for elsif key=VK_DOWN then for y=1 to 4 do prev = 13 nxt = 4 for x=4 to 1 by -1 do move_y(x,y,-1) end for end for elsif key=VK_RIGHT then for x=1 to 4 do prev = 13 nxt = 4 for y=4 to 1 by -1 do move_x(x,y,-1) end for end for elsif key=VK_UP then for y=1 to 4 do prev = 13 nxt = 1 for x=1 to 4 do move_y(x,y,+1) end for end for end if return valid
end function
function game_won()
for i=1 to length(board) do if find(12,board[i]) then return 1 end if end for return 0
end function
constant valid_keys = {VK_LEFT,VK_DOWN,VK_RIGHT,VK_UP}
function no_valid_moves() sequence saved_board = board
for i=1 to length(valid_keys) do if move(valid_keys[i]) then board = saved_board return 0 -- OK end if end for return 1 -- game over...
end function
integer dw = 0, dh = 0 -- client area width and height atom bmView integer vwX = 0, vwY = 0 -- actual size of the view bitmap
integer ox,oy, -- top tight coords
os,ts, -- overall and tile size ts2 -- half tile, for number positioning
function mainHandler(integer id, integer msg, atom wParam, object lParam) integer tx, ty, bxy string mbmsg
if msg=WM_SIZE then {{},{},dw,dh} = getClientRect(main) if dw>vwX or dh>vwY then -- we need a bigger bitmap bmView = c_func(xCreateCompatibleBitmap, {mainDC, dw, dh}) {} = deleteObject(selectObject(viewDC,bmView)) {vwX,vwY} = {dw,dh} end if if dw>=dh then ox = floor((dw-dh)/2) oy = 0 os = dh else ox = 0 oy = floor((dh-dw)/2) os = dw end if ts = floor((os-10)/4-7) ts2 = floor(ts/2+5)-10 elsif msg=WM_PAINT then if newgame then board = repeat(repeat(1,4),4) add_rand(2) newgame = 0 end if setPenColor(#EFF8FA) drawRectangleh(viewDC, True, 0, 0, dw, dh) setPenColor(#A0ADBB) drawRoundRecth(viewDC, True, ox+5, oy+5, ox+os-5, oy+os-5, 10, 10) tx = ox+15 for y=1 to 4 do ty = oy+15 for x=1 to 4 do bxy = board[x][y] setPenColor(tile_colours[bxy]) drawRoundRecth(viewDC, True, tx, ty, tx+ts-10, ty+ts-10, 5, 5) if bxy>1 then setPenColor(iff(bxy<=3?#656E77:#F2F6F9)) {} = selectObject(viewDC,iff(bxy>10?hFont32:hFont40)) wPuts2(viewDC, tx+ts2, ty+ts2-25-iff(bxy<11?7:0), power(2,bxy-1)) end if ty += ts+5 end for tx += ts+5 end for void = c_func(xBitBlt,{mainDC,0,0,dw,dh,viewDC,0,0,SRCCOPY}) elsif msg=WM_CHAR then if wParam=VK_ESCAPE then closeWindow(main) if id or object(lParam) then end if -- suppress warnings end if elsif msg=WM_KEYDOWN then if move(wParam) then mbmsg = "" if game_won() then mbmsg = "!!!YOU WON!!!\n\nAnother Go?" else add_rand(1) repaintWindow(main) if no_valid_moves() then mbmsg = "You Lost.\n\nAnother Go?" end if end if if length(mbmsg) then if messageBox("Game Over",mbmsg,MB_YESNO)=IDYES then newgame=1 else closeWindow(main) end if end if end if elsif msg=WM_GETMINMAXINFO then poke4(lParam+MINMAXINFO_ptMinTrackSize,{440,450}) end if return 0
end function setHandler({main},routine_id("mainHandler"))
WinMain(main, SW_NORMAL)</lang>
PicoLisp
<lang PicoLisp>(load "@lib/simul.l")
(seed (in "/dev/urandom" (rd 8)))
(setq *G (grid 4 4) *D NIL)
(de cell ()
(use This (while (get (setq This (intern (pack (char (+ 96 (rand 1 4))) (rand 1 4) ) ) ) 'N ) ) (=: N (if (> 90 (rand 1 100)) 2 4) ) ) (setq *D (fish '((This) (: N)) *G)) )
(de redraw (G S D)
# zeroize *G (mapc '((I) (mapc '((This) (=: N NIL)) I) ) *G ) # draw again (mapc '((X This) (while (and This X) (=: N (pop 'X)) (setq This (D This)) ) ) G S ) )
(de summ (Lst)
(mapcar '((L) (make (while L (ifn (= (car L) (cadr L)) (link (car L)) (link (+ (car L) (cadr L))) (pop 'L) ) (pop 'L) ) ) ) Lst ) )
(de vertical ()
(mapcar '((X) (extract '((This) (: N)) X)) *G ) )
(de horizontal ()
(mapcar '((This) (make (while This (when (: N) (link @)) (setq This (east This)) ) ) ) (car *G) ) )
(de finish? ()
(nor (fish '((This) (when (atom This) (= NIL (: N))) ) *G ) (find '((L) (find '((This) (when (: N) (find '((D) (= (: N) (get (D This) 'N)) ) (quote north south west east) ) ) ) L ) ) *G ) ) )
(de board (D)
(space 3) (prin '+) (for I G (prin (if (D (car I)) " +" "---+")) ) (prinl) )
(de display ()
(let G (mapcar reverse *G) (board north) (while (caar G) (space 3) (prin '|) (for I G (with (car I) (prin (if (: N) (align 3 (: N)) " ") (if (east This) " " '|) ) ) ) (prinl) (board south) (map pop G) ) (do 2 (prinl) ) ) )
(do 2
(cell) )
(display) (loop
(case (pack (make (link (key)) (while (key 100) (link @) ) ) ) ("^[[D" #left (redraw (summ (horizontal)) '(a1 a2 a3 a4) east) ) ("^[[C" #rigth (redraw (summ (mapcar reverse (horizontal))) '(d1 d2 d3 d4) west) ) ("^[[B" #down (redraw (summ (vertical)) '(a1 b1 c1 d1) north) ) ("^[[A" #up (redraw (summ (mapcar reverse (vertical))) '(a4 b4 c4 d4) south) ) ) (when (diff *D (fish '((This) (: N)) *G)) (cell) ) (display) (T (finish?) (println 'Finish)) (T (fish '((This) (= 512 (: N))) *G) (println 'Maximum) ) )
(bye)</lang>
Python
<lang python>
- !/usr/bin/env python3
import curses from random import randrange, choice # generate and place new tile from collections import defaultdict
letter_codes = [ord(ch) for ch in 'WASDRQwasdrq'] actions = ['Up', 'Left', 'Down', 'Right', 'Restart', 'Exit'] actions_dict = dict(zip(letter_codes, actions * 2))
def get_user_action(keyboard): char = "N" while char not in actions_dict: char = keyboard.getch() return actions_dict[char]
def transpose(field): return [list(row) for row in zip(*field)]
def invert(field): return [row[::-1] for row in field]
class GameField(object): def __init__(self, height=4, width=4, win=2048): self.height = height self.width = width self.win_value = 2048 self.score = 0 self.highscore = 0 self.reset()
def reset(self): if self.score > self.highscore: self.highscore = self.score self.score = 0 self.field = [[0 for i in range(self.width)] for j in range(self.height)] self.spawn() self.spawn()
def move(self, direction): def move_row_left(row): def tighten(row): # squeese non-zero elements together new_row = [i for i in row if i != 0] new_row += [0 for i in range(len(row) - len(new_row))] return new_row
def merge(row): pair = False new_row = [] for i in range(len(row)): if pair: new_row.append(2 * row[i]) self.score += 2 * row[i] pair = False else: if i + 1 < len(row) and row[i] == row[i + 1]: pair = True new_row.append(0) else: new_row.append(row[i]) assert len(new_row) == len(row) return new_row return tighten(merge(tighten(row)))
moves = {} moves['Left'] = lambda field: \ [move_row_left(row) for row in field] moves['Right'] = lambda field: \ invert(moves['Left'](invert(field))) moves['Up'] = lambda field: \ transpose(moves['Left'](transpose(field))) moves['Down'] = lambda field: \ transpose(moves['Right'](transpose(field)))
if direction in moves: if self.move_is_possible(direction): self.field = moves[direction](self.field) self.spawn() return True else: return False
def is_win(self): return any(any(i >= self.win_value for i in row) for row in self.field)
def is_gameover(self): return not any(self.move_is_possible(move) for move in actions)
def draw(self, screen): help_string1 = '(W)Up (S)Down (A)Left (D)Right' help_string2 = ' (R)Restart (Q)Exit' gameover_string = ' GAME OVER' win_string = ' YOU WIN!' def cast(string): screen.addstr(string + '\n')
def draw_hor_separator(): top = '┌' + ('┬──────' * self.width + '┐')[1:] mid = '├' + ('┼──────' * self.width + '┤')[1:] bot = '└' + ('┴──────' * self.width + '┘')[1:] separator = defaultdict(lambda: mid) separator[0], separator[self.height] = top, bot if not hasattr(draw_hor_separator, "counter"): draw_hor_separator.counter = 0 cast(separator[draw_hor_separator.counter]) draw_hor_separator.counter += 1
def draw_row(row): cast(.join('│{: ^5} '.format(num) if num > 0 else '| ' for num in row) + '│')
screen.clear() cast('SCORE: ' + str(self.score)) if 0 != self.highscore: cast('HGHSCORE: ' + str(self.highscore)) for row in self.field: draw_hor_separator() draw_row(row) draw_hor_separator() if self.is_win(): cast(win_string) else: if self.is_gameover(): cast(gameover_string) else: cast(help_string1) cast(help_string2)
def spawn(self): new_element = 4 if randrange(100) > 89 else 2 (i,j) = choice([(i,j) for i in range(self.width) for j in range(self.height) if self.field[i][j] == 0]) self.field[i][j] = new_element
def move_is_possible(self, direction): def row_is_left_movable(row): def change(i): # true if there'll be change in i-th tile if row[i] == 0 and row[i + 1] != 0: # Move return True if row[i] != 0 and row[i + 1] == row[i]: # Merge return True return False return any(change(i) for i in range(len(row) - 1))
check = {} check['Left'] = lambda field: \ any(row_is_left_movable(row) for row in field)
check['Right'] = lambda field: \ check['Left'](invert(field))
check['Up'] = lambda field: \ check['Left'](transpose(field))
check['Down'] = lambda field: \ check['Right'](transpose(field))
if direction in check: return check[direction](self.field) else: return False
def main(stdscr): curses.use_default_colors() game_field = GameField(win=32) state_actions = {} # Init, Game, Win, Gameover, Exit def init(): game_field.reset() return 'Game'
state_actions['Init'] = init
def not_game(state): game_field.draw(stdscr) action = get_user_action(stdscr) responses = defaultdict(lambda: state) responses['Restart'], responses['Exit'] = 'Init', 'Exit' return responses[action]
state_actions['Win'] = lambda: not_game('Win') state_actions['Gameover'] = lambda: not_game('Gameover')
def game(): game_field.draw(stdscr) action = get_user_action(stdscr) if action == 'Restart': return 'Init' if action == 'Exit': return 'Exit' if game_field.move(action): # move successful if game_field.is_win(): return 'Win' if game_field.is_gameover(): return 'Gameover' return 'Game'
state_actions['Game'] = game
state = 'Init' while state != 'Exit': state = state_actions[state]()
curses.wrapper(main) </lang>
Ruby
inspired by the Perl6 version <lang ruby>
- !/usr/bin/ruby
require 'io/console'
class Board
def initialize size=4, win_limit=2048, cell_width = 6 @size = size; @cw = cell_width; @win_limit = win_limit @board = Array.new(size) {Array.new(size, 0)} @moved = true; @score = 0; @no_more_moves = false spawn end
def draw print "\n\n" if @r_vert print ' ' if @r_hori print '┌' + (['─' * @cw] * @size).join('┬') + '┐' @board.each do |row| print "\n" formated = row.map {|num| num == 0 ? ' ' * @cw : format(num)} print ' ' if @r_hori puts '│' + formated.join('│') + '│' print ' ' if @r_hori print '├' + ([' ' * @cw] * @size).join('┼') + '┤' end print "\r" print ' ' if @r_hori puts '└' + (['─' * @cw] * @size).join('┴') + '┘' end
def move direction case direction when :up @board = column_map {|c| logic(c)} @r_vert = false if $rumble when :down @board = column_map {|c| logic(c.reverse).reverse} @r_vert = true if $rumble when :left @board = row_map {|r| logic(r)} @r_hori = false if $rumble when :right @board = row_map {|r| logic(r.reverse).reverse} @r_hori = true if $rumble end spawn @moved = false end
def print_score puts "Your Score is #@score." puts "Congratulations, you have won!" if to_enum.any? {|e| e >= @win_limit} end
def no_more_moves?; @no_more_moves; end def won?; to_enum.any? {|e| e >= @win_limit}; end def reset!; initialize @size, @win_limit, @cw; end
private
def set x, y, val @board[y][x] = val end
def spawn free_pos = to_enum.select{|elem,x,y| elem == 0}.map{|_,x,y| [x,y]} unless free_pos.empty? set *free_pos.sample, rand > 0.1 ? 2 : 4 if @moved else snap = @board unless @stop @stop = true %i{up down left right}.each{|s| move(s)} @no_more_moves = true if snap.flatten == @board.flatten @board = snap @stop = false end end end
def logic list jump = false result = list.reduce([]) do |res, val| if res.last == val && !jump
res[-1] += val @score += val
jump = true elsif val != 0
res.push val
jump = false end res end result += [0] * (@size - result.length) @moved ||= list != result result end
def column_map xboard = @board.transpose xboard.map!{|c| yield c } xboard.transpose end
def row_map @board.map {|r| yield r } end
def to_enum @enum ||= Enumerator.new(@size * @size) do |yielder| (@size*@size).times do |i|
yielder.yield (@board[i / @size][i % @size]), (i % @size), (i / @size )
end end @enum.rewind end
def format(num) if $color cstart = "\e[" + $colors[Math.log(num, 2)] + "m" cend = "\e[0m" else cstart = cend = "" end cstart + num.to_s.center(@cw) + cend end
end
$color = true $colors = %W{0 1;97 1;93 1;92 1;96 1;91 1;95 1;94 1;30;47 1;43 1;42 1;46 1;41 1;45 1;44 1;33;43 1;33;42 1;33;41 1;33;44} $rumble = false
$check_score = true unless ARGV.empty?
puts "Usage: #$0 [gridsize] [score-threshold] [padwidth] [--no-color] [--rumble]"; exit if %W[-h --help].include?(ARGV[0]) args = ARGV.map(&:to_i).reject{|n| n == 0} b = Board.new(*args) unless args.empty? $rumble = true if ARGV.any?{|a| a =~ /rumble/i } $color = false if ARGV.any?{|a| a =~ /no.?color/i}
end
b ||= Board.new puts "\e[H\e[2J" b.draw puts "Press h for help, q to quit" loop do
input = STDIN.getch if input == "\e" 2.times {input << STDIN.getch} end
case input when "\e[A", "w" then b.move(:up) when "\e[B", "a" then b.move(:down) when "\e[C", "s" then b.move(:right) when "\e[D", "d" then b.move(:left) when "q","\u0003","\u0004" then b.print_score; exit
when "h" puts <<-EOM.gsub(/^\s*/, ) ┌─ ─┐ │Use the arrow-keys or WASD on your keyboard to push board in the given direction. │Tiles with the same number merge into one. │Get a tile with a value of #{ARGV[1] || 2048} to win. │In case you cannot move or merge any tiles anymore, you loose. │You can start this game with different settings by providing commandline argument: │For instance: │ %> #$0 6 8192 --rumble └─ ─┘ PRESS q TO QUIT (or Ctrl-C or Ctrl-D) EOM input = STDIN.getch end
puts "\e[H\e[2J" b.draw
if b.no_more_moves? or $check_score && b.won? b.print_score if b.no_more_moves? puts "No more moves possible" puts "Again? (y/n)" exit if STDIN.gets.chomp.downcase == "n" $check_score = true b.reset! puts "\e[H\e[2J" b.draw else puts "Continue? (y/n)" exit if STDIN.gets.chomp.downcase == "n" $check_score = false puts "\e[H\e[2J" b.draw end end
end </lang>
Tcl
Text mode
<lang tcl>
- A minimal implementation of the game 2048 in Tcl.
package require Tcl 8.5 package require struct::matrix package require struct::list
- Board size.
set size 4
- Iterate over all cells of the game board and run script for each.
- The game board is a 2D matrix of a fixed size that consists of elements
- called "cells" that each can contain a game tile (corresponds to numerical
- values of 2, 4, 8, ..., 2048) or nothing (zero).
- - cellList is a list of cell indexes (coordinates), which are
- themselves lists of two numbers each. They each represent the location
- of a given cell on the board.
- - varName1 are varName2 are names of the variables the will be assigned
- the index values.
- - cellVarName is the name of the variable that at each step of iteration
- will contain the numerical value of the present cell. Assigning to it will
- change the cell's value.
- - script is the script to run.
proc forcells {cellList varName1 varName2 cellVarName script} {
upvar $varName1 i upvar $varName2 j upvar $cellVarName c foreach cell $cellList { set i [lindex $cell 0] set j [lindex $cell 1] set c [cell-get $cell] uplevel $script cell-set "$i $j" $c }
}
- Generate a list of cell indexes for all cells on the board, i.e.,
- {{0 0} {0 1} ... {0 size-1} {1 0} {1 1} ... {size-1 size-1}}.
proc cell-indexes {} {
global size set list {} foreach i [::struct::list iota $size] { foreach j [::struct::list iota $size] { lappend list [list $i $j] } } return $list
}
- Check if a number is a valid cell index (is 0 to size-1).
proc valid-index {i} {
global size expr {0 <= $i && $i < $size}
}
- Return 1 if the predicate pred is true when applied to all items on the list
- or 0 otherwise.
proc map-and {list pred} {
set res 1 foreach item $list { set res [expr {$res && [$pred $item]}] if {! $res} break } return $res
}
- Check if list represents valid cell coordinates.
proc valid-cell? cell {
map-and $cell valid-index
}
- Get the value of a game board cell.
proc cell-get cell {
board get cell {*}$cell
}
- Set the value of a game board cell.
proc cell-set {cell value} {
board set cell {*}$cell $value
}
- Filter a list of board cell indexes cellList to only have those indexes
- that correspond to empty board cells.
proc empty {cellList} {
::struct::list filterfor x $cellList {[cell-get $x] == 0}
}
- Pick a random item from the given list.
proc pick list {
lindex $list [expr {int(rand() * [llength $list])}]
}
- Put a "2" into an empty cell on the board.
proc spawn-new {} {
set emptyCell [pick [empty [cell-indexes]]] if {[llength $emptyCell] > 0} { forcells [list $emptyCell] i j cell { set cell 2 } } return $emptyCell
}
- Return vector sum of lists v1 and v2.
proc vector-add {v1 v2} {
set result {} foreach a $v1 b $v2 { lappend result [expr {$a + $b}] } return $result
}
- If checkOnly is false try to shift all cells one step in the direction of
- directionVect. If checkOnly is true just say if that move is possible.
proc move-all {directionVect {checkOnly 0}} {
set changedCells 0
forcells [cell-indexes] i j cell { set newIndex [vector-add "$i $j" $directionVect] set removedStar 0
# For every nonempty source cell and valid destination cell... if {$cell != 0 && [valid-cell? $newIndex]} { if {[cell-get $newIndex] == 0} { # Destination is empty. if {$checkOnly} { # -level 2 is to return from both forcells and move-all. return -level 2 true } else { # Move tile to empty cell. cell-set $newIndex $cell set cell 0 incr changedCells } } elseif {([cell-get $newIndex] eq $cell) && [string first + $cell] == -1} { # Destination is the same number as source. if {$checkOnly} { return -level 2 true } else { # When merging two tiles into one mark the new tile with # the marker of "+" to ensure it doesn't get combined # again this turn. cell-set $newIndex [expr {2 * $cell}]+ set cell 0 incr changedCells } } } }
if {$checkOnly} { return false }
# Remove "changed this turn" markers at the end of the turn. if {$changedCells == 0} { forcells [cell-indexes] i j cell { set cell [string trim $cell +] } } return $changedCells
}
- Is it possible to move any tiles in the direction of directionVect?
proc can-move? {directionVect} {
move-all $directionVect 1
}
- Check win condition. The player wins when there's a 2048 tile.
proc check-win {} {
forcells [cell-indexes] i j cell { if {$cell == 2048} { puts "You win!" exit 0 } }
}
- Check lose condition. The player loses when the win condition isn't met and
- there are no possible moves.
proc check-lose {possibleMoves} {
set values [dict values $possibleMoves] if {!(true in $values || 1 in $values)} { puts "You lose." exit 0 }
}
- Pretty-print the board. Specify an index in highlight to highlight a cell.
proc print-board {{highlight {-1 -1}}} {
forcells [cell-indexes] i j cell { if {$j == 0} { puts "" } puts -nonewline [ if {$cell != 0} { if {[::struct::list equal "$i $j" $highlight]} { format "\[%4s\]" $cell* } else { format "\[%4s\]" $cell }
} else { lindex "......" } ] } puts "\n"
}
proc main {} {
global size
struct::matrix board
# Generate an empty board of a given size. board add columns $size board add rows $size forcells [cell-indexes] i j cell { set cell 0 }
set controls { h {0 -1} j {1 0} k {-1 0} l {0 1} }
# Game loop. while true { set playerMove 0 set possibleMoves {}
# Add new tile to the board and print the board highlighting this tile. print-board [spawn-new]
check-win
# Find possible moves. foreach {button vector} $controls { dict set possibleMoves $button [can-move? $vector] } check-lose $possibleMoves
# Get valid input from the player. while {$playerMove == 0} { # Print prompt. puts -nonewline "Move (" foreach {button vector} $controls { if {[dict get $possibleMoves $button]} { puts -nonewline $button } } puts ")?"
set playerInput [gets stdin]
# Validate input. if {[dict exists $possibleMoves $playerInput] && [dict get $possibleMoves $playerInput]} { set playerMove [dict get $controls $playerInput] } }
# Apply current move until no changes occur on the board. while true { if {[move-all $playerMove] == 0} break } }
}
main </lang>