vck3000 / ProAvalon

Online platform for The Resistance!
https://www.ProAvalon.com
MIT License
88 stars 72 forks source link

Game redesign #455

Closed vck3000 closed 6 months ago

vck3000 commented 2 years ago

Currently, the server has this one massive "Game" class that acts as a generic engine for resistance games. It takes in roles, phases and cards that are defined in their own files and acts like a plug-n-play system. This system works alright, but there are a few major issues with it due to my initial inexperience:

Data is scattered and not collected into a neat little variable. Ideally, it's all in a single object called data and we can simply do JSON.stringify(data) to convert in and out of the memory representation and a string. This is an important step in making the server scalable because currently, the server can only have one instance running due to the games being stored in the application memory, and not on some external service like Redis.

The task here is to redesign and reimplement the Game class to be:

Another interesting detail about the roles is the discussion between "Inheritance vs Composition". The roles currently are very much an inheritance-based model, resulting in a lot of inefficiencies, duplicated code, and headaches overall. A solution I have analysed and explored is an ECS system, which is commonly used in a lot of games.

Essentially it involves a base entity that is "composed" of a bunch of components. Such components could be "CanVoteTeam", "CanVoteMission", "SeesSpies", "CanShoot", "CanBeShot", "SeesMerlinMorgana", etc. The idea is that it makes creating new roles, such as a combined MorganaAssassin role much easier, by simply adding an extra component to the entity, rather than duplicating a lot of code as the current codebase has.

Refactoring the Game class will also set us up to make game replays a thing on the server. If we can record each "event" and play it back in a simulated environment that is isolated from the external concerns (mainly sockets), then it can make life a lot nicer.

I fear redesigning the Game will also change the API/data sent to the frontend. In that case, it may also be a great chance to update the Game frontend to a React implementation.

neyney10 commented 2 years ago

Hi, It looks like this is a to-do task for yourself, so it is probably a bit awkward for me to comment on it. A few years ago I started to make an Avalon web server of my own, and after a few months I my thoughts were pretty similar to what is written here. So I thought that maybe I could share a bit.

interface Connection { ... }

class IOConnection implements Connection {
    private socketIO: ...;
}

Now, it changed everything. for example, I intended to support multiple platforms such as Windows and Android. While the Socket.IO library is available in all, sometimes it as easier to simply use a regular TCP socket, so all I needed to do to support it is simply:

class TCPConnection implements Connection {
    private tcpSocket: ...;
}

Now, I could even create bots with "fake" or a virtual connection, Where there isnt any real connection at all:

class VirtualConnection implements Connection {
    ...
}

With those bots I can then:

  1. Create AI or Simple Bots in games.
  2. Automate games - such as replays, as the bots mimic the player's actions.
  3. Automate tests - I didnt need any real connections, frontend, or anything like that to test the game engine/server. I simply created bots to play with each other a game scenario. I could even choose that one of the players would be me (with others being bots) for debugging purposes in cases I needed a closer inspection.
  4. More separation of concerns and better abstraction of game events and handling.

Although, another work (avalonfun) had another idea, maybe simpler. He created a Game Engine, which doesnt interact with players at all, but all it does is handle the game flow and emit game events, such that an outer module can listen to and then emit them to the players via Socket.IO or w/e.

All in all, I really like your persistence, your repository achieved over 60 stars! and it seems to work well. I, Unfortunately, didnt publish my code or design because I didn't have time to finalize/complete the game with a proper front-end design. However, friends of mine really love the game, and even played on your website a couple of times, and decided to create their own Avalon game with tons of extensions and a unique design (for the better or worse). I came across your repository while looking for ideas on how to help them. Sadly, they won't contribute to your project because they want to make a pet project for themselves. But nevertheless, keep up with the good work, and I hope that I don't sound condescending in my long post, I just wanted to share some of my thoughts back then ;).

vck3000 commented 2 years ago

Thank you @neyney10 for your kind comment. It was very thoughtful and well written.

