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



Sunday, September 21, 2014

Commodore 128 BASIC 7.0 - graphics programming.

     I have never owned Commodore 128 computer in my youth. I recently purchased two C-128-s from e-bay due to my fascination with CP/M operating system. However there is plenty more to do with this versatile machine than just CP/M or playing games. Compared to its famous predecessor, model 64, it is in many aspects a better microcomputer. Not only it is 100% compatible with C64, making it possible to use all that huge code base created for C-64. It incorporates also a Z-80 microprocessor, making it compatible with CP/M - a huge business oriented software library exists for that operating system. And of course C-128 offers its own improved C-128 native mode with bigger memory (128 KB, banked) and better memory management thanks to the MMU on chip and new graphic facilities, like 80-column text mode (also very useful in CP/M which by default runs in 80-column mode). Last weekend I explored its built in BASIC. Anyone who owned C-64 and made first steps in programming using built in BASIC on that machine knows full well how inadequate that dialect was. Based on Microsoft's version 2, there were no machine specific keywords to take advantage of the built in hardware. All that had to be done with peek'n'poke trickery. No structural constructs available as well. Shortly speaking - a mess and it wasn't easy to create a structured readable program. The program also had to take more space in memory and was slower due to all that extra peek and pokes programmer had to use to generate sounds or create graphics on the screen. There were few options available, all at the cost - switching to machine language (difficult and long development cycle), using extended BASIC dialects (at the cost of memory available for code and variables, sometimes speed) or using different than BASIC languages (also at the cost of memory and/or speed).
Brilliant engineers at Commodore tried to address most of these issues in this next model (and as it turned out later, the last 8-bit computer by Commodore). BASIC 7.0 is much improved comparing to version 2.0 seen in C-64. It offers keywords that take advantage of the built in graphic and sound hardware as well as memory management (bank selection with BANK command) without the need of remembering the addresses of all the involved hardware registers. The new BASIC also adds some structural constructs like multiple line IF/THEN/BEGIN/BEND decision branches, open-ended (with optional condition) flow/loop control constructs like DO/WHILE/LOOP, DO/LOOP/WHILE, DO/UNTIL/LOOP, DO/LOOP/UNTIL in addition to standard BASIC's FOR/NEXT loop. C-128 offers so much more than C-64 that in fact I put it on my permanent retro-computer desk and replaced C-64 that was there before. Now I have 3-in-1 system (C-64, CP/M and C-128) taking up only slightly more space than C-64.

To do a little BASIC 7.0 exercise, I decided to write a simple analog clock application that would use the basic graphic capabilities of the machine. I will add sound/alarm abilities to it later, it should be as easy as graphics part with all the extensions that BASIC 7.0 provides.

To switch C-128 to high resolution mode (or a bitmap mode), BASIC command GRAPHIC is used.

Formats: GRAPHIC mode, [clear], [split]
                 GRAPHICS CLR

Where mode can be one of:

0 - 40 columns (composite or monochrome monitor, tv set),
1 - hi-res graphics (composite or monochrome monitor, tv set),
2 - hi-res graphics (split screen, the same type of output like 0 or 1),
3 - multicolor graphics (the same output as above),
4 - multicolor graphics (split screen, the same output as above),
5 - 80 columns (RGB or monochrome monitor).

I did not mention before that C-128 features 2 separate video output ports, the composite/TV and RGB. Composite/TV output is of the same format as C-64 video output, that provides 40 column text and high-resolution (320x200) graphics mode as well as other graphics modes (multicolor). Technically it is possible for C-128 to work with two monitors at the same time, one RGB and one composite (or a TV set). This is the setup that I have. User can freely switch between these modes in immediate mode and in program mode where application can output text to 80-column (RGB) monitor while drawing graphics on the composite monitor or alternate text output to both monitors by switching the modes with GRAPHIC command. I think this is pretty cool for a 8-bit 30 years old computer to be able to do that.
The "GRAPHICS CLR" command is NOT a screen clearing command. It clears and de-allocates memory used for graphics operations reclaiming it back for BASIC. To clear any given graphics mode, use flag clear in the GRAPHIC command or command SCNCLR.
You may want to use "GRAPHIC CLR" if you run out of memory for BASIC and you're no longer using hi-res graphics.
Flag split lets user have graphics and text on the same screen, thus allowing to present text output of the application in the more convenient text mode while having hi-res picture on top of it as well as allowing the standard text input from user (INPUT command). The number used in split tells BASIC interpreter at which line number the split screen starts. The text can also be mixed with graphics on the hi-res screen, but BASIC command PRINT can not be used for that - CHAR command is the graphics command designed for that purpose.

To draw a circle in one of the bitmap modes, BASIC command CIRCLE can be used:

Format: CIRCLE color_source, x_center, y_center, x_radius, y_radius, [start_angle], [end_angle], [rotation], [segment]

Computer must be in proper graphics mode to be able to draw a circle.

The color source is a number from 0 to 3, as in the COLOR command:

0 - 40-column (composite) background,
1 - 40-column foreground,
2 - multicolor 1,
3 - multicolor 2.

The COLOR command stores a color into the color source register.

Format: COLOR color_source, color_number

Please refer to the C-128 system guide for the colors and their assigned numbers.

BASIC command DRAW puts a line on the hi-res screen.

Formats: DRAW color_source, x1, y1 [TO x2, y2]
                 DRAW TO x, y

DRAW is used to draw points and lines in the hi-res and multicolor modes. It can draw a line from one specified location (x1, y1) to another (x2, y2) or from the current graphic cursor position to the specified coordinates (DRAW TO). If the 1st format is used but the 2nd set of coordinates is ommited (TO x2, y2), a point is turned on. The color_source as in COLOR and CIRCLE commands tells BASIC which color to use for a point/line. This can also be used to remove existing points or lines, when we draw with background as a color source (color_source=0). You can also specify relative coordinates by supplying a distance and an angle. This method of locating a point requires to use semicolon in the command.

Example: GRAPHIC 1:LOCATE 50,50:DRAW 1,25;90

will plot a point at coordinates 75,50.

Example: DRAW 0,160,100 to 50;hr%*(360/12)+(mi%/15)*(360/48)

will plot a line from point at coordinates 160,100, length=50 at the angle calculated from hr% and mi% variables (this particular example shows how to draw a small hand of the analog clock).

Without further ado, let me present the BASIC code that will display graphical (very simplified) analog clock image that is updated every minute (except seconds which are drawn more often) based on the system time TI$. I used petcat program (part of the WinVICE Commodore emulator software) to convert native BASIC code (PRG) to the text format.

petcat -70 -o analogclock.txt  -- analogclock.prg


10 rem analog clock v1.0
20 rem (c) by marek karcz 2014
30 rem all rights reserved
40 rem
50 rem variables
60 hr%=0:mi%=0:se%=0:pr%=0:hm%=0
70 hr$="":mi$="":se$="":hm$=""
80 input "set time (hhmmss) ";tm$
90 ti$=tm$
900 goto 10000
1000 rem initgr
1005 scnclr
1010 graphic 1,1:width 2
1020 color 0,1:color 1,6:color 5,6
1030 circle 1,160,100,110,90
1040 return
1050 rem draw short hand
1060 draw 1,160,100 to 50;hr%*(360/12)+(mi%/15)*(360/48)
1070 return
1080 rem draw long hand
1090 draw 1,160,100 to 90;mi%*(360/60)
1110 return
1130 rem eraseshorthand
1140 locate 160,100
1150 draw 0,160,100 to 50;hr%*(360/12)+(mi%/15)*(360/48)
1160 return
1170 rem eraselonghand
1180 draw 0,160,100 to 90;mi%*(360/60)
1190 return
1200 rem gethr
1210 hr$=mid$(ti$,1,2)
1220 hr%=val(hr$)
1225 if hr%>11 then hr%=hr%-12
1230 return
1240 rem getmi
1250 mi$=mid$(ti$,3,2)
1260 mi%=val(mi$)
1270 return
1280 rem getsec
1290 se$=mid$(ti$,5,2)
1300 se%=val(se$)
1310 return
1320 rem draw seconds
1330 char 1,0,24,se$,1
1340 return
1350 rem gethrmi
1360 hm$=mid$(ti$,1,4)
1370 hm%=val(hm$)
1380 return
10000 rem main
10010 gosub 1000
10015 gosub 1200:gosub 1240:gosub 1280:rem get hr%,mi%,se%
10017 rem gosub 1050:gosub 1090:rem draw hands
10020 do
10025 gosub 1350:get hrmi
10030 if pr%<>hm% then begin:
10040 gosub 1130:gosub 1170:rem erase hands
10050 gosub 1200:gosub 1240:rem get hr%,mi%
10060 gosub 1050:gosub 1090:rem draw hands
10070 bend:
10080 gosub 1280:rem get se%
10090 gosub 1320:rem draw seconds
10100 pr%=hm%
10110 loop:rem loop forever
20000 end

Thanks to support for graphics modes in BASIC, code is short and clear.
Because image speaks 1,000 words, here are some pictures of this code in action:









Thank you for your time.

Literature:

COMPUTE!'s 128 Programmer's Guide, ISBN 0-87455-031-9.

Marek Karcz
9/21/2014

Saturday, March 22, 2014

Maintaining your ancient CBM hardware.

You will not believe (or perhaps most of you seasoned Commodore equipment users will) how much time in troubleshooting costed me putting an old diskette from e-bay in my Commodore disk drives.

It all started when my GEOS-128 set arrived from e-bay. Two manuals, 6 diskettes, all visually in a very good shape. The smell of these items suggested they possibly spent long time at the bottom of some closet. No indication of dampness. The pages in one of the books were only yellowed a little bit. The diskettes looked clean (perhaps except one, which only you could notice by looking at the surface at certain angle that there is some residue on it, which of course I noticed after all my troubles).
So, I opened the GEOS manual on the page with instruction how to boot the system for the first time, I put the system diskette in my primary drive (I use three 1571 drives, first one with Jiffy DOS installed and a uIEC-SD device, this setup works very nice with CP/M) and booted it. The 1st disturbing thing to notice was a whining sound from the drive. The disk did not boot of course. I made a few extra attempts with the same result.
"OK" - I thought. - "This is just an old disk, I will try to read its directory."
This of course did not work, but perhaps because it is a GEOS diskette?
Anyway, I tried in my other drives. Then I put some of the other diskettes that arrived with the set in all of my drives and tried to read them. None has worked.
This is kind of expected and it happens with old diskettes. The most disturbing part however was that my normally working disks now has stopped reading too in any of my disk drives.

"What is it?" - I thought. - "The day of broken Commodore drives?".

I realized it is rather unlikely that all of the disk drives went down the same day. But, still clueless, I opened my first drive to see for obvious mechanical issues. Did not find any (even magnetic head looked clean). Next I opened my worst 1571 (the one I owned the longest time and looks like the one most worn down by time) and attempted to align it. Nothing worked. I gave up some time in the morning. 

The next day I got my revelation moment - "A dirty head!".

Yep! That was it. These old disks must have been stored in some not too good conditions, perhaps for some time exposed to moisture and/or high temperature. Their magnetic layer literally peeled off their surface and it deposited on magnetic heads choking them up! I swabbed them with alcohol and they are all like new again.

I know most of the Commodore or any retro computer equipment users know this stuff. But if you are a young inexperienced retro computer technology enthusiast, beware! Better to have a designated disk drive (instead of your best primary disk drive) for reading/examining the diskettes from unknown sources, especially if they are listed as untested or their condition is not mentioned by the selling party. And if your equipment stops working as expected, first look for the obvious reasons (like neglected maintenance in my case, head cleaning was long overdue, regardless of the dirty diskettes put in recently). I swabbed the heads but I think it is better (and more convenient) to use one of those head cleaning diskettes, which unfortunately are not being made these days. I saw them on e-bay though and I am going to order some for myself.

Cheers!