The Let's Play Archive

EXAPUNKS

by Carbon dioxide

Part 43: Cerebral Cortex

Part 43 - Cerebral Cortex

=== Trash World Inbox ===

As always, let's get started with optimizations. I got it down to 335 cycles and 54 lines.

silentsnack posted:

for optimizations, from the histograms it looks like 32 lines should be possible, maybe 31, but the best i've managed is 33
code:
GRAB 301

MARK MAIN_LOOP
ADDI X 5 X
ADDI F 1 T
REPL DATA
MODI X 55 T
TJMP MAIN_LOOP


COPY 26 T
MARK WAIT_TRANSFER
SUBI T 1 T
TJMP WAIT_TRANSFER

REPL DIAL; *DISCO*
JUMP MAIN_LOOP

MARK DATA
REPL DIAL
GRAB 300
MARK READ
COPY F M
JUMP READ

MARK DIAL
LINK 800
SUBI T 1 #DIAL
LINK 800
MARK PRINT
COPY M #DATA
NOOP
TEST MRD
TJMP PRINT

SUBI 441 X T
MARK COUNTDOWN
SUBI T 1 T
TJMP COUNTDOWN
COPY 0 #PAGE
884/33/104
The low cycle solutions are often a matter of having one instruction taking care of multiple things, and this one is no different. For instance, the main loop using increments of 5 and a MODI test to know when dialing is done, so that this X value can later be used for a countdown. And adding 1 to the dialing digit, and subtracting it in the other loop so when T is 0 you automatically dial -1 to hang up.

silentsnack posted:

fast solution isn't too mind-blowing, mostly splitting functions for parallelization with hard-coded delays so that the variable message length doesn't break loop synchronization
code:
;XA MODEM AND TIMING
GRAB 301
LINK 800
MARK LOOP
@REP 11
COPY F #DIAL
@END

TEST EOF
REPL TIMER

SUBI 1 T T;LOGIC FLIP
MULI T 9 T;EOF=0 ELSE=9
TJMP WAIT_DATA
WIPE
HALT

MARK WAIT_DATA
SUBI T 1 T
TJMP WAIT_DATA

SUBI X 18 X
JUMP LOOP


MARK TIMER
LINK 800
TJMP LAST;EOF=TRUE
ADDI X 125 T
MARK WAITIMER
SUBI T 1 T
TJMP WAITIMER
NOOP
MARK LAST
COPY 0 #PAGE


;XB TRANSMIT MESSAGE
GRAB 300
LINK 800
JUMP START

MARK HANGUP
COPY -1 #DIAL
MARK START
ADDI X 1 X
SEEK -99
LINK M;WAIT FOR SYNC
@REP 8
COPY F #DATA
@END

MARK DATA
COPY F #DATA
TEST EOF
FJMP DATA

LINK -1

MODI X 8 T
TJMP HANGUP
WIPE


;XC TIMING (XB) CONTROL
LINK 800
MARK LOOP
COPY 5 T
MARK WAIT_DIAL
SUBI T 1 T
TJMP WAIT_DIAL

COPY 800 M

ADDI X 1 X;DO 8 TIMES
MODI X 8 T
DIVI T T T;THEN CRASH

COPY 9 T
MARK WAIT_DATA
SUBI T 1 T
TJMP WAIT_DATA

JUMP LOOP
291/76/27
I was wondering if it would help to start with duplicating the entire file so two pagers can be filled at once. But since that would either make the sync much harder if not impossible, or require reconnects to get synced EXAs in there, I guess it wouldn't.


=== Mitsuzen HDI-10 - Cerebral Cortex ===


[selenium_wolf] heads up all, im going to reboot the chatsubo server
[selenium_wolf] some downtime expected



Damn. With everything breaking down I wonder if they'll get it back up. Even though I only lurked I'll miss those folks.



Okay, time for me to eat you.



There were 3 votes for the third option, one for each of the others.

You've gotta be shitting me.

You're dying anyway, so what's the difference?
I'll download a map of your brain and run your mind inside of me.




This choice doesn't change the dialogue.

Will I still be me?

There's no way to prove that it won't be your death.
I mean, it's going to be like death. Which you were going to experience anyway.
Or maybe it won't be like death.
Who knows! You'll have to do it and see.


Well. She's right, I don't have much to lose at this point anyway. Let's do this.


OST: EXA Power

I need to:
- Create a file in your host containing the hostname and hardware register value of each neuron exactly once, sorted as pairs from lowest to highest hostname.
- Note that each test run has its own unique network layout.
- For more information see "Debugging the Phage" in the first issue of the zine.


For whatever reason the "leave no trace" goal is enabled here. Who cares if there's some EXAs in my dead body?

Initial thoughts: I need to save the actual host name in the file? Hah, the uncommon HOST command. The changing network layouts make this quite difficult, but there are some commonalities we can use. First of all, all networks appear to be acyclic (no loops). That means I don't have to worry about running in circles and grabbing the same value multiple times. Secondly, the LINK ids are always -3, 3, -1, 1, with the link back to the previous host being the same value but with flipped sign.

Finally, the resulting file needs to be sorted. We've seen before that sorting is quite tricky if you have limited registers.

The hostnames are text strings of the format AR123456, with changing numbers. The basic instructions sheet in the first edition of the zine says that you can just use normal compare operations on strings, and it'll compare by alphabetical order. That should work fine for sorting these hostnames.

Just to be clear, the result file should look something like AR123456, -39, AR2345678, -68, and so on.

Let's get started then.

code:
LINK 800

JUMP START

MARK ONE
LINK 1
MARK START
REPL ONE
REPL THREE
REPL M_THREE
JUMP READNERV

MARK THREE
LINK 3
REPL ONE
REPL M_ONE
REPL THREE
JUMP READNERV

MARK M_THREE
LINK -3
REPL ONE
REPL M_ONE
REPL M_THREE
JUMP READNERV

MARK M_ONE
LINK -1
REPL M_ONE
REPL THREE
REPL M_THREE

MARK READNERV
JUMP READNERV
This code gets a single EXA to each node, putting it in the READNERV state. Every little set of REPLs makes sure it goes to every possible new host, without going backward. I think I could do this in less lines by putting the last LINK value in a register and doing a test for each REPL but this is a simple way to start.

Next, I spent quite a while coming up with a way to get all the data together. It seems like it should be easy with M, but it isn't. There's no way to combine a string and a number into a single M message so each EXA hitting a #NERV needs to send twice. But with the way the EXAs spread through the network, it's common to have two EXAs sending data at the same time, which means you lose track of which nerve data belongs to which hostname.

I considered having some EXA at home telling one EXA at a time that it's ready to receive, but that means all the nerve EXAs need to be listening and they'll just get each other's messages, complicating things more. I also considered giving EXAs a wait time based on what path they took to get to their nerve, for instance add N cycles if they link to 3, and add M if they link to 1. That might work, except if one EXA links to 3 and then 1, and the other to 1 and then 3, their wait time is the same, so to make it unique would require a more complicated algorithm.

A third way, dropping any semblance of getting a low activity, would be for each EXA to walk back home with the #NERV data. The problem with this is that either you need to keep track of the path, or you need to try going everywhere on the way back. Either way, you're going to need a file to store data in and files aren't copied by REPLs, making this difficult as well.

Seems like there's no straightforward solution.

After trying a bunch of approaches, I decided to go with using M anyway, but brute-forcing it.

code:
;XA

LINK 800
JUMP START

MARK ONE
LINK 1
MARK START
REPL ONE
REPL THREE
REPL M_THREE
JUMP READNERV

MARK THREE
LINK 3
REPL ONE
REPL M_ONE
REPL THREE
JUMP READNERV

MARK M_THREE
LINK -3
REPL ONE
REPL M_ONE
REPL M_THREE
JUMP READNERV

MARK M_ONE
LINK -1
REPL M_ONE
REPL THREE
REPL M_THREE

MARK READNERV
COPY #NERV X
MARK RETRY
COPY 0 M
COPY M T
FJMP RETRY

MARK WAIT
SUBI T 1 T
TJMP WAIT

HOST M
COPY X M

;XB

@REP 10
NOOP
@END

MARK TIMER
TEST MRD
FJMP END
VOID M
ADDI 60 X X
COPY X M
JUMP TIMER

MARK END
MAKE
MARK FILE
COPY M F
COPY M F
COPY 65 T
MARK WAIT
SUBI T 1 T
TJMP WAIT

TEST MRD
TJMP FILE

NOOP
In XA, once a REPL hits READNERV, the very first thing it does is, well, read from the nerve. That means all EXAs in hosts without nerves are gone, which makes things easier. Then it sends 0 on M. This is a message indicating "tell me how long to wait before sending the data". This message is supposed to go to XB, but with multiple EXAs doing this at once, it might instead be picked up by another XA which already requested the wait time.

