For bots in our game, we simply use the colyseus.js client libraries and have the matchmaker add them to the game room if they are needed. Each bot is an object of the Bot class.
We then setup listeners for the bot similar to how the actual client listens, like when a card is played then do something.
Generally we try to avoid treating the bot differently from an actual client which makes our workflow and loadtesting much easier as the game does not care what kind of client has joined + we can offload bots to a different process or server if needed.
We leave it up to the matchmaker if the game needs a bot then simply create one and join them to the room, to the player they look exactly the same as a real player. We also have special Bot rooms where the client might want to intentionally play against a bot, like in a campaign.
Here, we simply extend the base game and have the bot join as part of the joining logic, i.e, we create and join the bot once the player has joined the room.
If there is anything specific you would like me to expand on then I will try to get back to you.