Update garden handling to SQLite3
parent
59dada94b6
commit
2726b34c2f
|
@ -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
|
||||
|
|
141
botany.py
141
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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue