#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ascii_minecraft_game.py
Full Integrated ASCII Minecraft‑inspired Game
Phases 1–4:
- Core systems: crafting, smelting, tools, inventory
- Phase 2: dimensions, portals, bosses
- Phase 3: biomes, structures, mobs, breeding, villagers
- Phase 4: enchanting, redstone, rails, brewing, XP, events, hardcore, modding
Requires Python 3.6+ (Tkinter, json, pickle).
"""
import tkinter as tk
from tkinter import ttk, simpledialog, filedialog
import random, time, pickle, math, json, os
# -----------------------------------------------------------------------------
# GLOBAL SETTINGS
# -----------------------------------------------------------------------------
OVER_W, OVER_H = 200, 60
NETH_W, NETH_H = 100, 40
END_W , END_H = 120, 50
VIEW_W, VIEW_H = 80, 20
CELL = 8
STACK_LIMIT = 64
DAY_LEN = 120
SEASON_LEN = 300
START_T = time.time()
DIMENSIONS = ["overworld","nether","end"]
DIFFICULTIES = ["Peaceful","Easy","Normal","Hard","Hardcore"]
EVENT_SCHEDULE = {0:"Blood Moon",300:"Harvest Festival"}
# -----------------------------------------------------------------------------
# LOAD OPTIONAL DATAPACK (JSON)
# -----------------------------------------------------------------------------
DATA_PACK = {}
if os.path.exists("datapack.json"):
with open("datapack.json") as f:
DATA_PACK = json.load(f)
# -----------------------------------------------------------------------------
# BIOMES & WEATHER FUNCTIONS
# -----------------------------------------------------------------------------
BIOMES = ["plains","desert","mountains","forest","jungle","taiga","ice_spikes","swamp","snowy_tundra"]
def choose_biome(x):
return BIOMES[x % len(BIOMES)]
def choose_weather(biome, season):
if biome=="desert": return "clear"
if season=="Winter" and biome in ["snowy_tundra","ice_spikes","taiga"]:
return random.choice(["clear","snow"])
return random.choices(["clear","rain","thunderstorm"],weights=[0.6,0.3,0.1])[0]
# -----------------------------------------------------------------------------
# BLOCK & ITEM COLOR DEFINITIONS
# -----------------------------------------------------------------------------
BLOCKS = {
"AIR":"#FFFFFF","DIRT":"#8B4513","STONE":"#808080","COBBLESTONE":"#555555",
"GRASS":"#00AA00","WOOD":"#A0522D","LEAF":"#66CC66","SAND":"#EDC9AF",
"WATER":"#0000FF","LAVA":"#FF4500","OBSIDIAN":"#440044",
"NETHERRACK":"#A52A2A","SOUL_SAND":"#806040","GLOWSTONE":"#FFFFE0",
"END_STONE":"#D2B48C","PORTAL":"#0000FF","END_PORTAL_FRAME":"#550088",
"TORCH":"#FFFF00","FURNACE":"#CC6600","CRAFTING_TABLE":"#D2691E","CHEST":"#DEB887",
"WOOD_PLANK":"#DEB887","STICK":"#AAAAAA","RAW_MEAT":"#8B0000","COOKED_MEAT":"#FFA07A",
"IRON_ORE":"#CC6600","GOLD_ORE":"#FFD700","DIAMOND":"#55FFFF",
"IRON_INGOT":"#CCAA66","GOLD_INGOT":"#FFF700",
"WOODEN_PICKAXE":"#CD853F","STONE_PICKAXE":"#A9A9A9","IRON_PICKAXE":"#CC6600","DIAMOND_PICKAXE":"#55FFFF",
"WOODEN_SWORD":"#CD853F","STONE_SWORD":"#A9A9A9","IRON_SWORD":"#CC6600","DIAMOND_SWORD":"#55FFFF",
"WOODEN_AXE":"#CD853F","STONE_AXE":"#A9A9A9","IRON_AXE":"#CC6600","DIAMOND_AXE":"#55FFFF",
"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",
"LADDER":"#8B4513","RAIL":"#CCCCCC","MINECART":"#444444",
"ENCHANT_TABLE":"#AA00AA","BREWING_STAND":"#666600"
}
# extend via datapack
BLOCKS.update(DATA_PACK.get("blocks",{}))
PLACEABLE = set(BLOCKS) - {"AIR"}
# -----------------------------------------------------------------------------
# CRAFTING & SMELTING RECIPES
# -----------------------------------------------------------------------------
crafting = {
"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},
"WOODEN_SWORD":{"WOOD_PLANK":2,"STICK":1},"STONE_SWORD":{"COBBLESTONE":2,"STICK":1},
"IRON_SWORD":{"IRON_INGOT":2,"STICK":1},"DIAMOND_SWORD":{"DIAMOND":2,"STICK":1},
"WOODEN_AXE":{"WOOD_PLANK":3,"STICK":2},"STONE_AXE":{"COBBLESTONE":3,"STICK":2},
"IRON_AXE":{"IRON_INGOT":3,"STICK":2},"DIAMOND_AXE":{"DIAMOND":3,"STICK":2},
"FURNACE":{"COBBLESTONE":8},"CRAFTING_TABLE":{"WOOD_PLANK":4},"CHEST":{"WOOD_PLANK":8},
"BUCKET":{"IRON_INGOT":3},"TORCH":{"STICK":1,"COAL_ORE":1},
"FLINT_AND_STEEL":{"IRON_INGOT":1,"FLINT":1},
"GLASS":{"SAND":4},"BLAZE_POWDER":{"BLAZE_ROD":1},
"EYE_OF_ENDER":{"BLAZE_POWDER":1,"ENDER_PEARL":1},
"END_PORTAL_FRAME":{"EYE_OF_ENDER":1}
}
crafting.update(DATA_PACK.get("crafting",{}))
smelting = {
"COBBLESTONE":"STONE","IRON_ORE":"IRON_INGOT",
"GOLD_ORE":"GOLD_INGOT","RAW_MEAT":"COOKED_MEAT","SAND":"GLASS"
}
smelting.update(DATA_PACK.get("smelting",{}))
# -----------------------------------------------------------------------------
# ENCHANTMENTS & BREWING
# -----------------------------------------------------------------------------
ENCHANTS = {"sharpness":lambda t:t.endswith("SWORD"),
"efficiency":lambda t:t.endswith("PICKAXE"),
"unbreaking": lambda t:True}
BREWING = {("awkward","nether_wart"):("awkward","potion_of_healing")}
BREWING.update(DATA_PACK.get("brewing",{}))
# -----------------------------------------------------------------------------
# XP & LEVEL SYSTEM
# -----------------------------------------------------------------------------
def xp_to_level(xp): return xp//100
# -----------------------------------------------------------------------------
# ENTITY CLASSES: Player, Mob, Animal, Villager, Minecart
# -----------------------------------------------------------------------------
class Player:
def __init__(self,x,y):
self.x,self.y=x,y
self.health=100; self.hunger=100
self.xp=0; self.level=0; self.sp=0
self.dimension="overworld"
self.inventory={b:0 for b in BLOCKS}
self.dur={} # durability per tool
self.enchants={} # applied enchants
self.ender_chest={}; self.shulker_boxes={}
self.active=None; self.hardcore=False
def gain_xp(self,amt):
self.xp+=amt
lvl=xp_to_level(self.xp)
if lvl>self.level:
self.sp+=lvl-self.level
self.level=lvl
class Mob:
def __init__(self,x,y,k="zombie"):
self.x,self.y=x,y; self.k=k
stats={"zombie":(20,"Z"),"skeleton":(20,"S"),
"creeper":(20,"C"),"wither":(300,"W"),
"ender_dragon":(200,"D")}
self.hp,self.sym=stats.get(k,(10,"?"))
self.tick=0
class Animal:
def __init__(self,x,y,kind="cow"):
self.x,self.y, self.kind=x,y,kind
self.age=0
class Villager:
def __init__(self,x,y):
self.x,self.y=x,y
self.prof=random.choice(["farmer","librarian","blacksmith"])
self.trades={}
self._make_trades()
def _make_trades(self):
if self.prof=="farmer": self.trades={"WHEAT":5,"EMERALD":1}
if self.prof=="librarian": self.trades={"PAPER":8,"EMERALD":1}
if self.prof=="blacksmith": self.trades={"IRON_INGOT":4,"EMERALD":1}
class Minecart:
def __init__(self,x,y): self.x,self.y=x,y
def move(self,world):
# follow rails
for dx,dy in [(1,0),(-1,0),(0,1),(0,-1)]:
if world[self.y][self.x]=="RAIL":
self.x+=dx; self.y+=dy; break
# -----------------------------------------------------------------------------
# MAIN GAME CLASS
# -----------------------------------------------------------------------------
class Game:
def __init__(self,root):
self.root=root; root.title("ASCII Minecraft")
# Canvas & UI
self.canvas=tk.Canvas(root,width=VIEW_W*CELL,height=VIEW_H*CELL,bg="black")
self.canvas.pack()
self.hotbar=tk.Frame(root); self.hotbar.pack(fill=tk.X)
self.status=tk.StringVar(); ttk.Label(root,textvariable=self.status).pack(fill=tk.X)
# Startup menu
menu=tk.Frame(root); menu.place(relx=0.5,rely=0.5,anchor="center")
ttk.Label(menu,text="ASCII Minecraft",font=("Arial",18)).pack(pady=10)
self.diff=tk.StringVar(value="Normal")
ttk.OptionMenu(menu,self.diff,"Normal",*DIFFICULTIES).pack(pady=5)
ttk.Button(menu,text="Start",command=lambda m=menu:self.start(m)).pack(pady=5)
ttk.Button(menu,text="Quit",command=root.destroy).pack()
def start(self,menu):
self.difficulty=self.diff.get()
menu.destroy()
self.init_world(); self.bind_events(); self.schedule_events()
self.draw(); self.loop()
def init_world(self):
# Generate all dimensions
self.worlds={}
# Overworld
biome=[choose_biome(i) for i in range(OVER_W)]
ow=[["AIR"]*OVER_W for _ in range(OVER_H)]
mid=OVER_H//2
for x in range(OVER_W): ow[mid][x]="GRASS"; ow[mid+1][x]="DIRT"
self.worlds["overworld"]=ow; self.biome_map=biome
# Nether
nr=[["NETHERRACK"]*NETH_W for _ in range(NETH_H)]; self.worlds["nether"]=nr
# End
ed=[["AIR"]*END_W for _ in range(END_H)]
for x in range(END_W): ed[END_H-3][x]="END_STONE"
cx,cy=END_W//2,END_H-4
for dx in(-1,0,1):
for dy in(-1,0,1):
if abs(dx+dy)%2==1: ed[cy+dy][cx+dx]="END_PORTAL_FRAME"
self.worlds["end"]=ed
# Structures
self._gen_villages(); self._gen_mineshafts(); self._gen_temples()
# Initial
self.dim="overworld"; self.world=self.worlds[self.dim]
self.player=Player(OVER_W//2, mid-1)
self.mobs=[]; self.carts=[]
self.spawners=[]; self.animals=[]; self.villagers=[]
# Populate animals, spawners, villagers
for _ in range(10):
x=random.randint(0,OVER_W-1); y=random.randint(mid,OVER_H-2)
self.animals.append(Animal(x,y,kind=random.choice(["cow","pig","chicken"])))
for _ in range(5):
x=random.randint(0,OVER_W-1); y=random.randint(mid,OVER_H-2)
self.spawners.append((x,y,"zombie"))
for _ in range(8):
x=random.randint(0,OVER_W-1); y=mid-2
self.villagers.append(Villager(x,y))
# Structure generators
def _gen_villages(self):
w,h=OVER_W,OVER_H; bm=self.biome_map; ow=self.worlds["overworld"]
for i,bio in enumerate(bm):
if bio in ("plains","desert") and random.random()<0.02:
cx,cy=i, h//2-2
for dx in range(-2,3):
for dy in range(-2,3):
if 0<=cx+dx<w and 0<=cy+dy<h:
ow[cy+dy][cx+dx]="WOOD_PLANK"
def _gen_mineshafts(self):
w,h=OVER_W,OVER_H; ow=self.worlds["overworld"]
for _ in range(3):
x=random.randint(0,w-1); y=random.randint(h//2,h-2)
for _ in range(20):
if 0<=x<w and 0<=y<h:
ow[y][x]="AIR"
if random.random()<0.2:
nx=x+random.choice([-1,1])
if 0<=nx<w: ow[y][nx]="AIR"
y+=1
def _gen_temples(self):
w,h=OVER_W,OVER_H; bm=self.biome_map; ow=self.worlds["overworld"]
for i,bio in enumerate(bm):
if bio=="desert" and random.random()<0.01:
cx,cy=i,h//2-1
for dx in range(-1,2):
for dy in range(-1,2):
ow[cy+dy][cx+dx]="SANDSTONE"
def bind_events(self):
c=self.canvas; r=self.root
c.focus_set()
c.bind("<Key>", self.on_key)
c.bind("<Button-1>", self.on_click)
r.bind("e", lambda e:self.open_enchant())
r.bind("b", lambda e:self.open_brewing())
r.bind("u", lambda e:self.open_adv_inventory())
r.bind("t", lambda e:self.travel_dimension())
r.bind("E", lambda e:self.use_item())
r.bind("p", lambda e:self.save_game())
r.bind("l", lambda e:self.load_game())
def schedule_events(self):
self.events=list(EVENT_SCHEDULE.items())
def get_camera(self):
cx=self.player.x-VIEW_W//2; cy=self.player.y-VIEW_H//2
cx=max(0,min(cx, len(self.world[0])-VIEW_W))
cy=max(0,min(cy, len(self.world)-VIEW_H))
return cx, cy
def draw(self):
self.canvas.delete("all")
cx,cy=self.get_camera()
for y in range(VIEW_H):
for x in range(VIEW_W):
wx,wy=cx+x, cy+y
blk=self.world[wy][wx] if 0<=wy<len(self.world) and 0<=wx<len(self.world[0]) else "AIR"
col=BLOCKS.get(blk,"#000000")
self.canvas.create_rectangle(x*CELL,y*CELL,(x+1)*CELL,(y+1)*CELL,
fill=col, outline="gray")
# Rails and carts
for cart in self.carts:
rx,ry=cart.x-cx,cart.y-cy
if 0<=rx<VIEW_W and 0<=ry<VIEW_H:
self.canvas.create_text((rx+0.5)*CELL,(ry+0.5)*CELL,text="C")
# Spawners
for sx,sy,k in self.spawners:
rx,ry=sx-cx,sy-cy
if 0<=rx<VIEW_W and 0<=ry<VIEW_H:
self.canvas.create_text((rx+0.5)*CELL,(ry+0.5)*CELL,text="S")
# Animals
for a in self.animals:
rx,ry=a.x-cx,a.y-cy
if 0<=rx<VIEW_W and 0<=ry<VIEW_H:
self.canvas.create_text((rx+0.5)*CELL,(ry+0.5)*CELL,
text=a.kind[0].upper(),fill="white")
# Villagers
for v in self.villagers:
rx,ry=v.x-cx,v.y-cy
if 0<=rx<VIEW_W and 0<=ry<VIEW_H:
self.canvas.create_text((rx+0.5)*CELL,(ry+0.5)*CELL,
text="V",fill="brown")
# Mobs
for m in self.mobs:
rx,ry=m.x-cx,m.y-cy
if 0<=rx<VIEW_W and 0<=ry<VIEW_H:
self.canvas.create_text((rx+0.5)*CELL,(ry+0.5)*CELL,
text=m.sym,fill="black")
# Player
px,py=self.player.x-cx,self.player.y-cy
self.canvas.create_oval(px*CELL+2,py*CELL+2,px*CELL+CELL-2,py*CELL+CELL-2,
fill="red",outline="white")
self._update_hotbar(); self._update_status()
def _update_hotbar(self):
for w in self.hotbar.winfo_children(): w.destroy()
items=[k for k,v in self.player.inventory.items() if v>0]; items.sort()
for i in range(9):
if i<len(items):
itm=items[i]; cnt=self.player.inventory[itm]
dur = f" D:{self.player.dur.get(itm,'-')}" if itm in self.player.dur else ""
btn=tk.Button(self.hotbar, text=f"{i+1}:{itm}({cnt}){dur}",
width=12, command=lambda it=itm:self.select_item(it))
else:
btn=tk.Button(self.hotbar, text=f"{i+1}:----", width=12, state="disabled")
btn.grid(row=0, column=i, padx=2, pady=2)
def _update_status(self):
weather=choose_weather(self.biome_map[self.player.x], self._season())
self.status.set(
f"HP:{int(self.player.health)} Hunger:{int(self.player.hunger)} "
f"Dim:{self.dim} Weather:{weather} XP:{self.player.xp} Lv:{self.player.level}"
)
def _season(self):
return ["Spring","Summer","Fall","Winter"][int((time.time()-START_T)//SEASON_LEN)%4]
# ------------------------
# Event Handlers
# ------------------------
def select_item(self,itm):
self.player.active = itm
def on_key(self,e):
k=e.keysym.lower()
dx,dy=0,0
if k=="a": dx=-1
if k=="d": dx=1
if k=="w": dy=-1
if k=="s": dy=1
if k=="space" and self.world[self.player.y+1][self.player.x]!="AIR":
self.player.y -= 2
# move
nx,ny = self.player.x+dx, self.player.y+dy
if 0<=nx<len(self.world[0]) and 0<=ny<len(self.world) and self.world[ny][nx]=="AIR":
self.player.x, self.player.y = nx, ny
# Other keys: E eat/use, R sort inventory, etc.
if k=="r": self.player.inventory = dict(sorted(self.player.inventory.items()))
self.draw()
def on_click(self,e):
cx,cy=self.get_camera()
gx,gy = cx + e.x//CELL, cy + e.y//CELL
itm = self.player.active
# Break or place
if abs(gx-self.player.x)<=1 and abs(gy-self.player.y)<=1:
blk = self.world[gy][gx]
if blk!="AIR":
# break block
self.player.inventory[blk] = min(self.player.inventory.get(blk,0)+1,STACK_LIMIT)
self.world[gy][gx] = "AIR"
elif itm in PLACEABLE and self.player.inventory.get(itm,0)>0:
self.world[gy][gx] = itm
self.player.inventory[itm] -= 1
self.draw()
def use_item(self):
itm = self.player.active
# Eat food
if itm in ("COOKED_MEAT",):
if self.player.inventory[itm]>0:
self.player.hunger = min(100, self.player.hunger+20)
self.player.inventory[itm]-=1
# Flint & Steel on portal
if itm=="FLINT_AND_STEEL":
self._light_portal()
self.draw()
def _light_portal(self):
# Light any Obsidian frame adjacent
for dx,dy in [(1,0),(-1,0),(0,1),(0,-1)]:
x,y=self.player.x+dx,self.player.y+dy
if self.world[y][x]=="OBSIDIAN":
self.world[y][x] = "PORTAL"
def travel_dimension(self):
# Step into portal
if self.world[self.player.y][self.player.x]=="PORTAL":
idx=DIMENSIONS.index(self.dim)
self.dim=DIMENSIONS[(idx+1)%3]
self.world=self.worlds[self.dim]
sizes={"overworld":(OVER_W,OVER_H),"nether":(NETH_W,NETH_H),"end":(END_W,END_H)}
w,h = sizes[self.dim]
self.player.x,self.player.y = w//2, h//2
# spawn bosses
if self.dim=="nether": self.mobs=[Mob(self.player.x, self.player.y, "wither")]
if self.dim=="end": self.mobs=[Mob(self.player.x, self.player.y-2, "ender_dragon")]
self.draw()
def open_enchant(self):
w=tk.Toplevel(self.root); w.title("Enchanting Table")
ttk.Label(w,text="Select enchant for active tool",font=("Arial",12)).pack(pady=5)
ttk.Button(w,text="Enchant (-10 XP)",command=lambda:self._apply_enchant(w)).pack(pady=5)
def _apply_enchant(self,win):
tool=self.player.active
if tool and self.player.xp>=10:
choice=random.choice([e for e in ENCHANTS if ENCHANTS[e](tool)])
self.player.enchants.setdefault(tool,[]).append(choice)
self.player.xp-=10
win.destroy()
def open_brewing(self):
w=tk.Toplevel(self.root); w.title("Brewing Stand")
ttk.Label(w,text="Brewing not fully implemented",font=("Arial",12)).pack(pady=5)
ttk.Button(w,text="Close",command=w.destroy).pack(pady=5)
def open_adv_inventory(self):
w=tk.Toplevel(self.root); w.title("Advanced Inventory")
ttk.Button(w,text="Open Ender Chest",command=lambda:self._show_container(w,"ender")).pack(pady=5)
ttk.Button(w,text="Manage Shulker Boxes",command=lambda:self._show_container(w,"shulker")).pack(pady=5)
def _show_container(self,win,kind):
# stub: show ender_chest or a shulker box
tk.Label(win,text=f"{kind.title()} not implemented").pack()
def save_game(self):
data={"player":self.player,"worlds":self.worlds,"dim":self.dim}
with open("savegame.dat","wb") as f: pickle.dump(data,f)
self.status.set("Game Saved")
def load_game(self):
try:
with open("savegame.dat","rb") as f: data=pickle.load(f)
self.player = data["player"]
self.worlds = data["worlds"]
self.dim = data["dim"]
self.world=self.worlds[self.dim]
self.status.set("Game Loaded")
self.draw()
except:
self.status.set("Load Failed")
# -----------------------------------------------------------------------------
# MAIN LOOP: MOB SPAWNING, AI, EVENTS, MINECARTS, BREEDING
# -----------------------------------------------------------------------------
def loop(self):
# Seasonal/weather
season=self._season()
for b in set(self.biome_map):
# update weather map if needed
pass
# Spawners
for sx,sy,k in self.spawners:
if random.random()<0.02: self.mobs.append(Mob(sx,sy,k))
# Animals aging
for a in self.animals:
a.age = max(0, a.age-1)
# Mob AI
for m in list(self.mobs):
dx = (1 if m.x<self.player.x else -1 if m.x>self.player.x else 0)
dy = (1 if m.y<self.player.y else -1 if m.y>self.player.y else 0)
if self.world[m.y][m.x]=="AIR":
m.x+=dx; m.y+=dy
if abs(m.x-self.player.x)<=1 and abs(m.y-self.player.y)<=1:
self.player.health-=1
if self.player.health<=0:
return self._game_over()
# Minecarts
for cart in self.carts: cart.move(self.world)
# Scheduled events
now = time.time()-START_T
for t,ev in list(self.events):
if now>=t:
# handle event ev
self.events.remove((t,ev))
self.draw()
self.root.after(200, self.loop)
def _game_over(self):
self.canvas.delete("all")
self.canvas.create_text(VIEW_W*CELL//2, VIEW_H*CELL//2,
text="GAME OVER", fill="red", font=("Arial",24))
self.status.set("You died")
if self.player.hardcore:
try: os.remove("savegame.dat")
except: pass
# -----------------------------------------------------------------------------
# LAUNCHER
# -----------------------------------------------------------------------------
if __name__=="__main__":
root=tk.Tk()
Game(root)
root.mainloop()