Skip to content

Instantly share code, notes, and snippets.

@joshuafcole
Created December 2, 2016 01:13
Show Gist options
  • Save joshuafcole/b01c381fe930561b8912b06415fa8cac to your computer and use it in GitHub Desktop.
Save joshuafcole/b01c381fe930561b8912b06415fa8cac to your computer and use it in GitHub Desktop.

2048

A version of 2048 written in Eve.

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