This is indeed a tough problem. Something I've learnt over the years is that any complex piece of logic, no matter how much you try to keep it tidy, abstracted, interfaced, separated, etc., will always still result in complex code. Of course, this doesn't mean to just throw things together (like I did on my first write-up; it was my first proper large scale programming project), which is why we are fortunate to have these conversations.

I'm intrigued by your notion of a "HaveSecrets". Correct me if I'm wrong, but it sounds like a secret is a generic container that stores special features about a particular role? In that sense it sounds like it'll run into an issue where you have too many of these secrets in one Component (Secrets component) and feel like multiple-inheritance problems.

For example, if we take AssassinOberon (assassin can shoot, can see spies; Oberon can't see other spies), I could create an entity with simply [CanShoot] and omit the SeeSpies component. Whereas in yours it sounds like Secrets will contain assassin shoot, and see spies, and then you have to subtract see spies for the Oberon effect?

I'm also curious how you will implement some logic for example "On the next turn, someone who has been carded with X may not vote on the team.". In this case, I could simply remove the CanVoteTeam component to implement this behavior on the entity. Would you be able to do that in Secrets or some other way?

Indeed extensibility is a tricky concept. The more extensible we make it, the more complex it becomes. The more hooks we expose, the more specific our game engine (which is ideally generic) becomes. This breakdown of each component that I had come up with seems to fit this nicely, where systems do most of the heavy lifting for the "quirks" of the special roles in the game engine.

Thank you for your kind compliment. This project has taken up much of my time, and I regrettably do not have the capacity to work on this further. No offence taken at all. Your friends are welcome to do whatever they like with the code. We do have a decently sized community that is brilliant at coming up with whack roles, so if your friends need any inspiration feel free to join the discord. Link is in the chat box when you log in.

neyney10 commented 2 years ago

Hi @vck3000, sorry in advance for the long reply below.

This is indeed a tough problem. Something I've learnt over the years is that any complex piece of logic, no matter how much you try to keep it tidy, abstracted, interfaced, separated, etc., will always still result in complex code. Of course, this doesn't mean to just throw things together (like I did on my first write-up; it was my first proper large scale programming project), which is why we are fortunate to have these conversations.

True, sometimes the design complexity is inherent to the domain. Complex domains require complex design. Now, while it is true that simple is better than complex in terms of maintainability, Complex, in general, isn't necessarily a bad thing if the situation requires it. But as you said, it is still important to keep it tidy as most as possible and follow the design principles of software engineering.

I'm intrigued by your notion of a "HaveSecrets". Correct me if I'm wrong, but it sounds like a secret is a generic container that stores special features about a particular role? In that sense, it sounds like it'll run into an issue where you have too many of these secrets in one Component (Secrets component) and feel like multiple-inheritance problems.

Well, I've hidden many (probably important) details here, while the idea is the same, it is important to mention that I've had to highly abstract those components as my friend's aspirations are big. We wanted to support +100 unique characters, which can come with their own unique vote cards (i.e., The game now has more than just Fail Or Success), character variants (such as AssasinOberon, which is a union of two unique characters) can quadruple the number of optional game character variants to +400. We wanted to support Plot Cards (Cards that affect the game flow and almost anything, such as increasing the number of next quest participants, switching quests, doubling the voting power, etc...), Personal Plot Cards (per game participants - it is a bit different than the previous Plot Cards). We wanted to support some popular extensions such as Lady of the Lake, Excalibur, and Power of Avalon (let's put the details aside for now). Most, if not all of these ideas come from exploring the web for extensions for Avalon games.

