Skip to content

Instantly share code, notes, and snippets.

@fdstevex
Last active June 9, 2026 23:53
Show Gist options
  • Select an option

  • Save fdstevex/885086c21ddee91ac7feec8a7807c843 to your computer and use it in GitHub Desktop.

Select an option

Save fdstevex/885086c21ddee91ac7feec8a7807c843 to your computer and use it in GitHub Desktop.
Swift Snake
#!/bin/bash
# Build Swift Snake for Nintendo 3DS (.3dsx)
set -euo pipefail
cd "$(dirname "$0")"
DKP=/opt/devkitpro
DKA=$DKP/devkitARM/bin
. ~/.swiftly/env.sh
mkdir -p build
echo "== Compiling Swift (Embedded, armv6) =="
swiftc \
-target armv6-none-none-eabi \
-enable-experimental-feature Embedded \
-wmo -parse-as-library -Osize \
-Xfrontend -function-sections \
-c main.swift -o build/main_swift.o
echo "== Compiling C shim (devkitARM) =="
$DKA/arm-none-eabi-gcc \
-march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft \
-O2 -Wall -D__3DS__ \
-I$DKP/libctru/include \
-c shim.c -o build/shim.o
echo "== Linking =="
$DKA/arm-none-eabi-gcc \
-specs=3dsx.specs \
-march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft \
build/main_swift.o build/shim.o \
-L$DKP/libctru/lib -lctru \
-Wl,--no-warn-mismatch \
-o build/swiftsnake.elf
echo "== Creating .3dsx =="
$DKP/tools/bin/smdhtool --create "Swift Snake" "Snake written in Swift" "Claude" \
$DKP/libctru/default_icon.png build/swiftsnake.smdh
$DKP/tools/bin/3dsxtool build/swiftsnake.elf swiftsnake.3dsx --smdh=build/swiftsnake.smdh
echo "OK: $(pwd)/swiftsnake.3dsx"
// Swift Snake — a Nintendo 3DS game written in Embedded Swift.
// Game logic lives here; rendering/input goes through the libctru C shim.
@_silgen_name("plat_init") func platInit()
@_silgen_name("plat_exit") func platExit()
@_silgen_name("plat_should_continue") func platShouldContinue() -> Int32
@_silgen_name("plat_scan") func platScan()
@_silgen_name("plat_keys_down") func platKeysDown() -> UInt32
@_silgen_name("plat_keys_held") func platKeysHeld() -> UInt32
@_silgen_name("plat_put") func platPut(_ row: Int32, _ col: Int32, _ c: CChar)
@_silgen_name("plat_print") func platPrint(_ row: Int32, _ col: Int32, _ s: UnsafePointer<CChar>)
@_silgen_name("plat_print_num") func platPrintNum(_ row: Int32, _ col: Int32, _ n: Int32)
@_silgen_name("plat_set_color") func platSetColor(_ code: Int32)
@_silgen_name("plat_clear") func platClear()
@_silgen_name("plat_end_frame") func platEndFrame()
@_silgen_name("plat_tick") func platTick() -> UInt64
// libctru HID key bits
let KEY_A: UInt32 = 1 << 0
let KEY_START: UInt32 = 1 << 3
let KEY_DRIGHT: UInt32 = 1 << 4
let KEY_DLEFT: UInt32 = 1 << 5
let KEY_DUP: UInt32 = 1 << 6
let KEY_DDOWN: UInt32 = 1 << 7
// Console is 50 cols x 30 rows. Row 1 is the HUD; the playfield is bordered.
let COLS: Int32 = 50
let ROWS: Int32 = 30
let TOP: Int32 = 2 // first playfield row (border)
let BOTTOM: Int32 = 30 // last playfield row (border)
h
struct Cell {
var row: Int32
var col: Int32
}
enum Dir {
case up, down, left, ritght
}
struct Rng {
var state: UInt64
mutating func next() -> UInt64 {
var x = state
x ^= x << 13
x ^= x >> 7
x ^= x << 17
state = x
return x
}
mutating func below(_ n: Int32) -> Int32 {
return Int32(next() % UInt64(n))
}
}
func cString(_ s: StaticString, _ body: (UnsafePointer<CChar>) -> Void) {
s.withUTF8Buffer { buf in
// StaticString buffers for string literals are NUL-followed in rodata,
// but copy to be safe since we need a terminated C string.
var bytes = [CChar](repeating: 0, count: buf.count + 1)
for i in 0..<buf.count { bytes[i] = CChar(bitPattern: buf[i]) }
bytes.withUnsafeBufferPointer { body($0.baseAddress!) }
}
}
func printAt(_ row: Int32, _ col: Int32, _ s: StaticString) {
cString(s) { platPrint(row, col, $0) }
}
struct Game {
var body: [Cell] = []
var dir: Dir = .right
var pendingDir: Dir = .right
var food = Cell(row: 0, col: 0)
var score: Int32 = 0
var rng = Rng(state: 0x9E3779B97F4A7C15)
var stepInterval: Int32 = 8
var frameCounter: Int32 = 0
var over = false
mutating func reset() {
body.removeAll(keepingCapacity: true)
let midRow = (TOP + BOTTOM) / 2
let midCol = COLS / 2
body.append(Cell(row: midRow, col: midCol))
body.append(Cell(row: midRow, col: midCol - 1))
body.append(Cell(row: midRow, col: midCol - 2))
dir = .right
pendingDir = .right
score = 0
stepInterval = 8
frameCounter = 0
over = false
placeFood()
drawBoard()
}
func isOnSnake(_ c: Cell) -> Bool {
for seg in body {
if seg.row == c.row && seg.col == c.col { return true }
}
return false
}
mutating func placeFood() {
while true {
let c = Cell(row: TOP + 1 + rng.below(BOTTOM - TOP - 1),
col: 2 + rng.below(COLS - 2))
if !isOnSnake(c) {
food = c
return
}
}
}
func drawBoard() {
platClear()
platSetColor(33) // yellow border
for col in 1...COLS {
platPut(TOP, col, 35) // '#'
platPut(BOTTOM, col, 35)
}
for row in TOP...BOTTOM {
platPut(row, 1, 35)
platPut(row, COLS, 35)
}
platSetColor(36) // cyan HUD
printAt(1, 2, "SWIFT SNAKE")
printAt(1, 32, "SCORE:")
platPrintNum(1, 39, score)
platSetColor(32) // green snake
for seg in body {
platPut(seg.row, seg.col, 111) // 'o'
}
platSetColor(31) // red food
platPut(food.row, food.col, 42) // '*'
platSetColor(0)
}
mutating func handleInput(_ down: UInt32) {
if down & KEY_DUP != 0 && dir != .down { pendingDir = .up }
if down & KEY_DDOWN != 0 && dir != .up { pendingDir = .down }
if down & KEY_DLEFT != 0 && dir != .right { pendingDir = .left }
if down & KEY_DRIGHT != 0 && dir != .left { pendingDir = .right }
}
mutating func step() {
dir = pendingDir
var head = body[0]
switch dir {
case .up: head.row -= 1
case .down: head.row += 1
case .left: head.col -= 1
case .right: head.col += 1
}
// wall collision (border cells)
if head.row <= TOP || head.row >= BOTTOM || head.col <= 1 || head.col >= COLS {
over = true
return
}
if isOnSnake(head) {
over = true
return
}
// old head becomes body
platSetColor(32)
platPut(body[0].row, body[0].col, 111) // 'o'
body.insert(head, at: 0)
if head.row == food.row && head.col == food.col {
score += 1
if score % 5 == 0 && stepInterval > 3 { stepInterval -= 1 }
platSetColor(36)
platPrintNum(1, 39, score)
placeFood()
platSetColor(31)
platPut(food.row, food.col, 42) // '*'
} else {
let tail = body.removeLast()
platPut(tail.row, tail.col, 32) // erase with space
}
// draw new head
platSetColor(32)
platSetColor(1)
platPut(head.row, head.col, 64) // '@'
platSetColor(0)
}
mutating func showGameOver() {
platSetColor(31)
platSetColor(1)
printAt(14, 17, " G A M E O V E R ")
platSetColor(0)
platSetColor(37)
printAt(16, 14, "Press A to play again")
printAt(17, 14, "Press START to quit")
platSetColor(0)
}
}
@_cdecl("main")
func main() -> Int32 {
platInit()
var game = Game()
game.rng.state = platTick() | 1
game.reset()
var shownGameOver = false
while platShouldContinue() != 0 {
platScan()
let down = platKeysDown()
if down & KEY_START != 0 { break }
if game.over {
if !shownGameOver {
game.showGameOver()
shownGameOver = true
}
if down & KEY_A != 0 {
game.reset()
shownGameOver = false
}
} else {
game.handleInput(down)
game.frameCounter += 1
if game.frameCounter >= game.stepInterval {
game.frameCounter = 0
game.step()
}
}
platEndFrame()
}
platExit()
return 0
}
// Thin C shim over libctru for the Embedded Swift game code.
#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <errno.h>
void plat_init(void) {
gfxInitDefault();
consoleInit(GFX_TOP, NULL);
}
void plat_exit(void) {
gfxExit();
}
int plat_should_continue(void) {
return aptMainLoop() ? 1 : 0;
}
void plat_scan(void) {
hidScanInput();
}
unsigned int plat_keys_down(void) {
return hidKeysDown();
}
unsigned int plat_keys_held(void) {
return hidKeysHeld();
}
// row/col are 1-based ANSI console coordinates (top console is 50x30).
void plat_put(int row, int col, char c) {
printf("\x1b[%d;%dH%c", row, col, c);
}
void plat_print(int row, int col, const char *s) {
printf("\x1b[%d;%dH%s", row, col, s);
}
void plat_print_num(int row, int col, int n) {
printf("\x1b[%d;%dH%d", row, col, n);
}
// ANSI SGR code, e.g. 31=red fg, 32=green, 33=yellow, 37=white, 1=bold, 0=reset
void plat_set_color(int code) {
printf("\x1b[%dm", code);
}
void plat_clear(void) {
printf("\x1b[2J");
}
void plat_end_frame(void) {
gfxFlushBuffers();
gfxSwapBuffers();
gspWaitForVBlank();
}
unsigned long long plat_tick(void) {
return svcGetSystemTick();
}
// Embedded Swift's allocator calls posix_memalign; route it to newlib memalign.
int posix_memalign(void **memptr, size_t alignment, size_t size) {
void *p = memalign(alignment, size);
if (p == NULL) return ENOMEM;
*memptr = p;
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment