#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Comprehensive Minecraft-Inspired Survival Game Prototype
Upgrades applied:
• Map enlarged to 1000×200 (Overworld)
• Mob spawn cap set to 10
• Safe-callback wrapper around all Tkinter binds
• True LAN multiplayer (automatic host/join, unlimited players)
• Block place/break synchronization over LAN
• Inventory UI (opens with E), click to select active block
"""
import tkinter as tk
from tkinter import ttk
import math, random, time
import socket, threading, pickle
# --------------------------
# GLOBAL SETTINGS
# --------------------------
OVERWORLD_WIDTH = 1000
OVERWORLD_HEIGHT = 200
NETHER_WIDTH = 120
NETHER_HEIGHT = 60
END_WIDTH = 150
END_HEIGHT = 80
VIEWPORT_WIDTH = 80
VIEWPORT_HEIGHT = 20
CELL_SIZE = 10
DAY_LENGTH = 120
START_TIME = time.time()
STACK_LIMIT = 64
MAX_MOBS = 10
NETWORK_PORT = 50000
# --------------------------
# BLOCKS & ITEMS (colors)
# --------------------------
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",
"wood_plank": "#DEB887",
"wooden_pickaxe": "#CD853F",
"apple": "#FF0000",
"PORTAL": "#0000FF",
"NETHERRACK": "#A52A2A",
"END_STONE": "#D2B48C",
}
PLACEABLE = set(BLOCKS) - {"AIR"}
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},
}
DIMENSIONS = ["overworld","nether","end"]
# ─── Safe-callback decorator ───
def safe_tk(fn):
def wrapper(event):
try:
return fn(event)
except Exception as e:
print(f"[Tk error in {fn.__name__}]:", e)
return wrapper
# ───────────────────────────────
class Player:
def __init__(self, x, y, dimension="overworld"):
self.x = x; self.y = y
self.health = 100; self.hunger = 100
self.dimension = dimension
self.inventory = {k:0 for k in BLOCKS if k!="AIR"}
for it in ["wood_plank","wooden_pickaxe","crafting_table","apple","TORCH"]:
self.inventory.setdefault(it,0)
self.active_item = None
self.orientation = 0
self.pid = random.randint(1,1<<30)
c = self.pid & 0xFFFFFF
self.color = f"#{c:06x}"
class Enemy:
def __init__(self, x, y, kind="zombie"):
self.x = x; self.y = y
self.kind = kind
self.health = 20 if kind=="zombie" else 50
class Game:
def __init__(self, root):
self.root = root
root.title("Minecraft-Inspired Survival")
# Canvas setup
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()
# Bind events
self.canvas.bind("<KeyPress>", safe_tk(self.on_key))
self.canvas.bind("<Button-1>", safe_tk(self.on_left_click))
self.canvas.bind("<Motion>", safe_tk(self.on_mouse_move))
self.canvas.bind("<MouseWheel>", safe_tk(self.on_mouse_wheel))
self.canvas.bind("<Button-4>", safe_tk(self.on_mouse_wheel))
self.canvas.bind("<Button-5>", safe_tk(self.on_mouse_wheel))
# Status label
self.status_var = tk.StringVar()
ttk.Label(root, textvariable=self.status_var).pack(fill=tk.X)
# World setup
self.current_dimension = "overworld"
self.world = self.generate_world(self.current_dimension)
self.world_width, self.world_height = self.get_world_dimensions(self.current_dimension)
# Player & enemies
cx, cy = self.world_width//2, self.world_height//2
self.player = Player(cx, cy, self.current_dimension)
self.enemies = []
# Multiplayer state
self.players = {self.player.pid: self.player}
self.sock = None
self.is_host = False
self.pending_block_changes = []
self.start_network()
# Game loop
self.last_update = time.time()
self.update_game()
# ─── Inventory UI methods ───
def open_inventory(self):
inv = tk.Toplevel(self.root)
inv.title("Inventory")
inv.geometry("300x400")
ttk.Label(inv, text="Click an item to select for placement", font=('Arial',12)).pack(pady=10)
frame = ttk.Frame(inv); frame.pack(fill=tk.BOTH, expand=True)
c = tk.Canvas(frame); sb = ttk.Scrollbar(frame, orient="vertical", command=c.yview)
inner = ttk.Frame(c)
inner.bind("<Configure>", lambda e: c.configure(scrollregion=c.bbox("all")))
c.create_window((0,0), window=inner, anchor="nw")
c.configure(yscrollcommand=sb.set)
c.pack(side=tk.LEFT, fill=tk.BOTH, expand=True); sb.pack(side=tk.RIGHT, fill=tk.Y)
for item, count in self.player.inventory.items():
btn = ttk.Button(inner, text=f"{item}: {count}",
command=lambda i=item: self.select_inventory(i, inv))
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()
# ──────────────────────────────
# ─── Networking methods (unchanged except block sync) ───
def start_network(self):
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.settimeout(0.5)
udp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)
udp.bind(('', NETWORK_PORT))
try:
data,addr = udp.recvfrom(1024)
if data==b"MCHOST":
self.connect_host(addr[0]); return
except socket.timeout: pass
self.is_host = True
threading.Thread(target=self._broadcast, daemon=True).start()
threading.Thread(target=self._serve, daemon=True).start()
def _broadcast(self):
b=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
b.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)
for _ in range(5):
b.sendto(b"MCHOST",('<broadcast>',NETWORK_PORT))
time.sleep(0.2)
def _serve(self):
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('',NETWORK_PORT)); s.listen()
while True:
conn,_=s.accept()
threading.Thread(target=self._handle_client,args=(conn,),daemon=True).start()
def _handle_client(self,conn):
while True:
data=conn.recv(4096)
if not data: break
st=pickle.loads(data)
if 'block_change' in st:
ch=st['block_change']
self.world[ch['y']][ch['x']]=ch['block']
self.pending_block_changes.append(ch)
pid,pd=st['pid'],st['ply']
p=self.players.get(pid) or Player(pd['x'],pd['y'],pd['dim'])
p.x,p.y,p.dimension=pd['x'],pd['y'],pd['dim']
self.players[pid]=p
snap={'players':{pid:{'x':pl.x,'y':pl.y,'dim':pl.dimension}
for pid,pl in self.players.items()},
'enemies':[(e.x,e.y) for e in self.enemies],
'block_changes': self.pending_block_changes}
self.pending_block_changes=[]
conn.sendall(pickle.dumps(snap))
def connect_host(self,ip):
c=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
c.connect((ip,NETWORK_PORT)); self.sock=c
threading.Thread(target=self._talk_server,daemon=True).start()
def _talk_server(self):
while True:
state={'pid':self.player.pid,
'ply':{'x':self.player.x,'y':self.player.y,'dim':self.player.dimension}}
self.sock.sendall(pickle.dumps(state))
data=self.sock.recv(65536)
if not data: break
snap=pickle.loads(data)
for ch in snap.get('block_changes',[]):
self.world[ch['y']][ch['x']] = ch['block']
for pid,pd in snap['players'].items():
if pid==self.player.pid: continue
p=self.players.get(pid) or Player(pd['x'],pd['y'],pd['dim'])
p.x,p.y,p.dimension=pd['x'],pd['y'],pd['dim']
self.players[pid]=p
self.enemies=[Enemy(x,y) for x,y in snap['enemies']]
time.sleep(0.1)
# ───────────────────────────────────────────────────────
# --------------------------
# WORLD GENERATION
# --------------------------
def get_world_dimensions(self,dim):
if dim=="overworld": return OVERWORLD_WIDTH,OVERWORLD_HEIGHT
if dim=="nether": return NETHER_WIDTH,NETHER_HEIGHT
if dim=="end": return END_WIDTH,END_HEIGHT
return 80,20
def generate_world(self,dim):
w,h=self.get_world_dimensions(dim)
grid=[]
if dim=="overworld":
for y in range(h):
row=[]
for x in range(w):
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")
grid.append(row)
for _ in range(50):
tx=random.randint(0,w-1); ty=40
if grid[ty][tx]=="GRASS":
th=random.randint(3,5)
for i in range(th): grid[ty-i-1][tx]="WOOD"
for dx in(-1,0,1):
for dy in(-2,-1):
nx,ny=tx+dx,ty-th+dy
if 0<=nx<w and 0<=ny<h: grid[ny][nx]="LEAF"
elif dim=="nether":
for y in range(h): grid.append(["NETHERRACK"]*w)
for _ in range(10):
x=random.randint(0,w-1); y=random.randint(0,h-1)
grid[y][x]="PORTAL"
else:
for y in range(h):
grid.append(["END_STONE" if y>h-8 else "AIR"]*w)
mx,my=w//2,h-9; grid[my][mx]="PORTAL"
return grid
# --------------------------
# VIEWPORT & DRAWING
# --------------------------
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.world_width-VIEWPORT_WIDTH))
cy=max(0,min(cy,self.world_height-VIEWPORT_HEIGHT))
return cx,cy
def draw_world(self):
self.canvas.delete("all")
cx,cy=self.get_camera_offset()
# draw tiles …
for y in range(VIEWPORT_HEIGHT):
for x in range(VIEWPORT_WIDTH):
bx,by=cx+x,cy+y
clr=BLOCKS.get(self.world[by][bx],"#000000")
elapsed=(time.time()-START_TIME)%DAY_LENGTH
if elapsed>DAY_LENGTH/2: clr=self.adjust_color(clr,0.5)
self.canvas.create_rectangle(x*CELL_SIZE,y*CELL_SIZE,
(x+1)*CELL_SIZE,(y+1)*CELL_SIZE,
fill=clr,outline="gray")
pad=2
# draw enemies …
for e in self.enemies:
ex,ey=e.x-cx,e.y-cy
if 0<=ex<VIEWPORT_WIDTH and 0<=ey<VIEWPORT_HEIGHT:
self.canvas.create_oval(ex*CELL_SIZE+pad,ey*CELL_SIZE+pad,
(ex+1)*CELL_SIZE-pad,(ey+1)*CELL_SIZE-pad,
fill="black",outline="white")
# draw all players …
for p in self.players.values():
px,py=p.x-cx,p.y-cy
if 0<=px<VIEWPORT_WIDTH and 0<=py<VIEWPORT_HEIGHT:
self.canvas.create_oval(px*CELL_SIZE+pad,py*CELL_SIZE+pad,
(px+1)*CELL_SIZE-pad,(py+1)*CELL_SIZE-pad,
fill=p.color,outline="white")
# draw self on top …
px,py=self.player.x-cx,self.player.y-cy
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")
self.canvas.update()
def adjust_color(self,hex_color,factor):
rgb=[int(hex_color[i:i+2],16) for i in (1,3,5)]
return "#%02x%02x%02x"%tuple(int(c*factor) for c in rgb)
# --------------------------
# EVENT HANDLERS
# --------------------------
def on_key(self,event):
k=event.keysym.lower()
if k in("w","a","s","d"):
dx=(k=="d")-(k=="a"); dy=(k=="s")-(k=="w")
self._move(self.player,dx,dy)
elif k=="t":
self._switch_dim()
elif k=="e": # Inventory key ───
self.open_inventory()
self.draw_world()
self.update_status()
def on_left_click(self,event):
cam_x,cam_y=self.get_camera_offset()
gx,gy=cam_x+event.x//CELL_SIZE,cam_y+event.y//CELL_SIZE
if 0<=gx<self.world_width and 0<=gy<self.world_height:
if self.world[gy][gx]!="AIR":
self.world[gy][gx]="AIR"
# add to inventory
blk = self.world[gy][gx]
cnt = self.player.inventory.get(blk,0)
if cnt<STACK_LIMIT: self.player.inventory[blk]=cnt+1
elif self.player.active_item:
self.world[gy][gx]=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
change={'x':gx,'y':gy,'block':self.world[gy][gx]}
if self.is_host:
self.pending_block_changes.append(change)
elif self.sock:
try: self.sock.sendall(pickle.dumps({'block_change':change}))
except: pass
self.draw_world()
self.update_status()
def on_mouse_move(self,event): pass
def on_mouse_wheel(self,event): pass
def _move(self,p,dx,dy):
w,h=self.get_world_dimensions(p.dimension)
nx,ny=p.x+dx,p.y+dy
if 0<=nx<w and 0<=ny<h and self.world[ny][nx]=="AIR":
p.x,p.y=nx,ny
def _switch_dim(self):
idx=DIMENSIONS.index(self.player.dimension)
nd=DIMENSIONS[(idx+1)%3]
self.player.dimension=nd
self.world=self.generate_world(nd)
self.world_width,self.world_height=self.get_world_dimensions(nd)
self.player.x,self.player.y=self.world_width//2,self.world_height//2
# --------------------------
# GAME LOOP
# --------------------------
def update_game(self):
now=time.time(); dt=now-self.last_update; self.last_update=now
if (self.player.dimension=="overworld"
and len(self.enemies)<MAX_MOBS
and random.random()<0.01):
x=random.randint(0,self.world_width-1)
y=random.randint(45,self.world_height-1)
if abs(x-self.player.x)>5 and abs(y-self.player.y)>5:
self.enemies.append(Enemy(x,y))
for e in self.enemies:
dx=(1 if e.x<self.player.x else -1 if e.x>self.player.x else 0)
dy=(1 if e.y<self.player.y else -1 if e.y>self.player.y else 0)
self._move(e,dx,dy)
self.draw_world()
self.root.after(100,self.update_game)
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 or "None"
self.status_var.set(
f"Health:{int(self.player.health)} Hunger:{int(self.player.hunger)}"
f" Active:{active} Dim:{self.player.dimension} {phase}"
)
# --------------------------
# MAIN
# --------------------------
def main():
root=tk.Tk()
game=Game(root)
root.mainloop()
if __name__=="__main__":
main()