- Receive player turn data.
- Process data.
- React to data using the ai vtuber (or not).
- Send back the result.
If you are unaware of the format of WCP messages, please refer to: https://github.com/wAIfu-DEV/WebsocketCollabClient?tab=readme-ov-file#format-of-json-messages
The player turn data will be sent in a data
message with the payload label onu-player-turn
The data itself will be a stringified JSON string in payload.content
The parsed data will look like this:
{
"TurnDirection": "Right", /* or "Left" */
"TopStackCard": Card,
"HandCards": [], /* Array of Card */
"PlayerHands": [], /* Array of PlayerHand */
"PlayerOrder": [] /* Array of player names (string) ordered by turn (assuming right turn) */
}
Enums:
enum CardEffect {
NONE = 0,
REVERSE = 1,
SKIP_TURN = 2,
PICK_2 = 3,
WILD_CARD = 4,
WILD_PICK_4 = 5
}
enum CardColor {
BLUE = 0,
RED = 1,
YELLOW = 2,
GREEN = 3,
SPECIAL = 4
}
enum PlayAction {
PLAY_CARD = 0,
PICK_CARD = 1
}
Card:
{
"Color": CardColor,
"Number": Number, /* Number on card, is -1 if is an effect card. Is in range -1 to 9 inclusive */
"Effect": CardEffect
}
PlayerHand:
{
"Name": String, /* Name of the player */
"CardCount": Number /* Number of cards in the player's hand */
}
Once you are finished processing the data, here are is the data you will send back to the server:
{
"Action": PlayAction, /* PlayAction.PLAY_CARD if you can play a card,
or PlayAction.PICK_CARD if you cannot play with the current hand */
"CardIndex": Number, /* Index of the Card you want to play */
"WildColor": CardColor, /* Color to switch to when playing a Wild Card */
}
In order to send the data back to the server, you need to create a data
message with the payload label onu-player-action
.
The data itself should then be stringified and set as the payload content.
Due to the way ONU works, you will want to receive messages that might be sent by you (for example if you are the host).
Since messages sent by the user are discarded by the onDataMessage
listener, it is necessary to use the onAllMessages
to receive the necessary data.
const WebsocketCollabClient = require("./wcc");
const WS_URL = "<URL>";
const USER = "<USER>";
const PASS = "<PASS>";
const CHANNEL_ID = "<CHANNEL>";
let wcc = null;
exports.onLoad = () => {
wcc = new WebsocketCollabClient();
wcc.connect(WS_URL, CHANNEL_ID, {
user: USER,
pass: PASS,
})
.then(() => {
wcc.onAllMessages = (json) => {
let to_you = json["to"].includes(USER) || json["to"].includes("all");
let data_label = json["payload"]["name"];
let data_content = json["payload"]["content"];
if (to_you && data_label == "onu-player-turn") {
logger.print("Received turn data.");
playTurn(data_content, json);
return;
}
};
})
.catch((reason) => {
logger.warn("Could not connect to server:", reason);
});
};
exports.onQuit = () => {
if (wcc) wcc.disconnect();
wcc = null;
};
const CardEffect = {
NONE: 0,
REVERSE: 1,
SKIP_TURN: 2,
PICK_2: 3,
WILD_CARD: 4,
WILD_PICK_4: 5,
};
const CardColor = {
BLUE: 0,
RED: 1,
YELLOW: 2,
GREEN: 3,
SPECIAL: 4,
};
const PlayAction = {
PLAY_CARD: 0,
PICK_CARD: 1,
};
function playTurn(data_string, message) {
if (typeof data_string != "string") {
logger.warn("Received data of unknown type.");
return;
}
let data;
try {
data = JSON.parse(data_string);
} catch {
logger.warn("Could not parse data from onu.");
return;
}
let host_name = message["from"];
let stack_card = data.TopStackCard;
let optimal_color = getOptimalColor(data.HandCards);
let picked_card = pickCardIndex(data.HandCards, stack_card, optimal_color);
let action_data = JSON.stringify({
Action: picked_card == null ? PlayAction.PICK_CARD : PlayAction.PLAY_CARD,
CardIndex: picked_card,
WildColor: optimal_color,
});
wcc.sendData("onu-player-action", action_data);
}
function getOptimalColor(cards) {
let color_count = {};
color_count[CardColor.BLUE] = 0;
color_count[CardColor.RED] = 0;
color_count[CardColor.YELLOW] = 0;
color_count[CardColor.GREEN] = 0;
let optimal_color = CardColor.BLUE;
for (let i = 0; i < cards.length; ++i) {
let card = cards[i];
if (card.Color == CardColor.SPECIAL) continue;
if (++color_count[card.Color] > color_count[optimal_color])
optimal_color = card.Color;
}
return optimal_color;
}
function pickCardIndex(cards, stack_card, optimal_color) {
let picked_cards = [];
for (let i = 0; i < cards.length; ++i) {
let card = cards[i];
let base_weight = 0.0;
if (card.Effect != CardEffect.NONE) {
base_weight += 0.5;
}
if (card.Color == optimal_color) {
base_weight += 0.25;
}
if (card.Color == CardColor.SPECIAL) {
picked_cards.push({
CardIndex: i,
Weight: 1.0 + base_weight,
});
continue;
}
if (
card.Effect != CardEffect.NONE &&
card.Effect == stack_card.Effect
) {
picked_cards.push({
CardIndex: i,
Weight: 0.75 + base_weight,
});
continue;
}
if (card.Color == stack_card.Color) {
picked_cards.push({
CardIndex: i,
Weight: 0.5 + base_weight,
});
continue;
}
if (card.Number == stack_card.Number && card.Number >= 0) {
picked_cards.push({
CardIndex: i,
Weight: 0.5 + base_weight,
});
continue;
}
}
if (!picked_cards.length) return null;
picked_cards.sort((a, b) => b.Weight - a.Weight);
return picked_cards[0].CardIndex;
}
Use Godot 4.1.1 to open and build the project