Refactor display, add confirmations for restart
parent
2726b34c2f
commit
81ec3f1167
11
README.md
11
README.md
|
@ -8,6 +8,8 @@ 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 to live!
|
||||||
|
|
||||||
|
*"We do not "come into" this world; we come out of it, as leaves from a tree." - Alan Watts*
|
||||||
|
|
||||||
*(work in progress)*
|
*(work in progress)*
|
||||||
|
|
||||||
## getting started
|
## getting started
|
||||||
|
@ -27,10 +29,12 @@ If your plant goes 5 days without water, it will die!
|
||||||
|
|
||||||
## features
|
## features
|
||||||
* Curses-based menu system, optimized for 80x24 terminal
|
* Curses-based menu system, optimized for 80x24 terminal
|
||||||
* ASCII art display of plant
|
* 20+ Species of plants w/ ASCII art for each
|
||||||
* Persistent aging system that allows your plant to grow even when app is closed
|
* Persistent aging system that allows your plant to grow even when app is closed
|
||||||
|
* Random and rare mutations can occur at any point in a plant's life
|
||||||
* SQLite Community Garden of other users' plants (for shared unix servers)
|
* SQLite Community Garden of other users' plants (for shared unix servers)
|
||||||
* Data file is created in the user's home (~) directory, along with a JSON file that can be used in other apps.
|
* Data files are created in the user's home (~) directory, along with a JSON file that can be used in other apps.
|
||||||
|
* Data is created for your current plant and harvested plants
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
|
@ -49,9 +53,8 @@ If your plant goes 5 days without water, it will die!
|
||||||
|
|
||||||
### to-dos
|
### to-dos
|
||||||
* Finish garden feature
|
* Finish garden feature
|
||||||
* Allows you to water neighbor's plants
|
* Water neighbor's plants
|
||||||
* Harvest plant at end of life (gather seeds)
|
* Harvest plant at end of life (gather seeds)
|
||||||
* Create harvest file with a log of all previous plants
|
|
||||||
* Plant pollination - cross-breed with neighbor plants to unlock second-gen plants
|
* Plant pollination - cross-breed with neighbor plants to unlock second-gen plants
|
||||||
* Share seeds with other users
|
* Share seeds with other users
|
||||||
* Global events
|
* Global events
|
||||||
|
|
11
botany.py
11
botany.py
|
@ -2,18 +2,13 @@ from __future__ import division
|
||||||
import time
|
import time
|
||||||
import pickle
|
import pickle
|
||||||
import json
|
import json
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import getpass
|
import getpass
|
||||||
import threading
|
import threading
|
||||||
import errno
|
import errno
|
||||||
import uuid
|
import uuid
|
||||||
import fcntl
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from collections import OrderedDict
|
|
||||||
from operator import itemgetter
|
|
||||||
from menu_screen import *
|
from menu_screen import *
|
||||||
|
|
||||||
# development plan
|
# development plan
|
||||||
|
@ -260,8 +255,7 @@ class Plant(object):
|
||||||
self.kill_plant()
|
self.kill_plant()
|
||||||
while self.write_lock:
|
while self.write_lock:
|
||||||
# Wait for garden writer to unlock
|
# Wait for garden writer to unlock
|
||||||
# garden datafile needs to register that plant has died before
|
# garden db needs to update before allowing the user to reset
|
||||||
# allowing the user to reset
|
|
||||||
pass
|
pass
|
||||||
if not self.write_lock:
|
if not self.write_lock:
|
||||||
self.new_seed(self.file_name)
|
self.new_seed(self.file_name)
|
||||||
|
@ -482,7 +476,6 @@ class DataManager(object):
|
||||||
def data_write_json(self, this_plant):
|
def data_write_json(self, this_plant):
|
||||||
# create personal json file for user to use outside of the game (website?)
|
# create personal json file for user to use outside of the game (website?)
|
||||||
json_file = os.path.join(self.botany_dir,self.this_user + '_plant_data.json')
|
json_file = os.path.join(self.botany_dir,self.this_user + '_plant_data.json')
|
||||||
json_leaderboard = os.path.join(self.game_dir + '_garden.json')
|
|
||||||
# also updates age
|
# also updates age
|
||||||
age_formatted = self.plant_age_convert(this_plant)
|
age_formatted = self.plant_age_convert(this_plant)
|
||||||
plant_info = {
|
plant_info = {
|
||||||
|
@ -518,6 +511,7 @@ class DataManager(object):
|
||||||
else:
|
else:
|
||||||
this_harvest = {}
|
this_harvest = {}
|
||||||
new_file_check = True
|
new_file_check = True
|
||||||
|
|
||||||
this_harvest[this_plant_id] = plant_info
|
this_harvest[this_plant_id] = plant_info
|
||||||
|
|
||||||
# dump harvest file
|
# dump harvest file
|
||||||
|
@ -540,6 +534,7 @@ if __name__ == '__main__':
|
||||||
my_data.data_write_json(my_plant)
|
my_data.data_write_json(my_plant)
|
||||||
my_plant.start_life()
|
my_plant.start_life()
|
||||||
my_data.start_threads(my_plant)
|
my_data.start_threads(my_plant)
|
||||||
|
# TODO: curses wrapper
|
||||||
botany_menu = CursedMenu(my_plant,my_data)
|
botany_menu = CursedMenu(my_plant,my_data)
|
||||||
my_data.save_plant(my_plant)
|
my_data.save_plant(my_plant)
|
||||||
my_data.data_write_json(my_plant)
|
my_data.data_write_json(my_plant)
|
||||||
|
|
138
menu_screen.py
138
menu_screen.py
|
@ -1,4 +1,9 @@
|
||||||
import curses, os, traceback, threading, time, datetime, pickle, operator, random, sqlite3
|
import curses
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
class CursedMenu(object):
|
class CursedMenu(object):
|
||||||
#TODO: name your plant
|
#TODO: name your plant
|
||||||
|
@ -17,8 +22,6 @@ class CursedMenu(object):
|
||||||
self.plant_string = self.plant.parse_plant()
|
self.plant_string = self.plant.parse_plant()
|
||||||
self.plant_ticks = str(self.plant.ticks)
|
self.plant_ticks = str(self.plant.ticks)
|
||||||
self.exit = False
|
self.exit = False
|
||||||
self.instructiontoggle = False
|
|
||||||
self.gardenmenutoggle = False
|
|
||||||
self.infotoggle = 0
|
self.infotoggle = 0
|
||||||
self.maxy, self.maxx = self.screen.getmaxyx()
|
self.maxy, self.maxx = self.screen.getmaxyx()
|
||||||
# Highlighted and Normal line definitions
|
# Highlighted and Normal line definitions
|
||||||
|
@ -73,7 +76,7 @@ class CursedMenu(object):
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
# Makes sure data is saved in event of a crash due to window resizing
|
# Makes sure data is saved in event of a crash due to window resizing
|
||||||
self.screen.clear()
|
self.screen.clear()
|
||||||
self.screen.addstr(0,0,"Enlarge terminal!")
|
self.screen.addstr(0, 0, "Enlarge terminal!", curses.A_NORMAL)
|
||||||
self.screen.refresh()
|
self.screen.refresh()
|
||||||
self.__exit__()
|
self.__exit__()
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -91,7 +94,7 @@ class CursedMenu(object):
|
||||||
# Also calls __exit__, but adds traceback after
|
# Also calls __exit__, but adds traceback after
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
self.screen.clear()
|
self.screen.clear()
|
||||||
self.screen.addstr(0,0,"Enlarge terminal!")
|
self.screen.addstr(0, 0, "Enlarge terminal!", curses.A_NORMAL)
|
||||||
self.screen.refresh()
|
self.screen.refresh()
|
||||||
self.__exit__()
|
self.__exit__()
|
||||||
#traceback.print_exc()
|
#traceback.print_exc()
|
||||||
|
@ -104,7 +107,7 @@ class CursedMenu(object):
|
||||||
this_string = this_file.readlines()
|
this_string = this_file.readlines()
|
||||||
this_file.close()
|
this_file.close()
|
||||||
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()
|
||||||
|
|
||||||
def draw_plant_ascii(self, this_plant):
|
def draw_plant_ascii(self, this_plant):
|
||||||
|
@ -148,25 +151,25 @@ 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.addstr(2,2, self.title, curses.A_STANDOUT) # Title for this menu
|
self.screen.addstr(2, 2, self.title, curses.A_STANDOUT) # Title for this menu
|
||||||
self.screen.addstr(4,2, self.subtitle, curses.A_BOLD) #Subtitle for this menu
|
self.screen.addstr(4, 2, self.subtitle, curses.A_BOLD) #Subtitle for this menu
|
||||||
# clear menu on screen
|
# clear menu on screen
|
||||||
for index in range(len(self.options)+1):
|
for index in range(len(self.options)+1):
|
||||||
self.screen.addstr(5+index,4, clear_bar, curses.A_NORMAL)
|
self.screen.addstr(5+index, 4, clear_bar, curses.A_NORMAL)
|
||||||
# display all the menu items, showing the 'pos' item highlighted
|
# display all the menu items, showing the 'pos' item highlighted
|
||||||
for index in range(len(self.options)):
|
for index in range(len(self.options)):
|
||||||
textstyle = self.normal
|
textstyle = self.normal
|
||||||
if index == self.selected:
|
if index == self.selected:
|
||||||
textstyle = self.highlighted
|
textstyle = self.highlighted
|
||||||
self.screen.addstr(5+index,4, clear_bar, curses.A_NORMAL)
|
self.screen.addstr(5+index ,4, clear_bar, curses.A_NORMAL)
|
||||||
self.screen.addstr(5+index,4, "%d - %s" % (index+1, self.options[index]), textstyle)
|
self.screen.addstr(5+index ,4, "%d - %s" % (index+1, self.options[index]), textstyle)
|
||||||
|
|
||||||
self.screen.addstr(11,2, clear_bar, curses.A_NORMAL)
|
self.screen.addstr(11, 2, clear_bar, curses.A_NORMAL)
|
||||||
self.screen.addstr(12,2, clear_bar, curses.A_NORMAL)
|
self.screen.addstr(12, 2, clear_bar, curses.A_NORMAL)
|
||||||
self.screen.addstr(11,2, "plant: ", curses.A_DIM)
|
self.screen.addstr(11, 2, "plant: ", curses.A_DIM)
|
||||||
self.screen.addstr(11,9, self.plant_string, curses.A_NORMAL)
|
self.screen.addstr(11, 9, self.plant_string, curses.A_NORMAL)
|
||||||
self.screen.addstr(12,2, "score: ", curses.A_DIM)
|
self.screen.addstr(12, 2, "score: ", curses.A_DIM)
|
||||||
self.screen.addstr(12,9, self.plant_ticks, curses.A_NORMAL)
|
self.screen.addstr(12, 9, self.plant_ticks, curses.A_NORMAL)
|
||||||
|
|
||||||
if not self.plant.dead:
|
if not self.plant.dead:
|
||||||
if int(time.time()) <= self.plant.watered_timestamp + 24*3600:
|
if int(time.time()) <= self.plant.watered_timestamp + 24*3600:
|
||||||
|
@ -198,7 +201,7 @@ 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__()
|
||||||
# DEBUG KEYS - enable to see curses key codes
|
## DEBUG KEYS - enable to see curses key codes
|
||||||
# self.screen.addstr(1, 1, str(user_in), curses.A_NORMAL)
|
# self.screen.addstr(1, 1, str(user_in), curses.A_NORMAL)
|
||||||
# self.screen.refresh()
|
# self.screen.refresh()
|
||||||
|
|
||||||
|
@ -220,12 +223,15 @@ class CursedMenu(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
# increment or Decrement
|
# increment or Decrement
|
||||||
down_keys = [curses.KEY_DOWN, 14, 106]
|
down_keys = [curses.KEY_DOWN, 14, ord('j')]
|
||||||
up_keys = [curses.KEY_UP, 16, 107]
|
up_keys = [curses.KEY_UP, 16, ord('k')]
|
||||||
|
|
||||||
if user_in in down_keys: # down arrow
|
if user_in in down_keys: # down arrow
|
||||||
self.selected += 1
|
self.selected += 1
|
||||||
if user_in in up_keys: # up arrow
|
if user_in in up_keys: # up arrow
|
||||||
self.selected -=1
|
self.selected -=1
|
||||||
|
|
||||||
|
# modulo to wrap around
|
||||||
self.selected = self.selected % len(self.options)
|
self.selected = self.selected % len(self.options)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -247,35 +253,25 @@ class CursedMenu(object):
|
||||||
return paginated_list
|
return paginated_list
|
||||||
|
|
||||||
def draw_garden(self):
|
def draw_garden(self):
|
||||||
# draws neighborhood
|
# draws community garden
|
||||||
clear_bar = " " * (self.maxx-2) + "\n"
|
|
||||||
clear_block = clear_bar * 5
|
|
||||||
control_keys = [curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT]
|
|
||||||
# load data from sqlite db
|
# load data from sqlite db
|
||||||
this_garden = self.user_data.retrieve_garden_from_db()
|
this_garden = self.user_data.retrieve_garden_from_db()
|
||||||
# format data
|
# format data
|
||||||
|
self.clear_info_pane()
|
||||||
plant_table_pages = []
|
plant_table_pages = []
|
||||||
if self.infotoggle != 2:
|
if self.infotoggle != 2:
|
||||||
# if infotoggle isn't 2, the screen currently displays other stuff
|
# if infotoggle isn't 2, the screen currently displays other stuff
|
||||||
# we want to prep the screen for showing the garden
|
|
||||||
# Clear text area of other text (look info, etc) first
|
|
||||||
for y, line in enumerate(clear_block.splitlines(), 2):
|
|
||||||
self.screen.addstr(y+12, 2, line)
|
|
||||||
self.screen.refresh()
|
|
||||||
plant_table_pages = self.format_garden_data(this_garden)
|
plant_table_pages = self.format_garden_data(this_garden)
|
||||||
self.infotoggle = 2
|
self.infotoggle = 2
|
||||||
else:
|
else:
|
||||||
# the screen IS currently showing the garden (1 page), make the
|
# the screen IS currently showing the garden (1 page), make the
|
||||||
# text a bunch of blanks to clear it out
|
# text a bunch of blanks to clear it out
|
||||||
big_clear_block = clear_bar * (self.maxy - 14)
|
|
||||||
plant_table_pages.append(big_clear_block.splitlines())
|
|
||||||
self.infotoggle = 0
|
self.infotoggle = 0
|
||||||
|
|
||||||
# print garden information OR clear it
|
# print garden information OR clear it
|
||||||
for page_num, page in enumerate(plant_table_pages, 1):
|
for page_num, page in enumerate(plant_table_pages, 1):
|
||||||
# Print page text
|
# Print page text
|
||||||
for y, line in enumerate(page, 2):
|
self.draw_info_text(page)
|
||||||
self.screen.addstr(y+12, 2, line)
|
|
||||||
if len(plant_table_pages) > 1:
|
if len(plant_table_pages) > 1:
|
||||||
# Multiple pages, paginate and require keypress
|
# Multiple pages, paginate and require keypress
|
||||||
page_text = "(%d/%d) --- press any key ---" % (page_num, len(plant_table_pages))
|
page_text = "(%d/%d) --- press any key ---" % (page_num, len(plant_table_pages))
|
||||||
|
@ -283,8 +279,7 @@ class CursedMenu(object):
|
||||||
self.screen.getch()
|
self.screen.getch()
|
||||||
self.screen.refresh()
|
self.screen.refresh()
|
||||||
# Clear page before drawing next
|
# Clear page before drawing next
|
||||||
for y, line in enumerate(range(self.maxy-16), 2):
|
self.clear_info_pane()
|
||||||
self.screen.addstr(y+12, 2, clear_bar)
|
|
||||||
self.infotoggle = 0
|
self.infotoggle = 0
|
||||||
self.screen.refresh()
|
self.screen.refresh()
|
||||||
else:
|
else:
|
||||||
|
@ -425,29 +420,22 @@ class CursedMenu(object):
|
||||||
return output_text
|
return output_text
|
||||||
|
|
||||||
def draw_plant_description(self, this_plant):
|
def draw_plant_description(self, this_plant):
|
||||||
clear_bar = " " * (self.maxx-2) + "\n"
|
|
||||||
# If menu is currently showing something other than the description
|
# If menu is currently showing something other than the description
|
||||||
|
self.clear_info_pane()
|
||||||
if self.infotoggle != 1:
|
if self.infotoggle != 1:
|
||||||
# Clear lines before printing description
|
|
||||||
output_string = clear_bar * (self.maxy - 15)
|
|
||||||
for y, line in enumerate(output_string.splitlines(), 2):
|
|
||||||
self.screen.addstr(y+12, 2, line)
|
|
||||||
self.screen.refresh()
|
|
||||||
# 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)
|
||||||
|
self.draw_info_text(output_string)
|
||||||
self.infotoggle = 1
|
self.infotoggle = 1
|
||||||
else:
|
else:
|
||||||
# otherwise just set data as blanks
|
# otherwise just set toggle
|
||||||
output_string = clear_bar * 3
|
|
||||||
self.infotoggle = 0
|
self.infotoggle = 0
|
||||||
|
|
||||||
for y, line in enumerate(output_string.splitlines(), 2):
|
|
||||||
self.screen.addstr(y+12, 2, line)
|
|
||||||
self.screen.refresh()
|
|
||||||
|
|
||||||
def draw_instructions(self):
|
def draw_instructions(self):
|
||||||
# TODO: tidy this up
|
# Draw instructions on screen
|
||||||
if not self.instructiontoggle:
|
self.clear_info_pane()
|
||||||
|
if self.infotoggle != 4:
|
||||||
instructions_txt = """welcome to botany. you've been given a seed
|
instructions_txt = """welcome to botany. you've been given a seed
|
||||||
that will grow into a beautiful plant. check
|
that will grow into a beautiful plant. check
|
||||||
in and water your plant every 24h to keep it
|
in and water your plant every 24h to keep it
|
||||||
|
@ -456,31 +444,49 @@ plant depends on you to live! more info is
|
||||||
available in the readme :)
|
available in the readme :)
|
||||||
cheers,
|
cheers,
|
||||||
curio"""
|
curio"""
|
||||||
self.instructiontoggle = not self.instructiontoggle
|
self.draw_info_text(instructions_txt)
|
||||||
|
self.infotoggle = 4
|
||||||
else:
|
else:
|
||||||
instructions_txt = """
|
self.infotoggle = 0
|
||||||
|
|
||||||
|
def clear_info_pane(self):
|
||||||
|
# Clears bottom part of screen
|
||||||
|
clear_bar = " " * (self.maxx-2) + "\n"
|
||||||
|
clear_block = clear_bar * (self.maxy - 15)
|
||||||
|
for y, line in enumerate(clear_block.splitlines(), 2):
|
||||||
"""
|
self.screen.addstr(y+12, 2, line, curses.A_NORMAL)
|
||||||
self.instructiontoggle = not self.instructiontoggle
|
|
||||||
for y, line in enumerate(instructions_txt.splitlines(), 2):
|
|
||||||
self.screen.addstr(self.maxy-12+y,self.maxx-47, line)
|
|
||||||
self.screen.refresh()
|
self.screen.refresh()
|
||||||
|
|
||||||
def harvest_confirmation():
|
def draw_info_text(self, info_text):
|
||||||
#TODO: confirm users want to restart when harvesting
|
if type(info_text) is str:
|
||||||
pass
|
info_text = info_text.splitlines()
|
||||||
|
|
||||||
|
for y, line in enumerate(info_text, 2):
|
||||||
|
self.screen.addstr(y+12, 2, line, curses.A_NORMAL)
|
||||||
|
self.screen.refresh()
|
||||||
|
|
||||||
|
def harvest_confirmation(self):
|
||||||
|
self.clear_info_pane()
|
||||||
|
# get plant description before printing
|
||||||
|
harvest_text = "If you harvest your plant you'll start over from a seed.\nContinue? (Y/n)"
|
||||||
|
self.draw_info_text(harvest_text)
|
||||||
|
try:
|
||||||
|
user_in = self.screen.getch() # Gets user input
|
||||||
|
except Exception as e:
|
||||||
|
self.__exit__()
|
||||||
|
|
||||||
|
if user_in == ord('Y'):
|
||||||
|
self.plant.start_over()
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
self.clear_info_pane()
|
||||||
|
|
||||||
def handle_request(self, request):
|
def handle_request(self, request):
|
||||||
'''this is where you do things with the request'''
|
# Menu options call functions here
|
||||||
if request == None: return
|
if request == None: return
|
||||||
if request == "harvest":
|
if request == "harvest":
|
||||||
|
#TODO: should harvest be separate from dead plant start over?
|
||||||
self.plant.start_over()
|
self.harvest_confirmation()
|
||||||
if request == "water":
|
if request == "water":
|
||||||
self.plant.water()
|
self.plant.water()
|
||||||
if request == "look":
|
if request == "look":
|
||||||
|
|
Loading…
Reference in New Issue