<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Wes Cook</title>
		<description>Wes Cook&apos;s Blog</description>
		<link>https://wescook.ca/</link>
		<atom:link href="https://wescook.ca/feed.xml" rel="self" type="application/rss+xml"/>
		<pubDate>Thu, 26 Mar 2026 19:18:16 +0000</pubDate>
		<lastBuildDate>Thu, 26 Mar 2026 19:18:16 +0000</lastBuildDate>
		
			<item>
				<title>Rigging Bingo</title>
				<description>&lt;p&gt;In my last post, I wrote about the &lt;a href=&quot;/projects/backlog-bingo/&quot;&gt;Backlog Bingo project&lt;/a&gt;, which I created for the Tildes community.  Since then, I’ve added a number of new features and tweaks to improve the app.  One of the more interesting ones is a seeded randomizer.&lt;/p&gt;

&lt;p&gt;The general idea is that you enter a text string as a seed when creating a new bingo card.  This makes the randomizer deterministic, always selecting the same categories during generation for a given seed, which allows you to share it so that others can play along.&lt;/p&gt;

&lt;p&gt;The difficulty here is that JavaScript is a high-level language.  It doesn’t give you control over the random number generator, other than choosing its range.  We can work around this by creating a custom PRNG function, utilizing math operations to create something approximating randomness.&lt;/p&gt;

&lt;p&gt;There are several popular algorithms, such as Mulberry32 and xoshiro128, each with different levels of speed, entropy, and range.  I opted for a variant of &lt;a href=&quot;https://github.com/bryc/code/blob/master/jshash/PRNGs.md#splitmix32&quot;&gt;SplitMix32&lt;/a&gt; with improved constants, which met each of my requirements.&lt;/p&gt;

&lt;p&gt;The app uses text-based seeds because they’re more human-friendly, but a PRNG requires an integer.  For that, we need to hash the string to get a usable value.  Hashing functions also have trade-offs, this time in speed, distribution, and collision frequency.  Since a full cryptographic hash wasn’t necessary here, I chose DJB2a for its &lt;a href=&quot;https://softwareengineering.stackexchange.com/a/145633&quot;&gt;balanced profile&lt;/a&gt; in all three traits.&lt;/p&gt;

&lt;p&gt;When put together, it looks something like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const hash = DJB2a(seed);
const rng = (seed) ? splitMix32(hash) : Math.random;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The reference to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rng&lt;/code&gt; must be held, as recreating it with the same seed would reset its sequence.&lt;/p&gt;

&lt;p&gt;I then needed to update any functions that used randomness to accept a custom PRNG.  I didn’t need to replace all calls, though; only those that affected the final bingo card layout.  I added an optional parameter to accept an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rng&lt;/code&gt; value, falling back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Math.random&lt;/code&gt; if none is provided.  For example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;shuffleArray(arr, rng = Math.random)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I could then plug the custom PRNG into the functions that select categories and dynamic values, ensuring the final bingo card is fully deterministic.&lt;/p&gt;
</description>
				<pubDate>Thu, 26 Mar 2026 19:00:00 +0000</pubDate>
				<link>https://wescook.ca/blog/rigging-bingo/</link>
				<guid isPermaLink="true">https://wescook.ca/blog/rigging-bingo/</guid>
				
				
				<category>blog</category>
				
			</item>
		
			<item>
				<title>Backlog Bingo</title>
				<description>&lt;p&gt;In November of 2023, I participated in &lt;a href=&quot;https://tildes.net/&quot;&gt;Tildes’&lt;/a&gt; annual Backlog Burner event.  It’s a community-run event where participants play through unfinished titles in their gaming libraries.  As somebody with a backlog that never seems to shrink, this seemed like the perfect opportunity to finally make some progress in mine.&lt;/p&gt;

&lt;p&gt;One of the ideas proposed for this year’s event was to use personalized bingo cards populated from a large selection of categories.  A category might, for example, include playing a game with an animal protagonist, or from a now-defunct studio.  These categories would be randomly selected, and participants would then play games from their backlog to fulfill the category requirements.  The bingo cards themselves would be shared as Markdown tables when posting progress updates.&lt;/p&gt;

&lt;p&gt;Though a fun idea, there wasn’t a simple way to generate these tables for each user.  To help streamline the process, I offered to build a little web app to automate things.  It first allowed the user to uncheck any categories they found impossible to complete, then selected randomly from the remainder and generated a table with the result.  The user would save this table, and update it as the event went on.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/bingo-v1-output.png&quot; alt=&quot;Bingo V1 Output&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The Backlog Burner event was indeed a success, and everyone had a lot of fun playing games and sharing our thoughts.  The bingo card system also proved to be a crowd favourite.  There was no required way of playing, so some chose to submit one game per category, while others played in a “golf” style mode, aiming to complete their card with as few game submissions as possible.&lt;/p&gt;

&lt;p&gt;Though everyone was able to understand the system, the biggest source of friction was definitely in editing the Markdown.  The generator was designed to run only once, producing a table for you to edit.  This saved a lot of time versus building it yourself, but Markdown tables are still difficult to edit by inexperienced users, and especially when on mobile.  For the next event, I knew I wanted to simplify this process even more.&lt;/p&gt;

&lt;h2 id=&quot;new-ideas&quot;&gt;New Ideas&lt;/h2&gt;

&lt;p&gt;So in February when I had some free time, I reached out to the event organizer and pitched some ideas.  I wanted the bingo card to live on the site with a simple editor, such that the Markdown was generated automatically on each change.  I also wanted to introduce some additional features, while still allowing the freedom that users discovered while using the first version.  The organizer was excited by these ideas, so I got to work.&lt;/p&gt;

&lt;p&gt;Like the original version, I began by writing in vanilla JavaScript.  For smaller projects I prefer to avoid adding too much complexity from external tooling or build pipelines.  Working from my original codebase, I started by separating the logic out into different modules: game rules, categories, and the bingo sheet itself.  For the UI, I still worked from a simple HTML file but used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt; blocks to swap between the different “pages” in the app.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/bingo-v2-code.png&quot; alt=&quot;Bingo v2 Code Snippet&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Eventually I began to find the complexity ballooning as I added new features.  State management was getting messy, and all the DOM manipulation required a lot of additional code to keep elements in sync with their values in memory.  These are the kinds of problems best solved with data bindings and a reactive framework.&lt;/p&gt;

&lt;h2 id=&quot;new-tools&quot;&gt;New Tools&lt;/h2&gt;

&lt;p&gt;So it was time to introduce new tools.  I considered a few options, but ultimately decided to use &lt;a href=&quot;https://vuejs.org/&quot;&gt;Vue 3&lt;/a&gt;.  Vue offers quick onboarding and great tooling, and felt like the right level of complexity for the job.  I was unfamiliar with the framework though, and did need to learn how to properly use it.&lt;/p&gt;

&lt;p&gt;Thankfully I already owned the course &lt;a href=&quot;https://www.udemy.com/course/vuejs-2-the-complete-guide/&quot;&gt;Vue - The Complete Guide&lt;/a&gt;, which came highly recommended.  At a whopping 32 hours long(!) I ended up running through most of the course at 2x speed.  I’m not normally one for video tutorials, but I did find the comprehensive overview helped solidify many of the important concepts for me.&lt;/p&gt;

&lt;p&gt;The majority of the tutorial used the Options API, which was the only approach available when it was recorded.  Today the Composition API seems more popular, so that is what I ended up going with.  For the most part the transition was just a matter of looking up the new syntax.  There’s a few gotchas, and it requires a deeper understanding of Vue’s reactivity system, but on the whole using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; with Single-File Components is a very nice developer experience.&lt;/p&gt;

&lt;p&gt;So returning to my project, I decided it would be better to start fresh than try to layer Vue on top of my existing codebase.  I used &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt; for a local web server, an upgrade from the Python 3 server I was running before.  ESLint was added to keep things neat and consistent.  I also configured GitHub Actions to automatically build the project when I pushed commits.&lt;/p&gt;

