Created
September 9, 2011 19:06
-
-
Save FeepingCreature/1207053 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module tetris; | |
import sdl; // For graphics output | |
import std.math, std.file, std.random; | |
extern(C) int time(int*); | |
// Board size. size[0] is x is horizontal. size[1] is y is vertical. | |
alias size = (7, 14); | |
/* | |
The main board. Tracks a field of blocks, together with code to | |
check if a field is free and remove lines (and replace them with | |
the lines above). | |
Basic housekeeping stuff, nothing special. | |
*/ | |
class TetrisBoard { | |
int x (size[0] * size[1]) field; | |
void init() { for int i <- 0..field.length field[i] = 0; } | |
bool isFree(int x, y) { | |
if (x < 0 || y < 0 || x !< size[0] || y !< size[1]) return false; | |
return eval field[y * size[0] + x] == 0; | |
} | |
void set(int x, y, value) { field[y * size[0] + x] = value; } | |
int get(int x, y) { return field[y * size[0] + x]; } | |
void removeLine(int i) { | |
for (int y = i; y > 0; --y) { | |
for int x <- 0..size[0] { | |
set(x, y, get(x, y-1)); | |
} | |
} | |
for int x <- 0..size[0] | |
set(x, 0, value => 0); | |
} | |
void removeFullLines() { | |
for int y <- 0..size[1] { | |
bool isFull = true; | |
for int x <- 0..size[0] { | |
if isFree(x, y) isFull = false; | |
} | |
if (isFull) removeLine(y); | |
} | |
} | |
} | |
// Color 0 == "block is free". | |
alias Free = 0; | |
/* | |
A Tetris block is implemented as a series of positions on the board, | |
and a color. | |
vec2i == vector of two integers. | |
*/ | |
class Block { | |
vec2i[] poslist; | |
int color; | |
void init(int color) { this.color = color; } | |
/* | |
Try to move the block to a new position on a board. | |
First the old block positions are removed. | |
Then for every new position: if the block is already in use, | |
restore our old positions and return "false" for "update failed". | |
Otherwise fill in the new positions and return true. | |
*/ | |
bool tryUpdate(TetrisBoard tb, vec2i[] newlist) { | |
// remove old me | |
for auto pos <- poslist tb.set(pos.xy, Free); | |
void writePos() { for auto pos <- poslist tb.set(pos.xy, color); } | |
// check new me | |
for auto pos <- newlist if !tb.isFree(pos) { writePos; /* restore */ return false; } | |
// commit new me | |
poslist = newlist; | |
writePos; | |
return true; | |
} | |
} | |
/* | |
Rotate a list of coordinates. | |
This first finds the bounding box, | |
then translates the coordinates so that the middle | |
of the bounding box is at 0, | |
do a simple rotation around the origin via | |
(x, y) = (-y, x) | |
and translate back. | |
*/ | |
void rotate(vec2i[] inp) { | |
// find the bounding box of the input coordinates | |
vec2i | |
tl = vec2i(minlist [for v <- inp: v.x], minlist [for v <- inp: v.y]), | |
br = vec2i(maxlist [for v <- inp: v.x], maxlist [for v <- inp: v.y]); | |
// function to shift our list by an offset | |
void offsetBy(vec2i delta) { | |
for int i <- 0..inp.length | |
inp[i] += delta; | |
} | |
// the center point | |
auto half = (tl + br) / 2; | |
// make the center point the origin | |
offsetBy -half; | |
// rotation | |
for int i <- 0..inp.length using inp[i] | |
(x, y) = (-y, x); | |
// and move it back. | |
offsetBy half; | |
// A little hack to make rotation of the 2x2 box come out right. | |
// Even with this, this function still exhibits drift; | |
// that is, if you keep rotating a block it will wander to the right. | |
// I could fix that by introducing a per-block rotation counter and using that to offset the result | |
// but | |
// meh. | |
offsetBy vec2i(1, 0); | |
} | |
// Returns true if the block could be moved down on the board. | |
bool tryMoveDown(Block bl, TetrisBoard tb) { | |
auto list2 = bl.poslist.dup; // Copy the position list | |
for int i <- 0..list2.length | |
list2[i].y++; | |
return bl.tryUpdate(tb, list2); // and try to update the block's list. | |
} | |
// Main function! Huzzah! | |
void main() { | |
// give us a good prng .. we could seed this with time(), but a constant is easier to test. | |
auto rng = getPRNG 23; | |
auto tb = new TetrisBoard; | |
// Colours. Black Blue Green, Aqua Purple Yellow Dark Yellow Teal | |
auto cols = [vec3f(0), vec3f(0, 0, 1), vec3f(0, 1, 0), vec3f(0, 1, 1), vec3f(1, 0, 1), vec3f(1, 1, 0), vec3f(0.5, 0.5, 0), vec3f(0, 0.5, 0.5)]; | |
// get the screen position for a board position. | |
vec2i getPos(int x, int y) { | |
return vec2i(display.w * x / size[0], display.h * y / size[1]); | |
} | |
// "Render" our tetris board. | |
void draw(TetrisBoard tb) { | |
// Cross operator works like this: | |
// cross(0..2, 0..3) => [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)] | |
for (int x, int y) <- cross(0..size[0], 0..size[1]) { | |
auto col = cols[tb.get(x, y)]; | |
auto from = getPos(x, y), to = getPos(x+1, y+1); | |
// and fill in the box. | |
// technically speaking I should be doing int y2, int x2 here for speed reasons | |
// (this is very painful on the data cache, because it keeps jumping cache lines) | |
// but hell, it's a Tetris. Not exactly high-demand on performance. | |
for (int x2, int y2) <- cross(from.x..to.x, from.y..to.y) | |
display.pset(x2, y2, col); | |
} | |
} | |
// Open an SDL window. | |
screen (320, 480); | |
Block current; // the block currently controlled by the player. | |
alias NumBlocks = 7; // number of possible blocks | |
// This function tries to spawn a new block at the top. | |
bool tryRemakeCurrentBlock() { | |
auto kind = rng.rand() % NumBlocks; | |
current = new Block(kind + 1); // remember, 0 is "no block". Thus, start with 1. | |
vec2i[] blocklist; | |
// This is just the position list for each of the basic blocks. | |
if (kind == 0) blocklist = [for tup <- [(0, 0), (0, 1), (0, 2), (0, 3)]: vec2i(tup)].eval; // I block | |
if (kind == 1) blocklist = [for tup <- [(0, 0), (0, 1), (1, 1), (0, 2)]: vec2i(tup)].eval; // |- block | |
if (kind == 2) blocklist = [for tup <- [(0, 0), (0, 1), (0, 2), (1, 2)]: vec2i(tup)].eval; // L block | |
if (kind == 3) blocklist = [for tup <- [(1, 0), (1, 1), (1, 2), (0, 2)]: vec2i(tup)].eval; // Reverse L block | |
if (kind == 4) blocklist = [for tup <- [(0, 0), (0, 1), (1, 1), (1, 0)]: vec2i(tup)].eval; // Square block | |
if (kind == 5) blocklist = [for tup <- [(0, 0), (0, 1), (1, 1), (1, 2)]: vec2i(tup)].eval; // Inverse Z block | |
if (kind == 6) blocklist = [for tup <- [(1, 0), (1, 1), (0, 1), (0, 2)]: vec2i(tup)].eval; // Regular Z block | |
for int i <- 0..blocklist.length | |
blocklist[i].x += size[0] / 2; // Move the block from the left side to the center of the screen. | |
// Aaand .. try to update the block we created above. | |
// If this fails, it's game over. | |
return current.tryUpdate(tb, blocklist); | |
} | |
void tryRotate() { | |
if !current return; // no active block | |
auto newlist = current.poslist.dup; | |
rotate(newlist); | |
current.tryUpdate(tb, newlist); | |
} | |
// Sideways movement | |
void tryShift(int xshift) { | |
if !current return; | |
auto newlist = current.poslist.dup; | |
for int i <- 0..newlist.length | |
newlist[i].x += xshift; | |
current.tryUpdate(tb, newlist); | |
} | |
// Downwards movement. | |
void tryDrop() { | |
if !current return; | |
auto newlist = current.poslist.dup; | |
for int i <- 0..newlist.length | |
newlist[i].y ++; | |
current.tryUpdate(tb, newlist); | |
} | |
// Should be clear. | |
void handleInput() { | |
if (keyPushed[SDLK_UP]) tryRotate; | |
else if (keyPushed[SDLK_LEFT]) tryShift -1; | |
else if (keyPushed[SDLK_RIGHT]) tryShift 1; | |
else if (keyPressed[SDLK_DOWN]) tryDrop; | |
else return; | |
draw(tb); | |
} | |
// Spawn an initial block. | |
tryRemakeCurrentBlock; | |
int last = 0; // timekeeping. Blocks are dropped once per second. | |
int phase; // Simple two-phase state machine. | |
void dropPhase() { // phase 0 | |
if (!tryMoveDown(current, tb)) { // if this fails, our block has hit the bottom. | |
current = null; | |
phase = 1; | |
} | |
} | |
void scorePhase() { // 1 | |
tb.removeFullLines(); | |
if (!tryRemakeCurrentBlock()) | |
raise-error new Error "you lose lol"; // lol | |
phase = 0; // and go back to dropPhase. | |
} | |
// The main action of the game. | |
void updateGame() { | |
[&dropPhase, &scorePhase][phase](); | |
draw(tb); | |
} | |
/* | |
Main loop. | |
Once every second, update the game state machine. | |
Also handle input. | |
*/ | |
while true { | |
int t = time(null); | |
if (t != last) { last = t; updateGame; } | |
flip; // SDL helper function to update the screen and poll events. | |
handleInput; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment