Function | Purpose |
Monitor | Show the status of all the registers |
Monitor_PushedRegister | Push a register pair... this function will show it's contents |
Monitor_BreakPoint | Show the program counter at this point - you can then use the emulator debugger to check the location and memory |
Monitor_BreakPointOnce | Same as above... but this version deletes itself - designed to be put in a loop , the breakpoint will only occur once |
These functions require the ShowHex
function... this will show a byte as Hex chars onscreen, and not
modify any registers. The Monitor is split into two parts... the SimpleMonitor one for breakpoints and PushedRegsisters.. and the Monitor for general register viewing... you can use the simple version only when memory is limited There are also 2 extra options... Monitor_Full if defined will show all registers in the Monitor... Monitor_Pause will cause a wait for keypress |
![]() |
![]() |
The code for
this lesson is really complex... so don't try to type it in!...
See MonitorTest.ASM
for the usage example It's in the Sources.7z file archive... so just download it and use it from there... That said, it's worth knowing how it works... there's some clever tricks in there... and you'll want to know how it works, so you can customize it and make it better if you need more functionality! |
call Monitor_BreakPointOnce and
call Monitor_BreakPoint
both work the same... they will show the program
counter to the screen The Program Counter is the location in memory the code ended up when it ran, you can then check this memory location in the debugger if you want! |
![]() ![]() ![]() |
Want to see the value of a
register?... just push it onto the stack and
Call
Monitor_PushedRegister... it will show
the value of the register pair, and take it off the
stack... so your program can continue! |
![]() ![]() |
If you really need to know
everything...use Call Monitor... it will show the state of all
Registers! if you define Monitor_Full it will show IX/IY and Shadow registers... otherwise it'll just show the basics, but use less memory! |
![]() ![]() |
Need
to debug, but short on memory? remove the definition of
Monitor_Full... Still too big... well you can always just use Monitor_Simple to just show breakpoints and single register pairs! |
![]() |
This is not called by the user
directly, it's called by the other modules and shows HL as hex,
surrounded by ** symbols If needed it will pause the system after showing the Breakpoint or register |
![]() |
This module will pop a register and show it to screen... because
we need HL, but don't want to use the stack we back it up with
self modifying code... Then we pop a pair of the stack... this will be the return address... Next we swap HL with the pair on the stack... HL will now be the pair pushed before the call, and the return address will be at the top of the stack... All that's left to do is show HL... then restore HL and return to our program! |
![]() |
This module is super simple! all it does is get the return address off the stack.. show it, and return! | ![]() |
![]() |
This can help you check why your program
isn't working right! Once you know the Program Counter, you can use the emulator's internal Disassembler or memory monitor to check what's going on in the code! Maybe self-modifying code has gone wrong... or the memory is corrupted.... Knowing where in memory a command is ending up after compiling is the first stage of solving the problem! |
More complex than the last
version... this one overwrites the 3 bytes preceding the return
address... These will be the CALL command that called the monitor - the end result is that the monitor is only called once... so it can be used in a repeating loop to find out the program counter location (or if the loop even ran - if your program is crashing) without slowing down the program with lots of pauses. |
![]() |
Now this is a weird trick! (That won't help you lose
weight!!)... When we do LD A,I... something secret happens... the
PO flag is altered... if Interrupts were disabled,the PO will
be set! Here we use this to allow us to detect if EI or DI is set, show it to screen... and restore the interrupt state at the end of our Monitor function! in this block B is the letter 'D' or 'E' for the onscreen label... and &F3 is the DI , and &FB is EI for a self-modifying command! |
![]() |
While this works
on many Z80's... some may misbehave! On the Enterprise NMOS Z80 if a firmware interrupt fires before the PO, then the flag may be changed! There is a more complex alternative to fix this below! |
![]() |
on the Enterprise's NMOS Z80 ... LD A,I may not be enough! We need to check if an interrupt JUST fired... we do this by pushing &00 onto the stack... if the interrupt fired, that &00 will be replaced with &38... then we use LD A,R to check the Parity status. once we've done our check, we just need to make sure PO wasn't set by an interrupt that just fired, so we POP A and check if it's still &00.. if it isn't then an interrupt must have just fired (so Interrupts were enabled). The result is this function returns no carry (NC) if interrupts are enabled (EI)... and carry (C ) if they are disabled. |
![]() |
The first thing we need to do is
push all the registers we're interested in onto the stack, this
also backs them up, so we don't need to worry about changing them! |
![]() |
Next we work out what the stack
pointer was before we started pushing.. and put that on the stack
too... We load DE with the address of the stack (and all the register values)... HL points to the text labels of our registers... and we use B as our counter register! |
![]() |
Now we're going to show each of the
registers.. we show a 2 character label, then we display the two bytes - they're stored in Little Endian (Low-High) ... so we have to INC DE then DEC DE to show them as the user expects... We repeat this for each register we pushed! |
![]() |
DI/EI were stored in a string... they have no matching byte-pair on the stack... but we need to show them anyway, so we do it here! | ![]() |
We pushed the stack pointer onto
the stack - so we get rid of it with a POP HL... Next we Pause for the user if needed... Then we restore all the registers ... using two INC SP's to skip over the pushed I and R register that we don't restore. |
![]() |
Last, but not least... we restore the interrupt state... and return to the program that called us! | ![]() |
![]() |
The Monitor
covers all the Z80 basics... but you could go even further!...
Maybe we want to check the status of the memory bank mapping, or
rom paging... or perhaps we need to show the contents of the
stack, or dump some memory... Anything is possible! but of course we don't want to end up with a 'Monitor' that uses up all the ram, leaving no room for our actual program! |
Here is our WaitString Command, we
need to CALL it with two parameters, HL
needs to be a destination address, and B
is the maximum number of characters to read... of course the function will overwrite that many characters plus 1 (for the end of string Char 255) The function will return some useful parameters too! HL will now be the end of the string, DE will be the start... B will be the remaining unused characters... and C will be the number of characters entered! How's it work? pretty simple! DE is used to back up the start of the string - we need that for later! Each character is read in, and if it's not DELETE or ENTER key, we store it in the string, and repeat if B is still >0 When we're done, we store a 255 as the last character in the string. |
![]() |
Now we need to handle the backspace
key! First we need to check if any keys were pressed, we could do LD A,C then OR A... but lets try a different trick! By doing INC C... DEC C... we can set the Zero flag in the same way, but leave A untouched - a trick worth bearing in mind! If the keycount is zero, we give up - there's nothing to delete! otherwise we INC B - set the last character to a space (to delete the one onscreen! next we get the current cursor position, subtract the length of the string from the Xpos in H, and redraw it (with the new space at the end)... the line will be redrawn Finally we DECrease the key count in C and return back to wait for more keys! |
![]() |
![]() |
These examples all use 255 as the
end-of-string byte... while 0 is more common in many examples,
this was used as an end-of-message command by ChibiAkumas Some examples even add 128 (&80) to the last character in the string, to save a byte from the string length, but this causes problems for character sets that need more than A-Z in their alphabet |
We're going to create a String to
Hex processor soon, but we're going to assume all the characters
are uppercase, So lets create a ToUpper function that will do that for us, it's pretty simple! we process all the characters in the string until we get to a char 255... if a character is between 'a' and 'z' then we add 32 to it, which will convert it to 'A'-'Z' |
![]() |
This function will take a pair of
characters from HL,
convert them from HEX, and return them as a byte in A We use the function AsciiToHex to convert as single character, then we do 4 RLCA's to move the bottom nibble to the top, and back it up in B Then we run AsciiToHex again to get the second character, and OR the top nibble in B in... we're done now, so we just return! AsciiToHex reads in a character from HL, we subtract the character '0' to convert the numbers from ASCII a number... then we check if our symbol is below 'A' and return if it is (we have to subtract '0' to counter the previous change to A) if it's not, then we need to alter A, so that it's correct - we do this by subtracting the difference between 'A' and 'O', less the amount we need to add (which is Hex A) |
![]() |
Beware,
we're not being very thorough with AsciiToHex... it will return
weird values for characters other than 0-F!... We're going to add more String processing functions in later lessons, as we use our text reader for more interesting things! |
![]() |
This is the main call routine for adding test code to our
program. Because we don't want to use any of the registers our program uses, we're going to do a little trick, and store the parameters after the call! - so a call to our dumper will look like this: call Monitor_MemDump db 33 dw &1234 Where the first byte after the call... '33' is a one-byte count of the number of bytes to dump to screen and the following word '&1234' is the address to dump from! First we get the address that called us, by swapping HL with the top of the stack pointer... next we back up all our registers... Now we read in C (bytecount) and HL (address) from the following bytes... and call our actual display routine! finally we restore the registers... and skip over the 3 bytes of parameters... last we re-swap HL with the top of the stack pointer, and all the registers will be restored the way they were before the call! |
![]() |
We can Call MemDumpDirect directly
with an address in HL
and bytecount in C First we show HL to the screen as a label, so we can tell what we're viewing... Next we need to decide how many bytes to show onscreen... on TI-83 we can only show 4... but on everything else we show 8! Now we check if we have a whole line of bytes left to show, and limit the amount to show if we have more than a line left. Next we subtract the number of bytes we're going to show from the remaining count C |
![]() |
We're ready to start displaying
it... First we show HEX, but we'll show ASCII later too, so we back up the variables in BC and HL we show each byte as a hexadecimal pair with a space afterwards (eg 'FF ')... we repeat for the count in B We've backed up HL, and BC, because we'll do the same to show the characters as ASCII |
![]() |
Now we show the characters to the
screen as ASCII. Characters below 32 have special meanings, so we convert them to '.' characters to keep things working ok Also, on ZX Spectrum or Sam Coupe we can't use characters above 128 either. I've defined BuildZXSv and BuildSAMv... they will be 1 only on those systems, and 0 otherwise. IF statements on WinApe will compile lines where the condition is nonzero... by using BuildZXSv+BuildSAMv the following lines will compile if EITHER of these systems are being used. On TI, or 32 character systems, we're all done!, but on 40 character wide systems we need to do a newline... Finally we check C - and if there are any more lines to draw, we repeat the whole procedure again. |
![]() |
![]() |
By adding BuildSAMv an BuildZXSv together, and using that as a condition in an IF statement, we can define a block that compiles on multiple systems... if our assembler didn't support it, we could just use a pair of IFDEF blocks, but it could get quite messy! |
We define a buffer of 16 characters
labeled TestString for the user input (We'll actually only need 5
chars) We read in up to 4 characters using our WaitString Function (5th char is 255) Next we want to check if the first character is 255 (if the user has entered no characters)... and return if they have we do a trick here, rather than doing CP 255.. we can do INC A - if A becomes zero, it was 255... INC A saves one byte of code over CP 255... but of course A is changed in the process! Next we convert the string to Uppercase using ToUpper Then we convert the first 2 characters to hex, and store them in D Then we convert the next 2 characters to hex, and store them in E Now we swap the memory address from DE to HL On the TI we can only show 6 lines... on the everything we'll show 128.. we store this into C finally we call our MemDumpDirect routine, and repeat the whole procedure. |
![]() |
One thing to note
is that the AsciiToHexPair and WaitString functions are not
restricting the keys pressed, and WaitString is not checking if
4 characters were entered... both these factors mean 'weird'
resulting memory addresses if, for example the address 'AZZZ'
was entered. Because we're only showing memory addresses, and this is only a test it doesn't really matter, but you may want to add some code to limit the keys Waitstring allows to be pressed, and check 4 characters were really entered if you want to make it better! |
![]() |
We want to split a string up by
spaces, to do this we'll convert 'Space' (Char 32) into Char 255 This Replace char function will process the string pointed to by HL, and any characters that match register D will be swapped to E |
![]() |
CompareString is our other
function, We pass two string pointers, one at HL, and the other at DE... this function will set
the Carry flag if the strings don't match, and Clear it if they
do! We check each character in the strings, to see if they match, and if we find one that doesn't before BOTH strings end... then they don't match! This function doesn't check which is greater, or less than... so you'd need something else if that's what you need! SCF is Set carry flag, and this is done if the strings don't match. We load A from the string at DE... and we AND it with the character from DE... this means A will be 255 if BOTH are 255... then we do INC A, and RET Z as a quick way of checking if A is 255... also AND and OR clear the carry flag, so we don't need to do anything to clear it if the strings match |
![]() |
We're going to process contents of
a user entered string, and match it to a list of words. First we'll need to define all the strings, and then we will create a table of pointers to the addresses of those strings... the list will end with &FFFF as an 'end of table' marker. Using a list of pointers will make the data neatly structured, and make things easier for us to work with! |
![]() |
Having
a
list of pointers to the strings will give us far more
flexibility... if we want to print String 2 we just set DE to 2,
and set HL to the wordlist, add DE twice, and we'll be pointing
to the word! Lists of pointers are used all the time, so while they are confusing, you'll have to try to get used to it! |
![]() |
First we load in the address of the
next string in the list into DE. Like before, we AND both D and E together... and add one - this effectively checks if both are 255, if they are, we've no words left to check, and we never found a match. otherwise, we compare the word to the one we're searching for, and repeat if it's not found... We return the number of the match in A, but if it was never found we return 255 |
![]() |
![]() |
By converting
a string to a integer, we'll be able to get assembly to process
the user's input easily! We're going to process up to two commands... so we'll be able to do things like 'North' or 'Take C64' and have the program respond to those commands! |
First we print a prompt to the
user, then we read up to 20 characters from the user. We're going to read in two strings, but the user may have only entered one, so for safety we add an extra 255 to the end of the string! |
![]() |
We need to prepare our string, First we change all the characters to capital letters, Then we replace any spaces in the string to char 255's... this means our string code will treat it as two separate strings Finally we start a new line, we'll show the matches numerically soon! |
![]() |
We now compare the user's string (TestString) with the contents
of the wordlist, and show the result with ShowHex Because we converted Spaces to Char 255, we'll only match the first word |
![]() |
Now we want to find the second word... this is a rare chance to
use CPIR (compare inc
repeat) We need scan up to 255 characters, so we load BC with 255.. We want to find 255 so we need A to be set to 255... we know C contains this value, so we load A with C... then we run our CPIR Now we need to check if the user has entered multiple spaces, not just one, so we check if the next char is also a 255 (using our inc a jp z trick) and skip if it is. |
![]() |
Now we shift the starting searchpos into DE, and do another
Scan... again we show the result with ShowHex Finally we start a new line, We'll repeat the whole procedure again until the user gets bored! |
![]() |
![]() |
The contents
of todays lesson may not be *That* interesting, but we're going
to extend it into the template for a little text adventure! so
tune in next week for... "Majokko Sakuya in the land of the
Chibi-Chibi's'... or something like that!... ok it's only going
to be a little 'joke game' but it'll show us how we could create
a real text adventure. |
If
you want to see all the code, check out the Sources download, or
the video - we're only going to cover the essentials here! Why?... Because it's too complex... and you're puny mind could not take it!!!... no, actually it's because it would be long and boring... and all the source code is commented anyway! |
![]() |
Our game 'world' will be made up of rooms, each room will have a
description, some items we can take, and you will be able to
perform special actions in those rooms Each room definition is made up of three 16-bit words... Description - is the address of the string which describes the room - it will be shown to the player when they are in the room Itemlist - is the address of the items in the room, the item numbers will be one byte each, and the list ends with a 255 LocalCommands - is the address of a function which will handle the special actions a user can perform in a room |
![]() |
The Description
is just a line of text, ending in a 255 The Itemlist contains the 'Word' numbers of the items in the room, ending in a 255 The 'LocalCommands' is a routine which will be called after 'global commands' are processed... when the function has been run, A will contain the first command number (so will B)... C will contain the second command... when the routine is called, the main loop will repeat. |
![]() |
![]() |
We've only
created two rooms, but you could make as many as you want... at
least up to 255! Defining the links to the rooms is done by detecting the commands, and changing the room number... of course, you could process other local commands like 'Use Spoon' and have a special action occur in that room. |
We need to handle items, but we also need an easy way to handle the room definitions themselves, by creating generic routines to do the work for us, we can save development time, and if we change the structure - like adding a fourth room definition - we can just modify the the generic routines, and everything else will continue to work unchanged! | ![]() |
When we want to get the data for a
room number in A, we can do this quite easily. Each room has 6 bytes of data, so we need to move 6 x A bytes... We do this by loading HL with A... doubling it, and then adding this to the start of the room Definitions... HL will then point to the address of the Description... two bytes along are the Items in the room... and four bytes along are the local commands! |
![]() |
We've got various lists of items,
lists of items in rooms, and a list of items in our inventory. We need some commands to handle items in these lists... we need to add items to the inventory, take them out of the room, and see if an item exists when we want to take or use it. We have 3 functions to do this, RemoveItem, which will take the item pointed to by HL out the list, by scanning through the list to the end, moving each item up one, overwriting the item that was pointed to by HL Additem will find the end of the list pointed to by HL, and add the item in C to the end of the list FindItem will scan through the list pointed to by HL, and find the item C... if it's found the Carry flag will be Clear (NC)... if it's not found it will be set (C) |
![]() |
![]() |
We've got all
the tools we need! Now we just need to use them to show the info
about the room, let the player take the itmes, and move around
the game world! Of course, there are many ways you could design a Text adventure, and this probably isn't the best... if you can think of a better one, go ahead an use it! |
ArkosPlayer
and
tracker were written by Julien N�vo (Targhan)...They are
absolutely amazing, and ChibiAkumas would be nowhere near as
good as it is without them! Check out the examples in the Sources.7z download for the examples in this tutorial. ArkosOriginal.asm will show the classic ArkosPlayer... and Arkos.asm will show this tutorial's author's cut down version! (which adds Enterprise and Sam Coupe support via AY emulation) |
![]() |
![]() |
That's enough
'support' code to get ArkosTracker usable, so how do we actually
make it work? First let's take a look at the 'default CPC/MSX
arkosplayer' (there is also an official Spectrum player, though
it is slightly different) |
First you need to initialize your
Music and SFX, DE must point to the correct data when you run
these commands... You MUST initialize SFX even if you don't plan to use it! Don't want SFX? Just run the INIT with DE pointing to your music! |
![]() |
To update the music you just call 'PLY_Play' ... but make sure you don't run it more than 50 times a second - or your music will play too fast! | ![]() |
Playing a sound effect requires
quite a few settings! the example to the right will play
instrument 1 (in
L) with default settings |
![]() |
![]() |
ArkosPlayer
has lots of other features - see the code comments in
"ArkosTrackerPlayer_CPCStable_MSX.asm"... You can even use it from basic!... but we're not covering it in these tutorials! Why?... Because its an Assembly tutorial, silly! |
That's
all it takes to get music and SFX working on ArkosPlayer! The author of these tutorials wrote a 'cut down' version... it's simpler, and adds Enterprise and Sam Coupe support!... these were made to reduce the player by every possible byte for ChibiAKumas Ep2 ... They remove the ability to use the 4th Special Track (for speed changes and Digi Drums)... and force SFX to play on the middle channel! |
![]() |
If you're using my cut down version, you don't need to specify the address of the music and SFX, the player assumes the labels "Akuyou_MusicPos" and "Akuyou_SfxPos" point to the music! | ![]() |
The PLY_SFX command is different too! You can only specify the pitch and Instrument... all other options are 'default' |
![]() |
This version of ArkosPlayer has
been modified, and works in this way on MSX, CPC,Spectrum,Sam
Coupe an Enterprise... Note Sam Coupe and Enterprise use 'AY Emulation' and will not sound quite the same! |
![]() |
![]() |
The author of
these tutorials is far too stupid to write AY emulation! the
credit for the amazing Enterprise AY emulator goes to 'IstvanV'
with conversion by 'Geco'! I'm not sure who made the Sam Coupe one... it was taken from the 'Cookies Hot Butter demo'... so whoever did that kicks total ass! |
Lesson
M6 - Advanced Interrupt handler template We just covered Interrupt Mode 2 in Lesson A2 of the Advanced series, and we covered IM1 in Lesson 7 of the Basic series. Now lets create a 'definitive' interrupt handler template, which we can use in the same way on all systems, whether they need IM1 or IM2... and we'll allow it to preserve regular registers, or shadow registers as well! Not impressed yet? well... we'll make it work even when you're misusing the stack for fast reading or writing... how about that! |
![]() |
![]() |
![]() |
The interrupt handler we're going
to look at this lesson does everything for us! If you don't care how it works, All you need to do is turn it on with Interrupts_Install The code you want to run each interrupt should be between the Start and End code blocks. Interrupts_FastTick is needed for the CPC... if you need something to happen at 300 hz, put it here! Use Interrupts_Uninstall when you need to restore the firmware interrupt handler! |
![]() |
Depending on what your interrupt handler needs, you may want to
enable some options! on systems with rom at &0038 you'll need to use IM2 If your interrupt handler needs regular registers , use 'NeedAllRegisters' If your program needs shadow registers, use 'ProtectShadowRegisters' If you're altering the stack pointer, and using it to Write or Read data, you can enable 'AllowStackMisuse' (Warning! There are restrictions) ... you'll need to provide an address for a 'Shadow Stack' which will be used by the interrupt handler |
![]() |
If you're using Interrupt Mode 2, you'll need to allocate around &200 bytes of space at &8000... see Lesson A2 if you don't know why! | ![]() |
First and foremost, the Interrupt
handler will deal with any 'oddities' of your system! On the CPC, there are 6x the normal (50 hz) number of interrupts On the MSX we need to read the VDP status register to clear the interrupt On the Enterprise |
![]() |
By default it's assumed the
Shadow Registers are exclusively available for the interrupt
handler, and your interrupt handler won't need the 'normal
registers' But if that's not true (Eg: for Arkos Tracker) you can turn on Interrupts_NeedAllRegisters and Interrupts_ProtectShadowRegisters |
![]() |
If you want to 'Misuse' the stack
pointer, to allow fast reading or writing, you can enable 'Interrupts_AllowStackMisuse'...
This will use a second stack during the interrupt routine called the 'Shadow Stack' meaning when an interrupt fires, only 2 bytes of the stack will be affected... it will also put whatever is in DE back at the top of the stack, replacing the 2 bytes of the address to return to after the interrupt. This means if you're careful, you can misuse the stack pointer provided the following are true: 1. If you're WRITING... there are at least 2 bytes free above the stack pointer that can be overwritten (by the first and only call to &0038... the shadow stack will be used for any pushes/calls in the interrupt handler) 2. if you're READING... all reading is done with POP DE, and DE is loaded when interrupts are first enabled. |
Interrupt Handler
Start:![]() Interrupt Handler END: ![]() |
Here's an example of Writing... We disable interrupts before writing the last byte pair, so that nothing will be corrupted if the interrupt fires after the last PUSH DE |
![]() |
Here's an example of Reading... If you need to do any commands between enabling interrupts and the first POP DE (eg: the 3x NOP), you should preload DE with PUSH DE, POP DE... this will ensure if an interrupt fires (during the 3 NOPs) the data at the stack pointer will not be overwritten by random data already in DE |
![]() |
![]() |
AkuSprite
Editor is pretty basic at this stage, as it wasonly
developed it enough to do the conversion of ChibiAkumas, but
it's opensource, so feel free to improve it! The Author of these tutorials will try to make it better if he gets time! but don't count on it! |
![]() |
Because the
hardware buttons are in a fairly 'random' order we cannot tell
what letter key each button is... but to solve this we have
defined a 'keymap'... as the keymap matches the hardware layout,
we can detect letter buttons (and other buttons like shift or
control - if we want) and convert them to a letter or
'universal' (across all our platforms) keycode |
![]() |
Because all
the Key and Joystick devices are being converted into a common
buffer (whatever the system) we can now allow the user to
redefine their controls, and process the resulting 'joy/key map'
in a common way via a single 'Button map' byte... there's one
for each player, so we support 2 players... You could support more... but keyclash will probably stop you! |
![]() |
The
CursorDoCheck code could be built into the main routine, but
it's been split out o save some bytes. This code was originally used in the menus of ChibiAkumas, but it's been improved and re-purposed for a new game that's in progress! |
We need to repeat the same kind of procedure for the other directions, just with different registers, and addresses for the boundary variables | ![]() |
This
routine is rather simplistic, but it works, and it's easy to
reconfigure - so you can use it for various purposes... If you change the X or Y speed to Zero, you can disable one of the Axis - so (for example) you can use it to control an onscreen 'spinner' |
![]() |
Lesson
M10 - Making a PONG - Part 2 Lets continue with the pong - we need a ball, a CPU player, and some collision detection and the like... Let's finish the job!! |
![]() |
![]() |
![]() |
The start of the main loop needs to
clear the two paddles, and the ball We'll use the paddle routine we saw last time, and a 'ball drawing routine' to clear the ball |
![]() |
![]() |
![]() |
You won't have
much trouble outsmarting the CPU - but it's only an example...
feel free to make it smarter if you want! |
The joystick code is the same as last time | ![]() |
Remember... the
co-ordinate in the Player and CPU XY are the top of the
paddle... Any time we need to consider the bottom we need to add 4 - if you want to work with a different paddle height, you'll have to reprogram the code! |
![]() |
![]() |
This example
will on the CPC, Speccy and most of the other Z80 computers - it
doesn't work yet on the consoles... this is because it has been
kept as simple as possible for the person who requested it -
adding support for GBZ80 and smaller screens makes things more
complex... And alternative version, called 'PONG2.ASM' will work on the SMS/GG and even Gameboy! |