2019-10-30 17:26:49 +00:00
#!/usr/bin/env python3
2017-04-02 11:28:30 +00:00
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
2023-12-05 06:04:00 +00:00
from plant import Plant
2017-03-06 21:38:31 +00:00
2017-03-31 19:06:08 +00:00
# TODO:
2023-12-05 06:04:00 +00:00
# - switch from personal data file to row in DB
# - is threading necessary?
2023-12-07 04:56:11 +00:00
# - use a different curses window for plant, menu, info window, score
# notes from vilmibm
2023-12-05 06:04:00 +00:00
# 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 ' )
2017-03-10 01:02:19 +00:00
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 '
2017-03-15 06:51:52 +00:00
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 ) :
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-03-10 01:02:19 +00:00
2017-05-03 19:51:21 +00:00
# migrate data structure to create data for empty/nonexistent plant
# properties
this_plant . migrate_properties ( )
2017-03-10 01:02:19 +00:00
# get status since last login
2017-03-09 01:36:41 +00:00
is_watered = this_plant . water_check ( )
2018-10-03 18:53:49 +00:00
is_dead = this_plant . dead_check ( )
2017-03-09 01:36:41 +00:00
2017-03-08 23:04:09 +00:00
if not is_dead :
2017-03-09 01:36:41 +00:00
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
2022-05-20 13:19:05 +00:00
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
2017-03-10 01:02:19 +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 = ( " %d d: %d h: %d m: %d s " % ( 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 :
2017-03-10 01:02:19 +00:00
json . dump ( this_garden , outfile )
2017-03-23 01:49:38 +00:00
pass
2017-03-10 01:02:19 +00:00
def save_plant ( self , this_plant ) :
# create savefile
this_plant . last_time = int ( time . time ( ) )
2018-05-23 19:21:50 +00:00
temp_path = self . savefile_path + " .temp "
with open ( temp_path , ' wb ' ) as f :
2017-03-10 01:02:19 +00:00
pickle . dump ( this_plant , f , protocol = 2 )
2018-05-23 19:21:50 +00:00
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 ) :
2017-03-10 01:02:19 +00:00
# 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
2017-03-10 01:02:19 +00:00
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
}
2017-04-02 11:28:30 +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 ]
2017-04-02 11:28:30 +00:00
if this_plant . mutation != 0 :
2017-09-21 21:29:11 +00:00
plant_info [ " mutation " ] = this_plant . mutation_list [ this_plant . mutation ]
2017-04-02 11:28:30 +00:00
if this_plant . stage > = 4 :
2017-09-21 21:29:11 +00:00
plant_info [ " color " ] = this_plant . color_list [ this_plant . color ]
2017-04-02 11:28:30 +00:00
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-24 00:08:27 +00:00
2017-03-21 19:55:11 +00:00
this_harvest [ this_plant_id ] = plant_info
# dump harvest file
2018-05-23 19:21:50 +00:00
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 )
2018-05-23 19:21:50 +00:00
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 )
2017-03-10 01:02:19 +00:00
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 )
2023-12-05 06:04:00 +00:00
2018-05-23 19:45:51 +00:00
try :
2023-12-03 06:31:32 +00:00
botany_menu = ms . CursedMenu ( my_plant , my_data )
2018-05-23 19:45:51 +00:00
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 ( )