├── .gitignore ├── art ├── sun.txt ├── seed.txt ├── template.txt ├── lithops1.txt ├── lithops2.txt ├── flytrap1.txt ├── seedling.txt ├── cactus1.txt ├── fern1.txt ├── lithops3.txt ├── pansy1.txt ├── columbine1.txt ├── daffodil1.txt ├── jadeplant1.txt ├── sage1.txt ├── aloe1.txt ├── moon.txt ├── pachypodium1.txt ├── poppy1.txt ├── hemp1.txt ├── snapdragon1.txt ├── sunflower1.txt ├── palm1.txt ├── columbine2.txt ├── iris1.txt ├── pansy2.txt ├── pansy3.txt ├── sage2.txt ├── daffodil2.txt ├── moss1.txt ├── moss2.txt ├── moss3.txt ├── brugmansia1.txt ├── poppy2.txt ├── poppy3.txt ├── snapdragon2.txt ├── snapdragon3.txt ├── agave1.txt ├── aloe2.txt ├── bee.txt ├── cactus2.txt ├── cactus3.txt ├── hemp2.txt ├── sage3.txt ├── columbine3.txt ├── fern2.txt ├── iris2.txt ├── pachypodium2.txt ├── sunflower2.txt ├── sunflower3.txt ├── hemp3.txt ├── baobab1.txt ├── brugmansia2.txt ├── daffodil3.txt ├── fern3.txt ├── iris3.txt ├── jadeplant2.txt ├── jadeplant3.txt ├── pachypodium3.txt ├── brugmansia3.txt ├── agave2.txt ├── palm2.txt ├── palm3.txt ├── aloe3.txt ├── flytrap2.txt ├── baobab2.txt ├── flytrap3.txt ├── agave3.txt ├── rip.txt ├── baobab3.txt ├── ficus1.txt ├── jackolantern.txt ├── ficus2.txt └── ficus3.txt ├── clear_weekly_users.py ├── LICENSE ├── completer.py ├── botany-view.py ├── testsql.py ├── README.md ├── botany.py ├── plant.py └── menu_screen.py /.gitignore: -------------------------------------------------------------------------------- 1 | garden_db.sqlite 2 | *.pyc 3 | garden_file.dat 4 | garden_file.json 5 | sqlite/ 6 | *.swp 7 | -------------------------------------------------------------------------------- /art/sun.txt: -------------------------------------------------------------------------------- 1 | . 2 | \ | / 3 | '-.;;;;;.-' 4 | -==;;;;;;;==- 5 | .-';;;;;'-. 6 | / | \ 7 | ' 8 | -------------------------------------------------------------------------------- /art/seed.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | . , _ . . _ , _ ., _ . 10 | ^ ' o ` ' 11 | -------------------------------------------------------------------------------- /art/template.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | . , _ . ., , _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/lithops1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | __ __ 9 | . , _ . ( | ) ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/lithops2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | .__v___ 9 | . , _ .( | )., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/flytrap1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | C % c 8 | (\C/ 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/seedling.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | . ; 8 | \| 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/cactus1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | _\_\/ 7 | -( / )- 8 | \_/ 9 | . , _ . .,@@@ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/fern1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | \|/, 7 | .\|/ |/ 8 | \|/ l/ 9 | . , _ . ., l /_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/lithops3.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ***** 7 | \V/ 8 | .__v___ 9 | . , _ .( | )., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/pansy1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | \ o / 7 | o| |o o 8 | \/ / 9 | . , _ . ., l/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/columbine1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | \ | | / 7 | | \|&| 8 | &\/&/ 9 | . , _ . ., &/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/daffodil1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -\ 6 | |/- 7 | - | 8 | \| 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/jadeplant1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | . , 7 | o%O %,o 8 | \%o' 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/sage1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | '\+/` /` 7 | '\|/`|/` 8 | \|/ |/` 9 | . , _ . . ,l-/`,., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/aloe1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | . . . 6 | |\.,n/|./| 7 | \\\/ /| / 8 | '| \ / / 9 | . , _ . .\ |,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/moon.txt: -------------------------------------------------------------------------------- 1 | _.._ 2 | .' .-'` 3 | / / 4 | | | 5 | \ '.___.; 6 | '._ _.' 7 | `` 8 | -------------------------------------------------------------------------------- /art/pachypodium1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | _ / _ 5 | / \/|// \ 6 | < > 7 | < .> 8 | < ` '> 9 | ` , <' > _ . _ . 10 | ^ ' . ` ' 11 | -------------------------------------------------------------------------------- /art/poppy1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | O 5 | | 6 | \o 7 | |o 8 | \/ 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/hemp1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ^ 5 | l% 6 | %\| _ 7 | |/ % 8 | %\| /% 9 | . , _ . ., l/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/snapdragon1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | + 5 | |/ 6 | \| 7 | |/ 8 | \| 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/sunflower1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | O 5 | \// 6 | || 7 | ||/ 8 | \|| 9 | . , _ . ., || _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/palm1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | _ , . _ 5 | / \`\ / /^\ 6 | | /\ ||/ \ | 7 | | /\||\ | 8 | || 9 | . , _ . .,|_| _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/columbine2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | \ | / 5 | | &|/ |& / 6 | &\ | / / 7 | \| \|&| 8 | &\/&/ 9 | . , _ . ., &/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/iris1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | _, . , 5 | / \ \V / /\ 6 | |\ \\v/ |` 7 | /\ v ||/ \ 8 | \\v// 9 | . , _ . .,\V/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/pansy2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | o o 5 | \o /_o o 6 | o\| / /__o 7 | | |o /o 8 | \/ /o 9 | . , _ . ., l/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/pansy3.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | % % 5 | \% /_% % 6 | %\| / /__% 7 | | |% /% 8 | \/ /% 9 | . , _ . ., l/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/sage2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | `\' 5 | `\' /` 6 | `| '\|/` /` 7 | `\|'\|/`|/` 8 | ,'\ \|/ |/` 9 | . , _ .\|/,l-/`,., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/daffodil2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | -\, 4 | |/- 5 | \| 6 | -| |/- 7 | \ | | /- 8 | \|/.| 9 | . , _ . ., l,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/moss1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /\ __ 5 | _/ / \ 6 | _/ / / `--. 7 | __/ ' _ \_ \ 8 | / / _/ ##- \ \ \ 9 | ._ / _ . .#### _|., _ .| 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/moss2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /\ __ 5 | _/##/ \ 6 | _/ /##/####`--. 7 | __/ ' _###\_ ### \ 8 | / / #_/###-#\####\ \ 9 | ._ / _#########_|#,#_ .| 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/moss3.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /\ __ 5 | _/##/ \ 6 | _/ /*#/*#*#`--. 7 | __/ ' _###\_ *## \ 8 | / / #_/##*-#\###*\ \ 9 | ._ / _###*#*###_|#,#_ .| 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/brugmansia1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | _. 4 | //\\ //\ 5 | | |\\ // \\ 6 | \V//| | 7 | || 8 | ||/ 9 | . , _ . ., || _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/poppy2.txt: -------------------------------------------------------------------------------- 1 | 2 | O 3 | o | 4 | | | o 5 | o| |&| 6 | &\\o/ 7 | ||o 8 | &\/ 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/poppy3.txt: -------------------------------------------------------------------------------- 1 | 2 | % 3 | % | 4 | | | % 5 | %| |&| 6 | &\\%/ 7 | ||% 8 | &\/ 9 | . , _ . ., l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/snapdragon2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | + 4 | | , 5 | + , |/ 6 | '\ \| , 7 | \| ,|/ + , 8 | \|/ \| |/ 9 | . , _ . \, l,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/snapdragon3.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | % 4 | | % 5 | % % |/ 6 | %\ \| % 7 | \| ,|/ % % 8 | \|/ \| |/ 9 | . , _ . \, l,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/agave1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | . , , . 5 | |\ |`, || /| ., 6 | |\v| \v /|v/| |/ | 7 | \| \\\/ /| / V / 8 | \_| \ / / ,/ 9 | . , _ .\ \ |,/_/., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/aloe2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | .\ .| 4 | \\ || ._ 5 | ..\ \\. //. // 6 | \ \.\|v ||// 7 | \\\ || // 8 | '| \ / / 9 | . , _ . .\ |,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/bee.txt: -------------------------------------------------------------------------------- 1 | _ 2 | /_) 3 | (8_))}- . 4 | \_) '. 5 | .--. . 6 | : '. .' 7 | '---'`; 8 | . 9 | _.' 10 | .' 11 | ' _ 12 | '._. , ' `, . 13 | -------------------------------------------------------------------------------- /art/cactus2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | +-+, 4 | ,+\/|`| \ 5 | \' | |'| ., 6 | \ `| | |/ ) 7 | | |'| , / 8 | |'| |, / 9 | . , _ . |_|_| | ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/cactus3.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | +*+, 4 | *+\/|`* \ 5 | \' | |'| ., 6 | * `| | |/ ) 7 | | |'| , / 8 | |'| |, / 9 | . , _ . |_|_| | ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/hemp2.txt: -------------------------------------------------------------------------------- 1 | 2 | ^ 3 | ^+^ 4 | + ^+^+ 5 | \+^+^/ 6 | ^+\|^/^/ 7 | \+^\|/ ^ 8 | ^+\|+/+/ 9 | . , _ . ., l/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/sage3.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | ++ 4 | +\+ + 5 | + +\+ /+ 6 | +| '\|/` /+ 7 | `\|'\|/`|/` 8 | ,'\ \|/ |/` 9 | . , _ .\|/,l-/`,., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/columbine3.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | % % 4 | %\ | / % 5 | % | &|/ |& / 6 | %&\ | / /% 7 | \| \|&| 8 | &\/&/ 9 | . , _ . ., &/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/fern2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | | 4 | \|/ | 5 | \|/, \|/ 6 | .| \|/,\// . 7 | \|/ \|/ |/ |/` 8 | \\/,\|/|l///, 9 | . , _ .`\,\lv/_// _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/iris2.txt: -------------------------------------------------------------------------------- 1 | 2 | /\ 3 | /\ | /\ 4 | _ | || / 5 | / \ \||/ /\ 6 | |\ \\v/ ` 7 | /\ v ||/\ 8 | \\v// 9 | . , _ . .,\V/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/pachypodium2.txt: -------------------------------------------------------------------------------- 1 | _ / _ 2 | /_\/|//u 3 | U | \ 4 | < > 5 | <`> _/ 6 | <, > <> 7 | < .><.> 8 | < ` >'> 9 | ` , <' o >_ . _ . 10 | ^ / ' \ . ` ' 11 | -------------------------------------------------------------------------------- /art/sunflower2.txt: -------------------------------------------------------------------------------- 1 | 2 | __ 3 | (##) 4 | & || 5 | \//& 6 | || 7 | ||/& 8 | &\|| 9 | . , _ . ., || _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/sunflower3.txt: -------------------------------------------------------------------------------- 1 | 2 | \||/ 3 | |--OO-- 4 | -o-/||\ 5 | |\//& 6 | || 7 | ||/& 8 | &\|| 9 | . , _ . ., || _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/hemp3.txt: -------------------------------------------------------------------------------- 1 | 2 | % 3 | ~ %+% 4 | + %+%+ ~ 5 | ~ \+%+%/ 6 | ^%\|%/%/ ~ 7 | ~ \+^\|/ ^ 8 | ^+\|+/+/ 9 | . , _ . ., l/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/baobab1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | && & & & && 4 | ^ \ | ^ | ^/ 5 | ^|^ |^ ^|^/ 6 | | * | 7 | |. ; `| 8 | | . | 9 | . , _ .(. ) _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/brugmansia2.txt: -------------------------------------------------------------------------------- 1 | 2 | _ /n\ 3 | //.\ || \| 4 | // \\|`//\ 5 | | ||\|// \ 6 | /\V/| | 7 | | || 8 | ||/\ 9 | . , _ . ., || _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/daffodil3.txt: -------------------------------------------------------------------------------- 1 | 2 | /|< 3 | >|\ / 4 | |/-|< 5 | \| 6 | >|-| |/-|< 7 | \ | | /-|< 8 | \|/.| 9 | . , _ . ., l,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/fern3.txt: -------------------------------------------------------------------------------- 1 | 2 | % 3 | % 4 | \|/ % 5 | \|/, \%/ 6 | .% \|/,\// . 7 | \%/ \|/ %/ |/% 8 | \\/,\|/|l///, 9 | . , _ .`\,\lv/_// _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/iris3.txt: -------------------------------------------------------------------------------- 1 | 2 | /\ 3 | %/\ | % /\ 4 | _ | || / % 5 | / \ \||/ /\ 6 | % |\ \\v/ % 7 | /\ v ||/\ 8 | % \\v// % 9 | . , _ . .,\V/ _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/jadeplant2.txt: -------------------------------------------------------------------------------- 1 | 2 | % % %% 3 | %% |O%o| %o % 4 | &&O%\o|%|/% /& 5 | & &\o%O %,o& 6 | O &\%o& 7 | ||.|/ 8 | \\ | 9 | . , _ . .|,l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/jadeplant3.txt: -------------------------------------------------------------------------------- 1 | 2 | o o %* 3 | %* |O%o| *o % 4 | *oO%\o|o|/% *& 5 | * &\o%O *,o& 6 | O *\*o& 7 | ||.|/ 8 | \\ | 9 | . , _ . .|,l, _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/pachypodium3.txt: -------------------------------------------------------------------------------- 1 | _ <, _ 2 | <_\/|//\\ 3 | // | \\ U 4 | U // >\) _ 5 | u<`> _/ | 6 | <, >/<>\ 7 | U.> 8 | < ` >'> 9 | ` , <' o >_ . _ . 10 | ^ / ' \ . ` ' 11 | -------------------------------------------------------------------------------- /art/brugmansia3.txt: -------------------------------------------------------------------------------- 1 | 2 | _ /n\ 3 | //.\^||^\^ 4 | // \\|`//\ 5 | /^ ||\|// ^\^ 6 | ^ /\V/|^ ^ 7 | | ||^ 8 | ^ ||/\ 9 | . , _ . ., || _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/agave2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | , , . 4 | . |`. |\ /| ., 5 | . |\ | | /||^| | /| 6 | \ \v| \v /|v/| |/ | 7 | \ |\ |/ /| / V / 8 | \^\ \ \ \ / / ,| / 9 | . , \ \\ \ |,/_/./ / . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/palm2.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | ',\ / / '-. 3 | /--\ \ / /^\ 4 | |/ /\ ||/ \,| 5 | ` | /\=|\,| ` 6 | ` |_| ` 7 | |_| 8 | |_|. 9 | . , _ . .|__| _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/palm3.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | ',* /*/ '-. 3 | /--\ **/ /^\ 4 | |/ /\**|/ \,| 5 | ` | /\=|\,| ` 6 | ` |_| ` 7 | |_| 8 | |_|. 9 | . , _ . .|__| _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/aloe3.txt: -------------------------------------------------------------------------------- 1 | =\= 2 | =|= 3 | .\ | .| 4 | \\ | || ._ 5 | ..\ \\||//. // 6 | \ \.\|v ||// 7 | \\\ || // 8 | '| \ / / 9 | . , _ . .\ |,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/flytrap2.txt: -------------------------------------------------------------------------------- 1 | 2 | ._, 3 | .__, ,/ | _,_,, 4 | ._\ \, |--/ /\ \ \< 5 | '\ \ | \/ ||\ \,|< 6 | '\_+/, || // \_/` 7 | ' \\ ||// , 8 | \\\|/ / 9 | . , _ . .\ l,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/baobab2.txt: -------------------------------------------------------------------------------- 1 | 2 | &&& && && &&& 3 | &&&&\& & v & /& && 4 | && \ | \^ , &/^/& 5 | | |^ 6 | | , | 7 | |. ; | 8 | / . \ 9 | . , _ ( . )_ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/flytrap3.txt: -------------------------------------------------------------------------------- 1 | % % 2 | \| ._, 3 | .__,| ,/ | _,_,, 4 | ._\ \,\|--/ /\ \ \< 5 | '\ \ | \\/ ||\ \,|< 6 | '\_+/, ||%// \_/` 7 | ' \\ ||// % 8 | \\\|/ / 9 | . , _ . .\ l,/_ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/agave3.txt: -------------------------------------------------------------------------------- 1 | oo|oo 2 | oo|oo 3 | , oo|oo . 4 | . |`. \|\ /| ., 5 | . |\ | | /||^| | /| 6 | \ \v| \v /|v/| |/ | 7 | \ |\ |/ /| / V / 8 | \^\ \ \ \ / / ,| / 9 | . , \ \\ \ |,/_/./ / . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/rip.txt: -------------------------------------------------------------------------------- 1 | ______________ 2 | / \ 3 | | | 4 | | | 5 | | R.I.P. | 6 | | | 7 | | | 8 | | | 9 | . |, _\/ .. \. \ /,|_ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/baobab3.txt: -------------------------------------------------------------------------------- 1 | * *& * * 2 | &&& *&& && &&& 3 | &*&&\* & * & /* && 4 | *& \ | \^ , &/*/& 5 | *| |^ 6 | | , | 7 | |. ; | 8 | / . \ 9 | . , _ ( . )_ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/ficus1.txt: -------------------------------------------------------------------------------- 1 | & \ & & 2 | &\|,/ |/& && 3 | &|/& / & & 4 | \ { |___/_& 5 | { {/ / & 6 | `, \{______/_& 7 | } }{ \_& 8 | }{{ 9 | . , , -=-~{ .-^- _ _ . 10 | ^ ' 11 | -------------------------------------------------------------------------------- /art/jackolantern.txt: -------------------------------------------------------------------------------- 1 | /)) HAPPY 2 | __(((__ HALLOWEEN 3 | .' _`""`_`'. 4 | / /\\ /\\ \ 5 | | /)_\\/)_\\ | 6 | | _ _()_ _ | 7 | | \\/\\/\\// | 8 | \ \/\/\/\/ / 9 | . , .'.___..___.' _ ., _ . 10 | ^ ' ` ' 11 | -------------------------------------------------------------------------------- /art/ficus2.txt: -------------------------------------------------------------------------------- 1 | &&&\/& &&& 2 | &\|,/ |/& && 3 | &&/ / /_&& && 4 | \ { |_____/_& 5 | { / / & & &&& 6 | `, \{___________/_&& 7 | } }{ &\____& 8 | }{{ `&\&& 9 | {}{ && 10 | . , , -=-~{ .-^- _ _ . 11 | -------------------------------------------------------------------------------- /art/ficus3.txt: -------------------------------------------------------------------------------- 1 | &*&\/& *&& 2 | &\|,/ |/& *& 3 | *&/ / /_&& && 4 | \ { |_____/_* 5 | {* / / & & *&& 6 | `, \{___________/_*& 7 | } }{ *\____& 8 | }{{ `&\*& 9 | {}{ && 10 | . , , -=-~{ .-^- _ _ . 11 | -------------------------------------------------------------------------------- /clear_weekly_users.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | 4 | game_dir = os.path.dirname(os.path.realpath(__file__)) 5 | garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite') 6 | conn = sqlite3.connect(garden_db_path) 7 | c = conn.cursor() 8 | c.execute("DELETE FROM visitors") 9 | print("Cleared weekly users") 10 | conn.commit() 11 | conn.close() 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017, Jacob Funke 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /completer.py: -------------------------------------------------------------------------------- 1 | class LoginCompleter: 2 | """ A loop-based completion system for logins """ 3 | def __init__(self, menu): 4 | self.s = "" 5 | self.logins = None 6 | self.completions = [] 7 | # completion_id has a value of -1 for the base user input 8 | # and between 0 and len(completions)-1 for completions 9 | self.completion_id = -1 10 | self.completion_base = "" 11 | self.menu = menu 12 | 13 | def initialize(self): 14 | """ Initialise the list of completable logins """ 15 | garden = self.menu.user_data.retrieve_garden_from_db() 16 | self.logins = set() 17 | for plant_id in garden: 18 | if not garden[plant_id]: 19 | continue 20 | entry = garden[plant_id] 21 | if "owner" in entry: 22 | self.logins.add(entry["owner"]) 23 | self.logins = sorted(list(self.logins)) 24 | 25 | def update_input(self, s): 26 | """ Update the user input and reset completion base """ 27 | self.s = s 28 | self.completion_base = self.s 29 | self.completion_id = -1 30 | 31 | def complete(self, direction = 1): 32 | """ 33 | Returns the completed string from the user input 34 | Loops forward in the list of logins if direction is positive, and 35 | backwards if direction is negative 36 | """ 37 | def loginFilter(x): 38 | return x.startswith(self.s) & (x != self.s) 39 | 40 | # Refresh possible completions after the user edits 41 | if self.completion_id == -1: 42 | if self.logins is None: 43 | self.initialize() 44 | self.completion_base = self.s 45 | self.completions = list(filter(loginFilter, self.logins)) 46 | 47 | self.completion_id += direction 48 | # Loop from the back 49 | if self.completion_id == -2: 50 | self.completion_id = len(self.completions) - 1 51 | # If we are at the base input, return it 52 | if self.completion_id == -1 or self.completion_id == len(self.completions): 53 | self.completion_id = -1 54 | return self.completion_base 55 | return self.completions[self.completion_id] 56 | -------------------------------------------------------------------------------- /botany-view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | 5 | from botany import * 6 | 7 | def ascii_render(filename): 8 | # Prints ASCII art from file at given coordinates 9 | this_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),"art") 10 | this_filename = os.path.join(this_dir,filename) 11 | this_file = open(this_filename,"r") 12 | this_string = this_file.read() 13 | this_file.close() 14 | print(this_string) 15 | 16 | def draw_plant_ascii(this_plant): 17 | # this list should be somewhere where it could have been inherited, instead 18 | # of hardcoded in more than one place... 19 | plant_art_list = [ 20 | 'poppy', 21 | 'cactus', 22 | 'aloe', 23 | 'flytrap', 24 | 'jadeplant', 25 | 'fern', 26 | 'daffodil', 27 | 'sunflower', 28 | 'baobab', 29 | 'lithops', 30 | 'hemp', 31 | 'pansy', 32 | 'iris', 33 | 'agave', 34 | 'ficus', 35 | 'moss', 36 | 'sage', 37 | 'snapdragon', 38 | 'columbine', 39 | 'brugmansia', 40 | 'palm', 41 | 'pachypodium', 42 | ] 43 | if this_plant.dead == True: 44 | ascii_render('rip.txt') 45 | elif datetime.date.today().month == 10 and datetime.date.today().day == 31: 46 | ascii_render('jackolantern.txt') 47 | elif this_plant.stage == 0: 48 | ascii_render('seed.txt') 49 | elif this_plant.stage == 1: 50 | ascii_render('seedling.txt') 51 | elif this_plant.stage == 2: 52 | this_filename = plant_art_list[this_plant.species]+'1.txt' 53 | ascii_render(this_filename) 54 | elif this_plant.stage == 3 or this_plant.stage == 5: 55 | this_filename = plant_art_list[this_plant.species]+'2.txt' 56 | ascii_render(this_filename) 57 | elif this_plant.stage == 4: 58 | this_filename = plant_art_list[this_plant.species]+'3.txt' 59 | ascii_render(this_filename) 60 | 61 | if __name__ == '__main__': 62 | my_data = DataManager() 63 | # if plant save file exists 64 | if my_data.check_plant(): 65 | my_plant = my_data.load_plant() 66 | # otherwise create new plant 67 | else: 68 | my_plant = Plant(my_data.savefile_path) 69 | my_data.data_write_json(my_plant) 70 | draw_plant_ascii(my_plant) 71 | -------------------------------------------------------------------------------- /testsql.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | garden_db_path = "sqlite/garden_db.sqlite" 4 | 5 | def init_database(): 6 | #TODO: does this need permissions? 7 | conn = sqlite3.connect(garden_db_path) 8 | init_table_string = """CREATE TABLE IF NOT EXISTS garden ( 9 | plant_id tinytext PRIMARY KEY, 10 | owner text, 11 | description text, 12 | age text, 13 | score integer, 14 | is_dead text 15 | )""" 16 | 17 | c = conn.cursor() 18 | c.execute(init_table_string) 19 | conn.close() 20 | def update_garden_db(): 21 | # insert or update this plant id's entry in DB (should happen 22 | # regularly) 23 | # TODO: create a second function that is called to retrieve garden 24 | # when called by display controller 25 | 26 | 27 | conn = sqlite3.connect(garden_db_path) 28 | c = conn.cursor() 29 | ##try to insert or replace 30 | update_query = """INSERT OR REPLACE INTO garden ( 31 | plant_id, owner, description, age, score, is_dead 32 | ) VALUES ( 33 | '{pid}', '{pown}', '{pdes}', '{page}', {psco}, '{pdead}' 34 | ) 35 | """.format(pid = "asdfaseeeedf", pown = "jaeeke", pdes = "bigger ceeooler plant", page="28dee", psco = str(244400), pdead = str(False)) 36 | # update_query = """INSERT INTO garden ( 37 | # plant_id, owner, description, age, score, is_dead 38 | # ) VALUES ( 39 | # '{pid}', '{pown}', '{pdes}', '{page}', {psco}, '{pdead}' 40 | # ) 41 | # """.format(pid = "asdfasdf", pown = "jake", pdes = "big cool plant", page="25d", psco = str(25), pdead = str(False)) 42 | 43 | print(c.execute(update_query)) 44 | conn.commit() 45 | conn.close() 46 | #print("bigggg booom") 47 | 48 | def retrieve_garden_from_db(garden_db_path): 49 | # Builds a dict of dicts from garden sqlite db 50 | garden_dict = {} 51 | conn = sqlite3.connect(garden_db_path) 52 | conn.row_factory = sqlite3.Row 53 | c = conn.cursor() 54 | c.execute('SELECT * FROM garden ORDER BY owner') 55 | tuple_list = c.fetchall() 56 | conn.close() 57 | # Building dict from table rows 58 | for item in tuple_list: 59 | garden_dict[item[0]] = { 60 | "owner":item[1], 61 | "description":item[2], 62 | "age":item[3], 63 | "score":item[4], 64 | "dead":item[5], 65 | } 66 | return garden_dict 67 | 68 | #init_database() 69 | #update_garden_db() 70 | results = retrieve_garden_from_db(garden_db_path) 71 | print(results) 72 | 73 | 74 | # con = sqlite3.connect(garden_db_path) # 75 | # con.row_factory = sqlite3.Row # 76 | # cur = con.cursor() # 77 | # cur.execute("select * from garden ORDER BY score desc") # 78 | # blah = cur.fetchall() # 79 | # con.close() 80 | # print(blah) 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # botany 2 | ![Screencap](https://tilde.town/~curiouser/botany.png) 3 | 4 | by Jake Funke - jifunks@gmail.com - tilde.town/~curiouser - http://jakefunke.online/ 5 | 6 | A command line, realtime, community plant buddy. 7 | 8 | You've been given a seed that will grow into a beautiful plant. 9 | 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! 10 | 11 | *"We do not 'come into' this world; we come out of it, as leaves from a tree." - Alan Watts* 12 | 13 | ## getting started 14 | botany is designed for unix-based systems. Clone into a local directory using `$ git clone https://github.com/jifunks/botany.git`. 15 | 16 | Run with `$ python3 botany.py`. 17 | 18 | *Note - botany.py must initially be run by the user who cloned/unzipped botany.py - this initializes the shared data file permissions.* 19 | 20 | Water your seed to get started. You can come and go as you please and your plant continues to grow. 21 | 22 | Make sure to come back and water every 24 hours or your plant won't grow. 23 | 24 | If your plant goes 5 days without water, it will die! Recruit your friends to water your plant for you! 25 | 26 | A once-weekly cron on clear_weekly_users.py should be set up to keep weekly visitors tidy. 27 | 28 | 29 | ## features 30 | * Curses-based menu system, optimized for 80x24 terminal 31 | * 20+ Species of plants w/ ASCII art for each 32 | * Persistent aging system that allows your plant to grow even when app is closed 33 | * Multiplayer! Water your friends plants & see who's visited your garden. 34 | * Generations: each plant you bring to its full growth potential rewards you 35 | with 20% growth speed for the next plant 36 | * Random and rare mutations can occur at any point in a plant's life 37 | * SQLite Community Garden of other users' plants (for shared unix servers) 38 | * Data files are created in the user's home (~) directory, along with a JSON file that can be used in other apps. 39 | * Data is created for your current plant and harvested plants 40 | 41 | ``` 42 | { 43 | "description":"common screaming mature jade plant", 44 | "generation":1, 45 | "file_name":"/home/curiouser/.botany/curiouser_plant.dat", 46 | "owner":"curiouser", 47 | "species":"jade plant", 48 | "stage":"mature", 49 | "age":"24d:2h:16m:19s", 50 | "rarity":"common", 51 | "score":955337.0, 52 | "mutation":"screaming", 53 | "last_watered":1529007007, 54 | "is_dead":false 55 | } 56 | ``` 57 | 58 | ### to-dos 59 | * Plant pollination - cross-breed with neighbor plants to unlock second-gen plants 60 | * Share seeds with other users 61 | * Global events 62 | * Server API to have rain storms, heat waves, insects 63 | * Hybridization, lineage tracking 64 | 65 | ## requirements 66 | * Unix-based OS (Mac, Linux) 67 | * Python 3.x 68 | * Recommended: 80x24 minimum terminal, fixed-width font 69 | 70 | ## credits 71 | * thank you [tilde.town](http://tilde.town/) for inspiration! 72 | 73 | ## praise for botany 74 | ![Screencap](https://tilde.town/~curiouser/praise1.png) 75 | ![Screencap](https://tilde.town/~curiouser/praise2.png) 76 | ![Screencap](https://tilde.town/~curiouser/praise3.png) 77 | 78 | ## Featured in Linux Magazine! 79 | ![IMG_7497](https://github.com/user-attachments/assets/5b778452-6cd1-489d-aeba-082f31538eb7) 80 | (via Graham Morrison "FOSSPicks" *Linux Magazine*, Issue 273, 2023) 81 | 82 | 83 | -------------------------------------------------------------------------------- /botany.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | import pickle 5 | import json 6 | import os 7 | import getpass 8 | import threading 9 | import errno 10 | import sqlite3 11 | import menu_screen as ms 12 | from plant import Plant 13 | 14 | # TODO: 15 | # - switch from personal data file to row in DB 16 | # - is threading necessary? 17 | # - use a different curses window for plant, menu, info window, score 18 | 19 | # notes from vilmibm 20 | 21 | # there are threads. 22 | # - life thread. sleeps a variable amount of time based on generation bonus. increases tick count (ticks == score). 23 | # - screen: sleeps 1s per loop. draws interface (including plant). for seeing score/plant change without user input. 24 | # meanwhile, the main thread handles input and redraws curses as needed. 25 | 26 | # affordance index 27 | # - main screen 28 | # navigable menu, plant, score, etc 29 | # - water 30 | # render a visualization of moistness; allow to water 31 | # - look 32 | # print a description of plant with info below rest of UI 33 | # - garden 34 | # runs a paginated view of every plant on the computer below rest of UI. to return to menu navigation must hit q. 35 | # - visit 36 | # 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. 37 | # - instructions 38 | # prints some explanatory text below the UI 39 | # - exit 40 | # quits program 41 | 42 | # 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. 43 | # ideally, multiple windows would be used: 44 | # - the menu. it doesn't change unless the plant dies OR the plant hits stage 5, then "harvest" is dynamically added. 45 | # - the plant viewer. this is updated in "real time" as the plant grows. 46 | # - the status display: score and plant description 47 | # - the infow window. updated by visit/garden/instructions/look 48 | 49 | class DataManager(object): 50 | # handles user data, puts a .botany dir in user's home dir (OSX/Linux) 51 | # handles shared data with sqlite db 52 | # TODO: .dat save should only happen on mutation, water, death, exit, 53 | # harvest, otherwise 54 | # data hasn't changed... 55 | # can write json whenever bc this isn't ever read for data within botany 56 | 57 | user_dir = os.path.expanduser("~") 58 | botany_dir = os.path.join(user_dir,'.botany') 59 | game_dir = os.path.dirname(os.path.realpath(__file__)) 60 | this_user = getpass.getuser() 61 | 62 | savefile_name = this_user + '_plant.dat' 63 | savefile_path = os.path.join(botany_dir, savefile_name) 64 | #set this.savefile_path to guest_garden path 65 | 66 | garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite') 67 | garden_json_path = os.path.join(game_dir, 'garden_file.json') 68 | harvest_file_path = os.path.join(botany_dir, 'harvest_file.dat') 69 | harvest_json_path = os.path.join(botany_dir, 'harvest_file.json') 70 | 71 | def __init__(self): 72 | self.this_user = getpass.getuser() 73 | # check if instance is already running 74 | # check for .botany dir in home 75 | try: 76 | os.makedirs(self.botany_dir) 77 | except OSError as exception: 78 | if exception.errno != errno.EEXIST: 79 | raise 80 | self.savefile_name = self.this_user + '_plant.dat' 81 | 82 | def check_plant(self): 83 | # check for existing save file 84 | if os.path.isfile(self.savefile_path) and os.path.getsize(self.savefile_path) > 0: 85 | return True 86 | else: 87 | return False 88 | 89 | def load_plant(self): 90 | # load savefile 91 | with open(self.savefile_path, 'rb') as f: 92 | this_plant = pickle.load(f) 93 | 94 | # migrate data structure to create data for empty/nonexistent plant 95 | # properties 96 | this_plant.migrate_properties() 97 | 98 | # get status since last login 99 | is_watered = this_plant.water_check() 100 | is_dead = this_plant.dead_check() 101 | 102 | if not is_dead: 103 | if is_watered: 104 | time_delta_last = int(time.time()) - this_plant.last_time 105 | ticks_to_add = min(time_delta_last, 24*3600) 106 | this_plant.time_delta_watered = 0 107 | self.last_water_gain = time.time() 108 | else: 109 | ticks_to_add = 0 110 | this_plant.ticks += ticks_to_add * round(0.2 * (this_plant.generation - 1) + 1, 1) 111 | return this_plant 112 | 113 | def plant_age_convert(self,this_plant): 114 | # human-readable plant age 115 | age_seconds = int(time.time()) - this_plant.start_time 116 | days, age_seconds = divmod(age_seconds, 24 * 60 * 60) 117 | hours, age_seconds = divmod(age_seconds, 60 * 60) 118 | minutes, age_seconds = divmod(age_seconds, 60) 119 | age_formatted = ("%dd:%dh:%dm:%ds" % (days, hours, minutes, age_seconds)) 120 | return age_formatted 121 | 122 | def init_database(self): 123 | # check if dir exists, create sqlite directory and set OS permissions to 777 124 | sqlite_dir_path = os.path.join(self.game_dir,'sqlite') 125 | if not os.path.exists(sqlite_dir_path): 126 | os.makedirs(sqlite_dir_path) 127 | os.chmod(sqlite_dir_path, 0o777) 128 | conn = sqlite3.connect(self.garden_db_path) 129 | init_table_string = """CREATE TABLE IF NOT EXISTS garden ( 130 | plant_id tinytext PRIMARY KEY, 131 | owner text, 132 | description text, 133 | age text, 134 | score integer, 135 | is_dead numeric 136 | )""" 137 | 138 | c = conn.cursor() 139 | c.execute(init_table_string) 140 | conn.close() 141 | 142 | # init only, creates and sets permissions for garden db and json 143 | if os.stat(self.garden_db_path).st_uid == os.getuid(): 144 | os.chmod(self.garden_db_path, 0o666) 145 | open(self.garden_json_path, 'a').close() 146 | os.chmod(self.garden_json_path, 0o666) 147 | 148 | def migrate_database(self): 149 | conn = sqlite3.connect(self.garden_db_path) 150 | migrate_table_string = """CREATE TABLE IF NOT EXISTS visitors ( 151 | id integer PRIMARY KEY, 152 | garden_name text, 153 | visitor_name text, 154 | weekly_visits integer 155 | )""" 156 | c = conn.cursor() 157 | c.execute(migrate_table_string) 158 | conn.close() 159 | return True 160 | 161 | def update_garden_db(self, this_plant): 162 | # insert or update this plant id's entry in DB 163 | self.init_database() 164 | self.migrate_database() 165 | age_formatted = self.plant_age_convert(this_plant) 166 | conn = sqlite3.connect(self.garden_db_path) 167 | c = conn.cursor() 168 | # try to insert or replace 169 | update_query = """INSERT OR REPLACE INTO garden ( 170 | plant_id, owner, description, age, score, is_dead 171 | ) VALUES ( 172 | '{pid}', '{pown}', '{pdes}', '{page}', {psco}, {pdead} 173 | ) 174 | """.format(pid = this_plant.plant_id, 175 | pown = this_plant.owner, 176 | pdes = this_plant.parse_plant(), 177 | page = age_formatted, 178 | psco = str(this_plant.ticks), 179 | pdead = int(this_plant.dead)) 180 | c.execute(update_query) 181 | # clean other instances of user 182 | clean_query = """UPDATE garden set is_dead = 1 183 | where owner = '{pown}' 184 | and plant_id <> '{pid}' 185 | """.format(pown = this_plant.owner, 186 | pid = this_plant.plant_id) 187 | c.execute(clean_query) 188 | conn.commit() 189 | conn.close() 190 | 191 | def retrieve_garden_from_db(self): 192 | # Builds a dict of dicts from garden sqlite db 193 | garden_dict = {} 194 | conn = sqlite3.connect(self.garden_db_path) 195 | # Need to allow write permissions by others 196 | conn.row_factory = sqlite3.Row 197 | c = conn.cursor() 198 | c.execute('SELECT * FROM garden ORDER BY owner') 199 | tuple_list = c.fetchall() 200 | conn.close() 201 | # Building dict from table rows 202 | for item in tuple_list: 203 | garden_dict[item[0]] = { 204 | "owner":item[1], 205 | "description":item[2], 206 | "age":item[3], 207 | "score":item[4], 208 | "dead":item[5], 209 | } 210 | return garden_dict 211 | 212 | def update_garden_json(self): 213 | this_garden = self.retrieve_garden_from_db() 214 | with open(self.garden_json_path, 'w') as outfile: 215 | json.dump(this_garden, outfile) 216 | pass 217 | 218 | def save_plant(self, this_plant): 219 | # create savefile 220 | this_plant.last_time = int(time.time()) 221 | temp_path = self.savefile_path + ".temp" 222 | with open(temp_path, 'wb') as f: 223 | pickle.dump(this_plant, f, protocol=2) 224 | os.rename(temp_path, self.savefile_path) 225 | 226 | def data_write_json(self, this_plant): 227 | # create personal json file for user to use outside of the game (website?) 228 | json_file = os.path.join(self.botany_dir,self.this_user + '_plant_data.json') 229 | # also updates age 230 | age_formatted = self.plant_age_convert(this_plant) 231 | plant_info = { 232 | "owner":this_plant.owner, 233 | "description":this_plant.parse_plant(), 234 | "age":age_formatted, 235 | "score":this_plant.ticks, 236 | "is_dead":this_plant.dead, 237 | "last_watered":this_plant.watered_timestamp, 238 | "file_name":this_plant.file_name, 239 | "stage": this_plant.stage_list[this_plant.stage], 240 | "generation": this_plant.generation, 241 | } 242 | if this_plant.stage >= 3: 243 | plant_info["rarity"] = this_plant.rarity_list[this_plant.rarity] 244 | if this_plant.mutation != 0: 245 | plant_info["mutation"] = this_plant.mutation_list[this_plant.mutation] 246 | if this_plant.stage >= 4: 247 | plant_info["color"] = this_plant.color_list[this_plant.color] 248 | if this_plant.stage >= 2: 249 | plant_info["species"] = this_plant.species_list[this_plant.species] 250 | 251 | with open(json_file, 'w') as outfile: 252 | json.dump(plant_info, outfile) 253 | 254 | def harvest_plant(self, this_plant): 255 | # TODO: plant history feature - could just use a sqlite query to retrieve all of user's dead plants 256 | 257 | # harvest is a dict of dicts 258 | # harvest contains one entry for each plant id 259 | age_formatted = self.plant_age_convert(this_plant) 260 | this_plant_id = this_plant.plant_id 261 | plant_info = { 262 | "description":this_plant.parse_plant(), 263 | "age":age_formatted, 264 | "score":this_plant.ticks, 265 | } 266 | if os.path.isfile(self.harvest_file_path): 267 | # harvest file exists: load data 268 | with open(self.harvest_file_path, 'rb') as f: 269 | this_harvest = pickle.load(f) 270 | new_file_check = False 271 | else: 272 | this_harvest = {} 273 | new_file_check = True 274 | 275 | this_harvest[this_plant_id] = plant_info 276 | 277 | # dump harvest file 278 | temp_path = self.harvest_file_path + ".temp" 279 | with open(temp_path, 'wb') as f: 280 | pickle.dump(this_harvest, f, protocol=2) 281 | os.rename(temp_path, self.harvest_file_path) 282 | # dump json file 283 | with open(self.harvest_json_path, 'w') as outfile: 284 | json.dump(this_harvest, outfile) 285 | 286 | return new_file_check 287 | 288 | if __name__ == '__main__': 289 | my_data = DataManager() 290 | # if plant save file exists 291 | if my_data.check_plant(): 292 | my_plant = my_data.load_plant() 293 | # otherwise create new plant 294 | else: 295 | my_plant = Plant(my_data.savefile_path) 296 | my_data.data_write_json(my_plant) 297 | # my_plant is either a fresh plant or an existing plant at this point 298 | my_plant.start_life(my_data) 299 | 300 | ms.main(my_plant, my_data) 301 | my_data.save_plant(my_plant) 302 | my_data.data_write_json(my_plant) 303 | my_data.update_garden_db(my_plant) 304 | -------------------------------------------------------------------------------- /plant.py: -------------------------------------------------------------------------------- 1 | import random 2 | import os 3 | import json 4 | import threading 5 | import time 6 | import uuid 7 | import getpass 8 | 9 | class Plant: 10 | # This is your plant! 11 | stage_list = [ 12 | 'seed', 13 | 'seedling', 14 | 'young', 15 | 'mature', 16 | 'flowering', 17 | 'seed-bearing', 18 | ] 19 | 20 | color_list = [ 21 | 'red', 22 | 'orange', 23 | 'yellow', 24 | 'green', 25 | 'blue', 26 | 'indigo', 27 | 'violet', 28 | 'white', 29 | 'black', 30 | 'gold', 31 | 'rainbow', 32 | ] 33 | 34 | rarity_list = [ 35 | 'common', 36 | 'uncommon', 37 | 'rare', 38 | 'legendary', 39 | 'godly', 40 | ] 41 | 42 | species_list = [ 43 | 'poppy', 44 | 'cactus', 45 | 'aloe', 46 | 'venus flytrap', 47 | 'jade plant', 48 | 'fern', 49 | 'daffodil', 50 | 'sunflower', 51 | 'baobab', 52 | 'lithops', 53 | 'hemp', 54 | 'pansy', 55 | 'iris', 56 | 'agave', 57 | 'ficus', 58 | 'moss', 59 | 'sage', 60 | 'snapdragon', 61 | 'columbine', 62 | 'brugmansia', 63 | 'palm', 64 | 'pachypodium', 65 | ] 66 | 67 | mutation_list = [ 68 | '', 69 | 'humming', 70 | 'noxious', 71 | 'vorpal', 72 | 'glowing', 73 | 'electric', 74 | 'icy', 75 | 'flaming', 76 | 'psychic', 77 | 'screaming', 78 | 'chaotic', 79 | 'hissing', 80 | 'gelatinous', 81 | 'deformed', 82 | 'shaggy', 83 | 'scaly', 84 | 'depressed', 85 | 'anxious', 86 | 'metallic', 87 | 'glossy', 88 | 'psychedelic', 89 | 'bonsai', 90 | 'foamy', 91 | 'singing', 92 | 'fractal', 93 | 'crunchy', 94 | 'goth', 95 | 'oozing', 96 | 'stinky', 97 | 'aromatic', 98 | 'juicy', 99 | 'smug', 100 | 'vibrating', 101 | 'lithe', 102 | 'chalky', 103 | 'naive', 104 | 'ersatz', 105 | 'disco', 106 | 'levitating', 107 | 'colossal', 108 | 'luminous', 109 | 'cosmic', 110 | 'ethereal', 111 | 'cursed', 112 | 'buff', 113 | 'narcotic', 114 | 'gnu/linux', 115 | 'abraxan', # rip dear friend 116 | ] 117 | 118 | 119 | def __init__(self, this_filename, generation=1): 120 | # Constructor 121 | self.plant_id = str(uuid.uuid4()) 122 | self.life_stages = (3600*24, (3600*24)*3, (3600*24)*10, (3600*24)*20, (3600*24)*30) 123 | # self.life_stages = (2, 4, 6, 8, 10) # debug mode 124 | self.stage = 0 125 | self.mutation = 0 126 | self.species = random.randint(0,len(self.species_list)-1) 127 | self.color = random.randint(0,len(self.color_list)-1) 128 | self.rarity = self.rarity_check() 129 | self.ticks = 0 130 | self.age_formatted = "0" 131 | self.generation = generation 132 | self.dead = False 133 | self.write_lock = False 134 | self.owner = getpass.getuser() 135 | self.file_name = this_filename 136 | self.start_time = int(time.time()) 137 | self.last_time = int(time.time()) 138 | # must water plant first day 139 | self.watered_timestamp = int(time.time())-(24*3600)-1 140 | self.watered_24h = False 141 | self.visitors = [] 142 | 143 | def migrate_properties(self): 144 | # Migrates old data files to new 145 | if not hasattr(self, 'generation'): 146 | self.generation = 1 147 | if not hasattr(self, 'visitors'): 148 | self.visitors = [] 149 | 150 | def parse_plant(self): 151 | # Converts plant data to human-readable format 152 | output = "" 153 | if self.stage >= 3: 154 | output += self.rarity_list[self.rarity] + " " 155 | if self.mutation != 0: 156 | output += self.mutation_list[self.mutation] + " " 157 | if self.stage >= 4: 158 | output += self.color_list[self.color] + " " 159 | output += self.stage_list[self.stage] + " " 160 | if self.stage >= 2: 161 | output += self.species_list[self.species] + " " 162 | return output.strip() 163 | 164 | def rarity_check(self): 165 | # Generate plant rarity 166 | CONST_RARITY_MAX = 256.0 167 | rare_seed = random.randint(1,int(CONST_RARITY_MAX)) 168 | common_range = round((2.0/3)*CONST_RARITY_MAX) 169 | uncommon_range = round((2.0/3)*(CONST_RARITY_MAX-common_range)) 170 | rare_range = round((2.0/3)*(CONST_RARITY_MAX-common_range-uncommon_range)) 171 | legendary_range = round((2.0/3)*(CONST_RARITY_MAX-common_range-uncommon_range-rare_range)) 172 | 173 | common_max = common_range 174 | uncommon_max = common_max + uncommon_range 175 | rare_max = uncommon_max + rare_range 176 | legendary_max = rare_max + legendary_range 177 | godly_max = CONST_RARITY_MAX 178 | 179 | if 0 <= rare_seed <= common_max: 180 | rarity = 0 181 | elif common_max < rare_seed <= uncommon_max: 182 | rarity = 1 183 | elif uncommon_max < rare_seed <= rare_max: 184 | rarity = 2 185 | elif rare_max < rare_seed <= legendary_max: 186 | rarity = 3 187 | elif legendary_max < rare_seed <= godly_max: 188 | rarity = 4 189 | return rarity 190 | 191 | def dead_check(self): 192 | if self.dead: 193 | return True 194 | # if it has been >5 days since watering, sorry plant is dead :( 195 | time_delta_watered = int(time.time()) - self.watered_timestamp 196 | if time_delta_watered > (5 * (24 * 3600)): 197 | self.dead = True 198 | return self.dead 199 | 200 | def update_visitor_db(self, visitor_names): 201 | game_dir = os.path.dirname(os.path.realpath(__file__)) 202 | garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite') 203 | conn = sqlite3.connect(garden_db_path) 204 | for name in (visitor_names): 205 | c = conn.cursor() 206 | c.execute("SELECT * FROM visitors WHERE garden_name = '{}' AND visitor_name = '{}' ".format(self.owner, name)) 207 | data=c.fetchone() 208 | if data is None: 209 | sql = """ INSERT INTO visitors (garden_name,visitor_name,weekly_visits) VALUES('{}', '{}',1)""".format(self.owner, name) 210 | c.execute(sql) 211 | else: 212 | sql = """ UPDATE visitors SET weekly_visits = weekly_visits + 1 WHERE garden_name = '{}' AND visitor_name = '{}'""".format(self.owner, name) 213 | c.execute(sql) 214 | conn.commit() 215 | conn.close() 216 | 217 | def guest_check(self): 218 | user_dir = os.path.expanduser("~") 219 | botany_dir = os.path.join(user_dir,'.botany') 220 | visitor_filepath = os.path.join(botany_dir,'visitors.json') 221 | guest_timestamps = [] 222 | visitors_this_check = [] 223 | if os.path.isfile(visitor_filepath): 224 | with open(visitor_filepath, 'r') as visitor_file: 225 | data = json.load(visitor_file) 226 | if data: 227 | for element in data: 228 | if element['user'] not in self.visitors: 229 | self.visitors.append(element['user']) 230 | if element['user'] not in visitors_this_check: 231 | visitors_this_check.append(element['user']) 232 | # prevent users from manually setting watered_time in the future 233 | if element['timestamp'] <= int(time.time()) and element['timestamp'] >= self.watered_timestamp: 234 | guest_timestamps.append(element['timestamp']) 235 | try: 236 | self.update_visitor_db(visitors_this_check) 237 | except: 238 | pass 239 | with open(visitor_filepath, 'w') as visitor_file: 240 | visitor_file.write('[]') 241 | else: 242 | with open(visitor_filepath, mode='w') as f: 243 | json.dump([], f) 244 | os.chmod(visitor_filepath, 0o666) 245 | if not guest_timestamps: 246 | return self.watered_timestamp 247 | all_timestamps = [self.watered_timestamp] + guest_timestamps 248 | all_timestamps.sort() 249 | # calculate # of days between each guest watering 250 | timestamp_diffs = [(j-i)/86400.0 for i, j in zip(all_timestamps[:-1], all_timestamps[1:])] 251 | # plant's latest timestamp should be set to last timestamp before a 252 | # gap of 5 days 253 | # TODO: this considers a plant watered only on day 1 and day 4 to be 254 | # watered for all 4 days - need to figure out how to only add score 255 | # from 24h after each watered timestamp 256 | last_valid_element = next((x for x in timestamp_diffs if x > 5), None) 257 | if not last_valid_element: 258 | # all timestamps are within a 5 day range, can just use latest one 259 | return all_timestamps[-1] 260 | last_valid_index = timestamp_diffs.index(last_valid_element) 261 | # slice list to only include up until a >5 day gap 262 | valid_timestamps = all_timestamps[:last_valid_index + 1] 263 | return valid_timestamps[-1] 264 | 265 | def water_check(self): 266 | self.watered_timestamp = self.guest_check() 267 | self.time_delta_watered = int(time.time()) - self.watered_timestamp 268 | if self.time_delta_watered <= (24 * 3600): 269 | if not self.watered_24h: 270 | self.watered_24h = True 271 | return True 272 | else: 273 | self.watered_24h = False 274 | return False 275 | 276 | def mutate_check(self): 277 | # Create plant mutation 278 | # Increase this # to make mutation rarer (chance 1 out of x each second) 279 | CONST_MUTATION_RARITY = 10000 280 | mutation_seed = random.randint(1,CONST_MUTATION_RARITY) 281 | if mutation_seed == CONST_MUTATION_RARITY: 282 | # mutation gained! 283 | mutation = random.randint(0,len(self.mutation_list)-1) 284 | if self.mutation == 0: 285 | self.mutation = mutation 286 | return True 287 | else: 288 | return False 289 | 290 | def growth(self): 291 | # Increase plant growth stage 292 | if self.stage < (len(self.stage_list)-1): 293 | self.stage += 1 294 | 295 | def water(self): 296 | # Increase plant growth stage 297 | if not self.dead: 298 | self.watered_timestamp = int(time.time()) 299 | self.watered_24h = True 300 | 301 | def start_over(self): 302 | # After plant reaches final stage, given option to restart 303 | # increment generation only if previous stage is final stage and plant 304 | # is alive 305 | if not self.dead: 306 | next_generation = self.generation + 1 307 | else: 308 | # Should this reset to 1? Seems unfair.. for now generations will 309 | # persist through death. 310 | next_generation = self.generation 311 | self.write_lock = True 312 | self.kill_plant() 313 | while self.write_lock: 314 | # Wait for garden writer to unlock 315 | # garden db needs to update before allowing the user to reset 316 | pass 317 | if not self.write_lock: 318 | self.__init__(self.file_name, next_generation) 319 | 320 | def kill_plant(self): 321 | self.dead = True 322 | 323 | def unlock_new_creation(self): 324 | self.write_lock = False 325 | 326 | def start_life(self, dm): 327 | # runs life on a thread 328 | thread = threading.Thread(target=self.life, args=(dm,)) 329 | thread.daemon = True 330 | thread.start() 331 | 332 | def life(self, dm): 333 | # I've created life :) 334 | counter = 0 335 | generation_bonus = round(0.2 * (self.generation - 1), 1) 336 | score_inc = 1 * (1 + generation_bonus) 337 | while True: 338 | counter += 1 339 | if not self.dead: 340 | if self.watered_24h: 341 | self.ticks += score_inc 342 | if self.stage < len(self.stage_list)-1: 343 | if self.ticks >= self.life_stages[self.stage]: 344 | self.growth() 345 | if self.mutate_check(): 346 | pass 347 | 348 | if self.water_check(): 349 | # Do something 350 | pass 351 | 352 | if self.dead_check(): 353 | dm.harvest_plant(self) 354 | self.unlock_new_creation() 355 | 356 | if counter % 3 == 0: 357 | dm.save_plant(self) 358 | dm.data_write_json(self) 359 | dm.update_garden_db(self) 360 | 361 | if counter % 30 == 0: 362 | dm.update_garden_json() 363 | counter = 0 364 | 365 | # TODO: event check 366 | time.sleep(2) 367 | -------------------------------------------------------------------------------- /menu_screen.py: -------------------------------------------------------------------------------- 1 | import curses 2 | import datetime 3 | import getpass 4 | import json 5 | import math 6 | import os 7 | import random 8 | import re 9 | import sqlite3 10 | import string 11 | import threading 12 | import time 13 | from typing import TYPE_CHECKING 14 | 15 | import completer 16 | from plant import Plant 17 | 18 | if TYPE_CHECKING: 19 | from botany import DataManager 20 | 21 | 22 | class CursedMenu(object): 23 | # TODO: name your plant 24 | '''A class which abstracts the horrors of building a curses-based menu system''' 25 | def __init__( 26 | self, 27 | stdscr: curses.window, 28 | this_plant: Plant, 29 | this_data: "DataManager", 30 | ): 31 | '''Initialization''' 32 | self.initialized = False 33 | self.screen = stdscr 34 | try: 35 | curses.curs_set(0) 36 | except curses.error: 37 | # Not all terminals support this functionality. 38 | # When the error is ignored the screen will look a little uglier, but that's not terrible 39 | # So in order to keep botany as accessible as possible to everyone, it should be safe to ignore the error. 40 | pass 41 | self.screen.keypad(1) 42 | self.plant = this_plant 43 | self.visited_plant = None 44 | self.user_data = this_data 45 | self.plant_string = self.plant.parse_plant() 46 | self.plant_ticks = str(int(self.plant.ticks)) 47 | self.exit = False 48 | self.infotoggle = 0 49 | self.maxy, self.maxx = self.screen.getmaxyx() 50 | # Highlighted and Normal line definitions 51 | if curses.has_colors(): 52 | self.define_colors() 53 | self.highlighted = curses.color_pair(1) 54 | else: 55 | self.highlighted = curses.A_REVERSE 56 | self.normal = curses.A_NORMAL 57 | # Threaded screen update for live changes 58 | screen_thread = threading.Thread(target=self.update_plant_live, args=()) 59 | screen_thread.daemon = True 60 | screen_thread.start() 61 | # Recursive lock to prevent both threads from drawing at the same time 62 | self.screen_lock = threading.RLock() 63 | self.screen.clear() 64 | 65 | def define_colors(self): 66 | # TODO: implement colors 67 | # set curses color pairs manually 68 | curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) 69 | curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) 70 | curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) 71 | curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) 72 | curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) 73 | curses.init_pair(6, curses.COLOR_YELLOW, curses.COLOR_BLACK) 74 | curses.init_pair(7, curses.COLOR_RED, curses.COLOR_BLACK) 75 | curses.init_pair(8, curses.COLOR_CYAN, curses.COLOR_BLACK) 76 | 77 | def show(self, options, title, subtitle): 78 | # Draws a menu with parameters 79 | self.set_options(options) 80 | self.update_options() 81 | self.title = title 82 | self.subtitle = subtitle 83 | self.selected = 0 84 | self.initialized = True 85 | self.draw_menu() 86 | 87 | def update_options(self): 88 | # Makes sure you can get a new plant if it dies 89 | if self.plant.dead or self.plant.stage == 5: 90 | if "harvest" not in self.options: 91 | self.options.insert(-1,"harvest") 92 | else: 93 | if "harvest" in self.options: 94 | self.options.remove("harvest") 95 | 96 | def set_options(self, options): 97 | # Validates that the last option is "exit" 98 | if options[-1] != 'exit': 99 | options.append('exit') 100 | self.options = options 101 | 102 | def draw(self): 103 | # Draw the menu and lines 104 | self.maxy, self.maxx = self.screen.getmaxyx() 105 | with self.screen_lock: 106 | self.screen.refresh() 107 | try: 108 | self.draw_default() 109 | self.screen.refresh() 110 | except curses.error as exception: 111 | # Makes sure data is saved in event of a crash due to window resizing 112 | self.screen.clear() 113 | self.screen.addstr(0, 0, "Enlarge terminal!", curses.A_NORMAL) 114 | self.screen.refresh() 115 | 116 | def draw_menu(self): 117 | # Actually draws the menu and handles branching 118 | request = "" 119 | while request != "exit": 120 | self.draw() 121 | request = self.get_user_input() 122 | self.handle_request(request) 123 | 124 | def ascii_render(self, filename, ypos, xpos): 125 | # Prints ASCII art from file at given coordinates 126 | this_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),"art") 127 | this_filename = os.path.join(this_dir,filename) 128 | this_file = open(this_filename,"r") 129 | this_string = this_file.readlines() 130 | this_file.close() 131 | with self.screen_lock: 132 | for y, line in enumerate(this_string, 2): 133 | self.screen.addstr(ypos+y, xpos, line, curses.A_NORMAL) 134 | 135 | def draw_plant_ascii(self, this_plant): 136 | ypos = 0 137 | xpos = int((self.maxx-37)/2 + 25) 138 | plant_art_list = [ 139 | 'poppy', 140 | 'cactus', 141 | 'aloe', 142 | 'flytrap', 143 | 'jadeplant', 144 | 'fern', 145 | 'daffodil', 146 | 'sunflower', 147 | 'baobab', 148 | 'lithops', 149 | 'hemp', 150 | 'pansy', 151 | 'iris', 152 | 'agave', 153 | 'ficus', 154 | 'moss', 155 | 'sage', 156 | 'snapdragon', 157 | 'columbine', 158 | 'brugmansia', 159 | 'palm', 160 | 'pachypodium', 161 | ] 162 | if this_plant.dead == True: 163 | self.ascii_render('rip.txt', ypos, xpos) 164 | elif datetime.date.today().month == 10 and datetime.date.today().day == 31: 165 | self.ascii_render('jackolantern.txt', ypos, xpos) 166 | elif this_plant.stage == 0: 167 | self.ascii_render('seed.txt', ypos, xpos) 168 | elif this_plant.stage == 1: 169 | self.ascii_render('seedling.txt', ypos, xpos) 170 | elif this_plant.stage == 2: 171 | this_filename = plant_art_list[this_plant.species]+'1.txt' 172 | self.ascii_render(this_filename, ypos, xpos) 173 | elif this_plant.stage == 3 or this_plant.stage == 5: 174 | this_filename = plant_art_list[this_plant.species]+'2.txt' 175 | self.ascii_render(this_filename, ypos, xpos) 176 | elif this_plant.stage == 4: 177 | this_filename = plant_art_list[this_plant.species]+'3.txt' 178 | self.ascii_render(this_filename, ypos, xpos) 179 | 180 | def draw_default(self): 181 | # draws default menu 182 | clear_bar = " " * (int(self.maxx*2/3)) 183 | with self.screen_lock: 184 | self.screen.addstr(1, 2, self.title, curses.A_STANDOUT) # Title for this menu 185 | self.screen.addstr(3, 2, self.subtitle, curses.A_BOLD) #Subtitle for this menu 186 | # clear menu on screen 187 | for index in range(len(self.options)+1): 188 | self.screen.addstr(4+index, 4, clear_bar, curses.A_NORMAL) 189 | # display all the menu items, showing the 'pos' item highlighted 190 | for index in range(len(self.options)): 191 | textstyle = self.normal 192 | if index == self.selected: 193 | textstyle = self.highlighted 194 | self.screen.addstr(4+index ,4, clear_bar, curses.A_NORMAL) 195 | self.screen.addstr(4+index ,4, "%d - %s" % (index+1, self.options[index]), textstyle) 196 | 197 | self.screen.addstr(12, 2, clear_bar, curses.A_NORMAL) 198 | self.screen.addstr(13, 2, clear_bar, curses.A_NORMAL) 199 | self.screen.addstr(12, 2, "plant: ", curses.A_DIM) 200 | self.screen.addstr(12, 9, self.plant_string, curses.A_NORMAL) 201 | self.screen.addstr(13, 2, "score: ", curses.A_DIM) 202 | self.screen.addstr(13, 9, self.plant_ticks, curses.A_NORMAL) 203 | 204 | # display fancy water gauge 205 | if not self.plant.dead: 206 | water_gauge_str = self.water_gauge() 207 | self.screen.addstr(4,14, water_gauge_str, curses.A_NORMAL) 208 | else: 209 | self.screen.addstr(4,13, clear_bar, curses.A_NORMAL) 210 | self.screen.addstr(4,14, "( RIP )", curses.A_NORMAL) 211 | 212 | # draw cute ascii from files 213 | if self.visited_plant: 214 | # Needed to prevent drawing over a visited plant 215 | self.draw_plant_ascii(self.visited_plant) 216 | else: 217 | self.draw_plant_ascii(self.plant) 218 | 219 | def water_gauge(self): 220 | # build nice looking water gauge 221 | water_left_pct = 1 - ((time.time() - self.plant.watered_timestamp)/86400) 222 | # don't allow negative value 223 | water_left_pct = max(0, water_left_pct) 224 | water_left = int(math.ceil(water_left_pct * 10)) 225 | water_string = "(" + (")" * water_left) + ("." * (10 - water_left)) + ") " + str(int(water_left_pct * 100)) + "% " 226 | return water_string 227 | 228 | def update_plant_live(self): 229 | # updates plant data on menu screen, live! 230 | while not self.exit: 231 | self.plant_string = self.plant.parse_plant() 232 | self.plant_ticks = str(int(self.plant.ticks)) 233 | if self.initialized: 234 | self.update_options() 235 | self.draw() 236 | time.sleep(1) 237 | 238 | def get_user_input(self): 239 | # gets the user's input 240 | user_in = self.screen.getch() # Gets user input 241 | if user_in == -1: # Input comes from pipe/file and is closed 242 | raise OSError("Failed to read from input") 243 | ## DEBUG KEYS - enable these lines to see curses key codes 244 | # self.screen.addstr(2, 2, str(user_in), curses.A_NORMAL) 245 | # self.screen.refresh() 246 | 247 | # Resize sends curses.KEY_RESIZE, update display 248 | if user_in == curses.KEY_RESIZE: 249 | self.maxy,self.maxx = self.screen.getmaxyx() 250 | self.screen.clear() 251 | self.screen.refresh() 252 | 253 | # enter, exit, and Q Keys are special cases 254 | if user_in == 10: 255 | return self.options[self.selected] 256 | if user_in == 27: 257 | return self.options[-1] 258 | if user_in == 113: 259 | self.selected = len(self.options) - 1 260 | return 261 | 262 | # this is a number; check to see if we can set it 263 | if user_in >= ord('1') and user_in <= ord(str(min(7,len(self.options)))): 264 | self.selected = user_in - ord('0') - 1 # convert keypress back to a number, then subtract 1 to get index 265 | return 266 | 267 | # increment or Decrement 268 | down_keys = [curses.KEY_DOWN, 14, ord('j')] 269 | up_keys = [curses.KEY_UP, 16, ord('k')] 270 | 271 | if user_in in down_keys: # down arrow 272 | self.selected += 1 273 | if user_in in up_keys: # up arrow 274 | self.selected -=1 275 | 276 | # modulo to wrap menu cursor 277 | self.selected = self.selected % len(self.options) 278 | return 279 | 280 | def format_garden_data(self,this_garden): 281 | # Returns list of lists (pages) of garden entries 282 | plant_table = [] 283 | for plant_id in this_garden: 284 | if this_garden[plant_id]: 285 | if not this_garden[plant_id]["dead"]: 286 | this_plant = this_garden[plant_id] 287 | plant_table.append((this_plant["owner"], 288 | this_plant["age"], 289 | int(this_plant["score"]), 290 | this_plant["description"])) 291 | return plant_table 292 | 293 | def format_garden_entry(self, entry): 294 | return "{:14.14} - {:>16} - {:>8}p - {}".format(*entry) 295 | 296 | def sort_garden_table(self, table, column, ascending): 297 | """ Sort table in place by a specified column """ 298 | def key(entry): 299 | entry = entry[column] 300 | # In when sorting ages, convert to seconds 301 | if column == 1: 302 | coeffs = [24*60*60, 60*60, 60, 1] 303 | nums = [int(n[:-1]) for n in entry.split(":")] 304 | if len(nums) == len(coeffs): 305 | entry = sum(nums[i] * coeffs[i] for i in range(len(nums))) 306 | return entry 307 | 308 | return table.sort(key=key, reverse=not ascending) 309 | 310 | def filter_garden_table(self, table, pattern): 311 | """ Filter table using a pattern, and return the new table """ 312 | def filterfunc(entry): 313 | if len(pattern) == 0: 314 | return True 315 | entry_txt = self.format_garden_entry(entry) 316 | try: 317 | result = bool(re.search(pattern, entry_txt)) 318 | except re.PatternError: 319 | # In case of invalid regex, don't match anything 320 | result = False 321 | return result 322 | return list(filter(filterfunc, table)) 323 | 324 | def draw_garden(self): 325 | # draws community garden 326 | # load data from sqlite db 327 | this_garden = self.user_data.retrieve_garden_from_db() 328 | # format data 329 | self.clear_info_pane() 330 | 331 | if self.infotoggle == 2: 332 | # the screen IS currently showing the garden (1 page), make the 333 | # text a bunch of blanks to clear it out 334 | self.infotoggle = 0 335 | return 336 | 337 | # if infotoggle isn't 2, the screen currently displays other stuff 338 | plant_table_orig = self.format_garden_data(this_garden) 339 | self.infotoggle = 2 340 | 341 | # print garden information OR clear it 342 | index = 0 343 | sort_column, sort_ascending = 0, True 344 | sort_keys = ["n", "a", "s", "d"] # Name, Age, Score, Description 345 | plant_table = plant_table_orig 346 | self.sort_garden_table(plant_table, sort_column, sort_ascending) 347 | while True: 348 | entries_per_page = self.maxy - 16 349 | index_max = min(len(plant_table), index + entries_per_page) 350 | plants = plant_table[index:index_max] 351 | page = [self.format_garden_entry(entry) for entry in plants] 352 | with self.screen_lock: 353 | self.draw_info_text(page) 354 | # Multiple pages, paginate and require keypress 355 | page_text = "(%d-%d/%d) | sp/next | bksp/prev | s /sort | f/filter | q/quit" % (index, index_max, len(plant_table)) 356 | self.screen.addstr(self.maxy-2, 2, page_text) 357 | self.screen.refresh() 358 | c = self.screen.getch() 359 | if c == -1: # Input comes from pipe/file and is closed 360 | raise OSError("Failed to read from input") 361 | self.infotoggle = 0 362 | 363 | # Quit 364 | if c == ord("q") or c == ord("x") or c == 27: 365 | break 366 | # Next page 367 | elif c in [curses.KEY_ENTER, curses.KEY_NPAGE, ord(" "), ord("\n")]: 368 | index += entries_per_page 369 | if index >= len(plant_table): 370 | break 371 | # Previous page 372 | elif c == curses.KEY_BACKSPACE or c == curses.KEY_PPAGE: 373 | index = max(index - entries_per_page, 0) 374 | # Next line 375 | elif c == ord("j") or c == curses.KEY_DOWN: 376 | index = max(min(index + 1, len(plant_table) - 1), 0) 377 | # Previous line 378 | elif c == ord("k") or c == curses.KEY_UP: 379 | index = max(index - 1, 0) 380 | # Sort entries 381 | elif c == ord("s"): 382 | c = self.screen.getch() 383 | if c == -1: # Input comes from pipe/file and is closed 384 | raise OSError("Failed to read from input") 385 | column = -1 386 | if c < 255 and chr(c) in sort_keys: 387 | column = sort_keys.index(chr(c)) 388 | elif ord("1") <= c <= ord("4"): 389 | column = c - ord("1") 390 | if column != -1: 391 | if sort_column == column: 392 | sort_ascending = not sort_ascending 393 | else: 394 | sort_column = column 395 | sort_ascending = True 396 | self.sort_garden_table(plant_table, sort_column, sort_ascending) 397 | # Filter entries 398 | elif c == ord("/") or c == ord("f"): 399 | self.screen.addstr(self.maxy-2, 2, "Filter: " + " " * (len(page_text)-8)) 400 | pattern = self.get_user_string(10, self.maxy-2, lambda x: x in string.printable) 401 | plant_table = self.filter_garden_table(plant_table_orig, pattern) 402 | self.sort_garden_table(plant_table, sort_column, sort_ascending) 403 | index = 0 404 | 405 | # Clear page before drawing next 406 | self.clear_info_pane() 407 | self.clear_info_pane() 408 | 409 | def get_plant_description(self, this_plant): 410 | output_text = "" 411 | this_species = this_plant.species_list[this_plant.species] 412 | this_color = this_plant.color_list[this_plant.color] 413 | this_stage = this_plant.stage 414 | 415 | stage_descriptions = { 416 | 0:[ 417 | "You're excited about your new seed.", 418 | "You wonder what kind of plant your seed will grow into.", 419 | "You're ready for a new start with this plant.", 420 | "You're tired of waiting for your seed to grow.", 421 | "You wish your seed could tell you what it needs.", 422 | "You can feel the spirit inside your seed.", 423 | "These pretzels are making you thirsty.", 424 | "Way to plant, Ann!", 425 | "'To see things in the seed, that is genius' - Lao Tzu", 426 | ], 427 | 1:[ 428 | "The seedling fills you with hope.", 429 | "The seedling shakes in the wind.", 430 | "You can make out a tiny leaf - or is that a thorn?", 431 | "You can feel the seedling looking back at you.", 432 | "You blow a kiss to your seedling.", 433 | "You think about all the seedlings who came before it.", 434 | "You and your seedling make a great team.", 435 | "Your seedling grows slowly and quietly.", 436 | "You meditate on the paths your plant's life could take.", 437 | ], 438 | 2:[ 439 | "The " + this_species + " makes you feel relaxed.", 440 | "You sing a song to your " + this_species + ".", 441 | "You quietly sit with your " + this_species + " for a few minutes.", 442 | "Your " + this_species + " looks pretty good.", 443 | "You play loud techno to your " + this_species + ".", 444 | "You play piano to your " + this_species + ".", 445 | "You play rap music to your " + this_species + ".", 446 | "You whistle a tune to your " + this_species + ".", 447 | "You read a poem to your " + this_species + ".", 448 | "You tell a secret to your " + this_species + ".", 449 | "You play your favorite record for your " + this_species + ".", 450 | ], 451 | 3:[ 452 | "Your " + this_species + " is growing nicely!", 453 | "You're proud of the dedication it took to grow your " + this_species + ".", 454 | "You take a deep breath with your " + this_species + ".", 455 | "You think of all the words that rhyme with " + this_species + ".", 456 | "The " + this_species + " looks full of life.", 457 | "The " + this_species + " inspires you.", 458 | "Your " + this_species + " makes you forget about your problems.", 459 | "Your " + this_species + " gives you a reason to keep going.", 460 | "Looking at your " + this_species + " helps you focus on what matters.", 461 | "You think about how nice this " + this_species + " looks here.", 462 | "The buds of your " + this_species + " might bloom soon.", 463 | ], 464 | 4:[ 465 | "The " + this_color + " flowers look nice on your " + this_species +"!", 466 | "The " + this_color + " flowers have bloomed and fill you with positivity.", 467 | "The " + this_color + " flowers remind you of your childhood.", 468 | "The " + this_color + " flowers remind you of spring mornings.", 469 | "The " + this_color + " flowers remind you of a forgotten memory.", 470 | "The " + this_color + " flowers remind you of your happy place.", 471 | "The aroma of the " + this_color + " flowers energize you.", 472 | "The " + this_species + " has grown beautiful " + this_color + " flowers.", 473 | "The " + this_color + " petals remind you of that favorite shirt you lost.", 474 | "The " + this_color + " flowers remind you of your crush.", 475 | "You smell the " + this_color + " flowers and are filled with peace.", 476 | ], 477 | 5:[ 478 | "You fondly remember the time you spent caring for your " + this_species + ".", 479 | "Seed pods have grown on your " + this_species + ".", 480 | "You feel like your " + this_species + " appreciates your care.", 481 | "The " + this_species + " fills you with love.", 482 | "You're ready for whatever comes after your " + this_species + ".", 483 | "You're excited to start growing your next plant.", 484 | "You reflect on when your " + this_species + " was just a seedling.", 485 | "You grow nostalgic about the early days with your " + this_species + ".", 486 | ], 487 | 99:[ 488 | "You wish you had taken better care of your plant.", 489 | "If only you had watered your plant more often..", 490 | "Your plant is dead, there's always next time.", 491 | "You cry over the withered leaves of your plant.", 492 | "Your plant died. Maybe you need a fresh start.", 493 | ], 494 | } 495 | # self.life_stages is tuple containing length of each stage 496 | # (seed, seedling, young, mature, flowering) 497 | if this_plant.dead: 498 | this_stage = 99 499 | 500 | this_stage_descriptions = stage_descriptions[this_stage] 501 | description_num = random.randint(0,len(this_stage_descriptions) - 1) 502 | # If not fully grown 503 | if this_stage <= 4: 504 | # Growth hint 505 | if this_stage >= 1: 506 | last_growth_at = this_plant.life_stages[this_stage - 1] 507 | else: 508 | last_growth_at = 0 509 | ticks_since_last = this_plant.ticks - last_growth_at 510 | ticks_between_stage = this_plant.life_stages[this_stage] - last_growth_at 511 | if ticks_since_last >= ticks_between_stage * 0.8: 512 | output_text += "You notice your plant looks different.\n" 513 | 514 | output_text += this_stage_descriptions[description_num] + "\n" 515 | 516 | # if seedling 517 | if this_stage == 1: 518 | species_options = [this_plant.species_list[this_plant.species], 519 | this_plant.species_list[(this_plant.species+3) % len(this_plant.species_list)], 520 | this_plant.species_list[(this_plant.species-3) % len(this_plant.species_list)]] 521 | random.shuffle(species_options) 522 | plant_hint = "It could be a(n) " + species_options[0] + ", " + species_options[1] + ", or " + species_options[2] 523 | output_text += plant_hint + ".\n" 524 | 525 | # if young plant 526 | if this_stage == 2: 527 | if this_plant.rarity >= 2: 528 | rarity_hint = "You feel like your plant is special." 529 | output_text += rarity_hint + ".\n" 530 | 531 | # if mature plant 532 | if this_stage == 3: 533 | color_options = [this_plant.color_list[this_plant.color], 534 | this_plant.color_list[(this_plant.color+3) % len(this_plant.color_list)], 535 | this_plant.color_list[(this_plant.color-3) % len(this_plant.color_list)]] 536 | random.shuffle(color_options) 537 | plant_hint = "You can see the first hints of " + color_options[0] + ", " + color_options[1] + ", or " + color_options[2] 538 | output_text += plant_hint + ".\n" 539 | 540 | return output_text 541 | 542 | def draw_plant_description(self, this_plant): 543 | # If menu is currently showing something other than the description 544 | self.clear_info_pane() 545 | if self.infotoggle != 1: 546 | # get plant description before printing 547 | output_string = self.get_plant_description(this_plant) 548 | growth_multiplier = 1 + (0.2 * (this_plant.generation-1)) 549 | output_string += "Generation: {}\nGrowth rate: {:.1f}x".format(self.plant.generation, growth_multiplier) 550 | self.draw_info_text(output_string) 551 | self.infotoggle = 1 552 | else: 553 | # otherwise just set toggle 554 | self.infotoggle = 0 555 | 556 | def draw_instructions(self): 557 | # Draw instructions on screen 558 | self.clear_info_pane() 559 | if self.infotoggle != 4: 560 | instructions_txt = ("welcome to botany. you've been given a seed\n" 561 | "that will grow into a beautiful plant. check\n" 562 | "in and water your plant every 24h to keep it\n" 563 | "growing. 5 days without water = death. your\n" 564 | "plant depends on you & your friends to live!\n" 565 | "more info is available in the readme :)\n" 566 | "https://github.com/jifunks/botany/blob/master/README.md\n" 567 | " cheers,\n" 568 | " curio\n" 569 | ) 570 | self.draw_info_text(instructions_txt) 571 | self.infotoggle = 4 572 | else: 573 | self.infotoggle = 0 574 | 575 | def clear_info_pane(self): 576 | # Clears bottom part of screen 577 | with self.screen_lock: 578 | clear_bar = " " * (self.maxx - 3) 579 | this_y = 14 580 | while this_y < self.maxy: 581 | self.screen.addstr(this_y, 2, clear_bar, curses.A_NORMAL) 582 | this_y += 1 583 | self.screen.refresh() 584 | 585 | def draw_info_text(self, info_text, y_offset = 0): 586 | # print lines of text to info pane at bottom of screen 587 | with self.screen_lock: 588 | if type(info_text) is str: 589 | info_text = info_text.splitlines() 590 | for y, line in enumerate(info_text, 2): 591 | this_y = y+12 + y_offset 592 | if len(line) > self.maxx - 3: 593 | line = line[:self.maxx-3] 594 | if this_y < self.maxy: 595 | self.screen.addstr(this_y, 2, line, curses.A_NORMAL) 596 | self.screen.refresh() 597 | 598 | def harvest_confirmation(self): 599 | self.clear_info_pane() 600 | # get plant description before printing 601 | max_stage = len(self.plant.stage_list) - 1 602 | harvest_text = "" 603 | if not self.plant.dead: 604 | if self.plant.stage == max_stage: 605 | harvest_text += "Congratulations! You raised your plant to its final stage of growth.\n" 606 | harvest_text += "Your next plant will grow at a speed of: {:.1f}x\n".format(1 + (0.2 * self.plant.generation)) 607 | harvest_text += "If you harvest your plant you'll start over from a seed.\nContinue? (Y/n)" 608 | self.draw_info_text(harvest_text) 609 | user_in = self.screen.getch() # Gets user input 610 | if user_in == -1: # Input comes from pipe/file and is closed 611 | raise OSError("Failed to read from input") 612 | 613 | if user_in in [ord('Y'), ord('y'), 10]: 614 | self.plant.start_over() 615 | else: 616 | pass 617 | self.clear_info_pane() 618 | 619 | def build_weekly_visitor_output(self, visitors): 620 | visitor_block = "" 621 | visitor_line = "" 622 | for visitor in visitors: 623 | this_visitor_string = str(visitor) + "({}) ".format(visitors[str(visitor)]) 624 | if len(visitor_line + this_visitor_string) > self.maxx-3: 625 | visitor_block += '\n' 626 | visitor_line = "" 627 | visitor_block += this_visitor_string 628 | visitor_line += this_visitor_string 629 | return visitor_block 630 | 631 | def build_latest_visitor_output(self, visitors): 632 | visitor_line = "" 633 | for visitor in visitors: 634 | if len(visitor_line + visitor) > self.maxx-10: 635 | visitor_line += "and more" 636 | break 637 | visitor_line += visitor + ' ' 638 | return [visitor_line] 639 | 640 | def get_weekly_visitors(self): 641 | game_dir = os.path.dirname(os.path.realpath(__file__)) 642 | garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite') 643 | conn = sqlite3.connect(garden_db_path) 644 | c = conn.cursor() 645 | c.execute("SELECT * FROM visitors WHERE garden_name = '{}' ORDER BY weekly_visits".format(self.plant.owner)) 646 | visitor_data = c.fetchall() 647 | conn.close() 648 | visitor_block = "" 649 | visitor_line = "" 650 | if visitor_data: 651 | for visitor in visitor_data: 652 | visitor_name = visitor[2] 653 | weekly_visits = visitor[3] 654 | this_visitor_string = "{}({}) ".format(visitor_name, weekly_visits) 655 | if len(visitor_line + this_visitor_string) > self.maxx-3: 656 | visitor_block += '\n' 657 | visitor_line = "" 658 | visitor_block += this_visitor_string 659 | visitor_line += this_visitor_string 660 | else: 661 | visitor_block = 'nobody :(' 662 | return visitor_block 663 | 664 | def get_user_string(self, xpos=3, ypos=15, filterfunc=str.isalnum, completer=None): 665 | # filter allowed characters using filterfunc, alphanumeric by default 666 | user_string = "" 667 | user_input = 0 668 | if completer: 669 | completer = completer(self) 670 | while user_input != 10: 671 | user_input = self.screen.getch() 672 | if user_input == -1: # Input comes from pipe/file and is closed 673 | raise OSError("Failed to read from input") 674 | with self.screen_lock: 675 | # osx and unix backspace chars... 676 | if user_input == 127 or user_input == 263: 677 | if len(user_string) > 0: 678 | user_string = user_string[:-1] 679 | if completer: 680 | completer.update_input(user_string) 681 | self.screen.addstr(ypos, xpos, " " * (self.maxx-xpos-1)) 682 | elif user_input in [ord('\t'), curses.KEY_BTAB] and completer: 683 | direction = 1 if user_input == ord('\t') else -1 684 | user_string = completer.complete(direction) 685 | self.screen.addstr(ypos, xpos, " " * (self.maxx-xpos-1)) 686 | elif user_input < 256 and user_input != 10: 687 | if filterfunc(chr(user_input)) or chr(user_input) == '_': 688 | user_string += chr(user_input) 689 | if completer: 690 | completer.update_input(user_string) 691 | self.screen.addstr(ypos, xpos, str(user_string)) 692 | self.screen.refresh() 693 | return user_string 694 | 695 | def visit_handler(self): 696 | self.clear_info_pane() 697 | self.draw_info_text("whose plant would you like to visit?") 698 | self.screen.addstr(15, 2, '~') 699 | if self.plant.visitors: 700 | latest_visitor_string = self.build_latest_visitor_output(self.plant.visitors) 701 | self.draw_info_text("since last time, you were visited by: ", 3) 702 | self.draw_info_text(latest_visitor_string, 4) 703 | self.plant.visitors = [] 704 | weekly_visitor_text = self.get_weekly_visitors() 705 | self.draw_info_text("this week you've been visited by: ", 6) 706 | self.draw_info_text(weekly_visitor_text, 7) 707 | guest_garden = self.get_user_string(completer = completer.LoginCompleter) 708 | if not guest_garden: 709 | self.clear_info_pane() 710 | return None 711 | if guest_garden.lower() == getpass.getuser().lower(): 712 | self.screen.addstr(16, 2, "you're already here!") 713 | self.screen.getch() 714 | self.clear_info_pane() 715 | return None 716 | home_folder = os.path.dirname(os.path.expanduser("~")) 717 | guest_json = home_folder + "/{}/.botany/{}_plant_data.json".format(guest_garden, guest_garden) 718 | guest_plant_description = "" 719 | if os.path.isfile(guest_json): 720 | with open(guest_json) as f: 721 | visitor_data = json.load(f) 722 | guest_plant_description = visitor_data['description'] 723 | self.visited_plant = self.get_visited_plant(visitor_data) 724 | guest_visitor_file = home_folder + "/{}/.botany/visitors.json".format(guest_garden, guest_garden) 725 | if os.path.isfile(guest_visitor_file): 726 | water_success = self.water_on_visit(guest_visitor_file) 727 | if water_success: 728 | self.screen.addstr(16, 2, "...you watered ~{}'s {}...".format(str(guest_garden), guest_plant_description)) 729 | if self.visited_plant: 730 | self.draw_plant_ascii(self.visited_plant) 731 | else: 732 | self.screen.addstr(16, 2, "{}'s garden is locked, but you can see in...".format(guest_garden)) 733 | else: 734 | self.screen.addstr(16, 2, "i can't seem to find directions to {}...".format(guest_garden)) 735 | try: 736 | self.screen.getch() 737 | self.clear_info_pane() 738 | self.draw_plant_ascii(self.plant) 739 | finally: 740 | self.visited_plant = None 741 | 742 | def water_on_visit(self, guest_visitor_file): 743 | visitor_data = {} 744 | # using -1 here so that old running instances can be watered 745 | guest_data = {'user': getpass.getuser(), 'timestamp': int(time.time()) - 1} 746 | if os.path.isfile(guest_visitor_file): 747 | if not os.access(guest_visitor_file, os.W_OK): 748 | return False 749 | with open(guest_visitor_file) as f: 750 | visitor_data = json.load(f) 751 | visitor_data.append(guest_data) 752 | with open(guest_visitor_file, mode='w') as f: 753 | f.write(json.dumps(visitor_data, indent=2)) 754 | return True 755 | 756 | def get_visited_plant(self, visitor_data): 757 | """ Returns a drawable pseudo plant object from json data """ 758 | class VisitedPlant: pass 759 | plant = VisitedPlant() 760 | plant.stage = 0 761 | plant.species = 0 762 | 763 | if "is_dead" not in visitor_data: 764 | return None 765 | plant.dead = visitor_data["is_dead"] 766 | if plant.dead: 767 | return plant 768 | 769 | if "stage" in visitor_data: 770 | stage = visitor_data["stage"] 771 | if stage in self.plant.stage_list: 772 | plant.stage = self.plant.stage_list.index(stage) 773 | 774 | if "species" in visitor_data: 775 | species = visitor_data["species"] 776 | if species in self.plant.species_list: 777 | plant.species = self.plant.species_list.index(species) 778 | else: 779 | return None 780 | elif plant.stage > 1: 781 | return None 782 | return plant 783 | 784 | def handle_request(self, request): 785 | # Menu options call functions here 786 | match request: 787 | case None: 788 | return 789 | case "harvest": 790 | self.harvest_confirmation() 791 | case "water": 792 | self.plant.water() 793 | case "look": 794 | self.draw_plant_description(self.plant) 795 | case "instructions": 796 | self.draw_instructions() 797 | case "visit": 798 | self.visit_handler() 799 | case "garden": 800 | self.draw_garden() 801 | 802 | 803 | def menu(stdscrn: curses.window, *, this_plant, this_data): 804 | menu = CursedMenu(stdscrn, this_plant=this_plant, this_data=this_data) 805 | menu.show( 806 | ["water", "look", "garden", "visit", "instructions"], 807 | title=" botany ", 808 | subtitle="options", 809 | ) 810 | 811 | 812 | def main(this_plant: Plant, this_data: "DataManager"): 813 | return curses.wrapper(menu, this_plant=this_plant, this_data=this_data) 814 | --------------------------------------------------------------------------------