Jekyll2023-04-19T14:31:26+00:00https://blog.jackcrane.rocks/feed.xmlJack CraneA 'blog-type' list of stories and posts about projects I have built or am working onMaking a bulletproof volunteer registration system2023-03-25T15:44:36+00:002023-03-25T15:44:36+00:00https://blog.jackcrane.rocks/2023/03/25/paddlefest-volunteer<h2 id="the-task">The Task</h2>
<p>Last year, I was a volunteer coordinator for Paddlefest, a 3-day event that brings together 100+ volunteers to help run a 3-day event. Due to the complexity of the event and the multitude of moving parts of volunteer participation, we needed a custom solution to manage our volunteers. We needed a system that could:</p>
<ul>
<li>Allow volunteers to sign up for shifts under specific roles</li>
<li>Sort said shifts under different locations</li>
<li>Allow volunteers to sign up for multiple shifts</li>
<li>Collect waiver permissions</li>
</ul>
<p>Last year we took a really good stab at an initial solution. The UI was excellent but the backend was not as reliable as we would have liked, leading to a lot of data loss and manual work to recover from oddities. This year, I wanted to further develop the system to create a more robust solution that could handle more traffic and be more resilient.</p>
<h2 id="the-problems">The Problems</h2>
<p>The first problem was that the system was just generally unreliable. This was caused by a few different factors:</p>
<ul>
<li>Rushed deployment led to cutting corners in development</li>
<li>Poor testing that didn’t cover a wide range of scenarios and potential mistakes</li>
<li>Utilization of raw MongoDB instead of using a relational database or ORM to enforce data consistency.</li>
<li>Complete lack of error handling and logging. High risk operations were not wrapped in try/catch blocks and errors were not logged.</li>
<li>Deployment on a new, unstable kubernetes cluster that was not properly configured for production workloads.</li>
</ul>
<h2 id="the-solution">The Solution</h2>
<p>The solution was to completely rewrite the backend and completely re-imagine the way data was stored and handled. The new system was built with the following goals in mind:</p>
<ul>
<li><strong>Reliability</strong>: The system should be able to handle a large amount of traffic and be able to recover from errors without manual intervention.</li>
<li><strong>Scalability</strong>: The system should be able to handle a large amount of traffic without breaking.</li>
<li><strong>Maintainability</strong>: The system should be easy to understand and modify.</li>
<li><strong>Security</strong>: The system should be secure and not allow for any data tampering or unauthorized access.</li>
<li><strong>Autonomy</strong>: The system should be able to run without any developer intervention in future years.</li>
</ul>
<p>This was no small task. The backend has been completely rewritten with tons of tiny new features and improvements.</p>
<h3 id="backend-is-now-written-exclusively-in-nodejs-without-any-external-shell-scripts">Backend is now written exclusively in node.js without any external shell scripts</h3>
<p>Last year, we used a shell script to handle tasks like sending emails and managing database migrations and checks. This year, everything has been verticalized into node.js, allowing for a more reliable and debuggable system.</p>
<h3 id="app-is-now-deployed-on-a-stable-kubernetes-cluster-with-proper-configuration-for-production-workloads">App is now deployed on a stable kubernetes cluster with proper configuration for production workloads</h3>
<p>Deployment on a Kubernetes cluster provides a lot of benefits, including scaling, health checks, deployment, and fatal error recovery out of the box. Kubernetes is not new to the Paddlefest volunteer system but this year we are using a stable cluster that has been properly configured for production workloads.</p>
<h3 id="now-using-a-relational-database-mysql-instead-of-mongodb">Now using a relational database (MySQL) instead of MongoDB</h3>
<p>Last year we used a database called MongoDB, which you could think of as saving data in independent files. These files do not connect to one another and are not guaranteed to be consistent. This led to some really weird bugs and behaviors last year where the way some data was stored randomly changed a few times throughout the cycle as a side-effect of unrelated code changes. This year we are using a relational database called MySQL, which is a lot more stable and predictable.</p>
<h3 id="now-using-an-orm-prisma-to-enforce-data-consistency">Now using an ORM (Prisma) to enforce data consistency</h3>
<p>We are not writing raw SQL queries anymore. Instead, we are using an ORM called Prisma to handle all of our database interactions. This allows us to write code that is more readable and maintainable while also enforcing data consistency. It also makes the developer experience far better because we can use autocomplete and type checking to ensure that we are writing valid code, which abstracts the frustration and risk of writing raw SQL queries.</p>
<h3 id="now-using-a-proper-logging-system-and-error-handling-system-sentry-to-proactively-catch-errors-and-fix-them-even-without-a-bug-report">Now using a proper logging system and error handling system (Sentry) to proactively catch errors and fix them even without a bug report</h3>
<p>Last year we were depending on users to report bugs and errors. This year, we are using a proper logging system (Sentry) to proactively catch errors and fix them even without a bug report. This will allow us to catch errors before they become a problem and fix them before they become a problem. This will help reduce the probability of major bugs going unnoticed and causing downtime or a reduced user experience.</p>
<h3 id="improved-api-coverage-to-reduce-the-number-of-requests-needed-to-get-the-same-data-reducing-load-and-improving-performance">Improved API coverage to reduce the number of requests needed to get the same data, reducing load and improving performance</h3>
<p>Previously, it took hundreds of API calls to get the basic information needed to render the volunteer registration system. This year, it has been drastically improved to 3 API calls. This will reduce the load on the server and improve performance.</p>
<h3 id="improved-api-error-handling-to-provide-more-useful-error-messages">Improved API error handling to provide more useful error messages</h3>
<p>Previously, the API would return a generic error message if something went wrong. This year, the API will return a more useful error message that will help know what went wrong and how to fix it, as well as providing the user with a reason that something went wrong.</p>
<h3 id="revamped-onboarding-experience-after-a-user-clicks-register">Revamped onboarding experience after a user clicks register</h3>
<p>From the moment a user clicks register, we register their information in the database, claim the shifts they have registered for, and send them an email and text with detailed information about the event and their registration, as well as a link for them to view their registration.</p>
<h3 id="rewritten-email-handler-to-send-more-accurate-readable-and-beautiful-emails">Rewritten email handler to send more accurate, readable, and beautiful emails</h3>
<p>Last year emails were positively <em>okay</em>. They showed all the necessary information, but everything was text-based and hard to understand. This year we are using a new custom email handler that sends emails with a beautiful HTML template that is easy to read and understand, as well as a link to view the same information in a browser.</p>
<h3 id="implemented-a-database-migration-strategy-to-allow-for-database-schema-updates-without-the-risk-of-downtime-or-data-loss">Implemented a database migration strategy to allow for database schema updates without the risk of downtime or data loss</h3>
<p>Last year, we had to manually update the database schema and manually migrate the data. This year, we are using a database migration strategy that allows us to update the database schema without the risk of downtime or data loss.</p>
<h3 id="implemented-hourly-data-backups-to-prevent-data-loss-in-the-event-of-a-database-or-app-failure">Implemented hourly data backups to prevent data loss in the event of a database or app failure</h3>
<p>This year we are starting to back-up data. The data will be backed up on the application server, the database server, and a storage bucket, allowing for both automatic and manual recovery in the event of a database or app failure.</p>
<h3 id="implemented-uptime-monitoring-to-notify-me-if-the-app-goes-down">Implemented uptime monitoring to notify me if the app goes down</h3>
<p>I have added the volunteer registration system to my uptime monitoring system, which will notify me if the app goes down. This will allow me to quickly fix the issue and get the app back up and running.</p>
<h3 id="added-a-check-at-the-beginning-of-the-registration-process-to-ensure-that-the-server-is-healthy-and-ready-to-accept-new-users">Added a check at the beginning of the registration process to ensure that the server is healthy and ready to accept new users</h3>
<p>This will prevent users from spending the time to register only to find out that the server is down or not ready to accept new users.</p>
<h3 id="implemented-automated-cloud-build-and-deployment-to-improve-the-time-to-production-of-new-features">Implemented automated cloud build and deployment to improve the time-to-production of new features</h3>
<p>This year I implemented a GitHub action to build the docker container and automatically deploy it to the Kubernetes cluster. This will allow me to quickly deploy new features and fixes to the production environment.</p>
<h3 id="implemented-automated-internal-testing-to-ensure-that-new-features-dont-break-existing-functionality">Implemented automated internal testing to ensure that new features don’t break existing functionality</h3>
<p>This year I implemented a testing library that will automatically run tests on the backend to ensure that new features don’t break existing functionality.</p>
<h2 id="remaining-steps">Remaining steps</h2>
<p>The only big step that is left is to implement an admin panel. This will allow us to view, manage, and edit volunteers and shifts without messing with the database directly. This will also allow us to view and manage the data in a more user-friendly way.</p>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>The TaskDesigning & building a robot2022-11-22T15:44:36+00:002022-11-22T15:44:36+00:00https://blog.jackcrane.rocks/2022/11/22/robot<p><img src="/images/robot-banner.png" alt="Promotional banner" /></p>
<h2 id="the-task">The Task</h2>
<p>I founded and camtained my high school robotics team for all 4 years of high school, and took on the role of build lead. As such, much of the design of our FIRST Tech Challenge robot became my undertaking. This is the story of the entire process, from the initial strategy to the final robot.</p>
<h2 id="the-competition">The Competition</h2>
<p>Our team competes in FIRST Tech Challenge, a robotics competition for high school students. The game is played on a 12’ x 12’ field, with two alliances of two teams each. Each alliance has a robot, and the goal is to score as many points as possible by placing wiffle balls in goals. The game is played in two rounds, with the winner of each round being the alliance with the most points. The winner of the match is the alliance with the most points after both rounds.</p>
<p>This post is written about the 2021-2022 season, called Freight Frenzy. This is the official reveal animation, giving a thorough breakdown of the competition at hand.</p>
<!-- original dimensions 560x315 -->
<iframe width="100%" height="400" src="https://www.youtube-nocookie.com/embed/I6lX12idAf8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
<p>The basic gist is there are Whiffle Balls and 2” orange cubes that have to be moved (one by one) to an elevated basket. The challenge lies in the fact that there is a barrier between the balls and cubes (“shipping elements”) and the basket (the “goal”). There are narrow gaps on each side, and the barrier is composed of 2 polycarbonate pipes that are just high enough to be a challenge.</p>
<p><em>From here on, the article assumes you have a functional knowledge of FIRST Tech Challenge and of the season.</em></p>
<h2 id="project-management">Project Management</h2>
<p>We followed the standard engineering design process when designing our robot:</p>
<p><img src="/images/robot-design-process.png" alt="Engineering Design Process" /></p>
<h2 id="problem-definition">Problem Definition</h2>
<p>As the entry to our cycle and the most important phase in project management, we had to define our problem, which starts with defining our goals and our constraints. Simply put, the problem definition is our initial understanding of the task we are attempting to solve. On our team, this often comes as watching the game reveal animation and our first look at Game Manual part 2.</p>
<h3 id="goals">Goals</h3>
<ul>
<li>Score as many points as possible (obviously)</li>
<li>Be built completely in-house</li>
<li>Be extremely reliable</li>
<li>Be a good example for the team moving into the future</li>
</ul>
<h3 id="constraints">Constraints</h3>
<ul>
<li><strong>Size</strong> A maximum drivebase size of 14x14 inches. – the small size allows us to skirt around many of the obstacles around the field, so (even though we are able to handle the obstacles) we do not have to, and it allows us to add small mechanisms that can overflow the edges without violating the maximum size.</li>
<li><strong>Maneuverability</strong> We have used Mecanum wheels in the past and have grown comfortable with the superior maneuverability afforded by the multi-directional driving, so decided to stick with them.</li>
<li><strong>Access</strong> Obviously we have to get into the space behind the obstacles, so we wanted a robot that could both go around the obstacles and go right over them, hence our auxiliary wheel system.</li>
<li><strong>Speed</strong> Each year we have massively increased our speed potential, this year we are using 435 RPM GoBilda YellowJacket motors as our drive motors, connected to our primary wheels with a 1:1 ratio. We have discovered this is the highest speed we can go while staying within the YellowJacket family while still having enough torque to move our robot around easily.</li>
</ul>
<h2 id="gathering-information">Gathering information</h2>
<p>Getting a more in-depth understanding of the challenge through thoroughly reading GM2, and using the one-page game description to plot each task based on point value and perceived difficulty, giving us a roadmap of our priorities. Read more about this process in our game strategy section.</p>
<h2 id="concept-development">Concept development</h2>
<p>Regardless of position on the team, every member is given a stack of note cards and a pen to draw a brief sketch of their ideas for how to solve each task. Once an idea is chosen or developed, the team discusses the mechanism, pros and cons, and assess the build, programming, and driving complexity.</p>
<p>A huge part of concept development is strategizing how we want to break down our problem. The task of building a robot is a monumental one, taking several people multiple months, so it is critical that we know we are working in the right direction.</p>
<p>Our team uses a <code class="language-plaintext highlighter-rouge">point value vs. percieved difficulty</code> graph:</p>
<p><img src="/images/robot-plot.png" alt="Point value vs. percieved difficulty" /></p>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>Building a OneWheel Clone2022-10-03T15:44:36+00:002022-10-03T15:44:36+00:00https://blog.jackcrane.rocks/2022/10/03/onewheel-clone<p><img src="/images/onewheel.png" alt="Promotional banner" /></p>
<h2 id="the-idea">The Idea</h2>
<p>I’m a college student, which opens the door to 3 issues: I have to cover reasonably large distances in small amounts of time, I’m extremely lazy, and I dont have the <strong>$2200</strong> to spend on a OneWheel (Like what thats absurd). So, being the reasonable individual I am, decided that instead of looking into alternatives (like idk a bike), I would design and build my own OneWheel clone. I have certainly drawn inspiration from the original, using it for basic reference items like overall dimensions and tire information, but have designed my own structure, electronics system, and powertrain.</p>
<h2 id="the-structure">The structure</h2>
<p>Most of this project will be built using an Ender 3 Pro 3d printer. This allows me to rapidly prototype by physically holding the component and feeling if it will work or not, and allows me to build shapes that I would otherwise have no ability to manufacture. Obviously as strong as 3d prints are, they will not be able to hold my weight under the stresses of navigating and bouncing around, so the actual structure will be supplied by 4 parallel layers on 3/16 6062 Aluminum frames. These will (hopefully) provide the strength and stability needed hold me and to support the entire machine. The 3d printed parts will provide some bracing and support, but are mostly cosmetic, as I simply don’t trust the plastic.</p>
<h2 id="the-control-layer">The control layer</h2>
<p>The computing will occur on-device with an Arduino Uno and a pitch-aware accelerometer running a PID loop. The motor (Hopefully a GoBilda 5203 motor @ 312RPM) will be driven by a GoBilda motor driver (the cheapest I could find) via PWM. There will be a hardwired safety switch that will have to be pressed in order for the board to run, and the motor will automatically cut off if a tilt of over 17 degrees (z) and 10 degrees (x) is detected. The power system will run off a 12v 2.6 Ah SLA battery (Grainger <code class="language-plaintext highlighter-rouge">5EFF9</code>).</p>
<h2 id="the-powertrain">The powertrain</h2>
<p><img src="/images/onewheel-powertrain.png" alt="Belt-driven wheel" /></p>
<p>The motor and wheel are linked via a 95 tooth, 5mm pitch belt, connected to a GoBilda pulley attached to the motor and to a <a href="https://us.misumi-ec.com/vona2/detail/110300406030/?KWSearch=Timing+Pulleys+Key&searchFlow=results2products&curSearch=%7b%22field%22%3a%22%40search%22%2c%22seriesCode%22%3a%22110300406030%22%2c%22innerCode%22%3a%22%22%2c%22sort%22%3a1%2c%22specSortFlag%22%3a0%2c%22allSpecFlag%22%3a0%2c%22page%22%3a1%2c%22pageSize%22%3a%2260%22%2c%2200000029798%22%3a%22nvd00000000000002%22%2c%2200000029802%22%3a%22mig00000001419449%22%2c%2200000029803%22%3a%22a%22%2c%2200000029800%22%3a%2200000029800.b!00028%22%2c%2200000045545%22%3a%22mig00000000001994%22%2c%2200000045546%22%3a%22mig00000000003570%22%2c%22typeCode%22%3a%22HTPN%23%23S5M%23%23%23%23INCHBORE%22%2c%22fixedInfo%22%3a%22MDM0000162916011030040603011%7c11%22%7d&Tab=preview">misumi pulley</a> on the other. As terrible as mixing vendors tends to be on the design side, this was done to convert from the 8mm REX shaft on the motor to the 5/8” keyshaft (Grainger 30F575).</p>
<p><img src="/images/onewheel-shaft.png" alt="The shaft" /></p>
<p>The delivery from the pulley to the actual wheel is slightly more complex because of the multitude of forces that can potentially be applied to the shaft. The shaft is supported by ball bearings both in the longitudinal and lattitudal direction, and is connected to the metal frame via the pillow components (3d printed; will have to be insanely sturdy) (not pictured).</p>
<h2 id="the-components">The components</h2>
<h3 id="outer-structural-frame">Outer structural frame</h3>
<p><img src="/images/onewheel-outer-frame.png" alt="Outer structural frame" /></p>
<p>These relatively simple components serve as the outside-facing frame as well as part of the structure system.</p>
<h3 id="sled-system">Sled system</h3>
<p><img src="/images/onewheel-sleds.png" alt="Sled system" /></p>
<p>While the longitudinal support is mostly provided by the structural frames, the latitudinal support (z axix) is provided mostly by the boot boards and by 2 3d printed sleds. The sleds are both cosmetic and structural, and provide mounting points for the electronics and the battery. These parts took the most care to design as I was hoping to fit within the sizing volume of my Ender 3 Pro (which I did, barely). I ran a test print of the more elaborate sled (the one responsible for bolding the battery) and it came out less than perfect. I am satisfied with my design, but the print settings required some tuning. These prints take about 30 hours and about 800g of filament, so I am trying to print as few prototypes as possible.</p>
<p>The first prototype highlighted a few issues with my slicer configuration, mostly accuracy and fillament pull issues that had not shown themselves on much smaller prints:</p>
<p><img src="/images/onewheel-sled-broken.jpg" alt="Sled prototype" /></p>
<p>You can see a few issues in this print, most notably that the panel on the left is literally hanging on by a thread. This is because the layer height was set too high, and the print only had 3 outer perimeters. I have found that (even though it is most likely overkill) 6 perimeter layers provides an excellent amount of rigidity and puncture resistance. I also had my support material misconfigured on this print, there were way more supports than necessary, and I was not able to get many of them off cleanly, which resulted in the rough edges you can see (especially on the front edge). I have since fixed these issues and smaller prints are coming out much cleaner.</p>
<h2 id="pillow-components">Pillow components</h2>
<p><img src="/images/onewheel-pillows.png" alt="Pillow components" /></p>
<p>The pillows are used as a way to stabilize the driveshaft and to laterally hold the wheel in place. They are mostly just fancy spacers.</p>
<p><img src="/images/onewheel-pillow-cut.png" alt="Pillow section view" /></p>
<p>As you can see in the cutaway, most of the volume of the pillow is just solid print, but there is a metal insert that will hold the shaft. This is mostly to reduce wear on the 3d print as the shaft spins. My prototyping for this particularly unremarkable part has taken by far the most time and effort. Working with an unfamiliar printer where I don’t have a feel for it’s accuracy led to several parts being too tight or too loose to house the insert: (this should be perfectly flush with the edge)</p>
<p><img src="/images/onewheel-bearing-insert-back.jpg" alt="Pillow prototype" /></p>
<p><img src="/images/onewheel-bearing-insert-front.jpg" alt="Pillow prototype" /></p>
<p>I did not run off a full prototype of the proper ID, but did eventaully discover the correct inner diameter to fit the bearing insert.</p>
<p>The bearing insert will be inserted <em>mid-print</em> by passing a <code class="language-plaintext highlighter-rouge">M601</code> command, allowing me to insert the bearing, then it being held in place by the print. This is one of the really cool capabilities of a 3d printer, because <em>embedding</em> a part in another solid component would otherwise be impossible.</p>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>Apple Music Presence: Technical Discussion2022-05-02T15:44:36+00:002022-05-02T15:44:36+00:00https://blog.jackcrane.rocks/2022/05/02/apple-music-presence<p><img src="/images/amp.png" alt="Promotional banner" /></p>
<h2 id="the-idea">The Idea</h2>
<p>I was about sick of my friends who use Spotify having the song, artist, and album cover of the song they are listening to displayed on Discord and us Apple Music fans not having the same feature. After a little research, I discovered AppleScript, a seemingly forgotten-about language used on MacOS to interact with apps, and it allowed me to pull information on the current track from Apple Music. It then uploads the cover art to a S3 bucket (because you can only submit URLs to Discord) and finally sends everything to Discord.</p>
<h2 id="the-tech-stack">The tech stack</h2>
<ul>
<li>Node.js</li>
<li>DigitalOcean S3 bucket for storing photos</li>
<li>AppleScript</li>
</ul>
<h2 id="community-involvement">Community Involvement</h2>
<p>I have no idea how many people use this app, but what I can say is that the GitHub repo has 26 stars at the moment, I have recieved $40 in donations, and the s3 bucket saw 7,000 new album cover images last month.</p>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>Paddlefest App: Technical Discussion2022-04-22T15:44:36+00:002022-04-22T15:44:36+00:00https://blog.jackcrane.rocks/2022/04/22/paddlefest-app<p><img src="/images/paddlefest.png" alt="Promotional banner" /></p>
<h2 id="the-idea">The Idea</h2>
<p>Paddlefest is an event on the Ohio River where we put thousands of canoes and kayaks on the river. It is a fundraiser for Outdoor Adventure Crew, and I am a member of the executive board for the event. Many large-scale events like this feature mobile apps allowing attendees to interact with the event and so the event can push urgent updates.</p>
<h2 id="features">Features</h2>
<ul>
<li>Paddlefest attendees are able to access a calendar of all the events that are happening both for the riverfront festivities and the acutal paddle</li>
<li>Paddlefest is able to send notification blasts to all attendees, helping drive engagement and improve safety and efficiency</li>
<li>A live photo feed allows participants to share their photos with the event and other eventgoers. All photos are filtered on the backend and are stored on an aws s3 bucket.</li>
<li>A map is availible that shows the location of each booth, important event, and upcoming activties.</li>
<li>Volunteers are able to access a hidden page that provides them with more information on their volunteer roles and who to contact for help.</li>
</ul>
<h2 id="the-tech-stack">The tech stack</h2>
<ul>
<li>React Native (Expo)</li>
<li>Node.JS (Express) server</li>
<li>Next.js (React) admin panel</li>
<li>AWS S3 bucket for storing photos</li>
<li>Backend is hosted on DigitalOcean Kubernetes</li>
</ul>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>DigitalOcean App: Technical Discussion2022-03-19T15:44:36+00:002022-03-19T15:44:36+00:00https://blog.jackcrane.rocks/2022/03/19/digitalocean-app<p><img src="/images/main-do-ph.png" alt="Promotional banner" /></p>
<h2 id="the-idea">The Idea</h2>
<p>I was frustrated with the lack of a high quality mobile app to use to control my DigitalOcean products. The official DigitalOcean web dashboard is not mobile friendly, and the mobile apps out there are either extremely difficult to use, not feature rich, or expensive.</p>
<h2 id="the-features-so-far">The Features so far</h2>
<ul>
<li>💵 Current billing</li>
<li>🏦 Historical invoice viewing</li>
<li>💧 Droplet information view</li>
<li>📈 Droplet CPU performance information</li>
<li>🏗 Creating a droplet</li>
<li>📱 Viewing information for Apps</li>
</ul>
<p>This is far from a full complement to DigitalOcean’s products, so here is my current roadmap:</p>
<ul>
<li>⚙️ Change ‘account’ link to ‘settings’ include more settings for the app</li>
<li>📊 Database listing and editing. This will require some thinking and work. Design suggestions are appreciated!</li>
<li>🚏 DNS Service support, adding, editing, and deleting entries</li>
<li>📶 Networking setup with Load Balancers and firewalls</li>
<li>💿 Add Spaces support, including uploading, downloading, and sharing files</li>
<li>⛴ Add Kubernetes support with link to the Kube dashboard (?) if possible</li>
<li>🤖 Make availible on Android</li>
<li>💦 Implement disk, memory, and networking monitoring for Droplets</li>
<li>📲 Adding a button that allows the creation of apps</li>
<li>❗️ Improving netcode error handling</li>
</ul>
<h2 id="the-tech-stack">The tech stack</h2>
<ul>
<li>React Native</li>
<li>DigitalOcean API</li>
<li>Cloudflare Workers</li>
</ul>
<h2 id="interactions-with-digitalocean">Interactions with DigitalOcean</h2>
<p>On a whim, I sent this app to DigitalOcean to see if they thought it was cool, I had no other goals when I originally reached out. In the interactions with them, however, I scored some sweet free swag and met some really awesome people! We are still in discussions regarding the future of the app but I am excited to see what the future holds.</p>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>Github readme Tik Tak Toe: Technical Discussion2021-10-19T20:32:00+00:002021-10-19T20:32:00+00:00https://blog.jackcrane.rocks/2021/10/19/gh-tik-tak-toe<h2 id="github-readme-tik-tak-toe">Github readme tik-tak-toe</h2>
<p>Powered by Cloudflare Workers, this is meant to be a tik-tak-toe game visitors to my GitHub Profile can play. Due to the limitations of GH being frontend, it works by requesting an image for that particular cell in a 3x3 table wrapped in a link that is also affiliated with the cell on the backend. This allows the game to be played from static sites around the internet. Unfortunately, GitHub’s caching service breaks the images, even though I have been working on fixing caching issues on the backend.</p>
<p>I know what I am trying to do with dynamic images is possible because of the existance of badges on GitHub readmes that often have to change quickly. I have reached out to GH support and am waiting to hear back.</p>
<p><strong>How I made this and the full code is at the bottom :)</strong></p>
<p><a href="https://github.com/jackcrane">Play it live on GitHub</a> (or on the grid below)</p>
<p><a href="https://github.com/jackcrane/gh-tik-tak-toe">View the source code</a></p>
<hr />
<h2 id="the-game">The game</h2>
<blockquote>
<p>Unfortunately this does not work on most mobile phones because of their aggressive cahcing policy. I cannot find any way to combat this, so for now, the only realy way to play this is on a computer. Sorry!</p>
</blockquote>
<p>Current player:</p>
<p><img src="https://gh-tik-tak-toe.jackcrane.workers.dev/current-player?escape-cache" /></p>
<table>
<tr>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/0">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/0?escape-cache" />
</a>
</td>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/1">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/1?escape-cache" />
</a>
</td>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/2">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/2?escape-cache" />
</a>
</td>
</tr>
<tr>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/3">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/3?escape-cache" />
</a>
</td>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/4">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/4?escape-cache" />
</a>
</td>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/5">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/5?escape-cache" />
</a>
</td>
</tr>
<tr>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/6">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/6?escape-cache" />
</a>
</td>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/7">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/7?escape-cache" />
</a>
</td>
<td>
<a href="https://gh-tik-tak-toe.jackcrane.workers.dev/move/8">
<img src="https://gh-tik-tak-toe.jackcrane.workers.dev/image/8?escape-cache" />
</a>
</td>
</tr>
</table>
<p>(You will need to reload the page after you make a move, otherwise new images will not be fetched)</p>
<p><a href="https://gh-tik-tak-toe.jackcrane.workers.dev/reset">Reset Game</a></p>
<hr />
<h2 id="wow-cool-how-does-it-work">Wow cool! How does it work?</h2>
<h3 id="first-the-tech-stack">First, the tech stack:</h3>
<ul>
<li>Cloudflare Workers</li>
<li>Cloudflare Workers KV</li>
<li>GitHub <code class="language-plaintext highlighter-rouge">jackcrane/jackcrane</code> repo readme.md</li>
</ul>
<h3 id="the-constraints">The constraints</h3>
<p>Due to the whole idea of GitHub readmes, there is no backend or javascript support, which is what makes this otherwise trivial game a fun project to hack at.</p>
<h3 id="the-idea--frontend">The idea & frontend</h3>
<p>I started by making a 3x3 table in my GH readme (and on this blog post) with a nested link and nested image. Because of all the nesting, I found I needed to use HTML over markdown for the table element:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><table></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/0"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/0"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/1"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/1"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/2"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/2"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
... (2 more tr's)
<span class="nt"></table></span>
</code></pre></div></div>
<p>Lets break down each of those links. If you have even a foundational understanding of HTML, you will recognize the <code class="language-plaintext highlighter-rouge">a</code> tag as a link and the <code class="language-plaintext highlighter-rouge">img</code> tag as an image. Each of the <code class="language-plaintext highlighter-rouge">a</code> tags are used to handle clicks and the <code class="language-plaintext highlighter-rouge">img</code>s are used for rendering game states. Notice how both the link and image in each ‘cell’ are the same between eachother but unique against the other cells in the table. This is how I am handling clicks, passed in the route.</p>
<p>Once rendered, the table will look like: (each number is the reference to the cell in our code)</p>
<table>
<tbody>
<tr>
<td>0</td>
<td>1</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
<td>5</td>
</tr>
<tr>
<td>6</td>
<td>7</td>
<td>8</td>
</tr>
</tbody>
</table>
<h3 id="the-logic--backend">The logic & backend</h3>
<p>The backend is broken into 5 routes:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">move/:cell</code></li>
<li><code class="language-plaintext highlighter-rouge">image/:cell</code></li>
<li><code class="language-plaintext highlighter-rouge">debug</code></li>
<li><code class="language-plaintext highlighter-rouge">reset</code></li>
<li><code class="language-plaintext highlighter-rouge">current-player</code></li>
</ul>
<p>The <code class="language-plaintext highlighter-rouge">move</code> route handles all of the actual logic. It is where each the links lead and it handles executing the move, determining if there is a winner, and saving states to Cloudflare’s Workers KV database engine.</p>
<p>The <code class="language-plaintext highlighter-rouge">image</code> route simply returns the image of the piece (blank, circle, or x) in the given cell.</p>
<p>The <code class="language-plaintext highlighter-rouge">debug</code> route just returns the game board as JSON</p>
<p>The <code class="language-plaintext highlighter-rouge">reset</code> route sets the states in the KV database to an empty board and resets everything in the backend</p>
<p>The <code class="language-plaintext highlighter-rouge">current-player</code> route returns the image of the current player (circle or x) for use in the ‘current player’ interface.</p>
<h3 id="starting-the-code">Starting the code</h3>
<h4 id="default-code">Default code</h4>
<p>Running <code class="language-plaintext highlighter-rouge">wrangler init</code> results in the following code in <code class="language-plaintext highlighter-rouge">index.js</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">fetch</span><span class="dl">"</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">respondWith</span><span class="p">(</span><span class="nx">handleRequest</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">request</span><span class="p">))</span>
<span class="p">})</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">handleRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello worker!</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text/plain</span><span class="dl">"</span> <span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="helpers">Helpers</h4>
<p>And at the top lets add all our helper functions and objects:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">table</span> <span class="o">=</span> <span class="p">[</span>
<span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span>
<span class="p">]</span>
<span class="kd">const</span> <span class="nx">winningConditions</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span>
<span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">],</span>
<span class="p">[</span><span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">],</span>
<span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">],</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">7</span><span class="p">],</span>
<span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">8</span><span class="p">],</span>
<span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">],</span>
<span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">6</span><span class="p">]</span>
<span class="p">];</span>
<span class="c1">// https://stackoverflow.com/a/1349426/9503170</span>
<span class="kd">const</span> <span class="nx">makeid</span> <span class="o">=</span> <span class="p">(</span><span class="nx">length</span><span class="o">=</span><span class="mi">64</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">result</span> <span class="o">=</span> <span class="dl">''</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">characters</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">charactersLength</span> <span class="o">=</span> <span class="nx">characters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">result</span> <span class="o">+=</span> <span class="nx">characters</span><span class="p">.</span><span class="nx">charAt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">*</span> <span class="nx">charactersLength</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">gameRunning</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">fetch</span><span class="dl">"</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
</code></pre></div></div>
<p>And now for explaining:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">table</code> is a variable we will be using to hold the state of the game. Remember serverless functions are (by nature) ephemeral so we don’t want to waste time and and effort fetching an immutable game state from the database, so we fetch it once and drop it in this array.</li>
<li><code class="language-plaintext highlighter-rouge">winningConditions</code> is a 2-dimensional array holing all of the possible ways to win a tik-tak-toe game. We use this array to determine if a player has won the game.</li>
<li><code class="language-plaintext highlighter-rouge">makeid</code> is a random string generator we use to generate a random alphanumeric string that we use to tell some services’ caches that this is a new image and it should override whatever they have cached.</li>
<li><code class="language-plaintext highlighter-rouge">gameWon</code> holds if a game has been won. It is used several times determining if we need to execute logic or what we need to return to the users.</li>
</ul>
<h4 id="the-registermove-function">The <code class="language-plaintext highlighter-rouge">registerMove</code> function</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">registerMove</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">spot</span><span class="p">,</span> <span class="nx">activePlayer</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">gs</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">table</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">gs</span><span class="p">)</span>
<span class="k">if</span><span class="p">(</span><span class="nx">table</span><span class="p">[</span><span class="nx">spot</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">spot</span><span class="p">]</span> <span class="o">=</span> <span class="nx">activePlayer</span><span class="p">;</span>
<span class="k">for</span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">winningConditions</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span>
<span class="p">(</span>
<span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]]</span> <span class="o">==</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]]</span>
<span class="p">)</span> <span class="o">&&</span> <span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]]</span> <span class="o">==</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">2</span><span class="p">]]</span>
<span class="p">)</span> <span class="o">&&</span> <span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]]</span> <span class="o">==</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">2</span><span class="p">]]</span>
<span class="p">)</span>
<span class="p">)</span> <span class="o">&&</span> <span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&&</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&&</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">2</span><span class="p">]]</span> <span class="o">!=</span> <span class="mi">0</span>
<span class="p">)</span>
<span class="p">)</span> <span class="p">{</span>
<span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="nx">activePlayer</span> <span class="o">===</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">2</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">activePlayer</span> <span class="o">===</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">table</span><span class="p">))</span>
<span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Please ignore the absurd if statement formatting, but it was the only way I could get it to look that didnt give me a headache.</p>
<p>We start by declaring an async function called <code class="language-plaintext highlighter-rouge">registerMove</code> that takes a <code class="language-plaintext highlighter-rouge">spot</code> and <code class="language-plaintext highlighter-rouge">activePlayer</code> argument. The <code class="language-plaintext highlighter-rouge">spot</code> is the cell the player chose, and the <code class="language-plaintext highlighter-rouge">activePlayer</code> is the active player (surprising, right).</p>
<p>We set the <code class="language-plaintext highlighter-rouge">gs</code> variable to the data we pull from the Cloudflare KV database we have set up with the name <code class="language-plaintext highlighter-rouge">GAMESTORE</code>. Keep in mind this data is JSON, so we need to parse it before use, which we do and update the variable <code class="language-plaintext highlighter-rouge">table</code> with it.</p>
<p>In our if statement, we need to be sure the cell is not already taken so we dont overwrite the value.</p>
<p>Then we set the cell in question to the activePlayer (1 or 2).</p>
<p>Next we have to iterate over the <code class="language-plaintext highlighter-rouge">winningConditions</code> array, trying to determine if the match has been won. The logic is as follows:</p>
<p><code class="language-plaintext highlighter-rouge">winningConditions[i] = [0, 1, 2]</code>. We need to make sure all values in <code class="language-plaintext highlighter-rouge">winningConditions[i]</code> match our 2 test cases:</p>
<ol>
<li>They are not <code class="language-plaintext highlighter-rouge">0</code></li>
<li>They are all the same</li>
</ol>
<p>If they pass our test cases, we know the game has been won, so we update our <code class="language-plaintext highlighter-rouge">gameWon</code> variable accordingly.</p>
<p>The final action in our if statement is to switch the active player, switching the number and saving it to the KV database.</p>
<p>Finally, we save the game state (saved in the <code class="language-plaintext highlighter-rouge">table</code> variable) to our KV database, then return <code class="language-plaintext highlighter-rouge">true</code>.</p>
<h4 id="update-the-handlerequest-function">Update the <code class="language-plaintext highlighter-rouge">handleRequest</code> function</h4>
<p>We need to update the <code class="language-plaintext highlighter-rouge">handleRequest</code> function to handle simple routing and function calls. For readability purposes I converted handleRequest to an arrow function, but this is not necessary.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">handleRequest</span> <span class="o">=</span> <span class="k">async</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">))</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">null</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">))</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">null</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">table</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">))</span>
<span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">/move/</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Move route goes here</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">/image/</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Image route goes here</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">debug</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Debug route goes here</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">reset</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Reset route goes here</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">current-player</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Current player route goes here</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Something went wrong</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">handleRequest</code> function is what accepts the request and breaks it into routes.</p>
<p>It starts by verifying the presence of the necessary keys in the KV database, and if they are not present it creates them. Then it gets the <code class="language-plaintext highlighter-rouge">gamestate</code> from the db and saves it to the <code class="language-plaintext highlighter-rouge">table</code> variable.</p>
<p>Next the function breaks the requests in to routes. For simplicity sake, I chose not to use a router script but rather to implement my own extremely basic router simply based on text included in strings.</p>
<h4 id="the-move-route">The <code class="language-plaintext highlighter-rouge">move</code> route</h4>
<p>Insert the following code at the comment <code class="language-plaintext highlighter-rouge">Move route goes here</code> comment above.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">/move/</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/move/</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">?</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="kd">let</span> <span class="nx">player</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">));</span>
<span class="k">if</span><span class="p">(</span><span class="k">await</span> <span class="nx">registerMove</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">player</span><span class="p">))</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nx">gameWon</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Player has won</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Play successfully registered</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Player has won</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">})</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">/image/</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="p">...</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">move</code> route is what handles the logic of the game. We start by getting the parameter out of the url (we know it is after <code class="language-plaintext highlighter-rouge">/move/</code> and is before the <code class="language-plaintext highlighter-rouge">?</code> potentially used in debugging). Then we fetch the active player from the KV database.</p>
<p>We call the <code class="language-plaintext highlighter-rouge">registerMove</code> function which returns <code class="language-plaintext highlighter-rouge">true</code> if there are no errors, and will return <code class="language-plaintext highlighter-rouge">null</code> if there are any errors or someone won. Next we check to see if the player won several times, and if the game has been won, we reset it by setting the <code class="language-plaintext highlighter-rouge">player</code> back to <code class="language-plaintext highlighter-rouge">1</code> and by setting the <code class="language-plaintext highlighter-rouge">gamestate</code> back to <code class="language-plaintext highlighter-rouge">[0,0,0,0,0,0,0,0,0]</code>. If the match has been won, we return <code class="language-plaintext highlighter-rouge">Player has won</code> to the client, and otherwise we return <code class="language-plaintext highlighter-rouge">Play successfully registered</code>.</p>
<h4 id="the-image-route">The <code class="language-plaintext highlighter-rouge">image</code> route</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">/image/</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">blob</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">blobBlank</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/unchecked-checkbox.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobXed</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/fluency-systems-regular/96/000000/x.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobCircle</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/circled.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobError</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/50/000000/error--v1.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">path</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/image/</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">?</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]);</span>
<span class="k">switch</span><span class="p">(</span><span class="nx">table</span><span class="p">[</span><span class="nx">path</span><span class="p">])</span> <span class="p">{</span>
<span class="k">case</span> <span class="mi">0</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobBlank</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">1</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobXed</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">2</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobCircle</span>
<span class="k">break</span><span class="p">;</span>
<span class="nl">default</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobError</span><span class="p">;</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Cache-Control</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-cache</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">ETag</span><span class="dl">'</span><span class="p">:</span> <span class="nx">makeid</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">debug</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="p">...</span>
</code></pre></div></div>
<p>We start the <code class="language-plaintext highlighter-rouge">image</code> route by setting a functionally-scoped variable called <code class="language-plaintext highlighter-rouge">blob</code> (note this is left undefined). We also set <code class="language-plaintext highlighter-rouge">blobBlank</code>, <code class="language-plaintext highlighter-rouge">blobXed</code>, <code class="language-plaintext highlighter-rouge">blobCircle</code>, and <code class="language-plaintext highlighter-rouge">blobError</code> to PNG images we have fetched then turned to ReadableStreams using the <code class="language-plaintext highlighter-rouge">blob()</code> functions.</p>
<p>Then we get the path just like we did in the <code class="language-plaintext highlighter-rouge">move</code> route. This path is the cell that holds the image we are returning in our frontend. We drop that into a switch for the 3 possible states of the table in that given position, 0, 1, or 2. If it is a 0, we set the functionally-scoped <code class="language-plaintext highlighter-rouge">blob</code> variable to the variable holding the blobified blank image (actually more of a round square because I couldn’t find a blank one but it gets the job done). If it is 1, we set it to the variable holding the X image, and if it is a 2, we set it to the variable holding the circle image.</p>
<p>Finally, we return the blob of the image as well as the headers for a <code class="language-plaintext highlighter-rouge">content-type</code> of <code class="language-plaintext highlighter-rouge">image/png</code>, <code class="language-plaintext highlighter-rouge">Cache-Control</code> of <code class="language-plaintext highlighter-rouge">no-cache</code> and <code class="language-plaintext highlighter-rouge">ETag</code> of the 64-character long random string returned by the <code class="language-plaintext highlighter-rouge">makeid()</code> function. The <code class="language-plaintext highlighter-rouge">Cache-Control</code> and the <code class="language-plaintext highlighter-rouge">ETag</code> headers are critical to keep GitHub or other providers from caching your images for an eternity.</p>
<h4 id="the-debug-route">The <code class="language-plaintext highlighter-rouge">debug</code> route</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">debug</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">v</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="p">{</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">}})</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">reset</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="p">...</span>
</code></pre></div></div>
<p>The debug route is not actually necessary and is hopefully pretty self-explanatory at this point, so I wont explain it.</p>
<h4 id="the-reset-route">The <code class="language-plaintext highlighter-rouge">reset</code> route</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">reset</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Game reset</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">current-player</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="p">...</span>
</code></pre></div></div>
<p>The reset route implements much of the same logic for when a player has won the game. We put an array of 9 zeros and reset the player to 1.</p>
<h4 id="the-current-player-route">The current-player route</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">current-player</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">blob</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">blobBlank</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/unchecked-checkbox.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobXed</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/fluency-systems-regular/96/000000/x.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobCircle</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/circled.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobError</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/50/000000/error--v1.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">player</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">player</span><span class="p">)</span>
<span class="k">switch</span><span class="p">(</span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">player</span><span class="p">))</span> <span class="p">{</span>
<span class="k">case</span> <span class="mi">0</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobBlank</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">1</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobXed</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">2</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobCircle</span>
<span class="k">break</span><span class="p">;</span>
<span class="nl">default</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobError</span><span class="p">;</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Cache-Control</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-cache</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">ETag</span><span class="dl">'</span><span class="p">:</span> <span class="nx">makeid</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="p">...</span>
</code></pre></div></div>
<p>You will notice the <code class="language-plaintext highlighter-rouge">current-player</code> route implements almost all the same logic as the <code class="language-plaintext highlighter-rouge">image</code> route, just instead of using the value of the path in the table, we are using the <code class="language-plaintext highlighter-rouge">player</code> variable pulled from the database.</p>
<h2 id="the-full-code">The full code:</h2>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">table</span> <span class="o">=</span> <span class="p">[</span>
<span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span>
<span class="p">]</span>
<span class="kd">const</span> <span class="nx">winningConditions</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">],</span>
<span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">],</span>
<span class="p">[</span><span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">],</span>
<span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">],</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">7</span><span class="p">],</span>
<span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">8</span><span class="p">],</span>
<span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">],</span>
<span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">6</span><span class="p">]</span>
<span class="p">];</span>
<span class="kd">const</span> <span class="nx">printTable</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">table</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nx">table</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nx">table</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">table</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="nx">table</span><span class="p">[</span><span class="mi">4</span><span class="p">],</span> <span class="nx">table</span><span class="p">[</span><span class="mi">5</span><span class="p">])</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">table</span><span class="p">[</span><span class="mi">6</span><span class="p">],</span> <span class="nx">table</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span> <span class="nx">table</span><span class="p">[</span><span class="mi">8</span><span class="p">])</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">makeid</span> <span class="o">=</span> <span class="p">(</span><span class="nx">length</span><span class="o">=</span><span class="mi">64</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">result</span> <span class="o">=</span> <span class="dl">''</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">characters</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">charactersLength</span> <span class="o">=</span> <span class="nx">characters</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">result</span> <span class="o">+=</span> <span class="nx">characters</span><span class="p">.</span><span class="nx">charAt</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">*</span> <span class="nx">charactersLength</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">registerMove</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">spot</span><span class="p">,</span> <span class="nx">position</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">gs</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">table</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">gs</span><span class="p">)</span>
<span class="k">if</span><span class="p">(</span><span class="nx">table</span><span class="p">[</span><span class="nx">spot</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">spot</span><span class="p">]</span> <span class="o">=</span> <span class="nx">position</span><span class="p">;</span>
<span class="k">for</span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">winningConditions</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span>
<span class="p">(</span>
<span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]]</span> <span class="o">==</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]]</span>
<span class="p">)</span> <span class="o">&&</span> <span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]]</span> <span class="o">==</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">2</span><span class="p">]]</span>
<span class="p">)</span> <span class="o">&&</span> <span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]]</span> <span class="o">==</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">2</span><span class="p">]]</span>
<span class="p">)</span>
<span class="p">)</span> <span class="o">&&</span> <span class="p">(</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&&</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&&</span>
<span class="nx">table</span><span class="p">[</span><span class="nx">winningConditions</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">2</span><span class="p">]]</span> <span class="o">!=</span> <span class="mi">0</span>
<span class="p">)</span>
<span class="p">)</span> <span class="p">{</span>
<span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="nx">position</span> <span class="o">===</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">2</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">position</span> <span class="o">===</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">table</span><span class="p">))</span>
<span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">fetch</span><span class="dl">'</span><span class="p">,</span> <span class="nx">event</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">event</span><span class="p">.</span><span class="nx">respondWith</span><span class="p">(</span><span class="nx">handleRequest</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">request</span><span class="p">))</span>
<span class="p">})</span>
<span class="kd">const</span> <span class="nx">handleRequest</span> <span class="o">=</span> <span class="k">async</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">))</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">null</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">))</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">null</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">table</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">))</span>
<span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">/move/</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/move/</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">?</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="kd">let</span> <span class="nx">player</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">));</span>
<span class="k">if</span><span class="p">(</span><span class="k">await</span> <span class="nx">registerMove</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">player</span><span class="p">))</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nx">gameWon</span><span class="p">)</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Player has won</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Play successfully registered</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">gameWon</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Player has won</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">})</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">/image/</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">blob</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">blobBlank</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/unchecked-checkbox.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobXed</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/fluency-systems-regular/96/000000/x.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobCircle</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/circled.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobError</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/50/000000/error--v1.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">path</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/image/</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">?</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">pos</span><span class="dl">'</span><span class="p">,</span> <span class="nx">path</span><span class="p">,</span> <span class="nx">table</span><span class="p">[</span><span class="nx">path</span><span class="p">])</span>
<span class="k">switch</span><span class="p">(</span><span class="nx">table</span><span class="p">[</span><span class="nx">path</span><span class="p">])</span> <span class="p">{</span>
<span class="k">case</span> <span class="mi">0</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobBlank</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">1</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobXed</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">2</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobCircle</span>
<span class="k">break</span><span class="p">;</span>
<span class="nl">default</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobError</span><span class="p">;</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Cache-Control</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-cache</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">ETag</span><span class="dl">'</span><span class="p">:</span> <span class="nx">makeid</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">debug</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">v</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">v</span><span class="p">,</span> <span class="p">{</span> <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">}})</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">reset</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">gamestate</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">[0,0,0,0,0,0,0,0,0]</span><span class="dl">'</span><span class="p">)</span>
<span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">)</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Game reset</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">current-player</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">blob</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">blobBlank</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/unchecked-checkbox.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobXed</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/fluency-systems-regular/96/000000/x.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobCircle</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/100/000000/circled.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">blobError</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://img.icons8.com/ios/50/000000/error--v1.png</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">r</span> <span class="o">=></span> <span class="nx">r</span><span class="p">.</span><span class="nx">blob</span><span class="p">());</span>
<span class="kd">let</span> <span class="nx">player</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">GAMESTORE</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">player</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">player</span><span class="p">)</span>
<span class="k">switch</span><span class="p">(</span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">player</span><span class="p">))</span> <span class="p">{</span>
<span class="k">case</span> <span class="mi">0</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobBlank</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">1</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobXed</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="mi">2</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobCircle</span>
<span class="k">break</span><span class="p">;</span>
<span class="nl">default</span><span class="p">:</span>
<span class="nx">blob</span> <span class="o">=</span> <span class="nx">blobError</span><span class="p">;</span>
<span class="k">break</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">blob</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Cache-Control</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">no-cache</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">ETag</span><span class="dl">'</span><span class="p">:</span> <span class="nx">makeid</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">Something went wrong</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/plain</span><span class="dl">'</span> <span class="p">}</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Current player:
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/current-player?escape-cache"</span><span class="nt">></span>
<span class="nt"><table></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/0"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/0?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/1"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/1?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/2"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/2?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/3"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/3?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/4"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/4?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/5"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/5?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/6"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/6?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/7"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/7?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/move/8"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://gh-tik-tak-toe.jackcrane.workers.dev/image/8?escape-cache"</span><span class="nt">></span>
<span class="nt"></a></span>
<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></table></span>
(You will need to reload the page after you make a move, otherwise new images will not be fetched)
<span class="p">[</span><span class="nv">Reset Game</span><span class="p">](</span><span class="sx">https://gh-tik-tak-toe.jackcrane.workers.dev/reset</span><span class="p">)</span>
</code></pre></div></div>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>Github readme tik-tak-toeBuilding an Open Graph Image Generator2021-08-21T15:44:36+00:002021-08-21T15:44:36+00:00https://blog.jackcrane.rocks/2021/08/21/build-an-og-generator<h2 id="introduction">Introduction</h2>
<p>If you don’t know what Open Graph is, it is a powerful set of meta tags that can be included in a website that allows rich data to be displayed on social media platforms and search engines. <a href="https://ogp.me/">Learn more about Open Graph</a>.</p>
<p>This project is inspired by Vercel’s <a href="https://og-image.vercel.app/">similar project</a>.</p>
<h2 id="steps">Steps</h2>
<ul class="task-list">
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Install prerequisites</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Create our Express app</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Set up our image generation</li>
</ul>
<h2 id="the-tech-stack">The tech stack</h2>
<ul>
<li>Node.js
<ul>
<li>Express</li>
</ul>
</li>
</ul>
<h2 id="installing-prerequisites">Installing Prerequisites</h2>
<p>First, we need to create an NPM project:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm init
</code></pre></div></div>
<p>We need Express and Canvas:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i express canvas
</code></pre></div></div>
<h2 id="getting-started-writing-code">Getting Started writing code</h2>
<p>In <code class="language-plaintext highlighter-rouge">index.js</code>, we need to import the prerequisites we just installed:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">createCanvas</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
</code></pre></div></div>
<p>And set up our express server:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/og/:title/:subtitle</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">generateImage</span><span class="p">({</span>
<span class="na">title</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span>
<span class="na">subtitle</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">subtitle</span>
<span class="p">});</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">writeHead</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Content-Length</span><span class="dl">'</span><span class="p">:</span> <span class="nx">buffer</span><span class="p">.</span><span class="nx">length</span>
<span class="p">});</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
<span class="p">})</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">80</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">App listening on localhost:80</span><span class="dl">'</span><span class="p">);</span>
<span class="p">})</span>
</code></pre></div></div>
<p>Let’s generate the images</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">generateImage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// Create and configure the canvas:</span>
<span class="kd">const</span> <span class="nx">width</span> <span class="o">=</span> <span class="mi">1200</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">height</span> <span class="o">=</span> <span class="mi">627</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nx">createCanvas</span><span class="p">(</span><span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// Set a black background:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">black</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillRect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="c1">// Set a white border:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">strokeStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">white</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">lineWidth</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">beginPath</span><span class="p">();</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">rect</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="nx">width</span> <span class="o">-</span> <span class="mi">20</span><span class="p">,</span> <span class="nx">height</span> <span class="o">-</span> <span class="mi">20</span><span class="p">)</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">stroke</span><span class="p">();</span>
<span class="c1">// Draw the title:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">font</span> <span class="o">=</span> <span class="s2">`bold 70pt arial`</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">white</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillText</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">170</span><span class="p">);</span>
<span class="c1">// Draw the subtitle:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">font</span> <span class="o">=</span> <span class="s2">`bold 40pt arial`</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">white</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillText</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">subtitle</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">170</span><span class="p">);</span>
<span class="c1">// Render and return our image:</span>
<span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">toBuffer</span><span class="p">(</span><span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">buffer</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="lets-give-it-a-shot">Lets give it a shot!</h2>
<p>In your browser, visit <a href="localhost/og/title/subtitle">localhost/og/title/subtitle</a> and see if you got what you are looking for!</p>
<h2 id="full-code">Full Code</h2>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">createCanvas</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">generateImage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// Create and configure the canvas:</span>
<span class="kd">const</span> <span class="nx">width</span> <span class="o">=</span> <span class="mi">1200</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">height</span> <span class="o">=</span> <span class="mi">627</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nx">createCanvas</span><span class="p">(</span><span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">context</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// Set a black background:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">black</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillRect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
<span class="c1">// Set a white border:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">strokeStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">white</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">lineWidth</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">beginPath</span><span class="p">();</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">rect</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="nx">width</span> <span class="o">-</span> <span class="mi">20</span><span class="p">,</span> <span class="nx">height</span> <span class="o">-</span> <span class="mi">20</span><span class="p">)</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">stroke</span><span class="p">();</span>
<span class="c1">// Draw the title:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">font</span> <span class="o">=</span> <span class="s2">`bold 70pt arial`</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">white</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillText</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">170</span><span class="p">);</span>
<span class="c1">// Draw the subtitle:</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">font</span> <span class="o">=</span> <span class="s2">`bold 40pt arial`</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">white</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">context</span><span class="p">.</span><span class="nx">fillText</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">subtitle</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="mi">170</span><span class="p">);</span>
<span class="c1">// Render and return our image:</span>
<span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">toBuffer</span><span class="p">(</span><span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">buffer</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/og/:title/:subtitle</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">generateImage</span><span class="p">({</span>
<span class="na">title</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span>
<span class="na">subtitle</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">subtitle</span>
<span class="p">});</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">writeHead</span><span class="p">(</span><span class="mi">200</span><span class="p">,</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image/png</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Content-Length</span><span class="dl">'</span><span class="p">:</span> <span class="nx">buffer</span><span class="p">.</span><span class="nx">length</span>
<span class="p">});</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
<span class="p">})</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">80</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">App listening on localhost:80</span><span class="dl">'</span><span class="p">);</span>
<span class="p">})</span>
</code></pre></div></div>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>IntroductionOg-image: Technical Discussion2021-08-20T15:44:36+00:002021-08-20T15:44:36+00:00https://blog.jackcrane.rocks/2021/08/20/og-image<p><img src="https://jackcrane.rocks/images/og.png" alt="Og-image Screenshot" /></p>
<h2 id="the-idea">The Idea</h2>
<p>I was inspired by <a href="https://og-image.vercel.app/">Vercel’s opengraph image generator</a> but didn’t want to paste their logo over everything (especially because I dont use vercel), and I was not happy with their options for images, so I decided to clone it. It provides a simple endpoint (called data.png) with not GET arguments so it will always work with social media platforms.</p>
<h2 id="the-features-or-planned-features">The Features (or planned features)</h2>
<ul class="task-list">
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Simple generator UI</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Several selectable backgrounds</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Input for title, subtitle, URL, and URL Protocol</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Live preview with image and code sample</li>
</ul>
<h2 id="the-tech-stack">The tech stack</h2>
<p>Frontend:</p>
<ul>
<li>HTML</li>
<li>JavaScript</li>
<li>CSS</li>
</ul>
<p>Backend:</p>
<ul>
<li>Node.js (express)</li>
<li>DigitalOcean App Platform</li>
</ul>
<h2 id="what-happened">What happened</h2>
<p>This is a live and evolving project, I made it primarily for myself but figured I may as well make it availble to the world. After a few months, OG-Image Generator now sees over 100,000 generations per month for over 6,000 unique domains. I have partnered with Webcode Tools to help market the service, and that has become my primary source of user acquisition.</p>
<p>In my opinion, this is the coolest project I have ever built. It is not a huge technical achievement, but I am extremely proud to have become a part of the ecosystem for so many developers and websites. This project is offered for 100% free, and I am happy to see it being used by so many people.</p>
<p>This project was originally deployed to DigitalOcean’s App Platform but i later migrated to Kubernetes (still on DigitalOcean. ❤️ u digitalocean!) so I could run multiple projects on the same cluster with the redundancy, scalability, and uptime that Kubernetes provides. Although I cannot say I understand even 1% of what Kubernetes has to offer, I am slowly picking up more and adding more complexity as my projects warrant it.</p>
<h2 id="live-site">Live site</h2>
<p><a href="https://og-image.xyz">https://og-image.xyz</a></p>
<h2 id="github">GitHub</h2>
<p><a href="https://github.com/jackcrane/og-image">jackcrane/og-image</a></p>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>JVerify: Technical Discussion2021-08-18T15:44:36+00:002021-08-18T15:44:36+00:00https://blog.jackcrane.rocks/2021/08/18/jverify<p><img src="https://jackcrane.rocks/images/jv.png" alt="JVerify Screenshot" /></p>
<h2 id="the-idea">The Idea</h2>
<p>JVerify is an effortless, secure, and reliable phone number verification service (VaaS) designed to make it easier to create a more reliable web.</p>
<p>Developers who wish to easily verify user’s phone numbers can use JVerify’s APIs to send and verify a pin to a user’s device.</p>
<h2 id="the-features-or-planned-features">The Features (or planned features)</h2>
<ul class="task-list">
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Large-company looking website</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Usable documentation</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Stripe integration for metered billing</li>
</ul>
<h2 id="the-tech-stack">The tech stack</h2>
<p>Frontend:</p>
<ul>
<li>HTML</li>
<li>JavaScript</li>
<li>CSS</li>
</ul>
<p>Backend:</p>
<ul>
<li>Node.js (express)</li>
<li>Stripe</li>
<li>MySQL</li>
<li>Redux</li>
<li>DigitalOcean App Platform</li>
</ul>
<h2 id="what-happened">What happened</h2>
<p>This is a live and evolving project. I am the sole individual on this project, working as the buisness manager, developer, designer, and marketer. This app has had a ‘soft-launch’ but has not seen a large-scale launch or marketing campaign.</p>
<p>JVerify has not turned out to be a commercial success, but I am very proud of it: it was the first production-ready, high quality product I have brought forward. This was the first programming project that I truly applied the Engineering Process of a systematic approach to determining exactly what the problem I was trying to solve was, determining the best way to solve it (both technically and with a pleasant UX), properly pseudo-coding the solution, then implementing it in an organized and efficient manner. In doing this, I was able to iron out most of the logic issues before I even touched the code, and was able to efficiently write the code in a mantainable, minimalistic, and readable manner.</p>
<p>Although I was the only developer on this project, I forced myself to use the entire suite of tools availible on GitHub, like creating different branches for new features, properly testing them with staging servers, opening issues and pull requests, and using the GitHub project board to track progress and bugs. This was extremely valuable, and while not necessarily practical to implement in small single-developer projects, it was a great way to keep this project organized and an excellent way for me to learn how to use GitHub.</p>
<p>As far as deployment is concerned, this is the first time I deployed my code to a manged solution (DigitalOcean App Platform). Although it was more expensive, I did not need the extra configurability offered by a bare VM, and the easy deployment from GitHub and guranteed zero downtime made it a no-brainer.</p>
<h2 id="live-site">Live site</h2>
<p><a href="https://jverify.us">https://jverify.us</a></p>
<h2 id="github">GitHub</h2>
<p>This is my only closed-source project.</p>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="jackcrane" data-description="Support me on Buy me a coffee!" data-message="Feeling generous?" data-color="#FFDD00" data-position="Right" data-x_margin="18" data-y_margin="18"></script>