Last active
June 9, 2026 23:53
-
-
Save fdstevex/885086c21ddee91ac7feec8a7807c843 to your computer and use it in GitHub Desktop.
Swift Snake
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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