Reader

Read the latest posts from The Blog of the EMMA Cooperative.

from andy

The EMMA Technology Cooperative is a worker-owned coop doing contract creative-technologist work. To over-simplify it, we are programmers who work on interesting things! We work on a range of different projects but they tend to circle around installations, performance, games & interaction. We’re in our fourth year of operation and are going strong!

One of the most liberating but challenging aspects of starting a coop like EMMA is figuring out how we want to be paid. As worker-owners we can dictate exactly how members are compensated for their work (within legal bounds of course). We were aware of relatively few tech coops doing work similar to ours so determining our compensation policy started as a blue-sky exercise when we were first discussing the potential of starting a coop.

Since we incorporated, we have gone through three iterations of this policy, but the guiding principles and most of the mechanics of it have stayed the same.

In this post we’ll cover the goals that we came in with as well as several iterations of the policy.

Goals

One of the initial reasons for EMMA to exist at all was to even out the jagged nature of cash-flow in freelance. Because we are nerds, we referred to this as a financial capacitor. A capacitor is an electronics component that stores up energy and discharges it at a predictable rate. They are often used to smooth out noisy signals. We wanted to apply that principle to the boom and bust periods inherent in freelance work that can make it difficult to plan life. We wanted to smooth out this flow into a more predictable pattern of regular payouts. We also wanted to rely on each other to spread out the risks of dry spells. Instead of one member going broke during a bad year, they can be buoyed by members who are doing well, and then provide that same support when they are the ones with high-paying gigs.

capacitor.svg

The financial capacitor

From the outset we wanted a policy that would feel equitable but knew that we did not necessarily want a flat pay structure. Different members of the coop have different financial obligations in life and different desires for how much they wanted to work. For this reason we wanted a structure where individual members could scale their own work up or down without requiring that all other members do the same.

People will sometimes doubt equitable arrangements like these, assuming that they are easy to exploit. After three years we have certainly hit some snags, but a lazy member “taking advantage” of our setup has not been one of them.

To summarize, our goals were:

  • Reduce the jagged nature of freelance pay
  • Provide a minimum salary that members could expect
  • Allow members some control over how much they work and earn
  • Explicitly rely on each other
  • Do this in a way that felt equitable

Hypothetical Policy

When EMMA was just an idea and not yet an incorporated business, we discussed a policy that looked like this:

  • members would receive 70% of the income on contracts they brought in and 30% would go to EMMA
  • If a member could not find work in a given month, they would receive $5,000 from EMMA

We called this “Normal Mode.”

The idea was that members would get the bulk of the money they brought in with some going to EMMA for administrative costs as well as building up a reserve to provide members who were not working at that time, establishing a minimum salary of $5k per month ($60k/year).

This policy was never put into practice. We started EMMA with no investments and no member buy-ins, so when we first began there was no money in the bank account to pay salaries. For this reason, we opted to start with members receiving 70% of the income from their contracts but offered no safety-net salary if they were not working. We called this “startup mode”. A member would exit startup mode and enter Normal Mode once they had brought in a $70k startup cost.

This was buoyed by an extremely lucrative first year. It remains our best year by a wide margin and we would likely have had a much more difficult time getting out of startup mode without that.

Issues with this Policy

The time we spent in Startup Mode was useful not only for filling our coffers, but also to let us consider the ramifications of this policy.

Providing $5k/month to members unable to work was doing what we wanted on a conceptual level, but it was clunky in practice. Determining “working or not” during a given period can be surprisingly hard as contractors if we are only considering when a payment lands. Is a member not working for two months if they are on a three month contract that only pays at the end?

Another example where this policy gets weird: if Member A earns $100k in a single month and does not work for 4, they would be entitled to the $5k in those 4 months. A member who earns $20k per month during the same period has brought in the same amount of money but would not be entitled to the $5k because they were working the whole time, giving Member A $20k more arbitrarily.

For this reason, before any member reached Normal Mode we decided to redo the system to be a flat base salary that all members get no matter what in addition to a percentage of their contracts. This made our bookkeeping and payroll math easier because it removed the question of when a member was working or not. Putting more emphasis on the base salary (members will always receive a salary as opposed to it only kicking in when they did not have work) also meant that we were more actively supporting each other which was in line with our values.

This brings us to

Compensation Policy V1 – October 2022

During Normal Mode (called Default Mode in the policy) members are compensated in two ways:

  • Salary
    • This is fixed at $60,000 per member annually, paid out twice per month
  • Revenue Share
    • Members receive 50% of the money brought in from contracts, with the remaining 50% staying in the EMMA bank account
    • Members are paid once the client pays

Although we have updated it a few times, this policy essentially describes the strategy we are using to this day. The mechanism is the same, but the details and the numbers have changed.

The coop is promising to pay members base-salary even if they are not bringing in money. This obviously means that if members are not clearing enough contracts EMMA will start to lose money. This is acceptable for a while. We waited until we had money in the bank before entering Normal Mode specifically to ensure that we could do this. We generally want to be charging the capacitor, but there is no reason to have it if we never expect it to discharge.

The other important thing in this policy is the creation of a Salary-Only Mode. If our available funds dipped below about 3 months of salary for all members, we could elect to stop paying ourselves the 50% commission until cash-flow stabilized. At that point we would call a meeting and come to a decision about when to return to Normal Mode. If none of us were working for too long, the coop would still collapse in Salary-Only Mode, but in theory it would cushion the fall.

About halfway through 2023 we had to test our theory and enter Salary-Only Mode. It was a rough year for tech work in general and we got hit with an unexpected tax burden from the previous year. Luckily, we had stored up a bit of a nest egg in out first year.

Compensation Policy V2 – February 2024

The main updates from V1 was the creation of a third mode: Survival Mode

  • In Survival Mode members do not receive revenue share and their salary is deferred
  • Survival Mode is a mode we enter after Salary-Only Mode
  • Deferred salary is tracked and paid back at a later point when the coop has the funds

With this policy we essentially setup operational tiers based on the coop’s financial health:

  • Normal Mode – This is where we want to be with members getting base pay + 50% of the contracts they bring in
  • Salary-Only Mode – Things are bad: No revenue share on contracts in order to bolster the bank account. Members still get regular paychecks.
  • Survival Mode – As the name implies, we do not want to be here! Nobody is getting paid.

We were not excited about it, but we flipped the switch and entered Salary-Only Mode in June 2023. As intended, this cushioned us for a while, but work was still pretty dry and we eventually had to enter Survival Mode near the end of the year when we did not stabilize.

It is hard to imagine working like this for a normal company, but EMMA is something we own collectively and built together. We had a strong incentive to keep it afloat and a belief that it would bounce back and pay us back — which it did! We were able to re-enter Salary-Only Mode in February of 2024 and have since resumed Normal Mode

As painful as it was to briefly enter Survival Mode, it was heartening to see that we were able to store enough away in 2022 to ride out almost all of 2023. Coops have a higher rate of survival over traditional businesses in hard times and after this experience that statistic is not surprising. This is an organization that we all have a deeply personal stake in. Furthermore, by not chasing infinite growth or immediate profits, we were able to store a lot of our profits from the previous year to get us through a lean time. If we had paid all of the 2022 excess out in bonuses or to investors, EMMA would have almost certainly closed its doors in 2023.

Compensation Policy V3 – January 2025

  • Changes to Revenue Split:
    • Instead of a 50/50 split, it is now 70/30 (in favor of the coop)
  • Changes to how money is pooled that generally shifts risk from the individual to the coop
    • For example, Members can now be paid when they invoice, not when the client pays

The biggest change in this version is a major adjustment to the revenue split to accommodate the changing economy. We arrived at this number by looking at the average revenue across our members for 2024. This means that in order to break even each member needs to invoice an average of $6,500 per month.

The three modes still do what they did previously, but we changed our thinking on how to approach member work. We are pooling our resources to make sure that everybody gets paid fairly so we decided that inconsistent or delayed client payments should represent an issue for the coop and not just the member working with them.

To this end, we have decided to have the coop calculate what a member is owed when that member invoices rather than when that invoice clears. Operating this way obviously requires a cushion in the bank account to pay members potentially before the money for a gig comes in, but we were very conservative about when we would leave Salary-Only Mode so we’ve given ourselves that wiggle room.

We also modified our policy so that exchange costs or fees for services like PayPal are absorbed by the org and not the member who’s client pays this way.

The ideology behind these changes is that we are operating as a collective. We formed EMMA partially to ensure a smoother flow of money than any of us would have operating as individual freelancers. We are expanding that slightly and applying it to some of the administrative headaches that come with our line of work.

Compensation Policy V4 – April 2025

A very minor update on V3. specifically We now exclude fixed project costs from the calculation of member revenue split. That is if the client paid $1000 but we spend $100 on supplies then the Member would receive 30% of $900.

The Future

As we grow and continue working, our compensation policy will almost certainly continue to change and be updated. The most likely aspect being the exact amounts of our EMMA/member split. The industry has changed very dramatically and we only returned to our Normal Mode in January of 2025. This number feels correct to our intuition but but there’s no substitute for real data.

Our core goals have remained the same, but the way we express those goals through mechanical actions have been revised as we find shortcomings or opportunities.

We are planning to write more about how we structure EMMA so stay tuned. And if you are in the need for talented creative technologists, please be in touch!

You can follow us on social media as well:

 
Read more...

from andy

(This post was cross-posted from my Tumblr)

This guide covers setting up a brand new Windows 11 computer to serve as an arcade machine, but it is relevant for any project that where a single program should run in a gallery environment.

I'm a member of the arcade non-profit, Arcade Commons. As you might guess, we frequently make arcade machines that need to work as easily and reliably as possible. They are often on loan to venues, so the more automatic the machine is, the more likely it is to actually be running. This is equally true for setting up interactive projects in galleries.

The goal is for the computer to turn on as soon as it gets power, launch the game on startup, and not be interrupted by myriad updates and other popups that plague the modern Windows operating system. I also added an AutoHotkey script to handle restarting the game if it crashes.

This is an update to my previous post on how to do this for Windows 10.

That post was itself based on this excellent guide by Eva Schindlin on setting up Windows 7 for permanent installation.

I'm writing this post because I just got a new computer to serve as a dedicated device for Salmon Roll: The Upstream Team, a game I developed with Jane Friedhoff and Diego Garcia. I took notes as I setup the machine, following along with my old post and adding some new things along the way.

I am working with a GMKtec NucBox G5 running Windows 11 Pro.

Required software

Before we get started, get installers for Notepad++ and AutoHotkey on a thumb drive.

I say to put these on a thumbdrive, because I do not want to connect my new computer to the internet if I can avoid it.

AutoHotkey is used for automation. If you don't plan on using it, you can skip this. Note: the AutoHotkey scripts I provide in this post are for v1.1, NOT v2.0. Make sure you grab the right installer.

Notepad++ is an excellent free text editor for Windows. Maybe you can get by doing things with TextEdit, but taking the 30 seconds to give yourself a good text editor will pay dividends.

Finally, I add the game I want to run.

Initial setup

The first time you plug the computer in, you'll get the usual Windows setup flow. If you are not connected to the internet, you should be able to skip linking the device to a Microsoft account.

If you do need to be online, it is possible to circumvent the seemingly required Microsoft account login. Here are a few posts on the subject.

Set the username as something simple with no spaces. I went with “salmon”.

Leave the password field blank. This prevents needing the password to login.

On the privacy screen I unchecked every option. No tailored experiences etc.

After that I hit confirm, and the computer thinks for a bit.

Prepping the computer

This is the bulk of the process! We're going to change a lot of settings to keep things running smoothly and without interruption.

1. Run those installers

Install Notepad++ and AutoHotkey.

2. Test the game

Now is a great time to make sure your game actually runs well on this computer.

3. Prevent sleeping

We do not want the computer to go to sleep ever.

  • Control Panel
  • Hardware and Sound
  • Power Options
  • Change when the computer sleeps
  • Set it all to “never”

4. Disable Windows Fast Startup

If you set a wake time in BIOS (more on that in a bit) but it doesn't work, that may be because Windows Fast Startup is turned on. This makes the computer startup faster, but does it by hibernating instead of actually shutting down.

While in Power Options:

  • Select Change What Power Buttons Do
  • Click “Change settings that are currently unavailable”
  • Uncheck “Turn on fast startup”
  • Save changes

On this computer, “Turn on fast startup” was unchecked by default, but that is not always the case.

5. Turn off screen saver

Go to settings and turn the screen saver off.

  • Right click on the desktop and select “personalize”
  • Then search for “screensaver” in the search bar of the personalize window

This computer already had screensaver set to “none” but it's worth checking.

6. Change the desktop background

The desktop will be visible during startup and might be seen if the game crashes. It really breaks the illusion of an arcade or art installation to see the default Windows desktop.

While in personalize, you can:

  • Search for “desktop”
  • Select “Choose your desktop background”

If you have a logo for your project you can set that image. Otherwise, from the dropdown, select “solid color” and pick one. I go with black.

For the DreamBoxXx we set the desktop to be an image that looked like a loading screen, which worked amazingly well!

7. Set the computer time

Make sure the system clock has the correct time. This is important to automate startup/shutdown.

  • Right click the time in the bottom right corner of the taskbar
  • Select “Adjust date/time”

8. Disabling updates

Updates have a good chance at messing up the setup, so it is worth avoiding them. As much as possible, you want this install to be frozen in amber.

I'm trying to avoid this by simply never connecting to the internet, but that will often not be viable.

Poking around online and the consensus seems to be that there is really no way to stop them short of staying offline.

Some possible solutions that I have not tested:

9. Disabling startup processes

  • Press the Windows key and search for “startup”
  • Select “Startup Apps”
  • Disable all of them

If your project requires QuickTime or some other Apple product, set Apple Software Update to never check for updates.

10. Hide the taskbar & widgets

  • Right click the taskbar and select “Taskbar settings”
  • Turn Widgets off
  • Scroll down and expand “Taskbar behaviors”
  • Check “Automatically hide the taskbar”

11. Add shortcuts/aliases to the startup folder

We'll want to have items that run on startup. This means dropping them in the startup folder (which is different from the panel where you disabled the startup processes).

You can open this folder by pressing Win-R for the run prompt and entering shell:startup

If you don't mind doing this every time, it's fine to just memorize that command, but I like to make a shortcut to the startup folder.

With the startup folder open:

  • Go up one level by selecting the enclosing folder, Programs
  • Right click the Startup folder
  • Select “Show more options”
  • Select Send to->Desktop (create shortcut)

screenshot of Windows Finder showing where the Programs folder is relative to Startup

A shortcut is an alias, a link to another file or folder on the computer. It is not a copy.

You can move this shortcut anywhere. It does not need to stay on the desktop.

Double clicking it will open the startup folder, same as entering “shell:startup” on the run prompt.

I'll dive into some AutoHotkey stuff further down, but if you just want your game to launch on startup, you can make a shortcut for the game and drop that shortcut into the startup folder.

  • Go to the exe for your game
  • Right click and select “Show more options”
  • Select Send to –> Desktop (create shortcut)
  • Find the shortcut/alias on the desktop
  • Drag it into the startup folder

This should be enough to launch your game on startup.

12. Bypass the login screen

If you kept the password blank and did not connect to the internet, the computer should skip the login screen. No muss no fuss.

But if you are getting a login screen, here's what I did on Windows 10. It may still work.

By default, you need to enter your password on startup, but this can be turned off. Here are the steps in this guide to bypass this.

  • Search for “netplwiz” in the start bar
  • Uncheck the box labeled “Users must enter a user name and password to use this computer.”
  • Hit OK

If you do not see this checkbox in the netplwiz screen, you may need to disable “Require Windows Hello sign-in for Microsoft Accounts.” You can follow these steps from Tenforums user Faslane to do it pretty easily:

Just open the sign in options at settings/accounts/sign in options and turn the “Require Windows Hello sign-in for Microsoft Accounts” to off, then re-open Netplwiz. Voila! it's back ;–)

Modern Windows 10 PCs are set up with a PIN and not a password. Presumably this is true in Windows 11 as well. You will need to disable the PIN first, otherwise the option to bypass the password won’t show up in netplwiz.

Furthermore, if you don’t have a password OR pin, then the message “Require Windows Hello Sign-In” will be checked ON but greyed out so you can’t change it! So you need to then ADD a pin, uncheck the box, then you will see the checkbox in netplwiz.

Although I have not tested these steps for Windows 11, in favor of never connecting to the internet, they seem to match up with this Windows 11 answer from Tobias Schneider on the Microsoft forums.

13. Preventing the “Let’s Finish Setting Up Your Device” blue screen

Fellow Arcade Commons member Mark Kleeb recommended I add this as it has come up in a bunch of our machines. This screen can be really annoying because it often waits a few days or months before popping up and ruining your installation.

Windows Finish Setting Up Your Device screen

This post on the Microsoft forums has a solution for Windows 10.

The exact options are a little different on Windows 11:

  • Open Settings
  • Select System
  • Select Notifications
  • Scroll down to Additional Settings and select it
  • Uncheck everything

While I was in the Notifications page, I went ahead and unchecked everything because why not.

14. Hide desktop icons

This is optional, but I like to put the various files I need on the desktop. I also don't want it to look cluttered if people see the desktop during startup:

  • Right click anywhere on the desktop
  • Select View
  • Uncheck “Show desktop icons”

You can still view the desktop files by opening a file explorer window.

15. Setting the computer to shut down automatically

For some installations, this will not matter, but we often have the arcade machines living at venues. We can't count on the staff there turning them on and off, so it's good to automate this so the computer is not running 24/7.

Shutting the computer down can be done via the Windows task scheduler. Turning it back on is done via BIOS (we'll get to that in a second).

You can ignore this if you're in an environment when you will be around to turn the computer on and off.

Eva Schindling’s guide has this process with pictures and it is relatively unchanged in Windows 11, but here are the steps:

  • Control Panel
  • System and Security
  • Windows Tools (in Windows 7 and Windows 10 this was “Administrative Tools”)
  • Task Scheduler
  • Action Menu
  • Create Basic Task
  • Add title and description
  • Task Trigger: select Daily
  • Set your shut-down time and recurrence
  • Action: select Start a program
  • Program/script: C:\Windows\System32\shutdown.exe
  • Add arguments: /s
  • Click Finish

Make sure you remembered to set the computer time!

BIOS settings

BIOS is short for Basic Input/Output System and it is the firmware that lives on your computer at a lower level than your operating system.

There's a good chance you've never needed to look at it, but this is where we can do things like ensuring that the computer turns on as soon as it receives power, or having it turn itself on at a given time (as long as it is plugged in).

Every computer has a different BIOS and there are different ways to open up BIOS, but it generally involves holding a function key during startup. Restart the computer, and start holding the key as soon as it shuts down. You generally need to be holding it the moment it starts to enter BIOS.

The specific instructions here are for a GMKtec NucBox because that's what I'm setting up, but the general principle can be applied to any computer.

You may need a wired keyboard to do this. Wireless keyboards may not be connected yet during the startup process. Often it's fine (it was on this computer), but if you're not getting a response, try a wired keyboard.

For this computer I had to hold the delete key during restart. Other computers I've worked on have used F12 or F2. Search for “enter BIOS [brand of computer]” for this info. Some computers will print the key you need to press for BIOS during the startup process, but many don't.

BIOS may be a keyboard-only DOS-looking screen, or it may have a simple GUI (although one that you can probably navigate with just keyboard). Likewise, the exact settings will be a little different, but I'll tell you what to look for.

Make sure to save and exit when you're done. The computer should startup normally after that.

BIOS to turn the computer on at a set time

You are looking for a setting along the lines of “Resume by Alarm” or “Wake Settings.”

For example, on the computer we used for the DreamBoxXx, it was under advanced->S5 RTC Wake Settings.

On my GMKtec NucBox it was under Power->S5 RTC Wake Settings.

Then I set it to “Enabled” and selected “Fixed Time”.

Note: Make sure you disabled Windows Fast Startup earlier or this may not work.

BIOS to turn the computer on when plugged in

It's very handy to not need to press the power button to start the computer. By setting the device to turn on as soon as it receives power, your whole installation can be turned on by flipping on a power strip.

In the BIOS settings, there will also often be an option to have the computer turn on when it is connected to power. The setting is often primarily for what to do after a power outage, but we can use it for our purposes. The name of the setting changes from machine to machine, but it is typically something along the lines of “AC Back On” or “Action When Lost Power” or “Wake on Power”.

For my GMKtec NucBox, it was under Chipset:

Chipset –> PCH-IO –> Wake on Power –> S0 State

You really have to dig to find this option sometimes. Searching the web for “[Computer band] Wake on Power” can help.

AutoHotkey

This part is optional, but using an AutoHotkey script to automate things, like relaunching the game if it crashes, can be useful.

In the case of Salmon Roll, simply putting an alias to the game in the startup folder is not enough because when it launches, there's the little Unity popup where you have to click “Play”.

Salmon Roll launch popup

So instead I made an AutoHotkey script that launches the game, waits a few seconds and then presses ENTER.

It also checks every 10 seconds to see if there is no window matching the window title of the game (which is what will happen if the game crashes or somebody closes it), and it will relaunch the game if that happens.

You can see my script here. Feel free to modify it for your own purposes: https://gist.github.com/andymasteroffish/41a02deb5d2924dadb4e3c005e564b8f

If you do not need it to press ENTER after launching the program, just remove these lines:

        Sleep 5000  ; Wait 5 seconds
        Send {Enter}  ; Press Enter

Full disclosure: I am script-kiddie-level with AutoHotkey and this is mostly clobbered together from old scripts and random things from the internet.

I HIGHLY recommend putting the hotkey to kill the script in writing on the computer. It can be super confusing to try to work on a machine where an AutoHotkey script is running in the background opening apps or taking control of the mouse etc. I always use Win-Z to kill my scripts, but it's up to you. Do not write an AutoHotkey script that has no way to exit.

It is easy to forget to do this and you will regret it.

Once you have your AutoHotkey script, you can put it directly in the startup folder. If you are using AutoHotkey, make sure that you do not also have an alias to the game in the startup folder or it may launch twice.

That's it!

Hopefully that gets your arcade game or art installation running from now until the end of time.

Remember: do not connect to the internet if you don't have to!

And if you are more technically inclined, consider Linux for tasks like this.

If you use this guide, hit me up on Mastodon or Tumblr and show me what you made!

Follow Arcade Commons on Twitter, Instagram or Bluesky to see our games in action!

 
Read more...

from andy

I tend to work on a lot of small projects with a smattering of larger ones mixed in. Often I end the year thinking I didn't do all that much, but then I start going through my posts and realize that I did more than I thought! Hard to believe that stuff I did last January was this year!

I've fallen out of the habit of doing these year-in-review posts, which is a shame, but I'm turning that around right now! Here's a quick look at all the things I made last year.

EMMA

emma

Probably the biggest thing for me is another successful year of the coop I'm a part of, EMMA Technology Cooperative. After a somewhat tough 2023, we bounced back this year. Seeing us survive a lean time and watching the coffers refill in the past year really drove home the value of organizing around cooperation as opposed to infinite growth. And if you're reading this and in need of a creative technologist, please drop us a line!

Taper – January

taper

A the start of this year I was on the editorial board of Taper, online literary journal for computational poetry. I only assisted with one issue (which I also contributed to!) but it was a very new experience for me and I'm happy I could be a part of it!

A few years ago I kind of stumbled into the occasional piece of digital poetry without ever realizing. I was doing a literal poetry reading when I realized that I write poems from time to time. It's very fun that my practice occasionally pulls me in unexpected directions.

MAGFest – January

magfest

Every year, we bring all of the games in the Arcade Commons collection to the Music and Games Festival just outside of Washington DC. It is our biggest show of the year by a mile and I love it every time. There is a DIY spirit to MAGFest that really sets it apart from any other con I've been to. We're well underway for 2025 as I write this. As I frequently do, I emceed a bunch of our tournaments.

The Algorithm – February

the algorithm

What a fun project this was! Alia ElKattan and Lujain Ibrahim contacted me about their Mozilla Foundation-funded project to explain how algorithmic feeds work. I created modular P5JS animations to serve as the “content” for their app.

They found me via the P5 tweetcarts I had been posting during the pandemic. I got to explore some of those same ideas without the super tight character restrictions.

Check it out here.

EMMA Skillshare – March

emma skillshare banner

I did a small workshop on getting PICO-8 to communicate with Javascript. This was the basis for my project Pico Pond which I wrote about on the EMMA Blog.

This skillshare took those ideas and presented them in a livestream, which you can watch here.

Our Generation – May

our generation page

I was contacted by Nick Montfort to submit a piece of computer generated poetry for a small-run book called Our Generation: Programs + Computer-Generated Texts. I channeled my inner Jenny Holzer for this and really enjoyed it. The full src appears at the top of the page and it was interesting to factor that in to make sure the output exactly filled the page. I keep thinking it would be fun to do a series using this size restriction but I haven't done it yet.

You can see my full page here.

Tastebud Tapdancer – May

tastebud tapdancer

You've been swallowed by a giant serpent but you're trying to make the best of it.

A little video game with a source code of just 500 characters! Made for TweetTweetJam 9 You can play it here.

The Indomitable Rocket Dog – June

rocket dog

Around this point in the year is where I started working on my current project, The Indomitable Rocket Dog. I'm enjoying this game so much. I haven't made a real twitchy arcade game since PARTICLE MACE, which just celebrated its 10th birthday. It is still very early but I feel really good about this one!

After MAGFest 2024 I was percolating on physics-y arcade games mostly because I am so deeply in love with Hoverburger by Nick Santaniello. Before I knew it my love of N++ was mixed in there as well.

I'm writing all of my own physics in C++ because I had a vague sense of how I want it to feel and it's a hobby project so I can do what I want.

I've been documenting my development on it in a mastodon thread.

Isabelle Poppy And Bling – August

Isabelle Poppy And Bling

OK, this one isn't new at all! This is a flash game I made in 2009 for musician Justin Braun. But this year I got it playable on the web again!

Like so many game developers my age, I got my start with Macromedia Flash. This game felt like the culmination of my style at the time, which was heavily influenced by Animutation. I was starting to be more deliberate with my collage style as opposed to aiming for totally random elements. I think I got paid $600 for it, which felt huge at the time.

1K Pac-Man – September

1k pacman

Another size coded game. This time it's as faithful a recreation of Pac-Man as I could muster in 1024 bytes of source code. All of the ghost logic is accurate to the original!

You can play it here. I'm very pleased with the mouth animation.

I made it for PICK-1K Jam 2024. I do wish I had read the page a little more carefully though. The jam required compressed source to be 1024 bytes and I did raw source. The game is only 723 compressed bytes so I had a lot of room to expand, but by the time I realized that I had painstakingly trimmed and optimized my code and I couldn't imagine untangling it. Oops.

Cloud Gobblers – November

Cloud Gobblers

Arcade Commons received a grant from the Brooklyn Arts Council to create an arcade machine showing a collection of games that incorporate weather data into the gameplay. EMMA was commissioned to make one of the games. Cloud Gobblers is a snake-style game where the playfield is a video satellite field of global cloud coverage over a period of 48 hours.

This was the first time all of the members of EMMA worked together on one project!

Three Tapestries – October

Three Tapestries

I didn't make many pen plotter drawings this year, but I really liked this one. You can see the full thing here.

It's currently still available in my shop! Until somebody buys it it will be hanging on the wall in my office.

Lever Up Jam – December

Lever Up Jam

Back in 2021, I worked with Matt Lepage on an alt-control game jam called Jam Jam Revolution. We prepped a parts list so all participants could build the same controller and make games for it. This year we decided to dust off that idea with the Lever Up Jam. This time there will be a full-sized cabinet at MAGFest that the games can be played on!

My favorite TweetCarts / Postcarts This Year

I no longer use Twitter, but I have been making the occasional tweetcart (a PICO-8 sketch with source code 280 bytes or less) and posting them on tumblr and mastodon. But there was an exciting development towards the end of the year! Lexaloffle, the creator of Pico-8 created a section on the BBS for 300 char or less “Postcarts”, so we now have a centralized home for these little byte-sized demos.

Here are three of my favorite postcarts that I made this year:

Spinning Cube v3 – April

spinning cube tweetcart

p=pset::_::cls()for i=0,99do
u=0s=i/4+t()/5x=64+sin(s)*39a=64+sin(s+.25)*39y=39+cos(s)*9b=39+cos(s+.25)*9line(x,y,a,b,6)for k=0,99do
if x>a and i<4and k<50then
line(x,y+k,a,b+k,k>48and 7or 9+i)p(x,y+k,7)p(a,b+k)end
v=pget(i,k)u+=v
if(v>6)break
if(u>0)p(i,k,8)end
end
flip()goto _

Big year for making things spin. You can check out my tumblr post to see all my attempts at perfecting this spinning cube. The outlines were the real cherry on top for me.

Balatro Spinning Card – May

spinning card tweetcart

a=abs::_::cls()e=t()for r=0,46do
for p=0,1,.025do
j=sin(e)*20k=cos(e)*5f=1-p
h=a(17-p*34)v=a(23-r)c=1+min(23-v,17-h)%5/3\1*6u=(r-1)/80z=a(p-.2)if(e%1<.5)c=a(r-5)<5and z<u+.03and(r==5or z>u)and 8or 8-sgn(h+v-9)/2
g=r+39pset((64+j)*p+(64-j)*f,(g+k)*p+(g-k)*f,c)end
end
flip()goto _

This was so hard to make! I can't believe I did it. I wrote a giant writeup breaking down the 279 byte source code on the EMMA blog.

Fuji – October

fuji tweetcart

pal({7,12,140,13,129,1,5,8,8,14,142,143,7},1)r=rnd::_::x=r(128)y=r(128)c=9+y/26-2+r(1.5)
if(y<((x+40)/6)^1.6and y<((178-x)/6)^1.6and y<79+r(2))c=2.5-sgn(y-45-sin(x/21)*r(4))*1.5+r(2.5-sgn(x-69-r(8)-y/3+25)*.7)
pset(x,128-y,c)a=r(1)d=r(25)pset(28+sin(a)*d,30+cos(a)*d,9)goto _

I made this during some downtime on a vacation to Japan. I was hoping to see Fuji the next day and the clouds wound up being kind to us, giving an amazing view.

I almost never make representational art and I was really happy with how this turned out.

That's it! I hope you all have a great 2025!

 
Read more...

from andy

I recently made a tweetcart of a spinning playing card inspired by finally playing Balatro, the poker roguelike everybody is talking about.

If you don't know what a tweetcart is, it's a type of size-coding where people write programs for the Pico-8 fantasy console where the source code is 280 characters of less, the length of a tweet.

I'm actually not on twitter any more, but I still like 280 characters as a limitation. I posted it on my mastodon and my tumblr.

Here's the tweetcart I'm writing about today:

tweet_2024-04-27_balatro.gif

And here is the full 279 byte source code for this animation:

a=abs::_::cls()e=t()for r=0,46do
for p=0,1,.025do
j=sin(e)*20k=cos(e)*5f=1-p
h=a(17-p*34)v=a(23-r)c=1+min(23-v,17-h)%5/3\1*6u=(r-1)/80z=a(p-.2)if(e%1<.5)c=a(r-5)<5and z<u+.03and(r==5or z>u)and 8or 8-sgn(h+v-9)/2
g=r+39pset((64+j)*p+(64-j)*f,(g+k)*p+(g-k)*f,c)end
end
flip()goto _

You can copy/paste that code into a blank Pico-8 file to try it yourself. I wrote it on Pico-8 version 0.2.6b.

I'm very pleased with this cart! From a strictly technical perspective I think it's my favorite that I've ever made. There is quite a bit going on to make the fake 3D as well as the design on the front and back of the card. In this post I'll be making the source code more readable as well as explaining some tools that are useful if you are making your own tweetcarts or just want some tricks for game dev and algorithmic art.

Expanding the Code

Tweetcarts tend to look completely impenetrable, but they are often less complex than they seem. The first thing to do when breaking down a tweetcart (which I highly recommend doing!) is to just add carriage returns after each command.

Removing these line breaks is a classic tweetcart method to save characters. Lua, the language used in Pico-8, often does not need a new line if a command does not end in a letter, so we can just remove them. Great for saving space, bad for readability. Here's that same code with some line breaks, spaces and indentation added:

a=abs
::_::
cls()
e=t()
for r=0,46 do
    for p=0,1,.025 do
        j=sin(e)*20
        k=cos(e)*5
        f=1-p
        h=a(17-p*34)
        v=a(23-r)
        c=1+min(23-v,17-h)%5/3\1*6
        u=(r-1)/80
        z=a(p-.2)
        if(e%1<.5)  c= a(r-5) < 5 and z < u+.03 and (r==5 or z>u) and 8 or 8-sgn(h+v-9)/2
        g=r+39
        pset((64+j)*p+(64-j)*f,(g+k)*p+(g-k)*f,c)
    end
end
flip()goto _

Note: the card is 40 pixels wide and 46 pixels tall. Those number will come up a lot. As will 20 (half of 40) and 23 (half of 46).

Full Code with Variables and Comments

Finally, before I get into what each section is doing, here is an annotated version of the same code. In this code, variables have real names and I added comments:

::_::
cls()
time = t()
-- the card has 46 vertical rows
for row = 0,46 do
    -- the card is drawn as individual points moving in a line horizontaly across the card
    -- this is represented as a percentage going from 0 to 1
    for prc = 0,1,.025 do
        -- get the offset of the top of the card as it spins
        -- pnt A will use the positive values and point B will use the negative ones
        x_dist = sin(time)*20
        y_dist = cos(time)*5

        -- both the diamond and the card back make use of knowing how far this pixel is from the center
        horizontal_dist_from_center = abs(17-prc*34)
        vertical_dist_from_center = abs(23-row)

        -- initially set the color assuming that we are on the back side
        -- this will either be 1 (dark blue) or 7 (white)
        color = 1 + min(23-vertical_dist_from_center,17-horizontal_dist_from_center)%5/3\1*6

        -- for the diamond and the little "A" we do a whole lot of calculations
        -- most of this is for the A. The diamond shape really just cares if the combined vertical and horizontal distance is greater than 9
        -- the resulting color will be either 7 (white) or 8 (red)
        slope = (row-1)/80
        dist_from_center = abs(prc-.2)
        if(time %1<.5)  color = abs(row-5) < 5 and dist_from_center < slope+.03 and (row==5 or dist_from_center > slope) and 8 or 8-sgn(horizontal_dist_from_center+vertical_dist_from_center-9)/2
        
        -- now we figure out where to draw the pixel as a percentage between two points defined by x_dist and y_dist
        -- these points are on opposite sides of the circle
        y_pos = row+39
        pset( (64+x_dist)*prc + (64-x_dist)*(1-prc),  (y_pos+y_dist)*prc + (y_pos-y_dist)*(1-prc), color)
    end
end

-- draw everything to the screen and return to ::_::
flip()goto _

This may be all you need to get a sense of how I made this animation, but the rest of this post will be looking at how each section of the code contributes to the final effect. Part of why I wanted to write this post is because I was happy with how many different tools I managed to use in such a small space.

flip() goto_

This pattern shows up in nearly every tweetcart:

::_::
    MOST OF THE CODE
flip()goto _

This has been written about in Pixienop's Tweetcart Basics which I highly recommend for anybody curious about the medium! The quick version is that using goto is shorter than declaring the full draw function that Pico-8 carts usually use.

Two Spinning Points

The card is drawn in rows starting from the top and going to the bottom. Each of these lines is defined by two points that move around a center point in an elliptical orbit.

The center of the top of the card is x=64 (dead center) and y=39 (a sort of arbitrary number that looked nice).

Then I get the distance away from that center that my two points will be using trigonometry.

x_dist = sin(time)*20
y_dist = cos(time)*5

Here are those points:

spinning_points_0.gif

P1 adds x_dist and y_dist to the center point and P2 subtracts those same values.

Those are just the points for the very top row. The outer for loop is the vertical rows. The center x position will be the same each time, but the y position increases with each row like this: y_pos = row+39

Here's how it looks when I draw every 3rd row going down:

spinning_points_lines.gif

It is worth noting that Pico-8 handles sin() and cos() differently than most languages. Usually the input values for these functions are in radians (0 to two pi), but in Pico-8 it goes from 0 to 1. More info on that here. It takes a little getting used to but it is actually very handy. More info in a minute on why I like values between 0 and 1.

Time

In the shorter code, e is my time variable. I tend to use e for this. In my mind it stands for “elapsed time”. In Pico-8 time() returns the current elapsed time in seconds. However, there is a shorter version, t(), which obviously is better for tweetcarts. But because I use the time value a lot, even the 3 characters for t() is often too much, so I store it in the single-letter variable e.

Because it is being used in sine and cosine for this tweetcart, every time e reaches 1, we've reached the end of a cycle. I would have liked to use t()/2 to slow this cart down to be a 2 second animation, but after a lot of fiddling I wound up being one character short. So it goes.

e is used in several places in the code, both to control the angle of the points and to determine which side of the card is facing the camera.

Here you can see how the sine value of e controls the rotation and how we go from showing the front of the card to showing the back when e%1 crosses the threshold of 0.5.

time_graph_0.gif

Drawing and Distorting the Lines

Near the top and bottom of the loop we'll find the code that determines the shape of the card and draws the horizontal lines that make up the card. Here is the loop for drawing a single individual line using the code with expanded variable names:

for prc = 0,1,.025 do
    x_dist = sin(time)*20
    y_dist = cos(time)*5

    ...

    y_pos = row+39
    pset( (64+x_dist)*prc + (64-x_dist)*(1-prc),  (y_pos+y_dist)*prc + (y_pos-y_dist)*(1-prc), color)
end

You might notice that I don't use Pico-8's line function! That's because each line is drawn pixel by pixel.

This tweetcart simulates a 3D object by treating each vertical row of the card as a line of pixels. I generate the points on either side of the card(p1 and p2 in this gif), and then interpolate between those two points. That's why the inner for loop creates a percentage from 0 to 1 instead of pixel positions. The entire card is drawn as individual pixels. I draw them in a line, but the color may change with each one, so they each get their own pset() call.

Here's a gif where I slow down this process to give you a peek at how these lines are being drawn every frame. For each row, I draw many pixels moving across the card between the two endpoints in the row.

draw_exmaple

Here's the loop condition again: for prc = 0,1,.025 do

A step of 0.025 means there are 40 steps (0.025 * 40 = 1.0). That's the exact width of the card! When the card is completely facing the camera head-on, I will need 40 steps to make it across without leaving a gap in the pixels. When the card is skinnier, I'm still drawing all 40 pixels, but many of them will be in the same place. That's fine. The most recently drawn one will take priority.

Getting the actual X and Y position

I said that the position of each pixel is interpolated between the two points, but this line of code may be confusing:

y_pos = row+39
pset( (64+x_dist)*prc + (64-x_dist)*(1-prc),  (y_pos+y_dist)*prc + (y_pos-y_dist)*(1-prc), color)

So let's unpack it a little bit. If you've ever used a Lerp() function in something like Unity you've used this sort of math. The idea is that we get two values (P1 and P2 in the above example), and we move between them such that a value of 0.0 gives us P1 and 1.0 gives us P2.

Here's a full cart that breaks down exactly what this math is doing:

line_draw_demo.gif

::_::
cls()
time = t()/8
for row = 0,46 do
    for prc = 0,1,.025 do
        x_dist = sin(time)*20
        y_dist = cos(time)*5

        color = 9 + row % 3
        
        p1x = 64 + x_dist
        p1y = row+39 + y_dist

        p2x = 64 - x_dist
        p2y = row+39 - y_dist

        x = p2x*prc + p1x*(1-prc)
        y = p2y*prc + p1y*(1-prc)
        pset( x, y, color)
    end
end
flip()goto _

I'm defining P1 and P2 very explicitly (getting an x and y for both), then I get the actual x and y position that I use by multiplying P2 by prc and P1 by (1-prc) and adding the results together.

This is easiest to understand when prc is 0.5, because then we're just taking an average. In school we learn that to average a set of numbers you add them up and then divide by how many you had. We can think of that as (p1+p2) / 2. This is the same as saying p1*0.5 + p2*0.5.

But the second way of writing it lets us take a weighted average if we want. We could say p1*0.75 + p2*0.25. Now the resulting value will be 75% of p1 and 25% of p2. If you laid the two values out on a number line, the result would be just 25% of the way to p2. As long as the two values being multiplied add up to exactly 1.0 you will get a weighted average between P1 and P2.

I can count on prc being a value between 0 and 1, so the inverse is 1.0 - prc. If prc is 0.8 then 1.0-prc is 0.2. Together they add up to 1!

I use this math everywhere in my work. It's a really easy way to move smoothly between values that might otherwise be tricky to work with.

Compressing

I'm using a little over 400 characters in the above example. But in the real cart, the relevant code inside the loops is this:

j=sin(e)*20
k=cos(e)*5
g=r+39
pset((64+j)*p+(64-j)*f,(g+k)*p+(g-k)*f,c)

which can be further condensed by removing the line breaks:

j=sin(e)*20k=cos(e)*5g=r+39pset((64+j)*p+(64-j)*f,(g+k)*p+(g-k)*f,c)

Because P1, P2 and the resulting interpolated positions x and y are never used again, there is no reason to waste chars by storing them in variables. So all of the interpolation is done in the call to pset().

There are a few parts of the calculation that are used more than once and are four characters or more. Those are stored as variables (j, k & g in this code). These variables tend to have the least helpful names because I usually do them right at the end to save a few chars so they wind up with whatever letters I have not used elsewhere.

Spinning & Drawing

Here's that same example, but with a checker pattern and the card spinning. (Keep in mind, in the real tweetcart the card is fully draw every frame and would not spin mid-draw)

draw_example_spinning

This technique allows me to distort the lines because I can specify two points and draw my lines between them. Great for fake 3D! Kind of annoying for actually drawing shapes, because now instead of using the normal Pico-8 drawing tools, I have to calculate the color I want based on the row (a whole number between0 and 46) and the x-prc (a float between 0 and 1).

Drawing the Back

Here's the code that handles drawing the back of the card:

h=a(17-p*34)
v=a(23-r)
c=1+min(23-v,17-h)%5/3\1*6

This is inside the nested for loops, so r is the row and p is a percentage of the way across the horizontal line.

c is the color that we will eventually draw in pset().

h and v are the approximate distance from the center of the card. a was previously assigned as a shorthand for abs() so you can think of those lines like this:

h=abs(17-p*34)
v=abs(23-r)

v is the vertical distance. The card is 46 pixels tall so taking the absolute value of 23-r will give us the distance from the vertical center of the card. (ex: if r is 25, abs(23-r) = 2. and if r is 21, abs(23-r) still equals 2 )

As you can probably guess, h is the horizontal distance from the center. The card is 40 pixels wide, but I opted to shrink it a bit by multiplying p by 34 and subtracting that from half of 34 (17). The cardback just looks better with these lower values, and the diamond looks fine.

The next line, where I define c, is where things get confusing. It's a long line doing some clunky math. The critical thing is that when this line is done, I need c to equal 1 (dark blue) or 7 (white) on the Pico-8 color pallette.

Here's the whole thing: c=1+min(23-v,17-h)%5/3\1*6

Here is that line broken down into much more discrete steps.

c = 1                           --start with a color of 1

low_dist = min(23-v,17-h)       --get the lower inverted distance from center
val = low_dist % 5              --mod 5 to bring it to a repeating range of 0 to 5
val = val / 3                   --divide by 3. value is now 0 to 1.66
val = flr(val)                  --round it down. value is now 0 or 1
val = val * 6                   --multiply by 6. value is now 0 or 6

c += val                        --add value to c, making it 1 or 7

The first thing I do is c=1. That means the entire rest of the line will either add 0 or 6 (bumping the value up to 7). No other outcome is acceptable. min(23-v,17-h)%5/3\1*6 will always evaluate to 0 or 6.

I only want the lower value of h and v. This is what will give it the nice box shape. If you color the points inside a rectangle so that ones that are closer to the center on their X are one color and ones that are closer to the center on their Y are a different color you'll get a pattern with clean diagonal lines running from the center towards the corners like this:

card_back_abs_dist_0

You might think I would just use min(v,h) instead of the longer min(23-v,17-h) in the actual code. I would love to do that, but it results in a pattern that is cool, but doesn't really look like a card back.

card_back_inverted.png

I take the inverted value. Instead of having a v that runs from 0 to 23, I flip it so it runs from 23 to 0. I do the same for h. I take the lower of those two values using min().

Then I use modulo (%) to bring the value to a repeating range of 0 to 5. Then I divide that result by 3 so it is 0 to ~1.66. The exact value doens't matter too much because I am going round it down anyway. What is critical is that it will become 0 or 1 after rounding because then I can multiply it by a specific number without getting any values in between.

Wait? If I'm rounding down, where is flr() in this line: c=1+min(23-v,17-h)%5/3\1*6?

It's not there! That's because there is a sneaky tool in Pico-8. You can use \1 to do the same thing as flr(). This is integer division and it generally saves a 3 characters.

Finally, I multiply the result by 6. If it is 0, we get 0. If it is 1 we get 6. Add it to 1 and we get the color we want!

Here's how it looks with each step in that process turned on or off:

card_back_anim.gif

A Note About Parentheses

When I write tweetcarts I would typically start by writing this type of line like this: c=1+ (((min(23-v,17-h)%5)/3) \1) *6

This way I can figure out if my math makes sense by using parentheses to ensure that my order of operations works. But then I just start deleting them willy nilly to see what I can get away with. Sometimes I'm surprised and I'm able to shave off 2 characters by removing a set of parentheses.

The Face Side

The face side with the diamond and the “A” is a little more complex, but basically works the same way as the back. Each pixel needs to either be white (7) or red (8). When the card is on this side, I'll be overwriting the c value that got defined earlier.

card_front.png

Here's the code that does it (with added white space). This uses the h and v values defined earlier as well as the r and p values from the nested loops.

u=(r-1)/80
z=a(p-.2)
if(e%1<.5)  c= a(r-5) < 5 and z < u+.03 and (r==5 or z>u) and 8 or 8-sgn(h+v-9)/2

Before we piece out what this is doing, we need to talk about the structure for conditional logic in tweetcarts.

The Problem with If Statements

The lone line with the if statement is doing a lot of conditional logic in a very cumbersome way designed to avoid writing out a full if statement.

One of the tricky things with Pico-8 tweetcarts is that the loop and conditional logic of Lua is very character intensive. While most programming language might write an if statement like this:

if (SOMETHING){
    CODE
}

Lua does it like this:

if SOMETHING then
    CODE
end

Using “then” and “end” instead of brackets means we often want to bend over backwards to avoid them when we're trying to save characters.

Luckily, Lua lets you drop “then” and “end” if there is a single command being executed inside the if.

This means we can write

if(e%1 < 0.5) c=5

instead of

if e%1 < 0.5 then c=5 end

This is a huge savings! To take advantage of this, it is often worth doing something in a slightly (or massively) convoluted way if it means we can reduce it to a single line inside the if. This brings us to:

Lua's Weird Ternary Operator

In most programming language there is an inline syntax to return one of two values based on a conditional. It's called the Ternary Operator and in most languages I use it looks like this:

myVar = a>b ? 5 : 10

The value of myVar will be 5 if a is greater than b. Otherwise is will be 10.

Lua has a ternary operator... sort of. You can read more about it here but it looks something like this:

myVar = a>b and 5 or 10

Frankly, I don't understand why this works, but I can confirm that it does.

In this specific instance, I am essentially using it to put another conditional inside my if statement, but by doing it as a single line ternary operation, I'm keeping the whole thing to a single line and saving precious chars.

The Face Broken Out

The conditional for the diamond and the A is a mess to look at. The weird syntax for the ternary operator doesn't help. Neither does the fact that I took out any parentheses that could make sense of it.

Here is the same code rewritten with a cleaner logic flow.

--check time to see if we're on the front half
if e%1 < .5 then

    --this if checks if we're in the A
    u=(r-1)/80
    z=a(p-.2)
    if a(r-5) < 5 and z < u+.03 and (r==5 or z>u) then
        c = 8
    
    --if we're not in the A, set c based on if we're in the diamond
    else
        c = 8-sgn(h+v-9)/2

    end
end

The first thing being checked is the time. As I explained further up, because the input value for sin() in Pico-8 goes from 0 to 1, the midpoint is 0.5. We only draw the front of the card if e%1 is less than 0.5.

After that, we check if this pixel is inside the A on the corner of the card or the diamond. Either way, our color value c gets set to either 7 (white) or 8 (red).

Let's start with diamond because it is easier.

The Diamond

This uses the same h and v values from the back of the card. The reason I chose diamonds for my suit is that they are very easy to calculate if you know the vertical and horizontal distance from a point! In fact, I sometimes use this diamond shape instead of proper circular hit detection in size-coded games.

Let's look at the line: c = 8-sgn(h+v-9)/2

This starts with 8, the red color. Since the only other acceptable color is 7 (white), tha means that sgn(h+v-9)/2 has to evaluate to either 1 or 0.

sgn() returns the sign of a number, meaning -1 if the number is negative or 1 if the number is positive. This is often a convenient way to cut large values down to easy-to-work-with values based on a threshold. That's exactly what I'm doing here!

h+v-9 takes the height from the center plus the horizontal distance from the center and checks if the sum is greater than 9. If it is, sgn(h+v-9) will return 1, otherwise -1. In this formula, 9 is the size of the diamond. A smaller number would result in a smaller diamond since that's the threshold for the distance being used. (note: h+v is NOT the actual distance. It's an approximation that happens to make a nice diamond shape.)

OK, but adding -1 or 1 to 8 gives us 7 or 9 and I need 7 or 8.

That's where /2 comes in. Pico-8 defaults to floating point math, so dividing by 2 will turn my -1 or 1 into -0.5 or 0.5. So this line c = 8-sgn(h+v-9)/2 actually sets c to 7.5 or 8.5. Pico-8 always rounds down when setting colors so a value of 7.5 becomes 7 and 8.5 becomes 8. And now we have white for most of the card, and red in the space inside the diamond!

The A

The A on the top corner of the card was the last thing I added. I finished the spinning card with the card back and the diamond and realized that when I condensed the whole thing, I actually had about 50 characters to spare. Putting a letter on the ace seemed like an obvious choice. I struggled for an evening trying to make it happen before deciding that I just couldn't do it. The next day I took another crack at it and managed to get it in, although a lot of it is pretty ugly! Luckily, in the final version the card is spinning pretty fast and it is harder to notice how lopsided it is.

I mentioned earlier that my method of placing pixels in a line between points is great for deforming planes, but makes a lot of drawing harder. Here's a great example. Instead of just being able to call print("a") or even using 3 calls to line() I had to make a convoluted conditional to check if each pixel is “inside” the A and set it to red if it is.

I'll do my best to explain this code, but it was hammered together with a lot of trial and error. I kept messing with it until I found an acceptable balance between how it looked and how many character it ate up.

