DECLARE SUB SetConfig (code%, value%, p%)
DECLARE SUB LoadConfig ()
DECLARE SUB MoveIntro (dir%, p%)
DECLARE SUB DrawIntro (p%)
DECLARE SUB ResetPlayer (p%)
DECLARE SUB ProcessTopOut (p%)
DECLARE SUB LoadBits ()
DECLARE FUNCTION Expired% (deadline!)
DECLARE SUB DrawText (x0%, y%, text$)
DECLARE SUB LoadFont ()
DECLARE SUB PieceSize (sx0%, sy0%, sx1%, sy1%, r%, s%)
DECLARE SUB DrawNext (p%, old%)
DECLARE SUB ProcessFlash (p%)
DECLARE SUB DrawBoard (bx0i%, by0i%, bx1i%, by1i%, p%)
DECLARE SUB LockPiece (p%)
DECLARE SUB MovePiece (dir%, p%)
DECLARE SUB RaiseJunk (j%, n%, p%)
DECLARE SUB ClearBoard (p%)
DECLARE FUNCTION Bottom% (p%)
DECLARE SUB SpawnPiece (p%, hold%)
DECLARE FUNCTION Hit% (bx0%, by0%, r%, s%, p%)
DECLARE SUB LoadShapes ()
DECLARE SUB LoadTiles ()
DECLARE SUB LoadPalette ()
DECLARE FUNCTION RedFromHsv! (h!, s!, v!)
DECLARE FUNCTION GrnFromHsv! (h!, s!, v!)
DECLARE FUNCTION BluFromHsv! (h!, s!, v!)
DECLARE SUB DrawBackground ()
DECLARE SUB DrawOutline (p%)
DECLARE SUB SetScore (code%, value%, p%)
DECLARE SUB IncrementScore (code%, value%, p%)
DECLARE FUNCTION FallTime! (p%)
' Written by Andy Goth <andrew.m.goth@gmail.com> http://fb.com/andygoth
' 2020-04-12 1.0: Initial release
' 2020-04-12 1.1: Correct bugs regarding piece holding
DEFINT A-Z

CONST verMajor = 1      ' Major version number
CONST verMinor = 1      ' Minor version number
CONST verDate = "12 APRIL 2020" ' Version date string, all caps
CONST FALSE = 0         ' All bits zero
CONST TRUE = -1         ' All bits one
CONST pi! = 3.141593    ' Ratio between circumference and diameter
CONST flashCycles = 10  ' Number of flash cycles before lines are cleared
CONST flashTime! = .05  ' Time in seconds between flash cycles
CONST slowTime! = .5    ' Slowest gravity time
CONST fastTime! = .05   ' Fastest gravity time
CONST fastLevel = 9     ' Zero-based level with fastest gravity
CONST loseTime! = .05   ' Top out animation time
CONST moveNone = 0, moveLeft = 1, moveRight = 2, moveUp = 3, moveDown = 4
CONST moveGravity = 5, moveDrop = 6, moveCcw = 7, moveCw = 8, moveStart = 9
CONST moveHold = 10
CONST scoreLevel = 1, scoreWins = 2, scoreTotal = 3, scoreSoftDrop = 4
CONST scoreHardDrop = 5, scoreLines = 6, scoreSingles = 7, scoreDoubles = 8
CONST scoreTriples = 9, scoreQuads = 10, scoreT = 11, scoreJ = 12, scoreZ = 13
CONST scoreO = 14, scoreS = 15, scoreL = 16, scoreI = 17
CONST configStartingLevel = 0, configStartingHeight = 1
CONST configStartingDensity = 2, configLinesPerLevel = 3
CONST config2xJunkHeight = 4, config2xJunkDensity = 5
CONST config3xJunkHeight = 6, config3xJunkDensity = 7
CONST config4xJunkHeight = 8, config4xJunkDensity = 9
CONST shapeZ = 0, shapeL = 1, shapeO = 2, shapeS = 3, shapeI = 4
CONST shapeJ = 5, shapeT = 6

TYPE player
    x0 AS INTEGER       ' Board leftmost X screen coordinate
    x AS INTEGER        ' Piece column
    y AS INTEGER        ' Piece row
    r AS INTEGER        ' Piece rotation (0 through 3)
    s AS INTEGER        ' Piece shape (0=Z, 1=L, 2=O, 3=S, 4=I, 5=J, 6=T)
    n AS INTEGER        ' Next piece shape
    h AS INTEGER        ' Held piece shape, or -1 if none
    justHeld AS INTEGER ' TRUE if hold is currently unavailable
    level AS INTEGER    ' Zero-based level number
    flash AS INTEGER    ' Flash cycle count, 0 if inactive
    tFlash AS SINGLE    ' Next flash cycle time
    tGravity AS SINGLE  ' Next gravity movement time
    lose AS INTEGER     ' Top out animation cycle count, 0 if inactive
    tLose AS SINGLE     ' Top out animation update time
    intro AS INTEGER    ' TRUE if the intro screen is shown
    config AS INTEGER   ' Configuration cursor row
END TYPE
TYPE yRange
    y0 AS INTEGER       ' Upper row
    y1 AS INTEGER       ' Lower row
END TYPE

DIM SHARED shape(3, 3, 3, 6)            ' sx, sy, r, s
DIM SHARED board(9, 19, 1)              ' bx, by, p
DIM SHARED tile(51, 29)                 ' data, t
DIM SHARED player(1) AS player          ' p
DIM SHARED score(1 TO 17, 1) AS LONG    ' code, p
DIM SHARED flash(3, 1)                  ' i, p
DIM SHARED font(16, 43)                 ' data, c
DIM SHARED config(9, 1)                 ' code, p
DIM SHARED configMin(9)                 ' code
DIM SHARED configMax(9)                 ' code
DIM SHARED bits(19) AS LONG             ' 2^index lookup table

font:
'    0        1        2        3        4        5        6        7
DATA &Hfbbbf, &H6e66f, &Hf3fcf, &Hf373f, &Hbbf33, &Hfcf3f, &Hfcfdf, &Hf36cc
'    8        9        A        B        C        D        E        F
DATA &Hfbfbf, &Hfbf3f, &H6bfbb, &Hedede, &H6dcd6, &Heddde, &Hfcecf, &Hfcecc
'    G        H        I        J        K        L        M        N
DATA &H78b97, &Hbbfbb, &Hf666f, &H733b6, &Hddedd, &Hccccf, &Hdf999, &H9dfb9
'    O        P        Q        R        S        T        U        V
DATA &H6bbb6, &Hedecc, &H6bba5, &Hededd, &H7cf3e, &Hf6666, &Hbbbbf, &Hbbba4
'    W        X        Y        Z        ,        -        .        /
DATA &H99bfd, &Hdd6bb, &Hbb73e, &Hf36cf, &H00064, &H00f00, &H00066, &H336cc
'    :        <        >        @
DATA &H66066, &H36c63, &Hc636c, &H6bb86
' Piece shapes, expressed as 4x4 bitfields.
ShapeData: DATA &H4620, &H44c0, &H0660, &H2640, &H4444, &Hc440, &H4c40
' Piece hues, to be multiplied by 2*pi.
HueData: DATA 0, .15, .18, .3, .5, .6, .75
' Configuration code/minimum/maximum/default.
config:
DATA configStartingLevel  , 1, 10,  1
DATA configStartingHeight , 0, 15,  3
DATA configStartingDensity, 1,  9,  6
DATA configLinesPerLevel  , 1, 99, 10
DATA config2xJunkHeight   , 0, 10,  1
DATA config2xJunkDensity  , 1,  9,  9
DATA config3xJunkHeight   , 0, 10,  1
DATA config3xJunkDensity  , 1,  9,  7
DATA config4xJunkHeight   , 0, 10,  2
DATA config4xJunkDensity  , 1,  9,  5

' Maximize keyboard typematic rate.
WAIT &H64, &H2, &H2: OUT &H60, &HF3
WAIT &H64, &H2, &H2: OUT &H60, 0
WAIT &H64, &H2, &H2: OUT &H60, 0

' Clear flash arrays.
FOR p = 0 TO 1
    FOR i = 0 TO 3
        flash(i, p) = -1
    NEXT
NEXT

' Initialize board positions.
player(0).x0 = 8
player(1).x0 = 212

' Pick initial pieces
player(0).n = INT(RND * 7)
player(1).n = INT(RND * 7)

' Initialize screen.
RANDOMIZE TIMER
CLS
SCREEN 13

' Load data.
LoadBits
LoadShapes
LoadPalette
LoadTiles
LoadFont
LoadConfig

' Initialize score and draw boards.
FOR p = 0 TO 1
    player(p).intro = TRUE
    DrawIntro p
    SetScore scoreWins, 0, p
    ResetPlayer p
NEXT
DrawBackground

' Main loop.
DO
    ' Decode input.
    dir = moveNone
    p = 0
    c$ = INKEY$
    SELECT CASE LEFT$(c$, 1)
    CASE "a", "A": p = 0: dir = moveLeft
    CASE "d", "D": p = 0: dir = moveRight
    CASE "w", "W": p = 0: dir = moveUp
    CASE "s", "S": p = 0: dir = moveDown
    CASE " ":      p = 0: dir = moveDrop
    CASE "z", "Z": p = 0: dir = moveCcw
    CASE "x", "X": p = 0: dir = moveCw
    CASE "c", "C": p = 0: dir = moveHold
    CASE ",", "<": p = 1: dir = moveCcw
    CASE ".", ">": p = 1: dir = moveCw
    CASE "/", "?": p = 1: dir = moveDrop
    CASE "'", CHR$(34): p = 1: dir = moveHold
    CASE CHR$(27): EXIT DO
    CASE CHR$(0):
        SELECT CASE ASC(RIGHT$(c$, 1))
        CASE 59: p = 0: dir = moveStart
        CASE 60: p = 1: dir = moveStart
        CASE 75: p = 1: dir = moveLeft
        CASE 77: p = 1: dir = moveRight
        CASE 72: p = 1: dir = moveUp
        CASE 80: p = 1: dir = moveDown
        END SELECT
    END SELECT

    ' Perform requested movement.
    IF dir <> moveNone THEN
        IF player(p).intro THEN
            MoveIntro dir, p
        ELSEIF player(p).lose = 0 AND (player(p).flash <= 0 OR (dir <> moveDown AND dir <> moveDrop)) THEN
            MovePiece dir, p
        END IF
    END IF

    ' Process timed events.
    FOR p = 0 TO 1
        IF player(p).intro THEN
            ' There are no timed events while the intro is displayed.
        ELSEIF player(p).lose > 0 THEN
            ' Process top out animation.
            IF Expired(player(p).tLose) THEN
                ProcessTopOut p
            END IF
        ELSEIF player(p).flash > 0 THEN
            ' Process flashing.
            IF Expired(player(p).tFlash) THEN
                ProcessFlash p
            END IF
        ELSEIF Expired(player(p).tGravity) THEN
            ' Periodically force downward movement.
            MovePiece moveGravity, p
            player(p).tGravity = FallTime!(p)
        END IF
    NEXT
LOOP

FUNCTION BluFromHsv! (h!, s!, v!)
BluFromHsv! = v! * (s! * (COS((h! + 1.333333) * 2 * pi!) / 2 - .5) + 1)
END FUNCTION

FUNCTION Bottom (p)
FOR by = player(p).y TO 19
    IF Hit(player(p).x, by, player(p).r, player(p).s, p) THEN
        EXIT FOR
    END IF
NEXT
Bottom = by - 1
END FUNCTION

SUB ClearBoard (p)
FOR by = 0 TO 19
    FOR bx = 0 TO 9
        board(bx, by, p) = -1
    NEXT
NEXT
END SUB

SUB DrawBackground
' Draw vertical divider line.
LINE (159, 0)-(159, 199), 20, , &HCCCC
LINE (159, 0)-(159, 199), 18, , &H3333
LINE (160, 0)-(160, 199), 19, , &HCCCC
LINE (160, 0)-(160, 199), 17, , &H3333

' Draw labels.
FOR x = 118 TO 162 STEP 44
    DrawText x, 0, "NEXT"
    y = 32
    DrawText x, y, "HOLD": y = y + 6
    DrawText x, y, "LEVEL": y = y + 12
    DrawText x, y, "WINS": y = y + 12
    DrawText x, y, "SCORE": y = y + 12
    DrawText x, y, "SOFTDROP": y = y + 12
    DrawText x, y, "HARDDROP": y = y + 12
    DrawText x, y, "LINES": y = y + 12
    DrawText x, y, "SINGLES": y = y + 12
    DrawText x, y, "DOUBLES": y = y + 12
    DrawText x, y, "TRIPLES": y = y + 12
    DrawText x, y, "QUADS": y = y + 12
    DrawText x, y, "T": y = y + 6
    DrawText x, y, "J": y = y + 6
    DrawText x, y, "Z": y = y + 6
    DrawText x, y, "O": y = y + 6
    DrawText x, y, "S": y = y + 6
    DrawText x, y, "L": y = y + 6
    DrawText x, y, "I"
NEXT
END SUB

SUB DrawBoard (bx0i, by0i, bx1i, by1i, p)
' Inhibit drawing while the intro is shown.
IF player(p).intro THEN EXIT SUB

' Clip draw region to board extents.
IF bx0i < 0 THEN bx0 = 0 ELSE bx0 = bx0i
IF by0i < 0 THEN by0 = 0 ELSE by0 = by0i
IF bx1i > 9 THEN bx1 = 9 ELSE bx1 = bx1i
IF by1i > 19 THEN by1 = 19 ELSE by1 = by1i
IF bx0 > bx1 OR by0 > by1 THEN EXIT SUB

' Determine background tile according to level.
bg = 15 + player(p).level MOD 7

' Loop over all rows in the clipped draw region.
x0 = player(p).x0 + bx0 * 10
y = by0 * 10
sy = by0 - player(p).y
gy = by0 - Bottom(p)
FOR by = by0 TO by1
    ' Check if this row is flashing.
    flashing = FALSE
    IF player(p).flash THEN
        FOR i = 0 TO 3
            IF flash(i, p) = by THEN
                flashing = TRUE
                EXIT FOR
            END IF
        NEXT
    END IF

    ' Loop over all tiles to be drawn in this row.
    x = x0
    sx = bx0 - player(p).x
    FOR bx = bx0 TO bx1
        T = board(bx, by, p)                ' Locked piece tile.
        IF T < 0 THEN
            T = bg                          ' Background tile.
        ELSEIF flashing THEN
            IF player(p).flash AND 1 THEN
                T = 0                       ' Flash: gray tile.
            ELSE
                T = T + 7                   ' Flash: active piece tile.
            END IF
        END IF
        IF 0 <= sx AND sx <= 3 THEN
            IF 0 <= gy AND gy <= 3 THEN
                IF shape(sx, gy, player(p).r, player(p).s) THEN
                    T = player(p).s + 23    ' Ghost tile.
                END IF
            END IF
            IF 0 <= sy AND sy <= 3 THEN
                IF shape(sx, sy, player(p).r, player(p).s) THEN
                    T = player(p).s + 8     ' Active piece tile.
                END IF
            END IF
        END IF
        PUT (x, y), tile(0, T), PSET
        x = x + 10: sx = sx + 1
    NEXT
    y = y + 10: sy = sy + 1: gy = gy + 1
NEXT
END SUB

SUB DrawIntro (p)
' Draw intro text.
x0 = player(p).x0
LINE (x0, 0)-(x0 + 99, 199), 0, BF
y = 0
GOSUB Divider
IF p = 0 THEN
    DrawText x0, y, "PLAYER 1 CONTROLS": y = y + 6
    GOSUB Divider
    DrawText x0, y, "MOVE LEFT     A": y = y + 6
    DrawText x0, y, "MOVE RIGHT    D": y = y + 6
    DrawText x0, y, "MOVE UP       W": y = y + 6
    DrawText x0, y, "MOVE DOWN     S": y = y + 6
    DrawText x0, y, "ROTATE CCW    Z": y = y + 6
    DrawText x0, y, "ROTATE CW     X": y = y + 6
    DrawText x0, y, "HARD DROP     SPACE": y = y + 6
    DrawText x0, y, "HOLD PIECE    C": y = y + 6
ELSE
    DrawText x0, y, "PLAYER 2 CONTROLS": y = y + 6
    GOSUB Divider
    DrawText x0, y, "MOVE LEFT     LEFT": y = y + 6
    DrawText x0, y, "MOVE RIGHT    RIGHT": y = y + 6
    DrawText x0, y, "MOVE UP       UP": y = y + 6
    DrawText x0, y, "MOVE DOWN     DOWN": y = y + 6
    DrawText x0, y, "ROTATE CCW    COMMA": y = y + 6
    DrawText x0, y, "ROTATE CW     PERIOD": y = y + 6
    DrawText x0, y, "HARD DROP     SLASH": y = y + 6
    DrawText x0, y, "HOLD PIECE    QUOTE": y = y + 6
END IF
GOSUB Divider
DrawText x0, y, "COMMON CONTROLS": y = y + 6
GOSUB Divider
DrawText x0, y, "PAUSE GAME    PAUSE": y = y + 6
DrawText x0, y, "QUIT GAME     ESCAPE": y = y + 6
DrawText x0, y, "START AGAIN   F5": y = y + 6
DrawText x0, y, "QUIT TO DOS   ALT-FX": y = y + 6
GOSUB Divider
DrawText x0, y, "CONFIGURATION": y = y + 6
GOSUB Divider
DrawText x0, y, "STARTING LEVEL": y = y + 6
DrawText x0, y, "STARTING HEIGHT": y = y + 6
DrawText x0, y, "STARTING DENSITY": y = y + 6
DrawText x0, y, "LINES PER LEVEL": y = y + 6
DrawText x0, y, "2X JUNK HEIGHT": y = y + 6
DrawText x0, y, "2X JUNK DENSITY": y = y + 6
DrawText x0, y, "3X JUNK HEIGHT": y = y + 6
DrawText x0, y, "3X JUNK DENSITY": y = y + 6
DrawText x0, y, "4X JUNK HEIGHT": y = y + 6
DrawText x0, y, "4X JUNK DENSITY": y = y + 6
GOSUB Divider
IF p = 0 THEN
    DrawText x0, y, "VERSION " + LTRIM$(STR$(verMajor)) + "." + LTRIM$(STR$(verMinor)): y = y + 6
    DrawText x0, y, verDate$: y = y + 6
    GOSUB Divider
ELSE
    DrawText x0, y, "WRITTEN BY ANDY GOTH": y = y + 6
    GOSUB Divider
    DrawText x0, y, "<ANDREW.M.GOTH": y = y + 6
    DrawText x0, y, "@GMAIL.COM>": y = y + 6
    DrawText x0, y, "//FB.COM/ANDYGOTH": y = y + 6
END IF
y = 190
GOSUB Divider
IF p = 0 THEN
    DrawText x0, y, "PRESS F1 TO START": y = y + 6
ELSE
    DrawText x0, y, "PRESS F2 TO START": y = y + 6
END IF
GOSUB Divider

' Draw configuration.
FOR code = 0 TO UBOUND(config)
    SetConfig code, config(code, p), p
NEXT
MoveIntro moveNone, p
EXIT SUB

Divider:
' Draw a horizontal line.
LINE (x0, y)-(x0 + 99, y), 125: y = y + 1
LINE (x0, y)-(x0 + 99, y), 116: y = y + 1
RETURN
END SUB

SUB DrawNext (p, old)
' Locate the indicator onscreen.
x0 = player(p).x0
IF p = 0 THEN
    x0 = x0 + 110
ELSE
    x0 = x0 - 50
END IF

' Find dimensions and screen coordinates of next piece.
s = player(p).n
PieceSize sx0, sy0, sx1, sy1, 0, s
nx0 = x0 + (3 + sx0 - sx1) * 5
ny0 = (3 + sy0 - sy1) * 5 - 1
nx1 = nx0 + (1 + sx1 - sx0) * 10 - 1
ny1 = ny0 + (1 + sy1 - sy0) * 10 - 1

' Draw the piece.
y = ny0
FOR sy = sy0 TO sy1
    x = nx0
    FOR sx = sx0 TO sx1
        IF shape(sx, sy, 0, s) THEN
            PUT (x, y), tile(0, s + 1), PSET
        ELSE
            LINE (x, y)-(x + 9, y + 9), 0, BF
        END IF
        x = x + 10
    NEXT
    y = y + 10
NEXT

' Find dimensions and screen coordinates of prior piece.
PieceSize sx0, sy0, sx1, sy1, 0, old
ox0 = x0 + (3 + sx0 - sx1) * 5
oy0 = (3 + sy0 - sy1) * 5 - 1
ox1 = ox0 + (1 + sx1 - sx0) * 10 - 1
oy1 = oy0 + (1 + sy1 - sy0) * 10 - 1

' Erase anything left over from the prior piece.
IF oy0 < ny0 THEN LINE (ox0, oy0)-(ox1, ny0 - 1), 0, BF
IF ox0 < nx0 THEN LINE (ox0, ny0)-(nx0 - 1, ny1), 0, BF
IF ox1 > nx1 THEN LINE (nx1 + 1, ny0)-(ox1, ny1), 0, BF
IF oy1 > ny1 THEN LINE (ox0, ny1 + 1)-(ox1, oy1), 0, BF

' Display the name of the held piece.
SELECT CASE player(p).h
    CASE shapeZ: name$ = "Z"
    CASE shapeL: name$ = "L"
    CASE shapeO: name$ = "O"
    CASE shapeS: name$ = "S"
    CASE shapeI: name$ = "I"
    CASE shapeJ: name$ = "J"
    CASE shapeT: name$ = "T"
    CASE ELSE: name$ = "-"
END SELECT
DrawText x0 + 35, 32, name$
END SUB

SUB DrawOutline (p%)
x0 = player(p).x0
c = 31 + ((player(p).level + 1) MOD 7) * 16
FOR x = 0 TO 7
    LINE (x0 - 1 - x, 0)-(x0 - 1 - x, 199), c - x * 2 - 1, , &HAAAA
    LINE (x0 - 1 - x, 0)-(x0 - 1 - x, 199), c - x * 2, , &H5555
    LINE (x0 + 100 + x, 0)-(x0 + 100 + x, 199), c - x * 2 - 1, , &HAAAA
    LINE (x0 + 100 + x, 0)-(x0 + 100 + x, 199), c - x * 2, , &H5555
NEXT
END SUB

