From 2726b34c2f27a0d681208d5ea990bbce52812861 Mon Sep 17 00:00:00 2001 From: Jake Funke Date: Thu, 23 Mar 2017 01:49:38 +0000 Subject: [PATCH] Update garden handling to SQLite3 --- README.md | 3 +- botany.py | 141 ++++++++++++++++++++++++++++++------------------- menu_screen.py | 32 +++++++---- 3 files changed, 110 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 24b8e5f..86099c4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/botany.py b/botany.py index f339fd0..c36ffff 100644 --- a/botany.py +++ b/botany.py @@ -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) + 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) diff --git a/menu_screen.py b/menu_screen.py index 2c3fe4f..769ca19 100644 --- a/menu_screen.py +++ b/menu_screen.py @@ -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()