wilty/wilty.py

320 lines
12 KiB
Python

import json
import os
import pwd
import random
import time
class Wilty():
def __init__(self):
self.name = "wilty"
self.version = self.name + " 0.2"
self.save_dir = ".botany"
self.plant_file = "_plant_data.json"
self.visit_file = "visitors.json"
self.harvest_file = "harvest_file.json"
self.water_user = self.name
self.users = {}
self.plants = {}
self.live_plants = {}
self.stats_model = {
"total": 0,
"live": 0,
"seed": 0,
"young": 0,
"mature": 0,
"succ": 0,
"tree": 0,
"nonflower": 0,
"flower": 0,
"gen": 0,
"avg_gen": 0,
"harv": 0,
"avg_harv": 0,
"score": 0,
"avg_score": 0
}
self.border = "-" * 47
def showVersion(self):
"""Show version info."""
print(self.version)
def showHelp(self):
"""Show usage info."""
print("Usage: " + self.name + \
" list [user|plant], stats [live], water")
def mapCommands(self, args):
"""Map a list of command keywords to functions."""
if len(args) == 1:
if args[0] == "list":
self.getRawData()
self.listLivePlants()
elif args[0] == "stats":
self.getRawData()
self.listStats()
elif args[0] == "water":
self.getRawData()
self.waterRandomPlant()
elif args[0] == "version":
self.showVersion()
else:
self.showHelp()
elif len(args) == 2:
if args[0] == "list" and args[1] == "user":
self.getRawData()
self.listLivePlants(sort=user)
elif args[0] == "list" and args[1] == "plant":
self.getRawData()
self.listLivePlants(sort=user)
elif args[0] == "stats" and args[1] == "live":
self.getRawData()
self.listStats(subset="live")
else:
self.showHelp()
else:
self.showHelp()
def formatTime(self, ts):
"""Format an integer timestamp (seconds) to a timestamp string (days,
hours and minutes), e.g. 12d 12h 12m.
"""
days = int(ts / 86400)
hours = int((ts % 86400) / 3600)
mins = round(((ts % 86400) % 3600) / 60)
return str(days) + "d " + str(hours) + "h " + str(mins) + "m"
def getUsers(self):
"""Load usernames into the plants dictionary as keys. Only users whose
plant data is peer-readable will be added.
"""
sys_users = pwd.getpwall()
for u in sys_users:
sv_dir = "/home/" + u.pw_name + "/" + self.save_dir + "/"
if os.path.exists(sv_dir) and \
os.access(sv_dir + u.pw_name + self.plant_file, os.R_OK):
self.users[u.pw_name] = {}
def checkPlant(self, user):
"""Given the username, calculates the last time since the user's
plant was watered, estimates whether it is dead and updates the plants
dictionary.
"""
# Get last visitor timestamp and check if later than user's
visitor_ts = 0
if self.plants[user]["visitors"] != []:
visitor_ts = self.plants[user]["visitors"][-1]["timestamp"]
watered = max(self.plants[user]["last_watered"], visitor_ts)
time_since = int(time.time()) - watered
self.plants[user]["time_since"] = time_since
# >5 days = dead plant
if time_since > 432000:
self.plants[user]["is_dead"] = True
self.plants[user]["time_since_fmt"] = self.formatTime(time_since)
def getRawData(self):
"""Load save file data into the plants dictionary and update plant
states.
"""
self.getUsers()
for u in self.users:
sv_dir = "/home/" + u + "/" + self.save_dir + "/"
# Get plant data
with open(sv_dir + u + self.plant_file) as plant_fh:
plant_json = plant_fh.read()
try:
# Load plant data if json is valid syntax
self.plants[u] = json.loads(plant_json)
# Get visitor data
if os.access(sv_dir + self.visit_file, os.R_OK) and \
os.access(sv_dir + self.visit_file, os.W_OK):
with open(sv_dir + self.visit_file) as visit_fh:
visit_json = visit_fh.read()
self.plants[u]["visitors"] = json.loads(visit_json)
self.plants[u]["allow_visit"] = True
else:
self.plants[u]["allow_visit"] = False
self.plants[u]["visitors"] = []
# Get harvest data
if os.access(sv_dir + self.harvest_file, os.R_OK):
with open(sv_dir + self.harvest_file) as harvest_fh:
harvest_json = harvest_fh.read()
self.plants[u]["harvests"] = json.loads(harvest_json)
else:
self.plants[u]["harvests"] = {}
# Update plant state and live plants dictionary
self.checkPlant(u)
if not self.plants[u]["is_dead"]:
self.live_plants[u] = self.plants[u]
except json.decoder.JSONDecodeError:
pass
def waterRandomPlant(self):
"""Randomly select a live plant to water."""
# Select a user's plant that is open to visitors
user = list(self.live_plants)[random.randint(0, \
len(self.live_plants) - 1)]
while not self.live_plants[user]["allow_visit"]:
user = list(self.live_plants)[random.randint(0, \
len(self.live_plants) - 1)]
# Contenate string to ensure fixed order of keys in the json,
# to be visually consistent with the in-game visitor watering quirk.
# (Notably, there is an extra space after the comma of the timestamp
# value, and after the comma of the preceding item's closing brace.)
visit_entry = " {\n \"timestamp\": " + str(int(time.time())) + \
", " + "\n \"user\": " + "\"" + self.water_user + "\"" + \
"\n }"
sv_dir = "/home/" + user + "/" + self.save_dir + "/"
with open(sv_dir + self.visit_file, "r") as visit_rfh:
visit_json = visit_rfh.read()
# 1st visitor
if "[]" in visit_json:
visit_data = visit_json.replace("[]", "[\n" + visit_entry + "\n]")
# nth visitor
else:
visit_data = visit_json.replace("\n]", \
", \n" + visit_entry + "\n]")
with open(sv_dir + self.visit_file, "w") as visit_wfh:
visit_wfh.write(visit_data)
print("You watered " + user + "\'s plant. Thanks!")
def listLivePlants(self, *args, **kwargs):
"""Prints a list of living plants open to visitors. Optionally specify
a sort flag to sort the list alphabetically by username or plant type,
e.g. sort=\"plant\". By default, the list is sorted by the longest
interval since the last watered time descending.
"""
sort_l = []
for p in self.live_plants:
if self.live_plants[p]["allow_visit"]:
sort_l.append((p, \
self.live_plants[p]["description"].split()[-1], \
self.live_plants[p]["time_since_fmt"], \
self.live_plants[p]["time_since"]))
# Sort by column
if kwargs.get("sort", "") == "user":
sort_l.sort(key=lambda c: c[0])
elif kwargs.get("sort", "") == "plant":
sort_l.sort(key=lambda c: c[1])
else:
sort_l.sort(key=lambda c: c[3], reverse=True)
# Format output
print(self.border)
print(time.strftime("Date: (%a) %Y-%m-%d %H:%M:%S"))
print(self.border)
print("{:<20s}{:<15s}{:<15s}".format("User", "Plant", "Last Watered"))
print(self.border)
for i in sort_l:
print("{:<20s}{:<15s}{:<15s}".format(i[0], i[1], i[2]))
print(self.border)
def countPlantStages(self, key, count):
"""Count the plants in different stages (seedling, young, mature). Take
as arguments a key for plants dictionary lookup and a dictionary to
append the results.
"""
if "stage" in self.plants[key]:
if self.plants[key]["stage"] == ("mature" or "flowering" or \
"seed-bearing"):
count["mature"] += 1
elif self.plants[key]["stage"] == "young":
count["young"] += 1
else:
count["seed"] += 1
else:
count["seed"] += 1
def countPlantGroups(self, key, count):
"""Count plants in type groups."""
succulents = ["agave", "aloe", "cactus", "lithops", "pachypodium"]
trees = ["baobab", "ficus", "palm"]
noflowers = ["fern", "moss"]
plant_type = self.plants[key]["description"].split()[-1]
if plant_type in succulents:
count["succ"] += 1
elif plant_type in trees:
count["tree"] += 1
elif plant_type in noflowers:
count["nonflower"] += 1
elif plant_type != ("seed" or "seedling"):
count["flower"] += 1
def countGen(self, key, count):
"""Count total generations."""
if "generation" in self.plants[key]:
count["gen"] += self.plants[key]["generation"]
else:
count["gen"] += 1
def avgGen(self, count):
"""Calculate the average plant generation."""
count["avg_gen"] = round(count["gen"] / count["total"], 3)
def countHarvests(self, key, count):
"""Count the total number of harvests."""
if self.plants[key]["harvests"] != {}:
count["harv"] += len(self.plants[key]["harvests"])
else:
count["harv"] += 0
def avgHarvests(self, count):
"""Calculate the average number of harvests per user."""
count["avg_harv"] = round(count["harv"] / count["total"], 3)
def countScores(self, key, count):
"""Count total scores."""
count["score"] += self.plants[key]["score"]
def avgScore(self, count):
"""Calculate the average user score."""
count["avg_score"] = int(count["score"] / count["total"])
def genStats(self, subset):
"""Generate garden stats of all or only live plants."""
stats = dict(self.stats_model)
if subset == "live":
plants_set = self.live_plants
stats["total"] = len(self.live_plants)
else:
plants_set = self.plants
stats["total"] = len(self.plants)
for p in plants_set:
self.countPlantStages(p, stats)
self.countPlantGroups(p, stats)
self.countGen(p, stats)
self.countHarvests(p, stats)
self.countScores(p, stats)
self.avgGen(stats)
self.avgHarvests(stats)
self.avgScore(stats)
return stats
def listStats(self, *args, **kwargs):
"""Print plant stats. Optionally use a subset flag to get stats on live
plants only, e.g. subset=\"live\". By default, show stats based on the
total number of plants.
"""
if kwargs.get("subset", "all") == "live":
stats = self.genStats("live")
else:
stats = self.genStats("all")
print(self.border)
print("Garden Stats (" + kwargs.get("subset", "all") + " plants)")
print(self.border)
print(
"Avg. generation: " + str(stats["avg_gen"]) +
"\nAvg. harvests per user: " + str(stats["avg_harv"]) +
"\nAvg. score: " + str(stats["avg_score"]) +
"\nSeedlings: " + str(stats["seed"]) +
"\nYoung plants: " + str(stats["young"]) +
"\nMature plants: " + str(stats["mature"]) +
"\nFlowering shrubs: " + str(stats["flower"]) +
"\nNon-flowering plants: " + str(stats["nonflower"]) +
"\nSucculents: " + str(stats["succ"]) +
"\nTrees: " + str(stats["tree"]) +
"\nTotal plants: " + str(stats["total"])
)
print(self.border)