new bot manager

This commit is contained in:
RebeccaNod1 2025-08-02 10:00:14 -05:00
parent 7e849d57c7
commit d96b92a989
2 changed files with 381 additions and 129 deletions

53
Bot Manager Normal file
View file

@ -0,0 +1,53 @@
// Bot Manager for Peril Dice
// Handles logic for test players (bots): picking numbers and rolling dice
// Configuration
integer LISTEN_CHANNEL = -9999; // Change if needed to match main controller
// Helper to parse and respond to pick commands
doBotPick(string botName, integer count, integer diceMax) {
list picks;
while (llGetListLength(picks) < count) {
integer roll = 1 + (integer)(llFrand((float)diceMax));
if (llListFindList(picks, [roll]) == -1) {
picks += roll;
}
}
string pickString = llDumpList2String(picks, ",");
llRegionSay(LISTEN_CHANNEL, "BOT_PICKED:" + botName + ":" + pickString);
}
// Helper to respond to peril roll commands
doBotRoll(string botName, integer diceMax) {
integer roll = 1 + (integer)(llFrand((float)diceMax));
llRegionSay(LISTEN_CHANNEL, "BOT_ROLL:" + botName + ":" + (string)roll);
}
default {
state_entry() {
llListen(LISTEN_CHANNEL, "", NULL_KEY, "");
llOwnerSay("[Bot Manager] Ready and listening on channel " + (string)LISTEN_CHANNEL);
}
listen(integer channel, string name, key id, string message) {
if (llSubStringIndex(message, "BOT_PICK:") == 0) {
// Format: BOT_PICK:bot_1:3:20
list parts = llParseStringKeepNulls(message, [":"], []);
if (llGetListLength(parts) == 4) {
string botName = llList2String(parts, 1);
integer count = (integer)llList2String(parts, 2);
integer diceMax = (integer)llList2String(parts, 3);
doBotPick(botName, count, diceMax);
}
}
else if (llSubStringIndex(message, "BOT_PERIL_ROLL:") == 0) {
// Format: BOT_PERIL_ROLL:bot_1:20
list parts = llParseStringKeepNulls(message, [":"], []);
if (llGetListLength(parts) == 3) {
string botName = llList2String(parts, 1);
integer diceMax = (integer)llList2String(parts, 2);
doBotRoll(botName, diceMax);
}
}
}
}

View file

@ -1,159 +1,358 @@
// === Game Helper Functions Module (Max 10 players) === // === Main Peril Dice Controller (Refactored with Game Helpers Integration, with dynamic player join) ===
// This version chooses a dice size based on the number of players,
// ensuring at least three numbers are available per player. //
// This version of the main game controller includes support for players (including
// the owner) joining the game at runtime via a MSG_REGISTER_PLAYER message. When
// a new player registers, they are added to the internal lists (players, names,
// lives and picksData), a floating display is rezzed for them, and helpers are
// updated. This mirrors the behaviour used when adding a test player, but now
// applies to any avatar joining the game via the dialog handler.
integer syncChannel = -77777;
integer numberPickChannel = -77888;
integer rollDialogChannel = -77999;
integer DIALOG_CHANNEL = -88888;
list players = [];
list names = [];
list lives = [];
string perilPlayer = "";
list globalPickedNumbers = [];
list picksData = [];
list readyPlayers = [];
list pickQueue = [];
integer currentPickerIdx = 0;
integer diceType = 6;
integer MSG_SHOW_DIALOG = 101;
integer MSG_ROLL_RESULT = 102;
integer MSG_UPDATE_FLOAT = 103;
integer MSG_CLEANUP_FLOAT = 104;
integer MSG_REZ_FLOAT = 105;
integer MSG_REGISTER_PLAYER = 106;
integer MSG_SYNC_GAME_STATE = 107;
integer MSG_SHOW_MENU = 201;
integer MSG_SHOW_ROLL_DIALOG = 301;
integer MSG_GET_DICE_TYPE = 1001; integer MSG_GET_DICE_TYPE = 1001;
integer MSG_DICE_TYPE_RESULT = 1005;
integer MSG_GET_PICKS_REQUIRED = 1002; integer MSG_GET_PICKS_REQUIRED = 1002;
integer MSG_GET_PICKER_INDEX = 1003; integer MSG_GET_PICKER_INDEX = 1003;
integer MSG_DICE_TYPE_RESULT = 1005;
integer MSG_SERIALIZE_GAME_STATE = 1004; integer MSG_SERIALIZE_GAME_STATE = 1004;
integer MSG_SYNC_GAME_STATE = 107;
integer MSG_SYNC_PICKQUEUE = 2001; integer MSG_SYNC_PICKQUEUE = 2001;
list lives; // Maximum number of players allowed (including test players). This should
list picksData; // mirror the value used in Floater Manager to avoid inconsistencies.
string perilPlayer; integer MAX_PLAYERS = 10;
list names;
list pickQueue;
// List of supported dice sizes; ensure the last entry can accommodate 10 players × 3 picks = 30 integer TIMEOUT_SECONDS = 600;
list STANDARD_DICE = [6, 12, 20, 30]; integer timeoutTimer;
// Determine which die to use based on player count. integer warning2min = 120;
// Each player should have at least 3 numbers available to choose from. integer warning5min = 300;
integer getDiceType(integer playerCount) { integer warning9min = 540;
integer minimumSides = playerCount * 3; integer lastWarning = 0;
integer i;
for (i = 0; i < llGetListLength(STANDARD_DICE); i++) { key currentPicker;
integer sides = llList2Integer(STANDARD_DICE, i);
if (sides >= minimumSides) { // Forward game state to helpers when it changes
return sides; updateHelpers() {
} string serialized = llList2CSV(lives) + "~" + llList2CSV(picksData) +
} "~" + perilPlayer + "~" + llList2CSV(names);
// If no predefined die has enough sides, return the largest supported die (d30) llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, serialized, NULL_KEY);
return llList2Integer(STANDARD_DICE, llGetListLength(STANDARD_DICE) - 1); llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY);
} }
// Determine how many picks a player should make based on the peril player's lives // Request helper-calculated values
integer getPicksRequiredFromName(string name) { requestDiceType() {
integer idx = llListFindList(names, [name]); llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
if (idx == -1) {
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string testName = llList2String(names, i);
if (llSubStringIndex(testName, name) != -1) {
idx = i;
i = llGetListLength(names);
}
}
}
if (idx == -1) return 0;
integer lifeCount = (integer)llList2String(lives, idx);
if (lifeCount >= 3) return 1;
if (lifeCount == 2) return 2;
return 3;
} }
// Find the index of a player in the pickQueue by name requestPicksRequired(integer idx) {
integer getPickerIndex(string name) { llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY);
return llListFindList(pickQueue, [name]);
} }
// Serialize the current game state for syncing with other scripts requestPickerIndex(string name) {
string serializeGameState() { llMessageLinked(LINK_SET, MSG_GET_PICKER_INDEX, name, NULL_KEY);
}
string generateSerializedState() {
return llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names); return llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names);
} }
// Show a pick management dialog for the owner. resetGame() {
// LSL does not support a void return type, so this returns an integer which is ignored. // Remove floats in reverse order so indices remain correct as entries are
integer showPickManager(string player, key id) { // deleted from the Floater Manager's lists. Otherwise, deleting index 0
string searchName = player; // first shifts subsequent indices and some floats are left behind.
if (llSubStringIndex(searchName, "~") != -1) {
list parts = llParseString2List(searchName, ["~"], []);
searchName = llList2String(parts, 1);
}
integer idx = llListFindList(names, [searchName]);
if (idx == -1) {
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string testName = llList2String(names, i);
if (llSubStringIndex(testName, searchName) != -1) {
idx = i;
llOwnerSay("✅ Matched pick list for: " + testName);
i = llGetListLength(names);
}
}
if (idx == -1) {
llOwnerSay("⚠️ Could not find player: " + player);
return 0;
}
}
integer maxPicks = getPicksRequiredFromName(llList2String(names, idx));
string currentData = llList2String(picksData, idx);
list parts = llParseString2List(currentData, ["|"], []);
list currentPicks = [];
if (llGetListLength(parts) > 1) {
currentPicks = llParseString2List(llList2String(parts, 1), [","], []);
}
list buttons = [];
integer i; integer i;
for (i = 1; i <= 6; i++) { for (i = llGetListLength(names) - 1; i >= 0; i--) {
string numStr = (string)i; integer ch = -777000 + i;
if (llListFindList(currentPicks, [numStr]) != -1) { llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
buttons += ["❌ " + numStr];
} else if (llGetListLength(currentPicks) < maxPicks) {
buttons += ["➕ " + numStr];
}
} }
players = names = lives = picksData = globalPickedNumbers = [];
llOwnerSay("🛠 Showing pick dialog to: " + (string)id); perilPlayer = "";
llDialog(id, "🛠 Managing picks for: " + searchName + "\nCurrent: " + llList2CSV(currentPicks), buttons, -88888); pickQueue = [];
currentPickerIdx = 0;
// Return dummy value since LSL does not have a void return type llSay(syncChannel, "RESET");
return 0; llOwnerSay(" Game reset!");
llSleep(0.2);
llSetTimerEvent(0);
updateHelpers();
} }
startNextRound() {
// Do not start a round if there are fewer than 2 players. This guard prevents
// accidental starts when only one player is present (e.g. after a reset or
// before any other participants join).
if (llGetListLength(names) < 2) {
llOwnerSay("⚠️ Need at least 2 players to start the game.");
return;
}
// If somehow called with exactly one player, treat them as the winner and reset.
if (llGetListLength(names) == 1) {
llSay(0, " " + llList2String(names, 0) + " is the last player standing and wins the game!");
resetGame();
return;
}
picksData = [];
globalPickedNumbers = [];
pickQueue = names;
currentPickerIdx = 0;
integer j;
for (j = 0; j < llGetListLength(names); j++) {
picksData += [llList2String(names, j) + "|"];
}
updateHelpers();
// requestDiceType(); // Removed to avoid recursion loop
}
showNextPickerDialog() {
string firstName = llList2String(pickQueue, currentPickerIdx);
currentPicker = llList2Key(players, llListFindList(names, [firstName]));
string dialogPayload = firstName + "|" + (string)diceType;
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, currentPicker);
timeoutTimer = llGetUnixTime();
lastWarning = 0;
llSetTimerEvent(60.0);
}
integer roundStarted = FALSE;
default { default {
state_entry() {
llListen(DIALOG_CHANNEL, "", NULL_KEY, "");
}
touch_start(integer total_number) {
llOwnerSay("Touched by: " + (string)llDetectedKey(0));
key toucher = llDetectedKey(0);
integer idx = llListFindList(players, [toucher]);
if (toucher == llGetOwner()) {
// Determine if this will be the first registered player (starter) before registration
integer isStarter = FALSE;
if (idx == -1) {
// No index found; the owner is not yet registered.
// If there are no existing players, they will become the starter.
if (llGetListLength(players) == 0) {
isStarter = TRUE;
}
string ownerName = llKey2Name(toucher);
// Send a registration request; Main.lsl will handle adding them and rezzing a float
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, ownerName + "|" + (string)toucher, NULL_KEY);
} else {
// Already registered; starter if at index 0
isStarter = (idx == 0);
}
// Show the owner menu with the appropriate starter flag
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher);
} else if (idx != -1) {
integer isStarter = (idx == 0);
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
}
}
link_message(integer sender, integer num, string str, key id) { link_message(integer sender, integer num, string str, key id) {
if (num == MSG_SYNC_GAME_STATE) { // Player list and pick list handling remain unchanged
if (num == 202 && str == "REQUEST_PLAYER_LIST") {
string namesCSV = llList2CSV(names);
llMessageLinked(LINK_SET, 203, namesCSV, id);
return;
}
if (num == 206) {
string targetName = str;
integer i;
for (i = 0; i < llGetListLength(picksData); i++) {
string rawEntry = llList2String(picksData, i);
list parts = llParseString2List(rawEntry, ["|"], []);
if (llList2String(parts, 0) == targetName) {
llMessageLinked(LINK_SET, 205, targetName + "|" + llList2String(parts, 1), id);
return;
}
}
llMessageLinked(LINK_SET, 205, targetName + "|", id);
return;
}
if (num == 208) {
string playerName = str;
integer idx = llListFindList(names, [playerName]);
if (idx != -1) {
string lifeVal = llList2String(lives, idx);
llMessageLinked(LINK_SET, 208, playerName + "|" + lifeVal, id);
}
return;
}
// Handle dice type result from helper
if (num == MSG_DICE_TYPE_RESULT) {
if (!roundStarted) {
roundStarted = TRUE;
diceType = (integer)str;
llOwnerSay(" Dice type set to: " + str);
showNextPickerDialog();
}
return;
}
if (num == MSG_GET_PICKS_REQUIRED) {
integer picksRequired = (integer)str;
llOwnerSay(" Picks required: " + str);
return;
}
if (num == MSG_GET_PICKER_INDEX) {
integer pickIndex = (integer)str;
llOwnerSay(" Picker index: " + str);
return;
}
if (num == MSG_SERIALIZE_GAME_STATE) {
string serialized = str;
llOwnerSay(" Serialized game state: " + serialized);
return;
}
// Handle pick actions
if (num == 204) {
list parts = llParseString2List(str, ["~"], []); list parts = llParseString2List(str, ["~"], []);
lives = llCSV2List(llList2String(parts, 0)); string action = llList2String(parts, 0);
picksData = llCSV2List(llList2String(parts, 1)); list args = llParseString2List(llList2String(parts, 1), ["|"], []);
perilPlayer = llList2String(parts, 2); string name = llList2String(args, 0);
names = llCSV2List(llList2String(parts, 3)); string pick = llList2String(args, 1);
integer i;
for (i = 0; i < llGetListLength(picksData); i++) {
string entry = llList2String(picksData, i);
list pdParts = llParseString2List(entry, ["|"], []);
if (llList2String(pdParts, 0) == name) {
list pickList = [];
string rawPicks = llList2String(pdParts, 1);
if (rawPicks != "") {
pickList = llParseString2List(rawPicks, [","], []);
}
if (action == "ADD_PICK") {
if (llListFindList(pickList, [pick]) == -1) {
pickList += [pick];
llOwnerSay("➕ Added " + pick + " to " + name);
}
} else if (action == "REMOVE_PICK") {
integer idx = llListFindList(pickList, [pick]);
if (idx != -1) {
pickList = llDeleteSubList(pickList, idx, idx);
llOwnerSay("➖ Removed " + pick + " from " + name);
}
}
picksData = llListReplaceList(picksData, [name + "|" + llList2CSV(pickList)], i, i);
updateHelpers();
return;
}
}
return;
} }
else if (num == MSG_SYNC_PICKQUEUE) { // New: handle dynamic registration of players (owner or players) via MSG_REGISTER_PLAYER
pickQueue = llCSV2List(str); if (num == MSG_REGISTER_PLAYER) {
// str is in the format "Name|<key>"
list parts = llParseString2List(str, ["|"], []);
string newName = llList2String(parts, 0);
key newKey = (key)llList2String(parts, 1);
// Do not register if already present
integer existingIdx = llListFindList(players, [newKey]);
if (existingIdx == -1) {
// Add to local lists
players += [newKey];
names += [newName];
lives += [3];
picksData += [newName + "|"];
// The float will be rezzed by the Floater Manager after it processes
// this registration. Avoid sending MSG_REZ_FLOAT here to prevent
// race conditions; instead, request the float from Floater Manager
// after it has updated its own lists. Immediately propagate
// helper updates now.
updateHelpers();
// Notify owner of registration
llOwnerSay("🔔 Added player: " + newName);
}
return;
} }
else if (num == MSG_GET_DICE_TYPE) { }
integer playerCount = (integer)str;
llOwnerSay("📊 playerCount received: " + (string)playerCount); listen(integer channel, string name, key id, string msg) {
integer result = getDiceType(playerCount); if (channel == DIALOG_CHANNEL) {
// Broadcast the dice type result across the entire linkset so that // Owner-specific commands: only owner messages in the dialog should be processed here.
// the main game controller can receive it. Using LINK_THIS if (id == llGetOwner()) {
// restricted the message to the helper's prim, preventing the if (msg == "Reset Game") {
// controller from seeing MSG_DICE_TYPE_RESULT and causing the resetGame();
// game not to start. return;
llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY); }
} // Note: do not handle "Start Game" here; allow the generic start logic below to apply,
else if (num == MSG_GET_PICKS_REQUIRED) { // so that minimum player checks are enforced and non-owner starters can initiate the game.
integer result = getPicksRequiredFromName(str); if (msg == "Dump Players") {
llMessageLinked(LINK_THIS, MSG_GET_PICKS_REQUIRED, (string)result, NULL_KEY); llOwnerSay(" Players: " + llList2CSV(names));
} return;
else if (num == MSG_GET_PICKER_INDEX) { }
integer result = getPickerIndex(str); if (msg == "Manage Picks") {
llMessageLinked(LINK_THIS, MSG_GET_PICKER_INDEX, (string)result, NULL_KEY); llOwnerSay(" Fetching list of players for pick management...");
} llMessageLinked(LINK_SET, 202, "REQUEST_PLAYER_LIST", llGetOwner());
else if (num == MSG_SERIALIZE_GAME_STATE) { return;
llMessageLinked(LINK_THIS, MSG_SERIALIZE_GAME_STATE, serializeGameState(), NULL_KEY); }
} if (msg == "Add Test Player") {
else if (num == 206) { // Ensure we do not exceed the maximum allowed players
llOwnerSay("📨 MSG 206 received for: " + str + " from: " + (string)id); if (llGetListLength(players) >= MAX_PLAYERS) {
showPickManager(str, id); llOwnerSay("⚠️ Cannot add test player; the game is full (max " + (string)MAX_PLAYERS + ").");
return;
}
// Generate a unique name and key for the test player
string testName = "TestBot" + (string)llGetUnixTime();
key fake = llGenerateKey();
// Delegate registration to the MSG_REGISTER_PLAYER handler; it will add to lists and rez a float
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, testName + "|" + (string)fake, NULL_KEY);
// Show the owner menu again (not flagged as starter)
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", llGetOwner());
return;
}
}
// Allow the first registered player (index 0) or the owner to start the game.
// When the starter clicks "Start Game", begin the round.
if (msg == "Start Game") {
// Enforce a minimum of 2 players before starting
if (llGetListLength(players) < 2) {
llOwnerSay("⚠️ Need at least 2 players to start the game.");
return;
}
// Check if the sender is the owner
if (id == llGetOwner()) {
startNextRound();
requestDiceType();
return;
}
// Otherwise, check if the sender is the very first player registered
if (llGetListLength(players) > 0) {
key firstPlayer = llList2Key(players, 0);
if (id == firstPlayer) {
startNextRound();
requestDiceType();
return;
}
}
}
// If the message text matches a player name, request their pick list
if (llListFindList(names, [msg]) != -1) {
llMessageLinked(LINK_SET, 206, msg, id);
}
} }
} }
} }