5
\$\begingroup\$

This is the third rewrite of the poker bot I am writing. The first one was such a mess of if / elses that I could not deal with it. The second version was cluttered, and I had trouble following what happened but did work. I decided to scrap it for a new clean module based / more functional version but it seems to fall into the same traps.

This is my first major project in any language, and version 1 was my first JavaScript project ever. I tried to read some formatting articles such as the Google formatting guide and follow those but I haven't read any formal books yet. I should probably do that. Any recommendations would rock.

I was going to wait until it was finished to post here, however if there were any major changes to anything it would be easier to do before the rest of the functions were finished.

The project is in node.js using the Node - Steam library.

This is the main program that calls the module running the game. Some values are hard-coded in, which will have to change once I finish the first module and start on a second.

var fs = require('fs')
var Steam = require('steam')
var _ = require('lodash')
var startedRooms = []
if (fs.existsSync('servers')) {
 Steam.servers = JSON.parse(fs.readFileSync('servers'))
}
var bot = new Steam.SteamClient()
bot.logOn({
 accountName: 'SpencerFlem',
 password: 'Ap3rtur3',
 shaSentryfile: fs.readFileSync('sentryfile')
})
bot.on('loggedOn', function() {
 var mainChat = '103582791434524271'
 console.log('Logged in!')
 bot.setPersonaName('Dealer')
 bot.setPersonaState(Steam.EPersonaState.Online); // to display your bot's status as "Online"
 bot.joinChat(mainChat);
})
bot.on('servers', function(servers) {
 fs.writeFile('servers', JSON.stringify(servers))
})
bot.on('chatEnter', function(room) {
 startupRoom(room)
})
// MAIN
var allData = {}
gameLookup = {
 '103582791434524271':'./Games/Poker/5 Card Draw/5 Card Draw.js' // This to the variable to use functions from?
}
var room103582791434524271 = require('./Games/Poker/5 Card Draw/5 Card Draw.js') // Object / array instead of variable? idk if possible 
// OR: require all by default and use its name as variable
function setupData(room) {
 condensedUsers = {}
 for (var i = 0; i < Object.keys(bot.chatRooms[room]).length; i++ ) {
 condensedUsers[Object.keys(bot.chatRooms[room])[i]] = bot.users[Object.keys(bot.chatRooms[room])[i]]
 }
 allData[room].users = condensedUsers
 allData[room].steamID = bot.steamID
 allData[room].thisChatRoom = bot.chatRooms[room]
 allData[room].friends = 'NONEXISTANT' //Make this not so!
 allData[room].stored = {}
}
var players = []
function startupRoom(room) { // If went offline and rejoined done restart room?
 allData[room] = {}
 setupData(room)
 progress = room103582791434524271.startup(allData[room])
 applyProgress(progress, room)
}
function applyProgress(progress, room) {
 allData[room].stored = progress.storedData
 commandList = Object.keys(progress.commands)
 if (commandList.indexOf('sendMessage') !== -1) {
 for (var i = 0; i < progress.commands.sendMessage.length; i++) {
 if (progress.commands.sendMessage[i][0] === 'room') { //why cant it know its room?
 bot.sendMessage(room, progress.commands.sendMessage[i][1])
 }
 else {
 bot.sendMessage(progress.commands.sendMessage[i][0], progress.commands.sendMessage[i][1])
 }
 }
 }
 if (commandList.indexOf('lockChat') !== -1) {
 if (lockChat === true) {
 bot.lockChat(room)
 }
 }
 if (commandList.indexOf('unlockChat') !== -1) {
 if (unlockChat === true) {
 bot.unlockChat(room)
 }
 }
 if (commandList.indexOf('setModerated') !== -1) {
 if (setModerated === true) {
 bot.setModerated(room)
 }
 }
 if (commandList.indexOf('setUnmoderated') !== -1) {
 if (setUnmoderated === true) {
 bot.setUnmoderated(room)
 }
 }
 if (commandList.indexOf('kick') !== -1) {
 for (var i = 0; i < progress.commands.kick.length; i++) {
 bot.kick(room, progress.commands.kick[i])
 }
 }
 if (commandList.indexOf('ban') !== -1) {
 for (var i = 0; i < progress.commands.ban.length; i++) {
 bot.ban(room, progress.commands.ban[i])
 }
 }
 if (commandList.indexOf('unban') !== -1) {
 for (var i = 0; i < progress.commands.unban.length; i++) {
 bot.unban(room, progress.commands.unban[i])
 }
 }
}
bot.on('chatStateChange', function(type, player, room, agent) {
 //console.log(type + '-' + player + '-' + room + '-' + agent)
})
bot.on('chatMsg', function(room, message, type, player) {
 progress = room103582791434524271.checkMsg(allData[room], message, type, player)
 applyProgress(progress, room)
 // Do something with progress, prehaps an eval function
})

