diff --git a/src/app.s b/src/app.s index 0309a33..9df5088 100644 --- a/src/app.s +++ b/src/app.s @@ -1,28 +1,47 @@ -// SlowAPI Hotel CRUD Application -// Demonstrates path parameters, JSON building, and in-memory database +// SlowAPI Chess Application +// A chess game API written in pure ARM64 assembly .section .text .include "src/slowapi/macros.s" -// Hotel record layout (stored in database) -.equ HOTEL_NAME_OFF, 0 // 32 bytes: name (null-terminated) -.equ HOTEL_CITY_OFF, 32 // 32 bytes: city (null-terminated) -.equ HOTEL_SIZE, 64 -.equ HOTEL_NAME_MAX, 31 // max name length (leave 1 for null) -.equ HOTEL_CITY_MAX, 31 // max city length - //============================================================================= -// JSON CONTEXT SIZE (from json.s) +// Chess Constants //============================================================================= -.equ JSON_CTX_SIZE, 16 + +// Piece encoding (high nibble = color: 0=white, 1=black) +.equ PIECE_EMPTY, 0x00 +.equ PIECE_KING, 0x01 +.equ PIECE_QUEEN, 0x02 +.equ PIECE_ROOK, 0x03 +.equ PIECE_BISHOP, 0x04 +.equ PIECE_KNIGHT, 0x05 +.equ PIECE_PAWN, 0x06 +.equ COLOR_BLACK, 0x10 // OR with piece type for black pieces + +// Game record layout (96 bytes) +.equ GAME_BOARD, 0 // 64 bytes: board state +.equ GAME_NEXT_TURN, 64 // 1 byte: 0=white, 1=black +.equ GAME_STATUS, 65 // 1 byte: 0=waiting, 1=active, 2=finished +.equ GAME_MOVE_COUNT, 66 // 2 bytes: number of moves made +.equ GAME_WHITE_ID, 68 // 4 bytes: white player ID (0=empty) +.equ GAME_BLACK_ID, 72 // 4 bytes: black player ID (0=empty) +.equ GAME_INVITE, 76 // 16 bytes: invite secret +.equ GAME_SIZE, 96 + +// Game status values +.equ STATUS_WAITING, 0 +.equ STATUS_ACTIVE, 1 +.equ STATUS_FINISHED, 2 //============================================================================= -// Stack Frame Sizes (named constants for clarity) +// JSON and Stack Frame Sizes //============================================================================= -.equ LIST_HOTELS_LOCAL, JSON_CTX_SIZE + 1024 -.equ GET_HOTEL_LOCAL, JSON_CTX_SIZE + 512 -.equ CREATE_HOTEL_LOCAL, HOTEL_SIZE + JSON_CTX_SIZE + 512 +.equ JSON_CTX_SIZE, 16 +.equ LIST_GAMES_LOCAL, JSON_CTX_SIZE + 2048 +.equ GET_GAME_LOCAL, JSON_CTX_SIZE + 1024 +.equ CREATE_GAME_LOCAL, GAME_SIZE + JSON_CTX_SIZE + 512 +.equ MOVE_LOCAL, JSON_CTX_SIZE + 512 //============================================================================= // ROUTES @@ -36,130 +55,144 @@ handler_index: ldr w1, [x1] b resp_html -// GET /api/hotels - List all hotels -ENDPOINT METHOD_GET, "/api/hotels" -handler_list_hotels: - FRAME_ENTER 0, LIST_HOTELS_LOCAL +// GET /api/games - List all games +ENDPOINT METHOD_GET, "/api/games" +handler_list_games: + FRAME_ENTER 0, LIST_GAMES_LOCAL - JSON_INIT sp, 1024 + JSON_INIT sp, 2048 JSON_ARR_START sp - // Iterate over all hotels - ldr x0, =list_hotel_callback - mov x1, sp // pass JSON context as user data + ldr x0, =list_game_callback + mov x1, sp bl db_list JSON_ARR_END sp JSON_RESPOND sp - FRAME_LEAVE 0, LIST_HOTELS_LOCAL + FRAME_LEAVE 0, LIST_GAMES_LOCAL -// Callback for db_list: adds a hotel to JSON array -// x0 = id, x1 = data ptr, x2 = data size, x3 = context (JSON ctx) -list_hotel_callback: +// Callback for db_list: adds a game summary to JSON array +list_game_callback: FRAME_ENTER 2 - mov w19, w0 // id - mov x20, x1 // data ptr (hotel record) + mov w19, w0 // game id + mov x20, x1 // game data ptr mov x21, x3 // JSON context - // Check if we need a comma (check if array already has content) - // Simple heuristic: if length > 1 (just '['), add comma - ldr w0, [x21, #12] // JSON_LEN + // Add comma if needed + ldr w0, [x21, #12] cmp w0, #1 - b.le .no_comma_needed - + b.le .list_no_comma JSON_COMMA x21 +.list_no_comma: -.no_comma_needed: JSON_OBJ_START x21 - // Add "id": + // "id": JSON_KEY x21, key_id, 2 JSON_INT x21, w19 JSON_COMMA x21 - // Add "name": "" - JSON_KEY x21, key_name, 4 - - add x22, x20, #HOTEL_NAME_OFF - mov x0, x22 - bl strlen_simple - mov w1, w0 + // "status": "" + JSON_KEY x21, key_status, 6 + ldrb w0, [x20, #GAME_STATUS] + cmp w0, #STATUS_WAITING + b.eq .status_waiting + cmp w0, #STATUS_ACTIVE + b.eq .status_active + b .status_finished +.status_waiting: mov x0, x21 - mov x2, x1 // length - mov x1, x22 // name pointer + ldr x1, =str_waiting + mov x2, #7 bl json_add_string + b .status_done - JSON_COMMA x21 +.status_active: + mov x0, x21 + ldr x1, =str_active + mov x2, #6 + bl json_add_string + b .status_done - // Add "city": "" - JSON_KEY x21, key_city, 4 +.status_finished: + mov x0, x21 + ldr x1, =str_finished + mov x2, #8 + bl json_add_string - add x22, x20, #HOTEL_CITY_OFF - mov x0, x22 - bl strlen_simple - mov w1, w0 +.status_done: + JSON_COMMA x21 + // "next_turn": "white" or "black" + JSON_KEY x21, key_next_turn, 9 + ldrb w0, [x20, #GAME_NEXT_TURN] + cbz w0, .turn_white mov x0, x21 - mov x2, x1 - mov x1, x22 + ldr x1, =str_black + mov x2, #5 + bl json_add_string + b .turn_done +.turn_white: + mov x0, x21 + ldr x1, =str_white + mov x2, #5 bl json_add_string +.turn_done: JSON_OBJ_END x21 - // Return 0 to continue iteration mov x0, #0 - FRAME_LEAVE 2 -// GET /api/hotels/{id} - Get single hotel -ENDPOINT METHOD_GET, "/api/hotels/{id}" -handler_get_hotel: - FRAME_ENTER 1, GET_HOTEL_LOCAL +// GET /api/games/{id} - Get single game with full board state +ENDPOINT METHOD_GET, "/api/games/{id}" +handler_get_game: + FRAME_ENTER 1, GET_GAME_LOCAL - mov x19, x0 // request context + mov x19, x0 - // Parse ID from path param + // Parse game ID ldr x0, [x19, #REQ_PATH_PARAM] ldr w1, [x19, #REQ_PATH_PARAM_LEN] bl parse_int - cbz x0, .get_hotel_err - mov w20, w0 // hotel ID + cbz x0, .get_game_err + mov w20, w0 - // Get hotel from database + // Get game from database mov w0, w20 bl db_get - cbz x0, .get_hotel_err - mov x19, x0 // hotel data + cbz x0, .get_game_err + mov x19, x0 // Build JSON response - JSON_INIT sp, 512 + JSON_INIT sp, 1024 mov x0, sp mov x1, x19 mov w2, w20 - bl build_hotel_json + bl build_game_json JSON_RESPOND sp - b .get_hotel_exit + b .get_game_exit -.get_hotel_err: +.get_game_err: mov w0, #STATUS_NOT_FOUND bl resp_error -.get_hotel_exit: - FRAME_LEAVE 1, GET_HOTEL_LOCAL +.get_game_exit: + FRAME_LEAVE 1, GET_GAME_LOCAL -// POST /api/hotels - Create hotel -// Body format: "name,city" -ENDPOINT METHOD_POST, "/api/hotels" -handler_create_hotel: - FRAME_ENTER 2, CREATE_HOTEL_LOCAL +// POST /api/games - Create new game +// Body: "W" or "B" for desired color +ENDPOINT METHOD_POST, "/api/games" +handler_create_game: + FRAME_ENTER 2, CREATE_GAME_LOCAL - mov x19, x0 // request context + mov x19, x0 // Get body ldr x20, [x19, #REQ_BODY] @@ -168,66 +201,73 @@ handler_create_hotel: cbz x20, .create_err_400 cbz w21, .create_err_400 - // Parse "name,city" format - find comma - mov x0, x20 - mov w1, w21 - mov w2, #',' - bl find_char - cbz x0, .create_err_400 - mov x22, x0 // comma position - - // Build hotel record on stack - add x0, sp, #JSON_CTX_SIZE + 512 // hotel record buffer - - // Copy name (from body start to comma) - sub w1, w22, w20 // name length - cmp w1, #HOTEL_NAME_MAX - b.gt .create_err_400 - cbz w1, .create_err_400 - - mov x2, x0 // dest - mov x3, x20 // src (body start) -.copy_name: - cbz w1, .name_copied - ldrb w4, [x3], #1 - strb w4, [x2], #1 - sub w1, w1, #1 - b .copy_name -.name_copied: - strb wzr, [x2] // null terminate - - // Copy city (from after comma to end) + // Parse desired color (W or B) + ldrb w22, [x20] + cmp w22, #'W' + b.eq .create_white + cmp w22, #'B' + b.eq .create_black + b .create_err_400 + +.create_white: + mov w22, #0 // white_id = 1 (placeholder), black_id = 0 + b .create_init + +.create_black: + mov w22, #1 // white_id = 0, black_id = 1 (placeholder) + +.create_init: + // Initialize game record on stack + add x0, sp, #JSON_CTX_SIZE + 512 + + // Zero out the entire record first + mov x1, #GAME_SIZE + mov x2, x0 +.zero_game: + cbz x1, .zero_done + strb wzr, [x2], #1 + sub x1, x1, #1 + b .zero_game +.zero_done: + + // Set up initial board position add x0, sp, #JSON_CTX_SIZE + 512 - add x3, x22, #1 // skip comma - add x2, x0, #HOTEL_CITY_OFF - - // Calculate city length - add x4, x20, x21 // body end - sub w1, w4, w3 // city length - cmp w1, #HOTEL_CITY_MAX - b.gt .create_err_400 - cbz w1, .create_err_400 - -.copy_city: - cbz w1, .city_copied - ldrb w4, [x3], #1 - strb w4, [x2], #1 - sub w1, w1, #1 - b .copy_city -.city_copied: - strb wzr, [x2] // null terminate + bl setup_initial_board + + // Set game status to waiting + add x0, sp, #JSON_CTX_SIZE + 512 + mov w1, #STATUS_WAITING + strb w1, [x0, #GAME_STATUS] + + // Set player ID based on color choice + cbz w22, .set_white_player + // Black player + mov w1, #1 // placeholder player ID + str w1, [x0, #GAME_BLACK_ID] + b .player_set +.set_white_player: + mov w1, #1 // placeholder player ID + str w1, [x0, #GAME_WHITE_ID] +.player_set: + + // Generate simple invite secret (just use counter for now) + ldr x1, =invite_counter + ldr w2, [x1] + add w3, w2, #1 + str w3, [x1] + str w2, [x0, #GAME_INVITE] // Create record in database add x0, sp, #JSON_CTX_SIZE + 512 - mov x1, #HOTEL_SIZE + mov x1, #GAME_SIZE bl db_create cbz x0, .create_err_500 - mov w20, w0 // new hotel ID + mov w20, w0 - // Get the record back to build response + // Get the record back mov w0, w20 bl db_get - mov x21, x0 // hotel data ptr + mov x21, x0 // Build JSON response JSON_INIT sp, 512 @@ -235,11 +275,10 @@ handler_create_hotel: mov x0, sp mov x1, x21 mov w2, w20 - bl build_hotel_json + bl build_game_json mov x0, sp bl json_finish - // x0 = buffer, x1 = length mov x2, x1 mov x1, x0 @@ -259,51 +298,482 @@ handler_create_hotel: bl resp_error .create_exit: - FRAME_LEAVE 2, CREATE_HOTEL_LOCAL + FRAME_LEAVE 2, CREATE_GAME_LOCAL -// DELETE /api/hotels/{id} - Delete hotel -ENDPOINT METHOD_DELETE, "/api/hotels/{id}" -handler_delete_hotel: - FRAME_ENTER 1 +// POST /api/games/{id}/join - Join a game +// Query: secret= +ENDPOINT METHOD_POST, "/api/games/{id}/join" +handler_join_game: + FRAME_ENTER 2, GET_GAME_LOCAL - mov x19, x0 // request context + mov x19, x0 - // Parse ID from path param + // Parse game ID ldr x0, [x19, #REQ_PATH_PARAM] ldr w1, [x19, #REQ_PATH_PARAM_LEN] bl parse_int - cbz x0, .delete_err + cbz x0, .join_err_404 mov w20, w0 - // Delete from database + // Get game mov w0, w20 - bl db_delete - cmp x0, #0 - b.ne .delete_err + bl db_get + cbz x0, .join_err_404 + mov x21, x0 + + // Check game is waiting for player + ldrb w0, [x21, #GAME_STATUS] + cmp w0, #STATUS_WAITING + b.ne .join_err_400 + + // Check which slot is empty and fill it + ldr w0, [x21, #GAME_WHITE_ID] + cbz w0, .join_as_white + ldr w0, [x21, #GAME_BLACK_ID] + cbz w0, .join_as_black + b .join_err_400 // Game is full + +.join_as_white: + mov w0, #2 // placeholder player ID for second player + str w0, [x21, #GAME_WHITE_ID] + b .join_activate + +.join_as_black: + mov w0, #2 + str w0, [x21, #GAME_BLACK_ID] + +.join_activate: + // Set game to active + mov w0, #STATUS_ACTIVE + strb w0, [x21, #GAME_STATUS] + + // No need to call db_update - db_get returns direct pointer, changes are in-place + + // Build response + JSON_INIT sp, 1024 - // 204 No Content - bl resp_no_content - b .delete_exit + mov x0, sp + mov x1, x21 + mov w2, w20 + bl build_game_json + + JSON_RESPOND sp + b .join_exit + +.join_err_400: + mov w0, #STATUS_BAD_REQUEST + b .join_err -.delete_err: +.join_err_404: mov w0, #STATUS_NOT_FOUND + +.join_err: bl resp_error -.delete_exit: - FRAME_LEAVE 1 +.join_exit: + FRAME_LEAVE 2, GET_GAME_LOCAL + +// POST /api/games/{id}/move - Make a move +// Body: "e2e4" format (from_file from_rank to_file to_rank) +ENDPOINT METHOD_POST, "/api/games/{id}/move" +handler_move: + FRAME_ENTER 3, MOVE_LOCAL + + mov x19, x0 + + // Parse game ID + ldr x0, [x19, #REQ_PATH_PARAM] + ldr w1, [x19, #REQ_PATH_PARAM_LEN] + bl parse_int + cbz x0, .move_err_404 + mov w20, w0 + + // Get game + mov w0, w20 + bl db_get + cbz x0, .move_err_404 + mov x21, x0 + + // Check game is active + ldrb w0, [x21, #GAME_STATUS] + cmp w0, #STATUS_ACTIVE + b.ne .move_err_400 + + // Get body (e.g., "e2e4") + ldr x22, [x19, #REQ_BODY] + ldr w23, [x19, #REQ_BODY_LEN] + + cmp w23, #4 + b.lt .move_err_400 + + // Parse from square + ldrb w0, [x22] // from_file char + bl parse_file + cmp w0, #-1 + b.eq .move_err_400 + mov w24, w0 // from_file (0-7) + + ldrb w0, [x22, #1] // from_rank char + bl parse_rank + cmp w0, #-1 + b.eq .move_err_400 + mov w25, w0 // from_rank (0-7) + + // Parse to square + ldrb w0, [x22, #2] // to_file char + bl parse_file + cmp w0, #-1 + b.eq .move_err_400 + mov w26, w0 // to_file (0-7) + + ldrb w0, [x22, #3] // to_rank char + bl parse_rank + cmp w0, #-1 + b.eq .move_err_400 + mov w27, w0 // to_rank (0-7) + + // Calculate board indices + // from_idx = from_rank * 8 + from_file + lsl w0, w25, #3 + add w0, w0, w24 // from_idx in w0 + + // Get piece at from square + add x1, x21, #GAME_BOARD + ldrb w2, [x1, x0] // piece at from + + // Check there's a piece there + cbz w2, .move_err_400 + + // Check piece color matches current turn + ldrb w3, [x21, #GAME_NEXT_TURN] + lsr w4, w2, #4 // piece color (0=white, 1=black) + cmp w3, w4 + b.ne .move_err_403 // Not your turn + + // Calculate to_idx = to_rank * 8 + to_file + lsl w5, w27, #3 + add w5, w5, w26 // to_idx in w5 + + // Check destination isn't occupied by same color + ldrb w6, [x1, x5] // piece at to + cbz w6, .dest_ok + lsr w7, w6, #4 // dest piece color + cmp w4, w7 + b.eq .move_err_400 // Can't capture own piece +.dest_ok: + + // Validate move based on piece type + and w2, w2, #0x0F // piece type (without color) + mov w0, w2 // piece type + mov w1, w24 // from_file + mov w2, w25 // from_rank + mov w3, w26 // to_file + mov w4, w27 // to_rank + bl validate_move + cbz w0, .move_err_400 // Invalid move + + // Make the move + add x1, x21, #GAME_BOARD + + // Calculate indices again + lsl w0, w25, #3 + add w0, w0, w24 // from_idx + lsl w5, w27, #3 + add w5, w5, w26 // to_idx + + // Move piece + ldrb w2, [x1, x0] // get piece + strb wzr, [x1, x0] // clear from + strb w2, [x1, x5] // set to + + // Toggle turn + ldrb w0, [x21, #GAME_NEXT_TURN] + eor w0, w0, #1 + strb w0, [x21, #GAME_NEXT_TURN] + + // Increment move count + ldrh w0, [x21, #GAME_MOVE_COUNT] + add w0, w0, #1 + strh w0, [x21, #GAME_MOVE_COUNT] + + // No need to call db_update - db_get returns direct pointer, changes are in-place + + // Build response + JSON_INIT sp, 512 + + mov x0, sp + mov x1, x21 + mov w2, w20 + bl build_game_json + + JSON_RESPOND sp + b .move_exit + +.move_err_400: +.move_err_403: + mov w0, #STATUS_BAD_REQUEST + b .move_err + +.move_err_404: + mov w0, #STATUS_NOT_FOUND + +.move_err: + bl resp_error + +.move_exit: + FRAME_LEAVE 3, MOVE_LOCAL //============================================================================= // HELPER FUNCTIONS //============================================================================= -// build_hotel_json: Build JSON for a hotel -// Input: x0 = JSON context, x1 = hotel data ptr, w2 = hotel ID -build_hotel_json: - FRAME_ENTER 2 +// parse_file: Convert file char ('a'-'h' or 'A'-'H') to index (0-7) +// Input: w0 = char +// Output: w0 = index (0-7) or -1 if invalid +parse_file: + // Check lowercase + cmp w0, #'a' + b.lt .try_upper_file + cmp w0, #'h' + b.gt .invalid_file + sub w0, w0, #'a' + ret + +.try_upper_file: + cmp w0, #'A' + b.lt .invalid_file + cmp w0, #'H' + b.gt .invalid_file + sub w0, w0, #'A' + ret + +.invalid_file: + mov w0, #-1 + ret + +// parse_rank: Convert rank char ('1'-'8') to index (0-7) +// Input: w0 = char +// Output: w0 = index (0-7) or -1 if invalid +parse_rank: + cmp w0, #'1' + b.lt .invalid_rank + cmp w0, #'8' + b.gt .invalid_rank + sub w0, w0, #'1' + ret + +.invalid_rank: + mov w0, #-1 + ret + +// validate_move: Check if a move is valid for the piece type +// Input: w0 = piece type (1-6), w1 = from_file, w2 = from_rank, +// w3 = to_file, w4 = to_rank +// Output: w0 = 1 if valid, 0 if invalid +validate_move: + stp x29, x30, [sp, #-16]! + mov x29, sp + + // Calculate file and rank differences + sub w5, w3, w1 // file_diff (signed) + sub w6, w4, w2 // rank_diff (signed) + + // Absolute values + cmp w5, #0 + cneg w7, w5, lt // abs_file_diff + cmp w6, #0 + cneg w8, w6, lt // abs_rank_diff + + // Check piece type + cmp w0, #PIECE_KING + b.eq .validate_king + cmp w0, #PIECE_QUEEN + b.eq .validate_queen + cmp w0, #PIECE_ROOK + b.eq .validate_rook + cmp w0, #PIECE_BISHOP + b.eq .validate_bishop + cmp w0, #PIECE_KNIGHT + b.eq .validate_knight + cmp w0, #PIECE_PAWN + b.eq .validate_pawn + + mov w0, #0 + b .validate_done + +.validate_king: + // King moves one square in any direction + cmp w7, #1 + b.gt .validate_fail + cmp w8, #1 + b.gt .validate_fail + // Must move at least one square + orr w0, w7, w8 + cbz w0, .validate_fail + b .validate_ok + +.validate_queen: + // Queen moves like rook or bishop + cbz w7, .validate_ok // straight vertical + cbz w8, .validate_ok // straight horizontal + cmp w7, w8 + b.eq .validate_ok // diagonal + b .validate_fail + +.validate_rook: + // Rook moves straight + cbz w7, .validate_ok // vertical + cbz w8, .validate_ok // horizontal + b .validate_fail + +.validate_bishop: + // Bishop moves diagonally + cmp w7, w8 + b.eq .validate_ok + b .validate_fail + +.validate_knight: + // Knight moves in L-shape + cmp w7, #1 + b.ne .knight_check2 + cmp w8, #2 + b.eq .validate_ok + b .validate_fail +.knight_check2: + cmp w7, #2 + b.ne .validate_fail + cmp w8, #1 + b.eq .validate_ok + b .validate_fail + +.validate_pawn: + // Simplified pawn: moves forward 1 (or 2 from start), captures diagonal + // Note: not checking for captures properly, just basic movement + cbz w7, .pawn_forward + // Diagonal move (capture) + cmp w7, #1 + b.ne .validate_fail + cmp w8, #1 + b.ne .validate_fail + b .validate_ok + +.pawn_forward: + // Forward move + cmp w8, #1 + b.eq .validate_ok + cmp w8, #2 + b.ne .validate_fail + // Two squares only from starting rank + cmp w2, #1 // white start rank + b.eq .validate_ok + cmp w2, #6 // black start rank + b.eq .validate_ok + b .validate_fail + +.validate_ok: + mov w0, #1 + b .validate_done + +.validate_fail: + mov w0, #0 + +.validate_done: + ldp x29, x30, [sp], #16 + ret + +// setup_initial_board: Set up the starting chess position +// Input: x0 = pointer to game record +setup_initial_board: + stp x29, x30, [sp, #-16]! + mov x29, sp + stp x19, x20, [sp, #-16]! + + mov x19, x0 // game record ptr + + // White pieces (rank 0 = row 1) + // Rooks at a1, h1 + mov w0, #PIECE_ROOK + strb w0, [x19, #0] // a1 + strb w0, [x19, #7] // h1 + + // Knights at b1, g1 + mov w0, #PIECE_KNIGHT + strb w0, [x19, #1] // b1 + strb w0, [x19, #6] // g1 + + // Bishops at c1, f1 + mov w0, #PIECE_BISHOP + strb w0, [x19, #2] // c1 + strb w0, [x19, #5] // f1 + + // Queen at d1 + mov w0, #PIECE_QUEEN + strb w0, [x19, #3] // d1 + + // King at e1 + mov w0, #PIECE_KING + strb w0, [x19, #4] // e1 + + // White pawns at rank 1 (indices 8-15) + mov w0, #PIECE_PAWN + mov w1, #8 +.white_pawns: + strb w0, [x19, x1] + add w1, w1, #1 + cmp w1, #16 + b.lt .white_pawns + + // Black pieces (rank 7 = row 8, indices 56-63) + // Rooks at a8, h8 + mov w0, #PIECE_ROOK + orr w0, w0, #COLOR_BLACK + strb w0, [x19, #56] // a8 + strb w0, [x19, #63] // h8 + + // Knights at b8, g8 + mov w0, #PIECE_KNIGHT + orr w0, w0, #COLOR_BLACK + strb w0, [x19, #57] // b8 + strb w0, [x19, #62] // g8 + + // Bishops at c8, f8 + mov w0, #PIECE_BISHOP + orr w0, w0, #COLOR_BLACK + strb w0, [x19, #58] // c8 + strb w0, [x19, #61] // f8 + + // Queen at d8 + mov w0, #PIECE_QUEEN + orr w0, w0, #COLOR_BLACK + strb w0, [x19, #59] // d8 + + // King at e8 + mov w0, #PIECE_KING + orr w0, w0, #COLOR_BLACK + strb w0, [x19, #60] // e8 + + // Black pawns at rank 6 (indices 48-55) + mov w0, #PIECE_PAWN + orr w0, w0, #COLOR_BLACK + mov w1, #48 +.black_pawns: + strb w0, [x19, x1] + add w1, w1, #1 + cmp w1, #56 + b.lt .black_pawns + + ldp x19, x20, [sp], #16 + ldp x29, x30, [sp], #16 + ret + +// build_game_json: Build full JSON for a game +// Input: x0 = JSON context, x1 = game data ptr, w2 = game ID +build_game_json: + FRAME_ENTER 3 mov x19, x0 // JSON context - mov x20, x1 // hotel data - mov w21, w2 // hotel ID + mov x20, x1 // game data + mov w21, w2 // game ID JSON_OBJ_START x19 @@ -313,34 +783,158 @@ build_hotel_json: JSON_COMMA x19 - // "name": "" - JSON_KEY x19, key_name, 4 + // "status": "" + JSON_KEY x19, key_status, 6 + ldrb w0, [x20, #GAME_STATUS] + cmp w0, #STATUS_WAITING + b.eq .json_status_waiting + cmp w0, #STATUS_ACTIVE + b.eq .json_status_active + mov x0, x19 + ldr x1, =str_finished + mov x2, #8 + bl json_add_string + b .json_status_done +.json_status_waiting: + mov x0, x19 + ldr x1, =str_waiting + mov x2, #7 + bl json_add_string + b .json_status_done +.json_status_active: + mov x0, x19 + ldr x1, =str_active + mov x2, #6 + bl json_add_string +.json_status_done: - add x22, x20, #HOTEL_NAME_OFF - mov x0, x22 - bl strlen_simple + JSON_COMMA x19 - mov x2, x0 + // "next_turn": "white" or "black" + JSON_KEY x19, key_next_turn, 9 + ldrb w0, [x20, #GAME_NEXT_TURN] + cbz w0, .json_turn_white + mov x0, x19 + ldr x1, =str_black + mov x2, #5 + bl json_add_string + b .json_turn_done +.json_turn_white: mov x0, x19 - mov x1, x22 + ldr x1, =str_white + mov x2, #5 bl json_add_string +.json_turn_done: JSON_COMMA x19 - // "city": "" - JSON_KEY x19, key_city, 4 + // "move_count": + JSON_KEY x19, key_move_count, 10 + ldrh w0, [x20, #GAME_MOVE_COUNT] + mov w1, w0 + mov x0, x19 + bl json_add_int - add x22, x20, #HOTEL_CITY_OFF - mov x0, x22 - bl strlen_simple + JSON_COMMA x19 - mov x2, x0 + // "board": "<64-char string>" + JSON_KEY x19, key_board, 5 mov x0, x19 - mov x1, x22 - bl json_add_string + add x1, x20, #GAME_BOARD + bl json_add_board JSON_OBJ_END x19 + FRAME_LEAVE 3 + +// json_add_board: Add board as a 64-char string showing piece positions +// Input: x0 = JSON context, x1 = board pointer (64 bytes) +json_add_board: + FRAME_ENTER 2 + stp x23, x24, [sp, #-16]! + + mov x19, x0 // JSON context + mov x20, x1 // board ptr + + // Get buffer position + ldr x21, [x19, #0] // buffer ptr + ldr w22, [x19, #12] // current length + + // Add opening quote + mov w0, #'"' + strb w0, [x21, x22] + add w22, w22, #1 + + // Add 64 characters for board + mov w23, #0 // index +.board_loop: + ldrb w0, [x20, x23] // get piece + + // Convert piece to char + cbz w0, .piece_empty + and w1, w0, #0x0F // piece type + lsr w2, w0, #4 // color (0=white, 1=black) + + // Get base char for piece type + cmp w1, #PIECE_KING + b.eq .piece_king + cmp w1, #PIECE_QUEEN + b.eq .piece_queen + cmp w1, #PIECE_ROOK + b.eq .piece_rook + cmp w1, #PIECE_BISHOP + b.eq .piece_bishop + cmp w1, #PIECE_KNIGHT + b.eq .piece_knight + cmp w1, #PIECE_PAWN + b.eq .piece_pawn + b .piece_empty + +.piece_king: + mov w0, #'K' + b .piece_color +.piece_queen: + mov w0, #'Q' + b .piece_color +.piece_rook: + mov w0, #'R' + b .piece_color +.piece_bishop: + mov w0, #'B' + b .piece_color +.piece_knight: + mov w0, #'N' + b .piece_color +.piece_pawn: + mov w0, #'P' + +.piece_color: + // If black, convert to lowercase + cbz w2, .piece_store + add w0, w0, #32 // to lowercase + +.piece_store: + b .store_char + +.piece_empty: + mov w0, #'.' + +.store_char: + strb w0, [x21, x22] + add w22, w22, #1 + add w23, w23, #1 + cmp w23, #64 + b.lt .board_loop + + // Add closing quote + mov w0, #'"' + strb w0, [x21, x22] + add w22, w22, #1 + + // Update length + str w22, [x19, #12] + + ldp x23, x24, [sp], #16 FRAME_LEAVE 2 //============================================================================= @@ -350,32 +944,52 @@ build_hotel_json: key_id: .asciz "id" -key_name: - .asciz "name" -key_city: - .asciz "city" +key_status: + .asciz "status" +key_next_turn: + .asciz "next_turn" +key_move_count: + .asciz "move_count" +key_board: + .asciz "board" + +str_waiting: + .asciz "waiting" +str_active: + .asciz "active" +str_finished: + .asciz "finished" +str_white: + .asciz "white" +str_black: + .asciz "black" html_index: - .ascii "SlowAPI Hotel API" - .ascii "

SlowAPI Hotel API

" - .ascii "

A CRUD API for hotels, written in pure ARM64 assembly

" + .ascii "SlowAPI Chess" + .ascii "

SlowAPI Chess API

" + .ascii "

A chess game API written in pure ARM64 assembly

" .ascii "

Endpoints:

" .ascii "
    " - .ascii "
  • GET /api/hotels - List all hotels
  • " - .ascii "
  • GET /api/hotels/{id} - Get a specific hotel
  • " - .ascii "
  • POST /api/hotels - Create a hotel (body: name,city)
  • " - .ascii "
  • DELETE /api/hotels/{id} - Delete a hotel
  • " + .ascii "
  • GET /api/games - List all games
  • " + .ascii "
  • GET /api/games/{id} - Get game state
  • " + .ascii "
  • POST /api/games - Create game (body: W or B for color)
  • " + .ascii "
  • POST /api/games/{id}/join - Join a game
  • " + .ascii "
  • POST /api/games/{id}/move - Make move (body: e2e4)
  • " .ascii "
" + .ascii "

Board format:

" + .ascii "

64-char string, a1-h1, a2-h2, ..., a8-h8

" + .ascii "

Uppercase=white, lowercase=black, .=empty

" + .ascii "

K=king, Q=queen, R=rook, B=bishop, N=knight, P=pawn

" .ascii "

Try it:

" .ascii "
"
-    .ascii "# Create a hotel\n"
-    .ascii "curl -X POST -d 'Hilton,Toronto' http://localhost:8888/api/hotels\n\n"
-    .ascii "# List all hotels\n"
-    .ascii "curl http://localhost:8888/api/hotels\n\n"
-    .ascii "# Get a specific hotel\n"
-    .ascii "curl http://localhost:8888/api/hotels/1\n\n"
-    .ascii "# Delete a hotel\n"
-    .ascii "curl -X DELETE http://localhost:8888/api/hotels/1"
+    .ascii "# Create a game as white\n"
+    .ascii "curl -X POST -d 'W' http://localhost:8888/api/games\n\n"
+    .ascii "# Join game 1\n"
+    .ascii "curl -X POST http://localhost:8888/api/games/1/join\n\n"
+    .ascii "# Move pawn e2 to e4\n"
+    .ascii "curl -X POST -d 'e2e4' http://localhost:8888/api/games/1/move\n\n"
+    .ascii "# Get game state\n"
+    .ascii "curl http://localhost:8888/api/games/1"
     .ascii "
" .ascii "" html_index_end: @@ -383,3 +997,6 @@ html_index_end: .section .data html_index_len: .word html_index_end - html_index + +invite_counter: + .word 1000