# 670ef0a20477eac45f9087e897006c4cbe048d65647f250ce62c616504f3c9ee 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 rnd set to new Random()
  call rnd.initialiseFromClock()
  variable game set to (new Game(rnd)).withNewApple()
  while game.isOn
    set blocks to updateGraphics(game, blocks)
    call displayBlocks(blocks)
    call sleep_ms(150)
    set game to clockTick(game, getKey())
  end while
  call print($"Game Over! Score: {score(game)}")
end main

function clockTick(g as Game, k as String) returns Game
  let g2 be if(k.equals(""), g, g.withKey(k))
  let g3 be moveSnake(g2)
  let g4 be eatAppleIfPoss(g3)
  return if(gameOver(g4), g4.withIsOn(false), g4)
end function

function updateGraphics(g as Game, b as List<of List<of Int>>) returns List<of List<of Int>>
  let b2 be graphicsPut(b, g.apple.x, g.apple.y, red)
  let b3 be graphicsPut(b2, g.head.x, g.head.y, green)
  let tail be g.body[0]
  let tailColour be if(tail.equals(g.priorTail), green, white)
  return graphicsPut(b3, tail.x, tail.y, tailColour)
end function

# Temporary solution pending creation of withPut as an extension method to ListOfList
function graphicsPut(graphics as List<of List<of Int>>, x as Int, y as Int, colour as Int) returns List<of List<of Int>>
  return graphics.withSet(x, graphics[x].withSet(y, colour))
end function

function score(g as Game) returns Int
  return g.body.length() - 2
end function

function moveSnake(g as Game) returns Game
  let k be g.key
  let x be g.head.x
  let y be g.head.y
  let newX be if(k.equals("a"), x - 1, if(k.equals("d"), x + 1, x))
  let newY be if(k.equals("w"), y - 1, if(k.equals("s"), y + 1, y))
  return g.withBody(g.body.withAppend(g.head)).withHead(new Square(newX, newY))
end function

function eatAppleIfPoss(g as Game) returns Game
  let tail be g.body[0]
  let moveTail be g.body.subList(1, g.body.length())
  return if(headOverApple(g), g.withNewApple(), g.withPriorTail(tail).withBody(moveTail))
end function

function headOverApple(g as Game) returns Boolean
  return g.head.equals(g.apple)
end function

function gameOver(g as Game) returns Boolean
  return g.body.contains(g.head) or hasHitEdge(g)
end function

function hasHitEdge(g as Game) returns Boolean
  let x be g.head.x
  let y be g.head.y
  return (x is -1) or (y is -1) or (x is 40) or (y is 30)
end function

class Game
  property head as Square

  property body as List<of Square>

  property priorTail as Square

  property apple as Square

  property isOn as Boolean

  property rnd as Random

  property key as String

  constructor(rnd as Random)
    set this.head to new Square(22, 15)
    set this.body to [new Square(20, 15), new Square(21, 15)]
    set this.priorTail to new Square(0, 0)
    set this.key to "d"
    set this.isOn to true
    set this.apple to new Square(12, 15)
    set this.rnd to rnd
  end constructor

  function toString() returns String
    return ""
  end function

  function withNewApple() returns Game
    let x be this.rnd.asInt(0, 39)
    let rnd2 be this.rnd.nextGen()
    let y be rnd2.asInt(0, 29)
    let rnd3 be rnd2.nextGen()
    let apple2 be new Square(x, y)
    let g2 be this.withApple(apple2).withRnd(rnd3)
    return if(g2.body.contains(apple2), g2.withNewApple(), g2)
  end function

  function withHead(value as Square) returns Game
    let copyOfThis be copy(this)
    set copyOfThis.head to value
    return copyOfThis
  end function

  function withBody(value as List<of Square>) returns Game
    let copyOfThis be copy(this)
    set copyOfThis.body to value
    return copyOfThis
  end function

  function withPriorTail(value as Square) returns Game
    let copyOfThis be copy(this)
    set copyOfThis.priorTail to value
    return copyOfThis
  end function

  function withApple(value as Square) returns Game
    let copyOfThis be copy(this)
    set copyOfThis.apple to value
    return copyOfThis
  end function

  function withIsOn(value as Boolean) returns Game
    let copyOfThis be copy(this)
    set copyOfThis.isOn to value
    return copyOfThis
  end function

  function withRnd(value as Random) returns Game
    let copyOfThis be copy(this)
    set copyOfThis.rnd to value
    return copyOfThis
  end function

  function withKey(value as String) returns Game
    let copyOfThis be copy(this)
    set copyOfThis.key to value
    return copyOfThis
  end function

end class

class Square
  property x as Int

  property y as Int

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

  function withX(value as Int) returns Square
    let copyOfThis be copy(this)
    set copyOfThis.x to value
    return copyOfThis
  end function

  function withY(value as Int) returns Square
    let copyOfThis be copy(this)
    set copyOfThis.y to value
    return copyOfThis
  end function

  function toString() returns String
    return $"{this.x}, {this.y}"
  end function

end class