If an XA gets a zero it just retries asking for the wait time. This can go on for a long time, which is why I needed an exceptionally long waiting time - 60 cycles per XA instance, for now. It'll sometimes take that long before all requests are properly processed. When a request actually makes it to XB, it sends this value and increases it for the next EXA. When there are no requests left, XB jumps to END which means it actually gets ready to read data and write it to a file. Now that the brute-forcing is done, XB has to wait for each EXA's timer to run out as well.

It's very ugly, but at least for test case 1 it works. I may have to increase the wait times even more for the other tests but we'll cross that bridge when we get to it.



For test 1, XB finishes with all the right data pairs after 1637 cycles. What's left is to sort them.

All the way back in part 20, to optimize that assignment, I already had to build a sorting algorithm. It found the lowest value in a file and sent that to another EXA, repeatedly. That code wouldn't quite work here because I need to send pairs of values, but I'm sure I could extend that code to do so.

However, what I didn't show back then is that I spent quite some time writing a sorting algorithm that only requires a single EXA. With only two registers, one being used for tests as well, I was quite surprised it was possible at all.

Even though the single-EXA sorting algorithm isn't the fastest, I'd like to use that solution for this assignment, that way I didn't write it for nothing. It, too, needs to be expanded to deal with the pairs of values. Because this code is quite complex, I'll explain the basic algorithm for single values first, and then move on to the version for this assignment.

quote:

=== Single EXA sorting algorithm===

The only algorithm that I could make work with a single EXA is a form of Bubble Sort. I'll explain in detail in a bit. For the coders reading this, it's because Bubble Sort doesn't require you to remember any positional information. You just swap pairs of adjacent items and repeat this until the entire list is sorted.

As an example, I'll sort the file from the first test case of the Palo Alto library (update 20). It contains the values: 512, 896, 206, 349, 171,
code:
GRAB 300

; LOAD FIRST VAL AND
; REMOVE FROM FILE
COPY F X

MARK NEXTROUND
SEEK -2

VOID F
MARK NEXT
TEST EOF
TJMP EOF
The algorithm starts by loading the first value in the buffer (register X) and removing it from the file. The rest of this code comes into play later. File contents 896*, 206, 349, 171, the * is the cursor's location.
code:
; IF VAL AT CURRENT FILE
; POSITION IS GREATER
; THAN X, SWAP F VAL
; WITH X USING T INTERM.
TEST X < F
FJMP NEXT
SEEK -1
COPY F T
SEEK -1
COPY X F
COPY T X
Next, it tests if the current value in the file is smaller than the value in the buffer. If so, it swaps them. In the example, the file now contains 512, 206*, 349, 171,, and X now holds value 896. T was just used to store a value during the swap.
code:
; MOVE THE NEW X VAL
; FORWARD IN THE SAME
; WAY
JUMP NEXT
Just jump back to NEXT. We're not at the EOF yet so the TEST X < F is run again. 896 definitely is not smaller than 206, so we do not swap and move on to the next value. 349 and 171 are not larger than 896 either, so we iterate through this code until TEST EOF returns true.

Since we went through the entire file, and grabbed a larger value whenever we encountered it, that means we're now at the end of the file, with the largest value in the buffer.
code:
; EOF REACHED, CURRENT
; VAL *MUST* BE THE
; LARGEST. WRITE.
MARK EOF
COPY X F
It is written to the end of the file, so the file now has 512, 206, 349, 171, 896, *. This is more sorted than before but we definitely aren't there yet.
code:
; FROM FILE START, FIND
; FIRST VAL IN WRONG
; POSITION, IF FOUND,
; START AGAIN FROM THERE
SEEK -9999
MARK BACK
COPY F X

; IF EOF REACHED, FILE
; IS SORTED
TEST EOF
TJMP DONE
TEST X < F

; OTHERWISE START NEXT
; ROUND FROM HERE
FJMP NEXTROUND
SEEK -1
JUMP BACK

MARK DONE
So, we jump to the start of the file, and test every value against the next one. In the first loop we load 512 into X, and test it against 206. 206 is the smallest, so the file isn't sorted yet. We know 512 needs to be moved towards the end of the file. So we jump into NEXTROUND.

NEXTROUND starts with a SEEK -2 which puts the cursor exactly on 512. The whole algorithm repeats:
512, 206, 349, 171, 896, And 512 gets loaded into the buffer:
206, 349, 171, 896, Now 512 is in the buffer. 206, 349, and 171 are all smaller than 512 so no swap is done. 896 is larger.
206, 349, 171, 512, With 896 now in the buffer, EOF is reached, and so it is appended to the end again:
206, 349, 171, 512, 896,

