Alright, I got my files uploaded to a fork and I
think
I did the pull request right -- please let me know if I effed something up!
I had three main objectives for the code I've been working on:
1. Make it as robust as (reasonably) possible, so that mods with fairly bizarre or nonstandard voting rules in their setups can still make use of the tool, provided they're willing to put in the effort writing the config!
2. Make a standardized representation of the state of a game, and allow only clearly-defined operations upon it, called Events, which can be generated from JSON, so that other tools like Psyche's thread crawler can be utilized to create JSON events to be fed directly into the state object.
3. Make a system for turning the game state into posts like vote counts, player lists, role PMs, etc. in a way that is extremely customizable -- as Psyche said, mods have all sorts of ways they like to make their vote counts, so being able to accommodate all of them (including myself!) would probably be appreciated by a lot of users, and all the better if the method of customizing output does not require any Python or code knowledge whatsoever.
So, what exactly do I have so far? Basically, this:
Firstly, the classes and methods needed to create a "GameState" object which holds (ideally) the entire state of a particular game. Nothing is ever deleted out of the GameState: if a vote is no longer active (due to a player unvoting or voting for someone else), it gets its "end" timestamp set to reflect that. Consequently, it's possible to go to the GameState and ask for a vote count accurate to any post in the game, and it should be able to go back and pull a vote count for you. The code I have written in my Main.py currently does essentially that. The 'pie-in-the-sky' use for this would be, if for some reason this code were someday integrated into the site directly, that you could go to any post in a game, click a button, and a vote count as of that post would be generated for you. With a system like that, it might not even be necessary to have the mod post a vote count on every page.
Of course, this is all just me setting the bar high so as to encourage myself to make the system as robust as possible. Anyway, what all is included in this module?
The classes under the GameState umbrella would be:
GameState
: The container for all of the game's state data. Pretty straightforward.
Player
: The container for a particular player's data. This represents a 'slot' in the game, not a particular user who has been cast into that slot. A Player contains a list of all its users (needed to represent replacements), and also its role and alignment.
User
: A representation of a Mafiascum user. The "display_name" should be the user's MS username, and "aliases" can be used to store nicknames or hydra head names.
Phase
: A container for data corresponding to a phase of the game, e.g., Day, Night, etc. Each game starts in a phase called "Pregame" to represent the fact that the first day/night is not in effect the instant a game thread is made. The Phase abstraction exists so that, for instance, if a mod wanted to create a Nightless setup, or a variant setup with two lynching phases (Morning and Afternoon) for each night, they could do that without a lot of hassle.
PhaseType
: A representation of an idealized "type" of phase. For example, Day 2 is a particular phase that is an instance of the idealized "Day" phase. The "new" method is used to make a Phase from a PhaseType.
Election
: An election is any instance of an outcome being voted on in a game, most commonly a lynch vote (but it could be, for instance, the Social Policy vote from Civilization Mafia or w/e a mod wants). Each election corresponds to a phase and contains Electors and Votes.
ElectionType
: A representation of an idealized "type" of election. For example, the Day 3 lynch vote is an Election, and the type "Lynch" is an ElectionType.
Elector
: Any entity capable of voting or receiving votes in an election. Usually corresponds to a player, but I've created a separate object for it because 1) sometimes a label needs to be capable of receiving votes, e.g., a social policy in Civ Mafia or a bundle of players in a Partition game; and 2) a player might receive a modifier that only applies for a particular election and not in general, e.g., having doublevote for the lynch vote, but not for the vote for a town leader.
Vote
: A vote cast by an Elector for another Elector. Can have a "power" assigned to indicate the strength of the vote; for instance, a doublevoter casts votes with a power of 2.
There's also a couple of abstract classes that are inherited all over the place:
Temporal
: Anything that inherits Temporal has a start and end post, and helper methods like active() and past() to make it easier for other code to do time comparisons.
Modifiable
: Contains the Modifier class; any object that inherits Modifiable gets a dict called "modifiers" and some helper methods to add and fetch keys from it. The idea of this is to encapsulate special behavior into little tags that can be passed in via a configuration JSON string. Modifiers are Temporal and anytime a modifier changes, the old value of the modifier sticks around in the modifiers dict. The only use of this right now that I'm a little torn on is to track a player being alive -- a player with the "alive" == True modifier is considered alive, otherwise they're not. I did this just in case some weirdo mod wanted to make it possible to revive players in a game.
Then, to manage the GameState, you've got a couple of other classes:
Game
: This is just intended to be an interface into a GameState so it's not a massive pain to tell it to do things. The externally-accessible methods to Game should all be pretty self-explanatory, user-facing stuff. Still a major work in progress.
Event
: This is the part where I should make explicit that this code is
not
intended to go scrape pages for votes, as I think Psyche's already doing an amazing job on that front. Instead, I use JSON strings called Events to manipulate the GameState. Ideally, these should be the only interfaces into a GameState capable of modifying its state -- everything else should look and not touch. Events are things that happen that change the GameState, such as a "death" event for a player dying, a "vote" or "unvote" event for votes, a "vote_count" event to record that a vote count has been posted to the thread (handy if mods want their vote counts to link back to old vote counts), a "deadline" event when deadlines are set, a "phase_change" event when the phase changes (e.g., Day to Night), etc. etc. Events are loaded into the Game object (currently just from a file) using the load_events() method and then are processed using the process_events() method. The Game object keeps a big ol' list of events that it runs through in chronological order to calculate the GameState. Currently, I have no way to allow events to be processed out of order, so if an event is received out of order, the GameState has to be reset and calculated from scratch.
Right now, I've got a bunch of admittedly-pretty-bad code lying around that grabs the setup data and events data from JSON files in the repository (in the respective subfolders). These are just big chunks of JSON that contain the setup data and a huge list of all events, respectively. For my tests thus far, I've had to read through game threads myself and manually write event JSON for each thing that happens -- for an example of that, I recently wrote out the setup and event data for
Mini Theme 1974 by hand, and put in some code needed to generate vote counts for it in the Main.py file. You can get a sense of how the JSON is laid out from that. I think it's going to be important to standardize the JSON for setups, events, votes, etc. earlier rather than later so please let me know if you have any suggestions for this! I've actually never worked with JSON before this project so there are undoubtedly some things I'm doing weirdly.
Finally, there are things I'm calling
Components
which are just ways to pull data from a GameState and its members and represent them in a lovely text format -- stuff like vote counts, player lists, role PMs (not there yet though), etc. I think this is easily the least intuitive part of what I've written here, but I like how modular it is so I'll take a stab at documenting it in the next couple of days so it's clear what it's doing. The important thing to note is that when you generate a vote count through the Game object, you can pass a Style in which tells the component how to format stuff. In Main.py I've passed in a style called "Micc" which just replicates the style of vote count Micc was using for Mini Theme 1974, just as a proof-of-concept for how customizing output works. All of the files for each style are in the "styles" subdirectory. Basically, you have two types of file inside each style:
- "body" files: These define the overall structure of each component. The "vote_count.component.body" file (i.e., the body file for the vote_count component) is probably the easiest to visually parse. The Component code just plugs 'n' chugs data into the body using Python's .format() function. And, yes, components can contain other components, so for instance, in order to represent the actual 'vote count' meat of a each vote count, I have a "list" subcomponent of newline-separated "vote_count_vote" components, which is a composite of the votee's "player_label" component, plus the a "list" subcomponent of comma-delimited "player_label" subcomponents for each voter. It's kind of messy but I think it'll be worth it, as mods can make a global change in their Style directory to how, say, they want player names displayed, and it'll affect all "player_label" components used anywhere in their Style.
- "configs" files: These are parameters to be passed into each component when it's created to define some customizable behaviors. For example, in the "_default" style, the "players_list" component gets a parameter passed in so that replacements for each player are displayed, as this is common practice for OPs pretty much everywhere. If for some reason a mod didn't want to display replacements, they could flip it to False in their own Style file.
... phew, anyway, I hope that was informative. I don't know how much interest there will be from others when it comes to using this, but I hope at least anyone who's interested will offer pointers and suggestions. Despite wanting to move professionally in that direction, I am not a developer, so undoubtedly the folks here who do this stuff for a living will be shaking their heads at some of what I'm doing here. Feedback (yes, negative feedback too) is very welcome!
So, first TODO for me: I really like the .md readme thing you have going on for ModBot, Psyche, so I'm going to try to create one for my files to make it easier for people to tell what the heck is going on.
Also, yea or nay on having most classes in their own file? I'm not sure if that's standard in Python world but it's what I've been doing up until now.