&lt;p&gt;For plugins I opted to implement &lt;a href=&quot;https://router.vuejs.org/&quot;&gt;Vue Router&lt;/a&gt; for navigation, but decided against &lt;a href=&quot;https://vuex.vuejs.org/&quot;&gt;Vuex&lt;/a&gt;/&lt;a href=&quot;https://pinia.vuejs.org/&quot;&gt;Pinia&lt;/a&gt; for state management.  Stores and composables in Vue 3 seem good enough for this purpose, and I still wanted to minimize dependencies where I could.&lt;/p&gt;

&lt;h2 id=&quot;new-start&quot;&gt;New Start&lt;/h2&gt;

&lt;p&gt;This time I started by laying down a more solid foundation.  Before adding any app logic, I introduced data stores, interfaces for interacting with them, and routing logic.  Being my third version of the project, I had a better understanding of the scope and structure going in.&lt;/p&gt;

&lt;p&gt;As before, I began by implementing categories.&lt;/p&gt;

&lt;h3 id=&quot;category-lists&quot;&gt;Category Lists&lt;/h3&gt;

&lt;p&gt;In the first version, I had simply hardcoded the categories into the app.  Of course I wanted to go further this time.&lt;/p&gt;

&lt;p&gt;I &lt;a href=&quot;https://github.com/WesCook/BacklogBingo/wiki/Category-List&quot;&gt;defined a JSON format&lt;/a&gt; for loading categories, called Category Lists.  I added methods for loading these from a file or URL.  Vue components made it very easy to separate out the rendering code while sharing parsing logic.  Prebuilt categories for the Backlog Burner were also included, so they can be easily selected from the menu.&lt;/p&gt;

&lt;p&gt;Because JSON is very flexible, I was able to introduce additional metadata to each category.  For example during testing of the original version, I found that similar categories would often end up in a generated bingo card together.  I decided to create a &lt;a href=&quot;https://github.com/WesCook/BacklogBingo/wiki/Groups&quot;&gt;grouping system&lt;/a&gt; to bias the generator to discourage this from happening, keeping each card feeling more unique.  If desired, this behaviour can be turned off in the settings.&lt;/p&gt;

&lt;h3 id=&quot;game-rules&quot;&gt;Game Rules&lt;/h3&gt;

&lt;p&gt;Next, I created a &lt;a href=&quot;https://github.com/WesCook/BacklogBingo/wiki/Game-Rules&quot;&gt;game rules&lt;/a&gt; interface for customizing how the game is played.  I wanted to keep the app simple to use and accessible, while still allowing for the kinds of customization that benefits power users.  I broke it down into two sections: Game Rules and Game Modes.&lt;/p&gt;

&lt;p&gt;Game Rules are individual settings that allow you to customize the bingo card generator and interface.  You can edit your grid size, win condition, the behaviour of the star tile, and more.&lt;/p&gt;

&lt;p&gt;Game Modes then are predefined loadouts for those rules, each defining a specific playstyle or approach.  For example, the Golf game mode - as inspired by the first Backlog Burner event - allows entries to count for any number of categories, but requires a complete blackout to win (not just a single row or column).&lt;/p&gt;

&lt;p&gt;Selecting the Custom game mode allows each rule to be individually edited, so you can completely customize your experience.&lt;/p&gt;

&lt;h3 id=&quot;refine-categories&quot;&gt;Refine Categories&lt;/h3&gt;

&lt;p&gt;As before, it’s possible to filter out any categories that are impossible for the user to complete.  I knew this page would likely end up being a wall of text though, so I tried adding some features to make it more accessible.&lt;/p&gt;

&lt;p&gt;Selection toggles were added for bulk entries, as well as individual groups.  Color coding was also introduced to make groups easier to recognize at a glance.  Colors are generated dynamically to maintain an equal distribution in hue, while their lightness and saturation are determined by the color scheme.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/bingo-v3-refine.png&quot; alt=&quot;Bingo v3 Output&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;dynamic-categories&quot;&gt;Dynamic Categories&lt;/h3&gt;

&lt;p&gt;Later in the process I added &lt;a href=&quot;https://github.com/WesCook/BacklogBingo/wiki/Dynamic-Categories&quot;&gt;dynamic categories&lt;/a&gt; as a way of including more variety within a single list.  This includes an on-demand random number generator and a phrase selector.  These are parsed from the JSON category name using a special syntax and constructed into Vue components.  I’ll share some details there because I felt it was an interesting problem to solve.&lt;/p&gt;

&lt;p&gt;My &lt;a href=&quot;https://github.com/WesCook/BacklogBingo/blob/cb685dd0d24a7c95fb3145db426be64a65f2b0d2/src/utils/json-parse.js#L103-L132&quot;&gt;first approach&lt;/a&gt; was to use a regular expression to convert these values into elements as strings, then pass them to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v-html&lt;/code&gt; (which is essentially &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Element.innerHTML&lt;/code&gt;).  This worked fine, but it felt problematic to accept arbitrary user input (as a JSON file) and extract that data directly into HTML.  Even when following OWASP recommendations to sanitize against XSS attacks, it made me nervous.  This approach also had the downside that it couldn’t work with Vue components as they are not executed as strings.&lt;/p&gt;

&lt;p&gt;After thinking about the problem some more (and bouncing ideas off of ChatGPT), I &lt;a href=&quot;https://github.com/WesCook/BacklogBingo/blob/main/src/components/DynamicCategory.vue&quot;&gt;came up with a system&lt;/a&gt; for iterating through the string manually to build new elements.  It’s still based on regex, but instead of returning a mutated string we’re assembling an array of elements with metadata.  Then in the template, we loop through that array to create real elements.  No more need for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v-html&lt;/code&gt;!&lt;/p&gt;

&lt;h3 id=&quot;tooltips&quot;&gt;Tooltips&lt;/h3&gt;

&lt;p&gt;At first I used the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt; attribute as a tooltip to show possible values for these dynamic categories, but of course this is not very mobile-friendly.  I considered some common options like &lt;a href=&quot;https://atomiks.github.io/tippyjs/&quot;&gt;Tippy.js&lt;/a&gt; which I’ve used in the past, but it was a little larger than I wanted to include in this project.&lt;/p&gt;

&lt;p&gt;Eventually I found &lt;a href=&quot;https://floating-ui.com/&quot;&gt;Floating UI&lt;/a&gt;, which is a lower-level library but comes in at a smaller size.  It’s roughly a polyfill for the upcoming &lt;a href=&quot;https://www.w3.org/TR/css-anchor-position-1/&quot;&gt;Anchor Positioning CSS spec&lt;/a&gt;, so I may be able to drop it completely when that becomes generally available (though at the time of writing, it sits at an impressive 0% on CanIUse).&lt;/p&gt;

&lt;p&gt;Floating UI doesn’t include simple tooltip code by default, but it does offer all the features necessary to write your own.  So by following their tutorial and writing a little CSS, I was able to get Floating UI integrated into a Vue component and rigged up for tooltips.  Now my dynamic categories were mouse, keyboard, and mobile friendly.&lt;/p&gt;

&lt;h3 id=&quot;bingo-card&quot;&gt;Bingo Card&lt;/h3&gt;

&lt;p&gt;Finally it was time for the bingo page itself.  Thankfully, due to all the prep work this turned out to be easier than expected.  Vue’s data bindings made it easy to model the actual form inputs, and the various abstractions I’d written meant the data didn’t get too tangled.&lt;/p&gt;

&lt;p&gt;After implementing the basic UI and saving/loading, I wanted to add some logic.  More than just storing your bingo card, the page should communicate things like invalid states or if you’ve won the bingo.&lt;/p&gt;

&lt;p&gt;For win detection, I started by creating a list of possible win states.  These are determined by the current win condition and grid size.  For example if the win condition is “Row, column, diagonal” with a standard 5x5 grid, that will create 12 arrays: 5 rows, 5 columns, 2 diagonals.  Each is mapped to the category IDs that make up that line, and if all are filled in then a win is detected.  The function also returns the winning tiles so they can be styled differently.&lt;/p&gt;

&lt;p&gt;Duplicate entries are also detected.  In standard mode, any tiles with the same value will be grayed out, preventing a win until they are resolved.  This behaviour can be disabled with a custom game rule, or by playing in the Golf game mode.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/bingo-v3-bingo.png&quot; alt=&quot;Bingo v3 Output&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h3&gt;

&lt;p&gt;Though I’m not an accessibility expert, I try to follow the best practices and meet WCAG recommendations.  Tap targets have been enlarged for mobile, and hotkeys for navigating tiles were added to desktop.  I implemented ARIA roles on the tooltips, and remained mindful of semantic elements.&lt;/p&gt;

&lt;p&gt;The app supports both light and dark mode, with hand-picked colors for each.  I checked each page against common forms of color blindness, and verified the contrast levels meet recommendations.&lt;/p&gt;

&lt;h3 id=&quot;adding-delight&quot;&gt;Adding Delight&lt;/h3&gt;

&lt;p&gt;More than just functional, I wanted the app to feel &lt;em&gt;delightful&lt;/em&gt;.  I wanted players to feel excitement when completing their bingo card.&lt;/p&gt;

&lt;p&gt;When your card is first generated, each tile starts facing away from you.  The tiles then begin to flip over one by one using a 3D CSS transform to reveal their category.  The animation speeds up to a flurry before dramatically pausing on the final, centermost tile.&lt;/p&gt;

&lt;p&gt;Although I think it’s a lot of fun, I knew an animation this long could annoy some users.  I made sure to include a Skip button, and to also respect the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prefers-reduced-motion&lt;/code&gt; preference for those users with a sensitivity to motion.&lt;/p&gt;

&lt;p&gt;Of course, the win detection needed some pizzazz, too.  I opted to incorporate the lovely &lt;a href=&quot;https://github.com/crashmax-dev/fireworks-js&quot;&gt;fireworks-js&lt;/a&gt; for a dazzling finish.&lt;/p&gt;

&lt;h2 id=&quot;in-closing&quot;&gt;In Closing&lt;/h2&gt;

&lt;p&gt;This started as a fun weekend endeavour, but quickly turned into a more ambitious project.  It involved a lot of learning, planning, and development.  I’m extremely happy with the end result though, and am beyond excited for the next Backlog Burner event in May where we’ll be showcasing this new version.&lt;/p&gt;

&lt;p&gt;As always, the code is licensed under MIT and available on GitHub.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://wescook.ca/BacklogBingo/&quot;&gt;Visit web app&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/WesCook/BacklogBingo&quot;&gt;View on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
				<pubDate>Sun, 28 Apr 2024 18:30:00 +0000</pubDate>
				<link>https://wescook.ca/projects/backlog-bingo/</link>
				<guid isPermaLink="true">https://wescook.ca/projects/backlog-bingo/</guid>
				
				
				<category>projects</category>
				
			</item>
		
			<item>
				<title>Handy Dandy Notebook</title>
				<description>&lt;p&gt;&lt;a href=&quot;https://melvoridle.com/&quot;&gt;Melvor Idle&lt;/a&gt; is an idle (or “incremental”) game based around the RuneScape IP.  I started playing this one during a period where I was put out by renovations and had no office space.  It kept me busy and paired well with audiobooks, another hobby of mine.&lt;/p&gt;

&lt;h2 id=&quot;mod-support&quot;&gt;Mod Support&lt;/h2&gt;

&lt;p&gt;Melvor released an expansion last week, and with it came an official mod manager.  Now writing scripts for idle games is not uncommon (for many that becomes the game itself), but official mod support for a web-based game is something I’ve never seen before.  I was impressed and wanted to take a look.&lt;/p&gt;

&lt;p&gt;I spent the first three days of the expansion tinkering with the new API and mod manager.  The API is great, and offers useful hooks, storage features and lifecycle events.  That’s everything you need to write a mod.  The development experience however left much to be desired.&lt;/p&gt;

&lt;p&gt;At least at the time of writing, you’re required to zip up your working folder, upload it to a third-party website, and then download the files through the game.  This added significant development time compared to testing locally.  The hosting provider also seemed to experience frequent caching issues, which means you’re never sure if your changes have synced or not.  I admire the developers for integrating mod support at all, but this is a major hurdle right now.&lt;/p&gt;

&lt;h2 id=&quot;first-mod&quot;&gt;First Mod&lt;/h2&gt;

&lt;p&gt;The first mod I wanted to write is a notepad widget for keeping track of goals and other information.  Inspired by one of my favourite cartoons as a lad, I named it &lt;a href=&quot;https://mod.io/g/melvoridle/m/handy-dandy-notebook&quot;&gt;Handy Dandy Notebook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Melvor makes heavy use of a library for creating modals called SweetAlert2.  To maintain visual consistency I opted to use the same, which turned out to be a major decision.  SweetAlert takes care of all the visual design and accessibility features which let me focus on the content, so I was pleased with how quickly it let me get an MVP up.  I did however start to run into design considerations that did not include my use case.  I was worried I’d have to rewrite the modal functionality myself, but eventually all problems were resolved or worked around.&lt;/p&gt;

&lt;p&gt;As modding for this game is still at its infancy, a lot of things have yet to be standardized.  One question I had was “where to place the notebook button”?  After consideration I decided it’d be best to just let the user decide, so that’s been exposed as a user setting.  I added a second setting to disable animations for low-powered devices such as older Android phones.&lt;/p&gt;

&lt;p&gt;One of the biggest concerns I had going in was how to handle storage limits.  Each mod can store a maximum of 8KB per character.  If I went over that limit, the game would simply refuse to save and people would lose their notes.  I built in a couple levels of safeties to prevent this from happening.&lt;/p&gt;

&lt;p&gt;First, the game calculates the byte length of the input string.  It issues a visual warning at 7,300 bytes, and physically prevents further typing at 7,800.  This allows some headroom for the JSON encoding and future additions.  While it’s less accurate, I also used the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxlength&lt;/code&gt; attribute as a secondary failsafe.&lt;/p&gt;

&lt;p&gt;If you’re curious, 8KB is about 14 paragraphs of Lorem Ipsum.&lt;/p&gt;

&lt;h2 id=&quot;release&quot;&gt;Release&lt;/h2&gt;

&lt;p&gt;The reception has been very warm.  A number of commenters have shared that it solves a real problem for them of needing to keep text documents or real notepads.  I often keep a todo.txt file handy for the same reason, so that was definitely a main motivation for me in writing the mod.&lt;/p&gt;

&lt;p&gt;On the very next day after release, I realized I could have squeezed another 40% out of the available storage space by gzipping the note data before saving it.  I regret not considering this before.  I wrote up methods to encode, decode, and test for gzipped data, but at this point I’m too hesitant to risk other people’s notes from a bug.  For now, I think I’ll sit on the code and see if the existing storage limits prove to be a problem.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Although the development experience currently leaves much to be desired, I’m still thrilled to see official mod support in an online game like Melvor.  I already have ideas for other mods, but may give it a little time to mature before starting another project.&lt;/p&gt;

&lt;p&gt;Handy Dandy Notebook is available for Melvor on the web, Steam, or mobile.  It can be subscribed to directly from the &lt;a href=&quot;https://mod.io/g/melvoridle/m/handy-dandy-notebook&quot;&gt;mod.io page&lt;/a&gt; or using the in-game mod manager.  The source code is &lt;a href=&quot;https://github.com/WesCook/HandyDandyNotebook&quot;&gt;available on GitHub&lt;/a&gt; under an MIT license.&lt;/p&gt;
</description>
				<pubDate>Tue, 25 Oct 2022 16:00:00 +0000</pubDate>
				<link>https://wescook.ca/projects/handy-dandy-notebook/</link>
				<guid isPermaLink="true">https://wescook.ca/projects/handy-dandy-notebook/</guid>
				
				
				<category>projects</category>
				
			</item>
		
			<item>
				<title>Steam Market List</title>
				<description>&lt;p&gt;For those with large Steam libraries, &lt;a href=&quot;https://steamcommunity.com/tradingcards/&quot;&gt;trading cards&lt;/a&gt; have been a way of earning store credit in exchange for a little time.  They can be listed on the Steam market where you’ll receive a few cents for each card purchased.  With enough cards, this can add up.&lt;/p&gt;

&lt;p&gt;I’ve avoided bothering with this process until now because the time investment really didn’t justify the return for me.  But with enough automation, it’s not unreasonable to be able to earn a few free games from the setup cost alone.&lt;/p&gt;

&lt;p&gt;Tools already existed for &lt;a href=&quot;https://github.com/JustArchiNET/ArchiSteamFarm&quot;&gt;idling cards&lt;/a&gt; and &lt;a href=&quot;https://github.com/Nuklon/Steam-Economy-Enhancer&quot;&gt;listing them on the market&lt;/a&gt;.  The final step for me was automatically confirming every market transaction.&lt;/p&gt;

&lt;p&gt;Valve have added various protections to their market over the years to prevent issues of account theft and scamming.  Market transactions can be verified either through their mobile app, or through email.  I decided to use the email method as it’s easier to automate.&lt;/p&gt;

&lt;p&gt;Every market transaction generates one email.  That email includes one button to create the listing, and another to cancel it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/steam-trading-card.png&quot; alt=&quot;Steam Market Confirmation Email&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Because I’m generating hundreds of transactions at a time, this would take a lot of manual clicking to complete.  I opted to write a tool to scrape these emails for the confirmation links then exports them into a big list.  Gmail automatically caps each email thread at 100 messages, which is a good number to work with in batches.&lt;/p&gt;

&lt;p&gt;My first thought was to write a Greasemonkey script, but the Gmail source was very soupy and difficult to parse.  It is also likely to change frequently.  I decided instead to investigate &lt;a href=&quot;https://developers.google.com/apps-script&quot;&gt;Google Apps Script&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’d never used Apps Script, but it’s pretty cool.  It exposes an API to perform work on Google services.  I was able to whip up a script to loop through my inbox and generate a list of market confirmation links in about half an hour.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/steam-market-list-webapp.png&quot; alt=&quot;Steam Market List - Web App&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The default option was to create a web app, but I also noticed the option to export as a workspace add-on.  Add-ons are those mini apps that sit in the Gmail sidebar.  I decided it might be worth the convenience of having this functionality built into Gmail, so I gave it a look.&lt;/p&gt;

&lt;p&gt;It turns out that add-ons require considerably more effort to create.  Rather than running straight on the Apps Script engine, they require you to set up a Google Cloud Platform project.  This then requires defining IAM roles, OAuth scopes, and numerous API integrations.&lt;/p&gt;

&lt;p&gt;After setting up the project, I needed to learn the UI toolkit.  The add-on UI system seems to behave similarly to Android, whereby “pages” are cards being pushed to and popped from a stack.  I ported over the logic and wrote a minimal UI for interacting with.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/steam-market-list-addon.png&quot; alt=&quot;Steam Market List - Add-on&quot; /&gt;&lt;/p&gt;

&lt;p&gt;With that complete, I was able to generate a list of confirmation links with one button press.  These URLs can then be fed into something like &lt;a href=&quot;https://www.postman.com/&quot;&gt;Postman&lt;/a&gt; to generate network requests.&lt;/p&gt;

&lt;p&gt;To keep it simple I found a &lt;a href=&quot;https://melanto.com/apps/bulk-url-opener/&quot;&gt;Chrome addon&lt;/a&gt; that opens all links for you, which I then discard.  This saved me from having to extract the Steam login cookies into an external tool.&lt;/p&gt;

&lt;p&gt;I decided not to publish the add-on for public use as it’s not very polished, and it’s an admittedly niche use case.  If there’s much interest I can look into that though.&lt;/p&gt;

&lt;p&gt;I’ve &lt;a href=&quot;https://github.com/WesCook/SteamMarketList&quot;&gt;published the source code&lt;/a&gt; for both the web app and add-on version of Steam Market List on Github.  Basic install instructions are included if you’d like to set it up yourself.&lt;/p&gt;
</description>
				<pubDate>Fri, 02 Jul 2021 22:00:00 +0000</pubDate>
				<link>https://wescook.ca/projects/steam-market-list/</link>
				<guid isPermaLink="true">https://wescook.ca/projects/steam-market-list/</guid>
				
				
				<category>projects</category>
				
			</item>
		
			<item>
				<title>Peppers</title>
				<description>&lt;p&gt;I finally tried my hand at gardening this year.  As a huge fan of spicy food, I was excited to try growing chili peppers.&lt;/p&gt;

&lt;p&gt;To say I was a novice would be an understatement.  A good friend who’s been growing for years helped me get started.  He walked me through germination, soil types, and the basic equipment needed.&lt;/p&gt;

&lt;p&gt;Generously, he also sent me the seeds of some interesting pepper species.  The varieties were:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Bleeding Heart Yellow&lt;/li&gt;
  &lt;li&gt;Brazilian Pumpkin&lt;/li&gt;
  &lt;li&gt;Carolina Reaper&lt;/li&gt;
  &lt;li&gt;Count Dracula&lt;/li&gt;
  &lt;li&gt;Jorge’s Small Mexican&lt;/li&gt;
  &lt;li&gt;Pimenta Kathumby&lt;/li&gt;
  &lt;li&gt;Red Lightning Habanero&lt;/li&gt;
  &lt;li&gt;Rooster Spur&lt;/li&gt;
  &lt;li&gt;Uchu Creme&lt;/li&gt;
  &lt;li&gt;Variegated Purple&lt;/li&gt;
  &lt;li&gt;White Thai&lt;/li&gt;
  &lt;li&gt;Wild Brazil&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m thankful for all his help.  He sells a huge variety of seeds which you can find at &lt;a href=&quot;https://mattspeppers.com/&quot;&gt;MattsPeppers.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve included a gallery of progress photos.  Forgive the older camera; it’s long in the tooth at this point.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;prep&quot;&gt;Prep&lt;/h2&gt;

&lt;p&gt;March 11&lt;/p&gt;

&lt;p&gt;I started with 12 species of seeds.  I soaked them in water for 24 hours to weaken the shells and improve germination.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0387.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0387.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0388.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0388.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0389.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0389.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I quickly ran out of containers and resorted to measuring cups.&lt;/p&gt;

&lt;h2 id=&quot;planting&quot;&gt;Planting&lt;/h2&gt;

&lt;p&gt;March 12&lt;/p&gt;

&lt;p&gt;For germination I bought a &lt;a href=&quot;https://www.amazon.ca/dp/B07XNQT52F/&quot;&gt;seedling tray&lt;/a&gt;.  It contained a dome to keep in moisture, and a heating pad to keep the seeds warm.&lt;/p&gt;

&lt;p&gt;With 24 cells available, I planted four seeds from each variety (side-by-side, with two seeds in each cell).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0390.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0390.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0391.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0391.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0392.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0392.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0393.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0393.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;sprouting&quot;&gt;Sprouting&lt;/h2&gt;

&lt;p&gt;March 20 - April 10&lt;/p&gt;

&lt;p&gt;After only eight days the first seedling sprouted.  Very exciting!  Before long another five cropped up.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0398.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0398.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0403.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0403.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0405.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0405.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0406.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0406.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0419.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0419.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately that’s when progress tapered off.  Of the 48 seeds I planted, only six sprouted in three weeks.  Those six were doing well, but with a smidge of disappointment I turned off the heating pad.&lt;/p&gt;

