
// SnakeGame.java
// Rui Afonso Pereira, 2015
// https://github.com/rpereira/lanterna-snake
// Modified by Andrew Davison, ad@coe.psu.ac.th, March 2023

/* Move a snake around the window, eating food (f) and avoiding
   bombs (b). Death ensues if the snake touches a wall or
   itself. As the snake eats, it increases in length.

   The snake is moved using the four arrow keys.

   There are two banner screens: "Snake" at the start of the game,
   and "Game Over" at the end.

   The program consists of:
     * SnakeGame.java (this file!)
     * Snake.java
     * SnakeDirection.java

   Uses Lanterna 3
   https://github.com/mabe02/lanterna

   Usage (Windows):
     > compile.bat SnakeGame.java
     > run.bat SnakeGame

   Usage (Linux):
     > ./compile.sh SnakeGame.java
     > ./run.sh SnakeGame

*/

import java.io.*;
import java.awt.*;
import java.util.*;

import com.googlecode.lanterna.*;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.screen.*;
import com.googlecode.lanterna.terminal.*;



public class SnakeGame
{
  // string representations
  private final static String WALL = "X";
  private final static String FOOD = "f";
  private final static String BOMB = "B";

  // The interval at which objects are generated.
  private final static int OBJ_INTERVAL = 6;
                                  // in cycleCount units

  // score penalty when the snake hits a bomb
  private final static int BOMB_PENALTY = 25;

  private static Screen scr;
  private static int numCols, numRows;

  private Random rand = new Random();

  private final int gameHeight, gameWidth;

  private Snake snake;
  private LinkedList<Point> food;
  private LinkedList<Point> bombs;
  private int score = 0;


  private SnakeGame()
  {
    scr = Utils.createScreen();
    scr.setCursorPosition(null);
    
    TerminalSize termSz = scr.getTerminalSize();
    numCols = termSz.getColumns();
    numRows = termSz.getRows();

    // there is a wall of width 1 around the window
    // which limits the game's dimensions
    gameWidth  = numCols - 1;
    gameHeight = numRows - 2;

    // game setup
    drawStart();
    Utils.refresh(scr);
    Utils.pause(500);
    scr.clear();

    int cycleCount = 0;
    snake = new Snake(SnakeDirection.RIGHT, 
                              gameWidth, gameHeight);
    food = new LinkedList<Point>();
    bombs = new LinkedList<Point>();

    drawWalls();
    snake.draw(scr);
    drawAt(4, gameHeight+1, "SCORE: ", TextColor.ANSI.CYAN);
    drawScore();

    // the game loop
    while (snake.isAlive()) {
      if (cycleCount % OBJ_INTERVAL == 0) {
        makeFood();
        makeBomb();
      }

      readKeyboard();
      updateGame();
      Utils.pause(200);
      cycleCount++;
    }

    // game is over
    clearGameObjects();
    drawGameOver();
    Utils.refresh(scr);

    Utils.waitQuit(scr);
    Utils.close(scr);
  } /* SnakeGame() */



  private void drawWalls()
  {
    // Draw top and bottom walls
    for( int i = 0; i < numCols; i++) {
      drawAt(i, 0, WALL);
      drawAt(i, gameHeight, WALL);
    }

    // Draw left and right walls
    for( int i = 0; i < numRows; i++) {
      drawAt(0, i, WALL);
      drawAt(gameWidth, i, WALL);
    }
  } /* drawWalls */



  private void drawScore()
  {  drawAt(10, gameHeight+1, ""+score);  }


  private void makeFood()
  {
    Point p = genCoord();
    food.add( new Point(p.x, p.y));
    drawAt(p.x, p.y, FOOD, TextColor.ANSI.GREEN);
  } /* makeFood */



  private void makeBomb()
  {
    Point p = genCoord();
    bombs.add( new Point(p.x, p.y));
    drawAt(p.x, p.y, BOMB, TextColor.ANSI.RED);
  } /* makeBomb */



  private void readKeyboard()
  // control the snake's direction
  {
    KeyStroke ks = Utils.readInput(scr);
    if (ks != null)
      switch (ks.getKeyType()) {
        case ArrowUp:
          snake.setDir(SnakeDirection.UP);
          break;
	  
        case ArrowDown:
          snake.setDir(SnakeDirection.DOWN);
          break;
	  
        case ArrowLeft:
          snake.setDir(SnakeDirection.LEFT);
          break;
	  
        case ArrowRight:
          snake.setDir(SnakeDirection.RIGHT);
          break;
	  
        case Escape:
        case EOF:
          snake.kill();
          break;

        default:
          break;
    }
  } /* readKeyboard */



  private void updateGame()
  /* Move the snake, draw it, and then check if it has eaten
     food or has collided. The latter will cause game over.
  */
  {
    // erase the snake's tail before it moves
    Point tail = snake.getTail();
    clearAt(tail.x, tail.y);

    snake.move();
    snake.draw(scr);

    Point head = snake.getHead();
    if (isWall(head) || snake.selfCollided()) {
      drawAt(head.x, head.y, "X", TextColor.ANSI.RED);
                         // highlight crash position
      snake.kill(); // will stop the game loop
    }
    else if (snake.ateFruit(food)) {
      snake.increaseSize();
      score += (snake.getBody().size() * 2+bombs.size());
      drawScore();
      makeFood();
    }
    else if (snake.hitBomb(bombs)) {
      score -= BOMB_PENALTY;
      drawScore();  
    }

    Utils.refresh(scr);
  } /* updateGame */



  private boolean isWall(Point p)
  // Checks if the given position matches a board's wall.
  {
    return ((p.x == 0) || (p.x == gameWidth) ||
            (p.y == 0) || (p.y == gameHeight) );
  } /* isWall */




  private void drawStart()
  {
    int x = 10;
    int y = 2;
    drawAt(x,y,   "########  ######  ##  #########  ##    ##  ######");
    drawAt(x,++y, "##        ##  ##  ##  ##     ##  ##   ##   ##    ");
    drawAt(x,++y, "##        ##  ##  ##  ##     ##  ##  ##    ##    ");
    drawAt(x,++y, "########  ##  ##  ##  #########  ######    ######");
    drawAt(x,++y, "      ##  ##  ##  ##  ##     ##  ##  ##    ##    ");
    drawAt(x,++y, "      ##  ##  ##  ##  ##     ##  ##   ##   ##    ");
    drawAt(x,++y, "########  ##  ######  ##     ##  ##    ##  ######");
  } /* drawStart */




  private void clearGameObjects()
  // Clear snake, food, bombs from the screen
  {
    for(Point p : snake.getBody())
      clearAt(p.x, p.y);

    for (Point p : food)
      clearAt(p.x, p.y);

    for (Point p : bombs)
      clearAt(p.x, p.y);
  } /* clearGameObjects */



  private void drawGameOver()
  {
    int x = 20;
    int y = 2;
    drawAt(x, y,   "#######  #######  ##### #####  ######");
    drawAt(x, ++y, "##       ##   ##  ## ## ## ##  ##    ");
    drawAt(x, ++y, "## ####  #######  ## ##### ##  ######");
    drawAt(x, ++y, "##   ##  ##   ##  ##  ###  ##  ##    ");
    drawAt(x, ++y, "#######  ##   ##  ##       ##  ######");

    y++;  // blank line
    drawAt(x, ++y, "########  ###  ###  ######  ######  ");
    drawAt(x, ++y, "##    ##  ###  ###  ###     ##  ##  ");
    drawAt(x, ++y, "##    ##   ######   ######  ######  ");
    drawAt(x, ++y, "##    ##    ####    ###     ##   ## ");
    drawAt(x, ++y, "########     ##     ######  ##    ##");

  } /* drawGameOver */



  private Point genCoord()
  // Generate a random position that is not already occupied
  {
    Point p = null;
    do {
      p = new Point( randNum(1, gameWidth), 
                     randNum(1, gameHeight));
    } while (!isEmpty(p));

   return p;
  } /* genCoord */


  private int randNum(int min, int max)
  {  return rand.nextInt(max - min) + min;  }



  private boolean isEmpty(Point p)
  /* is the specified position 
     not occupied by the snake, food, or a bomb?
  */
  {
    return !(snake.inBody(p) || food.contains(p) || 
             bombs.contains(p));
  }



  private void drawAt(int x, int y, String s, 
                                 TextColor.ANSI color)
  {  Utils.putString(scr, x, y, s, color);   }


  private void drawAt(int x, int y, String s)
  {  Utils.putString(scr, x, y, s, TextColor.ANSI.CYAN);   }


  private void clearAt(int x, int y)
  {   Utils.putString(scr, x, y, " ");  }



  // ---------------------------------------

  public static void main(String[] args)
  {
    new SnakeGame();
  }


}  // end of SnakeGame class
