program snake;

{$I- No I/O checking}
{$R- No range checking}
{$V- No string checking}

(*

  Snake game for CP/M-80 V2.2 and VTxx/ANSI terminal

  Original version (C) 2018, Karl A. Brokstad (www.z80.no)

  Turbo Pascal conversion and other mods with permission
     (C) 2018, linker3000 (linker3000-at-gmail-dot-com)

  V1.2T: 21-Jul 2019
         Corrected Y boundary max in putFood.

  V1.1T: 14-Oct-2018
         Command line switch -m (monochrome) suppresses all colour codes
         Version numbering detached from Karl A. Brokstad's original

  V23N:  24-Sep-2018 First public release
         Game map used for collision detection and food position
         generation - this takes out several long loops.

 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*)

Type

  Str255 = string[255];

Const

  progVersion   : string[6] = '1.2acn';
  borderChar    : char = '#';
  snakeHeadChar : char = 'O';
  snakeBodyChar : char = '#';

  xMax          : Byte = 80;
  yMax          : Byte = 25;

var
   snakeX   : array [1..100] of byte;
   snakeY   : array [1..100] of byte;
   gameGrid : array [2..80, 2..25] of byte; {Tracks snake body and tail}

   snakeBodyANSI : String[8];
   foodANSI      : String[8];
   snakeHeadANSI : String[8];
   borderANSI    : String[8];
   resetANSI     : String[4];
   msgANSI       : String[8];

   snakeHead, snakeTail, snakeLength : integer;

   I, level, score : integer;
   inp, dir        : char;
   X, Y            : byte;
   crash, escaped  : boolean;
   time            : integer;  {Game delay factor}
   food            : boolean;
   foodX, foodY, foodV : byte;

(* * * * * * * * * * writeCtr * * * * * * * * * *)

procedure writeCtr(Line : Byte; S : Str255);
{ Write a centered line of text}
Var
 I : Integer;

begin
  I := 40 - round(Length(S)/2);
  If I < 1 then I := 1;
  gotoxy(I,Line);
  write(S);
end;

(* * * * * * * * * * clrKbd * * * * * * * * * *)

procedure clrKbd; {Clear keyboard buffer}
begin
  repeat until (bdos(6,255)) = 0;
end;

(* * * * * * * * * * readKbd * * * * * * * * * *)

procedure readKbd; {Check for direction keys}
begin
  if keypressed then case upcase(chr(bdos(6,255))) of

       '4','H','A' : dir  := 'L';    (* left *)
       '8','K','W' : dir  := 'U';    (* up *)
       '2','J','S' : dir  := 'D';    (* down *)
       '6','L','D' : dir  := 'R';    (* right *)
       '+'    : time := (time + 5) and 255;
       '-'    : begin
                  time := time - 5;
                  if time < 0 then time := 0;
                end;
       'Q'    : escaped := true;
     end;
end;

(* * * * * * * * * conIn * * * * * * * * * * * *)

function conIn : char;
begin
   conIn := chr(bdos(6, 255)); (* SILENT READ CHARACTER *)
end;

(* * * * * * * * sDelay * * * * * * * * * * * * * *)

{A non-blocking-ish delay during which we check the keyboard}

procedure sDelay (D : integer);
var I : integer;

begin

  for I := 1 to D do
  begin
    Delay(1);
    readKbd;
  end;
end;

(* * * * * * * * * SPLASH SCREEN * * * * * * * * * * * *)