Here are the relevant bits again:

u=(r-1)/80
z=a(p-.2)
if a(r-5) < 5 and z < u+.03 and (r==5 or z>u) then
    c = 8

The two variables above the if are just values that get used multiple times. Let's give them slightly better names. While I'm making edits, I'll expand a too since that was just a replacement for abs().

slope = (r-1)/80
dist_from_center = abs(p-.2)
if abs(r-5) < 5 and dist_from_center < slope+.03 and (r==5 or dist_from_center>slope) then
    c = 8

Remember that r is the current row and p is the percentage of the way between the two sides where this pixel falls.

u/slope here is basically how far from the center line of the A the legs are at this row. As r increases, so does slope (but at a much smaller rate). The top of the A is very close to the center, the bottom is further out. I'm subtracting 1 so that when r is 0, slope is negative and will not be drawn. Without this, the A starts on the very topmost line of the card and looks bad.

z/dist_from_center is how far this particular p value is from the center of the A (not the center of the card), measured in percentage (not pixels). The center of the A is 20% of the way across the card. This side of the card starts on the right (0% is all the way right, 100% is all the way left), which is why you see the A 20% away from the right side of the card.

diamond_divide.png

These values are important because the two legs of the A are basically tiny distance checks where the slope for a given r is compared against the dist_from_center. There are 3 checks used to determine if the pixel is part of the A.