SUB DrawText (x0, y, text$)
x = x0
FOR i = 1 TO LEN(text$)
    c = ASC(MID$(text$, i, 1))
    IF 48 <= c AND c <= 57 THEN
        PUT (x, y), font(0, c - 48), PSET
    ELSEIF 65 <= c AND c <= 90 THEN
        PUT (x, y), font(0, c - 55), PSET
    ELSE
        SELECT CASE c
        CASE 44: PUT (x, y), font(0, 36), PSET
        CASE 45: PUT (x, y), font(0, 37), PSET
        CASE 46: PUT (x, y), font(0, 38), PSET
        CASE 47: PUT (x, y), font(0, 39), PSET
        CASE 58: PUT (x, y), font(0, 40), PSET
        CASE 60: PUT (x, y), font(0, 41), PSET
        CASE 62: PUT (x, y), font(0, 42), PSET
        CASE 64: PUT (x, y), font(0, 43), PSET
        CASE ELSE: LINE (x, y)-(x + 4, y + 5), 0, BF
        END SELECT
    END IF
    x = x + 5
NEXT
END SUB

FUNCTION Expired (deadline!)
T! = TIMER
IF deadline! < 86400 THEN
    Expired = deadline! <= T!
ELSE
    Expired = deadline! - 86400 <= T! AND T! < 43200
END IF
END FUNCTION

FUNCTION FallTime! (p)
IF player(p).level < fastLevel THEN
    delay! = slowTime + (fastTime - slowTime) * player(p).level / fastLevel
    FallTime = TIMER + delay!
ELSE
    FallTime = TIMER + fastTime
END IF
END FUNCTION

FUNCTION GrnFromHsv! (h!, s!, v!)
GrnFromHsv! = v! * (s! * (COS((h! + .6666667) * 2 * pi!) / 2 - .5) + 1)
END FUNCTION

FUNCTION Hit (bx0, by0, r, s, p)
by = by0
FOR sy = 0 TO 3
    bx = bx0
    FOR sx = 0 TO 3
        IF shape(sx, sy, r, s) THEN
            IF bx < 0 OR 9 < bx OR 19 < by THEN
                Hit = -1
                EXIT FUNCTION
            ELSEIF 0 <= by THEN
                IF board(bx, by, p) >= 0 THEN
                    Hit = -1
                    EXIT FUNCTION
                END IF
            END IF
        END IF
        bx = bx + 1
    NEXT
    by = by + 1
NEXT
Hit = 0
END FUNCTION

SUB IncrementScore (code, value, p)
SetScore code, score(code, p) + value, p
END SUB

SUB LoadBits
FOR i = 0 TO UBOUND(bits)
    bits(i) = 2 ^ i
NEXT
END SUB

SUB LoadConfig
RESTORE config
FOR code = 0 TO UBOUND(config)
    READ dummy$, configMin(code), configMax(code), config(code, 0)
    config(code, 1) = config(code, 0)
NEXT
END SUB

SUB LoadFont
DIM img(4, 5) 'x, y

' Load each character.
RESTORE font
FOR c = 0 TO UBOUND(font, 2)
    ' Clear fringe.
    FOR x = 0 TO 4
        img(x, 5) = 0
    NEXT
    FOR y = 0 TO 5
        img(4, y) = 0
    NEXT

    ' Unpack font into image buffer.
    READ v&
    i = 19
    FOR y = 0 TO 4
        FOR x = 0 TO 3
            img(x, y) = (v& AND bits(i)) <> 0
            i = i - 1
        NEXT
    NEXT

    ' Adjust colors and add shadows.
    FOR y = 0 TO 4
        FOR x = 0 TO 3
            IF img(x, y) < 0 THEN
                img(x, y) = 31 - y \ 2 - x
                IF img(x + 1, y + 0) = 0 THEN
                    img(x + 1, y + 0) = 17 + y \ 2 + x
                END IF
                IF img(x + 1, y + 1) = 0 THEN
                    img(x + 1, y + 1) = 17 + (y + 1) \ 2 + x
                END IF
                IF img(x + 0, y + 1) = 0 THEN
                    img(x + 0, y + 1) = 16 + (y + 1) \ 2 + x
                END IF
            END IF
        NEXT
    NEXT

    ' Store character image into memory.
    font(0, c) = 40
    font(1, c) = 6
    DEF SEG = VARSEG(font(2, c))
    ptr& = VARPTR(font(2, c))
    FOR y = 0 TO 5
        FOR x = 0 TO 4
            POKE ptr&, img(x, y)
            ptr& = ptr& + 1
        NEXT
    NEXT
NEXT
DEF SEG
END SUB

SUB LoadPalette
OUT &H3C8, 32
FOR k = 0 TO 1
    RESTORE HueData
    FOR j = 0 TO 6
        READ h!
        FOR i = 0 TO 15
            s! = COS((i - 7 + k) * pi! / 24)
            v! = i / 16
            IF k THEN
                v! = v! * .6666667 + .3333333
            END IF
            OUT &H3C9, INT(RedFromHsv(h!, s!, v!) * 63)
            OUT &H3C9, INT(GrnFromHsv(h!, s!, v!) * 63)
            OUT &H3C9, INT(BluFromHsv(h!, s!, v!) * 63)
        NEXT
    NEXT
NEXT
END SUB

SUB LoadShapes
' Loop over each shape.
RESTORE ShapeData
FOR s = 0 TO 6
    ' Load unrotated shape.
    READ v&
    i = 15
    FOR sy = 0 TO 3
        FOR sx = 0 TO 3
            shape(sy, sx, 0, s) = (v& AND bits(i)) <> 0
            i = i - 1
        NEXT
    NEXT

    ' Use 4x4 rotation for O and I; 3x3 rotation for Z, L, S, J, and T.
    a = -(s <> shapeO AND s <> shapeI)

    ' Construct rotations.
    FOR r = 1 TO 3
        FOR sy = 0 TO 3
            FOR sx = 0 TO 3 - a
                shape(sy, sx, r, s) = shape(3 - sx - a, sy, r - 1, s)
            NEXT
        NEXT
    NEXT
NEXT
END SUB