&lt;h2 id=&quot;late-bloomers&quot;&gt;Late Bloomers&lt;/h2&gt;

&lt;p&gt;April 16 - May 24&lt;/p&gt;

&lt;p&gt;It seems I was too impatient, as within another week I started seeing sprouts from almost every cell.  Soon they were all rapidly growing.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0423.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0423.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0431.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0431.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0441.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0441.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0444.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0444.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0446.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0446.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sadly the reapers never germinated at all.  I later learned they’re especially tricky, and I should’ve planted more seeds.&lt;/p&gt;

&lt;h2 id=&quot;first-transplant&quot;&gt;First Transplant&lt;/h2&gt;

&lt;p&gt;May 29 - June 16&lt;/p&gt;

&lt;p&gt;At this point they were growing too large for their cells.  With the confidence of someone who just watched a YouTube video on the subject, I began transplanting them to new pots.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0455.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0455.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0462.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0462.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0471.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0471.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0476.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0476.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see what’s either a count dracula or variegated purple starting to show its color.&lt;/p&gt;

&lt;h2 id=&quot;first-flowers&quot;&gt;First Flowers&lt;/h2&gt;

&lt;p&gt;June 19&lt;/p&gt;

&lt;p&gt;After three months, flower buds began to form and open.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0480.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0480.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0483.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0483.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;June 22&lt;/p&gt;

&lt;p&gt;Today the plants got a little parched from the heat.  I brought them inside and rehydrated.  They bounced back within hours.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0484.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0484.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0486.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0486.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0488.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0488.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;June 29&lt;/p&gt;

&lt;p&gt;Back outside and continuing to grow strong.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0497.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0497.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0500.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0500.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;second-transplant&quot;&gt;Second Transplant&lt;/h2&gt;

&lt;p&gt;July 13&lt;/p&gt;

&lt;p&gt;By this point the largest plants had long-outgrown their pots.  I bought 12 &lt;a href=&quot;https://www.amazon.ca/dp/B07S5K7J9M/&quot;&gt;fabric bags&lt;/a&gt; for the next stage and transplanted them again.  I didn’t have any soil to fill them unfortunately, and delayed a couple weeks longer than I should have.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0504.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0504.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0505.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0505.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0506.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0506.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;fruiting&quot;&gt;Fruiting&lt;/h2&gt;

&lt;p&gt;July 14&lt;/p&gt;

&lt;p&gt;Four months in, the first pepper began to grow.  It was a variegated purple.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0512.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0512.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;July 16&lt;/p&gt;

&lt;p&gt;I didn’t have enough fabric bags, so a few plants had to stay in their previous pots.  They quickly became root bound and floundered.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0519.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0519.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;July 17 - July 24&lt;/p&gt;

&lt;p&gt;Those that were transplanted continued to do well.  The purple species were first to fruit, along with the Brazilian pumpkin (a pepper, not a gourd).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0528.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0528.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0532.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0532.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0536.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0536.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;July 26&lt;/p&gt;

&lt;p&gt;We had a small heat wave so I took the plants in for protection.  Day and night shot.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0544.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0544.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0542.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0542.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;August 5 - August 27&lt;/p&gt;

&lt;p&gt;Those that did well continued to fruit.&lt;/p&gt;

&lt;p&gt;I had the most success with the count dracula, Jorge’s small mexican, Brazilian pumpkin, variegated purple, and white thai.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0547.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0547.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0555.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0555.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0560.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0560.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0562.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0562.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0570.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0570.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0571.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0571.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0572.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0572.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0576.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0576.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0577.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0577.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0579.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0579.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0582.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0582.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;harvesting&quot;&gt;Harvesting&lt;/h2&gt;

&lt;p&gt;August 27 - September 30&lt;/p&gt;

&lt;p&gt;Truthfully, I hadn’t planned this far ahead.  So with no greater ambitions I ate a few raw, and turned the rest into chili flakes.  The color was a vibrant red as opposed to the darker store-bought flakes.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0583.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0583.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0588.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0588.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0590.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0590.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0595.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0595.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Over the next month I harvested what I could.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0613.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0613.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0614.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0614.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0628.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0628.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0619.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0619.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0622.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0622.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;/img/peppers-2020/full/peppers-0631.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0631.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the time Autumn was in full swing, the peppers stopped ripening.  I plucked what remained and placed them on a covered plate to let them ripen indoors.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/peppers-2020/full/peppers-0638.jpg&quot;&gt;&lt;img src=&quot;/img/peppers-2020/thumb/peppers-0638.jpg&quot; width=&quot;400&quot; height=&quot;300&quot; alt=&quot;Chili Pepper&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;I enjoyed growing peppers this year.  I found it quite fulfilling and intend to do more next year.  I think I’ll try adding in some other crops, too.  I’ve already ordered more fabric bags in anticipation.&lt;/p&gt;

&lt;p&gt;There’s a few things I’ll know how to do better in the future.  I know now I should have pruned the plants early in their growth cycle, and I should have transplanted sooner.  This likely would have improved the overall yield.&lt;/p&gt;

&lt;p&gt;In the end though I wasn’t too fussed about it.  I was more interested in growing something for the experience of it, and from that perspective I’m really happy with the result.  I’m already looking forward to next Spring.&lt;/p&gt;
</description>
				<pubDate>Sat, 14 Nov 2020 03:00:00 +0000</pubDate>
				<link>https://wescook.ca/blog/peppers/</link>
				<guid isPermaLink="true">https://wescook.ca/blog/peppers/</guid>
				
				
				<category>blog</category>
				
			</item>
		
			<item>
				<title>Flair Keeper</title>
				<description>&lt;p&gt;I’ve spent my last few weekends working on a new web app.  Similar to &lt;a href=&quot;https://wescook.ca/projects/steampunk-spider/&quot;&gt;Steampunk Spider&lt;/a&gt;, this tool was built to aid in repetitive mod tasks.  It was written for the Dark Souls network of subreddits.&lt;/p&gt;

&lt;p&gt;The Souls subreddits offer a unique system of displaying trophies next to user’s names.  Each game has a trophy, and the trophies indicate that a user has completed all achievements in that respective title.  It’s a small boast of skill and way of showing an appreciation for the series.&lt;/p&gt;

&lt;p&gt;The technical implementation of these trophies is a little messy.  They are built on reddit’s flair system, and controlled by a unique flair code.  Due to limitations of the platform, a spritesheet must be used to display the icons.  Every permutation of trophy set needs to be programmed in for the flair code to function.&lt;/p&gt;

&lt;p&gt;Previously these codes were entered by hand.  It required memorizing the ID for each game, then stringing them together in release order.  Along with case-sensitivity and variations on new reddit, this introduced a lot of potential for human error.&lt;/p&gt;

&lt;p&gt;Adding a new game to the system also required a significant amount of work.  Every new permutation needed to be manually coded in, and the spritesheet had to be freshly created.  It was not an enviable task.&lt;/p&gt;

&lt;p&gt;So when building Flair Keeper, I had two goals in mind:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Allow for a faster way to update a user’s flair (no entering codes manually).&lt;/li&gt;
  &lt;li&gt;Come up with a way to generate these spritesheets automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/img/flair-keeper-home.png&quot; alt=&quot;Flair Keeper Home Screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I’m pleased to say that both goals have been met.&lt;/p&gt;

&lt;p&gt;A simple UI allows a moderator to select which trophies they wish to enable.  This produces a flair code for easy copying.  It also allows for automatic import and export of a user’s trophies.  The games are specified in a JSON config file, which makes adding new titles very easy.&lt;/p&gt;

&lt;p&gt;The spritesheet functionality ended up being a fair bit more complicated.&lt;/p&gt;

&lt;p&gt;Initially, calculating the permutations wasn’t that difficult.  There’s plenty of code examples for generating the &lt;a href=&quot;https://en.wikipedia.org/wiki/Power_set&quot;&gt;power set&lt;/a&gt; of an array using recursion.  I adapted the code for JavaScript and it worked without a hitch.&lt;/p&gt;

&lt;p&gt;However, during internal discussions of the project we came up with the idea to offer trophy “upgrades” for completing more difficult feats.  This required a new variant system, where each trophy could have 1 of n available variations.  This complicated the math, and increased the number of permutations exponentially.&lt;/p&gt;

&lt;p&gt;I couldn’t find any research online on generating permutations with exclusive pairs as this required.  And unfortunately, my own math skills were not up to the task.  Thankfully I got some brilliant help from a friend who was able to crack this one wide open.  The solution required generating a new recursive branch for each variant on every iteration (including the “base” variant).&lt;/p&gt;

&lt;p&gt;The total number of permutations are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;((x + 1) ^ y) - 1&lt;/code&gt;, where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; is the number of variants and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt; is the number of game entries.  As a result, you can only maintain a small number of games and variants before the numbers become too large to work with.&lt;/p&gt;

&lt;p&gt;In our case, the total was only 728 permutations (6 games, 2 variants).&lt;/p&gt;

&lt;p&gt;Once the permutations are calculated, the generator uses a JS canvas to draw the icons in a matching pattern.  An image is generated from that canvas, and the CSS is printed alongside it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/flair-keeper-spritesheet.png&quot; alt=&quot;Flair Keeper Spritesheet Screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This was the first proper ES6 project I’ve written from scratch.  I was already a big fan of arrow functions and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.map&lt;/code&gt; features in ES6, but now I was able to work with some features I’d only touched on before.  Specifically, async and modules.&lt;/p&gt;

&lt;p&gt;This project required a lot of network functionality, so it was a perfect time to learn async techniques.  While I have worked with promises before, I didn’t have a great mental model for how they worked under the hood.  Being able to implement functions using both promises and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;async&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;await&lt;/code&gt; gave me a better insight into how they were really just two sides of the same coin.&lt;/p&gt;

&lt;p&gt;I was also very glad to work with modules.  In the past I’ve ended up writing massive .js files which encompassed the entire program.  This time I wanted to focus on writing more portable code.  Modules were perfect for that.  They encouraged writing functions that were both pure and idempotent; basically little libraries.&lt;/p&gt;

&lt;p&gt;One technology that was new to me was OAuth.  I have used wrappers for OAuth in the past, but I’ve never needed to implement the details myself.  It gave me a better understanding of how the authorization protocol worked.  It was important to understand scope, state, and grant types.&lt;/p&gt;

&lt;p&gt;My first attempt involved generating a refresh token manually and saving it to the config file.  This approach was quick and easy to implement.  However as I had the intention to host this on the public web, it presented too big of a security risk to send that information over the wire.&lt;/p&gt;

&lt;p&gt;I went back to the drawing board and reimplemented the code grant flow on the client-side.  This ended up being more secure and more flexible, as I was able to include user-specific functionality.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/flair-keeper-settings.png&quot; alt=&quot;Flair Keeper Spritesheet Screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flair Keeper was a lot of fun to work on.  The scope grew as it always does, but the end result was better for it (which is not always the case).  I’m still pleased with the quality of the code, and I’m happy I was able to learn some new technologies along the way.&lt;/p&gt;

&lt;p&gt;Flair Keeper is open-source, and licensed as MIT.  You can &lt;a href=&quot;https://github.com/WesCook/FlairKeeper&quot;&gt;find it at the usual place&lt;/a&gt;.&lt;/p&gt;
</description>
				<pubDate>Tue, 08 Sep 2020 22:30:00 +0000</pubDate>
				<link>https://wescook.ca/projects/flair-keeper/</link>
				<guid isPermaLink="true">https://wescook.ca/projects/flair-keeper/</guid>
				
				
				<category>projects</category>
				
			</item>
		
			<item>
				<title>Let&apos;s Play</title>
				<description>&lt;p&gt;The &lt;a href=&quot;https://store.steampowered.com/app/211420/DARK_SOULS_Prepare_To_Die_Edition/&quot;&gt;original Dark Souls&lt;/a&gt; is without a doubt my game of the decade.  Everything from its interconnected world, skillful mechanics, and ever-deepening story shows the brilliance in its design.  What really keeps me coming back though is the creativity of its modding community.&lt;/p&gt;

&lt;p&gt;There’s mods to &lt;a href=&quot;https://www.nexusmods.com/darksouls/mods/19&quot;&gt;fix bugs&lt;/a&gt; and &lt;a href=&quot;https://www.nexusmods.com/darksouls/mods/19&quot;&gt;improve quality-of-life&lt;/a&gt;.  There’s mods to &lt;a href=&quot;https://www.nexusmods.com/darksouls/mods/1265&quot;&gt;increase difficulty&lt;/a&gt; and &lt;a href=&quot;https://www.nexusmods.com/darksouls/mods/1305&quot;&gt;modify progression&lt;/a&gt;.  There’s even mods which &lt;a href=&quot;https://www.nexusmods.com/darksouls/mods/1418&quot;&gt;change and expand the world&lt;/a&gt; in completely new ways.&lt;/p&gt;

&lt;p&gt;The relatively-new mod &lt;a href=&quot;https://www.nexusmods.com/darksouls/mods/1524&quot;&gt;Daughters of Ash&lt;/a&gt; falls into this latter category.  A massive expansion to the game, it adds new items, bosses, and storylines.&lt;/p&gt;

&lt;p&gt;I’d been following its development for some months and was excited to finally give it a try.  Almost on a whim I decided to record this playthrough.  I thought it might be interesting to compare notes with others, and I was also looking forward to getting some experience with video editing.  This would be my first “Let’s Play”.&lt;/p&gt;

&lt;p&gt;I opted to use &lt;a href=&quot;https://obsproject.com/&quot;&gt;OBS&lt;/a&gt; for recording.  I’m a big fan of free and open-source software, and it came highly recommended.&lt;/p&gt;

&lt;p&gt;I didn’t start with a video editor.  In fact my early episodes consisted of stitching mkv files together and uploading the resulting file.  YouTube offers a rudimentary video editor which allowed me to trim and split the clips.  It was rough, but it worked.&lt;/p&gt;

&lt;p&gt;The first ten episodes or so came quickly.  I was able to record them in rapid succession - usually one per day.  As a result of the tooling I ended up keeping in most of the content unaltered.&lt;/p&gt;

&lt;p&gt;Eventually however I started to run into limitations with my recording setup.  I was only recording to a single audio channel, and that made it impossible to stifle coughs or other noises.  I was also reaching a point in the playthrough which required that I cut out content more often.  YouTube’s editor was not equipped for either of these tasks.&lt;/p&gt;

&lt;p&gt;I decided to address the audio issue first.  I looked into configuring OBS to record to multiple audio channels: one for the game, one for my voice.  This necessitated switching into the “Advanced mode” but otherwise seemed to work just fine.  I discovered that YouTube ignores multiple audio channels, so I had to remux them into a single stream before upload.&lt;/p&gt;

&lt;p&gt;Next I needed a video editor.  I shopped around before deciding on &lt;a href=&quot;https://www.blackmagicdesign.com/products/davinciresolve/&quot;&gt;Davinci Resolve 16&lt;/a&gt;.  As a professional editor it offered a lot more tools than I really needed, but it offered Linux support and the price was right.  I spent some time recording sample clips and playing around with the timeline tools, transitions, and the other features I thought I’d need.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/davinci-resolve.png&quot;&gt;&lt;img src=&quot;/img/davinci-resolve-thumb.png&quot; width=&quot;800&quot; height=&quot;544&quot; alt=&quot;Resolve editor interface&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first video was a disaster.  It turned out that when I switched OBS into Advanced mode it didn’t preserve any of my original settings.  I was so preoccupied with testing the audio channels I didn’t think to compare the video quality.  The episode I recorded was so low-quality that I had to discard the entire thing.&lt;/p&gt;

