Update garden handling to SQLite3

pull/1/head
Jake Funke 2017-03-23 01:49:38 +00:00
parent 59dada94b6
commit 2726b34c2f
3 changed files with 110 additions and 66 deletions

View File

@ -29,7 +29,7 @@ If your plant goes 5 days without water, it will die!
* Curses-based menu system, optimized for 80x24 terminal
* ASCII art display of plant
* Persistent aging system that allows your plant to grow even when app is closed
* Community garden of other users' plants (for shared unix servers)
* SQLite Community Garden of other users' plants (for shared unix servers)
* Data file is created in the user's home (~) directory, along with a JSON file that can be used in other apps.
```
@ -49,7 +49,6 @@ If your plant goes 5 days without water, it will die!
### to-dos
* Finish garden feature
* Switch to database instead of .dat file
* Allows you to water neighbor's plants
* Harvest plant at end of life (gather seeds)
* Create harvest file with a log of all previous plants

139
botany.py
View File

@ -11,6 +11,8 @@ import threading
import errno
import uuid
import fcntl
import sqlite3
from collections import OrderedDict
from operator import itemgetter
from menu_screen import *
@ -29,7 +31,6 @@ from menu_screen import *
#
# 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
@ -183,7 +184,6 @@ class Plant(object):
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
@ -299,17 +299,17 @@ class Plant(object):
time.sleep(1)
class DataManager(object):
# TODO: garden file stuff has a race condition - need to find and
# eliminate it.
# 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_file_path = os.path.join(game_dir, 'garden_file.dat')
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')
@ -349,18 +349,24 @@ class DataManager(object):
if is_dead:
self.save_plant(this_plant)
self.data_write_json(this_plant)
self.garden_update(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.garden_update(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
@ -391,54 +397,81 @@ class DataManager(object):
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,
}
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
)"""
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()
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()
# 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
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()
# dump garden file
with open(self.garden_file_path, 'wb') as f:
pickle.dump(this_garden, f, protocol=2)
# dump json file
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)
return new_file_check
pass
def save_plant(self, this_plant):
# create savefile
@ -465,6 +498,9 @@ class DataManager(object):
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)
@ -504,8 +540,7 @@ if __name__ == '__main__':
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)
botany_menu = CursedMenu(my_plant,my_data)
my_data.save_plant(my_plant)
my_data.data_write_json(my_plant)
my_data.garden_update(my_plant)
my_data.update_garden_db(my_plant)

View File

@ -1,9 +1,9 @@
import curses, os, traceback, threading, time, datetime, pickle, operator, random
import curses, os, traceback, threading, time, datetime, pickle, operator, random, sqlite3
class CursedMenu(object):
#TODO: name your plant
'''A class which abstracts the horrors of building a curses-based menu system'''
def __init__(self, this_plant, this_garden_file_path):
def __init__(self, this_plant, this_data):
'''Initialization'''
self.initialized = False
self.screen = curses.initscr()
@ -13,7 +13,7 @@ class CursedMenu(object):
curses.curs_set(0)
self.screen.keypad(1)
self.plant = this_plant
self.garden_file_path = this_garden_file_path
self.user_data = this_data
self.plant_string = self.plant.parse_plant()
self.plant_ticks = str(self.plant.ticks)
self.exit = False
@ -184,7 +184,6 @@ class CursedMenu(object):
def update_plant_live(self):
# updates plant data on menu screen, live!
# will eventually use this to display ascii art...
while not self.exit:
self.plant_string = self.plant.parse_plant()
self.plant_ticks = str(self.plant.ticks)
@ -194,12 +193,12 @@ class CursedMenu(object):
time.sleep(1)
def get_user_input(self):
# gets the user's input and acts appropriately
# gets the user's input
try:
user_in = self.screen.getch() # Gets user input
except Exception as e:
self.__exit__()
# DEBUG KEYS
# DEBUG KEYS - enable to see curses key codes
# self.screen.addstr(1, 1, str(user_in), curses.A_NORMAL)
# self.screen.refresh()
@ -252,9 +251,8 @@ class CursedMenu(object):
clear_bar = " " * (self.maxx-2) + "\n"
clear_block = clear_bar * 5
control_keys = [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT]
# load data
with open(self.garden_file_path, 'rb') as f:
this_garden = pickle.load(f)
# load data from sqlite db
this_garden = self.user_data.retrieve_garden_from_db()
# format data
plant_table_pages = []
if self.infotoggle != 2:
@ -385,6 +383,7 @@ class CursedMenu(object):
this_stage_descriptions = stage_descriptions[this_stage]
description_num = random.randint(0,len(this_stage_descriptions) - 1)
# If not fully grown
if this_stage <= 4:
# Growth hint
if this_stage >= 1:
@ -398,6 +397,7 @@ class CursedMenu(object):
output_text += this_stage_descriptions[description_num] + "\n"
# if seedling
if this_stage == 1:
species_options = [this_plant.species_dict[this_plant.species],
this_plant.species_dict[(this_plant.species+3) % len(this_plant.species_dict)],
@ -406,12 +406,14 @@ class CursedMenu(object):
plant_hint = "It could be a(n) " + species_options[0] + ", " + species_options[1] + ", or " + species_options[2]
output_text += plant_hint + ".\n"
# if young plant
if this_stage == 2:
# TODO: more descriptive rarity
if this_plant.rarity >= 2:
rarity_hint = "You feel like your plant is special."
output_text += rarity_hint + ".\n"
# if mature plant
if this_stage == 3:
color_options = [this_plant.color_dict[this_plant.color],
this_plant.color_dict[(this_plant.color+3) % len(this_plant.color_dict)],
@ -424,16 +426,18 @@ class CursedMenu(object):
def draw_plant_description(self, this_plant):
clear_bar = " " * (self.maxx-2) + "\n"
# load data
# format data
# If menu is currently showing something other than the description
if self.infotoggle != 1:
# Clear lines before printing description
output_string = clear_bar * (self.maxy - 15)
for y, line in enumerate(output_string.splitlines(), 2):
self.screen.addstr(y+12, 2, line)
self.screen.refresh()
# get plant description before printing
output_string = self.get_plant_description(this_plant)
self.infotoggle = 1
else:
# otherwise just set data as blanks
output_string = clear_bar * 3
self.infotoggle = 0
@ -442,6 +446,7 @@ class CursedMenu(object):
self.screen.refresh()
def draw_instructions(self):
# TODO: tidy this up
if not self.instructiontoggle:
instructions_txt = """welcome to botany. you've been given a seed
that will grow into a beautiful plant. check
@ -466,10 +471,15 @@ available in the readme :)
self.screen.addstr(self.maxy-12+y,self.maxx-47, line)
self.screen.refresh()
def harvest_confirmation():
#TODO: confirm users want to restart when harvesting
pass
def handle_request(self, request):
'''this is where you do things with the request'''
if request == None: return
if request == "harvest":
self.plant.start_over()
if request == "water":
self.plant.water()