diff --git a/botany.py b/botany.py index 16d9537..6e2b8ee 100644 --- a/botany.py +++ b/botany.py @@ -3,7 +3,8 @@ import time import pickle import json import math -import os.path +import sys +import os import random import getpass import threading @@ -15,16 +16,11 @@ from menu_screen import * # development plan # build plant lifecycle just stepping through -# - What else should it do during life? growth alone is not all that -# interesting. # - how long should each stage last ? thinking realistic lmao # seed -> seedling -> sprout -> young plant -> mature plant -> flower -> # pollination -> fruit -> seeds # - TODO: pollination and end of life # -# interaction -# - look at plant, how do you feel? (also gets rid of pests) -# # events # - heatwave # - rain @@ -39,11 +35,11 @@ from menu_screen import * # - create rarer species by diff gens # - if neighbor plant dies, node will be removed from list # -# garden system -# - can plant your plant in the garden to start a new plant - # build ascii trees +# Make it fun to keep growing after seed level (what is reward for continuing +# instead of starting over? +# Reward for bringing plant to full life - second gen plant w/ more variety class Plant(object): # This is your plant! @@ -140,6 +136,9 @@ class Plant(object): def __init__(self, this_filename): # Constructor self.plant_id = str(uuid.uuid4()) + # TODO: change from debug + self.life_stages = (10, 20, 30, 40, 50) + # self.life_stages = (3600, (3600*24)*3, (3600*24)*10, (3600*24)*20, (3600*24)*30) self.stage = 0 self.mutation = 0 self.species = random.randint(0,len(self.species_dict)-1) @@ -148,6 +147,7 @@ class Plant(object): self.ticks = 0 self.age_formatted = "0" self.dead = False + self.write_lock = False self.owner = getpass.getuser() self.file_name = this_filename self.start_time = int(time.time()) @@ -157,9 +157,20 @@ class Plant(object): # self.watered_timestamp = int(time.time()) # debug self.watered_24h = False - def new_seed(self,this_filename): - # Creates life after death - self.__init__(this_filename) + def parse_plant(self): + # reads plant info (maybe want to reorg this into a different class + # with the reader dicts...) + output = "" + if self.stage >= 3: + output += self.rarity_dict[self.rarity] + " " + if self.mutation != 0: + output += self.mutation_dict[self.mutation] + " " + if self.stage >= 4: + output += self.color_dict[self.color] + " " + output += self.stage_dict[self.stage] + " " + if self.stage >= 2: + output += self.species_dict[self.species] + " " + return output.strip() def rarity_check(self): # Generate plant rarity @@ -189,23 +200,6 @@ class Plant(object): rarity = 4 return rarity - def growth(self): - # Increase plant growth stage - if self.stage < (len(self.stage_dict)-1): - self.stage += 1 - # do stage growth stuff - else: - # do stage 5 stuff (after fruiting) - 1==1 - - def water(self): - # Increase plant growth stage - # TODO: overwatering? if more than once a day it dies? - if not self.dead: - self.watered_timestamp = int(time.time()) - self.watered_24h = True - - def dead_check(self): time_delta_watered = int(time.time()) - self.watered_timestamp # if it has been >5 days since watering, sorry plant is dead :( @@ -213,9 +207,6 @@ class Plant(object): self.dead = True return self.dead - def kill_plant(self): - self.dead = True - def water_check(self): # if plant has been watered in 24h then it keeps growing # time_delta_watered is difference from now to last watered @@ -230,7 +221,7 @@ class Plant(object): # Create plant mutation # TODO: when out of debug this needs to be set to high number (1000 # even maybe) - CONST_MUTATION_RARITY = 10 # Increase this # to make mutation rarer (chance 1 out of x) + CONST_MUTATION_RARITY = 2000 # Increase this # to make mutation rarer (chance 1 out of x) mutation_seed = random.randint(1,CONST_MUTATION_RARITY) if mutation_seed == CONST_MUTATION_RARITY: # mutation gained! @@ -241,20 +232,39 @@ class Plant(object): else: return False - def parse_plant(self): - # reads plant info (maybe want to reorg this into a different class - # with the reader dicts...) - output = "" - if self.stage >= 3: - output += self.rarity_dict[self.rarity] + " " - if self.mutation != 0: - output += self.mutation_dict[self.mutation] + " " - if self.stage >= 4: - output += self.color_dict[self.color] + " " - output += self.stage_dict[self.stage] + " " - if self.stage >= 2: - output += self.species_dict[self.species] + " " - return output.strip() + def new_seed(self,this_filename): + # Creates life after death + self.__init__(this_filename) + + def growth(self): + # Increase plant growth stage + if self.stage < (len(self.stage_dict)-1): + self.stage += 1 + # do stage growth stuff + else: + # do stage 5 stuff (after fruiting) + 1==1 + + def water(self): + # Increase plant growth stage + if not self.dead: + self.watered_timestamp = int(time.time()) + self.watered_24h = True + + def start_over(self): + self.write_lock = True + self.kill_plant() + while self.write_lock: + # Wait for garden writer to unlock + 1==1 + if not self.write_lock: + self.new_seed(self.file_name) + + def kill_plant(self): + self.dead = True + + def unlock_new_creation(self): + self.write_lock = False def start_life(self): # runs life on a thread @@ -265,7 +275,7 @@ class Plant(object): def life(self): # I've created life :) # TODO: change out of debug - life_stages = (5, 15, 30, 45, 60) + # TODO: variable stages of life # day = 3600*24 # life_stages = (1*day, 2*day, 3*day, 4*day, 5*day) # leave this untouched bc it works for now @@ -275,7 +285,7 @@ class Plant(object): if self.watered_24h: self.ticks += 1 if self.stage < len(self.stage_dict)-1: - if self.ticks >= life_stages[self.stage]: + if self.ticks >= self.life_stages[self.stage]: self.growth() if self.mutate_check(): 1==1 @@ -299,6 +309,8 @@ class DataManager(object): def __init__(self): self.this_user = getpass.getuser() + # check if instance is already running + self.lock_file() # check for .botany dir in home try: os.makedirs(self.botany_dir) @@ -307,6 +319,19 @@ class DataManager(object): raise self.savefile_name = self.this_user + '_plant.dat' + def lock_file(self): + # Only allow one instance of game + pid = str(os.getpid()) + this_filename = "instance.lock" + self.pid_file_path = os.path.join(self.botany_dir,this_filename) + if os.path.isfile(self.pid_file_path): + print "botany already running, exiting. (pid %s)" % pid + sys.exit() + file(self.pid_file_path, 'w').write(pid) + + def clear_lock(self): + os.unlink(self.pid_file_path) + def check_plant(self): # check for existing save file if os.path.isfile(self.savefile_path): @@ -333,6 +358,7 @@ class DataManager(object): self.save_plant(this_plant) self.data_write_json(this_plant) self.garden_update(this_plant) + this_plant.unlock_new_creation() time.sleep(.1) def autosave(self, this_plant): @@ -379,7 +405,6 @@ class DataManager(object): def garden_update(self, this_plant): # garden is a dict of dicts # garden contains one entry for each plant id - age_formatted = self.plant_age_convert(this_plant) this_plant_id = this_plant.plant_id plant_info = { @@ -408,7 +433,7 @@ class DataManager(object): # if plant ticks for id is greater than current ticks of plant id else: current_plant_ticks = this_garden[this_plant_id]["score"] - if this_plant.ticks > current_plant_ticks: + if this_plant.ticks >= current_plant_ticks: this_garden[this_plant_id] = plant_info with open(self.garden_file_path, 'wb') as f: pickle.dump(this_garden, f, protocol=2) @@ -444,9 +469,6 @@ class DataManager(object): with open(json_file, 'w') as outfile: json.dump(plant_info, outfile) - # update leaderboard 'garden' for display in game - # also should be a pickle file bc... let's be honest ppl want to cheat - if __name__ == '__main__': my_data = DataManager() # if plant save file exists @@ -454,13 +476,13 @@ if __name__ == '__main__': my_plant = my_data.load_plant() # otherwise create new plant else: - #TODO: onboarding, select seed, select whatever else my_plant = Plant(my_data.savefile_path) my_data.data_write_json(my_plant) my_plant.start_life() my_data.start_threads(my_plant) -botany_menu = CursedMenu(my_plant,my_data.garden_file_path) + botany_menu = CursedMenu(my_plant,my_data.garden_file_path) botany_menu.show(["water","look","garden","instructions"], title=' botany ', subtitle='options') my_data.save_plant(my_plant) my_data.data_write_json(my_plant) my_data.garden_update(my_plant) + my_data.clear_lock() diff --git a/menu_screen.py b/menu_screen.py index 1e6b34a..f2c45f9 100644 --- a/menu_screen.py +++ b/menu_screen.py @@ -1,7 +1,8 @@ -import curses, os, traceback, threading, time, datetime, pickle +import curses, os, traceback, threading, time, datetime, pickle, operator, random class CursedMenu(object): #TODO: create a side panel with log of events..? + #TODO: name your plant '''A class which abstracts the horrors of building a curses-based menu system''' def __init__(self, this_plant, this_garden_file_path): '''Initialization''' @@ -18,14 +19,14 @@ class CursedMenu(object): self.plant_ticks = str(self.plant.ticks) self.exit = False self.instructiontoggle = False - #TODO: debugging - # self.gardenmenutoggle = True self.gardenmenutoggle = False + self.looktoggle = False self.maxy, self.maxx = self.screen.getmaxyx() # Highlighted and Normal line definitions curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) self.highlighted = curses.color_pair(1) self.normal = curses.A_NORMAL + # Threaded screen update for live changes screen_thread = threading.Thread(target=self.update_plant_live, args=()) screen_thread.daemon = True screen_thread.start() @@ -44,16 +45,18 @@ class CursedMenu(object): def update_options(self): # Makes sure you can get a new plant if it dies if self.plant.dead: - if "kill" in self.options: - self.options.remove("kill") - if "new" not in self.options: - self.options.insert(-1,"new") + # if "start over" in self.options: + # self.options.remove("start over") + if "start over" not in self.options: + self.options.insert(-1,"start over") else: # TODO: remove after debug or bury in settings - if "new" in self.options: - self.options.remove("new") - if "kill" not in self.options: - self.options.insert(-1,"kill") + if self.plant.stage == 5: + if "start over" not in self.options: + self.options.insert(-1,"start over") + else: + if "start over" in self.options: + self.options.remove("start over") def set_options(self, options): # Validates that the last option is "exit" @@ -61,6 +64,22 @@ class CursedMenu(object): options.append('exit') self.options = options + def draw(self): + # Draw the menu and lines + # TODO: this needs to either display the default menu screen or the + # garden/leaderboard thing based on self.gardenmenutoggle + # TODO: display refresh is hacky. Could be more precise + self.screen.refresh() + self.screen.border(0) + try: + self.draw_default() + self.screen.refresh() + except Exception as exception: + # Makes sure data is saved in event of a crash due to window resizing + self.screen.addstr(0,0,"Enlarge terminal!") + self.__exit__() + traceback.print_exc() + def draw_menu(self): # Actually draws the menu and handles branching request = "" @@ -81,6 +100,9 @@ class CursedMenu(object): clear_bar = " " * (int(self.maxx*2/3)) self.screen.addstr(2,2, self.title, curses.A_STANDOUT) # Title for this menu self.screen.addstr(4,2, self.subtitle, curses.A_BOLD) #Subtitle for this menu + # Clear menu on screen + for index in range(len(self.options)+1): + self.screen.addstr(5+index,4, clear_bar, curses.A_NORMAL) # Display all the menu items, showing the 'pos' item highlighted for index in range(len(self.options)): textstyle = self.normal @@ -104,62 +126,6 @@ class CursedMenu(object): self.screen.addstr(5,13, clear_bar, curses.A_NORMAL) self.screen.addstr(5,13, " - you can't water a dead plant :(", curses.A_NORMAL) - def format_garden_data(self,this_garden): - plant_table = "" - # TODO: include only live plants maybe - for plant_id in this_garden: - if this_garden[plant_id]: - if not this_garden[plant_id]["dead"]: - this_plant = this_garden[plant_id] - plant_table += this_plant["owner"] + " - " - plant_table += this_plant["age"] + " - " - plant_table += this_plant["description"] + " - " - plant_table += str(this_plant["score"]) + "\n" - return plant_table - - def draw_garden(self): - # Draws neighborhood - clear_bar = " " * (self.maxx-2) + "\n" - control_keys = [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT] - # load data - with open(self.garden_file_path, 'rb') as f: - this_garden = pickle.load(f) - # format data - if not self.gardenmenutoggle: - plant_table_formatted = self.format_garden_data(this_garden) - self.gardenmenutoggle = not self.gardenmenutoggle - else: - plant_table_formatted = "" - for plant in this_garden: - if not this_garden[plant]["dead"]: - plant_table_formatted += clear_bar - self.gardenmenutoggle = not self.gardenmenutoggle - - for y, line in enumerate(plant_table_formatted.splitlines(), 2): - self.screen.addstr(y+12, 2, line) - self.screen.refresh() - - def draw(self): - # Draw the menu and lines - # TODO: this needs to either display the default menu screen or the - # garden/leaderboard thing based on self.gardenmenutoggle - # TODO: display refresh is hacky. Could be more precise - self.screen.refresh() - self.screen.border(0) - # TODO: separate screen for garden menu, interactive - # if self.gardenmenutoggle: - # self.draw_garden() - # else: - # self.draw_default() - try: - self.draw_default() - self.screen.refresh() - except Exception as exception: - # Makes sure data is saved in event of a crash due to window resizing - self.screen.addstr(0,0,"Enlarge terminal!") - self.__exit__() - traceback.print_exc() - def update_plant_live(self): # Updates plant data on menu screen, live! # Will eventually use this to display ascii art... @@ -194,6 +160,167 @@ class CursedMenu(object): self.selected = self.selected % len(self.options) return + def format_garden_data(self,this_garden): + + plant_table = "" + for plant_id in this_garden: + if this_garden[plant_id]: + if not this_garden[plant_id]["dead"]: + this_plant = this_garden[plant_id] + plant_table += this_plant["owner"] + " - " + plant_table += this_plant["age"] + " - " + plant_table += str(this_plant["score"]) + " points - " + plant_table += this_plant["description"] + "\n" + return plant_table + + def draw_garden(self): + # Draws neighborhood + clear_bar = " " * (self.maxx-2) + "\n" + control_keys = [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT] + # load data + with open(self.garden_file_path, 'rb') as f: + this_garden = pickle.load(f) + # format data + if not self.gardenmenutoggle: + plant_table_formatted = self.format_garden_data(this_garden) + self.gardenmenutoggle = not self.gardenmenutoggle + else: + plant_table_formatted = clear_bar + for plant in this_garden: + if not this_garden[plant]["dead"]: + plant_table_formatted += clear_bar + self.gardenmenutoggle = not self.gardenmenutoggle + + for y, line in enumerate(plant_table_formatted.splitlines(), 2): + self.screen.addstr(y+17, 2, line) + self.screen.refresh() + + def get_plant_description(self, this_plant): + 1==1 + output_text = "" + this_species = this_plant.species_dict[this_plant.species] + this_color = this_plant.color_dict[this_plant.color] + this_stage = this_plant.stage + + stage_descriptions = { + 0:[ + "You're excited about your new seed.", + "You wonder what kind of plant your seed will grow into.", + "You're ready for a new start with this plant.", + "You're tired of waiting for your seed to grow.", + "You wish your seed could tell you what it needs.", + ], + 1:[ + "The seedling fills you with hope.", + "You can make out a tiny leaf - or is that a thorn?", + "You can feel the seedling looking back at you.", + "You kiss your seedling good night.", + "You think about all the seedlings who came before it.", + "You and your seedling make a great team.", + ], + 2:[ + "The " + this_species + " makes you feel relaxed.", + "You sing a song to your " + this_species + ".", + "You quietly sit with your " + this_species + " for a few minutes.", + "Your " + this_species + " looks pretty good.", + "You play loud techno to your " + this_species + ".", + ], + 3:[ + "Your " + this_species + " is growing nicely!", + "You're proud of the dedication it took to grow your " + this_species + ".", + "The " + this_species + " looks good.", + "You think how good this " + this_species + " would look on steroids.", + "The buds of your " + this_species + " are about to bloom.", + ], + 4:[ + "The " + this_color + " flowers look nice on your " + this_species +"!", + "The " + this_color + " flowers have bloomed and fill you with desire.", + "The " + this_color + " flowers of your " + this_species + " remind you of your childhood.", + "The " + this_species + " has grown beautiful " + this_color + " flowers.", + "The " + this_color + " petals remind you of your favorite shirt.", + ], + 5:[ + "You fondly remember all of the time you spent caring for your " + this_species + ".", + "Your " + this_species + " looks old and wise.", + "Seed pods have grown on your " + this_species + ".", + "The " + this_species + " fills you with love.", + "The " + this_species + " reminds you of your first crush.", + ], + 99:[ + "You wish you had taken better care of your plant.", + "If only you had watered your plant more often..", + "Your plant is dead, there's always next time.", + "You cry over the withered leaves of your plant.", + "Your plant died. Maybe you need a fresh start.", + ], + } + # self.life_stages is tuple containing length of each stage + # (seed, seedling, young, mature, flowering) + # if stage == 0 == seed + if this_plant.dead: + this_stage = 99 + + this_stage_descriptions = stage_descriptions[this_stage] + description_num = random.randint(0,len(this_stage_descriptions) - 1) + if this_stage <= 4: + # Growth hint + if this_stage >= 1: + last_growth_at = this_plant.life_stages[this_stage - 1] + else: + last_growth_at = 0 + ticks_since_last = this_plant.ticks - last_growth_at + ticks_between_stage = this_plant.life_stages[this_stage] - last_growth_at + if ticks_since_last >= ticks_between_stage * 0.8: + output_text += "You notice your plant looks different.\n" + + output_text += this_stage_descriptions[description_num] + "\n" + + # issue 1 - referencing anything past 4 on life stages breaks + # issue 2 - 80% using plant ticks doesn't really work since it shifts + # each time. need to use the difference between 2 stages, and then + # plant ticks minus last stage + + if this_stage == 1: + species_options = [this_plant.species_dict[this_plant.species], + this_plant.species_dict[(this_plant.species+3) % len(this_plant.species_dict)], + this_plant.species_dict[(this_plant.species-3) % len(this_plant.species_dict)]] + random.shuffle(species_options) + plant_hint = "It could be a(n) " + species_options[0] + ", " + species_options[1] + ", or " + species_options[2] + output_text += plant_hint + ".\n" + + if this_stage == 2: + # TODO: more descriptive rarity + if this_plant.rarity >= 2: + rarity_hint = "You feel like your plant is special." + output_text += rarity_hint + ".\n" + + if this_stage == 3: + color_options = [this_plant.color_dict[this_plant.color], + this_plant.color_dict[(this_plant.color+3) % len(this_plant.color_dict)], + this_plant.color_dict[(this_plant.color-3) % len(this_plant.color_dict)]] + random.shuffle(color_options) + plant_hint = "You can see the first hints of " + color_options[0] + ", " + color_options[1] + ", or " + color_options[2] + output_text += plant_hint + ".\n" + + return output_text + + def draw_plant_description(self, this_plant): + clear_bar = " " * (self.maxx-2) + "\n" + control_keys = [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT] + # load data + # format data + if not self.looktoggle: + output_string = self.get_plant_description(this_plant) + self.looktoggle = not self.looktoggle + else: + output_string = clear_bar + output_string += clear_bar*3 + self.looktoggle = not self.looktoggle + + for y, line in enumerate(output_string.splitlines(), 2): + self.screen.addstr(y+12, 2, line) + self.screen.refresh() + def draw_instructions(self): if not self.instructiontoggle: instructions_txt = """welcome to botany. you've been given a seed @@ -222,12 +349,17 @@ available in the readme :) def handle_request(self, request): '''This is where you do things with the request''' if request == None: return - if request == "kill": - self.plant.kill_plant() - if request == "new": - self.plant.new_seed(self.plant.file_name) + if request == "start over": + self.plant.start_over() if request == "water": self.plant.water() + if request == "look": + # try: + self.draw_plant_description(self.plant) + # except Exception as exception: + # self.screen.addstr(0,0,"Enlarge terminal!") + # self.__exit__() + # traceback.print_exc() if request == "instructions": try: self.draw_instructions()