I was poking around on my Raspberry Pi that I hadn't turned on or used in a while, and I stumbled upon this Python script that I wrote ~1.5 years ago, and I was intrigued.
Essentially, it's a 2D Minecraft clone with no concept of survival. It does have a slightly, complex control scheme, so here's a list of buttons that can be used, and what they do:
- a, d - Move left/right through the world.
- LMB - Place a tile at the desired position.
- RMB - Destroy a tile at the desired position.
- SHIFT/SPACE - Change the currently selected tile.
- q - Switch to the underground "realm".
- e - Switch to the above ground "realm".
Even though I wrote this quite a while ago, and if I had written it now, it would have looked better, I'm still curious as to what all can be improved.
#!usr/bin/env python
# Basic 2d building game, Builder
import pygame
import random
import shutil
import time
import math
import sys
import os
debug_game_data = int(sys.argv[1])
current_version = "0.5.0"
WIDTH = 700
HEIGHT = 700
pygame.init()
pygame.display.set_caption("Builder {}".format(current_version))
screen = pygame.display.set_mode((WIDTH, HEIGHT))
FONT = pygame.font.SysFont("monospace.ttf", 13)
clock = pygame.time.Clock()
mouse_pos = pygame.mouse.get_pos
key_pressed = pygame.key.get_pressed
mouse_x = 0
mouse_y = 1
TILE_SIZE = 25
round_tile_size = lambda x: (
1+(x-1)/TILE_SIZE)*TILE_SIZE if x%TILE_SIZE>=10 else (
x/TILE_SIZE)*TILE_SIZE
# Dictionary of game textures
TEXTURES = {
"dirt": pygame.transform.scale(pygame.image.load("./images/gameDirtBlock.png"), (TILE_SIZE, TILE_SIZE)),
"grass": pygame.transform.scale(pygame.image.load("./images/gameGrassBlock.png"), (TILE_SIZE, TILE_SIZE)),
"stone": pygame.transform.scale(pygame.image.load("./images/gameStoneBlock.png"), (TILE_SIZE, TILE_SIZE)),
"sand": pygame.transform.scale(pygame.image.load("./images/gameSandBlock.png"), (TILE_SIZE, TILE_SIZE)),
"water": pygame.transform.scale(pygame.image.load("./images/gameWaterBlock.png"), (TILE_SIZE, TILE_SIZE)),
"wood": pygame.transform.scale(pygame.image.load("./images/gameWoodBlock.png"), (TILE_SIZE, TILE_SIZE)),
"leaf": pygame.transform.scale(pygame.image.load("./images/gameLeafBlock.png"), (TILE_SIZE, TILE_SIZE)),
}
# Lists containing tile data for overworld and underground
overworld_data = []
underground_data = []
# Variables concerning whether the player is underground, or aboveground
player_underground = False
player_aboveground = True
# Dictionary containing tile types
TILE_TYPES = {
"dirt": 0,
"grass": 1,
"stone": 2,
"sand": 3,
"water": 4,
"wood": 5,
"leaf": 6
}
# Colors for the game
BLUE_LIGHT = (0, 0, 220)
GRAY_DARK = (100, 100, 100)
BLUE_DARK = (0, 0, 175)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
# Background class, for rendering the day/night cycle
class Background(object):
def __init__(self):
self.SKY_COLOR = BLUE_LIGHT
self.UNDERGROUND_SKY_COLOR = GRAY_DARK
# Render the background
def render_background(self):
if player_aboveground:
screen.fill(self.SKY_COLOR)
if player_underground:
screen.fill(self.UNDERGROUND_SKY_COLOR)
# Tile class, to be used to create a chunk with the Chunk class
class Tile(object):
def __init__(self, x, y, tile_type, tile_image):
self.x = x
self.y = y
self.tile_type = tile_type
self.tile_image = tile_image
# Update the position of a tile
def update_tile_position(self, x_change, y_change):
self.x += x_change
self.y += y_change
# Render the tile on the screen
def render_tile(self):
screen.blit(self.tile_image, (self.x, self.y))
# Chunk class, for generating a chunk and rendering it
class Chunk(object):
def __init__(self, chunk_x):
self.chunk_x = chunk_x
self.chunk_data_overworld = []
self.chunk_data_underground = []
# Generate an overworld chunk for the center
def generate_overworld_chunk_center(self):
# Generate the stone section
stone_end = HEIGHT-round_tile_size(random.randint(3, 4)*TILE_SIZE)
for y in range(HEIGHT, stone_end, -TILE_SIZE):
self.chunk_data_overworld.append(Tile(self.chunk_x, y,
TILE_TYPES["stone"], TEXTURES["stone"]))
# Generate the dirt section
dirt_end = stone_end-round_tile_size(random.randint(1, 2)*TILE_SIZE)
for y in range(stone_end, dirt_end, -TILE_SIZE):
self.chunk_data_overworld.append(Tile(self.chunk_x, y,
TILE_TYPES["dirt"], TEXTURES["dirt"]))
# Generate the grass section
grass_end = dirt_end-round_tile_size(TILE_SIZE)
for y in range(dirt_end, grass_end, -TILE_SIZE):
self.chunk_data_overworld.append(Tile(self.chunk_x, y,
TILE_TYPES["grass"], TEXTURES["grass"]))
# Generate a tree based on a probability
value1 = random.random()*random.randint(1, 10)
value2 = random.random()*random.randint(1, 10)
value3 = random.random()*random.randint(1, 10)
if value1 < value2 < value3:
# Generate the wood section
tree_end = grass_end-round_tile_size(random.randint(4, 5)*TILE_SIZE)
for y in range(grass_end, tree_end, -TILE_SIZE):
self.chunk_data_overworld.append(Tile(self.chunk_x, y,
TILE_TYPES["wood"], TEXTURES["wood"]))
# Generate the leaf section
leaf_end = tree_end-round_tile_size(random.randint(1, 2)*TILE_SIZE)
for y in range(tree_end, leaf_end, -TILE_SIZE):
self.chunk_data_overworld.append(Tile(self.chunk_x, y,
TILE_TYPES["leaf"], TEXTURES["leaf"]))
# Generate an overworld chunk for the sides
def generate_overworld_chunk_side(self):
beach_end = HEIGHT-round_tile_size(random.randint(4, 6)*TILE_SIZE)
for y in range(HEIGHT, beach_end, -TILE_SIZE):
self.chunk_data_overworld.append(Tile(self.chunk_x, y,
TILE_TYPES["sand"], TEXTURES["sand"]))
# Generate an overworld chunk for the far sides
def generate_overworld_chunk_far_side(self):
water_end = HEIGHT-(TILE_SIZE*3)
for y in range(HEIGHT, water_end, -TILE_SIZE):
self.chunk_data_overworld.append(Tile(self.chunk_x, y,
TILE_TYPES["water"], TEXTURES["water"]))
# Generate an underground chunk
def generate_underground_chunk(self):
# Generate the top section
stone_end1 = 0+round_tile_size(random.randint(3, 5)*TILE_SIZE)
for y in range(0, stone_end1, TILE_SIZE):
self.chunk_data_underground.append(Tile(self.chunk_x, y,
TILE_TYPES["stone"], TEXTURES["stone"]))
# Generate the bottom section
stone_end2 = HEIGHT-round_tile_size(random.randint(3, 5)*TILE_SIZE)
for y in range(HEIGHT, stone_end2, -TILE_SIZE):
self.chunk_data_underground.append(Tile(self.chunk_x, y,
TILE_TYPES["stone"], TEXTURES["stone"]))
# Add new underground chunk data
def add_new_chunk_data_underground(self):
self.chunk_data_underground.append(self.chunk_x)
self.generate_underground_chunk()
underground_data.append(self.chunk_data_underground)
# Add a new chunk for the far sides
def add_new_chunk_data_far_side(self):
self.chunk_data_overworld.append(self.chunk_x)
self.generate_overworld_chunk_far_side()
overworld_data.append(self.chunk_data_overworld)
# Add new chunk data for the sides
def add_new_chunk_data_side(self):
self.chunk_data_overworld.append(self.chunk_x)
self.generate_overworld_chunk_side()
overworld_data.append(self.chunk_data_overworld)
# Add chunk data to the world for the center
def add_new_chunk_data_center(self):
self.chunk_data_overworld.append(self.chunk_x)
self.generate_overworld_chunk_center()
overworld_data.append(self.chunk_data_overworld)
# WorldGenerator class, responsible for finalizing world data
class WorldGenerator(object):
def __init__(self):
self.START_X_CENTER = -2000
self.END_X_CENTER = 2000
# Start and end for left side
self.START_X_LEFT = self.START_X_CENTER-500
self.END_X_LEFT = self.START_X_CENTER
# Start and end for right side
self.START_X_RIGHT = self.END_X_CENTER
self.END_X_RIGHT = self.END_X_CENTER+500
# Start and end for far left side
self.START_X_FAR_LEFT = self.START_X_LEFT-750
self.END_X_FAR_LEFT = self.START_X_LEFT
# Start and end for far right side
self.START_X_FAR_RIGHT = self.END_X_RIGHT
self.END_X_FAR_RIGHT = self.END_X_RIGHT+750
# Start and end for underground
self.UNDER_START_X = -3250
self.UNDER_END_X = 3250
# Generate the world
def generate_world(self):
# Generate the far left side
for chunk_x in range(self.START_X_FAR_LEFT, self.END_X_FAR_LEFT, TILE_SIZE):
chunk = Chunk(chunk_x)
chunk.add_new_chunk_data_far_side()
# Generate the left side
for chunk_x in range(self.START_X_LEFT, self.END_X_LEFT, TILE_SIZE):
chunk = Chunk(chunk_x)
chunk.add_new_chunk_data_side()
# Generate the center
for chunk_x in range(self.START_X_CENTER, self.END_X_CENTER, TILE_SIZE):
chunk = Chunk(chunk_x)
chunk.add_new_chunk_data_center()
# Generate the right side
for chunk_x in range(self.START_X_RIGHT, self.END_X_RIGHT, TILE_SIZE):
chunk = Chunk(chunk_x)
chunk.add_new_chunk_data_side()
# Generate the far right side
for chunk_x in range(self.START_X_FAR_RIGHT, self.END_X_FAR_RIGHT, TILE_SIZE):
chunk = Chunk(chunk_x)
chunk.add_new_chunk_data_far_side()
# Generate the underground portion
for chunk_x in range(self.UNDER_START_X, self.UNDER_END_X, TILE_SIZE):
chunk = Chunk(chunk_x)
chunk.add_new_chunk_data_underground()
# ChunkLoader class, used for rendering chunks
class ChunkLoader(object):
def __init__(self):
pass
# Render the current loaded chunks aboveground
def render_current_loaded_chunks_aboveground(self):
for chunk in overworld_data:
for tile in chunk[1:]:
if tile.x >= 0 and tile.x <= WIDTH:
tile.render_tile()
# Render the current loaded chunks underground
def render_current_loaded_chunks_underground(self):
for chunk in underground_data:
for tile in chunk[1:]:
if tile.x >= 0 and tile.x <= WIDTH:
tile.render_tile()
# Player class, containing all player data
class Player(object):
def __init__(self):
self.chunk_loader = ChunkLoader()
self.selected_tile = 0
self.inventory = {
0: [0, pygame.transform.scale(pygame.image.load("./images/gameDirtBlock.png"), (17, 17))],
1: [0, pygame.transform.scale(pygame.image.load("./images/gameGrassBlock.png"), (17, 17))],
2: [0, pygame.transform.scale(pygame.image.load("./images/gameStoneBlock.png"), (17, 17))],
3: [0, pygame.transform.scale(pygame.image.load("./images/gameSandBlock.png"), (17, 17))],
4: [0, pygame.transform.scale(pygame.image.load("./images/gameWaterBlock.png"), (17, 17))],
5: [0, pygame.transform.scale(pygame.image.load("./images/gameWoodBlock.png"), (17, 17))],
6: [0, pygame.transform.scale(pygame.image.load("./images/gameLeafBlock.png"), (17, 17))],
}
# Loop through the world and change tile positions belowground
def loop_through_world_belowground(self, x_change, y_change):
for chunk in underground_data:
for tile in chunk[1:]:
tile.update_tile_position(x_change, y_change)
# Move through the world belowground
def move_player_belowground(self):
if key_pressed()[pygame.K_a]:
self.loop_through_world_belowground(TILE_SIZE, 0)
if key_pressed()[pygame.K_d]:
self.loop_through_world_belowground(-TILE_SIZE, 0)
# Loop through the world and change tile positions aboveground
def loop_through_world_aboveground(self, x_change, y_change):
for chunk in overworld_data:
for tile in chunk[1:]:
tile.update_tile_position(x_change, y_change)
# Move through the world aboveground
def move_player_aboveground(self):
if key_pressed()[pygame.K_a]:
self.loop_through_world_aboveground(TILE_SIZE, 0)
if key_pressed()[pygame.K_d]:
self.loop_through_world_aboveground(-TILE_SIZE, 0)
# Render the player's hitbox on the screen
def render_player_hitbox(self):
pygame.draw.rect(screen, WHITE,
pygame.Rect(round_tile_size(mouse_pos()[mouse_x]-(TILE_SIZE/2)),
round_tile_size(mouse_pos()[mouse_y]-(TILE_SIZE/2)),
TILE_SIZE, TILE_SIZE), 1)
# Render the player's inventory
def render_player_inventory(self):
pygame.draw.rect(screen, GRAY_DARK, (3, 218, 22, 142), 0)
# Positions for inventory items
item_x = 5
item_y = 220
# Render the item "icons"
for item in self.inventory:
item_image = self.inventory[item][1]
screen.blit(item_image, (item_x, item_y))
item_y += 20
# Change item_x and item_y
item_x = 7.5
item_y = 226.5
# Render the item counts
for item in self.inventory:
item_count = FONT.render(
"{}".format(self.inventory[item][0]), 1, WHITE)
screen.blit(item_count, (item_x, item_y))
item_y += 20
# Render the selected tile
pygame.draw.rect(screen, GRAY_DARK, (3, 193, 22, 22), 0)
screen.blit(self.inventory[self.selected_tile][1], (5, 195))
# Change the selected tile
def change_selected_tile(self):
if pygame.key.get_pressed()[pygame.K_SPACE]:
if self.selected_tile <= len(self.inventory)-2:
self.selected_tile += 1
else:
self.selected_tile = 0
# Change between underground and aboveground
def change_aboveground_underground(self):
# Make aboveground & belowground variables global
global player_aboveground
global player_underground
# Change to belowground
if pygame.key.get_pressed()[pygame.K_q]:
player_aboveground = False
player_underground = True
# Change to aboveground
if pygame.key.get_pressed()[pygame.K_e]:
player_aboveground = True
player_underground = False
# Delete a tile from the world aboveground
def delete_tile_aboveground(self):
if pygame.mouse.get_pressed()[0]:
for chunk in overworld_data:
for tile in chunk[1:]:
if round_tile_size(mouse_pos()[mouse_x]-(TILE_SIZE/2)) == tile.x and round_tile_size(mouse_pos()[mouse_y]-(TILE_SIZE/2)) == tile.y:
if self.inventory[tile.tile_type][0] <= 127:
self.inventory[tile.tile_type][0] += 1
del overworld_data[overworld_data.index(chunk)][overworld_data[overworld_data.index(chunk)].index(tile)]
break
# Add a tile to the world aboveground
def add_tile_aboveground(self):
if pygame.mouse.get_pressed()[2]:
for chunk in overworld_data:
if round_tile_size(mouse_pos()[mouse_x]-(TILE_SIZE/2)) == chunk[0]:
if self.inventory[self.selected_tile][0] >= 1:
self.inventory[self.selected_tile][0] -= 1
chunk.append(Tile(
round_tile_size(mouse_pos()[mouse_x]-(TILE_SIZE/2)),
round_tile_size(mouse_pos()[mouse_y]-(TILE_SIZE/2)),
self.selected_tile,
pygame.transform.scale(self.inventory[self.selected_tile][1], (TILE_SIZE, TILE_SIZE))
))
break
# Delete a tile from the world belowground
def delete_tile_belowground(self):
if pygame.mouse.get_pressed()[0]:
for chunk in underground_data:
for tile in chunk[1:]:
if round_tile_size(mouse_pos()[mouse_x]-(TILE_SIZE/2)) == tile.x and round_tile_size(mouse_pos()[mouse_y]-(TILE_SIZE/2)) == tile.y:
if self.inventory[tile.tile_type][0] <= 127:
self.inventory[tile.tile_type][0] += 1
del underground_data[underground_data.index(chunk)][underground_data[underground_data.index(chunk)].index(tile)]
break
# Add a tile to the world belowground
def add_tile_belowground(self):
if pygame.mouse.get_pressed()[2]:
for chunk in underground_data:
if round_tile_size(mouse_pos()[mouse_x]-(TILE_SIZE/2)) == chunk[0]:
if self.inventory[self.selected_tile][0] >= 1:
self.inventory[self.selected_tile][0] -= 1
chunk.append(Tile(
round_tile_size(mouse_pos()[mouse_x]-(TILE_SIZE/2)),
round_tile_size(mouse_pos()[mouse_y]-(TILE_SIZE/2)),
self.selected_tile,
pygame.transform.scale(self.inventory[self.selected_tile][1], (TILE_SIZE, TILE_SIZE))
))
break
# Game class, where the events are executed
class Game(object):
def __init__(self):
self.player = Player()
self.chunk_loader = ChunkLoader()
self.background = Background()
# Run all game events
def run_events(self):
self.background.render_background()
# Player aboveground?
if player_aboveground:
self.chunk_loader.render_current_loaded_chunks_aboveground()
# Player underground?
if player_underground:
self.chunk_loader.render_current_loaded_chunks_underground()
# Move player aboveground?
if player_aboveground:
self.player.move_player_aboveground()
# Move player underground?
if player_underground:
self.player.move_player_belowground()
# Other player methods
self.player.change_selected_tile()
self.player.render_player_hitbox()
self.player.render_player_inventory()
self.player.change_aboveground_underground()
# Add & delete tiles aboveground?
if player_aboveground:
self.player.delete_tile_aboveground()
self.player.add_tile_aboveground()
# Add and delete tile belowground?
if player_underground:
self.player.delete_tile_belowground()
self.player.add_tile_belowground()
# Initalize the game
def initalize_game():
clock.tick(75)
background = Background()
generator = WorldGenerator()
generator.generate_world()
# Print game debug data
def print_debug_data():
if debug_game_data:
for chunk in overworld_data:
for tile in chunk[1:]:
print tile.tile_type, tile.x, tile.y
# Main program loop where all game events are executed
def main_program_loop():
game = Game()
while True:
game.run_events()
pygame.display.update()
# Check if the user wants to quit
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit(0)
# Start the main program loop
if __name__ == "__main__":
initalize_game()
print_debug_data()
main_program_loop()
For those who are curious, the tile textures can be found here.