#sizecoding
Explore tagged Tumblr posts
Text
Fishin' 500: A Pico-8 Game in 495 characters

Play it here
This is my entry for #TweetTweetJam 10, a game jam where games have a limit of 500 characters of source code and no external assets!
It's a simple fishing game with a final size of 495 characters.
Here's the full Pico-8 source code for the game:
q={}c=circfill r=rnd::z::x=20y=20h=0v=0g=0m=0::_::cls(12)l=line c(65,800,760,1)for i=30,45do l(0,i,75-i,i,4+i%2)end x+=h*g y+=v*g v+=g/9f=.88+sgn(40-y)/9h*=f v*=f if btnp()>0then if(g<1)h=1+r() g=1v-=1.5end if(r()<.03)add(q,{x=-4,y=60+r(60),c=r(5)}) d=4for o in all(q)do a=o.x b=o.y c(a,b,2,8+o.c)l(a-3,b-2,a-3,b+2)pset(a,b-1,1)o.x+=r() if d>abs(a-x)+abs(b-y)then o.x=x+1o.y=y d=0end if(b<40)del(q,o)end m=max(m,y)c(x,y,1,7)l(11,29,20,20)l(x,min(40,y),5)l(x,y) if(m>40and y<40)goto z flip()goto _
You can find a much more thoroughly commented version of this code that explains what's going on here.
Don't forget to check out my entry from last year: Tastebud Tapdancer
4 notes
·
View notes
Text
i am going to Shamelessly advertise my article on my blog because this is My house
additionally where is forthblr i am so disappointed in y'all
3 notes
·
View notes
Text
youtube
#bad apple#c'mon guys watch the lovebyte stream#it has treasures like this#also a very welcoming demo scene community focused on sizecoding#rosszalma.mov#Youtube
5 notes
·
View notes
Text
#codetober 2023. 10. 09.
Little progress sadly, had a busy day.
Tried the graphics pipeline and of course it didn't work for the first time. Some googling yielded the solution: have to force the usage of vulkan rendering.
Now this out of the way I have the starting point to actually visually observe what the code is doing with the entities.
What kind of stuff do you like to program?
Mostly I'm interested in simulating things. Physics bodies, agents in an environment, particles, you get the idea.
Also I am interested in game development but have little experience in it.
Sizecoding is also a thing but not really a theme rather a format.
4 notes
·
View notes
Text
Mini Short Sleeve T-Shirt, Uniqlo https://www.uniqlo.com/ph/en/products/E465760-000?colorCode=COL00&sizeCode=SMA004&gad_source=1&gclid=Cj0KCQiA_Yq-BhC9ARIsAA6fbAiWPJ69kViTwd06wSUSbrYjuzsz6BqjX7Rbu24GTLRqXaV-l8qezOYaAuwVEALw_wcB
0 notes
Text
I've spent the last 22 years or so deep in assembly, sizecoding, and kernel dev; it would take me at least an hour of fucking around to be able to successfully hit write and exit cleanly. 10/10 effort IMO!
@k1nky-r0b0t-g1rl Hey, I have this file for you to download. I'm definitely not making a bot net. Trust me.
643 notes
·
View notes
Note
shdjflshfklasdhfakfh was it these?? uniqlo.com/sg/en/products/E456493-000?colorCode=COL22&sizeCode=SMA002 (also, I love how southeast asians will just be like "uniqlo 💖✌️"
CRYING. YES THESE WERE THE ONES MY DAD ALMOST BOUGHT THEM
2 notes
·
View notes
Text
I'll try to do something different
Hi. I'm Jori, also known as jobe. You might know me from the demoscene releases I've made for the PICO-8 and TIC-80 fantasy platforms. That all started from a desire to do something different. I had a PICO-8 license to create music for other coders and their demos, but those demos never really materialized. Which, let me just state outright, I have no hard feelings about. Creating things is hard, creating things and balancing that with other things going on in your life is very hard, so I fully understand that hobby projects don't always get finished. I know mine don't.
But I had this tool, and I wanted to work on demos, and since no one else would make my demos for me, why not make a demo myself? How hard can that be?
Long story short, surprisingly hard in some aspects, surprisingly easy in others, and in the end, I had created something people seemed to like.
youtube
A couple of things that people seemed to like.
But doing the same thing over and over again can put you in a bit of a rut, even if you're getting better at the thing you're doing. I keep thinking it would be fun to make another PICO-8 demo, but after Puroresu no Seishin, I don't know where I would go from that. I feel like I've done everything I want to do on that platform.
I looked at another platform. TIC-80 was the logical next step, as it's also a fantasy platform, also Lua based, but with less technical restrictions. Less of a challenge, but that also meant I could do things that weren't feasible within PICO-8's virtual CPU cycle limit.
There's another difference. The cart format. There are two ways to release a PICO-8 cart - an uncompressed text file with the source code, or a .p8.png file that contains a label image. A TIC-80 cart, however, doesn't necessarily contain anything else than compressed source code. A seemingly minor detail but one that has huge implications for sizecoding. It's not impossible to do a tiny production on the PICO-8, as this 256 byte intro by Marquee Design illustrates, but as you can see from this TIC-80 256 byte intro also by Marquee Design, there's a world of difference between 256 bytes of compressed an uncompressed code.
Last September, I made my first 256 byte intro. I had no idea I would enjoy sizecoding as much as I do. It's a different kind of challenge from making a demo, as you hit the wall almost immediately and have to start thinking of the size limit from the first character of code you write. It's also much less of an undertaking than a full demo project - a way to quickly express one audiovisual idea and be done with it. I'll be doing more sizecoding later for sure. Lovebyte, an online demoscene event, is taking place a month from now and I'll definitely try to get something done for the event.
There's one other thing I hadn't done before 2021. I hadn't made a game.
That's not entirely true, of course. After all, I did work in game development for six years. The problem with the auteur theory is that attributing the whole creative process to a Ken Levine or a Hidetaka Miyazaki does a massive disservice to everyone else involved in creating a Bioshock or a Dark Souls.

Having both contributed to demos as team efforts and created them all by myself, however, to me the solo efforts have felt more like "my" demos. And of course, there are games like Undertale and Axiom Verge where the entirety or overwhelming majority of the game is created by a single developer, resulting in a highly personal expression of that developer's vision.
I finished "my" first game about a month ago. Deckpest, a combination of a deckbuilder and a Tempest-style arcade shooter, made for the TIC-80. A course work for school, Deckpest is almost entirely a solo creation - another person on the course made the music, while I made the graphics and designed and coded the game. The purpose of the course was to learn project management, and it was very successful at that. I don't know if I could have finished the project if I didn't adopt Kanban.
The scope of Deckpest was limited by the strict course deadline, but it was also limited by the platform. While there are no CPU cycle or token amount restrictions, TIC-80 still restricts you to 64 kilobytes of written code. Currently, Deckpest is at 63629 bytes. I managed this without any size optimization, but if I wanted to expand the game any more, I would have to find ways to cram more code into the cart, probably sacrificing readability and maintainability.
But I enjoy the core gameplay loop of Deckpest so much that I feel like I could do something more with it. Make it bigger.
So I'm taking it to LÖVE.
LÖVE is a free and open source game development framework. It's the perfect choice for building upon Deckpest. It's designed for 2D games, so it's very suitable for a 2.5D game like Deckpest. It uses Lua, so I've actually been able to port some of the code from Deckpest almost directly. It's multi-platform, and while I'm not making any promises (I don't have a Mac to test things on so getting the shaders to work on Mac OS X might be a bit difficult), I'll at least try to bring it to other platforms than just Windows.
It's going to be a big project, one that will have to take lower priority to all kinds of adult responsibilities. The full scope of the thing is a bit nebulous this point. My uneducated estimate is that I'll have version 1.0 out sometime near the end of 2023, and until then, it's going to be my main creative outlet. So, don't expect any demos from me any time soon. I won't be abandoning demoscene activities completely, as sizecoding and live coding are great, less time consuming ways for me to express myself, but I won't be committing myself to creating a full demo for now.
Why, then, am I writing this instead of working on the game? Because for me, creating is communicating, and I don't see why creating a game would be any different from creating a demo. However, I am still far from having any kind of playable preview I could comfortably hand to more than very few people. I feel that if I blog about my process, it will feel more like I'm communicating. It will feel less lonely.
So, if you're still reading this, thank you for letting me communicate to you. See you in the next post!
1 note
·
View note
Link
SizeCoding.org is a wiki dedicated to the art of creating very tiny programs for the 80x86 family of CPUs. By "very tiny programs", we mean programs that are 256 bytes or less in size, typically created by members of the demoscene as a show of programming skill. The size of these tiny programs is me... https://ift.tt/2br02O2
0 notes
Text
that's not bad yeah! makes sense that it would take a bit of back and forth to get the model to spit something out but it's a very creditable attempt... by which I mean if I saw a beginner make this during the demo workshop I'd tell them like nice work (and maybe suggest trying out multi octave noise, different shading models etc.). if it was entered into a compo nobody would give a shit lol but I didn't expect originality here. here's what a real 4k intro can look like at the absolute high end:
youtube
youtube
asking a LLM for a 4k demo specifically is kind of brutal really, getting the executable that small requires real specialist knowledge to compress and unpack the demo to beyond writing a functional opengl code. something I don't really know enough personally to evaluate yet, but I'm looking forward to learning more. (the sizecoding wiki is a good resource).
I think it's reasonable to describe what LLMs do as interpolating within the space of examples provided to them, with a certain amount of reasoning capability to modify and make it more specific., e.g. it's able to figure out the right place to make modifications to the code. so to begin with it's able to spit out something pretty close to the various OpenGL examples that exist on the internet. and it's learned that an ocean can be modelled with sine waves, and is typically modelled with a grid mesh. but while they can generate text describing various other techniques (e.g. Gerstner waves), they seem to have a hard time actually applying that 'knowledge'.
crucially, they don't have the human feedback loop between sensory input and the internal model: we can look at a demo and think 'I like where this is going' or 'this doesn't really look much like an ocean', but that is not a functionality the LLM is capable of executing. (not sure if the increased ability of models to interpret image data may make it possible one day.)
one oddity with my experiments was DeepSeek talking (in its <think> block) about how it should research certain things. so far as I know, DeepSeek does not have much ability to 'research' anything - maybe the full model can do web searches but how far can it actually interpret and act on that information? that just seems to be roleplaying a human trying to solve a problem. but maybe I need to play more with the full version of deepseek rather than these small models.
can an LLM write a demo?
ongoing LLM probing efforts: I tried giving them a challenge to "write code for a 4k demo to render an ocean scene".
note, in demoscene parlance, a '4k demo' refers to a demo that fits in 4 kilobytes, not one that renders to a 4k monitor. this is a stupidly difficult high-context problem and I didn't expect to really get perfect output. well, shocker, the output was largely not all that impressive in human terms.
Here's the best result I was able to get after a fairly extended dialogue with DeepSeek R1 70b, a 300kb demo using opengl:
many wave, very ocean
I'm kind of wondering why I did this at this point, but I think the main reason was that I started to buy a bit of the hype and wanted to reassure myself that LLMs are still a bit daft?
first I tried two LLMs on lmarena.ai but the site bugged out when I rated them rather than tell me which bots I was talking to.
Both generated what looked like a valid OpenGL program (though I did not attempt to compile either), however, looking closer the output was flawed in various ways. The left one decided to do some limited raytracing in the fragment shader rather than displace a mesh. It claimed to be using Gerstner waves, which would be cool, but a closer look at the output showed it was actually just sines. I'm also not sure quite what it thinks it's doing with the projection - it just seems to take the fragment position as if it were the 3D position.
The second AI does better, generating a plausible-looking vertex and fragment shader file with sine-based vertex displacement. There are some oddities, though, like the fact that it doesn't actually use the generated vertex and fragment shaders as external files, writing them out again as strings in the actual program. Overall, I could believe that if I compiled this it would look like a basic sinusoidal ocean with Phong shading. Old-school but reasonable. Unfortunately I closed the tab so I can't actually test it anymore.
Curious about what might be going on inside these models, I tried asking DeepSeek R1:14b the same challenge. Predictably this smaller model did worse. Its chain of thought prompting gave it a pretty coherent description of how you would write a demo like this, but also revealed some interesting confusions, for example multiple times referring to 'example code' that didn't exist, or quoting things I didn't say ('the user mentioned OpenGL and Vulkan').
When it came to output, though, it only gave me a list of steps to follow and omitted actual code:
There is no 'detailed response provided'.
After issuing some clarifications, DeepSeek R1:14b came up with the idea of creating a text-based demo instead, and generated some plausible-looking code in C++. I figured I might actually compile this, but it used a header file conio.h without explanation. Asking it to clarify led to it figuring out this is an old Windows header, replace it with standard library code, and actually spontaneously add a conditional compilation check for a Windows/Linux difference.
I tried compiling the provided code and ran into some missing libraries. A little coaxing gave a lot of blather to tell me 'you need to #include <cmath>'. A little more coaxing got it to tell me what compiler flags would be needed.
Thus I can present to you Deepseek R1:14b's demo:
Beautiful. Sure to win first place. The 'press q to quit' thing doesn't work. And the compiled binary definitely doesn't fit in 4kb (though it might if I stripped it etc.). But... it tried?
For fairness sake, I'll flood my RAM to try the 70b version as well. To its credit, its 'think' block immediately understands what a '4k demo' is supposed to be. Unfortunately it then goes off the rails and decides to do it in pygame, which is... babe you ain't gonna make a 4k demo in pygame lmao. As the output continued, it forgot that 4k referred to binary size rather than resolution, resolving to test the pygame program which is... not something an LLM can do.
Curiously (and this is something I have noticed a couple of times with DeepSeek), the 'actual' answer after the <think> block basically ignored all that Python stuff and wrote me a basic 'hello triangle' OpenGL program in C. So what was the point of all that thinking? Apparently when it maps from the 'think' LLM path to the 'final output' LLM path, DeepSeek can just... ignore what it was thinking about? The shaders it generated were pretty limited, it basically generates one big triangle over the screen with a scrolling sine wave on it, but I decided to see if it would compile anyway.
I tried asking it for advice on setting up GLFW and GLEW with MinGW and its answer was mostly quite good... but garbled some details (suggesting inconsistent places in where to put the libraries), which luckily I know enough to be able to spot. In the end we get this as the resulting demo:
I've lowered my expectations a lot by this point, but I will give DeepSeek a lot of credit for helping me get a working MinGW/OpenGL build environment. Given that it's a long time since I've fucked about with C/C++, and there's nothing so nice as cargo in this ecosystem, it was a lot faster than figuring it out from the docs.
The executable was more like 400kb than 4kb, so I thought I'd see if I could coax DeepSeek R1-70b to make it smaller. The chain of thought generated here was a genuinely solid discussion of sizecoding techniques, but the real proof would be whether DeepSeek could apply the ideas it pulled out concretely. In the end it gave me a list of ideas to try, including a couple of compiler flags - with this I shaved off 100kb, but it's still far too large.
(Ironically it suggested using "minimalistic frameworks often found in demoscene communities".)
I think I've spent as much time investigating this as I want to. Overall, DeepSeek R1 70b did a pretty good job of understanding what I wanted and generating relevant output, and tbh I could definitely imagine a LLM being useful if I needed to quickly reference info while writing a demo, but evaluated on the original question of 'can this LLM write a 4k demo depicting an ocean scene', the answer is a pretty emphatic no.
Running this on my computer, this took ages to generate the full output token by token - the full interaction ended up taking a couple of hours. But if I did this from scratch, having to look up docs and everything with zero experience with the APIs, I think it would probably take me about the same time to get a working OpenGL program.
Could the 'full size' models do better? Quite probably, but I ain't spending money on this shit.
24 notes
·
View notes
Text
Only cause I worked so hard on it LOL 🔴 - Reasonable Stuff LOL (White - XS) https://www.victoriassecret.com/bras/shop-all-bras-mobile/scalloped-crochet-bralette-the-victorias-secret-bralette-collection?ProductID=314415&CatalogueType=OLS (Black - XS) https://www.victoriassecret.com/lingerie/shop-all-lingerie-mobile/long-sleeve-plunge-teddy-beautiful-by-victorias-secret?ProductID=312574&CatalogueType=OLS (Black - XS) https://www.victoriassecret.com/lingerie/shop-all-lingerie-mobile/lace-plunge-teddy-dream-angels?ProductID=309148&CatalogueType=OLS (Candy Apple - Short, XS) https://www.victoriassecret.com/sleepwear/shop-all-sleep-mobile/the-sleepover-knit-pajama?ProductID=302510&CatalogueType=OLS (Black - XS) https://www.victoriassecret.com/pink/all-apparel/reversible-bomber-jacket-pink?ProductID=313417&CatalogueType=OLS (Black) https://www.victoriassecret.com/pink/all-active-pink/metal-water-bottle-pink?ProductID=314712&CatalogueType=OLS (Soft Begonia) https://www.victoriassecret.com/pink/shop-all-accessories/wheelie-bag-pink?ProductID=314901&CatalogueType=OLS 🔴 (Tan - 7) http://m.dsw.com/shop/product/362443/?colorCode=001&sizeCode=7.0&widthCode=M (Either color - S) http://www.target.com/p/-/A-51308131 (Grey - S) https://m.hollisterco.com/shop/us/girls-gift-guide-6898233/off-the-shoulder-ribbed-sweater-8295730_03 (Perfection or Drama, I already have Adored) https://www.victoriassecret.com/beauty/lip/velvet-matte-cream-lip-stain-victorias-secret?ProductID=311968&CatalogueType=
0 notes
Text
Balatro-Inspired Spinning Card Tweetcart Breakdown
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:
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 _
This post is available with much nicer formatting on the EMMA blog. You can read it here.
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:
[editor's note. this one came out terribly on tumblr. Please read the post on my other blog to see it]
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:
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:
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.
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.
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:
::_:: 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)
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:
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.
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:
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.
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.
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 _
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 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:
Pico-Mace
Cold Sun Surf
1k Jump
Hand Cram
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.
19 notes
·
View notes
Text
youtube
Happy Valentine's Day, nerds!
Lovebyte is a sizecoding party / community / competition which takes place (or time really) every year around V-day.
The community gathered around the sizecoding event is wholesome and welcoming, especially towards newcomers to the demo scene.
The intro itself is a DOS (.com) executable of size 127 bytes, nicely fit for the 128 byte intro highend competition.
Thanks for all the help and support for my beautiful and talented (and patient :D) wife: @pidzson
14 notes
·
View notes
Text
stary sky / diamond eyes 280 bytes
(there's room to further minimize but I'm happy to keep it halfway readable)
::❖:: cls() p=t()%1 for c=0,9do for r=0,9do x=c*16-32*p y=r*16-32*p+(c%2)*8 pset(x+8,y-p*16,5) m=min(7+sin(y/(70+sin(t()/3)*30))*3,(64-abs(x-64))/4)\1 for d=1,m do line() for i=0,1,.25do line(x+cos(i)*d,y+sin(i)*d,d<m and 7+(d/2)%2or 2+(m/8)\1*10) end end end end flip()goto ❖
14 notes
·
View notes
Text
Seneca 279 bytes
h=128::_::?'⁶1⁶c' k=0for i=1,50,7do for s=0,6,i/220do k+=1z=h-i p=-(t()/(2+i/15)+s)%6x=p<2and i or z a=p<1and i or z y=p<1and h or i b=p<2and i or h c=p>1 and p<2 and i==8and 12or 6 if(p<3)p=p%1r=1-p?("cruelty springs from weakness ")[31-k%31],r*x+p*a-1,r*y+p*b,c end end goto _
7 notes
·
View notes
Text
Finally started playing that poker rougelike! If you got mad about the source code of that game, wait until you see how I'm rendering a playing card.
279 bytes
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 _
8 notes
·
View notes