procedure SplashScreen;
begin
   clrscr;
   write (msgANSI);
   writeCtr( 4,'SNAKE '+progVersion+' by Linker3000');
   writeCtr( 5,'From the original by Karl A. Brokstad (www.Z80.no)');
   writeCtr( 7,'This program comes with ABSOLUTELY NO WARRANTY.');
   writeCtr( 9,'This is free software, and you are welcome to redistribute it');
   writeCtr(10,'under certain conditions - see GPLV2 license:');
   writeCtr(11,'https://www.gnu.org/licenses/gpl-2.0.html');
   writeCtr(13,'Requirements:');
   writeCtr(15,'80x25 ANSI/VT100 compatible console');
   if (msgANSI = '') then
     writeCtr(16,'(Monochrome mode was chosen by command line switch)')
   else
     writeCtr(16,'(Start program with -m switch to disable ANSI colors)');
   writeCtr(18,'Movement:');
   writeCtr(20,'4 = left, 8 = up, 2 = down, 6 = right OR use HJKL keys');
   writeCtr(21,'Q = Quit to here during game');
   writeCtr(23,'Press ENTER to START or Q to QUIT');
   repeat
     repeat until keypressed;
     inp := conIn;
     if ((inp = 'Q') or (inp = 'q')) then
     begin
       write (resetANSI);
       halt;
     end;
   until (inp = #13);
end;

(* Put down food *)
Procedure putFood;
begin
  (* food value 1-9 *)
  foodV := 1 + random(9);

  repeat {Find a new food location}
    readKbd;
    foodX := 3 + random(xMax-3);      {Stay within walls}
    foodY := 3 + random(yMax-4);
  until (gameGrid[foodX,foodY] = 0); {Don't put food down over snake}

  food := true;
  gotoXY(foodX,foodY); {Draw food}
  write(foodANSI, foodV ,resetANSI);
  gotoXY(1,1);
  readKbd;
end; {food}

(* * * * * * * * * DRAW SCREEN * * * * * * * * * * * *)

procedure DrawScreen;
var I : integer;
begin
   clrscr;
   write(borderANSI);

   for I:=1 to xMax do
   begin
     gotoXY(I,1);
     write(borderChar);
     gotoXY(I,yMax);
     write(borderChar);
   end;

   for I:=1 to yMax do
   begin
     gotoXY(1,I);
     write(borderChar);
     gotoXY(xMax,I);
     write(borderChar);
   end;

   write(resetANSI);
end;

(* * * * * * * * * * * * * * * * * * * * * * *)
(* * * * * * * * * * MAIN * * * * * * * * * * *)

begin

  snakeBodyANSI := #27'[40m';
  foodANSI      := #27'[33;40m'; {Yellow on black background}
  snakeHeadANSI := #27'[40m';
  borderANSI    := #27'[31;44m';
  resetANSI     := #27'[0m';     {Reset ANSI attributes}
  msgANSI       := #27'[33;40m';

  if (ParamCount > 0) and ((ParamStr(1) = '-m') or (ParamStr(1) = '-M')) then
  begin
    snakeBodyANSI := '';
    foodANSI      := '';
    snakeHeadANSI := '';
    borderANSI    := '';
    resetANSI     := '';
    msgANSI       := '';
  end;

  repeat
    randomize;
    SplashScreen;  (* show splash screen *)

(* * * * * * * * * INIT GAME * * * * * * * * *)

    escaped := false;
    crash   := false;
    score   := 1;
    level   := 1;
    time    := 100; (* delay time *)

(* * * * * * * * * GAME LOOP * * * * * * * * *)
    while (not escaped) and (not crash) do
    begin
      {Clear the object location grid}
      for Y := 2 to yMax do for X := 2 to xMax do gameGrid[X,Y] := 0;

      DrawScreen;

      snakeHead := 1;               (* first position *)
      snakeLength := 1;             (* length and last position *)
      snakeTail := 2;               (* position to erase snakeTail := snakeHead + snakeLength *)

      X := 39;                      (* position in middle of screen *)
      Y := 12;

      snakeX[snakeLength] := X;
      snakeY[snakeLength] := Y;

      score := score + snakeLength;    (* write level and score *)
      gotoXY(30,1);
      write(' LEVEL ',level,'  SCORE : ',score,' ');

      food := false;

      for I := 5 downto 0 do
      begin
        gotoXY(X,Y);
        write(I);
        delay(500);
      end;

      dir := 'R';

(* * * * * * * * * START GAME LEVEL  * * * * * * * * *)

      repeat {Game level}

        readKbd; {Check keyboard for input}

        case dir of        (* MOVE readKbd *)
          'L' : X:=X-1;    (* left *)
          'R' : X:=X+1;    (* right *)
          'D' : Y:=Y+1;    (* down *)
          'U' : Y:=Y-1;    (* up *)
        end;

(* Save snake position *)
(* PUSH snake positions down the line *)
(* Always use full array size to keep processing speed consistent *)

        for I := 100 downto 2 do
        begin
          readKbd; {Check keyboard for input}
          snakeX[I] := snakeX[I-1];
          snakeY[I] := snakeY[I-1];
        end;

        snakeX[snakeHead] := X;
        snakeY[snakeHead] := Y;

        gotoXY(snakeX[snakeHead],snakeY[snakeHead]);   (* Draw new head *)
        write (snakeHeadANSI,snakeHeadChar);

        if snakeLength > 1 then
        begin
          readKbd;
          gotoXY(snakeX[snakeHead+1],snakeY[snakeHead+1]);   (* Draw body *)
          gameGrid[snakeX[snakeHead+1],snakeY[snakeHead+1]] := 1; {Where the body is}
          write (snakeBodyANSI,snakeBodyChar);
        end;

        gotoXY(snakeX[snakeTail],snakeY[snakeTail]);   (* erase tail *)
        gameGrid[snakeX[snakeTail],snakeY[snakeTail]] := 0; {Remove tail from map}

        write (resetANSI,' ');
        gotoXY(1,1);

        if (food = false) then putFood; {Put down some food}

        {Test snake position}
        if (X < 2) or (X >= xMax)     {Snake crashes into wall or own body }
          or (Y < 2) or (Y >= yMax)
          or (gameGrid[X,Y] = 1) then crash := true;

        if (X = foodX) and (Y = foodY) then {Snake eats food }
        begin
          readKbd;
          snakeLength := snakeLength + foodV;
          food := false;
          snakeTail := snakeLength +1;
          score := score + (foodV * level);
          gotoXY(30,1);
          write(' LEVEL ',level,'  SCORE : ',score,' ');
          gotoXY(1,1);
          readKbd;
        end;

        sDelay(time);       (* delay *)

      until crash or escaped or (snakeLength > 99); {game level}

(* * * * * * * * * * END GAME * * * * * * * *)

      write(msgANSI);
      if (crash or escaped) then                        (* game over *)
        if crash then writeCtr(12,' YOU CRASHED ')
          else writeCtr(12,' YOU QUIT! ');

      if snakeLength > 99 then                 (* advance to next level *)
      begin
        level := level + 1;
        time := time - 10;
        if (time < 1) then time := 1;
        writeCtr(12,' YOU MADE IT TO THE NEXT LEVEL ');
      end;
      write (resetANSI);

      clrKbd;
      writeCtr(14,' PRESS ENTER ');
      repeat until conIn = #13;
    end {while}
  until false;

(* * * * * * * * * * FINISH  * * * * * * * *)

end.

