import pygame
import sys
import json
import math
import time
from datetime import datetime
# -------------------------
# Config
# -------------------------
SAVE_FILE = "plant_save.json"
WIDTH, HEIGHT = 720, 560
FPS = 60
# In-game time scale: how many in-game minutes pass per real second
TIME_SCALE_MIN_PER_SEC = 5.0 # 1 real sec = 5 in-game minutes
TURBO_MULTIPLIER = 10.0 # Turbo mode speed-up
# Decay & growth tuning (per in-game minute)
MOISTURE_DECAY = 0.10 # moisture decreases per minute
NUTRIENT_DECAY = 0.02 # nutrients decreases per minute
HEALTH_INC_RATE = 0.02 # health regen per minute (good care)
HEALTH_DEC_RATE = 0.05 # health loss per minute (bad care)
HEALTH_SEVERE_PENALTY = 0.20 # extra loss if moisture is 0
WATER_AMOUNT = 35 # moisture gained per watering
FERTILIZER_AMOUNT = 25 # nutrients gained per fertilize
GOOD_MOISTURE = 50
GOOD_NUTRIENTS = 50
LOW_MOISTURE = 30
LOW_NUTRIENTS = 30
# Growth stages by age (in-game minutes)
STAGES = [
("Seed", 0),
("Sprout", 12 * 60), # 12 hours
("Young", 36 * 60), # 1.5 days
("Mature", 3 * 24 * 60), # 3 days
("Blooming", 7 * 24 * 60), # 1 week
]
# Colors
WHITE = (255, 255, 255)
BG = (238, 245, 239)
TEXT = (34, 40, 49)
CARD = (245, 250, 245)
GREEN = (72, 157, 77)
YELLOW = (240, 190, 40)
RED = (220, 76, 70)
BLUE = (66, 135, 245)
BROWN = (99, 68, 48)
SOIL = (84, 59, 42)
GREY = (200, 205, 210)
pygame.init()
pygame.display.set_caption("Virtual Plant Care Simulator 🌱")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
FONT = pygame.font.SysFont("consolas", 18)
FONT_BIG = pygame.font.SysFont("consolas", 24, bold=True)
# -------------------------
# Helpers
# -------------------------
def clamp(v, lo=0.0, hi=100.0):
return max(lo, min(hi, v))
def now_iso():
return datetime.now().isoformat()
def load_state():
try:
with open(SAVE_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# basic sanity defaults
data.setdefault("name", "Leafy")
data.setdefault("age_minutes", 0.0)
data.setdefault("moisture", 70.0)
data.setdefault("nutrients", 60.0)
data.setdefault("health", 80.0)
data.setdefault("alive", True)
data.setdefault("last_update_iso", now_iso())
data.setdefault("turbo", False)
return data
except FileNotFoundError:
return {
"name": "Leafy",
"age_minutes": 0.0,
"moisture": 70.0,
"nutrients": 60.0,
"health": 80.0,
"alive": True,
"last_update_iso": now_iso(),
"turbo": False
}
def save_state(state):
with open(SAVE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
def stage_from_age(age_min):
current = STAGES[0][0]
for name, threshold in STAGES:
if age_min >= threshold:
current = name
else:
break
return current
def draw_bar(surface, x, y, w, h, pct, color, label):
pygame.draw.rect(surface, GREY, (x, y, w, h), border_radius=6)
inner_w = int(w * clamp(pct/100.0, 0, 1))
pygame.draw.rect(surface, color, (x, y, inner_w, h), border_radius=6)
txt = FONT.render(f"{label}: {int(pct)}%", True, TEXT)
surface.blit(txt, (x + 8, y + h//2 - txt.get_height()//2))
def draw_button(surface, rect, text, active=True):
col = CARD if active else (230,230,230)
pygame.draw.rect(surface, col, rect, border_radius=8)
pygame.draw.rect(surface, GREY, rect, width=1, border_radius=8)
t = FONT.render(text, True, TEXT if active else (140,140,140))
surface.blit(t, (rect.x + rect.w//2 - t.get_width()//2,
rect.y + rect.h//2 - t.get_height()//2))
def nice_time(minutes):
d = int(minutes // (24*60))
h = int((minutes % (24*60)) // 60)
m = int(minutes % 60)
parts = []
if d: parts.append(f"{d}d")
if h: parts.append(f"{h}h")
parts.append(f"{m}m")
return " ".join(parts)
# -------------------------
# Plant rendering
# -------------------------
def draw_pot(surface, cx, base_y):
pot_w = 160
pot_h = 70
rim_h = 18
# pot body
pygame.draw.rect(surface, BROWN, (cx - pot_w//2, base_y - pot_h, pot_w, pot_h), border_radius=10)
# soil
pygame.draw.rect(surface, SOIL, (cx - pot_w//2 + 10, base_y - pot_h + rim_h, pot_w - 20, pot_h - rim_h - 6), border_radius=8)
# rim
pygame.draw.rect(surface, (120, 84, 60), (cx - pot_w//2, base_y - pot_h - 6, pot_w, rim_h), border_radius=8)
def draw_plant(surface, cx, base_y, age_min, health, moisture, nutrients, t):
"""
Procedural plant:
- Height scales with age & health
- Stem sways using sin wave
- Leaves hue via moisture/nutrients
"""
# growth factor
stage = stage_from_age(age_min)
stage_index = [s[0] for s in STAGES].index(stage)
height = 40 + stage_index * 35 + (health/100.0) * 30 # 40..~220
# sway
sway = math.sin(t * 2.0) * 8 # px
top_x = cx + sway
top_y = base_y - height
# stem
points = []
segments = 8
for i in range(segments+1):
y = base_y - (height * i / segments)
x = cx + math.sin(t*2 + i*0.6) * (8 * (i/segments))
points.append((x, y))
# draw stem as polyline (thicker at base)
for i in range(len(points)-1):
w = int(8 - 6*(i/segments))
pygame.draw.line(surface, (46, 120, 52), points[i], points[i+1], max(2, w))
# leaf color based on care
care_score = (moisture/100.0 + nutrients/100.0 + health/100.0) / 3.0
leaf_col = (
int(60 + 120 * care_score), # R
int(120 + 120 * care_score), # G
int(60 + 80 * care_score) # B
)
# leaves along stem
for i in range(2, segments):
px, py = points[i]
lr = 18 + 6*(i/segments)
angle = math.sin(t*3 + i) * 0.5 + (1 if i%2==0 else -1)*0.6
# left leaf
leaf_poly(surface, px, py, lr, angle, leaf_col)
# right leaf
leaf_poly(surface, px, py, lr, -angle, leaf_col)
# bud/flower at top for late stages
if stage in ("Mature", "Blooming"):
r = 10 if stage == "Mature" else 14 + 4*math.sin(t*4)
pygame.draw.circle(surface, (240, 110, 130), (int(points[-1][0]), int(points[-1][1])-8), int(abs(r)))
def leaf_poly(surface, x, y, r, angle, col):
# Draw a simple rotated leaf (ellipse-ish polygon)
pts = []
steps = 10
for i in range(steps+1):
theta = -math.pi/2 + math.pi * (i/steps)
px = x + r * math.cos(theta) * 0.4
py = y + r * math.sin(theta)
# rotate around (x,y)
rx = x + (px - x) * math.cos(angle) - (py - y) * math.sin(angle)
ry = y + (px - x) * math.sin(angle) + (py - y) * math.cos(angle)
pts.append((rx, ry))
pygame.draw.polygon(surface, col, pts)
# -------------------------
# Update logic
# -------------------------
def update_state(state, dt_minutes):
if not state["alive"]:
return
# age grows faster when healthy
health_factor = 0.5 + (state["health"]/100.0) * 0.5 # 0.5..1.0
state["age_minutes"] += dt_minutes * health_factor
# resource decay
state["moisture"] = clamp(state["moisture"] - MOISTURE_DECAY * dt_minutes)
state["nutrients"] = clamp(state["nutrients"] - NUTRIENT_DECAY * dt_minutes)
# health dynamics
good_care = (state["moisture"] >= GOOD_MOISTURE) and (state["nutrients"] >= GOOD_NUTRIENTS)
low_care = (state["moisture"] < LOW_MOISTURE) or (state["nutrients"] < LOW_NUTRIENTS)
if good_care:
state["health"] = clamp(state["health"] + HEALTH_INC_RATE * dt_minutes)
if low_care:
penalty = HEALTH_DEC_RATE * dt_minutes
# severe dehydration hurts more
if state["moisture"] <= 0.1:
penalty += HEALTH_SEVERE_PENALTY * dt_minutes
state["health"] = clamp(state["health"] - penalty)
if state["health"] <= 0.0:
state["alive"] = False
def water(state):
if not state["alive"]: return
state["moisture"] = clamp(state["moisture"] + WATER_AMOUNT)
def fertilize(state):
if not state["alive"]: return
state["nutrients"] = clamp(state["nutrients"] + FERTILIZER_AMOUNT)
def revive(state):
# Soft revive to keep testing
state["alive"] = True
state["health"] = max(state["health"], 40.0)
state["moisture"] = max(state["moisture"], 40.0)
# -------------------------
# UI setup
# -------------------------
BUTTONS = {
"water": pygame.Rect(40, 460, 140, 36),
"fertilize": pygame.Rect(200, 460, 140, 36),
"save": pygame.Rect(360, 460, 120, 36),
"turbo": pygame.Rect(500, 460, 160, 36),
"revive": pygame.Rect(40, 510, 120, 34),
"new": pygame.Rect(180, 510, 120, 34),
"quit": pygame.Rect(320, 510, 120, 34),
}
def reset_new_plant():
return {
"name": "Leafy",
"age_minutes": 0.0,
"moisture": 70.0,
"nutrients": 60.0,
"health": 80.0,
"alive": True,
"last_update_iso": now_iso(),
"turbo": False
}
# -------------------------
# Main loop
# -------------------------
def main():
state = load_state()
last_real = time.time()
running = True
while running:
real_now = time.time()
real_dt = real_now - last_real
last_real = real_now
# Time scale
speed = TIME_SCALE_MIN_PER_SEC * (TURBO_MULTIPLIER if state.get("turbo") else 1.0)
dt_minutes = real_dt * speed
# Update plant based on dt
update_state(state, dt_minutes)
# Draw
screen.fill(BG)
# Left info card
pygame.draw.rect(screen, CARD, (24, 24, 280, 410), border_radius=12)
pygame.draw.rect(screen, GREY, (24, 24, 280, 410), width=1, border_radius=12)
title = FONT_BIG.render("Plant Status", True, TEXT)
screen.blit(title, (40, 36))
# Bars
draw_bar(screen, 40, 80, 240, 20, state["health"], GREEN if state["health"] >= 50 else YELLOW if state["health"] >= 25 else RED, "Health")
draw_bar(screen, 40, 120, 240, 20, state["moisture"], BLUE if state["moisture"] >= 40 else YELLOW if state["moisture"] >= 20 else RED, "Moisture")
draw_bar(screen, 40, 160, 240, 20, state["nutrients"], GREEN if state["nutrients"] >= 40 else YELLOW if state["nutrients"] >= 20 else RED, "Nutrients")
# Text stats
lines = [
f"Name: {state['name']}",
f"Stage: {stage_from_age(state['age_minutes'])}",
f"Age: {nice_time(state['age_minutes'])}",
f"Alive: {'Yes' if state['alive'] else 'No'}",
f"Speed: {'Turbo' if state.get('turbo') else 'Normal'} (x{int(TURBO_MULTIPLIER) if state.get('turbo') else 1})",
"",
"Tips:",
"- Keep moisture & nutrients ≥ 50.",
"- Turbo to fast-forward growth.",
"- Save to keep progress.",
"- Revive for testing if it dies."
]
y = 200
for ln in lines:
txt = FONT.render(ln, True, TEXT)
screen.blit(txt, (40, y))
y += 22
# Plant area (right)
pygame.draw.rect(screen, CARD, (324, 24, 372, 410), border_radius=12)
pygame.draw.rect(screen, GREY, (324, 24, 372, 410), width=1, border_radius=12)
arena = pygame.Rect(324, 24, 372, 410)
cx = arena.x + arena.w//2
base_y = arena.y + arena.h - 40
# Ground line
pygame.draw.line(screen, (180, 180, 180), (arena.x + 20, base_y), (arena.right - 20, base_y), 2)
# Draw pot & plant
draw_pot(screen, cx, base_y)
t = pygame.time.get_ticks() / 1000.0
draw_plant(screen, cx, base_y, state["age_minutes"], state["health"], state["moisture"], state["nutrients"], t)
# Buttons
draw_button(screen, BUTTONS["water"], "💧 Water", active=state["alive"])
draw_button(screen, BUTTONS["fertilize"], "🧪 Fertilize", active=state["alive"])
draw_button(screen, BUTTONS["save"], "💾 Save")
draw_button(screen, BUTTONS["turbo"], f"⚡ Turbo: {'ON' if state.get('turbo') else 'OFF'}")
draw_button(screen, BUTTONS["revive"], "❤️ Revive")
draw_button(screen, BUTTONS["new"], "🌱 New Plant")
draw_button(screen, BUTTONS["quit"], "🚪 Quit")
# Warning labels
warn_y = 430
if state["moisture"] < LOW_MOISTURE:
wtxt = FONT.render("Low Moisture! Water me 💧", True, BLUE)
screen.blit(wtxt, (40, warn_y)); warn_y += 22
if state["nutrients"] < LOW_NUTRIENTS:
wtxt = FONT.render("Low Nutrients! Fertilize 🧪", True, (150, 100, 20))
screen.blit(wtxt, (40, warn_y)); warn_y += 22
if not state["alive"]:
wtxt = FONT_BIG.render("The plant has died. Try Revive or New Plant.", True, RED)
screen.blit(wtxt, (40, warn_y))
pygame.display.flip()
# Events
for event in pygame.event.get():
if event.type == pygame.QUIT:
save_state(state)
running = False
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mx, my = event.pos
if BUTTONS["water"].collidepoint(mx, my) and state["alive"]:
water(state)
elif BUTTONS["fertilize"].collidepoint(mx, my) and state["alive"]:
fertilize(state)
elif BUTTONS["save"].collidepoint(mx, my):
save_state(state)
elif BUTTONS["turbo"].collidepoint(mx, my):
state["turbo"] = not state.get("turbo", False)
elif BUTTONS["revive"].collidepoint(mx, my):
revive(state)
elif BUTTONS["new"].collidepoint(mx, my):
state = reset_new_plant()
elif BUTTONS["quit"].collidepoint(mx, my):
save_state(state)
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_w and state["alive"]:
water(state)
elif event.key == pygame.K_f and state["alive"]:
fertilize(state)
elif event.key == pygame.K_s:
save_state(state)
elif event.key == pygame.K_t:
state["turbo"] = not state.get("turbo", False)
elif event.key == pygame.K_r:
revive(state)
elif event.key == pygame.K_n:
state = reset_new_plant()
elif event.key == pygame.K_ESCAPE:
save_state(state)
running = False
clock.tick(FPS)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
No comments:
Post a Comment