test test_clockTick
  let g1 be new Game(new Random())
  let g2 be g1.withNewApple()
  let g3 be clockTick(g2, "s")
  assert g3.head is new Square(22, 16)
  assert g3.body.length() is 2
  assert g3.priorTail is g2.body[0]
  assert g3.isOn is true
  let g4 be g3.withApple(new Square(22, 17))
  let g5 be clockTick(g4, "s")
  assert g5.body.length() is 3
  assert g5.priorTail is g4.priorTail
  assert g5.isOn is true
  let g6 be g5.withHead(new Square(22, 29))
  let g7 be clockTick(g6, "s")
  assert g7.isOn is false
end test

test test_updateGraphics
  let blocks be createBlockGraphics(white)
  let g1 be new Game(new Random())
  let blocks2 be updateGraphics(g1, blocks)
  assert blocks2[12][15] is red
  assert blocks2[22][15] is green
  assert blocks2[21][15] is white
  let g3 be clockTick(g1, "d")
  let blocks3 be updateGraphics(g3, blocks2)
  assert blocks3[12][15] is red
  assert blocks3[22][15] is green
  assert blocks3[23][15] is green
end test

test test_testnewApple
  let g1 be new Game(new Random())
  assert g1.apple is new Square(12, 15)
  let g2 be g1.withNewApple()
  assert g2.apple is new Square(12, 15)
  let g3 be g2.withNewApple()
  assert g3.apple is new Square(10, 12)
  # test that apple is never over snake
  let g4 be (new Game(new Random()))
  let g5 be g4.withBody([new Square(10, 12)])
  let g6 be g5.withNewApple()
  assert g4.apple is new Square(12, 15)
end test

test test_score
  let g1 be new Game(new Random())
  assert score(g1) is 0
  let g2 be g1.withBody([new Square(4, 4), new Square(5, 4)])
  assert score(g2) is 0
  let g3 be g1.withBody([new Square(3, 4), new Square(4, 4), new Square(5, 4)])
  assert score(g3) is 1
  let g4 be g1.withBody([new Square(3, 4), new Square(4, 4), new Square(5, 4), new Square(5, 5)])
  assert score(g4) is 2
end test

test test_moveSnake
  let g1 be new Game(new Random())
  let g2 be g1.withKey("a")
  let g3 be moveSnake(g2)
  assert g3.head is new Square(21, 15)
  let g4 be g1.withKey("d")
  let g5 be moveSnake(g4)
  assert g5.head is new Square(23, 15)
  let g6 be g1.withKey("w")
  let g7 be moveSnake(g6)
  assert g7.head is new Square(22, 14)
  let g8 be g1.withKey("s")
  let g9 be moveSnake(g8)
  assert g9.head is new Square(22, 16)
end test

test test_eatAppleIfPoss
  let g1 be new Game(new Random())
  assert g1.body.length() is 2
  # negative case
  let g2 be g1.withApple(new Square(23, 15))
  let g3 be eatAppleIfPoss(g2)
  assert g3.body.length() is 1
  assert g3.apple is g2.apple
  assert g3.priorTail is g2.body[0]
  # positive case
  let g4 be g2.withHead(new Square(23, 15))
  let g5 be eatAppleIfPoss(g4)
  assert g5.body.length() is 2
  assert g5.apple is new Square(12, 15)
  assert g5.priorTail is g1.priorTail
end test

test test_overApple
  let g1 be new Game(new Random())
  let g2 be g1.withApple(new Square(23, 15))
  assert headOverApple(g2) is false
  let g3 be g2.withHead(new Square(23, 15))
  assert headOverApple(g3) is true
end test

test test_gameOver
  let g1 be new Game((new Random()))
  assert gameOver(g1) is false
  let g2 be g1.withHead(new Square(0, 0))
  assert gameOver(g2) is false
  let g3 be g1.withHead(new Square(40, 15))
  assert gameOver(g3) is true
  let g4 be g1.withHead(new Square(21, 15))
  assert gameOver(g4) is true
end test

test test_headIsAtEdge
  let g1 be new Game(new Random())
  assert hasHitEdge(g1) is false
  let g2 be g1.withHead(new Square(40, 15))
  assert hasHitEdge(g2) is true
  let g3 be g1.withHead(new Square(-1, 15))
  assert hasHitEdge(g3) is true
  let g4 be g1.withHead(new Square(20, 30))
  assert hasHitEdge(g4) is true
  let g5 be g1.withHead(new Square(20, -1))
  assert hasHitEdge(g5) is true
end test

test test_newSquare
  let sq be new Square(3, 4)
  assert sq.x is 3
  assert sq.y is 4
end test

test test_newGame
  let rnd be new Random()
  let game be new Game(rnd)
  let totest be game.rnd.equals(rnd)
  assert totest is true
  assert game.head is new Square(22, 15)
  let body be game.body
  assert body.length() is 2
  assert body[0] is new Square(20, 15)
  assert body[1] is new Square(21, 15)
  assert game.priorTail is new Square(0, 0)
  assert game.key is "d"
  assert game.isOn is true
end test
