;;==========================================================================;; ;; Joe Zbiciak's 4-TRIS, A "Falling Tetrominoes" Game for Intellivision. ;; ;; Copyright 2000, Joe Zbiciak, im14u2c AT primenet DOT com. ;; ;; http://www.primenet.com/~im14u2c/intv/ ;; ;;==========================================================================;; ;* ======================================================================== *; ;* This program is free software; you can redistribute it and/or modify *; ;* it under the terms of the GNU General Public License as published by *; ;* the Free Software Foundation; either version 2 of the License, or *; ;* (at your option) any later version. *; ;* *; ;* This program is distributed in the hope that it will be useful, *; ;* but WITHOUT ANY WARRANTY; without even the implied warranty of *; ;* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *; ;* General Public License for more details. *; ;* *; ;* You should have received a copy of the GNU General Public License *; ;* along with this program; if not, write to the Free Software *; ;* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. *; ;* ======================================================================== *; ;* Copyright (c) 2000, Joseph Zbiciak *; ;* ======================================================================== *; ROMW 16 ; Just for the heck of it. ORG $5000 ; Standard Mattel cartridge memory map ;------------------------------------------------------------------------------ ; Magic Constants ;------------------------------------------------------------------------------ EDGEL EQU 15 ; Left edge column EDGER EQU 24 ; Right edge column EDGEB EQU 21 ; Bottom edge row MOVEINC EQU 5 ; Per-downward-move score increment DANGER EQU 6 ; Trigger point at which music goes 2x COPYR EQU $13A ; Character # for Copyright circle-C NXTPCX EQU 33 ; X coord where next piece is shown NXTPCY EQU 11 ; Y coord where next piece is shown LVLROW EQU 4 ; Row 'Level' banner is displayed on LVLCOL EQU 2 ; Column 'Level' banner is displayed on LVLLOC EQU $200 + 20*(LVLROW - 1) + LVLCOL LINROW EQU 7 ; Row 'Lines' banner is displayed on LINCOL EQU 2 ; Column 'Lines' banner is displayed on LINLOC EQU $200 + 20*(LINROW - 1) + LINCOL NXTROW EQU (NXTPCY - 3)/2 ; Row 'Next' banner is displayed on NXTCOL EQU (NXTPCX - 3)/2 ; Column 'Next' banner is displayed on NXTLOC EQU $200 + 20*(NXTROW - 1) + NXTCOL ;------------------------------------------------------------------------------ ; Magic memory locations ;------------------------------------------------------------------------------ VBLANK EQU $20 ; Vertical-blank Handshake COLSTK EQU $21 ; Color-stack/FGBG switch CS0 EQU $28 ; Color Stack 0 CS1 EQU $29 ; Color Stack 1 CS2 EQU $2A ; Color Stack 2 CS3 EQU $2B ; Color Stack 3 CB EQU $2C ; Color for border ISRVEC EQU $100 ; ISR jump vector ; $130 .. $136 ; Save area used by DRAWPIECE/TESTPIECE TMPPC STRUCT $137 ; Temporary storage @@C EQU $ + 0 ; (R0) Color of current piece UNUSED @@X EQU $ + 1 ; (R1) Pivot X coordinate for current piece @@Y EQU $ + 2 ; (R2) Pivot Y coordinate for current piece @@N EQU $ + 3 ; (R3) Piece number for current piece. ENDS CURPC STRUCT $13B ; Current active piece @@C EQU $ + 0 ; (R0) Color of current piece @@X EQU $ + 1 ; (R1) Pivot X coordinate for current piece @@Y EQU $ + 2 ; (R2) Pivot Y coordinate for current piece @@N EQU $ + 3 ; (R3) Piece number for current piece. ENDS PXQ_L0 EQU $140 ; Putpixel Queue length -- not committed PXQ_L1 EQU $141 ; Putpixel Queue length -- committed PXQ_XY EQU $142 ;..$161 ; Putpixel Queue XY data, interleaved. VOLSV EQU $170 ;..$172 ; Volume save-area during pause PAUSED EQU $173 ; Flag saying we paused. OVRFLO EQU $1A0 ; Number of overflows observed WASDOWN EQU $1A1 ; Flag: This movement was 'down' WASHAND EQU $1A2 ; Flag: This movement was 'hand' SLEVEL EQU $1A8 ; Current game level number LEVEL EQU $1A9 ; Current game level number SFXPRIO EQU $1AA ; Priority of currently playing sound SCRPOS EQU $1AB ; Position onscreen to display the score SCRDIG EQU $1AC ; 10 - Number of digits to display in score SHOWNXT EQU $1AD ; Show next piece NXTPC EQU $1AE ; Next Piece REPEAT EQU $1AF ; Flag: Set to keypress we're repeating LHAND EQU $1B0 ; Last hand-controller input LHKPD EQU $1B1 ; Last input was pad if non-zero MUTE EQU $1B2 ; Last input was pad if non-zero CSAVE EQU $1B3 ; Global 'c' save area TSKDQ EQU $1C0 ;..$1DF ; $1C0..$1DF: Task data queue XSAVE EQU $1E0 ; Global 'x' save area YSAVE EQU $1E1 ; Global 'y' save area ;$1E2 .. $1E6, reserved DIDNXT EQU $1EB ; Next Piece was displayed some time this piece HEIGHT EQU $1EC ; Height of blocks in well. TSKACT EQU $1ED ; Number of highest numbered task + 1 TSKQHD EQU $1EE ; Head pointer for task queue (0..15) TSKQTL EQU $1EF ; Tail pointer for task queue (0..15) PSG0 EQU $1F0 ;..$1FD ; PSG base address CTRL0 EQU $1FE ; Right hand controller CTRL1 EQU $1FF ; Left hand controller MAXTSK EQU 4 ; Right now allow 4 active tasks TSKQ EQU $320 ;..$32F ; $320..$32F: Task queue TSKTBL EQU $330 ;..$33F ; $330..$33F: Task table (four tasks) SNDSTRM EQU $340 ;..$347 ; $340..$347: Sound stream table PREVBL EQU $348 ; Pre-VBLANK routine address HANDFN EQU $34A ; Keypad dispatch function table. SCRCOL EQU $34B ; Color that score is displayed in WTIMER EQU $34C ; Countdown timer for WAIT REGSV EQU $34D ;..$351 ; $34D..$351: Register save area for SLEEP PSCORL EQU $352 ; Player's score (lo 16 bits) PSCORH EQU $353 ; Player's score (hi 16 bits) DSCORL EQU $354 ; Displayed score (lo 16 bits) DSCORH EQU $355 ; Displayed score (hi 16 bits) LINES EQU $356 ; Number of lines cleared so far MSCORE EQU $358 ; Total of per-move scores TMP EQU $35D RANDLO EQU $35E ; Low word of random number generator RANDHI EQU $35F ; High word of random number generator SPEED EQU TSKTBL + 3 ; Piece movement speed (IN TASK TABLE!) GROM EQU $3000 GRAM EQU $3800 ;------------------------------------------------------------------------------ ROMHDR: WORD $0000 ; Movable object data (ignored) DECLE $01,$00 ; RTAB (ignored) WORD START ; Program start address (ignored) WORD $0000 ; Background graphics WORD ROMHDR + 2 ; Card table -- stored above in header. :-) WORD T1TLE ; Title string: '4-TRIS' DECLE $3C0 ; run title code, clicks off, INTY2 on, no ECS DECLE $00 ; -> to STIC $32 DECLE $00 ; 0 = color stack, 1 = f/b mode DECLE 0, 0, 0, 0 ; color stack elements 1 - 4 DECLE $00 ; border color ;------------------------------------------------------------------------------ ;;==========================================================================;; ;; SNDTBL ;; ;; Contains pointers and parameters for all sound effects and music. ;; ;;==========================================================================;; SNDTBL PROC @@bip DECLE BIP, $2744 ; Played when player moves a piece @@byump DECLE BYUMP, $2744 ; Played when the player places a piece @@bomb DECLE BOMB, $27CC ; Played when clearing a line @@bomb4 DECLE BOMB4, $3FFF ; Played when clearing FOUR lines @@boom2 DECLE BOOM2, $3FFF ; Played when well overflows @@ding3 DECLE DING3, $35EE ; Played to signal changing level @@title DECLE NUTMARCH, $3FFF ; Title screen music @@game DECLE CHINDNCE, $3FFF ; In-game music @@over DECLE BEHAPPY, $3FFF ; Game-over music @@silence DECLE SILENCE, $3FFF ; Silence! ENDP FXBIP EQU SNDTBL.bip - SNDTBL FXBOMB EQU SNDTBL.bomb - SNDTBL FXBOMB4 EQU SNDTBL.bomb4 - SNDTBL FXBOOM2 EQU SNDTBL.boom2 - SNDTBL FXDING3 EQU SNDTBL.ding3 - SNDTBL FXBYUMP EQU SNDTBL.byump - SNDTBL M_TITLE EQU SNDTBL.title - SNDTBL M_GAME EQU SNDTBL.game - SNDTBL M_OVER EQU SNDTBL.over - SNDTBL FX_OFF EQU SNDTBL.silence - SNDTBL ;;==========================================================================;; ;; TITLE / START ;; ;; ;; ;; This contains the title string and the startup code. We pre-empt the ;; ;; EXEC's initialization sequence by setting the "Special Copyright" bit ;; ;; in location $500C. This causes the code at 'START' to run before the ;; ;; built-in title screen is completely displayed. ;; ;; ;; ;; The Startup code does very little. Mainly, it sets the Interrupt ;; ;; Service Routine vector to point to our _real_ initialization routine, ;; ;; INIT. This is done because we can only get to GRAM and STIC registers ;; ;; during the vertical retrace, and vertical retrace is signaled by an ;; ;; interrupt. (Actually, we can have access to GRAM/STIC for longer ;; ;; if we don't hit the STIC 'handshake' at location $20, but then the ;; ;; display blanks. During INIT, the display does blank briefly.) ;; ;;==========================================================================;; TITLE: BYTE 100, "*-TRIS", 0 ; Title: 4-TRIS, Copyright 2000 ; Intercept/preempt EXEC initialization and just do our own. ; We call no EXEC routines in this game. START: MVI $1FF, R2 AND $1FE, R2 CLRR R4 ; Prepare to zero all system RAM, PSG0,& STIC. MVII #$20, R1 ; $00...$1F. (The STIC) JSRD R5, FILLZERO ADDI #8, R4 ; $28...$32. (The rest of the STIC) MVII #11, R1 CALL FILLZERO MVII #$F0, R4 ; $F0...$35D. We spare the rand seed values MVII #$26D, R1 ; in $35E..$35F to add some randomness. CALL FILLZERO COMR R2 MVO R2, LHAND ; Set up initial hand-controller state MVII #INIT, R0 ; Our initialization routine MVII #ISRVEC, R4 ; ISR vector MVO@ R0, R4 ; Write low half SWAP R0 ; MVO@ R0, R4 ; Write high half MVI RANDLO, R0 ; Get whatever garbage data is in memory in the MVI RANDHI, R1 ; random number generator fields MOVR PC, R5 ; Loop starting at the next instruction. @@spin: INCR R0 ; Seed random number generator while DECR R1 ; we wait for our first interrupt. MVO R0, RANDLO MVO R1, RANDHI EIS ; This falls through to STUB which causes ; our loop (saves a couple DECLEs.) ;;==========================================================================;; ;; STUB ;; ;; Null routine used in dispatchers where no behavior is defined/desired. ;; ;;==========================================================================;; STUB: JR R5 ; Stub routine ;;==========================================================================;; ;; INIT ;; ;; Initializes the ISR, etc. Gets everything ready to run. ;; ;; This is called via the ISR dispatcher, so it's safe to bang GRAM from ;; ;; here, too. ;; ;; ;; ;; -- Zero out memory to get started ;; ;; -- Set up variables that need to be set up here and there ;; ;; -- Set up GRAM image ;; ;; -- Drop into the main game state-machine. ;; ;;==========================================================================;; INIT: PROC DIS MVII #$2F0, R6 ; Reset the stack pointer MVI COLSTK, R1 ; Force display to color-stack mode ; Make sure random number generator is non-zero. MOVR PC, R0 ; The PC is guaranteed to be non-zero. XOR RANDLO, R0 BEQ @@randok ; If the XOR result is zero, we're ok, MVO R0, RANDLO ; otherwise make RANDLO non-zero. @@randok: ; Stub out all of the task hooks MVII #STUB, R0 ; Prepare to stub out some hooks. MVO R0, PREVBL ; Initial pre-VBL task is NULL MVII #MAINISR, R0 ; Point ISR vector to our ISR MVO R0, ISRVEC ; store low half of ISR vector SWAP R0 ; MVO R0, ISRVEC+1 ; store high half of ISR vector ; Do the face? MVII #$D9, R0 CMP $1FE, R0 BEQ DOFACE CMP $1FF, R0 BEQ DOFACE ; Default the GRAM image to be same as GROM. MVII #GROM, R5 ; Point R5 at GROM MVII #GRAM, R4 ; Point R4 at GRAM MVII #$200, R0 @@gromcopy: MVI@ R5, R1 MVO@ R1, R4 DECR R0 BNEQ @@gromcopy ; Copy our GRAM font into GRAM overtop of default. CALL LOADFONT DECLE FONT ; Ok, everything's ready to roll now. EIS ;;==================================================================;; ;; Game phases: ;; ;; -- Title screen ;; ;; -- (optional) Sound Test ;; ;; -- In Game ;; ;; -- Game over ;; ;; ;; ;; The INIT code cycles between these gae phases by first ;; ;; calling the subroutine which sets up the tasks for a given ;; ;; game phase, and then calling the main event loop which ;; ;; runs the game. This assumes that all tasks are stopped when ;; ;; the MAINLOOP exits. Calling SCHEDEXIT accomplishes a clean ;; ;; exit from MAINLOOP by scheduling an exit while stopping all ;; ;; other tasks. (Neat, eh?) ;; ;;==================================================================;; @@loop: CALL TITLESCREEN ; Set up 'Title Screen' CALL MAINLOOP ; Do title screen. MVI SLEVEL, R0 ; If starting level == 0, do 'Sound Test' TSTR R0 BEQ @@soundtest CALL INGAME ; Set up 'In Game' CALL MAINLOOP ; Do the Game. CALL GAMEOVER ; Set up the 'Game Over' sequence. CALL MAINLOOP ; Let it run. B @@loop ; Start over at the title screen. @@soundtest: CALL SOUNDTEST ; Sound-test requested. Set that up. CALL MAINLOOP ; Run the sound-test. B @@loop ; Go back to title screen when done. ENDP ;;==========================================================================;; ;; LOADFONT -- Load a compressed FONT into GRAM. ;; ;; ;; ;; Note: This should be called only when the STIC is already in CPU- ;; ;; controlled mode, such as from an interrupt handler. Loading a large ;; ;; font may cause the screen to blank for one or more frames. ;; ;; ;; ;; Font data is broken up into spans of characters that are copied ;; ;; into GRAM. Each span is defined as follows. ;; ;; ;; ;; Span Header: 2 Decles ;; ;; DECLE Skip Length (in bytes of GRAM memory) ;; ;; DECLE Span Length (in bytes of GRAM memory) ;; ;; Span Data -- up to Span Length Decles. ;; ;; ;; ;; Span Data is run-length encoded using the upper two bits of the ;; ;; decle to specify the run length. Valid run lengths are 0..3, ;; ;; with 0 meaning "just copy this byte to the GRAM", and 3 meaning ;; ;; "copy this byte to GRAM and make three more copies in the locations ;; ;; afterwards". To see what I mean, look at the font data in ;; ;; "font.asm". ;; ;; ;; ;; The run length encoding does not change the value used for ;; ;; 'span length'. The span length is always given in terms of ;; ;; # of GRAM locations, and not number of decles in the FONT data. ;; ;; ;; ;; The font is terminated with a span of length 0. ;; ;; ;; ;; INPUTS: ;; ;; R5 -- Points to word (1 decle in 16-bit ROM) containing ptr to font ;; ;; info. Code returns after this word. ;; ;; ;; ;; OUTPUTS: ;; ;; R0, R1, R4, R5 trashed. ;; ;; GRAM is updated according to the font specification. ;; ;; ;; ;;==========================================================================;; LOADFONT PROC MVI@ R5, R0 PSHR R5 MOVR R0, R5 MVII #GRAM, R4 ; Point R4 at GRAM @@gramloop: MVI@ R5, R0 ; Get skip & span len. (in GRAM bytes) TSTR R0 ; Quit if skip/span == 0. BEQ @@gramdone MOVR R0, R1 ANDI #$7F8, R0 ; Extract span length. XORR R0, R1 ; Clear away span bits from word SWAP R1 ; Extrack skip value. ADDR R1, R4 ; Skip our output pointer. SLR R0, 1 ; Divide count by 2. @@charloop: MVI@ R5, R1 ; Get two bytes MVO@ R1, R4 ; Put the first byte SWAP R1 ; Put the other byte into position MVO@ R1, R4 ; Put the second byte DECR R0 ; Sheesh, do I have to spell this out? BNEQ @@charloop ; inner loop B @@gramloop ; outer loop @@gramdone: PULR PC ENDP ;;==========================================================================;; ;; MAINISR ;; ;; This is the main interrupt service routine. It has to perform the ;; ;; following tasks: ;; ;; ;; ;; -- Run any miscellaneous "pre-VBLANK" task requested by main program. ;; ;; -- Hit the vertical blank handshake to keep screen enabled. ;; ;; -- Update any active sound streams. ;; ;; -- Drain and pixels queued up in the Pixel Queue. ;; ;; -- Count down task timers and schedule tasks when timers expire. ;; ;; -- Count down the 'busy-wait' timer if it is set. ;; ;; -- Detect a "Pause" request and pause the game if needed. ;; ;; ;; ;; This particular program currently does not sequence GRAM at all after ;; ;; the initial bootup. Therefore, we can hit the VBLANK handshake almost ;; ;; immediately after entering the ISR. We do support a pre-VBLANK ;; ;; routine though for other purposes, such as setting color-stack regs. ;; ;; ;; ;; Our code does not rely on any EXEC routines at all, except the ISR ;; ;; dispatch routine which saves and restores all the registers. (On a ;; ;; real Intellivision, we have no choice. On an emulator, that routine ;; ;; can be replaced with a separate non-EXEC routine which performs a ;; ;; similar dispatch, if we want to distribute this program without an ;; ;; EXEC ROM image but with an emulator.) ;; ;; ;; ;;==========================================================================;; MAINISR PROC PSHR R5 ; Save return address ;; Call out to user-defined pre-VBLANK routine. ;; This routine should point to the stub if no routine is to ;; be called. (simplifies the code w/out really slowing it down.) MVII #@@rl0, R5 ; Point to @@rl0 MVI PREVBL, PC ; Call subroutine @@rl0: ;; Hit the VBLANK handshake now MVO R0, $20 ; Allow STIC to display this frame. ;; Update sound streams. All sound streams update at 60Hz. ;; Stream #0 can update at "60Hz * 2" if double-speed mode is on. CALL SNDPRIO ; Make sure music & sfx play nice ; Update music CALL DOSNDSTREAM ; Update the sound stream DECLE SNDSTRM ; Point to the music stream ; Update sfx CALL DOSNDSTREAM ; Update the sound stream DECLE SNDSTRM+4 ; Point to the sfx stream ; If well is too tall, play music fast. MVII #DANGER, R0 CMP HEIGHT, R0 BLT @@slowmusic MVII #SNDSTRM, R4 CALL DOSNDSTREAM ; Update the sound stream DECLE SNDSTRM ; Point to the music stream @@slowmusic: MVII #RANDHI,R1 MVI SNDSTRM+1, R2 XOR@ R1, R2 MVO@ R2, R1 ; Throw some bits into the random bucket ;; Drain any pixels that were queued and committed. CALL DRAINPXQ ;; Count down task timers and schedule tasks. We have up to MAXTSK ;; active tasks at one time. We decrement all counters by two. ;; -- If the count is initially <= 0, the task is inactive and ;; is skipped. ;; -- If the count goes to zero, the task is triggered and its ;; count is reinitialized. ;; -- If the count goes negative, the task is triggered and its ;; count is not reinitialized. ;; This allows us to have one-shot and repeating tasks pretty ;; easily. Repeating tasks clear bit 0 of their period count, ;; and one-shot tasks set bit 0. ;; ;; When tasks are triggered, their procedure address is written ;; to the task queue which is drained by the non-ISR main task. ;; The task queue is a small circular buffer in 16-bit memory with ;; head and tail pointers in 8-bit memory. The circular buffer is ;; 16 entries large. I hope this is enough. Overflow can cause ;; strange effects. ;; ;; Task control table entry layout: ;; ;; Word 0: Function Ptr ;; Word 1: Instance data ;; Word 2: Current down-count ;; Word 3: Reinit count MVI TSKACT, R0 ; Iterate over active tasks TSTR R0 BEQ @@notasks MVII #TSKTBL+2, R3 ; Point to the task control table @@taskloop: MVI@ R3, R1 ; Get tasks's current tick count TSTR R1 ; Is it <= 0? BLE @@nexttask ; ... yes? Skip it. MOVR R1, R2 ADD RANDLO, R2 MVO R2, RANDLO ; Throw some bits into the random bucket SUBI #2, R1 ; Count it down. BNEQ @@noreinit ; If it went to zero, reinit count INCR R3 MVI@ R3, R1 ; Get new period for task DECR R3 SUBR R2, R2 ; Z = 1, S = 0 (causes the BGT to not be taken) @@noreinit: MVO@ R1, R3 ; Store new period. BGT @@nexttask ; If this count didn't expire, ... ; ... go to the next task @@q: ; Schedule this task by adding it to the task queue MOVR R3, R4 SUBI #2, R4 MVI@ R4, R2 ; Get task function pointer MVI TSKQHD, R1 ; Get task queue head INCR R1 ; Move to next slot ANDI #$F, R1 ; Stay within 16-entry circ buffer CMP TSKQTL, R1 BEQ @@overflow ; Drop this task if queue overflows. (!) MOVR R1, R5 ADDR R1, R5 ; R5 is for task data queue ADDI #TSKDQ, R5 ; Point to task data queue MVO R1, TSKQHD ; Store updated task queue head ADDI #TSKQ, R1 ; Point to actual task queue MVO@ R2, R1 ; Store the function pointer MVI@ R4, R2 ; Get task instance data MVO@ R2, R5 ; Store low half of instance data SWAP R2, 1 MVO@ R2, R5 ; Store high half of instance data @@nexttask: ADDI #4, R3 ; Point to next task struct DECR R0 BNEQ @@taskloop ; Loop until done with all tasks B @@notasks @@overflow: MVI OVRFLO, R0 INCR R0 MVO R0, OVRFLO @@notasks: @@wtimer: ;; Count down the wait-timer, if there is one MVI WTIMER, R5 DECR R5 BMI @@expired MVO R5, WTIMER @@expired: ;; Check to see if the user has requested a pause. MVII #$5A, R0 ; Keypad 1 + Keypad 9 CMP $1FE, R0 ; Pause on first controller? BEQ @@pausing CMP $1FF, R0 ; Pause on second controller? BEQ @@pausing PULR PC ; Return @@pausing: DIS ; Disable interrupts while paused. ; Grab PSG volume settings aside and force volume to zero. MVII #VOLSV, R2 MVII #$1FB, R3 MOVR R2, R5 MOVR R3, R4 MVO R4, PAUSED ; Set 'we paused' flag MVI@ R4, R0 ; Channel a MVO@ R0, R5 MVI@ R4, R0 ; Channel b MVO@ R0, R5 MVI@ R4, R0 ; Channel c MVO@ R0, R5 CLRR R1 MOVR R3, R4 MVO@ R1, R4 MVO@ R1, R4 MVO@ R1, R4 CLRC CALL @@waithand ; Wait for both controllers to be released. SETC CALL @@waithand ; Wait for either controller to be pressed CLRC CALL @@waithand ; Wait for both controllers to be released. MOVR R3, R5 MOVR R2, R4 MVI@ R4, R0 ; Channel a MVO@ R0, R5 MVI@ R4, R0 ; Channel b MVO@ R0, R5 MVI@ R4, R0 ; Channel c MVO@ R0, R5 EIS PULR PC @@waitpress: BNEQ @@waitdone @@waithand: MVII #100, R1 ; Debounce factor @@waitloop: MVII #$1FE, R4 SDBD MVI@ R4, R0 COMR R0 BC @@waitpress BNEQ @@waithand @@waitdone: DECR R1 BNEQ @@waitloop JR R5 ENDP ;;==========================================================================;; ;; MAINLOOP ;; ;; The main loop runs asynchronously from the MAINISR, running tasks as ;; ;; they get scheduled. There is also a background task that runs when ;; ;; nothing else is in the queue. ;; ;; ;; ;; -- Grab task from task queue, if any ;; ;; -- run that task ;; ;; -- look for next task ;; ;; -- run background task once if no tasks available ;; ;; ;; ;; The background task scans hand controllers. I do this because ;; ;; hand-controller scanning could be fairly expensive. Also, scanning ;; ;; hand controllers may cause new tasks to get scheduled (eg. the triggers ;; ;; for various hand controller events) and I don't want to overrun the ;; ;; task queue. ;; ;; ;; ;; The background task also updates the random number generator state. I ;; ;; do this to make the random numbers a bit more random since the game ;; ;; loading varies according to what's going on. ;; ;;==========================================================================;; MAINLOOP PROC PSHR R5 ; Save return address, since any ; task is allowed to exit the main ; game loop, say, when switching ; between game phases. ; ; There are three game phases: ; -- Title Screen ; -- In Game ; -- Game Over. MVII #EDGEB, R1 ; Set the well height so that music MVO R1, HEIGHT ; plays correctly MVO R1, MUTE ; Also, un-mute music. @@loop: MOVR PC, R5 ; Set our return address to @@loop DECR R5 ; This is 2 decles vs. 4 for SDBD/MVII ; Run next scheduled task from queue DIS ; Shut off interrupts MVI TSKQTL, R1 ; Get the tail of the queue CMP TSKQHD, R1 ; Are there tasks in the queue? BEQ @@bktsk ; No: Do background task INCR R1 ; Pop task from queue by ANDI #$F, R1 ; ... moving the queue tail MVO R1, TSKQTL ; Store new task-queue tail MOVR R1, R4 ADDR R4, R4 ADDI #TSKDQ, R4 SDBD MVI@ R4, R2 ; Load instance data for task ADDI #TSKQ, R1 ; Point to task queue MVI@ R1, R0 ; Get function pointer from queue ; Dispatch to user's task. Make sure interrupts are enabled again EIS ; Done with critical section JR R0 ; Jump to subroutine ; Note that this implicitly loops back to @@oloop here @@bktsk: EIS ; Done with critical section CALL NEXTRAND ; Update random number state CALL SCANHAND ; Scan the hand controllers. EC_LOC EQU $CF00 EC_MAG EQU $69 EC_POLL EQU $CF01 MVI EC_LOC, R0 CMPI #EC_MAG,R0 BNEQ @@loop CALL EC_POLL B @@loop ; Run around the inner loop ENDP ;;==========================================================================;; ;; QTASK ;; ;; Add a task to the task queue. ;; ;; NOTE: CALL THIS WITH INTERRUPTS OFF!!!! (eg. use JSRD). Interrupts ;; ;; will be enabled before return. ;; ;; ;; ;; INPUTS: ;; ;; R0 -- Task function pointer ;; ;; R1 -- Task instance data ;; ;; R5 -- Return address. ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- intact. ;; ;; R1 -- swapped ;; ;; R2, R4 -- trashed. ;; ;;==========================================================================;; QTASK PROC MVI TSKQHD, R2 ; Get head of task queue INCR R2 ; Move to next queue slot ANDI #$F, R2 ; Stay in circular buffer CMP TSKQTL, R2 ; Are we overflowing the queue? BEQ @@overflow ; Yes ... don't overflow it! MVO R2, TSKQHD ; Store new task queue head MOVR R2, R4 ; Generate instance data pointer ADDR R2, R4 ; (remember mult by two for data ptr) ADDI #TSKQ, R2 ; Point into task queue MVO@ R0, R2 ; Store function pointer to task q ; moved addi for STIC interruptibility fix ADDI #TSKDQ, R4 ; Point into task data queue MVO@ R1, R4 ; Store instance data to data q SWAP R1 ; .... cont'd MVO@ R1, R4 ; .... cont'd EIS ; Re-enable ints after crit section JR R5 ; Return. @@overflow: MVI OVRFLO, R2 INCR R2 MVO R2, OVRFLO EIS ; Re-enable ints after crit section JR R5 ; Return. ENDP ;;==========================================================================;; ;; SCHEDEXIT ;; ;; Flushes the task queue and schedules the exit task. ;; ;; ;; ;; INPUTS: ;; ;; R5 -- Return address. ;; ;;==========================================================================;; SCHEDEXIT: PROC PSHR R5 ; Push return address. JSRD R5,STOPALLTASKS ; Flush the pending task queue MVII #@@exit, R0 ; Schedule the exit task ... PULR R5 B QTASK ; ... and chain the return. @@exit: PULR PC ENDP ;;==========================================================================;; ;; STOPALLTASKS ;; ;; Stops all tasks by setting the task count to zero and by ;; ;; flushing the task queue (sets head/tail to 0). Returns old task count. ;; ;; NOTE: CALL THIS WITH INTERRUPTS OFF!!!! (eg. use JSRD) Interrupts ;; ;; are NOT re-enabled by this function. ;; ;; ;; ;; INPUTS: ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- Previous TSKACT ;; ;; R1 -- Cleared ;; ;; All tasks stopped. ;; ;;==========================================================================;; STOPALLTASKS PROC MVI TSKACT, R0 CLRR R1 MVO R1, TSKACT MVO R1, TSKQHD MVO R1, TSKQTL DECR R1 MVO R1, TSKTBL+2 ; period == -1 for task 0 MVO R1, TSKTBL+6 ; period == -1 for task 1 MVO R1, TSKTBL+10 ; period == -1 for task 2 NOP ; (interruptible, for STIC) MVO R1, TSKTBL+14 ; period == -1 for task 3 JR R5 ENDP ;;==========================================================================;; ;; STARTTASK ;; ;; Puts a task into a task slot for general timer-based scheduling ;; ;; ;; ;; INPUTS: ;; ;; R5 -- Invocation record: ;; ;; DECLE -- Task number ;; ;; DECLE -- Task function pointer ;; ;; DECLE -- Task initial period * 2. If bit 0==1, this is one-shot task. ;; ;; DECLE -- Task period re-init. Useful if first delay!=recurring delay. ;; ;; R2 -- Task instance data (optional) ;; ;; ;; ;; OUTPUTS: ;; ;; Task record is set up. (User still needs to update TSKACT to make ;; ;; the task records active, if necessary.) ;; ;; ;; ;; Registers R0, R4 are trashed. ;; ;;==========================================================================;; STARTTASK PROC MVI@ R5, R0 SLL R0, 2 ; R0 = R0 * 4 (four words/entry) MVII #TSKTBL,R4 ; R4 = &TSKTBL[0] ADDR R0, R4 ; R4 = &TSKTBL[n] DIS ; Entering critical section. MVI@ R5, R0 ; Get Function pointer MVO@ R0, R4 ; ... and write it MVO@ R2, R4 ; Write task instance data MVI@ R5, R0 ; Get task period MVO@ R0, R4 ; ... and write it MVI@ R5, R0 ; Get task period reinit MVO@ R0, R4 ; ... and write it EIS JR R5 ENDP ;;==========================================================================;; ;; STOPTASK ;; ;; Stops a given task by setting its period to -1. Task can be ;; ;; retriggered/restarted by calling RETRIGGERTASK. ;; ;; ;; ;; INPUTS: ;; ;; R3 -- Task number. ;; ;; ;; ;; OUTPUTS: ;; ;; Task is disabled. ;; ;; R0 == -1, R3 points to task delay count. ;; ;;==========================================================================;; STOPTASK PROC SLL R3, 2 ; R3 = R3 * 4 (four words/entry) ADDI #TSKTBL+2, R3 ; R3 = &TSKTBL[n].delay CLRR R0 DECR R0 ; R0 == -1. MVO@ R0, R3 ; TSKTBL[n].delay = -1 JR R5 ENDP ;;==========================================================================;; ;; RETRIGGERTASK ;; ;; Restarts a task by copying its delay re-init field to its delay field. ;; ;; Can be used on one-shot tasks or tasks which have been stopped with ;; ;; 'StopTask'. ;; ;; ;; ;; INPUTS: ;; ;; R3 -- Task number. ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- Task delay ;; ;; R3 -- points to task delay count ;; ;;==========================================================================;; RETRIGGERTASK: PROC SLL R3, 2 ; R3 = R3 * 4 (four words/entry) ADDI #TSKTBL+3, R3 ; R3 = &TSKTBL[n].reload DIS MVI@ R3, R0 ; Get reload value @@chain: DECR R3 ; Offset 2 is delay count MVO@ R0, R3 EIS JR R5 ENDP ;;==========================================================================;; ;; WAIT ;; ;; Busy-waits for the number of ticks specified in R0. ;; ;; ;; ;; INPUTS: ;; ;; R0 -- Number of ticks to wait ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- cleared ;; ;;==========================================================================;; WAIT PROC MVI@ R5, R0 MVO R0, WTIMER CLRR R0 @@loop: CMP WTIMER, R0 BNEQ @@loop JR R5 ENDP ;;==========================================================================;; ;; SLEEP ;; ;; Causes a one-shot process to sleep by scheduling a second one-shot ;; ;; process to reawake it. This is VERY DIFFICULT TO USE, and can be ;; ;; used by ONE PROCESS ONLY at a time. ;; ;; ;; ;; SPAWN ;; ;; Same as sleep, only the sleeping task isn't a one-shot. It keeps ;; ;; retriggering automatically at the designated interval, or can be ;; ;; retriggered manually if the period is odd. ;; ;; ;; ;; INPUTS: ;; ;; R5 -- Invocation record, in following format: ;; ;; Period/sleep time 1 Decle (Must be ODD for SLEEP) ;; ;; Process # to use 1 Decle ;; ;; Top of Stack -- Return address from _this_ process back to scheduler. ;; ;; Task will wake at address after invocation record. ;; ;; ;; ;; OUTPUTS: ;; ;; R0, R1, R2, R3, R4 restored ;; ;; R5 points to your return address ;; ;; Top of stack has return address for scheduler ;; ;;==========================================================================;; SLEEP: PROC SPAWN: MVO R4, REGSV+4 ; Save R4 MVII #REGSV, R4 MVO@ R0, R4 ; Save R0 MVO@ R1, R4 ; Save R1 MVO@ R2, R4 ; Save R2 MVO@ R3, R4 ; Save R3 MVI@ R5, R2 ; Get sleep time MVI@ R5, R3 ; Get PID SLL R3, 2 ; R3 = R3 * 4 (four words/entry) MVII #TSKTBL,R4 ; R4 = &TSKTBL[0] ADDR R3, R4 ; R4 = &TSKTBL[n] MVII #@@wake,R0 MVO@ R0, R4 ; Address to wake up at MVO@ R5, R4 ; Instance data (R2 restore) MVO@ R2, R4 MVO@ R2, R4 PULR PC @@wake: PSHR R5 ; Remember return address MOVR R2, R5 MVII #REGSV, R4 ; Point to register save area MVI@ R4, R0 ; Restore R0 MVI@ R4, R1 ; Restore R1 MVI@ R4, R2 ; Restore R2 MVI@ R4, R3 ; Restore R3 MVI@ R4, R4 ; Restore R4 JR R5 ; Go there ENDP ;;==========================================================================;; ;; DOFACE ;; ;;==========================================================================;; DOFACE PROC DIS MVII #$2F0, SP MVII #@@loadface, R0 MVO R0, PREVBL SWAP R0 MVO R0, PREVBL+1 CLRR R0 MVII #$F0, R1 MVII #$200, R4 CALL FILLMEM MVII #$200 + 8 + 20, R4 MVII #FACE, R5 MVII #9, R0 MVO R0, CB MVO R0, CS0 @@f_oloop: MVII #6, R2 @@f_iloop: MVI@ R5, R1 MVO@ R1, R4 DECR R2 BNEQ @@f_iloop ADDI #14, R4 DECR R0 BNEQ @@f_oloop CALL DRAWSTRING3 DECLE 0, $200 + 20*10 ;01234567890123456789 BYTE $4A, $6F, $65, $20, $5A, $62, $69, $63, $69, $61, $6B BYTE $20, $73, $61, $79, $73, $20, $48, $49, $21, $0A, $00 EIS DECR PC @@loadface: MVII #@@shunt, R0 MVO R0, PREVBL SWAP R0 MVO R0, PREVBL+1 MVII #9, R0 MVO R0, CS0 MVO R0, CB CALL LOADFONT DECLE FACEFONT @@shunt: MVO R0, $20 PULR PC ENDP ;;==========================================================================;; ;; Colored squares pixel-manipulation routines. ;; ;; J. Zbiciak, July 1999 ;; ;;==========================================================================;; ; Data tables for pixel routines MSKTBL: DECLE $0007, $0038, $01C0, $2600 CLRTBL: DECLE $0000, $0249, $0492, $06DB DECLE $2124, $236D, $25B6, $27FF ;;==========================================================================;; ;; PIXCALC -- Pixel Calculation: Convert x,y into addr,mask ;; ;; ;; ;; INPUTS: ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R1 -- Pixel mask ;; ;; R2 -- Address in display memory. ;; ;; R3, R4 -- Clobbered. ;; ;;==========================================================================;; PIXCALC PROC SLL R2, 1 ; y' = y << 1 MOVR R2, R4 ; mskidx = y << 1 ANDI #2, R4 ; mskidx = (y << 1) & 2 SUBR R4, R2 ; y' = (y & ~1) << 1 MOVR R2, R3 ; save y' SLL R2, 2 ; y'' = (y & ~1) << 3 ADDR R3, R2 ; y''' = y' + y'' = (y >> 1) * 20 SARC R1, 1 ; c = x & 1; x' = x >> 1 ADCR R4 ; mskidx = ((y << 1) & 2) + (x & 1) ADDR R1, R2 ; ofs = x' + y''' = (x >> 1) + (y >> 1) * 20 ADDI #$200, R2 ; ptr = ofs + $200 = &BACKTAB[ofs] ==> R2 ADDI #MSKTBL,R4 ; mskptr = MSKTBL + mskidx' = &MSKTBL[mskidx] MVI@ R4, R1 ; mask = *mskptr ==> R1 JR R5 ; return ENDP ;;==========================================================================;; ;; PUTPIXEL -- Put pixel at x, y coordinates with color 'c' (0-7) ;; ;; ;; ;; INPUTS: ;; ;; R0 -- Color ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R1 -- Word stored at R2 ;; ;; R2 -- Display memory address ;; ;; R0, R3, R4, R5 -- Clobbered. ;; ;;==========================================================================;; PUTPIXEL PROC PSHR R5 ; Save return address MVII #PUTPIXELR+1,R5 ; Return to actual pixel draw below B PIXCALC ; Convert x,y into mask,addr ;;==========================================================================;; ;; PUTPIXELR -- Put pixel 'raw': accepts address, mask, and color ;; ;; (Alternate entry point for PUTPIXEL.) ;; ;; ;; ;; INPUTS: ;; ;; R0 -- Color ;; ;; R1 -- Pixel mask ;; ;; R2 -- Display memory address ;; ;; ;; ;; OUTPUTS: ;; ;; R1 -- Word stored at R2 ;; ;; R2 -- Display memory address ;; ;; R0,R5 -- Clobbered. ;; ;;==========================================================================;; PUTPIXELR: PSHR R5 ; Save return address MOVR R0, R5 ADDI #CLRTBL,R5 ; clrptr = CLRTBL + c' = &CLRTBL[c] MVI@ R5, R0 ; color = CLRTBL[c] ANDR R1, R0 ; color = color & mask (select desired pix) COMR R1 ; invert mask AND@ R2, R1 ; pix = BACKTAB & ~mask (clear pixel) ADDR R0, R1 ; pix' = pix + color (merge pixels) MVO@ R1, R2 ; BACKTAB[ofs] = pix' (write pixels) PULR PC ; return. ENDP ;;==========================================================================;; ;; PIXELCLIP ;; ;; Returns with C==1 if pixel is onscreen ;; ;; ;; ;; INPUTS: ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; Carry == 1 if onscreen, 0 if offscreen. ;; ;;==========================================================================;; PIXELCLIP PROC TSTR R1 ; Off left? BMI @@offscr ; Yes: Set carry and exit TSTR R2 ; Off top? BMI @@offscr ; Yes: Set carry and exit CMPI #39, R1 ; Off right? BGT @@offscr ; Yes: Set carry and exit CMPI #23, R2 ; Off bottom? BGT @@offscr ; Yes: Set carry and exit SETC ; No to all: Clear carry and exit JR R5 @@offscr CLRC JR R5 ENDP ;;==========================================================================;; ;; GETPIXELS -- Calls GETPIXEL, but saves/restores X, Y ;; ;; ;; ;; INPUTS: ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R5 -- Return addr. ;; ;; ;; ;; OUTPUTS ;; ;; R0 -- Color ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R3, R4, R5 -- Clobbered. ;; ;;==========================================================================;; GETPIXELS PROC PSHR R5 @@chain: MVO R1, XSAVE MVO R2, YSAVE CALL GETPIXEL MVI XSAVE, R1 MVI YSAVE, R2 PULR PC ENDP ;;==========================================================================;; ;; PUTPIXELS -- Calls PUTPIXEL, but saves/restores X, Y ;; ;; ;; ;; INPUTS: ;; ;; R0 -- Color ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R5 -- Return addr. ;; ;; ;; ;; OUTPUTS ;; ;; R0 -- Color ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R3, R4, R5 -- Clobbered. ;; ;;==========================================================================;; PUTPIXELS PROC PSHR R5 @@chain: MVO R1, XSAVE MVO R2, YSAVE MVO R0, CSAVE CALL PUTPIXEL MVI CSAVE, R0 MVI XSAVE, R1 MVI YSAVE, R2 PULR PC ENDP ;;==========================================================================;; ;; GETPIXELSC ;; ;; Calls GETPIXELS or returns R0 unchanged if pixel is off screen ;; ;; ;; ;; INPUTS: ;; ;; R0 -- Color to return if off-screen ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R5 -- Return addr. ;; ;; ;; ;; OUTPUTS ;; ;; R0 -- Color ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R3, R4, R5 -- Clobbered. ;; ;;==========================================================================;; GETPIXELSC PROC PSHR R5 ; Save return address CALL PIXELCLIP ; See if pixel is onscreen ADCR PC ; If it is, then skip the return PULR PC ; Otherwise return. B GETPIXELS.chain ; If onscreen chain over to GETPIXELS ENDP ;;==========================================================================;; ;; GETPIXEL -- Get pixel at x, y address ;; ;; ;; ;; INPUTS: ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- Color ;; ;; R2 -- Display memory address ;; ;; R1, R3, R4, R5 -- Clobbered. ;; ;;==========================================================================;; GETPIXEL PROC PSHR R5 ; Save return address MVII #GETPIXELR+1,R5 ; Return to actual pixel read below B PIXCALC ; Convert x,y into mask,addr ;;==========================================================================;; ;; GETPIXELR -- Get pixel 'raw': accepts address, mask ;; ;; (Alternate entry point for getpixel) ;; ;; INPUTS: ;; ;; R1 -- Pixel mask ;; ;; R2 -- Display memory address ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- Color ;; ;; R2 -- Display memory address ;; ;; R1, R5 -- Clobbered. ;; ;;==========================================================================;; GETPIXELR: PSHR R5 ; Save return address MVI@ R2, R0 ; pix = BACKTAB[ofs] ANDR R1, R0 ; pix' = pix & mask MOVR R0, R1 ; SWAP R0, 1 ; ANDI #$20, R0 ; ADDR R1, R0 ; merge oddball bit from lower right pixel SLL R1, 2 ; SWAP R1, 1 ; ADDR R1, R0 ; Fold upper, lower pairs of pixels MOVR R0, R1 ; SLR R0, 1 ; SLR R0, 2 ; ADDR R1, R0 ; Fold left, right pixels ANDI #7, R0 ; Select remaining pixel PULR PC ; return ENDP ;;==========================================================================;; ;; PUTPIXELSCQ ;; ;; ;; ;; Queues a pixel for display if it's onscreen. This routine does not ;; ;; draw the pixel directly, but rather puts it in a queue of pixels that ;; ;; are waiting to be drawn. This queue is emptied during the interrupt ;; ;; service routine after the queue is "committed" for display with a call ;; ;; to COMMITPXQ. ;; ;; ;; ;; The pixel queue serves as a mechanism for reducing/eliminating the ;; ;; flicker associated with redrawing a game piece. Without the queue, ;; ;; piece movements could result in a significant amount of flicker. ;; ;; To further reduce flicker, the queue insert routine attempts to ;; ;; optimize multiple 'PUTPIXELs' to the same location, with the last ;; ;; PUTPIXEL taking precedence. ;; ;; ;; ;; Because this queue is intended for drawing game pieces only, it is ;; ;; not very large. No checking is performed for queue overflow, either. ;; ;; Also, because double-buffering is not performed, this routine will ;; ;; block if there is a committed pixel queue waiting to be drawn. ;; ;; ;; ;; INPUTS: ;; ;; R0 -- Color ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; ;; ;; OUTPUTS ;; ;; R0 -- Color ;; ;; R1 -- X coordinate ;; ;; R2 -- Y coordinate ;; ;; R3, R4, R5 -- Clobbered. ;; ;;==========================================================================;; PUTPIXELSCQ PROC PSHR R5 ; Save return address CALL PIXELCLIP ; See if pixel is onscreen ADCR PC ; If it is, then skip the return PULR PC ; Otherwise return. ; Merge Y coordinate and color up-front, since we store ; them merged in the queue. MOVR R0, R3 ; Work on a copy of our color SWAP R3 SLR R3, 2 SLR R3, 1 ; R3 = color << 5 XORR R3, R2 ; Put color in bits 5..7 of R2 DIS ; Critical section: Queue management. ; First, check to see if any pixels are queued at all. If there ; aren't any, just jump straight to the queue-insert. MVI PXQ_L0, R3 TSTR R3 BEQ @@insert_eis ; Next, check to see if there are any committed pixels waiting ; to be drawn in the pixel queue. Wait until the queue is drained. CLRR R3 CMP PXQ_L1, R3 ; If there are no committed pixels, we EIS ; Interrupts can be enabled now. BEQ @@check ; still need to check for redundant pixels. @@drain: CMP PXQ_L1, R3 ; Otherwise, let the queue drain and jump BNEQ @@drain ; straight to the insert since it's empty B @@insert @@check: ; If we reach here, we have a non-empty pixel queue, and no ; committed pixels. Step through the queue to see if we have ; any redundant pixels. MVII #PXQ_XY - 1, R4 ; Point at start of Queue minus 1 MVI PXQ_L0, R3 ; Get queue length @@notx: INCR R4 ; Where we branch when X didn't match. @@noty: DECR R3 ; Decrement loop count BMI @@insert ; Get out of here when it expires. @@rloop: CMP@ R4, R1 ; Compare X coords BNEQ @@notx ; X miscompared. MOVR R2, R5 XOR@ R4, R5 ANDI #$1F, R5 ; Cmp Y coords (lower 5 bits of queue entry) BNEQ @@noty ; Y miscompared ; If we get here, the coordinates match, so just update the color ; and exit the queue insertion process entirely. DECR R4 B @@styc ; Store Y coordinate and color, and leave @@insert_eis: EIS @@insert: ; If we get here, then this is not a duplicate pixel, so we need ; to extend the queue. Warning: No bounds checking is done here. MVI PXQ_L0, R3 MOVR R3, R4 ; Copy the old queue length to R4 ADDR R3, R4 ; Multiply it by 2 INCR R3 MVO R3, PXQ_L0 ADDI #PXQ_XY, R4 ; Point R4 at pixel queue @@stx: MVO@ R1, R4 ; Store the X coordinate @@styc: MVO@ R2, R4 ; Store the Y coordinate and color ANDI #$1F, R2 ; Strip the color from the Y + color. PULR PC ; Return. ENDP ;;==========================================================================;; ;; DRAINPXQ ;; ;; Drains the pixel queue (intended to be called from an ISR). ;; ;; ;; ;; INPUTS: ;; ;; R5 -- Return address ;; ;; Pixels in the committed pixel queue ;; ;; ;; ;; OUTPUTS: ;; ;; R0..R5 clobbered. ;; ;;==========================================================================;; DRAINPXQ PROC PSHR R5 DIS ; Critical section: Grab committed pixels. MVI PXQ_L1, R3 TSTR R3 BEQ @@leave CLRR R1 MVO R1, PXQ_L0 ; Clear count of queued pixels. MVO R1, PXQ_L1 ; Clear count of comitted pixels. EIS MVII #PXQ_XY, R4 @@pixloop: MVI@ R4, R1 ; Get X coordinate MVI@ R4, R2 ; Get Y coordinate and color MOVR R2, R0 ; Separate Y coordinate and color SLL R0, 2 SLL R0, 1 SWAP R0 ; Color is now in 3 lsbs. ANDI #$7, R0 ; Mask away undesired bits in color ANDI #$1F, R2 ; Mask away undesired bits in Y coord. PSHR R3 PSHR R4 CALL PUTPIXEL ; Display the pixel PULR R4 PULR R3 DECR R3 ; Decrement our loop count BNEQ @@pixloop ; Keep looping until we've displayed them all @@leave: EIS PULR PC ENDP ;;==========================================================================;; ;; COMMITPXQ ;; ;; Commits queued pixels in the pixel queue to be displayed. ;; ;; ;; ;; INPUTS: ;; ;; R5 -- Return address ;; ;; Pixels in the pixel queue. ;; ;; ;; ;; OUTPUTS: ;; ;; Pixels are committed for display. ;; ;;==========================================================================;; COMMITPXQ PROC PSHR R5 ; Save return address on stack. DIS ; Disable interrupts (critical section) MVI PXQ_L1, R5 ; See if there are already committed pixels TSTR R5 ; ... waiting to be drawn. (oops!) BNEQ @@leave ; Yes.. then leave. MVI PXQ_L0, R5 ; No? Get our count of non-committed pixels. MVO R5, PXQ_L1 ; Store it to our count of committed pixels. @@leave: EIS ; Done with critical section. PULR PC ; Return to caller ENDP ;;==========================================================================;; ;; PIECE ;; ;; The Piece Shape Table ;; ;; ;; ;; Shape encoding for the game pieces ;; ;; ;; ;; The game pieces are encoded as a series of moves from the "center ;; ;; point". Blocks are placed _after_ each move. All pieces are ;; ;; composed of four moves except piece 6 (the T) which requires 5 ;; ;; moves, and piece 5 which uses an extra move so that it is centered ;; ;; in the "next piece" box. ;; ;; ;; ;; Each move is encoded in two bits. The LS bit specifies the axis ;; ;; (up/down or left/right), and the MS bit specifies the sign of ;; ;; the direction on that axis (1==+1 and 0==-1). Moves are processed ;; ;; from the LS bit pair up to the MS bit pair. This encoding allows ;; ;; each piece to be described in exactly one decle. ;; ;; ;; ;; NOTE: The fifth move may NOT be "up". The rotation code ;; ;; distiguishes between four-move and five-move pieces by whether the ;; ;; upper two bits are zero. If they're both zero, it's a four-move ;; ;; piece, otherwise the two bits specify the fifth move. (Recall that ;; ;; 00 corresponds to "up".) ;; ;; ;; ;; Right now, rotations are performed in a brute-force manner: We ;; ;; just encode all of the rotations in our data set here. It's ;; ;; seems to be alot cheaper than writing the code to rotation code, ;; ;; although I can't be sure unless I do them both. At the very least, ;; ;; the brute-force approach is alot easier. ;; ;; ;; ;; Notation: pXrY is "Piece #X, Rotation #Y". X == 0..6, Y == 0..3. ;; ;; ;; ;; up#, dn#, lf# and rt# indicate a movement in a direction ;; ;; on a given move number. ;; ;; ;; ;; Pieces are shown in the comments in their initial ;; ;; orientation. The block marked with %% is the "pivot" ;; ;; block. ;; ;;==========================================================================;; PIECE PROC ; handy constants for defining the moves. @@up0 EQU 0000000000b @@dn0 EQU 0000000010b @@lf0 EQU 0000000001b @@rt0 EQU 0000000011b @@up1 EQU 0000000000b @@dn1 EQU 0000001000b @@lf1 EQU 0000000100b @@rt1 EQU 0000001100b @@up2 EQU 0000000000b @@dn2 EQU 0000100000b @@lf2 EQU 0000010000b @@rt2 EQU 0000110000b @@up3 EQU 0000000000b @@dn3 EQU 0010000000b @@lf3 EQU 0001000000b @@rt3 EQU 0011000000b ;;up4 not allowed @@dn4 EQU 1000000000b @@lf4 EQU 0100000000b @@rt4 EQU 1100000000b ; Piece 0: The Long Bar ; ; ### ; ### ; %%% ; %%% ; ### ; ### ; ### ; ### ; @@p0r0 BYTE @@up0 + @@dn1 + @@dn2 + @@dn3 @@p0r1 BYTE @@lf0 + @@rt1 + @@rt2 + @@rt3 @@p0r2 BYTE @@up0 + @@dn1 + @@dn2 + @@dn3 @@p0r3 BYTE @@lf0 + @@rt1 + @@rt2 + @@rt3 ; Piece 1: The Square Block ; ; ###%%% ; ###%%% ; ###### ; ###### ; @@p1r0 BYTE @@dn0 + @@lf1 + @@up2 + @@rt3 @@p1r1 BYTE @@dn0 + @@lf1 + @@up2 + @@rt3 @@p1r2 BYTE @@dn0 + @@lf1 + @@up2 + @@rt3 @@p1r3 BYTE @@dn0 + @@lf1 + @@up2 + @@rt3 ; Piece 2: The L shape ; ; ###%%%### ; ###%%%### ; ### ; ### ; @@p2r0 BYTE @@rt0 + @@lf1 + @@lf2 + @@dn3 @@p2r1 BYTE @@dn0 + @@up1 + @@up2 + @@lf3 @@p2r2 BYTE @@lf0 + @@rt1 + @@rt2 + @@up3 @@p2r3 BYTE @@up0 + @@dn1 + @@dn2 + @@rt3 ; Piece 3: The Reverse-L shape ; ; ###%%%### ; ###%%%### ; ### ; ### ; @@p3r0 BYTE @@lf0 + @@rt1 + @@rt2 + @@dn3 @@p3r1 BYTE @@up0 + @@dn1 + @@dn2 + @@lf3 @@p3r2 BYTE @@rt0 + @@lf1 + @@lf2 + @@up3 @@p3r3 BYTE @@dn0 + @@up1 + @@up2 + @@rt3 ; Piece 4: The S shape ; ; %%%### ; %%%### ; ###### ; ###### ; @@p4r0 BYTE @@rt0 + @@lf1 + @@dn2 + @@lf3 @@p4r1 BYTE @@up0 + @@dn1 + @@rt2 + @@dn3 @@p4r2 BYTE @@rt0 + @@lf1 + @@dn2 + @@lf3 @@p4r3 BYTE @@up0 + @@dn1 + @@rt2 + @@dn3 ; Piece 5: The Z shape ; ; ###SSS Note: SSS brick is the actual starting brick. ; ###SSS ; %%%### ; %%%### ; @@p5r0 DECLE @@dn0 + @@rt1 + @@lf2 + @@up3 + @@lf4 @@p5r1 DECLE @@dn0 + @@up1 + @@dn2 + @@lf3 + @@dn4 @@p5r2 DECLE @@dn0 + @@rt1 + @@lf2 + @@up3 + @@lf4 @@p5r3 DECLE @@dn0 + @@up1 + @@dn2 + @@lf3 + @@dn4 ; Piece 6: The T shape ; ; ###%%%### ; ###%%%### ; ### ; ### ; @@p6r0 DECLE @@lf0 + @@rt1 + @@dn2 + @@up3 + @@rt4 @@p6r1 DECLE @@up0 + @@dn1 + @@lf2 + @@rt3 + @@dn4 @@p6r2 DECLE @@lf0 + @@rt1 + @@up2 + @@dn3 + @@rt4 @@p6r3 DECLE @@up0 + @@dn1 + @@rt2 + @@lf3 + @@dn4 ENDP ;;==========================================================================;; ;; PUTPIECEREC ;; ;; Writes a piece record to memory. ;; ;; ;; ;; PUTPIECEREC.NC ;; ;; Writes a piece record to memory, except color. ;; ;; ;; ;; INPUTS: ;; ;; R0 -- piece color ;; ;; R1 -- X coord of pivot ;; ;; R2 -- Y coord of pivot ;; ;; R3 -- piece number/rotation ;; ;; R4 -- Address of piece information record (+1 if PUTPIECEREC.NC) ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R0..R3, R5 -- not modified ;; ;; R4 -- points just past end of of piece information record ;; ;;==========================================================================;; PUTPIECEREC PROC MVO@ R0, R4 ; Put piece color @@NC: MVO@ R1, R4 ; Put piece pivot X coordinate MVO@ R2, R4 ; Put piece pivot Y coordinate MVO@ R3, R4 ; Put piece number/rotation JR R5 ; Return ENDP ;;==========================================================================;; ;; GETPIECEREC ;; ;; Reads a piece record from memory. ;; ;; ;; ;; GETPIECEREC.NC ;; ;; Reads a piece record from memory, except color. ;; ;; ;; ;; INPUTS: ;; ;; R4 -- Address of piece information record (+1 if GETPIECEREC.NC) ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- piece color (unless GETPIECEREC.NC) ;; ;; R1 -- X coord of pivot ;; ;; R2 -- Y coord of pivot ;; ;; R3 -- piece number/rotation ;; ;;==========================================================================;; GETPIECEREC PROC MVI@ R4, R0 ; Get piece color @@NC: MVI@ R4, R1 ; Get piece pivot X coordinate MVI@ R4, R2 ; Get piece pivot Y coordinate ; Sign-extend R1 and R2 since they're in 8-bit RAM. MVII #$7F, R3 ; Generate constant $FF80 COMR R3 ; R3 is "magic sign-extend constant" ADDR R3, R1 ; Propogate sign-bit info to upper-half XORR R3, R1 ; R1 is fully sign-extended here. ADDR R3, R2 ; Propogate sign-bit info to upper-half XORR R3, R2 ; R2 is fully sign-extended here. MVI@ R4, R3 ; Get piece number/rotation JR R5 ; Return ENDP ;;==========================================================================;; ;; DRAWPIECE ;; ;; Draws a piece on the screen in the desired color (0..7). ;; ;; ;; ;; TESTPIECE ;; ;; Determines if a piece can be drawn in a given location by tracing its ;; ;; path and doing GETPIXELSC calls. ;; ;; ;; ;; INPUTS: ;; ;; R0 -- If DRAWPIECE, color of piece to draw (0..7) ;; ;; R1 -- X coordinate of pivot ;; ;; R2 -- Y coordinate of pivot ;; ;; R3 -- Piece number ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- If TESTPIECE, Zero == OK, non-zero == not OK; else, trashed. ;; ;; R4 -- Minimum Y coordinate ;; ;; R5 trashed. ;; ;; ;; ;; $130..$135 used for temporary storage. ;; ;;==========================================================================;; DRAWPIECE PROC @@colsv EQU $130 ; Piece color save area @@pvxsv EQU $131 ; Pivot X coordinate save area @@pvysv EQU $132 ; Pivot Y coordinate save area @@pnrsv EQU $133 ; Piece number/rotation save area @@pcwsv EQU $134 ; Loop count save area @@cntsv EQU $135 ; Loop count save area @@miny EQU $136 ADDR R0, R0 ; Shift color left by 1 INCR R0 ; Set the LSB on the color INCR PC ; Skip the CLRR R0 TESTPIECE: CLRR R0 ; Start with color = 0, LSB = 0 PSHR R5 ; Save return address MVII #@@colsv, R4 ; Point R4 at save area CALL PUTPIECEREC MVO R2, @@miny ; Start with Min Y == pivot point ; Convert piece number to control word. ADDI #PIECE, R3 ; Point into PIECE array entry for piece MVI@ R3, R3 ; Get the piece control word MVII #3, R4 ; At least four moves in piece @@loop: MOVR R3, R0 ; Copy Piece Control Word ANDI #2, R0 ; Get sign for move (set == +1, clear == -1) DECR R0 ; R0 is either +1/-1 according to move SARC R3, 2 ; Shift move away from PCW. Carry==axis BC @@xaxis ; Carry Clear == Y-Axis, Carry Set == X-Axis ADDR R0, R2 ; Add move to Y axis ; See if this Y coordinate is our new min-Y coord CMP @@miny, R2 BGE @@cont MVO R2, @@miny INCR PC ; Skip add-to-X (next instruction) @@xaxis: ADDR R0, R1 ; Add move to X axis @@cont: ; Note, at most 8 bits remain in PCW here, so we save in 8-bit loc. MVO R3, @@pcwsv ; Save remaining Piece Control Word (max 8 bit) MVO R4, @@cntsv ; Save our loop trip count MVI @@colsv, R0 ; Get saved color SARC R0, 1 ; If bit-0 set, this is DRAW, else TEST BC @@draw ; @@test: ; Note R0 should be 0 here. This treats off-screen pixels as black, ; which is what we want. CMPI #EDGEL, R1 ; Are we off left? BLT @@notok ; Yes: Bad! CMPI #EDGER, R1 ; Are we off left? BGT @@notok ; Yes: Bad! CALL GETPIXELSC ; Get pixel, with clipping. CMPI #7, R0 ; Ignore color 7, since we use that BEQ @@pixelok ; for the active piece. TSTR R0 ; Otherwise, make sure it's black. BEQ @@pixelok ; B @@notok ; Didn't pass the tests? It's not ok then. @@draw: CALL PUTPIXELSCQ ; Put the pixel on the screen, with clipping. @@pixelok: MVI @@pcwsv, R3 ; Get our saved piece number MVI @@cntsv, R4 ; Get our loop trip count. DECR R4 BPL @@loop ; Loop as long as R4 > 0 CLRR R4 TSTR R3 ; See if this is a 5-move piece. BNEQ @@loop ; Loop one last time if it is. MVI @@colsv, R0 ; Restore color register (if TESTPIECE is ok, SLR R0, 1 ; ...OR if this is DRAWPIECE). INCR PC ; (skip the MOVR.) @@notok: ; Else, skip the restore if TESTPIECE and MOVR PC, R0 ; ...pixel is not ok, so force R0 non-zero MVII #@@pvxsv, R4 ; Point to save area + 1 CALL GETPIECEREC.NC MVI @@miny, R4 ; Get minimum Y value into R4 PULR PC ; Return to the caller ENDP ;;==========================================================================;; ;; GETPIECECOLOR ;; ;; Gets Piece Color, given the piece number/rotation. ;; ;; ;; ;; INPUTS: ;; ;; R3 -- Piece number/rot. Bits 0..1 are rotation, Bits 2..4 are piece # ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- Piece color ;; ;;==========================================================================;; GETPIECECOLOR PROC PSHR R3 ; Save R3 so that it's not clobbered SLR R3, 2 ; Generate index into color table ADDI #@@color,R3 ; R3 = PCCOLOR[piece] MVI@ R3, R0 ; Get the piece color into R0 PULR R3 ; Restore R3 JR R5 ; Return. @@color BYTE $2, $1, $3, $4, $5, $6, $1 ENDP ;;==========================================================================;; ;; PICKPIECE ;; ;; Grabs the 'next piece' and makes it the current piece. Then picks a ;; ;; new 'next piece'. ;; ;;==========================================================================;; PICKPIECE PROC PSHR R5 @@chain: @@randloop: MVII #4, R2 ; Generate some new random bits CALL NEXTRANDX ANDI #$1C, R0 ; Mask to [0..7] * 4 BEQ @@randloop ; If it comes up 0, try again SUBI #4, R0 ; Make into piece #0..#6 CALL NEXTCLEAR ; Clear previous 'next-piece' MVI NXTPC, R3 ; Get previous 'next-piece'. MVO R0, NXTPC ; Save new 'next-piece'. MVI SHOWNXT, R1 ; Are we showing next piece? MVO R1, DIDNXT ; Initialize our "showed next piece" flag TSTR R1 BEQ @@noshow PSHR R3 ; Save R3 -- new current piece number CALL NEXTSHOW ; Show next piece PULR R3 ; Restore R3 @@noshow: CALL GETPIECECOLOR ; Get the current piece's color. MVO R0, CS1 ; Force color-stack to piece's color MVO R0, CS3 ; Force color-stack to piece's color MVII #EDGEL+5, R1 ; Start out in middle of well... MVII #3, R2 ; ...just a little off top of screen COMR R2 ; (Y = -3) MVII #CURPC.C, R4 ; Write to "current piece" record. CALL PUTPIECEREC ; Make this the current piece. PULR PC ; Return. ENDP ;;==========================================================================;; ;; CLEARWELL ;; ;; Clears the well in a dramatic fashion ;; ;;==========================================================================;; CLEARWELL PROC @@bot EQU $1E2 PSHR R5 CLRR R0 MVO R0, CURPC.C ; make sure current piece number is cleared. @@wellloop: MVII #EDGEB, R3 CMP HEIGHT, R3 ; R3 has actual well height. BLE @@return ; Do nothing if well is empty. @@randloop: MVII #5, R2 CALL NEXTRANDX ; Get a random number (advance by 5 bits) ANDI #31, R0 ADD HEIGHT, R0 CMPR R3, R0 ; Make sure it's less than the well height. BGE @@randloop MOVR R0, R2 MVII #EDGEL+1, R1 CALL GETPIXELS CMPI #7, R0 BEQ @@nukeit @@fillit: CALL NUKELINE B @@wellloop @@nukeit: MVO R2, @@bot CALL WAIT ; Wait a few ticks DECLE 2 ; MVII #@@wellloop, R5 PSHR R5 B DOSCORE.nukeit ; Nuke the line, collapsing the well. ; This will return to @@wellloop @@return: PULR PC ; Return. ENDP ;;==========================================================================;; ;; SCOREDROP ;; ;; Loops over a four line window and marks rows for deletion. ;; ;; Returns line-clearing score in R0 and leaves lines to be cleared set ;; ;; to color #7. ;; ;; ;; ;; NUKELINE ;; ;; Force-clears a line as if it were full. ;; ;; ;; ;; INPUTS: ;; ;; R2 == Row number to start at. ;; ;; ;; ;; OUTPUTS: ;; ;; R0 = Number of lines cleared. ;; ;; R2 = original value plus 4 ;; ;; R3 = Number of lines cleared. ;; ;; R1, R4, R5 trashed ;; ;; ;; ;; $1E0..$1E3 used as temp storage. ;; ;;==========================================================================;; SCOREDROP PROC @@trip EQU $1E2 @@full EQU $1E3 PSHR R5 ; Save return address CLRR R3 ; Clear count of lines to score MVO R3, @@full MVII #3, R4 ; Loop trip count B @@doscore NUKELINE: PSHR R5 CLRR R4 MVO R4, @@full MVO R4, @@trip B @@nuke @@doscore: MVO R4, @@trip MVII #EDGEL, R1 ; Iterate over columns 15..24 @@scoreloop: CALL GETPIXELS ; Read a pixel TSTR R0 ; Is it zero? BEQ @@sloop ; If so, stop -- line's not full. INCR R1 CMPI #EDGER, R1 ; At the end of the well? BLE @@scoreloop ; If not, keep looping @@nuke: MVI @@full, R3 INCR R3 MVO R3, @@full ;; Prepare to set entire row in well to color 7 MVII #EDGEL, R1 ; Start at left edge of well MVII #7, R0 ; Color = 7 @@floop: CALL PUTPIXELS ; Draw the pixel INCR R1 ; Move to next column CMPI #EDGER, R1 ; Are we at the right edge? BLE @@floop ; No: Keep looping. @@sloop: MVI @@trip, R4 INCR R2 ; Go to next line DECR R4 ; Decrement loop count BPL @@doscore ; Keep going as long as R4 >= 0 @@sdone: ; Scoring complete MVI @@full, R3 ;; If this was a 4-line clear, play the special sound effect CMPI #4, R3 BNEQ @@return MVII #2, R1 ; High priority sound effect CALL PLAY.sfxp DECLE FXBOMB4 @@return: MOVR R3, R0 ; Return number of cleared lines. PULR PC ; Return to caller. ENDP ;;==========================================================================;; ;; DOSCORE ;; ;; Calls SCOREDROP to score a dropped piece and mark lines for deletion. ;; ;; ;; ;; Calculates the score based on number of downward moves the player ;; ;; made, as well as the number of lines cleared. ;; ;; ;; ;; Collapses lines that have been marked for deletion by the scoring ;; ;; routines. Processes the four lines ABOVE R2. (This works pretty well, ;; ;; since the scoring routines leave R2 pointing to the line AFTER the four ;; ;; lines that were scored.) Lines are scanned in the leftmost column, ;; ;; and lines containing a pixel of color 7 in this column are cleared. ;; ;; ;; ;; INPUTS: ;; ;; R2 == Line at top of 4-line window to score ;; ;; ;; ;; OUTPUTS: ;; ;; R0, R1, R2, R4, R5 trashed? ;; ;; ;; ;; Locations $1E0..$1E3, $1E6 used as temporary storage. ;; ;;==========================================================================;; SCORETABLE DECLE 500, 1500, 3000, 6000 DOSCORE PROC @@bot EQU $1E2 @@top EQU $1E3 @@clr EQU $1E6 PSHR R5 MVII #EDGEB-4, R3 ; Make sure we're not too close to CMPR R3, R2 ; the bottom of the well. BLE @@rowok MOVR R3, R2 ; If we are, move our window up some. @@rowok: PSHR R2 ; First, add in any "move points" MVI MSCORE, R0 MVI PSCORL, R1 ; Add this to our score MVI PSCORH, R4 ADDR R0, R1 ; Add moves to lower half of 32-bit score ADCR R4 ; Propogate carry from lower to upper half MVO R1, PSCORL MVO R4, PSCORH CLRR R2 MVO R2, MSCORE ; Clear the total. ; Wait one tick to make sure any queued pixels have been drawn. ; This works because the ISR always drains the committed pixel ; queue completely, and WAIT busywaits on a counter maintained ; by the ISR. Cool, eh? CALL WAIT DECLE 1 PULR R2 ; Next, see if we need to clear any lines due to this drop CALL SCOREDROP ; Score the drop. MOVR R0, R5 ; If we didn't score anything, return BEQ @@done ; (chain return through PICKPIECE) @@hmm ADDI #SCORETABLE-1, R5 ; R5 = &scoretable[lines] MVI@ R5, R0 ; Load base score for this # of lines MVI LEVEL, R3 MVI PSCORL, R1 ; Otherwise, add this to our score MVI PSCORH, R4 @@scoreloop: ADDR R0, R1 ; (multiply by level thru repeated addition) ADCR R4 DECR R3 BPL @@scoreloop ; Go around 'level + 1' times. MVO R1, PSCORL MVO R4, PSCORH @@notnuke: SUBI #4, R2 ; Find top of window MVO R2, @@top ; Remember top row ADDI #3, R2 ; Move to bottom of window CALL SPAWN ; Fork the line clearing as new task. DECLE 13, 3 ; Period == 6 ticks (10Hz), task #3 ;;--------------------------------------------------------------------;; ;; NOTE: THIS IS A NEW TASK HERE!!! IT IS CALLED FOR EACH LINE!!! ;; ;; We do this to avoid having tasks pile up in the queue while we ;; ;; do our work. Things such as score updates (most common) tend to ;; ;; pile up in the queue otherwise. ;; ;;--------------------------------------------------------------------;; @@scan: MVII #EDGEL, R1 ; Scan along left edge MVO R2, @@bot ; Remember current y position CALL GETPIXEL ; Grab a pixel. MVI @@bot, R2 ; Restore row position MVI @@top, R1 ; Restore top of window CMPI #7, R0 ; Is this pixel color 7? BEQ @@collapseok ; It is: go collapse this row. DECR R2 ; Move up a row on the screen CMPR R1, R2 ; Are we out of our window? BGE @@scan ; If not, keep scanning @@done: CALL SLEEP DECLE 31, 3 ; Sleep for 1/4th second after drop. PULR R5 B PICKPIECE ; Chain return through PICKPIECE @@collapseok: ;; When we collapse a line, the top of our scanning window ;; actually moves down a row, and our scan position does NOT move ;; up a row. So, look at the scan row, and increment the top ;; row value. INCR R1 ; Move top down one row MVO R1, @@top ; Store new top of window ;; Increment our "cleared lines" count and queue up the "show lines" ;; task. MVI LINES, R1 INCR R1 MVO R1, LINES MVII #UPDATESTATS, R0 JSRD R5, QTASK ;; We spawned as a one-shot task. We retrigger only after we're ;; sure we might need it, so that we don't get over-triggered. ;; (We will get triggered up to 5 times, with the fifth trigger ;; doing the call to PICKPIECE.) MVII #3, R3 CALL RETRIGGERTASK @@nukeit: MVII #1, R1 CALL PLAY.sfxp ; Trigger the collapse soundeffect. DECLE FXBOMB @@nosfx: MVII #EDGEL, R1 ; Start at left edge. @@xloop: MVI HEIGHT, R2 ; Start at top of screen MVO R1, XSAVE ; Save our current column CLRR R4 ; Propogate a black pixel from top @@yloop: MVO R4, @@clr ; Remember prev row's color MVO R2, YSAVE ; Save our current row CALL PIXCALC ; Calculate mask/addr for this pixel MOVR R1, R4 ; Remember mask CALL GETPIXELR ; Get color to copy to next row MOVR R4, R1 ; Restore mask MOVR R0, R4 ; Remember color for next row MVI @@clr, R0 ; Restore previous rows color CALL PUTPIXELR ; Draw prev rows color in this row MVI YSAVE, R2 ; Restore row position MVI XSAVE, R1 ; Restore column position INCR R2 ; Move down one row CMP @@bot, R2 ; Are we at the last row? BLE @@yloop ; No? Keep looping. INCR R1 ; Move to next column CMPI #EDGER, R1 ; Are we at the right edge yet? BLE @@xloop ; No? Keep looping. ; Decrement one from our well height here, so that music speed ; changes after we collapse a row, and our scan window changes as ; well for speed. MVI HEIGHT, R1 ; Decrement the well height now. INCR R1 ; (Note: 'HEIGHT' is actually MVO R1, HEIGHT ; EDGEB - height. Hence the INCR.) PULR PC ; Return... we will be retriggered ; if there are more lines to scan. ENDP ;;==========================================================================;; ;; UPDATESCORE ;; ;; Shows current score, by incrementally updating a displayed score value. ;; ;;==========================================================================;; UPDATESCORE PROC MVII #PSCORL,R4 ; Point to player/displayed score MVI@ R4, R2 ; Get lo 16 of player's actual score MVI@ R4, R3 ; Get hi 16 of player's actual score MVI@ R4, R0 ; Get lo 16 of displayed score MVI@ R4, R1 ; Get hi 16 of displayed score CMPR R3, R1 ; Compare upper halves. BEQ @@next ; Displayed = Actual: Test lower halves BNC @@doincr ; Displayed < Actual: Do the increment BNEQ @@uhoh ; Displayed > Actual? UHOH! @@next: CMPR R2, R0 ; Compare lower halves. BEQ @@return ; Displayed = Actual: Do nothing. BNC @@doincr ; Displayed < Actual: Do the increment ; Displayed > Actual? UHOH! @@uhoh: ; If we get here, we accidentally overshot the player's actual ; score somehow. Compensate by setting the displayed score to the ; actual score. :-P (Note: We also use this code to force a ; redisplay of the score on purpose by setting the displayed ; score to 0xFFFFFFFF, which is always higher than the player's ; score (or is it? How good are YOU?)). MOVR R2, R0 ; Set lo half of displayed to actual MOVR R3, R1 ; Set hi half of displayed to actual B @@disp ; Display the fixed score. @@return: JR R5 @@doincr: ; First, find the difference between the displayed and actual ; scores. This is a 32-bit subtract. SUBR R0, R2 ; Subtract the lo halves. ADCR R3 ; Subtract the borrow from the hi half DECR R3 ; as a "not borrow" SUBR R1, R3 ; Subtract the hi halves ; Divide the difference by 8, rounding upwards. ; This is effectively "num = (num + 7) / 8". It should ; produce a non-zero number if num starts out non-zero. ADDI #7, R2 ; Add rounding factor ADCR R3 ; ... and carry into upper half ; Unsigned right shift by 3 CLRC RRC R3, 1 ; Shift the first bit. Can't SARC (sign bit) RRC R2, 1 ; SARC R3, 2 ; SARC is safe now: sign bit is for sure 0 RRC R2, 2 ; ; TSTR R2 ; BNEQ @@addit ; TSTR R3 ; BNEQ @@addit ; HLT ; THIS SHOULD NEVER HAPPEN ; INCR R2 @@addit: ADDR R2, R0 ; Add the new increment's lo half ADCR R1 ; Propogate the carry ADDR R3, R1 ; Add the new increment's hi half @@disp: MVO R0, DSCORL ; Store the updated display value (lo) MVO R1, DSCORH ; Store the updated display value (hi) MVI SCRDIG, R2 MVI SCRCOL, R3 MVI SCRPOS, R4 B DEC32B ; Display with leading zeros. ; (Chained return) ENDP ;;==========================================================================;; ;; UPDATESTATS ;; ;; Updates game information such as # of lines, level, and drop speed. ;; ;;==========================================================================;; UPDATESTATS PROC PSHR R5 MVI SCRCOL, R3 ; Get score color MVI LINES, R0 ; Get current number of lines MVI LEVEL, R1 ; Get current level TSTR R1 BEQ @@initial ; First time through, force an update. ADDR R1, R1 ; R1 = 2 * (level) MOVR R1, R2 SLL R2, 2 ; R2 = 8 * (level) ADDR R2, R1 ; R1 = 10 * (level) CMPR R1, R0 ; See if we've cleared enough lines to go up ; a level. BLT @@samelevel ; If not new level, just update line counter. MVII #3, R1 ; Sound effect priority == 3 CALL PLAY.sfxp ; DECLE FXDING3 ; Ding to signify "Next Level". ;B @@nextlevel @@nextlevel: MVI LEVEL, R2 ; Get current level number INCR R2 ; Update our level number MVO R2, LEVEL ; Store the new level number B @@skipinitial @@initial: MVI SLEVEL, R2 ; Get starting level MVO R2, LEVEL ; Store it to our level number @@skipinitial: MVII #20, R4 ; See if level > 20. If so, clamp to that CMPR R4, R2 ; for purposes of setting speed. BLE @@speedok MOVR R4, R2 @@speedok: ADDI #SPDTBL+1, R2 ; Point R2 into Speed Table MVI@ R2, R2 MVO R2, SPEED ; Set piece speed from speed table MVII #LVLLOC + 20, R4 MVII #2, R2 MVI LEVEL, R0 ; Increment the level number CALL DEC16A ; Display the updated level number MVI LINES, R0 @@samelevel: MVII #LINLOC + 20, R4 MVII #2, R2 PULR R5 B DEC16A ENDP ;;==========================================================================;; ;; SPDTBL ;; ;; Speed table. Contains rates at which pieces drop for all the levels. ;; ;; Rates are all in "Ticks * 2". ;; ;;==========================================================================;; SPDTBL PROC DECLE 160 + 0 ; Level 1 DECLE 120 + 0 ; Level 2 DECLE 100 + 0 ; Level 3 DECLE 80 + 0 ; Level 4 DECLE 60 + 0 ; Level 5 DECLE 50 + 0 ; Level 6 DECLE 40 + 0 ; Level 7 DECLE 34 + 0 ; Level 8 DECLE 28 + 0 ; Level 9 DECLE 24 + 0 ; Level 10 DECLE 20 + 0 ; Level 11 DECLE 18 + 0 ; Level 12 DECLE 16 + 0 ; Level 13 DECLE 14 + 0 ; Level 14 DECLE 12 + 0 ; Level 15 DECLE 10 + 0 ; Level 16 DECLE 8 + 0 ; Level 17 DECLE 6 + 0 ; Level 18 DECLE 4 + 0 ; Level 19 DECLE 2 + 0 ; Level 20 and beyond ENDP ;;==========================================================================;; ;; MARQUEE ;; ;; Cycles colors on a string of text. ;; ;; ;; ;; INPUTS: ;; ;; R2 -- Task instance data: ;; ;; Low byte of instance data gives screen offset. ;; ;; High byte of instance data gives length. ;; ;;==========================================================================;; MARQUEE PROC MOVR R2, R3 ANDI #$FF, R3 ; R3 == screen offset. XORR R3, R2 SWAP R2 ; R2 == length. ADDI #$200, R3 ; Convert screen offset into actual address. MVI@ R3, R0 ; Get first display word from string. MVII #$7, R1 ; Color mask for three LSBs. ANDR R1, R0 ; Get color from leading character in string. COMR R1 ; Invert our mask, so we can replace colors. @@loop: DECR R0 ; Cycle color by decrementing BNEQ @@ok ; Make sure it didn't go to zero! MVII #$7, R0 ; If it did, set it back to 7. @@ok: MVI@ R3, R4 ; Get a character from the display. ANDR R1, R4 ; Mask away its color bits. ADDR R0, R4 ; Replace them with our new color bits. MVO@ R4, R3 ; Write the modified character to the display. INCR R3 ; Move to next character. DECR R2 ; Loop until done. BNEQ @@loop JR R5 ; Return. ENDP ;;==========================================================================;; ;; ANIDROP ;; ;; Color cycles CS1 and CS3 so that collapse lines "flash", or sets a ;; ;; solid color while piece is active. ;; ;;==========================================================================;; ANIDROP PROC MVI CURPC.C, R2 ; Is there a current piece active? TSTR R2 BNEQ @@active MVI CS1, R2 ; No? Just flash colors then. INCR R2 @@active: MVO R2, CS1 ; Set up colorstack #1 and #3 MVO R2, CS3 JR R5 ; Return. ENDP ;;==========================================================================;; ;; MUTETOGGLE ;; ;; Toggles MUTE state ;; ;;==========================================================================;; MUTETOGGLE PROC DIS MVI MUTE, R0 ; Get current mute status TSTR R0 ; If zero, we're muted. BEQ @@unmute ; Mute the music by zeroing its volume registers. CLRR R0 MVI SNDSTRM+3,R1 ; Get current stream mask SLL R1, 2 ; Move volume-register bits to top SLLC R1 ; If channel C's vol control is enabled .... BNC @@notc MVO R0, PSG0+13 ; ... clear it @@notc: SLLC R1 ; If channel B's vol control is enabled .... BNC @@notb MVO R0, PSG0+12 ; ... clear it @@notb: SLLC R1 ; If channel A's vol control is enabled .... BNC @@nota MVO R0, PSG0+11 ; ... clear it @@nota: INCR PC ; skip INCR R0. @@unmute: INCR R0 ; Un-mute the audio MVO R0, MUTE ; Store the new mute status EIS JR R5 ENDP ;;==========================================================================;; ;; ROTPIECE_CW ;; ;; Rotates a piece clockwise by updating the two LSBs of its piece ;; ;; index #. ;; ;; ;; ;; ROTPIECE_CCW ;; ;; Rotates a piece counter clockwise by updating the two LSBs of its ;; ;; piece index #. ;; ;; ;; ;; INPUTS: ;; ;; R3 -- Piece number/rotation ;; ;; R5 -- Return address ;; ;; ;; ;; OUTPUTS ;; ;; R3 -- New piece/rotation ;; ;; R0 -- Trashed ;; ;;==========================================================================;; ROTPIECE_CW: PROC SETC ; Flag that this is a clockwise rotate INCR PC ; (skip the CLRC) ROTPIECE_CCW: CLRC ; Flag that this is a counter-clockwise rotate MOVR R3, R0 ; Copy piece number/rotation to R0 ADCR PC ; If counter-clockwise, complement R0, COMR R0 ; ... else don't. SETC RLC R0, 1 ANDI #3, R0 XORR R0, R3 ; Magic! JR R5 ENDP ;;==========================================================================;; ;; GRAVITY ;; ;; Makes the pieces fall! ;; ;; ;; ;; MVPIECE ;; ;; Moves/Rotates a piece around on the screen. ;; ;; ;; ;; INPUTS: ;; ;; R2 -- Event number: ;; ;; 13: Disc Left Move piece left ;; ;; 14: Disc Right Move piece right ;; ;; 15: Disc Down Move piece down ;; ;; 16: Action Top Rotate CCW ;; ;; 17: Action Left Rotate CW ;; ;; 18: Action Right Rotate CW ;; ;; ;; ;; THIS CODE SUCKS. ;; ;; (Although, it sucks less than the original code that I had here.) ;; ;;==========================================================================;; GRAVITY PROC MVII #15, R2 ; Event #15 is 'down' CLRR R0 ; Flag that this is NOT a hand-ctrl input INCR PC ; (skip 'MOVR') MVPIECE: MOVR R2, R0 ; Flag that this IS a hand-ctrl input MVO R0, WASHAND ; Store 'WASHAND' flag. PSHR R5 ; Save return address PSHR R2 ; Save keypad # MVII #CURPC.C, R4 CALL GETPIECEREC ; Load current piece's record into R0..R3 PULR R4 ; Get keypad # into R4 TSTR R0 ; Is there an active piece? BEQ @@return ; Do nothing if no piece is active. MVII #@@check, R5 ; Set up "return address" MVO R4, WASDOWN ; Default "WAS DOWN" to "NOT" SUBI #13, R4 ; Scale range down to 0..6, check for R4==13 BMI @@return ; Bogus input R4 < 13 (should never happen) BNEQ @@not_left ; If taken, R4 != 13 DECR R1 ; Move left JR R5 ; Jump to '@@check' @@not_left: SUBI #2, R4 ; Check for R4==14 or R4==15 BMI @@is_right ; Bogus input R4 == 13 (should never happen) BNEQ @@not_down ; If taken, R4 != 14 MVO R4, WASDOWN ; Set 'WASDOWN' to 'Yes, it was DOWN'. INCR R2 ; Move down one JR R5 ; Jump to '@@check' @@is_right: INCR R1 ; Move right JR R5 ; Jump to '@@check' @@not_down: DECR R4 ; Check for R4==16 or R4 > 16 ; Note: Both of these branches chain return through ROTPIECE_xxx BEQ ROTPIECE_CCW ; If taken, R4==16 (top action button) B ROTPIECE_CW ; If taken, R4>=17 (bottom action buttons) @@check: ;; All movement paths lead to here. At this point, R0..R3 ;; contain the proposed piece state (rotation, X, Y) CALL TESTPIECE ; Is the piece placeable here? TSTR R0 BNEQ @@cantmove ; If we couldn't move the piece, leave. ; Prepare to move the piece. CALL GETPIECECOLOR ; Get the piece's color. MVO R0, CURPC.C ; Set piece's color in its record. MVO R0, CS1 ; Force color-stack to piece's color MVO R0, CS3 ; Force color-stack to piece's color MVII #TMPPC.X, R4 CALL PUTPIECEREC.NC ; Save updated piece in temp storage CALL GETPIECEREC ; Restore original piece info CLRR R0 ; Force color to 0 for erase. CALL DRAWPIECE ; Erase previous piece MVII #TMPPC.X, R4 CALL GETPIECEREC.NC ; Restore updated piece info INCR R4 MVII #7, R0 ; Always draw in color 7 here! CALL PUTPIECEREC.NC ; Store it to the current piece info CALL DRAWPIECE ; Draw the updated piece on the screen ; Bip and increment the per-move total MVI WASHAND, R0 ; Was this a hand-controller input? TSTR R0 BEQ @@nothand ; No... don't bip, and don't add to MSCORE CALL PLAY.sfx ; Yes... BIP! DECLE FXBIP MVI WASDOWN, R0 ; If this was a hand movement that pushed TSTR R0 ; down, increment the MSCORE. BNEQ @@notdown ; No... don't increment MSCORE MVI MSCORE, R0 ADDI #MOVEINC,R0 ; Yes... increment MSCORE! MVO R0, MSCORE @@notdown: @@nothand: PULR R5 ; Chain Return via COMMITPXQ B COMMITPXQ ; Commit the pixels to be drawn to screen. @@cantmove: MVI WASDOWN, R0 ; Was this a downward move? TSTR R0 BNEQ @@return ; No? Just return then. PULR R5 B PLACEPIECE ; Yes? Place the piece (w/ chained return) @@return: PULR PC ; Return ENDP ;;==========================================================================;; ;; NEXTSHOW ;; ;; Shows the currently displayed piece in the next-piece box. ;; ;; ;; ;; NEXTCLEAR ;; ;; Clears the currently displayed piece in the next-piece box. ;; ;; ;; ;; INPUTS: ;; ;; Location 'NXTPC' holds current next piece. ;; ;; R5 -- return address. ;; ;; ;; ;; OUTPUTS: ;; ;; R0 -- saved ;; ;; R1 -- NXTPCX ;; ;; R2 -- NXTPCY ;; ;; R3 -- Next piece's piece number/rotation ;; ;; R4, R5 -- trashed ;; ;;==========================================================================;; NEXTSHOW PROC MVO PC, DIDNXT ; Set the "We showed next piece" flag. ; (Note: PC guaranteed to be non-zero) SETC ; Set the "This is NEXTSHOW" flag. INCR PC ; Skip the CLRC. NEXTCLEAR: CLRC ; Set the "This is NEXTCLEAR" flag. PSHR R5 ; Save our return address. PSHR R0 ; Save R0. MVI NXTPC, R3 ; Get the next piece's piece number/rotation BNC @@clear ; If carry cleared, set color to 0. CALL GETPIECECOLOR ; Otherwise, get the piece's color. INCR PC ; Skip the CLRR. @@clear: CLRR R0 ; Clear the piece's color. MVII #NXTPCX, R1 ; Position ourself in the next-piece box. MVII #NXTPCY, R2 CALL DRAWPIECE PULR R0 ; Restore R0. PULR R5 ; Chain return via COMMITPXQ B COMMITPXQ ; Commit the pixels for display immediately. ENDP ;;==========================================================================;; ;; NEXTTOGGLE ;; ;; Toggles whether next-piece is shown. ;; ;;==========================================================================;; NEXTTOGGLE PROC MVI SHOWNXT, R1 ; Are we currently showing next piece? TSTR R1 BEQ @@turnon ; No... then start showing it. @@turnoff: CLRR R1 ; Yes... turn it off! MVO R1, SHOWNXT B NEXTCLEAR ; Chain return through NEXTCLEAR @@turnon: INCR R1 MVO R1, SHOWNXT B NEXTSHOW ; Chain return through NEXTSET ENDP ;;==========================================================================;; ;; PLACEPIECE ;; ;; Make piece permanent where it is at. ;; ;;==========================================================================;; PLACEPIECE PROC DIS PSHR R5 MVII #CURPC.C, R4 ; Write to "current piece" record. CALL GETPIECEREC ; Make this the current piece. EIS TSTR R0 ; Make sure we actually had an active piece BNEQ @@active PULR PC ; Not active? Just return! @@active: CALL DRAWPIECE ; Draw it. CALL COMMITPXQ ; Commit the pixels. CALL WAIT ; Wait one tick, so pixels are committed. DECLE 1 CLRR R5 ; Mark our current piece as inactive. MVO R5, CURPC.C ; It will get reactivated by DOSCORE. ; We do this after the 'wait' so that we're ; sure our queued pixels are drawn. CMPI #EDGEB, R4 ; Is this piece off-screen? BLE @@notgameover ; No... keep going CALL NEXTCLEAR PULR R5 CLRR R0 MVO R0, HEIGHT B SCHEDEXIT ; Yes... game over! @@notgameover: CMP HEIGHT, R4 ; Did this piece form the new top-of-well? BGE @@nottop MVO R4, HEIGHT @@nottop: MVI DIDN