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:
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