Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions chess-game/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.vscode/
.idea/
*.log
.DS_Store
*.swp
*.bak
*.tmp
15 changes: 3 additions & 12 deletions chess-game/server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

val appModuleName = "chess"
val dbModuleName = "chessDB"
val logicModuleName = "chessLogic"

val webApp = project(":webapp");
val buildDir = layout.buildDirectory.get()
Expand All @@ -25,24 +24,16 @@ tasks.register("build") {

tasks.register<Exec>("compileAppModule") {
val libDir = "${rootProject.projectDir}/lib"
val srcModule = "$projectDir/main/x/$appModuleName.x"
val srcModule = "$projectDir/chess/main/x/$appModuleName.x"
val resourceDir = "${webApp.projectDir}"

dependsOn("compileDbModule", "compileLogicModule")
dependsOn("compileDbModule")

commandLine("xcc", "--verbose", "-o", buildDir, "-L", buildDir, "-r", resourceDir, srcModule)
}

tasks.register<Exec>("compileDbModule") {
val srcModule = "${projectDir}/main/x/$dbModuleName.x"
val srcModule = "${projectDir}/chessDB/main/x/$dbModuleName.x"

commandLine("xcc", "--verbose", "-o", buildDir, srcModule)
}

tasks.register<Exec>("compileLogicModule") {
val srcModule = "${projectDir}/main/x/$logicModuleName.x"

dependsOn("compileDbModule")

commandLine("xcc", "--verbose", "-o", buildDir, "-L", buildDir, srcModule)
}
71 changes: 71 additions & 0 deletions chess-game/server/chess/main/x/chess.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Chess Game Server Module
*
* This module implements a web-based chess game server using the XTC web framework.
* It provides a RESTful API for managing chess games with both single-player (vs AI)
* and online two-player multiplayer modes.
*
* Key features:
* - Turn-based chess gameplay with simplified rules (no castling, en-passant, or check detection)
* - Single-player mode with automated opponent (Black player) with AI-driven move selection
* - Online multiplayer mode with room-based matchmaking
* - Game state persistence using the chess database schema
* - RESTful API endpoints for moves, game state, room management, and game reset
* - Static content serving for the web client interface
*/
@WebApp
module chess.examples.org {
// Package imports: organize dependencies from different modules
package db import chessDB.examples.org; // Database schema and data models
package web import web.xtclang.org; // Web framework for HTTP handling

// Import specific web framework components
import web.*;
// Import database schema and models
import db.ChessSchema;
import db.GameRecord;
import db.GameMode;
import db.GameStatus;
import db.Color;
import db.OnlineGame;
import db.ChatMessage;

// ===== Chat API Response Types =====

/**
* ChatMessageResponse - API response format for a single chat message
*/
const ChatMessageResponse(String playerId,
String playerColor,
String? message = Null,
Int timestamp);

/**
* ChatHistoryResponse - API response containing chat messages
*/
const ChatHistoryResponse(Boolean success,
String? error,
ChatMessageResponse[] messages = []);

/**
* SendMessageResponse - API response after sending a message
*/
const SendMessageResponse(Boolean success,
String? error,
String? message = Null);

/**
* SendMessageRequest - API request body for sending a message
*/
const SendMessageRequest(String? message = Null);

/**
* Home Service
*
* Serves the static web client (HTML, CSS, JavaScript) for the chess game.
* All requests to the root path "/" are served with the index.html file
* from the public directory.
*/
@StaticContent("/static", /public/index.html)
service Home {}
}
112 changes: 112 additions & 0 deletions chess-game/server/chess/main/x/chess/BoardUtils.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import db.Color;

/**
* Board Utilities Service
* This module provides basic board operations:
* - Coordinate system and algebraic notation
* - Board cloning and manipulation
* - Square validation and color detection
*/
service BoardUtils {
// ----- Constants -------------------------------------------------

static Int BOARD_SIZE = 8;
static Int FILE_STEP = 1;
static Int RANK_STEP = 8;
static Char FILE_MIN = 'a';
static Char FILE_MAX = 'h';
static Char RANK_MIN = '1';
static Char RANK_MAX = '8';
static Int INVALID_SQUARE = -1;

// ----- Algebraic Notation -------------------------------------------------

/**
* Parse algebraic notation (e.g., "e4") to board index.
*/
static Int parseSquare(String square) {
if (square.size != 2) {
return INVALID_SQUARE;
}
Char file = square[0];
Char rank = square[1];
if (file < FILE_MIN || file > FILE_MAX || rank < RANK_MIN || rank > RANK_MAX) {
return INVALID_SQUARE;
}
Int fileIdx = (file - FILE_MIN).toInt();
Int rankIdx = (RANK_MAX - rank).toInt();
return rankIdx * RANK_STEP + fileIdx;
}

/**
* Convert board index to algebraic notation.
*/
static String toAlgebraic(Int index) {
Int fileIdx = index % BOARD_SIZE;
Int rankIdx = index / BOARD_SIZE;
Char file = (FILE_MIN.toInt() + fileIdx).toChar();
Char rank = (RANK_MAX.toInt() - rankIdx).toChar();
return $"{file}{rank}";
}

// ----- Board Operations -------------------------------------------------

/**
* Clone the board to a mutable array.
*/
static Char[] cloneBoard(String board) {
Char[] mutable = new Char[64](i -> board[i]);
return mutable;
}

/**
* Convert board string to array of 8 row strings.
*/
static String[] boardRows(String board) {
String[] rows = new Array<String>(8, i -> board[i * 8 ..< (i + 1) * 8]);
return rows;
}

/**
* Get the color of a piece character.
* Lowercase = Black, Uppercase = White
*/
static Color colorOf(Char piece) {
return piece >= 'a' && piece <= 'z' ? Color.Black : Color.White;
}

/**
* Check if a square index is valid.
*/
static Boolean isValidSquare(Int index) {
return index >= 0 && index < 64;
}

/**
* Get file (column) index from square index.
*/
static Int getFile(Int index) {
return index % BOARD_SIZE;
}

/**
* Get rank (row) index from square index.
*/
static Int getRank(Int index) {
return index / BOARD_SIZE;
}

/**
* Calculate distance between two squares (max of file/rank distance).
*/
static Int getDistance(Int from, Int to) {
Int fromFile = getFile(from);
Int fromRank = getRank(from);
Int toFile = getFile(to);
Int toRank = getRank(to);
Int fileDist = (fromFile - toFile).abs();
Int rankDist = (fromRank - toRank).abs();
return fileDist.maxOf(rankDist);
}
}

156 changes: 156 additions & 0 deletions chess-game/server/chess/main/x/chess/ChatApi.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* ChatApi Service
*
* RESTful API service for online chat functionality in multiplayer chess games.
* Provides endpoints for:
* - Sending chat messages in a game room
* - Retrieving chat history for a room
*
* All operations require a valid room code and player ID for authentication.
*/
@WebService("/api/chat")
service ChatApi {
// Injected dependencies
@Inject ChessSchema schema;
@Inject Clock clock;

/**
* POST /api/chat/send/{roomCode}/{playerId}
*
* Sends a chat message to the specified room.
*
* @param roomCode The room code identifying the game
* @param playerId The player's session ID
* @param request The request body containing the message
* @return SendMessageResponse indicating success or failure
*/
@Post("send/{roomCode}/{playerId}")
@Produces(Json)
SendMessageResponse sendMessage(String roomCode, String playerId, @BodyParam SendMessageRequest request) {
using (schema.createTransaction()) {
// Verify the room exists
if (OnlineGame game := schema.onlineGames.get(roomCode)) {
// Verify the player is in this room
if (!game.hasPlayer(playerId)) {
return new SendMessageResponse(False, "You are not a player in this room", Null);
}

// Get player's color
Color? color = game.getPlayerColor(playerId);
if (color == Null) {
return new SendMessageResponse(False, "Could not determine player color", Null);
}

// Validate message content
String trimmed = request.message.trim();
if (trimmed.size == 0) {
return new SendMessageResponse(False, "Message cannot be empty", Null);
}
if (trimmed.size > 500) {
return new SendMessageResponse(False, "Message too long (max 500 characters)", Null);
}

// Create and store the chat message
Int timestamp = clock.now.milliseconds;
ChatMessage msg = new ChatMessage(roomCode, playerId, color, trimmed, timestamp);
String msgKey = $"{roomCode}_{timestamp}_{playerId}";
schema.chatMessages.put(msgKey, msg);

return new SendMessageResponse(True, Null, "Message sent successfully");
}
return new SendMessageResponse(False, "Room not found", Null);
}
}

/**
* GET /api/chat/history/{roomCode}/{playerId}?limit={number}
*
* Retrieves chat message history for the specified room.
* Only returns messages from the current room.
*
* @param roomCode The room code identifying the game
* @param playerId The player's session ID
* @param limit Optional limit on number of messages (default: 100)
* @return ChatHistoryResponse with array of messages
*/
@Get("history/{roomCode}/{playerId}")
@Produces(Json)
ChatHistoryResponse getHistory(String roomCode, String playerId, @QueryParam("limit") Int limit = 100) {
using (schema.createTransaction()) {
// Verify the room exists
if (OnlineGame game := schema.onlineGames.get(roomCode)) {
// Verify the player is in this room
if (!game.hasPlayer(playerId)) {
return new ChatHistoryResponse(False, "You are not a player in this room", []);
}

// Filter messages for this room and convert to response format
ChatMessageResponse[] messages = new Array<ChatMessageResponse>();
Int count = 0;

// Iterate through all chat messages and filter by room code
for (ChatMessage msg : schema.chatMessages.values) {
if (msg.roomCode == roomCode) {
String colorName = msg.playerColor == White ? "White" : "Black";
messages.add(new ChatMessageResponse(
msg.playerId,
colorName,
msg.message,
msg.timestamp
));
count++;
if (count >= limit) {
break;
}
}
}

return new ChatHistoryResponse(True, Null, messages.freeze(inPlace=True));
}
return new ChatHistoryResponse(False, "Room not found", []);
}
}

/**
* GET /api/chat/recent/{roomCode}/{playerId}/{since}
*
* Retrieves chat messages sent after a specific timestamp.
* Used for polling to get only new messages.
*
* @param roomCode The room code identifying the game
* @param playerId The player's session ID
* @param since Timestamp (milliseconds) - only return messages after this time
* @return ChatHistoryResponse with array of new messages
*/
@Get("recent/{roomCode}/{playerId}/{since}")
@Produces(Json)
ChatHistoryResponse getRecent(String roomCode, String playerId, Int since) {
using (schema.createTransaction()) {
// Verify the room exists
if (OnlineGame game := schema.onlineGames.get(roomCode)) {
// Verify the player is in this room
if (!game.hasPlayer(playerId)) {
return new ChatHistoryResponse(False, "You are not a player in this room", []);
}

// Filter messages for this room that are newer than 'since'
ChatMessageResponse[] messages = new Array<ChatMessageResponse>();

for (ChatMessage msg : schema.chatMessages.values) {
if (msg.roomCode == roomCode && msg.timestamp > since) {
String colorName = msg.playerColor == White ? "White" : "Black";
messages.add(new ChatMessageResponse(
msg.playerId,
colorName,
msg.message,
msg.timestamp
));
}
}

return new ChatHistoryResponse(True, Null, messages.freeze(inPlace=True));
}
return new ChatHistoryResponse(False, "Room not found", []);
}
}
}
Loading