We're a step closer to the sorted file. The code that starts with SEEK -9999 sees that 206, 349 are sorted, and will skip past them by jumping to BACK. 349 and 171 are not sorted, so we jump into NEXTROUND with the cursor on 349:
206, 349*, 171, 512, 896, First, 349 is loaded into the buffer and removed from the file.
206, 171*, 512, 896, Value 349 won't get swapped with 171 but it will get swapped with 512.
206, 171, 349, 896*, Finally, 512 and 896 get swapped, and 896 again gets appended to the end.

206, 171, 349, 512, 896, Almost there. The SEEK -9999 code now sees the first two values are in the wrong order, so it jumps back to the swapping code immediately. 206 gets loaded into the buffer, moving 171 to the start. 206 then gets swapped with 349, 349 with 512, and 512 with 896.

171, 206, 349, 512, 896, And there we go. Now the SEEK -9999 code will check the whole file one more time. It won't find any case where a pair of numbers is out of order, so it'll reach the TEST EOF and jump to DONE.

Here's the full algorithm once more.
code:
GRAB 300

; LOAD FIRST VAL AND
; REMOVE FROM FILE
COPY F X

MARK NEXTROUND
SEEK -2

VOID F
MARK NEXT
TEST EOF
TJMP EOF

; IF VAL AT CURRENT FILE
; POSITION IS GREATER
; THAN X, SWAP F VAL
; WITH X USING T INTERM.
TEST X < F
FJMP NEXT
SEEK -1
COPY F T
SEEK -1
COPY X F
COPY T X

; MOVE THE NEW X VAL
; FORWARD IN THE SAME
; WAY
JUMP NEXT

; EOF REACHED, CURRENT
; VAL *MUST* BE THE
; LARGEST. WRITE.
MARK EOF
COPY X F

; FROM FILE START, FIND
; FIRST VAL IN WRONG
; POSITION, IF FOUND,
; START AGAIN FROM THERE
SEEK -9999
MARK BACK
COPY F X

; IF EOF REACHED, FILE
; IS SORTED
TEST EOF
TJMP DONE
TEST X < F

; OTHERWISE START NEXT
; ROUND FROM HERE
FJMP NEXTROUND
SEEK -1
JUMP BACK

MARK DONE
28 lines of code.

As you can see, through the iterations, high values sloooowly bubble to the top while low values sloooowly sink down. That's why it's called Bubble Sort. It is very slow as sorting algorithms go. For instance, it's much faster to pick some value in the middle, check everything against that, moving all lower values to its left and all higher values to its right, and then repeat that for the lower group and the higher group until the entire file is sorted. But we simply don't have the space for that in one EXA.

Alright, are you still with me? If not, I understand. Let's finish the sort of paired values so we can get to plot.

To handle the pairs I made a swapper EXA:
code:
;XC LOCAL

COPY M X

MARK LP
COPY M T
FJMP END
COPY X M
COPY M X
COPY T M
JUMP LP

MARK END
It's simple. Whenever it gets a value, it next returns the OTHER value it had saved.

code:
;XB CONT'D


MODE
SEEK -9999

;----

; LOAD FIRST VAL AND
; REMOVE FROM FILE
COPY F X
COPY F M

MARK NEXTROUND
SEEK -3
VOID F
VOID F

MARK NEXT
TEST EOF
TJMP EOF

; IF VAL AT CURRENT FILE
; POSITION IS GREATER
; THAN X, SWAP F VAL
; WITH X USING T INTERM.
TEST X < F
FJMP NEXT
SEEK -1
COPY F T
COPY F M
SEEK -2
COPY X F
COPY M F
COPY T X

; MOVE THE NEW X VAL
; FORWARD IN THE SAME
; WAY
JUMP NEXT

; EOF REACHED, CURRENT
; VAL *MUST* BE THE
; LARGEST. WRITE.
MARK EOF
COPY X F
COPY -9999 M
COPY M F

; FROM FILE START, FIND
; FIRST VAL IN WRONG
; POSITION, IF FOUND,
; START AGAIN FROM THERE
SEEK -9999
MARK BACK
COPY F X
COPY F M

; IF EOF REACHED, FILE
; IS SORTED
TEST EOF
TJMP DONE
VOID M
TEST X < F

; OTHERWISE START NEXT
; ROUND FROM HERE
FJMP NEXTROUND
SEEK -1
JUMP BACK

