<commit_before><!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>&kappa;&lambda;&omega;&nu;&alpha;&lambda;&gamma;</title>
    <meta name="description" content="Flappy bird AI but they drive cars around a race track with five magic laser beam sensors"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <link rel="stylesheet" type="text/css" href="/sheep3.css">
    <script src="/sheep3.js" charset="utf-8"></script>
    <style>
      html,
      body {
        height: 100%;
      }
      body {
        font-size: 0;
        margin: 0;
        background-color: black;
        font-family: monospace;
      }
      #canvas {
        width: 100%;
        height: 100%;
      }
      #network {
        position: fixed;
        top: 0;
        right: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <canvas id="network"></canvas>
    <script type="module">
import { BrainType } from '../brain-type.mjs'
import { GenerationManager } from '../generation-manager.mjs'

import { Vector2 } from '../vectors.mjs'
import { rayPolylineIntersection, getClosestToPolyline } from '../ray-intersection.mjs'

import { RenderSimulation } from '../render-simulation.mjs'
import { MindReader } from '../mind-reader.mjs'

const CAR_WIDTH = 5 // Distance from centre to side
const CAR_LENGTH = 10 // Distance from centre to front

const MAX_RANGE = 100
const SENSOR_DISTRIB_ANGLE = Math.PI / 8

const TURN_SPEED = 2
const ENGINE_ACCEL = 10
const FRICTION_ACCEL = 1
class Car {
  constructor (brainType) {
    this.brainType = brainType
    this.position = new Vector2()
  }

  get vertices () {
    return [
      new Vector2(CAR_LENGTH, CAR_WIDTH),
      new Vector2(-CAR_LENGTH, CAR_WIDTH),
      new Vector2(-CAR_LENGTH, -CAR_WIDTH),
      new Vector2(CAR_LENGTH, -CAR_WIDTH)
    ].map(vec => vec.rotate(this.angle).add(this.position))
  }

  /**
   * Set the weights of the car's neural network
   */
  loadIdeology (ideology) {
    this.ideology = ideology
    return this
  }

  /**
   * Store a reference to the current course
   */
  recognizeReality (reality) {
    this.reality = reality
    return this
  }

  start (position, angle) {
    this.position.set(position)
    this.speed = 0
    this.angle = angle

    this.progress = 0
    this.dead = false
    return this
  }

  sense () {
    const front = new Vector2(CAR_LENGTH, 0).rotate(this.angle).add(this.position)
    const intersections = []
    let angle = -SENSOR_DISTRIB_ANGLE * 2
    const stop = SENSOR_DISTRIB_ANGLE * 2
    while (angle <= stop) {
      intersections.push(rayPolylineIntersection(front,
        new Vector2(1, 0).rotate(angle), this.reality.walls))
      angle += SENSOR_DISTRIB_ANGLE
    }
    return { front, intersections }
  }

  getInputs () {
    const { front, intersections } = this.sense()
    return intersections.map(point => point
      ? Math.min(point.sub(front).length, MAX_RANGE) / MAX_RANGE : 1)
  }

  driveWithFreeWill () {
    const [turn, engine] = this.brainType.ponder(this.ideology, this.getInputs())
    return {
      turnSpeed: (turn - 0.5) * TURN_SPEED,
      engineAccel: (engine - 0.5) * ENGINE_ACCEL
    }
  }

  /**
   * Test each corner of the car and see if the velocity will make it collide
   * with the course
   */
  collision (velocity) {
    const speed = Math.abs(this.speed)
    return this.vertices.some(anchor => {
      const intersection = rayPolylineIntersection(anchor, velocity, this.reality.walls)
      return intersection && intersection.sub(anchor).length < speed
    })
  }

