Compare commits
1 Commits
master
...
Ensiss-gar
Author | SHA1 | Date |
---|---|---|
Jake Funke | b0876fb7e5 |
|
@ -3,4 +3,3 @@ garden_db.sqlite
|
|||
garden_file.dat
|
||||
garden_file.json
|
||||
sqlite/
|
||||
*.swp
|
||||
|
|
41
README.md
41
README.md
|
@ -1,21 +1,22 @@
|
|||
# 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/
|
||||
|
||||
A command line, realtime, community plant buddy.
|
||||
|
||||
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*
|
||||
|
||||
## getting started
|
||||
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.
|
||||
|
||||
|
@ -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!
|
||||
|
||||
A once-weekly cron on clear_weekly_users.py should be set up to keep weekly visitors tidy.
|
||||
|
||||
|
||||
## features
|
||||
* 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",
|
||||
"generation":1,
|
||||
"file_name":"/home/curiouser/.botany/curiouser_plant.dat",
|
||||
"owner":"curiouser",
|
||||
"species":"jade plant",
|
||||
"stage":"mature",
|
||||
"age":"24d:2h:16m:19s",
|
||||
"rarity":"common",
|
||||
"score":955337.0,
|
||||
"mutation":"screaming",
|
||||
"last_watered":1529007007,
|
||||
"is_dead":false
|
||||
"description": "common singing blue seed-bearing poppy",
|
||||
"file_name": "/Users/jakefunke/.botany/jakefunke_plant.dat",
|
||||
"age": "0d:2h:3m:16s",
|
||||
"score": 1730,
|
||||
"owner": "jakefunke",
|
||||
"is_dead": false,
|
||||
"last_watered": 1489113197,
|
||||
"generation": 2
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -64,14 +59,8 @@ A once-weekly cron on clear_weekly_users.py should be set up to keep weekly visi
|
|||
|
||||
## requirements
|
||||
* Unix-based OS (Mac, Linux)
|
||||
* Python 3.x
|
||||
* Python 2.x
|
||||
* Recommended: 80x24 minimum terminal, fixed-width font
|
||||
|
||||
## credits
|
||||
* thank you [tilde.town](http://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)
|
||||
|
||||
* thank you tilde.town for inspiration!
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
/)) HAPPY
|
||||
__(((__ HALLOWEEN
|
||||
.' _`""`_`'.
|
||||
/ /\\ /\\ \
|
||||
| /)_\\/)_\\ |
|
||||
| _ _()_ _ |
|
||||
| \\/\\/\\// |
|
||||
\ \/\/\/\/ /
|
||||
. , .'.___..___.' _ ., _ .
|
||||
^ ' ` '
|
|
@ -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
432
botany.py
|
@ -1,50 +1,353 @@
|
|||
#!/usr/bin/env python3
|
||||
#!/usr/bin/python2
|
||||
|
||||
from __future__ import division
|
||||
import time
|
||||
import pickle
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import getpass
|
||||
import threading
|
||||
import errno
|
||||
import uuid
|
||||
import sqlite3
|
||||
import menu_screen as ms
|
||||
from plant import Plant
|
||||
from menu_screen import *
|
||||
|
||||
# TODO:
|
||||
# - switch from personal data file to row in DB
|
||||
# - is threading necessary?
|
||||
# - use a different curses window for plant, menu, info window, score
|
||||
# - Switch from personal data file to table in DB
|
||||
|
||||
# notes from vilmibm
|
||||
class Plant(object):
|
||||
# This is your plant!
|
||||
stage_list = [
|
||||
'seed',
|
||||
'seedling',
|
||||
'young',
|
||||
'mature',
|
||||
'flowering',
|
||||
'seed-bearing',
|
||||
]
|
||||
|
||||
# there are threads.
|
||||
# - life thread. sleeps a variable amount of time based on generation bonus. increases tick count (ticks == score).
|
||||
# - screen: sleeps 1s per loop. draws interface (including plant). for seeing score/plant change without user input.
|
||||
# meanwhile, the main thread handles input and redraws curses as needed.
|
||||
color_list = [
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'indigo',
|
||||
'violet',
|
||||
'white',
|
||||
'black',
|
||||
'gold',
|
||||
'rainbow',
|
||||
]
|
||||
|
||||
# affordance index
|
||||
# - main screen
|
||||
# navigable menu, plant, score, etc
|
||||
# - water
|
||||
# render a visualization of moistness; allow to water
|
||||
# - look
|
||||
# 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
|
||||
rarity_list = [
|
||||
'common',
|
||||
'uncommon',
|
||||
'rare',
|
||||
'legendary',
|
||||
'godly',
|
||||
]
|
||||
|
||||
# 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.
|
||||
# ideally, multiple windows would be used:
|
||||
# - the menu. it doesn't change unless the plant dies OR the plant hits stage 5, then "harvest" is dynamically added.
|
||||
# - the plant viewer. this is updated in "real time" as the plant grows.
|
||||
# - the status display: score and plant description
|
||||
# - the infow window. updated by visit/garden/instructions/look
|
||||
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',
|
||||
]
|
||||
|
||||
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):
|
||||
# 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):
|
||||
# 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
|
||||
else:
|
||||
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):
|
||||
# load savefile
|
||||
with open(self.savefile_path, 'rb') as f:
|
||||
|
@ -96,8 +435,8 @@ class DataManager(object):
|
|||
this_plant.migrate_properties()
|
||||
|
||||
# get status since last login
|
||||
is_watered = this_plant.water_check()
|
||||
is_dead = this_plant.dead_check()
|
||||
is_watered = this_plant.water_check()
|
||||
|
||||
if not is_dead:
|
||||
if is_watered:
|
||||
|
@ -107,7 +446,7 @@ class DataManager(object):
|
|||
self.last_water_gain = time.time()
|
||||
else:
|
||||
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
|
||||
|
||||
def plant_age_convert(self,this_plant):
|
||||
|
@ -124,7 +463,7 @@ class DataManager(object):
|
|||
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, 0o777)
|
||||
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,
|
||||
|
@ -141,9 +480,9 @@ class DataManager(object):
|
|||
|
||||
# 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, 0o666)
|
||||
os.chmod(self.garden_db_path, 0666)
|
||||
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):
|
||||
conn = sqlite3.connect(self.garden_db_path)
|
||||
|
@ -160,6 +499,8 @@ class DataManager(object):
|
|||
|
||||
def update_garden_db(self, this_plant):
|
||||
# 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.migrate_database()
|
||||
age_formatted = self.plant_age_convert(this_plant)
|
||||
|
@ -178,13 +519,6 @@ class DataManager(object):
|
|||
psco = str(this_plant.ticks),
|
||||
pdead = int(this_plant.dead))
|
||||
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.close()
|
||||
|
||||
|
@ -295,12 +629,12 @@ if __name__ == '__main__':
|
|||
my_plant = Plant(my_data.savefile_path)
|
||||
my_data.data_write_json(my_plant)
|
||||
# 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:
|
||||
botany_menu = ms.CursedMenu(my_plant,my_data)
|
||||
botany_menu = CursedMenu(my_plant,my_data)
|
||||
my_data.save_plant(my_plant)
|
||||
my_data.data_write_json(my_plant)
|
||||
my_data.update_garden_db(my_plant)
|
||||
finally:
|
||||
ms.cleanup()
|
||||
cleanup()
|
||||
|
|
|
@ -6,6 +6,6 @@ garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
|
|||
conn = sqlite3.connect(garden_db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM visitors")
|
||||
print("Cleared weekly users")
|
||||
print "Cleared weekly users"
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
|
55
completer.py
55
completer.py
|
@ -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]
|
107
menu_screen.py
107
menu_screen.py
|
@ -10,8 +10,6 @@ import json
|
|||
import sqlite3
|
||||
import string
|
||||
import re
|
||||
import completer
|
||||
import datetime
|
||||
|
||||
class CursedMenu(object):
|
||||
#TODO: name your plant
|
||||
|
@ -36,7 +34,7 @@ class CursedMenu(object):
|
|||
self.visited_plant = None
|
||||
self.user_data = this_data
|
||||
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.infotoggle = 0
|
||||
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.daemon = True
|
||||
screen_thread.start()
|
||||
# Recusive lock to prevent both threads from drawing at the same time
|
||||
self.screen_lock = threading.RLock()
|
||||
self.screen.clear()
|
||||
self.show(["water","look","garden","visit", "instructions"], title=' botany ', subtitle='options')
|
||||
|
||||
def define_colors(self):
|
||||
# TODO: implement colors
|
||||
# set curses color pairs manually
|
||||
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
||||
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
||||
|
@ -80,7 +75,11 @@ class CursedMenu(object):
|
|||
|
||||
def update_options(self):
|
||||
# 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:
|
||||
self.options.insert(-1,"harvest")
|
||||
else:
|
||||
if self.plant.stage == 5:
|
||||
if "harvest" not in self.options:
|
||||
self.options.insert(-1,"harvest")
|
||||
else:
|
||||
|
@ -89,14 +88,13 @@ class CursedMenu(object):
|
|||
|
||||
def set_options(self, options):
|
||||
# Validates that the last option is "exit"
|
||||
if options[-1] != 'exit':
|
||||
if options[-1] is not 'exit':
|
||||
options.append('exit')
|
||||
self.options = options
|
||||
|
||||
def draw(self):
|
||||
# Draw the menu and lines
|
||||
self.maxy, self.maxx = self.screen.getmaxyx()
|
||||
self.screen_lock.acquire()
|
||||
self.screen.refresh()
|
||||
try:
|
||||
self.draw_default()
|
||||
|
@ -108,13 +106,12 @@ class CursedMenu(object):
|
|||
self.screen.refresh()
|
||||
self.__exit__()
|
||||
traceback.print_exc()
|
||||
self.screen_lock.release()
|
||||
|
||||
def draw_menu(self):
|
||||
# Actually draws the menu and handles branching
|
||||
request = ""
|
||||
try:
|
||||
while request != "exit":
|
||||
while request is not "exit":
|
||||
self.draw()
|
||||
request = self.get_user_input()
|
||||
self.handle_request(request)
|
||||
|
@ -127,10 +124,6 @@ class CursedMenu(object):
|
|||
self.screen.refresh()
|
||||
self.__exit__()
|
||||
#traceback.print_exc()
|
||||
except IOError as exception:
|
||||
self.screen.clear()
|
||||
self.screen.refresh()
|
||||
self.__exit__()
|
||||
|
||||
def ascii_render(self, filename, ypos, xpos):
|
||||
# Prints ASCII art from file at given coordinates
|
||||
|
@ -139,11 +132,9 @@ class CursedMenu(object):
|
|||
this_file = open(this_filename,"r")
|
||||
this_string = this_file.readlines()
|
||||
this_file.close()
|
||||
self.screen_lock.acquire()
|
||||
for y, line in enumerate(this_string, 2):
|
||||
self.screen.addstr(ypos+y, xpos, line, curses.A_NORMAL)
|
||||
# self.screen.refresh()
|
||||
self.screen_lock.release()
|
||||
|
||||
def draw_plant_ascii(self, this_plant):
|
||||
ypos = 0
|
||||
|
@ -174,8 +165,6 @@ class CursedMenu(object):
|
|||
]
|
||||
if this_plant.dead == True:
|
||||
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:
|
||||
self.ascii_render('seed.txt', ypos, xpos)
|
||||
elif this_plant.stage == 1:
|
||||
|
@ -193,7 +182,6 @@ class CursedMenu(object):
|
|||
def draw_default(self):
|
||||
# draws default menu
|
||||
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(3, 2, self.subtitle, curses.A_BOLD) #Subtitle for this menu
|
||||
# clear menu on screen
|
||||
|
@ -228,7 +216,6 @@ class CursedMenu(object):
|
|||
self.draw_plant_ascii(self.visited_plant)
|
||||
else:
|
||||
self.draw_plant_ascii(self.plant)
|
||||
self.screen_lock.release()
|
||||
|
||||
def water_gauge(self):
|
||||
# build nice looking water gauge
|
||||
|
@ -255,10 +242,8 @@ class CursedMenu(object):
|
|||
user_in = self.screen.getch() # Gets user input
|
||||
except Exception as e:
|
||||
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
|
||||
# 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()
|
||||
|
||||
# Resize sends curses.KEY_RESIZE, update display
|
||||
|
@ -267,17 +252,14 @@ class CursedMenu(object):
|
|||
self.screen.clear()
|
||||
self.screen.refresh()
|
||||
|
||||
# enter, exit, and Q Keys are special cases
|
||||
# enter and exit Keys are special cases
|
||||
if user_in == 10:
|
||||
return self.options[self.selected]
|
||||
if user_in == 27:
|
||||
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
|
||||
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
|
||||
return
|
||||
|
||||
|
@ -308,21 +290,11 @@ class CursedMenu(object):
|
|||
return plant_table
|
||||
|
||||
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):
|
||||
""" Sort table in place by a specified column """
|
||||
def key(entry):
|
||||
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)
|
||||
return table.sort(key=lambda x: x[column], reverse=not ascending)
|
||||
|
||||
def filter_garden_table(self, table, pattern):
|
||||
""" Filter table using a pattern, and return the new table """
|
||||
|
@ -356,6 +328,7 @@ class CursedMenu(object):
|
|||
self.infotoggle = 2
|
||||
|
||||
# print garden information OR clear it
|
||||
# TODO: pagination control with hjkl/arrow keys/esc-or-x to close
|
||||
index = 0
|
||||
sort_column, sort_ascending = 0, True
|
||||
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)
|
||||
plants = plant_table[index:index_max]
|
||||
page = [self.format_garden_entry(entry) for entry in plants]
|
||||
self.screen_lock.acquire()
|
||||
self.draw_info_text(page)
|
||||
# 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))
|
||||
self.screen.addstr(self.maxy-2, 2, page_text)
|
||||
self.screen.refresh()
|
||||
self.screen_lock.release()
|
||||
c = self.screen.getch()
|
||||
if c == -1: # Input comes from pipe/file and is closed
|
||||
raise IOError
|
||||
self.infotoggle = 0
|
||||
|
||||
# Quit
|
||||
|
@ -398,8 +367,6 @@ class CursedMenu(object):
|
|||
# Sort entries
|
||||
elif c == ord("s"):
|
||||
c = self.screen.getch()
|
||||
if c == -1: # Input comes from pipe/file and is closed
|
||||
raise IOError
|
||||
column = -1
|
||||
if c < 255 and chr(c) in sort_keys:
|
||||
column = sort_keys.index(chr(c))
|
||||
|
@ -564,7 +531,7 @@ class CursedMenu(object):
|
|||
# get plant description before printing
|
||||
output_string = self.get_plant_description(this_plant)
|
||||
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.infotoggle = 1
|
||||
else:
|
||||
|
@ -581,7 +548,6 @@ class CursedMenu(object):
|
|||
"growing. 5 days without water = death. your\n"
|
||||
"plant depends on you & your friends to live!\n"
|
||||
"more info is available in the readme :)\n"
|
||||
"https://github.com/jifunks/botany/blob/master/README.md\n"
|
||||
" cheers,\n"
|
||||
" curio\n"
|
||||
)
|
||||
|
@ -592,18 +558,15 @@ class CursedMenu(object):
|
|||
|
||||
def clear_info_pane(self):
|
||||
# Clears bottom part of screen
|
||||
self.screen_lock.acquire()
|
||||
clear_bar = " " * (self.maxx - 3)
|
||||
this_y = 14
|
||||
while this_y < self.maxy:
|
||||
self.screen.addstr(this_y, 2, clear_bar, curses.A_NORMAL)
|
||||
this_y += 1
|
||||
self.screen.refresh()
|
||||
self.screen_lock.release()
|
||||
|
||||
def draw_info_text(self, info_text, y_offset = 0):
|
||||
# print lines of text to info pane at bottom of screen
|
||||
self.screen_lock.acquire()
|
||||
if type(info_text) is str:
|
||||
info_text = info_text.splitlines()
|
||||
for y, line in enumerate(info_text, 2):
|
||||
|
@ -613,7 +576,6 @@ class CursedMenu(object):
|
|||
if this_y < self.maxy:
|
||||
self.screen.addstr(this_y, 2, line, curses.A_NORMAL)
|
||||
self.screen.refresh()
|
||||
self.screen_lock.release()
|
||||
|
||||
def harvest_confirmation(self):
|
||||
self.clear_info_pane()
|
||||
|
@ -630,10 +592,8 @@ class CursedMenu(object):
|
|||
user_in = self.screen.getch() # Gets user input
|
||||
except Exception as e:
|
||||
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()
|
||||
else:
|
||||
pass
|
||||
|
@ -684,36 +644,22 @@ class CursedMenu(object):
|
|||
visitor_block = 'nobody :('
|
||||
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
|
||||
user_string = ""
|
||||
user_input = 0
|
||||
if completer:
|
||||
completer = completer(self)
|
||||
while user_input != 10:
|
||||
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...
|
||||
if user_input == 127 or user_input == 263:
|
||||
if len(user_string) > 0:
|
||||
user_string = user_string[:-1]
|
||||
if completer:
|
||||
completer.update_input(user_string)
|
||||
self.screen.addstr(ypos, xpos, " " * (self.maxx-xpos-1))
|
||||
elif user_input in [ord('\t'), curses.KEY_BTAB] and completer:
|
||||
direction = 1 if user_input == ord('\t') else -1
|
||||
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) == '_':
|
||||
if user_input < 256 and user_input != 10:
|
||||
if filterfunc(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.refresh()
|
||||
self.screen_lock.release()
|
||||
return user_string
|
||||
|
||||
def visit_handler(self):
|
||||
|
@ -728,7 +674,7 @@ class CursedMenu(object):
|
|||
weekly_visitor_text = self.get_weekly_visitors()
|
||||
self.draw_info_text("this week you've been visited by: ", 6)
|
||||
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:
|
||||
self.clear_info_pane()
|
||||
return None
|
||||
|
@ -747,35 +693,26 @@ class CursedMenu(object):
|
|||
self.visited_plant = self.get_visited_plant(visitor_data)
|
||||
guest_visitor_file = home_folder + "/{}/.botany/visitors.json".format(guest_garden, guest_garden)
|
||||
if os.path.isfile(guest_visitor_file):
|
||||
water_success = self.water_on_visit(guest_visitor_file)
|
||||
if water_success:
|
||||
self.water_on_visit(guest_visitor_file)
|
||||
self.screen.addstr(16, 2, "...you watered ~{}'s {}...".format(str(guest_garden), guest_plant_description))
|
||||
if 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:
|
||||
self.screen.addstr(16, 2, "i can't seem to find directions to {}...".format(guest_garden))
|
||||
try:
|
||||
self.screen.getch()
|
||||
self.clear_info_pane()
|
||||
self.draw_plant_ascii(self.plant)
|
||||
finally:
|
||||
self.visited_plant = None
|
||||
|
||||
def water_on_visit(self, guest_visitor_file):
|
||||
visitor_data = {}
|
||||
# using -1 here so that old running instances can be watered
|
||||
guest_data = {'user': getpass.getuser(), 'timestamp': int(time.time()) - 1}
|
||||
guest_data = {'user': getpass.getuser(), 'timestamp': int(time.time())}
|
||||
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:
|
||||
visitor_data = json.load(f)
|
||||
visitor_data.append(guest_data)
|
||||
with open(guest_visitor_file, mode='w') as f:
|
||||
f.write(json.dumps(visitor_data, indent=2))
|
||||
return True
|
||||
|
||||
def get_visited_plant(self, visitor_data):
|
||||
""" Returns a drawable pseudo plant object from json data """
|
||||
|
|
366
plant.py
366
plant.py
|
@ -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)
|
|
@ -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))
|
||||
|
||||
print(c.execute(update_query))
|
||||
print c.execute(update_query)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
#print("bigggg booom")
|
||||
#print "bigggg booom"
|
||||
|
||||
def retrieve_garden_from_db(garden_db_path):
|
||||
# Builds a dict of dicts from garden sqlite db
|
||||
|
@ -68,7 +68,7 @@ def retrieve_garden_from_db(garden_db_path):
|
|||
#init_database()
|
||||
#update_garden_db()
|
||||
results = retrieve_garden_from_db(garden_db_path)
|
||||
print(results)
|
||||
print results
|
||||
|
||||
|
||||
# con = sqlite3.connect(garden_db_path) #
|
||||
|
@ -77,7 +77,7 @@ print(results)
|
|||
# cur.execute("select * from garden ORDER BY score desc") #
|
||||
# blah = cur.fetchall() #
|
||||
# con.close()
|
||||
# print(blah)
|
||||
# print blah
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue