botany/botany.py

307 lines
12 KiB
Python
Raw Normal View History

2019-10-30 17:26:49 +00:00
#!/usr/bin/env python3
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
2017-03-14 22:23:28 +00:00
import os
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
2017-03-23 01:49:38 +00:00
import sqlite3
2023-12-03 06:31:32 +00:00
import menu_screen as ms
from plant import Plant
2017-03-06 21:38:31 +00:00
2017-03-31 19:06:08 +00:00
# TODO:
# - switch from personal data file to row in DB
# - is threading necessary?
# - use a different curses window for plant, menu, info window, score
# notes from vilmibm
# there are threads.
# - life thread. sleeps a variable amount of time based on generation bonus. increases tick count (ticks == score).
# - screen: sleeps 1s per loop. draws interface (including plant). for seeing score/plant change without user input.
# meanwhile, the main thread handles input and redraws curses as needed.
# affordance index
# - main screen
# navigable menu, plant, score, etc
# - water
# render a visualization of moistness; allow to water
# - look
# print a description of plant with info below rest of UI
# - garden
# runs a paginated view of every plant on the computer below rest of UI. to return to menu navigation must hit q.
# - visit
# runs a prompt underneath UI where you can see who recently visited you and type in a name to visit. must submit the prompt to get back to menu navigation.
# - instructions
# prints some explanatory text below the UI
# - exit
# quits program
# part of the complexity of all this is everything takes place in one curses window; thus, updates must be manually synchronized across the various logical parts of the screen.
# ideally, multiple windows would be used:
# - the menu. it doesn't change unless the plant dies OR the plant hits stage 5, then "harvest" is dynamically added.
# - the plant viewer. this is updated in "real time" as the plant grows.
# - the status display: score and plant description
# - the infow window. updated by visit/garden/instructions/look
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
2018-05-21 18:00:53 +00:00
# TODO: .dat save should only happen on mutation, water, death, exit,
# harvest, otherwise
# data hasn't changed...
# can write json whenever bc this isn't ever read for data within botany
2017-03-23 01:49:38 +00:00
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)
2018-03-03 05:20:29 +00:00
#set this.savefile_path to guest_garden path
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) and os.path.getsize(self.savefile_path) > 0:
2017-03-07 01:56:11 +00:00
return True
else:
return False
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)
2017-05-03 19:51:21 +00:00
# migrate data structure to create data for empty/nonexistent plant
# properties
this_plant.migrate_properties()
# get status since last login
is_watered = this_plant.water_check()
2018-10-03 18:53:49 +00:00
is_dead = this_plant.dead_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 * round(0.2 * (this_plant.generation - 1) + 1, 1)
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)
2019-10-30 17:26:49 +00:00
os.chmod(sqlite_dir_path, 0o777)
2017-03-23 01:49:38 +00:00
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():
2019-10-30 17:26:49 +00:00
os.chmod(self.garden_db_path, 0o666)
2017-03-15 20:56:00 +00:00
open(self.garden_json_path, 'a').close()
2019-10-30 17:26:49 +00:00
os.chmod(self.garden_json_path, 0o666)
2017-03-15 20:56:00 +00:00
2018-03-11 20:46:45 +00:00
def migrate_database(self):
conn = sqlite3.connect(self.garden_db_path)
migrate_table_string = """CREATE TABLE IF NOT EXISTS visitors (
id integer PRIMARY KEY,
garden_name text,
visitor_name text,
weekly_visits integer
)"""
c = conn.cursor()
c.execute(migrate_table_string)
conn.close()
return True
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
self.init_database()
2018-03-11 20:46:45 +00:00
self.migrate_database()
2017-03-23 01:49:38 +00:00
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)
2023-03-20 00:26:43 +00:00
# clean other instances of user
clean_query = """UPDATE garden set is_dead = 1
where owner = '{pown}'
and plant_id <> '{pid}'
""".format(pown = this_plant.owner,
pid = this_plant.plant_id)
c.execute(clean_query)
2017-03-23 01:49:38 +00:00
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())
temp_path = self.savefile_path + ".temp"
with open(temp_path, 'wb') as f:
pickle.dump(this_plant, f, protocol=2)
os.rename(temp_path, self.savefile_path)
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')
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-09-21 21:29:11 +00:00
"stage": this_plant.stage_list[this_plant.stage],
2017-05-03 23:21:43 +00:00
"generation": this_plant.generation,
2017-03-09 02:06:35 +00:00
}
if this_plant.stage >= 3:
2017-09-21 21:29:11 +00:00
plant_info["rarity"] = this_plant.rarity_list[this_plant.rarity]
if this_plant.mutation != 0:
2017-09-21 21:29:11 +00:00
plant_info["mutation"] = this_plant.mutation_list[this_plant.mutation]
if this_plant.stage >= 4:
2017-09-21 21:29:11 +00:00
plant_info["color"] = this_plant.color_list[this_plant.color]
if this_plant.stage >= 2:
2017-09-21 21:29:11 +00:00
plant_info["species"] = this_plant.species_list[this_plant.species]
2017-05-03 19:51:21 +00:00
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):
2018-03-11 21:30:10 +00:00
# TODO: plant history feature - could just use a sqlite query to retrieve all of user's dead plants
2017-03-23 01:49:38 +00:00
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
2017-03-21 19:55:11 +00:00
this_harvest[this_plant_id] = plant_info
# dump harvest file
temp_path = self.harvest_file_path + ".temp"
with open(temp_path, 'wb') as f:
2017-03-21 19:55:11 +00:00
pickle.dump(this_harvest, f, protocol=2)
os.rename(temp_path, self.harvest_file_path)
2017-03-21 19:55:11 +00:00
# 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-05-03 19:51:21 +00:00
# my_plant is either a fresh plant or an existing plant at this point
2023-12-07 05:36:15 +00:00
my_plant.start_life(my_data)
try:
2023-12-03 06:31:32 +00:00
botany_menu = ms.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)
finally:
2023-12-03 06:31:32 +00:00
ms.cleanup()