Tom Says: Safe code is boring code!
I like to think of LittleCoder as a Ruby interpreter with DirectX. It handles the main event loop and all the drawing functions for you, leaving only the fun parts of graphics coding (like game-making!). I discussed it in "Beginning Programming."
I dislike this code, but I think it's very much in the same style as the sample that comes with LittleCoder. Given LittleCoder's API, this was very easy to implement incrementally. It's longer and dirtier than I expected it to be, but since there may be some interest in what can be done with LittleCoder in a short amount of time, I may as well toss this out there.
I tested it with LittleCoder 0.4.
Also, a friend's XP computer blue-screened and immediately rebooted after running this, so I'd like to know if that can be reproduced by anyone else. I don't think it's the code; I think it's his wonky Windows install, which also crashes for some DirectX-based games. Whatever happens, let me know.
# Time between downward shifts, in seconds
$fall_time = 0.4
## Code begins
# Set up the camera
Scene.set_camera_pos 0, 0, -40
#Scene.set_posteffect_active true
# Debug function
$debug_count = 0
def debug
bloop = Scene.new_tile("demoblob.png")
bloop.set_pos(-10,11 - $debug_count * 2)
$debug_count += 1
end
# Define grid position and scale
$grid_left = -7
$grid_top = 11
$grid_width = 8
$grid_height = 12
$grid_spacing = 2
# Define the piece types
# When rotated, they pivot about the center
$types = [
[[ 0, 0, 0, 0, 0 ], ## 0
[ 0, 0, 0, 0, 0 ], ##
[ 0, 0, 1, 1, 0 ],
[ 0, 0, 1, 1, 0 ],
[ 0, 0, 0, 0, 0 ]],
[[ 0, 0, 0, 0, 0 ], # 1
[ 0, 0, 1, 0, 0 ], #
[ 0, 0, 1, 0, 0 ], #
[ 0, 0, 1, 0, 0 ], #
[ 0, 0, 1, 0, 0 ]],
[[ 0, 0, 0, 0, 0 ], ## 2
[ 0, 0, 0, 0, 0 ], #
[ 0, 1, 1, 0, 0 ], #
[ 0, 0, 1, 0, 0 ],
[ 0, 0, 1, 0, 0 ]],
[[ 0, 0, 0, 0, 0 ], ## 3
[ 0, 0, 0, 0, 0 ], #
[ 0, 0, 1, 1, 0 ], #
[ 0, 0, 1, 0, 0 ],
[ 0, 0, 1, 0, 0 ]],
[[ 0, 0, 0, 0, 0 ], ## 4
[ 0, 0, 0, 0, 0 ], ##
[ 0, 0, 1, 1, 0 ],
[ 0, 1, 1, 0, 0 ],
[ 0, 0, 0, 0, 0 ]],
[[ 0, 0, 0, 0, 0 ], ## 5
[ 0, 0, 0, 0, 0 ], ##
[ 0, 1, 1, 0, 0 ],
[ 0, 0, 1, 1, 0 ],
[ 0, 0, 0, 0, 0 ]] ]
# Set rotation properties to emulate "real" Tetris better
# This is hacky, depending on how you think about it
# Just think nice thoughts
# (Type 0, the square, doesn't rotate or alternate, so it's not listed)
$types_that_rotate = [2, 3] # (0, 90, 180, 270) degrees
$types_that_alternate = [1, 4, 5] # (0, 90) degrees
# Game vars
$start_pos = [2, $grid_height - 1] # Starting position for new bricks
# Rotates a square matrix clockwise
def rotate matrix
out = []
matrix.length.times { out << [nil] * matrix.length }
pivot = matrix.length - 1
0.upto(matrix.length - 1) do |row|
0.upto(matrix.length - 1) do |col|
out[row][col] = matrix[-col + pivot][row]
end
end
out
end
# Translates grid coordinates into screen coordinates
def grid_to_screen x, y
[$grid_left + x * $grid_spacing,
$grid_top + y * $grid_spacing - ($grid_height - 1) * $grid_spacing]
end
# If the falling piece were at (x,y), would it collide with
# blocks in the graveyard? Being out of bounds counts as a
# collision, except with the upper bound when check_upper is
# false (when the piece first enters the grid)
def collision? x, y, check_upper = false
0.upto($falling_piece.length - 1) do |offy|
0.upto($falling_piece.length - 1) do |offx|
# Is this part of the matrix non-empty?
if $falling_piece[offy][offx] != 0
# Out of the playing area counts as a collision
return true if x + offx < 0 || x + offx >= $grid_width
return true if y + offy < 0
# Only care about upper bound when check_upper is true
if y + offy >= $grid_height
return true if check_upper
next # skip the graveyard check
end
# Check if it collides with a block in the graveyard
return true unless $graveyard[y + offy][x + offx].nil?
end
end
end
# Default to false if no collisions were found
false
end
# Creates four chunks to be used to draw a brick
def generate_chunks
$chunks = (1..4).map { Scene.new_tile "demoblob.png" }
end
# Position the sprites to show the brick's current position
# and rotation.
def position_chunks
chunk = 0
0.upto($falling_piece.length - 1) do |row|
0.upto($falling_piece.length - 1) do |col|
if $falling_piece[row][col] != 0
$chunks[chunk].set_pos *grid_to_screen(col + $x, row + $y)
chunk += 1
end
end
end
end
# Adds the chunks from $falling_piece to the graveyard
def add_to_graveyard
chunk = 0
0.upto($falling_piece.length - 1) do |row|
0.upto($falling_piece.length - 1) do |col|
if $falling_piece[row][col] != 0
$graveyard[row + $y][col + $x] = $chunks[chunk]
chunk += 1
end
end
end
end
# Initialize the falling piece
def create_piece type
$type = type
$falling_piece = $types[type]
$x, $y = $start_pos
$alternated = false # whether it's in its alternate position
# Generate the brick's sprites
generate_chunks
end
# Delete completed rows if there are any
# Known Bug: Multiple rows aren't cleared; this should be an easy fix
def clear_rows
($graveyard.length - 1).downto(0) do |row|
while $graveyard[row].all? { |block| !block.nil? }
# Delete this row's blocks
$graveyard[row].each { |block| block.active = false }
# Move down the rows above
row.upto($graveyard.length - 1) do |row|
$graveyard[row].each_index do |col|
$graveyard[row][col] = $graveyard[row + 1][col]
unless $graveyard[row][col].nil?
$graveyard[row][col].set_pos *grid_to_screen(col, row)
end
end
end
end
end
end
# Initialize the game
def init
$game_over = false
# Set up the game board
$graveyard = []
$grid_height.times { $graveyard << [nil] * $grid_width }
# Display the background
$background = Scene.new_tile "graphics/tetrisbg.png"
$background.set_scale 8, 12
$background.set_layer -1
# Load sounds
$beep = Scene.new_sound "sounds/beep.ogg"
# Create the first falling piece
create_piece rand($types.length)
position_chunks
$last_shift = Scene.run_time
$beep.play
end
# Handles keypresses
def keyboard in_key, in_down_flag
return unless in_down_flag
return if $game_over
# Allow left, right, and down movement
if in_key == 'left' and !collision?($x - 1, $y)
$x -= 1
elsif in_key == 'right' and !collision?($x + 1, $y)
$x += 1
elsif in_key == 'down' and !collision?($x, $y - 1)
$y -= 1
$last_shift = Scene.run_time
# Drop the brick as far as it can go
elsif in_key == 'space'
shifted = false
while !collision?($x, $y - 1)
$y -= 1
shifted = true
end
$last_shift = Scene.run_time if shifted
# Rotate the brick
elsif in_key == 'up'
if $types_that_rotate.include? $type
old_piece = $falling_piece
$falling_piece = rotate $falling_piece
$falling_piece = old_piece if collision?($x, $y)
elsif $types_that_alternate.include? $type
old_piece = $falling_piece
$falling_piece = rotate $falling_piece
# Rotate twice more if it was already in its alternate position
if $alternated
$falling_piece = rotate(rotate $falling_piece)
end
if collision?($x, $y)
$falling_piece = old_piece
else
$alternated = !$alternated
end
end
end
# Update the sprite locations for where the brick is
position_chunks
end
# Called every frame
def update
if $game_over
# The graveyard's blocks fade in and out after game over
$fade += $fade_increment
$fade = 0.2 and $fade_increment = -$fade_increment if $fade < 0.2
$fade = 1.0 and $fade_increment = -$fade_increment if $fade > 1.0
$graveyard.each do |row|
row.each do |block|
block.set_color 1, 1, 1, $fade unless block.nil?
end
end
else
# This tetris clone operates in $fall_time second jerks
if Scene.run_time - $last_shift >= $fall_time
# If we can't move down any more...
if collision? $x, $y - 1
$beep.play
# If even the current position is a collision, we lost
if collision? $x, $y, true
$game_over = true
$fade = 1.0
$fade_increment = -0.05
return
else
add_to_graveyard
create_piece rand($types.length)
clear_rows
end
end
$y -= 1 # Move the brick down one
position_chunks # Update the sprite positions
$last_shift = Scene.run_time
end
end
end
init
An old friend made it clear that I made some early design decisions which unnecessarily complicated the code. That the code is ridiculously complex is clear, but I didn't myself see the ways in which it could be done.
Posted May 01, 2007, in the morning. Updated updated Jun 12, 2007, in the late, late night: Added beginning of a list of improvements.