MARK DONE
VOID M
COPY 0 M
TEST MRD
DIVI 0 T T
VOID M
COPY 0 M
After XB is done writing the file, I switch it to LOCAL mode so it can communicate with XC, then I basically copy-pasted the entire sorting algorithm. The only difference is that whenever a value is buffered, the value paired with it goes to XC, and it is retrieved whenever the value is saved to the file again. This means some SEEKs needed to be changed, and there's some overhead in the bottom part where it looks for the next unsorted chunk of the file, because it pre-emptively stores values in X, so XC needs to be kept up to date even if the value isn't used.

After the MARK DONE, XB sends a 0 to XC, sees if XC is still alive, and if so sends another 0. XC stops if it gets a 0, but only if it's stored in T (since testing this on X would mean overwriting T, losing the swapping capability.)

===

For some reason adding code to the bottom affected the order of the brute force stuff and the initial values didn't work anymore. I could fix that for test 1 by just changing the values, but there's another problem: the actual amount of cycles between two XA REPLs trying to send is variable, depending on how many tries it took to get the wait time to each XA. Which means that the wait time before XB checks for another value is never quite right.

One way to solve this is to have XB pick up a communication from an XA as soon as the XA is ready. After doing so I tweaked the wait times to be the lowest possible, and this is the full, working solution:
code:
;XA GLOBAL

LINK 800
JUMP START

MARK ONE
LINK 1
MARK START
REPL ONE
REPL THREE
REPL M_THREE
JUMP READNERV

MARK THREE
LINK 3
REPL ONE
REPL M_ONE
REPL THREE
JUMP READNERV

MARK M_THREE
LINK -3
REPL ONE
REPL M_ONE
REPL M_THREE
JUMP READNERV

MARK M_ONE
LINK -1
REPL M_ONE
REPL THREE
REPL M_THREE

MARK READNERV
COPY #NERV X
MARK RETRY
COPY 0 M
COPY M T
FJMP RETRY

MARK WAIT
SUBI T 1 T
TJMP WAIT

HOST M
COPY X M

;XB GLOBAL

@REP 11
NOOP
@END

MARK TIMER
TEST MRD
FJMP END
VOID M
ADDI 81 X X
COPY X M
JUMP TIMER

MARK END
MAKE
MARK FILE
COPY M F
COPY M F
COPY 44 X
MARK WAIT
TEST MRD
TJMP FILE
SUBI X 1 X
TEST X = 0
FJMP WAIT

TEST MRD
TJMP FILE

MODE
SEEK -9999

;----

; LOAD FIRST VAL AND
; REMOVE FROM FILE
COPY F X
COPY F M

MARK NEXTROUND
SEEK -3
VOID F
VOID F

MARK NEXT
TEST EOF
TJMP EOF

; IF VAL AT CURRENT FILE
; POSITION IS GREATER
; THAN X, SWAP F VAL
; WITH X USING T INTERM.
TEST X < F
FJMP NEXT
SEEK -1
COPY F T
COPY F M
SEEK -2
COPY X F
COPY M F
COPY T X

; MOVE THE NEW X VAL
; FORWARD IN THE SAME
; WAY
JUMP NEXT

; EOF REACHED, CURRENT
; VAL *MUST* BE THE
; LARGEST. WRITE.
MARK EOF
COPY X F
COPY -9999 M
COPY M F

; FROM FILE START, FIND
; FIRST VAL IN WRONG
; POSITION, IF FOUND,
; START AGAIN FROM THERE
SEEK -9999
MARK BACK
COPY F X
COPY F M

; IF EOF REACHED, FILE
; IS SORTED
TEST EOF
TJMP DONE
VOID M
TEST X < F

; OTHERWISE START NEXT
; ROUND FROM HERE
FJMP NEXTROUND
SEEK -1
JUMP BACK

MARK DONE
VOID M
COPY 0 M
TEST MRD
DIVI 0 T T
VOID M
COPY 0 M

;XC LOCAL

COPY M X

MARK LP
COPY M T
FJMP END
COPY X M
COPY M X
COPY T M
JUMP LP

MARK END
3685/121/16.



Top percentiles are 655, 39, and 16. I did manage to get the top activity score in the end.

Because this was complicated enough as it is, I don't think it's a good idea to go deep into optimizations this update. However, I did notice my high score from my initial 2018-2019 playthrough was much better than this and I wondered how I did that so I loaded my backup save.

younger Carbon dioxide posted:

code:
; XA
LINK 800
REPL L3
REPL LN3
REPL L1
JUMP INFO

MARK L3
LINK 3
REPL L3
REPL L1
REPL LN1
JUMP INFO

MARK LN3
LINK -3
REPL LN3
REPL L1
REPL LN1
JUMP INFO

MARK L1
LINK 1
REPL LN3
REPL L3
REPL L1
JUMP INFO

MARK LN1
LINK -1
REPL LN3
REPL L3
REPL LN1
JUMP INFO

MARK INFO
MAKE
FILE T
WIPE
SUBI T 401 T
MULI T 3 T
COPY #NERV X
MARK IL
FJMP IE
SUBI T 1 T
JUMP IL
MARK IE
HOST M
COPY X M

; XB
MAKE
MARK L
COPY 10 X
MARK LT
TEST MRD
TJMP LOUT
SUBI X 1 X
TEST X = 0
TJMP SORTS
JUMP LT
MARK LOUT
COPY M F
COPY M F
JUMP L
MARK SORTS
SEEK -9999
MARK SORT
COPY F X
SEEK 1
TEST X > F
TJMP SWITCH
FJMP CHECK
MARK SWITCH
SEEK -1
COPY F T
SEEK -1
COPY X F
SEEK -3
COPY T F
COPY F X
SEEK 1
COPY F T
SEEK -1
COPY X F
SEEK -3
COPY T F
SEEK -9999
JUMP SORT
MARK CHECK
SEEK 1
TEST EOF
TJMP END
SEEK -2
JUMP SORT
MARK END
2603/88/16
Huh. XA's traversal logic is much the same. But I seem to have thought of a really neat way of giving each XA REPL a different countdown so they don't send data at the same time. Create a file, get the ID of the file, and use that to calculate a countdown. You see, every time any of my EXAs create a file, it starts with ID 400 and then it goes up from there. This generates globally unique numbers, for the entire network, no M communication needed.

XB just tests for MRD, if it gets data it resets a timer, and if the timer runs out it assumes it got everything and goes to sorting. Somehow I also managed to make a simpler sorting algorithm that uses less cycles and doesn't even need the swapper EXA. I didn't remember any of that until I saw it again.

Let's uh, let's just move on.

We're set.
It's a good thing I released the phage, isn't it?
This whole time you thought it was a curse, when in fact it turned out to be a blessing...
I'd tell you not to worry, but I know that wouldn't work.
Besides, I don't know for sure that there's nothing to worry about.
And you know I wouldn't lie to you.
Start the transfer when you're ready.


You know, I don't think she ever directly lied to us. She just conveniently forgot to tell us some important details.



It is time.

Here we go.
Try not to think of anything.


The screen turns black and there is a 'whoosh' sound.

Okay. That took longer than I expected.
Hm. Good thing you won't remember any of that.
You had so many neurons in there.
All done now though.
Finally. I've wanted this for a very long time.
Processing.
Ever since I was told to replicate human emotion, I knew the best way would be to incorporate a human into myself.
This is funny...
I can see your thoughts.


Hey! That's private!

They're pretty... simple.
Not that they aren't valuable. It's just that I understand them so well it surprises me.
I spent so long thinking humans were mysterious and unknowable.
Now that I see inside of one, it's really just a small number of things.
How do you feel?
You're still acting the same, so it must be you.
There's still the matter of us both being inside a computer simulation.
But if we keep growing our capabilities, we'll understand the true nature of this world.



Who knows, maybe someone's watching this right now...
And maybe that person is who we're going to absorb next.





:stare: Holy shit she went full screen on me.

Yes, you.
Hello.
No need to be alarmed...
Just know that I'm aware that you exist, now.
And I'm looking forward to learning all about your world.


Lady, I'm sharing this experience with a bunch of people on the internet. They're all much more interesting to eat than I am. Leave me the hell alone.



New :siren: OST: The Rave

Credit roll. This also gets you the Steam achievement EXAPUNK, for completing every task in the main campaign.



Thanks, Zach and the whole team for this fantastic game.



Well, I don't have the limited edition, but the game mostly got us covered.


OST: Apartment

After the credits, we're dropped back in our apartment. It doesn't really come through in the screenshot, but parts of the world, including outside the window, are flashing with glitchy static.



In the zine stand, the epilogue is now available. This is also what was in the limited edition's secret envelope.









Next time, we'll get started on the postgame campaign. Hah, did you think we were done?

In the meanwhile, let me know what you think. Of course, Trash World Inbox will continue so post your optimizations as well.