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.


$ include "seed7_05.s7i";
  include "console.s7i";
  include "keybd.s7i";
 
const integer: boardLength is 4;
const integer: boardSize is boardLength * boardLength;
const integer: target is 2048;
 
const type: stateType is new struct
    var integer: fieldsOccupied is 0;
    var integer: largestNumber is 0;
    var integer: score is 0;
    var array array integer: board is boardLength times boardLength times 0;
  end struct;
 
const proc: addTile (inout stateType: state) is func
  local
    var integer: row is 0;
    var integer: col is 0;
    var integer: field is 2;
  begin
    if state.fieldsOccupied < boardSize then
      repeat
        col := rand(1, boardLength);
        row := rand(1, boardLength);
      until state.board[row][col] = 0;
      if rand(1, 10) = 10 then
        field := 4;
      end if;
      state.board[row][col] := field;
      incr(state.fieldsOccupied);
      state.largestNumber := max(field, state.largestNumber);
    end if;
  end func;
 
const proc: showBoard (in stateType: state) is func
  local
    var integer: row is 0;
    var integer: field is 0;
  begin
    writeln("┌────┬────┬────┬────┐");
    for key row range state.board do
      for field range state.board[row] do
        if field = 0 then
          write("│    ");
        else
          write("│" <& field lpad (5 + length(str(field))) div 2 rpad 4);
        end if;
      end for;
      writeln("│");
      if row < maxIdx(state.board) then
        writeln("├────┼────┼────┼────┤");
      end if;
    end for;
    writeln("└────┴────┴────┴────┘");
  end func;
 
const func boolean: doMove (inout stateType: state, in integer: startRow,
    in integer: startCol, in integer: deltaRow, in integer: deltaCol, in boolean: doMerge) is func
  result
    var boolean: boardChanged is FALSE;
  local
    const set of integer: boardRange is {1 .. boardLength};
    var integer: row is 1;
    var integer: col is 1;
    var integer: nextRow is 0;
    var integer: nextCol is 0;
  begin
    row := startRow;
    col := startCol;
    while row in boardRange and col in boardRange do
      while row in boardRange and col in boardRange do
        nextRow := row + deltaRow;
        nextCol := col + deltaCol;
        if state.board[row][col] = 0 and
            nextRow in boardRange and nextCol in boardRange and
            state.board[nextRow][nextCol] <> 0 then
          boardChanged := TRUE;
          state.board[row][col] := state.board[nextRow][nextCol];
          state.board[nextRow][nextCol] := 0;
          if row - deltaRow in boardRange and col - deltaCol in boardRange then
            nextRow := row - deltaRow;
            nextCol := col - deltaCol;
          end if;
        end if;
        row := nextRow;
        col := nextCol;
      end while;
 
      if doMerge then
        if deltaRow <> 0 then
          row := startRow;
        elsif deltaCol <> 0 then
          col := startCol;
        end if;
        while row in boardRange and col in boardRange do
          nextRow := row + deltaRow;
          nextCol := col + deltaCol;
          if state.board[row][col] <> 0 and
              nextRow in boardRange and nextCol in boardRange and
              state.board[nextRow][nextCol] = state.board[row][col] then
            boardChanged := TRUE;
            state.board[row][col] *:= 2;
            state.largestNumber := max(state.board[row][col], state.largestNumber);
            state.score +:= state.board[row][col];
            state.board[nextRow][nextCol] := 0;
            decr(state.fieldsOccupied);
          end if;
          row := nextRow;
          col := nextCol;
        end while;
      end if;
 
      if deltaRow = 0 then
        incr(row);
        col := startCol;
      elsif deltaCol = 0 then
        incr(col);
        row := startRow;
      end if;
    end while;
    if doMerge and boardChanged then
      ignore(doMove(state, startRow, startCol, deltaRow, deltaCol, FALSE));
    end if;
  end func;
 
const func boolean: canMove (in stateType: state) is func
  result
    var boolean: canMove is FALSE;
  local
    var integer: row is 0;
    var integer: col is 0;
  begin
    for row range 1 to boardLength until canMove do
      for col range 1 to boardLength until canMove do
        canMove := state.board[row][col] = 0 or
            (row < boardLength and state.board[row][col] = state.board[succ(row)][col]) or
            (col < boardLength and state.board[row][col] = state.board[row][succ(col)]);
      end for;
    end for;
  end func;
 
const proc: main is func
  local
    var stateType: state is stateType.value;
    var integer: highscore is 0;
    var char: command is ' ';
    var boolean: quit is FALSE;
    var boolean: moveOkay is FALSE;
  begin
    OUT := open(CONSOLE);
    addTile(state);
    repeat
      setPos(STD_CONSOLE, 1, 1);
      showBoard(state);
      highscore := max(highscore, state.score);
      writeln("Score = " <& state.score <& "  Highscore = " <& highscore);
      if canMove(state) and state.largestNumber < target then
        writeln("Press arrow keys to move, R to Restart, Q to Quit");
      elsif state.largestNumber >= target then
        writeln("You win! Press R to Restart, Q to Quit           ");
      else
        writeln("Game over! Press R to Restart, Q to Quit         ");
      end if;
      repeat
        moveOkay := FALSE;
        command := getc(KEYBOARD);
        case command of
          when {'r', 'R'}:
            state := stateType.value;
            clear(STD_CONSOLE);
            moveOkay := TRUE;
          when {'q', 'Q'}:
            moveOkay := TRUE;
            quit := TRUE;
          when {KEY_LEFT}:
            moveOkay := doMove(state,           1,           1,  0,  1, TRUE);
          when {KEY_RIGHT}:
            moveOkay := doMove(state,           1, boardLength,  0, -1, TRUE);
          when {KEY_UP}:
            moveOkay := doMove(state,           1,           1,  1,  0, TRUE);
          when {KEY_DOWN}:
            moveOkay := doMove(state, boardLength,           1, -1,  0, TRUE);
        end case;
	if moveOkay and not quit then
	  addTile(state);
        end if;
      until moveOkay;
    until quit;
  end func;