jasondelaat [Lexaloffle Blog Feed]https://www.lexaloffle.com/bbs/?uid=54967 Animation Operators <p>Code for combining simple animations together to create more complex animations using + and * operators. </p> <p> <table><tr><td> <a href="/bbs/?pid=134087#p"> <img src="/bbs/thumbs/pico8_animation_operators-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=134087#p"> animation_operators</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=134087#p"> [Click to Play]</a> </td></tr></table> </p> <p>The example uses four simple animations&mdash;each one just a different coloured circle starting in a different corner and moving to the diagonally opposite corner&mdash;and combines them in different ways to create the final animation. </p> <p>For simple animations A and B:</p> <ul> <li>The + operators first runs A and, once it's finished, runs B.</li> <li>The * operator runs both A and B at the same time.</li> </ul> <p>As with normal addition and multiplication you can string together as many animations as you want and use parentheses to indicate a particular ordering.</p> <p>To create animations use <code>new_animation()</code> and then give the animation object an <code>init</code> method. The <code>init</code> method should return a table containing a <code>draw</code> and an <code>update</code> function; <code>update</code> should return true when the animation has finished.</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> a = new_animation() a.init = function() local offset = 0 return { update=function() offset += 5 if offset &gt; 128 then return true end end, draw=function() circfill(offset, offset, 2, 7) end } end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The animation object then needs to be instantiated to use it.</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> anim = a() -- or a.init(), same thing function _update() anim.update() end function _draw() cls() anim.draw() end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Having the <code>init</code> function means you can use the same animation multiple times since any internal values, like <code>offset</code>, will be re-initialized each time. You don't need to initialize the animation each time when combining animations, that's handled internally, you only need to initialize the combined animation as a whole.</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- creates a compound animation which will play the simple animation -- three times in a row. anim = (a + a + a)() -- or (a + a + a).init(). Again, same thing.</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Lua Code (indented 3 spaces)<br /> <div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"></p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>do local ani_meta = { __add=function(self, other) local an = new_animation() an.init = function() local children = {self(), other()} local i = 1 return { update=function() local a = children[i] if a.update() then i += 1 if i &gt; 2 then return true end end end, draw=function() local a = children[i] a.draw() end } end return an end, __mul=function(self, other) local an = new_animation() an.init = function() local children = {self(), other()} return { update=function() local done = true for a in all(children) do local d = a.update() done = done and d end return done end, draw=function() for a in all(children) do a.draw() end end } end return an end, __call=function(self) return self.init() end, } function new_animation() return setmetatable({}, ani_meta) end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p></div></div></div></p> <p>Lua Code (indented 1 space)<br /> <div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"></p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>do local ani_meta = { __add=function(self, other) local an = new_animation() an.init = function() local children = {self(), other()} local i = 1 return { update=function() local a = children[i] if a.update() then i += 1 if i &gt; 2 then return true end end end, draw=function() local a = children[i] a.draw() end } end return an end, __mul=function(self, other) local an = new_animation() an.init = function() local children = {self(), other()} return { update=function() local done = true for a in all(children) do local d = a.update() done = done and d end return done end, draw=function() for a in all(children) do a.draw() end end } end return an end, __call=function(self) return self.init() end, } function new_animation() return setmetatable({}, ani_meta) end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p></div></div></div></p> https://www.lexaloffle.com/bbs/?tid=54044 https://www.lexaloffle.com/bbs/?tid=54044 Sat, 09 Sep 2023 11:10:38 UTC Two Minor BBS UI annoyances <p>If I view a post on the BBS and scroll down to the bottom to post a comment, there are two buttons: Submit, on the left and Preview, on the right.</p> <p>If I click Preview I'm taken to a new screen. The first issue is that I've just clicked a Preview button but I'm not shown a preview just a larger text box. To actually see the preview I have to scroll down and click the Preview button again.</p> <p>Which brings me to the second issue: The Preview button is now on the left and the submit button, now called Publish Changes, is on the right. I would expect the buttons to still be in the same place and the number of times I've almost posted half a comment because I almost clicked Publish when what I wanted was Preview are many.</p> <p>Minor issues. Not the end of the world if it never gets fixed&mdash;at this point fixing it might cause as many or even more mistakes than not fixing it so maybe it's just not worth doing&mdash;but thought I'd point it out anyway.</p> https://www.lexaloffle.com/bbs/?tid=53854 https://www.lexaloffle.com/bbs/?tid=53854 Wed, 23 Aug 2023 12:18:19 UTC A very long walk in the woods <p>This is just something I've been playing with in between working on other things.</p> <p> <table><tr><td> <a href="/bbs/?pid=130640#p"> <img src="/bbs/thumbs/pico8_walkinthewoods-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=130640#p"> walkinthewoods</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=130640#p"> [Click to Play]</a> </td></tr></table> </p> <p>I didn't know about sprite stacking until <a href="https://www.lexaloffle.com/bbs/?tid=39014">this old post</a> popped up a couple of months ago. Although that example rotates the sprites themselves it turns out you can get some interesting results even without it. I haven't bothered with collision detection so you can walk right through the trees and the rocks. Waking through the water was intentional though. I wanted it to look like actually walking in water.</p> <p>The space is procedurally generated but persistent so it's the same every time the cart is run. And it's effectively infinite in all directions. Not <em>actually</em> infinite obviously but pretty darn big for no particularly good reason.</p> https://www.lexaloffle.com/bbs/?tid=53003 https://www.lexaloffle.com/bbs/?tid=53003 Wed, 07 Jun 2023 12:01:51 UTC Drafting Table <p>This cart uses devkit mode so you'll need a mouse and keyboard to use it.</p> <p> <table><tr><td> <a href="/bbs/?pid=129079#p"> <img src="/bbs/thumbs/pico8_drafting_table-4.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=129079#p"> drafting_table</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=129079#p"> [Click to Play]</a> </td></tr></table> <br /> <img style="margin-bottom:16px" border=0 src="/media/54967/47_main_0.png" alt="" /><img style="margin-bottom:16px" border=0 src="/media/54967/main_1.png" alt="" /></p> <p>Update:<br /> <div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> New version updates the s-expression parser and the importer code. The old parser was incredibly slow; the new one is much faster. And the importer now properly handles larger images that don't compress as much which it handled incorrectly before.<br /> </div></div></div></p> <p>This started off as a level editor for the <a href="https://www.lexaloffle.com/bbs/?tid=52384">Space Taxi Remake</a> I'm working on. At some point I realized I was going a bit overboard and instead of doing the sensible thing and saying, &quot;well that's good enough for my purposes I guess I'll stop now,&quot; I just kinda leaned into it.</p> <p>Obviously any images you make with this are yours to do with as you please. If you use it and like it credit is, of course, appreciated but not required.</p> <p>I've posted a new comment in the <a href="https://www.lexaloffle.com/bbs/?tid=52384">Space Taxi</a> thread with a few very rough screens made with this if anyone wants to check that out.</p> <h3>Saving and Loading</h3> <p>Saving doesn't work from the BBS embedded player or on Education edition so you'll need to run the cart locally to do that. Files are saved as .p8l files. They're just plain text with a lisp-like syntax describing the points and shapes, etc. of the drawing. </p> <p>By default files save to the desktop and are overwritten if they have the same name. You can change this behaviour by modifying the variables <code>overwrite</code> and <code>save_to_desktop</code> at the very top of the code (Tab 0.)</p> <p>Loading is done via dragging the file into the program and that does work on the BBS player! <a href="https://www.lexaloffle.com/bbs/files/54967/drafting.txt">This sample save file</a> loads the drawing of the words &quot;Drafting Table&quot; as seen on the cart image. Just save it locally as a text file then drag it in to load.</p> <p>You can load more than one file and it'll just keep adding in the objects from each. This is so you can have, for instance, a file with a bunch of different platform types and one with different obstacles, etc. and combine them without having to redraw them every time.</p> <h3>Exporting and Importing</h3> <p>To compress and export (to the clipboard) the image as a string of binary data press the button at the bottom right of the group in the center or press '8'. <table><tr><td width=32> <img src="https://www.lexaloffle.com/bbs/gfxc/54967_28.png" width=32 height=32> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_54967_28"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/54967_28.txt", function (retdata){ var el = document.getElementById("gfxcode_54967_28"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [8x8]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_54967_28" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>To import the image into your own cart, paste the binary string into the cart and copy and uncomment the code from tab 0 of this cart. There are only three functions you need to know about:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>store_image(dest, str) converts the binary data string 'str' to bytes and stores them in memory beginning at memory address 'dest'. This data is still compressed and not the actual image. Returns the memory address immediately after the data which you can use to store additional images or other data. next = store_image(0x8000, img1) -- where my img1 is a string exported by Drafting Table store_image(next, img2) -- as is img2. load_image_from_memory(dest, src) used in conjunction with 'store_image'. Reads the compressed data from memory address 'src' and writes the decompressed images data to memory address 'dest'. This can then be drawn to the screen with 'memcpy'. load_image_from_memory(0x1000, 0x8000) memcpy(0x6000, 0x1000, 0x2000) -- draw the image to the screen load_image_from_string(dest, str) you can instead decompress the image directly from 'str' to memory address 'dest' without storing the compressed data to memory. load_image_from_string(0x1000, img1) memcpy(0x6000, 0x1000, 0x2000)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p><img style="margin-bottom:16px" border=0 src="/media/54967/drafting p8_0.png" alt="" /><img style="margin-bottom:16px" border=0 src="/media/54967/main_1.gif" alt="" /><br /> The rotation tool in action and &quot;Drafting Table&quot; exported as collision demo &quot;level geometry&quot; for my Space Taxi remake.</p> <h3>Some caveats</h3> <h4>Available drawing area</h4> <p>The UI at the bottom of Drafting Table and the UI at the bottom of Space Taxi are the same size leaving me exactly enough drawing space for a single level and that's all that gets exported. If anyone thinks that they'd find this useful it shouldn't be too hard to add a shortcut to hide the UI and allow editing and exporting the full screen. Unless/until someone asks though it will stay as it is because it suits my purpose.</p> <h4>Positive and negative rotations</h4> <p>When using numerical input for rotations I've gone with the convention in mathematics: positive angles rotate <em>counterclockwise</em> and negative angles rotate <em>clockwise.</em> Additionally, angles are given as a number between 0 and 1 as in Pico-8 itself. So 0.25 is a 90 degree rotation counterclockwise while -0.125 is a 45 degree rotation clockwise.</p> <p>All objects rotate around their own center point even if multiple objects are selected at once unless grouped together with 'g' in which case all grouped objects rotate as a unit around their common center point.</p> <h4>Moving objects ignores snap</h4> <p>This is entirely because my sleep deprived brain was having trouble getting it to work. It'll be fixed eventually. For now the workaround is to just get it in approximately the right spot then use the arrow keys to place it precisely.</p> <h4>Polygons are a bit weird</h4> <p>Due to the way I'm drawing/filling/detecting collisions with the various shapes, the free polygon tool sometimes gives unexpected results like not filling the whole thing or, sometimes, making it completely un-selectable except via the 'select all' shortcut. This only happens when the polygon in <em>not</em> a <a href="https://en.wikipedia.org/wiki/Convex_polygon">convex polygon</a>.</p> <p><img style="margin-bottom:16px" border=0 src="/media/54967/main_2.png" alt="" /><img style="margin-bottom:16px" border=0 src="/media/54967/main_3.png" alt="" /><br /> The &quot;same&quot; non-convex polygon in outline and fill mode.</p> <p>So to make sure you're getting what you expect, stick to convex polygons (all interior angles less than 180 degrees) and for complex shapes, draw separate parts and group them together. </p> <p><img style="margin-bottom:16px" border=0 src="/media/54967/main_4.png" alt="" /><img style="margin-bottom:16px" border=0 src="/media/54967/main_5.png" alt="" /><br /> Same shape as above but constructed from two shapes grouped together.</p> https://www.lexaloffle.com/bbs/?tid=52529 https://www.lexaloffle.com/bbs/?tid=52529 Sun, 30 Apr 2023 13:24:41 UTC Space-Taxi: Morning Shift Playtest version <p>The original Space Taxi divided up its 24 levels into three &quot;shifts&quot; of eight levels each corresponding to the easy, medium and hard levels. And it turns out eight levels is about what I'm able to fit on a single cart so I've decided to release the game by shift and then merge them all together into a single multi-cart game when I'm done. It's actually already multi-cart with one for displaying the menu and loading the level data into upper memory and the other for actually running the game.</p> <p>The first one, Morning Shift, is not quite finished but it <em>is</em> fully playable. The first level is a copy of the first level from the original game as sort of an homage to it but the rest of the levels are original. And they could use some play testing. If anyone is willing I'd be happy to hear any feedback.</p> <p> <table><tr><td> <a href="/bbs/?pid=128419#p"> <img src="/bbs/thumbs/pico8_space_taxi-2.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=128419#p"> space_taxi</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=128419#p"> [Click to Play]</a> </td></tr></table> </p> <p>In particular I'm interested in hearing:</p> <ol> <li>Are the blinking platform markers helpful?</li> <li>Would they be more helpful if they blinked a different colour?</li> <li>Keeping in mind that these are the <em>easy</em> levels&mdash;with 16 more levels to come at some point in the future&mdash;how does the progression feel?</li> <li>Would you change the order of any of the levels?</li> <li>Was there anything that you particularly liked or disliked about the game?</li> </ol> <p>Don't feel like you have to address all (or any) of those and it's by no means an exhaustive list so any other comments are also appreciated.</p> <h2>Things to do</h2> <ul> <li>Level art :: You'll see that I've spent more time on some of the levels than others. The layouts are, I think, pretty much set but I'm still working on making some of the levels more visually interesting.</li> <li>SFX and Music :: The original had voice synthesis which I don't but I'd like to add some sound effects when passengers appear, get knocked over, etc. as well as other sound effects generally. And while the original didn't have music I'll probably add some.</li> <li>Fix how money (aka: score) tips, etc. work</li> <li>Add saved high scores</li> <li>Add an actual win screen</li> <li>Various tweaks (refuel faster, etc.)</li> </ul> <h4>Original Post Below</h4> <p>I don't know if anyone else remembers Space Taxi from the C64 era but I used to love playing it as a kid. I'm not sure why it popped into my mind a while back but I thought I'd take a shot at implementing a Pico-8 version.</p> <p>This is a work in progress so it only has a few very boring levels for testing out the mechanics. I'm a bit too close to it and have gotten used to the controls so if anybody who isn't me felt like giving it a shot and letting me know how the controls feel I'd be grateful.</p> <p> <table><tr><td> <a href="/bbs/?pid=128419#p"> <img src="/bbs/thumbs/pico8_space_taxi-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=128419#p"> space_taxi</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=128419#p"> [Click to Play]</a> </td></tr></table> </p> <p>There will likely be a second screen under the &quot;Controls&quot; menu option which explains the UI but I haven't implemented it yet. So briefly:</p> <ul> <li>The money indicator on the left is your &quot;Earned Money&quot; aka, your score.</li> <li>Above that is your lives</li> <li>The money indicator on the right is your &quot;Tip.&quot; Deliver passengers faster, get a bigger tip.</li> <li>The &quot;clock&quot; on the right is the level indicator. (1:00 is level 1, 12:00 is level 12, etc.)</li> <li>The black box at bottom center is where passenger instructions are displayed (where to take them)</li> <li>Above that is the fuel gauge. Fuel usage is tied to how long you're in the air <em>not</em> how much you use the thrusters and fuel resets between levels. The third (and last) sample level has a fuel platform to test out the refuelling mechanic but not all levels have fuel platforms.</li> <li>The gray checked bar above the other UI elements is the landing indicator and flashes either green, yellow or red. Green for a soft landing, yellow for a harder landing and red when you crash. Your tip is reduced (well not yet but it will be) for &quot;yellow&quot; landings.</li> </ul> <p>Once I've got the mechanics dialed in I'll probably throw together a quick level editor and start making some actual levels. Hopefully I've got room for a full 24. I've got a little under 3000 tokens left and about half my character space and I haven't tried to optimize it yet but I suspect I may end up using a second cart for the level data.</p> <p>Thanks to anybody who tries it out!</p> https://www.lexaloffle.com/bbs/?tid=52384 https://www.lexaloffle.com/bbs/?tid=52384 Mon, 10 Apr 2023 19:50:53 UTC Simple 'Types' and Type Constructors <p>This function creates simple extensible 'types' with a few additional (potentially) handy properties. If you find it useful, help yourself to it. Credit appreciated but not required.</p> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"></p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- 52 tokens function new_type(names) local names = split(names, ' ') local meta = {} return setmetatable( meta, { __call=function(self, ...) local obj = {...} for i=1,#names do obj[names[i]] = obj[i] end return setmetatable(obj, {__index = meta}) end } ) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p></div></div></div></p> <p>You use it like this:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- a 2d point object point = new_type('x y') -- argument names separated by spaces p = point(1, 2) print(p.x) -- prints 1 print(p.y) -- prints 2</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>As for the handy properties, the first is that the object's data is stored both by key and as an array. Which means you can access the properties using the dot notation, numerical indexes, or even unpacking the object, all of which can come in handy in different situations.</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> p = point(1, 2) x, y = unpack(p) -- extract all the data in order -- all of these print 1 print(p.x) print(p[1]) print(x) -- and all of these print 2 print(p.y) print(p[2]) print(y)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The downside of this is that each object is storing the data twice and, therefore, requires twice as much space. So if you have a lot of active objects in your program at once you'll run out of memory about twice as fast. But you can always modify <code>new_type()</code> to only use one or the other of these methods instead of both if you prefer.</p> <p>The second handy property is that functions created with <code>new_type()</code> aren't actually functions, they're objects which you can add methods to. Those methods are then available to every object of that 'type'. For instance:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> point = new_type('x y') point.add = function(a, b) return point(a.x + b.x, a.y + b.y) end p = point(1, 2) q = point(2, 3) r = p:add(q) print(r.x) -- prints 3 print(r.y) -- prints 5</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>It even works if the methods are defined <em>after</em> you've already created the objects:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> point.dot_product = function(a, b) return a.x * b.x + a.y * b.y end print(p:dot_product(r)) -- prints 13</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> https://www.lexaloffle.com/bbs/?tid=51945 https://www.lexaloffle.com/bbs/?tid=51945 Fri, 10 Mar 2023 01:20:55 UTC Bug: metatables and __tostring (v0.2.5g) <p>Not sure if this is actually a bug or expected behaviour. Seems like a bug to me. That said, don't know if it's a problem with PICO-8 or just with Lua in general.</p> <p>Here's the problem. If I define a metatable using the <code>__metatable</code> property then the <code>__tostring</code> method doesn't get called automatically (by print for example) when expected.</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>m = {} x = setmetatable( {}, { __index=m, __metatable=m, __tostring=function(self) return 'blah blah blah' end } ) print(x) print(tostring(x))</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Output:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>[table] blah blah blah</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>So, I can call the tostring() function explicitly but it's not called automatically by print as I would expect. If I leave out the <code>__metatable</code> property though it works as expected:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>m = {} x = setmetatable( {}, { __index=m, __tostring=function(self) return 'blah blah blah' end } ) print(x) print(tostring(x))</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Output:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>blah blah blah blah blah blah</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Putting the <code>__tostring</code> function in m also doesn't work:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>m = { __tostring=function(self) return 'blah blah blah' end } x = setmetatable( {}, { __index=m, __metatable=m } ) print(x) print(tostring(x))</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Output:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>table: 0x30bb9cc table: 0x30bb9cc</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Nor does giving m its own metatable with a <code>__tostring</code> method:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>m = setmetatable({}, { __tostring=function(self) return 'blah blah blah' end }) x = setmetatable( {}, { __index=m, __metatable=m } ) print(x) print(tostring(x))</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Output:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>[table] table: 0x30bb9cc</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The <code>__metatable</code> property doesn't seem to interfere with other metamethods. Haven't checked them all though. Example:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>m = {} x = setmetatable( {}, { __index=m, __metatable=m, __add=function(a, b) return 1 end, __tostring=function(self) return 'blah blah blah' end } ) print(x) print(x + x)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Output:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>[table] 1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This isn't really a major problem, I'm mostly just messing around but it seems weird so might be worth taking a look at at some point.</p> https://www.lexaloffle.com/bbs/?tid=51807 https://www.lexaloffle.com/bbs/?tid=51807 Wed, 01 Mar 2023 12:55:40 UTC Song: Home Stretch <p> <iframe src="sfxp.php?id=54967_28" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_28"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_28.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_28"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_28" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>I don't know if people are still having problems with the inline music player but here's a cart with the song just in case.</p> <p> <table><tr><td> <a href="/bbs/?pid=125018#p"> <img src="/bbs/thumbs/pico8_mibamawojo-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=125018#p"> mibamawojo</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=125018#p"> [Click to Play]</a> </td></tr></table> </p> <p>I've made no attempt to optimize space and used pretty much all the sfx slots. Though some are duplicated just so if you happen to listen on the editor music/sfx tab it's kind of satisfying to watch. To me at least.</p> <img style="margin-bottom:16px" border=0 src="/media/54967/home-stretch_0.gif" alt="" /> https://www.lexaloffle.com/bbs/?tid=51416 https://www.lexaloffle.com/bbs/?tid=51416 Mon, 30 Jan 2023 11:33:00 UTC Noisy marching squares <p>Just a little weekend &quot;I need a break from my projects&quot; project.</p> <p> <table><tr><td> <a href="/bbs/?pid=121419#p"> <img src="/bbs/thumbs/pico8_noise_march-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=121419#p"> noise_march</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=121419#p"> [Click to Play]</a> </td></tr></table> </p> <p>Uses the marching squares algorithm to determine tile placement at each step and 3 dimensional value noise to animate it.</p> https://www.lexaloffle.com/bbs/?tid=50368 https://www.lexaloffle.com/bbs/?tid=50368 Sat, 26 Nov 2022 17:50:14 UTC Overloading the sub() function <p>This isn't really anything special, just a little quality of life improvement I've found handy and keep in a file of utility functions I use frequently.</p> <p>Overloads <code>sub()</code> so it works on arrays as well as strings. I find it useful to be able to slice and dice arrays in the same way as strings and especially now that we can index strings with square bracket notation the difference between them isn't always important. I've got something with a length and I want a sub-section of it. Makes sense to use the same function for both, in my mind at least.</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>_sub = sub function sub(lst, i, j) if type(lst) == 'string' then return _sub(lst, i, j) else local r = {} for n=i,j or #lst do add(r, lst[n]) end return r end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Creates a new array without modifying the original. Doesn't handle negative indices because I've not really needed them so far but it would be easy enough to add.</p> <p>Potentially a feature suggestion <a href="https://www.lexaloffle.com/bbs/?uid=1"> @zep</a>? I don't mind writing the function myself but I'd also be happy not to. But maybe I'm the only one who finds it useful.</p> https://www.lexaloffle.com/bbs/?tid=50186 https://www.lexaloffle.com/bbs/?tid=50186 Sat, 12 Nov 2022 13:15:28 UTC Procedural Generation: Graph Rewriting <p>I'm working on a game using procedural generation and although the game is nowhere near done I thought the generation technique itself was interesting enough on its own to post about.</p> <p>Rather than try to explain it myself I'll just send you to someone who already has, better than I likely could. <a href="https://www.boristhebrave.com/2021/04/02/graph-rewriting/">BorisTheBrave: Graph Rewriting for Procedural Level Generation</a></p> <p>Instead, I'll just share some of my initial results. The general idea is to generate the &quot;big picture&quot; details first and then successively refine them to build a fleshed out so the levels generated here are shown at 1/8 scale. Eventually every pixel in these maps will expand to be one tile at full scale with additional generation filling in the fine details along the way.</p> <p> <table><tr><td> <a href="/bbs/?pid=120219#p"> <img src="/bbs/thumbs/pico8_nisofunajo-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=120219#p"> nisofunajo</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=120219#p"> [Click to Play]</a> </td></tr></table> </p> <p>The rules I'm using a pretty simple for now; they were mostly intended to help me shake out any bugs in the graph rewriting system itself. The actual rules I end up using will probably be somewhat more complex but even so I think I'm already getting some decent results.</p> <h2>The Cycle</h2> <p>The first thing I do is generate a cycle with a start (green square) and an end (red square.) The idea for each level is you go in, get whatever is waiting for you at the end&mdash;treasure, a boss fight, whatever&mdash;and then have to get back out. Having a cycle gives you multiple potential routes to accomplish that. </p> <p>At the moment the cycles go one-way&mdash;although it's not shown&mdash;with one path leading toward the end and the other path leading from the end back to the start. But you could have two paths heading towards the start where one might be shorter but full of enemies and the other is longer but relatively safe, and all sorts of other variations.</p> <h2>Terrain</h2> <p>Next up is terrain. For this demo I just have two terrain types, stone and water. You can have both types on a single tile giving you wet stone. The rules are written so that stone and water will never touch directly but will always have wet stone in between. This also results in the occasional patch of wet stone bordered by either stone on both sides or water on both sides which helps to vary things up a bit more.</p> <p>For the actual game I'm thinking I'll just have abstract terrain types: terrain-1, terrain-2, etc. Then each level will have a randomly chosen theme and that theme will give me a terrain palette which will assign actual terrain types. So you could have fire level and ice levels and so on.</p> <h2>Enemies</h2> <p>The enemy rules are fairly straight forward. There are four types:</p> <ol> <li>Large enemies (big X)</li> <li>Small enemies (small x)</li> <li>Swarms (clusters of small enemies that attack together)</li> <li>Guardians (in a white box with a border. When you come into their<br /> area everything closes off and you can't continue until you defeat<br /> them. Basically mini-bosses.)</li> </ol> <p>The red ones will be visible as you approach and the yellow ones will be hidden and ambush you. You'll never get two large enemies right next to each other but any other combination of enemies is possible.</p> <h2>Obstacles</h2> <p>Obstacles show up on the map as locks and keys but they may not be actual locked doors requiring keys. They're just anything which requires you to do or obtain something in one part of the level in order to progress in another part. The &quot;key&quot; could be an actual key but it could also be a lever or a puzzle or a new ability. Maybe you need to drain water in one area to reveal a door in another. Whatever.</p> <p>This, in particular, is an area where I think graph rewriting shines. Since at this stage the whole level is represented as a graph and the path from start to end and back to start is a directed sub-graph, it's relatively easy to ensure that keys never end up <em>behind</em> their associated lock. The lock and key are initially spawned on the same tile with keys optionally then being moved backwards along the path but never past the start.</p> <p>At the moment there's a 50% chance of a key moving on each iteration of the rules so, on average, keys are found fairly close to their locks but I can tweak the probabilities to have them move either more or less.</p> <h2>Rooms</h2> <p>Rooms are added to empty areas near the main path. Rooms are either big or small and once placed one or more doors are added to the room connecting it to other rooms and/or the main path. </p> <p>The dark gray bars connecting the rooms to the path are just to show roughly where doors are and how they connect up to the main path. There won't be literal corridors to every room. Either the rooms will be moved closer to the main path or the surrounding terrain will be expanded to the edge of the room.</p> <p>At the moment the rooms are all just kind of plopped down on top of each other. Whether overlapping rooms merge into a bigger room or where exactly the walls between them will be will be figured out at a later stage.</p> <h2>Next steps</h2> <p>Next is to take this simple set of rules and and basic level layouts and turn them into full-fledged level maps to get the basic infrastructure in place to handle that process.</p> <p>After that I'll probably take a break to work on actual game mechanics before really diving into the level generation rules.</p> https://www.lexaloffle.com/bbs/?tid=50076 https://www.lexaloffle.com/bbs/?tid=50076 Sun, 06 Nov 2022 19:31:39 UTC 32768 and Beyond: Arbitrarily large integers <p>A few days back <a href="https://www.lexaloffle.com/bbs/?uid=15232"> @dw817</a> posted a <a href="https://www.lexaloffle.com/bbs/?tid=49971">thread</a> about the number of possible combinations in 256 bytes in which they asked:</p> <p>&gt; Is there a way to calculate 256^256 ?</p> <p>And knowing there are all sorts of tools and languages which can handle such huge numbers I cheekily posted the answer and caused a bit of a derail.</p> <p>Anyway, I had some time on my hands so I figured I'd implement large number support for Pico-8.<br /> <table><tr><td> <a href="/bbs/?pid=120151#p"> <img src="/bbs/thumbs/pico8_jd_bignum-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=120151#p"> jd_bignum</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=120151#p"> [Click to Play]</a> </td></tr></table> </p> <h5>Edit: Fixed a bug in divide()</h5> <p>Is this likely to be of use to anybody? Probably not. Is it major overkill? You betcha! Though in fairness, at 706 tokens it came out smaller than I was expecting.</p> <h2>Features/Functions</h2> <ul> <li> <p><strong><code>bignum(n)</code>:</strong> Create a 'bignum' with value <code>n</code> which is just a regular Pico-8 number. Most operations require both numbers to be bignums. So <code>bignum(16000)*bignum(1254)</code> but not <code>bignum(16000)*1254</code>. </p> <p><code>bignum()</code> is equivalent to <code>bignum(0)</code></p> </li> <li> <p><strong><code>divide(bn)</code>:</strong> You can use the division and mod operators (/, %) on bignums if you only need one or the other. The <code>divide</code> function returns both the quotient and remainder at once.</p> <p><code>q, r = divide(bignum(3)^16, bignum(2)^15)</code></p> </li> <li><strong><code>show(bn)</code>:</strong> Converts a bignum into its decimal representation as a string.</li> <li><strong><code>factorial(n)</code>:</strong> Calculates the factorial of <code>n</code> and returns the result as a bignum. Factorials get very big, <em>very</em> fast so the input to <code>factorial</code> is just a regular number.</li> <li><strong><code>+ - / % ^</code>:</strong> Arithmetic operators behave how you would expect. All require both arguments to be bignums with the exception of <code>^</code> where only the first argument is a bignum and the second is a regular number. Division is integer division only so <code>bignum(3)/bignum(4)==bignum(0)</code>.</li> <li><strong><code>&gt; &gt;= &lt;= &lt; ==</code>:</strong> Comparisons similarly require both arguments to be bignums.</li> </ul> <h2>Issues</h2> <ul> <li>The arithmetic operators are all fairly efficent but probably not optimally so.</li> <li>I wasn't taking negative numbers into account at all so I make no promises about what will happen if you try to use them. Maybe it will work! (It probably won't work.)</li> <li><code>show</code> is <em>extremely</em> inefficient. I'm sure there are better ways to convert very long binary strings to decimal but I'm not familiar with them so this is what I've got. Large numbers (like 52!) may take a bit of time and <em>very</em> large numbers will crash the program as it runs out of memory. (bignum(256)^256 does this, sadly. It calculates the number fine but crashes when you try to convert it.)</li> </ul> https://www.lexaloffle.com/bbs/?tid=50060 https://www.lexaloffle.com/bbs/?tid=50060 Sat, 05 Nov 2022 13:35:22 UTC Testo-8: Test Framework <p>I know a lot of people, myself included, usually write their pico~8 code a little off the cuff tinkering with it until it works. Which tends to be more fun in my experience. But it can also be incredibly frustrating if I'm working on sometime more complex where every time I change something I break something else. And in those cases planning out some formal tests helps me maintain my sanity and get the thing actually working much faster than I probably would otherwise. And since I'm working on something fairly complex at the moment, I took a bit of a detour and put together a little test framework and thought I'd make it available for anybody else who might find it useful.</p> <p>The code is on github: <a href="https://github.com/jasondelaat/pico8-tools/tree/release/testo-8">https://github.com/jasondelaat/pico8-tools/tree/release/testo-8</a><br /> Or you can just copy it here:<br /> <div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"></p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-------------------------------- -- testo-8: testing framework -- copyright (c) 2022 jason delaat -- mit license: https://github.com/jasondelaat/pico8-tools/blob/release/license -------------------------------- do local all_tests = {} local function smt(t, mt) return setmetatable(t, {__index=mt}) end local function shallow_copy(lst) local copy = {} for l in all(lst) do add(copy, l) end return copy end local function filter(f, lst) local results = {} for l in all(lst) do if f(l) then add(results, l) end end return results end local execute_meta = { execute=function(self) local result = self[4](self[3](self[2]())) if self._cleanup[1] then self._cleanup[1]() end return { result, self[1]..self.when_txt..self.result_txt } end } local when_result_meta local result_meta = { result=function(self, txt, fn) local t = shallow_copy(self) t.when_txt = self.when_txt t.result_txt = 'result '..txt..'\n' t._cleanup = self._cleanup add(t, fn) add(all_tests, smt(t, execute_meta)) return smt(self, when_result_meta) end } local _cleanup local when_meta = { when=function(self, txt, fn) _cleanup = {} local t = shallow_copy(self) t.when_txt = 'when '..txt..'\n' t[3] = fn t._cleanup = _cleanup return smt(t, result_meta) end } when_result_meta = { when=when_meta.when, result=result_meta.result, cleanup=function(self, f) add(_cleanup, f) return self end } local given_meta = { given=function(self, txt, fn) local msg = self[1]..'given '..txt..'\n' return smt({msg, fn}, when_meta) end } function test(name) _cleanup = {} local t = smt({name..':\n', _cleanup=_cleanup}, given_meta) return t end local function run_tests() cls() cursor(0, 7) local results = {} for t in all(all_tests) do add(results, t:execute()) end local failures = results and filter(function(r) return not r[1] end, results) or 0 if #failures == 0 then print('all '..#all_tests..' tests passed!', 0, 0, 11) else for f in all(failures) do print(f[2]) end rectfill(0, 0, 127, 6, 0) print(#failures..'/'..#all_tests..' tests failed:\n', 0, 0, 8) cursor(0, 127) end end function _init() run_tests() end end -- end testo-8 ------------------------------</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p></div></div></div></p> <p>And here's a cart with some simple functions containing errors and a few tests, most of which fail just to give you an idea of how it works.<br /> <table><tr><td> <a href="/bbs/?pid=119475#p"> <img src="/bbs/thumbs/pico8_testo8_demo-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=119475#p"> testo8_demo</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=119475#p"> [Click to Play]</a> </td></tr></table> </p> <h3>Testo-8</h3> <p>The framework is pretty simple. It's just a single function <code>test</code> which returns an object exposing methods&mdash; <code>given</code>, <code>when</code>, <code>result</code> and <code>cleanup</code> &mdash;for defining the test. </p> <p>Testo-8 defines an <code>_init</code> function which automatically runs the tests. Just <code>#include testo-8.lua</code>, write tests and run the cart. If you've defined your own <code>_init</code> you'll probably need to comment it out to get the tests to run.</p> <p>A very simple test might look something like this:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>test('some simple addition') :given('the number one', function() return 1 end) :when('adding 2', function(n) return n+2 end) :result('should equal 3', function(r) return r == 3 end)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The methods <code>given</code>, <code>when</code> and <code>result</code> &mdash;which I'll call clauses&mdash;all take a string as their first argument and a function as their second, while <code>test</code> takes a string argument only. The strings are used to build the output message if the test fails.</p> <p>The function arguments taken by the other methods serve different purposes:</p> <ul> <li><code>given</code> should return the object(s) being tested. (1 in the example)</li> <li><code>when</code> takes the object(s) being tested as input and does something with it returning the result(s) (add 2)</li> <li><code>result</code> takes the result(s) and should return a boolean, true if the test passes and false if it fails. (does 1+2 == 3?)</li> </ul> <p>Each test has exactly one <code>given</code> clause below which will be one or more <code>when</code> clauses. Each <code>when</code> clause contains one or more <code>result</code> clauses and can optionally be ended with a <code>cleanup</code> clause. More on that later. So an actual test might look something like this:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- the ...'s are just a placeholders for some appropriate function test('some test') :given('an object to test', ...) :when('1st when', ...) :result('result 1', ...) :result('result 2', ...) :result('result 3', ...) :when('2nd when', ...) :result('result 4', ...) :result('result 5', ...) :result('result 6', ...) :cleanup(...) :when('3rd when', ...) :result('result 7', ...) :result('result 8', ...)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The number of <code>result</code> clauses is the actual number of tests that will be run so the above example would be eight tests. Each <code>result</code> clause is executed as follows: The <code>given</code> clause is executed to generate the object(s) to test. The test object(s) are passed to the <code>when</code> clause which appears <em>above</em> the <code>result</code> and finally the results are passed to the <code>result</code> clause which determines whether the test passes or fails.</p> <p>So in the above example the <code>given</code> clause will run eight times, once for every <code>result</code>. The first <code>when</code> clause will be called three times and so will the second while the third <code>when</code> clause will only be called twice.</p> <p><code>cleanup</code> takes a single function as its argument and is used to clean up after a test if, for instance, the test modifies some global state which needs to be reset in-between tests. The <code>cleanup</code> clause is optional but if it exists will be called <em>after</em> each <code>result</code> clause inside the same <code>when</code>. The <code>cleanup</code> in the above example would therefore be called three times, once after each of <code>result</code>s 4, 5 and 6.</p> <p>For anyone interested in seeing actual tests, here are some I wrote for a simple s-expression parser.<br /> <div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"></p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function test_given(s) return s, function() return s end end function len(n) return function(lst) return #lst == n end end function index_eq(n, s) return function(lst) return lst[n] == s end end function is_table(r) return type(r) == 'table' end function is_nil(r) return r == nil end test('empty s-exp') :given(test_given('()')) :when('parsed', parse) :result('should be a table', is_table) :result('should have 0 elements', len(0)) test('single element s-exp') :given(test_given('(atom)')) :when('parsed', parse) :result('should have 1 element', len(1)) :result('element should be atom', index_eq(1, 'atom')) test('multi-element s-exp') :given(test_given('(a b c d e)')) :when('parsed', parse) :result('should have 5 elements', len(5)) :result('1st should be a', index_eq(1, 'a')) :result('2nd should be b', index_eq(2, 'b')) :result('3rd should be c', index_eq(3, 'c')) :result('4th should be d', index_eq(4, 'd')) :result('5th should be e', index_eq(5, 'e')) test('nested s-exp') :given(test_given('((atom))')) :when('parsed', parse) :result('should have 1 element', len(1)) :result('element should be a table', function(r) return is_table(r[1]) end) :result('element should have length 1', function(r) return #r[1] == 1 end) :result('nested element should be atom', function(r) return r[1][1] == 'atom' end ) test('multi-element nested s-exp') :given(test_given('((a b) (c d) (e f))')) :when('parsed', parse) :result('should have 3 elements', len(3)) :result('each element should be length 2', function(r) for i in all(r) do if #i != 2 then return false end end return true end ) :result('1st contains a and b', function(r) return r[1][1] == 'a' and r[1][2] == 'b' end ) :result('2nd contains c and d', function(r) return r[2][1] == 'c' and r[2][2] == 'd' end ) :result('3rd contains e and f', function(r) return r[3][1] == 'e' and r[3][2] == 'f' end ) test('multiply nested s-exp') :given(test_given('((a (b c)))')) :when('parsed', parse) :result('should have 1 element', len(1)) :result('element length 2', function(r) return #r[1] == 2 end ) :result('element [1][1] == a', function(r) return r[1][1] == 'a' end ) :result('element [1][2] is table', function(r) return is_table(r[1][2]) end ) :result('element #[1][2] == 2', function(r) return #r[1][2] == 2 end ) :result('element [1][2][1] == b', function(r) return r[1][2][1] == 'b' end ) :result('element [1][2][2] == c', function(r) return r[1][2][2] == 'c' end ) test('empty string fails to parse') :given(test_given('')) :when('parsed', parse) :result('should be nil', is_nil) test('unclosed parens fails to parse') :given(test_given('((a b) (c)')) :when('parsed', parse) :result('should be nil', is_nil) test('too many closed parens fails to parse') :given(test_given('((a b) (c)))')) :when('parsed', parse) :result('should be nil', is_nil) test('parsing with newlines') :given(test_given('(a \n (b c))')) :when('parsed', parse) :result('should have 2 elements', len(2)) :result('1st element should be a', index_eq(1, 'a')) :result('r[2][1] == b', function(r) return r[2][1] == 'b' end) :result('r[2][2] == c', function(r) return r[2][2] == 'c' end)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p></div></div></div></p> <h3>Modules and Testing</h3> <p>I rely heavily on an external editor and spreading all my code around a bunch of files. If that's not how you work this may not be super practical. But here's a quick run-down of how I (currently) work on a project.</p> <p>Even though Pico-8 Lua doesn't technically have modules I generally try to write things in a modular way and <code>#include</code> with the help of <code>do...end</code> gives me something module-like.</p> <p>A vastly oversimplified example would be something like this:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- player.lua local pos = {x=64, y=64} local s = 1 local function move(var, dist) return function() pos[var] += dist end end move_left = move('x', -2) move_right = move('x', 2) move_up = move('y', -2) move_down = move('y', 2) function draw_player() spr(s, pos.x, pos.y) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Which I include inside of a <code>do...end</code> block like so:</p> <img style="margin-bottom:16px" border=0 src="/media/54967/demo_0.png" alt="" /> <p>Writing modules like this doesn't really cost much extra because:</p> <ol> <li>These are all functions I'd write anyway</li> <li>The <code>local</code> keyword doesn't use any tokens</li> <li>The <code>do...end</code> costs just a single token</li> <li>The added encapsulation given module local variables means I can't accidentally mess of things like the player position from other parts of my code because <code>pos</code> doesn't exist outside of the module.</li> </ol> <p>Importantly, I don't put the surrounding <code>do...end</code> in the module file itself. Because when it come to writing the actual tests, I'll put those in another separate file and then include it inside the same <code>do...end</code> block as before.</p> <img style="margin-bottom:16px" border=0 src="/media/54967/demo_1.png" alt="" /> <p>This makes the tests part of the same module so they can access and test all the local data and functions. Once I'm sure everything is working properly I can just comment out the <code>#include</code> for the test file and free up all those tokens. </p> <h3>Issues</h3> <ol> <li>Since Lua doesn't have exception handling capabilities like <code>try...catch</code> or similar, I'm not able to intercept certain errors and report them as test failures. So things like attempting to index a nil value, etc. will still cause the cart to crash and you'll have to fix those problems before the test will run.</li> <li>The above can also lead to occasionally cryptic error messages saying that there's an error with testo-8 itself. This is certainly possible but usually it means you've passed nil, or something else, where testo-8 is expecting a function. If you're frequently commenting out parts of your code make sure you haven't commented out a function which you're using in a test.</li> </ol> https://www.lexaloffle.com/bbs/?tid=49888 https://www.lexaloffle.com/bbs/?tid=49888 Sun, 23 Oct 2022 14:00:43 UTC Untitled WIP:push your luck, dice builder, roguelike/RPG...thing. <p> <table><tr><td> <a href="/bbs/?pid=119177#p"> <img src="/bbs/thumbs/pico8_jd_dice_wip-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=119177#p"> jd_dice_wip</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=119177#p"> [Click to Play]</a> </td></tr></table> </p> <p>So I've started working on this. It's still very early and I've only got some of the UI elements and the very beginnings of procedural generation in place but I think there's enough to make it worth sharing.</p> <h3>Controls</h3> <p>While moving around:</p> <ul> <li>🅾️ to switch to the ability selector. (Coloured boxed bottom-middle)</li> </ul> <p>In ability selector:</p> <ul> <li>🅾️ to select highlighted item</li> <li>❎ to cancel</li> </ul> <p>Choosing 'END TURN' pulls up a 'Dice Selector' menu which doesn't do anything yet.<br /> Press either ❎ or 🅾️ to dismiss it.</p> <h3>Notes</h3> <ul> <li>Actual movement in the game will be tile based but I wanted people to be able to zoom around the big empty levels fairly quickly for now because it's not like there's much to see.</li> <li>At the moment the levels are just a big loop of straight, boring grey hallways surrounded by blackness but eventually room will be able to expand into the available empty spaces, there will be locks, keys, enemies, puzzles, etc.</li> <li>The mini-map may or may not be in the actual game but I wanted to give an overall idea of the shape of the generated levels.</li> <li>The green square in-level is the level entrance and the red one the level exit. Nothing happens yet when you get to them.</li> <li>The stats on the bottom left are hard-coded and don't do/mean anything yet.</li> </ul> <p>I'm trying my hand at procedural generation using graph re-writing. There's a fairly straight-forward summary of the process here for anyone interested: <a href="https://www.boristhebrave.com/2021/04/02/graph-rewriting/">https://www.boristhebrave.com/2021/04/02/graph-rewriting/</a></p> <p>This version isn't actually using re-writing yet which is why all the interesting stuff is missing from the level. It's just a graph with a basic loop. I've got the system written and&mdash;I think&mdash;working as expected so next steps are to start testing some rules for slightly more interesting levels to shake out all the bugs and then take it from there. Oh yeah, and all the actual game mechanics. Those will probably help too.</p> https://www.lexaloffle.com/bbs/?tid=49807 https://www.lexaloffle.com/bbs/?tid=49807 Sun, 16 Oct 2022 15:21:50 UTC Local functions. Who knew? <p>So maybe this is old news to everybody but me but I just discovered it by accident. Turns out you can define local functions! It makes sense but it somehow never occurred to me to try before. </p> <p>Here's a quick example:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>do local function a() print('in a') end function b() print('in b: calling a') a() end print('in local scope') print(a) print(b) a() b() print('leaving local scope\n\n') end print('in global scope') print(a) print(b) b()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Output:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>in local scope [function] [function] in a in b: calling a in a leaving local scope in global scope [nil] [function] in b: calling a in a</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>That's it. Just thought it was interesting.</p> https://www.lexaloffle.com/bbs/?tid=49582 https://www.lexaloffle.com/bbs/?tid=49582 Sun, 02 Oct 2022 18:10:54 UTC Text adventures on Pico-8. For some reason. <p>I had a bit of time to tinker yesterday. I'm not sure what made me think of the old Infocom text adventures but here we are.</p> <p>If you're familiar with Text Adventures (or Interactive Fiction) you may know that one of&mdash;maybe <em>the</em>&mdash;most popular tool for creating them has been the <a href="https://en.wikipedia.org/wiki/Inform">Inform</a> language. The current version is <a href="https://ganelson.github.io/inform-website/">Inform 7</a> but <em>waaaaay</em> back when I was first learning, it is was <a href="https://github.com/DavidKinder/Inform6">Inform 6.</a></p> <p>Anyway, I decided to throw together a quick little <a href="https://github.com/jasondelaat/pico8-tools/tree/release/pico-informant">IF authoring API loosely based on Inform 6.</a> It is by no means complete or particularly advanced. Basically, I followed the first tutorial for the game <em>Heidi</em> from <a href="https://ifarchive.org/if-archive/infocom/compilers/inform6/manuals/IBG.pdf">The Inform Beginner's Guide</a> and implemented just enough to make it work. But work it does! Mostly. I think...</p> <p>The command parser is <em>extremely</em> simple so don't expect too much from it. All commands should be in the form:</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>verb [noun] [noun]</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Where the nouns are optional depending on what you're trying to do. So <code>east</code> or <code>e</code> to walk to the east; <code>take object</code> to pick up an object; and <code>put object target</code> to put an object on/in something else. There are enough verbs, including synonyms, to complete this game and the API even lets you add new ones but don't try to be too creative with your commands. It was mostly just something to do on a Saturday afternoon so I may or may not develop it further.</p> <p> <table><tr><td> <a href="/bbs/?pid=117587#p"> <img src="/bbs/thumbs/pico8_jd_heidi_if-3.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=117587#p"> jd_heidi_if</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=117587#p"> [Click to Play]</a> </td></tr></table> </p> <p>The API clocks in at 926 tokens while the example game itself is only 173.</p> <p>For simplicity's sake here's the code for just the game itself if you're interested in how it works. You can find the original in Appendix B of The Inform Beginner's Guide linked above if you want to see how the two versions compare.</p> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"></p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>--&gt;8 -- example game: heidi -- adapted from: -- 'the inform beginner's guide' -- https://ifarchive.org/if-archive/infocom/compilers/inform6/manuals/IBG.pdf -------------------------------- -- defining the title screen story( 'heidi', &quot;a simple text adventure written\nby roger firth and sonja\nkesserich.\n\nadapted to pico-8 from\nthe inform beginner's guide.\nby jason delaat.\n\n\npress enter to begin.&quot;) -- rooms and objects before_cottage = object('in front of a cottage') description(&quot;you stand outside a cottage.\nthe forest stretches east.\n&quot;) has(light) forest = object('deep in the forest') description(&quot;through the dense foliage you\nglimpse a building to the west.\na track heads to the northeast.&quot;) has(light) bird = object('baby bird', forest) description(&quot;too young to fly, the nestling\ntweets helplessly.&quot;) name('baby', 'bird', 'nestling') clearing = object('a forest clearing') description(&quot;a tall sycamore stands in the\nmiddle of this clearing. the\npath winds southwest through the\ntrees.&quot;) has(light) nest = object(&quot;bird's nest&quot;, clearing) description(&quot;the nest is carefully woven of \ntwigs and moss.\n &quot;) name(&quot;bird's&quot;, 'nest', 'twigs', 'moss') has(container|open) tree = object('tall sycamore tree', clearing) description(&quot;standing proud in the middle of \n the clearing, the stout tree \n looks easy to climb.\n &quot;) name('tall', 'sycamore', 'tree', 'stout', 'proud') has(scenery) top_of_tree = object('at the top of the tree') description(&quot;you cling precariously to the \ntrunk.&quot;) has(light) each_turn(function(_ENV) if contains(branch.contents, nest) then print('you win!') stop() end end) branch = object('wide firm bough', top_of_tree) description(&quot;it's flat enough to support a \nsmall object.\n &quot;) name('wide', 'firm', 'flat', 'bough', 'branch') has(static|supporter) -- connecting the rooms before_cottage :e_to(forest) forest :w_to(before_cottage) :ne_to(clearing) clearing :sw_to(forest) :u_to(top_of_tree) top_of_tree :d_to(clearing) -- initialization function _init() location(before_cottage) max_carried = 1 end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p></div></div></div></p> <h3>Walkthrough</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> This example game is very short with only four rooms and a handful of objects. The goal is to return the baby bird to its nest in the tree. You can only carry one object at a time&mdash;as defined by the max_carried variable in the _init() function&mdash;but an object inside of something else counts as a single object.</p> <div> <div class=scrollable_with_touch style="width:100%; max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>in front of a cottage -------------------------------- you stand outside a cottage. the forest stretches east. &gt; east deep in the forest -------------------------------- through the dense foliage you glimpse a building to the west. a track heads to the northeast. you see a baby bird. &gt; take bird you take the baby bird. &gt; ne a forest clearing -------------------------------- a tall sycamore stands in the middle of this clearing. the path winds southwest through the trees. you see a bird's nest. &gt; put bird nest you put the baby bird in the bird's nest. &gt; take nest you take the bird's nest. &gt; climb at the top of the tree -------------------------------- you cling precariously to the trunk. you see a wide firm bough. &gt; put nest bough you win!</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Some of the words have multiple aliases. Instead of 'bough' you could type 'put nest branch' and that would also work.<br /> </div></div></div></p> https://www.lexaloffle.com/bbs/?tid=49378 https://www.lexaloffle.com/bbs/?tid=49378 Sun, 18 Sep 2022 10:44:36 UTC midi 122 Seashore <p> <iframe src="sfxp.php?id=54967_27" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_27"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_27.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_27"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_27" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>Made for the <a href="https://www.lexaloffle.com/bbs/?tid=49265">midilib custom SFX instrument project</a></p> <p>And in action:<br /> <table><tr><td> <a href="/bbs/?pid=117347#p"> <img src="/bbs/thumbs/pico8_jdmidi_122_seashore-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=117347#p"> jdmidi_122_seashore</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=117347#p"> [Click to Play]</a> </td></tr></table> </p> <h5>The animation is taken from one of my other projects. The palm tree image is adapted from the tutorial <a href="https://www.youtube.com/watch?v=0ifChJ0nJfM&amp;amp;t=1233s">The basics of Painting with Maths</a> by Inigo Quilez.</h5> https://www.lexaloffle.com/bbs/?tid=49321 https://www.lexaloffle.com/bbs/?tid=49321 Tue, 13 Sep 2022 19:06:14 UTC midi 125 Helicopter <p>I mean, who <em>doesn't</em> compose for helicopter, right?</p> <p>Made for the <a href="https://www.lexaloffle.com/bbs/?tid=49265">midilib custom SFX instrument project</a></p> <p>Credit's always nice but not required.</p> <p> <iframe src="sfxp.php?id=54967_26" width="769" height="97" style="border:none; overflow:hidden"></iframe><a style="cursor:pointer; font-size:8pt" onclick=' var el = document.getElementById("sfxcode_54967_26"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("/bbs/sfxc/54967_26.txt", function (retdata){ var el = document.getElementById("sfxcode_54967_26"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [sfx] </a> <textarea rows=3 class=lexinput id="sfxcode_54967_26" style="width:480px;background-color:#fed;display:none;overflow:hidden; font-size:4pt;"></textarea> </p> <p>Here it is in action. Please forgive the poorly and hastily drawn helicopter.<br /> <table><tr><td> <a href="/bbs/?pid=117345#p"> <img src="/bbs/thumbs/pico8_jdmidi_125_helicopter-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=117345#p"> jdmidi_125_helicopter</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=117345#p"> [Click to Play]</a> </td></tr></table> </p> https://www.lexaloffle.com/bbs/?tid=49320 https://www.lexaloffle.com/bbs/?tid=49320 Tue, 13 Sep 2022 10:12:06 UTC The Animation of Final Fantasy <p>So this isn't strictly Pico-8 related but I just came across a video series on Youtube which I thought others here might enjoy.</p> <p><a href="https://www.youtube.com/playlist?list=PLugegG07di39Hn8m_Q5WwqZJsvYiThnO7">The Animation of Final Fantasy</a></p> <p>The intention seems to be to follow the evolution of FF animation all the way from FF-I up to FF-whatever-number-they're-up-to-by-the-time-he-finishes-the-series.</p> <p>So far he's only up to FF-IV (the first Super Famicon/SNES release) but I think the first three (the Famicon/NES releases) are probably most interesting to Pico-8 developers. I thought it was an interesting look into how the developers managed with so few assets and&mdash;especially in FF-II&mdash;how they were able to use those assets to tell a story and convey at least some <em>very</em> basic sense of character.</p> https://www.lexaloffle.com/bbs/?tid=49301 https://www.lexaloffle.com/bbs/?tid=49301 Mon, 12 Sep 2022 09:53:39 UTC PICO Composer <h1>PICO Composer v0.3</h1> <h3>Changes</h3> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> 2022-06-15:</p> <ul> <li>Set default tempo to 120 bpm</li> <li>Set default time signature to 4/4</li> <li>Implemented exporter and importer code</li> </ul> <p>2022-06-06:</p> <ul> <li>Fixed display code which wasn't properly accounting for sharps causing notes to eventually be drawn offscreen.<br /> </div></div></div></li> </ul> <p> <table><tr><td> <a href="/bbs/?pid=111852#p"> <img src="/bbs/thumbs/pico8_picomposer-5.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=111852#p"> picomposer</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=111852#p"> [Click to Play]</a> </td></tr></table> </p> <h2>PICO Composer main menu</h2> <p>When you start the cart you'll be prompted to load an album or create a new one. Use the up/down arrow keys to navigate the menu and <code>enter</code> to select. Either, or both, of the tempo and time signature can be left blank and you'll be given the defaults of 120 bpm and a 4/4 time signature. Any time signature that can't be parsed properly will default to 4/4.</p> <p>When creating a new album the album name will be the name of the file&mdash;no need to specify .p8 extension&mdash;where your work will be saved. At the moment each &quot;album&quot; can only have a single song, with a single voice, on instrument 0. The idea though is to have each album contain multiple songs. Space constraints may end up making that infeasible but that's the plan for now at least.</p> <h2>Controls:</h2> <p>PICO Composer is intended to be run locally with a full keyboard and will save your work to a separate .p8 file.</p> <img style="margin-bottom:16px" border=0 src="/media/54967/pico composer v0_3 controls.png" alt="" /> <h2>Demo Song/Album</h2> <p>Download <a href="https://www.lexaloffle.com/bbs/files/54967/song6.p8">this cart</a> and save it in the same directory as <code>picomposer.p8.png</code> Load and run picomposer and when prompted select <code>load album</code>, type <code>song6</code> and press enter. It should load and after a couple of seconds you'll see the last few notes of the song. Press <code>space</code> to play the song from the beginning.</p> <p>When you export an album (press <code>e</code>) a new file <code>album_name.music.p8</code> will be created. This file can't be edited in Pico Composer and is intended to be imported for use into your own carts. To do so copy/paste the code from the example below and use the <code>load_music()</code> and <code>play()</code> functions.</p> <p> <table><tr><td> <a href="/bbs/?pid=111852#p"> <img src="/bbs/thumbs/pico8_picomposer_import-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=111852#p"> picomposer_import</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=111852#p"> [Click to Play]</a> </td></tr></table> </p> <p>(Note: The song data for this cart is in the spritesheet and it's &quot;importing&quot; from itself into upper memory. Usually you'll be importing from the generated .music.p8 file but I wanted a self contained example.)</p> <p>The importer is currently pretty heavy sitting somewhere around 600 tokens. Once I've got a few more features implemented I'll revist and try to bring the size down.</p> <p><div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> Song 6 refers to my <a href="https://www.lexaloffle.com/bbs/?tid=47406">Tunes!</a> thread. This demo song is the same song as the 6th (currently last) song I've posted there.</p> <p>As a point of comparison: By my calculation &quot;Song 6&quot; as I've written it in the tracker requires 752 bytes (10 sfx patterns at 68 bytes each plus 18 music patterns by 4 bytes each) while the PICO Composer song data comes in at 750. Considering I've not yet implemented any way of repeating sections&mdash;I just had to enter every single note&mdash;that gives me hope that I'll be able to keep song sizes down.<br /> </div></div></div></p> <h2>Future Development/Feature Wishlist</h2> <ul> <li>Select/edit note(s)</li> <li>Allow all the pico-8 instruments and custom instruments</li> <li>Multiple songs per album</li> <li>Dynamics: volume pp, p, mp, mf, f, ff, fff </li> <li>Allow up to 4 voices and ability to switch around between them </li> <li>Clefs. As is the staff is in treble clef. Adding, at least, bass clef would be useful </li> <li>Bar lines </li> <li>crescendo, decrecendo </li> <li>Repeat symbols and voltas </li> <li>Effects/articulations (tremolo, etc.) </li> <li>Maybe a mode specifically for notating drum parts </li> <li>Possibly the ability to convert to/from the pico-8 tracker format </li> <li>Joining notes when possible (connected 8th notes, for instance) </li> <li>Ties and slurs </li> <li>key signatures </li> <li>Draw leger lines </li> <li>Start screen with nicer menus</li> <li>Reduce importer code size.</li> </ul> <p>Proof of concept version:<br /> <div><div><input type="button" value=" Show " onClick="if (this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display != '') { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = ''; this.innerText = ''; this.value = ' Hide '; } else { this.parentNode.parentNode.getElementsByTagName('div')[1].getElementsByTagName('div')[0].style.display = 'none'; this.innerText = ''; this.value = ' Show '; }"></div><div><div style="display: none;"><br /> This is just a proof of concept/work-in-progress to see how much interest I have in continuing to work on it and how much (or little) interest other people might have in using it.</p> <p>Here's a quick little gif demo. No sound obviously but imagine each note playing as it's entered and also on playback:</p> <img style="margin-bottom:16px" border=0 src="/media/54967/pi_composer p8_0.gif" alt="" /> <p>Try it out:<br /> <table><tr><td> <a href="/bbs/?pid=111852#p"> <img src="/bbs/thumbs/pico8_picomposer-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=111852#p"> picomposer</a><br><br> by <a href="/bbs/?uid=54967"> jasondelaat</a> <br><br><br> <a href="/bbs/?pid=111852#p"> [Click to Play]</a> </td></tr></table> </p> <h2>Controls and (current lack of) Features</h2> <p>Like I said, this is currently just a proof of concept and not (yet?) a useful tool so don't expect too much from it. All input is currently via keyboard. </p> <p>At the start you'll be prompted for a title, bpm and time signature. None of these matter at the moment. You're locked into 4/4 at 150bpm.</p> <h3>Note Entry</h3> <h4>Note Values</h4> <p>The top right corner shows you what note value will be inserted next. You can change this with number keys 1-5:</p> <ul> <li>1 - whole notes</li> <li>2 - half notes</li> <li>3 - quarter notes</li> <li>4 - eighth notes</li> <li>5 - sixteenth notes</li> </ul> <h4>Pitches</h4> <p>To insert actual notes of the selected value use the bottom row, keys z-m, just like entering notes in the tracker with 'z' being C and 'm' being B. At the moment it's just the bottom row/white keys but I'd obviously include the sharps/flats as well in a real tool.</p> <p>It's a bit awkward at the moment because the notes go up until F and then drops down to G. I did that because I didn't want to mess around with ledger lines at the moment and this way everything stays on the staff. In reality I'd have the notes all going up in a particular octave and then using the up/down arrow keys&mdash;or something else&mdash;to change which octave you're in.</p> <p>I'm not using the top rows for the higher octave like the tracker does because I want to reserve those keys for additional functionality like saving, adding instruments, etc.</p> <h4>Playback</h4> <p>At any time while you're entering notes press space to play the whole song from the start. When it's done you'll be back in note entry mode.</p> <h3>Future Development</h3> <p>So that's all it does at the moment. Currently there's no way to save the song, edit notes, add rests or...anything else. But here's a probably incomplete list of things I'd probably like to add at some point in no particular order.</p> <ul> <li>Save/load songs to memory and/or another cart</li> <li>Octaves for a full range of notes as mentioned above</li> <li>Clefs. As is the staff is in treble clef. Adding, at least, bass clef would be useful</li> <li>Actually allow you to enter tempo and time signature</li> <li>Bar lines</li> <li>Allow up to 4 voices and ability to switch around between them</li> <li>Allow all the pico-8 instruments and custom instruments</li> <li>Dynamics (p, f, mf, pp, crescendos, etc.)</li> <li>Repeat symbols and voltas</li> <li>Effects/articulations (tremolo, etc.)</li> <li>Maybe a mode specifically for notating drum parts</li> <li>A way to load and play the songs in carts</li> <li>Possibly the ability to convert to/from the pico-8 tracker format</li> <li>Joining notes when possible (connected 8th notes, for instance)</li> <li>Ties and slurs</li> <li>Add rests</li> <li>Select/edit/delete note(s)</li> <li>key signatures</li> </ul> <p>That's what I've got off the top of my head. Suggestions welcome.<br /> </div></div></div></p> https://www.lexaloffle.com/bbs/?tid=47819 https://www.lexaloffle.com/bbs/?tid=47819 Mon, 16 May 2022 10:44:52 UTC