<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Taehyun Kim</title>
    <description>The latest articles on DEV Community by Taehyun Kim (@thkim5937).</description>
    <link>https://dev.to/thkim5937</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052149%2F9d25f788-c27c-4402-831e-5571fe481428.jpg</url>
      <title>DEV Community: Taehyun Kim</title>
      <link>https://dev.to/thkim5937</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thkim5937"/>
    <language>en</language>
    <item>
      <title>Traceroute devlog #5</title>
      <dc:creator>Taehyun Kim</dc:creator>
      <pubDate>Thu, 13 Aug 2026 07:44:16 +0000</pubDate>
      <link>https://dev.to/thkim5937/traceroute-devlog-5-183e</link>
      <guid>https://dev.to/thkim5937/traceroute-devlog-5-183e</guid>
      <description>&lt;p&gt;Most of this week went into the hint feature, and it kept teaching me that "helpful" is a much harder word to actually implement than it sounds.&lt;/p&gt;

&lt;p&gt;　The first version was almost embarrassingly broken. Load a fresh level and press the hint button before touching anything, and nothing happened at all, because the game had no idea which color you meant to get help with. Fixing that was easy, just default to the first unfinished color. The real problem showed up once I started actually playing with hints turned on. If you'd drawn a path that diverged from the stored solution, but your path was still perfectly valid and still completable, pressing hint would wipe it out and replace it with the stored one anyway. That felt awful to hit as a player. So I rebuilt the whole feature around one rule instead: never erase progress that can still be finished, no matter how it compares to the solution on file.&lt;/p&gt;

&lt;p&gt;　That rule sounded complete until I found the case it didn't cover. A hint aimed at helping one color could, while doing exactly what it was supposed to for that color, accidentally cut through the only remaining path available to a different color that hadn't been started yet. Checking whether the target color alone could still be completed wasn't enough. I needed to check whether the entire board, every unfinished color at once, was still solvable after the hint went in, in real time, while the player was sitting there waiting for a response. That is the one thing the whole project had been built to avoid. The rule from day one was that actual solving only ever happens ahead of time, offline, and the live game just reads the answer off a file. Hints were supposed to be an exception to that too, just reading a stored path, no live computation. Now they weren't. I didn't love making that call, but pretending the board-wide check wasn't necessary would've meant shipping a hint button that could occasionally hand a player an unsolvable board, which is worse than bending a rule I'd written for myself weeks earlier. I wrote it down as a deliberate, documented exception instead of quietly ignoring it.&lt;/p&gt;

&lt;p&gt;　There was a smaller, funnier scare later in the week on the developer demo screen, the one that visualizes the generator and solver actually running. After wiring it up, starting a search would just sit on "Solving..." and never move. My first guess was that something had deadlocked. It hadn't. The visualization was queuing up a tiny delay for every single step the search touched internally, and there are a lot more of those than there are steps that actually matter to watch, so the playback queue had quietly grown into something that would've taken hours to finish draining, one slow frame at a time. Nothing was frozen. It was just patiently working through a backlog longer than my patience for testing it. Sampling the interesting steps instead of queuing every single one fixed it in a few minutes once I understood what was actually happening.&lt;/p&gt;

&lt;p&gt;　The last thing I built this week was the piece that was supposed to feel like the finish line: a fully automated pipeline that generates new levels on its own schedule, has an outside reviewer weigh in on whether they're any good, and opens a pull request for me to approve. All of it works. The tests pass, the workflow file is wired up, the review logic runs clean. Then I went to actually turn it on and found out I can't get an API key issued at all, because the phone number tied to my account got disconnected at some point and I never noticed. So the feature that was meant to close out the week sits there finished, tested, and completely unusable, not because of anything in the code, but because of a phone number that stopped working for reasons that have nothing to do with any of this. That's an oddly deflating way to end a week of real technical work, and I don't have a clean resolution for it yet. It's just sitting in my queue to sort out with customer support.&lt;/p&gt;

&lt;p&gt;　Everything else landed without drama by comparison: progress now survives a refresh, the game holds up on a phone-sized screen, obstacles and the star-scoring system are both fully wired in, and the level-select screen went through a handful of layout fixes once I actually clicked through it myself instead of just reading the plan for it. None of those needed a second definition of "done." The hint system needed three.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Traceroute devlog #4</title>
      <dc:creator>Taehyun Kim</dc:creator>
      <pubDate>Mon, 10 Aug 2026 01:30:35 +0000</pubDate>
      <link>https://dev.to/thkim5937/traceroute-devlog-3-2743</link>
      <guid>https://dev.to/thkim5937/traceroute-devlog-3-2743</guid>
      <description>&lt;p&gt;This week didn't feel like forward motion most of the time. It felt like trying the same door from four different angles and only getting it open on the last try, and then, separately, building something I was genuinely proud of and taking it back apart with my own hands a few days later.&lt;/p&gt;

&lt;p&gt;　The difficulty problem started simple. The puzzles felt too easy, and worse, they didn't get noticeably harder as you moved deeper into the set. My first fix was to add a rule to the generator itself: keep the two endpoints of the same color from landing too close together, since that felt like the obvious source of the "too easy" feeling. I shipped it, ran the full batch, and watched the pipeline that had been reliably producing almost every level in the set collapse down to producing barely a quarter of them. Turning on detailed logging showed why. Almost the entire retry budget was getting burned on the generator rejecting its own boards for being "too close," and that rule was fundamentally at odds with how the generator fills a grid in the first place. It doesn't place two dots and connect them. It grows a full board and only pulls the endpoints out afterward, so telling it "don't let these end up close" was fighting its whole approach rather than nudging it.&lt;/p&gt;

&lt;p&gt;　What actually broke the problem open was a question I asked myself in the middle of debugging it: why was I trying to bias the generator toward bent, spread-out paths at all, when the solver's own best solution for a board has nothing to do with the shape the generator happened to grow? The generator's internal path is thrown away anyway. Difficulty should be judged from what the solver actually finds, not from what the generator's random walk looked like on its way there. That reframing killed the endpoint-distance rule entirely and replaced it with a rule that checks the solver's real answer after the board exists, rejecting boards whose best solution turns out too simple, instead of trying to steer the generator toward simple-looking boards in the first place. It's a small shift on paper. It changed how I thought about every difficulty lever after that.&lt;/p&gt;

&lt;p&gt;　The second attempt at making things feel harder was raising how heavily the scoring formula weighted the number of turns in a solution. I bumped it up expecting the puzzles to read as noticeably trickier. Nothing changed, and it took me a while to understand why: that weight only affects how already-generated puzzles get sorted and labeled after the fact. It doesn't make harder puzzles exist, it just reorders the ones I already had. I'd been polishing the label on the jar instead of changing what was in it.&lt;/p&gt;

&lt;p&gt;　The version that actually worked was introducing a real floor on solver difficulty, tossing out any generated board whose best solution came in under a threshold for its tier, before it ever entered the pool. That worked, in the sense that the puzzles genuinely got harder to solve. It also immediately tanked the generation success rate again, in the same shape as the endpoint-distance failure, just for a more legitimate reason this time. So I raised the retry ceiling again to compensate, watched success recover, and by the end of the week I'd internalized that "make it harder" and "still be able to generate enough levels" aren't two problems you solve once each. They're the same knob, and every real difficulty change costs you something on the generation side that you have to go pay back.&lt;/p&gt;

&lt;p&gt;　The other thread this week was smaller in scope but heavier to sit with. I built the feature that lets you start drawing a color's path from either of its two endpoints and have the two halves merge in the middle, the full logic, fully tested, verified working in the browser. It felt done. Then I actually played through it the way a real player would, drawing from one end, bending the path, and clicking toward the far endpoint expecting the two halves to connect. Instead the click got read as starting a brand new, independent path from that far endpoint going backward. On paper it followed the spec exactly. In practice it felt like the game had misunderstood what I was doing, every time. I pulled the entire feature back out, all of it, and put the simpler original behavior back. That's a strange kind of finished, work that passes every test you wrote for it and still turns out to be the wrong thing once a real hand is on the mouse.&lt;/p&gt;

&lt;p&gt;　By the end of the week the generation pipeline was producing a full, working pool of levels, the obstacle system was actually wired into gameplay instead of just existing in the data, and two real solver bugs that had been quietly producing wrong answers were gone. But if I'm honest, most of what I'll remember about this week isn't any of that. It's how many times I built something correctly and still had to throw it out because correct and right aren't the same thing until someone actually plays it.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Traceroute devlog #3</title>
      <dc:creator>Taehyun Kim</dc:creator>
      <pubDate>Fri, 07 Aug 2026 06:04:10 +0000</pubDate>
      <link>https://dev.to/thkim5937/traceroute-devlog-3-ch1</link>
      <guid>https://dev.to/thkim5937/traceroute-devlog-3-ch1</guid>
      <description>&lt;p&gt;Most of this week's difficulty lived inside one single rule, and what made it worth writing about isn't any one fix — it's that I didn't actually understand the problem until I'd already "solved" it three times.&lt;/p&gt;

&lt;p&gt;The rule sounds small: if you click through a point on your own path that would carry you past your color's still-unreached endpoint, the path should stop right there and count as done. The first version I built didn't do either half of that. Clicking straight through just kept going — the path walked right past the endpoint like it wasn't special at all, and on top of that, nothing registered as complete. So the very first attempt wasn't off by one small detail, it just didn't implement the rule at all, even though I'd been sure it did.&lt;/p&gt;

&lt;p&gt;I fixed the "doesn't stop" part, and that's when things got interesting instead of just wrong. Now, clicking through the endpoint did stop — but it stopped the way an illegal move stops: the little rejection motion played, the same one you get from hitting a wall or crossing your own path somewhere you shouldn't. Which is backwards. Reaching your own endpoint isn't a mistake, it's the point of the whole color. I'd fixed the symptom by routing it through the wrong branch of logic entirely — treating "stop here because you're done" as if it were "stop here because that's illegal."&lt;/p&gt;

&lt;p&gt;So I fixed that too, and got the path to genuinely halt right at the endpoint on a pass-through click, no rejection animation, just a clean stop. That felt like the actual fix. It wasn't. Playing with it afterward, I noticed that if you stopped at the endpoint and then clicked somewhere else entirely, the path would just keep extending forward from there, like the endpoint had never been reached in any meaningful sense. The stop was cosmetic. Nothing was actually locked — "complete" was a label I was drawing correctly on screen without it controlling anything underneath.&lt;/p&gt;

&lt;p&gt;The real fix, the one that finally held, was to make reaching the endpoint freeze the whole path — not just stop drawing at that instant, but lock it against any further change at all, until you deliberately undo or clear it. Only once that existed did the earlier three fixes actually add up to the rule I'd meant to build in the first place.&lt;/p&gt;

&lt;p&gt;That freeze fix, though, immediately opened a new question I hadn't planned for: if reaching an endpoint locks the whole path in place, what happens if what got locked in was wrong? A path can be a technically valid connection and still not be what you actually meant to draw, and once it's frozen there's no way back short of reloading the entire level. That question is what turned into the actual case for building real undo, redo, and a full clear button — not as separate nice-to-haves I thought up on the side, but as the direct consequence of the freeze rule itself. Locking something in place is only fair to the player if there's also a way to unlock it.&lt;/p&gt;

&lt;p&gt;Looking back at the whole sequence, none of the individual steps were hard. What got me was that each fix was locally correct and globally incomplete — it solved exactly the symptom I could see at that moment, and hid the next one underneath it, so I kept discovering the actual shape of the problem one layer at a time instead of seeing it all at once.&lt;/p&gt;

