# 60d79fddcf520e8bd701a2b4d1ea981089f5c7d6c7bc6a9f2b3fc63e2b0658a9 Elan 2.0.0-alpha4 valid

# Use the W,A,S,D keys to change Snake direction
main
  variable blocks set to createBlockGraphics(white)
  variable snake set to new Snake()
  variable apple set to new Apple()
  call apple.newRandomPosition(snake)
  while not snake.gameOver()
    call snake.updateBlocks(blocks)
    call apple.updateBlocks(blocks)
    call displayBlocks(blocks)
    call sleep_ms(150)
    call snake.clockTick(getKey(), apple)
  end while
  call print($"Game Over! Score: {snake.score()}")
end main

class Snake
  constructor()
    variable tail set to new Square(20, 15)
    set this.currentDir to Direction.right
    set this.body to [tail]
    set this.head to tail.getAdjacentSquare(this.currentDir)
    set this.priorTail to tail
  end constructor

  function toString() returns String
    return ""
  end function

  private property currentDir as Direction

  private property head as Square

  private property body as List<of Square>

  private property priorTail as Square

  procedure clockTick(key as String, apple as Apple)
    call this.setDirection(key)
    set this.priorTail to this.body[0]
    variable body set to this.body
    call body.append(this.head)
    set this.head to this.head.getAdjacentSquare(this.currentDir)
    if this.head.equals(apple.location) then
      call apple.newRandomPosition(this)
    else
      set this.body to this.body.subList(1, this.body.length())
    end if
  end procedure

  procedure updateBlocks(blocks as List<of List<of Int>>)
    set blocks[this.head.x][this.head.y] to green
    if not this.body[0].equals(this.priorTail) then
      set blocks[this.priorTail.x][this.priorTail.y] to white
    end if
  end procedure

  function score() returns Int
    return this.body.length() - 1
  end function

  function bodyCovers(sq as Square) returns Boolean
    variable result set to false
    for seg in this.body
      if (seg.equals(sq)) then
        set result to true
      end if
    end for
    return result
  end function

  function gameOver() returns Boolean
    return this.bodyCovers(this.head) or this.head.hasHitEdge()
  end function

  private procedure setDirection(key as String)
    if key.equals("w") then
      set this.currentDir to Direction.up
    elif key.equals("s") then
      set this.currentDir to Direction.down
    elif key.equals("a") then
      set this.currentDir to Direction.left
    elif key.equals("d") then
      set this.currentDir to Direction.right
    end if
  end procedure

end class

class Apple
  constructor()
    set this.location to new Square(0, 0)
  end constructor

  function toString() returns String
    return ""
  end function

  property location as Square

  procedure newRandomPosition(snake as Snake)
    variable changePosition set to true
    while changePosition
      variable ranX set to randint(0, 39)
      variable ranY set to randint(0, 29)
      set this.location to new Square(ranX, ranY)
      if not snake.bodyCovers(this.location) then
        set changePosition to false
      end if
    end while
  end procedure

  procedure updateBlocks(blocks as List<of List<of Int>>)
    set blocks[this.location.x][this.location.y] to red
  end procedure

end class

class Square
  constructor(x as Int, y as Int)
    set this.x to x
    set this.y to y
  end constructor

  function toString() returns String
    return ""
  end function

  property x as Int

  property y as Int

  function getAdjacentSquare(d as Direction) returns Square
    variable newX set to this.x
    variable newY set to this.y
    if d is Direction.left then
      set newX to this.x - 1
    elif d is Direction.right then
      set newX to this.x + 1
    elif d is Direction.up then
      set newY to this.y - 1
    elif d is Direction.down then
      set newY to this.y + 1
    end if
    return new Square(newX, newY)
  end function

  function hasHitEdge() returns Boolean
    return (this.x is -1) or (this.y is -1) or (this.x is 40) or (this.y is 30)
  end function

end class

enum Direction up, down, left, right

test test_snake
  variable snake set to new Snake()
  # bodyCovers
  assert snake.bodyCovers(new Square(20, 15)) is true
  assert snake.bodyCovers(new Square(21, 15)) is false
  # gameOver, score - can only test test_for default - which is not thorough test
  assert snake.gameOver() is false
  assert snake.score() is 0
end test

test test_apple
  # no tests
end test

test test_square
  # constructor - not testable as properties are private
  # getAdjacentSquare
  variable sq1 set to new Square(3, 4)
  assert sq1.getAdjacentSquare(Direction.up) is new Square(3, 3)
  assert sq1.getAdjacentSquare(Direction.down) is new Square(3, 5)
  assert sq1.getAdjacentSquare(Direction.left) is new Square(2, 4)
  assert sq1.getAdjacentSquare(Direction.right) is new Square(4, 4)
  variable sq2 set to new Square(0, 0)
  variable sq3 set to new Square(-1, 0)
  assert sq2.getAdjacentSquare(Direction.left) is sq3
  # hasHitEdge
  assert (new Square(0, 0)).hasHitEdge() is false
  assert (new Square(39, 20)).hasHitEdge() is false
  assert (new Square(-1, 3)).hasHitEdge() is true
  assert (new Square(3, -1)).hasHitEdge() is true
  assert (new Square(40, 3)).hasHitEdge() is true
  assert (new Square(3, 30)).hasHitEdge() is true
end test
