Skip to content

COTR

An infinite descent game.

Can you get past the second win screen? Or get a win without taking damage (and thus without the "But at what cost?" message)?

COTR: click/space to start, arrow keys to move, collect coins to go fast.

A bit of lore: Initially, I thought of the circle as somehow transcending beyond the mortal plane by collecting coins. Now this doesn't make any sense: transcendence does not come from riches, and depending on who you ask, it even comes from a lack of riches! Plus, the only "transcendence" achieved is moving faster, while being just as vulnerable to the "mortal" world—not very transcedent at all.
Yet, two things have remained of that initial ideation: the name, COTR, as a portmanteau of "coin" and "transcend", and the circle glowing brighter as more "coins", for the lack of better word, are collected.

The map generation algorithm was inspired by the BBC Article on Entombed's maze generation.

let playerX = 200
let keyCooldown = 0
let gridOffset = 0
let grid = []
let lost = true
let crashless = true
let main = true
let hp = 1
let won = 0
let coins = 0
let speedUp = 1
let mood = 0
let moodChangeTime = 0
let invincibleTime = 0

function setup() {
  createCanvas(416, 400);
  playerX = width / 2
}

function draw() {
  background(220);
  fill(0, 0, 0)
  rectMode(CENTER)
  noStroke()
  
  if (!lost) {
    moveGrid()
    movePlayer()
  }
  
  drawGrid()
  drawPlayer()
  drawScore()
  
  if (lost) {
    restartScreen()
  }
}
function drawScore() {
  if (coins > 0) {
    noStroke()
    fill(255, 255, 255)
    let filled = coins / 512
    rect(width, 0, filled * -width * 2, 17)
    if (filled > 1) {
      rect(0, 0, 17, (filled - 1) * height * 2)
    }
    if (filled > 2) {
      rect(0, height, (filled - 2) * width * 2, 17)
    }
    if (filled > 3) {
      rect(width, height, 17, (filled - 3) * height * 2)
    }
    fill(0)
    textFont('monospace')
    textSize(10)
    textAlign(RIGHT, TOP)
    text(coins, width, 0)
    if (keyIsDown(17)) {
      text(speedUp.toFixed(2), width, 12)
    }
    textAlign(LEFT, TOP)
    text(moods[mood], 0, 0)
  }
  fill(255, 0, 0)
  rect(0, 0, max(1 - hp, 0) * -width * 2, 3)
}

function restartScreen() {
  fill(255, 0, 0)
  stroke(255, 255, 255)
  strokeWeight(5)
  strokeJoin(ROUND)
  textFont('monospace')
  textSize(40)
  textAlign(CENTER, CENTER)
  if (main) {
    text("$ COTR", 200, 150)
    textSize(35)
  }
  text("> START", 200, 200)
  // gridOffset += speedUp * 0.8
  if (mouseIsPressed || keyIsDown(32)) {
    mouseIsPressed = false
    grid = []
    for (let i = 0; i < height / 32 + 9; i ++) {
      grid[i] = []
    }
    grid[grid.length - 9] = ['?']
    grid[grid.length - 8] = ['X']
    grid[grid.length - 6] = ['.']
    grid[grid.length - 5] = ['*']
    grid[grid.length - 3] = ['!']
    main = false
    lost = false
    crashless = true
    hp = 1
    won = 0
    coins = 0
    speedUp = 2
    mood = 0
  }
}

function moveGrid() {
  if (gridOffset <= -16) {
    gridOffset += 32
    grid.shift()
    grid.push(createGridRow())
    speedUp = speedUp * 0.94
  }
  let speed = (1 + speedUp + coins/200) + 1 * keyIsDown(DOWN_ARROW)
  gridOffset -= min(deltaTime * 0.06 * speed, 32)
}

function drawGrid() {
  for (let i = 0; i < grid.length; i ++) {
    let rowOffset = (width - (grid[i].length - 1) * 32) / 2
    let squareY = i * 32 + gridOffset
    for (let j = 0; j < grid[i].length; j ++) {
      let squareX = j * 32 + rowOffset
      if (grid[i][j] == 'X') {
        fill(0, 0, 0)
        noStroke()
        rect(squareX, squareY, 32)
        if (i == 1 && abs(playerX - squareX) < 24 && invincibleTime < 0) {
          crashless = false
          //hp -= 0.2 * deltaTime * 0.06
          //playerX += (playerX < squareX ? -1 : 1) * deltaTime * 0.06 * 3
          hp -= (32 - abs(playerX - squareX)) / 40
          playerX += (playerX < squareX ? -1 : 1) * (32 - abs(playerX - squareX))
        }
      }
      if (grid[i][j] == '*') {
        fill(255, 255, 0)
        stroke(200 - abs(gridOffset) / 2)
        strokeWeight(5)
        circle(squareX, squareY, 16)
        if (squareY > -8 && squareY < 40 && abs(playerX - squareX) < 32) {
          coins ++
          speedUp += 0.7
          grid[i][j] = ' '
        }
      }
      if (grid[i][j] == '?' || grid[i][j] == '.' || grid[i][j] == '!' || grid[i][j] == '$' || grid[i][j] == '&') {
        fill(255, 0, 0)
        stroke(255, 255, 255)
        strokeWeight(3)
        textFont('monospace')
        textSize(20)
        textAlign(CENTER, CENTER)
        if (grid[i][j] == '?') {
          text("> Arrow keys to move", squareX, squareY)
        } else if (grid[i][j] == '.') {
          text("> Collect coins to go faster", squareX, squareY)
        } else if (grid[i][j] == '!') {
          text("> Good luck", squareX, squareY)
        } else if (grid[i][j] == '$') {
          text("> You win!", squareX, squareY)
        } else if (grid[i][j] == '&') {
          text("> But at what cost?", squareX, squareY)
        }
      }
    }
  }
}