SUB LoadTiles
CONST max = 9                                   ' Tile size minus one
CONST half = max \ 2                            ' Center of tile
DIM img(1, -1 TO max + 1, -1 TO max + 1)        ' Image buffer
DIM kernel(-1 TO 1, -1 TO 1)                    ' 2D convolution kernel

' Initialize convolution kernel.
kernel(-1, -1) = 1: kernel(0, -1) = 2: kernel(1, -1) = 1
kernel(-1, 0) = 2: kernel(0, 0) = 4: kernel(1, 0) = 2
kernel(-1, 1) = 1: kernel(0, 1) = 2: kernel(1, 1) = 1

' Render piece tiles.
FOR y = 0 TO half
    FOR x = y TO max - y
        img(0, y, x) = 15 - y * 2
        img(0, x, y) = 15 - y * 2
        img(0, max - y, x) = 1 + y * 2
        img(0, x, max - y) = 1 + y * 2
    NEXT
NEXT
rounds = 2
mask = 1
GOSUB Filter
FOR T = 0 TO 14
    c = 16 * (T + 1)
    GOSUB Store
NEXT

' Render grid tiles.
FOR y = 0 TO max
    FOR x = 0 TO max
        img(0, x, y) = 0
    NEXT
NEXT
FOR x = 0 TO max
    img(0, 0, x) = 12
    img(0, x, 0) = 12
    img(0, max, x) = 4
    img(0, x, max) = 4
NEXT
rounds = 1
mask = 2
GOSUB Filter
FOR T = 15 TO 29
    c = 16 * (T - 14)
    GOSUB Store
NEXT
DEF SEG
EXIT SUB

Filter:
' Repeatedly filter tile.
FOR k = 1 TO rounds
    ' Blur tile using convolution.
    FOR y = 0 TO max
        FOR x = 0 TO max
            v = 0
            FOR y2 = -1 TO 1
                FOR i2 = -1 TO 1
                    v = v + img(0, x + i2, y + y2) * kernel(i2, y2)
                NEXT
            NEXT
            img(1, x, y) = v \ 15
        NEXT
    NEXT

    ' Copy back to first buffer, and apply special effect.
    FOR y = 0 TO max
        FOR x = 0 TO max
            img(0, x, y) = img(1, x, y) + (x + y + l AND mask)
        NEXT
    NEXT
NEXT
RETURN

Store:
tile(0, T) = (max + 1) * 8  ' Width in bits
tile(1, T) = (max + 1)      ' Height in pixels
DEF SEG = VARSEG(tile(2, T))
ptr& = VARPTR(tile(2, T))
FOR y = 0 TO max
    FOR x = 0 TO max
        POKE ptr&, img(0, x, y) + c
        ptr& = ptr& + 1
    NEXT
NEXT
RETURN
END SUB

SUB LockPiece (p)
by = player(p).y
FOR sy = 0 TO 3
    ' Lock this row of the piece into the board.
    bx = player(p).x
    FOR sx = 0 TO 3
        IF shape(sx, sy, player(p).r, player(p).s) THEN
            IF by < 0 THEN
                IF NOT player(1 - p).intro THEN
                    IncrementScore scoreWins, 1, 1 - p
                END IF
                player(p).lose = 1
                player(p).tLose = TIMER + loseTime
                EXIT SUB
            END IF
            board(bx, by, p) = player(p).s + 1
        END IF
        bx = bx + 1
    NEXT

    ' If this row is full, make it flash.
    IF by >= 0 THEN
        FOR bx = 0 TO 9
            IF board(bx, by, p) < 0 THEN EXIT FOR
        NEXT
        IF bx = 10 THEN
            player(p).tFlash = TIMER + flashTime!
            player(p).flash = flashCycles
            FOR i = 0 TO 3
                IF flash(i, p) < 0 THEN
                    flash(i, p) = by
                    EXIT FOR
                END IF
            NEXT
        END IF
    END IF

    ' Advance one row.
    by = by + 1
    IF by > 19 THEN EXIT FOR
NEXT

' Spawn and draw the next piece.
bx = player(p).x
by = player(p).y
SpawnPiece p, FALSE
player(p).justHeld = FALSE
DrawBoard bx, by, bx + 3, by + 3, p
END SUB

SUB MoveIntro (dir, p)
code = player(p).config
SELECT CASE dir
CASE moveNone
    GOSUB DrawCursor
CASE moveLeft
    IF configMin(code) < config(code, p) THEN
        SetConfig code, config(code, p) - 1, p
    END IF
CASE moveRight
    IF config(code, p) < configMax(code) THEN
        SetConfig code, config(code, p) + 1, p
    END IF
CASE moveUp
    GOSUB EraseCursor
    IF code > 0 THEN
        player(p).config = code - 1
    ELSE
        player(p).config = UBOUND(config)
    END IF
    GOSUB DrawCursor
CASE moveDown
    GOSUB EraseCursor
    IF code < UBOUND(config) THEN
        player(p).config = code + 1
    ELSE
        player(p).config = 0
    END IF
    GOSUB DrawCursor
CASE moveStart
    player(p).intro = FALSE
    ResetPlayer p
END SELECT
EXIT SUB

LocateCursor:
x = player(p).x0 + 80
y = 102 + player(p).config * 6
RETURN

EraseCursor:
GOSUB LocateCursor
LINE (x, y)-(x + 9, y + 4), 0, BF
RETURN

DrawCursor:
GOSUB LocateCursor
LINE (x + 7, y + 0)-(x + 9, y + 2), 141
LINE (x + 0, y + 1)-(x + 7, y + 1), 132
LINE (x + 0, y + 2)-(x + 9, y + 2), 141
LINE (x + 0, y + 3)-(x + 7, y + 3), 132
LINE (x + 7, y + 4)-(x + 9, y + 2), 141
RETURN
END SUB

SUB MovePiece (dir, p)
DIM yRange(3) AS yRange    ' Y coordinate ranges of dirty rectangles.

' Make working copies of piece position variables.
bx = player(p).x
by = player(p).y
r = player(p).r

' The first two dirty rectangles erase the old piece and ghost.
yRange(0).y0 = player(p).y
yRange(1).y0 = Bottom(p)

' Process the requested movement.
SELECT CASE dir
CASE moveLeft
    bx = bx - 1
    IF Hit(bx, by, r, player(p).s, p) THEN EXIT SUB
CASE moveRight
    bx = bx + 1
    IF Hit(bx, by, r, player(p).s, p) THEN EXIT SUB
CASE moveDown
    by = by + 1
    IF Hit(bx, by, r, player(p).s, p) THEN LockPiece p: EXIT SUB
    IncrementScore scoreSoftDrop, 1, p
    IncrementScore scoreTotal, 1, p
CASE moveGravity
    by = by + 1
    IF Hit(bx, by, r, player(p).s, p) THEN LockPiece p: EXIT SUB
CASE moveDrop
    by = Bottom(p)
    IncrementScore scoreHardDrop, by - player(p).y, p
    IncrementScore scoreTotal, 2 * (by - player(p).y), p
CASE moveCcw
    IF r < 3 THEN r = r + 1 ELSE r = 0
    GOSUB Wallkick
CASE moveCw
    IF r > 0 THEN r = r - 1 ELSE r = 3
    GOSUB Wallkick
CASE moveHold
    IF player(p).justHeld THEN
        EXIT SUB
    ELSEIF player(p).h < 0 THEN
        player(p).h = player(p).s
        SpawnPiece p, FALSE
    ELSE
        SpawnPiece p, TRUE
    END IF
    SWAP bx, player(p).x
    SWAP by, player(p).y
    player(p).justHeld = TRUE
END SELECT

' Find the X coordinates of the dirty rectangles.
IF player(p).x < bx THEN
    ux0 = player(p).x
    ux1 = bx + 3
ELSE
    ux0 = bx
    ux1 = player(p).x + 3
END IF

' Update the piece variables.
player(p).x = bx
player(p).y = by
player(p).r = r

' The last two dirty rectangles draw the new piece and ghost.
yRange(2).y0 = player(p).y
yRange(3).y0 = Bottom(p)

' Sort the dirty rectangles by Y coordinate.
yLimit = 3
FOR j = 0 TO 2
    FOR i = j + 1 TO 3
        IF yRange(j).y0 > yRange(i).y0 THEN SWAP yRange(j).y0, yRange(i).y0
    NEXT
NEXT

' Initially, each dirty rectangle is four rows tall.
FOR i = 0 TO 3
    yRange(i).y1 = yRange(i).y0 + 3
NEXT

' Merge adjacent and overlapping dirty rectangles.
FOR j = 0 TO 2
    DO WHILE j < yLimit AND yRange(j).y1 + 1 >= yRange(j + 1).y0
        yRange(j).y1 = yRange(j + 1).y1
        yLimit = yLimit - 1
        FOR i = j + 1 TO yLimit
            yRange(i) = yRange(i + 1)
        NEXT
    LOOP
NEXT

' Draw dirty rectangles.
FOR i = 0 TO yLimit
    DrawBoard ux0, yRange(i).y0, ux1, yRange(i).y1, p
NEXT

' Instantly lock piece after hard drop.
IF dir = moveDrop THEN LockPiece p
EXIT SUB

Wallkick:
' If rotation results in a collision, try to move the piece to compensate.  If
' no reasonable movement will clear the collision, reject the rotation.
IF Hit(bx, by, r, player(p).s, p) THEN
    IF NOT Hit(bx + 1, by, r, player(p).s, p) THEN
        bx = bx + 1
    ELSEIF NOT Hit(bx - 1, by, r, player(p).s, p) THEN
        bx = bx - 1
    ELSEIF NOT Hit(bx + 2, by, r, player(p).s, p) THEN
        bx = bx + 2
    ELSEIF NOT Hit(bx - 2, by, r, player(p).s, p) THEN
        bx = bx - 2
    ELSE
        EXIT SUB
    END IF
END IF
RETURN
END SUB

SUB PieceSize (sx0, sy0, sx1, sy1, r, s)
sy0 = 3: sy1 = 0: sx0 = 3: sx1 = 0
FOR sy = 0 TO 3
    FOR sx = 0 TO 3
        IF shape(sx, sy, r, s) THEN
            IF sy < sy0 THEN sy0 = sy
            IF sx < sx0 THEN sx0 = sx
            IF sy1 < sy THEN sy1 = sy
            IF sx1 < sx THEN sx1 = sx
        END IF
    NEXT
NEXT
END SUB

SUB ProcessFlash (p)
IF player(p).flash > 1 THEN
    player(p).tFlash = TIMER + flashTime!
    player(p).flash = player(p).flash - 1
    FOR i = 0 TO 3
        DrawBoard 0, flash(i, p), 9, flash(i, p), p
    NEXT
ELSE
    ' When the flash animation finishes, clear the lines.
    player(p).flash = 0
    by1 = -1
    n = 0
    FOR i = 0 TO 3
        IF flash(i, p) >= 0 THEN
            IF flash(i, p) > by1 THEN
                by1 = flash(i, p)
            END IF
            FOR by = flash(i, p) TO 1 STEP -1
                FOR bx = 0 TO 9
                    board(bx, by, p) = board(bx, by - 1, p)
                NEXT
            NEXT
            FOR bx = 0 TO 9
                board(bx, 0, p) = -1
            NEXT
            flash(i, p) = -1
            n = n + 1
        END IF
    NEXT
    DrawBoard 0, 0, 9, by1, p

    ' Update the score.
    IncrementScore scoreLines, n, p
    SELECT CASE n
    CASE 1
        IncrementScore scoreSingles, 1, p
        IncrementScore scoreTotal, (level + 1) * 100, p
    CASE 2
        IncrementScore scoreDoubles, 1, p
        IncrementScore scoreTotal, (level + 1) * 300, p
        RaiseJunk config(config2xJunkHeight, 1 - p), config(config2xJunkDensity, 1 - p), 1 - p
    CASE 3
        IncrementScore scoreTriples, 1, p
        IncrementScore scoreTotal, (level + 1) * 500, p
        RaiseJunk config(config3xJunkHeight, 1 - p), config(config3xJunkDensity, 1 - p), 1 - p
    CASE 4
        IncrementScore scoreQuads, 1, p
        IncrementScore scoreTotal, (level + 1) * 800, p
        RaiseJunk config(config4xJunkHeight, 1 - p), config(config4xJunkDensity, 1 - p), 1 - p
    END SELECT
