botany/botany.py

547 lines
18 KiB
Python
Raw Normal View History

2017-03-06 22:22:13 +00:00
from __future__ import division
2017-03-06 21:38:31 +00:00
import time
2017-03-06 22:22:13 +00:00
import pickle
2017-03-06 21:38:31 +00:00
import json
import math
2017-03-14 22:23:28 +00:00
import sys
import os
2017-03-06 21:38:31 +00:00
import random
2017-03-07 01:56:11 +00:00
import getpass
2017-03-07 18:07:13 +00:00
import threading
2017-03-08 02:35:04 +00:00
import errno
import uuid
import fcntl
2017-03-23 01:49:38 +00:00
import sqlite3
from collections import OrderedDict
from operator import itemgetter
2017-03-08 02:35:04 +00:00
from menu_screen import *
2017-03-06 21:38:31 +00:00
# development plan
2017-03-06 21:38:31 +00:00
# build plant lifecycle just stepping through
2017-03-07 00:57:11 +00:00
# - 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
#
2017-03-07 21:04:14 +00:00
# events
# - heatwave
# - rain
# - bugs
#
# build multiplayer
2017-03-07 21:04:14 +00:00
# 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
2017-03-06 21:38:31 +00:00
2017-03-14 22:23:28 +00:00
# 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
2017-03-06 21:38:31 +00:00
2017-03-07 01:56:11 +00:00
class Plant(object):
2017-03-07 21:04:14 +00:00
# This is your plant!
2017-03-06 23:20:31 +00:00
stage_dict = {
0: 'seed',
1: 'seedling',
2: 'young',
2017-03-06 23:29:23 +00:00
3: 'mature',
4: 'flowering',
5: 'seed-bearing',
2017-03-06 23:20:31 +00:00
}
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',
2017-03-07 00:57:11 +00:00
4: 'jade plant',
2017-03-06 23:20:31 +00:00
5: 'fern',
2017-03-06 23:29:23 +00:00
6: 'daffodil',
7: 'sunflower',
8: 'baobab',
9: 'lithops',
2017-03-18 00:33:40 +00:00
10: 'hemp',
2017-03-09 02:38:09 +00:00
11: 'pansy',
12: 'iris',
13: 'agave',
14: 'ficus',
2017-03-17 19:00:46 +00:00
15: 'moss',
16: 'sage',
17: 'snapdragon',
18: 'columbine',
19: 'brugmansia',
2017-03-17 19:00:46 +00:00
20: 'palm',
2017-03-06 23:20:31 +00:00
}
mutation_dict = {
0: '',
1: 'humming',
2: 'noxious',
3: 'vorpal',
2017-03-06 23:29:23 +00:00
4: 'glowing',
5: 'electric',
2017-03-07 00:57:11 +00:00
6: 'icy',
2017-03-06 23:29:23 +00:00
7: 'flaming',
8: 'psychic',
2017-03-07 00:57:11 +00:00
9: 'screaming',
10: 'chaotic',
2017-03-07 00:57:11 +00:00
11: 'hissing',
12: 'gelatinous',
13: 'deformed',
14: 'shaggy',
15: 'scaly',
16: 'depressed',
17: 'anxious',
18: 'metallic',
19: 'glossy',
2017-03-09 02:06:35 +00:00
20: 'psychedelic',
2017-03-09 02:38:09 +00:00
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',
2017-03-06 23:20:31 +00:00
}
2017-03-07 21:04:14 +00:00
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)
2017-03-06 21:38:31 +00:00
self.stage = 0
self.mutation = 0
2017-03-06 23:20:31 +00:00
self.species = random.randint(0,len(self.species_dict)-1)
2017-03-07 00:57:11 +00:00
self.color = random.randint(0,len(self.color_dict)-1)
2017-03-06 21:38:31 +00:00
self.rarity = self.rarity_check()
2017-03-07 00:57:11 +00:00
self.ticks = 0
self.age_formatted = "0"
2017-03-07 18:07:13 +00:00
self.dead = False
2017-03-14 22:23:28 +00:00
self.write_lock = False
self.owner = getpass.getuser()
2017-03-08 08:18:45 +00:00
self.file_name = this_filename
self.start_time = int(time.time())
self.last_time = int(time.time())
2017-03-08 23:04:09 +00:00
# must water plant first day
2017-03-09 19:32:40 +00:00
self.watered_timestamp = int(time.time())-(24*3600)-1
self.watered_24h = False
2017-03-06 21:38:31 +00:00
2017-03-14 22:23:28 +00:00
def parse_plant(self):
2017-03-18 00:33:40 +00:00
# Converts plant data to human-readable format
2017-03-14 22:23:28 +00:00
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()
2017-03-09 19:32:40 +00:00
2017-03-06 21:38:31 +00:00
def rarity_check(self):
2017-03-07 21:04:14 +00:00
# Generate plant rarity
2017-03-06 22:22:13 +00:00
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
2017-03-08 08:18:45 +00:00
godly_max = CONST_RARITY_MAX
2017-03-06 22:22:13 +00:00
if 0 <= rare_seed <= common_max:
2017-03-06 21:38:31 +00:00
rarity = 0
2017-03-06 22:22:13 +00:00
elif common_max < rare_seed <= uncommon_max:
2017-03-06 21:38:31 +00:00
rarity = 1
2017-03-06 22:22:13 +00:00
elif uncommon_max < rare_seed <= rare_max:
2017-03-06 21:38:31 +00:00
rarity = 2
2017-03-06 22:22:13 +00:00
elif rare_max < rare_seed <= legendary_max:
2017-03-06 21:38:31 +00:00
rarity = 3
2017-03-08 08:18:45 +00:00
elif legendary_max < rare_seed <= godly_max:
2017-03-06 21:38:31 +00:00
rarity = 4
return rarity
2017-03-08 23:04:09 +00:00
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
2017-03-08 08:18:45 +00:00
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
2017-03-07 21:04:14 +00:00
def mutate_check(self):
# Create plant mutation
2017-03-17 19:00:46 +00:00
# TODO: when out of debug this needs to be set to high number
2017-03-18 00:33:40 +00:00
# Increase this # to make mutation rarer (chance 1 out of x each second)
2017-03-21 19:55:11 +00:00
CONST_MUTATION_RARITY = 5000
2017-03-07 21:04:14 +00:00
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
2017-03-14 22:23:28 +00:00
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)
2017-03-17 19:00:46 +00:00
pass
2017-03-14 22:23:28 +00:00
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):
2017-03-18 00:33:40 +00:00
# After plant reaches final stage, given option to restart
2017-03-14 22:23:28 +00:00
self.write_lock = True
self.kill_plant()
while self.write_lock:
# Wait for garden writer to unlock
2017-03-18 00:33:40 +00:00
# garden datafile needs to register that plant has died before
# allowing the user to reset
2017-03-17 19:00:46 +00:00
pass
2017-03-14 22:23:28 +00:00
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
2017-03-06 21:38:31 +00:00
2017-03-07 18:07:13 +00:00
def start_life(self):
2017-03-07 21:04:14 +00:00
# runs life on a thread
2017-03-07 18:07:13 +00:00
thread = threading.Thread(target=self.life, args=())
thread.daemon = True
thread.start()
def life(self):
2017-03-07 00:57:11 +00:00
# I've created life :)
2017-03-09 19:32:40 +00:00
while True:
if not self.dead:
if self.watered_24h:
self.ticks += 1
if self.stage < len(self.stage_dict)-1:
2017-03-14 22:23:28 +00:00
if self.ticks >= self.life_stages[self.stage]:
2017-03-09 19:32:40 +00:00
self.growth()
if self.mutate_check():
2017-03-17 19:00:46 +00:00
pass
if self.water_check():
# Do something
2017-03-17 19:00:46 +00:00
pass
2017-03-09 19:32:40 +00:00
if self.dead_check():
# Do something else
2017-03-17 19:00:46 +00:00
pass
# TODO: event check
2017-03-21 19:55:11 +00:00
time.sleep(1)
2017-03-07 01:56:11 +00:00
class DataManager(object):
2017-03-07 21:04:14 +00:00
# handles user data, puts a .botany dir in user's home dir (OSX/Linux)
2017-03-23 01:49:38 +00:00
# handles shared data with sqlite db
2017-03-07 21:04:14 +00:00
user_dir = os.path.expanduser("~")
botany_dir = os.path.join(user_dir,'.botany')
game_dir = os.path.dirname(os.path.realpath(__file__))
2017-03-07 21:04:14 +00:00
this_user = getpass.getuser()
2017-03-23 01:49:38 +00:00
2017-03-07 21:04:14 +00:00
savefile_name = this_user + '_plant.dat'
savefile_path = os.path.join(botany_dir, savefile_name)
2017-03-23 01:49:38 +00:00
garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
2017-03-15 20:56:00 +00:00
garden_json_path = os.path.join(game_dir, 'garden_file.json')
2017-03-21 19:55:11 +00:00
harvest_file_path = os.path.join(botany_dir, 'harvest_file.dat')
harvest_json_path = os.path.join(botany_dir, 'harvest_file.json')
2017-03-07 21:04:14 +00:00
2017-03-07 01:56:11 +00:00
def __init__(self):
self.this_user = getpass.getuser()
2017-03-14 22:23:28 +00:00
# check if instance is already running
2017-03-08 02:35:04 +00:00
# check for .botany dir in home
try:
2017-03-07 21:04:14 +00:00
os.makedirs(self.botany_dir)
2017-03-08 02:35:04 +00:00
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
2017-03-07 01:56:11 +00:00
self.savefile_name = self.this_user + '_plant.dat'
def check_plant(self):
2017-03-07 21:04:14 +00:00
# check for existing save file
if os.path.isfile(self.savefile_path):
2017-03-07 01:56:11 +00:00
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)
2017-03-23 01:49:38 +00:00
self.update_garden_db(this_plant)
2017-03-21 19:55:11 +00:00
self.harvest_plant(this_plant)
2017-03-14 22:23:28 +00:00
this_plant.unlock_new_creation()
time.sleep(.1)
2017-03-07 01:56:11 +00:00
2017-03-09 02:06:35 +00:00
def autosave(self, this_plant):
2017-03-21 19:55:11 +00:00
# running on thread, saves plant every 5s
2017-03-23 01:49:38 +00:00
file_update_count = 0
2017-03-09 02:06:35 +00:00
while True:
2017-03-23 01:49:38 +00:00
file_update_count += 1
2017-03-09 02:06:35 +00:00
self.save_plant(this_plant)
self.data_write_json(this_plant)
2017-03-23 01:49:38 +00:00
self.update_garden_db(this_plant)
if file_update_count == 12:
# only update garden json every 60s
self.update_garden_json()
2017-03-09 02:06:35 +00:00
time.sleep(5)
2017-03-23 01:49:38 +00:00
file_update_count %= 12
2017-03-09 02:06:35 +00:00
2017-03-07 01:56:11 +00:00
def load_plant(self):
2017-03-07 21:04:14 +00:00
# load savefile
with open(self.savefile_path, 'rb') as f:
2017-03-07 01:56:11 +00:00
this_plant = pickle.load(f)
# get status since last login
2017-03-08 23:04:09 +00:00
is_dead = this_plant.dead_check()
is_watered = this_plant.water_check()
2017-03-08 23:04:09 +00:00
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
2017-03-08 08:45:21 +00:00
return this_plant
2017-03-07 01:56:11 +00:00
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
2017-03-23 01:49:38 +00:00
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)
2017-03-15 20:56:00 +00:00
open(self.garden_json_path, 'a').close()
2017-03-23 01:49:38 +00:00
os.chmod(self.garden_json_path, 0666)
2017-03-15 20:56:00 +00:00
2017-03-23 01:49:38 +00:00
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()
2017-03-15 20:56:00 +00:00
with open(self.garden_json_path, 'w') as outfile:
json.dump(this_garden, outfile)
2017-03-23 01:49:38 +00:00
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)
2017-03-09 02:18:01 +00:00
2017-03-07 01:56:11 +00:00
def data_write_json(self, this_plant):
# create personal json file for user to use outside of the game (website?)
2017-03-07 21:04:14 +00:00
json_file = os.path.join(self.botany_dir,self.this_user + '_plant_data.json')
json_leaderboard = os.path.join(self.game_dir + '_garden.json')
2017-03-09 02:18:01 +00:00
# also updates age
age_formatted = self.plant_age_convert(this_plant)
2017-03-09 02:06:35 +00:00
plant_info = {
"owner":this_plant.owner,
"description":this_plant.parse_plant(),
2017-03-09 02:18:01 +00:00
"age":age_formatted,
2017-03-09 02:06:35 +00:00
"score":this_plant.ticks,
"is_dead":this_plant.dead,
2017-03-09 02:18:01 +00:00
"last_watered":this_plant.watered_timestamp,
2017-03-09 02:06:35 +00:00
"file_name":this_plant.file_name,
}
2017-03-07 21:04:14 +00:00
with open(json_file, 'w') as outfile:
2017-03-09 02:06:35 +00:00
json.dump(plant_info, outfile)
2017-03-07 00:57:11 +00:00
2017-03-21 19:55:11 +00:00
def harvest_plant(self, this_plant):
2017-03-23 01:49:38 +00:00
# TODO: could just use a sqlite query to retrieve all of user's dead
# plants
2017-03-21 19:55:11 +00:00
# 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
2017-03-06 21:38:31 +00:00
if __name__ == '__main__':
2017-03-07 01:56:11 +00:00
my_data = DataManager()
2017-03-07 21:04:14 +00:00
# if plant save file exists
2017-03-07 01:56:11 +00:00
if my_data.check_plant():
my_plant = my_data.load_plant()
# otherwise create new plant
else:
2017-03-07 21:04:14 +00:00
my_plant = Plant(my_data.savefile_path)
my_data.data_write_json(my_plant)
2017-03-07 18:07:13 +00:00
my_plant.start_life()
my_data.start_threads(my_plant)
2017-03-23 01:49:38 +00:00
botany_menu = CursedMenu(my_plant,my_data)
2017-03-07 01:56:11 +00:00
my_data.save_plant(my_plant)
my_data.data_write_json(my_plant)
2017-03-23 01:49:38 +00:00
my_data.update_garden_db(my_plant)