480 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			480 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from __future__ import division
 | |
| import time
 | |
| import pickle
 | |
| import json
 | |
| import math
 | |
| import sys
 | |
| import os
 | |
| import random
 | |
| import getpass
 | |
| import threading
 | |
| import errno
 | |
| import uuid
 | |
| import fcntl
 | |
| from operator import itemgetter
 | |
| from menu_screen import *
 | |
| 
 | |
| # development plan
 | |
| 
 | |
| # build plant lifecycle just stepping through
 | |
| #   - 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
 | |
| #
 | |
| # events
 | |
| # - heatwave
 | |
| # - rain
 | |
| # - bugs
 | |
| #
 | |
| # build multiplayer
 | |
| # neighborhood system
 | |
| # - create plant id (sort of like userid)
 | |
| # - list sorted by plantid that wraps so everybody has 2 neighbors :)
 | |
| # - can water neighbors plant once (have to choose which)
 | |
| # - pollination - seed is combination of your plant and neighbor plant
 | |
| #   - create rarer species by diff gens
 | |
| # - if neighbor plant dies, node will be removed from list
 | |
| #
 | |
| # 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!
 | |
|     stage_dict = {
 | |
|         0: 'seed',
 | |
|         1: 'seedling',
 | |
|         2: 'young',
 | |
|         3: 'mature',
 | |
|         4: 'flowering',
 | |
|         5: 'seed-bearing',
 | |
|     }
 | |
| 
 | |
|     color_dict = {
 | |
|         0: 'red',
 | |
|         1: 'orange',
 | |
|         2: 'yellow',
 | |
|         3: 'green',
 | |
|         4: 'blue',
 | |
|         5: 'indigo',
 | |
|         6: 'violet',
 | |
|         7: 'white',
 | |
|         8: 'black',
 | |
|         9: 'gold',
 | |
|         10: 'rainbow',
 | |
|     }
 | |
| 
 | |
|     rarity_dict = {
 | |
|         0: 'common',
 | |
|         1: 'uncommon',
 | |
|         2: 'rare',
 | |
|         3: 'legendary',
 | |
|         4: 'godly',
 | |
|     }
 | |
| 
 | |
|     species_dict = {
 | |
|         0: 'poppy',
 | |
|         1: 'cactus',
 | |
|         2: 'aloe',
 | |
|         3: 'venus flytrap',
 | |
|         4: 'jade plant',
 | |
|         5: 'fern',
 | |
|         6: 'daffodil',
 | |
|         7: 'sunflower',
 | |
|         8: 'baobab',
 | |
|         9: 'lithops',
 | |
|         10: 'cannabis',
 | |
|         11: 'pansy',
 | |
|         12: 'iris',
 | |
|     }
 | |
| 
 | |
|     mutation_dict = {
 | |
|         0: '',
 | |
|         1: 'humming',
 | |
|         2: 'noxious',
 | |
|         3: 'vorpal',
 | |
|         4: 'glowing',
 | |
|         5: 'electric',
 | |
|         6: 'icy',
 | |
|         7: 'flaming',
 | |
|         8: 'psychic',
 | |
|         9: 'screaming',
 | |
|         10: 'chaotic',
 | |
|         11: 'hissing',
 | |
|         12: 'gelatinous',
 | |
|         13: 'deformed',
 | |
|         14: 'shaggy',
 | |
|         15: 'scaly',
 | |
|         16: 'depressed',
 | |
|         17: 'anxious',
 | |
|         18: 'metallic',
 | |
|         19: 'glossy',
 | |
|         20: 'psychedelic',
 | |
|         21: 'bonsai',
 | |
|         22: 'foamy',
 | |
|         23: 'singing',
 | |
|         24: 'fractal',
 | |
|         25: 'crunchy',
 | |
|         26: 'goth',
 | |
|         27: 'oozing',
 | |
|         28: 'stinky',
 | |
|         29: 'aromatic',
 | |
|         30: 'juicy',
 | |
|         31: 'smug',
 | |
|         32: 'vibrating',
 | |
|         33: 'lithe',
 | |
|         34: 'chalky',
 | |
|         35: 'naive',
 | |
|         36: 'ersatz',
 | |
|         37: 'disco',
 | |
|         38: 'levitating',
 | |
|         39: 'colossal',
 | |
|     }
 | |
| 
 | |
|     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)
 | |
|         self.color = random.randint(0,len(self.color_dict)-1)
 | |
|         self.rarity = self.rarity_check()
 | |
|         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())
 | |
|         self.last_time = int(time.time())
 | |
|         # must water plant first day
 | |
|         self.watered_timestamp = int(time.time())-(24*3600)-1
 | |
|         # self.watered_timestamp = int(time.time()) # debug
 | |
|         self.watered_24h = 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 rarity_check(self):
 | |
|         # Generate plant rarity
 | |
|         CONST_RARITY_MAX = 256.0
 | |
|         rare_seed = random.randint(1,CONST_RARITY_MAX)
 | |
|         common_range =    round((2/3)*CONST_RARITY_MAX)
 | |
|         uncommon_range =  round((2/3)*(CONST_RARITY_MAX-common_range))
 | |
|         rare_range =      round((2/3)*(CONST_RARITY_MAX-common_range-uncommon_range))
 | |
|         legendary_range = round((2/3)*(CONST_RARITY_MAX-common_range-uncommon_range-rare_range))
 | |
|         # godly_range =     round((2/3)*(CONST_RARITY_MAX-common_range-uncommon_range-rare_range-legendary_range))
 | |
| 
 | |
|         common_max = common_range
 | |
|         uncommon_max = common_max + uncommon_range
 | |
|         rare_max = uncommon_max + rare_range
 | |
|         legendary_max = rare_max + legendary_range
 | |
|         godly_max = CONST_RARITY_MAX
 | |
| 
 | |
|         if   0 <= rare_seed <= common_max:
 | |
|             rarity = 0
 | |
|         elif common_max < rare_seed <= uncommon_max:
 | |
|             rarity = 1
 | |
|         elif uncommon_max < rare_seed <= rare_max:
 | |
|             rarity = 2
 | |
|         elif rare_max < rare_seed <= legendary_max:
 | |
|             rarity = 3
 | |
|         elif legendary_max < rare_seed <= godly_max:
 | |
|             rarity = 4
 | |
|         return rarity
 | |
| 
 | |
|     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 :(
 | |
|         if time_delta_watered > (5 * (24 * 3600)):
 | |
|             self.dead = True
 | |
|         return self.dead
 | |
| 
 | |
|     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
 | |
|         self.time_delta_watered = int(time.time()) - self.watered_timestamp
 | |
|         if self.time_delta_watered <= (24 * 3600):
 | |
|             return True
 | |
|         else:
 | |
|             self.watered_24h = False
 | |
|             return False
 | |
| 
 | |
|     def mutate_check(self):
 | |
|         # Create plant mutation
 | |
|         # TODO: when out of debug this needs to be set to high number (1000
 | |
|         # even maybe)
 | |
|         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!
 | |
|             mutation = random.randint(0,len(self.mutation_dict)-1)
 | |
|             if self.mutation == 0:
 | |
|                 self.mutation = mutation
 | |
|                 return True
 | |
|         else:
 | |
|             return False
 | |
| 
 | |
|     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
 | |
|        thread = threading.Thread(target=self.life, args=())
 | |
|        thread.daemon = True
 | |
|        thread.start()
 | |
| 
 | |
|     def life(self):
 | |
|         # I've created life :)
 | |
|         # TODO: change out of debug
 | |
|         # 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
 | |
|         while True:
 | |
|             time.sleep(1)
 | |
|             if not self.dead:
 | |
|                 if self.watered_24h:
 | |
|                     self.ticks += 1
 | |
|                     if self.stage < len(self.stage_dict)-1:
 | |
|                         if self.ticks >= self.life_stages[self.stage]:
 | |
|                             self.growth()
 | |
|                     if self.mutate_check():
 | |
|                         1==1
 | |
|             if self.water_check():
 | |
|                 1==1
 | |
|             if self.dead_check():
 | |
|                 1==1
 | |
|             # TODO: event check
 | |
| 
 | |
| class DataManager(object):
 | |
|     # handles user data, puts a .botany dir in user's home dir (OSX/Linux)
 | |
|     # TODO: windows... lol
 | |
|     user_dir = os.path.expanduser("~")
 | |
|     botany_dir = os.path.join(user_dir,'.botany')
 | |
|     game_dir = os.path.dirname(os.path.realpath(__file__))
 | |
| 
 | |
|     this_user = getpass.getuser()
 | |
|     savefile_name = this_user + '_plant.dat'
 | |
|     savefile_path = os.path.join(botany_dir, savefile_name)
 | |
|     garden_file_path = os.path.join(game_dir, 'garden_file.dat')
 | |
|     garden_json_path = os.path.join(game_dir, 'garden_file.json')
 | |
| 
 | |
|     def __init__(self):
 | |
|         self.this_user = getpass.getuser()
 | |
|         # check if instance is already running
 | |
|         # check for .botany dir in home
 | |
|         try:
 | |
|             os.makedirs(self.botany_dir)
 | |
|         except OSError as exception:
 | |
|             if exception.errno != errno.EEXIST:
 | |
|                 raise
 | |
|         self.savefile_name = self.this_user + '_plant.dat'
 | |
| 
 | |
|     def check_plant(self):
 | |
|         # check for existing save file
 | |
|         if os.path.isfile(self.savefile_path):
 | |
|             return True
 | |
|         else:
 | |
|             return False
 | |
| 
 | |
|     def start_threads(self,this_plant):
 | |
|         # creates threads to save files every minute
 | |
|         death_check_thread = threading.Thread(target=self.death_check_update, args=(this_plant,))
 | |
|         death_check_thread.daemon = True
 | |
|         death_check_thread.start()
 | |
| 
 | |
|         autosave_thread = threading.Thread(target=self.autosave, args=(this_plant,))
 | |
|         autosave_thread.daemon = True
 | |
|         autosave_thread.start()
 | |
| 
 | |
|     def death_check_update(self,this_plant):
 | |
|         # .1 second updates to minimize race condition
 | |
|         # TODO: improve how this is handled to eliminate race condition
 | |
|         while True:
 | |
|             is_dead = this_plant.dead_check()
 | |
|             if is_dead:
 | |
|                 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):
 | |
|         # running on thread
 | |
|         while True:
 | |
|             self.save_plant(this_plant)
 | |
|             self.data_write_json(this_plant)
 | |
|             self.garden_update(this_plant)
 | |
|             # TODO: change after debug
 | |
|             #time.sleep(60)
 | |
|             time.sleep(5)
 | |
| 
 | |
|     def load_plant(self):
 | |
|         # load savefile
 | |
|         with open(self.savefile_path, 'rb') as f:
 | |
|             this_plant = pickle.load(f)
 | |
| 
 | |
|         # get status since last login
 | |
|         is_dead = this_plant.dead_check()
 | |
|         is_watered = this_plant.water_check()
 | |
| 
 | |
|         if not is_dead:
 | |
|             if is_watered:
 | |
|                 time_delta_last = int(time.time()) - this_plant.last_time
 | |
|                 ticks_to_add = min(time_delta_last, 24*3600)
 | |
|                 this_plant.time_delta_watered = 0
 | |
|                 self.last_water_gain = time.time()
 | |
|             else:
 | |
|                 ticks_to_add = 0
 | |
|             this_plant.ticks += ticks_to_add
 | |
| 
 | |
|         return this_plant
 | |
| 
 | |
|     def plant_age_convert(self,this_plant):
 | |
|         # human-readable plant age
 | |
|         age_seconds = int(time.time()) - this_plant.start_time
 | |
|         days, age_seconds = divmod(age_seconds, 24 * 60 * 60)
 | |
|         hours, age_seconds = divmod(age_seconds, 60 * 60)
 | |
|         minutes, age_seconds = divmod(age_seconds, 60)
 | |
|         age_formatted = ("%dd:%dh:%dm:%ds" % (days, hours, minutes, age_seconds))
 | |
|         return age_formatted
 | |
| 
 | |
|     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 = {
 | |
|                 "owner":this_plant.owner,
 | |
|                 "description":this_plant.parse_plant(),
 | |
|                 "age":age_formatted,
 | |
|                 "score":this_plant.ticks,
 | |
|                 "dead":this_plant.dead,
 | |
|         }
 | |
| 
 | |
|         if os.path.isfile(self.garden_file_path):
 | |
|             # garden file exists: load data
 | |
|             with open(self.garden_file_path, 'rb') as f:
 | |
|                 this_garden = pickle.load(f)
 | |
|             new_file_check = False
 | |
|         else:
 | |
|             # create empty garden list and initalize file permissions
 | |
|             this_garden = {}
 | |
|             new_file_check = True
 | |
|             open(self.garden_file_path, 'a').close()
 | |
|             open(self.garden_json_path, 'a').close()
 | |
|             # If user has access, modify permissions to allow others to write
 | |
|             # This means the first run has to be by the file owner.
 | |
|             if os.stat(self.garden_file_path).st_uid == os.getuid():
 | |
|                 os.chmod(self.garden_file_path, 0666)
 | |
|             if os.stat(self.garden_json_path).st_uid == os.getuid():
 | |
|                 os.chmod(self.garden_json_path, 0666)
 | |
| 
 | |
|         # if current plant ID isn't in garden list
 | |
|         if this_plant.plant_id not in this_garden:
 | |
|             this_garden[this_plant_id] = plant_info
 | |
|         # 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:
 | |
|                 this_garden[this_plant_id] = plant_info
 | |
| 
 | |
|         # dump garden file
 | |
|         with open(self.garden_file_path, 'wb') as f:
 | |
|             pickle.dump(this_garden, f, protocol=2)
 | |
|         # dump json file
 | |
|         with open(self.garden_json_path, 'w') as outfile:
 | |
|             json.dump(this_garden, outfile)
 | |
| 
 | |
|         return new_file_check
 | |
| 
 | |
|     def save_plant(self, this_plant):
 | |
|         # create savefile
 | |
|         this_plant.last_time = int(time.time())
 | |
|         with open(self.savefile_path, 'wb') as f:
 | |
|             pickle.dump(this_plant, f, protocol=2)
 | |
| 
 | |
|     def data_write_json(self, this_plant):
 | |
|         # create personal json file for user to use outside of the game (website?)
 | |
|         json_file = os.path.join(self.botany_dir,self.this_user + '_plant_data.json')
 | |
|         json_leaderboard = os.path.join(self.game_dir + '_garden.json')
 | |
|         # also updates age
 | |
|         age_formatted = self.plant_age_convert(this_plant)
 | |
|         plant_info = {
 | |
|                 "owner":this_plant.owner,
 | |
|                 "description":this_plant.parse_plant(),
 | |
|                 "age":age_formatted,
 | |
|                 "score":this_plant.ticks,
 | |
|                 "is_dead":this_plant.dead,
 | |
|                 "last_watered":this_plant.watered_timestamp,
 | |
|                 "file_name":this_plant.file_name,
 | |
|         }
 | |
|         with open(json_file, 'w') as outfile:
 | |
|             json.dump(plant_info, outfile)
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     my_data = DataManager()
 | |
|     # if plant save file exists
 | |
|     if my_data.check_plant():
 | |
|         my_plant = my_data.load_plant()
 | |
|     # otherwise create new plant
 | |
|     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)
 | |
|     my_data.save_plant(my_plant)
 | |
|     my_data.data_write_json(my_plant)
 | |
|     my_data.garden_update(my_plant)
 |