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)