Compare commits
80 Commits
Ensiss-gar
...
master
Author | SHA1 | Date |
---|---|---|
Nate Smith | 49ae1125d3 | |
Nate Smith | df01d7397c | |
Nate Smith | ccc08316fe | |
Noelle Leigh | 43333db825 | |
Noelle Leigh | c6aed375da | |
Nate Smith | ad0d78e133 | |
Nate Smith | 26df7b5a1f | |
Nate Smith | 9162888a7d | |
Nate Smith | 8d45997537 | |
Nate Smith | 84bb89b48c | |
Nate Smith | be770c6b5e | |
Nate Smith | ed7498bd4a | |
Nate Smith | 99c1fda072 | |
Nate Smith | ea358f641d | |
nate smith | 151a700774 | |
Marcos Marado | 1fffd41783 | |
Jake Funke | 070a880f12 | |
Tiago Epifanio | c92b3d3667 | |
Tiago Epifanio | c63a186d32 | |
Jake Funke | bdfb113063 | |
Jake Funke | b81516eea5 | |
Jake Funke | c2f00e9b0e | |
Jake Funke | 36b6507766 | |
Ngô Ngọc Đức Huy | 4ba1d6d46a | |
Nguyễn Gia Phong | 27b8733bf4 | |
Ben Harris | 29369dae13 | |
Jake Funke | 38f7f17b66 | |
Jake Funke | d0a9bf22a4 | |
Jake Funke | f5dd135c91 | |
J.M. de Jong | 85d51f40f5 | |
Jaume Delclòs Coll | 729e5268e4 | |
troido | 0556b3f75a | |
Jake Funke | f243391418 | |
Akronymus | 979c925234 | |
Jake Funke | da33b69be4 | |
Jake Funke | 54bcd2fc61 | |
Jake Funke | d93c5aad8f | |
Jake Funke | c003d183fd | |
Jake Funke | a0be5be6bc | |
Jake Funke | ea92373f37 | |
Marcos Marado | c44f9a1ed1 | |
Marcos Marado | 34d1f1be2e | |
Marcos Marado | 2b9cc31b17 | |
Paper Mountain Studio | c089f8fe67 | |
Jake Funke | 595a4d7e31 | |
Paper Mountain Studio | 58ac0d3250 | |
Jake Funke | a38c64914f | |
Jake Funke | 9d7c5966d3 | |
Jake Funke | 9bb1f74f74 | |
Jake Funke | 8fd35a5927 | |
Jake Funke | 300b9e8d00 | |
Jake Funke | 70d02db493 | |
ahriman | c9979c93e3 | |
Jake Funke | 0b2e9177dc | |
Rick Carlino | 7fcfedade0 | |
Rick Carlino | 056df53b47 | |
Rick Carlino | 6ceffd9098 | |
Jake Funke | c5e0bbaef8 | |
Jake Funke | 2212596e5e | |
Jake Funke | 6f7cddd5bb | |
Jake Funke | e5261b79b1 | |
Jake Funke | e9b58bb61d | |
Jake Funke | 0c99c4f948 | |
Jake Funke | 6162d9f1ba | |
Ensis | 2619bd87bb | |
Jake Funke | 0f78cf20d3 | |
Jake Funke | a23656b1be | |
Jake Funke | dc7eab6aa4 | |
Jake Funke | f3a5459ef7 | |
Jake Funke | af57c4b8be | |
Jake Funke | 2315a3ecd6 | |
Jake Funke | 0218448d43 | |
Jake Funke | 94584a2a36 | |
Jake Funke | f758b8bbbf | |
Jake Funke | 2eb347e11b | |
Jake Funke | 93c8656c55 | |
Ensis | 8616904350 | |
Ensis | 8e18ecbee6 | |
Ensis | 5288ead6b6 | |
Jake Funke | 5b839650fe |
|
@ -3,3 +3,4 @@ garden_db.sqlite
|
||||||
garden_file.dat
|
garden_file.dat
|
||||||
garden_file.json
|
garden_file.json
|
||||||
sqlite/
|
sqlite/
|
||||||
|
*.swp
|
||||||
|
|
41
README.md
41
README.md
|
@ -1,22 +1,21 @@
|
||||||
# botany
|
# botany
|
||||||
[![Screencap](https://asciinema.org/a/bbsqp00meurz3v5ltzc2jywif.png)](https://asciinema.org/a/bbsqp00meurz3v5ltzc2jywif)
|
![Screencap](https://tilde.town/~curiouser/botany.png)
|
||||||
|
|
||||||
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 to live!
|
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!
|
||||||
|
|
||||||
*"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 `$ python botany.py`.
|
Run with `$ python3 botany.py`.
|
||||||
|
|
||||||
*Note - botany.py must initially be run by the user who cloned/unzipped
|
*Note - botany.py must initially be run by the user who cloned/unzipped botany.py - this initalizes the shared data file permissions.*
|
||||||
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.
|
||||||
|
|
||||||
|
@ -24,6 +23,8 @@ 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
|
||||||
|
@ -39,14 +40,18 @@ If your plant goes 5 days without water, it will die! Recruit your friends to wa
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"description": "common singing blue seed-bearing poppy",
|
"description":"common screaming mature jade plant",
|
||||||
"file_name": "/Users/jakefunke/.botany/jakefunke_plant.dat",
|
"generation":1,
|
||||||
"age": "0d:2h:3m:16s",
|
"file_name":"/home/curiouser/.botany/curiouser_plant.dat",
|
||||||
"score": 1730,
|
"owner":"curiouser",
|
||||||
"owner": "jakefunke",
|
"species":"jade plant",
|
||||||
"is_dead": false,
|
"stage":"mature",
|
||||||
"last_watered": 1489113197,
|
"age":"24d:2h:16m:19s",
|
||||||
"generation": 2
|
"rarity":"common",
|
||||||
|
"score":955337.0,
|
||||||
|
"mutation":"screaming",
|
||||||
|
"last_watered":1529007007,
|
||||||
|
"is_dead":false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -59,8 +64,14 @@ If your plant goes 5 days without water, it will die! Recruit your friends to wa
|
||||||
|
|
||||||
## requirements
|
## requirements
|
||||||
* Unix-based OS (Mac, Linux)
|
* Unix-based OS (Mac, Linux)
|
||||||
* Python 2.x
|
* Python 3.x
|
||||||
* Recommended: 80x24 minimum terminal, fixed-width font
|
* Recommended: 80x24 minimum terminal, fixed-width font
|
||||||
|
|
||||||
## credits
|
## credits
|
||||||
* thank you tilde.town for inspiration!
|
* 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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
/)) HAPPY
|
||||||
|
__(((__ HALLOWEEN
|
||||||
|
.' _`""`_`'.
|
||||||
|
/ /\\ /\\ \
|
||||||
|
| /)_\\/)_\\ |
|
||||||
|
| _ _()_ _ |
|
||||||
|
| \\/\\/\\// |
|
||||||
|
\ \/\/\/\/ /
|
||||||
|
. , .'.___..___.' _ ., _ .
|
||||||
|
^ ' ` '
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/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,353 +1,50 @@
|
||||||
#!/usr/bin/python2
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
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
|
||||||
from menu_screen import *
|
import menu_screen as ms
|
||||||
|
from plant import Plant
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
# - Switch from personal data file to table in DB
|
# - switch from personal data file to row in DB
|
||||||
|
# - is threading necessary?
|
||||||
|
# - use a different curses window for plant, menu, info window, score
|
||||||
|
|
||||||
class Plant(object):
|
# notes from vilmibm
|
||||||
# This is your plant!
|
|
||||||
stage_list = [
|
|
||||||
'seed',
|
|
||||||
'seedling',
|
|
||||||
'young',
|
|
||||||
'mature',
|
|
||||||
'flowering',
|
|
||||||
'seed-bearing',
|
|
||||||
]
|
|
||||||
|
|
||||||
color_list = [
|
# there are threads.
|
||||||
'red',
|
# - life thread. sleeps a variable amount of time based on generation bonus. increases tick count (ticks == score).
|
||||||
'orange',
|
# - screen: sleeps 1s per loop. draws interface (including plant). for seeing score/plant change without user input.
|
||||||
'yellow',
|
# meanwhile, the main thread handles input and redraws curses as needed.
|
||||||
'green',
|
|
||||||
'blue',
|
|
||||||
'indigo',
|
|
||||||
'violet',
|
|
||||||
'white',
|
|
||||||
'black',
|
|
||||||
'gold',
|
|
||||||
'rainbow',
|
|
||||||
]
|
|
||||||
|
|
||||||
rarity_list = [
|
# affordance index
|
||||||
'common',
|
# - main screen
|
||||||
'uncommon',
|
# navigable menu, plant, score, etc
|
||||||
'rare',
|
# - water
|
||||||
'legendary',
|
# render a visualization of moistness; allow to water
|
||||||
'godly',
|
# - 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
|
||||||
|
|
||||||
species_list = [
|
# 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.
|
||||||
'poppy',
|
# ideally, multiple windows would be used:
|
||||||
'cactus',
|
# - the menu. it doesn't change unless the plant dies OR the plant hits stage 5, then "harvest" is dynamically added.
|
||||||
'aloe',
|
# - the plant viewer. this is updated in "real time" as the plant grows.
|
||||||
'venus flytrap',
|
# - the status display: score and plant description
|
||||||
'jade plant',
|
# - the infow window. updated by visit/garden/instructions/look
|
||||||
'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)
|
||||||
|
@ -384,47 +81,11 @@ 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):
|
if os.path.isfile(self.savefile_path) and os.path.getsize(self.savefile_path) > 0:
|
||||||
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:
|
||||||
|
@ -435,8 +96,8 @@ class DataManager(object):
|
||||||
this_plant.migrate_properties()
|
this_plant.migrate_properties()
|
||||||
|
|
||||||
# get status since last login
|
# get status since last login
|
||||||
is_dead = this_plant.dead_check()
|
|
||||||
is_watered = this_plant.water_check()
|
is_watered = this_plant.water_check()
|
||||||
|
is_dead = this_plant.dead_check()
|
||||||
|
|
||||||
if not is_dead:
|
if not is_dead:
|
||||||
if is_watered:
|
if is_watered:
|
||||||
|
@ -446,7 +107,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 * (0.2 * (this_plant.generation - 1) + 1)
|
this_plant.ticks += ticks_to_add * round(0.2 * (this_plant.generation - 1) + 1, 1)
|
||||||
return this_plant
|
return this_plant
|
||||||
|
|
||||||
def plant_age_convert(self,this_plant):
|
def plant_age_convert(self,this_plant):
|
||||||
|
@ -463,7 +124,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, 0777)
|
os.chmod(sqlite_dir_path, 0o777)
|
||||||
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,
|
||||||
|
@ -480,9 +141,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, 0666)
|
os.chmod(self.garden_db_path, 0o666)
|
||||||
open(self.garden_json_path, 'a').close()
|
open(self.garden_json_path, 'a').close()
|
||||||
os.chmod(self.garden_json_path, 0666)
|
os.chmod(self.garden_json_path, 0o666)
|
||||||
|
|
||||||
def migrate_database(self):
|
def migrate_database(self):
|
||||||
conn = sqlite3.connect(self.garden_db_path)
|
conn = sqlite3.connect(self.garden_db_path)
|
||||||
|
@ -499,8 +160,6 @@ 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)
|
||||||
|
@ -519,6 +178,13 @@ 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()
|
||||||
|
|
||||||
|
@ -629,12 +295,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_plant.start_life(my_data)
|
||||||
my_data.start_threads(my_plant)
|
|
||||||
try:
|
try:
|
||||||
botany_menu = CursedMenu(my_plant,my_data)
|
botany_menu = ms.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:
|
||||||
cleanup()
|
ms.cleanup()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
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,6 +10,8 @@ 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
|
||||||
|
@ -34,7 +36,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(self.plant.ticks)
|
self.plant_ticks = str(int(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()
|
||||||
|
@ -49,10 +51,13 @@ 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)
|
||||||
|
@ -75,11 +80,7 @@ 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:
|
if self.plant.dead or self.plant.stage == 5:
|
||||||
if "harvest" not in self.options:
|
|
||||||
self.options.insert(-1,"harvest")
|
|
||||||
else:
|
|
||||||
if self.plant.stage == 5:
|
|
||||||
if "harvest" not in self.options:
|
if "harvest" not in self.options:
|
||||||
self.options.insert(-1,"harvest")
|
self.options.insert(-1,"harvest")
|
||||||
else:
|
else:
|
||||||
|
@ -88,13 +89,14 @@ class CursedMenu(object):
|
||||||
|
|
||||||
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] is not 'exit':
|
if options[-1] != '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()
|
||||||
|
@ -106,12 +108,13 @@ 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 is not "exit":
|
while request != "exit":
|
||||||
self.draw()
|
self.draw()
|
||||||
request = self.get_user_input()
|
request = self.get_user_input()
|
||||||
self.handle_request(request)
|
self.handle_request(request)
|
||||||
|
@ -124,6 +127,10 @@ 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
|
||||||
|
@ -132,9 +139,11 @@ 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
|
||||||
|
@ -165,6 +174,8 @@ 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:
|
||||||
|
@ -182,6 +193,7 @@ 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
|
||||||
|
@ -216,6 +228,7 @@ 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
|
||||||
|
@ -242,8 +255,10 @@ 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(1, 1, str(user_in), curses.A_NORMAL)
|
# self.screen.addstr(2, 2, 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
|
||||||
|
@ -252,14 +267,17 @@ class CursedMenu(object):
|
||||||
self.screen.clear()
|
self.screen.clear()
|
||||||
self.screen.refresh()
|
self.screen.refresh()
|
||||||
|
|
||||||
# enter and exit Keys are special cases
|
# enter, exit, and Q 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(9,len(self.options)+1))):
|
if user_in >= ord('1') and user_in <= ord(str(min(7,len(self.options)))):
|
||||||
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
|
||||||
|
|
||||||
|
@ -290,11 +308,21 @@ class CursedMenu(object):
|
||||||
return plant_table
|
return plant_table
|
||||||
|
|
||||||
def format_garden_entry(self, entry):
|
def format_garden_entry(self, entry):
|
||||||
return "{:14} - {:>16} - {:>8}p - {}".format(*entry)
|
return "{:14.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 """
|
||||||
return table.sort(key=lambda x: x[column], reverse=not ascending)
|
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)
|
||||||
|
|
||||||
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 """
|
||||||
|
@ -328,7 +356,6 @@ 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
|
||||||
|
@ -339,12 +366,16 @@ 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
|
||||||
|
@ -367,6 +398,8 @@ 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))
|
||||||
|
@ -531,7 +564,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: {}".format(self.plant.generation, growth_multiplier)
|
output_string += "Generation: {}\nGrowth rate: {:.1f}x".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:
|
||||||
|
@ -548,6 +581,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
@ -558,15 +592,18 @@ 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):
|
||||||
|
@ -576,6 +613,7 @@ 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()
|
||||||
|
@ -592,8 +630,10 @@ 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')]:
|
if user_in in [ord('Y'), ord('y'), 10]:
|
||||||
self.plant.start_over()
|
self.plant.start_over()
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
@ -644,22 +684,36 @@ 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):
|
def get_user_string(self, xpos=3, ypos=15, filterfunc=str.isalnum, completer=None):
|
||||||
# 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))
|
||||||
if user_input < 256 and user_input != 10:
|
elif user_input in [ord('\t'), curses.KEY_BTAB] and completer:
|
||||||
if filterfunc(chr(user_input)):
|
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) == '_':
|
||||||
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):
|
||||||
|
@ -674,7 +728,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()
|
guest_garden = self.get_user_string(completer = completer.LoginCompleter)
|
||||||
if not guest_garden:
|
if not guest_garden:
|
||||||
self.clear_info_pane()
|
self.clear_info_pane()
|
||||||
return None
|
return None
|
||||||
|
@ -693,26 +747,35 @@ 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):
|
||||||
self.water_on_visit(guest_visitor_file)
|
water_success = 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)
|
||||||
|
finally:
|
||||||
self.visited_plant = None
|
self.visited_plant = None
|
||||||
|
|
||||||
def water_on_visit(self, guest_visitor_file):
|
def water_on_visit(self, guest_visitor_file):
|
||||||
visitor_data = {}
|
visitor_data = {}
|
||||||
guest_data = {'user': getpass.getuser(), 'timestamp': int(time.time())}
|
# using -1 here so that old running instances can be watered
|
||||||
|
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 """
|
||||||
|
|
|
@ -0,0 +1,366 @@
|
||||||
|
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))
|
# """.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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue