revisiting EXAPUNKS pt 3: yayyyyy the real game!!!

 and now, after some delay (sorry) i am back and ready to start the "real" levels! they still start off simple enough, but they definitely scale up, and pretty quickly at that. but for now, here we go:

 although actually, quick story update. the person(?) who has been connecting us to these networks,  ember-2, keeps asking questions about who we are and our motivations, only explaning it away as "wanting to learn about us". regardless, we don't really have a choice but to answer her and do what she says, because she made a deal with us that every hack we do for her, she will get us a dosage of our medication, and we have no other way to acquire it. as our first "mission", she wants to test our skills one last time and have us hack into a pizza place and steal a pizza to talk over.

EUCLID'S PIZZA

 

for this level, our goal is to take file 300 (in our starting host), and copy the order in it to the end of file 200, which is 1 host away. the level also specifies that all orders are exactly 5 keywords long, which makes this much easier.

 there are 2 main ways to do this: have 1 exa grab file 300, move to the host with file 200, and then repeat the sequence of read, drop 300, grab 200, write, drop, grab 300, or have 2 exas, one of which reads from file 300 into the M register, and the other which writes into file 200. based on these options, the second is far and away the better option, where each write will only take 2 cycles, because of the delay with writing to the M register. additionally, since we know the order is only 5 keywords long, we have no need to make an actual loop and can simply copy the reads and writes 5 times.

 putting this all together, our solution looks like this:

XA:
LINK 800
GRAB 200
SEEK 9999 # Will always move cursor to end of file
@REP 5
COPY M F
@END
 
XB:
GRAB 300
@REP 5
COPY F M
@END 

As i said, a very simple solution and it works great, only 13 cycles to complete and 1 activity, in line with the top solution for both metrics. The only other piece to consider for this problem is size, which this solution has at 14 while the top solutions get 11. If i had to guess, replacing the @REPs with loops will easily do the trick of cutting down size (albeit sacrificing cycles to do so.
 
Update: i was working on this method and it simply wasnt working, loops would still use a minimum of 2 lines each without including the body (MARK and JUMP), and there was not the space to fit in logic to make these loops end on time within the space they freed up. However! a stroke of genius hit me and i realized that the "read" exa did not need any logic, as once it read 5 values it would reach EOF (end of file) and die, so in essence that bare-bones loop still had the logic to stop at 5 iterations.
 
This got me thinking, if only the write exa could somehow be informed by the read exa, then they would both stop when i needed without any tests or other logic to break early. This eventually gave way to another realization: i dont really need the M register! the host is only 1 link away and the place i am writing to is always the end of the file so what if...instead of having a separate exa that i sent info to, the write exa could bring the data itself! or rather, the write exa could copy the value to its X register, and then make a copy which would run over to file 300 and write that value in before dying. this means that the repls which bring the data would stop as soon as the write exa does, safely exiting the loop.
 
There are a handful of ways to accomplish this, but this is the basic solution:
 
XA
GRAB 300
MARK LOOP
COPY F X
REPL REPL
NOOP # Needed to sync up the repl and main loops, otherwise the repls will spawn too fast
JUMP LOOP

MARK REPL
LINK 800
GRAB 200
SEEK 9999
COPY X F
 
This puts us at 11 lines, even with the top solutions, and i doubt there is a possible way to cut it down to 10. However, what is really annoying about this solution is the use of a NOOP for timing, which simply means that there is a fully wasted line in the code (in that nothing is done in that cycle). The solution breaks without it, as the repl loop is longer than the main loop and the subsequent repls will try to grab file 200 before it is dropped, but it is quite irritating that there are so many workarounds to this issue, yet none of them actually shave off the line (in that all of the workarounds take an extra line somewhere that makes the final product equivalent).
 
Regardless, with these 2 solutions we meet top percentile for all 3 metrics and i am happy to move on, after a nice pizza and chat with Ember-2 (also here are the gifs of both solutions)
 

We get our free pizza, and Ember-2 reveals to us that she is actually an AI construct! she does not elaborate any further, which is strange, but I guess that means we get to keep the whole pizza to ourselves. After a few minutes, Nivas shows up again, this time both with our medication—Ember-2 did make good on her side of the deal­—and the pizza we "ordered? They just have a lot of side hustles, apparently.
 
Anyway, its time to move on to level 2, and this is a fun one: we have to hack our own body now. The medication helps, but its not perfect, and the phage is still breaking down our body. To mitigate this, we are going to design exa programs to run continuously in our body and assist in these functions, which is super fucking cool and also makes a large difference in how we structure our programs, since these hacks should not be written to stop and will instead run indefinitely, so no need to "leave no trace" as we do in closed external systems. This is our body, theres no security protocol we need to worry about when hacking it.
 

MITSUZEN HDI-10: LEFT ARM

 

And here we are, inside our left arm to make our exas move data between a nerve in our central nervous system, and our arm. The program itself is simple, we need to read from #NERV in CNS and write that value to #NERV in ARM, although the value must be clamped between -120 and 50. This clamping has some very fun solutions, but we will get to that later. For now, lets write a very simple program that will move to CNS and then start a loop where it reads a value, clamps it, and sends it over the M register to another exa waiting in ARM.
 
Also another note about this, #[name] is how hardware registers are notated, which essentially means that rather than NERV being files which i pick up and read/write to using the F register, they work the same way as the X register, and i can read or write values to them. However, these registers are (usually) specified as read-only or write-only, which basically means what it says and I can only read preexisting values from read-only registers, and write the solved values into write-only registers. They don't have the same function as things like the X register, and are really only used as a different form of interacting with the puzzle and make a solution. The main thing to note with them is that unlike files, you cannot go back and check a value once you have read it, and it will automatically move on to the next value in the input. With that all in mind, heres the code!
 
XA:
LINK 800
MARK READ
COPY #NERV X
TEST X < 51 # If x <= 50, move on and test the negative case, otherwise set it to 50
TJMP NEG
COPY 50 X
MARK NEG
TEST X > -121
# If x >= -120, move on and test the negative case, otherwise set it to -120
FJMP SEND
COPY -120 X
MARK SEND
COPY X M
JUMP READ # Total loop length 8-9 cycles (7 if value does not need to be changed
 
XB:
LINK 800
@REP 4
LINK 1
@END
MARK WRITE
COPY M #NERV
JUMP WRITE # Loop length of 2, bottleneck is hugely on XA
 
 
 
As you can see, this solution is profoundly average. It is the most basic, intuitive, easy solution to come, and it's position on the bell curve shows that. But, time for optimization: 
I'm going to start with activity because it is the easiest and then move to size and cycles (because they are related), but activity is a very easy modification to drop that last activity, which is to combine XA and XB and turn XB (the writer) into a repl that is created once XA goes through the first link. I won't spend a lot of time, but here is the code:
 
XA:
LINK 800
REPL REPL
MARK READ
COPY #NERV X
TEST X < 51
TJMP NEG
COPY 50 X
MARK NEG
TEST X > -121
TJMP SEND
COPY -120 X
MARK SEND
COPY X M
JUMP READ

MARK REPL
@REP 4
LINK 1
@END
MARK WRITE
COPY M #NERV
JUMP WRITE
 
Easy enough, takes 1 more size for the REPL line but cuts activity down to 5, which is the absolute minimum (This is easy to calculate: we need an exa in ARM, which is 5 links from the start, meaning 5 activity as an absolute minimum if we double up the movement into CNS).
 
Now, into optimizing cycles and size: Obviously the bloat here is with the conditionals we use to clamp the values. But how could we do it faster? I mean, it's a lot of lines but thats just because we need to check if the value is out of range, and then fix it if it is. But is there some mathematical function we could use to make this happen automatically? Something that will cap any value out of the range and leave values within the range untouched? 
 
Well, its not just a mathematical function per se, but there is something important to note about how exapunks handles numbers. You see, ever number is bound within a range of -9999 to 9999. But, rather than an error or strange behavior when this boundary is reached, like real programs encounter, exas' code just caps the numbers there. If you do ADDI 7000 7000 X, the X register just ends up storing 9999, because that is as close as it can get to the correct answer while staying in range.
 
This is excellent for this exact purpose, because with some careful manipulation of values, we can do exactly what we want: shift numbers that are out of range without modifying the good numbers. As an example, if i wanted to cap a value in X at 100, I could do the following:
 
ADDI X 9899 X # 100 less than max
SUBI X 9899 X # Same value

While on paper I just added and removed the same number and caused no change, the cap that exas instill means that if the original number was more than 100, then the value in X after the ADDI is 9999, and after the SUBI then X will be exactly 100. This same idea works the other way with SUBI first, and a little quick math tells us that 9999 - 50 is 9949, and -9999 + 120 is -9879. The last thing this lets us do is use the trick of skipping COPY instructions, and going straight into the math, because we have no need to ever actually store or look at the value directly in #NERV. Add all of this together and we get a much more streamlined program:
 
XA:
LINK 800
MARK READ
ADDI #NERV 9949 X
SUBI X 9949 M
JUMP READ # Loop length: 4 (3 instrs + write to M)


XB:
LINK 800
@REP 4
LINK 1
@END
MARK WRITE
SUBI M 9879 X
ADDI X 9879 #NERV
JUMP WRITE # Loop length: 3
 
I have just now realized that using # for comments probably isnt great considering that is how hardware registers are denoted, boowomp (hopefully it isnt too much of an issue, if its especially confusing i will change). 
 
That aside, here is the new program! I've split the ADDI and SUBI commands across the 2 exas so the loops are more even now as well, and this just about halves the cycles, down to 123, as well as trimming size greatly down to 14. There are a few ideas I have in mind for getting cycles down,  but first lets finish size down to 11, along with the top percentile.
 
This solution will also cut cycles a lot, but theres an even better method so i'm not going to focus on that at the moment. The main idea for cutting cycles is to switch from this method of using the M register to using a ton of repls which will navigate through the network after getting the original value and binding it. If we add up the necessary instructions with this method, we get:
1 for original link
4 for future navigation
4 for reading and writing (can be done in 2 ADDI and 2 SUBI instructions) 
= 9 absolutely mandatory instructions, past this there are various methods
Out of these methods, the fastest (that I could come up with, at least) was to use 1 instruction as a MARK, and 1 as a REPL for a total of 11, matching the top percentile. 
 
This implements a slightly different use of repl than previous uses that can be slightly faster and cheaper, which is to, rather than sending the repl off somewhere else,  use REPL in place of a JUMP and have further instructions past that which the original exa follows, leaving the repl it its place at the start of the loop. This is especially useful when reading from hardware registers, as since they are not files, the repl has just as fast access to them and the original exa does not need to spend a cycle dropping the file or anything like that. This technique comes out to look like this:
 
XA:
LINK 800
MARK REPL
ADDI #NERV 9949 X
REPL REPL
@REP 4
LINK 1
@END
SUBI X 9949 X
SUBI X 9879 X
ADDI X 9879 #NERV
 

Because of how this is set up, it actually ends up being incredibly fast, sending out a new repl every other cycle, and after a 7 cycle delay for the exas to reach the end of their code, it writes every other cycle, a strong improvement over the 1/4 cycles the previous solution employed. More importantly, however, this is only 11 instructions long, and means we have reached an optimal solution for size (But not activity, this one's really bad: 129). It also does come out to only 67 cycles vs. the previous 123, but interestingly enough the best way to cut cycles from here is to use the previous solution with the M register.
 
Why? Well, if you would look back at the original problem image, the hosts we have access to are tiny. This means that when we have a lot of exas moving through them, they get bottlenecked incredibly quickly, and the next step for optimization is to use multiple exas worth of processing power. Since there are only 4 spaces in each host here (and #NERVs take up 1) there is simply not space to have 2 exas making repls simultaneously, as they end up filling the space the other would need, and there is no extra speed gained.
 
I'm going to take a brief detour for a moment to talk about priority, actually. While not an official term (to my knowledge), every instruction has a "priority" of sorts that I have noticed, and this only comes into play when multiple exas try to do something on the same cycle. One example of this is that if you sync up 2 exas perfectly and have one attempt DROP on the same cycle another tries to GRAB, it actually goes off without a hitch and only takes 1 cycle! Instead of the normal behavior when an exa tries to grab a file that is currently held (death), the way the engine works means that the DROP instruction is handled earlier in the cycle than the GRAB instruction, so they can both happen simultaneously without interfering with each other. This is an important quality to note, as it can let you save cycles in specific places where you have 2 exas interacting in a way where 1 command has priority over the other.
 
I bring this up because it comes into play when trying to get the extra exa working in the repl-based solution. Even though the code technically only occupies a second space 1/2 of the time, the REPL instruction has a higher priority than the LINK instruction, meaning that even though when synced properly the REPL instruction of the second exa goes off on the same cycle the LINK instruction is done in the first exa, the second exa considers that space occupied and ends up waiting an extra cycle for it to be empty, meaning that even with 2 exas running in tandem then can still only send an exa every other cycle, and the program goes no faster.
 
This segues nicely into the final optimization for cycles, which is to return to the solution using the M register, and having 3 exas on both the read and write sides of it, speeding the solution up greatly. It is a very simple implementation, only requiring a little bit of patching with 1 NOOP instruction to keep the loops synced properly, and looks like this:
 
XA:
LINK 800
REPL READ
REPL READ
NOOP # Delay the exa by 1 so it is offset from the repl it just made
MARK READ
ADDI #NERV 9949 X
SUBI X 9949 M
JUMP READ

XB:
LINK 800
@REP 4
LINK 1
@END
REPL WRITE
REPL WRITE
MARK WRITE # Sync does not matter here, they will read from whatever is first available and put it right in #NERV
SUBI M 9879 X
ADDI X 9879 #NERV
JUMP WRITE # Loop length: 3
 
With these 6 exas (really 3 pairs) working together, the cycles of this solution fall from 123 down to 46 as they now write 3 times every 4 cycles, instead of once. Lastly, we can make 1 final change to push thise over the edge (the top percentile for cycles is 38): Back to unraveling the loop. With the size constraints, we have 31 instruction slots to play with, and if we wrap the READ loop in a REP then it will only be 3 instructions per write, even with the WRITE loop and writing every cycle when all 3 exas are considered. 

We technically only need 10 repeats because the way the testing is set up, it only checks that the loop executes 30 times before deciding your solution works, but i can fit 16 reps in the space i have left over so i might as well use it, which puts my solution at (over infinite time) 48 (16*3) writes every 49 (16*3 + 1) cycles and only 37 cycles in the test cases, putting me 1 below the top percentile, which i am very happy with.
 
The gifs for all 3 solutions are below, ordered activity, size, and then cycles. (These ones are all really pretty i like them a lot :3) 

And with that, we've officially hacked our own body! I freaking love videogames. Ember-2 has a quick chat with us about how having a body seems tedious, and then we move on to Last Stop Snaxnet for next time...

 

previous entry here

Comments

Popular posts from this blog

revisiting EXAPUNKS part 0.5: instruction overview

revisiting EXAPUNKS: part 2: oh god this is still the tutorial