HackMUD: acct_nt

Thanks to Thor streaming the game a bit, I’ve recently gotten into HackMUD. It’s a pretty cool game where you need to solve puzzles to collect resources, and can use JavaScript to help do it for you. There are a bunch of different kinds of puzzles, but one of the main ones you’ll encounter happens when you go after NPCs or other players to try and take their stuff, called locks. These are where the scripting really shines, because you have a limited time to solve them and they are often extremely repetitive (for example, “find the code word, then find the proper pass digit from 0 to 9”). In addition, the solutions to these locks will eventually change, called a “rotation.” I’ve seen 15 and 30 minutes thrown around, best to err on the side of caution and assume the shorter time.

One of the more confusing ones out there is a tier 2 lock, called acct_nt. This lock generates it’s solution based on *your* transactions. Part of why it’s confusing is that there is a lot of information out there that is either correct but awkwardly explained, or partially correct, and some that’s outdated or just plain wrong. However, we can glean some useful information on how to consistently solve the lock based on a combination of in game and out of game information.

Every rotation, acct_nt can randomly select from one of a few variations of the problem to solve:

  • A large deposit near a certain timestamp
  • A large withdraw near a certain timestamp
  • A net sum of all transactions between start and end
  • An absolute sum of all of the transactions with memos between start and end
  • An absolute sum of all of the transactions without memos between start and end

Based on this, we can see that there are essentially two kinds of question: a single transaction value or an absolute sum of a list. The single transaction version is pretty simple, just scan through your transactions and try each of them. There are definitely faster ways and that is important (hint, output the index of both the transaction matching the timestamp, and the index of the transaction with the answer), but it works enough to be a starting point. As such, for the most part we’re going to focus on the sums.

The first clue is on the unofficial wiki. It states that the solution for the range will include the end transaction, but not the start transaction. If I recall correctly, the mathematical notation is (start, end]. This is very strange, since the vast majority of situations you will encounter is the opposite, [start, end).

However, if you combine it with the second clue, suddenly it makes sense: The return value of the accts.transactions call is ordered from newest to oldest. If you don’t think of start and end as timestamps but hints for indexes, you’ll realize they’re just presenting the data backwards, [end, start).

The last hint is that the acct_nt item has an extra bit of data, acct_nt_min. I am not entirely sure what this controls, but from all of the info I’ve been able to find, it can go up to 19 or 20. I also know that the we’ve seen for an acct_nt solution has been 20 transactions, so if I had to guess, when the lock rotates it generates two random numbers between acct_nt_min and 20, and uses those for a start index and a length. An alternative that I haven’t had a chance to look into is that the start index is fixed (perhaps at acct_nt_min to keep every lock from being the exact same) and it’s just the random length.

Either way, if we assume both start and length are random up to 20, this puts a cap on the number of transactions we need to scan at around 40. All we need to do is find the end timestamp first, and then add transactions based on with/without/any memos until we hit the end transaction timestamp, right?

Not quite. The timestamps have a minute resolution, and there’s nothing stopping you from having multiple transactions in one minute. In addition, the lock seems to add/subtract a sub minute random amount of time to the timestamps presented, which can sometimes just barely bump a transaction index’s timestamp into the wrong minute.

Dealing with the uncertainty of which transaction is actually the start and end is annoying, but not hard. You can scan through the transactions twice; the first time you’re just recording metadata and the second time you do the actual sums. In the first loop, you go through everything, and if the transaction timestamp matches either the end time, or the end time minus one minute, record the index of first one you come across, and for all of them add one to the “end” transaction count. Additionally, *but not exclusively*, if the transaction matches the start time or the start time plus one, add one to the “start” count, and record the index of last one you find. I say not exclusively because if you have a particularly busy minute, both the start and end timestamps can be the same minute, and the code fails if you use else if (ask me how I know).

Annoyingly, you have to remember that the names are backwards from what you’re actually doing. The “end” transaction index is actually the start, and the “start” transaction is actually the end. At least it’s easy to iterate the possible permutations of the sums. You then just need to total the memo-matching transactions between (endIndex + endOffset) and (startIndex - startOffset) where endOffset and startOffset are just loop values (think for(let endOffset = 0; endOffset < endTxCount; endOffset++) and the same for startOffset). Just remember that if you are the sender, you need to make the transaction amount negative before adding it to the running total!

The last annoying bit is the fact that you generally need an absolute sum. Fortunately “net” can be negative, and “earned” is already positive, so all you need to do is multiply the answer to “spent” by -1 before trying it.

A small fun cursed thing, you can use tri state logic for the memos conditional. let memos = null to define the variable, then set memos to true or false if the lock wants with or without specifically. Then the conditional becomes a very simple if(memos !== !tx.memo) total += tx.amount * signValue; This uses the fact that undefined, null, and "" are falsy, and anything else is truthy. null will never exactly equal either true or false and so !== will pass and add everything to the total, and the !tx.memo will convert the presence of a memo to the opposite boolean value, which will cause the !== to pass and add only the correct values.

One final little thing to remember: from what I can tell, since acct_nt secretly works via indexes, if you get a transaction during your attempt to attack an NPC or player that will change the sum it’s looking for. However, if you have the indexes saved, you can just try the appropriate sum from that first instead of jumping straight to starting over from the beginning. The only tricky thing is handling if you manage to get a transaction in the middle of trying your start/end permutations, and even then it’s just a matter of recognizing that the transaction happened and refreshing the transaction array.

I’m sure there are further details I’m missing that could help simplify things even more, but this approach has worked consistently enough for my lock breaker script. If you are familiar with the game and know of better information, don’t be afraid to let us know in the Pirate Software discord’s HackMUD channels. Just be sure to spoiler tag it for the newbies :3