Now, back to the topic, In my case, the GetSecrets() method of HaveSecrets returns the secrets of a character (we had another semantic meaning for the term "role" ^_^"), for example, Percival should have the secrets of knowing who is Merlin. While Morgana is interfering with that ability by using her PretendToBe(Merlin, to=Percival) (this is not a real code that I've written, it is for simplicity). which then adds another secret to Percival's knowledge of [Merlin, is also ...]. At the end of the day, Percival has the knowledge of two participants, which both can be Merlin.

I won't get into the whole domain details of user-created characters, But similarly for Merlin and Mordred (which is hidden to merlin) Merlin's GetSecrets() would return all spies in the game (as this is its original behavior definition), while Mordred will have something like HiddenFrom(merlin) - Which the component itself 'edits' the Secrets knowledge of Merlin (similar to the scenario with Percival-Morgana) and removes Oberon from the list.

You use the GetSecrets() to retrieve the secrets once, and then store it somewhere else (probably in GameData or somethin), therefore, other components can edit it. Another Idea is to make the GetSecrets() more powerful by letting it has white and black lists, a white list dynamically adds new secrets, while the black list removes secrets, and then we have GetSecrets(whiteList, blackList). One example is that we have a character named Sir Kay which is seen as a spy to Merlin, but his loyalty is otherwise good as part of the resistance, In that case we can add a component of SeenByAs(Merlin, spy).

... multiple-inheritance problems.

I dislike inheritance, due to the high implementation coupling between the classes. the only inheritance I use is from interfaces to classes (or abstract classes). I don't see where the issue is and why would it happen. Care to clarify?

For example, if we take AssassinOberon (assassin can shoot, can see spies; Oberon can't see other spies), I could create an entity with simply [CanShoot] and omit the SeeSpies component. Whereas in yours it sounds like Secrets will contain assassin shoot, and see spies, and then you have to subtract see spies for the Oberon effect?

Yes, with ECS you can still create simple designs, In my case, I just failed to see why would I separate the component from the System. Probably an important detail that I've omitted is that the GameData is visible to all components. (As the Plot Cards that I've mentioned earlier can pretty much cause any change in the game).

... Whereas in yours it sounds like Secrets will contain assassin shoot, and see spies, and then you have to subtract see spies for the Oberon effect?

Correct, partially, the Secrets do not contain assassin shoot because it is not part of the secrets, My implementation would probably be almost identical as well. Although, In my case, each variant such as this is a new character by itself with its own components. Of course, if the components are small enough and generic enough, they can cover a wide range of cases, I wouldn't probably need to create a specific component for AssasinOberon, but rather use the existing components like CanAssasinate of Assassin (your [canShoot]), If Oberon can't see other spies, then he is literally blind, he has no secrets, no need for HaveSecrets component. (Omitting your [SeeSpies]).

I'm also curious how you will implement some logic for example "On the next turn, someone who has been carded with X may not vote on the team.". In this case, I could simply remove the CanVoteTeam component to implement this behavior on the entity. Would you be able to do that in Secrets or some other way?

Now, don't get confused, the secrets component is responsible for secrets. What you are describing here is a voting behavior, probably irrelevant to the character's secrets. Your solution sounds pretty simple, and doesn't contain much logic, hence, the 'system' here is very thin. Although, in my case, components are mostly behavior, but can expose data via that behavior. I would probably have something almost identical to yours. I would have CanVoteTeamComponent WHich has CanVote(): boolean as default for all characters - in the voting phase the phase will iterate through all CanVoteTeamComponent and collect the participants who can vote, and another ChangeableVoteTeamComponent which lets a phase change the Voting ability of someone. Note that I've separated both behaviors into different components, in order to have small components that focus on specific things, maybe in the future someone would request a new character that is Immune to changing the voting privileges.

If I didn't understand correctly your example, please correct me.

so if your friends need any inspiration feel free to join the discord. Link is in the chat box when you log in.

Thank you, I might join on their behalf.

Edit: Oh, when I think of it, the game phases are quite similar to the 'Systems' in ECS, as they call all the components and stuff. It is just that my components contain encapsulation and behavior, and the systems know when to call them. Rather than running the systems on every game step/frame as they are intended originally, for iteratively running X steps of the game per second. For example:

A good thread (comments included) that I found about EC vs ECS for turn-based games and others . https://www.reddit.com/r/roguelikedev/comments/i3xekn/ec_vs_ecs_for_roguelikes/

vck3000 commented 6 months ago

Closing this as we probably won't need a full re-write anymore.