6
\$\begingroup\$

I've developed a quick Sinclair ZX80 game using z88dk. I used the beautiful C programming language along with a small amount of Z80 assembly.

Notes:

  • I'm using integer maths, which is faster and more memory efficient, and also a limitation of Sinclair ZX80 BASIC;
  • There is a mix of generic (reusable) and logic specifically for this game (or similar games);
  • It's not easy to have an animated screen on the Sinclair ZX80 without custom routines written in assembly, therefore the screen is only static whilst it is waiting for a user input (so whenever the processing is happening, the screen blanks);
  • The game only has reel matching logic, there is no nudge logic, nor are there any 'mini-games' as the latter would require animation;
  • Due to no animation, the reels don't 'roll'; and
  • £ signed are handled using \xa3 in the print statements and strings.

Once compiled, this game works with all 4K ROM ZX80 clones (ZXMore, Minstrel clone, Spectrum Next ZX80 mode etc...) along with emulators and the real machines as long as you have a way to transfer the build to cassette, or you have a ZXPand interface.

The total game build is around the 4K mark, so that means it will work on any expanded machine with 8K or more. The latest release candidate is on GitHub

#include <stdio.h>
#include <stdlib.h>

/**
 * Function prototypes
 */
int main();

/**
 * Game function prototypes
 */
unsigned short checkReels(unsigned char reel1, unsigned char reel2, unsigned char reel3);
unsigned char setReel(unsigned char reel);
void startGame();
void titleScreen();
void playAgain();

/**
 * Base function prototypes
 */
void cls();
void printSpc(unsigned char spc, unsigned char txt[31]);
void printTab(unsigned char tab, unsigned char txt[28]);
void prompt(unsigned char txt[32], unsigned char lineNumber);
void randomise();
void zx80Init();

/**
 * Reusable/API variables
 */
unsigned char _strBuffer[33];
unsigned char random;
unsigned char i;

/**
 * Game variables
 */
unsigned short pounds       = 500;
unsigned char winLine[]     =
{
    45, 45, 45
};

/**
 * Game constants
 */
unsigned char REEL          = 16;
unsigned short SPINCOST     = 25;

unsigned char REEL1[]       = "*$\xa3x*$*x*-x*x-x*";
unsigned char REEL2[]       = "$\xa3x*$*x*-x*x-x**";
unsigned char REEL3[]       = "\xa3x*$*x*-x*x-x**$";

/**
 * Main entry point of game
 *
 * @param   na
 * @author  sbebbington
 * @date    26 Nov 2017
 * @version 1.0
 */
int main()
{
    zx80Init();
    titleScreen();
    gets(_strBuffer);
    randomise();
    startGame();
    pounds = 500;
    playAgain();
    cls();
    printf("thanks for playing");
    return 0;
}

/**
 * Prompts player, y restarts game,
 * any other entry exits it
 *
 * @author  sbebbington
 * @date    3 Dec 2017
 * @version 1.0
 */
void playAgain()
{
    printf("unfortunately your money is\nspent. the management do not\ngive credit. we welcome back\npaying customers who enter Y\nand press return, otherwise\nplease move along.");
    prompt("", 2);
    gets(_strBuffer);
    if(_strBuffer[0] == 121)
    {
        main();
    }
}

/**
 * Show title screen
 *
 * @param   na
 * @author  sbebbington
 * @date    4 Dec 2017
 * @version 1.0
 */
void titleScreen()
{
    printSpc(7, "donkeysoft  mmxvii\n\n");
    printSpc(4, "and monument  microgames\n\n");
    printSpc(12, "presents\n\n");
    printSpc(10, "QuIcK FrUiTs\n\nyou start with");
    printTab(1,"\xa3");
    printf("5.00\neach spin costs");
    printSpc(3,"\xa3" "0.25\nWIN TABLE:\n");
    printTab(2,"\xa3 \xa3 \xa3 = \xa3" "10.00\n");
    printTab(2, "$ $ $ = \xa3" "7.50\n");
    printTab(2, "x x x = \xa3" "4.00\n");
    printTab(2, "* * * = \xa3" "2.00\n");
    printTab(2, "? ? - = \xa3" "1.00\n");
    printTab(2, "? - ? = \xa3" "?.??\n");
    printTab(2, "- - - = \xa3" "ZERO\n");
    prompt("press any key to play", 2);
}

/**
 * Starts the reels spinning,
 * continues until GBP is 0
 *
 * @author  sbebbington
 * @date    4 Dec 2017
 * @version 1.0
 */
void startGame()
{
    unsigned char _reel, pence = 0;
    while(pounds)
    {
        cls();
        pounds -= 25;
        winLine[0] = setReel(0);
        winLine[1] = setReel(1);
        winLine[2] = setReel(2);
        printf("#######\n");
        printf("#%c#%c#%c#\n", winLine[0], winLine[1], winLine[2]);
        printf("####\"\"#\n");
        printf("#######\n");
        pounds += checkReels(winLine[0], winLine[1], winLine[2]);
        pence = pounds % 100;
        printf("\nMONEY REMAINING \xa3%d.%d", pounds / 100, pence);
        if(!pence)
        {
            printf("0");
        }
        prompt("", 2);

        gets(_strBuffer);
    }
}

/**
 * Sets each reel position
 *
 * @param   unsigned char
 * @author  sbebbington
 * @date    4 Dec 2017
 * @version 1.0
 * @return  unsigned char
 */
unsigned char setReel(unsigned char reel)
{
    unsigned char reelPos = 0;
    randomise();
    if(!reel)
    {
        reelPos = REEL1[srand(random) % REEL];
    }
    else if(reel == 1)
    {
        reelPos = REEL2[srand(random) % REEL];
    }
    else
    {
        reelPos = REEL3[srand(random) % REEL];
    }
    return reelPos;
}

/**
 * Checks to see if reels have
 * matched
 *
 * @param   unsigned char, unsigned char, unsigned char
 * @author  sbebbington
 * @date    26 Nov 2017
 * @version 1.0
 * @return  short
 */
unsigned short checkReels(unsigned char reel1, unsigned char reel2, unsigned char reel3)
{
    unsigned short winnings = 0;
    unsigned char pennies = 0;
    if(reel1 != 45){
        if(reel1 == reel2 && reel1 == reel3)
        {
            if(reel1 == 163)
            {
                winnings = 1000;
            }
            if(reel1 == 36)
            {
                winnings = 750;
            }
            if(reel1 == 120)
            {
                winnings = 400;
            }
            if(reel1 == 42)
            {
                winnings = 200;
            }
        }
        if(reel1 == reel2 && reel2 != reel3)
        {
            winnings = 100;
        }
        if(reel1 != reel2 && reel1 == reel3)
        {
            randomise();
            winnings = 25;
            winnings *= srand(random) % 7;
            if(!winnings)
            {
                winnings += 75;
            }
        }
        if(winnings)
        {
            pennies = winnings % 100;
            printf("\nyou win \xa3%d.%d", winnings / 100, pennies);
            if(!pennies)
            {
                printf("0");
            }
            printf("\n");
        }
    }
    return winnings;
}

/**
 * This does the basic initialisation
 * for the game and for the ZX80 itself
 *
 * @param   na
 * @author  sbebbington
 * @date    20 Aug 2017
 * @version 1.0
 */
void zx80Init()
{
    unsigned char y;
    for(y = 24; y > 0; y--)
    {
        printf("                                \n");
    }
    cls();
}

/**
 * Outputs the prompt, also accepts
 * a string and on which line the
 * prompt should appear
 *
 * @param   unsigned char, unsigned char
 * @author  sbebbington
 * @date    21 Aug 2017
 * @version 1.1
 */
void prompt(unsigned char txt[32], unsigned char lineNumber)
{
    if(lineNumber)
    {
        for(; lineNumber > 0; lineNumber--)
        {
            printf("\n");
        }
    }
    if(txt[0])
    {
        printf("%s\n",txt);
    }
    printf("c:>");
}

/**
 * Simulates the BASIC PRINT SPC(x);"TEST"
 * function
 *
 * @param   unsigned char, unsigned char[]
 * @author  sbebbington
 * @date    27 Nov 2017
 * @version 1.0
 */
void printSpc(unsigned char spc, unsigned char txt[31])
{
    for(; spc > 0; spc--)
    {
        printf(" ");
    }
    printf("%s", txt);
}

/**
 * Assumes 4 spaces is one tab (PRINT TAB(x);"TEST")
 *
 * @param   unsigned char, unsigned char[]
 * @author  sbebbington
 * @date    27 Nov 2017
 * @version 1.0
 */
void printTab(unsigned char tab, unsigned char txt[28])
{
    tab *= 4;
    printSpc(tab, txt);
}

/**
 * Clears the screen
 *
 * @param   na
 * @author  sbebbington
 * @date    22 Aug 2017
 * @version 1.1a
 */
void cls()
{
    __asm
    exx
    ld hl, ($400c)
    ld bc, $0300
    ld d, $21
    inc l
    CLS:
        dec d
        jr z, NEWLINE
        ld (hl), $00
    DECC:
        inc hl
        dec c
        jr z, DECB
        jr CLS
    DECB:
        dec b
        jr z, EXIT
        jr CLS
    NEWLINE:
        ld (hl), $76
        ld d, $21
        jr DECC
    EXIT:
    call $0747
    exx
    __endasm;
}

/**
 * Hopefully this will be a
 * better randomiser 0x401e
 * is the 'frame counter'
 *
 * @author  sbebbington
 * @date    27 Nov 2017
 * @version 1.0
 */
void randomise()
{
    __asm
    ld hl, ($401e)
    ld a, l
    ld (_random), a
    __endasm;
}

Feedback always welcome, and if there is anything reusable then please feel free to do so and, if you're really kind, put a greetz to Donkeysoft when you do. Here is a screen shot for you (title screen):

Sinclair ZX80 screen shot

\$\endgroup\$
  • 2
    \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Ludisposed Dec 5 '17 at 13:46
3
\$\begingroup\$

So, because you are not using a standard modern ISA, I'm not sure how good your compiler is, but I'm going to assume its good enough that splitting up functions won't hurt performance.

First I have some more general questions.

  • Why didn't you split into .h and .c? Necessary to avoid polluting namespaces, but I'm not sure if you have a legitimate scenario for not doing so here.

  • Why did you call main from a function recursively? stack overflow will eventually happen with that, call main from a while loop with an exit condition that is set from user input.

  • You don't need per function dates or version, I'm not sure if you are using a documentation generator like doxygen but even with doxygen this is not needed. you also should be explaining parameters but only if they need explaining instead of just reiterating types in the @param designator, similarly, you should be explaining what you are returning not the type of it (which can be clearly seen and auto generated.

Functions

  • Say what you mean, only use abbreviations where they wouldn't hurt comprehension.

  • 'checkReels' implies it returns a boolean value, except it totally doesn't actually do that, even your comment seems to imply this, this is not the case.

  • check reels actually returns the integer valued winnings, not a short that is boolean that corresponds to what you actually won.

  • You should actually call this is calculateWinningsFromReels. Why? Because that is what this thing does. Additionally it may be helpful to use typedef to typedef winnings as, well "Winnings" to make it clear to the programmer what you are actually returning, you may find it even to be helpful to hide it behind a struct to make sure that only functions that deal with winnings will do anything to it, making it more clear and making your intentions even more explicit.

  • Additionally, I'm unsure as to why you are using cascading ifs instead of if elses? You should be using if elses because those cases are mutually exclusive (and if they aren't, you have bigger problems).

Your pennies variable is only useful when printing winnings, honestly that should be its own function instead, saving you from having to put that confusing variable in this function.

  • You also use magic numbers for your reels, I assume those correspond with ascii? You need to make it clear. You also need to change your REAL1, REAL2, REAL3 vars to be way more clear what you mean (you use a very odd escape sequence, while valid, is very uncommon to use) I suggest you use an initialization list instead of a string in order to set the character arrays up and name each character use use.

  • The names of each parameter would be better named like so to avoid 1l confusion: real_1, real_2, real_3. Additionally you probably want to put these into a struct.

  • also don't use the implicit compare against zero ness, that makes code harder to understand.

  • additionally to put leading zeros in do this, you don't need to do conditionals, this also fixes a bug in your code with single digit numbers

  • also use switch case with flat logic integer primitives, enums, etc

  • reels should probably be enums, as they only cover a certain range as well, this fits well with switch case.

  • Finally you might want to split things up more in this function to make it more readable.

  • Rename cls to clearScreen because that is what it does, no need to abreviate.

  • avoid global variables, there's no need.

  • Also noticed you had three of essentially the same string for REEL1-3 variables with an offset. changed that, as you were doing random selection anyway, you literally have the same chance of hitting any given value on any "reel" character array.

Here are some of my recommended changes, there ended up being too much to have to go through and change so I just limited my changes to things that had to do with checkReel which is no defunct.

note split up some functions, renamed randomise and strBuffer

// using define here to make sure these don't take up actual memory, and are inlined into code. 
#define POUND_CHAR '\xa3'

typedef int bool;
#define true 1
#define false 0

#define SPINCOST 25

enum Reel {
    rank_1st = POUND_CHAR, 
    rank_2nd = '$', 
    rank_3rd = 'x', 
    rank_4th = '*', 
    rank_5th = '-',
    rank_default = '-'
};

struct ReelLine{
    enum Reel reel_1;
    enum Reel reel_2;
    enum Reel reel_3;
};

struct ReelLine winLine{rank_default, rank_default, rank_default};

#define REEL_CHOICE_COUNT 16

const enum Reel REEL_CHOICES[] = 
{rank_4th, rank_2nd, rank_1st, rank_3rd, rank_4th, ...
// above is supposed to be "* $ \xa3 x * $ * x * - x * x - x * " used to be REEL1, now used for all three;


bool allReelsEqual(struct ReelLine* reelLine){
    return (reelLine->reel_1 == reelLine->reel_2 && reelLine->reel_1 == reelLine->reel_3)
}

bool onlyFirstReelsEqual(struct ReelLine* reelLine){
    return (reelLine->reel_1 == reelLine->reel_2 && reelLine->reel_1 != reelLine->reel_3)
}

bool onlyLastReelsEqual(struct ReelLine* reelLine){
    return (reelLine->reel_1 != reelLine->reel_2 && reelLine->reel_1 == reelLine->reel_3)
}

void printReelLine(struct ReelLine* reelLine){
    printf("#######\n");
    printf("#%c#%c#%c#\n", winLine->reel_1, winLine->reel_2, winLine->reel_3);
    printf("####\"\"#\n");
    printf("#######\n");
}

unsigned short getThreeReelMatchWinnings(enum Reel threeReelMatchValue){
    unsigned short winnings = 0;
    switch (threeReelMatchValue){
        case rank_1st:
            winnings = 1000;
            break;
        case rank_2nd:
            winnings = 750;
            break;
        case rank_3rd:
            winnings = 400;
            break;
        case rank_4th:
            winnings = 200;
            break;
    }
    return winnings;
}

void printCurrency(short moneyInPence){
    unsigned short pounds = winnings / 100;
    unsigned char pence = winnings % 100;
    printf("\xa3%d.%02d", pounds, pence);
}

void printWinnings(short winnings){
    printf("\nyou win "); 
    printCurrency(winnings);
    printf("\n");
}

void printRemaining(short winnings){
    printf("\nMONEY REMAINING "); 
    printCurrency(winnings);
    printf("\n");
}


/**
 * Finds the winnings from ReelLine values
 *
 * @param   reelLine used to look for winnings from. 
 * @return  the winnings calculated from reelline
 */
unsigned short calculateWinningsFromReels(struct ReelLine* reelLine)
{
    unsigned short winnings = 0;
    if(reelLine->reel_1 != rank_5th){
        if(allReelsEqual(reelLine))
        {
           winnings = getThreeReelMatchWinnings(reelLine->reel_1);
        }
        else if(onlyFirstReelsEqual(reelLine))
        {
            winnings = 100;
        }
        else if(onlyLastReelsEqual(reelLine))
        {
            randomize();
            winnings = 25;
            winnings *= srand(random) % 7;
            if(winnings = 0)
            {
                winnings += 75;
            }
        }
    }
    if(winnings != 0)
    {
        printWinnings(winnings);
    }
    return winnings;
}

void spinReelLine(struct ReelLine* reelLine){
    randomize();
    reelLine->reel_l = REEL_CHOICES[srand(random) % REEL_CHOICE_COUNT];
    randomize();
    reelLine->reel_2 = REEL_CHOICES[srand(random) % REEL_CHOICE_COUNT];
    randomize();
    reelLine->reel_3 = REEL_CHOICES[srand(random) % REEL_CHOICE_COUNT];
}

void promptForNextSpin(){
    prompt("", 2);
    gets(_stringBuffer);
}

void startGame()
{
    while(pounds)
    {
        clearScreen();
        pounds -= SPINCOST;
        spinReelLine(&winLine);
        printReelLine(&winLine);
        pounds += calculateWinningsFromReels(&winLine);
        printRemaining(pounds);
        promptForNextSpin();
    }
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I tried using enum types but I couldn't get it to work as in your example; structs certainly make the script much cleaner but have a byte overhead (I have to reserve at least 704 bytes for the screen out of 16384 minus var stack), also within my structs I have to use unsigned char (or other C types). I decided on having actual 'reels' because I'm using this as a base for a ZX81 version which can have an animated screen so I can add in nudge logic, but you are right that it's not necessary here. Your code is also too efficient for the randomiser as the ZX80 frame hasn't updated yet... \$\endgroup\$ – Shaun Bebbers Dec 5 '17 at 9:29
  • \$\begingroup\$ ...I therefore need a better way to randomise the numbers so may drop srand() and return a value from my randomise() function in a future revision by setting and returning the BC register or something. \$\endgroup\$ – Shaun Bebbers Dec 5 '17 at 9:31
  • 1
    \$\begingroup\$ @ShaunBebbers Really? You couldn't get enums to work? I did forget a semi colon, so that could be the issue, but If you tell me the error I might be able to help with that. Additionally, where do structs have an extra byte overhead? If its padding then I suspect if you made multiple 3 byte arrays the same thing would happen (I'm assuming padding is happening on 16 bit boundaries?) in which case structs are still no worse. For randomization, you can just ditch the current method and go to Linear Congruence Generators (cont...) \$\endgroup\$ – whn Dec 5 '17 at 14:00
  • 1
    \$\begingroup\$ @ShaunBebbers I believe C's rand() function already does this, so you would theoretically only need to call srand() once, then call rand(), though I'm not sure why the custom random assembly was necesary \$\endgroup\$ – whn Dec 5 '17 at 14:03
  • 1
    \$\begingroup\$ @ShaunBebbers Z80 may have an actual non PRNG randomizer, but I would just use C's built in utilities for this with seeding based on the clock of your system. Additionally enums are not a machine specific thing, enumerations are literally just integral types underneath, there simply cannot be "limitations" in enum types. There is no "special" way to implement them, they are simply there to make cleaner more readable code with better verification. I think that you need to preface enum types with enum when you declare them, so that might be the issue, (enum type name = ...) \$\endgroup\$ – whn Dec 5 '17 at 15:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.