Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
Nate Smith | 49ae1125d3 | |
Nate Smith | df01d7397c | |
Nate Smith | ccc08316fe | |
Noelle Leigh | 43333db825 | |
Noelle Leigh | c6aed375da | |
Marcos Marado | 1fffd41783 |
42
botany.py
42
botany.py
|
@ -46,46 +46,6 @@ from plant import Plant
|
||||||
# - the status display: score and plant description
|
# - the status display: score and plant description
|
||||||
# - the infow window. updated by visit/garden/instructions/look
|
# - the infow window. updated by visit/garden/instructions/look
|
||||||
|
|
||||||
# thoughts on storage
|
|
||||||
|
|
||||||
# Right now, plants are stored in a few ways:
|
|
||||||
# - as a row in the garden db for displaying when users choose to view a garden. this can be thought of as a cache over the plant files in individual user's home .botany dirs. this is a sqlite3 file kept in the source directory for botany's code.
|
|
||||||
# - as a json file local to ~/.botany; this is, afaict, for people to use as a kind of API?
|
|
||||||
# - as a .dat file--a pickled representation of that plant--in ~/.botany. this is the actual savefile for a plant.
|
|
||||||
#
|
|
||||||
# the fundmental pieces of data for a plant are:
|
|
||||||
# - birth timestamp
|
|
||||||
# - last water timestamp
|
|
||||||
# - generation
|
|
||||||
# - mutation
|
|
||||||
#
|
|
||||||
# everything else is either flavor (art, adjectives, rarity) or can be derived
|
|
||||||
# (alive/dead, stage, growth rate, water level)
|
|
||||||
#
|
|
||||||
# the visitor system allows for other people to touch the last water timestamp
|
|
||||||
# which is an interesting permissions problem.
|
|
||||||
#
|
|
||||||
# it's tempting to re-imagine all of this as something centralized: a shared
|
|
||||||
# sqlite3 db tracks 100% of information on all plants and on visitation. This
|
|
||||||
# would simplify a lot about botany. I think there is something romantic about
|
|
||||||
# having a garden/plant file in one's home directory. This also reduces the
|
|
||||||
# likelihood of a centralized error wiping out plant data.
|
|
||||||
#
|
|
||||||
# So can a new botany be designed that
|
|
||||||
# - keeps plant files distributed
|
|
||||||
# - allows for visitation
|
|
||||||
# - can render an overview of a system's garden
|
|
||||||
# - can handle changes to a plant when botany isn't itself running
|
|
||||||
# - can handle changes to a plant from out-of-band (eg visitor) when botany is running
|
|
||||||
#
|
|
||||||
# that minimizes the chances for race conditions and reduces the redundancy of
|
|
||||||
# plant serialization?
|
|
||||||
#
|
|
||||||
# reduce down to a single game loop
|
|
||||||
# start by filling up .botany (json or sqlite3?) and consider garden generalization as an out of band task.
|
|
||||||
# create a world-writable sqlite3 db for visitors to write to
|
|
||||||
|
|
||||||
|
|
||||||
class DataManager(object):
|
class DataManager(object):
|
||||||
# handles user data, puts a .botany dir in user's home dir (OSX/Linux)
|
# handles user data, puts a .botany dir in user's home dir (OSX/Linux)
|
||||||
# handles shared data with sqlite db
|
# handles shared data with sqlite db
|
||||||
|
@ -121,7 +81,7 @@ class DataManager(object):
|
||||||
|
|
||||||
def check_plant(self):
|
def check_plant(self):
|
||||||
# check for existing save file
|
# check for existing save file
|
||||||
if os.path.isfile(self.savefile_path):
|
if os.path.isfile(self.savefile_path) and os.path.getsize(self.savefile_path) > 0:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
404
main.py
404
main.py
|
@ -1,404 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
from enum import StrEnum
|
|
||||||
import random
|
|
||||||
import curses
|
|
||||||
import datetime
|
|
||||||
from os import path
|
|
||||||
from getpass import getuser
|
|
||||||
import os
|
|
||||||
from datetime import timezone
|
|
||||||
import sqlite3
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
from time import sleep
|
|
||||||
from typing import Optional, Tuple, TypeVar
|
|
||||||
|
|
||||||
BOTANY_DIR = ".botany"
|
|
||||||
MIN_SCREEN_WIDTH = 70
|
|
||||||
MIN_SCREEN_HEIGHT = 20
|
|
||||||
INTERVAL = 1
|
|
||||||
|
|
||||||
dt = datetime.datetime
|
|
||||||
|
|
||||||
def now() -> dt:
|
|
||||||
return dt.now(timezone.utc)
|
|
||||||
|
|
||||||
# flavor dict keys
|
|
||||||
# - color
|
|
||||||
# - rarity
|
|
||||||
# - species
|
|
||||||
# - mutation
|
|
||||||
PLOT_SCHEMA = """
|
|
||||||
CREATE TABLE IF NOT EXISTS plot (
|
|
||||||
-- Each row is a plant
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
|
|
||||||
watered TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
|
|
||||||
generation INTEGER,
|
|
||||||
dead INTEGER,
|
|
||||||
flavor JSON
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
VISITORS_SCHEMA = """
|
|
||||||
CREATE TABLE IF NOT EXISTS visitors (
|
|
||||||
-- Each row is a visit from another user
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT,
|
|
||||||
at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime'))
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO code for generating a global garden database (finds all most recent plants for users on system and fills a sqlite3 db with computed values)
|
|
||||||
|
|
||||||
def mkdir(p: str) -> Optional[Exception]:
|
|
||||||
if not path.isdir(p):
|
|
||||||
try:
|
|
||||||
os.makedirs(p)
|
|
||||||
except Exception as e:
|
|
||||||
return Exception(f"failed to create {p}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def mkdb(p: str, sql: str) -> Optional[Exception]:
|
|
||||||
try:
|
|
||||||
conn = sqlite3.connect(p)
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute(sql)
|
|
||||||
conn.close()
|
|
||||||
except Exception as e:
|
|
||||||
return Exception(f"failed to initialize {p}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def conndb(p: str) -> Tuple[Optional[sqlite3.Connection], Optional[Exception]]:
|
|
||||||
try:
|
|
||||||
return sqlite3.connect(p), None
|
|
||||||
except Exception as e:
|
|
||||||
return None, Exception(f"could not connect to {p}: {e}")
|
|
||||||
|
|
||||||
def setup(bdir: str, plotdb_path: str, visitordb_path: str) -> Optional[Exception]:
|
|
||||||
e = mkdir(bdir)
|
|
||||||
if e is not None:
|
|
||||||
return e
|
|
||||||
|
|
||||||
dbdir = path.join(bdir, "db")
|
|
||||||
e = mkdir(dbdir)
|
|
||||||
if e is not None:
|
|
||||||
return e
|
|
||||||
|
|
||||||
e = mkdb(plotdb_path, PLOT_SCHEMA)
|
|
||||||
if e is not None:
|
|
||||||
return e
|
|
||||||
|
|
||||||
e = mkdb(visitordb_path, VISITORS_SCHEMA)
|
|
||||||
if e is not None:
|
|
||||||
return e
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
class UI:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.quitting = False
|
|
||||||
self.pwin = curses.initscr()
|
|
||||||
self.menuwin = curses.newwin(10, 30, 0, 0)
|
|
||||||
self.plantwin = curses.newwin(30, 40, 0, 31)
|
|
||||||
self.scorewin = curses.newwin(2, 30, 15, 2)
|
|
||||||
# TODO info area (is this where prompt is rendered?)
|
|
||||||
|
|
||||||
self.menu_opts = [
|
|
||||||
"water",
|
|
||||||
"look",
|
|
||||||
"garden",
|
|
||||||
"visit",
|
|
||||||
"instructions",
|
|
||||||
"quit"]
|
|
||||||
|
|
||||||
self.selected_opt = 0
|
|
||||||
|
|
||||||
self.pwin.keypad(True)
|
|
||||||
curses.noecho()
|
|
||||||
curses.raw()
|
|
||||||
if curses.has_colors():
|
|
||||||
curses.start_color()
|
|
||||||
try:
|
|
||||||
curses.curs_set(0)
|
|
||||||
except curses.error:
|
|
||||||
# Not all terminals support this functionality.
|
|
||||||
# When the error is ignored the screen will be slightly uglier but functional
|
|
||||||
# so we ignore this error for terminal compatibility.
|
|
||||||
pass
|
|
||||||
|
|
||||||
if curses.COLS < MIN_SCREEN_WIDTH:
|
|
||||||
raise Exception("the terminal window is too narrow")
|
|
||||||
if curses.LINES < MIN_SCREEN_HEIGHT:
|
|
||||||
raise Exception("the terminal window is too short")
|
|
||||||
|
|
||||||
def quit(self) -> None:
|
|
||||||
self.quitting = True
|
|
||||||
|
|
||||||
def handle_input(self) -> None:
|
|
||||||
while True:
|
|
||||||
c = self.pwin.getch()
|
|
||||||
if c == -1 or c == ord("q") or c == ord("x") or c == 27:
|
|
||||||
self.quit()
|
|
||||||
break
|
|
||||||
if c == curses.KEY_DOWN or c == ord("j"):
|
|
||||||
self.selected_opt += 1
|
|
||||||
self.selected_opt %= len(self.menu_opts)
|
|
||||||
self.draw_menu()
|
|
||||||
if c == curses.KEY_UP or c == ord("k"):
|
|
||||||
self.selected_opt -= 1
|
|
||||||
if self.selected_opt < 0:
|
|
||||||
self.selected_opt = 0
|
|
||||||
self.draw_menu()
|
|
||||||
|
|
||||||
def draw_menu(self) -> None:
|
|
||||||
# TODO water gauge
|
|
||||||
self.menuwin.addstr(1, 2, " botany ", curses.A_STANDOUT)
|
|
||||||
self.menuwin.addstr(3, 2, "options", curses.A_BOLD)
|
|
||||||
x = 0
|
|
||||||
for o in self.menu_opts:
|
|
||||||
style = curses.A_NORMAL
|
|
||||||
|
|
||||||
if x == self.selected_opt:
|
|
||||||
style = curses.A_STANDOUT
|
|
||||||
|
|
||||||
self.menuwin.addstr(4 + x, 4, f"{x+1} - {o}", style)
|
|
||||||
|
|
||||||
x += 1
|
|
||||||
self.menuwin.refresh()
|
|
||||||
|
|
||||||
def draw(self) -> None:
|
|
||||||
self.draw_menu()
|
|
||||||
# TODO draw score
|
|
||||||
# TODO info window
|
|
||||||
# Draw plant
|
|
||||||
# TODO actually do
|
|
||||||
# TODO make conditional on plant ascii actually changing
|
|
||||||
lol = "TODO plant ascii"
|
|
||||||
plant = ""
|
|
||||||
x = 0
|
|
||||||
while x < len(lol):
|
|
||||||
flip = random.randint(0, 1)
|
|
||||||
if flip == 0:
|
|
||||||
plant += lol[x]
|
|
||||||
else:
|
|
||||||
plant += lol[x].upper()
|
|
||||||
x += 1
|
|
||||||
|
|
||||||
self.plantwin.addstr(0,0, plant, curses.A_STANDOUT)
|
|
||||||
self.plantwin.refresh()
|
|
||||||
|
|
||||||
class PlantStage(StrEnum):
|
|
||||||
SEED = "seed"
|
|
||||||
SEEDLING = "seedling"
|
|
||||||
YOUNG = "young"
|
|
||||||
MATURE = "mature"
|
|
||||||
FLOWERING = "flowering"
|
|
||||||
SEEDBEARING = "seed-bearing"
|
|
||||||
|
|
||||||
class PlantColor(StrEnum):
|
|
||||||
RED = "red"
|
|
||||||
ORANGE = "orange"
|
|
||||||
YELLOW = "yellow"
|
|
||||||
GREEN = "green"
|
|
||||||
BLUE = "blue"
|
|
||||||
INDIGO = "indigo"
|
|
||||||
VIOLET = "violet"
|
|
||||||
WHITE = "white"
|
|
||||||
BLACK = "black"
|
|
||||||
GOLD = "gold"
|
|
||||||
RAINBOW = "rainbow"
|
|
||||||
|
|
||||||
class PlantRarity(StrEnum):
|
|
||||||
COMMON = "common"
|
|
||||||
UNCOMMON = "uncommon"
|
|
||||||
RARE = "rare"
|
|
||||||
LEGENDARY = "legendary"
|
|
||||||
GODLY = "godly"
|
|
||||||
|
|
||||||
class PlantSpecies(StrEnum):
|
|
||||||
POPPY = "poppy"
|
|
||||||
CACTUS = "cactus"
|
|
||||||
ALOE = "aloe"
|
|
||||||
FLYTRAP = "venus flytrap"
|
|
||||||
JADE = "jade plant"
|
|
||||||
FERN = "fern"
|
|
||||||
DAFFODIL = "daffodil"
|
|
||||||
SUNFLOWER = "sunflower"
|
|
||||||
BAOBAB = "baobab"
|
|
||||||
LITHOPS = "lithops"
|
|
||||||
HEMP = "hemp"
|
|
||||||
PANSY = "pansy"
|
|
||||||
IRIS = "iris"
|
|
||||||
AGAVE = "agave"
|
|
||||||
FICUS = "ficus",
|
|
||||||
MOSS = "moss",
|
|
||||||
SAGE = "sage",
|
|
||||||
SNAPDRAGON = "snapdragon"
|
|
||||||
COLUMBINE = "columbine"
|
|
||||||
BRUGMANSIA = "brugmansia"
|
|
||||||
PALM = "palm"
|
|
||||||
PACHYPODIUM = "pachypodium"
|
|
||||||
|
|
||||||
class PlantMutation(StrEnum):
|
|
||||||
HUMMING = 'humming',
|
|
||||||
NOXIOUS = 'noxious',
|
|
||||||
VORPAL = 'vorpal',
|
|
||||||
GLOWING = 'glowing',
|
|
||||||
ELECTRIC = 'electric',
|
|
||||||
ICY = 'icy',
|
|
||||||
FLAMING = 'flaming',
|
|
||||||
PSYCHIC = 'psychic',
|
|
||||||
SCREAMING = 'screaming',
|
|
||||||
CHAOTIC = 'chaotic',
|
|
||||||
HISSING = 'hissing',
|
|
||||||
GELATINOUS = 'gelatinous',
|
|
||||||
DEFORMED = 'deformed',
|
|
||||||
SHAGGY = 'shaggy',
|
|
||||||
SCALY = 'scaly',
|
|
||||||
DEPRESSED = 'depressed',
|
|
||||||
ANXIOUS = 'anxious',
|
|
||||||
METALLIC = 'metallic',
|
|
||||||
GLOSSY = 'glossy',
|
|
||||||
PSYCHEDELIC = 'psychedelic',
|
|
||||||
BONSAI = 'bonsai',
|
|
||||||
FOAMY = 'foamy',
|
|
||||||
SINGING = 'singing',
|
|
||||||
FRACTAL = 'fractal',
|
|
||||||
CRUNCHY = 'crunchy',
|
|
||||||
GOTH = 'goth',
|
|
||||||
OOZING = 'oozing',
|
|
||||||
STINKY = 'stinky',
|
|
||||||
AROMATIC = 'aromatic',
|
|
||||||
JUICY = 'juicy',
|
|
||||||
SMUG = 'smug',
|
|
||||||
VIBRATING = 'vibrating',
|
|
||||||
LITHE = 'lithe',
|
|
||||||
CHALKY = 'chalky',
|
|
||||||
NAIVE = 'naive',
|
|
||||||
ERSATZ = 'ersatz',
|
|
||||||
DISCO = 'disco',
|
|
||||||
LEVITATING = 'levitating',
|
|
||||||
COLOSSAL = 'colossal',
|
|
||||||
LUMINOUS = 'luminous',
|
|
||||||
COSMIC = 'cosmic',
|
|
||||||
ETHEREAL = 'ethereal',
|
|
||||||
CURSED = 'cursed',
|
|
||||||
BUFF = 'buff',
|
|
||||||
NARCOTIC = 'narcotic',
|
|
||||||
GNULINUX = 'gnu/linux',
|
|
||||||
ABRAXAN = 'abraxan', # rip dear friend
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Plant:
|
|
||||||
created: dt
|
|
||||||
watered: dt
|
|
||||||
color: PlantColor
|
|
||||||
rarity: PlantRarity
|
|
||||||
species: PlantSpecies
|
|
||||||
mutation: PlantMutation
|
|
||||||
generation: int
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stage(self) -> PlantStage:
|
|
||||||
# TODO calculate based on aged
|
|
||||||
return PlantStage.SEED
|
|
||||||
|
|
||||||
@property
|
|
||||||
def score(self) -> int:
|
|
||||||
# TODO calculate based on things
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dead(self) -> bool:
|
|
||||||
# TODO calculate based on watered
|
|
||||||
return False
|
|
||||||
|
|
||||||
def mkplant() -> Plant:
|
|
||||||
# TODO randomize properties as needed
|
|
||||||
return Plant()
|
|
||||||
|
|
||||||
def get_or_create_plant(conn: sqlite3.Connection) -> Tuple[Plant, Optional[Exception]]:
|
|
||||||
p = Plant()
|
|
||||||
err = None
|
|
||||||
try:
|
|
||||||
c = conn.cursor()
|
|
||||||
sql = "SELECT created, watered, generation, dead, flavor FROM plot WHERE dead = 0"
|
|
||||||
rows = cur.execute(sql).fetchall()
|
|
||||||
if len(rows) > 1:
|
|
||||||
return p, Exception("that's my purse. i don't know you")
|
|
||||||
if len(rows) == 0:
|
|
||||||
p = mkplant()
|
|
||||||
sql = """
|
|
||||||
INSERT INTO plot (watered, generation, dead, flavor) VALUES (
|
|
||||||
?, 1, 0, ?
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
err = e
|
|
||||||
finally:
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
return p, err
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> Optional[Exception]:
|
|
||||||
username = getuser() # TODO unused
|
|
||||||
|
|
||||||
bdir = path.expanduser(path.join("~", BOTANY_DIR))
|
|
||||||
plotdb_path = path.join(bdir, "db/plot.db")
|
|
||||||
visitordb_path = path.join(bdir, "db/visitors.db")
|
|
||||||
|
|
||||||
e = setup(bdir, plotdb_path, visitordb_path)
|
|
||||||
if e is not None:
|
|
||||||
return e
|
|
||||||
|
|
||||||
# time to think about db connections. I don't think there is much harm in
|
|
||||||
# keeping them open, so let's open them.
|
|
||||||
|
|
||||||
plotdb, e = conndb(plotdb_path)
|
|
||||||
if e is not None:
|
|
||||||
return e
|
|
||||||
|
|
||||||
plant, e = get_or_create_plant(plotdb)
|
|
||||||
if e is not None:
|
|
||||||
return Exception(f"could not find or make a plant: {e}")
|
|
||||||
# If the latest plant is dead, the user can choose to create a new plant with harvest option.
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
try:
|
|
||||||
ui = UI()
|
|
||||||
except Exception as e:
|
|
||||||
return Exception(f"could not initialize UI: {e}")
|
|
||||||
|
|
||||||
ithread = threading.Thread(target=ui.handle_input, args=())
|
|
||||||
ithread.start()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if ui.quitting:
|
|
||||||
break
|
|
||||||
|
|
||||||
# TODO get plant info from db
|
|
||||||
# TODO update in-memory representation of derived characteristics / plant info
|
|
||||||
ui.draw()
|
|
||||||
sleep(INTERVAL)
|
|
||||||
|
|
||||||
try:
|
|
||||||
curses.curs_set(2)
|
|
||||||
except curses.error:
|
|
||||||
# cursor not supported; just ignore
|
|
||||||
pass
|
|
||||||
curses.endwin()
|
|
||||||
os.system('clear')
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
ret = 0
|
|
||||||
e = main()
|
|
||||||
if e is not None:
|
|
||||||
print(e, file=sys.stderr)
|
|
||||||
ret = 1
|
|
||||||
sys.exit(ret)
|
|
|
@ -564,7 +564,7 @@ class CursedMenu(object):
|
||||||
# get plant description before printing
|
# get plant description before printing
|
||||||
output_string = self.get_plant_description(this_plant)
|
output_string = self.get_plant_description(this_plant)
|
||||||
growth_multiplier = 1 + (0.2 * (this_plant.generation-1))
|
growth_multiplier = 1 + (0.2 * (this_plant.generation-1))
|
||||||
output_string += "Generation: {}\nGrowth rate: {}x".format(self.plant.generation, growth_multiplier)
|
output_string += "Generation: {}\nGrowth rate: {:.1f}x".format(self.plant.generation, growth_multiplier)
|
||||||
self.draw_info_text(output_string)
|
self.draw_info_text(output_string)
|
||||||
self.infotoggle = 1
|
self.infotoggle = 1
|
||||||
else:
|
else:
|
||||||
|
|
3
plant.py
3
plant.py
|
@ -164,7 +164,7 @@ class Plant:
|
||||||
def rarity_check(self):
|
def rarity_check(self):
|
||||||
# Generate plant rarity
|
# Generate plant rarity
|
||||||
CONST_RARITY_MAX = 256.0
|
CONST_RARITY_MAX = 256.0
|
||||||
rare_seed = random.randint(1,CONST_RARITY_MAX)
|
rare_seed = random.randint(1,int(CONST_RARITY_MAX))
|
||||||
common_range = round((2.0/3)*CONST_RARITY_MAX)
|
common_range = round((2.0/3)*CONST_RARITY_MAX)
|
||||||
uncommon_range = round((2.0/3)*(CONST_RARITY_MAX-common_range))
|
uncommon_range = round((2.0/3)*(CONST_RARITY_MAX-common_range))
|
||||||
rare_range = round((2.0/3)*(CONST_RARITY_MAX-common_range-uncommon_range))
|
rare_range = round((2.0/3)*(CONST_RARITY_MAX-common_range-uncommon_range))
|
||||||
|
@ -311,7 +311,6 @@ class Plant:
|
||||||
self.write_lock = True
|
self.write_lock = True
|
||||||
self.kill_plant()
|
self.kill_plant()
|
||||||
while self.write_lock:
|
while self.write_lock:
|
||||||
# TODO is this the culprit for the intermittent full-runaway botany?
|
|
||||||
# Wait for garden writer to unlock
|
# Wait for garden writer to unlock
|
||||||
# garden db needs to update before allowing the user to reset
|
# garden db needs to update before allowing the user to reset
|
||||||
pass
|
pass
|
||||||
|
|
Loading…
Reference in New Issue