{ Nibbles engine }

unit Game;

interface

uses Keyb, Crt, Grafix;

const
  { Snake buffer size }
  MaxSnakeLength = 500;

  { Parameters to 'Level' procedure }
  StartOver = 1;
  SameLevel = 2;
  NextLevel = 3;

  { Draw parameters }
  SnakeBody  = #219;
  BackGround = #197;
  Wall       = #219;

  { Keys }
  kbPlayer1Up    = kbUpArrow;
  kbPlayer1Down  = kbDownArrow;
  kbPlayer1Left  = kbLeftArrow;
  kbPlayer1Right = kbRightArrow;

  kbPlayer2Up    = Ord('W'); { "W" key }
  kbPlayer2Down  = Ord('S'); { "S" key }
  kbPlayer2Left  = Ord('A'); { "A" key }
  kbPlayer2Right = Ord('D'); { "D" key }

  kbPause        = Ord('P'); { "P" key }
  kbDone         = kbEsc;      { Esc key }

type
  { Some types }
  TDirection = (Up, Down, Left, Right);

  TSnakeBody = record
    X,Y: byte;
  end;

  { Snake body }
  TSnake = record
    Head,
    X, Y,
    Length,
    Lives,
    SColor: integer;
    Score: longint;
    Body: array[0..MaxSnakeLength] of TSnakeBody;
    Direction: TDirection;
    Alive: boolean;
  end;

  TArenaPoint = record
    X, Y,
    AColor: byte;
  end;

  { Setup new game properties }
  { Skill - 1..Easy
            2..Medim
            3..Hard
            4..Very Hard }
  procedure InitGame(Skill: byte; APlayers: byte);
  { Main routine that controls game play and return players scories }
  procedure PlayGame(var Score1, Score2: longint);

implementation

var
  { Global variables }
  Arena: array[1..80, 1..50] of TArenaPoint;
  Snake: array[1..2] of TSnake;
  Speed: integer;
  Players,
  Number,
  CurrLevel: byte;
  NoNumber,
  DoneGame,
  PlayerDied: boolean;

{ Sets row and column on playing field to given color to facilitate moving }
{  of snakes around the field.                                             }
procedure Draw(x, y, color: byte; What: char);
begin
  { Check bounds }
  If (x >= 1) and (x <= 80) and
     (y >= 1) and (y <= 50) then
  begin
    Arena[X, Y].AColor := Color;    { assign color to arena }

    { Draw body into screen }
    If What = BackGround then
      Print(x, y, Color, 0, What)
    else
      Print(x, y, Color, ColorTable[4], What);
  end;
end;

{ Erase whole player snake }
procedure EraseSnake(Player: byte);
var
  i, tail: integer;
begin
  with Snake[Player] do
  for i := 0 to Snake[Player].Length do
  begin
    tail := (Head + MaxSnakeLength - i) mod MaxSnakeLength;
    Draw(Body[tail].X, Body[tail].Y, ColorTable[4], BackGround);
  end;
end;

{ Initializes playing field colors }
procedure InitField;
var
  X, Y: byte;
begin
  { Screen mode 80x50 }
  TextBackGround(0);
  ClrScr;

  { Clear arena }
  for Y := 1 to 50 do
    for X := 1 TO 80 do
      begin
        Arena[X, Y].AColor := ColorTable[4];
        Print(x, y, 1, 0,'Å');
      end;

  { Draws header }
  TextColor(15);
  TextBackGround(0);
  Write('ÄÄ Player       ÄÄÄÄÄÄÄ ÄÄ Ä                      Ä ÄÄ ÄÄÄÄÄÄÄÄÄ Player       ÄÄ');

  TextColor(7);
  Write(' score         nibbles                                  nibbles    score        ');

  TextColor(ColorTable[2]);
  GotoXY(11,1); Write('T W O');
  TextColor(ColorTable[1]);
  GotoXY(73,1); Write('O N E');

  TextColor(1);
  Center(40, 1, 'N I B B L E S !');
  TextColor(9);
  Center(40, 1, 'I B B L E ');
  TextColor(11);
  Center(40, 1, 'B B L ');
  TextColor(15);
  Center(40, 1, 'B ');

  { Set (turn on) pixels for screen border }
  for X := 1 TO 80 do
  begin
    Draw(X, 3,  ColorTable[3], Wall);
    Draw(X, 50, ColorTable[3], Wall);
  end;
  for Y := 3 TO 49 do
  begin
    Draw(1,  Y, ColorTable[3], Wall);
    Draw(80, Y, ColorTable[3], Wall);
  end;
end;

{ Setup new game properties }
procedure InitGame;
begin
  case Skill of
    1: begin
         Speed := 150;
       end;
    2: begin
         Speed := 100;
       end;
    3: begin
         Speed := 50;
       end
    else
      begin
        Speed := 25;
      end
  end;
  Players := APlayers;

  InitField;
end;

{ Level setup procedure }
procedure Level(WhatToDo: byte);
var
  i: byte;
begin
  case WhatToDo of
    StartOver: CurrLevel := 1;
    NextLevel: Inc(CurrLevel);
  end;

  { Initialize Snakes }
  with Snake[1] do
  begin
    Head   := 1;
    Length := 3;
    Alive  := true;
  end;
  with Snake[2] do
  begin
    Head   := 1;
    Length := 3;
    Alive  := true;
  end;

  InitField;

  { Setup level }
  case CurrLevel of
    1: begin
         with Snake[1] do
         begin
           Y := 25; X := 50;
           Direction := Right;
         end;
         with Snake[2] do
         begin
           Y := 25; X := 30;
           Direction := Left;
         end;
       end;
    2: begin
         For i := 20 to 60 do Draw(i, 25, ColorTable[3], Wall);

         with Snake[1] do
         begin
           Y := 7; X := 60;
           Direction := Left;
         end;
         with Snake[2] do
         begin
           Y := 43; X := 20;
           Direction := Right;
         end;
       end;
    3: begin
         For i := 10 to 40 do
         begin
           Draw(20, i, ColorTable[3], Wall);
           Draw(60, i, ColorTable[3], Wall);
         end;

         with Snake[1] do
         begin
           Y := 25;
           X := 50;
           Direction   := Up;
         end;
         with Snake[2] do
         begin
           Y := 25;
           X := 30;
           Direction   := Down;
         end;
       end;
    4: begin
         For i := 4 to 30 do
         begin
           Draw(20, i, ColorTable[3], Wall);
           Draw(60, 53 - i, ColorTable[3], Wall);
         end;
         For i := 2 to 40 do
         begin
           Draw(i, 38, ColorTable[3], Wall);
           Draw(81 - i, 15, ColorTable[3], Wall);
         end;

         with Snake[1] do
         begin
           Y := 7;
           X := 60;
           Direction   := Left;
         end;
         with Snake[2] do
         begin
           Y := 43;
           X := 20;
           Direction   := Right;
         end;
       end;
    5: begin
         For i := 13 to 39 do
         begin
           Draw(21, i, ColorTable[3], Wall);
           Draw(59, i, ColorTable[3], Wall);
         end;
         For i := 23 to 57 do
         begin
           Draw(i, 11, ColorTable[3], Wall);
           Draw(i, 41, ColorTable[3], Wall);
         end;

         with Snake[1] do
         begin
           Y := 25;
           X := 50;
           Direction   := Up;
         end;
         with Snake[2] do
         begin
           Y := 25;
           X := 30;
           Direction   := Down;
         end;
       end;
    6: begin
         for i := 4 to 49 do
         if (i > 30) or (i < 23) then
         begin
           Draw(10, i, ColorTable[3], Wall);
           Draw(20, i, ColorTable[3], Wall);
           Draw(30, i, ColorTable[3], Wall);
           Draw(40, i, ColorTable[3], Wall);
           Draw(50, i, ColorTable[3], Wall);
           Draw(60, i, ColorTable[3], Wall);
           Draw(70, i, ColorTable[3], Wall);
         end;

         with Snake[1] do
         begin
           Y := 7;
           X := 65;
           Direction   := Down;
         end;
         with Snake[2] do
         begin
           Y := 43;
           X := 15;
           Direction   := Up;
         end;
       end;
    7: begin
         i := 4;
         repeat
           Draw(24, i, ColorTable[3], Wall);
           Draw(55, i + 1, ColorTable[3], Wall);
           Inc(i, 2);
         until i >= 49;

         with Snake[1] do
         begin
           Y := 7;
           X := 65;
           Direction   := Down;
         end;
         with Snake[2] do
         begin
           Y := 43;
           X := 15;
           Direction   := Up;
         end;
       end;
    8: begin
         for i := 4 to 40 do
         begin
           Draw(10, i, ColorTable[3], Wall);
           Draw(20, 53 - i, ColorTable[3], Wall);
           Draw(30, i, ColorTable[3], Wall);
           Draw(40, 53 - i, ColorTable[3], Wall);
           Draw(50, i, ColorTable[3], Wall);
           Draw(60, 53 - i, ColorTable[3], Wall);
           Draw(70, i, ColorTable[3], Wall);
         end;

         with Snake[1] do
         begin
           Y := 7;
           X := 65;
           Direction   := Down;
         end;
         with Snake[2] do
         begin
           Y := 43;
           X := 15;
           Direction   := Up;
         end;
       end;
    9: begin
         for i := 6 to 47 do
         begin
           Draw(i, i, ColorTable[3], Wall);
           Draw(i + 28, i, ColorTable[3], Wall);
         end;

         with Snake[1] do
         begin
           Y := 40;
           X := 75;
           Direction   := Up;
         end;
         with Snake[2] do
         begin
           Y := 15;
           X := 5;
           Direction   := Down;
         end;
       end;
    10: begin
          For i := 13 to 39 do
          begin
            Draw(21, i, ColorTable[3], Wall);
            Draw(59, i, ColorTable[3], Wall);
          end;
          For i := 23 to 57 do
          begin
            Draw(i, 11, ColorTable[3], Wall);
            Draw(i, 41, ColorTable[3], Wall);
          end;
          For i := 3 to 10 do
          If i <> 7 then
          begin
            Draw(40, i, ColorTable[3], Wall);
            Draw(40, i + 39, ColorTable[3], Wall);
          end;
          For i := 2 to 21 do
          if i <> 11 then
          begin
            Draw(i, 26, ColorTable[3], Wall);
            Draw(i + 58, 26, ColorTable[3], Wall);
          end;
          with Snake[1] do
          begin
            Y := 25;
            X := 50;
            Direction   := Up;
          end;
          with Snake[2] do
          begin
            Y := 25;
            X := 30;
            Direction   := Down;
          end;
        end;
    else
      begin
        for i := 16 to 38 do
        begin
          Draw(10, i, ColorTable[3], Wall);
          Draw(24, i, ColorTable[3], Wall);
          Draw(38, i, ColorTable[3], Wall);
          Draw(49, i, ColorTable[3], Wall);
          Draw(58, i, ColorTable[3], Wall);
          Draw(64, i div 2 + 19, ColorTable[3], Wall);
          Draw(70, i, ColorTable[3], Wall);
        end;
        for i := 22 to 32 do
        begin
          Draw(i div 2, 16, ColorTable[3], Wall);
          Draw(i div 2, 38, ColorTable[3], Wall);
          Draw(i div 2 + 14, 38, ColorTable[3], Wall);
          Draw(i + 16, 16, ColorTable[3], Wall);
          Draw(i + 37, 38, ColorTable[3], Wall);
        end;
        for i := 40 to 47 do
          Draw(i, 31, ColorTable[3], Wall);


        with Snake[1] do
        begin
          Y := 7;
          X := 70;
          Direction   := Left;
        end;
        with Snake[2] do
        begin
          Y := 47;
          X := 10;
          Direction   := Right;
        end;
      end;
    end; { case }
end;

{ Pauses game play and waits for space bar to be pressed before continuing }
procedure SpacePause(const text: string);
var
  x, y, color: byte;
begin
  TextColor(ColorTable[5]);
  TextBackGround(ColorTable[6]);

  Center(40, 22, 'ÛßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÛ');
  Center(40, 23, 'Û                                       Û');
  Center(40, 23, text);
  Center(40, 24, 'Û                                       Û');
  Center(40, 25, 'Û            Press space key            Û');
  Center(40, 26, 'ÛÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÛ');

  { Wait for a space key }
  repeat
  until ReadKey = ' ';

  TextColor(15);
  TextBackGround(ColorTable[4]);

  { Restore the screen background }
  for y := 22 to 26 do
    for x := 20 to 60 do
    begin
      Color := Arena[X, Y].AColor;
      { Check for background }
      If Color = ColorTable[4] then
        Draw(x, y, Arena[X, Y].AColor, BackGround)
      else
        Draw(x, y, Arena[X, Y].AColor, SnakeBody)
    end;
end;

{ Writes players scores and number of lives remaining }
procedure WriteScore;
begin
  TextBackGround(0);

  { Player TWO }
  If Players = 2 then
  begin
    TextColor(ColorTable[2]);
    GotoXY(8, 2);  Write(Snake[2].Score:6);
    GotoXY(24, 2); Write(Snake[2].Lives);
  end;

  { Player ONE }
  TextColor(ColorTable[1]);
  GotoXY(65, 2); Write(Snake[1].Lives);
  GotoXY(74, 2); Write(Snake[1].Score:6);
end;

{ Checks the global arena array to see if the boolean flag is set }
function PointIsThere(X, Y: byte; Color: byte): boolean;
begin
  If Arena[X, Y].AColor <> Color then
    PointIsThere := True
  else
    PointIsThere := false;
end;

{ Main routine that controls game play }
procedure PlayGame;
var
  i: byte;
  txt: string;
  NumberPos: TArenaPoint;
  Key: word;
  Tail: integer;

begin
  { Initialize Snakes }
  for i := 1 to 2 do
  with Snake[i] do
  begin
    Lives  := 5;
    Score  := 0;
    SColor := ColorTable[i]
  end;

  Level(StartOver);

  Str(CurrLevel, txt);
  SpacePause('Level ' + txt);

  DoneGame := false;

  { Play Nibbles until finished }
  repeat
    If Players = 1 then Snake[2].Y := 0;

    Number     := 1;    { Current number that snakes are trying to run into }
    NoNumber   := TRUE; { NoNumber = TRUE if a number is not on the screen  }
    PlayerDied := FALSE;

    WriteScore;

    repeat
        { Print number if no number exists }
        if NoNumber then
        begin
          repeat
            with NumberPos do
            begin
              Y := Random(46) + 3;
              X := Random(78) + 2;
            end;
          until not PointIsThere(NumberPos.X, NumberPos.Y, ColorTable[4]);

          NoNumber := false;
          TextColor(ColorTable[7]);
          TextBackGround(ColorTable[4]);
          GotoXY(NumberPos.X, NumberPos.Y);
          Write(Number);
        end;

        { Delay game }
        Delay(Speed);

        { Get keyboard input & Change direction accordingly }
        Key := GetUpCasedKey;

        with Snake[1] do
        case Key of
          kbPlayer1Up   : If Direction <> Down  then Direction := Up;
          kbPlayer1Down : If Direction <> Up    then Direction := Down;
          kbPlayer1Left : If Direction <> Right then Direction := Left;
          kbPlayer1Right: If Direction <> Left  then Direction := Right;
        end;

        with Snake[2] do
        case Key of
          kbPlayer2Up   : If Direction <> Down  then Direction := Up;
          kbPlayer2Down : If Direction <> Up    then Direction := Down;
          kbPlayer2Left : If Direction <> Right then Direction := Left;
          kbPlayer2Right: If Direction <> Left  then Direction := Right;
          kbPause       : SpacePause('Game Paused');
          kbDone        : DoneGame := true;
        end;


        for i := 1 to Players do
        with Snake[i] do
        begin

          { Move the snakes }
          case Direction of
            Up    : Dec(Y);
            Down  : Inc(Y);
            Left  : Dec(X);
            Right : Inc(X);
          end;

          { If snake hits number, respond accordingly }
          IF (X = NumberPos.X) and
             (Y = NumberPos.Y) then
          begin
            If Length < (MaxSnakeLength - 30) then Inc(Length, Number * 4);
            Inc(Score, Number * 100 + (150 - Speed) * 2);

            WriteScore;

            Inc(Number);
            If number = 10 then
            begin
              EraseSnake(1);
              EraseSnake(2);
              GotoXY(NumberPos.X, NumberPos.Y);
              TextColor(ColorTable[4]);
              Write('Å');
              Level(NextLevel);
              WriteScore;
              Str(CurrLevel, txt);
              SpacePause('Level ' + txt);
              if Players = 1 then Snake[2].Y := 0;
              Number := 1;
            end;

            NoNumber := true;
          end;
        end;

        for i := 1 to Players do
        with Snake[i] do
        begin
          { If player runs into any point, or the head of the other snake, }
          { it dies.                                                       }
          If PointIsThere(Snake[i].X, Snake[i].Y, colorTable[4]) OR
             ((Snake[1].Y = Snake[2].Y) and
              (Snake[1].X = Snake[2].X)) then
          begin
            { Clear number }
            TextColor(ColorTable[4]);
            TextBackground(0);
            GotoXY(NumberPos.X, NumberPos.Y);
            Write('Å');
            PlayerDied := true;
            Snake[i].Alive := false;
            Dec(Snake[i].Lives);
          end else
          { Otherwise, move the snake, and erase the tail }
          with Snake[i] do
          begin
            Head := (Head + 1) mod MaxSnakeLength;
            Body[Head].X := X;
            Body[Head].Y := Y;
            Tail := (Head + MaxSnakeLength - Length) mod MaxSnakeLength;
            Draw(Body[tail].X, Body[tail].Y, ColorTable[4], BackGround);
            Body[tail].Y := 0;
            Draw(X, Y, SColor, SnakeBody);
          end;
        end;
    until PlayerDied or DoneGame;

    for i := 1 to Players do
    begin
      EraseSnake(i);
      with Snake[i] do
      { If dead, then erase snake }
      if not Alive then
      begin
        { Update score }
        WriteScore;
        If i = 1 then
          SpacePause('Player "O n e" Dies !')
        else
          SpacePause('Player "T w o" Dies !');
      end;
    end;
    Level(SameLevel);
    WriteScore;

  { Play next round, until either of snake's lives have run out. }
  until (Snake[1].Lives = 0) or (Snake[2].Lives = 0) or DoneGame;

  TextColor(ColorTable[5]);
  TextBackGround(ColorTable[6]);
  SpacePause('. . . G A M E  O V E R . . .');
  Score1 := Snake[1].Score;
  Score2 := Snake[2].Score;
end;

end.