snake-1
2012. 05. 20.

Snakes in a cage.

» launch snake-1

# Snake(s) v1. March 17, 2011. Junegunn Choi.
# Ported to CoffeeScript on May 20th, 2012.

p5            = processing
min_len       = 2
base_len      = 5
grid_size     = 40
max_snakes    = 50
num_snakes    = /snakes=([1-9][0-9]*)/.exec(window.location.href)
num_snakes    = if num_snakes? then parseInt(num_snakes[1]) else 20
joint_map     = {}
snakes        = []
spacing       = new PVector grid_size, grid_size, grid_size
look_ahead    = 3
wall          = 1
turn_prob     = 0.3
grow_interval = 10

hash          = (x, y) -> "#{x}-#{y}"
hash_position = (p) -> hash p.x, p.y

v = {
  north: new PVector( 0, -1,  0),
  east:  new PVector( 1,  0,  0),
  south: new PVector( 0,  1,  0),
  west:  new PVector(-1,  0,  0),
  up:    new PVector( 0,  0,  1),
  down:  new PVector( 0,  0, -1)
}

class SnakeJoint
  constructor: (@pos, life, num_snakes) ->
    @doomsday = p5.frameCount + life

  life_expectancy:  -> @doomsday - p5.frameCount
  prolong:          -> @doomsday += 1
  alive:            -> p5.frameCount < @doomsday
  wriggle_position: ->
    wpos = new PVector(@pos.x, @pos.y, @pos.z)
    wpos.x += (noise(wpos.y * 10 + 0.5, p5.frameCount * 0.01 + 0,5) - 0.5) * 4
    wpos.y += (noise(wpos.x * 10 + 0.5, p5.frameCount * 0.01 + 0.5) - 0.5) * 4
    wpos

class Snake
  constructor: (@pos, @vel, @base_life, @thickness) ->
    @joints = []
    @stuck  = false
    @add_joint()

  # Greedy (aka dumb) algorithm
  # - look left - how far?
  # - look right - how far?
  # - turn to the direction with more space
  move: ->
    # Time to turn
    if occupied(@next_position(1)) > 0 or random() < turn_prob
      front_dof = 0
      for i in [1..look_ahead]
        break if occupied(@next_position(i)) > (i - 1) * num_snakes
        front_dof += 1

      left_dof = 0
      @turn_left()
      for i in [1..look_ahead]
        break if occupied(@next_position(i)) > (i - 1) * num_snakes
        left_dof += 1
      @turn_right()

      right_dof = 0
      @turn_right()
      for i in [1..look_ahead]
        break if occupied(@next_position(i)) > (i - 1) * num_snakes
        right_dof += 1
      @turn_left()

      # Can turn
      if left_dof > 0 or right_dof > 0
        if left_dof < right_dof
          @turn_right()
        else if left_dof == right_dof
          if random() < 0.5
            @turn_left()
          else
            @turn_right()
        else
          @turn_left()

        @stuck = false
      # Can't turn
      else if front_dof > 0
        @stuck = false
      # Stuck
      else
        @stuck = true

    joints = []
    for j in @joints
      if j.alive()
        joints.push j
      else
        delete joint_map[ hash_position(j.pos) ]
    @joints = joints

    if @stuck
      @base_life = max min_len, @base_life - 1
      @add_joint() if @joints.length < min_len
    else
      @pos.add @vel
      @add_joint()

  add_joint: ->
    cpos = new PVector(@pos.x, @pos.y, @pos.z)
    new_joint = new SnakeJoint(cpos, @base_life, num_snakes)
    @joints.push new_joint
    joint_map[ hash_position(cpos) ] = new_joint

  turn_left:  -> @vel = @vel.cross(v.up)
  turn_right: -> @vel = @vel.cross(v.down)

  lengthen: ->
    return if @stuck
    @base_life += 1
    j.prolong() for j in @joints

  next_position: (steps) ->
    ret = new PVector(@pos.x, @pos.y, @pos.z)
    for i in [0...steps]
      ret.add(@vel)
    ret

