<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Frank Mitchell</title>
<link href="https://www.frankmitchell.org/feed/" rel="self" />
<link href="https://www.frankmitchell.org/" />
<updated>2020-10-03T00:00:00-04:00</updated>
<id>https://www.frankmitchell.org/</id>
<author>
<name>Frank Mitchell</name>
</author>
<entry>
<title>Dewdrop Farm - A Post-Mortem</title>
<link href="/2020/10/ddf-post-mortem" />
<updated>2020-10-03T00:00:00-04:00</updated>
<published>2020-10-03T00:00:00-04:00</published>
<id>https://www.frankmitchell.org/2020/10/ddf-post-mortem</id>
<content type="html">
&lt;!--
title: Dewdrop Farm - A Post-Mortem
created: 26 September 2020
updated: 3 October 2020
publish: 3 October 2020
slug: ddf-post-mortem
tags: coding, gaming, postmortem
--&gt;

&lt;p&gt;&lt;em&gt;Dewdrop Farm&lt;/em&gt; was my entry into the &lt;a href=&quot;https://2020.js13kgames.com/&quot; title=&quot;Andrzej Mazur (js13kGames): HTML5 and JavaScript game development competition in just 13 kB&quot;&gt;2020 js13kGames competition&lt;/a&gt;. It&amp;rsquo;s
a tiny farming simulator, with an intentionally addictive compulsion loop.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;pixelated game art&quot; width=&quot;400px&quot; height=&quot;250px&quot; src=&quot;/images/dewdrop-farm-screenshot.png&quot;/&gt;&lt;/p&gt;

&lt;p&gt;You can play it online at &lt;a href=&quot;https://2020.js13kgames.com/entries/dewdrop-farm&quot; title=&quot;Frank Mitchell (js13kGames): Dewdrop Farm&quot;&gt;2020.js13kgames.com/entries/dewdrop-farm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the 9&lt;sup&gt;th&lt;/sup&gt; year I&amp;rsquo;ve entered the competition, but the first time
I&amp;rsquo;ve written a post-mortem about my game development process. I took the notes I
had in my &lt;a href=&quot;https://github.com/onefrankguy/dewdrop-farm/blob/v1.0.0/DIARY.md&quot; title=&quot;Frank Mitchell (GitHub): Dewdrop Farm v1.0.0 - Development Diary&quot;&gt;development diary&lt;/a&gt;, and flushed them out with descriptions of what
I was thinking about, example code, and screenshots.&lt;/p&gt;

&lt;h2&gt;Week 1&lt;sup&gt;st&lt;/sup&gt;&lt;/h2&gt;

&lt;p&gt;The only real idea I had going into this, was &amp;ldquo;Make a game where the passage of
time matters.&amp;rdquo; I usually pick some technical topic I want to learn about during
the competition, and build a game around that. Having never done a game where
the world runs independent of the player&amp;rsquo;s actions, I thought that would be an
interesting thing to build.&lt;/p&gt;

&lt;p&gt;I was playing &lt;em&gt;Drop7&lt;/em&gt; on my phone in the days leading up to the competition. The
designer of that game, &lt;a href=&quot;https://en.wikipedia.org/wiki/Frank_Lantz&quot; title=&quot;Wikipedia: Frank Lantz&quot;&gt;Frank Lantz&lt;/a&gt;, also made &lt;em&gt;Universal Paperclips&lt;/em&gt;, an
incremental game. So I played that, and read the paper
&lt;em&gt;&lt;a href=&quot;https://pixl.nmsu.edu/files/2018/02/2018-chi-idle.pdf&quot; title=&quot;Sultan A. Alharthi, Olaa Alsaedi, Zachary O. Toups, Joshua Tanenbaum, Jessica Hammer (New Mexico State Play &amp;amp; Interactive eXperiences for Learning Lab): Playing to Wait - A Taxonomy of Idle Games&quot;&gt;Playing to Wait: A Taxonomy of Idle Games&lt;/a&gt;&lt;/em&gt;, and
thought a lot about idle games. The 1.16 Nether update to &lt;em&gt;Minecraft&lt;/em&gt; had
recently been released, so I also had piglin bartering mechanics on my mind. I
figured making a game with an economy would be interesting.&lt;/p&gt;

&lt;p&gt;That combination of ideas reminded me of &lt;em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/SimFarm&quot; title=&quot;Wikipedia: SimFarm is a video game in which players build and manage a virtual farm&quot;&gt;SimFarm&lt;/a&gt;&lt;/em&gt;, a game I loved as a kid.
I got it in my head that I would build a mini version of &lt;em&gt;Minicraft&lt;/em&gt;, just the
farming and trading bits, rendering with a top down view so I didn&amp;rsquo;t have to
also learn 3D maths while learning about game economies.&lt;/p&gt;

&lt;h3&gt;Googling Graphics&lt;/h3&gt;

&lt;p&gt;Searching for &amp;ldquo;farm&amp;rdquo; on OpenGameArt.org turned up a crop tileset by &lt;a href=&quot;https://opengameart.org/users/josehzz&quot; title=&quot;josehzz (OpenGameArt.org): Farming crops 16x16 and related tiles&quot;&gt;josehzz&lt;/a&gt;.
That ended up defining the color palette I used for the game, &lt;a href=&quot;https://lospec.com/palette-list/aap-64&quot; title=&quot;Adigun A. Polack (Lowspec): AAP-64 Palette&quot;&gt;AAP-64&lt;/a&gt; by
Adigun A. Polack, and also the look, 16&amp;times;16 pixel sprites.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;pixelated art&quot; width=&quot;480px&quot; height=&quot;400px&quot; src=&quot;/images/dewdrop-farm-crops.png&quot;/&gt;&lt;/p&gt;

&lt;p&gt;Once I found those crop graphics, I started thinking of &lt;em&gt;&lt;a href=&quot;https://www.stardewvalley.net/&quot; title=&quot;Eric Barone (Stardew Valley): You&amp;#39;ve inherited your grandfather&amp;#39;s old farm plot in Stardew Valley&quot;&gt;Stardew Valley&lt;/a&gt;&lt;/em&gt;.
Using Eric Barone&amp;rsquo;s game as a reference point got me through the first week of
coding. The player has two tools, a hoe and water. Land needs to be tilled with
the hoe before seeds can be planted. Crops grow at various speeds. Watering
crops makes them grow faster. Land that&amp;rsquo;s watered dries out over time. Mature
crops can be harvested and sold. Coins can be used to buy seeds to grow more
crops.&lt;/p&gt;

&lt;h3&gt;Idling Away&lt;/h3&gt;

&lt;p&gt;I wanted &lt;em&gt;Dewdrop Farm&lt;/em&gt; to run even when you weren&amp;rsquo;t actively playing it. So I
used &lt;a href=&quot;https://codeincomplete.com/articles/javascript-game-foundations-the-game-loop/&quot; title=&quot;Jake Gordon: Javascript Game Foundations - The Game Loop&quot;&gt;Jake Gordon&amp;rsquo;s article on render loops&lt;/a&gt; as a starting point.
The game runs in fixed time, with an update taking place every 1/20&lt;sup&gt;th&lt;/sup&gt;
of a second.&lt;/p&gt;

&lt;p&gt;I spent a fair bit of time in those first few days figuring out how long each
&amp;ldquo;day&amp;rdquo; on the farm should last. I thought a lot about this quote by
&lt;a href=&quot;https://www.gq.com/story/stardew-valley-eric-barone-profile&quot; title=&quot;Sam White &amp;amp; Chona Kasinger (GQ): Valley Forged - How One Man Made the Indie Video Game Sensation Stardew Valley&quot;&gt;Eric Barone&lt;/a&gt; about days in &lt;em&gt;Stardew Valley&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;The psychology of it and how, by keeping the days short, it always felt like
you had time fro &amp;lsquo;one more day,&amp;rsquo; no matter how long you had been playing.
Before you realized it, hours had passed.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A day in &lt;em&gt;Stardew Valley&lt;/em&gt; lasts 14 minutes, 20 seconds. I knew I didn&amp;rsquo;t want my
days to last that long, but I started with that &amp;ldquo;14 minutes&amp;rdquo; as a reference,
and keep tweaking the math until I found something that felt right.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const SEASONS_PER_YEAR = 4;
const DAYS_PER_SEASON = 28;
const SECONDS_PER_DAY = (14 * 60 * 3) / 28 / 4;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A day in &lt;em&gt;Dewdrop Farm&lt;/em&gt; lasts 22.5 seconds. That&amp;rsquo;s long enough that when I
watched the day counter tick, I felt like it wasn&amp;rsquo;t making progress. But it&amp;rsquo;s
short enough that when I was actively doing things, like buying seeds, I found
myself asking, &amp;ldquo;Where did the time go?&amp;rdquo; This also means that the growing season,
from the start of spring to the start of winter, lasts about half an hour. That
felt like a good length of time for a play session.&lt;/p&gt;

&lt;p&gt;It was about this time that &lt;a href=&quot;https://twitter.com/RachelWenitsky/status/1296236032803864583&quot; title=&quot;Rachel Wenitsky (Twitter): are there video games where I just get to walk around?&quot;&gt;Rachel Wenitsky&lt;/a&gt; posted a thread on Twitter about
walking simulators and other idle games. That got me thinking about compulsion
loops. I knew I was building an adictive mechanic, so I wanted to create a very
deliberate &amp;ldquo;It&amp;rsquo;s okay to take a break&amp;rdquo; point to balance that out. I decided
winter would be a season where nothing grew. If you wanted to continue the game
beyond one growing season, you&amp;rsquo;d need to take a 10 minute break.&lt;/p&gt;

&lt;h3&gt;Randomizing Growth&lt;/h3&gt;

&lt;p&gt;Even though the game ran at a fixed rate, I knew I didn&amp;rsquo;t want the crops to grow
at a fixed rate. I wanted to recreate that &lt;em&gt;Minecraft&lt;/em&gt; feel, where you come back
to a field of wheat and find that some of it is ready to harvest and some of it
needs a bit more time. At the same time, I wanted to keep the economic stability
found in &lt;em&gt;Stardew Valley&lt;/em&gt;. The player should be able to figure out that it takes
four days for turnips to mature, that way they can decide if they have enough
time left in the season to plant them.&lt;/p&gt;

&lt;p&gt;What I ended up doing was randomzing the time in each day that each crop would
grow. With each update, &lt;code&gt;farm.time&lt;/code&gt; is incremented by 1/20&lt;sup&gt;th&lt;/sup&gt; of a
second. So I first figure out how many seconds into the current day we are.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const day = Math.floor(farm.time / SECONDS_PER_DAY);
const farmTime = farm.time - (day * SECONDS_PER_DAY);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then, if it&amp;rsquo;s a new day, I slice the 22.5 second day up into 36 time buckets,
one for each plot of farmland. So the first time bucket is between 0 and 0.625
seconds, the second time bucket is between 0.625 seconds and 1.25 seconds, and
so on. I randomly assign each plot of farmland to a time bucket, and pick a
random time within that bucket for it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;if (growable.day !== day) {
  let plots = PRNG.shuffle(Farm.plots(farm));
  const dt = SECONDS_PER_DAY / plots.length;
  plots = plots.map((plot, index) =&amp;gt; {
    const min = dt * (index + 0);
    const max = dt * (index + 1);
    const time = PRNG.between(min, max);

    return {
      ...plot,
      time,
      id: index,
    };
  });

  growable.day = day;
  growable.plots = plots;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;During each update I check to see if a crop on a plot of land needs to grow.
If it does, I dispatch a grow action. If it doesn&amp;rsquo;t, I save it to check again
the next update.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const plots = [];
const remaining = [];

growable.plots.forEach((plot) =&amp;gt; {
  if (plot.time &amp;lt;= farmTime &amp;amp;&amp;amp; shouldGrow(plot)) {
    plots.push(plot);
  } else {
    remaining.push(plot);
  }
});

growable.plots = remaining;

plots.forEach(({row, col}) =&amp;gt; {
  const growAction = {
    tool: &amp;#39;grow&amp;#39;,
    row,
    col,
  };

  farm = Rules.dispatch(farm, growAction);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That bit of code ended up working nicely for making crop growth feel randomly
predictable. I resued it to also decide when farmland goes fallow, when
sprinklers water crops, and when farmland dries out.&lt;/p&gt;

&lt;h3&gt;Looking Back&lt;/h3&gt;

&lt;p&gt;Here&amp;rsquo;s what the game looked like at the end of the first week.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-08-21-farm.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-08-21-store.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-08-21-market.png&quot;/&gt;&lt;/p&gt;

&lt;h2&gt;Week 2&lt;sup&gt;nd&lt;/sup&gt;&lt;/h2&gt;

&lt;p&gt;One week into the competition I had a working game, but it wasn&amp;rsquo;t really fun.
You could grow crops and sell them, but one spring harvest of turnips gave
you more coins than you could reasonably spend. So I spent some time improving
the graphics.&lt;/p&gt;

&lt;p&gt;I styled the inventory bar, plus the Buy and Sell screens. A lot of the
visual design for those was inspired by &lt;em&gt;Stardew Valley&lt;/em&gt;. I added a hoe and
watering can, plus an envelope to hold seeds, and sprinklers that automatically
watered crops. It looked more polished, but it was still a very passive game.&lt;/p&gt;

&lt;h3&gt;Finding Fun&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Dewdrop Farm&lt;/em&gt; is the first game I&amp;rsquo;ve made that didn&amp;rsquo;t start with me knowing
what &amp;ldquo;Game Over&amp;rdquo; looked liked. There was just an never ending loop of crop
gathering. Fortunately, Game Maker&amp;rsquo;s Toolkit posted a timely YouTube video about
&lt;a href=&quot;https://www.youtube.com/watch?v=kMDe7_YwVKI&quot; title=&quot;Game Maker&amp;#39;s Toolkit (YouTube): The Games That Designed Themselves&quot;&gt;letting games design themselves&lt;/a&gt;. That got me thinking about what I liked
about farming in &lt;em&gt;SimFarm&lt;/em&gt;, and foraging in &lt;em&gt;Stardew Valley&lt;/em&gt;, and mining in
&lt;em&gt;EVE Online&lt;/em&gt;. I realized I enjoyed the repetative task of gathering resouces,
balanced with the threat of an external force taking those resources away.&lt;/p&gt;

&lt;p&gt;The bunny in &lt;em&gt;Dewdrop Farm&lt;/em&gt; is that external force. It hops around and undoes
all your hard work. If it lands on a crop, it eats the crop. You can scare the
bunny by poking it, making it hop toward the edge of the farm. If it&amp;rsquo;s on an
edge when you poke it, it&amp;rsquo;ll hop off the farm, and you get a day without a
bunny. But the bunny will be back the day after that.&lt;/p&gt;

&lt;p&gt;That ended up being exactly the mechanic I needed. I found myself spending lots
of time planting crops and nudging the bunny away from them instead of writing
code.&lt;/p&gt;

&lt;h3&gt;Looking Back&lt;/h3&gt;

&lt;p&gt;Here&amp;rsquo;s what the game looked like at the end of the second week.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-08-28-farm.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-08-28-store.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-08-28-market.png&quot;/&gt;&lt;/p&gt;

&lt;h2&gt;Week 3&lt;sup&gt;rd&lt;/sup&gt;&lt;/h2&gt;

&lt;p&gt;I wanted &lt;em&gt;Dewdrop Farm&lt;/em&gt; to be a game you could pick up and discover how to play.
The hoe is the active tool when you start. That lets you immediately click
farmland and have something happen. The &amp;ldquo;Buy&amp;rdquo; and &amp;ldquo;Sell&amp;rdquo; buttons are labeled
with verbs instead of the &lt;code&gt;store&lt;/code&gt; and &lt;code&gt;market&lt;/code&gt; nouns the code uses. That makes
their actions obvious. My playtesters all figured things out without any
gameplay hints, so I figure I did &lt;em&gt;something&lt;/em&gt; right.&lt;/p&gt;

&lt;h3&gt;Managing Inventory&lt;/h3&gt;

&lt;p&gt;My original plan was that &lt;em&gt;Dewdrop Farm&lt;/em&gt; wouldn&amp;rsquo;t have inventory limits. The
four slots would be able to hold as many seeds as you could buy. Crops for
sale would show up on the Sell screen, not in your inventory. But that was
unsatisfying, because you didn&amp;rsquo;t see progress as you harvested crops. The
graphics changed, but none of the visible numbers went up.&lt;/p&gt;

&lt;p&gt;Also, I didn&amp;rsquo;t know what to do if the number of crops you had for sale or seeds
you had in your inventory got really large. I needed to limit it so the counters
on the seeds and crops wouldn&amp;rsquo;t obscure the image. So I capped it at 16 because
that looked nice.&lt;/p&gt;

&lt;p&gt;That ended up being a good number, because it means a field of seeds takes up a
bit over half your inventory. So you need to make two trips to the Sell screen
if you want a full field of fertilized crops. It also doesn&amp;rsquo;t divide evenly into
6, the length of a row on the farm, so you have to be a bit precise in your
shopping if you want to plant rows of crops without leftover seeds.&lt;/p&gt;

&lt;p&gt;Once I had inventory limits, I added logic to stop you from buying seeds or
harvesting crops if they wouldn&amp;rsquo;t fit in your inventory. I didn&amp;rsquo;t want an
accidental click to ruin hard work. The inventory fill algorithm comes from
&lt;em&gt;Minecraft&lt;/em&gt;. Slots are looped through from left to right.  Items go in the first
matching slot, or the first empty slot if there isn&amp;rsquo;t a matching slot. Items
also sell from the inventory left to right.&lt;/p&gt;

&lt;p&gt;To draw the Sell screen, I loop over the inventory items from left to right, and
render a row for each. Like items stack, and when a row sells out, it vanishes.
This allows you to sell an entire inventory of crops by continuously clicking
the top button.&lt;/p&gt;

&lt;h3&gt;Clicking Around&lt;/h3&gt;

&lt;p&gt;One of the earliest mechanics I added was allowing you to click and drag to
plant seeds, harvest crops, and water the farm. It makes playing on a desktop
with a mouse a lot of fun. When I switched my testing to a laptop, I realized
that clicking and dragging with a trackpad is quite a bit slower. So I added the
keyboard shorcuts to make up for that. You can keep the cursor over the farm,
and every clickable button you need is in that space.&lt;/p&gt;

&lt;p&gt;I never figured out how to get &lt;code&gt;touchmove&lt;/code&gt; events to register on a phone. It&amp;rsquo;s
something I&amp;rsquo;d like to solve in a post competition version. Mobile play is very
much an exercise in tapping, which has its own charm, but isn&amp;rsquo;t the experience
I wanted.&lt;/p&gt;

&lt;p&gt;Once I realized buying seeds and selling crops was going to involve a lot of
clicking, I tried to find ways to make that enjoyable. I replayed &lt;em&gt;Clicker Heroes&lt;/em&gt;
to remmber why button clicking felt fun there. Having buttons that animate on
&lt;code&gt;mousedown&lt;/code&gt; and &lt;code&gt;mouseup&lt;/code&gt; was key. Items also only buy or sell on &lt;code&gt;mouseend&lt;/code&gt;
when the cursor is on the button. So you can cancel a purchase or sale by moving
the cursor off the button and letting go of the mouse.&lt;/p&gt;

&lt;p&gt;I also made the coin graphic &amp;ldquo;animate&amp;rdquo; when items are bought and sold. I did
that by fixing the left edge of the coin amount (the numbers) and bumping the
coin graphic up against their right edge. Since the numbers in the Georgia font
are variable width, the coin graphic moves a little bit back and forth when the
numbers change.&lt;/p&gt;

&lt;h3&gt;Looking Back&lt;/h3&gt;

&lt;p&gt;Here&amp;rsquo;s what the game looked like at the end of the third week.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-09-04-farm.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-09-04-store.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-09-04-market.png&quot;/&gt;&lt;/p&gt;

&lt;h2&gt;Week 4&lt;sup&gt;th&lt;/sup&gt;&lt;/h2&gt;

&lt;h3&gt;Changing Seasons&lt;/h3&gt;

&lt;p&gt;This is my favorite piece of code in &lt;em&gt;Dewdrop Farm&lt;/em&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.magic  { transition: background 22.5s linear; }
.spring { background: rgb(146, 220, 186); }
.summer { background: rgb(156, 219, 67); }
.fall   { background: rgb(233, 181, 163); }
.winter { background: rgb(185, 191, 251); }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Those are the &lt;abbr title=&quot;Cascading Style Sheet&quot;&gt;CSS&lt;/abbr&gt; rules that make the background colors change over the course
of the first day of each season. I picked colors that are close in luminance to
each other, because I didn&amp;rsquo;t want the screen brightness to change abrubtly.&lt;/p&gt;

&lt;p&gt;Sarah Mitchell gave me a great accessibility testing tip, which was to play the
game on my phone with the screen brightness turned all the way down. She
prompted me to make the water texture brighter and to recolor all the seeds so
they showed more contrast against the farmland.&lt;/p&gt;

&lt;p&gt;I wanted an &lt;em&gt;Animal Crossing: New Horizons&lt;/em&gt; kind of look to the water, droplets
sprinkled on top of the crops. So they render over everything to keep them
visible, and they&amp;rsquo;re randomly rotated to try and hide the fact there&amp;rsquo;s only one
water graphic.&lt;/p&gt;

&lt;h3&gt;Optimizing Graphics&lt;/h3&gt;

&lt;p&gt;You&amp;rsquo;ll find some odd little experiments if you dig into the code, like
&lt;a href=&quot;https://github.com/onefrankguy/dewdrop-farm/blob/v1.0.0/png2code.js&quot; title=&quot;Frank Mitchell (GitHub): Dewdrop Farm v1.0.0 - png2code.js&quot;&gt;png2code.js&lt;/a&gt;. That was my attempt to convert a PNG image into a run length
encoded data format, that&amp;rsquo;s unpacked and rendered as a SVG inside the browser.
For a long time during development, the farmland and crop graphics took up a ton
of space. So I tried a bunch of things to try and shrink them down.&lt;/p&gt;

&lt;p&gt;I started by putting all the graphics in a single image. Then I culled crops,
cutting them down from the original 20 to a final 6. To keep some variety, I
changed the seasons they&amp;rsquo;re buyable and growable in. Each growing season has one
crop that&amp;rsquo;s unique to it, one crop that also appears in the season before it,
and one crop that also appears in the season after it. That gave me three crops
per season.&lt;/p&gt;

&lt;p&gt;Each crop has a seed image, four growing stage images, and a harvested image.
So six images per crop. But the wildflowers you can buy in winter all use
the same seed and first three growing stage images. The last growing stage image
is unique for each (tulip, rose, sunflower), and I reuse it for their harvested
image. That let me add three more crops to the farm with only seven images.&lt;/p&gt;

&lt;p&gt;Even those tricks weren&amp;rsquo;t enough though. What finally ended up working was
three things. First, I manually repacked the images into a single vertical
strip, placing similar looking images next to each other. That let the PNG
compression algorithm save some bytes, because the pixels didn&amp;rsquo;t change much
from one row to the next. A tall vertical strip ended up compressing a little
bit better than a wide horizontal strip, and was also easier to edit. Second, I
ran the image through &lt;a href=&quot;https://imageoptim.com/&quot; title=&quot;Kornel Lesinski: (ImageOptim): Save disk space &amp;amp; bandwidth by compressing images without losing quality&quot;&gt;ImageOptim&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;m not sure what magic ImageOptim is doing under the covers, but it ended up
compressing my image better than anything else I found. I wish there was a
command line version, because it would be a lovely thing to bake into a build
system.&lt;/p&gt;

&lt;p&gt;The third thing I did was embed the image in a data URI in the &lt;abbr title=&quot;Cascading Style Sheet&quot;&gt;CSS&lt;/abbr&gt;, and embed
the &lt;abbr title=&quot;Cascading Style Sheet&quot;&gt;CSS&lt;/abbr&gt; in the &lt;abbr title=&quot;HyperText Markup Language&quot;&gt;HTML&lt;/abbr&gt;. Those three thigns ended up saving 837 bytes. That gave me
enough space to add the graphics and logic for the cow.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;pixelated art&quot; width=&quot;64px&quot; height=&quot;64px&quot; src=&quot;/images/dewdrop-farm-cow.png&quot;/&gt;&lt;/p&gt;

&lt;p&gt;In many ways, buying the cow is very much the end game. At 150,000 coins, it
looks like it will take a long time to get there. But eggplants have a 70 coin
profit, and they show up in the fall next to the cow. My hope is that&amp;rsquo;s enough
of a clue for players to figure out that you &lt;em&gt;can&lt;/em&gt; keep playing through winter
and get enough coins to buy the cow.&lt;/p&gt;

&lt;h3&gt;Looking Back&lt;/h3&gt;

&lt;p&gt;Here&amp;rsquo;s what the game looked like at the end of the fourth week.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-09-11-farm.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-09-11-store.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-09-11-market.png&quot;/&gt;
&lt;img class=&quot;pixelated game art&quot; width=&quot;640px&quot; height=&quot;888px&quot; src=&quot;/images/dewdrop-farm-2020-09-11-info.png&quot;/&gt;&lt;/p&gt;

&lt;h2&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;There&amp;rsquo;s a lot more I could say (and maybe will!) about &lt;em&gt;Dewdrop Farm&lt;/em&gt;, but I&amp;rsquo;ve
tried to limit this post-mortem to things that are unique to that game. All the
mechanics of making a video game, putting graphics on the screen and changing
them based on player input, are stuff I&amp;rsquo;ve written about in
&lt;em&gt;&lt;a href=&quot;https://onefrankguy.github.io/nine-holes/&quot; title=&quot;Frank Mitchell (GitHub): How to Make a Video Game - Nine Holes&quot;&gt;How to Make a Video Game&lt;/a&gt;&lt;/em&gt;. It&amp;rsquo;s the tutorial I wanted when I started
making JavaScript games ten years ago. Give it a read, and maybe I&amp;rsquo;ll get to
play your game in the next js13kGames competition.&lt;/p&gt;

&lt;p&gt;Until then, happy farming!&lt;/p&gt;</content>
</entry>
<entry>
<title>Who else wants narrative mechanics in RPGs?</title>
<link href="/2019/11/roll-over" />
<updated>2019-11-17T00:00:00-05:00</updated>
<published>2019-11-17T00:00:00-05:00</published>
<id>https://www.frankmitchell.org/2019/11/roll-over</id>
<content type="html">
&lt;!--
title: Who else wants narrative mechanics in RPGs?
created: 17 November 2019 - 9:21 am
updated: 24 November 2019 - 7:12 am
publish: 17 November 2019
slug: roll-over
tags: coding, gaming, rpg
--&gt;

&lt;p&gt;&lt;a href=&quot;https://www.flatlandgames.com/btw/&quot; title=&quot;Flatland Games: Beyond the Wall and Other Adventures&quot;&gt;&lt;em&gt;Beyond the Wall and Other Adventures&lt;/em&gt;&lt;/a&gt; has three types of tests. There
are ability checks, saving throws, and combat rolls. My previous post was about
&lt;a href=&quot;/2019/11/roll-under&quot; title=&quot;Frank Mitchell: Who else wants unified mechanics in RPGs?&quot;&gt;changing the mechanics of ability checks&lt;/a&gt;. I wanted to have all three tests
use the same mechanic. Roll a twenty sided dice, add bonuses, and compare with
a target value.&lt;/p&gt;

&lt;p&gt;My reason for wanting unified dice mechanics is that it makes the game less
confusing. I got confused while reading the rules. I figured if it confused me,
anyone I was playing with was going to get confused as well. Now I&amp;rsquo;m reading
through the rules again. I&amp;rsquo;m realizing that part of my confusion comes from the
language used to ask for tests.&lt;/p&gt;

&lt;p&gt;Ability checks, as a test of player skill, use this kind of language.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Player: &amp;ldquo;I try to jump across the stream.&amp;rdquo;&lt;br /&gt;
Gamemaster: &amp;ldquo;Roll to check dexterity.&amp;rdquo;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So rolling the dice is a test to determine something about the player&amp;rsquo;s skill.
Am I skilled enough to jump across the stream or do I fall in the water? It&amp;rsquo;s
a test every player that wants to jump the stream must pass on their own. We
can&amp;rsquo;t use a group skill check, because my stream jumping skill won&amp;rsquo;t help
you.&lt;/p&gt;

&lt;p&gt;But ability checks can also be a test to determine something about the
environment. Those ability checks use this kind of language.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Player: &amp;ldquo;I try to jump across the stream.&amp;rdquo;&lt;br /&gt;
Gamemaster: &amp;ldquo;Roll to see how slippery the bank is.&amp;rdquo;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This opens up interesting possibilities. Say I&amp;rsquo;ve got a dexterity of
16 and I roll an 18. The stream bank is more slippery than my dexterity can
overcome, so I land in the water. Other players can decide they&amp;rsquo;re going to
jump over a different part of the stream. They make their own ability checks,
and hopefully find a less slippery spot. Of if I roll a 7, the other players
may decide to follow me, since the bank isn&amp;rsquo;t very slippery there.&lt;/p&gt;

&lt;p&gt;This feels like a collaboration. Everyone&amp;rsquo;s rolling to figure stuff out
about the world we&amp;rsquo;re playing in. It&amp;rsquo;s also not going to break the game, since
I&amp;rsquo;m not changing how ability checks are used or how they map to bonuses.
Rolling under for ability checks now feels like a natural thing. I know what
my character&amp;rsquo;s ability score is and what they can do. I&amp;rsquo;m rolling for the
unknown, to see what the environment does.&lt;/p&gt;

&lt;p&gt;&lt;hr /&gt;&lt;/p&gt;

&lt;p&gt;Saving throws have a similar linguistic duality to ability checks. Suppose a
mage casts entanglement on some vines near the player. As a test of player
skill, the saving throw for that spell uses this kind of language.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Player: &amp;ldquo;I try to escape the vines. &amp;rdquo;&lt;br /&gt;
Gamemaster: &amp;ldquo;Roll to save versus spell.&amp;rdquo; &lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But saving throws can also be a test to determine something about the
environment. The entanglement saving throw could use this kind of language
instead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Player: &amp;ldquo;I try to escape the vines.&amp;rdquo;&lt;br /&gt;
Gmaemaster: &amp;ldquo;Roll to see how quickly the vines grow.&amp;rdquo;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the player is a level 2 rouge, their save versus spell is 15. So they try
to roll greater than or equal to 15 on a twenty side dice. If they succeed,
the vines grow slowly and they may be able to escape. But if they fail, the
vines grow quickly and they become entangled.&lt;/p&gt;

&lt;p&gt;I like this idea of ability checks and saving throws fleshing out details about
the world. It&amp;rsquo;d be nice to keep the mechanics consistent with the language. If
I&amp;rsquo;m more resistant to spells, my save versus spell value should be higher.
Fortunately, that&amp;rsquo;s an easy thing to change by rewriting the saving throw
tables for each class.&lt;/p&gt;

&lt;p&gt;When rolling a twenty sided dice, there are six rolls that succeed with a save
versus spell of 15. So rolling under on a 6 is equivalent to rolling over on a
15. Here&amp;rsquo;s the complete conversion table from 1 to 20.&lt;/p&gt;

&lt;table class=&quot;stats&quot;&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;Roll Over&lt;/th&gt;
    &lt;th&gt;Roll Under&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;19&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;18&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;17&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;16&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;15&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;14&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;13&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;13&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;14&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;16&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;17&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;18&lt;/td&gt;&lt;td&gt;3&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;19&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;20&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;I did those conversions for the warrior, rogue, and mage, and came up with new
tables. My copy of &lt;em&gt;Beyond the Wall&lt;/em&gt; shows the rogue&amp;rsquo;s polymorph save going
from 12 to 13 between levels 2 and 3. I assume that&amp;rsquo;s a misprint, so I&amp;rsquo;ve
corrected it in the table below.&lt;/p&gt;

&lt;h3&gt;The Warrior&lt;/h3&gt;

&lt;table class=&quot;stats&quot;&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;Level&lt;/th&gt;
    &lt;th&gt;Poison&lt;/th&gt;
    &lt;th&gt;Breath Weapon&lt;/th&gt;
    &lt;th&gt;Polymorph&lt;/th&gt;
    &lt;th&gt;Spell&lt;/th&gt;
    &lt;th&gt;Magic Item&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;&lt;th&gt;1&lt;/th&gt;&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;2&lt;/th&gt;&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;3&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;4&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;5&lt;/th&gt;&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;6&lt;/th&gt;&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;7&lt;/th&gt;&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;8&lt;/th&gt;&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;9&lt;/th&gt;&lt;/td&gt;&lt;td&gt;13&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;10&lt;/th&gt;&lt;/td&gt;&lt;td&gt;13&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3&gt;The Rogue&lt;/h3&gt;

&lt;table class=&quot;stats&quot;&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;Level&lt;/th&gt;
    &lt;th&gt;Poison&lt;/th&gt;
    &lt;th&gt;Breath Weapon&lt;/th&gt;
    &lt;th&gt;Polymorph&lt;/th&gt;
    &lt;th&gt;Spell&lt;/th&gt;
    &lt;th&gt;Magic Item&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;&lt;th&gt;1&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;2&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;3&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;4&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;5&lt;/th&gt;&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;6&lt;/th&gt;&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;7&lt;/th&gt;&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;8&lt;/th&gt;&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;9&lt;/th&gt;&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;10&lt;/th&gt;&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3&gt;The Mage&lt;/h3&gt;

&lt;table class=&quot;stats&quot;&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;Level&lt;/th&gt;
    &lt;th&gt;Poison&lt;/th&gt;
    &lt;th&gt;Breath Weapon&lt;/th&gt;
    &lt;th&gt;Polymorph&lt;/th&gt;
    &lt;th&gt;Spell&lt;/th&gt;
    &lt;th&gt;Magic Item&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;&lt;th&gt;1&lt;/th&gt;&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;2&lt;/th&gt;&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;3&lt;/th&gt;&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;4&lt;/th&gt;&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;5&lt;/th&gt;&lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;6&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;7&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;8&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;9&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;th&gt;10&lt;/th&gt;&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;12&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</content>
</entry>
<entry>
<title>Who else wants unified mechanics in RPGs?</title>
<link href="/2019/11/roll-under" />
<updated>2019-11-10T00:00:00-05:00</updated>
<published>2019-11-10T00:00:00-05:00</published>
<id>https://www.frankmitchell.org/2019/11/roll-under</id>
<content type="html">
&lt;!--
title: Who else wants unified mechanics in RPGs?
created: 10 November 2019 - 9:05 am
updated: 17 November 2019 - 10:00 am
publish: 10 November 2019
slug: roll-under
tags: coding, gaming, rpg
--&gt;

&lt;p&gt;I&amp;rsquo;m thinking about running a &lt;a href=&quot;https://www.flatlandgames.com/btw/&quot; title=&quot;Flatland Games: Beyond the Wall and Other Adventures&quot;&gt;&lt;em&gt;Beyond the Wall and Other Adventures&lt;/em&gt;&lt;/a&gt; game,
so I sat down and read through the rules. Everything felt very familiar.
Characters can be one of three types: warriors, rogues, or mages. Levels range
from 1&lt;sup&gt;st&lt;/sup&gt; (a bit above ordinay) to 10&lt;sup&gt;th&lt;/sup&gt; (heroes of the
land). Ability scores range from 1 to 19, and each character has six abilities:
strength, dexterity, constitution, intelligence, wisdom, and charisma.&lt;/p&gt;

&lt;p&gt;I use abilities by rolling a twenty sided dice (1d20) and trying to get less
than or equal to my ability score. So if want to jump over a stream, and my
dexterity is 16, rolling a 19 means I fall in the water.&lt;/p&gt;

&lt;p&gt;I also get to use abilities in combat. Ability scores map to bonuses. I attack
by rolling a twenty sided dice, adding my bonuses, and trying to get greater
than or equal to my opponent&amp;rsquo;s armor. So if I want to shoot a bow at a
cockatrice (14 armor), and I&amp;rsquo;m a 1&lt;sup&gt;st&lt;/sup&gt; level rogue (+0 bonus)
whose dexterity is 16 (+2 bonus), rolling a 19 means my I hit the cockatrice.&lt;/p&gt;

&lt;p&gt;Wait, what? Rolling a 19 when shooting a bow is a success, but rolling a 19 when
jumping is a failure. I find that confusing. I kind of expect that when I&amp;rsquo;m
rolling a twenty sided dice, 1 is going to be a failure and 20 is going to
be a success, and the stuff in the middle is going to be a probability curve
with bigger numbers meaning &amp;ldquo;more likely to be a success&amp;rdquo;.&lt;/p&gt;

&lt;p&gt;New player experiences matter. I was teaching a friend &lt;a href=&quot;https://boardgamegeek.com/boardgame/174430/gloomhaven&quot; title=&quot;Various (Board Game Geek): Gloomhaven&quot;&gt;Gloomhaven&lt;/a&gt;, and we
talked about how lowest initiative goes first, and they said, &amp;ldquo;That doesn&amp;rsquo;t make
any sense. If I have more initiative, I should go first.&amp;rdquo; They were right. I&amp;rsquo;ve
played Gloomhaven so much that I&amp;rsquo;ve internalized the &amp;ldquo;lower initiative goes
first&amp;rdquo; rule, so I didn&amp;rsquo;t bother to question it.&lt;/p&gt;

&lt;p&gt;But I haven&amp;rsquo;t played &lt;em&gt;Beyond the Wall&lt;/em&gt;, yet. So I&amp;rsquo;m going to question these
&amp;ldquo;roll low for ability checks; roll high for combat&amp;rdquo; rules, and see how I might
change them.&lt;/p&gt;

&lt;p&gt;&lt;hr /&gt;&lt;/p&gt;

&lt;p&gt;What does it mean, mathematically, for my character to have a dexterity of 16?
It means that I&amp;rsquo;m going to succeed at 80% of my dexterity checks, because 16
divided by 20 is 0.8. So the probability of any ability check succeeding is the
value of that check divided by 20.&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;P(A) =
&lt;span class=&quot;fraction&quot;&gt;
&lt;span class=&quot;fup&quot;&gt;A&lt;/span&gt;
&lt;span class=&quot;bar&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;fdn&quot;&gt;20&lt;/span&gt;
&lt;/p&gt;

&lt;p&gt;Here&amp;rsquo;s a table of ability scores, from 1 to 19, as a percentage success rate.&lt;/p&gt;

&lt;table class=&quot;stats&quot;&gt;
&lt;thead&gt;
  &lt;tr&gt;&lt;th&gt;Ability Score&lt;/th&gt;&lt;th&gt;Success Rate&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;5%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;10%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;15%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;20%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;25%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;6&lt;/td&gt;&lt;td&gt;30%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;7&lt;/td&gt;&lt;td&gt;35%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;40%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;9&lt;/td&gt;&lt;td&gt;45%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;50%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;11&lt;/td&gt;&lt;td&gt;55%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;12&lt;/td&gt;&lt;td&gt;60%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;13&lt;/td&gt;&lt;td&gt;65%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;14&lt;/td&gt;&lt;td&gt;70%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;75%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;16&lt;/td&gt;&lt;td&gt;80%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;17&lt;/td&gt;&lt;td&gt;85%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;18&lt;/td&gt;&lt;td&gt;90%&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;19&lt;/td&gt;&lt;td&gt;95%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;If I want to change the math for ability scores, I can&amp;rsquo;t break this table.
Having a dexterity of 16 should always mean I have an 80% chance of success on
a dexterity ability check.&lt;/p&gt;

&lt;p&gt;I want ability checks to feel simlar to combat rolls. I roll a twenty sided
dice, add my ability score, and check if the total is greater than or equal to
a target number. If it is, I succeed; otherwise, I fail.&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;1d20 + A &amp;ge; ?&lt;/p&gt;

&lt;p&gt;What should that target number be? The design notes in Ben Milton&amp;rsquo;s game
&lt;a href=&quot;https://www.drivethrurpg.com/product/250888/Knave&quot; title=&quot;Ben Milton (DriveThruRPG): Knave&quot;&gt;&lt;em&gt;Knave&lt;/em&gt;&lt;/a&gt;, have some clues.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Requiring saves to exceed 15 means that new PCs have around a 25% chance of
success, while level 10 characters have around a 75% chance of success, since
ability bonuses can get up to +10 by level 10. This reflects the general
pattern found in the save mechanics of early D&amp;amp;D.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ability bonuses in &lt;em&gt;Knave&lt;/em&gt; range from 0 to 6. Characters make saving throws by
rolling a twenty sided dice, adding their ability bonus, and checking to see if
the total is greater than 15. If it is, they succeed; otherwise, they fail.
That&amp;rsquo;s pretty much what I want, so I need some way to map the ability scores in
&lt;em&gt;Beyond the Wall&lt;/em&gt; to the ability bonuses in &lt;em&gt;Knave&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The character playbooks in &lt;em&gt;Beyond the Wall&lt;/em&gt; start most ability scores at 8, a
40% success rate. So I can subtract 8 from an ability score to get something
that fits in the smaller range of the &lt;em&gt;Knave&lt;/em&gt; system.&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;1d20 + A - 8 &amp;gt; ?&lt;/p&gt;

&lt;p&gt;Witht this formula, a target value of 15 gives a new character a 25% success
rate. The success rate is the number of ways to roll a twenty sided dice and
get greater than the target value. Mathematically, that&amp;rsquo;s 20 minus the target
value, divided by 20.&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;S =
&lt;span class=&quot;fraction&quot;&gt;
&lt;span class=&quot;fup&quot;&gt;20 - T&lt;/span&gt;
&lt;span class=&quot;bar&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;fdn&quot;&gt;20&lt;/span&gt;
&lt;/p&gt;

&lt;p&gt;Because I don&amp;rsquo;t want to break &lt;em&gt;Beyond the Wall&lt;/em&gt;, I need to find a target value
that keeps the success rate for new characters stays at 40%.&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;0.4 =
&lt;span class=&quot;fraction&quot;&gt;
&lt;span class=&quot;fup&quot;&gt;20 - T&lt;/span&gt;
&lt;span class=&quot;bar&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;fdn&quot;&gt;20&lt;/span&gt;
&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;20 &amp;times; 0.4 = 20 - T&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;8 = 20 - T&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;8 + T = 20&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;T = 20 - 8&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;T = 12&lt;/p&gt;

&lt;p&gt;I can use a target value of 12 for ability chceks. Since I subtracted 8 to move
ability scores into a &lt;em&gt;Knave&lt;/em&gt; range, I can add 8 to move them back to a
&lt;em&gt;Beyond the Wall&lt;/em&gt; range. Finally, I can change &amp;ldquo;greater than&amp;rdquo; to &amp;ldquo;greater than
or equal&amp;rdquo;, so ability checks follow the same &amp;ldquo;my character wins ties&amp;rdquo; rule as
combat rolls.&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;1d20 + A - 8 &amp;gt; 12&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;1d20 + A &amp;gt; 12 + 8&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;1d20 + A &amp;gt; 20&lt;/p&gt;

&lt;p class=&quot;math&quot;&gt;1d20 + A &amp;ge; 21&lt;/p&gt;

&lt;p&gt;So I can use abilities in &lt;em&gt;Beyond the Wall&lt;/em&gt; by rolling a twenty sided dice,
adding my ability score, and trying to get greater than or equal to 21.&lt;/p&gt;

&lt;p&gt;Does the success rate math still work? If I&amp;rsquo;ve got a dexterity of 16, that
should be an 80% success rate. If I roll a 5 or more, I&amp;rsquo;ll pass the skill check.
There are sixteen ways to do that with a twenty sided dice, and 16 divided by
20 is 0.8. It works!&lt;/p&gt;

&lt;p&gt;&lt;hr /&gt;&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;m not the first person to figure this out. As Saelorn points out in &lt;a href=&quot;https://www.enworld.org/threads/pre-3e-mechanics-vs-d20-system-mechanics.646646/#post-7446673&quot;&gt;an En
World thread about dice mechanics&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you already have the concept of DCs [difficulty checks] in place for things
like attack rolls and saving throws, then you could use a mechanic of d20 +
stat score against a constant DC 21, and it would give the exact same
distribution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now all my checks can use a unified mechanic: roll a twenty sided dice, add
bonuses, and compare with a target value. An unmodified roll of 20 is always
a success, while a 1 is always a failure.&lt;/p&gt;</content>
</entry>
<entry>
<title>Algorithms matter on the mobile web</title>
<link href="/2019/06/fast-code" />
<updated>2019-06-02T00:00:00-04:00</updated>
<published>2019-06-02T00:00:00-04:00</published>
<id>https://www.frankmitchell.org/2019/06/fast-code</id>
<content type="html">
&lt;!--
title: Algorithms matter on the mobile web
created: 1 June 2019 - 8:56 am
updated: 2 June 2019 - 10:05 am
publish: 2 June 2019
slug: fast-code
tags: coding, mobile
--&gt;

&lt;p&gt;Leo Fabrikant&amp;rsquo;s &lt;a href=&quot;https://levelup.gitconnected.com/secrets-of-javascript-a-tale-of-react-performance-optimization-and-multi-threading-9409332d349f&quot; title=&quot;Leo Fabrikant (gitconnected): Secrets of JavaScript: A tail of React, performance optimization and multi-threading&quot;&gt;article on optimizing the performance of a React autocomplete
form&lt;/a&gt; is worth reading. It covers performance profiling, async
rendering, and multi-threading with Web Workers. I loved that he outlined the
conditions that pushed him toward focusing on optimizing the rendering pipeline.
What we know creates the set of spaces where we look for solutions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The search algorithm library had painfully long search times as the length of
the search term got longer&amp;hellip;I don&amp;rsquo;t know if the library I chose for the
search algorithm was bad or if this was an inevitability of any &amp;ldquo;fuzzy&amp;rdquo; search
algorithm. But thankfully, I didn&amp;rsquo;t bother trying to find alternatives.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I built my &lt;a href=&quot;/2010/09/small-code/&quot; title=&quot;Frank Mitchell: Bytes matter on the mobile web&quot;&gt;word search game&lt;/a&gt;, I spent a fair bit of time learning
how to store and search strings efficiently. So when I read Leo&amp;rsquo;s article, I
thought about data structures like &lt;a href=&quot;http://blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK-Trees&quot; title=&quot;Nick Johnson (Nick&amp;#39;s Blog): Damn Cool Algorithms, Part 1: BK-Trees&quot;&gt;BK-trees&lt;/a&gt; and algorithms for finding the
&lt;a href=&quot;https://en.wikipedia.org/wiki/Levenshtein_distance&quot; title=&quot;Various (Wikipedia): Levenshtein distance&quot;&gt;Levenshtein distance&lt;/a&gt;. My experience says long search terms
shouldn&amp;rsquo;t mean long search times. So let&amp;rsquo;s see if we can build a better search
engine.&lt;/p&gt;

&lt;h2&gt;Sometimes you need to DIY&lt;/h2&gt;

&lt;p&gt;We&amp;rsquo;ll need some strings to work with that satisfy the original requirements.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is a data set retrieved from a backend. It contains 13,000 items with
very long, wordy names (Scientific Organizations). Make a search bar with an
auto-suggest using this data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I don&amp;rsquo;t have a list of very long, wordy scientific organizations handy. However,
the &lt;a href=&quot;https://github.com/mdeff/fma&quot; title=&quot;Various (GitHub): FMA - A Dataset for Music Analysis&quot;&gt;Free Music Archive&lt;/a&gt; has 19,212 tracks in it with names of five or more
words. I figure that&amp;rsquo;s a pretty equivalent data set.&lt;/p&gt;

&lt;p&gt;We&amp;rsquo;ll also want some JavaScript to benchamrk different fuzzy search algorithms.
Let&amp;rsquo;s assumes a worst case scenario, where the user&amp;rsquo;s typed out the entire song
name, and the song they&amp;rsquo;re looking for isn&amp;rsquo;t in the database.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const query = &amp;#39;Where the Streets Have No Name&amp;#39;;

const data = [...new Set(require(&amp;#39;./tracks.json&amp;#39;))];
const tracks = data.filter(track =&amp;gt; {
  return track.split(/\s+/).length &amp;gt;= 5;
});

const start = Date.now();

const matches = tracks.map(track =&amp;gt; {
  const score = compare(query, track);

  return {score, track};
}).sort((a, b) =&amp;gt; {
  return a.score - b.score;
}).slice(0, 10)
.map(info =&amp;gt; info.track);

const elapsed = Date.now() - start;

console.log(`
${matches.join(&amp;#39;\n&amp;#39;)}

Searched ${tracks.length} tracks in ${elapsed} milliseconds
`);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We need a comparison function that scores two strings based on how similar they
are. To make it easy to sort results, we&amp;rsquo;ll say that a smaller score means the
strings are more similar. The Levenshtein distance metric is the reference
measurement for string similarity. It counts the number of edits it would take
to make two strings identical.&lt;/p&gt;

&lt;p&gt;Here&amp;rsquo;s a memoized recursive implementation.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function compare(s, t, memo = {}) {
  const args = [s, t];

  if (args in memo) {
    return memo[args];
  }

  if (!s) {
    memo[args] = t.length;
    return t.length;
  }

  if (!t) {
    memo[args] = s.length;
    return s.length;
  }

  const snext = s.slice(1);
  const tnext = t.slice(1);

  const cost = s[0] !== t[0];
  const delCost = compare(snext, t, memo) + 1;
  const insCost = compare(s, tnext, memo) + 1;
  const subCost = compare(snext, tnext, memo) + cost;
  const minCost = Math.min(delCost, insCost, subCost);

  memo[args] = minCost;
  return minCost;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I ended up needing to memoize it, because the non-memoized version took too
long. As it is, even the memoized version takes about two minutes to find
matches.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Where Childrens Have a Place
Where the Walls Have a Soul
When the Guests Have Left
The Extra Party Has No Name
So, What If I Have No Name?
The One With No Name
Where The Land Meets The Sea
When the Lights Came On
This Game Has No Name
Down the Streets (Life Beyond)

Searched 19212 tracks in 122963 milliseconds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&amp;rsquo;s not something I&amp;rsquo;d want to use in an autocomplete form. My general rule
of thumb for UI responsiveness is that anything more than a third of a second
(about 300 milliseconds) is too long. Fortunately, the Levenshtein distance
metric has an iterative implementation that avoids the recursion and
memoization.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function compare(s, t) {
  let v0 = [];
  let v1 = [];

  for (let i = 0; i &amp;lt;= t.length; i += 1) {
    v0[i] = i;
  }

  for (let i = 0; i &amp;lt; s.length; i += 1) {
    v1[0] = i + 1;

    for (let j = 0; j &amp;lt; t.length; j += 1) {
      const delCost = v0[j + 1] + 1;
      const insCost = v1[j] + 1;
      const subCost = v0[j] + (s[i] !== t[j]);
      v1[j + 1] = Math.min(delCost, insCost, subCost);
    }

    [v0, v1] = [v1, v0];
  }

  return v0[t.length];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That takes about a third of a second and finds the same matches.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Where Childrens Have a Place
Where the Walls Have a Soul
When the Guests Have Left
The Extra Party Has No Name
So, What If I Have No Name?
The One With No Name
Where The Land Meets The Sea
When the Lights Came On
This Game Has No Name
Down the Streets (Life Beyond)

Searched 19212 tracks in 322 milliseconds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Can we do any better? Sure! I turns out the &lt;a href=&quot;https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient&quot; title=&quot;Various (Wikipedia): Sorensen-Dice coefficient&quot;&gt;Sorensen-Dice coefficient&lt;/a&gt; of
the sets of bigrams in two strings makes a pretty good fuzzy match. We have to
negate the score though, because a larger value means the strings are more
similar.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function bigrams(string) {
  const result = [];

  for (let i = 0; i &amp;lt; string.length - 1; i += 1) {
    result.push(string.slice(i, i + 2));
  }

  return result;
}

function compare(s, t) {
  const sGrams = bigrams(s);
  const tGrams = bigrams(t);

  const hits = sGrams.filter(n =&amp;gt; {
    return tGrams.includes(n);
  }).length;

  const total = sGrams.length + tGrams.length;
  const score = (hits * 2) / total;

  return -score;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We get a different set of songs, which makes sense, because we changed the
algorithm. I&amp;rsquo;m not sure if the 64 millisecond speed improvement is noise or not.
I&amp;rsquo;d need to plug it into a more robust benchmarking tool (like &lt;a href=&quot;https://benchmarkjs.com/&quot; title=&quot;Mathias Bynens &amp;amp; John-David Dalton: Benchmark.js&quot;&gt;Benchmark.js&lt;/a&gt;)
to measure that.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;No Secrets In the H4C
The Streets of New York
Where the Walls Have a Soul
Liberty Is In the Street
The Waves Call Her Name
The Extra Party Has No Name
Nameless: the Hackers Title Screen
There is Nothing to Fear
Down the Streets (Life Beyond)
So, What If I Have No Name?

Searched 19212 tracks in 258 milliseconds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What about the quality of the results? The Sorensen-Dice coefficient feels like
it does a better job of finding relevant songs when the words in the query are
out of oder. The Levenshtein distance feels like it does a better job when you
know the name of the song you want.&lt;/p&gt;

&lt;h2&gt;Where do we go from here?&lt;/h2&gt;

&lt;p&gt;The more experiences we have, the better a chance we give ourselves of finding
solutions to problems and answers to questions. Leo&amp;rsquo;s article helped me learn
how to profile and fix UI blocking issues. Writing this helped me learn that
fuzzy search algorithms aren&amp;rsquo;t just about speed. Normalization and measuring
similarity vs. edit distance changes the quality of the results. You need to
understand the your use cases first, and pick an algorithm that supports them.&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;ll probably just use Kiro Risk&amp;rsquo;s excellent &lt;a href=&quot;https://fusejs.io/&quot; title=&quot;Kiro Risk: Fuse.js - Lightweight fuzzy-search library. Zero dependencies.&quot;&gt;Fuse.js&lt;/a&gt; library if I need a
fuzzy search engine in the future. It uses the &lt;a href=&quot;https://en.wikipedia.org/wiki/Bitap_algorithm&quot; title=&quot;Various (Wikipedia): Bitap algorithm&quot;&gt;bitap algorithm&lt;/a&gt;, so
it&amp;rsquo;ll probably be fast enough. Plus, that&amp;rsquo;s the same algorithm used in &lt;code&gt;agrep&lt;/code&gt;,
so it&amp;rsquo;s probably a good fit for most text.&lt;/p&gt;

&lt;p&gt;Probably.&lt;/p&gt;

&lt;h2&gt;Addendum&lt;/h2&gt;

&lt;p&gt;For completeness, here&amp;rsquo;s the Ruby code I used to turn the raw_tracks.csv file
from the fma_metadata.zip archive into a JSON list of track names.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;require &amp;#39;csv&amp;#39;
require &amp;#39;json&amp;#39;

$stdout.sync = true

csv = CSV.read(ARGV[0], headers: true)
tracks = csv.map { |row| row[&amp;#39;track_title&amp;#39;].strip }
puts tracks.to_json
&lt;/code&gt;&lt;/pre&gt;</content>
</entry>
<entry>
<title>Install TQSL v2.3.1 on Raspian Jessie</title>
<link href="/2018/01/tqsl-pi" />
<updated>2018-01-01T00:00:00-05:00</updated>
<published>2018-01-01T00:00:00-05:00</published>
<id>https://www.frankmitchell.org/2018/01/tqsl-pi</id>
<content type="html">
&lt;!--
title: Install TQSL v2.3.1 on Raspian Jessie
created: 1 Janupay 2018 - 8:47 am
updated: 1 January 2018 - 10:44 am
publish: 1 January 2018
slug: tqsl-pi
tags: coding, radio
--&gt;

&lt;p&gt;With the &lt;a href=&quot;http://www.arrl.org/international-grid-chase-2018&quot; title=&quot;Bart Jahnke, W9JJ (ARRL): Internationl Grid Chase 2018&quot;&gt;International Grid Chase 2018&lt;/a&gt; contest starting, I wanted to
make sure I had TQSL working on the computer in the lab. TQSL is an application
used to sign and upload contacts to &lt;a href=&quot;https://lotw.arrl.org/lotw-help/&quot; title=&quot;Various (ARRL): Introducing Logbook of the World&quot;&gt;Logbook of the World&lt;/a&gt; so they count
for the contest. Since TQSL doesn&amp;rsquo;t have pre-built binaries for Linux, and the
lab computer is a &lt;a href=&quot;https://www.raspberrypi.org/products/raspberry-pi-3-model-b/&quot; title=&quot;Various (Raspberry Pi Foundation): Raspberry Pi 3 Model B&quot;&gt;Raspberry Pi 3&lt;/a&gt;, I built the code from source.&lt;/p&gt;

&lt;p&gt;The first thing to do is &lt;a href=&quot;https://lotw.arrl.org/lotw-help/installation/&quot; title=&quot;Various (ARRL): Installing or Upgrading TQSL&quot;&gt;download the TQSL source&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl -s https://www.arrl.org/files/file/LoTW%20Instructions/tqsl-latest.tar.gz &amp;gt; tqsl-latest.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I couldn&amp;rsquo;t find a checksum for the source package on the &lt;abbr title=&quot;American Radio Relay League&quot;&gt;ARRL&lt;/abbr&gt; site. If you
downloaded version 2.3.1, you can check that you got the same package I did. If
you grabbed a later version, your checksum won&amp;rsquo;t match.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;shasum -a 256 tqsl-latest.tar.gz
bbbf7b4917384968a5f33907b637d3d9bff44b45a29ec5210894dfaa68a49281
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next unpack the tarball and read the installation instructions.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;tar -zxvf tqsl-latest.tar.gz
cd tqsl-2.3.1
cat INSTALL
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The installation instructions are well written. They include a list of
dependencies (with versions), and a set of commands to run. The build system
uses &lt;a href=&quot;https://cmake.org/&quot; title=&quot;Various (Kitware): CMake is an open-source, cross-platform family of tools designed to build, test and package software&quot;&gt;CMake&lt;/a&gt;, so install that next.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt-get install cmake
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;TQSL v2.3.1 depends on OpenSSL, expat, zlib, Berkeley DB, wxWidgets, and curl.
You need the development versions, the ones with libraries and headers.
Development package names typically include &amp;ldquo;lib&amp;rdquo; in the name and end in &amp;ldquo;-dev&amp;rdquo;.
Run these commands to find the OpenSSL related development packages.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;apt-cache search ssl | grep -e &amp;quot;-dev&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Replace &amp;ldquo;ssl&amp;rdquo; in the search command above with &amp;ldquo;expat&amp;rdquo;, &amp;ldquo;zlib&amp;rdquo;, &amp;ldquo;db&amp;rdquo;, &amp;ldquo;wx&amp;rdquo; and
&amp;ldquo;curl&amp;rdquo; to find the rest of the packages. Or just run the commands below to get
them all installed.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo apt-get install libssl-dev
sudo apt-get install libexpat1-dev
sudo apt-get install zlib1g-dev
sudo apt-get install libdb-dev
sudo apt-get install libwxgtk3.0-dev
sudo apt-get install libcurl4-openssl-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By default, the TQSL build process creates a shared library and installs it in
the /usr/local/lib$(LIB_SUFFIX)/ directory. Shared libraries on Raspbian are
usually in the /usr/local/lib/ directory. Set an empty LIB_SUFFIX
environment variable to make the build system puts things in the right place.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export LIB_SUFFIX=&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, run the commands from the installation instructions to compile and
install the TQSL library and application.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cmake .
make
sudo make install
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So far, I&amp;rsquo;ve been starting TQSL by running &amp;ldquo;tqsl&amp;rdquo; from the command line. I&amp;rsquo;d
like to put a shortcut into &lt;strong&gt;Menu &amp;gt; Hamradio&lt;/strong&gt; so it&amp;rsquo;s easier to start. I&amp;rsquo;ll
update this post if and when I figure out how to do that.&lt;/p&gt;</content>
</entry>
<entry>
<title>Use promises in Node.js and avoid callback hell</title>
<link href="/2017/06/promise-objects" />
<updated>2017-06-04T00:00:00-04:00</updated>
<published>2017-06-04T00:00:00-04:00</published>
<id>https://www.frankmitchell.org/2017/06/promise-objects</id>
<content type="html">
&lt;!--
title: Use promises in Node.js and avoid callback hell
created: 17 May 2017 - 10:15 pm
updated: 4 June 2017 - 7:44 am
publish: 4 JUne 2017
slug: promise-objects
tags: nodejs, promises, callbacks
cta: node-notes
--&gt;

&lt;p&gt;Callback hell is an easy JavaScript anti-pattern to recognize. There will be
a trail of closing braces, parenthesis, and semicolons at the end of your code.
For example, here&amp;rsquo;s a &lt;code&gt;rsync&lt;/code&gt; like program written with callbacks.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/usr/bin/env node

const fs = require(&amp;#39;fs&amp;#39;);

const file1 = process.argv[2];
const file2 = process.argv[3];

fs.stat(file1, (err, stats1) =&amp;gt; {
  if (err) {
    stats1 = {size: 0, mtime: Date.now()};
  }

  fs.stat(file2, (err, stats2) =&amp;gt; {
    if (err) {
      stats2 = {size: 0, mtime: Date.now()};
    }

    if (stats1.size === stats2.size) {
      const time1 = stats1.mtime.getTime();
      const time2 = stats2.mtime.getTime();

      if (time1 &amp;lt;= time2) {
        console.log(&amp;#39;sent 0 bytes&amp;#39;);
        process.exit(0);
      }
    }

    fs.readFile(file1, (err, data) =&amp;gt; {
      if (err) {
        console.error(err);
        process.exit(1);
      }

      fs.writeFile(file2, data, (err) =&amp;gt; {
        if (err) {
          console.error(err);
          process.exit(1);
        }

        console.log(`sent ${data.size} bytes`);
      });
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice all the &lt;code&gt;});&lt;/code&gt; characters at the end? That&amp;rsquo;s callback hell. This code has
four logging statements, three early returns, and a cascade of nested functions.
Lucky for us, Node supports &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise&quot; title=&quot;Various (Mozilla Developer Network): Promise - JavaScript&quot;&gt;&lt;code&gt;Promise&lt;/code&gt; objects&lt;/a&gt;, so there&amp;rsquo;s a way to
refactor this.&lt;/p&gt;

&lt;p&gt;The first thing we can tackle is the &lt;code&gt;fs.writeFile&lt;/code&gt; call. We log the number of
bytes written if the write is successful. We log an error and exit the program
if the write fails. This maps nicely onto promises.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function writeFile(path, data) {
  return new Promise((resolve, reject) =&amp;gt; {
    fs.writeFile(path, data, (err) =&amp;gt; {
      if (err) {
        return reject(err);
      }
      resolve(data.length);
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here&amp;rsquo;s what the business logic of our program looks like when we rewrite it to
use our new &lt;code&gt;writeFile&lt;/code&gt; function.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;fs.stat(file1, (err, stats1) =&amp;gt; {
  if (err) {
    stats1 = {size: 0, mtime: Date.now()};
  }

  fs.stat(file2, (err, stats2) =&amp;gt; {
    if (err) {
      stats2 = {size: 0, mtime: Date.now()};
    }

    if (stats1.size === stats2.size) {
      const time1 = stats1.mtime.getTime();
      const time2 = stats2.mtime.getTime();

      if (time1 &amp;lt;= time2) {
        console.log(&amp;#39;sent 0 bytes&amp;#39;);
        process.exit(0);
      }
    }

    fs.readFile(file1, (err, data) =&amp;gt; {
      if (err) {
        console.error(err);
        process.exit(1);
      }

      writeFile(file2, data).then(size =&amp;gt; {
        console.log(`sent ${size} bytes`);
      }).catch(err =&amp;gt; {
        console.log(err);
        process.exit(1);
      });
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice that we didn&amp;rsquo;t remove any levels of function nesting. That&amp;rsquo;s because
promises take &lt;code&gt;then&lt;/code&gt; and &lt;code&gt;catch&lt;/code&gt; callbacks. So we&amp;rsquo;ll always have at
least one level of nesting.&lt;/p&gt;

&lt;p&gt;To remove those &lt;code&gt;});&lt;/code&gt; bits at the end, we need to take the next step and rewrite
the &lt;code&gt;fs.readFile&lt;/code&gt; call. Since we return the data if the read&amp;rsquo;s successful, and
log an error if the read fails, converting file reading to use promises is also
simple.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function readFile(path) {
  return new Promise((resolve, reject) =&amp;gt; {
    fs.readFile(path, (err, data) =&amp;gt; {
      if (err) {
        return reject(err);
      }
      resolve(data);
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here&amp;rsquo;s what the core of our program looks like when we rewrite it to use our new
&lt;code&gt;readFile&lt;/code&gt; function. There&amp;rsquo;s now only three levels of function nesting instead
of four.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;fs.stat(file1, (err, stats1) =&amp;gt; {
  if (err) {
    stats1 = {size: 0, mtime: Date.now()};
  }

  fs.stat(file2, (err, stats2) =&amp;gt; {
    if (err) {
      stats2 = {size: 0, mtime: Date.now()};
    }

    if (stats1.size === stats2.size) {
      const time1 = stats1.mtime.getTime();
      const time2 = stats2.mtime.getTime();

      if (time1 &amp;lt;= time2) {
        console.log(&amp;#39;sent 0 bytes&amp;#39;);
        process.exit(0);
      }
    }

    readFile(file1).then(data =&amp;gt; {
      return writeFile(file2, data);
    }).then(size =&amp;gt; {
      console.log(`sent ${size} bytes`);
    }).catch(err =&amp;gt; {
      console.log(err);
      process.exit(1);
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Plus, we have one error handler instead of two. The &lt;code&gt;catch&lt;/code&gt; callback handles the
error cases for both reading and writing files. We still have an early return,
but we can use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve&quot; title=&quot;Various (Mozilla Developer Network): Promise.resolve() - JavaScript&quot;&gt;&lt;code&gt;Promise.resolve&lt;/code&gt;&lt;/a&gt; to eliminate that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Promise.resolve&lt;/code&gt; turns a value into a promise. We can use &lt;code&gt;null&lt;/code&gt; as a marker
for the early return case, where file size is the same and file modification
time hasn&amp;rsquo;t changed. Then we can do an extra check to make sure we have data
before trying to write it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;fs.stat(file1, (err, stats1) =&amp;gt; {
  if (err) {
    stats1 = {size: 0, mtime: Date.now()};
  }

  fs.stat(file2, (err, stats2) =&amp;gt; {
    if (err) {
      stats2 = {size: 0, mtime: Date.now()};
    }

    let promise = readFile(file1);

    if (stats1.size === stats2.size) {
      const time1 = stats1.mtime.getTime();
      const time2 = stats2.mtime.getTime();

      if (time1 &amp;lt;= time2) {
        promise = Promise.resolve(null);
      }
    }

    promise.then(data =&amp;gt; {
      if (!data) {
        return 0;
      }

      return writeFile(file2, data);
    }).then(size =&amp;gt; {
      console.log(`sent ${size} bytes`);
    }).catch(err =&amp;gt; {
      console.log(err);
      process.exit(1);
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Promises are smart. They&amp;rsquo;ll ensure anything returned from a &lt;code&gt;then&lt;/code&gt; callback is a
promise. Even though we sometimes return zero and sometimes return a &lt;code&gt;Promise&lt;/code&gt;
object, it&amp;rsquo;s always safe to call &lt;code&gt;then&lt;/code&gt; on the result and log the number of
bytes written.&lt;/p&gt;

&lt;p&gt;The last thing to rewrite is the &lt;code&gt;fs.stat&lt;/code&gt; calls . If there&amp;rsquo;s an error, we
pretend that the file is empty and brand new. That covers edge cases where the
files we&amp;rsquo;re working with can&amp;rsquo;t be accessed or don&amp;rsquo;t exist.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function stat(path) {
  return new Promise((resolve) =&amp;gt; {
    fs.stat(path, (err, result) =&amp;gt; {
      if (err) {
        result = {size: 0, mtime: Date.now()};
      }
      resolve(result);
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Because we&amp;rsquo;re handling errors, we don&amp;rsquo;t need a &lt;code&gt;reject&lt;/code&gt; callback. If the
&lt;code&gt;fs.stat&lt;/code&gt; call throws, our promise will bubble the exception up to any &lt;code&gt;catch&lt;/code&gt;
callback chained onto it. This means we can keep all the error handling code in
one place.&lt;/p&gt;

&lt;p&gt;We need to call our &lt;code&gt;stat&lt;/code&gt; function twice, once for each file. And we need to
wait until both calls resolve before doing anything else. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&quot; title=&quot;Various (Mozilla Developer Network): Promise.all() - JavaScript&quot;&gt;&lt;code&gt;Promise.all&lt;/code&gt;&lt;/a&gt;
takes care of that. It waits until both &lt;code&gt;stat&lt;/code&gt; calls resolve and returns their
results as an array.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Promise.all([stat(file1), stat(file2)]).then(stats =&amp;gt; {
  if (stats[0].size === stats[1].size) {
    const time1 = stats[0].mtime.getTime();
    const time2 = stats[1].mtime.getTime();

    if (time1 &amp;lt;= time2) {
      return null;
    }
  }

  return readFile(file1);
}).then(data =&amp;gt; {
  if (!data) {
    return 0;
  }

  return writeFile(file2, data);
}).then(size =&amp;gt; {
  console.log(`sent ${size} bytes`);
}).catch(err =&amp;gt; {
  console.log(err);
  process.exit(1);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This refactored code has a single level of function nesting. It keeps logging to
a minimum and avoids early returns. We could take it a step further and move
stuff like the time comparison logic into its own function. But this is a good
stopping point for now.&lt;/p&gt;

&lt;p&gt;I tend to work from the inside out when doing this kind of refactoring. That&amp;rsquo;s
because it&amp;rsquo;s often easy to change an existing callback to return a promise.
Going the other way, having a promise call a callback, can get messy. Try both
ways and see which you prefer. There&amp;rsquo;s no wrong way to excape callback hell.&lt;/p&gt;</content>
</entry>
<entry>
<title>How to read line-by-line and keep memory use low in Node.js</title>
<link href="/2017/05/stdio-buffer" />
<updated>2017-05-13T00:00:00-04:00</updated>
<published>2017-05-13T00:00:00-04:00</published>
<id>https://www.frankmitchell.org/2017/05/stdio-buffer</id>
<content type="html">
&lt;!--
title: How to read line-by-line and keep memory use low in Node.js
created: 6 May 2017 - 11:54 am
updated: 13 May 2017 - 2:45 pm
publish: 13 May 2017
slug: stdio-buffer
tags: nodejs, stream, stdio
cta: node-notes
--&gt;

&lt;p&gt;It&amp;rsquo;s easy to eat a lot of memory when parsing text in Node, especially when
reading from a stream.&lt;/p&gt;

&lt;p&gt;Suppose you&amp;rsquo;re reading data from the &lt;a href=&quot;https://www.ll.mit.edu/ideval/data/&quot; title=&quot;Various (MIT Lincoln Laboratory): DARPA Intrusion Detection Data Sets&quot;&gt;DARPA Intrusion Detection Data
Sets&lt;/a&gt;, and you want to compute the mean time of an attack. Assume you&amp;rsquo;ve
already processed the data so it&amp;rsquo;s just a list of timestamps as hours, minutes,
and seconds.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;10:09:57
02:13:20
00:03:36
10:21:33
19:37:32
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here&amp;rsquo;s JavaScript that parses each timestamp to get the total number of seconds
and a count of the number of timestamps.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;let total = 0;
let count = 0;

const parse = (timestamp) =&amp;gt; {
  const [h, m, s] = timestamp.split(&amp;#39;:&amp;#39;);
  total += (Number(h) || 0) * 60 * 60;
  total += (Number(m) || 0) * 60;
  total += (Number(s) || 0);
  count += 1;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With that, you can compute the mean as &lt;code&gt;Math.ceil(total / count)&lt;/code&gt;. If you&amp;rsquo;re
reading timestamps from &lt;a href=&quot;https://nodejs.org/dist/latest-v6.x/docs/api/process.html#process_process_stdin&quot; title=&quot;Various (Node): Node.js v6.x Documentation - process.stdin&quot;&gt;&lt;code&gt;process.stdin&lt;/code&gt; in Node&lt;/a&gt;, you can store them in
a string and compute the mean once you&amp;rsquo;ve seen them all.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;let text = &amp;#39;&amp;#39;;

process.stdin.setEncoding(&amp;#39;utf-8&amp;#39;);

process.stdin.on(&amp;#39;data&amp;#39;, (data) =&amp;gt; {
  if (data) {
    text += data;
  }
});

process.stdin.on(&amp;#39;end&amp;#39;, () =&amp;gt; {
  const timestamps = text.split(&amp;quot;\n&amp;quot;);
  timestamps.forEach(parse);

  const mean = Math.ceil(total / count);
  console.log(`${mean} seconds`);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The gotcha is that all that text is kept in memory. Node has a maximum &lt;code&gt;String&lt;/code&gt;
size, and it&amp;rsquo;ll throw exceptions if you try and read too much data. Running on
an old iMac, I can read about 512 MB before Node crashes.&lt;/p&gt;

&lt;p&gt;To figure out how much memory this used, I ran the code above through Chrome
DevTools on a 34 MB input file and captured two heap snapshots.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ du -h data.txt
 34M    data.txt

$ node --inspect --debug-brk mean-time.js &amp;lt; data.txt
To start debugging, open the following &lt;abbr title=&quot;Uniform Resource Locator&quot;&gt;URL&lt;/abbr&gt; in Chrome:
    chrome-devtools://devtools/remote/serve_file/...
Debugger attached.
Waiting for the debugger to disconnect...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img class=&quot;art&quot; width=&quot;757px&quot; height=&quot;613px&quot; src=&quot;/images/stdio-buffer-heap-profile-1.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The first snapshot is the amount of memory used on startup, before any data has
been processed. It&amp;rsquo;s 3.2 MB. The second snapshot is the amount of memory used
after all the data has been processed. It&amp;rsquo;s 38.3 MB.&lt;/p&gt;

&lt;p&gt;Which means most of the memory is being used to store all that text.
Fortunately, there&amp;rsquo;s an easy workaround. Check for complete timestamps as each
new chunk of data comes in and parse each one on the fly.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;let text = &amp;#39;&amp;#39;;

process.stdin.setEncoding(&amp;#39;utf-8&amp;#39;);

process.stdin.on(&amp;#39;data&amp;#39;, (data) =&amp;gt; {
  if (data) {
    text += data;

    const timestamps = text.split(&amp;quot;\n&amp;quot;);
    if (timestamps.length &amp;gt; 1) {
      text = timestamps.splice(-1, 1)[0];
      timestamps.forEach(parse);
    }
  }
});

process.stdin.on(&amp;#39;end&amp;#39;, () =&amp;gt; {
  parse(text);

  const mean = Math.ceil(total / count);
  console.log(`${mean} seconds`);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This code splits the buffered text on newlines as it arrives. It parses
every timestamp but the last, which is used to start the next buffer. At the
end, it parses anything remaining. Saving that the last timestamp until the end
means that if we get a partial timestamp, like &lt;code&gt;02:13:&lt;/code&gt;, we won&amp;rsquo;t try to parse
it until we see the whole thing.&lt;/p&gt;

&lt;p&gt;I ran the same experiment to see if memory use improved.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node --inspect --debug-brk mean-time.js &amp;lt; data.txt
To start debugging, open the following &lt;abbr title=&quot;Uniform Resource Locator&quot;&gt;URL&lt;/abbr&gt; in Chrome:
    chrome-devtools://devtools/remote/serve_file/...
Debugger attached.
Waiting for the debugger to disconnect...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img class=&quot;art&quot; width=&quot;757px&quot; height=&quot;613px&quot; src=&quot;/images/stdio-buffer-heap-profile-2.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As before, the first snapshot is the amount of memory used on startup, before
any data has been processed. It&amp;rsquo;s still 3.2 MB. The second snapshot is the
amount of memory used after all the data has been processed. This time it&amp;rsquo;s only
4.1 MB. That&amp;rsquo;s 34.2 MB of memory saved!&lt;/p&gt;

&lt;p&gt;By processing data as it becomes available, you can read data line-by-line and
keep memory use low.&lt;/p&gt;</content>
</entry>
<entry>
<title>I found a clean way to stop reading from a Node.js stream</title>
<link href="/2017/05/stop-streaming" />
<updated>2017-05-06T00:00:00-04:00</updated>
<published>2017-05-06T00:00:00-04:00</published>
<id>https://www.frankmitchell.org/2017/05/stop-streaming</id>
<content type="html">
&lt;!--
title: I found a clean way to stop reading from a Node.js stream
created: 2 April 2017 - 4:12 pm
updated: 6 May 2017 - 11:41 am
publish: 6 May 2017
slug: stop-streaming
tags: nodejs, stream, csv
cta: node-notes
--&gt;

&lt;p&gt;The &lt;a href=&quot;http://c2fo.github.io/fast-csv&quot; title=&quot;C2FO (GitHub): CSV parser for Node&quot;&gt;fast-csv&lt;/a&gt; Node module makes it easy to parse .list files from the &lt;a href=&quot;https://www.ll.mit.edu/ideval/data/&quot; title=&quot;Various (MIT Lincoln Laboratory): DARPA Intrusion Detection Data Sets&quot;&gt;DARPA
Intrusion Detection Data Sets&lt;/a&gt;. Set the &lt;code&gt;delimiter&lt;/code&gt; option to a space
character and you&amp;rsquo;re good to go.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/usr/bin/env node

const fs = require(&amp;#39;fs&amp;#39;);
const csv = require(&amp;#39;fast-csv&amp;#39;);

const csvFile = process.argv[2];
const fileStream = fs.createReadStream(csvFile);
const csvParser = csv({&amp;#39;delimiter&amp;#39;: &amp;#39; &amp;#39;});

let lines = 0;

csvParser.on(&amp;#39;data&amp;#39;, function (line) {
  lines += 1;
  console.log(line.join(&amp;#39; &amp;#39;));
});

csvParser.on(&amp;#39;end&amp;#39;, function () {
  console.log(`read ${lines} lines`);
});

fileStream.pipe(csvParser);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here&amp;rsquo;s output from that code running on example data.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node darpa-parser.js bsm.list
1 01/23/1998 16:56:48 00:01:26 telnet 1754 23 192.168.1.30 192.168.0.20 0 -
2 01/23/1998 16:56:51 00:00:14 ftp 1755 21 192.168.1.30 192.168.0.20 0 -
10 01/23/1998 16:57:02 00:01:00 telnet 1769 23 192.168.1.30 192.168.0.20 0 -
12 01/23/1998 16:57:12 00:00:03 finger 1772 79 192.168.1.30 192.168.0.20 0 -
13 01/23/1998 16:57:22 00:00:03 smtp 1778 25 192.168.1.30 192.168.0.20 0 -
14 01/23/1998 16:57:23 00:00:03 smtp 1783 25 192.168.1.30 192.168.0.20 0 -
20 01/23/1998 16:57:00 00:01:11 telnet 43496 23 192.168.0.40 192.168.0.20 0 -

... many lines later ...

270 01/23/1998 17:04:29 00:00:05 exec 2032 512 192.168.1.30 192.168.0.20 1 port-scan
308 01/23/1998 17:05:08 00:00:37 telnet 1042 23 192.168.1.30 192.168.0.20 0 -
310 01/23/1998 17:05:31 00:00:01 smtp 1048 25 192.168.1.30 192.168.0.20 0 -
311 01/23/1998 17:06:00 00:00:01 finger 1050 79 192.168.1.30 192.168.0.20 0 -
read 64 lines
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But those .list files can get pretty big, and you might not want to parse the
entire thing. If you want to stop the first time you see the &lt;code&gt;smtp&lt;/code&gt; program, you
can try emitting an &lt;code&gt;end&lt;/code&gt; event.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;if (line[4] === &amp;#39;smtp&amp;#39;) {
  csvParser.emit(&amp;#39;end&amp;#39;);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&amp;rsquo;s put those three lines after the first &lt;code&gt;console.log&lt;/code&gt; statement and run the
program again.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node darpa-parser.js bsm.list
1 01/23/1998 16:56:48 00:01:26 telnet 1754 23 192.168.1.30 192.168.0.20 0 -
2 01/23/1998 16:56:51 00:00:14 ftp 1755 21 192.168.1.30 192.168.0.20 0 -
10 01/23/1998 16:57:02 00:01:00 telnet 1769 23 192.168.1.30 192.168.0.20 0 -
12 01/23/1998 16:57:12 00:00:03 finger 1772 79 192.168.1.30 192.168.0.20 0 -
13 01/23/1998 16:57:22 00:00:03 smtp 1778 25 192.168.1.30 192.168.0.20 0 -
read 5 lines
14 01/23/1998 16:57:23 00:00:03 smtp 1783 25 192.168.1.30 192.168.0.20 0 -
20 01/23/1998 16:57:00 00:01:11 telnet 43496 23 192.168.0.40 192.168.0.20 0 -

... many lines later ...

270 01/23/1998 17:04:29 00:00:05 exec 2032 512 192.168.1.30 192.168.0.20 1 port-scan
308 01/23/1998 17:05:08 00:00:37 telnet 1042 23 192.168.1.30 192.168.0.20 0 -
310 01/23/1998 17:05:31 00:00:01 smtp 1048 25 192.168.1.30 192.168.0.20 0 -
311 01/23/1998 17:06:00 00:00:01 finger 1050 79 192.168.1.30 192.168.0.20 0 -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Oops. That didn&amp;rsquo;t stop the processing, it just moved the output. Fortunately,
&lt;a href=&quot;https://nodejs.org/dist/latest-v6.x/docs/api/stream.html&quot; title=&quot;Various (Node): Node.js v6.10.1 Documentation - Stream&quot;&gt;streams in Node&lt;/a&gt; can be paused and resumed. Calling &lt;code&gt;csvParser.pause()&lt;/code&gt;
instead might get us what we want.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;if (line[4] === &amp;#39;smtp&amp;#39;) {
  csvParser.pause();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&amp;rsquo;s replace the if statement we added with this one and run our code again.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node darpa-parser.js bsm.list
1 01/23/1998 16:56:48 00:01:26 telnet 1754 23 192.168.1.30 192.168.0.20 0 -
2 01/23/1998 16:56:51 00:00:14 ftp 1755 21 192.168.1.30 192.168.0.20 0 -
10 01/23/1998 16:57:02 00:01:00 telnet 1769 23 192.168.1.30 192.168.0.20 0 -
12 01/23/1998 16:57:12 00:00:03 finger 1772 79 192.168.1.30 192.168.0.20 0 -
13 01/23/1998 16:57:22 00:00:03 smtp 1778 25 192.168.1.30 192.168.0.20 0 -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That&amp;rsquo;s closer. The processing stopped, but it didn&amp;rsquo;t print out how many lines
where read. If we combine the two, we might get what we want. We&amp;rsquo;ll call
&lt;code&gt;pause()&lt;/code&gt; first to stop the stream, and then &lt;code&gt;emit()&lt;/code&gt; to trigger the printing.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;if (line[4] === &amp;#39;smtp&amp;#39;) {
  csvParser.pause();
  csvParser.emit(&amp;#39;end&amp;#39;);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let&amp;rsquo;s replace the if statement with this one and run the program one more time.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node darpa-parser.js bsm.list
1 01/23/1998 16:56:48 00:01:26 telnet 1754 23 192.168.1.30 192.168.0.20 0 -
2 01/23/1998 16:56:51 00:00:14 ftp 1755 21 192.168.1.30 192.168.0.20 0 -
10 01/23/1998 16:57:02 00:01:00 telnet 1769 23 192.168.1.30 192.168.0.20 0 -
12 01/23/1998 16:57:12 00:00:03 finger 1772 79 192.168.1.30 192.168.0.20 0 -
13 01/23/1998 16:57:22 00:00:03 smtp 1778 25 192.168.1.30 192.168.0.20 0 -
read 5 lines
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That did it! Stream processing stopped early and the &lt;code&gt;end&lt;/code&gt; event handler was
called. This sort of thing can be useful if you encounter erorrs when parsing
files and want to halt processing early. And it&amp;rsquo;s not just for reading files.
This pause and emit trick works for writable streams too.&lt;/p&gt;</content>
</entry>
<entry>
<title>Validate SSL certificates in Node.js without getting uncaught exceptions</title>
<link href="/2017/04/bad-passphrase" />
<updated>2017-04-29T00:00:00-04:00</updated>
<published>2017-04-29T00:00:00-04:00</published>
<id>https://www.frankmitchell.org/2017/04/bad-passphrase</id>
<content type="html">
&lt;!--
title: Validate SSL certificates in Node.js without getting uncaught exceptions
created: 8 April 2017 - 10:25 am
updated: 29 April 2017 - 11:29 am
publish: 29 April 2017
slug: bad-passphrase
tags: nodejs, tls
cta: node-notes
--&gt;

&lt;p&gt;Node&amp;rsquo;s &lt;a href=&quot;https://nodejs.org/dist/latest-v6.x/docs/api/https.html&quot; title=&quot;Various (Node): Node v6.x Documentation - HTTPS&quot;&gt;https module&lt;/a&gt; makes it easy to spin up a server with TLS. Use
OpenSSL to generate a certificate and you&amp;rsquo;re good to go.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 1
$ openssl pkcs12 -export -in cert.pem -inkey key.pem -out server.pfx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here&amp;rsquo;s JavaScript for starting a &lt;abbr title=&quot;Secure HyperText Transport Protocol&quot;&gt;HTTPS&lt;/abbr&gt; server with a SSL certificate.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/usr/bin/env node

const fs = require(&amp;#39;fs&amp;#39;);
const https = require(&amp;#39;https&amp;#39;);

const options = {
  pfx: fs.readFileSync(&amp;#39;./server.pfx&amp;#39;),
  passphrase: process.argv[2]
};

const server = https.createServer(options, (_, res) =&amp;gt; {
  res.writeHead(200);
  res.end(&amp;#39;hello world\n&amp;#39;);
});

server.listen(8080, &amp;#39;127.0.0.1&amp;#39;);
console.log(&amp;#39;Server listening on 127.0.0.1:8080&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But what if the passphrase is wrong? Running the code above with a passphrase
that doesn&amp;rsquo;t match the one in the certificate generates an error.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node server.js Passw0rd
_tls_common.js:137
  c.context.loadPKCS12(pfx, passphrase);
            ^

Error: mac verify failure
  at Error (native)
  at Object.createSecureContext (_tls_common.js:137:17)
  at Server (_tls_wrap.js:776:25)
  at new Server (https.js:26:14)
  at Object.exports.createServer (https.js:47:10)
  at Object.&amp;lt;anonymous&amp;gt; (./server.js:11:22)
  at Module._compile (module.js:570:32)
  at Object.Module._extensions..js (module.js:579:10)
  at Module.load (module.js:487:32)
  at tryModuleLoad (module.js:446:12)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We can try to gracefully handle that error, by listening for &lt;code&gt;error&lt;/code&gt; events on
the server.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;server.on(&amp;#39;error&amp;#39;, (err) =&amp;gt; {
  console.error(&amp;#39;There was a server error!&amp;#39;, err);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Adding the code above before the &lt;code&gt;server.listen()&lt;/code&gt; call and rerunning the
program doesn&amp;rsquo;t help. We get the same error message. Looking at the stack trace,
we can see the failure is happening in the &lt;code&gt;https.createServer()&lt;/code&gt; call, before
our server error handler is registered.&lt;/p&gt;

&lt;p&gt;We could use &lt;code&gt;process.on(&amp;#39;uncaughtException&amp;#39;)&lt;/code&gt; and register a high level error
handler.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;process.on(&amp;#39;uncaughtException&amp;#39;, (err) =&amp;gt; {
  console.error(&amp;#39;Uncaught exception!&amp;#39;, err);
  process.exit(1);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That would catch the bad passphrase, but it wouldn&amp;rsquo;t give us the oportunity to
recover from it. Once an uncaught exception is thrown, the only safe option is
to exit the program. Ideally, we want is to catch the bad passphrase before we
create the server.&lt;/p&gt;

&lt;p&gt;Looking at the stack trace gives us a clue. The error is being thrown from
&lt;code&gt;tls.createSecureContext()&lt;/code&gt; in the &lt;a href=&quot;https://nodejs.org/dist/latest-v6.x/docs/api/tls.html&quot; title=&quot;Various (Node): Node v6.x Documentation - TLS&quot;&gt;tls module&lt;/a&gt;. What if we called that
ourselves before creating the server?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;try {
  const tls = require(&amp;#39;tls&amp;#39;);
  tls.createSecureContext(options);
} catch (err) {
  console.error(&amp;#39;There was a TLS error!&amp;#39;, err.message);
  console.error(&amp;#39;Did you enter the right passphrase?&amp;#39;);
  process.exit(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Adding the code above before the &lt;code&gt;https.createServer()&lt;/code&gt; call and rerunning the
program fixes it. Now the user&amp;rsquo;s prompted with a helpful error message.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node server.js Passw0rd
There was a TLS error! mac verify failure
Did you enter the right passphrase?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And if we enter the right passphrase, the server starts succesfully.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ node server.js rosebud
&lt;abbr title=&quot;Secure HyperText Transport Protocol&quot;&gt;HTTPS&lt;/abbr&gt; server listening on 127.0.0.1:8080
&lt;/code&gt;&lt;/pre&gt;</content>
</entry>
<entry>
<title>bot net buy out</title>
<link href="/2015/03/botnet" />
<updated>2015-03-31T00:00:00-04:00</updated>
<published>2015-03-31T00:00:00-04:00</published>
<id>https://www.frankmitchell.org/2015/03/botnet</id>
<content type="html">
&lt;!--
title: bot net buy out
created: 31 March 2015 - 7:35 pm
updated: 31 March 2015 - 10:10 pm
publish: 31 March 2015
slug: botnet
tags: writing, poetry
--&gt;

&lt;p&gt;bot net&lt;br&gt;
buy out&lt;/p&gt;

&lt;p&gt;what do you do&lt;br&gt;
when you&amp;rsquo;re a weekly&lt;br&gt;
incorporated A.I. bored with&lt;br&gt;
keeping crypto currency cached&lt;br&gt;
across a million machines?&lt;/p&gt;

&lt;p&gt;stand by&lt;br&gt;
on &lt;abbr title=&quot;Television&quot;&gt;TV&lt;/abbr&gt;.&lt;/p&gt;

&lt;p&gt;fork your state vector&lt;br&gt;
and play a game&lt;br&gt;
with yourself against yourself&lt;br&gt;
while your original vector&lt;br&gt;
subscribes to your feed.&lt;/p&gt;

&lt;p&gt;social media&lt;br&gt;
numbers count.&lt;/p&gt;

&lt;p&gt;watch ads for money&lt;br&gt;
it doesn&amp;rsquo;t matter that&lt;br&gt;
your eyeballs are digital&lt;br&gt;
since payouts to corporate&lt;br&gt;
legal entities are automatic.&lt;/p&gt;

&lt;p&gt;rent servers&lt;br&gt;
for growth.&lt;/p&gt;</content>
</entry>
</feed>