  getProgress () {
    const closest = getClosestToPolyline(this.position, this.reality.route)
    if (closest) {
      // Score for one full lap
      const lap = this.reality.routePairs.length
      let { t } = closest
      // Intelligently allow multiple laps to be extra, and going backwards to
      // be quite bad
      if (this.progress - t > 0) {
        while (t < this.progress) {
          t += lap
        }
        if (Math.abs(this.progress - (t - lap)) < Math.abs(this.progress - t)) {
          t -= lap
        }
      } else {
        while (t > this.progress) {
          t -= lap
        }
        if (Math.abs(this.progress - (t + lap)) < Math.abs(this.progress - t)) {
          t += lap
        }
      }
      return t
    } else {
      throw new Error('Wucky: Shouldn\'t there always be a closest point?')
    }
  }

  simulate (time) {
    if (!this.dead) {
      const { turnSpeed, engineAccel } = this.driveWithFreeWill()
      this.angle += turnSpeed * time
      this.speed += engineAccel * time
      if (this.speed > 0) {
        this.speed -= FRICTION_ACCEL * time
        if (this.speed < 0) this.speed = 0
      } else {
        this.speed += FRICTION_ACCEL * time
        if (this.speed > 0) this.speed = 0
      }
      const velocity = new Vector2(1, 0).rotate(this.angle).scale(this.speed)
      if (this.collision(velocity)) {
        this.dead = true
      }
      // Add velocity after since `this.collision` relies on old position
      this.position.add(velocity)

      this.progress = this.getProgress()
    }
  }

  draw (ctx, amWinner) {
    ctx.fillStyle = this.dead ? 'rgba(127, 127, 127, 0.5)'
      : amWinner ? 'rgba(255, 255, 0, 0.8)' : 'rgba(255, 127, 0, 0.8)'
    ctx.beginPath()
    const [startPoint, ...lineTos] = this.vertices
    ctx.moveTo(...startPoint.comps)
    for (const lineTo of lineTos) {
      ctx.lineTo(...lineTo.comps)
    }
    ctx.closePath()
    ctx.fill()

    if (!this.dead) {
      const { intersections } = this.sense()
      ctx.beginPath()
      for (const vec of intersections) {
        if (vec) {
          const { x, y } = vec
          ctx.moveTo(x - 4, y - 4)
          ctx.lineTo(x + 4, y + 4)
          ctx.moveTo(x + 4, y - 4)
          ctx.lineTo(x - 4, y + 4)
        }
      }
      ctx.stroke()
    }
  }
}

const brainType = new BrainType({ inputs: 5, hiddens: [4, 3], outputs: 2, bias: true })
const generationManager = new GenerationManager({ brainType, count: 50 })
const cars = []
for (let i = 0; i < generationManager.count; i++) {
  cars.push(new Car(brainType))
}

let generation = generationManager.makeGeneration()
let generations = 1
let alive
let record = 0

let map

function loadMap (newMap) {
  for (const car of cars) {
    car.recognizeReality(newMap)
  }

  map = newMap
}

function getPositionInRoute (t) {
  t = t % map.routePairs.length
  const [a, b] = map.routePairs[Math.floor(t)]
  return b.clone().sub(a).scale(t % 1).add(a)
}

function startGen () {
  const startT = 0
  const position = getPositionInRoute(startT)
  const angle = getPositionInRoute(startT + 0.1).sub(position).angle

  for (let i = 0; i < generationManager.count; i++) {
    cars[i].loadIdeology(generation[i]).start(position, angle)
  }
  alive = [...cars]
}

function nextGen () {
  // TODO: Determine progress
  cars.sort((a, b) => b.progress - a.progress)
  if (cars[0].progress > record) {
    record = cars[0].progress
  }

  generation = generationManager.nextGeneration(cars.map(car => car.ideology))
  generations++
}

let scrollX = 0
let scrollY = 0

function simulate (simTime) {
  for (let i = 0; i < alive.length; i++) {
    alive[i].simulate(simTime)
    if (alive[i].dead) {
      alive.splice(i, 1)
      i--
    }
  }

  if (alive.length === 0) {
    nextGen()
    startGen()
  }

  const winner = alive.reduce((curr, acc) => curr.progress > acc.progress ? curr : acc)
  // Look ahead 50 units
  const centre = new Vector2(200, 0).rotate(winner.angle).add(winner.position)
  scrollX += (centre.x - screenWidth / 2 - scrollX) / 50
  scrollY += (centre.y - screenHeight / 2 - scrollY) / 50
}

const canvas = document.getElementById('canvas')
const c = canvas.getContext('2d')

const networkCanvas = document.getElementById('network')
const networkCtx = networkCanvas.getContext('2d')

const PADDING = 10
const mindReader = new MindReader({ brainType, x: PADDING, y: PADDING })
const networkSize = mindReader.getExpectedSize()
networkSize.width += PADDING * 2
networkSize.height += PADDING * 2
networkCanvas.style.width = networkSize.width + 'px'
networkCanvas.style.height = networkSize.height + 'px'

let screenWidth
let screenHeight

function resize () {
  const dpr = window.devicePixelRatio
  screenWidth = window.innerWidth
  screenHeight = window.innerHeight

  canvas.width = screenWidth * dpr
  canvas.height = screenHeight * dpr
  c.scale(dpr, dpr)

  networkCanvas.width = networkSize.width * dpr
  networkCanvas.height = networkSize.height * dpr
  networkCtx.scale(dpr, dpr)
}

window.addEventListener('resize', resize)
resize()

function render () {
  c.clearRect(0, 0, screenWidth, screenHeight)

  c.save()
  c.translate(-scrollX, -scrollY)

  c.lineWidth = 2
  c.lineJoin = 'round'
  c.lineCap = 'round'
  c.strokeStyle = 'rgba(255, 255, 255, 0.3)'
  c.beginPath()
  for (let i = 0; i < map.walls.length; i++) {
    const a = map.walls[i - 1]
    const b = map.walls[i]
    if (a && b) {
      c.moveTo(...a.comps)
      c.lineTo(...b.comps)
    }
  }
  c.stroke()

  const winner = alive.reduce((curr, acc) => curr.progress > acc.progress ? curr : acc)
  c.strokeStyle = 'rgba(0, 255, 255, 0.2)'
  for (const car of cars) {
    car.draw(c, car === winner)
  }

  c.restore()

  networkCtx.clearRect(0, 0, networkSize.width, networkSize.height)
  mindReader.render(networkCtx, winner.ideology, winner.getInputs())
}

const renderer = new RenderSimulation({ render, simulate })

Promise.all([
  fetch('./map1.json').then(r => r.ok ? r.json() : Promise.reject(r.status)),
  fetch('./map1-route.json').then(r => r.ok ? r.json() : Promise.reject(r.status))
]).then(([wallVertices, routeVertices]) => {
  const walls = []
  for (const vertices of wallVertices) {
    walls.push(null, ...vertices.map(comps => new Vector2(...comps)))
  }
  const route = []
  for (const vertices of routeVertices) {
    route.push(null, ...vertices.map(comps => new Vector2(...comps)))
  }

  // routePairs is an array of tuples containing the start and end points of each segment
  const routePairs = []
  for (const vertices of routeVertices) {
    for (let i = 1; i < vertices.length; i++) {
      routePairs.push([new Vector2(...vertices[i - 1]), new Vector2(...vertices[i])])
    }
  }
  loadMap({ walls, route, routePairs })

  startGen()
  renderer.start()
})

window.setSpeed = speed => renderer.speed = speed
window.alive = () => alive
    </script>
  </body>
</html>
<commit_msg>Better progress calculation, kill off those who don't move in time so it doesn't get stuck<commit_after><!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>&kappa;&lambda;&omega;&nu;&alpha;&lambda;&gamma;</title>
    <meta name="description" content="Flappy bird AI but they drive cars around a race track with five magic laser beam sensors"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <link rel="stylesheet" type="text/css" href="/sheep3.css">
    <script src="/sheep3.js" charset="utf-8"></script>
    <style>
      html,
      body {
        height: 100%;
      }
      body {
        font-size: 0;
        margin: 0;
        background-color: black;
        font-family: monospace;
      }
      #canvas {
        width: 100%;
        height: 100%;
      }
      #network {
        position: fixed;
        top: 0;
        right: 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <canvas id="network"></canvas>
    <script type="module">
import { BrainType } from '../brain-type.mjs'
import { GenerationManager } from '../generation-manager.mjs'

import { Vector2 } from '../vectors.mjs'
import { rayPolylineIntersection, getClosestToPolyline } from '../ray-intersection.mjs'

import { RenderSimulation } from '../render-simulation.mjs'
import { MindReader } from '../mind-reader.mjs'

const CAR_WIDTH = 5 // Distance from centre to side
const CAR_LENGTH = 10 // Distance from centre to front

const MAX_RANGE = 100
const SENSOR_DISTRIB_ANGLE = Math.PI / 8

const TURN_SPEED = 2
const ENGINE_ACCEL = 10
const FRICTION_ACCEL = 1
class Car {
  constructor (brainType) {
    this.brainType = brainType
    this.position = new Vector2()
  }

  get vertices () {
    return [
      new Vector2(CAR_LENGTH, CAR_WIDTH),
      new Vector2(-CAR_LENGTH, CAR_WIDTH),
      new Vector2(-CAR_LENGTH, -CAR_WIDTH),
      new Vector2(CAR_LENGTH, -CAR_WIDTH)
    ].map(vec => vec.rotate(this.angle).add(this.position))
  }

  /**
   * Set the weights of the car's neural network
   */
  loadIdeology (ideology) {
    this.ideology = ideology
    return this
  }

  /**
   * Store a reference to the current course
   */
  recognizeReality (reality) {
    this.reality = reality
    return this
  }

  start (position, angle) {
    this.position.set(position)
    this.speed = 0
    this.angle = angle

    this.progress = 0
    this.dead = false
    return this
  }

  sense () {
    const front = new Vector2(CAR_LENGTH, 0).rotate(this.angle).add(this.position)
    const intersections = []
    let angle = -SENSOR_DISTRIB_ANGLE * 2
    const stop = SENSOR_DISTRIB_ANGLE * 2
    while (angle <= stop) {
      intersections.push(rayPolylineIntersection(front,
        new Vector2(1, 0).rotate(angle), this.reality.walls))
      angle += SENSOR_DISTRIB_ANGLE
    }
    return { front, intersections }
  }

  getInputs () {
    const { front, intersections } = this.sense()
    return intersections.map(point => point
      ? Math.min(point.sub(front).length, MAX_RANGE) / MAX_RANGE : 1)
  }

  driveWithFreeWill () {
    const [turn, engine] = this.brainType.ponder(this.ideology, this.getInputs())
    return {
      turnSpeed: (turn - 0.5) * TURN_SPEED,
      engineAccel: (engine - 0.5) * ENGINE_ACCEL
    }
  }

  /**
   * Test each corner of the car and see if the velocity will make it collide
   * with the course
   */
  collision (velocity) {
    const speed = Math.abs(this.speed)
    return this.vertices.some(anchor => {
      const intersection = rayPolylineIntersection(anchor, velocity, this.reality.walls)
      return intersection && intersection.sub(anchor).length < speed
    })
  }

  getProgress () {
    const closest = getClosestToPolyline(this.position, this.reality.route)
    if (closest) {
      // Score for one full lap
      const lap = this.reality.lapLength
      const { t } = closest
      let progress = 0
      for (let i = 0; i < Math.floor(t); i++) {
        progress += this.reality.routePairs[i][2]
      }
      progress += this.reality.routePairs[Math.floor(t)][2] * (t % 1)
      // Intelligently allow multiple laps to be extra, and going backwards to
      // be quite bad
      if (this.progress - progress > 0) {
        while (progress < this.progress) {
          progress += lap
        }
        if (Math.abs(this.progress - (progress - lap)) < Math.abs(this.progress - progress)) {
          progress -= lap
        }
      } else {
        while (progress > this.progress) {
          progress -= lap
        }
        if (Math.abs(this.progress - (progress + lap)) < Math.abs(this.progress - progress)) {
          progress += lap
        }
      }
      return progress
    } else {
      throw new Error('Wucky: Shouldn\'t there always be a closest point?')
    }
  }