occupied = (pos) ->
  j = joint_map[ hash_position(pos) ]
  if j?
    if j == wall
      999
    else
      j.life_expectancy()
  else
    0

random_orientation = ->
  r = random()
  if r < 0.25
    v.north
  else if r < 0.5
    v.south
  else if r < 0.75
    v.east
  else
    v.west

setup = ->
  size      $(window).width(), $(window).height()
  frameRate 30
  colorMode RGB, 1.0

  slot_x     = floor(p5.width / grid_size)
  slot_y     = floor(p5.height / grid_size)
  max_snakes = min(parseInt(slot_x * slot_y / 3), max_snakes)
  num_snakes = constrain(
    window.prompt("Number of snakes? (1-#{max_snakes})", num_snakes),
    1, max_snakes)

  for i in [0...num_snakes]
    while true
      position =
        new PVector(
          floor(random 1, slot_x - 1),
          floor(random 1, slot_y - 1), 0)
      break if occupied(position) == 0

    snakes.push new Snake(
        position,
        random_orientation(),
        base_len,
        grid_size + grid_size * (i / num_snakes))

  min_pos = new PVector 0, 0, 0
  max_pos = new PVector slot_x, slot_y, 0

  for x in [min_pos.x...max_pos.x]
    joint_map[ hash x, min_pos.y ] =
    joint_map[ hash x, max_pos.y ] = wall
  for y in [min_pos.y...max_pos.y]
    joint_map[ hash min_pos.x, y ] =
    joint_map[ hash max_pos.x, y ] = wall

draw = ->
  background 1

  for snake in snakes
    snake.move()
    draw_snake snake
    if p5.frameCount % grow_interval == 0
      snake.lengthen()

draw_snake = (snake) ->
  joints = snake.joints
  noFill()

  # Body
  joint = null
  for step in [0..1]
    if step == 0
      if snake.stuck
        stroke 0.8, 0.5, 0.2
      else
        stroke 0.2, 0.4, 0.2
      strokeWeight snake.thickness
    else
      if snake.stuck
        stroke 0.6, 0.4, 0.2
      else
        stroke 0.2, 0.5, 0.2
      strokeWeight snake.thickness * 0.7

    beginShape()
    for i in [0...joints.length]
      joint = joints[i]
      pos   = joint.wriggle_position()
      x     = pos.x * spacing.x
      y     = pos.y * spacing.y
      curveVertex x + random(), y + random()
      if i == 0 || i == joints.length - 1
        curveVertex x, y
    endShape()

  # Face
  if joint
    pos = joint.wriggle_position()

    pushMatrix()
    translate pos.x * spacing.x, pos.y * spacing.y

    # Cannot differentiate between 90 and 270 (not signed)
    # rotate( acos( new PVector(1, 0, 0).dot( snake.vel ) ) )
    # Thus, signed_angle = atan2(  N * ( V1 x V2 ), V1 * V2  )
    rotate(
        atan2(
          v.up.dot( v.east.cross(snake.vel) ),
          v.east.dot( snake.vel )
        )
    )

    # Eyeballs
    strokeWeight grid_size / 2
    stroke 1
    point  0, - grid_size * 0.3
    point  0,   grid_size * 0.3

    # Pupils
    stroke 0
    if snake.stuck == false
      strokeWeight grid_size / 3
      point grid_size * 0.1, - grid_size * 0.3
      point grid_size * 0.1,   grid_size * 0.3
    else
      strokeWeight grid_size / 10
      line grid_size * 0.1, - grid_size * 0.4,
           grid_size * 0.1, - grid_size * 0.2
      line grid_size * 0.1, + grid_size * 0.4,
           grid_size * 0.1, + grid_size * 0.2

    # Tongue
    strokeWeight 1
    stroke       0.5, 0, 0
    fill         1,   0, 0
    beginShape()
    vertex grid_size * 0.5, + grid_size * 0.05
    vertex grid_size * 1,   + grid_size * 0.1
    vertex grid_size * 0.8,                 0
    vertex grid_size * 1,   - grid_size * 0.1
    vertex grid_size * 0.5, - grid_size * 0.05
    endShape()
    popMatrix()
  # End of face
» capture | close