Compare commits

..

1 Commits

Author SHA1 Message Date
Jake Funke b0876fb7e5 Tweaked text for menu screen display 2018-06-06 17:45:36 +00:00
10 changed files with 436 additions and 675 deletions

1
.gitignore vendored
View File

@ -3,4 +3,3 @@ garden_db.sqlite
garden_file.dat garden_file.dat
garden_file.json garden_file.json
sqlite/ sqlite/
*.swp

View File

@ -1,21 +1,22 @@
# botany # botany
![Screencap](https://tilde.town/~curiouser/botany.png) [![Screencap](https://asciinema.org/a/bbsqp00meurz3v5ltzc2jywif.png)](https://asciinema.org/a/bbsqp00meurz3v5ltzc2jywif)
by Jake Funke - jifunks@gmail.com - tilde.town/~curiouser - http://jakefunke.online/ by Jake Funke - jifunks@gmail.com - tilde.town/~curiouser - http://jakefunke.online/
A command line, realtime, community plant buddy. A command line, realtime, community plant buddy.
You've been given a seed that will grow into a beautiful plant. You've been given a seed that will grow into a beautiful plant.
Check in and water your plant every 24h to keep it growing. 5 days without water = death. Your plant depends on you and your friends to live! Check in and water your plant every 24h to keep it growing. 5 days without water = death. Your plant depends on you to live!
*"We do not 'come into' this world; we come out of it, as leaves from a tree." - Alan Watts* *"We do not 'come into' this world; we come out of it, as leaves from a tree." - Alan Watts*
## getting started ## getting started
botany is designed for unix-based systems. Clone into a local directory using `$ git clone https://github.com/jifunks/botany.git`. botany is designed for unix-based systems. Clone into a local directory using `$ git clone https://github.com/jifunks/botany.git`.
Run with `$ python3 botany.py`. Run with `$ python botany.py`.
*Note - botany.py must initially be run by the user who cloned/unzipped botany.py - this initalizes the shared data file permissions.* *Note - botany.py must initially be run by the user who cloned/unzipped
botany.py - this initalizes the shared data file permissions.*
Water your seed to get started. You can come and go as you please and your plant continues to grow. Water your seed to get started. You can come and go as you please and your plant continues to grow.
@ -23,8 +24,6 @@ Make sure to come back and water every 24 hours or your plant won't grow.
If your plant goes 5 days without water, it will die! Recruit your friends to water your plant for you! If your plant goes 5 days without water, it will die! Recruit your friends to water your plant for you!
A once-weekly cron on clear_weekly_users.py should be set up to keep weekly visitors tidy.
## features ## features
* Curses-based menu system, optimized for 80x24 terminal * Curses-based menu system, optimized for 80x24 terminal
@ -40,18 +39,14 @@ A once-weekly cron on clear_weekly_users.py should be set up to keep weekly visi
``` ```
{ {
"description":"common screaming mature jade plant", "description": "common singing blue seed-bearing poppy",
"generation":1, "file_name": "/Users/jakefunke/.botany/jakefunke_plant.dat",
"file_name":"/home/curiouser/.botany/curiouser_plant.dat", "age": "0d:2h:3m:16s",
"owner":"curiouser", "score": 1730,
"species":"jade plant", "owner": "jakefunke",
"stage":"mature", "is_dead": false,
"age":"24d:2h:16m:19s", "last_watered": 1489113197,
"rarity":"common", "generation": 2
"score":955337.0,
"mutation":"screaming",
"last_watered":1529007007,
"is_dead":false
} }
``` ```
@ -64,14 +59,8 @@ A once-weekly cron on clear_weekly_users.py should be set up to keep weekly visi
## requirements ## requirements
* Unix-based OS (Mac, Linux) * Unix-based OS (Mac, Linux)
* Python 3.x * Python 2.x
* Recommended: 80x24 minimum terminal, fixed-width font * Recommended: 80x24 minimum terminal, fixed-width font
## credits ## credits
* thank you [tilde.town](http://tilde.town/) for inspiration! * thank you tilde.town for inspiration!
## praise for botany
![Screencap](https://tilde.town/~curiouser/praise1.png)
![Screencap](https://tilde.town/~curiouser/praise2.png)
![Screencap](https://tilde.town/~curiouser/praise3.png)

View File

@ -1,10 +0,0 @@
/)) HAPPY
__(((__ HALLOWEEN
.' _`""`_`'.
/ /\\ /\\ \
| /)_\\/)_\\ |
| _ _()_ _ |
| \\/\\/\\// |
\ \/\/\/\/ /
. , .'.___..___.' _ ., _ .
^ ' ` '

View File

@ -1,67 +0,0 @@
#!/usr/bin/env python3
from botany import *
def ascii_render(filename):
# Prints ASCII art from file at given coordinates
this_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),"art")
this_filename = os.path.join(this_dir,filename)
this_file = open(this_filename,"r")
this_string = this_file.read()
this_file.close()
print(this_string)
def draw_plant_ascii(this_plant):
# this list should be somewhere where it could have been inherited, instead
# of hardcoded in more than one place...
plant_art_list = [
'poppy',
'cactus',
'aloe',
'flytrap',
'jadeplant',
'fern',
'daffodil',
'sunflower',
'baobab',
'lithops',
'hemp',
'pansy',
'iris',
'agave',
'ficus',
'moss',
'sage',
'snapdragon',
'columbine',
'brugmansia',
'palm',
'pachypodium',
]
if this_plant.dead == True:
ascii_render('rip.txt')
elif datetime.date.today().month == 10 and datetime.date.today().day == 31:
ascii_render('jackolantern.txt')
elif this_plant.stage == 0:
ascii_render('seed.txt')
elif this_plant.stage == 1:
ascii_render('seedling.txt')
elif this_plant.stage == 2:
this_filename = plant_art_list[this_plant.species]+'1.txt'
ascii_render(this_filename)
elif this_plant.stage == 3 or this_plant.stage == 5:
this_filename = plant_art_list[this_plant.species]+'2.txt'
ascii_render(this_filename)
elif this_plant.stage == 4:
this_filename = plant_art_list[this_plant.species]+'3.txt'
ascii_render(this_filename)
if __name__ == '__main__':
my_data = DataManager()
# if plant save file exists
if my_data.check_plant():
my_plant = my_data.load_plant()
# otherwise create new plant
else:
my_plant = Plant(my_data.savefile_path)
my_data.data_write_json(my_plant)
draw_plant_ascii(my_plant)

432
botany.py
View File

@ -1,50 +1,353 @@
#!/usr/bin/env python3 #!/usr/bin/python2
from __future__ import division
import time import time
import pickle import pickle
import json import json
import os import os
import random
import getpass import getpass
import threading import threading
import errno import errno
import uuid
import sqlite3 import sqlite3
import menu_screen as ms from menu_screen import *
from plant import Plant
# TODO: # TODO:
# - switch from personal data file to row in DB # - Switch from personal data file to table in DB
# - is threading necessary?
# - use a different curses window for plant, menu, info window, score
# notes from vilmibm class Plant(object):
# This is your plant!
stage_list = [
'seed',
'seedling',
'young',
'mature',
'flowering',
'seed-bearing',
]
# there are threads. color_list = [
# - life thread. sleeps a variable amount of time based on generation bonus. increases tick count (ticks == score). 'red',
# - screen: sleeps 1s per loop. draws interface (including plant). for seeing score/plant change without user input. 'orange',
# meanwhile, the main thread handles input and redraws curses as needed. 'yellow',
'green',
'blue',
'indigo',
'violet',
'white',
'black',
'gold',
'rainbow',
]
# affordance index rarity_list = [
# - main screen 'common',
# navigable menu, plant, score, etc 'uncommon',
# - water 'rare',
# render a visualization of moistness; allow to water 'legendary',
# - look 'godly',
# print a description of plant with info below rest of UI ]
# - garden
# runs a paginated view of every plant on the computer below rest of UI. to return to menu navigation must hit q.
# - visit
# runs a prompt underneath UI where you can see who recently visited you and type in a name to visit. must submit the prompt to get back to menu navigation.
# - instructions
# prints some explanatory text below the UI
# - exit
# quits program
# part of the complexity of all this is everything takes place in one curses window; thus, updates must be manually synchronized across the various logical parts of the screen. species_list = [
# ideally, multiple windows would be used: 'poppy',
# - the menu. it doesn't change unless the plant dies OR the plant hits stage 5, then "harvest" is dynamically added. 'cactus',
# - the plant viewer. this is updated in "real time" as the plant grows. 'aloe',
# - the status display: score and plant description 'venus flytrap',
# - the infow window. updated by visit/garden/instructions/look 'jade plant',
'fern',
'daffodil',
'sunflower',
'baobab',
'lithops',
'hemp',
'pansy',
'iris',
'agave',
'ficus',
'moss',
'sage',
'snapdragon',
'columbine',
'brugmansia',
'palm',
'pachypodium',
]
mutation_list = [
'',
'humming',
'noxious',
'vorpal',
'glowing',
'electric',
'icy',
'flaming',
'psychic',
'screaming',
'chaotic',
'hissing',
'gelatinous',
'deformed',
'shaggy',
'scaly',
'depressed',
'anxious',
'metallic',
'glossy',
'psychedelic',
'bonsai',
'foamy',
'singing',
'fractal',
'crunchy',
'goth',
'oozing',
'stinky',
'aromatic',
'juicy',
'smug',
'vibrating',
'lithe',
'chalky',
'naive',
'ersatz',
'disco',
'levitating',
'colossal',
'luminous',
'cosmic',
'ethereal',
]
def __init__(self, this_filename, generation=1):
# Constructor
self.plant_id = str(uuid.uuid4())
self.life_stages = (3600*24, (3600*24)*3, (3600*24)*10, (3600*24)*20, (3600*24)*30)
# self.life_stages = (2, 4, 6, 8, 10) # debug mode
self.stage = 0
self.mutation = 0
self.species = random.randint(0,len(self.species_list)-1)
self.color = random.randint(0,len(self.color_list)-1)
self.rarity = self.rarity_check()
self.ticks = 0
self.age_formatted = "0"
self.generation = generation
self.dead = False
self.write_lock = False
self.owner = getpass.getuser()
self.file_name = this_filename
self.start_time = int(time.time())
self.last_time = int(time.time())
# must water plant first day
self.watered_timestamp = int(time.time())-(24*3600)-1
self.watered_24h = False
self.visitors = []
def migrate_properties(self):
# Migrates old data files to new
if not hasattr(self, 'generation'):
self.generation = 1
if not hasattr(self, 'visitors'):
self.visitors = []
def parse_plant(self):
# Converts plant data to human-readable format
output = ""
if self.stage >= 3:
output += self.rarity_list[self.rarity] + " "
if self.mutation != 0:
output += self.mutation_list[self.mutation] + " "
if self.stage >= 4:
output += self.color_list[self.color] + " "
output += self.stage_list[self.stage] + " "
if self.stage >= 2:
output += self.species_list[self.species] + " "
return output.strip()
def rarity_check(self):
# Generate plant rarity
CONST_RARITY_MAX = 256.0
rare_seed = random.randint(1,CONST_RARITY_MAX)
common_range = round((2/3)*CONST_RARITY_MAX)
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))
common_max = common_range
uncommon_max = common_max + uncommon_range
rare_max = uncommon_max + rare_range
legendary_max = rare_max + legendary_range
godly_max = CONST_RARITY_MAX
if 0 <= rare_seed <= common_max:
rarity = 0
elif common_max < rare_seed <= uncommon_max:
rarity = 1
elif uncommon_max < rare_seed <= rare_max:
rarity = 2
elif rare_max < rare_seed <= legendary_max:
rarity = 3
elif legendary_max < rare_seed <= godly_max:
rarity = 4
return rarity
def dead_check(self):
# if it has been >5 days since watering, sorry plant is dead :(
time_delta_watered = int(time.time()) - self.watered_timestamp
if time_delta_watered > (5 * (24 * 3600)):
self.dead = True
return self.dead
def update_visitor_db(self, visitor_names):
game_dir = os.path.dirname(os.path.realpath(__file__))
garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
conn = sqlite3.connect(garden_db_path)
for name in (visitor_names):
c = conn.cursor()
c.execute("SELECT * FROM visitors WHERE garden_name = '{}' AND visitor_name = '{}' ".format(self.owner, name))
data=c.fetchone()
if data is None:
sql = """ INSERT INTO visitors (garden_name,visitor_name,weekly_visits) VALUES('{}', '{}',1)""".format(self.owner, name)
c.execute(sql)
else:
sql = """ UPDATE visitors SET weekly_visits = weekly_visits + 1 WHERE garden_name = '{}' AND visitor_name = '{}'""".format(self.owner, name)
c.execute(sql)
conn.commit()
conn.close()
def guest_check(self):
user_dir = os.path.expanduser("~")
botany_dir = os.path.join(user_dir,'.botany')
visitor_filepath = os.path.join(botany_dir,'visitors.json')
latest_timestamp = 0
visitors_this_check = []
if os.path.isfile(visitor_filepath):
with open(visitor_filepath, 'r') as visitor_file:
data = json.load(visitor_file)
# TODO: this needs to check if the latest timestamp is greater
# than 5 days
# need to check for delta of at minimum 5 days between waters
# to make sure plant is alive
# check each visitor time, calculate delta between this and last
# watering
if data:
for element in data:
if element['user'] not in self.visitors:
self.visitors.append(element['user'])
if element['user'] not in visitors_this_check:
visitors_this_check.append(element['user'])
# prevent users from manually setting watered_time in the future
if element['timestamp'] < int(time.time()):
# need to check here for delta between this
# element and last element (also json load might
# not be sorted...
if element['timestamp'] > latest_timestamp:
latest_timestamp = element['timestamp']
try:
self.update_visitor_db(visitors_this_check)
except:
pass
with open(visitor_filepath, 'w') as visitor_file:
visitor_file.write('[]')
else:
with open(visitor_filepath, mode='w') as f:
json.dump([], f)
os.chmod(visitor_filepath, 0666)
return latest_timestamp
def water_check(self):
latest_visitor_timestamp = self.guest_check()
if latest_visitor_timestamp > self.watered_timestamp:
visitor_delta_watered = latest_visitor_timestamp - self.watered_timestamp
if visitor_delta_watered <= (5 * (24 * 3600)):
self.watered_timestamp = latest_visitor_timestamp
self.time_delta_watered = int(time.time()) - self.watered_timestamp
if self.time_delta_watered <= (24 * 3600):
if not self.watered_24h:
self.watered_24h = True
return True
else:
self.watered_24h = False
return False
def mutate_check(self):
# Create plant mutation
# Increase this # to make mutation rarer (chance 1 out of x each second)
CONST_MUTATION_RARITY = 20000
mutation_seed = random.randint(1,CONST_MUTATION_RARITY)
if mutation_seed == CONST_MUTATION_RARITY:
# mutation gained!
mutation = random.randint(0,len(self.mutation_list)-1)
if self.mutation == 0:
self.mutation = mutation
return True
else:
return False
def growth(self):
# Increase plant growth stage
if self.stage < (len(self.stage_list)-1):
self.stage += 1
def water(self):
# Increase plant growth stage
if not self.dead:
self.watered_timestamp = int(time.time())
self.watered_24h = True
def start_over(self):
# After plant reaches final stage, given option to restart
# increment generation only if previous stage is final stage and plant
# is alive
if not self.dead:
next_generation = self.generation + 1
else:
# Should this reset to 1? Seems unfair.. for now generations will
# persist through death.
next_generation = self.generation
self.write_lock = True
self.kill_plant()
while self.write_lock:
# Wait for garden writer to unlock
# garden db needs to update before allowing the user to reset
pass
if not self.write_lock:
self.__init__(self.file_name, next_generation)
def kill_plant(self):
self.dead = True
def unlock_new_creation(self):
self.write_lock = False
def start_life(self):
# runs life on a thread
thread = threading.Thread(target=self.life, args=())
thread.daemon = True
thread.start()
def life(self):
# I've created life :)
while True:
if not self.dead:
if self.watered_24h:
self.ticks += 1
if self.stage < len(self.stage_list)-1:
if self.ticks >= self.life_stages[self.stage]:
self.growth()
if self.mutate_check():
pass
if self.water_check():
# Do something
pass
if self.dead_check():
# Do something else
pass
# TODO: event check
generation_bonus = 0.2 * (self.generation - 1)
adjusted_sleep_time = 1 / (1 + generation_bonus)
time.sleep(adjusted_sleep_time)
class DataManager(object): class DataManager(object):
# 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)
@ -81,11 +384,47 @@ class DataManager(object):
def check_plant(self): def check_plant(self):
# check for existing save file # check for existing save file
if os.path.isfile(self.savefile_path) and os.path.getsize(self.savefile_path) > 0: if os.path.isfile(self.savefile_path):
return True return True
else: else:
return False return False
def start_threads(self,this_plant):
# creates threads to save files every minute
death_check_thread = threading.Thread(target=self.death_check_update, args=(this_plant,))
death_check_thread.daemon = True
death_check_thread.start()
autosave_thread = threading.Thread(target=self.autosave, args=(this_plant,))
autosave_thread.daemon = True
autosave_thread.start()
def death_check_update(self,this_plant):
# .1 second updates and lock to minimize race condition
while True:
is_dead = this_plant.dead_check()
if is_dead:
self.save_plant(this_plant)
self.data_write_json(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 TODO: this is unnecessary
# and breaks shit probably
file_update_count = 0
while True:
file_update_count += 1
self.save_plant(this_plant)
self.data_write_json(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): def load_plant(self):
# load savefile # load savefile
with open(self.savefile_path, 'rb') as f: with open(self.savefile_path, 'rb') as f:
@ -96,8 +435,8 @@ class DataManager(object):
this_plant.migrate_properties() this_plant.migrate_properties()
# get status since last login # get status since last login
is_watered = this_plant.water_check()
is_dead = this_plant.dead_check() is_dead = this_plant.dead_check()
is_watered = this_plant.water_check()
if not is_dead: if not is_dead:
if is_watered: if is_watered:
@ -107,7 +446,7 @@ class DataManager(object):
self.last_water_gain = time.time() self.last_water_gain = time.time()
else: else:
ticks_to_add = 0 ticks_to_add = 0
this_plant.ticks += ticks_to_add * round(0.2 * (this_plant.generation - 1) + 1, 1) this_plant.ticks += ticks_to_add * (0.2 * (this_plant.generation - 1) + 1)
return this_plant return this_plant
def plant_age_convert(self,this_plant): def plant_age_convert(self,this_plant):
@ -124,7 +463,7 @@ class DataManager(object):
sqlite_dir_path = os.path.join(self.game_dir,'sqlite') sqlite_dir_path = os.path.join(self.game_dir,'sqlite')
if not os.path.exists(sqlite_dir_path): if not os.path.exists(sqlite_dir_path):
os.makedirs(sqlite_dir_path) os.makedirs(sqlite_dir_path)
os.chmod(sqlite_dir_path, 0o777) os.chmod(sqlite_dir_path, 0777)
conn = sqlite3.connect(self.garden_db_path) conn = sqlite3.connect(self.garden_db_path)
init_table_string = """CREATE TABLE IF NOT EXISTS garden ( init_table_string = """CREATE TABLE IF NOT EXISTS garden (
plant_id tinytext PRIMARY KEY, plant_id tinytext PRIMARY KEY,
@ -141,9 +480,9 @@ class DataManager(object):
# init only, creates and sets permissions for garden db and json # init only, creates and sets permissions for garden db and json
if os.stat(self.garden_db_path).st_uid == os.getuid(): if os.stat(self.garden_db_path).st_uid == os.getuid():
os.chmod(self.garden_db_path, 0o666) os.chmod(self.garden_db_path, 0666)
open(self.garden_json_path, 'a').close() open(self.garden_json_path, 'a').close()
os.chmod(self.garden_json_path, 0o666) os.chmod(self.garden_json_path, 0666)
def migrate_database(self): def migrate_database(self):
conn = sqlite3.connect(self.garden_db_path) conn = sqlite3.connect(self.garden_db_path)
@ -160,6 +499,8 @@ class DataManager(object):
def update_garden_db(self, this_plant): def update_garden_db(self, this_plant):
# insert or update this plant id's entry in DB # insert or update this plant id's entry in DB
# TODO: make sure other instances of user are deleted
# Could create a clean db function
self.init_database() self.init_database()
self.migrate_database() self.migrate_database()
age_formatted = self.plant_age_convert(this_plant) age_formatted = self.plant_age_convert(this_plant)
@ -178,13 +519,6 @@ class DataManager(object):
psco = str(this_plant.ticks), psco = str(this_plant.ticks),
pdead = int(this_plant.dead)) pdead = int(this_plant.dead))
c.execute(update_query) c.execute(update_query)
# clean other instances of user
clean_query = """UPDATE garden set is_dead = 1
where owner = '{pown}'
and plant_id <> '{pid}'
""".format(pown = this_plant.owner,
pid = this_plant.plant_id)
c.execute(clean_query)
conn.commit() conn.commit()
conn.close() conn.close()
@ -295,12 +629,12 @@ if __name__ == '__main__':
my_plant = Plant(my_data.savefile_path) my_plant = Plant(my_data.savefile_path)
my_data.data_write_json(my_plant) my_data.data_write_json(my_plant)
# my_plant is either a fresh plant or an existing plant at this point # my_plant is either a fresh plant or an existing plant at this point
my_plant.start_life(my_data) my_plant.start_life()
my_data.start_threads(my_plant)
try: try:
botany_menu = ms.CursedMenu(my_plant,my_data) 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.update_garden_db(my_plant) my_data.update_garden_db(my_plant)
finally: finally:
ms.cleanup() cleanup()

