import InfoBox from '../components/InfoBox.astro';
import PullQuote from '../components/PullQuote.astro';

A second game mode arrived in one day: Duel, a turn-based match against an AI opponent sharing the same board and the same fruit as the player. Every commit in the range, fourteen of them across three pull requests, came from GitHub's coding agent again, this time with no hand-branched feature work alongside it at all.

## One engine, two snakes

The interesting decision was not the new UI, it was what did not need to change: `buildAnimationSteps` and `rebuildGrid` each grew one optional argument for the opponent's body positions, treated as static walls a word can crash into, while the daily challenge's own code paths stayed untouched. [The engine that never met the framework](/posts/the-beginning#the-engine-never-met-the-framework) took a second snake as an argument, not a rewrite. Five new database tables carried the rest of the mode: a session record, a per-move replay log, a public leaderboard, and two more for daily-mode analytics the game had never collected until an AI opponent's move needed the same logging path the player's already had.

## A first, cautious opponent

Each opponent turn samples a handful of random dictionary words, scores each one by its word value plus whatever fruit and combos it would collect along the way, and sorts the results into safe and unsafe piles depending on whether the move ends in a collision. A safe word is preferred over an unsafe one no matter how the scores compare, with a minor tiebreaker layered on for otherwise-close safe choices; only once repeated sampling turns up nothing safe at all does the opponent settle for its best unsafe option.

<PullQuote>It would rather not lose than try to win.</PullQuote>

The same day surfaced a scoring bug in the win condition itself: the game had been awarding the win to whichever snake did not crash, regardless of either score, which the fix replaced with a direct comparison:

```typescript
if (newPlayerScore > newOpponentScore) newWinner = 'player';
else if (newOpponentScore > newPlayerScore) newWinner = 'opponent';
else newWinner = 'draw';
```

## Teaching two modes to look like one

The rest of the day made Duel feel like a sibling of Daily Challenge rather than a separate app bolted alongside it: the same on-screen keyboard component now drives both modes instead of a second, slightly different one built for Duel; the duel leaderboard gained the daily leaderboard's card layout, medal icons, and colour-coded results; and the how-to-play modal grew a Game Modes section explaining both at once, now re-shown every seven days instead of once ever, so a returning player who missed a new mode has a chance to find it.

<InfoBox title="Each mode points at the other">
  A low-emphasis card now sits below each leaderboard tab: the Daily tab says "Face an AI opponent
  on the same board, higher score wins" and links to Duel; the Duel tab says "Everyone plays the
  same board each day, compare your score on the leaderboard" and links back to Daily Challenge.
</InfoBox>

## Where this leaves things

The opponent that shipped today plays it safe by construction, never trading a guaranteed non-collision for a bigger word, and whether that makes for a match worth playing more than once is an open question rather than a settled one. What is settled is the shape underneath it: the same pure engine, the same board, the same fruit, extended by an argument rather than replaced by a second implementation, exactly the payoff the framework-free first day was betting on.