if a(r-5) < 5 and z < u+.03 and (r==5 or z>u) then

The first is abs(r-5) < 5. This checks if r is between 1 and 9, the height of my A.

The second is dist_from_center < slope+.03. This is checking if this pixel's x distance from the center of the A is no more than .03 bigger than the current slope value. This is the maximum distance that will be considered “inside” the A. All of this is a percentage, so the center of the A is 0.20 and the slope value will be larger the further down the A we get.

Because I am checking the distance from the center point (the grey line in the image above), this works on either leg of the A. On either side, the pixel can be less than slope+.03 away.

Finally, it checks (r==5 or dist_from_center>slope). If the row is exactly 5, that is the crossbar across the A and should be red. Otherwise, the distance value must be greater than slope (this is the minimum value it can have to be “inside” the A). This also works on both sides thanks to using distance.

Although I am trying to capture 1-pixel-wide lines to draw the shape of the A, I could not think of a cleaner way than doing this bounding check. Ignoring the crossbar on row 5, you can think about the 2nd and 3rd parts of the if statement essentially making sure that dist_from_center fits between slope and a number slightly larger than slope. Something like this:

slope < dist_from_center < slope+0.03

Putting it Together

All of this logic needed to be on a single line to get away with using the short form of the if statement so it got slammed into a single ternary operator. Then I tried removing parentheses one at a time to see what was structurally significant. I wish I could say I was more thoughtful than that but I wasn't. The end result is this beefy line of code:

if(e%1<.5)c=a(r-5)<5and z<u+.03and(r==5or z>u)and 8or 8-sgn(h+v-9)/2

Once we've checked that e (our time value) is in the phase where we show the face, the ternary operator checks if the pixel is inside the A. If it is, c is set to 8 (red). If it isn't, then we set c = 8-sgn(h+v-9)/2, which is the diamond shape described above.

That's It!

Once we've set c the tweetcart uses pset to draw the pixel as described in the section on drawing the lines.

Here's the full code and what it looks like when it runs again. Hopefully now you can pick out more of what's going on!

a=abs::_::cls()e=t()for r=0,46do
for p=0,1,.025do
j=sin(e)*20k=cos(e)*5f=1-p
h=a(17-p*34)v=a(23-r)c=1+min(23-v,17-h)%5/3\1*6u=(r-1)/80z=a(p-.2)if(e%1<.5)c=a(r-5)<5and z<u+.03and(r==5or z>u)and 8or 8-sgn(h+v-9)/2
g=r+39pset((64+j)*p+(64-j)*f,(g+k)*p+(g-k)*f,c)end
end
flip()goto _

tweet_2024-04-27_balatro.gif

I hope this was helpful! I had a lot of fun writing this cart and it was fun to break it down. Maybe you can shave off the one additional character needed to slow it down by using e=t()/2 a bit. If you do, please drop me a line on my mastodon or tumblr!

And if you want to try your hand at something like this, consider submitting something to TweetTweetJam hosted by Andrew Resit which just started! You'll get a luxurious 500 characters to work with!

Links and Resources

There are some very useful posts of tools and tricks for getting into tweetcarts. I'm sure I'm missing many but here are a few that I refer to regularly.

Pixienop's tweetcart basics and tweetcart studies are probably the single best thing to read if you want to learn more.

Trasevol_Dog's Doodle Insights are fascinating, and some of them demonstrate very cool tweetcart techniques.

Optimizing Character Count for Tweetcarts by Eli Piilonen / @2DArray

Guide for Making Tweetcarts by PrincessChooChoo

The official documentation for the hidden P8SCII Control Codes is worth a read. It will let you do wild things like play sound using the print() command.

I have released several size-coded Pico-8 games that have links to heavily annotated code:

And if you want to read more Pico-8 weirdness from me, I wrote a whole post on creating a networked Pico-8 tribute to Frog Chorus.

 
Read more...

from andy

(As you can probably guess, this post assumes some familiarity with the collectible card game Magic: The Gathering)

I just wrapped up a multi-week online Magic: The Gathering tournament that I've been wanting to do fo a while. The 3-Card Blind format is an interesting thought-exercise/meta-game challenge that has been played on forums since at least the early 2000s.

Instead of a normal game of Magic where players bring a deck of cards to face each other, in 3CB players submit a list of exactly 3 cards. All three of these cards start in hand and players do not lose for having no deck to draw from. All of the uncertainty, and even gameplay decisions, from a normal game are removed. The game is reduced to a sort of puzzle (or auto battler) where players try to come up with 3-card combinations that can win the game and even throw off opposing strategies.

some sample 3CB decks

In this post I'm going to talk about the rules of the format, what I did to make my tournament unique, and how you can run your own (including a template spreadsheet I created to make it easy for players to report their scores).

The main focus will be on the game design decisions I made to shape this particular tournament as well as the nuts-and-bolts of how it was run. Check out some of the links below for theory and strategy (which is fascinating in this format).

How It Works

Each week, players secretly submit their deck lists to an email account I made specifically for this purpose. I put their decks into a spreadsheet (preserved here), and at the end of each week, players would mark how they did against each other deck.

Scoring works like this: for each match, players play two games (one where they go first, one where the other player goes first). For each game they win, the player gets 3 points; for each where they tie they get 1 point; and they get 0 points for a loss. So the max number of points a player can get in a match is 6.

There are a few restrictions to keep things on track: * Decks cannot win on turn 1 * Decks cannot force an opponent to discard more than 2 cards * Random effects will always go in opponent's favor

The exact rules for my tournament can be found here on the page I used to introduce and maintain the event.

But how can participants play their games solo? This is the magic of this format: there is no randomness, players have perfect information & only 3 cards. Each game is more of a logic puzzle than a proper game. There is a “correct” sequence of plays, and it is possible to determine which deck comes out ahead (or if both decks stall out and tie). The MTG Fandom page linked below has more detail about edge cases or things that would introduce randomness, but the the take-away is that the skill is in submitting a well-positioned trio of cards, not in actually playing the games.

Example Game

Let's take an example from the last week of my tournament. We have the following 3 decks:

Player A: Thassa's Oracle, Cavern of Souls, Island

Thassa's Oracle, Cavern of Souls, Island

If unopposed, the Oracle deck will win on turn 2 (this was the “house deck” for the week. More on that in a minute). Not surprisingly, combo is weak to counterspells, so Cavern of Souls provides a little extra security.

Player B: Lupine Prototype, Mishra's Workshop, Trinisphere

Lupine Prototype, Mishra's Workshop, Trinisphere

Player B is looking to lock up the opponent with Trinisphere before playing a 5/5 that can end the game in 4 turns.

Player C: Blackmail, Cruel Sadist, Swamp

Blackmail, Cruel Sadist, Swamp

Player C is also playing a control angle, using Blackmail to pluck a critical card from the opponent and then taking their time with Cruel Sadist (which can hopefully get large enough to overcome an opposing blocker that manages to get around Blackmail).

How does it play out?

  • Player A vs Player B – On the play or on the draw, B can get Trinisphere out before A can play Thassa, completely locking them out since their 2 lands cannot produce 3 mana. (6 points for B, 0 points for A)

  • Player A vs Player C – Similarly, C can play Blackmail on turn 1, guaranteeing that they can pluck Thassa from the opponent. (6 points for C, 0 points for A)

  • Player B vs Player C – This one is interesting! When B goes first, they can get their Trinisphere down and prevent Blackmail or Cruel Sadist from being played. When C goes first, they can play blackmail and trash the Lupine Prototype. (3 points for each)

Although it tied in this example, the Blackmail/Cruel Sadist deck was the strongest against the field the week it was submitted. It was able to beat multiple decks that beat the Lupine Prototype deck.

All of these decks take advantage of the unique properties of the format to use cards that would otherwise be underpowered or at least require a lot more hoops to jump through. Thassa's Oracle is certainly powerful in its own right, but usually requires a lot more setup. Blackmail is not usually premium discard, but in this format it can hit every card in the opponent's hand (including lands). Lupine Prototype I had forgotten about entirely, but here it is a reliable 5/5 beater.

Pretty much every link below has other sample matchups that you can check out. These are filled with examples of cards that need to be re-evaluated in the context of 3CB. This is a format that really rewards players who dig for gold in older, mostly forgotten cards.

Resources

Before I dive further into my own event, I want to provide a few links that are helpful for anybody thinking about running a 3CB tournament or who just wants to read about this interesting offshoot of Magic. I certainly would not have been aware of 3CB or able to run my own tournament without them.

MTG Fandom Page – A solid primer and a good reference for the rules.

3-Card Magic by Brian Demars – This article on Channel Fireball was the first time I heard about the format.

Three Card Blind: A Whole Different Format by Goblinboy – A very detailed forum post from 2005 about the format. It's very fun to see the cards from that era evaluated for this format. Does a great job of pointing out cards that are unremarkable in normal Magic, but extremely strong in 3CB and vice versa.

3-Card Blind Metashape Tournament – This was a recent tournament run on Reddit by u/Lognu. My own tournament is heavily based on their setup. That link also has spreadsheets from the game and is a great resource to see decks that people have submitted.

My Tournament's Page – Here's where I laid out the rules for my tournament and maintained the ban-list.

My Tournament Spreadsheet – You can check out my decks submitted each week here! This was where the game took place. Player names have mostly been obscured and all comments have been removed (but they were lively!).

My Tournament

In terms of structure, I was heavily influenced by Lognu's Metashape Tournament. Rather than starting with an extensive ban list (as many 3CB games do), cards from the best performing decks are banned and the list evolves over time. This forces players to come up with new solutions each week.

A lot of the fun of this format is trying to beat the field. Although there are some combinations of cards that are quite strong, there is simply nothing that wins every match. So a huge part of the tournament is trying to figure out the meta and guess which decks other people will play. To this end I introduced one other major gameplay element: the house deck.

Each week, the highest performing deck has two cards banned. Then this deck becomes the house deck. In the following week it will be one of the decks that everybody faces. This is the only deck that players know they will face, so submitting a deck that beats it guarantees 6 points. However, all players know about this can can choose to design their own deck to prey on decks that will beat the house deck. The goal is to provide just enough known info about the next round for savvy players to predict the field. There is also the challenge of forcing players to beat the winning deck without having access to the same cards (since cards in the house deck have been banned).

During the course of the tournament, one player asked if Conspiracy cards could be used. This is a bit of an edge case since they aren't proper cards and are not typically available in constructed play, but I think it's fun when things get weird so I OKed them (with the exception of Power Play, because it undermined the structure of the tournament). I think it would be more than fair to ban them in your own tournament, but I liked the inclusion and they never felt like they broke things.

Ban List

My tournament ran for 5 weeks. In that time the following cards were banned:

Running The Game

Each round was given one week. Here is the cadence I listed in the game description:

  • Results will be announced on Monday, including the updates to the ban list.
  • Players have until Thursday 3PM EST to submit their decks.
  • Players should review their games by Saturday at 8PM.

Decks were submitted by people emailing their lists to a special email account I made for the purpose of this game. This meant that I had to submit my own deck before I officially opened submissions so that I did not see anybody else's deck before locking in my own.

Because players did not see each other's decks, I allowed players to update their decks until the Thursday deadline if they wanted to. I also tried to alert players if I noticed that their deck probably would not play out the way they expected it to, or if it broke some rule and would be disqualified.

On Thursday, a new page was made in the spreadsheet and players could start scoring their decks!

The spreadsheet

Much like Eve Online, this is a game played in spreadsheets.

I based my spreadsheet on the ones that Lognu made. The big difference is that in Lognu's game, players find their row and score themselves going right along each column. The decks are listed on the right, and I figured players know their own deck. So I switched this and had my players find their column and move down, allowing them to easily see the cards they faced in each match.

Here is a a gif I made of Jenny submitting her scores to explain the process to new players. (She has one win against Timmy for 3 points, and no wins against Spike)

Score submission example

You may notice that this system has duplicate information. For example, the result of cell E2 (Spike vs Timmy) can be derived from cell C4 (Timmy vs Spike). Since Timmy got a 0 against Spike, we know that Spike must have gotten a 6 against Timmy. But there is a reason for this: it means that every game has both players computing the results separately. Magic is a complex game and the optimal play is not always obvious. By having both players evaluate the games independently, there is some built in confirmation of the results. (This was further assisted by other players being generally interested. During the game, there was constant commentary on many of the cells as players discussed how games would play out).

Because I hated the thought of scanning the grid to determine if values did not line up, I wrote some formulas in my sheet to check if the values for a given pair of cells did not match. Then I used conditional formatting to highlight those cells in yellow to alert myself and other players that there was some issue here.

Let's take the same example from above. Let's say Spike shows up but thinks they win one game and tie one game against Timmy. grid with yellow highlight

The spreadsheet highlights the cells for that match in yellow. If one cell of a match is 0 (two losses), the other should be 6 (two wins). Alternatively, if one cell of a match is 4 (a win and a tie), the other should be 1 (a loss and a tie). The logic for this is frustratingly complex and I wrote a python script to export the formulas for the 225 cells needed for a 15 player game. If you are curious, you can check out rows 50 to 65 of my template sheet (they're hidden by default but you can expand them if you duplicate the sheet).

If you go through the weeks of my tournament, you'll find a few instances where I had to make a judge call (highlighted in purple), but in at least 90% of cases where the results did not match, players sorted it themselves (often with help from other players weighing in on the result via cell comments). Having the sheet automatically flag disputes did a lot encourage players to reevaluate and fix results. It also provided fun conversation fodder!

It is a credit to the group I was playing with that one of the most frequent disagreement was a player explaining why the other deck would actually beat them.

Week to Week

Dive into the spreadsheet if you want to see all the decks submitted each week. There are some very interesting trends as certain deck types had rises and falls in popularity. What I'm going to present here is the winning deck for each week (which became the house deck for the following week). It's a great microcosm of the strategies at play in 3CB.

Most players asked that I not include their names, but rest assured that none of these winning decks were submitted by me.

Week 1 – Inkmoth Nexus, The Tabernacle at Pendrell Vale, Urborg, Tomb of Yawgmoth Inkmoth Nexus, The Tabernacle at Pendrell Vale, Urborg, Tomb of YYawgmoth

Week 2 – Strip Mine, Chancellor of the Annex, Icatian Store (submitted by NaCl) Strip Mine, Chancellor of the Annex, Icatian Store (submitted by NaCl

Week 3 – Mesmeric Fiend, Leyline of Anticipation, Black Lotus Mesmeric Fiend, Leyline of Anticipation, Black Lotus

Week 4 – Memnite, Snapback, Force of Will Memnite, Snapback, Force of Will

Week 5 – Thassa's Oracle, Cavern of Souls, Island (submitted with only slight variations by 4 players) Thassa's Oracle, Cavern of Souls, Island

Week 6 – Blackmail, Cruel Sadist, Swamp (submitted by NaCl) Blackmail, Cruel Sadist, Swamp

You can see how each deck beats the one before it, but not necessarily the one two weeks prior!

Random Thoughts and Notes

Cell comments were an unexpected delight. I originally planned on including all participants on a group email each week to discuss the round, but players started adding comments directly onto the spreadsheet. It was very lively and became a space to discuss cool decks people had made in addition to sorting out tricky matches.

I do wish I had encouraged people to use burner email accounts and usernames. The comments in the spreadsheet are mostly tied to personal email accounts. It didn't occur to me that I might want to share the results of the game in a post like this, and at that point the comments were too linked to personal information. The spreadsheet I posted here has all of the decks, but the commentary and most of the names have been removed.

After one week of manually linking everybody's cards, I started asking players to submit their decks on a single line with each card name being a link to scryfall. It made the process of prepping the sheet much less time consuming.

If I did it again, I would green light conspiracy cards from the jump, as I thought they were a lot of fun.

In this game, I banned two cards from each winning deck (determined by the order in which they were submitted). This resulted in an interesting but slightly slow-moving ban list. In the future I'd ban all 3 cards in the winning deck and possibly take out one or two from other high scoring decks as well.

The house deck did exactly what I wanted it to do. It guided the field just enough that players could strategize around it.

Running Your Own / Using My Spreadsheet

If you are interested in running your own 3CB tournament, you absolutely should! It's a good time and you only need about 5 regular players to make it compelling.

You can use my or Lgnu's rules directly, but it's a lot of fun to make up your own spin on it as well.

If it is helpful, you can duplicate my spreadsheet template and get my nice conditional formatting.

If you do start a new game with this sheet, I'd love to hear about! Hit me up on twitter, mastodon or email me at andy[at]andymakes[dot]com.

While I have you here...

I'm a real sucker for games where people secretly submit their moves for the turn. I've been working on a wargame called Skirmish Society that was inspired by Diplomacy. It's played via Discord bot and you secretly DM your orders to the bot each turn (while plotting and making deals with other players in the public channel and over DM). I'm close to wrapping it up and would love to have a few more test groups. If that sounds fun to you, you can install it on your Discord for free over here!

 
Read more...

from gwen

I'm gonna start this blog with a softball I happened upon in Stack Overflow today. This snippet will give a user on a linux system password-free sudo permission but for a specific command(s).

In my case I want a little side project to be able to reload nginx's config but I very much do not want a hacky side project running with complete sudo access. This snippet is perfect as I can allow that user sudo access but only for this one task!

Process

Add a line like this to /etc/sudoers or /etc/sudoers.d/{user}

username ALL=(ALL) NOPASSWD: /path/to/command arguments, /path/to/another/command

Now log in as that user either via ssh or sudo su {user} and try your command.

$ systemctl reload nginx

Examples

This will allow the user gwen to reload nginx but nothing else

gwen ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx

And this will allow the user gwen to reload or restart nginx but nothing else

gwen ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx, /usr/bin/systemctl restart nginx

Credit for the solution goes to the original stack overflow post!

#sudo #linux #sysadmin

 
Read more...