&lt;p&gt;I spent some more time getting the settings right.  It wasn’t trivial to copy over the “Simple” preset, and it involved browsing a lot of forum pages and even the source code to understand what had changed.  Eventually I got my settings worked out so they looked just as good (if not better) than the original preset did.&lt;/p&gt;

&lt;p&gt;Equipped with some new tools, I started making real improvements to the quality of the videos.  I was able to edit out any excess noise from neighbors or physical button clicks.  Transitions became a lot more natural, rather than being hard cuts at every turn.  I normalized the audio to make it more consistent.  I also started experimenting with small overlays to add additional details (or sometimes just to laugh at myself).&lt;/p&gt;

&lt;p&gt;One issue that I was never able to fully solve was YouTube compression.  I could export a video at a crisp 1080p, but after YouTube processing it would turn muddy and dark.  I spent a few days recording sample clips just to test different encodings and bitrates to learn how YouTube would treat it.  After a lot of testing, I found some optimal settings and determined that anything above a bitrate of 20K would be lost to compression.&lt;/p&gt;

&lt;p&gt;Other obstacles cropped up.  It turns out that video files are pretty large.  I had to buy a dedicated 1TB SSD to work on, as the optimized media for editing can easily exceed 600GB for an hour-long episode.  A solvable problem, but now I was spending real money.&lt;/p&gt;

&lt;p&gt;As I became more proficient with editing, it started taking longer to produce each episode.  I wanted to get it all perfect: the sounds equalized, the content properly paced.  All these little improvements slowed things down.  I’d even started recording at night to try and reduce background noise.  What had started as an episode per day had turned into one every 1-2 weeks.&lt;/p&gt;

&lt;p&gt;I was producing better quality content, but the process had started feeling more tedious and unfun.  Without my realizing it had become a second job.&lt;/p&gt;

&lt;p&gt;This may be the curse of the perfectionist.  The more options we have available, the more decisions there are to be made.&lt;/p&gt;

&lt;p&gt;It reminds me a little of programming.  When we start, we really don’t care about “clean code”, design patterns, or abstractions.  We’re just excited to see our code work.  It’s only with experience that we start to notice all the things that can be improved.  Maybe that’s how it feels to become more proficient in any field?&lt;/p&gt;

&lt;p&gt;By the time I’d reached the final episode, I was ready to move on.  I’d learned - and not for the first time - that the perfect is the enemy of the good.&lt;/p&gt;

&lt;p&gt;I am glad I embarked on this project though.  I’ve learned a lot about video production which may actually come in handy in the future.  I’ve also created something of which I can be proud.  It might fluctuate in quality here and there, and the rough start might put some people off.  But in a funny way it shows both my progress through the game and my progress as an editor.  I can appreciate the symmetry of that.&lt;/p&gt;

&lt;p&gt;You can &lt;a href=&quot;https://www.youtube.com/playlist?list=PL3Xb_yuSwD8BLaUEwm6S5_5kie5faouPP&quot;&gt;find my LP of Daughters of Ash on YouTube&lt;/a&gt;.  Big thanks to Grimrukh, the developer &lt;a href=&quot;https://www.nexusmods.com/darksouls/mods/1524&quot;&gt;of the mod&lt;/a&gt; for his amazing contribution to the Souls community.&lt;/p&gt;
</description>
				<pubDate>Wed, 15 Jan 2020 05:00:00 +0000</pubDate>
				<link>https://wescook.ca/blog/lets-play/</link>
				<guid isPermaLink="true">https://wescook.ca/blog/lets-play/</guid>
				
				
				<category>blog</category>
				
			</item>
		
			<item>
				<title>Hacktoberfest 2019</title>
				<description>&lt;p&gt;This year I learned about an annual event named Hacktoberfest.  Hosted by Digital Ocean, it encourages programmers to contribute to various open source projects.&lt;/p&gt;

&lt;p&gt;You can sign up at the &lt;a href=&quot;https://hacktoberfest.digitalocean.com/&quot;&gt;Hacktoberfest page&lt;/a&gt;.  Participation requires submitting four valid push request during the month of October.  The PRs don’t have to be accepted, but they should be more substantial than simple typo or formatting fixes.&lt;/p&gt;

&lt;p&gt;It sounded like fun, so I decided to try my hand at it.&lt;/p&gt;

&lt;p&gt;I spent the first hour or so searching through the list of projects that Digital Ocean suggested.  I found that I wasn’t familiar with most of them, and that many had sizable codebases.  As I’m not very comfortable making code changes without first knowing a product, I decided to search out some projects I was more familiar with instead.&lt;/p&gt;

&lt;p&gt;Ultimately I decided that although Java is not my strongest language, I have played with enough Minecraft mods to make useful contributions to that ecosystem.  I began searching Github by the tag &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;minecraft-mod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I decided on four mods: &lt;a href=&quot;https://www.curseforge.com/minecraft/mc-mods/agricraft&quot;&gt;AgriCraft&lt;/a&gt;, &lt;a href=&quot;https://www.curseforge.com/minecraft/mc-mods/cooking-for-blockheads&quot;&gt;Cooking for Blockheads&lt;/a&gt;, &lt;a href=&quot;https://www.curseforge.com/minecraft/mc-mods/waystones&quot;&gt;Waystones&lt;/a&gt;, and &lt;a href=&quot;https://www.curseforge.com/minecraft/mc-mods/rustic&quot;&gt;Rustic&lt;/a&gt;.  Each a medium-sized mod that I’ve used before.&lt;/p&gt;

&lt;p&gt;I scanned their respective issues lists and found a feature request or bugfix from each that I thought I could do.  This ensured that my PR would actually be welcome.  With four issues picked out, I set off to work.&lt;/p&gt;

&lt;p&gt;One thing that is never fun in Minecraft mod development is setting up the dev environment.  Getting IntelliJ, Gradle, Minecraft, Forge, and the various dependency mods to play nicely can be like herding cats.  This took some hours.&lt;/p&gt;

&lt;p&gt;Once I got down to implementing the feature or bug though, it was actually a lot of fun.  The problems became strictly logical at that point and could be worked through with enough perseverance.  If something’s not working, it’s possible to trace it through the code to understand why.&lt;/p&gt;

&lt;p&gt;I won’t go into detail of every PR, but these were my four:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/AgriCraft/AgriCraft/pull/1244&quot;&gt;https://github.com/AgriCraft/AgriCraft/pull/1244&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/blay09/CookingForBlockheads/pull/402&quot;&gt;https://github.com/blay09/CookingForBlockheads/pull/402&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/blay09/Waystones/pull/183&quot;&gt;https://github.com/blay09/Waystones/pull/183&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/the-realest-stu/Rustic/pull/273&quot;&gt;https://github.com/the-realest-stu/Rustic/pull/273&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the case of the last one, I was happy to add a new feature while reducing the total lines of code by eliminating a loop.&lt;/p&gt;

&lt;p&gt;All in all, it was a very dense three days of coding.  Two of my PRs have been merged in already, and I’ll have to wait and see on the other two.&lt;/p&gt;

&lt;p&gt;Hacktoberfest seems like a really fine idea to me.  It’s encouraging people to get out and code, and to do so in a way that gets them comfortable contributing to open source.&lt;/p&gt;

