back to scratko.xyz
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md151
-rw-r--r--server/server.c71
-rw-r--r--server/server.h3
3 files changed, 173 insertions, 52 deletions
diff --git a/README.md b/README.md
index da73c73..d503d67 100644
--- a/README.md
+++ b/README.md
@@ -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 {