END IF
END SUB

SUB ProcessTopOut (p)
' Return to the intro after the animation completes.
IF player(p).lose = 75 THEN
    DrawIntro p
    player(p).intro = TRUE
    player(p).lose = 0
    EXIT SUB
END IF

IF player(p).lose <= 50 THEN
    ' Determine base color from current level.
    c0 = 32 + (player(p).level MOD 7) * 16

    ' Find color for vertical lines.
    x = player(p).lose - 1
    IF x < 50 THEN
        c = c0 + 15 - x \ 4
    ELSE
        c = c0 + (x - 52) \ 4
    END IF

    ' Find coordinates for vertical lines.
    x0 = player(p).x0 + x
    x1 = player(p).x0 + 99 - x

    ' Draw vertical lines.
    LINE (x0, 0)-(x0, 199), c, , &H5555
    LINE (x0, 0)-(x0, 199), c - 1, , &HAAAA
    LINE (x1, 0)-(x1, 199), c, , &H5555
    LINE (x1, 0)-(x1, 199), c - 1, , &HAAAA

    ' Draw colored static.
    c = c0 + (15 - player(p).lose \ 4)
    FOR x = x0 + 1 TO x1 - 1 STEP 3
        style& = bits(INT(RND * 16))
        IF style& > 32767 THEN style& = style& - 65536
        LINE (x, 0)-(x, 199), c, , style&
    NEXT
ELSE
    ' Draw black box.
    y = (player(p).lose - 51) * 8
    LINE (player(p).x0, y)-(player(p).x0 + 99, y + 7), 0, BF
END IF

' Update animation.
player(p).lose = player(p).lose + 1
player(p).tLose = TIMER + loseTime
END SUB

SUB RaiseJunk (j, n, p)
FOR by = 0 TO 19 - j
    FOR bx = 0 TO 9
        board(bx, by, p) = board(bx, by + j, p)
    NEXT
NEXT
FOR by = 20 - j TO 19
    FOR bx = 0 TO n - 1
        board(bx, by, p) = INT(RND * 7)
    NEXT
    FOR bx = n TO 9
        board(bx, by, p) = -1
    NEXT
    FOR i = 0 TO 9
        SWAP board(i, by, p), board(INT(RND * 10), by, p)
    NEXT
NEXT
DrawBoard 0, 0, 9, 19, p
END SUB

FUNCTION RedFromHsv! (h!, s!, v!)
RedFromHsv! = v! * (s! * (COS(h! * 2 * pi!) / 2 - .5) + 1)
END FUNCTION

SUB ResetPlayer (p)
ClearBoard p
player(p).h = -1
player(p).justHeld = FALSE
SpawnPiece p, FALSE
RaiseJunk config(configStartingHeight, p), config(configStartingDensity, p), p
SetScore scoreLevel, config(configStartingLevel, p), p
FOR code = scoreTotal TO UBOUND(score, 1)
    SetScore code, 0, p
NEXT
END SUB

SUB SetConfig (code, value, p)
config(code, p) = value
DrawText player(p).x0 + 90, 102 + code * 6, RIGHT$(STR$(config(code, p)), 2)
END SUB

SUB SetScore (code, value, p)
' Check if enough lines were cleared to go to the next level.
IF code = scoreLines THEN
    l = value \ config(configLinesPerLevel, p)
    IF l > player(p).level THEN
        SetScore scoreLevel, l + 1, p
    END IF
END IF

' Recolor the board when changing levels.
IF code = scoreLevel THEN
    player(p).level = value - 1
    DrawBoard 0, 0, 9, 19, p
    DrawOutline p
END IF

' Figure out where to put the score onscreen.
x = 118 + p * 44
IF code <= scoreQuads THEN
    y = 44 + (code - scoreLevel) * 12
    w = 8
ELSE
    y = 152 + (code - scoreQuads) * 6
    w = 6
    x = x + 10
END IF

' Display the score.
DrawText x, y, RIGHT$("       " + STR$(value), w)

' Save the new score.
score(code, p) = value
END SUB

SUB SpawnPiece (p, hold)
' Initialize the next piece.
old = player(p).n
IF hold THEN
    s = player(p).h
    player(p).n = player(p).s
    player(p).h = -1
ELSE
    s = player(p).n
    player(p).n = INT(RND * 7)
END IF
player(p).s = s
player(p).x = 3
player(p).y = (s = shapeZ OR s = shapeO OR s = shapeS) - 1
player(p).r = 0

' Draw the next piece.
DrawNext p, old
MovePiece moveNone, p

' Initiate gravity.
player(p).tGravity = FallTime(p)

' Update piece counts.
SELECT CASE s
    CASE shapeZ: IncrementScore scoreZ, 1, p
    CASE shapeL: IncrementScore scoreL, 1, p
    CASE shapeO: IncrementScore scoreO, 1, p
    CASE shapeS: IncrementScore scoreS, 1, p
    CASE shapeI: IncrementScore scoreI, 1, p
    CASE shapeJ: IncrementScore scoreJ, 1, p
    CASE shapeT: IncrementScore scoreT, 1, p
END SELECT
END SUB