Then, the real game part starts here

/// Invigorating Imports
var _ = require('lodash') //Not necessary? //so?
var fs = require('fs')
/// Sumptuous Variables
var data = {}
var roomData = {}
var playerData = {}
/// Useful Functions
var symbolNumbers = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
var symbolSuits = ['♣', '♦', '♥', '♠']
var wordNumbers = ['Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Jack', 'Queen', 'King', 'Ace']
var wordSuits = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
function setupLookup(numbers,suits) {
 var tempLookup = []
 for (var i=0; i<numbers.length; i++) {
 for (var j=0; j<suits.length; j++) {
 tempLookup.push([numbers[i],suits[j]])
 }
 }
 return tempLookup
}
var symbolLookup = setupLookup(symbolNumbers,symbolSuits)
var wordLookup = setupLookup(wordNumbers,wordSuits)
function extractNumbers(message) { //run regex and check for each middle part to get in order?
 var regex = /\D+/
 var numbers = []
 numbers = message.split(regex)
 _.pull(numbers, '') // !!! Commas ex: 1,984 would be 1 & 984 not: 1984 :( Also 1.5 or 1 and 1/2 would be weird
 // Copypasta from old bot incoming
 var numbersZeroToNine = ['ZERO','ONE','TWO','THREE','FOUR','FIVE','SIX','SEVEN','EIGHT','NINE']
 var numbersTenToNineteen = ['TEN','ELEVEN','TWELVE','THIRTEEN','FOURTEEN','FIFTEEN','SIXTEEN','SEVENTEEN','EIGHTEEN','NINETEEN']
 var numbersTwentyToNinety = ['ZERO','TEN','TWENTY','THIRTY','FORTY','FIFTY','SIXTY','SEVENTY','EIGHTY','NINETY']
 for (i = 1 ; i < numbersTwentyToNinety.length ; i++) {
 if (message.indexOf(numbersTwentyToNinety[i]) != -1) {
 submessage = message.substr(message.indexOf(numbersTwentyToNinety[i]))
 for (j = 1 ; j < numbersZeroToNine.length ; j++) {
 if (submessage.indexOf(numbersZeroToNine[j]) != -1) {
 numberBet = i*10+j
 numbers.push(numberBet) //numberBet is bad name
 }
 else if (j == numbersZeroToNine.length - 1) {
 numberBet = i*10
 numbers.push(numberBet)
 }
 }
 }
 }
 for (k = 0 ; k < numbersTenToNineteen.length ; k++) {
 if (message.indexOf(numbersTenToNineteen[k]) != -1) {
 numberBet = k+10
 numbers.push(numberBet)
 }
 }
 for (l = 0 ; l < numbersZeroToNine.length ; l++) {
 if (message.indexOf(numbersZeroToNine[l]) != -1) {
 numberBet = l
 numbers.push(numberBet)
 } 
 }
 // </copypasta>
 console.log('NUMBERSNUMBERSNUMBERS---' + numbers)
 return numbers // Array should be in order of distance, right now is not due to copypasta
}
function formatHand(player) { //unfinshed //Also, add if increase
 var optionsList = []
 if (player === roomData.turn) {
 if (playerData[player].mustSpecifyBetAmount === true) {
 optionsList.push('SAY HOW MUCH YOU BET')
 }
 else {
 if (playerData[player].canBet === true) { optionsList.push('Bet') }
 if (playerData[player].canCheck === true) { optionsList.push('Check') }
 if (playerData[player].canAllIn === true) { optionsList.push('All In (' + playerData[player].wallet + ')') }
 if (playerData[player].canRaise === true) { optionsList.push('Raise (#)') }
 if (playerData[player].canCall === true) { optionsList.push('Call (' + (roomData.currentBet - playerData[player].amountBet) + ')') }
 if (playerData[player].canFold === true) { optionsList.push('Fold') }
 if (playerData[player].canShow === true) { optionsList.push('Yes'); optionsList.push('No')} 
 }
 }
 else {
 optionsList.push('AWAIT YOUR TURN')
 }
 var shortHand = ''
 for (var i = 0; i < playerData[player].hand.length; i++) {
 shortHand += symbolLookup[ playerData[player].hand[i] ][0] 
 shortHand += symbolLookup[ playerData[player].hand[i] ][1]
 if (i !== playerData[player].hand.length - 1) {
 shortHand += ' '
 }
 }
 var shortOptions = ''
 for (var i = 0; i < optionsList.length; i++) {
 shortOptions += optionsList[i]
 if (i !== optionsList.length - 1) {
 shortOptions += ' | '
 }
 }
 var longHand = ' '
 for (var i = 0; i < playerData[player].hand.length; i++) {
 longHand += ' '
 longHand += symbolLookup[ playerData[player].hand[i] ][0]
 longHand += symbolLookup[ playerData[player].hand[i] ][1] 
 }
 var statusSpaces = ''
 var statusNumberList = ''
 statusNumberList += roomData.currentBet.toString() + roomData.pot.toString() + playerData[player].wallet.toString()
 var statusSpacesNumber = Math.floor((27-(statusNumberList.length * 2)) / 4 )
 for (var i = 0; i < statusSpacesNumber; i++ ) {
 statusSpaces += ' '
 }
 var statusBar = ''
 statusBar = statusSpaces + 'CURRENT BET: ' + roomData.currentBet + statusSpaces + 'YOUR WALLET: ' + playerData[player].wallet + statusSpaces + 'POT: ' + roomData.pot
 var longOptions = ''
 var optionsSize = 0
 for (var i = 0; i < optionsList.length; i++) {
 optionsSize += optionsList[i].length * 2
 }
 var longOptionsSpaces = ''
 var longOptionsSpacesSize = Math.floor( (91 - optionsSize) / (optionsList.length * 2) )
 for (var i = 0; i < longOptionsSpacesSize; i++) {
 longOptionsSpaces += ' '
 }
 for (var i = 0; i < (optionsList.length); i++ ) {
 if (i === 0) {
 longOptions += longOptionsSpaces + optionsList[i]
 }
 else {
 longOptions += longOptionsSpaces + '|' + longOptionsSpaces + optionsList[i]
 }
 }
 var specialMessage = '.'
 message = shortHand + '\n' + shortOptions + '\n' + '———————————————————————————' + '\n' + '.' + '\n' + longHand + '\n' + '.' + '\n' + '———————————————————————————' 
 + '\n' + statusBar + '\n' + '———————————————————————————' + '\n' + specialMessage + '\n' + longOptions
 return [player, message]
}
 /// PLAY FUNCTIONS BEGIN HERE
function check() {
 var messages = []
 if (roomData.nextTurn === roomData.originalTurn) {
 messages.push(['room', 'Everybody loses. Try harder next time.'])
 //EVERYONE LOSES
 }
 else {
 messages.push(['room', data.users[roomData.turn].playerName + ' checked.'])
 roomData.advanceTurn()
 }
 messages.push(formatHand(roomData.turn))
 if (roomData.turn !== roomData.nextTurn) {
 messages.push(formatHand(roomData.nextTurn))
 }
 return messages
}
function bet(amount) { //unfinished
 var messages = []
 if (amount.length === 0) {
 playerData[roomData.turn].mustSpecifyBetAmount = true
 messages.push(['room', 'How much do you bet?'])
 messages.push(formatHand(roomData.turn))
 }
 else if (amount.length > 1) {
 playerData[roomData.turn].mustSpecifyBetAmount = true
 var message = 'How much do you bet? You said '
 if (amount.length === 2) {
 message += 'both ' + amount[0] + ' and ' + amount[1] + '.'
 }
 else {
 for (var i = 0; i < amount.length; i++) {
 if (i === amount.length - 1) {
 message += 'and ' + amount[i] + '.'
 }
 else {
 message += amount[i] + ', '
 }
 }
 }
 messages.push(['room', message])
 messages.push(formatHand(roomData.turn))
 }
 else {
 playerData[roomData.turn].mustSpecifyBetAmount = false
 if (amount[0] === playerData[roomData.turn].wallet) {
 // ACTIVATE ALL IN
 }
 else if (amount[0] > playerData[roomData.turn].wallet) {
 messages.push(['room', 'You are too poor, please bet a little lower.'])
 }
 else {
 roomData.originalTurn = roomData.turn
 playerData[roomData.turn].wallet -= amount[0]
 roomData.pot += amount[0]
 roomData.currentBet = amount
 playerData[roomData.turn].amountBet = amount
 for (var i = 0; i < roomData.remainingPlayers.length; i++) {
 playerData[roomData.remainingPlayers[i]].canCheck = false
 playerData[roomData.remainingPlayers[i]].canBet = false
 playerData[roomData.remainingPlayers[i]].canRaise = true
 playerData[roomData.remainingPlayers[i]].canCall = true
 playerData[roomData.remainingPlayers[i]].canFold = true
 }
 messages.push(['room', data.users[roomData.turn].playerName + 'bet ' + amount + '.'])
 messages.push(formatHand(roomData.turn))
 messages.push(formatHand(roomData.nextTurn))
 roomData.advanceTurn()
 }
 }
 return messages
}
/// Interesting Exports (allowed to affect the real messages)
exports.startup = function(givenData) {
 data = givenData
 players = Object.keys(data.thisChatRoom) //shuffle?
 _.pull(players,data.steamID)
 /// OBJECT SETUP BEGINS HERE
 roomData = {
 deck: [],
 makeDeck: function() { roomData.deck = _.shuffle(_.range(0,52,1)) },
 pot: 0,
 currentBet: 0,
 players: players,
 remainingPlayers: players,
 get turn() {return roomData.remainingPlayers[0]},
 originalTurn: players[0], //used to be defined lower. Breaks?
 get nextTurn() {return _.last(roomData.remainingPlayers)},
 advanceTurn: function() { roomData.remainingPlayers.unshift(roomData.remainingPlayers.pop()) }
 }
 playerData.addPlayer = function(player) {
 this[player] = {}
 this[player].hand = []
 this[player].deal = function(amount) { // should this be here?
 for (var i = 0; i < amount; i++) { //could subtract for optimal awesome (var i = amount. . .)
 this.hand.push(roomData.deck.pop()) // check if possible 1st
 }
 }
 this[player].amountBet = 0
 this[player].wallet = 0 // REMOVE?
 this[player].canCheck = true
 this[player].canBet = true
 this[player].mustSpecifyBetAmount = false
 this[player].canCall = false
 this[player].canRaise = false
 this[player].canFold = false
 this[player].canAllIn = false
 this[player].canShow = false
 }
 /// OBJECT SETUP ENDS HERE
 roomData.makeDeck()
 setupLookup(symbolLookup,symbolNumbers,symbolSuits)
 setupLookup(wordLookup,wordNumbers,wordSuits)
 var messages = []
 for(var i=0; i < players.length; i++) {
 playerData.addPlayer(players[i])
 playerData[players[i]].deal(5)
 messages.push(formatHand(players[i]))
 }
 data.stored.roomData = roomData
 data.stored.playerData = playerData
 var commands = {}
 console.log('nn' + JSON.stringify(messages, null, 4))
 commands.sendMessage = messages
 /*
 other commands:
 .addFirend = [player1, player2]
 .lockChat = true
 .unlockChat = true
 .setModerated = true
 .setUnmoderated = true
 .kick = [player1, ...]
 .ban = [player1 ...]
 .unban = [player1, ...]
 */
 var progress = {}
 progress.storedData = data.stored
 progress.commands = commands
 return progress
}
exports.checkMsg = function(givenData, receivedMessage, type, player) { //unfinished
 data = givenData
 roomData = data.stored.roomData
 playerData = data.stored.playerData
 receivedMessage = receivedMessage.toUpperCase()
 var messages = []
 if (playerData[player].mustSpecifyBetAmount === true && roomData.turn) {
 if (receivedMessage.indexOf('CHECK') !== -1) { // better way than listing each one?
 messages.push(['room', 'Do you bet or check?'])
 playerData[player].mustSpecifyBetAmount = false
 }
 else {
 var amount = extractNumbers(receivedMessage)
 var localMessages = bet(amount)
 for (var i = 0; i < localMessages.length; i++) {
 messages.push(localMessages[i])
 }
 }
 messages.push(formatHand(player))
 }
 else {
 if (receivedMessage.indexOf('CHECK') !== -1 && roomData.turn === player && playerData[player].canCheck === true) { // if both?
 var localMessages = check()
 for (var i = 0; i < localMessages.length; i++) { // Array holding each to its required values and its outcome? Would solve both problem and allow for similar values
 messages.push(localMessages[i])
 }
 }
 if (receivedMessage.indexOf('BET') !== -1 && roomData.turn === player && playerData[player].canBet === true) {
 var amount = extractNumbers(receivedMessage)
 var localMessages = bet(amount)
 for (var i = 0; i < localMessages.length; i++) {
 messages.push(localMessages[i])
 }
 }
 }
 if(receivedMessage.indexOf('HIT ME') !== -1) {
 playerData[player].wallet += 10
 console.log(playerData[player].wallet)
 messages.push(formatHand(player))
 }
 console.log('mm' + JSON.stringify(messages, null, 4))
 data.stored.roomData = roomData
 data.stored.playerData = playerData
 var commands = {}
 commands.sendMessage = messages
 var progress = {}
 progress.storedData = data.stored
 progress.commands = commands
 return progress
}

The parts I am specifically worried about if / else monstrosities is where the chat is processed. Right now it is not too bad, but right now only checking and betting is possible. With raising, folding etc.. it is bound to get more complicated. It also isn't able to tell if the message contained two commands such as "bet check" which is a problem. According to the style guides ifs should only be 1 or two levels deep and functions should be shorter.

if (playerData[player].mustSpecifyBetAmount === true && roomData.turn) {
 if (receivedMessage.indexOf('CHECK') !== -1) { // better way than listing each one?
 messages.push(['room', 'Do you bet or check?'])
 playerData[player].mustSpecifyBetAmount = false
 }
 else {
 var amount = extractNumbers(receivedMessage)
 var localMessages = bet(amount)
 for (var i = 0; i < localMessages.length; i++) {
 messages.push(localMessages[i])
 }
 }
 messages.push(formatHand(player))
}
else {
 if (receivedMessage.indexOf('CHECK') !== -1 && roomData.turn === player && playerData[player].canCheck === true) { // if both?
 var localMessages = check()
 for (var i = 0; i < localMessages.length; i++) { // Array holding each to its required values and its outcome? Would solve both problem and allow for similar values
 messages.push(localMessages[i])
 }
 }
 if (receivedMessage.indexOf('BET') !== -1 && roomData.turn === player && playerData[player].canBet === true) {
 var amount = extractNumbers(receivedMessage)
 var localMessages = bet(amount)
 for (var i = 0; i < localMessages.length; i++) {
 messages.push(localMessages[i])
 }
 }
}

Possible ideas: I tried making an array of the trigger word, its conditions and the function to be executed which could be iterated through. Unfortinitely I could not figure out how to make the function return a value wihout it looking even messier.

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jun 6, 2014 at 4:03
\$\endgroup\$
3
  • \$\begingroup\$ See this link regarding scrapping a project and completely rewriting the code: joelonsoftware.com/articles/fog0000000069.html Hint: it's called Things You Should Never Do :) \$\endgroup\$ Commented Sep 12, 2014 at 16:29
  • \$\begingroup\$ @bazola, from reading the code it looks like OP copy pasted some things from the old bot into this version, so it's not a complete rewrite. \$\endgroup\$ Commented Sep 12, 2014 at 16:59
  • 1
    \$\begingroup\$ @bazola and Malachi, it was about half of each. The original was completely differently organized, and most of the stuff was 'technically' retyped, but some parts were pretty much taken from the first draft. Thanks for the link though, I definitely wont rewrite this draft. \$\endgroup\$ Commented Sep 13, 2014 at 20:42

1 Answer 1

8
\$\begingroup\$

All of these Nested if Statements can definitely be changed into one level if statements

if (commandList.indexOf('lockChat') !== -1) {
 if (lockChat === true) {
 bot.lockChat(room)
 }
}
if (commandList.indexOf('unlockChat') !== -1) {
 if (unlockChat === true) {
 bot.unlockChat(room)
 }
}
if (commandList.indexOf('setModerated') !== -1) {
 if (setModerated === true) {
 bot.setModerated(room)
 }
}
if (commandList.indexOf('setUnmoderated') !== -1) {
 if (setUnmoderated === true) {
 bot.setUnmoderated(room)
 }
}

so it would look like this when we add the nested if statements to the expression of the first.

if (commandList.indexOf('lockChat') !== -1 && lockChat === true) {
 bot.lockChat(room)
}
if (commandList.indexOf('unlockChat') !== -1 && unlockChat === true) {
 bot.unlockChat(room)
}
if (commandList.indexOf('setModerated') !== -1 && setModerated === true) {
 bot.setModerated(room)
}
if (commandList.indexOf('setUnmoderated') !== -1 && setUnmoderated === true) {
 bot.setUnmoderated(room)
}

While we are talking about shortening the if statements or making them cleaner, I assume that

  • lockChat
  • unlockChat
  • setModerated
  • setUnmoderated

are all boolean values.

If they are you can drop the === true part of those expressions.


I found a little blurb here

 if (receivedMessage.indexOf('BET') !== -1 && roomData.turn === player && playerData[player].canBet === true) {
 var amount = extractNumbers(receivedMessage)
 var localMessages = bet(amount)
 for (var i = 0; i < localMessages.length; i++) {
 messages.push(localMessages[i])
 }
 }

that you could eliminate a variable (middle man), amount you only use it once, just use extractNumbers(receivedMessage) inside bet(extractNumbers(receivedMessage)


other than these things, I am not sure that you can clean up those if/else statements, it looks like they are as clean as they can be while still doing what you want them to do.

I am sure that you could smoosh some things into functions

like this

if (receivedMessage.indexOf('CHECK') !== -1 && roomData.turn === player && playerData[player].canCheck === true) { // if both?
 var localMessages = check()
 for (var i = 0; i < localMessages.length; i++) { // Array holding each to its required values and its outcome? Would solve both problem and allow for similar values
 messages.push(localMessages[i])
 }
}
if (receivedMessage.indexOf('BET') !== -1 && roomData.turn === player && playerData[player].canBet === true) {
 var amount = extractNumbers(receivedMessage)
 var localMessages = bet(amount)
 for (var i = 0; i < localMessages.length; i++) {
 messages.push(localMessages[i])
 }
}

These both look like they are doing almost the same thing. once you get the functions running well, post a follow up question and we can take a look at them and see if they are efficient.

answered Sep 12, 2014 at 16:20
\$\endgroup\$
5
  • \$\begingroup\$ Thanks a ton! This is great. I'm going to polish up what I have now and post again like you suggest. One think I've been trying to figure out is that I want it to notice if you say both 'BET' and 'CHECK' but what it has right now is just picking one to be arbitrarily based on how high up the chain it is. I tried making an object with the trigger and its effect that could be looped, but I never got that to work. \$\endgroup\$ Commented Sep 14, 2014 at 0:07
  • \$\begingroup\$ Also, not to complain or anything, but I'm kinda curious, how did you find this three month old post? \$\endgroup\$ Commented Sep 14, 2014 at 0:36
  • 1
    \$\begingroup\$ @user31157 - it's a zombie \$\endgroup\$ Commented Sep 15, 2014 at 0:36
  • \$\begingroup\$ @user31157, glad to have you aboard the Code Review Question Askers! \$\endgroup\$ Commented Sep 15, 2014 at 13:19
  • 2
    \$\begingroup\$ It's good to put bad code in the OP inside blockquote, to distinguish the mess from your own suggestions \$\endgroup\$ Commented Sep 30, 2014 at 15:57

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.