Task

Implement a 2D sliding block puzzle game where blocks with numbers are combined to add their values.



Rules of the game

   The rules are that on each turn the player must choose a direction   (up, down, left or right).
   All tiles move as far as possible in that direction, some move more than others. 
   Two adjacent tiles (in that direction only) with matching numbers combine into one bearing the sum of those numbers. 
   A move is valid when at least one tile can be moved,   if only by combination. 
   A new tile with the value of   2   is spawned at the end of each turn at a randomly chosen empty square   (if there is one). 
   Adding a new tile on a blank space.   Most of the time,   a new   2   is to be added,   and occasionally   (10% of the time),   a   4.
   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 shall indicate which combination 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] 



   Check for valid moves.   The player shouldn't be able to skip their turn by trying a move that doesn't change the board.
   Check for a  win condition.
   Check for a lose condition.


/* -------------------------------------------------------------
   Entry point, just create a blank grid and enter a 'game loop'
   -------------------------------------------------------------*/
play_2048 :- 
	welcome_msg,
	length(Grid, 16), maplist(=(' '), Grid), % create a blank grid
	play(Grid, yes), !. % don't have to cut here but it makes the exit cleaner
 
 
/* -----------------------------------------------
   Messages that will be printed at various points 
   -----------------------------------------------*/
welcome_msg :- 	
	format('~nWelcome to the Prolog version of 2048~n~n'),
	format('To play using w,s,a,d keys for movement, q to quit~n~n').	
contrats_msg :- format('Congratulations, you reached 2048!~n~n').
loser_msg :- format('Uh Oh, you could not quite make it to 2048...~n~n').
quit_msg :- format('Bye then!~n~n').
 
 
/* -------------------
   End game conditions 
   -------------------*/
player_not_won_yet(Grid) :- maplist(dif(2048), Grid).
player_wins(Grid) :- member(2048, Grid).
player_loses(G) :- move(up, G, G), move(down, G, G), move(left, G, G), move(right, G, G).
 
 
/* ---------
   Game loop 
   ---------*/	
% First check if the player has reached the win condition, if not find how many spaces are left
play(Grid, _) :- 	
	player_wins(Grid), 
	draw_grid(Grid),
	contrats_msg.
play(Grid, CreateNewNum) :- 
	player_not_won_yet(Grid), 
	include(=(' '), Grid, Spaces), length(Spaces, NSpaces), % the number of spaces in the grid
	play(Grid, CreateNewNum, NSpaces).
 
% knowing the number of spaces, determine if there is a space, and if we need a new number, and generate.	
play(Grid, no, _) :- play(Grid).
play(Grid, yes, 0) :- play(Grid).
play(Grid, yes, NSpaces) :- 
	dif(NSpaces, 0), 
	random_space(NSpaces, Grid, GridWithRandom), 
	play(GridWithRandom).
 
% with the new number on the grid we can tell if the player has lost the game yet
% if not, draw the grid and get the next action by the player 
play(Grid) :- 
	player_loses(Grid), 
	draw_grid(Grid), 
	loser_msg.		
play(Grid) :- 
	\+ player_loses(Grid),
	draw_grid(Grid),
	next_move_by_player(Move),
	player_made_move(Grid, Move).
 
% determine the result of player move	
player_made_move(_, quit).
player_made_move(Grid, Move) :- 
	dif(Move, quit), 
	move(Move, Grid, Grid), % The move creating the same grid indicates that no merge was done
	play(Grid, no). % don't create a new number
player_made_move(Grid, Move) :- 
	dif(Move, quit), 
	move(Move, Grid, MovedGrid), 
	dif(Grid, MovedGrid),
	play(MovedGrid, yes).
 
 
/* ---------------------------------------
   Get the next move from the player input 
   ---------------------------------------*/	
next_move_by_player(Move) :- 
	repeat, 
	get_single_char(Char), 
	key_move(Char, Move).
 
% valid keys are: up = 'w', down = 's', left = 'a' right = 'd', quit = 'q'
key_move(119, up). key_move(115, down).	key_move(97, left). key_move(100, right). key_move(113, quit).
 
 
/* ------------------
   Draw the Game grid 
   ------------------*/
draw_grid([A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2,D3,D4]) :-
	format(   '+-------------------+~n'),
	row([A1,A2,A3,A4]), 
	row([B1,B2,B3,B4]), 
	row([C1,C2,C3,C4]), 
	maplist(draw, [D1,D2,D3,D4]),
	format('¦~n+-------------------+~n~n~n').
 
row([A,B,C,D]) :- maplist(draw, [A,B,C,D]), format('¦~n¦----+----+----+----¦~n').	
 
draw(' ') :- format('¦    ').
draw(X) :- member(X,[2,4,8]), format('¦  ~d ', X).
draw(X) :- member(X,[16,32,64]), format('¦ ~d ', X).
draw(X) :- member(X,[128,256,512]), format('¦ ~d', X).
draw(X) :- member(X,[1024,2048]), format('¦~d', X).
 
 
/* ----------------------------------------
   Populate a random space with a new value 
   ----------------------------------------*/
random_space(0, G, G).
random_space(1, Grid, GridWithRandom) :- 
	four_or_two(V), 
	select(' ', Grid, V, GridWithRandom).
random_space(N, Grid, GridWithRandom) :-
	N > 1,
	four_or_two(V),
	random(1, N, P),	
	replace_space(P, V, Grid, GridWithRandom).
 
replace_space(0, V, [' '|T], [V|T]).
replace_space(P, V, [' '|T], [' '|R]) :-	succ(NextP, P),	replace_space(NextP, V, T, R).
replace_space(P, V, [H|T], [H|R]) :-	dif(' ', H), replace_space(P, V, T, R).
 
four_or_two(V) :- random(1, 10, IsFour), IsFour = 1 -> V = 4 ; V = 2.
 
 
/* ------------------------------------------
   Process a game move based on the direction 
   ------------------------------------------*/
move(Direction, UnMoved, Moved) :-
	map_move(Direction, UnMoved, UnMovedMapped),
	maplist(combine_row, UnMovedMapped, MovedMapped),
	map_move(Direction, Moved, MovedMapped).
 
% convert the array to a set of lists that can be moved in the same
% direction. This can be reversed after the move is completed.
map_move(up, [A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2,D3,D4], [[D1,C1,B1,A1],[D2,C2,B2,A2],[D3,C3,B3,A3],[D4,C4,B4,A4]]).
map_move(down, [A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2,D3,D4], [[A1,B1,C1,D1],[A2,B2,C2,D2],[A3,B3,C3,D3],[A4,B4,C4,D4]]).
map_move(left, [A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2,D3,D4], [[A4,A3,A2,A1],[B4,B3,B2,B1],[C4,C3,C2,C1],[D4,D3,D2,D1]]).
map_move(right, [A1,A2,A3,A4,B1,B2,B3,B4,C1,C2,C3,C4,D1,D2,D3,D4], [[A1,A2,A3,A4],[B1,B2,B3,B4],[C1,C2,C3,C4],[D1,D2,D3,D4]]).
 
% remove all the spaces, then put them at the front of the list
combine_row(UnMoved, Moved) :-
	partition(=(' '), UnMoved, Blank, Set),
	append(Blank, Set, ReadyToMove),
	combine(ReadyToMove, Moved).
 
% combine based on the rules of the game.
combine([A,B,C,D], [A,B,C,D]) :- dif(A,B), dif(B,C), dif(C,D).	
combine([A,A,B,B], [' ',' ',Ad,Bd]) :- dbl(A,Ad), dbl(B,Bd).
combine([A,B,C,C], [' ',A,B,Cd]) :- dif(A,B), dbl(C,Cd).
combine([A,B,B,C], [' ',A,Bd,C]) :-	dif(B,C), dbl(B,Bd).
combine([A,A,B,C], [' ',Ad,B,C]) :- dif(A,B), dif(B,C), dbl(A, Ad).
combine([A,B,C,C], [' ',A,B,Cd]) :- dif(A,B), dif(B,C), dbl(C,Cd).
 
% this could be done using maths, but it is more prology this way.
dbl(' ', ' '). 
dbl(2,4). dbl(4,8). dbl(8,16). dbl(16,32). dbl(32,64). dbl(64,128). dbl(128,256). dbl(256,512). dbl(512,1028). dbl(1028,2048).
 