function movePlayer() {
  if (hp < 0) {
    lost = true
  } else {
    hp = min(hp + 0.005 * deltaTime * 0.06, 1)
  }
  if (keyIsDown(LEFT_ARROW)) {
    playerX -= deltaTime * 0.06 * 5
  }
  if (keyIsDown(RIGHT_ARROW)) {
    playerX += deltaTime * 0.06 * 5
  }
  if (playerX < 16) {
    playerX = 16
  }
  if (playerX > width - 16) {
    playerX = width - 16
  }
}

function drawPlayer() {
  if (lost) {
    fill(255, 0, 0)
  } else {
    fill(coins, coins, coins - 255)
  }
  if (invincibleTime > 0) {
    stroke(invincibleTime / 2, invincibleTime, invincibleTime)
  } else {
    noStroke()
  }
  if (hp >= 1 || lost || frameCount % 3 < hp * 3) {
    circle(playerX, 24, 30)
  }
}

let moods = [' ', 'care', 'coins', 'tunnels', 'farlands', 'farlands']
let moodProbabilities = [ // x[last mood I][new mood I] => probability
  eq([70, 3, 3, 0, 0, 0]),
  eq([5, 60, 3, 5, 0, 0]),
  eq([5, 0, 70, 0, 0, 0]),
  eq([6, 0, 0, 80, 2, 0]),
  eq([0, 0, 0, 0, 0, 1]),
  eq([0, 0, 2, 0, 0, 80]),
]
let objects = [' ', 'X', '*']
let objectProbabilities = [ // x[mood  I][type last  I][type new  I] => probability
  [ // ' '
    eq([80, 5, 9]),
    eq([80, 1, 9]),
    eq([80, 5, 9]),
  ],
  [ // 'care'
    eq([80, 10, 5]),
    eq([80, 10, 5]),
    eq([80, 10, 5]),
  ],
  [ // coins
    eq([80, 2, 20]),
    eq([80, 0, 20]),
    eq([80, 0, 10]),
  ],
  [ // tunnels
    eq([80, 2, 9]),
    eq([80, 80, 0]),
    eq([80, 0, 9]),
  ],
  [ // farlands (1)
    eq([20, 1, 0]),
    eq([1, 1, 0]),
    eq([1, 0, 0]),
  ],
  [ // farlands (2)
    eq([1, 0, 1]),
    eq([0, 1, 0]),
    eq([3, 0, 1]),
  ],
]

function eq(weights) {
  let sum = weights.reduce((a,b) => a + b)
  return weights.map(x => x / sum)
}

function pickProbability(probabilities) {
  let r = random()
  return max(probabilities.findIndex(x => (r -= x) <= 0), 0)
}

function createGridRow() {
  invincibleTime --
  moodChangeTime --
  if (coins > 32 && moodChangeTime < 0) {
    let oldMood = mood
    mood = pickProbability(moodProbabilities[mood])
    if (mood != oldMood) {
      moodChangeTime = 30 / random(1, 10)
    }
  }
  
  let row = []
  
  if (coins - floor(won / 100) * 512 > 512) {
    won ++
    if (won % 400 == 1) {
      return ['$']
    } else if (won % 400 == 2 && !crashless) {
      return ['&']
    } else {
      for (let j = 0; j < width / 32; j ++) {
        row[j] = '*'
      }
      return row
    }
  }
  
  for (let j = 0; j < width / 32; j ++) {
    let lastType = max(objects.indexOf(grid[grid.length - 1][j]), 0)
    let newType = pickProbability(objectProbabilities[mood][lastType])
    row[j] = objects[newType]
  }
  return row
}

(Originally seen at https://editor.p5js.org/bojidar-bg/sketches/zPCPYQkjM)

Experiments tagged p5 (16/85)

Experiments tagged game (2/11)

Experiments on this site (16/85)