# 2048

A version of [2048](https://gabrielecirulli.github.io/2048/) written in [Eve](http://witheve.com/).

## Setup

This section sets up the records we will use to represent the game board and store the current state. 

* The `#game` record will store the overall game state.
* `#axis` records represent the horizontal and vertical axis
* `#direction` records map key presses to movement along an axis
* `#colour` records map tile values to the colours to render them in

```
commit
  [#game, state: "init", seed: 0, score: 0]

  [#axis, idx: 0, max: 3]
  [#axis, idx: 1, max: 3]

  [#direction, key: 37, axis: 0, dir: -1]
  [#direction, key: 38, axis: 1, dir: -1]
  [#direction, key: 39, axis: 0, dir: 1]
  [#direction, key: 40, axis: 1, dir: 1]

  [#colour, value: 2, fg: "#776e65", bg: "#eee4da"]
  [#colour, value: 4, fg: "#776e65", bg: "#ede0c8"]
  [#colour, value: 8, fg: "#f9f6f2", bg: "#f2b179"]
  [#colour, value: 16, fg: "#f9f6f2", bg: "#f59563"]
  [#colour, value: 32, fg: "#f9f6f2", bg: "#f67c5f"]
  [#colour, value: 64, fg: "#f9f6f2", bg: "#f65e3b"]
  [#colour, value: 128, fg: "#f9f6f2", bg: "#edcf72"]
  [#colour, value: 256, fg: "#f9f6f2", bg: "#edcc61"]
  [#colour, value: 512, fg: "#f9f6f2", bg: "#edc850"]
  [#colour, value: 1024, fg: "#f9f6f2", bg: "#edc53f"]
  [#colour, value: 2048, fg: "#f9f6f2", bg: "#edc22e"]
```

So we can refer to a particular row or column, we create a `#line` record for each.

```
search
  axis = [#axis, max]
  idx = range[from: 0, to: max]

bind
  [#line axis idx] 
```

For each intersection of a horizontal and vertical line, we then create a `#cell` record to represent each avaiable postion on the grid.

These cell records are constants; We will indicate if there is a number in a cell by creating a `#tile` record which references the cell.
 

```
search
  line0 = [#line axis: [#axis idx: 0]]
  line1 = [#line axis: [#axis idx: 1]]

bind
  cell = [#cell line: line0, line: line1]
```

After the user has made a move, we need check if further moves are possible by merging or moving tiles. We create the following records that will be updated with this information after every move:

- `[#can-merge, axis, after-move, value]`: `value` will be `true` if after move `after-move` completed a merge is possible on the specified `axis`
- `[#can-move, axis, dir, after-move, value]`: `value` will be `true` if after move `after-move` completed a move is possible in the specified direction (`dir`) on the `axis`.

```
search
  not([#can-merge])
  axis = [#axis]
commit
  [#can-merge, axis, after-move: -1, value: false]
```

```
search
  not([#can-move])
  axis = [#axis]
  direction = [#direction dir]
commit
  [#can-move, axis, dir, after-move: -1, value: false]
```

## State changes

This section is responsible for moving the game between states (recorded in `state` in the `[#game]` record).

The game can be in any one of the following states:

- `init`: Initialising
- `adding`: Adding new tiles
- `detect-moves`: Detecting possible moves and merges 
- `detect-completion`: Detecting whether the player has won or lost
- `idle`: Awaiting a keypress from the user
- `moving`: Moving tiles in response to a keypress
- `won`: User has won
- `lost`: User has lost (no further moves possible)


### ANY -> `adding`

User has request a new game, so reset the game state, and indicate we want to add 2 new tiles.

```
search @session @event @browser
  [#click element: [#newgame]]
  [#time, frames]
  game = [#game]
  tile = if tile = [#tile] then tile else false 
  can-move = [#can-move]
  can-merge = [#can-merge]

commit
  tile := none
  can-move.after-move := -1
  can-merge.after-move := -1
  game <- [move: [idx: 0, axis: 0, dir: 0], seed: frames, add-count: 2, state: "adding", score: 0]
```

### `adding` -> `detect-moves`

When we have finished adding new tiles, check what moves are available.

```
search
  game = [#game, state: "adding", add-count = 0] 
commit
  game.state := "detect-moves"
```

### `detect-moves` -> `detect-completion`

After we have calculated the possible moves we are ready to determine if the game has ended.

```
search
  game = [#game, state: "detect-moves", move] 
  count[given: [#can-move]] = count[given: [#can-move, after-move = move.idx]]
commit
  game.state := "detect-completion"
```


### `detect-completion` -> `won`/`lost`/`idle`

The game is won if there is a 2048 tile present and is lost if the grid is full and there are no possible merges.

```
search
  game = [#game, state: "detect-completion"] 
  state =
      if count[given: [#tile, value: 2048]] > 0 then "won"

      else if count[given: [#tile]] < count[given: [#cell]] then "idle"

      else if count[given: [#can-merge, value = true]] > 0 then "idle"

      else "lost"

commit
  game.state := state
```

### `idle` -> `moving`

If the user pressed a direction key and there is a possible move for that direction, record the new move and begin `moving`.

```
search @event
  [#keyup, key]

search
  game = [#game, state: "idle", move]
  [#direction, key, axis: axis-idx, dir]
  [#can-move, axis: [#axis, idx: axis-idx], dir, value = true]
  
commit
  game <- [state: "moving", move: [idx: move.idx + 1, axis: axis-idx, dir]]
```

### `moving` -> `adding`

After a move has complete, add a new tile.

```
search
  game = [#game, state: "moving", move]
  moved-tiles = count[given: [#tile, last-move = move.idx]]
  all-tiles = count[given: [#tile]]
  moved-tiles = all-tiles

commit
  game <- [state: "adding", add-count: 1]
```

## Logic

This section contains the main game code that specifies what happens in each of the game states.

### `detect-moves`

First we determine if a merge is possible on either axis. A merge is possible if there are two tiles on the same line with the same value and there is no tile inbetween them.

We use `after-move` to indicate that we have calculated the `#can-merge` for the current move. 

```
search
  game = [#game, state: "detect-moves", move]
  axis = [#axis]
  other-axis = [#axis] != axis
  can-merge = [#can-merge, axis: other-axis, after-move < move.idx]
  value =
      if cell = [#cell, line, line: [#line, axis: other-axis, idx]]  
         tile = [#tile, cell: cell, value]
         [#tile, cell: [#cell, line, line: [#line, axis: other-axis, idx: idx1]], value]
         idx1 > idx
         not(
             [#tile, cell: [#cell, line, line: [#line, axis: other-axis, idx: idx2]]]
             idx < idx2 < idx1
         )
         then true

      else false

commit
  can-merge <- [value, after-move: move.idx]
```

Once we have calculated `#can-merge` for an axis, we can determine if there are any possible moves on the axis in either direction.

A move is possible for a particular direction if there is a merge available or there is an empty cell on the same line as a tile in the direction of movement.

```
search
  game = [#game, state: "detect-moves", move]
  [#direction, axis: axis-idx, dir]
  axis = [#axis, idx: axis-idx]
  other-axis = [#axis] != axis
  [#can-merge, axis: other-axis, after-move = move.idx]

  can-move = [#can-move, axis: other-axis, dir, after-move < move.idx]

  value =

      if [#can-merge, axis: other-axis, after-move: move.idx, value = true] then true

      else if
          cell = [#cell, line, line: [#line, axis: other-axis, idx]]  
          tile = [#tile, cell: cell]
          next-cell = [#cell, line, line: [#line, axis: other-axis, idx: idx + dir]]
          not([#tile, cell: next-cell])
      then true

      else false

commit
  can-move <- [value, after-move: move.idx]
```

### `adding`

Add a new tile at a random free cell and decrease `add-count`. Adding will continue until `add-count` is zero.

Since values in Eve are unordered sets it is little tricky to work out how to select a random cell from the list of free cells. To do this we use the `sort` function to associate an index with each of the avaiable cells. We can then constrain the index to be equal to a random number and use the associated cell as our tile location.

```
search
  game = [#game, state: "adding", move, seed, add-count] 
  add-count > 0
  cell = [#cell]
  not([#tile, cell])
  num-free-cells = count[given: cell] 
  sort[value: cell] = idx
  base-seed = seed + move.idx + add-count
  idx = 1 + round[value: random[seed: base-seed] * (num-free-cells - 1)]
  value = if random[seed: base-seed * 13] < 0.9 then 2 else 4
  new-count = add-count - 1

commit
  [#tile, cell, value, last-move: move.idx, last-merge: -1]
  game.add-count := new-count
```

### `moving`

When a move has been requested, `game.move` specifies the direction of the requested move.

The rules are:

* Move the closest tiles to the edge in the direction of movement first
* If a tile hits a tile with the same value, and that tile hasn't already been merged this turn, merge them 
* If a tile hits a tile with a different value, move it adjacent to it
* On a merge, remove the hit tile, double the value on the moved tile and add this to the game score


```
search
  game = [#game, state: "moving", move]
  move-axis = [#axis, idx: move.axis, max: max-pos]
  other-axis = [#axis] != move-axis
  move-tile = [#tile, cell, last-move < move.idx]
  cell = [#cell, line, line: other-line]
  line = [#line, axis: move-axis, idx: cur-pos] // the tile will move to a different line on this axis
  other-line = [#line, axis: other-axis] // the tile will move along this line (i.e. remain on it)

  // Don't move a tile if there are tiles closer to the edge that haven't been moved yet 

  not(move.dir < 0,
      cell-in-front = [#cell, line: [#line, axis: move-axis, idx < cur-pos], line: other-line],
      [#tile, cell: cell-in-front, last-move < move.idx]
  )
  not(move.dir > 0,
      cell-in-front = [#cell, line: [#line, axis: move-axis, idx > cur-pos], line: other-line],
      [#tile, cell: cell-in-front, last-move < move.idx]
  )

  // Find the position of the tile we will hit (if any) if we move in the requested direction

  hit-pos =
      if move.dir < 0,
           block-tile = [#tile, cell: [#cell, line: [#line, axis: move-axis, idx: tile-pos], line: other-line]]
           tile-pos < cur-pos
         then max[given: block-tile, value: tile-pos]
      else if move.dir > 0,
           block-tile = [#tile, cell: [#cell, line: [#line, axis: move-axis, idx: tile-pos], line: other-line]]
           tile-pos > cur-pos
         then min[given: block-tile, value: tile-pos]
      else false

  // Depending on hit-pos, determine whether the tile will move to hit-pos, merge with another tile, or move
  // adjacent to a tile already at that position

  (new-cell, new-value, merge-tile, last-merge, score-inc) =

    if hit-pos = false,
        move.dir > 0
        then ([#cell, line: [#line, axis: move-axis, idx: max-pos], line: other-line], move-tile.value, false, -1, 0)

    else if hit-pos = false
        then ([#cell, line: [#line, axis: move-axis, idx: 0], line: other-line], move-tile.value, false, -1, 0)

    else if hit-tile = [#tile, cell: [#cell, line: [#line, axis: move-axis, idx: hit-pos], line: other-line], value: hit-value, last-merge]
            hit-value = move-tile.value
            last-merge != move.idx
        then ([#cell, line: [#line, axis: move-axis, idx: hit-pos], line: other-line], move-tile.value * 2, hit-tile, move.idx, hit-value * 2) 

    else ([#cell, line: [#line, axis: move-axis, idx: hit-pos - move.dir], line: other-line], move-tile.value, false, -1, 0)

  // Update the game score

  new-score = game.score + sum[given: new-cell, value: score-inc]


commit
  move-tile <- [cell: new-cell, value: new-value, last-move: move.idx, last-merge]
  merge-tile := none
  game.score := new-score

```

## Rendering

This section renders the game on the browser.

Add score caption, new game button and a parent SVG element to contain the game grid.

```
search
  width = 250
  height = 250
  [#game, score, state]
  [#axis, idx: 0, max: max0]
  [#axis, idx: 1, max: max1]
  status = if state = "won" then "WON"
           else if state = "lost" then "LOST"
           else ""

bind @browser
  [#p, sort: 1, text: "Score {{score}} {{status}}"]
  [#svg, sort: 2, viewBox: "0 0 {{max0 + 1.25}} {{max1 + 1.25}}", width, height, children:
    [#rect, rx: 0.1, ry: 0.1, x:0, y:0, width: max0 + 1.25, height: max1 + 1.25, fill: "#bbada0"]
    [#g, #grid, transform: "translate(0.125, 0.125)"]
  ] 
  [#p, sort: 3, children:
    [#button, #newgame, style: [width: "10em"], text: "New Game"]
  ]
```

We set up a `viewBox` on the parent SVG element to modify the coordinate system so that we can just position cells with our line indexes.

We also apply a transform when rendering each cell so that it is scaled down slightly to provide a border and the origin is the centre of the cell.


```
search @browser
  grid = [#grid]

search
  axis0 = [#axis, idx: 0]
  axis1 = [#axis, idx: 1]
  cell = [#cell, line: [axis: axis0, idx: idx0], line: [axis: axis1, idx: idx1]]
  (x, y, fill, text-col, text) =
     if [#tile, cell, value] [#colour, value, bg, fg] then (idx0, idx1, bg, fg, value)
     else (idx0, idx1, "#cdc1b4", "",  "")

bind @browser
  grid.children +=
    [#g, transform: "translate({{x + 0.5}},{{y + 0.5}}) scale(0.85)", children: 
      [#rect, rx: 0.05, ry: 0.05,  x:-0.5, y:-0.5, width: 1, height: 1, fill]
      [#text, x:0, y:0, font-size: 0.4, font-weight: "bold", fill: text-col, dominant-baseline: "middle", text-anchor: "middle", text]
    ]
```