#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Ultimate Minecraft-Inspired Survival Game (Fixed)
-------------------------------------------------
All must-have crafting & smelting features are included, and the Play-button
crash is fixed by using a single Tk() instance. The menu frame is destroyed
when “Start” is clicked, and the Game UI is built on the same window.
Run with Python 3.6+ (Tkinter required).
"""
import tkinter as tk
from tkinter import ttk
import math, random, time, pickle
# ----------------------------
# GLOBAL SETTINGS
# ----------------------------
OVERWORLD_WIDTH, OVERWORLD_HEIGHT = 500, 100
NETHER_WIDTH, NETHER_HEIGHT = 120, 60
END_WIDTH, END_HEIGHT = 150, 80
VIEWPORT_WIDTH, VIEWPORT_HEIGHT = 80, 20
CELL_SIZE = 8
DAY_LENGTH = 120 # seconds per day
SEASON_LENGTH = 300 # seconds per season
START_TIME = time.time()
STACK_LIMIT = 64
# ----------------------------
# BLOCKS, ITEMS & COLOURS
# ----------------------------
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",
"CHEST": "#DEB887",
"TORCH": "#FFFF00",
"wood_plank": "#DEB887",
"wooden_pickaxe": "#CD853F",
"diamond_pickaxe": "#00FFFF",
"apple": "#FF0000",
"NETHERRACK": "#A52A2A",
"SOUL_SAND": "#806040",
"GLOWSTONE": "#FFFFE0",
"END_STONE": "#D2B48C",
"PORTAL": "#0000FF",
"END_PORTAL_FRAME":"#550088",
"SAND": "#EDC9AF",
"SANDSTONE": "#F5DEB3",
"GRAVEL": "#999999",
"WATER": "#0000FF",
"LAVA": "#FF4500",
"BUCKET": "#AAAAAA",
"WATER_BUCKET": "#6666FF",
"LAVA_BUCKET": "#FF8C00",
"FLINT": "#BFBFBF",
"FLINT_AND_STEEL": "#AAAAAA",
"BLAZE_ROD": "#FFD27F",
"BLAZE_POWDER": "#FFDC73",
"EYE_OF_ENDER": "#FFD700",
"NETHER_STAR": "#FFFF33",
"GLASS": "#BEC8C8",
"RAW_MEAT": "#8B0000",
"COOKED_MEAT": "#FFA07A",
"REDSTONE_ORE": "#FF0000",
"LADDER": "#8B4513",
}
PLACEABLE = set([
"GRASS","DIRT","STONE","COAL_ORE","IRON_ORE","DIAMOND_ORE",
"WOOD","LEAF","OBSIDIAN","FURNACE","CRAFTING_TABLE","CHEST","TORCH",
"wood_plank","SAND","SANDSTONE","GRAVEL","WATER","LAVA",
"SOUL_SAND","GLOWSTONE","END_PORTAL_FRAME","GLASS","LADDER"
])
# ----------------------------
# CRAFTING & SMELTING RECIPES
# ----------------------------
crafting_recipes = {
"wood_plank": {"WOOD":1},
"stick": {"wood_plank":2},
"wooden_pickaxe": {"wood_plank":3,"stick":2},
"stone_pickaxe": {"COBBLESTONE":3,"stick":2},
"iron_pickaxe": {"IRON_INGOT":3,"stick":2},
"diamond_pickaxe": {"DIAMOND":3,"stick":2},
"bucket": {"IRON_INGOT":3},
"flint_and_steel": {"IRON_INGOT":1,"FLINT":1},
"torch": {"stick":1,"COAL_ORE":1},
"furnace": {"COBBLESTONE":8},
"crafting_table": {"wood_plank":4},
"chest": {"wood_plank":8},
"ladder": {"stick":7},
"BLAZE_POWDER": {"BLAZE_ROD":1},
"EYE_OF_ENDER": {"BLAZE_POWDER":1,"ENDER_PEARL":1},
"END_PORTAL_FRAME":{"EYE_OF_ENDER":1},
"NETHER_STAR": {"END_PORTAL_FRAME":1},
"GLASS": {"SAND":4},
}
smelting_recipes = {
"IRON_ORE": "IRON_INGOT",
"GOLD_ORE": "GOLD_INGOT",
"COBBLESTONE": "STONE",
"SAND": "GLASS",
"RAW_MEAT": "COOKED_MEAT",
}
# ----------------------------
# BIOME & WEATHER
# ----------------------------
BIOMES = ["plains","desert","mountains","forest","snowy_tundra","swamp"]
def choose_biome(x,y):
r=random.random()
if r<0.15: return "desert"
if r<0.30: return "mountains"
if r<0.55: return "plains"
if r<0.75: return "forest"
if r<0.90: return "swamp"
return "snowy_tundra"
def choose_weather(biome,season):
if season=="Winter" and biome in ["snowy_tundra","mountains","forest"]:
return random.choices(["clear","snow"],weights=[0.4,0.6])[0]
if biome=="desert":
return random.choices(["clear","rain"],weights=[0.8,0.2])[0]
return random.choices(["clear","rain","thunderstorm"],weights=[0.6,0.3,0.1])[0]
# ----------------------------
# DIMENSIONS
# ----------------------------
DIMENSIONS = ["overworld","nether","end"]
def get_dimension_sizes(dim):
return {
"overworld":(OVERWORLD_WIDTH,OVERWORLD_HEIGHT),
"nether": (NETHER_WIDTH,NETHER_HEIGHT),
"end": (END_WIDTH,END_HEIGHT),
}[dim]
# ----------------------------
# PLAYER & MOB
# ----------------------------
class Player:
def __init__(self,x,y):
self.x,self.y=x,y
self.health=100; self.hunger=100
self.dimension="overworld"
self.inventory={k:0 for k in BLOCKS if k!="AIR"}
for itm in ["stick","COBBLESTONE","IRON_INGOT","GOLD_INGOT",
"GLASS","RAW_MEAT","COOKED_MEAT","BLAZE_ROD","BLAZE_POWDER",
"ENDER_PEARL","NETHER_STAR","bucket","water_bucket","lava_bucket"]:
self.inventory.setdefault(itm,0)
self.active_item=None
self.orientation=0; self.vy=0
self.tool_durability={"wooden_pickaxe":60,"diamond_pickaxe":1561}
class Mob:
def __init__(self,x,y,kind="zombie"):
self.x,self.y=x,y; self.kind=kind
mapping={
"zombie":(20,"Z"),"skeleton":(20,"S"),"creeper":(20,"C"),
"cow":(15,"O"),"pig":(15,"P"),"chicken":(10,"H"),
"ender_dragon":(200,"D"),"wither":(300,"W")
}
self.health,self.symbol=mapping.get(kind,(10,"?"))
# ----------------------------
# MAIN GAME ENGINE
# ----------------------------
class Game:
def __init__(self,root):
self.root=root
root.title("Ultimate Minecraft Survival")
# Canvas
self.canvas=tk.Canvas(root,
width=VIEWPORT_WIDTH*CELL_SIZE,
height=VIEWPORT_HEIGHT*CELL_SIZE,
bg="black")
self.canvas.pack()
# Hotbar & status
self.hotbar_frame=tk.Frame(root); self.hotbar_frame.pack(fill=tk.X)
self.status_var=tk.StringVar()
ttk.Label(root,textvariable=self.status_var,font=("Arial",12)).pack(fill=tk.X)
# Bind inputs
self.canvas.bind("<KeyPress>",self.on_key)
self.canvas.bind("<Button-1>",self.on_click)
self.canvas.bind("<Motion>",self.on_mouse_move)
self.canvas.bind("<MouseWheel>",self.on_scroll)
self.canvas.bind("<Button-4>",self.on_scroll)
self.canvas.bind("<Button-5>",self.on_scroll)
for i in range(1,10):
root.bind(str(i),self.on_number)
root.bind("o",lambda e:self.auto_sort())
root.bind("p",lambda e:self.save_game())
root.bind("l",lambda e:self.load_game())
root.bind("e",lambda e:self.use_eye())
root.bind("d",lambda e:self.use_bucket())
# World & player
self.current_dimension="overworld"
self.world=self.generate_world("overworld")
self.ww,self.wh=get_dimension_sizes("overworld")
self.biome_map=[choose_biome(x,0) for x in range(self.ww)]
self.weather={b:choose_weather(b,"Spring") for b in set(self.biome_map)}
self.season="Spring"
# spawn at surface
sy=39
sx=next((x for x in range(self.ww)
if self.world[40][x] in ["GRASS","SAND"]),self.ww//2)
self.player=Player(sx,sy)
self.mobs=[]
self.last=time.time()
self.portal_cd=0
self.canvas.focus_set()
self.update_hotbar()
self.update_game()
# -------------
# WORLD GENERATION
# -------------
def generate_world(self,dim):
w,h=get_dimension_sizes(dim)
world=[]
if dim=="overworld":
for y in range(h):
row=[]
for x in range(w):
if y<40: row.append("AIR")
elif y==40:
bio=self.biome_map[x]
if bio=="desert":
row.append("SANDSTONE" if random.random()<0.3 else "SAND")
elif bio=="snowy_tundra":
row.append("SNOW")
else:
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")
elif r<0.16: row.append("GRAVEL")
else: row.append("STONE")
world.append(row)
# Trees
for _ in range(50):
x=random.randint(0,w-1); bio=self.biome_map[x]
if bio in ["plains","forest","swamp"]:
y=40
if world[y][x] in ["GRASS","SAND"]:
ht=random.randint(3,5)
for i in range(1,ht+1): world[y-i][x]="WOOD"
for dx in (-1,0,1):
for dy in (-2,-1):
nx,ny=x+dx,y-ht+dy
if 0<=nx<w and 0<=ny<h: world[ny][nx]="LEAF"
elif dim=="nether":
for y in range(h):
row=[]
for x in range(w):
r=random.random()
if r<0.05: row.append("GLOWSTONE")
elif r<0.15: row.append("SOUL_SAND")
elif r<0.18: row.append("OBSIDIAN")
else: row.append("NETHERRACK")
world.append(row)
for _ in range(10):
x=random.randint(0,w-1); y=random.randint(0,h-1)
world[y][x]="PORTAL"
else: # end
for y in range(h):
row=[]
for x in range(w):
row.append("END_STONE" if y>h-8 else "AIR")
world.append(row)
world[h-9][w//2]="PORTAL"
return world
# -------------
# WEATHER & SEASONS
# -------------
def update_weather_and_season(self):
elapsed=time.time()-START_TIME
self.season=["Spring","Summer","Fall","Winter"][int(elapsed//SEASON_LENGTH)%4]
for b in set(self.biome_map):
self.weather[b]=choose_weather(b,self.season)
# -------------
# SAVE/LOAD
# -------------
def save_game(self,event=None):
data={
"player":self.player,"world":self.world,
"dimension":self.current_dimension,"inventory":self.player.inventory,
"mobs":self.mobs,"biome_map":self.biome_map,
"weather":self.weather,"season":self.season
}
with open("savegame.dat","wb") as f: pickle.dump(data,f)
self.status_var.set("Game Saved!")
def load_game(self,event=None):
try:
with open("savegame.dat","rb") as f: data=pickle.load(f)
self.player = data["player"]
self.world = data["world"]
self.current_dimension = data["dimension"]
self.player.inventory = data["inventory"]
self.mobs = data["mobs"]
self.biome_map = data["biome_map"]
self.weather = data["weather"]
self.season = data["season"]
self.ww,self.wh = get_dimension_sizes(self.current_dimension)
self.update_hotbar(); self.draw_world(); self.update_status()
self.status_var.set("Game Loaded!")
except Exception as e:
print("Load failed:",e)
self.status_var.set("Load Failed!")
# -------------
# VIEWPORT
# -------------
def get_camera_offset(self):
cx=self.player.x-VIEWPORT_WIDTH//2
cy=self.player.y-VIEWPORT_HEIGHT//2
cx=max(0,min(cx,self.ww-VIEWPORT_WIDTH))
cy=max(0,min(cy,self.wh-VIEWPORT_HEIGHT))
return cx,cy
# -------------
# DRAWING
# -------------
def draw_world(self):
self.canvas.delete("all")
cx,cy=self.get_camera_offset()
# brightness
t=(time.time()-START_TIME)%DAY_LENGTH
b=1.0 if t<=DAY_LENGTH/2 else 0.5
wb=self.weather[self.biome_map[self.player.x]]
eff={"clear":1.0,"rain":0.8,"thunderstorm":0.6,"snow":0.9}[wb]
b*=eff
for y in range(VIEWPORT_HEIGHT):
for x in range(VIEWPORT_WIDTH):
wx,wy=cx+x,cy+y
blk=self.world[wy][wx]
col=BLOCKS.get(blk,"#000000")
if b<1.0:
r=int(int(col[1:3],16)*b)
g=int(int(col[3:5],16)*b)
bl=int(int(col[5:],16)*b)
col=f"#{r:02x}{g:02x}{bl:02x}"
self.canvas.create_rectangle(
x*CELL_SIZE,y*CELL_SIZE,
(x+1)*CELL_SIZE,(y+1)*CELL_SIZE,
fill=col,outline="gray")
# player
px,py=self.player.x-cx,self.player.y-cy
pad=2
self.canvas.create_oval(
px*CELL_SIZE+pad,py*CELL_SIZE+pad,
(px+1)*CELL_SIZE-pad,(py+1)*CELL_SIZE-pad,
fill="red",outline="white")
# mobs
for m in self.mobs:
if cx<=m.x<cx+VIEWPORT_WIDTH and cy<=m.y<cy+VIEWPORT_HEIGHT:
vx,vy=m.x-cx,m.y-cy
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()
# -------------
# HOTBAR & STATUS
# -------------
def update_hotbar(self):
for w in self.hotbar_frame.winfo_children(): w.destroy()
frm=tk.Frame(self.hotbar_frame); frm.pack()
items=[i for i in PLACEABLE if self.player.inventory.get(i,0)>0]
items.sort()
for idx in range(9):
if idx<len(items):
itm=items[idx]; cnt=self.player.inventory[itm]
dur=""
if itm in self.player.tool_durability:
dur=f" D:{self.player.tool_durability[itm]}"
txt=f"{idx+1}:{itm}({cnt}){dur}"
btn=tk.Button(frm,text=txt,width=12,
command=lambda i=itm:self.set_active_item(i))
btn.bind("<Enter>",lambda e,i=itm:self.status_var.set(f"Hover: {i}"))
btn.bind("<Leave>",lambda e:self.update_status())
if self.player.active_item==itm:
btn.config(relief="sunken",bg="yellow")
else:
btn=tk.Button(frm,text=f"{idx+1}:----",width=12,state="disabled")
btn.grid(row=0,column=idx,padx=2,pady=2)
def set_active_item(self,itm):
self.player.active_item=itm
self.update_hotbar(); self.update_status()
def update_status(self):
phase="Day" if (time.time()-START_TIME)%DAY_LENGTH<=DAY_LENGTH/2 else "Night"
act=self.player.active_item or "None"
self.status_var.set(
f"HP:{int(self.player.health)} Hunger:{int(self.player.hunger)} "
f"Active:{act} Dim:{self.player.dimension} {phase} "
f"Season:{self.season} Weather:{self.weather[self.biome_map[self.player.x]]}"
)
self.update_hotbar()
# -------------
# INPUT HANDLERS
# -------------
def on_key(self,e):
k=e.keysym.lower()
if k in ["w","a","s","d"]: self.move_player(k)
elif k=="space": self.jump()
elif k=="i": self.open_inventory()
elif k=="c": self.open_crafting()
elif k=="f": self.open_furnace()
elif k=="h": self.open_help()
elif k=="t": self.travel_dimension()
elif k=="o": self.auto_sort()
self.draw_world(); self.update_status()
def on_click(self,e):
cx,cy=self.get_camera_offset()
gx,gy=e.x//CELL_SIZE,e.y//CELL_SIZE
wx,wy=cx+gx,cy+gy
if not(0<=wx<self.ww and 0<=wy<self.wh): return
if abs(wx-self.player.x)<=1 and abs(wy-self.player.y)<=1:
blk=self.world[wy][wx]
if blk=="GRAVEL":
drop=("FLINT" if random.random()<0.2 else "GRAVEL")
self.player.inventory[drop]=min(self.player.inventory.get(drop,0)+1,STACK_LIMIT)
self.world[wy][wx]="AIR"
elif blk=="OBSIDIAN":
if self.player.active_item!="diamond_pickaxe": return
self.player.tool_durability["diamond_pickaxe"]-=1
if self.player.tool_durability["diamond_pickaxe"]<=0:
self.player.inventory["diamond_pickaxe"]=0; self.player.active_item=None
self.player.inventory["OBSIDIAN"]=self.player.inventory.get("OBSIDIAN",0)+1
self.world[wy][wx]="AIR"
elif blk!="AIR":
self.player.inventory[blk]=min(self.player.inventory.get(blk,0)+1,STACK_LIMIT)
self.world[wy][wx]="AIR"
else:
itm=self.player.active_item
if itm and itm in PLACEABLE:
self.world[wy][wx]=itm
self.player.inventory[itm]-=1
if self.player.inventory[itm]==0: self.player.active_item=None
self.draw_world(); self.update_status()
def on_mouse_move(self,e):
cx,cy=self.get_camera_offset()
wx,wy=cx+e.x//CELL_SIZE,cy+e.y//CELL_SIZE
dx,dy=wx-self.player.x,self.player.y-wy
if dx!=0 or dy!=0:
self.player.orientation=math.degrees(math.atan2(dy,dx))
self.update_status()
def on_scroll(self,e):
items=[i for i in PLACEABLE if self.player.inventory.get(i,0)>0]
items.sort()
if not items: return
try: idx=items.index(self.player.active_item)
except: idx=-1
if e.num==4 or getattr(e,"delta",0)>0: idx=(idx+1)%len(items)
else: idx=(idx-1)%len(items)
self.player.active_item=items[idx]
self.update_status()
def on_number(self,e):
idx=int(e.char)-1
items=[i for i in PLACEABLE if self.player.inventory.get(i,0)>0]
items.sort()
if 0<=idx<len(items): self.player.active_item=items[idx]
self.update_status()
# -------------
# MOVEMENT & PHYSICS
# -------------
def move_player(self,dir):
dx,dy=0,0
if dir=="w": dy=-1
if dir=="s": dy=1
if dir=="a": dx=-1
if dir=="d": dx=1
nx,ny=self.player.x+dx,self.player.y+dy
if 0<=nx<self.ww and 0<=ny<self.wh and self.world[ny][nx]=="AIR":
self.player.x,self.player.y=nx,ny
def jump(self):
if self.world[self.player.y+1][self.player.x]!="AIR":
self.player.vy=-3
def apply_gravity(self,dt):
self.player.vy+=0.5*dt
newy=self.player.y+int(self.player.vy)
if newy>=self.wh-1:
newy=self.wh-1; self.player.vy=0
if self.world[newy][self.player.x]=="AIR":
self.player.y=newy
else:
self.player.vy=0
# -------------
# INVENTORY UI
# -------------
def open_inventory(self):
win=tk.Toplevel(self.root); win.title("Inventory"); win.geometry("400x500")
ttk.Label(win,text="Click to pick & swap. Press O to sort.",font=("Arial",12)).pack(pady=10)
frm=tk.Frame(win); frm.pack(fill=tk.BOTH,expand=True)
self.inv_buttons={}
items=list(self.player.inventory.items())
for i,(itm,cnt) in enumerate(items):
b=tk.Button(frm,text=f"{itm}\n({cnt})",width=12,height=3)
b.grid(row=i//4,column=i%4,padx=5,pady=5)
b.bind("<Button-1>",lambda e,itm=itm:self.inv_click(itm,win))
self.inv_buttons[itm]=b
ttk.Button(win,text="Done",command=win.destroy).pack(pady=10)
def inv_click(self,item,win):
if not hasattr(self,"dragged"):
self.dragged=item; self.status_var.set(f"Picked up {item}")
else:
a,b=self.dragged,item
self.player.inventory[a],self.player.inventory[b]=\
self.player.inventory[b],self.player.inventory[a]
self.player.active_item=a
del self.dragged
self.status_var.set(f"Swapped {a} & {b}")
self.update_hotbar(); self.update_status()
def auto_sort(self):
self.player.inventory=dict(sorted(self.player.inventory.items()))
self.status_var.set("Inventory sorted")
self.update_hotbar(); self.update_status()
# -------------
# CRAFTING UI
# -------------
def open_crafting(self):
win=tk.Toplevel(self.root); win.title("Crafting"); win.geometry("400x500")
ttk.Label(win,text="Crafting",font=("Arial",12)).pack(pady=10)
frm=tk.Frame(win); frm.pack(fill=tk.BOTH,expand=True)
for itm,req in crafting_recipes.items():
if all(self.player.inventory.get(k,0)>=v for k,v in req.items()):
txt=f"{itm}: " + ", ".join(f"{k}:{v}" for k,v in req.items())
b=ttk.Button(frm,text=txt,command=lambda i=itm:self.craft(i,win))
b.pack(fill=tk.X,padx=5,pady=5)
ttk.Button(frm,text="Close",command=win.destroy).pack(pady=10)
def craft(self,item,win):
req=crafting_recipes[item]
if all(self.player.inventory.get(k,0)>=v for k,v in req.items()):
for k,v in req.items(): self.player.inventory[k]-=v
self.player.inventory[item]=min(self.player.inventory.get(item,0)+1,STACK_LIMIT)
self.update_status()
win.destroy()
# -------------
# FURNACE UI
# -------------
def open_furnace(self):
win=tk.Toplevel(self.root); win.title("Furnace"); win.geometry("400x300")
ttk.Label(win,text="Furnace",font=("Arial",12)).pack(pady=10)
ttk.Label(win,text="Input:").pack(); inp=tk.Entry(win); inp.pack()
ttk.Label(win,text="Fuel:").pack(); fuel=tk.Entry(win); fuel.pack()
out_lbl=ttk.Label(win,text=""); out_lbl.pack(pady=5)
def smelt():
i=inp.get().upper(); f=fuel.get().upper()
if i in smelting_recipes and self.player.inventory.get(f,0)>0:
o=smelting_recipes[i]
self.player.inventory[o]+=1
self.player.inventory[i]-=1
self.player.inventory[f]-=1
out_lbl.config(text=f"Smelted +1 {o}")
else:
out_lbl.config(text="Failed")
self.update_status()
ttk.Button(win,text="Smelt",command=smelt).pack(pady=10)
# -------------
# BUCKET LOGIC
# -------------
def use_bucket(self):
itm=self.player.active_item
if itm=="bucket":
self.player.inventory["bucket"]-=1
self.player.inventory["water_bucket"]+=1
self.player.active_item="water_bucket"
elif itm=="water_bucket":
for dx,dy in [(-1,0),(1,0),(0,-1),(0,1)]:
x,y=self.player.x+dx,self.player.y+dy
if 0<=x<self.ww and 0<=y<self.wh and self.world[y][x]=="LAVA":
self.world[y][x]="OBSIDIAN"
self.player.active_item="bucket"
elif itm=="lava_bucket":
for dx,dy in [(-1,0),(1,0),(0,-1),(0,1)]:
x,y=self.player.x+dx,self.player.y+dy
if 0<=x<self.ww and 0<=y<self.wh and self.world[y][x]=="AIR":
self.world[y][x]="LAVA"
self.player.inventory["lava_bucket"]-=1
self.player.inventory["bucket"]+=1
self.player.active_item="bucket"
self.update_status()
# -------------
# PORTALS & EYE OF ENDER
# -------------
def travel_dimension(self):
if time.time()<self.portal_cd: return
idx=DIMENSIONS.index(self.player.dimension)
newd=DIMENSIONS[(idx+1)%3]
self.player.dimension=newd
self.world=self.generate_world(newd)
self.ww,self.wh=get_dimension_sizes(newd)
if newd=="overworld":
sx=next((x for x in range(self.ww)
if self.world[40][x] in ["GRASS","SAND"]),self.ww//2)
sy=39
else:
sx,sy=self.ww//2,self.wh//2
self.mobs=[]
if newd=="nether": self.mobs.append(Mob(sx,sy,"wither"))
if newd=="end": self.mobs.append(Mob(sx,sy,"ender_dragon"))
self.player.x,self.player.y=sx,sy
self.portal_cd=time.time()+3
self.update_status(); self.draw_world()
def use_eye(self):
self.status_var.set("Eye of Ender flies... (stub)")
# -------------
# GAME LOOP
# -------------
def update_game(self):
now=time.time(); dt=now-self.last; self.last=now
self.apply_gravity(dt); self.update_weather_and_season()
# hunger/health
self.player.hunger=max(0,self.player.hunger-0.05*dt)
if self.player.hunger>80 and self.player.health<100:
self.player.health=min(100,self.player.health+0.02*dt)
if self.player.hunger<20: self.player.health-=0.05*dt
# spawn mobs
if self.player.dimension=="overworld" and random.random()<0.01 and len(self.mobs)<10:
ex,ey=random.randint(0,self.ww-1),random.randint(40,self.wh-1)
if abs(ex-self.player.x)>5 and abs(ey-self.player.y)>5:
bio=self.biome_map[ex]
mtyp=("skeleton" if bio=="desert"
else random.choice(["zombie","cow","pig","chicken"])
if bio in ["plains","forest"] else "zombie")
self.mobs.append(Mob(ex,ey,mtyp))
# mob AI
for m in self.mobs:
if m.x<self.player.x and self.world[m.y][m.x+1]=="AIR": m.x+=1
elif m.x>self.player.x and self.world[m.y][m.x-1]=="AIR": m.x-=1
if m.y<self.player.y and self.world[m.y+1][m.x]=="AIR": m.y+=1
elif m.y>self.player.y and self.world[m.y-1][m.x]=="AIR": m.y-=1
if abs(m.x-self.player.x)<=1 and abs(m.y-self.player.y)<=1:
self.player.health-=2*dt
if self.player.health<=0:
return self.game_over()
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")
# -------------
# HELP WINDOW
# -------------
def open_help(self):
w=tk.Toplevel(self.root); w.title("Help"); w.geometry("400x400")
txt=(
"Controls:\n"
" WASD: Move Space: Jump\n"
" Click: Mine/Place 1-9/Wheel: Hotbar\n"
" I: Inventory O: Sort C: Crafting F: Furnace\n"
" T: Portal E: Eye of Ender D: Bucket\n"
" P: Save L: Load\n\n"
"Features:\n"
"- Gravel→Flint, Flint&Steel\n"
"- Buckets (water/lava)\n"
"- Furnace smelting (Iron, Gold, Meat, Sand→Glass)\n"
"- Crafting Table, Chest, Ladder\n"
"- Blaze/Eye of Ender, End Portal Frame, Nether Star stub\n"
"- Nether & End portals with boss stubs\n"
"- Mobs, weather, seasons, save/load"
)
ttk.Label(w,text=txt,wraplength=380,justify="left").pack(padx=10,pady=10)
ttk.Button(w,text="Close",command=w.destroy).pack(pady=10)
# ----------------------------
# STARTUP MENU & LAUNCHER
# ----------------------------
def startup_menu():
root=tk.Tk()
root.title("Ultimate Minecraft Survival")
menu_frame=tk.Frame(root)
menu_frame.pack(fill=tk.BOTH,expand=True)
ttk.Label(menu_frame,
text="Ultimate Minecraft Survival",
font=("Arial",18)).pack(pady=20)
def start():
menu_frame.destroy()
Game(root)
ttk.Button(menu_frame,text="Start",command=start).pack(pady=5)
ttk.Button(menu_frame,text="Help",
command=lambda: Game(root).open_help()).pack(pady=5)
ttk.Button(menu_frame,text="Quit",command=root.destroy).pack(pady=5)
root.mainloop()
if __name__=="__main__":
startup_menu()