Compare commits

..

80 Commits

Author SHA1 Message Date
Nate Smith 49ae1125d3
Merge pull request #51 from marado/corrupt
Check if the save file isn't corrupted
2024-04-04 15:29:38 -04:00
Nate Smith df01d7397c
Merge pull request #54 from noelleleigh/growth-rate-rounding
Round growth rate to single decimal
2024-04-04 15:26:07 -04:00
Nate Smith ccc08316fe
Merge pull request #53 from noelleleigh/Python-3.12
Support Python 3.12
2024-04-04 15:14:58 -04:00
Noelle Leigh 43333db825 Round growth rate to single decimal
At generation 8, the growth rate in the "look" message is displayed as:

> 2.4000000000000004x

To make it display "2.4x" instead, I used the [format specification][0]
to display growth rate with only a single decimal place.

[0]: https://docs.python.org/3/library/string.html#format-specification-mini-language
2024-03-03 17:52:03 -05:00
Noelle Leigh c6aed375da Support Python 3.12
Python 3.12 no longer supports using non-integer values as arguments for
random functions (see [Changes in the Python API for 3.12][0]). This PR
casts `CONST_RARITY_MAX` to an integer to prevent a `TypeError` from
being raised.

[0]: https://docs.python.org/3/whatsnew/3.12.html#changes-in-the-python-api
2024-03-03 17:20:32 -05:00
Nate Smith ad0d78e133 oops 2023-12-09 16:14:19 -08:00
Nate Smith 26df7b5a1f adjust mutation rarity to account for reduced number of life iterations 2023-12-08 21:42:24 -08:00
Nate Smith 9162888a7d consolidate autosave into life thread 2023-12-06 21:52:40 -08:00
Nate Smith 8d45997537 fix scoping on death_check change even more 2023-12-06 21:38:13 -08:00
Nate Smith 84bb89b48c fix scoping on death_check change more 2023-12-06 21:37:12 -08:00
Nate Smith be770c6b5e fix scoping on death_check change 2023-12-06 21:36:15 -08:00
Nate Smith ed7498bd4a reduce CPU usage dramatically
I noticed that on tilde.town users with a high botany score used up a
lot of CPU cycles. I skimmed through the code and didn't immediately see
any tight loops, but after profiling against a user's borrowed .botany
directory I saw the culprit: the score increase thread.

This thread was designed to increase the user's score by 1 every time
the thread did an iteration of its infinite loop. It would sleep for an
interval scaled *down* based on how high a user's generation bonus was.
This meant that the sleep interval trended towards zero, creating a
tight loop for high scoring users.

This commit changes the code to use a constant sleep inteveral but scale
the score increment *up* based on generation.

I also removed the death check thread entirely since we were already
checking for death in the score thread. I also short circuited the death
check.

This had the effect of reducing CPU load for a high scoring user by a
factor of about 50.
2023-12-06 21:08:43 -08:00
Nate Smith 99c1fda072 ignore *.swp 2023-12-04 22:04:29 -08:00
Nate Smith ea358f641d move Plant to its own module
Without this, trying to run this against cProfiler fails because pickler
can't find Plant in the "current" module. It's an annoying quirk of
pickle+cProfiler.
see: https://stackoverflow.com/questions/53890693/cprofile-causes-pickling-error-when-running-multiprocessing-python-code

this commit also adds a bunch of notes.
2023-12-04 22:04:00 -08:00
nate smith 151a700774 do not import * 2023-12-02 22:31:32 -08:00
Marcos Marado 1fffd41783 Check if the save file isn't corrupted
If the save file is empty, then it will not be loadable, and it is best
act as if none existed.
2023-09-13 15:38:08 +00:00
Jake Funke 070a880f12
Merge pull request #50 from epif4nio/master
On harvest confirmation message, default to Y when user presses enter
2023-03-21 10:47:44 -07:00
Tiago Epifanio c92b3d3667 Clean other instances of user 2023-03-20 00:26:43 +00:00
Tiago Epifanio c63a186d32 On harvest confirmation message, default to Y when user presses return 2023-03-19 23:51:39 +00:00
Jake Funke bdfb113063
Merge pull request #40 from tilde-team/master
fix visiting
2022-10-12 12:15:42 -07:00
Jake Funke b81516eea5
Merge pull request #46 from McSinyx/py3
Update shebang
2022-10-12 12:15:00 -07:00
Jake Funke c2f00e9b0e
Merge pull request #47 from Huy-Ngo/fix-floating-point
fix precision to first decimal place
2022-10-12 12:14:43 -07:00
Jake Funke 36b6507766
Update README.md 2022-10-12 12:14:17 -07:00
Ngô Ngọc Đức Huy 4ba1d6d46a
fix precision to first decimal place
fix precision to first decimal place to avoid floating-point error
2022-05-20 20:19:05 +07:00
Nguyễn Gia Phong 27b8733bf4
Update shebang 2022-05-13 13:40:23 +09:00
Ben Harris 29369dae13 fix visiting 2021-04-06 15:50:53 -04:00
Jake Funke 38f7f17b66
Merge pull request #34 from jmdejong/evilvisitors
Don't let evil visitors kill a plant by adding old timestamps
2021-01-25 14:19:04 -08:00
Jake Funke d0a9bf22a4
Merge pull request #36 from cosarara/master
don't use "is" to compare string literals
2021-01-25 14:18:39 -08:00
Jake Funke f5dd135c91
Merge pull request #37 from jmdejong/patch-2
Stop making all plants godly
2021-01-25 14:18:17 -08:00
J.M. de Jong 85d51f40f5
Stop making all plants godly
python2 division of integers always results in an integer, so `(2/3)` would result in `0`.
This caused all plants to get "godly" as rarity.
This change makes sure a float division is being used
2021-01-23 14:30:31 +01:00
Jaume Delclòs Coll 729e5268e4 don't use "is" to compare string literals 2020-10-25 02:09:29 +02:00
troido 0556b3f75a protect plants against old timestamps 2020-10-03 09:44:06 +02:00
Jake Funke f243391418
Merge pull request #32 from ChristophGra/master
Error handling when getch() throws an error
2020-09-15 12:15:58 -07:00
Akronymus 979c925234
Error handling when getch() throws an error
Currently untested, but should work well enough. Doing this as an reference implementation.
2020-09-15 19:45:54 +02:00
Jake Funke da33b69be4
Update README.md 2020-04-03 15:14:04 -07:00
Jake Funke 54bcd2fc61
Update README.md 2020-04-03 15:13:18 -07:00
Jake Funke d93c5aad8f
Merge pull request #27 from marado/master
Added info about where the readme actually is
2020-04-03 15:09:18 -07:00
Jake Funke c003d183fd
Merge pull request #28 from marado/view
botany-view: a new binary giving you a snapshot
2020-04-03 15:09:06 -07:00
Jake Funke a0be5be6bc
Merge pull request #29 from marado/patch-1
making the halloween easter egg ~-agnostic
2020-04-03 15:08:55 -07:00
Jake Funke ea92373f37
Merge pull request #24 from PaperMountainStudio/python3
Use python3
2020-04-03 14:55:10 -07:00
Marcos Marado c44f9a1ed1
making the halloween easter egg ~-agnostic 2020-04-03 00:47:18 +01:00
Marcos Marado 34d1f1be2e botany-view: a new binary giving you a snapshot
python botany-view.py will give you a snapshot look of your garden plot,
showing what is in there. Instead of an interactive curses interface, it
just shows you your plant as it currently is. It's like just taking a
peek through the window, or a picture of your plant to show to your
friends.
2020-04-02 15:35:57 +01:00
Marcos Marado 2b9cc31b17 Added info about where the readme actually is
The help menu points out to the readme for more info, but, in cases
where people are using this because it is installed on their server, but
they weren't the ones getting it from the web, they probably don't know
where to find that README. It might not happen to many people, but it
happened to me :-)

This patch just adds another line, with the URL for the README.
2020-04-02 00:03:05 +01:00
Paper Mountain Studio c089f8fe67 delete old import for python2 2020-01-14 17:21:21 +01:00
Jake Funke 595a4d7e31 found an oopsie 2019-10-30 23:53:56 +00:00
Paper Mountain Studio 58ac0d3250 Use python3 2019-10-30 18:27:45 +01:00
Jake Funke a38c64914f clarify growth rate 2019-08-26 16:34:08 +00:00
Jake Funke 9d7c5966d3 cleanup comments 2019-08-16 17:25:45 +00:00
Jake Funke 9bb1f74f74 fix harvest numeric input bug 2019-08-16 17:21:40 +00:00
Jake Funke 8fd35a5927 logic tweak 2019-02-28 00:41:15 +00:00
Jake Funke 300b9e8d00 Added alpine-like q to quit 2018-12-13 22:54:57 +00:00
Jake Funke 70d02db493
Merge pull request #22 from midnightpupil/master
changed shebang to increase portability @ahriman/tilde.town
2018-12-06 17:11:53 -08:00
ahriman c9979c93e3 changed shebang to increase portability @ahriman/tilde.town 2018-12-06 16:18:26 -05:00
Jake Funke 0b2e9177dc
Merge pull request #21 from RickCarlino/master
Limit length of longer usernames
2018-11-24 20:54:18 -08:00
Rick Carlino 7fcfedade0 Better strategy for truncating long usernames. 2018-11-24 22:47:28 -06:00
Rick Carlino 056df53b47 Revert previous changes. NEXT: draw_info_text stuff. 2018-11-24 22:34:04 -06:00
Rick Carlino 6ceffd9098 Limit length of longer usernames 2018-11-20 20:43:48 -06:00
Jake Funke c5e0bbaef8 allow underscore in names 2018-11-14 23:24:34 +00:00
Jake Funke 2212596e5e halloween feature and death check fix 2018-10-03 18:53:49 +00:00
Jake Funke 6f7cddd5bb tweaks 2018-08-27 21:01:46 +00:00
Jake Funke e5261b79b1 fixed bug where users with live botany instances couldnt be watered 2018-08-10 20:14:11 +00:00
Jake Funke e9b58bb61d tribute to abraxas. rest in peace friend. 2018-08-06 21:15:06 +00:00
Jake Funke 0c99c4f948 added some mutations 2018-07-12 17:42:34 +00:00
Jake Funke 6162d9f1ba
Merge pull request #20 from Ensiss/io-fix
Prevent infinite loops when piping to botany
2018-07-03 11:05:50 -07:00
Ensis 2619bd87bb Prevent infinite loops when piping to botany 2018-07-03 14:18:09 +02:00
Jake Funke 0f78cf20d3 fixed permissions bug 2018-06-19 23:58:51 +00:00
Jake Funke a23656b1be
Merge pull request #18 from Ensiss/login-autocomplete
Add autocompletion for logins in the visit prompt
2018-06-19 16:39:14 -07:00
Jake Funke dc7eab6aa4
Merge pull request #17 from Ensiss/thread-safe
Fix race conditions messing up the display
2018-06-19 16:36:24 -07:00
Jake Funke f3a5459ef7 menu range fix 2018-06-18 18:16:05 +00:00
Jake Funke af57c4b8be Merge branch 'master' of https://github.com/jifunks/botany 2018-06-14 21:07:54 +00:00
Jake Funke 2315a3ecd6 More accurate guest timing 2018-06-14 21:07:43 +00:00
Jake Funke 0218448d43
Update README.md 2018-06-14 13:20:51 -07:00
Jake Funke 94584a2a36 fixed long term absence visitor check 2018-06-14 20:11:07 +00:00
Jake Funke f758b8bbbf Merge branch 'master' of https://github.com/jifunks/botany 2018-06-13 00:07:13 +00:00
Jake Funke 2eb347e11b garden bar text 2018-06-13 00:06:57 +00:00
Jake Funke 93c8656c55
Merge pull request #16 from Ensiss/sort-fix
Fix sort by age
2018-06-12 17:05:35 -07:00
Ensis 8616904350 Add autocompletion for logins in the visit prompt 2018-06-10 12:05:17 +02:00
Ensis 8e18ecbee6 Fix race conditions messing up the display 2018-06-07 11:09:29 +02:00
Ensis 5288ead6b6 Fix sort by age 2018-06-06 22:34:30 +02:00
Jake Funke 5b839650fe
Merge pull request #15 from Ensiss/garden-controls
Add controls for the garden screen
2018-06-06 10:48:38 -07:00
10 changed files with 675 additions and 436 deletions

1
.gitignore vendored
View File

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

View File

@ -1,22 +1,21 @@
# 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/
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 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*
## 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 `$ python botany.py`.
Run with `$ python3 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.
@ -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!
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
@ -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",
"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
"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
}
```
@ -59,8 +64,14 @@ If your plant goes 5 days without water, it will die! Recruit your friends to wa
## requirements
* Unix-based OS (Mac, Linux)
* Python 2.x
* Python 3.x
* Recommended: 80x24 minimum terminal, fixed-width font
## 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)

View File

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

67
botany-view.py 100755
View File

@ -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
View File

@ -1,353 +1,50 @@
#!/usr/bin/python2
#!/usr/bin/env python3
from __future__ import division
import time
import pickle
import json
import os
import random
import getpass
import threading
import errno
import uuid
import sqlite3
from menu_screen import *
import menu_screen as ms
from plant import Plant
# 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):
# This is your plant!
stage_list = [
'seed',
'seedling',
'young',
'mature',
'flowering',
'seed-bearing',
]
# notes from vilmibm
color_list = [
'red',
'orange',
'yellow',
'green',
'blue',
'indigo',
'violet',
'white',
'black',
'gold',
'rainbow',
]
# 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.
rarity_list = [
'common',
'uncommon',
'rare',
'legendary',
'godly',
]
# 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
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)
# 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
class DataManager(object):
# 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):
# 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
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:
@ -435,8 +96,8 @@ class DataManager(object):
this_plant.migrate_properties()
# get status since last login
is_dead = this_plant.dead_check()
is_watered = this_plant.water_check()
is_dead = this_plant.dead_check()
if not is_dead:
if is_watered:
@ -446,7 +107,7 @@ class DataManager(object):
self.last_water_gain = time.time()
else:
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
def plant_age_convert(self,this_plant):
@ -463,7 +124,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, 0777)
os.chmod(sqlite_dir_path, 0o777)
conn = sqlite3.connect(self.garden_db_path)
init_table_string = """CREATE TABLE IF NOT EXISTS garden (
plant_id tinytext PRIMARY KEY,
@ -480,9 +141,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, 0666)
os.chmod(self.garden_db_path, 0o666)
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):
conn = sqlite3.connect(self.garden_db_path)
@ -499,8 +160,6 @@ 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)
@ -519,6 +178,13 @@ 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()
@ -629,12 +295,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.start_threads(my_plant)
my_plant.start_life(my_data)
try:
botany_menu = CursedMenu(my_plant,my_data)
botany_menu = ms.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:
cleanup()
ms.cleanup()

View File

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

55
completer.py 100644
View File

@ -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]

View File

@ -10,6 +10,8 @@ import json
import sqlite3
import string
import re
import completer
import datetime
class CursedMenu(object):
#TODO: name your plant
@ -34,7 +36,7 @@ class CursedMenu(object):
self.visited_plant = None
self.user_data = this_data
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.infotoggle = 0
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.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)
@ -75,26 +80,23 @@ class CursedMenu(object):
def update_options(self):
# 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:
self.options.insert(-1,"harvest")
else:
if "harvest" in self.options:
self.options.remove("harvest")
if "harvest" in self.options:
self.options.remove("harvest")
def set_options(self, options):
# Validates that the last option is "exit"
if options[-1] is not 'exit':
if options[-1] != '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()
@ -106,12 +108,13 @@ 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 is not "exit":
while request != "exit":
self.draw()
request = self.get_user_input()
self.handle_request(request)
@ -124,6 +127,10 @@ 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
@ -132,9 +139,11 @@ 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
@ -165,6 +174,8 @@ 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:
@ -182,6 +193,7 @@ 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
@ -216,6 +228,7 @@ 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
@ -242,8 +255,10 @@ 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(1, 1, str(user_in), curses.A_NORMAL)
# self.screen.addstr(2, 2, str(user_in), curses.A_NORMAL)
# self.screen.refresh()
# Resize sends curses.KEY_RESIZE, update display
@ -252,14 +267,17 @@ class CursedMenu(object):
self.screen.clear()
self.screen.refresh()
# enter and exit Keys are special cases
# enter, exit, and Q 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(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
return
@ -290,11 +308,21 @@ class CursedMenu(object):
return plant_table
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):
""" 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):
""" Filter table using a pattern, and return the new table """
@ -328,7 +356,6 @@ 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
@ -339,12 +366,16 @@ 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
@ -367,6 +398,8 @@ 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))
@ -531,7 +564,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: {}".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.infotoggle = 1
else:
@ -548,6 +581,7 @@ 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"
)
@ -558,15 +592,18 @@ 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.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):
@ -576,6 +613,7 @@ 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()
@ -592,8 +630,10 @@ 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')]:
if user_in in [ord('Y'), ord('y'), 10]:
self.plant.start_over()
else:
pass
@ -644,22 +684,36 @@ class CursedMenu(object):
visitor_block = 'nobody :('
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
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))
if user_input < 256 and user_input != 10:
if filterfunc(chr(user_input)):
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) == '_':
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):
@ -674,7 +728,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()
guest_garden = self.get_user_string(completer = completer.LoginCompleter)
if not guest_garden:
self.clear_info_pane()
return None
@ -693,26 +747,35 @@ 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):
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)
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))
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))
self.screen.getch()
self.clear_info_pane()
self.draw_plant_ascii(self.plant)
self.visited_plant = None
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 = {}
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 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 100644
View File

@ -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)

View File

@ -40,10 +40,10 @@ def update_garden_db():
# )
# """.format(pid = "asdfasdf", pown = "jake", pdes = "big cool plant", page="25d", psco = str(25), pdead = str(False))
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)