diff options
-rw-r--r-- | README.md | 136 | ||||
-rw-r--r-- | server/server.c | 71 | ||||
-rw-r--r-- | server/server.h | 3 | ||||
-rw-r--r-- | windows_client/client.c | 8 | ||||
-rw-r--r-- | windows_client/client.h | 3 |
5 files changed, 180 insertions, 41 deletions
@@ -3,44 +3,136 @@ <img src="durak.png" /> ## Description +This is a network-based console version of the classic Russian card game +"Podkidnoy Durak" (Throw-in Fool). Both the client and server are written in +pure C. -Network game. Client and server are written in pure C. -For network communication the socket system with TCP/IP protocol is used. Due to this it is possible to use streaming through read and write system calls, and there is a guarantee that the data will arrive in strict order. -The client and server are written in an event-driven programming model. Events are selected using the select system call, thus multiplexing I/O. +TCP/IP sockets are used for network communication, enabling reliable data +transfer via `read` and `write` system calls. The client and server follow an +event-driven programming model using `select()` for I/O multiplexing. -GUI: console version. -Card suits are labeled as %, #, v, ^. -Card ranks: 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, J, Q, K, A. -There are several stages: attacking, defending, and tossing cards. Unlike classic Fool's game, here you can toss cards depending on the reaction of the players --- whoever has time to toss a card, that card goes on the table. +## Gameplay +- Card suits: `%`, `#`, `v`, `^` +- Card ranks: `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`, `J`, `Q`, `K`, `A` +- Game phases: attack, defense, and throw-in -There are hints to help players. +The game supports dynamic throw-ins based on real-time player reactions — +whoever throws first, their card is accepted. - * When attacking: those cards that have the same ranks (or all of them). - * When defending: those cards that can be used to beat back the attacking card on the table (higher rank, or with a trump suit). - * When tossing: those cards that have the same rank. +## Hint system -It is possible to give up defense and accept attacking cards -- then press enter. It is also possible to discard the toss -- also press enter. -The server analyzes whether the defender can fight back. If so, the server analyzes whether the defender can fight back. The defender is shown one card at a time (the others see all cards), which he must defeat. If the server decides that the defender does not have any cards that can be used to fight off the attacking cards, then the tossing phase is analyzed. If the other players (except the defender, of course) have cards with the same rank as on the table, then the player moves to the tossing phase, otherwise the player moves to a new attack phase. +Players receive visual hints based on their role: -## Building +- Attacker: all cards of matching rank +- Defender: cards that beat the attacking one (higher of same suit or trump) +- Thrower: cards of the same rank as those already on the table -### On Linux (MacOS, FreeBSD, Android) +Players may press `Enter` to either surrender the defense or skip the throw-in phase. +The server analyzes each phase: + +- Determines whether the defender can beat the attack +- If defense fails, transitions to the throw-in phase +- If no throw-in is possible, moves to the next attack phase + +During defense, the defender sees only one attacking card at a time (others see all cards). + +## Game State Machine (ASCII Diagram) + +```text ++-----------------+ +| ATTACK | +| (attacker plays)| ++--------+--------+ + | + v ++-----------------+ +| DEFENSE | +| (defender tries | +| to beat cards)| ++--------+--------+ + | + v ++----------------------------+ +| CAN OTHERS THROW-IN? | +| (same rank as on table) | ++------+----------+----------+ + |YES |NO + v \ ++-----------------+ \ +| THROW-IN QUEUE | \ +| | \ +| - If defender | \ +| already failed| \ +| ≥1 card → | v +| put all queued| +----------------------------+ +| cards on table| | DEFENSE RESULT? | +| → check again | | - SUCCESS: defender becomes | +| | | next attacker | +| - If still | | - FAIL: player after | +| defending well| | defender attacks next | +| → throw 1 card| +----------------------------+ +| → DEFENSE | ++-----------------+ ``` -git clone https://git.scratko.xyz/durak -cd client -``` -To connect to the current game server (scratko.xyz), do the following: - - open the client.c file with any text editor. - - In line 14, change the ip from `127.0.0.1` to `109.107.161.30` +## Features & Technologies + +### Technologies + +- C programming language — for both client and server +- POSIX sockets — TCP/IP networking +- `select()` system call — event-driven I/O +- Cross-platform — Linux, Windows, Android (via Termux) +- Finite State Machine (FSM) for game phase control +- Session tracking for each connected client +- Separate adapted Windows client derived from the Linux version + +### Gameplay Features +- Full support for "Podkidnoy Durak" rules: attack, defense, throw-in +- Real-time interaction: whoever throws a card first wins the throw-in +- Turn-based logic, respecting the rules of defending, beating, or taking cards +- Hint system based on player role: + - Attacker — cards of matching rank + - Defender — suitable beating cards + - Thrower — cards matching any on the table +- Game logic transparency — only defender sees current attack card; others see all +- Supports multiple clients connected to the same server in real time +- Automatic phase switching based on server logic without client-side decision delays + +## Building + +### On Linux (MacOS, FreeBSD, Android) ``` +git clone https://git.scratko.xyz/durak +cd durak/linux_client make ./client ``` +Note: On Android, use a terminal like `Termux`, and install packages: `clang`, `make`, `git`. + ### On Windows -You can download the built version for the Windows platform (it already contains the IP address for connecting to this server). +You can download a prebuilt Windows version with the server IP preset: Windows version: <a href="https://scratko.xyz/games/durak.exe" target="_blank">Download</a> + +### Hosting Your Own Server + +1. Build and run the server: + ``` + ./server 1025 + ``` + Choose a free port (≥1024). + +2. Configure the client: + Open `linux_client/client.c` or `windows_client/client.c`: + - Line 14: set your server IP address (e.g., 127.0.0.1 for local) + - Line 15: set the same port used above + +3. Build and run the client (no arguments required). + +--- + +Questions? Contact: m@scratko.xyz diff --git a/server/server.c b/server/server.c index 0017888..bfc71ef 100644 --- a/server/server.c +++ b/server/server.c @@ -15,10 +15,17 @@ #include "card_stack.h" #include "card_queue.h" +static volatile sig_atomic_t timeout_is_over = 0; + void handler(int s) { - if(s == SIGPIPE) - signal(SIGPIPE, handler); + switch(s) { + case SIGPIPE: + signal(SIGPIPE, handler); + break; + case SIGALRM: + timeout_is_over = 1; + } } static void init_session(struct session *new_session, struct sockaddr_in *from, @@ -98,8 +105,7 @@ static void close_connection(struct server *serv) static int check_playable_player_number(struct server *serv) { - return serv->connected_players_counter >= 2 && - serv->connected_players_counter < 8; + return serv->connected_players_counter >= 2; } static int is_max_playable_player_number(struct server *serv) @@ -593,10 +599,21 @@ static void determine_server_state(struct server *serv) switch(serv->state) { case no_players: serv->state = first_player; + /* + * The second player must connect no later than TIMEOUT. + * This is done to protect against bots. + */ + signal(SIGALRM, handler); + alarm(timeout); break; case first_player: - if(check_playable_player_number(serv)) + if(check_playable_player_number(serv)) { serv->state = confirmation_waiting; + /* cancel alarm */ + alarm(0); + /* default disposition */ + signal(SIGALRM, SIG_DFL); + } break; case confirmation_waiting: set_record_status_for_all(serv); @@ -658,7 +675,7 @@ static int new_player_tracking_condition(enum server_states state) /* * when accepting new players */ -static int server_state_change_condition(struct server *serv) +static int check_server_state_change_conditions_before_game(struct server *serv) { return serv->state == no_players || @@ -678,7 +695,7 @@ static int accept_new_player(struct server *serv) } /* update info about connected players */ set_record_status_for_all(serv); - if(server_state_change_condition(serv)) + if(check_server_state_change_conditions_before_game(serv)) serv->change_server_state = 1; return 1; } @@ -736,7 +753,10 @@ static void set_up_player_tracking(struct server *serv, fd_set *readfds, } } -static int check_server_state_change_conditions(struct server *serv) +/* + * is checked after a write or read operation in make_data_transfer + */ +static int check_server_state_change_conditions_in_game(struct server *serv) { switch(serv->state) { case start_game: @@ -763,7 +783,7 @@ static int check_server_state_change_conditions(struct server *serv) static void make_data_transfer(struct server *serv, fd_set *readfds, fd_set *writefds) { - int i, result, close_connection_status = 0; + int i, result = 1, close_connection_status = 0; for(i = 0; i < serv->max_sess_arr_size; ++i) { if(!serv->sess_arr[i]) continue; @@ -840,13 +860,18 @@ static void make_data_transfer(struct server *serv, fd_set *readfds, close(i); serv->sess_arr[i]->fd = -1; close_connection_status = 1; - result = 1; /* next can be reading that's not ready */ + /* + * Flag reset + * The following clients may not have requested read or + * write operations + */ + result = 1; } } if(serv->state == start_game || serv->state == end_round || serv->state == end_game) sleep(2); - if(check_server_state_change_conditions(serv)) + if(check_server_state_change_conditions_in_game(serv)) serv->change_server_state = 1; /* connection is closed */ if(close_connection_status) { @@ -856,6 +881,23 @@ static void make_data_transfer(struct server *serv, fd_set *readfds, } } +static void close_bot_connection(struct server *serv) +{ + int i; + + for(i = 0; i < serv->max_sess_arr_size; ++i) { + if(!serv->sess_arr[i]) + continue; + close(serv->sess_arr[i]->fd); + serv->sess_arr[i]->fd = -1; + free(serv->sess_arr[i]); + serv->sess_arr[i] = NULL; + } + serv->change_server_state = 0; + serv->connected_players_counter = 0; + serv->state = no_players; +} + int main_loop(struct server *serv) { int maxfd, accept_result, select_result; @@ -875,8 +917,13 @@ int main_loop(struct server *serv) set_up_player_tracking(serv, &readfds, &writefds, &maxfd); select_result = select(maxfd + 1, &readfds, &writefds, NULL, NULL); - if(select_result == -1) + if(select_result == -1 && !timeout_is_over) return 3; + if(timeout_is_over && serv->connected_players_counter == 1) { + close_bot_connection(serv); + timeout_is_over = 0; + continue; + } if(FD_ISSET(serv->listen_socket, &readfds)) { accept_result = accept_new_player(serv); diff --git a/server/server.h b/server/server.h index a62f085..3841470 100644 --- a/server/server.h +++ b/server/server.h @@ -7,7 +7,8 @@ enum { max_buffer_size = 4096, listen_qlen = 32, - init_sess_arr_size = 12 /* it was 32 */ + init_sess_arr_size = 12, /* it was 32 */ + timeout = 30 }; enum { diff --git a/windows_client/client.c b/windows_client/client.c index 521dd0f..0388e34 100644 --- a/windows_client/client.c +++ b/windows_client/client.c @@ -211,14 +211,14 @@ static int fill_in_buffer(struct client *cl, int *data_size, /* the data has not yet been entered up to the newline character */ if(!end_status && input_status) { for(; (key = _getch()) != '\r'; ) { - if(key == back && buffer_idx > 0) { + if(key == backspace && buffer_idx > 0) { --buffer_idx; cl->buffer[buffer_idx] = '\0'; - putchar(back); + putchar(backspace); putchar(' '); - putchar(back); + putchar(backspace); continue; - } else if(key == back) + } else if(key == backspace) continue; cl->buffer[buffer_idx] = key; ++buffer_idx; diff --git a/windows_client/client.h b/windows_client/client.h index f3b1ce8..060afcd 100644 --- a/windows_client/client.h +++ b/windows_client/client.h @@ -8,8 +8,7 @@ enum { max_cot_arr_size = 18, card_size = 4, max_cq_arr_size = 5, - backspace = 127, - back = 8 + backspace = 8 }; enum client_states { |