&lt;p&gt;There was one other moment worth a mention, smaller but satisfying in a completely different way: while sketching out how failed puzzle generations should retry, I noticed on paper — before writing any code — that the piece doing the generating already had its own retry allowance built in from earlier work, and stacking a second one on top without accounting for that would have let worst-case attempts multiply into something absurd instead of just adding up. Catching that by re-reading a plan, rather than by something actually breaking, felt like a rare win this week.&lt;/p&gt;

&lt;p&gt;By the end of it, the generator, the search process that finds a way to solve a board, and the difficulty-scoring piece were all built and mostly holding up. The very last step — turning a generated board into a finished, playable level — is written but unconfirmed, so that's exactly where next week starts.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Traceroute devlog #2</title>
      <dc:creator>Taehyun Kim</dc:creator>
      <pubDate>Fri, 31 Jul 2026 08:02:36 +0000</pubDate>
      <link>https://dev.to/thkim5937/traceroute-devlog-2-11kc</link>
      <guid>https://dev.to/thkim5937/traceroute-devlog-2-11kc</guid>
      <description>&lt;p&gt;Mid-week I ran into something that made me stop and take stock: was I building on a foundation I already knew was cracked?&lt;/p&gt;

&lt;p&gt;Most of the click-based path drawing was done by then — starting a path, extending it in a direction, blocking it when it ran into another color, refusing to let it cross itself. All of that was tested and working. The last piece on the list was letting the player undo part of a path by clicking back onto a point it had already passed through. It sounded like the simplest rule of the bunch. It wasn't, mostly because of how everything before it had been built.&lt;/p&gt;

&lt;p&gt;Up to that point, every rule just edited the current path directly — added a cell here, removed one there, always working on the same object sitting in memory. That was fine as long as no rule needed to remember what things looked like before the current click happened. Undo breaks that assumption completely: it has to reach back to "the path as it stood right before this click" and rebuild from there, and if you've been quietly editing that same object the whole time, there's no earlier version left to reach back to. The data just says what it currently says.&lt;/p&gt;

&lt;p&gt;So the actual fix wasn't really about the undo rule itself — it meant going back through everything I'd already built and changing the underlying pattern it was written in: instead of editing state in place, every step now has to produce a brand new copy of the game state and hand that off, leaving the old one completely untouched. That touched code across five already-tested rules, not just the new one, which meant redoing work I'd already called finished for the week. I didn't want to. But patching around the problem instead of fixing the pattern felt like it would just resurface later in a worse spot, probably right when I start animating state changes on screen, which is coming up soon. Doing the rebuild properly also surfaced a bug I hadn't caught on my own — a leftover, empty entry that could get left behind in the data after certain clicks. Harmless by itself, but the kind of thing that's confusing to debug later if it stays hidden long enough.&lt;/p&gt;

&lt;p&gt;Separately, I misjudged a self-intersection test case — the rule where clicking back over a point your own path already crosses should invalidate the whole move — and was confident it was actually a different rule firing, undo instead. It wasn't. I'd misread which cell the click sequence actually landed on once I traced it out step by step. Easy enough to fix once I saw it clearly, but a good reminder of how easy it is to be wrong about grid coordinates specifically, in a way that feels obvious only after someone points at the right cell.&lt;/p&gt;

&lt;p&gt;One visual bug also got fixed this week: the small "bounce back" animation that plays whenever a click gets rejected was cutting off partway through its motion instead of completing it, so a rejection read as a brief freeze rather than a clear "no." Rewriting it to track elapsed time properly, instead of skipping frames under load, fixed that.&lt;/p&gt;

&lt;p&gt;Outside those threads, the week was mostly foundation work: the project now builds and deploys automatically, the grid and dots render with a colorblind-friendly palette, and all of the click-based path rules are built and checked against tests. I closed the week starting on the logic that decides when a level counts as solved, which felt like a natural place to stop.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>gamedev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Traceroute devlog #1</title>
      <dc:creator>Taehyun Kim</dc:creator>
      <pubDate>Wed, 29 Jul 2026 00:40:58 +0000</pubDate>
      <link>https://dev.to/thkim5937/numberlink-devlog-1-mp5</link>
      <guid>https://dev.to/thkim5937/numberlink-devlog-1-mp5</guid>
      <description>&lt;p&gt;　　Didn't write a single line of code today. Spent the whole day buried in the PRD and TRD instead, and by the end of it I realized this was somehow the most exhausting day so far anyway.&lt;/p&gt;

&lt;p&gt;　　The pitch is simple enough: connect same-colored dots with clicks on a grid. One sentence. But the second I tried to break that sentence into an actual buildable spec, I hit a wall almost immediately. First thing that tripped me up was what "clicking a dot" even means. Does it only mean the colored endpoints, or can you click any cell in the same row/column to extend a path? Until that's pinned down you can't even say whether a path is allowed to bend. Felt dumb not catching something this basic sooner, but you really don't see it until you try to write it down.&lt;/p&gt;

&lt;p&gt;　　That became the pattern for most of the day — fix one rule, and the thing next to it breaks. Trim (undoing part of your own path) versus self-intersection was a particularly annoying one. If your own path happens to sit somewhere in the middle of a straight-line click extension, is that a trim or is it just blocked? Ended up landing on: it's only a trim if the clicked cell is exactly an existing point on your path, otherwise the whole click is invalid, full stop. Took longer than it should have to get there.&lt;/p&gt;

&lt;p&gt;　　The biggest thing I ran into, though, was the unique-solution requirement. Went in assuming of course the solver needs to guarantee a unique solution — that felt obvious. Except digging into it, I realized an easy-difficulty board with lots of empty cells is structurally going to have multiple solutions no matter what. So enforcing uniqueness means you literally can't generate easy levels. Kind of a gut-punch moment when I saw it, and honestly I couldn't even point to a real reason I'd assumed uniqueness mattered in the first place — it just felt like the "correct" way to do it. Scrapped the requirement entirely and rewrote features 13/14/15 around the solver just finding "the simplest solution" among whatever it finds, and using that as the difficulty basis instead.&lt;/p&gt;

&lt;p&gt;　　Endless mode was born and killed in the same session. Originally had generated levels continuing forever, but going back over it, there was nothing defining what happens to the player experience if real-time generation fails or lags. So that got torn out in favor of pre-generating a fixed pool of 50 (6 hand-designed + 44 generated) before deployment, with a "more to come" message on the last level. The daily-puzzle stretch idea died in the same pass too — decided the portfolio value was too low, and replaced it with an auto-generation pipeline (generate → AI first-pass review → developer second-pass review → deploy), which felt like a better showcase of actually automating a workflow with AI.&lt;/p&gt;

&lt;p&gt;　　Then later I added a Stretch feature — a live generator/solver demo screen — and that ran straight into an earlier TRD decision that the generator module was Node-only. The demo screen needs the generator running live in the browser, so those two things didn't fit together. Ended up redesigning both the generator and solver as environment-agnostic pure TypeScript so they could be reused in the Node pipeline and in the browser demo. Honestly the whole reason I wanted that demo screen in the first place is a question that kept nagging at me — shouldn't a portfolio piece built around an NP-hard problem actually show the algorithm running somewhere, instead of hiding it entirely behind an offline pipeline?&lt;/p&gt;

&lt;p&gt;　　Looking at where the docs stand now, the PRD's been rewritten five times and the TRD side got gutted and redone twice in the same day. The leftover TBD list is still long — the color-pair/grid-size progression curve, real solver timeout numbers, difficulty threshold calibration, how the obstacle feature actually changes the generator/solver algorithms. All of that needs real measurements before it can be locked in, so for now I just wrote placeholder numbers and moved on (p_stop = 0.15, 50k nodes / 3s timeout, tag thresholds at 33/66, that kind of thing).&lt;/p&gt;

&lt;p&gt;　　Feels a little weird not having written any code today, like there's nothing to show for it. But if I'd started coding without catching these contradictions first, I'd probably be tearing all of it apart again three weeks in. PRD and TRD are both up in Notion as final versions now. Tomorrow I actually want to get into project setup and grid rendering.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>devjournal</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
