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.


 
use "term"
use "random"
use "time"
 
interface EdgeRow
  fun val row() : Iterator[U32] ref
  fun val inc() : I32
 
primitive TopRow is EdgeRow
  fun row() : Iterator[U32] ref => let r : Array[U32] box = [0,1,2,3]
    r.values()
  fun inc() : I32 => 4
 
primitive LeftRow is EdgeRow
  fun row() : Iterator[U32] ref => let r : Array[U32] box = [0,4,8,12]
    r.values()
  fun inc() : I32 => 1
 
primitive RightRow is EdgeRow
  fun row() : Iterator[U32] ref => let r : Array[U32] box = [3,7,11,15]
    r.values()
  fun inc() : I32 => -1
 
primitive BottomRow is EdgeRow
  fun row() : Iterator[U32] ref => let r : Array[U32] box = [12,13,14,15]
    r.values()
  fun inc() : I32 =>  -4
 
primitive LEFT
primitive RIGHT
primitive UP
primitive DOWN
type Move is (LEFT|RIGHT|UP|DOWN)
 
class  KeyboardHandler is ANSINotify
   let _game : Game tag
   new iso create(game : Game tag) => _game = game
 
   fun ref apply(term: ANSITerm ref, input: U8 val) =>
     if input == 113 then
       _game.quit()
       term.dispose()
     end
   fun ref left(ctrl: Bool, alt: Bool, shift: Bool)  => _game.move(LEFT)
   fun ref down(ctrl: Bool, alt: Bool, shift: Bool)  => _game.move(DOWN)
   fun ref up(ctrl: Bool, alt: Bool, shift: Bool)    => _game.move(UP)
   fun ref right(ctrl: Bool, alt: Bool, shift: Bool) => _game.move(RIGHT)
 
type ROW is (U32,U32,U32,U32)
 
primitive Merger
  fun tag apply(r : ROW) : ROW =>
    match r
    | (0,0,0,_)            => (r._4,0,0,0)
    | (0,0,_,r._3)         => (r._3<<1,0,0,0)
    | (0,0,_,_)            => (r._3,r._4,0,0)
    | (0,_,r._2,_)         => (r._2<<1,r._4,0,0)
    | (0,_,0,r._2)         => (r._2<<1,0,0,0)
    | (0,_,0,_)            => (r._2,r._4,0,0)
    | (0,_,_,r._3)         => (r._2,r._3<<1,0,0)
    | (0,_,_,_)            => (r._2,r._3,r._4,0)
    | (_, r._1, _, r._3)   => (r._1<<1, r._3<<1, 0, 0)
    | (_, r._1, 0, _)      => (r._1<<1, r._4, 0, 0)
    | (_, r._1, _, _)      => (r._1<<1, r._3, r._4, 0)
    | (_, 0,r._1, _)       => (r._1<<1,r._4,0,0)
    | (_, 0,0, r._1)       => (r._1<<1,0,0,0)
    | (_, 0,0, _)          => (r._1,r._4,0,0)
    | (_, 0,_, r._3)       => (r._1, r._3<<1,0,0)
    | (_, 0,_, _)          => (r._1, r._3,r._4,0)
    | (_,_,r._2,_)         => (r._1, r._2<<1,r._4,0)
    | (_,_,0,r._2)         => (r._1, r._2<<1,0,0)
    | (_,_,0,_)            => (r._1, r._2,r._4,0)
    | (_,_,_,r._3)         => (r._1, r._2,r._3<<1,0)
    else
       r
    end