View File

@ -6,6 +6,6 @@ garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
conn = sqlite3.connect(garden_db_path) conn = sqlite3.connect(garden_db_path)
c = conn.cursor() c = conn.cursor()
c.execute("DELETE FROM visitors") c.execute("DELETE FROM visitors")
print("Cleared weekly users") print "Cleared weekly users"
conn.commit() conn.commit()
conn.close() conn.close()

View File

@ -1,55 +0,0 @@
class LoginCompleter:
""" A loop-based completion system for logins """
def __init__(self, menu):
self.s = ""
self.logins = None
self.completions = []
# completion_id has a value of -1 for the base user input
# and between 0 and len(completions)-1 for completions
self.completion_id = -1
self.completion_base = ""
self.menu = menu
def initialize(self):
""" Initialise the list of completable logins """
garden = self.menu.user_data.retrieve_garden_from_db()
self.logins = set()
for plant_id in garden:
if not garden[plant_id]:
continue
entry = garden[plant_id]
if "owner" in entry:
self.logins.add(entry["owner"])
self.logins = sorted(list(self.logins))
def update_input(self, s):
""" Update the user input and reset completion base """
self.s = s
self.completion_base = self.s
self.completion_id = -1
def complete(self, direction = 1):
"""
Returns the completed string from the user input
Loops forward in the list of logins if direction is positive, and
backwards if direction is negative
"""
def loginFilter(x):
return x.startswith(self.s) & (x != self.s)
# Refresh possible completions after the user edits
if self.completion_id == -1:
if self.logins is None:
self.initialize()
self.completion_base = self.s
self.completions = list(filter(loginFilter, self.logins))
self.completion_id += direction
# Loop from the back
if self.completion_id == -2:
self.completion_id = len(self.completions) - 1
# If we are at the base input, return it
if self.completion_id == -1 or self.completion_id == len(self.completions):
self.completion_id = -1
return self.completion_base
return self.completions[self.completion_id]

View File

@ -10,8 +10,6 @@ import json
import sqlite3 import sqlite3
import string import string
import re import re
import completer
import datetime
class CursedMenu(object): class CursedMenu(object):
#TODO: name your plant #TODO: name your plant
@ -36,7 +34,7 @@ class CursedMenu(object):
self.visited_plant = None self.visited_plant = None
self.user_data = this_data self.user_data = this_data
self.plant_string = self.plant.parse_plant() self.plant_string = self.plant.parse_plant()
self.plant_ticks = str(int(self.plant.ticks)) self.plant_ticks = str(self.plant.ticks)
self.exit = False self.exit = False
self.infotoggle = 0 self.infotoggle = 0
self.maxy, self.maxx = self.screen.getmaxyx() self.maxy, self.maxx = self.screen.getmaxyx()
@ -51,13 +49,10 @@ class CursedMenu(object):
screen_thread = threading.Thread(target=self.update_plant_live, args=()) screen_thread = threading.Thread(target=self.update_plant_live, args=())
screen_thread.daemon = True screen_thread.daemon = True
screen_thread.start() screen_thread.start()
# Recusive lock to prevent both threads from drawing at the same time
self.screen_lock = threading.RLock()
self.screen.clear() self.screen.clear()
self.show(["water","look","garden","visit", "instructions"], title=' botany ', subtitle='options') self.show(["water","look","garden","visit", "instructions"], title=' botany ', subtitle='options')
def define_colors(self): def define_colors(self):
# TODO: implement colors
# set curses color pairs manually # set curses color pairs manually
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)
@ -80,23 +75,26 @@ class CursedMenu(object):
def update_options(self): def update_options(self):
# Makes sure you can get a new plant if it dies # Makes sure you can get a new plant if it dies
if self.plant.dead or self.plant.stage == 5: if self.plant.dead:
if "harvest" not in self.options: if "harvest" not in self.options:
self.options.insert(-1,"harvest") self.options.insert(-1,"harvest")
else: else:
if "harvest" in self.options: if self.plant.stage == 5:
self.options.remove("harvest") if "harvest" not in self.options:
self.options.insert(-1,"harvest")
else:
if "harvest" in self.options:
self.options.remove("harvest")
def set_options(self, options): def set_options(self, options):
# Validates that the last option is "exit" # Validates that the last option is "exit"
if options[-1] != 'exit': if options[-1] is not 'exit':
options.append('exit') options.append('exit')
self.options = options self.options = options
def draw(self): def draw(self):
# Draw the menu and lines # Draw the menu and lines
self.maxy, self.maxx = self.screen.getmaxyx() self.maxy, self.maxx = self.screen.getmaxyx()
self.screen_lock.acquire()
self.screen.refresh() self.screen.refresh()
try: try:
self.draw_default() self.draw_default()
@ -108,13 +106,12 @@ class CursedMenu(object):
self.screen.refresh() self.screen.refresh()
self.__exit__() self.__exit__()
traceback.print_exc() traceback.print_exc()
self.screen_lock.release()
def draw_menu(self): def draw_menu(self):
# Actually draws the menu and handles branching # Actually draws the menu and handles branching
request = "" request = ""
try: try:
while request != "exit": while request is not "exit":
self.draw() self.draw()
request = self.get_user_input() request = self.get_user_input()
self.handle_request(request) self.handle_request(request)
@ -127,10 +124,6 @@ class CursedMenu(object):
self.screen.refresh() self.screen.refresh()
self.__exit__() self.__exit__()
#traceback.print_exc() #traceback.print_exc()
except IOError as exception:
self.screen.clear()
self.screen.refresh()
self.__exit__()
def ascii_render(self, filename, ypos, xpos): def ascii_render(self, filename, ypos, xpos):
# Prints ASCII art from file at given coordinates # Prints ASCII art from file at given coordinates
@ -139,11 +132,9 @@ class CursedMenu(object):
this_file = open(this_filename,"r") this_file = open(this_filename,"r")
this_string = this_file.readlines() this_string = this_file.readlines()
this_file.close() this_file.close()
self.screen_lock.acquire()
for y, line in enumerate(this_string, 2): for y, line in enumerate(this_string, 2):
self.screen.addstr(ypos+y, xpos, line, curses.A_NORMAL) self.screen.addstr(ypos+y, xpos, line, curses.A_NORMAL)
# self.screen.refresh() # self.screen.refresh()
self.screen_lock.release()
def draw_plant_ascii(self, this_plant): def draw_plant_ascii(self, this_plant):
ypos = 0 ypos = 0
@ -174,8 +165,6 @@ class CursedMenu(object):
] ]
if this_plant.dead == True: if this_plant.dead == True:
self.ascii_render('rip.txt', ypos, xpos) self.ascii_render('rip.txt', ypos, xpos)
elif datetime.date.today().month == 10 and datetime.date.today().day == 31:
self.ascii_render('jackolantern.txt', ypos, xpos)
elif this_plant.stage == 0: elif this_plant.stage == 0:
self.ascii_render('seed.txt', ypos, xpos) self.ascii_render('seed.txt', ypos, xpos)
elif this_plant.stage == 1: elif this_plant.stage == 1:
@ -193,7 +182,6 @@ class CursedMenu(object):
def draw_default(self): def draw_default(self):
# draws default menu # draws default menu
clear_bar = " " * (int(self.maxx*2/3)) clear_bar = " " * (int(self.maxx*2/3))
self.screen_lock.acquire()
self.screen.addstr(1, 2, self.title, curses.A_STANDOUT) # Title for this menu self.screen.addstr(1, 2, self.title, curses.A_STANDOUT) # Title for this menu
self.screen.addstr(3, 2, self.subtitle, curses.A_BOLD) #Subtitle for this menu self.screen.addstr(3, 2, self.subtitle, curses.A_BOLD) #Subtitle for this menu
# clear menu on screen # clear menu on screen
@ -228,7 +216,6 @@ class CursedMenu(object):
self.draw_plant_ascii(self.visited_plant) self.draw_plant_ascii(self.visited_plant)
else: else:
self.draw_plant_ascii(self.plant) self.draw_plant_ascii(self.plant)
self.screen_lock.release()
def water_gauge(self): def water_gauge(self):
# build nice looking water gauge # build nice looking water gauge
@ -255,10 +242,8 @@ class CursedMenu(object):
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__()
if user_in == -1: # Input comes from pipe/file and is closed
raise IOError
## DEBUG KEYS - enable these lines to see curses key codes ## DEBUG KEYS - enable these lines to see curses key codes
# self.screen.addstr(2, 2, str(user_in), curses.A_NORMAL) # self.screen.addstr(1, 1, str(user_in), curses.A_NORMAL)
# self.screen.refresh() # self.screen.refresh()
# Resize sends curses.KEY_RESIZE, update display # Resize sends curses.KEY_RESIZE, update display
@ -267,17 +252,14 @@ class CursedMenu(object):
self.screen.clear() self.screen.clear()
self.screen.refresh() self.screen.refresh()
# enter, exit, and Q Keys are special cases # enter and exit Keys are special cases
if user_in == 10: if user_in == 10:
return self.options[self.selected] return self.options[self.selected]
if user_in == 27: if user_in == 27:
return self.options[-1] return self.options[-1]
if user_in == 113:
self.selected = len(self.options) - 1
return
# this is a number; check to see if we can set it # this is a number; check to see if we can set it
if user_in >= ord('1') and user_in <= ord(str(min(7,len(self.options)))): if user_in >= ord('1') and user_in <= ord(str(min(9,len(self.options)+1))):
self.selected = user_in - ord('0') - 1 # convert keypress back to a number, then subtract 1 to get index self.selected = user_in - ord('0') - 1 # convert keypress back to a number, then subtract 1 to get index
return return
@ -308,21 +290,11 @@ class CursedMenu(object):
return plant_table return plant_table
def format_garden_entry(self, entry): def format_garden_entry(self, entry):
return "{:14.14} - {:>16} - {:>8}p - {}".format(*entry) return "{:14} - {:>16} - {:>8}p - {}".format(*entry)
def sort_garden_table(self, table, column, ascending): def sort_garden_table(self, table, column, ascending):
""" Sort table in place by a specified column """ """ Sort table in place by a specified column """
def key(entry): return table.sort(key=lambda x: x[column], reverse=not ascending)
entry = entry[column]
# In when sorting ages, convert to seconds
if column == 1:
coeffs = [24*60*60, 60*60, 60, 1]
nums = [int(n[:-1]) for n in entry.split(":")]
if len(nums) == len(coeffs):
entry = sum(nums[i] * coeffs[i] for i in range(len(nums)))
return entry
return table.sort(key=key, reverse=not ascending)
def filter_garden_table(self, table, pattern): def filter_garden_table(self, table, pattern):
""" Filter table using a pattern, and return the new table """ """ Filter table using a pattern, and return the new table """
@ -356,6 +328,7 @@ class CursedMenu(object):
self.infotoggle = 2 self.infotoggle = 2
# print garden information OR clear it # print garden information OR clear it
# TODO: pagination control with hjkl/arrow keys/esc-or-x to close
index = 0 index = 0
sort_column, sort_ascending = 0, True sort_column, sort_ascending = 0, True
sort_keys = ["n", "a", "s", "d"] # Name, Age, Score, Description sort_keys = ["n", "a", "s", "d"] # Name, Age, Score, Description
@ -366,16 +339,12 @@ class CursedMenu(object):
index_max = min(len(plant_table), index + entries_per_page) index_max = min(len(plant_table), index + entries_per_page)
plants = plant_table[index:index_max] plants = plant_table[index:index_max]
page = [self.format_garden_entry(entry) for entry in plants] page = [self.format_garden_entry(entry) for entry in plants]
self.screen_lock.acquire()
self.draw_info_text(page) self.draw_info_text(page)
# Multiple pages, paginate and require keypress # Multiple pages, paginate and require keypress
page_text = "(%d-%d/%d) | sp/next | bksp/prev | s <col #>/sort | f/filter | q/quit" % (index, index_max, len(plant_table)) page_text = "(%d-%d/%d) | sp/next | bksp/prev | s <col #>/sort | f/filter | q/quit" % (index, index_max, len(plant_table))
self.screen.addstr(self.maxy-2, 2, page_text) self.screen.addstr(self.maxy-2, 2, page_text)
self.screen.refresh() self.screen.refresh()
self.screen_lock.release()
c = self.screen.getch() c = self.screen.getch()
if c == -1: # Input comes from pipe/file and is closed
raise IOError
self.infotoggle = 0 self.infotoggle = 0
# Quit # Quit
@ -398,8 +367,6 @@ class CursedMenu(object):
# Sort entries # Sort entries
elif c == ord("s"): elif c == ord("s"):
c = self.screen.getch() c = self.screen.getch()
if c == -1: # Input comes from pipe/file and is closed
raise IOError
column = -1 column = -1
if c < 255 and chr(c) in sort_keys: if c < 255 and chr(c) in sort_keys:
column = sort_keys.index(chr(c)) column = sort_keys.index(chr(c))
@ -564,7 +531,7 @@ class CursedMenu(object):
# get plant description before printing # get plant description before printing
output_string = self.get_plant_description(this_plant) output_string = self.get_plant_description(this_plant)
growth_multiplier = 1 + (0.2 * (this_plant.generation-1)) growth_multiplier = 1 + (0.2 * (this_plant.generation-1))
output_string += "Generation: {}\nGrowth rate: {:.1f}x".format(self.plant.generation, growth_multiplier) output_string += "Generation: {}\nGrowth rate: {}".format(self.plant.generation, growth_multiplier)
self.draw_info_text(output_string) self.draw_info_text(output_string)
self.infotoggle = 1 self.infotoggle = 1
else: else:
@ -581,7 +548,6 @@ class CursedMenu(object):
"growing. 5 days without water = death. your\n" "growing. 5 days without water = death. your\n"
"plant depends on you & your friends to live!\n" "plant depends on you & your friends to live!\n"
"more info is available in the readme :)\n" "more info is available in the readme :)\n"
"https://github.com/jifunks/botany/blob/master/README.md\n"
" cheers,\n" " cheers,\n"
" curio\n" " curio\n"
) )
@ -592,18 +558,15 @@ class CursedMenu(object):
def clear_info_pane(self): def clear_info_pane(self):
# Clears bottom part of screen # Clears bottom part of screen
self.screen_lock.acquire()
clear_bar = " " * (self.maxx - 3) clear_bar = " " * (self.maxx - 3)
this_y = 14 this_y = 14
while this_y < self.maxy: while this_y < self.maxy:
self.screen.addstr(this_y, 2, clear_bar, curses.A_NORMAL) self.screen.addstr(this_y, 2, clear_bar, curses.A_NORMAL)
this_y += 1 this_y += 1
self.screen.refresh() self.screen.refresh()
self.screen_lock.release()
def draw_info_text(self, info_text, y_offset = 0): def draw_info_text(self, info_text, y_offset = 0):
# print lines of text to info pane at bottom of screen # print lines of text to info pane at bottom of screen
self.screen_lock.acquire()
if type(info_text) is str: if type(info_text) is str:
info_text = info_text.splitlines() info_text = info_text.splitlines()
for y, line in enumerate(info_text, 2): for y, line in enumerate(info_text, 2):
@ -613,7 +576,6 @@ class CursedMenu(object):
if this_y < self.maxy: if this_y < self.maxy:
self.screen.addstr(this_y, 2, line, curses.A_NORMAL) self.screen.addstr(this_y, 2, line, curses.A_NORMAL)
self.screen.refresh() self.screen.refresh()
self.screen_lock.release()
def harvest_confirmation(self): def harvest_confirmation(self):
self.clear_info_pane() self.clear_info_pane()
@ -630,10 +592,8 @@ class CursedMenu(object):
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__()
if user_in == -1: # Input comes from pipe/file and is closed
raise IOError
if user_in in [ord('Y'), ord('y'), 10]: if user_in in [ord('Y'), ord('y')]:
self.plant.start_over() self.plant.start_over()
else: else:
pass pass
@ -684,36 +644,22 @@ class CursedMenu(object):
visitor_block = 'nobody :(' visitor_block = 'nobody :('
return visitor_block return visitor_block
def get_user_string(self, xpos=3, ypos=15, filterfunc=str.isalnum, completer=None): def get_user_string(self, xpos=3, ypos=15, filterfunc=str.isalnum):
# filter allowed characters using filterfunc, alphanumeric by default # filter allowed characters using filterfunc, alphanumeric by default
user_string = "" user_string = ""
user_input = 0 user_input = 0
if completer:
completer = completer(self)
while user_input != 10: while user_input != 10:
user_input = self.screen.getch() user_input = self.screen.getch()
if user_input == -1: # Input comes from pipe/file and is closed
raise IOError
self.screen_lock.acquire()
# osx and unix backspace chars... # osx and unix backspace chars...
if user_input == 127 or user_input == 263: if user_input == 127 or user_input == 263:
if len(user_string) > 0: if len(user_string) > 0:
user_string = user_string[:-1] user_string = user_string[:-1]
if completer:
completer.update_input(user_string)
self.screen.addstr(ypos, xpos, " " * (self.maxx-xpos-1)) self.screen.addstr(ypos, xpos, " " * (self.maxx-xpos-1))
elif user_input in [ord('\t'), curses.KEY_BTAB] and completer: if user_input < 256 and user_input != 10:
direction = 1 if user_input == ord('\t') else -1 if filterfunc(chr(user_input)):
user_string = completer.complete(direction)
self.screen.addstr(ypos, xpos, " " * (self.maxx-xpos-1))
elif user_input < 256 and user_input != 10:
if filterfunc(chr(user_input)) or chr(user_input) == '_':
user_string += chr(user_input) user_string += chr(user_input)
if completer:
completer.update_input(user_string)
self.screen.addstr(ypos, xpos, str(user_string)) self.screen.addstr(ypos, xpos, str(user_string))
self.screen.refresh() self.screen.refresh()
self.screen_lock.release()
return user_string return user_string
def visit_handler(self): def visit_handler(self):
@ -728,7 +674,7 @@ class CursedMenu(object):
weekly_visitor_text = self.get_weekly_visitors() weekly_visitor_text = self.get_weekly_visitors()
self.draw_info_text("this week you've been visited by: ", 6) self.draw_info_text("this week you've been visited by: ", 6)
self.draw_info_text(weekly_visitor_text, 7) self.draw_info_text(weekly_visitor_text, 7)
guest_garden = self.get_user_string(completer = completer.LoginCompleter) guest_garden = self.get_user_string()
if not guest_garden: if not guest_garden:
self.clear_info_pane() self.clear_info_pane()
return None return None
@ -747,35 +693,26 @@ class CursedMenu(object):
self.visited_plant = self.get_visited_plant(visitor_data) self.visited_plant = self.get_visited_plant(visitor_data)
guest_visitor_file = home_folder + "/{}/.botany/visitors.json".format(guest_garden, guest_garden) guest_visitor_file = home_folder + "/{}/.botany/visitors.json".format(guest_garden, guest_garden)
if os.path.isfile(guest_visitor_file): if os.path.isfile(guest_visitor_file):
water_success = self.water_on_visit(guest_visitor_file) self.water_on_visit(guest_visitor_file)
if water_success: self.screen.addstr(16, 2, "...you watered ~{}'s {}...".format(str(guest_garden), guest_plant_description))
self.screen.addstr(16, 2, "...you watered ~{}'s {}...".format(str(guest_garden), guest_plant_description)) if self.visited_plant:
if self.visited_plant: self.draw_plant_ascii(self.visited_plant)
self.draw_plant_ascii(self.visited_plant)
else:
self.screen.addstr(16, 2, "{}'s garden is locked, but you can see in...".format(guest_garden))
else: else:
self.screen.addstr(16, 2, "i can't seem to find directions to {}...".format(guest_garden)) self.screen.addstr(16, 2, "i can't seem to find directions to {}...".format(guest_garden))
try: self.screen.getch()
self.screen.getch() self.clear_info_pane()
self.clear_info_pane() self.draw_plant_ascii(self.plant)
self.draw_plant_ascii(self.plant) self.visited_plant = None
finally:
self.visited_plant = None
def water_on_visit(self, guest_visitor_file): def water_on_visit(self, guest_visitor_file):
visitor_data = {} visitor_data = {}
# using -1 here so that old running instances can be watered guest_data = {'user': getpass.getuser(), 'timestamp': int(time.time())}
guest_data = {'user': getpass.getuser(), 'timestamp': int(time.time()) - 1}
if os.path.isfile(guest_visitor_file): if os.path.isfile(guest_visitor_file):
if not os.access(guest_visitor_file, os.W_OK):
return False
with open(guest_visitor_file) as f: with open(guest_visitor_file) as f:
visitor_data = json.load(f) visitor_data = json.load(f)
visitor_data.append(guest_data) visitor_data.append(guest_data)
with open(guest_visitor_file, mode='w') as f: with open(guest_visitor_file, mode='w') as f:
f.write(json.dumps(visitor_data, indent=2)) f.write(json.dumps(visitor_data, indent=2))
return True
def get_visited_plant(self, visitor_data): def get_visited_plant(self, visitor_data):
""" Returns a drawable pseudo plant object from json data """ """ Returns a drawable pseudo plant object from json data """

366
plant.py
View File

@ -1,366 +0,0 @@
import random
import os
import json
import threading
import time
import uuid
import getpass
class Plant:
# This is your plant!
stage_list = [
'seed',
'seedling',
'young',
'mature',
'flowering',
'seed-bearing',
]
color_list = [
'red',
'orange',
'yellow',
'green',
'blue',
'indigo',
'violet',
'white',
'black',
'gold',
'rainbow',
]
rarity_list = [
'common',
'uncommon',
'rare',
'legendary',
'godly',
]
species_list = [
'poppy',
'cactus',
'aloe',
'venus flytrap',
'jade plant',
'fern',
'daffodil',
'sunflower',
'baobab',
'lithops',
'hemp',
'pansy',
'iris',
'agave',
'ficus',
'moss',
'sage',
'snapdragon',
'columbine',
'brugmansia',
'palm',
'pachypodium',
]
mutation_list = [
'',
'humming',
'noxious',
'vorpal',
'glowing',
'electric',
'icy',
'flaming',
'psychic',
'screaming',
'chaotic',
'hissing',
'gelatinous',
'deformed',
'shaggy',
'scaly',
'depressed',
'anxious',
'metallic',
'glossy',
'psychedelic',
'bonsai',
'foamy',
'singing',
'fractal',
'crunchy',
'goth',
'oozing',
'stinky',
'aromatic',
'juicy',
'smug',
'vibrating',
'lithe',
'chalky',
'naive',
'ersatz',
'disco',
'levitating',
'colossal',
'luminous',
'cosmic',
'ethereal',
'cursed',
'buff',
'narcotic',
'gnu/linux',
'abraxan', # rip dear friend
]
def __init__(self, this_filename, generation=1):
# Constructor
self.plant_id = str(uuid.uuid4())
self.life_stages = (3600*24, (3600*24)*3, (3600*24)*10, (3600*24)*20, (3600*24)*30)
# self.life_stages = (2, 4, 6, 8, 10) # debug mode
self.stage = 0
self.mutation = 0
self.species = random.randint(0,len(self.species_list)-1)
self.color = random.randint(0,len(self.color_list)-1)
self.rarity = self.rarity_check()
self.ticks = 0
self.age_formatted = "0"
self.generation = generation
self.dead = False
self.write_lock = False
self.owner = getpass.getuser()
self.file_name = this_filename
self.start_time = int(time.time())
self.last_time = int(time.time())
# must water plant first day
self.watered_timestamp = int(time.time())-(24*3600)-1
self.watered_24h = False
self.visitors = []
def migrate_properties(self):
# Migrates old data files to new
if not hasattr(self, 'generation'):
self.generation = 1
if not hasattr(self, 'visitors'):
self.visitors = []
def parse_plant(self):
# Converts plant data to human-readable format
output = ""
if self.stage >= 3:
output += self.rarity_list[self.rarity] + " "
if self.mutation != 0:
output += self.mutation_list[self.mutation] + " "
if self.stage >= 4:
output += self.color_list[self.color] + " "
output += self.stage_list[self.stage] + " "
if self.stage >= 2:
output += self.species_list[self.species] + " "
return output.strip()
def rarity_check(self):
# Generate plant rarity
CONST_RARITY_MAX = 256.0
rare_seed = random.randint(1,int(CONST_RARITY_MAX))
common_range = round((2.0/3)*CONST_RARITY_MAX)
uncommon_range = round((2.0/3)*(CONST_RARITY_MAX-common_range))
rare_range = round((2.0/3)*(CONST_RARITY_MAX-common_range-uncommon_range))
legendary_range = round((2.0/3)*(CONST_RARITY_MAX-common_range-uncommon_range-rare_range))
common_max = common_range
uncommon_max = common_max + uncommon_range
rare_max = uncommon_max + rare_range
legendary_max = rare_max + legendary_range
godly_max = CONST_RARITY_MAX
if 0 <= rare_seed <= common_max:
rarity = 0
elif common_max < rare_seed <= uncommon_max:
rarity = 1
elif uncommon_max < rare_seed <= rare_max:
rarity = 2
elif rare_max < rare_seed <= legendary_max:
rarity = 3
elif legendary_max < rare_seed <= godly_max:
rarity = 4
return rarity
def dead_check(self):
if self.dead:
return True
# if it has been >5 days since watering, sorry plant is dead :(
time_delta_watered = int(time.time()) - self.watered_timestamp
if time_delta_watered > (5 * (24 * 3600)):
self.dead = True
return self.dead
def update_visitor_db(self, visitor_names):
game_dir = os.path.dirname(os.path.realpath(__file__))
garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
conn = sqlite3.connect(garden_db_path)
for name in (visitor_names):
c = conn.cursor()
c.execute("SELECT * FROM visitors WHERE garden_name = '{}' AND visitor_name = '{}' ".format(self.owner, name))
data=c.fetchone()
if data is None:
sql = """ INSERT INTO visitors (garden_name,visitor_name,weekly_visits) VALUES('{}', '{}',1)""".format(self.owner, name)
c.execute(sql)
else:
sql = """ UPDATE visitors SET weekly_visits = weekly_visits + 1 WHERE garden_name = '{}' AND visitor_name = '{}'""".format(self.owner, name)
c.execute(sql)
conn.commit()
conn.close()
def guest_check(self):
user_dir = os.path.expanduser("~")
botany_dir = os.path.join(user_dir,'.botany')
visitor_filepath = os.path.join(botany_dir,'visitors.json')
guest_timestamps = []
visitors_this_check = []
if os.path.isfile(visitor_filepath):
with open(visitor_filepath, 'r') as visitor_file:
data = json.load(visitor_file)
if data:
for element in data:
if element['user'] not in self.visitors:
self.visitors.append(element['user'])
if element['user'] not in visitors_this_check:
visitors_this_check.append(element['user'])
# prevent users from manually setting watered_time in the future
if element['timestamp'] <= int(time.time()) and element['timestamp'] >= self.watered_timestamp:
guest_timestamps.append(element['timestamp'])
try:
self.update_visitor_db(visitors_this_check)
except:
pass
with open(visitor_filepath, 'w') as visitor_file:
visitor_file.write('[]')
else:
with open(visitor_filepath, mode='w') as f:
json.dump([], f)
os.chmod(visitor_filepath, 0o666)
if not guest_timestamps:
return self.watered_timestamp
all_timestamps = [self.watered_timestamp] + guest_timestamps
all_timestamps.sort()
# calculate # of days between each guest watering
timestamp_diffs = [(j-i)/86400.0 for i, j in zip(all_timestamps[:-1], all_timestamps[1:])]
# plant's latest timestamp should be set to last timestamp before a
# gap of 5 days
# TODO: this considers a plant watered only on day 1 and day 4 to be
# watered for all 4 days - need to figure out how to only add score
# from 24h after each watered timestamp
last_valid_element = next((x for x in timestamp_diffs if x > 5), None)
if not last_valid_element:
# all timestamps are within a 5 day range, can just use latest one
return all_timestamps[-1]
last_valid_index = timestamp_diffs.index(last_valid_element)
# slice list to only include up until a >5 day gap
valid_timestamps = all_timestamps[:last_valid_index + 1]
return valid_timestamps[-1]
def water_check(self):
self.watered_timestamp = self.guest_check()
self.time_delta_watered = int(time.time()) - self.watered_timestamp
if self.time_delta_watered <= (24 * 3600):
if not self.watered_24h:
self.watered_24h = True
return True
else:
self.watered_24h = False
return False
def mutate_check(self):
# Create plant mutation
# Increase this # to make mutation rarer (chance 1 out of x each second)
CONST_MUTATION_RARITY = 10000
mutation_seed = random.randint(1,CONST_MUTATION_RARITY)
if mutation_seed == CONST_MUTATION_RARITY:
# mutation gained!
mutation = random.randint(0,len(self.mutation_list)-1)
if self.mutation == 0:
self.mutation = mutation
return True
else:
return False
def growth(self):
# Increase plant growth stage
if self.stage < (len(self.stage_list)-1):
self.stage += 1
def water(self):
# Increase plant growth stage
if not self.dead:
self.watered_timestamp = int(time.time())
self.watered_24h = True
def start_over(self):
# After plant reaches final stage, given option to restart
# increment generation only if previous stage is final stage and plant
# is alive
if not self.dead:
next_generation = self.generation + 1
else:
# Should this reset to 1? Seems unfair.. for now generations will
# persist through death.
next_generation = self.generation
self.write_lock = True
self.kill_plant()
while self.write_lock:
# Wait for garden writer to unlock
# garden db needs to update before allowing the user to reset
pass
if not self.write_lock:
self.__init__(self.file_name, next_generation)
def kill_plant(self):
self.dead = True
def unlock_new_creation(self):
self.write_lock = False
def start_life(self, dm):
# runs life on a thread
thread = threading.Thread(target=self.life, args=(dm,))
thread.daemon = True
thread.start()
def life(self, dm):
# I've created life :)
counter = 0
generation_bonus = round(0.2 * (self.generation - 1), 1)
score_inc = 1 * (1 + generation_bonus)
while True:
counter += 1
if not self.dead:
if self.watered_24h:
self.ticks += score_inc
if self.stage < len(self.stage_list)-1:
if self.ticks >= self.life_stages[self.stage]:
self.growth()
if self.mutate_check():
pass
if self.water_check():
# Do something
pass
if self.dead_check():
dm.harvest_plant(self)
self.unlock_new_creation()
if counter % 3 == 0:
dm.save_plant(self)
dm.data_write_json(self)
dm.update_garden_db(self)
if counter % 30 == 0:
dm.update_garden_json()
counter = 0
# TODO: event check
time.sleep(2)

View File

@ -40,10 +40,10 @@ def update_garden_db():
# ) # )
# """.format(pid = "asdfasdf", pown = "jake", pdes = "big cool plant", page="25d", psco = str(25), pdead = str(False)) # """.format(pid = "asdfasdf", pown = "jake", pdes = "big cool plant", page="25d", psco = str(25), pdead = str(False))
print(c.execute(update_query)) print c.execute(update_query)
conn.commit() conn.commit()
conn.close() conn.close()
#print("bigggg booom") #print "bigggg booom"
def retrieve_garden_from_db(garden_db_path): def retrieve_garden_from_db(garden_db_path):
# Builds a dict of dicts from garden sqlite db # Builds a dict of dicts from garden sqlite db
@ -68,7 +68,7 @@ def retrieve_garden_from_db(garden_db_path):
#init_database() #init_database()
#update_garden_db() #update_garden_db()
results = retrieve_garden_from_db(garden_db_path) results = retrieve_garden_from_db(garden_db_path)
print(results) print results
# con = sqlite3.connect(garden_db_path) # # con = sqlite3.connect(garden_db_path) #
@ -77,7 +77,7 @@ print(results)
# cur.execute("select * from garden ORDER BY score desc") # # cur.execute("select * from garden ORDER BY score desc") #
# blah = cur.fetchall() # # blah = cur.fetchall() #
# con.close() # con.close()
# print(blah) # print blah