I started playing MafiaWars on my last job, where I had time to spare. As it has happened in the past with me playing on a mud, when the game got repetitive I start writing tools to speed things up.When I was on the mud I used macro-enabled clients, like tintin++ to perform simple tasks for me. Since MafiaWars is a Web 2.0 application, one must emulate a web client, so things are a bit trickier than what one can achieve using a simple wget or shell-script based bot.
Preparations
Digging through the Internet I couldn’t find a decent bot-base that I could use for my experiment. The only traces of bots I found were simple brainless fight bots implemented as IMacros.
First steps
As it seemed like a good idea at the time, I started by installing IMacro plugin, then saving a macro. My first goal was to allow for the bot to fight until it has stamina, then wait until some is regenerated. I soon discovered that while there is a LOOP
command, there is no conditional execution support in the IMacro language. Recognizing that not even a simple AI can be exclusively written in IMacro‘s own language, I switched over to JavaScript based Macros.
The basic idea of the bot is to extract some data off the rendered screen, then perform some action, then start from the begining.
Bot loop
function terminated() { // Check for termination here return false; } try { while (!terminated) { // Do your logic here } } catch (e) { // I use throw(0) for forced termination when debugging, and I don't want alerts then if (e) { alert (e); } } |
Logging
To make the bot’s work auditable, and to provide some help for debugging, I created a few simple logging functions.
var consoleWindow; /** * Opens a console window * * @return the console window handle */ function openConsole() { var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher); consoleWindow = ww.openWindow(null, '', "DescriptiveWindowName", "width=600,height=300", null); consoleWindow.document.open(); consoleWindow.document.title = 'MaffiaWars Console'; consoleWindow.document.write('<html>'); consoleWindow.document.write('<head>'); consoleWindow.document.write('<title>MaffiaWars Console</title>'); consoleWindow.document.write('</head>'); consoleWindow.document.write('<body>'); consoleWindow.document.write('<div id="status" style="font-size:10px;width:100%;background-color:red">Starting up</div>'); consoleWindow.document.write('<div id="log" style="font-size:10px"></div>'); consoleWindow.document.write('</body>'); consoleWindow.document.close(); setConsoleStatus(''); } function setConsoleStatus(content) { if (!consoleWindow) { consoleWindow = openConsole(); } statusE = consoleWindow.document.getElementById('status'); statusE.firstChild.nodeValue=content; } /** * Writes to the console window, if it exists, opens it if it doesn't * * @param content * @return */ function writeConsole(content,color) { if (!consoleWindow) { consoleWindow = openConsole(); } logE = consoleWindow.document.getElementById('log'); var p = consoleWindow.document.createElement("p"); var d = new Date(); if(color){ var styleA = document.createAttribute('style'); styleA.nodeValue='background-color:'+color; p.setAttributeNode(styleA); } p.appendChild(consoleWindow.document.createTextNode(d.toDateString() + " " + d.toLocaleTimeString() + ": " + content)); logE.insertBefore(p,logE.firstChild); } function isConsoleOpen (){ return consoleWindow!=null && !consoleWindow.closed; } /** * Closes the console window */ function closeConsole() { if (isConsoleOpen()) { consoleWindow.close(); } consoleWindow=null; } |
This implements a simple log window, where you can add new entries with the chosen background color. Since the code is for my own fun, several documentation comments are missing. The window contains two areas, the status area is used to display longer units of work, while the log area displays the logs in reverse order (newest first).
Using the log is pretty straightforward. To set the status you use setConsoleStatus(message) and to add a new log entry you use the function writeConsole(content,color).The log window is not reused, and the log is not written to file, because I don’t need it.
Business logic
The business logic of the bot is very simple. It is based on how I actually play the game. The features are implemented in separate functions.
There are some global variables in the script, to store data for workflow based operations. For example the fully mastered job tiers are stored in such a global array.
Completed features
- Search for and fight opponents with mafias of the same size or smaller, when stamina is available
The function searches the fight screen for the mafia of the smallest size, that is smaller than ours. If found it’s attacked, if there is no target, the function changes the city and checks the other location for targets. - Going to hospital in NY (I have more money there) when the health is critical
- Keeping track of fight results
This feature is not useful for determining opponents yet, as there is no way of identifying the opponents on the Fight screen. - Selling goods in Cuba, then depositing to bank
Selling goods was easier, when all items started with “Sell….” now there are the corrupt politicians, that are either collected or bought right in the middle. I didn’t see it introduced, so I thought there was something wrong with the logic… - Depositing to bank in NY
- Doing jobs
- Tiers are scanned sequentially to see if there is a job that is available. If the tier is fully mastered, it is stored and is avoided in later searches. The jobs, that are not mastered are checked against the specified requirements (sufficient energy and consumables are available) If there are more jobs that satisfy the requirements the one with most required energy is executed.
- Using up energy-packs wisely to get level boosts.
The function considers the amount of energy available, the amount of experience required for the next level, the 25% extra energy from the pack, and a rule-of-thumb multiplyer, to calculate the amount of XP per energy point.
Planned features
- Repair and protect buildings in NY, before going to bank
- Playing free lotto tickets
- Using profile points to boost attack/defense
- Identifying job pre-requisites in NY and mark those jobs available despite them mastered
Lessons learned
JavaScript issues
JavaScript has never been the language of my choice. My code probably features really poor practices, there were several things that drove me nuts though.
One of my biggest disappointment in JavaScript, is that it doesn’t support suspending execution in any way. One can’t write a simple while(true){}
loop, and use a sleep()
or yield()
function to allow his computer do other things then running the bot. To overcome this issue on Linux I used the cpulimit utility, and ran the browser in a different process than my normal process. In fact I used firefox 3.0, then later firefox 3.6 to run the bot, while using firefox 3.5 as my normal browser. If I was to start again I would be using setInterval() and create some kind of heartbeat based finite state machine.
JavaScript doesn’t allow for code inclusion, which was a surprise for me. I thought I could organize the code properly, into logging, utility and business logic modules. This comes from the browser based nature of JavaScript. Unfortunately modifying the parent document to include the other files is impossible, once you run your script from a plug-in. Maybe it can be worked around, but there is really no need for that on such a pet project.
The other JavaScript issue I had, is that the authors of MafiaWars used window level variables to store some data, and use that data to dynamically create URL’s. Thus in order to access the data, that is to be stored in the link I would have to be able to access this data. Unfortunately, I just can’t. I’ve tried everything, and even though the variables are visible using FireBug they are inaccessible through the IMacro plugin.
IMacro issues
IMacro has issues with stopping running JavaScript macros. Once you close the iMacros tab it says it will terminate the running JavaScript macro, but in fact it’s only suspended. I had to implement in all the loops I used logic, to terminate the loop on certain conditions, like navigating the window away from FaceBook or closing the log window.
There is a strange feature of the IMacro, it seems that stopping the script either normally or through an exception takes almost 2 minutes for the browser to recover. I couldn’t yet find a reason for this, but it certainly causes problems.
While IMacro plugin is certainly great for simple tasks, invoking it frequently from JavaScript kills it’s performance. After a while it got so bad, that my stamina was recharging faster, then it was spent! To overcome this issue I reimplemented the simple data extraction functions in JavaScript.
These changes have sped the execution up, and reduced the number of times the browser had to be forcefully restarted.
/** * Extracts the element matching the specified parameters * * @param tagName * type of tag to look for * @param pos * occurrence of the tag matching the search attributes * @param attributeTxt * attributes to look for. The attributes are : separated key=value * pairs, where the value is either a quoted string or a regular * expression * @return the element matching the search attributes, or null if it's not found */ function extractElement(tagName, pos, attributeTxt) { var attributeA = attributeTxt.split(':'); var attributes = new Array(); for(var i=0; i<attributeA.length; i++) { var s = attributeA[i].split('='); if(s.length!=2) { throw ('Error extracting attributes from string ' + attributeTxt); } attributes[s[0]]=s[1]; } elements = content.document.getElementsByTagName(tagName); var curPos = 0; outer_loop: for (var elementIx=0; elementIx<elements.length; elementIx++) { var curE = elements[elementIx]; for (var attrIx in attributes) { if(eval('!curE.'+attrIx+' || !(curE.'+attrIx+'.match('+attributes[attrIx]+'))')) { continue outer_loop; } } curPos++; if(curPos==pos){ return curE; } } return null; } /** * Extracts the text content from the specified element * * @param tagName * @param pos * @param attributeTxt * @return text content of the element * @see extractElement(tagName, pos, attributeTxt) */ function extractText(tagName, pos, attributeTxt) { var curE = extractElement(tagName, pos, attributeTxt); if (!curE){ throw ('Cannot find '+tagName+ ' on position ' + pos + (attributeTxt?' matching '+attributeTxt:'')); } return curE.textContent; } /** * Extracts the html content from the specified element * * @param tagName * @param pos * @param attributeTxt * @return html content of the element * @see extractElement(tagName, pos, attributeTxt) */ function extractHTML(tagName, pos, attributeTxt) { var curE = extractElement(tagName, pos, attributeTxt); if (!curE){ throw ('Cannot find '+tagName+ ' on position ' + pos + (attributeTxt?' matching '+attributeTxt:'')); } return curE.innerHTML; } |
The complete code
Did I ever say I was to share it?