  simulate (time) {
    if (!this.dead) {
      const { turnSpeed, engineAccel } = this.driveWithFreeWill()
      this.angle += turnSpeed * time
      this.speed += engineAccel * time
      if (this.speed > 0) {
        this.speed -= FRICTION_ACCEL * time
        if (this.speed < 0) this.speed = 0
      } else {
        this.speed += FRICTION_ACCEL * time
        if (this.speed > 0) this.speed = 0
      }
      const velocity = new Vector2(1, 0).rotate(this.angle).scale(this.speed)
      if (this.collision(velocity)) {
        this.dead = true
      }
      // Add velocity after since `this.collision` relies on old position
      this.position.add(velocity)

      this.progress = this.getProgress()
    }
  }

  draw (ctx, amWinner) {
    ctx.fillStyle = this.dead ? 'rgba(127, 127, 127, 0.5)'
      : amWinner ? 'rgba(255, 255, 0, 0.8)' : 'rgba(255, 127, 0, 0.8)'
    ctx.beginPath()
    const [startPoint, ...lineTos] = this.vertices
    ctx.moveTo(...startPoint.comps)
    for (const lineTo of lineTos) {
      ctx.lineTo(...lineTo.comps)
    }
    ctx.closePath()
    ctx.fill()

    if (!this.dead) {
      const { intersections } = this.sense()
      ctx.beginPath()
      for (const vec of intersections) {
        if (vec) {
          const { x, y } = vec
          ctx.moveTo(x - 4, y - 4)
          ctx.lineTo(x + 4, y + 4)
          ctx.moveTo(x + 4, y - 4)
          ctx.lineTo(x - 4, y + 4)
        }
      }
      ctx.stroke()
    }
  }
}

const brainType = new BrainType({ inputs: 5, hiddens: [4, 3], outputs: 2, bias: true })
const generationManager = new GenerationManager({ brainType, count: 50 })
const cars = []
for (let i = 0; i < generationManager.count; i++) {
  cars.push(new Car(brainType))
}

let generation = generationManager.makeGeneration()
let generations = 1
let alive
let minProgress
let record = 0

let map

function loadMap (newMap) {
  for (const car of cars) {
    car.recognizeReality(newMap)
  }

  map = newMap
}

function getPositionInRoute (t) {
  t = t % map.routePairs.length
  const [a, b] = map.routePairs[Math.floor(t)]
  return b.clone().sub(a).scale(t % 1).add(a)
}

const START_MIN_PROG = -3 // Initial minimum progress
const MIN_PROG_PER_S = 1 // Movement of the minimum progress per second
function startGen () {
  const startT = 0
  const position = getPositionInRoute(startT)
  const angle = getPositionInRoute(startT + 0.1).sub(position).angle
  minProgress = START_MIN_PROG

  for (let i = 0; i < generationManager.count; i++) {
    cars[i].loadIdeology(generation[i]).start(position, angle)
  }
  alive = [...cars]
}

function nextGen () {
  cars.sort((a, b) => b.progress - a.progress)
  if (cars[0].progress > record) {
    record = cars[0].progress
  }

  generation = generationManager.nextGeneration(cars.map(car => car.ideology))
  generations++
}

let scrollX = 0
let scrollY = 0

function simulate (simTime) {
  minProgress += MIN_PROG_PER_S * simTime

  for (let i = 0; i < alive.length; i++) {
    alive[i].simulate(simTime)
    if (alive[i].progress < minProgress) {
      alive[i].dead = true
    }
    if (alive[i].dead) {
      alive.splice(i, 1)
      i--
    }
  }

  if (alive.length === 0) {
    nextGen()
    startGen()
  }

  const winner = alive.reduce((curr, acc) => curr.progress > acc.progress ? curr : acc)
  // Look ahead 50 units
  const centre = new Vector2(200, 0).rotate(winner.angle).add(winner.position)
  scrollX += (centre.x - screenWidth / 2 - scrollX) / 50
  scrollY += (centre.y - screenHeight / 2 - scrollY) / 50
}

const canvas = document.getElementById('canvas')
const c = canvas.getContext('2d')

const networkCanvas = document.getElementById('network')
const networkCtx = networkCanvas.getContext('2d')

const PADDING = 10
const mindReader = new MindReader({ brainType, x: PADDING, y: PADDING })
const networkSize = mindReader.getExpectedSize()
networkSize.width += PADDING * 2
networkSize.height += PADDING * 2
networkCanvas.style.width = networkSize.width + 'px'
networkCanvas.style.height = networkSize.height + 'px'

let screenWidth
let screenHeight

function resize () {
  const dpr = window.devicePixelRatio
  screenWidth = window.innerWidth
  screenHeight = window.innerHeight

  canvas.width = screenWidth * dpr
  canvas.height = screenHeight * dpr
  c.scale(dpr, dpr)

  networkCanvas.width = networkSize.width * dpr
  networkCanvas.height = networkSize.height * dpr
  networkCtx.scale(dpr, dpr)
}

window.addEventListener('resize', resize)
resize()

function render () {
  c.clearRect(0, 0, screenWidth, screenHeight)

  c.save()
  c.translate(-scrollX, -scrollY)

  c.lineWidth = 2
  c.lineJoin = 'round'
  c.lineCap = 'round'
  c.strokeStyle = 'rgba(255, 255, 255, 0.3)'
  c.beginPath()
  for (let i = 0; i < map.walls.length; i++) {
    const a = map.walls[i - 1]
    const b = map.walls[i]
    if (a && b) {
      c.moveTo(...a.comps)
      c.lineTo(...b.comps)
    }
  }
  c.stroke()

  const winner = alive.reduce((curr, acc) => curr.progress > acc.progress ? curr : acc)
  c.strokeStyle = 'rgba(0, 255, 255, 0.2)'
  for (const car of cars) {
    car.draw(c, car === winner)
  }

  c.restore()

  c.fillStyle = 'white'
  c.font = '14px monospace'
  c.fillText(`Generation ${generations}`, 0, 16)
  c.fillText(`${alive.length}/${cars.length} alive`, 0, 32)
  c.fillText(`Furthest travelled: ${winner.progress.toFixed(2)}`, 0, 48)
  c.fillText(`Furthest travelled ever: ${record.toFixed(2)}`, 0, 64)

  networkCtx.clearRect(0, 0, networkSize.width, networkSize.height)
  mindReader.render(networkCtx, winner.ideology, winner.getInputs())
}

const renderer = new RenderSimulation({ render, simulate })

Promise.all([
  fetch('./map1.json').then(r => r.ok ? r.json() : Promise.reject(r.status)),
  fetch('./map1-route.json').then(r => r.ok ? r.json() : Promise.reject(r.status))
]).then(([wallVertices, routeVertices]) => {
  const walls = []
  for (const vertices of wallVertices) {
    walls.push(null, ...vertices.map(comps => new Vector2(...comps)))
  }
  const route = []
  for (const vertices of routeVertices) {
    route.push(null, ...vertices.map(comps => new Vector2(...comps)))
  }

  // routePairs is an array of tuples containing the start and end points of each segment
  const routePairs = []
  let lapLength = 0
  for (const vertices of routeVertices) {
    for (let i = 1; i < vertices.length; i++) {
      const a = new Vector2(...vertices[i - 1])
      const b = new Vector2(...vertices[i])
      const length = a.clone().sub(b).length
      lapLength += length
      routePairs.push([a, b, length])
    }
  }
  loadMap({ walls, route, routePairs, lapLength })

  startGen()
  renderer.start()
})

window.setSpeed = speed => renderer.speed = speed
window.alive = () => alive
window.minProgress = () => minProgress
window.map = () => map
    </script>
  </body>
</html>

{'fraction_non_alphanumeric': 0.14076923076923076, 'fraction_numerical': 0.01647058823529412, 'mean_word_length': 2.7383288227334237, 'pattern_counts': {'":': 0, '<': 73, '<?xml version=': 0, '>': 93, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5516616', 'n_tokens_mistral': 7780, 'n_tokens_neox': 7364, 'n_words': 1856}