Thursday, September 15, 2016

CP/M : Tetris Clone in Turbo Pascal 3.0.

CP/M being an 8-bit era MS-DOS equivalent, a platform for professional/business software had the advantage over most home computer platforms of that time, which were usually equipped and stuck with BASIC as their main programming language and operating system. CP/M was a “grown-up” OS, which defined distinct abstraction layers that separated user from hardware and was not oriented toward any particular programming language or hardware platform/architecture.
The abstraction layers consisted of Basic I/O System (BIOS), Basic Disk O.S. (BDOS) and Console Command Processor (CCP). The complexity of the system was mostly implemented in BDOS and in CCP layers, therefore it was enough to rewrite some BIOS routines to adapt the code to any particular hardware platform.
There were versions of CP/M later developed for different more advanced 16-bit processors (CP/M-86 for Intel 8086, CP/M-68k for Motorola 68000 or CP/M-8000 for Zilog Z8000), however CP/M originally targeted and was written for an Intel 8080/85/Z-80 microprocessor family (CP/M-80), therefore its native language was Intel 8080 assembly language. Programs therefore could be ported fairly well between different computer platforms in their original binary form under assumption that they were compiled to base Intel 8080 machine language and were not using any platform specific hardware features and were confined to 64 kB of RAM and ASCII mode. Assembly language is technically speaking not a programming language but merely a notation system that makes writing programs in machine code a bit easier. Also, the original version of CP/M wasn't even written in assembly, but rather in a higher level language called PL/M invented by Gary Kildall (May 19, 1942 – July 11, 1994) – who was also the original author of CP/M. So lets stick with that.

There were many professional development packages available for CP/M, including various assemblers, macro assemblers, BASIC, C, Pascal, FORTH, Modula-2, Fortran, Cobol, Lisp and variety of other obscure and less known languages. The package that stands out among these programming development tools is Borland's Turbo Pascal 3.0, which was released for CP/M in 1986.

Pros: Low price, small size and great speed of compilation (single pass compiler).

TP 3.0 supported overlays, which allowed programmer to go beyond the 64 kB memory limit.
Very good standard library was included and Turbo Pascal was very closely compatible with standard Pascal language. Included with product reference manual was very well written.

Pascal language is well structured and there is strong type control – that's its nature. The Pascal program starts with an optional heading section, which if present gives the program a name and optionally lists the parameters interfacing program to the environment.

Examples:

program Complex;
program Editor (Input, Output);

After that comes the declaration part which consists of 5 sections:

  • labels declaration
  • constants definition
  • types definition
  • variables declaration
  • procedures and functions declaration

Pascal defines that each of above sections is optional and may occur only once in exact order specified above. Turbo Pascal however allows any order and any number of occurrences of above sections.

You may find more information in this article on the net.

Commodore 128 is my favorite 8-bit era machine, CP/M my favorite OS of that time and C-128 can run CP/M, so it comes natural that I wanted to start a programming project on C-128 under CP/M in TP 3.0.

I have never created any arcade computer game. I was not active in game development in general and except some simple games like Tic-Tac-Toe or some Text Adventure my achievements in this are are quite poor if not non-existent. Great place to start learning and from the best that is – I decided to create my own Tetris clone for CP/M.

Those who know CP/M environment know its limitations. One is that the system doesn't provide any GUI. It is text based, 80 column standard. For a CP/M program to be portable across different hardware platforms, it should stick with that format.
But that's OK. It is possible to create a Tetris clone in text only. After all classic Tetris is made out of puzzles that are made out of square blocks. We can use alphanumeric characters or semi-graphics characters (like reverse-mode space) to build any shapes we need.

Classic Tetris block or a puzzle is made out of square blocks. The longest one is made out of 4 blocks in one dimension, so I decided the base for all my shapes will be a 4 x 4 matrix (like a table):




In above matrix I will place the squares to form different shapes.
Also, each of these shapes can be rotated, so we need an array of 4 of such matrices to create full sequence for shape animation.

E.g.:

1)



2) 


3) 


4) 



The type declaration of my program may start like this:

type
   Shape = array[1..4,1..4] of Char;
   Block = array[1..4] of Shape;
   All = array[1..6] of Block;
   PtrBlkInfo = ^BlkInfo;
   BlkInfo = record  { keep info about the block on the scene }
               PrevRow,PrevCol: Integer; {previous coord. (for repainting)}
               Row,Col:      Integer; {block coordinates}
               Repaint:      Boolean; {flag if block needs repainting}
               ShNum,SeqNum: Integer; {shape and sequence numbers}
               Next:         PtrBlkInfo; {pointer to next block}
             end;

Shape holds a single animation frame, Block holds all 4 variants of the Shape (rotated 0, 90, 180 and 270 degrees) and All holds all 6 Tetris blocks/shapes with their rotated variants.

PtrBlkInfo is a pointer to the BlkInfo record which in turn is a data structure defining exact position of the block on the screen, its previous coordinates (for re-painting purpose), flag Repaint that indicates if the shape was rotated or moved, the shape and sequence number and the pointer to the next block. Thus we defined the means to manage the Tetris puzzles and all that may be added to the game scene.

At the beginning of our program we may want to initialize all of these data structures with a procedure similar to this:

{
   -----------------------------------------------------------
   Init Shape Array.
   v - character to fill with.
   -----------------------------------------------------------
}
procedure InitShape(v: Char);
var
   i,j: Integer;
begin
   for i := 1 to 4 do
   begin
      for j:= 1 to 4 do
      begin
         sh[i,j] := v;
      end;
   end;
end;

{
   -----------------------------------------------------------
   Initialize All Shapes.
   -----------------------------------------------------------
}
procedure InitAllShapes;
var
   i,j: Integer;
begin
   {BlockSymb := Chr(178);}
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[1,3] := BlockSymb;
   sh[1,4] := BlockSymb;
   bl[1] := sh;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[2,1] := BlockSymb;
   sh[3,1] := BlockSymb;
   sh[4,1] := BlockSymb;
   bl[2] := sh;
   bl[3] := bl[1];
   bl[4] := bl[2];
   Pieces[1] := bl;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[2,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   bl[1] := sh;
   bl[2] := sh;
   bl[3] := sh;
   bl[4] := sh;
   Pieces[2] := bl;
   InitShape(EmptySymb);
   sh[2,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[3,2] := BlockSymb;
   bl[1] := sh;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[1,3] := BlockSymb;
   bl[2] := sh;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[2,1] := BlockSymb;
   sh[3,1] := BlockSymb;
   sh[2,2] := BlockSymb;
   bl[3] := sh;
   InitShape(EmptySymb);
   sh[2,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[2,3] := BlockSymb;
   bl[4] := sh;
   Pieces[3] := bl;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[2,1] := BlockSymb;
   sh[3,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   bl[1] := sh;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[2,1] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[2,3] := BlockSymb;
   bl[2] := sh;
   InitShape(EmptySymb);
   sh[3,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[3,2] := BlockSymb;
   bl[3] := sh;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[1,3] := BlockSymb;
   sh[2,3] := BlockSymb;
   bl[4] := sh;
   Pieces[4] := bl;
   InitShape(EmptySymb);
   sh[2,1] := BlockSymb;
   sh[3,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   bl[1] := sh;
   bl[3] := sh;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[2,3] := BlockSymb;
   bl[2] := sh;
   bl[4] := sh;
   Pieces[5] := bl;
   InitShape(EmptySymb);
   sh[1,1] := BlockSymb;
   sh[2,1] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[3,2] := BlockSymb;
   bl[1] := sh;
   bl[3] := sh;
   InitShape(EmptySymb);
   sh[2,1] := BlockSymb;
   sh[1,2] := BlockSymb;
   sh[2,2] := BlockSymb;
   sh[1,3] := BlockSymb;
   bl[2] := sh;
   bl[4] := sh;
   Pieces[6] := bl;
end;

BlockSymb and EmptySymb are defined in constants section of the program:

const
   ScWidth   = 16; {scene width}
   ScHeight  = 20; {scene height}
   ScRow     = 1;  {scene left-upper corner coordinate (Y or Row)}
   ScCol     = 1;  {scene left-upper corner coordinate (X or Col)}
   FallSpeed = 10;
   RefrRate  = 100;
   BoxVert   = 'I';
   BoxHoriz  = '-';
   BlockSymb = '#';
   EmptySymb = ' ';

Now we are ready to write our first test code, not yet a game, but we need to see if all our laboriously created blocks look good and if their rotated sequences are in order.
First we need to create a main game loop. The main game loop task is to take user input, perform game logic computations and update the scene and refresh screen.

The procedure may look like this, but feel free to design more advanced algorithm:

{ --------------------- MAIN PROGRAM ---------------- }

begin
   InitGame;
   while not GameEnd do
   begin
      UpdScene;
      GetInput;
      Delay(RefrRate);
   end;
   ClrScr;
   writeln('Thank you for playing mktetris!');
end.

InitGame is where we can draw the static elements of the game screen/scene, initialize the game variables, blocks positions, score etc.

procedure InitGame;
var xx,x1,x2,x3,x4,x5 : Integer;
begin
   InitAllShapes;
   Key := 'r';
   ValidKey := False;
   ClrScr;
   DrawBox(ScCol, ScRow, ScWidth, ScHeight);
   { Show all pieces - this will be removed in final version }
   xx := ScWidth; x1 := 3; x2 := 8; x3 := 13; x4 := 18; x5 := 23;
   DispBlock(xx+x1, 1, 1, 1, False);
   DispBlock(xx+x1, 5, 1, 2, False);
   DispBlock(xx+x1, 9, 2, 1, False);
   DispBlock(xx+x1, 13, 3, 1, False);
   DispBlock(xx+x2, 13, 3, 2, False);
   DispBlock(xx+x3, 13, 3, 3, False);
   DispBlock(xx+x4, 13, 3, 4, False);
   DispBlock(xx+x2, 1, 4, 1, False);
   DispBlock(xx+x3, 1, 4, 2, False);
   DispBlock(xx+x4, 1, 4, 3, False);
   DispBlock(xx+x5, 1, 4, 4, False);
   DispBlock(xx+x2, 5, 5, 1, False);
   DispBlock(xx+x3, 5, 5, 2, False);
   DispBlock(xx+x4, 5, 5, 3, False);
   DispBlock(xx+x5, 5, 5, 4, False);
   DispBlock(xx+x2, 9, 6, 1, False);
   DispBlock(xx+x3, 9, 6, 2, False);
   DispBlock(xx+x4, 9, 6, 3, False);
   DispBlock(xx+x5, 9, 6, 4, False);
   GotoXY(40, 18); write(':/; - move left/right');
   GotoXY(40, 19); write(',/. - rotate left/right');
   GotoXY(40, 20); write('  @ - start over');
   { Add 1st piece to the scene. }
   New(NewBlk);
   FrstBlk := NewBlk;
   CurrBlk := FrstBlk;
   if FrstBlk <> nil then
   begin
      with FrstBlk^ do
      begin
        Col := ScCol + 1;
        Row := ScRow;
        PrevCol := Col;
        PrevRow := Row;
        Repaint := True;
        ShNum := 4;
        SeqNum := 1;
        Next := nil;
      end;
   end;
end;

UpdScene is a procedure where we move/paint the pieces of the game depending on recent user input and game logic.

procedure UpdScene;
var
  Blk:        PtrBlkInfo;
  PrevSeqNum: Integer;
begin
  Blk := FrstBlk;
  PrevSeqNum := -1;
  while Blk <> nil do
  with Blk^ do
  begin
    if Repaint = True then
    begin
      DispBlock(PrevCol,PrevRow,ShNum,SeqNum,True);
      DispBlock(Col,Row,ShNum,SeqNum,False);
      PrevCol := Col;
      PrevRow := Row;
      Repaint := False;
    end;
    Blk := Next;
  end;
  { perform block rotation }
  if CurrBlk <> nil then
  with CurrBlk^ do
  begin

    if ValidKey and (Key = '.') then
    begin
      PrevSeqNum := SeqNum;
      SeqNum := SeqNum + 1;
      if SeqNum > 4 then SeqNum := 1;
    end;
    if ValidKey and (Key = ',') then
    begin
      PrevSeqNum := SeqNum;
      SeqNum := SeqNum - 1;
      if SeqNum < 1 then SeqNum := 4;
    end;

    if PrevSeqNum > 0 then
    begin
      DispBlock(Col,Row,ShNum,PrevSeqNum,True);
      DispBlock(Col,Row,ShNum,SeqNum,False);
    end;
  end;
  { perform block falling down and movement }
  if CurrBlk <> nil then
  with CurrBlk^ do
  begin
    if ValidKey and (Key = ';') then
    begin
      if Col + 4 < ScCol + 1 + ScWidth then
      begin
        Col := Col + 1;
        Repaint := True;
      end;
    end;
    if ValidKey and (Key = ':') then
    begin
      if Col > ScCol + 1 then
      begin
        Col := Col - 1;
        Repaint := True;
      end;
    end;
    if Row < ScHeight+ScRow-3 then
    begin
      Row := Row + 1;
      Repaint := True;
    end;
    if ValidKey and (Key = '@') then
    begin
      Row := 1;
      Repaint := True;
    end;
  end;
end;

DispBlock is a procedure responsible for displaying/erasing the individual block on the screen.

{
   -----------------------------------------------------------
   Display or erase block.
   x, y - screen coordinates (x: 1..40, y: 1..24)
   sn - shape # (1..5)
   rn - sequence/rotation # (1..4)
   era - True: erase block/False: paint block.
   -----------------------------------------------------------
}
procedure DispBlock(x,y,sn,rn: Integer; era: Boolean);
var
   i,j: Integer;
begin
   bl := Pieces[sn];
   sh := bl[rn];
   for i := 1 to 4 do
   begin
      for j := 1 to 4 do
      begin
         GotoXY(x*2+i*2-4,y+j-1);
         if sh[i,j] = BlockSymb then
         begin
            if era = True then
              write('  ')
            else
            begin
              write(Chr(27));
              write('G4  ');
              write(Chr(27));
              write('G0');
            end;
         end;
      end;
   end;
end;

The reverse color space character is used to paint a single square. Also, please note that I paint 2 characters (2 columns) per single row to bring the aspect ratio of the single square closer to 50/50, since the 80-column display has the letters much narrower than their height, thus appearing irregular and it creates undesirable effect, especially when rotating the block.

So, that's the beginning of a fun retro-programming project and I hope I will have time to continue and improve upon it and find some following audience that may possibly learn from this experience of mine.

Source code available on github.

Thanks for reading.

Marek K
9/15/2016