/**
* Game actor
*/
actor Game
  embed _grid : Array[U32] = Array[U32].init(0, 16)
  let _rand : Random = MT(Time.millis())
  let _env : Env
  let _board : String ref = recover String(1024) end
 
  new create(env: Env)=>
    _env = env
    _add_block()
    _add_block()
    _draw()
 
  fun _merge(start : U32, inc : I32) : (ROW | None) =>
    var st = start.i32()
    let rval : ROW = (_get(st),             _get(st + inc),
                      _get(st + (inc * 2)), _get(st + (inc * 3)))
    let rout = Merger(rval)
    if rout is rval then None else rout end
 
  fun ref _update(start : U32, inc : I32) : Bool =>
    match _merge(start, inc)
    | let rout : ROW =>
        var st = start.i32()
        _set(st,             rout._1)
        _set(st +  inc,      rout._2)
        _set(st + (inc * 2), rout._3)
        _set(st + (inc * 3), rout._4)
        true
    else
      false
    end
 
  fun ref _shift_to(edge : EdgeRow val) : Bool =>
    var updated = false
    for r in edge.row() do
      if _update(r, edge.inc()) then
        updated = true
      end
    end
    updated
 
  fun _fmt(i : U32) : String =>
    match i
    | 0 => " __ "
    | 2 => "\x1B[31m  2 \x1B[0m"
    | 4 => "\x1B[32m  4 \x1B[0m"
    | 8 => "\x1B[33m  8 \x1B[0m"
    | 16 => "\x1B[34m 16 \x1B[0m"
    | 32 => "\x1B[35m 32 \x1B[0m"
    | 64 => "\x1B[36m 64 \x1B[0m"
    | 128 => "\x1B[37m128 \x1B[0m"
    | 256 => "\x1B[41m\x1B[37m256 \x1B[0m"
    | 512 => "\x1B[42m\x1B[37m512 \x1B[0m"
    | 1024 => "\x1B[43m\x1B[37m1024\x1B[0m"
    | 2048 => "\x1B[47m\x1B[35m\x1B[1m\x1B[5m2048\x1B[0m"
    else
      i.string()
    end
 
  fun ref _draw() =>
    let s : String ref = _board
    s.truncate(0)
    var i : U32 = 0
    repeat
      if (i % 4) == 0 then
          s.append("---------------------\n")
      end
      s.append(_fmt(_get(i)))
      s.append(" ")
      i = i + 1
      if (i % 4) == 0 then
          s.append("\n")
      end
    until i==16 end
    _env.out.print(s.string())
    _env.out.print("Arrow keys to move. Press (q)uit key to quit.")
 
   fun ref _set(i:(I32|U32), v : U32) =>
     try
       _grid.update(i.usize(),v)
     else
       _env.out.print("cant update!")
     end
 
  fun _count() : U64 =>
     var c : U64 = 0
     for v in _grid.values() do
       c = c + if v == 0 then 0 else 1 end
     end
     c
 
  fun ref _add_block() =>
    let c = _count()
    if c == 16 then return end
 
    var hit =  _rand.int(16 - c)
    var i : U32 = 0
    while i < 16 do
      if (_get(i) == 0) then
        if hit == 0 then
          _set(i, if _rand.int(10) > 0 then 2 else 4 end)
          break
        end
        hit = hit - 1
      end
      i = i + 1
    end
 
  fun _get(i : (I32|U32)) : U32 => try  _grid(i.usize()) else 0  end
 
  fun _win() : Bool =>
    for v in _grid.values() do
      if v == 2048 then return true end
    end
    false
 
  fun _no_moves(edge : EdgeRow val) : Bool =>
    for r in edge.row() do
      match _merge(r, edge.inc())
      | let rout : ROW =>
        if (rout._1 == 0) or (rout._2 == 0) or
            (rout._3 == 0) or (rout._4 == 0) then
              return false
        end
      end
    end
    true
 
  fun _lose() : Bool =>
    (_grid.size() >= 16) and
    _no_moves(LeftRow) and
    _no_moves(RightRow) and
    _no_moves(TopRow) and
    _no_moves(BottomRow)
 
  be quit()=>
    _env.out.print("Exiting.. some terminals may require <ctrl-c>")
    _env.exitcode(0)
    _env.input.dispose()
 
  be move(m: Move) =>
    let updated =
      match m
      | LEFT =>  _shift_to(LeftRow)
      | RIGHT => _shift_to(RightRow)
      | UP =>    _shift_to(TopRow)
      | DOWN =>  _shift_to(BottomRow)
      else
        false
      end
 
    if _win() then
      _draw()
      _env.out.print("You win :)")
      quit()
    else
      if updated then
        _add_block()
        _draw()
      end
      if _lose() then
        _env.out.print("You lose :(")
        quit()
      end
    end
 
actor Main
  new create(env: Env) =>
    // unit test
    ifdef "test" then
      TestMain(env)
      return
    end
    // else game
    let input : Stdin tag = env.input
    env.out.print("Welcome to ponylang-2048...")
    let game = Game(env)
    let term = ANSITerm(KeyboardHandler(game), input)
 
    let notify : StdinNotify iso = object iso
        let term: ANSITerm = term
        let _in: Stdin tag = input
        fun ref apply(data: Array[U8] iso) => term(consume data)
        fun ref dispose() => _in.dispose()
    end
 
    input(consume notify)
 