
-----------------------------------
Insectoid
Sat Jan 21, 2017 8:51 pm

Tetris!
-----------------------------------
I haven't written any actual code in quite some time, other than bits and pieces here on the forums, so I whipped up a game of Tetris to make sure I still know how to program.

Neat features: 

-Major phases of the program are controlled by a state machine (the game itself, pause screen, lose screen, animations, etc). This means the main loop is really quite small, and everything that would have been in it has been moved off to their own 'state procedures'. As you might imagine, it makes switching between, creating and editing states really, really easy.

-Unlimited frame rate: There are no delays in the main game (some animations have delays). Timing is controlled by Time.Elapsed. This means frames are drawn and pushed to the screen as fast as possible, but more importantly, there's no input lag. Delays block Input.KeyDown, so some key presses (most, in the case of Tetris) would be missed.

-I think this project has more re-used code than anything else I've ever written. There were a few functions I thought would be hard, but by the time I got to them, I had so many small functions already written that I could glue together like legos to do what I wanted. One in particular would have been 20 or more lines long, but it turned out to be just two. I didn't plan that in advance, which was neat. It's not often in programming that you do something good by accident.

-I'm sure I did some other neat funky stuff in there but I can't think of any off hand. I tried to comment the code pretty well so you guys could learn from it. Have a gander and if you have any questions or suggestions, let me know.

There is one bug that I know of. Well, not really a bug, I'm just too lazy to do it. When a row is cleared, the column above drops down to that row and stops. What should happen, is the column should drop as far as it can without hitting something. It's basically just a reversal of the shiftBy function but I got bored so I didn't do it.

Oh yeah, uh, controls are left/right for left/right, up to rotate, down to go faster, p for pause. After you lose, hit enter to restart.

Don't try handing this in to your teacher. S/he will know you didn't write it. That's the Insectoid Guarantee!

%Tetris

/*
 ************************
 **** Declarations ****
 ************************
 */





%x/y coordinates
type coord :
    record
        x : int
        y : int
    end record

%This represents a piece on the board. 
%Note that it does not have absolute coordinates-
%There is only one active piece at a time, so I only have one set of global
%coordinates for the active piece- (cPieceX, cPieceY)
type piece :
    record
        cell : array 0 .. 3 of coord %Relative coordinates of each cell that makes up the piece
        shift : int %amount to shift by after rotating
        c : int %color of the piece
    end record

%height and width of the game board in cells
%Changing these shouldn't break the game itself, but rendering might not work.
%If you change height/width, you must also manually change height/width in the
%View.Set() line way at the bottom of this file.
const height := 50
const width := 10
const s := 10     %scale multiplier for drawing. s = width/height per cell in pixels.

var grid : array 0 .. width + 1, -3 .. height of int %A 2-d array represents the game board. Pieces spawn with negative y values, so the grid starts at -3
var pieces : array 0 .. 6 of piece     %This array holds one of each type of piece. Do not change. Should really be constant, but that just causes more pain.
var currentPiece : piece % This is the piece that is currently falling down
var cPieceX, cPieceY : int %The coordinates of the current piece

%timing
var tickRate : int %rate of fall, in milliseconds. Change this for different difficulty settings.
var currentTime : int %Stores the current time
var lastTime : int %Stores the time of last tick

%Input
var keys : array char of boolean
var keyReleased : int %keypresses only register when this is true. Prevents press-and-hold.

%Execution is controlled by a state machine
type states : enum (paused, playing, clearing, lose) %These are all the possible states
var state : states %the current state. This decides what code runs in the main loop.





/*
 **************************
 **** Piece Behavior ****
 **************************
 */






%Rotates a piece clockwise when r = 1, ccw when r = -1
%Should only be called on currentPiece or newly created pieces
%Do not use on pieces array
function rotatePiece (p : piece, r : int) : piece
    var out : piece %We don't want to edit p, so we make a new piece to return
    out.shift := p.shift
    out.c := p.c
    
    %Iterate over each piece segment and rotate it about the origin
    for i : 0 .. 3

        %This rotates the piece about the origin relative to the cells.
        %It's a long way of saying 'x := -y, y := -x'.
        out.cell (i).x := p.cell (i).y * r * -1
        out.cell (i).y := p.cell (i).x * r

        %This is an over-complicated way to say 'if left, shift right. If right, shift up'.
        %When we rotate a piece left, the x value becomes negative
        %When we rotate right, the y value becomes negative
        %So we shift up or right to push it back into positive land.
        %Can't just multiply x -1, because that flips the piece. We only want to
        %shift it
        out.cell (i).x += (r + 1) div 2 * p.shift
        out.cell (i).y -= (r - 1) div 2 * p.shift
    end for
    result out
end rotatePiece

%Here are some functions that just make things easier to read and write later on.
function rotateLeft (p : piece) : piece
    result rotatePiece (p, -1)
end rotateLeft

function rotateRight (p : piece) : piece
    result rotatePiece (p, 1)
end rotateRight

%returns true if cell is taken. False otherwise.
%We get a LOT of mileage out of this function.
function checkCell (x : int, y : int) : boolean
    result (grid (x, y) not= 0)
end checkCell


%returns true if piece p fits at (x,y)
function checkPiece (p : piece, x : int, y : int) : boolean
    for i : 0 .. 3
        if checkCell (p.cell (i).x + x, p.cell (i).y + y) then
            result false
        end if
    end for
    result true
end checkPiece

%Here's some more dinky little functions that make things easier to read

%result true if piece fits at (x, y+1)
function checkBelow (p : piece, x : int, y : int) : boolean
    result checkPiece (p, x, y + 1)
end checkBelow

%result true if piece fits at (x-1, y)
function checkLeft (p : piece, x : int, y : int) : boolean
    result checkPiece (p, x - 1, y)
end checkLeft

%result true if piece fits at (x+1, y)
function checkRight (p : piece, x : int, y : int) : boolean
    result checkPiece (p, x + 1, y)
end checkRight

%result true if piece can rotate right at (x, y)
function checkRotateRight (_p : piece, x : int, y : int) : boolean
    %I expected this function to be a pain to write,
    %but it turns out I already did all the work!
    %Just make a new piece, rotate it with the function we already have,
    %then check if that new piece can fit in (x, y), using the other function
    %we already have!
    var p : piece := rotateRight (_p)
    result checkPiece (p, x, y)
end checkRotateRight

%result true if piece can rotate left at (x,y)
function checkRotateLeft (_p : piece, x : int, y : int) : boolean
    var p : piece := rotateLeft (_p)
    result checkPiece (p, x, y)
end checkRotateLeft



%When a piece is falling, it is separate from the grid. It's drawn there,
%but if you look at the array itself, you'll find all 0's (ie empty) where the piece is supposed
%to be. The piece is only ever added to the grid array when it stop. This way, we 
%don't have to think about it anymore.
%This is the function that does that.
proc setPiece (p : piece, _x : int, _y : int)
    for i : 0 .. 3 %iterate over every segment of the piece
        var x := p.cell (i).x + _x %save it to the array
        var y := p.cell (i).y + _y
        grid (x, y) := p.c
    end for
end setPiece




/*
 ******************************
 **** Full Row Handling ****
 ******************************
*/



%Returns value of s at (_x, _y) for proc shiftColumn (_x, _y, s)
%shiftColumn needs to know how much to shift the column by.
%This function figures that out.
%Currently, when a row is deleted, blocks above will fall down and stop at that
%row, even if they could have fallen further. No plans to change at this time.
function shiftBy (_x : int, _y : int) : int
    var out : int := 0
    loop
        var y := (-1) * (_y - out) + height
        
        %These commented out lines visualize how this function works.
        %Uncomment them if you're curious.
        /*  
        %Draw.Box (10 * _x, 10 * y, 10 * _x + 10, 10 * y + 10, black)
        %View.Update
        %delay (10)
        */
        
        %...I forget how this part works.
        exit when _y - out  0 and s < height then
        for decreasing y : _y .. 0
            grid (_x, y + s) := grid (_x, y)
        end for
    end if
end shiftColumn

%shifts all columns at y down appropriately.
proc shiftColumns (y : int)
    for x : 1 .. width
        var n := shiftBy (x, y)
        % var n := 1
        shiftColumn (x, y - n, n)
    end for
end shiftColumns

%sets row at y to 0
proc deleteRow (y : int)
    for x : 1 .. width
        grid (x, y) := 0
    end for
end deleteRow

%sets row at y to 0, then shifts columns at y down.
proc deleteAndShift (y : int)
    deleteRow (y)
    shiftColumns (y)
end deleteAndShift



%return true if row y is full, false otherwise
function checkRow (y : int) : boolean
    for x : 1 .. width
        if grid (x, y) = 0 then
            result false %if we found a zero, the row can't be full, so return false
        end if
    end for
    result true %If we got this far, the row must be full, so return true.
end checkRow


%This finds the lowest full row and returns its y value
function checkRows : int
    for decreasing y : height - 1 .. 0
        if checkRow (y) then
            result y
        end if
    end for
    result - 1 %If we didn't find any, return -1.
end checkRows

%Returns true if the player lost. False otherwise.
function checkLose : boolean
    for x : 1 .. width
        if grid (x, 0) not= 0 then %Simply check if the top row has anything in it.
            result true
        end if
    end for
    result false
end checkLose


/*
 ***************************
 *****Initialization *****
 ***************************
 */




%This function makes things legible later on
proc initCell (p : int, c : int, x : int, y : int)     
    pieces (p).cell (c).x := x
    pieces (p).cell (c).y := y
end initCell

%initializes all pieces in original orientation
%Pieces live on a 3x3 or 4x4 grid. They consist of 4 cells, each
%with (x,y) coordinates relative to the origin of the piece itself.
%Because these coordinates are relative, you must add the absolute coordinates
%(cPieceX, cPieceY) to them to get the absolute position of each cell.
%Colors are also assigned at this time.
proc initPieces     
    %I
    initCell (0, 0, 0, 1)
    initCell (0, 1, 1, 1)
    initCell (0, 2, 2, 1)
    initCell (0, 3, 3, 1)
    pieces (0).shift := 3
    pieces (0).c := red

    %L
    initCell (1, 0, 0, 0)
    initCell (1, 1, 0, 1)
    initCell (1, 2, 1, 1)
    initCell (1, 3, 2, 1)
    pieces (1).shift := 2
    pieces (1).c := blue
    %J
    initCell (2, 0, 2, 0)
    initCell (2, 1, 0, 1)
    initCell (2, 2, 1, 1)
    initCell (2, 3, 2, 1)
    pieces (2).shift := 2
    pieces (2).c := green
    %S
    initCell (3, 0, 0, 0)
    initCell (3, 1, 1, 0)
    initCell (3, 2, 1, 1)
    initCell (3, 3, 2, 1)
    pieces (3).shift := 2
    pieces (3).c := yellow + 5
    %Z
    initCell (4, 0, 0, 1)
    initCell (4, 1, 1, 1)
    initCell (4, 2, 1, 0)
    initCell (4, 3, 2, 0)
    pieces (4).shift := 2
    pieces (4).c := purple
    %T
    initCell (5, 0, 0, 1)
    initCell (5, 1, 1, 1)
    initCell (5, 2, 2, 1)
    initCell (5, 3, 1, 0)
    pieces (5).shift := 2
    pieces (5).c := brown
    %

-----------------------------------
Insectoid
Sat Jan 21, 2017 9:30 pm

RE:Tetris!
-----------------------------------
Posting this gave me the motivation to fix that one issue, so now we've got sick chain reaction action! It makes the game a lot easier, so I reduced the tickrate to 100ms to increase the challenge. 

%Tetris

/*
 ************************
 **** Declarations ****
 ************************
 */





%x/y coordinates
type coord :
    record
        x : int
        y : int
    end record

%This represents a piece on the board.
%Note that it does not have absolute coordinates-
%There is only one active piece at a time, so I only have one set of global
%coordinates for the active piece- (cPieceX, cPieceY)
type piece :
    record
        cell : array 0 .. 3 of coord %Relative coordinates of each cell that makes up the piece
        shift : int %amount to shift by after rotating
        c : int %color of the piece
    end record

%height and width of the game board in cells
%Changing these shouldn't break the game itself, but rendering might not work.
%If you change height/width, you must also manually change height/width in the
%View.Set() line way at the bottom of this file.
const height := 50
const width := 10
const s := 10     %scale multiplier for drawing. s = width/height per cell in pixels.

var grid : array 0 .. width + 1, -3 .. height of int %A 2-d array represents the game board. Pieces spawn with negative y values, so the grid starts at -3
var pieces : array 0 .. 6 of piece     %This array holds one of each type of piece. Do not change. Should really be constant, but that just causes more pain.
var currentPiece : piece % This is the piece that is currently falling down
var cPieceX, cPieceY : int %The coordinates of the current piece