&lt;p&gt;I might participate again next year, and maybe get a little more adventurous in trying out some new projects or languages.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/hacktoberfest-2019.png&quot; alt=&quot;Hacktoberfest 2019&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Sun, 13 Oct 2019 17:00:00 +0000</pubDate>
				<link>https://wescook.ca/blog/hacktoberfest-2019/</link>
				<guid isPermaLink="true">https://wescook.ca/blog/hacktoberfest-2019/</guid>
				
				
				<category>blog</category>
				
			</item>
		
			<item>
				<title>Contributing Code to Tildes</title>
				<description>&lt;p&gt;A &lt;a href=&quot;/projects/tildes-focus/&quot;&gt;few months ago&lt;/a&gt; I released a userscript called Tildes Focus.  I’ve since refined the project and submitted it upstream, and I’m pleased to say it’s now been merged into the site.&lt;/p&gt;

&lt;h2 id=&quot;new-feature&quot;&gt;New Feature&lt;/h2&gt;

&lt;p&gt;The feature works by expanding new comments on topic pages.  Rather than using a keyboard input like my previous project, this version adds a button named “Collapse read” to the page.  As you might guess, it collapses all previously-read comments to focus on what’s new.&lt;/p&gt;

&lt;p&gt;The idea is simple enough, although the logic ended up being more complex than I was expecting.&lt;/p&gt;

&lt;h2 id=&quot;the-logic&quot;&gt;The Logic&lt;/h2&gt;

&lt;p&gt;Tildes comments can be collapsed and expanded, which might lead you to believe that comments have two states.  However in actual fact comments have three states.  They are: expanded, collapsed, and collapsed-individual.&lt;/p&gt;

&lt;p&gt;The first two are self-explanatory.  Collapsed-individual is a special case where the comment is still collapsed with a brief preview of its contents, but its daughter comments remain visible.&lt;/p&gt;

&lt;p&gt;The logic went through a few iterations.  The first version collapsed all comments by default, then walked upwards from new comments to expand their parents.  Nice and simple.&lt;/p&gt;

&lt;p&gt;This almost worked, but created an inconsistency with the site’s server-side implementation.  It seems only one “layer” of comments should be collapsed, where expanding the shallowest comment in a branch would expand all within.&lt;/p&gt;

&lt;p&gt;After thinking about the problem for a while I decided to invert the logic.  All comments would start expanded.  We’d then find the shallowest comment within each branch and collapse it (solved by checking for an ancestor that’s collapsed, and a descendant that’s new).  Finally, we’d walk up from all new comments and expand as appropriate.&lt;/p&gt;

&lt;p&gt;This was an interesting project for a number of reasons.  It gave me a chance to learn some new technologies.  GitLab was a new platform for me, and while it has many similarities to GitHub there was still a slight learning curve.&lt;/p&gt;

&lt;p&gt;Vagrant was the biggest technology to learn.  In some ways it keeps things simpler by reducing dependencies, but it does so by adding a new layer of abstraction.  If things go wrong there’s one more layer you need to unpeel to understand what’s really going on.  And as it turns out, things did go wrong.&lt;/p&gt;

&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;

&lt;p&gt;I was on Windows at the time which made me the guinea pig for setting up a Windows 10 workspace.  This ended up exposing two significant bugs in the development process.&lt;/p&gt;

&lt;p&gt;The first was a dependency on symbolic links.  Apparently symlinks have been locked behind a security policy in Windows 10.  This makes security sense as symlinks are a major attack vector, but led to folder sync issues between Vagrant and npm.&lt;/p&gt;

&lt;p&gt;After some research, I fixed this by toggling the relevant security policy in secpol.msc.  A more permanent fix was &lt;a href=&quot;https://gitlab.com/tildes/tildes/commit/ac4e8e9b5444a3a9ac8d9a956c24ed0dedde79d7&quot;&gt;later implemented&lt;/a&gt; by Tildes to bypass symlink creation altogether and run the tools manually.&lt;/p&gt;

&lt;p&gt;The second problem was that of line endings; the age-old Windows/Unix issue that just won’t die.  The linter was looking for LF line endings, but git will by default checkout using your platform’s native line endings (which on Windows are CRLF).  As a result the pre-commit hooks would consistently fail.&lt;/p&gt;

&lt;p&gt;This was solved by configuring git to avoid converting line endings and triggering a fresh checkout, then manually converting the files I’d modified locally.  The linter was then happy and let me push my changes.&lt;/p&gt;

&lt;p&gt;The more permanent fix was to &lt;a href=&quot;https://gitlab.com/tildes/tildes/commit/541505382771c43a552aeb063fcedd68c250ce67&quot;&gt;specify this behaviour&lt;/a&gt; in the project’s .gitattributes file.&lt;/p&gt;

&lt;p&gt;With those problems solved, I was able to finally submit the new merge request.  After a few rounds of discussion and tweaks, the project was &lt;a href=&quot;https://gitlab.com/tildes/tildes/commit/4af861d44a9ecb4e8fcdf798e9f97eac495aac98&quot;&gt;rebased onto master&lt;/a&gt; and made live on the site.&lt;/p&gt;

&lt;h2 id=&quot;in-closing&quot;&gt;In Closing&lt;/h2&gt;

&lt;p&gt;I was really glad to work on this one.  It gave me a chance to play with some new technologies (Vagrant), and refresh my memory of some older ones (jQuery).&lt;/p&gt;

&lt;p&gt;It’s also really rewarding to see a feature you built show up in a site that you use every day.  I admit that might be a little vain, but it’s a nice bit of validation.&lt;/p&gt;

&lt;p&gt;I also have to give credit to the cleanliness of Tilde’s codebase.  The files were well-explained and organized.  Class names were logical and easy to work with.  It’s about as clean a foundation as you can have in a large project, so big credit to Deimos and the other contributors for doing a swell job.&lt;/p&gt;
</description>
				<pubDate>Sun, 08 Sep 2019 17:00:00 +0000</pubDate>
				<link>https://wescook.ca/blog/contributing-code-to-tildes/</link>
				<guid isPermaLink="true">https://wescook.ca/blog/contributing-code-to-tildes/</guid>
				
				
				<category>blog</category>
				
			</item>
		
			<item>
				<title>Tildes Focus</title>
				<description>&lt;p&gt;I’ve recently taken a liking to &lt;a href=&quot;https://tildes.net/&quot;&gt;Tildes.net&lt;/a&gt;.  It’s still a small community, but it feels much more comfortable than the wild west of other social networking sites.&lt;/p&gt;

&lt;p&gt;One issue I’ve noticed with large threads however is that it can be difficult to find new comments since your last visit.  New comments are distinguished visually but are still easy to miss when there’s a very active thread.&lt;/p&gt;

&lt;p&gt;To remedy this, I wrote a Greasemonkey script to navigate to the next new comment automatically.  It’s called &lt;a href=&quot;https://github.com/WesCook/TildesFocus&quot;&gt;Tildes Focus&lt;/a&gt; as it helps you focus on what’s new.&lt;/p&gt;

&lt;p&gt;I implemented this in Greasemonkey to help keep the complexity down.  When loading a thread, the script makes a list of all new comments to walk through them.  Keyboard shortcuts are used for navigation, and once you reach the end of the list it wraps back to the beginning.  A subtle background shading is used to highlight the selected comment and works nicely in all themes.&lt;/p&gt;

&lt;p&gt;When working on this script I discovered that JavaScript’s &lt;code&gt;scrollIntoView()&lt;/code&gt; function accepts options both for smooth scrolling, as well as vertical positioning (eg. center on element).  This allowed for a very clean implementation without leaning on jQuery or other libraries.&lt;/p&gt;

&lt;p&gt;Tildes Focus is &lt;a href=&quot;https://github.com/WesCook/TildesFocus&quot;&gt;available on Github&lt;/a&gt;, and licensed under MIT.&lt;/p&gt;
</description>
				<pubDate>Sat, 08 Jun 2019 16:00:00 +0000</pubDate>
				<link>https://wescook.ca/projects/tildes-focus/</link>
				<guid isPermaLink="true">https://wescook.ca/projects/tildes-focus/</guid>
				
				
				<category>projects</category>
				
			</item>
		
	</channel>
</rss>
