PROGRAM LeGame;
{+
++ PROGRAM TITLE:	THE GAME
++ WRITTEN BY:		RAYMOND E. PENLEY
++
++ DATE WRITTEN:	AUGUST 7, 1981
++
++  COPYRIGHT (c) AUGUST 1981 by Raymond E. Penley
++  Permission to copy, modify and distribute, except for profit,
++  is hereby granted.
++
++ SUMMARY:
++	LeGame is a real time simulation game with a very simple
++   objective: to move into the other player thus giving the
++   mover an increased score.
++	LeGame is a simple game that moves two players around on a
++   game board.  The game is enhanced by the presence of a third
++   player, the Ghost, on the board.	The ghost player always moves
++   into a player and thereby causes that player to lose all his score.
++	The game is over in 1000 rounds or may be terminated by a
++   control-a key press.
++	The keys that control players movements are:
++
++	     PLAYER "+"  PLAYER "*"
++	     ----------  ----------
++	       Q W E	   I O P
++		\!/	    \!/
++	      A--S--D	  J--K--L
++		/!\	    /!\
++	       Z X C	   N M ,
++
++ NOTES:
++    The file 'TERMIO.PAS' contains terminal IO routines.  To use
++  TERMIO.PAS in your program, study the file TERMIO.PAS and
++  include those routines necessary into your source program.	There
++  are a couple of routines in TERMIO.PAS that may be included in
++  your Pascal source program at compile time: writes() and INITTERM.
++  Edit TERMIO.PAS and create a new file called TERMIO.LIB, then include
++  TERMIO.LIB in your Pascal program.	The Pascal/Z compiler will include
++  the source text as it compiles the main program.
++	All external modules may be found in the Pascal/Z Users' Group's
++  very useful library: ASL.REL [A Small Library].
++	The module 'gotoxy(x,y)' is included as source text in 'TERMIO.PAS'.
++
+}
CONST
  { DEFINE THE OUTER EDGE MARGINS }
  LM = 10;		{ left margin }
  RM = 70;		{ right margin }
  TM =	1;		{ top margin - remember: the top row is row 0 }
  BM = 17;		{ bottom margin }

  { DEFINE THE LIMITS OF THE PLAYING FIELD <THE GAME BOARD> }
  BLM = LM+2;		 { board left margin }
  BRM = RM-2;		 { board right margin }
  BTM = TM+2;		 { board top margin }
  BBM = BM-2;		 { board bottom margin }

  { DEFINE THE PLACEMENT FOR THE SCORE CARD }
  CardRow = BM+2;
  CardColumn = LM;
  alphalen = 10;		{<<< terminal IO routines >>>}

TYPE
  ACTION = ( NULL, EA, NE, NO, NW, WE, SW, SO, SE );
  BYTE = 0..255;
  alpha = array [0..alphalen] of byte;	{<<< terminal IO routines >>>}
  PLAYERTYPE = RECORD
		 X,		     { X-coordinates }
		 Y	  : BYTE;    { Y-coordinates }
		 CH	  : CHAR;    { Players identification }
		 STATE	  : ACTION;  { STATED ACTION }
		 SCORE	  : INTEGER;
	       END;
  STRING3 = PACKED ARRAY [1..3] OF CHAR;   { FOR TERMINAL STRINGS }
  STRING34 = PACKED ARRAY [1..34] OF CHAR;

VAR
  ASET, 		{ first set of keyboard control keys }
  BSET : SET OF CHAR;	{ second set of keyboard control keys }
  COUNT : INTEGER;	{ count number of rounds played }
  EXT : BYTE;		{ direct console character }
  FACTOR : INTEGER;	{ DELAY FACTOR }
  GAMEOVER : BOOLEAN;
  INCHAR : CHAR;	{ global input character }
  LASTMOVE : INTEGER;
  mover : byte; 	{ players turn to move }
  PLAYER_ONE,
  PLAYER_TWO,
  GHOST       : PLAYERTYPE;
  SEED : REAL;		{ for random numbers }

{$iTERMIO.VAR <<<terminal specific variables>>>}



FUNCTION CONCHAR: BYTE; EXTERNAL;
{ RETURNS A CONSOLE CHARACTER DIRECTLY WITH NO ECHO }


PROCEDURE DREAD( VAR CH: CHAR );
{ BY USING CONCHAR WE CREATE A READ ROUTINE THAT WAITS
  FOR A SINGLE KEYBOARD INPUT }
 VAR	 EXT: BYTE;
BEGIN
  REPEAT
    EXT := CONCHAR;
  UNTIL EXT<>0;
  CH := CHR( EXT );
END{ of DREAD };


FUNCTION TOUPPER(CH: CHAR): CHAR; EXTERNAL;
{ RETURNS THE CHARACTER IN UPPERCASE }


{$iTERMIO.LIB <<<procedures writes() and initterm>>>}


FUNCTION RANDOM( VAR SEED: REAL ): REAL;
{ RETURNS RANDOM NUMBERS IN RANGE 0 - 1 }
{ GLOBAL:
   SEED: REAL;
}
CONST PI = 3.14159;
VAR	X: REAL;
BEGIN
  X := SEED + PI;
  X := EXP(5.0 * LN(X));
  SEED := X - TRUNC(X);
  RANDOM := SEED
END{ of RANDOM };


PROCEDURE ERASE( VAR PLAYER: PLAYERTYPE );
BEGIN
  GOTOXY( PLAYER.X, PLAYER.Y );
  WRITE(' ')
END{ of ERASE };


FUNCTION DIRECTION( A: INTEGER ): ACTION;
{  RETURNS AN ACTION FROM A NUMERIC DIRECTION
   ACCORDING TO THE FOLLOWING CONVENTION:
	  NO=3
     NW=4 \ ! /NE=2
	   \!/
   WEST=5<--+-->EAST=1
	   /!\
    SW=6  / ! \ SE=8
	 SOUTH=7
}
BEGIN
  CASE A OF
    1: DIRECTION := EA;
    2: DIRECTION := NE;
    3: DIRECTION := NO;
    4: DIRECTION := NW;
    5: DIRECTION := WE;
    6: DIRECTION := SW;
    7: DIRECTION := SO;
    8: DIRECTION := SE
  END
END{ of Direction };


PROCEDURE Wappo( VAR PLAYER: PLAYERTYPE );
{ CHANGES PLAYERS DIRECTION AND COORDINATES }
BEGIN
  WITH PLAYER DO BEGIN
    ERASE( PLAYER );
    STATE := DIRECTION( TRUNC(8.0*RANDOM(SEED))+1 );
    { ESTABLISH NEW COORDINATES }
    X := TRUNC(BM*RANDOM(SEED)) + TM;
    Y := TRUNC(RM*RANDOM(SEED)) + LM
  END
END{ of Wappo };


Procedure ScoreCard;
const	sp = '    ';

   procedure sc_a;
   begin
     writes(INVON);write(sp);writes(INVOFF);
   end{ of sc_a };

begin
  { write player two's score first }
  gotoxy( (CardColumn+35),(CardRow+2) );
  sc_a; write( PLAYER_TWO.SCORE:5, '000' ); sc_a;

  { now write score for player one leaving cursor in center of screen }
  gotoxy( (CardColumn+5),(CardRow+2) );
  sc_a; write( PLAYER_ONE.SCORE:5, '000' ); sc_a;

  gotoxy(40,(TM+5)); { pull cursor up out of the way }
end{ of ScoreCard };


PROCEDURE InitScoreBoard;
const	blanks = '                  ';
var	CC1, CC2 : byte;

   procedure init_b( x,y: byte );
   begin
     gotoxy( x,y );
     writes(invon);write(blanks);writes(invoff);
   end{ of init_b };

BEGIN
  CC1 := CardColumn + 5;
  CC2 := CardColumn + 35;
  init_b( CC1, CardRow );
  init_b( CC2, CardRow );

  gotoxy( CC1,(CardRow+1) );
  writes(invon);write('    PLAYER (+)    ');writes(invoff);

  gotoxy( CC2,(CardRow+1) );
  writes(invon);write('    PLAYER (*)    ');writes(invoff);

  ScoreCard;
  init_b( CC1,(CardRow+3) );
  init_b( CC2,(CardRow+3) );
END{ of InitScoreBoard };


PROCEDURE GENSCORE( VAR PLAYER: PLAYERTYPE );

   function hit( var a,b: playertype ): boolean;
   { RETURNS TRUE IF BOTH PLAYERS HAVE THE SAME COORDINATES }
   begin
     hit := ((a.x=b.x) and (a.y=b.y))
   end;

BEGIN
  {  ARE ANY SQUARES OCCUPIED BY TWO PLAYERS	}
  IF HIT( PLAYER_ONE, PLAYER_TWO ) THEN BEGIN
     { GIVE PLAYER ON THE MOVE A BONUS SCORE }
     PLAYER.SCORE := PLAYER.SCORE + 500;
     Wappo( PLAYER_ONE );
     Wappo( PLAYER_TWO );
     ScoreCard;
  END
  ELSE BEGIN  { ARE ANY SQUARES OCCUPIED BY THE GHOST AND PLAYER 1 OR }
    { THE GHOST AND PLAYER 2 }
    IF HIT( GHOST, PLAYER_ONE ) THEN BEGIN
      PLAYER_ONE.SCORE := 0;
      Wappo( PLAYER_ONE );
      wappo( ghost );
      ScoreCard
    END
    ELSE IF HIT( GHOST, PLAYER_TWO ) THEN BEGIN
      PLAYER_TWO.SCORE := 0;
      Wappo( PLAYER_TWO );
      wappo( ghost );
      ScoreCard
    END
    ELSE
      PLAYER.SCORE := PLAYER.SCORE + 1
  END{ELSE}
END{ of GenScore };


FUNCTION GENSTATE( CH: CHAR ): ACTION;
{ GENERATES A NEW STATE DEPENDING UPON THE CHARACTER PASSED }
{ USING THE FOLLOWING CONVENTION:
    PLAYER 1	PLAYER 2
     Q W E	 I O P
      \!/	  \!/
    A--S--D	J--K--L
      /!\	  /!\
     Z X C	 N M ,
}
BEGIN
  CASE TOUPPER(CH) OF
   'S','K':	GENSTATE := NULL;
   'D','L':	GENSTATE := EA;
   'E','P':	GENSTATE := NE;
   'W','O':	GENSTATE := NO;
   'Q','I':	GENSTATE := NW;
   'A','J':	GENSTATE := WE;
   'Z','N':	GENSTATE := SW;
   'X','M':	GENSTATE := SO;
   'C',',','<': GENSTATE := SE
  END
END{ of GENSTATE };


PROCEDURE GenMove( VAR PLAYER: PLAYERTYPE; ext: byte );
var	ch: char;

   PROCEDURE DELAY( FACTOR: INTEGER);
   VAR IX : INTEGER;
   BEGIN
     FOR IX:=1 TO FACTOR DO {DELAY}
   END;

BEGIN
  { if new character entered from keyboard then generate }
  { a new direction for the player concerned }
  if ( ext<>0 ) then begin
     ch := chr(ext);
     IF ( CH IN ASET ) THEN
	PLAYER_ONE.STATE := GENSTATE(CH)
     ELSE IF ( CH IN BSET ) THEN
	PLAYER_TWO.STATE := GENSTATE(CH)
  end;

  WITH PLAYER DO BEGIN
    ERASE( PLAYER );

    CASE STATE OF
      NULL: {HOLD PRESENT POSITION};
	EA:  X := X + 1;
	NE:  BEGIN X := X + 1; Y := Y - 1 END;
	NO:  Y := Y - 1;
	NW:  BEGIN X := X - 1; Y := Y - 1 END;
	WE:  X := X - 1;
	SW:  BEGIN X := X - 1; Y := Y + 1 END;
	SO:  Y := Y + 1;
	SE:  BEGIN X := X + 1; Y := Y + 1 END
    END{CASE};

    { CHECK IF WE ARE MOVING OFF THE SCREEN }
    IF ( Y>BBM ) THEN
       Y := BTM
    ELSE IF ( Y<BTM ) THEN
       Y := BBM;
    IF X>BRM THEN
       X := BLM
    ELSE IF X<BLM THEN
       X := BRM;

    GOTOXY(X,Y); WRITE( CH )
  END{WITH};

  GENSCORE( PLAYER );
  DELAY(FACTOR)
END{ of GenMove };


PROCEDURE SIGN( TXT: STRING34 );
CONST
  border = '**********************************';
begin
  GOTOXY(25,7); { row=7 }
  writes(invon);write(border);writes(invoff);
  GOTOXY(25,8);
  writes(invon);write(txt);writes(invoff);
  GOTOXY(25,9);
  writes(invon);write(border);writes(invoff);
end{ of SIGN };


PROCEDURE INITIALIZE;
TYPE	MSTRING = STRING 255;
VAR	IX: BYTE;

   PROCEDURE HALT( TXT: MSTRING ); EXTERNAL;

BEGIN
  { INITIALIZE TERMINAL SPECIFIC VARIABLES }
  IF NOT INITTERM THEN
     HALT('File "TERMIO.FIL not found. Run INSTALL.');
  COUNT := 0;		{ ROUNDS COUNTER }
  SEED := 4.0;		{ THIS ISN'T TRULY RANDOM! }

  { init the first set of keyboard control keys }
  ASET := ['q','Q','w','W','e','E',
	   'a','A','s','S','d','D',
	   'z','Z','x','X','c','C'];

  { init the second set of keyboard control keys }
  BSET :=  ['i','I','o','O','p','P',
	    'j','J','k','K','l','L',
	    'n','N','m','M',',','<'];

  { clear the terminal screen and signon }
  writes( CLRSCR );
  SIGN( '***      T H E  G A M E        ***' );
  WRITELN;WRITELN;WRITELN;

  { SET UP DELAY FACTOR }
  WRITELN(' ':12, '1 - BEGINNING GAME');
  WRITELN(' ':12, '2 - ADVANCED GAME');
  WRITELN(' ':12, '3 - MASTER CRAFTSMAN');
  WRITELN;
  WRITE(' ':12, 'SELECT ->');
  DREAD(inchar);
  LASTMOVE := 5000;
  case inchar of
    '1': begin FACTOR := 500; lastmove := 1000 end;
    '2': FACTOR := 250;
    '3': FACTOR := 1;
   ELSE: FACTOR := 50
  end;

  writes( CLRSCR );
  writes( CRSOFF ); { TURN CURSOR DISPLAY OFF ON TERMINALS THAT CAN DO SO. }

  { PLACE A BOARDER AROUND THE PLAYING FIELD }
  for ix:=LM to RM do begin { top and bottom borders }
      gotoxy(ix,TM); write('=');
      gotoxy(ix,BM); write('=')
    end;
  for ix:=TM to BM do begin { left and right borders }
    gotoxy(LM,ix); write('=');
    gotoxy(RM,ix);write ('=')
  end;


  { INITIALIZE PLAYERS AND GHOST }
  WITH GHOST DO BEGIN
    CH := 'C';
    SCORE := 0
  END;
  WITH PLAYER_ONE DO BEGIN
    CH := '+';
    SCORE := 0
  END;
  WITH PLAYER_TWO DO BEGIN
    CH := '*';
    SCORE := 0
  END;

  InitScoreBoard;

  { PLACE THE BEGINNING MOVES }
  Wappo( GHOST );	{ FIRST - PICK RANDOM POINTS FOR PLACEMENT }
  Wappo( PLAYER_ONE );
  Wappo( PLAYER_TWO );

  GenMove( PLAYER_ONE, 0 );
  GenMove( PLAYER_TWO, 0 );
  GenMove( GHOST, 0 )
END{ of Initialize };



BEGIN{ MAIN PROGRAM }
  INITIALIZE;
  GAMEOVER := FALSE;
  mover := 1;
  ext := 0; { preload ext to no character input }
  WHILE not gameover do begin
    if ext=1 then begin
       gameover := true
    end
    else begin
      case mover of
	  1: GenMove( player_one, ext );
	2,4: GenMove( ghost, ext );
	  3: GenMove( player_two, ext )
      end;
      mover := mover + 1;
      if mover>4 then mover := 1;
      COUNT := COUNT + 1;
      gameover := ( count>lastmove );
      if ( count mod 6=0 ) then { TRY A NEW DIRECTION FOR THE GHOST }
	 GHOST.STATE := DIRECTION( TRUNC(8.0*RANDOM(SEED))+1 );
      { keep reading the console }
      ext := conchar
    end {else}
  END{WHILE};

  SIGN( '***      G A M E  O V E R      ***' );

  GOTOXY(0,0);
  writes( CRSON )    { TURN CURSOR BACK ON }
END{ of Program LeGame }.