%timing
var tickRate : int %rate of fall, in milliseconds. Change this for different difficulty settings.
var currentTime : int %Stores the current time
var lastTime : int %Stores the time of last tick

%Input
var keys : array char of boolean
var keyReleased : int %keypresses only register when this is true. Prevents press-and-hold.

%Execution is controlled by a state machine
type states : enum (paused, playing, clearing, lose) %These are all the possible states
var state : states %the current state. This decides what code runs in the main loop.





/*
 **************************
 **** Piece Behavior ****
 **************************
 */






%Rotates a piece clockwise when r = 1, ccw when r = -1
%Should only be called on currentPiece or newly created pieces
%Do not use on pieces array
function rotatePiece (p : piece, r : int) : piece
    var out : piece %We don't want to edit p, so we make a new piece to return
    out.shift := p.shift
    out.c := p.c

    %Iterate over each piece segment and rotate it about the origin
    for i : 0 .. 3

        %This rotates the piece about the origin relative to the cells.
        %It's a long way of saying 'x := -y, y := -x'.
        out.cell (i).x := p.cell (i).y * r * -1
        out.cell (i).y := p.cell (i).x * r

        %This is an over-complicated way to say 'if left, shift right. If right, shift up'.
        %When we rotate a piece left, the x value becomes negative
        %When we rotate right, the y value becomes negative
        %So we shift up or right to push it back into positive land.
        %Can't just multiply x -1, because that flips the piece. We only want to
        %shift it
        out.cell (i).x += (r + 1) div 2 * p.shift
        out.cell (i).y -= (r - 1) div 2 * p.shift
    end for
    result out
end rotatePiece

%Here are some functions that just make things easier to read and write later on.
function rotateLeft (p : piece) : piece
    result rotatePiece (p, -1)
end rotateLeft

function rotateRight (p : piece) : piece
    result rotatePiece (p, 1)
end rotateRight

%returns true if cell is taken. False otherwise.
%We get a LOT of mileage out of this function.
function checkCell (x : int, y : int) : boolean
    result (grid (x, y) not= 0)
end checkCell


%returns true if piece p fits at (x,y)
function checkPiece (p : piece, x : int, y : int) : boolean
    for i : 0 .. 3
        if checkCell (p.cell (i).x + x, p.cell (i).y + y) then
            result false
        end if
    end for
    result true
end checkPiece

%Here's some more dinky little functions that make things easier to read

%result true if piece fits at (x, y+1)
function checkBelow (p : piece, x : int, y : int) : boolean
    result checkPiece (p, x, y + 1)
end checkBelow

%result true if piece fits at (x-1, y)
function checkLeft (p : piece, x : int, y : int) : boolean
    result checkPiece (p, x - 1, y)
end checkLeft

%result true if piece fits at (x+1, y)
function checkRight (p : piece, x : int, y : int) : boolean
    result checkPiece (p, x + 1, y)
end checkRight

%result true if piece can rotate right at (x, y)
function checkRotateRight (_p : piece, x : int, y : int) : boolean
    %I expected this function to be a pain to write,
    %but it turns out I already did all the work!
    %Just make a new piece, rotate it with the function we already have,
    %then check if that new piece can fit in (x, y), using the other function
    %we already have!
    var p : piece := rotateRight (_p)
    result checkPiece (p, x, y)
end checkRotateRight

%result true if piece can rotate left at (x,y)
function checkRotateLeft (_p : piece, x : int, y : int) : boolean
    var p : piece := rotateLeft (_p)
    result checkPiece (p, x, y)
end checkRotateLeft



%When a piece is falling, it is separate from the grid. It's drawn there,
%but if you look at the array itself, you'll find all 0's (ie empty) where the piece is supposed
%to be. The piece is only ever added to the grid array when it stop. This way, we
%don't have to think about it anymore.
%This is the function that does that.
proc setPiece (p : piece, _x : int, _y : int)
    for i : 0 .. 3 %iterate over every segment of the piece
        var x := p.cell (i).x + _x %save it to the array
        var y := p.cell (i).y + _y
        grid (x, y) := p.c
    end for
end setPiece




/*
 ******************************
 **** Full Row Handling ****
 ******************************
 */



%Returns value of s at (_x, _y) for proc shiftColumn (_x, _y, s)
%shiftColumn needs to know how much to shift the column by.
%This function figures that out.
%Currently, when a row is deleted, blocks above will fall down and stop at that
%row, even if they could have fallen further. No plans to change at this time.
function shiftBy (_x : int, _y : int) : int
    var out : int := 0
    loop
        %var y := (-1) * (_y - out) + height

        %These commented out lines visualize how this function works.
        %Uncomment them if you're curious.
        /*
         %Draw.Box (10 * _x, 10 * y, 10 * _x + 10, 10 * y + 10, black)
         %View.Update
         %delay (10)
         */

        %...I forget how this part works.
        exit when _y - out = (height - 1) or grid (x, y + out) not= 0
        out += 1
    end loop
    result out
end countEmptyDown

%shift column with base at (_x, _y) down by s
proc shiftColumn (_x : int, _y : int, s : int)
    if s > 0 and s < height then
        for decreasing y : _y .. 0
            grid (_x, y + s) := grid (_x, y)
        end for
    end if
end shiftColumn

%shifts all columns at y down appropriately.
proc shiftColumns (y : int)
    for x : 1 .. width
        var n := shiftBy (x, y)
        % var n := 1
        shiftColumn (x, y - n, n+countEmptyDown (x, y))
    end for
end shiftColumns

%sets row at y to 0
proc deleteRow (y : int)
    for x : 1 .. width
        grid (x, y) := 0
    end for
end deleteRow

%sets row at y to 0, then shifts columns at y down.
proc deleteAndShift (y : int)
    deleteRow (y)
    shiftColumns (y)
end deleteAndShift



%return true if row y is full, false otherwise
function checkRow (y : int) : boolean
    for x : 1 .. width
        if grid (x, y) = 0 then
            result false %if we found a zero, the row can't be full, so return false
        end if
    end for
    result true %If we got this far, the row must be full, so return true.
end checkRow


%This finds the lowest full row and returns its y value
function checkRows : int
    for decreasing y : height - 1 .. 0
        if checkRow (y) then
            result y
        end if
    end for
    result - 1 %If we didn't find any, return -1.
end checkRows

%Returns true if the player lost. False otherwise.
function checkLose : boolean
    for x : 1 .. width
        if grid (x, 0) not= 0 then %Simply check if the top row has anything in it.
            result true
        end if
    end for
    result false
end checkLose


/*
 ***************************
 *****Initialization *****
 ***************************
 */




%This function makes things legible later on
proc initCell (p : int, c : int, x : int, y : int)
    pieces (p).cell (c).x := x
    pieces (p).cell (c).y := y
end initCell

%initializes all pieces in original orientation
%Pieces live on a 3x3 or 4x4 grid. They consist of 4 cells, each
%with (x,y) coordinates relative to the origin of the piece itself.
%Because these coordinates are relative, you must add the absolute coordinates
%(cPieceX, cPieceY) to them to get the absolute position of each cell.
%Colors are also assigned at this time.
proc initPieces
    %I
    initCell (0, 0, 0, 1)
    initCell (0, 1, 1, 1)
    initCell (0, 2, 2, 1)
    initCell (0, 3, 3, 1)
    pieces (0).shift := 3
    pieces (0).c := red

    %L
    initCell (1, 0, 0, 0)
    initCell (1, 1, 0, 1)
    initCell (1, 2, 1, 1)
    initCell (1, 3, 2, 1)
    pieces (1).shift := 2
    pieces (1).c := blue
    %J
    initCell (2, 0, 2, 0)
    initCell (2, 1, 0, 1)
    initCell (2, 2, 1, 1)
    initCell (2, 3, 2, 1)
    pieces (2).shift := 2
    pieces (2).c := green
    %S
    initCell (3, 0, 0, 0)
    initCell (3, 1, 1, 0)
    initCell (3, 2, 1, 1)
    initCell (3, 3, 2, 1)
    pieces (3).shift := 2
    pieces (3).c := yellow + 5
    %Z
    initCell (4, 0, 0, 1)
    initCell (4, 1, 1, 1)
    initCell (4, 2, 1, 0)
    initCell (4, 3, 2, 0)
    pieces (4).shift := 2
    pieces (4).c := purple
    %T
    initCell (5, 0, 0, 1)
    initCell (5, 1, 1, 1)
    initCell (5, 2, 2, 1)
    initCell (5, 3, 1, 0)
    pieces (5).shift := 2
    pieces (5).c := brown
    %
