Skip to content

Instantly share code, notes, and snippets.

@cmontella
Created December 12, 2016 23:33
Show Gist options
  • Save cmontella/61f2b330659610b7b329a97795527ae2 to your computer and use it in GitHub Desktop.
Save cmontella/61f2b330659610b7b329a97795527ae2 to your computer and use it in GitHub Desktop.
# 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]
]
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment