• Please review our updated Terms and Rules here

MagiDuck, a DOS / CGA text mode game project

Allright, got the first alpha-version done finally! Here's how things are looking now:


The game can be downloaded from indieDB: http://www.indiedb.com/games/magiduck

I've tested the game on DosBox with CGA, EGA, VGA and SVGA_S3 configs. Seems to be working alright, but maybe it's good to mention that the display-initialization has been completely written in Assembly now (in case anyone thinks this might be risky). The EGA-version doesn't seem to be able to disable blink in Dosbox but PakuPaku has the same issue, so I'm blaming that on Dosbox for now. The keyboard routine is now included in the Assembly-library too.

Just to confess my horrible sins: the previous pre-alpha release was actually dangerous. The keyboard initialization was haphazard and could cause a situation where it would jump to a wrong segment of memory. The Assembly-routines didn't preserve registers correctly either. All these should be corrected now. I've tested the game for long periods of time and have had no issues in Dosbox. Deathshadow's timer-routine probably had nothing to do with my previous memory issues, it simply made my old mistakes manifest themselves.

Most of the work lately has been about creating new content; levels, graphics and tweaking behavior code. Also spent a lot of time on tiny tech-tweaks, code-cleanup and some tools to improve my content pipeline.

There are still many things to optimize in the Assembly routines, so I think the next version will run slightly faster.

I added a new menu system, which allows for HTML-like layouts with text and sprites. It can easily be used for text-pages or pop-up windows too. It uses the game's normal actors to save menu elements so only 32 bytes of extra memory spent on that. The menu text elements are streamed from a file when needed and simply printed into video memory. Just to reduce disk-access, I also decided to spend ~1K of memory to precache some of the most often used menus. I can only guess how fast or slow the file-streamed menus will be from a floppy :|

The animation system is revised too, decided to KISS multi-part animations goodbye. The sprite atlas now uses more memory and it simply uses some bigger sprites, instead of sprites consisting of multiple parts. Every animation frame stores all the information needed to store a sprite, memory offsets, size, etc... With all this, the new system still uses less memory than the old one and has much less overhead, when adding sprite-data to the draw-list and clipping sprites against the screen bounds.

The actors in the game now form a linked list, so parenting is quite a bit easier and that can be used to create some multi-part entities instead of the animation system.

Levels are packed into one big file now and there's less file clutter in the game folder otherwise too. I'm hoping to create some kind of package format that would include both levels and tile-graphics at some point too.

Anyways, I'd be happy to hear any critiques or bug reports about this if anyone has the time :)
I'm sure the game will remain a tower-climber thing with very similar mechanics though, but any ideas for enemies or new
game-mechanics are very welcome too. Right now, the experience is admittedly a bit flat. Perhaps partly because of some lame level design, but I think it could use some more dynamic elements to create more interesting chains of events.
 
Excellent progress; it looks wonderful and is already quite polished.

FYI, I measured memory usage of the program and found it used 184KB of RAM. This is after playing a few levels. This is quite reasonable; your game will run on any system with 256KB or higher, which is nearly every single remaining system.
 
I'll give it a crack on my Laser XT clone sometime tonight. Judging by the Youtube clip it looks great though.
 
I gave it a try on my Turbo XT. When I ran the program it displayed 'Divide overflow' and then some random ascii characters appeared on the screen. The computer then hung and had to be hard reset.

I also tried it a 2nd time with more free RAM as the first attempt had some LAN drivers loaded. This time I got a beep and the screen went blank (and it hung again and required a hard reset).

Hardware:
8088 @ 4.77 or 10MHz (tried both speeds with the same result).
Paradise 8-bit VGA card
~600K of free RAM
 
Yeah, my 1000SX, Jr, 286 and 386 systems all choke on it. In fact I cannot get it to run on real hardware. :(

Though for me it just hangs on everything but the 386, but that has a VGA in it, the others are CGA... oh wait, one of those is MCGA...

1000SX is real fun, solid white screen and a screaming "BEEEEEEEEEEEEEEEEP" until you power it off. Even ignores the three finger salute.

P.S., that has to be one of the most annoying sites to try and download something from... I almost thought I was back using DROSSDOS on a Model III. "Are you sure? Are you really sure? Are you really really sure? Well tough. Of course given the accessibility train wreck and laundry list of how not to build a website this "indieDB" garbage is, I shouldn't have been surprised some six pages deep I still wasn't downloading.
 
Last edited:
Just tried it on a 5150 with the original CGA card... awesome work!

It runs just as smooth as the videos and there is no snow!

Great, just great work :D
 
Tried it on my IBM 5160 with CGA, worked fine, no snow, quite playable!
I guess the controls are aimed at an AT-style keyboard though. Ctrl + Alt are next to eachother on the left and the right there.
I have an XT style keyboard, where the Ctrl + Alt are on top of eachother, only on the left side of the keyboard.
Also, there is no separate arrow block, so only the numpad.
Perhaps you could use the '5' key on the numpad as a second 'down' key? It's rather hard to reach it now, where the down key is at '2'.
 
Thanks everyone, for the comments and testing! Sorry for the trouble caused too. :(

So three machines survived the game then. I have an idea what might be going on here...

When checking for a VGA-card, I'm saving the expected functionality/state data-dump to be stored in a string that isn't formatted in any way. I thought Qbasic might format every variable created, but maybe it doesn't after all (Dosbox must be doing that then)...

I'm guessing the VGA-state check is returning non-zero bytes, no matter what card you have and the game ends up detecting VGA and looping through VGA-initialization registers when it shouldn't.

Code:
;---------------------------------------------------------------------------
    mov es, [bp + 08]               ;ES = Write seg (video adapter data)
    mov di, [bp + 06]               ;DI = Write ofs (video adapter data)    
;---------------------------------------------------------------------------
test_VGA:
    xor ax, ax                      ;Interrupt call 10h AH=1Bh AL=0 BX=0
    mov ah, 01bh                    ;Get VGA functionality and state.
    xor bx, bx                      ;dumps 64 bytes at ES:[DI] (if VGA)
    
    int 10h
        
    cmp al, 01bh                    ;If AL=1Bh this is a VGA
    je detected_VGA
    
    mov ds, [bp + 08]               ;DS = seg (video adapter data)
    mov si, [bp + 06]               ;SI = ofs (video adapter data)
    xor bx, bx
    mov cx, 16
VGAstatedatacheck:
    lodsw                           ;AX = DS:[SI], SI+2
    or bx, ax                       ;BX: accumulate any bits from the data dump.
loop VGAstatedatacheck              ;If nothing's there, this isn't a VGA.
    
    cmp bx, 0
    jz test_EGA
    jmp detected_VGA

Here's the full display initialization code too:

Code:
;============================================================================
;
;   Checks for video adapters VGA, EGA, CGA or MONO/OTHER
;   
;   If MONO/OTHER is found, 0 is returned at ES:DI (and the game won't run).
;
;   VGA/EGA/CGA will initialize 40x50 text mode with 8x8 characters.
;
;============================================================================

; Parameter stack offsets
; Order is inverted from qbasic CALL ABSOLUTE parameter order

;00 bp
;02 Qbasic return segment
;04 Qbasic return offset

;06 Video adapter data offset   /   64 bytes of data to accommodate
;08 Video adapter data segment  /   VGA state check and adapter test result.

;============================================================================

    push es
    push di
    push ds
    push si
    push bp
    mov bp,sp
    add bp, 8

;---------------------------------------------------------------------------
    mov es, [bp + 08]               ;ES = Write seg (video adapter data)
    mov di, [bp + 06]               ;DI = Write ofs (video adapter data)
    
;---------------------------------------------------------------------------
test_VGA:
    xor ax, ax                      ;Interrupt call 10h AH=1Bh AL=0 BX=0
    mov ah, 01bh                    ;Get VGA functionality and state.
    xor bx, bx                      ;dumps 64 bytes at ES:[DI]
    
    int 10h
        
    cmp al, 01bh                    ;If AL=1Bh this is a VGA
    je detected_VGA
    
    mov ds, [bp + 08]               ;DS = seg (video adapter data)
    mov si, [bp + 06]               ;SI = ofs (video adapter data)
    xor bx, bx
    mov cx, 16
VGAstatedatacheck:
    lodsw                           ;AX = DS:[SI], SI+2
    or bx, ax                       ;BX: accumulate any bits from the data dump.
loop VGAstatedatacheck              ;If nothing's there, this isn't a VGA.
    
    cmp bx, 0
    jz test_EGA
    jmp detected_VGA

test_EGA:
    xor ax, ax
    xor bx, bx                      ;Interrupt call 10h:
    mov ah, 12h                     ;Get EGA information.
    mov bl, 10h                     ;Returns BL > 4 if not EGA.
                                    
    int 10h                         
                                    
    cmp bl, 4                       ;BL =< 4 means we're EGA
    ja test_CGA                     ;compatible.
    jmp detected_EGA

test_CGA:
    
    int 11h                         ;Interrupt call: Equipment check.
    
    and ax, 30h                     ;Check bits 4 & 5
    
    cmp ax, 30h                     ;If bits are on, this is a mono adapter.
    jz detected_MONO
    jmp detected_CGA

;---------------------------------------------------------------------------
    
detected_MONO:
    mov dx, 0                       ;Video adapter data for game
    jmp get_current_mode
    
detected_VGA:
    mov ax, 1003h                   ;Disable blink for VGA one extra time to be sure.
    xor bx, bx                      
    int 10h 
    mov dx, 3                       ;Video adapter data for game
    mov si, vVGAregs
    mov vWrap, 32767
    jmp get_current_mode
vVGAregs:
    dw  2
    dw 03D4h, 0009h, 03D5h, 0083h 

detected_EGA:
    mov dx, 2                       ;Video adapter data for game
    mov si, vEGAregs
    mov vWrap, 32767
    jmp get_current_mode
vEGAregs:       
    dw  18
    dw 03D4h, 0006h, 03D5h, 0004h
    dw 03D4h, 0007h, 03D5h, 0011h
    dw 03D4h, 0008h, 03D5h, 0000h
    dw 03D4h, 0009h, 03D5h, 0003h
    dw 03D4h, 0010h, 03D5h, 00E1h
    dw 03D4h, 0011h, 03D5h, 0024h
    dw 03D4h, 0012h, 03D5h, 00C7h
    dw 03D4h, 0015h, 03D5h, 00E0h
    dw 03D4h, 0016h, 03D5h, 00F0h

detected_CGA:
    mov dx, 1                       ;Video adapter data for game
    mov si, vCGAregs
    mov vWrap, 16383
    jmp get_current_mode
vCGAregs:
    dw  21
    dw 03D4h, 0000h, 03D5h, 0038h
    dw 03D4h, 0001h, 03D5h, 0028h
    dw 03D4h, 0002h, 03D5h, 002Dh
    dw 03D4h, 0003h, 03D5h, 000Ah
    dw 03D4h, 0004h, 03D5h, 003Fh
    dw 03D4h, 0005h, 03D5h, 0006h
    dw 03D4h, 0006h, 03D5h, 0032h
    dw 03D4h, 0007h, 03D5h, 0038h
    dw 03D4h, 0008h, 03D5h, 0002h
    dw 03D4h, 0009h, 03D5h, 0003h
    dw 03D8h, 0008h

get_current_mode:
    mov ax, 0F00h                   ;Get current video mode
    int 10h                         ;Stored to BL
    
    mov es, [bp + 08]               ;ES = Write seg (video adapter data)
    mov di, [bp + 06]               ;DI = Write ofs (video adapter data)
    add di, 2
    stosw                           ;AL = current video mode
    
    cmp dx, 1                       ;If adapter is EGA/VGA
    ja set_EGAVGA                   ;set mode with interrupts
    jmp loopVideoRegs
    
set_EGAVGA:
    xor ax, ax
    mov al, 01h                     ;Set video mode 01h
    int 10h                         ;40x25 colour text mode
    
    mov ax, 1112h                   ;Load and activate 8x8 character
    xor bx, bx                      ;set 0
    int 10h
    
    mov ax, 1003h                   ;Select FG Blink / 16 bg colors (BL = 0)
    xor bx, bx                      ;Clear whole BX to avoid problems on some adapters.
    int 10h

loopVideoRegs:
    push dx
    mov cx, cs:[si]
    inc si
    inc si
loopRegs:
    mov dx, cs:[si]
    inc si
    inc si
    mov ax, cs:[si]
    inc si
    inc si
    out dx, al
    loop loopRegs
    pop dx
    
    mov es, vSegment
    mov di, 0
    mov ax, 11DEh
    mov cx, vWrap
    inc cx
    shr cx, 1
clearLoop:
        stosw
    loop clearLoop
;------------------------------------------------------------------------------
exit:
    mov es, [bp + 08]               ;ES = Write seg (video adapter data)
    mov di, [bp + 06]               ;DI = Write ofs (video adapter data)
    
    mov ax, dx                      ;Store detected video adapter
    stosw                           ;data at ES:[DI]
    
    pop bp
    pop si
    pop ds
    pop di
    pop es
    retf 4

This is the only idea I have so far... I'll double-check some other things too later tonight. I'll release a hopefully-fixed version ASAP and include an EXE with much more debug output to find out where it crashes (if anyone dares to try that anymore :)) . I better read up on MCGA too I guess.

Currently you might get some idea where the game crashes if you check the "DEBUG.TXT", created by the game (if it runs far enough to create it).


P.S., that has to be one of the most annoying sites to try and download something from...
I agree, although at least it doesn't have captcha. Applies to just about any free download site IMHO. I'm hoping to set up my own site too at some point, but right now I'd rather spend my limited free time on developing rather than publishing and comparing different server providers :)
Thank you for the super-helpful test on multiple configurations!

I guess the controls are aimed at an AT-style keyboard though. Ctrl + Alt are next to eachother on the left and the right there.
I have an XT style keyboard, where the Ctrl + Alt are on top of eachother, only on the left side of the keyboard.
Also, there is no separate arrow block, so only the numpad.
Perhaps you could use the '5' key on the numpad as a second 'down' key? It's rather hard to reach it now, where the down key is at '2'.
Ah, hadn't thought of that at all! Yeah, I'll check the XT-layout and see if there are some better common key layouts. Mapping both 5 and 2 for down should be easy enough. I'll get that done for the next version too then. Thanks! :)
 
When checking for a VGA-card, I'm saving the expected functionality/state data-dump to be stored in a string that isn't formatted in any way. I thought Qbasic might format every variable created, but maybe it doesn't after all (Dosbox must be doing that then)...

I'm guessing the VGA-state check is returning non-zero bytes, no matter what card you have and the game ends up detecting VGA and looping through VGA-initialization registers when it shouldn't.

An alternative way to check for VGA could be to try to initialize mode 13h through int 10h ah=0, and then check the active video mode with int 10h ah=0Fh.
If mode 13h was set, you have a VGA/MCGA-compatible adapter. Else it must be EGA or lower (you can do the same test to distinguish between CGA and EGA by using eg mode 0Dh).
Perhaps that is a more compatible way to check on older machines?
 
Perhaps you could use the '5' key on the numpad as a second 'down' key? It's rather hard to reach it now, where the down key is at '2'.

Lightweight! :) I gamed for nearly a decade with 2-4-6-8 as arrow keys, so it seems natural. But, redefinable keys is a good suggestion, since there may also be some people who grew on up Speccy who will want q-a for up/down and o-p for left/right.
 
Lightweight! :) I gamed for nearly a decade with 2-4-6-8 as arrow keys, so it seems natural.

I actually never owned an XT-keyboard until the Philips P3105 and 5160 I got a few months ago :)
My Commodore PC10-III may have been XT-class, but Commodore shipped it with a 101-key keyboard, even back in 1988.
 
It's running fine on my Zenith Z286LP Plus using vga. Good work. I'll drag my xt clone out some time and set it up for EGA and see how that goes.

It also runs fine in a full screen dos session in OS/2 v3 on my DEC LPv+ 425sx with 486DX2/66 cpu.
 

Attachments

  • IMAG0187.jpg
    IMAG0187.jpg
    83.6 KB · Views: 1
Last edited:
I don't know if this will help, but might it be faster to simply change the starting address of the screen area and depend on the CRTC "wraparound"? Maybe you're doing that, but when I was programming terminal emulator packages, the problem was how to keep up with text coming in at 19200 bps. The answer was to scroll the screen by not scrolling--just shift the starting address up by 160 bytes for each line and "wrap" all your screen references.

I apologize if that's already been discussed.
 
P.S., that has to be one of the most annoying sites to try and download something from... I almost thought I was back using DROSSDOS on a Model III. "Are you sure? Are you really sure? Are you really really sure? Well tough. Of course given the accessibility train wreck and laundry list of how not to build a website this "indieDB" garbage is, I shouldn't have been surprised some six pages deep I still wasn't downloading.
I got it downloading on the 3rd page on this Linux box, extracted the contains to a 720k floppy, walked over to the room with the 286 and 486 were in, fired up the 286, put the disk in, accessed the a: drive and ran the program. Less than 5mins tops. Hardly a traumatic experience.
 
Last edited:
Uhm... couple questions:

add bp, 8

You sure you mean to do that? By the time you get to there is the SP really off by eight? Not seen that just for a ASM include.

mov es, [bp + 08] ;ES = Write seg (video adapter data)
mov di, [bp + 06] ;DI = Write ofs (video adapter data)

couldn't you "les di" that?

clearLoop:
stosw
loop clearLoop

Is there a reason you're not using a REP on that?

mov ax, dx ;Store detected video adapter
stosw ;data at ES:[DI]

Do you really need the result in AX? (don't think so) If not, why not just do a direct MOV?

mov es, [bp + 08] ;ES = Write seg (video adapter data)
mov di, [bp + 06] ;DI = Write ofs (video adapter data)
add di, 2
stosw ;AL = current video mode

I'm not 100% familiar with how qb passes variables, but that doesnae look right to me. This may also be another case where you might be better off just doing a "mov es:[di+2], al" since you don't seem to need DI set after that point.

Wouldn't it also be better to move the register data out of the middle of the code (since it's compile location doesn't matter) so that you could then at least drop the jump from the cga detected version?

Also, I thought (might be wrong) that QBASIC didn't require ES and DI to be preserved... Though I'm so used to TP I could be wrong on that; but it would mean you could pull the jump on the CGA detected version.

Just some thoughts.
 
Also...

mov vWrap, 32767


Not sure that's declared right... shouldn't that be something like [vWrap]? or CS:[vWrap] since that's unlikely an offset into QB's data segment...
 
FYI, I measured memory usage of the program and found it used 184KB of RAM. This is after playing a few levels. This is quite reasonable; your game will run on any system with 256KB or higher, which is nearly every single remaining system.

Good to know! I'm surprised it's that much, since Dosbox shows it might be about 110KB. I wonder if it would be less if I didn't use dynamic strings... Or maybe Qbasic's FRE(-1) isn't quite reliable for doing this test (as I'm doing it).

Perhaps that is a more compatible way to check on older machines?

I remember reading something like this, but can't find the source anymore :/

I don't know if this will help, but might it be faster to simply change the starting address of the screen area and depend on the CRTC "wraparound"? Maybe you're doing that, but when I was programming terminal emulator packages, the problem was how to keep up with text coming in at 19200 bps. The answer was to scroll the screen by not scrolling--just shift the starting address up by 160 bytes for each line and "wrap" all your screen references.
I apologize if that's already been discussed.

I believe this was discussed around the first 20 messages of this thread. I'm not sure if you mean using the different wrap-points between CGA and EGA/VGA to detect the video card or about scrolling the screen. But the game does scroll the screen as you described, by changing the start address and wrapping all the drawn graphics around the buffer.

add bp, 8
You sure you mean to do that? By the time you get to there is the SP really off by eight? Not seen that just for a ASM include.

After preserving the registers (push es, di, ds, si), I've moved the SP has moved off by eight, yes. This feels pretty ugly though, but I don't really understand why. I'd rather use familiar (+6, +8, +10...) stack offsets for parameters, to keep them the same between different procedures and maintain code readability. That's the main reason I opted for just adding 8 to BP.

mov es, [bp + 08] ;ES = Write seg (video adapter data)
mov di, [bp + 06] ;DI = Write ofs (video adapter data)
couldn't you "les di" that?

I have absolutely no idea how LES works, despite reading about it from various sources. I don't think there are any performance-critical places where I would need it, so I haven't got around to learning it just yet.

clearLoop:
stosw
loop clearLoop
Is there a reason you're not using a REP on that?

Stupidity and getting so used to BLIT loops that have required wrap checks for every byte. Fixed, cheers! :)

mov ax, dx ;Store detected video adapter
stosw ;data at ES:[DI]
Do you really need the result in AX? (don't think so) If not, why not just do a direct MOV?

mov es, [bp + 08] ;ES = Write seg (video adapter data)
mov di, [bp + 06] ;DI = Write ofs (video adapter data)
add di, 2
stosw ;AL = current video mode
I'm not 100% familiar with how qb passes variables, but that doesnae look right to me. This may also be another case where you might be better off just doing a "mov es:[di+2], al" since you don't seem to need DI set after that point.

Damn, seems like I fell in love with stosw and started to use it like a zombie.

Wouldn't it also be better to move the register data out of the middle of the code (since it's compile location doesn't matter) so that you could then at least drop the jump from the cga detected version?

Also, I thought (might be wrong) that QBASIC didn't require ES and DI to be preserved... Though I'm so used to TP I could be wrong on that; but it would mean you could pull the jump on the CGA detected version.

Main reason for the register data placement was I don't know how to get an offset for data variables.
Code:
regData	dw	0000h, 0001h, 0002h, 0003h
mov ax, regData + 2
That would make AX = 0001h. But how would I get the actual memory offset of that word? With mov ax, [regData] maybe?
To get the offset in jump-table style, I needed a label instead of a variable name. Lables only work inside a procedure, so I couldn't place them outside the PROC.

According to Qbasic Programmers Guide (http://qbasic.phatcode.net/TUT/GDE/MIXLANG.HTM) SI, DI, SS, DS and BP need to be preserved. I just preserved ES just in case, maybe partly because none of my Qbasic info sources seem 100% legit ;)

mov vWrap, 32767
Not sure that's declared right... shouldn't that be something like [vWrap]? or CS:[vWrap] since that's unlikely an offset into QB's data segment...

This is how I'm using CS variables in all my PROCs, including your timer-routine. I ran into some problems when using CS:variable, but can't remember what it was about exactly... Using MASM 6.11 here.

It's running fine on my Zenith Z286LP Plus using vga. Good work. I'll drag my xt clone out some time and set it up for EGA and see how that goes.
It also runs fine in a full screen dos session in OS/2 v3 on my DEC LPv+ 425sx with 486DX2/66 cpu.

Thanks for the awesome photo! :)

I gave it a try on my Turbo XT. When I ran the program it displayed 'Divide overflow' and then some random ascii characters appeared on the screen. The computer then hung and had to be hard reset.

I also tried it a 2nd time with more free RAM as the first attempt had some LAN drivers loaded. This time I got a beep and the screen went blank (and it hung again and required a hard reset).

This crash worries me the most. It suggests that the problem isn't with incorrectly detecting VGA after all.

'Divide overflow' isn't a Qbasic error message, so something very messed up must be happening here. (I'm not saying the game isn't to blame though).
 
Here are some things I've found & fixed so far:

- VGA-state check string was unformatted and might've caused VGA detection by mistake.

- Detecting MDA / Unknown adapter would crash. (Went through register loop without initializing read offsets instead of exit).

- Sprite loader was prone to crashing, due to unsafe use of signed integer memory offset calculations. (Would most likely cause a freeze and a blank screen)

- Keyboard state save might've caused some trouble. (Not likely though)

(not available for download yet)
 
I remember reading something like this, but can't find the source anymore :/

Yea, well, it's quite simple really :)
Just set the mode as you normally do, through ah=00, then use the ah=0fh call to see if the current mode was actually set (so if you set 13h, does it return 13h as the current mode?).
If not, then apparently your video card does not know that mode.
Don't forget to switch back to textmode :)

I have absolutely no idea how LES works, despite reading about it from various sources. I don't think there are any performance-critical places where I would need it, so I haven't got around to learning it just yet.

It's very simple... lds and les do pretty much what they say: load ds/es respectively.
So if you do this:
les di, [bp+06]
It will load a far pointer from the dword at [bp+06] into es:di

So you'd only need a single instruction instead of the two movs.

But how would I get the actual memory offset of that word? With mov ax, [regData] maybe?

You can use the 'offset' keyword for that:
mov ax, offset regData + 2
For some x86 assemblers (MASM at least, and various others that adhere to Intel syntax), the [] brackets are optional when you use memory variables, so mov ax, [regData] and mov ax, regData are equivalent.
Alternatively you can use the lea instruction to get the address:
lea ax, [regData + 2]

Lea means 'Load Effective Address', and will calculate the address of the variable into a register. I always see it as the equivalent of the '&' operator in C (address-of).
 
Back
Top