#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Comprehensive Minecraft-Inspired Survival Game Prototype
Features:
• Huge world (500×100) with three dimensions:
- Overworld: natural terrain (grass, dirt, stone, ores, trees)
- Nether: a red, hostile environment with lava and obsidian
- End: a void with End Stone and a boss (Ender Dragon)
• Only an 80×20 viewport (1,600 tiles) is visible; the camera scrolls.
• Core items/blocks: grass, dirt, stone, coal_ore, iron_ore, diamond_ore, wood, leaf, obsidian,
furnace, crafting_table, torch, wood_plank, apple.
• Crafting system (key C) with sample recipes.
• Inventory system (key I) with scrollable UI and stacking (max 64 per stack).
• Health & hunger mechanics.
• Day-night cycle (brightness adjustment).
• Basic enemy: a zombie in the Overworld; bosses (Ender Dragon, Wither) in End/Nether.
• Dimension travel (key T) cycles between Overworld, Nether, and End.
• Full mouse support:
- Left-click: break block or place active block.
- Mouse wheel: cycle active inventory items.
- Mouse movement: update player orientation.
Note: This prototype is very simplified relative to real Minecraft.
"""
import tkinter as tk
from tkinter import ttk
import math, random, time
# --------------------------
# GLOBAL SETTINGS
# --------------------------
# World dimensions (for each dimension)
OVERWORLD_WIDTH = 500
OVERWORLD_HEIGHT = 100
# For simplicity, Nether and End will be smaller.
NETHER_WIDTH = 120
NETHER_HEIGHT = 60
END_WIDTH = 150
END_HEIGHT = 80
# The viewport (visible tiles) is fixed:
VIEWPORT_WIDTH = 80 # columns
VIEWPORT_HEIGHT = 20 # rows
CELL_SIZE = 10 # pixel size (adjust for visibility)
DAY_LENGTH = 120 # seconds for full day cycle
START_TIME = time.time()
STACK_LIMIT = 64
# --------------------------
# BLOCKS & ITEMS (colors as hex strings)
# --------------------------
BLOCKS = {
"AIR": "#FFFFFF",
"GRASS": "#00AA00",
"DIRT": "#8B4513",
"STONE": "#808080",
"COAL_ORE": "#333333",
"IRON_ORE": "#CC6600",
"DIAMOND_ORE": "#55FFFF",
"WOOD": "#A0522D",
"LEAF": "#66CC66",
"OBSIDIAN": "#440044",
"FURNACE": "#CC6600",
"CRAFTING_TABLE": "#D2691E",
"TORCH": "#FFFF00",
# Crafted items:
"wood_plank": "#DEB887",
"wooden_pickaxe": "#CD853F",
"apple": "#FF0000",
# Portal block for dimension travel (not craftable in this prototype)
"PORTAL": "#0000FF", # Blue portal block
# Nether-specific block:
"NETHERRACK": "#A52A2A",
# End Stone:
"END_STONE": "#D2B48C",
}
# Only these blocks can be placed:
PLACEABLE = set(["GRASS", "DIRT", "STONE", "COAL_ORE", "IRON_ORE", "DIAMOND_ORE",
"WOOD", "LEAF", "OBSIDIAN", "FURNACE", "CRAFTING_TABLE", "TORCH",
"wood_plank"])
# --------------------------
# CRAFTING RECIPES (Sample, very simplified)
# --------------------------
crafting_recipes = {
"wood_plank": {"WOOD": 1},
"wooden_pickaxe": {"wood_plank": 3},
"furnace": {"STONE": 8},
"crafting_table": {"wood_plank": 4},
"torch": {"COAL_ORE": 1, "wood_plank": 1},
"apple": {"wood_plank": 1, "TORCH": 1},
}
# --------------------------
# DIMENSION ENUMERATION
# --------------------------
DIMENSIONS = ["overworld", "nether", "end"]
# --------------------------
# CLASSES
# --------------------------
class Player:
def __init__(self, x, y, dimension="overworld"):
self.x = x
self.y = y
self.health = 100
self.hunger = 100 # max 100
self.dimension = dimension # "overworld", "nether", or "end"
self.inventory = {key: 0 for key in BLOCKS if key != "AIR"}
for item in ["wood_plank", "wooden_pickaxe", "crafting_table", "apple", "TORCH"]:
self.inventory.setdefault(item, 0)
self.active_item = None # currently selected item for placement
self.orientation = 0 # angle in degrees (for possible future rendering)
class Enemy:
def __init__(self, x, y, kind="zombie"):
self.x = x
self.y = y
self.kind = kind # "zombie" in Overworld; could be others in Nether/End
self.health = 20 if kind=="zombie" else 50
self.symbol = "Z" if kind=="zombie" else "B" # B for boss (placeholder)
class Game:
def __init__(self, root):
self.root = root
self.root.title("Minecraft-Inspired Survival")
# Create main canvas with size determined by viewport.
self.canvas = tk.Canvas(root, width=VIEWPORT_WIDTH*CELL_SIZE,
height=VIEWPORT_HEIGHT*CELL_SIZE, bg="black")
self.canvas.pack(side=tk.LEFT)
self.canvas.focus_set()
# Status label for health, hunger, active item, dimension, day/night.
self.status_var = tk.StringVar()
self.status_label = ttk.Label(root, textvariable=self.status_var, font=('Arial', 12))
self.status_label.pack(side=tk.TOP, fill=tk.X)
# Bind events.
self.canvas.bind("<KeyPress>", self.on_key)
self.canvas.bind("<Button-1>", self.on_left_click)
self.canvas.bind("<Motion>", self.on_mouse_move)
self.canvas.bind("<MouseWheel>", self.on_mouse_wheel)
self.canvas.bind("<Button-4>", self.on_mouse_wheel)
self.canvas.bind("<Button-5>", self.on_mouse_wheel)
# Set up world for the current dimension.
self.current_dimension = "overworld"
self.world = self.generate_world(self.current_dimension)
# The world is stored as a 2D list of blocks.
self.world_width, self.world_height = self.get_world_dimensions(self.current_dimension)
self.player = Player(self.world_width//2, self.world_height//2, dimension=self.current_dimension)
self.enemies = [] # list of Enemy instances
self.last_update = time.time()
self.portal_cooldown = 0 # prevent spam switching dimensions
self.update_game()
# --------------------------
# WORLD GENERATION
# --------------------------
def get_world_dimensions(self, dimension):
if dimension == "overworld":
return OVERWORLD_WIDTH, OVERWORLD_HEIGHT
elif dimension == "nether":
return NETHER_WIDTH, NETHER_HEIGHT
elif dimension == "end":
return END_WIDTH, END_HEIGHT
else:
return 80, 20 # fallback
def generate_world(self, dimension):
# Generate different worlds based on the dimension.
width, height = self.get_world_dimensions(dimension)
world = []
if dimension == "overworld":
for y in range(height):
row = []
for x in range(width):
if y < 40:
row.append("AIR")
elif y == 40:
row.append("GRASS")
elif y < 45:
row.append("DIRT")
else:
r = random.random()
if r < 0.05:
row.append("COAL_ORE")
elif r < 0.08:
row.append("IRON_ORE")
elif r < 0.09:
row.append("DIAMOND_ORE")
else:
row.append("STONE")
world.append(row)
# Add trees in several locations.
for _ in range(50):
tx = random.randint(0, width-1)
ty = 40
if world[ty][tx] == "GRASS":
height_tree = random.randint(3, 5)
for h in range(1, height_tree+1):
if ty - h >= 0:
world[ty-h][tx] = "WOOD"
for dx in (-1, 0, 1):
for dy in (-2, -1):
nx, ny = tx+dx, ty-height_tree+dy
if 0 <= nx < width and 0 <= ny < height:
world[ny][nx] = "LEAF"
elif dimension == "nether":
# Nether: mostly NETHERRACK, with scattered OBISIDIAN and lava (simulated as "FURNACE" block here as a placeholder).
for y in range(height):
row = []
for x in range(width):
# Use a red-brown tone to represent netherrack.
if random.random() < 0.03:
row.append("OBSIDIAN")
else:
row.append("NETHERRACK")
world.append(row)
# Place some PORTAL blocks to simulate Nether portal remnants.
for _ in range(10):
x = random.randint(0, width-1)
y = random.randint(0, height-1)
world[y][x] = "PORTAL"
elif dimension == "end":
# End: mostly void with a platform of END_STONE near the bottom.
for y in range(height):
row = []
for x in range(width):
if y > height - 8:
row.append("END_STONE")
else:
row.append("AIR")
world.append(row)
# Place a portal/boss spawn (Ender Dragon) at the center.
midx = width // 2
midy = height - 9
world[midy][midx] = "PORTAL"
return world
# --------------------------
# VIEWPORT / SCROLLING
# --------------------------
def get_camera_offset(self):
# The viewport shows VIEWPORT_WIDTH x VIEWPORT_HEIGHT tiles from the current world.
cam_x = self.player.x - VIEWPORT_WIDTH // 2
cam_y = self.player.y - VIEWPORT_HEIGHT // 2
cam_x = max(0, min(cam_x, self.world_width - VIEWPORT_WIDTH))
cam_y = max(0, min(cam_y, self.world_height - VIEWPORT_HEIGHT))
return cam_x, cam_y
# --------------------------
# DRAWING THE WORLD
# --------------------------
def draw_world(self):
self.canvas.delete("all")
cam_x, cam_y = self.get_camera_offset()
# Adjust brightness for day-night.
elapsed = (time.time() - START_TIME) % DAY_LENGTH
brightness = 1.0 if elapsed <= DAY_LENGTH/2 else 0.5
for y in range(VIEWPORT_HEIGHT):
for x in range(VIEWPORT_WIDTH):
wx = cam_x + x
wy = cam_y + y
block = self.world[wy][wx]
color = BLOCKS.get(block, "#000000")
if brightness < 1.0:
color = self.adjust_color(color, brightness)
self.canvas.create_rectangle(
x*CELL_SIZE, y*CELL_SIZE,
(x+1)*CELL_SIZE, (y+1)*CELL_SIZE,
fill=color, outline="gray")
# Draw player as a red circle.
view_px = self.player.x - cam_x
view_py = self.player.y - cam_y
pad = 2
self.canvas.create_oval(
view_px*CELL_SIZE+pad, view_py*CELL_SIZE+pad,
(view_px+1)*CELL_SIZE-pad, (view_py+1)*CELL_SIZE-pad,
fill="red", outline="white")
# Draw enemies as black circles.
for enemy in self.enemies:
ex, ey = enemy.x, enemy.y
if cam_x <= ex < cam_x + VIEWPORT_WIDTH and cam_y <= ey < cam_y + VIEWPORT_HEIGHT:
vx = ex - cam_x
vy = ey - cam_y
self.canvas.create_oval(
vx*CELL_SIZE+pad, vy*CELL_SIZE+pad,
(vx+1)*CELL_SIZE-pad, (vy+1)*CELL_SIZE-pad,
fill="black", outline="white")
self.canvas.update()
def adjust_color(self, hex_color, factor):
hex_color = hex_color.lstrip("#")
rgb = [int(hex_color[i:i+2], 16) for i in (0, 2, 4)]
rgb = [max(0, int(c*factor)) for c in rgb]
return "#%02x%02x%02x" % tuple(rgb)
def update_status(self):
elapsed = (time.time() - START_TIME) % DAY_LENGTH
phase = "Day" if elapsed <= DAY_LENGTH/2 else "Night"
active = self.player.active_item if self.player.active_item else "None"
self.status_var.set(f"Health: {int(self.player.health)} Hunger: {int(self.player.hunger)} Active: {active} Dim: {self.player.dimension} {phase}")
# --------------------------
# EVENT HANDLERS
# --------------------------
def on_key(self, event):
key = event.keysym.lower()
if key in ["w", "a", "s", "d"]:
self.move_player(key)
elif key == "i":
self.open_inventory()
elif key == "c":
self.open_crafting()
elif key == "t":
self.travel_dimension()
self.draw_world()
self.update_status()
def on_left_click(self, event):
cam_x, cam_y = self.get_camera_offset()
grid_x = event.x // CELL_SIZE
grid_y = event.y // CELL_SIZE
world_x = cam_x + grid_x
world_y = cam_y + grid_y
if world_x < 0 or world_x >= self.world_width or world_y < 0 or world_y >= self.world_height:
return
# If there is a block, break it.
if self.world[world_y][world_x] != "AIR":
blk = self.world[world_y][world_x]
# For simplicity, if the block is a PORTAL, do nothing.
if blk == "PORTAL":
return
self.world[world_y][world_x] = "AIR"
cnt = self.player.inventory.get(blk, 0)
if cnt < STACK_LIMIT:
self.player.inventory[blk] = cnt + 1
else:
print(f"Stack for {blk} is full!")
else:
# Place active block if selected.
if self.player.active_item:
self.world[world_y][world_x] = self.player.active_item
cnt = self.player.inventory.get(self.player.active_item, 0)
if cnt > 0:
self.player.inventory[self.player.active_item] = cnt - 1
if self.player.inventory[self.player.active_item] == 0:
self.player.active_item = None
self.draw_world()
self.update_status()
def on_mouse_move(self, event):
cam_x, cam_y = self.get_camera_offset()
grid_x = event.x // CELL_SIZE
grid_y = event.y // CELL_SIZE
world_x = cam_x + grid_x
world_y = cam_y + grid_y
dx = world_x - self.player.x
dy = self.player.y - world_y
if dx == 0 and dy == 0:
angle = self.player.orientation
else:
angle = math.degrees(math.atan2(dy, dx))
self.player.orientation = angle
self.update_status()
def on_mouse_wheel(self, event):
valid_items = [item for item in PLACEABLE if self.player.inventory.get(item, 0) > 0]
if not valid_items:
return
try:
idx = valid_items.index(self.player.active_item)
except ValueError:
idx = -1
if event.num == 4 or (hasattr(event, "delta") and event.delta > 0):
idx = (idx + 1) % len(valid_items)
else:
idx = (idx - 1) % len(valid_items)
self.player.active_item = valid_items[idx]
self.update_status()
def move_player(self, key):
dx, dy = 0, 0
if key == "w":
dy = -1
elif key == "s":
dy = 1
elif key == "a":
dx = -1
elif key == "d":
dx = 1
new_x = self.player.x + dx
new_y = self.player.y + dy
if 0 <= new_x < self.world_width and 0 <= new_y < self.world_height:
if self.world[new_y][new_x] == "AIR":
self.player.x = new_x
self.player.y = new_y
# --------------------------
# INVENTORY WINDOW
# --------------------------
def open_inventory(self):
inv_win = tk.Toplevel(self.root)
inv_win.title("Inventory")
inv_win.geometry("300x400")
lbl = ttk.Label(inv_win, text="Click an item to select it for placement", font=('Arial', 12))
lbl.pack(pady=10)
frame = ttk.Frame(inv_win)
frame.pack(fill=tk.BOTH, expand=True)
canvas = tk.Canvas(frame)
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
scroll_frame = ttk.Frame(canvas)
scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
for item, count in self.player.inventory.items():
btn = ttk.Button(scroll_frame, text=f"{item}: {count}",
command=lambda i=item: self.select_inventory(i, inv_win))
btn.pack(fill=tk.X, padx=5, pady=2)
def select_inventory(self, item, window):
if item in PLACEABLE and self.player.inventory.get(item, 0) > 0:
self.player.active_item = item
else:
self.player.active_item = None
window.destroy()
self.update_status()
# --------------------------
# CRAFTING WINDOW
# --------------------------
def open_crafting(self):
craft_win = tk.Toplevel(self.root)
craft_win.title("Crafting")
craft_win.geometry("300x400")
lbl = ttk.Label(craft_win, text="Select a recipe to craft", font=('Arial', 12))
lbl.pack(pady=10)
frame = ttk.Frame(craft_win)
frame.pack(fill=tk.BOTH, expand=True)
for item, req in crafting_recipes.items():
if all(self.player.inventory.get(ing, 0) >= amt for ing, amt in req.items()):
req_str = ", ".join(f"{ing}:{amt}" for ing, amt in req.items())
btn = ttk.Button(frame, text=f"Craft {item} (requires: {req_str})",
command=lambda i=item: self.craft_item(i, craft_win))
btn.pack(fill=tk.X, padx=5, pady=2)
close_btn = ttk.Button(frame, text="Close", command=craft_win.destroy)
close_btn.pack(pady=10)
def craft_item(self, item, win):
recipe = crafting_recipes.get(item)
if not recipe:
return
if all(self.player.inventory.get(ing, 0) >= amt for ing, amt in recipe.items()):
for ing, amt in recipe.items():
self.player.inventory[ing] -= amt
cur = self.player.inventory.get(item, 0)
self.player.inventory[item] = min(cur + 1, STACK_LIMIT)
win.destroy()
self.update_status()
# --------------------------
# DIMENSION TRAVEL
# --------------------------
def travel_dimension(self):
# For simplicity: cycle dimensions: overworld->nether->end->overworld
if self.portal_cooldown > time.time():
return # Prevent rapid switching.
current = self.player.dimension
next_dim = DIMENSIONS[(DIMENSIONS.index(current)+1) % len(DIMENSIONS)]
self.player.dimension = next_dim
# Generate new world for that dimension.
self.world = self.generate_world(next_dim)
self.world_width, self.world_height = self.get_world_dimensions(next_dim)
# Place player at center of new world.
self.player.x = self.world_width // 2
self.player.y = self.world_height // 2
# Clear enemies; spawn new ones as needed.
self.enemies = []
self.portal_cooldown = time.time() + 3 # 3-second cooldown
# For bosses: in End, spawn an Ender Dragon; in Nether, spawn a Wither.
if next_dim == "end":
# For simplicity, add one boss enemy at center.
self.enemies.append(Enemy(self.world_width//2, self.world_height//2, kind="ender_dragon"))
elif next_dim == "nether":
self.enemies.append(Enemy(self.world_width//2, self.world_height//2, kind="wither"))
self.update_status()
self.draw_world()
# --------------------------
# GAME LOOP & SURVIVAL MECHANICS
# --------------------------
def update_game(self):
now = time.time()
delta = now - self.last_update
self.last_update = now
# Update hunger
self.player.hunger -= 0.05 * delta
if self.player.hunger < 0: self.player.hunger = 0
# Regenerate health if hunger is high; otherwise, damage.
if self.player.hunger > 80 and self.player.health < 100:
self.player.health += 0.02 * delta
if self.player.health > 100: self.player.health = 100
if self.player.hunger < 20:
self.player.health -= 0.05 * delta
# Basic enemy spawning in Overworld only.
if self.player.dimension == "overworld" and random.random() < 0.01 and len(self.enemies) < 5:
ex = random.randint(0, self.world_width-1)
ey = random.randint(45, self.world_height-1)
if abs(ex - self.player.x) > 5 and abs(ey - self.player.y) > 5:
self.enemies.append(Enemy(ex, ey, kind="zombie"))
# Update enemy movement.
for enemy in self.enemies:
if enemy.x < self.player.x and self.world[enemy.y][min(enemy.x+1, self.world_width-1)] == "AIR":
enemy.x += 1
elif enemy.x > self.player.x and self.world[enemy.y][max(enemy.x-1, 0)] == "AIR":
enemy.x -= 1
if enemy.y < self.player.y and self.world[min(enemy.y+1, self.world_height-1)][enemy.x] == "AIR":
enemy.y += 1
elif enemy.y > self.player.y and self.world[max(enemy.y-1, 0)][enemy.x] == "AIR":
enemy.y -= 1
if abs(enemy.x - self.player.x) <= 1 and abs(enemy.y - self.player.y) <= 1:
self.player.health -= 2 * delta
if self.player.health <= 0:
self.game_over()
return
self.draw_world()
self.update_status()
self.root.after(100, self.update_game)
def game_over(self):
self.canvas.delete("all")
self.canvas.create_text(VIEWPORT_WIDTH*CELL_SIZE//2, VIEWPORT_HEIGHT*CELL_SIZE//2,
text="GAME OVER", fill="red", font=('Arial', 32))
self.status_var.set("Game Over")
# --------------------------
# MAIN FUNCTION
# --------------------------
def main():
root = tk.Tk()
game = Game(root)
root.mainloop()
if __name__ == "__main__":
main()