diff --git a/icons/faction-eyrie-bot.png b/icons/faction-eyrie-bot.png new file mode 100644 index 0000000..27b8ec7 Binary files /dev/null and b/icons/faction-eyrie-bot.png differ diff --git a/icons/faction-marquise-bot.png b/icons/faction-marquise-bot.png new file mode 100644 index 0000000..751c4eb Binary files /dev/null and b/icons/faction-marquise-bot.png differ diff --git a/icons/faction-vagabond-bot.png b/icons/faction-vagabond-bot.png new file mode 100644 index 0000000..f49d9c9 Binary files /dev/null and b/icons/faction-vagabond-bot.png differ diff --git a/icons/faction-woodland-bot.png b/icons/faction-woodland-bot.png new file mode 100644 index 0000000..4ee670d Binary files /dev/null and b/icons/faction-woodland-bot.png differ diff --git a/index.html b/index.html index 13a1ddd..a0a8694 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,13 @@

Enter your parameters below, or die

+
+ +
@@ -41,7 +48,6 @@

PLAYERS

- diff --git a/js/app.js b/js/app.js index 9b33c40..faa7623 100644 --- a/js/app.js +++ b/js/app.js @@ -2,6 +2,7 @@ const PLAYER_LIST_STORAGE_KEY = "playerList"; const State = { playerList: [], + addBot: false, game: null } @@ -20,7 +21,7 @@ function populatePlayerListHtml() { while(playerList.firstChild) { playerList.removeChild(playerList.firstChild); } - State.playerList.forEach((playerName, index) => { + State.playerList.forEach((player, index) => { const newPlayerListItem = document.createElement("li"); const button = document.createElement("button"); @@ -30,7 +31,7 @@ function populatePlayerListHtml() { button.addEventListener("click", clearPlayer); newPlayerListItem.appendChild(button); - newPlayerListItem.appendChild(document.createTextNode(playerName)); + newPlayerListItem.appendChild(document.createTextNode(player.name)); playerList.appendChild(newPlayerListItem); }); @@ -62,20 +63,41 @@ function addPlayer(event) { event.preventDefault(); const playerName = document.getElementById("playerNameInput").value; - if (playerName === "") { - return; + // Don't add players with duplicate, bot, or empty names + if (!State.addBot) { + const badNames = State.playerList.map(player => player.name) + .concat(Object.values(DATA.BOT_PLAYERS).map(player => player.name)) + .concat(["", "Random Bot"]); + if (badNames.includes(playerName)) { + throw "Invalid player name."; + } } - - State.playerList.push(playerName); + // Don't add too many bot players + else if (State.addBot) { + const botPlayers = State.playerList.filter(player => player.bot).length; + const maxBots = Object.keys(DATA.BOT_PLAYERS).length; + if (botPlayers >= maxBots) { + throw "Maximum number of bot players reached."; + } + } + + State.playerList.push({ + name: playerName || "Random Bot", + faction: undefined, + iconFileName: null, + bot: State.addBot + }); savePlayersLocally(); populatePlayerListHtml(); document.getElementById("playerNameInput").value = ""; } -function canFactionBePicked(faction, selectedFactions) { - if (faction.onlyPresentWith && faction.onlyPresentWith.length > 0) { - const requiredFactions = faction.onlyPresentWith.map(presentWith => DATA.FACTIONS[presentWith] || console.error(`Invalid faction in onlyPresentWith for ${faction.name}: ${presentWith}`)); +function canFactionBePicked(faction, selectedFactions, forBot) { + if (forBot && !isBotFaction(faction)) return false; + const factionObj = getFaction(faction); + if (factionObj.onlyPresentWith && factionObj.onlyPresentWith.length > 0) { + const requiredFactions = factionObj.onlyPresentWith.map(presentWith => presentWith || console.error(`Invalid faction in onlyPresentWith for ${factionObj.name}: ${presentWith}`)); if (requiredFactions.filter(requiredFaction => selectedFactions.indexOf(requiredFaction) === -1).length > 0) { return false; } @@ -83,39 +105,56 @@ function canFactionBePicked(faction, selectedFactions) { return true; } -function randomizeFactions() { +function getFaction(faction) { + return DATA.FACTIONS[faction]; +} + +function isBotFaction(faction) { + return Object.values(DATA.BOT_PLAYERS).map(bot => bot.faction).includes(faction); +} + +function selectRandomFactions(numHumans, numBots, chosenFactions) { const availableFactions = Array.from(DATA.FACTION_LIST_BY_REACH); - const numPlayers = State.playerList.length; - const minReach = DATA.REACH_BY_PLAYER_COUNT[numPlayers]; + const numFactions = numHumans + numBots + chosenFactions.length; + if(numFactions <= 1) { + throw "Insufficient player count."; + } + else if(numFactions > availableFactions.length) { + throw "Not enough available factions for this player count."; + } + + const minReach = DATA.REACH_BY_PLAYER_COUNT[numFactions]; var currentReach = 0; const selectedFactions = []; - if(numPlayers > availableFactions.length) { - throw "Not enough available factions for this player count."; + function selectFaction(faction) { + selectedFactions.push(faction); + availableFactions.splice(availableFactions.indexOf(faction), 1); + currentReach += getFaction(faction).reach; } - for(var i = 0; i < numPlayers; i++) { + function pickReachableFaction(forBot) { // Calculate the minimum reach a faction can have to still be considered. We do this by summing the X biggest factions, where X is remaining players - 1, then // determining the minimum reach that the last faction could have to still hit the target reach value. - const biggestCombinationStartIndex = availableFactions.length - (numPlayers - 1 - selectedFactions.length); - const minimumFactionReach = minReach - currentReach - availableFactions.slice(biggestCombinationStartIndex).map(faction => faction.reach).reduce((total, reach) => total += reach, 0); + const biggestCombinationStartIndex = availableFactions.length - (numFactions - 1 - selectedFactions.length); + const minimumFactionReach = minReach - currentReach - availableFactions.slice(biggestCombinationStartIndex).map(faction => getFaction(faction).reach).reduce((total, reach) => total += reach, 0); // Drop any factions from the list that would no longer allow us to hit the target reach - while (availableFactions.length > 0 && availableFactions[0].reach < minimumFactionReach) { + while (availableFactions.length > 0 && getFaction(availableFactions[0]).reach < minimumFactionReach) { availableFactions.splice(0, 1); } if (availableFactions.length == 0) { throw "There is no combination of available factions which hits the target reach."; } // Pluck random faction - const pickableFactions = availableFactions.filter(faction => canFactionBePicked(faction, selectedFactions)); + const pickableFactions = availableFactions.filter(faction => canFactionBePicked(faction, selectedFactions, forBot)); const pickableFactionIndex = Math.floor(Math.random() * pickableFactions.length); const faction = pickableFactions[pickableFactionIndex]; - selectedFactions.push(faction); - const availableFactionsIndex = availableFactions.indexOf(faction); - availableFactions.splice(availableFactionsIndex, 1); - currentReach += faction.reach; + selectFaction(faction); } + chosenFactions.forEach(faction => selectFaction(faction)); + for(var i = 0; i < numBots; i++) pickReachableFaction(true); + for(var i = 0; i < numHumans; i++) pickReachableFaction(false); return selectedFactions; } @@ -142,18 +181,48 @@ function randomizeMap() { } } -function randomizePlayerSetup() { +function getBotPlayer(faction) { + if (!isBotFaction(faction)) throw "Bots cannot play this faction."; + const bots = Object.values(DATA.BOT_PLAYERS); + for (let i = 0; i < bots.length; i++) { + if (faction == bots[i].faction) return bots[i]; + } +} + +function assignPlayerFactions() { const players = Array.from(State.playerList); - const factions = randomizeFactions(); - // randomly assign factions to players - return players.map(player => ({ - player: player, - faction: factions.splice(Math.floor(Math.random() * factions.length), 1)[0], - })); + const numBots = players.filter(player => player.bot).length + const chosenFactions = players.filter(player => player.faction); + const factions = selectRandomFactions(players.length - numBots, numBots, chosenFactions); + const setup = []; + // Pre-chosen factions go first + players.forEach(player => { + if (player.faction) { + factions.splice(player.faction, 1); + setup.push(player); + } + }) + // Assign bot factions to all bots next + players.forEach(player => { + if (player.bot && player.faction === undefined) { + const botFaction = factions.filter(faction => isBotFaction(faction))[0]; + factions.splice(factions.indexOf(botFaction), 1); + setup.push(getBotPlayer(botFaction)); + } + }) + // Assign remaining factions to players last + players.forEach(player => { + if (!player.bot && player.faction === undefined) { + const newPlayer = {...player}; + newPlayer.faction = factions.splice(0,1)[0]; + setup.push(newPlayer); + } + }) + return setup; } function randomizeGame() { - const playerSetups = randomizePlayerSetup(); + const playerSetups = assignPlayerFactions(); return { tableSize: playerSetups.length, seats: playerSetups.sort(() => Math.random() - 0.5), @@ -161,15 +230,34 @@ function randomizeGame() { } } +function toggleBot() { + const nameInput = document.getElementById("playerNameInput"); + const addBot = document.getElementById("add-bot"); + if (addBot.checked) { + nameInput.setAttribute("readonly", true); + nameInput.setAttribute("placeholder", "Random Bot"); + nameInput.setAttribute("value", ""); + State.addBot = true; + } + else { + nameInput.removeAttribute("readonly"); + nameInput.setAttribute("placeholder", ""); + nameInput.setAttribute("value", ""); + State.addBot = false; + } +} + function getSeatListHtml(seats) { const seatList = document.createElement("ul"); seatList.setAttribute("class", "seat-list"); seats.forEach(seat => { const seatListItem = document.createElement("li"); - const iconPath = `./icons/${seat.faction.iconFileName}`; + const factionObj = getFaction(seat.faction); + const iconFileName = seat.iconFileName ?? factionObj.iconFileName; + const iconPath = `./icons/${iconFileName}`; const icon = iconPath ? `` : ""; - seatListItem.innerHTML = `${seat.player} will play ${seat.faction.name} ${icon}`; + seatListItem.innerHTML = `${seat.name} will play ${factionObj.name} ${icon}`; seatList.appendChild(seatListItem); }); return seatList; @@ -240,7 +328,7 @@ function generateGame(event) { event.preventDefault(); State.game = randomizeGame(); populateGameHtml(); - console.log(JSON.stringify(State.game, null, 1)); + console.log(JSON.stringify(State.game, null, 1)); } loadState(); diff --git a/js/data.js b/js/data.js index 6d81ad9..2fe6ed8 100644 --- a/js/data.js +++ b/js/data.js @@ -89,9 +89,36 @@ const _CLEARING_TYPES = { } } +const _BOT_PLAYERS = { + MECHANICAL_MARQUISE: { + name: "The Mechanical Marquise", + faction: "MARQUISE_DE_CAT", + iconFileName: "faction-marquise-bot.png", + bot: true + }, + ELECTRICAL_EYRIE: { + name: "The Electrical Eyrie", + faction: "EYRIE_DYNASTIES", + iconFileName: "faction-eyrie-bot.png", + bot: true + }, + AUTOMATED_ALLIANCE:{ + name: "The Automated Alliance", + faction: "WOODLAND_ALLIANCE", + iconFileName: "faction-woodland-bot.png", + bot: true + }, + VAGABOT: { + name: "The Vagabot", + faction: "VAGABOND_1", + iconFileName: "faction-vagabond-bot.png", + bot: true + } +} + const DATA = { FACTIONS: _FACTIONS, - FACTION_LIST_BY_REACH: Object.keys(_FACTIONS).map(factionKey => _FACTIONS[factionKey]).sort((a, b) => a.reach - b.reach), + FACTION_LIST_BY_REACH: Object.keys(_FACTIONS).sort((a, b) => _FACTIONS[a].reach - _FACTIONS[b].reach), REACH_BY_PLAYER_COUNT: { 2: 17, 3: 18, @@ -105,7 +132,8 @@ const DATA = { MAPS: _MAPS, MAP_LIST: Object.keys(_MAPS).map(mapKey => _MAPS[mapKey]), CLEARING_TYPES: _CLEARING_TYPES, - CLEARING_TYPES_LIST: Object.keys(_CLEARING_TYPES).map(clearingTypeKey => _CLEARING_TYPES[clearingTypeKey]) + CLEARING_TYPES_LIST: Object.keys(_CLEARING_TYPES).map(clearingTypeKey => _CLEARING_TYPES[clearingTypeKey]), + BOT_PLAYERS: _BOT_PLAYERS } console.log("-- DATA --") diff --git a/maps/fall.png b/maps/fall.png index c7ef69e..45e1ad3 100644 Binary files a/maps/fall.png and b/maps/fall.png differ diff --git a/stylesheets/style.css b/stylesheets/style.css index d16677e..0948e32 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -61,6 +61,10 @@ ul.seat-list { display: inline-flex; } +.options { + font-size: smaller; +} + .button { padding: .25em; margin: .25em 1em;