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
|
* 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
|
||||||
|
|
141
botany.py
141
botany.py
|
@ -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
|
os.chmod(self.garden_json_path, 0666)
|
||||||
# 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
|
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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue