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 * Curses-based menu system, optimized for 80x24 terminal
* ASCII art display of plant * ASCII art display of plant
* Persistent aging system that allows your plant to grow even when app is closed * 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. * 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 ### to-dos
* Finish garden feature * Finish garden feature
* Switch to database instead of .dat file
* Allows you to water neighbor's plants * Allows you to water neighbor's plants
* Harvest plant at end of life (gather seeds) * Harvest plant at end of life (gather seeds)
* Create harvest file with a log of all previous plants * Create harvest file with a log of all previous plants

139
botany.py
View File

@ -11,6 +11,8 @@ import threading
import errno import errno
import uuid import uuid
import fcntl import fcntl
import sqlite3
from collections import OrderedDict
from operator import itemgetter from operator import itemgetter
from menu_screen import * from menu_screen import *
@ -29,7 +31,6 @@ from menu_screen import *
# #
# build multiplayer # build multiplayer
# neighborhood system # neighborhood system
# - create plant id (sort of like userid)
# - list sorted by plantid that wraps so everybody has 2 neighbors :) # - list sorted by plantid that wraps so everybody has 2 neighbors :)
# - can water neighbors plant once (have to choose which) # - can water neighbors plant once (have to choose which)
# - pollination - seed is combination of your plant and neighbor plant # - 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)) uncommon_range = round((2/3)*(CONST_RARITY_MAX-common_range))
rare_range = round((2/3)*(CONST_RARITY_MAX-common_range-uncommon_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)) 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 common_max = common_range
uncommon_max = common_max + uncommon_range uncommon_max = common_max + uncommon_range
@ -299,17 +299,17 @@ class Plant(object):
time.sleep(1) time.sleep(1)
class DataManager(object): 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 user data, puts a .botany dir in user's home dir (OSX/Linux)
# handles shared data with sqlite db
user_dir = os.path.expanduser("~") user_dir = os.path.expanduser("~")
botany_dir = os.path.join(user_dir,'.botany') botany_dir = os.path.join(user_dir,'.botany')
game_dir = os.path.dirname(os.path.realpath(__file__)) game_dir = os.path.dirname(os.path.realpath(__file__))
this_user = getpass.getuser() this_user = getpass.getuser()
savefile_name = this_user + '_plant.dat' savefile_name = this_user + '_plant.dat'
savefile_path = os.path.join(botany_dir, savefile_name) 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') garden_json_path = os.path.join(game_dir, 'garden_file.json')
harvest_file_path = os.path.join(botany_dir, 'harvest_file.dat') harvest_file_path = os.path.join(botany_dir, 'harvest_file.dat')
harvest_json_path = os.path.join(botany_dir, 'harvest_file.json') harvest_json_path = os.path.join(botany_dir, 'harvest_file.json')
@ -349,18 +349,24 @@ class DataManager(object):
if is_dead: if is_dead:
self.save_plant(this_plant) self.save_plant(this_plant)
self.data_write_json(this_plant) self.data_write_json(this_plant)
self.garden_update(this_plant) self.update_garden_db(this_plant)
self.harvest_plant(this_plant) self.harvest_plant(this_plant)
this_plant.unlock_new_creation() this_plant.unlock_new_creation()
time.sleep(.1) time.sleep(.1)
def autosave(self, this_plant): def autosave(self, this_plant):
# running on thread, saves plant every 5s # running on thread, saves plant every 5s
file_update_count = 0
while True: while True:
file_update_count += 1
self.save_plant(this_plant) self.save_plant(this_plant)
self.data_write_json(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) time.sleep(5)
file_update_count %= 12
def load_plant(self): def load_plant(self):
# load savefile # load savefile
@ -391,54 +397,81 @@ class DataManager(object):
age_formatted = ("%dd:%dh:%dm:%ds" % (days, hours, minutes, age_seconds)) age_formatted = ("%dd:%dh:%dm:%ds" % (days, hours, minutes, age_seconds))
return age_formatted return age_formatted
def garden_update(self, this_plant): def init_database(self):
# garden is a dict of dicts # check if dir exists, create sqlite directory and set OS permissions to 777
# garden contains one entry for each plant id sqlite_dir_path = os.path.join(self.game_dir,'sqlite')
age_formatted = self.plant_age_convert(this_plant) if not os.path.exists(sqlite_dir_path):
this_plant_id = this_plant.plant_id os.makedirs(sqlite_dir_path)
plant_info = { os.chmod(sqlite_dir_path, 0777)
"owner":this_plant.owner, conn = sqlite3.connect(self.garden_db_path)
"description":this_plant.parse_plant(), init_table_string = """CREATE TABLE IF NOT EXISTS garden (
"age":age_formatted, plant_id tinytext PRIMARY KEY,
"score":this_plant.ticks, owner text,
"dead":this_plant.dead, description text,
} age text,
score integer,
is_dead numeric
)"""
if os.path.isfile(self.garden_file_path): c = conn.cursor()
# garden file exists: load data c.execute(init_table_string)
with open(self.garden_file_path, 'rb') as f: conn.close()
this_garden = pickle.load(f)
new_file_check = False # init only, creates and sets permissions for garden db and json
else: if os.stat(self.garden_db_path).st_uid == os.getuid():
# create empty garden list and initalize file permissions os.chmod(self.garden_db_path, 0666)
this_garden = {}
new_file_check = True
open(self.garden_file_path, 'a').close()
open(self.garden_json_path, 'a').close() 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) os.chmod(self.garden_json_path, 0666)
# if current plant ID isn't in garden list def update_garden_db(self, this_plant):
if this_plant.plant_id not in this_garden: # insert or update this plant id's entry in DB
this_garden[this_plant_id] = plant_info # TODO: is this needed?
# if plant ticks for id is greater than current ticks of plant id self.init_database()
else: age_formatted = self.plant_age_convert(this_plant)
current_plant_ticks = this_garden[this_plant_id]["score"] conn = sqlite3.connect(self.garden_db_path)
if this_plant.ticks >= current_plant_ticks: c = conn.cursor()
this_garden[this_plant_id] = plant_info # 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 def retrieve_garden_from_db(self):
with open(self.garden_file_path, 'wb') as f: # Builds a dict of dicts from garden sqlite db
pickle.dump(this_garden, f, protocol=2) garden_dict = {}
# dump json file 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: with open(self.garden_json_path, 'w') as outfile:
json.dump(this_garden, outfile) json.dump(this_garden, outfile)
pass
return new_file_check
def save_plant(self, this_plant): def save_plant(self, this_plant):
# create savefile # create savefile
@ -465,6 +498,9 @@ class DataManager(object):
json.dump(plant_info, outfile) json.dump(plant_info, outfile)
def harvest_plant(self, this_plant): 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 is a dict of dicts
# harvest contains one entry for each plant id # harvest contains one entry for each plant id
age_formatted = self.plant_age_convert(this_plant) age_formatted = self.plant_age_convert(this_plant)
@ -504,8 +540,7 @@ if __name__ == '__main__':
my_data.data_write_json(my_plant) my_data.data_write_json(my_plant)
my_plant.start_life() my_plant.start_life()
my_data.start_threads(my_plant) 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.save_plant(my_plant)
my_data.data_write_json(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): class CursedMenu(object):
#TODO: name your plant #TODO: name your plant
'''A class which abstracts the horrors of building a curses-based menu system''' '''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''' '''Initialization'''
self.initialized = False self.initialized = False
self.screen = curses.initscr() self.screen = curses.initscr()
@ -13,7 +13,7 @@ class CursedMenu(object):
curses.curs_set(0) curses.curs_set(0)
self.screen.keypad(1) self.screen.keypad(1)
self.plant = this_plant 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_string = self.plant.parse_plant()
self.plant_ticks = str(self.plant.ticks) self.plant_ticks = str(self.plant.ticks)
self.exit = False self.exit = False
@ -184,7 +184,6 @@ class CursedMenu(object):
def update_plant_live(self): def update_plant_live(self):
# updates plant data on menu screen, live! # updates plant data on menu screen, live!
# will eventually use this to display ascii art...
while not self.exit: while not self.exit:
self.plant_string = self.plant.parse_plant() self.plant_string = self.plant.parse_plant()
self.plant_ticks = str(self.plant.ticks) self.plant_ticks = str(self.plant.ticks)
@ -194,12 +193,12 @@ class CursedMenu(object):
time.sleep(1) time.sleep(1)
def get_user_input(self): def get_user_input(self):
# gets the user's input and acts appropriately # gets the user's input
try: try:
user_in = self.screen.getch() # Gets user input user_in = self.screen.getch() # Gets user input
except Exception as e: except Exception as e:
self.__exit__() 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.addstr(1, 1, str(user_in), curses.A_NORMAL)
# self.screen.refresh() # self.screen.refresh()
@ -252,9 +251,8 @@ class CursedMenu(object):
clear_bar = " " * (self.maxx-2) + "\n" clear_bar = " " * (self.maxx-2) + "\n"
clear_block = clear_bar * 5 clear_block = clear_bar * 5
control_keys = [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT] control_keys = [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT]
# load data # load data from sqlite db
with open(self.garden_file_path, 'rb') as f: this_garden = self.user_data.retrieve_garden_from_db()
this_garden = pickle.load(f)
# format data # format data
plant_table_pages = [] plant_table_pages = []
if self.infotoggle != 2: if self.infotoggle != 2:
@ -385,6 +383,7 @@ class CursedMenu(object):
this_stage_descriptions = stage_descriptions[this_stage] this_stage_descriptions = stage_descriptions[this_stage]
description_num = random.randint(0,len(this_stage_descriptions) - 1) description_num = random.randint(0,len(this_stage_descriptions) - 1)
# If not fully grown
if this_stage <= 4: if this_stage <= 4:
# Growth hint # Growth hint
if this_stage >= 1: if this_stage >= 1:
@ -398,6 +397,7 @@ class CursedMenu(object):
output_text += this_stage_descriptions[description_num] + "\n" output_text += this_stage_descriptions[description_num] + "\n"
# if seedling
if this_stage == 1: if this_stage == 1:
species_options = [this_plant.species_dict[this_plant.species], species_options = [this_plant.species_dict[this_plant.species],
this_plant.species_dict[(this_plant.species+3) % len(this_plant.species_dict)], 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] plant_hint = "It could be a(n) " + species_options[0] + ", " + species_options[1] + ", or " + species_options[2]
output_text += plant_hint + ".\n" output_text += plant_hint + ".\n"
# if young plant
if this_stage == 2: if this_stage == 2:
# TODO: more descriptive rarity # TODO: more descriptive rarity
if this_plant.rarity >= 2: if this_plant.rarity >= 2:
rarity_hint = "You feel like your plant is special." rarity_hint = "You feel like your plant is special."
output_text += rarity_hint + ".\n" output_text += rarity_hint + ".\n"
# if mature plant
if this_stage == 3: if this_stage == 3:
color_options = [this_plant.color_dict[this_plant.color], color_options = [this_plant.color_dict[this_plant.color],
this_plant.color_dict[(this_plant.color+3) % len(this_plant.color_dict)], 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): def draw_plant_description(self, this_plant):
clear_bar = " " * (self.maxx-2) + "\n" clear_bar = " " * (self.maxx-2) + "\n"
# load data # If menu is currently showing something other than the description
# format data
if self.infotoggle != 1: if self.infotoggle != 1:
# Clear lines before printing description
output_string = clear_bar * (self.maxy - 15) output_string = clear_bar * (self.maxy - 15)
for y, line in enumerate(output_string.splitlines(), 2): for y, line in enumerate(output_string.splitlines(), 2):
self.screen.addstr(y+12, 2, line) self.screen.addstr(y+12, 2, line)
self.screen.refresh() self.screen.refresh()
# get plant description before printing
output_string = self.get_plant_description(this_plant) output_string = self.get_plant_description(this_plant)
self.infotoggle = 1 self.infotoggle = 1
else: else:
# otherwise just set data as blanks
output_string = clear_bar * 3 output_string = clear_bar * 3
self.infotoggle = 0 self.infotoggle = 0
@ -442,6 +446,7 @@ class CursedMenu(object):
self.screen.refresh() self.screen.refresh()
def draw_instructions(self): def draw_instructions(self):
# TODO: tidy this up
if not self.instructiontoggle: if not self.instructiontoggle:
instructions_txt = """welcome to botany. you've been given a seed instructions_txt = """welcome to botany. you've been given a seed
that will grow into a beautiful plant. check 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.addstr(self.maxy-12+y,self.maxx-47, line)
self.screen.refresh() self.screen.refresh()
def harvest_confirmation():
#TODO: confirm users want to restart when harvesting
pass
def handle_request(self, request): def handle_request(self, request):
'''this is where you do things with the request''' '''this is where you do things with the request'''
if request == None: return if request == None: return
if request == "harvest": if request == "harvest":
self.plant.start_over() self.plant.start_over()
if request == "water": if request == "water":
self.plant.water() self.plant.water()