diff options
-rw-r--r-- | README.md | 151 | ||||
-rw-r--r-- | server/server.c | 71 | ||||
-rw-r--r-- | server/server.h | 3 |
3 files changed, 173 insertions, 52 deletions
@@ -3,32 +3,102 @@ <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. + +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. + +## 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 + +The game supports dynamic throw-ins based on real-time player reactions — +whoever throws first, their card is accepted. + +## Hint system + +Players receive visual hints based on their role: + +- 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 + +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 | ++-----------------+ +``` -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. - -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. - -There are hints to help players. - - * 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. - -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. +## 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 @@ -41,25 +111,28 @@ make ./client ``` -Note: for Android, you need to use a terminal like Termux. After installing it, you should to install packages: -clang, make, git. Then follow the steps above. +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> -### Use for your server - - * Build the server executable file. -Run the file on the command line, specify a free port to be used by the server (from 1024 to 65535) as the second argument. -Example: `./server 1025` - * Open the linux_client/client.c (or windows_client/client.c) file with any text editor. - * On line 14, change the ip from `109.107.161.30` to `%server ip-address%` (either `127.0.0.1` if you want to test on a local -computer). - * On line 15 in the same file, correct the port to the one specified above. - * Build the client file and run it without arguments. +### 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). --- -If you have any questions, please contact me: m@scratko.xyz
\ No newline at end of file +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 { |