botany/botany.py

547 lines
18 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
import sqlite3
from collections import OrderedDict
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
# - 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
# 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: 'hemp',
11: 'pansy',
12: 'iris',
13: 'agave',
14: 'ficus',
15: 'moss',
16: 'sage',
17: 'snapdragon',
18: 'columbine',
19: 'brugmansia',
20: 'palm',
}
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())
self.life_stages = (3600*24, (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_24h = False
def parse_plant(self):
# Converts plant data to human-readable format
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))
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
# Increase this # to make mutation rarer (chance 1 out of x each second)
CONST_MUTATION_RARITY = 5000
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)
pass
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):
# After plant reaches final stage, given option to restart
self.write_lock = True
self.kill_plant()
while self.write_lock:
# Wait for garden writer to unlock
# garden datafile needs to register that plant has died before
# allowing the user to reset
pass
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 :)
while True:
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():
pass
if self.water_check():
# Do something
pass
if self.dead_check():
# Do something else
pass
# TODO: event check
time.sleep(1)
class DataManager(object):
# handles user data, puts a .botany dir in user's home dir (OSX/Linux)
# handles shared data with sqlite db
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_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
garden_json_path = os.path.join(game_dir, 'garden_file.json')
harvest_file_path = os.path.join(botany_dir, 'harvest_file.dat')
harvest_json_path = os.path.join(botany_dir, 'harvest_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 and lock to minimize 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.update_garden_db(this_plant)
self.harvest_plant(this_plant)
this_plant.unlock_new_creation()
time.sleep(.1)
def autosave(self, this_plant):
# running on thread, saves plant every 5s
file_update_count = 0
while True:
file_update_count += 1
self.save_plant(this_plant)
self.data_write_json(this_plant)
self.update_garden_db(this_plant)
if file_update_count == 12:
# only update garden json every 60s
self.update_garden_json()
time.sleep(5)
file_update_count %= 12
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 init_database(self):
# check if dir exists, create sqlite directory and set OS permissions to 777
sqlite_dir_path = os.path.join(self.game_dir,'sqlite')
if not os.path.exists(sqlite_dir_path):
os.makedirs(sqlite_dir_path)
os.chmod(sqlite_dir_path, 0777)
conn = sqlite3.connect(self.garden_db_path)
init_table_string = """CREATE TABLE IF NOT EXISTS garden (
plant_id tinytext PRIMARY KEY,
owner text,
description text,
age text,
score integer,
is_dead numeric
)"""
c = conn.cursor()
c.execute(init_table_string)
conn.close()
# init only, creates and sets permissions for garden db and json
if os.stat(self.garden_db_path).st_uid == os.getuid():
os.chmod(self.garden_db_path, 0666)
open(self.garden_json_path, 'a').close()
os.chmod(self.garden_json_path, 0666)
def update_garden_db(self, this_plant):
# insert or update this plant id's entry in DB
# TODO: is this needed?
self.init_database()
age_formatted = self.plant_age_convert(this_plant)
conn = sqlite3.connect(self.garden_db_path)
c = conn.cursor()
# try to insert or replace
update_query = """INSERT OR REPLACE INTO garden (
plant_id, owner, description, age, score, is_dead
) VALUES (
'{pid}', '{pown}', '{pdes}', '{page}', {psco}, {pdead}
)
""".format(pid = this_plant.plant_id,
pown = this_plant.owner,
pdes = this_plant.parse_plant(),
page = age_formatted,
psco = str(this_plant.ticks),
pdead = int(this_plant.dead))
c.execute(update_query)
conn.commit()
conn.close()
def retrieve_garden_from_db(self):
# Builds a dict of dicts from garden sqlite db
garden_dict = {}
conn = sqlite3.connect(self.garden_db_path)
# Need to allow write permissions by others
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute('SELECT * FROM garden ORDER BY owner')
tuple_list = c.fetchall()
conn.close()
# Building dict from table rows
for item in tuple_list:
garden_dict[item[0]] = {
"owner":item[1],
"description":item[2],
"age":item[3],
"score":item[4],
"dead":item[5],
}
return garden_dict
def update_garden_json(self):
this_garden = self.retrieve_garden_from_db()
with open(self.garden_json_path, 'w') as outfile:
json.dump(this_garden, outfile)
pass
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)
def harvest_plant(self, this_plant):
# TODO: could just use a sqlite query to retrieve all of user's dead
# plants
# harvest is a dict of dicts
# harvest contains one entry for each plant id
age_formatted = self.plant_age_convert(this_plant)
this_plant_id = this_plant.plant_id
plant_info = {
"description":this_plant.parse_plant(),
"age":age_formatted,
"score":this_plant.ticks,
}
if os.path.isfile(self.harvest_file_path):
# harvest file exists: load data
with open(self.harvest_file_path, 'rb') as f:
this_harvest = pickle.load(f)
new_file_check = False
else:
this_harvest = {}
new_file_check = True
this_harvest[this_plant_id] = plant_info
# dump harvest file
with open(self.harvest_file_path, 'wb') as f:
pickle.dump(this_harvest, f, protocol=2)
# dump json file
with open(self.harvest_json_path, 'w') as outfile:
json.dump(this_harvest, outfile)
return new_file_check
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)
my_data.save_plant(my_plant)
my_data.data_write_json(my_plant)
my_data.update_garden_db(my_plant)