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!

%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 :array0.. 3of coord %Relative coordinates of each cell that makes up the piece
shift :int%amount to shift by after rotating
c :int%color of the pieceendrecord

%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 :=50const width :=10const s :=10%scale multiplier for drawing. s = width/height per cell in pixels.

var grid :array0.. width + 1, -3.. height ofint%A 2-d array represents the game board. Pieces spawn with negative y values, so the grid starts at -3var pieces :array0.. 6of 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 downvar cPieceX, cPieceY :int%The coordinates of the current piece

%timingvar tickRate :int%rate of fall, in milliseconds. Change this for different difficulty settings.var currentTime :int%Stores the current timevar lastTime :int%Stores the time of last tick

%Inputvar keys :arraycharofbooleanvar keyReleased :int%keypresses only register when this is true. Prevents press-and-hold.

%Execution is controlled by a state machinetype states :enum(paused, playing, clearing, lose)%These are all the possible statesvar state : states %the current state. This decides what code runs in the main loop.

%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 arrayfunction 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 originfor i :0.. 3

%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)div2* p.shift
out.cell (i).y -=(r - 1)div2* p.shift
endforresult 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

%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

%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 piecevar x := p.cell (i).x + _x %save it to the arrayvar y := p.cell (i).y + _y
grid (x, y):= p.c
endforend setPiece

%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):intvar out :int:=0loopvar y :=(-1)*(_y - out) + height

%return true if row y is full, false otherwisefunction checkRow (y :int):booleanfor x :1.. width
if grid (x, y)=0thenresultfalse%if we found a zero, the row can't be full, so return falseendifendforresulttrue%If we got this far, the row must be full, so return true.end checkRow

%Returns true if the player lost. False otherwise.function checkLose :booleanfor x :1.. width
if grid (x, 0)not= 0then%Simply check if the top row has anything in it.resulttrueendifendforresultfalseend checkLose

%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

%Pieces are timed to drop every tickRate milliseconds.%Instead of a delay that holds up the whole program, we let the program run%at max speed all the time. When enough time has passed, the piece drops.

if(currentTime - lastTime > tickRate)then%Check if tickRate milliseconds has elapsedif checkBelow (currentPiece, cPieceX, cPieceY)then
cPieceY +=1%If possible, move the pieceelse%If piece cannot move, then its position is recorded in the grid array
setPiece (currentPiece, cPieceX, cPieceY)
cPieceX :=5%move the piece back to the top, and pick a new shape
cPieceY := -2
currentPiece := pieces (Rand.Int (0, 6))
state := states.clearing %Switch states to check for full rowsif checkLose then%If we lost, switch to lose state
state := states.lose
endifendif
lastTime := currentTime %record the time this frame occurred.endifcls
draw
verticalWrite ("Tetris!")View.Updateend playing

%in this state, the game is paused until p is pressed.%This function executes instantly. It runs one frame, then returns control to%the state machine. No delays, no blocking.proc paused
Input.KeyDown(keys)if keys ('p')thenif keyReleased =1then
state := states.playing %if p is pressed, return to game
keyReleased :=0endifelse
keyReleased :=1%otherwise, stay in this stateendifcls
draw
verticalWrite ("PAUSED")View.Updateend paused

%This state animates deleting and shifting rows.%This does not execute instantly. It can take control for an arbitrary amount%of time. When it's finished it returns control to the state machine.proc clearing
var delete := checkRows
if delete < 0then
state := states.playing
returnendif

%The state machine is just a case statement in a loop. As code executes,%the state changes. Instead of, say, a menu function calling an 'instructions'%function, the menu function sets the state to 'instructions' and ends. Then%The state machine, seeing the state, executes the 'instructions' procedure.%When 'instructions' ends, it sets the state back to 'menu', and the machine%executes 'menu' again.%Obviously, I don't have a menu or instructions, but the concept is the same.

%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 :array0.. 3of coord %Relative coordinates of each cell that makes up the piece
shift :int%amount to shift by after rotating
c :int%color of the pieceendrecord

%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 :=50const width :=10const s :=10%scale multiplier for drawing. s = width/height per cell in pixels.

var grid :array0.. width + 1, -3.. height ofint%A 2-d array represents the game board. Pieces spawn with negative y values, so the grid starts at -3var pieces :array0.. 6of 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 downvar cPieceX, cPieceY :int%The coordinates of the current piece

%timingvar tickRate :int%rate of fall, in milliseconds. Change this for different difficulty settings.var currentTime :int%Stores the current timevar lastTime :int%Stores the time of last tick

%Inputvar keys :arraycharofbooleanvar keyReleased :int%keypresses only register when this is true. Prevents press-and-hold.

%Execution is controlled by a state machinetype states :enum(paused, playing, clearing, lose)%These are all the possible statesvar state : states %the current state. This decides what code runs in the main loop.

%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 arrayfunction 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 originfor i :0.. 3

%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)div2* p.shift
out.cell (i).y -=(r - 1)div2* p.shift
endforresult 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

%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

%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 piecevar x := p.cell (i).x + _x %save it to the arrayvar y := p.cell (i).y + _y
grid (x, y):= p.c
endforend setPiece

%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):intvar out :int:=0loop%var y := (-1) * (_y - out) + height

%return true if row y is full, false otherwisefunction checkRow (y :int):booleanfor x :1.. width
if grid (x, y)=0thenresultfalse%if we found a zero, the row can't be full, so return falseendifendforresulttrue%If we got this far, the row must be full, so return true.end checkRow

%Returns true if the player lost. False otherwise.function checkLose :booleanfor x :1.. width
if grid (x, 0)not= 0then%Simply check if the top row has anything in it.resulttrueendifendforresultfalseend checkLose

%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

%Pieces are timed to drop every tickRate milliseconds.%Instead of a delay that holds up the whole program, we let the program run%at max speed all the time. When enough time has passed, the piece drops.

if(currentTime - lastTime > tickRate)then%Check if tickRate milliseconds has elapsedif checkBelow (currentPiece, cPieceX, cPieceY)then
cPieceY +=1%If possible, move the pieceelse%If piece cannot move, then its position is recorded in the grid array
setPiece (currentPiece, cPieceX, cPieceY)
cPieceX :=5%move the piece back to the top, and pick a new shape
cPieceY := -2
currentPiece := pieces (Rand.Int (0, 6))
state := states.clearing %Switch states to check for full rowsif checkLose then%If we lost, switch to lose state
state := states.lose
endifendif
lastTime := currentTime %record the time this frame occurred.endifcls
draw
verticalWrite ("Tetris!")View.Updateend playing

%in this state, the game is paused until p is pressed.%This function executes instantly. It runs one frame, then returns control to%the state machine. No delays, no blocking.proc paused
Input.KeyDown(keys)if keys ('p')thenif keyReleased =1then
state := states.playing %if p is pressed, return to game
keyReleased :=0endifelse
keyReleased :=1%otherwise, stay in this stateendifcls
draw
verticalWrite ("PAUSED")View.Updateend paused

%This state animates deleting and shifting rows.%This does not execute instantly. It can take control for an arbitrary amount%of time. When it's finished it returns control to the state machine.proc clearing
var delete := checkRows
if delete < 0then
state := states.playing
returnendif

%The state machine is just a case statement in a loop. As code executes,%the state changes. Instead of, say, a menu function calling an 'instructions'%function, the menu function sets the state to 'instructions' and ends. Then%The state machine, seeing the state, executes the 'instructions' procedure.%When 'instructions' ends, it sets the state back to 'menu', and the machine%executes 'menu' again.%Obviously, I don't have a menu or instructions, but the concept is the same.