{"id":17280166,"url":"https://github.com/akuli/catris","last_synced_at":"2026-03-03T15:04:05.861Z","repository":{"id":36995665,"uuid":"454029631","full_name":"Akuli/catris","owner":"Akuli","description":"The classic game of falling blocks, but with online multiplayer","archived":false,"fork":false,"pushed_at":"2025-05-02T17:37:30.000Z","size":665,"stargazers_count":1,"open_issues_count":25,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-02T18:32:19.053Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://catris.net","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Akuli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-01-31T14:05:35.000Z","updated_at":"2025-05-02T17:37:33.000Z","dependencies_parsed_at":"2025-02-04T20:26:33.719Z","dependency_job_id":"7b4bf414-8467-4625-9b3a-d21021af2234","html_url":"https://github.com/Akuli/catris","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"purl":"pkg:github/Akuli/catris","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Akuli%2Fcatris","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Akuli%2Fcatris/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Akuli%2Fcatris/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Akuli%2Fcatris/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Akuli","download_url":"https://codeload.github.com/Akuli/catris/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Akuli%2Fcatris/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30050222,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T14:38:37.398Z","status":"ssl_error","status_checked_at":"2026-03-03T14:38:06.721Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T09:19:37.602Z","updated_at":"2026-03-03T15:04:05.841Z","avatar_url":"https://github.com/Akuli.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# catris\n\nThis is a Tetris clone for multiple players that connect to a server with netcat or a web interface.\nYou can play it here: https://catris.net/\n\n![Screenshot](screenshot.png)\n\nMy server is in Europe, so the game may be very laggy if you're not in Europe.\nPlease create an issue if this is a problem for you.\n\n\n## Architecture\n\nI bought `catris.net` from namecheap and connected it to a Linode server that has:\n- nginx (configuration is in `catris-nginx-site`, serves files from `web-ui/`)\n- the rust program (binary copied to server with `scp`, started and stopped with `catris.service`)\n- HTTPS setup done with LetsEncrypt and certbot\n\nThe rust program listens on two TCP ports,\n54321 for websocket connections and 12345 for plain TCP connections (e.g. netcat).\nWhen a client connects, the server auto-detects what kind of terminal is used\nby sending special escape sequences that terminals respond to automatically.\nCurrently [ANSI-compatible terminals](https://en.wikipedia.org/wiki/ANSI_escape_code)\nand [VT52-compatible terminals](https://en.wikipedia.org/wiki/VT52) are supported.\n\nThe javascript code in `web-ui/` behaves like an ANSI-compatible terminal.\nThis makes the rust code simpler and the javascript code more complicated,\nwhich is good because the javascript code is very simple and short\ncompared to the rust program.\n\nWhen running locally, the javascript code in `web-ui/` connects a websocket to port 54321.\nOn `catris.net`, it instead connects to port 443 (the default https port),\nand nginx proxies the connection to port 54321 on the server.\nIn fact, in `catris.net` the server listens to websocket connections only on localhost.\nThis has several advantages:\n- You can play catris if you have a firewall that only allows port 443 out.\n- The rust program doesn't need special privileges to listen on port 443.\n- I don't have to configure the rust code to look at the certificate files, because nginx does that.\n- If there is a security bug in the websocket libraries I use (unlikely in rust),\n    it may be impossible to exploit through nginx\n    because nginx parses and validates the structure of each request.\n- This seems to be a common design that many people are familiar with.\n\n\n## High-level overview of the rust code\n\nWhen the rust program starts, `main()` starts listening for connections.\nAfter a client connects, it mostly doesn't matter whether they use\na websocket connection or a plain TCP connection,\nas `connection.rs` abstracts the differences away.\n\nThe purpose of `ip_tracker.rs` is to limit the number of simultaneous connections for each IP address\nand to log IPs that spam the server with many connections.\nThis way most IP addresses are stored only in RAM (which is needed anyway),\nand not written to disk, as many users don't want me to know their IP address.\nThe IP address passed to `ip_tracker.rs` is determined in `connection.rs`:\nit uses a header named `X-Real-IP` (set in the nginx configuration) for proxied websocket connections,\nand otherwise uses the IP that the connection to the rust program came from.\nTo keep track of how many clients are currently connected from each IP,\n`ip_tracker.rs` also returns a token object that is dropped when a client disconnects.\n\nNext a `Client` object is created.\nIt is possible to receive and (indirectly) send through a `Client` object.\nSpecifically, `connection.rs` provides a method to receive a single key press,\nand the `Client` object re-exposes it.\nFor sending, the `Client` has a `RenderData`.\nInstead of sending bytes with `connection.rs`,\nyou usually set the `RenderData`'s `RenderBuffer` to what you want the user to see,\nand then fire a `Notify` which causes a task in `main.rs` to actually send screen updates.\nOnly the changes are sent, the entire screen isn't redrawn every time.\n\nNext:\n- We ask the client's name.\n- We ask whether the client wants to create a lobby or join an existing lobby.\n- If the client wants to join an existing lobby, we ask its ID and join it.\n- In the lobby, the client chooses a game.\n- The client plays the game, using `ingame_ui.rs` to keep the `RenderBuffer` up to date.\n\nEach item in the above list is a function in `views.rs`.\nThese functions take the `Client` as an argument, and send and receive through it.\n\nClients own their lobbies: a lobby is dropped automatically when all of its clients disconnect.\nThe lobby also knows about what clients it has, but it only contains `ClientInfo` objects,\nnot actual `Client` objects.\nUnlike `Client` objects, the `ClientInfo` objects can't be used to send or receive;\nthey are purely information for game logic and other clients.\n\nA lobby owns `GameWrapper`s, which take care of the timing and async aspects of a game.\nEach `GameWrapper` has an underlying `Game` object from `game_logic/game.rs`,\nand like the rest of the `game_logic/` folder, it's purely game logic without any async or IO.\nFor example, there are several async functions in `game_wrapper.rs`\nthat call methods of `Game` repeatedly\nto e.g. move the blocks down or decrement counters on bombs.\n\nInside the `game_logic/` folder,\nthe `Game` object has `Player`s, and each `Player` has a `FallingBlock`.\nFalling blocks and landed squares use `SquareContent` objects,\nwhich usually define the color of a square,\nbut they can also be a special bomb or drill square.\n\nWhen a player's block lands, the player gets a new block.\nThe block fails to land if it doesn't fit within the visible part of the game.\nWhen that happens, the player has to wait 30 seconds before they can continue playing.\nDuring that time, the player has a counter instead of a `FallingBlock`.\nThe game ends when all players are waiting simultaneously.\n\nWhen the game ends, the `GameWrapper` records the game results by calling a function in `high_scores.rs`,\nand sets the `GameWrapper`'s status so that `views.rs` notices it and displays the high scores.\nWhen the client is done with looking at high scores, they go back to choosing a game.\n\n\n## Development\n\nYou need to install rust (the compiler, not the game). Just google for some instructions.\nI'm on a Debian-based linux distro, so I first tried `sudo apt install cargo`,\nbut it was too old and I had to use `rustup` instead.\nYou might have better luck with your distro's package manager\nif you're reading this a few years after I wrote this\nor if you're using a different distro.\n\nOnce you have rust, you can start the server as you would expect:\n\n```\n$ git clone https://github.com/Akuli/catris\n$ cd catris\n$ cargo r\n```\n\nWhen the server is running, you can connect to it with netcat:\n\n```\n$ stty raw; nc localhost 12345; stty cooked\n```\n\nIf you want to develop the web UI, you need to run a web server in a separate terminal.\nYou need to have Python installed for this (or you could use some other web server instead).\nIf you're on Windows, use `py` instead of `python3`.\n\n```\n$ cd catris/web-ui\n$ python3 -m http.server\n```\n\nYou can then open `http://localhost:8000/` in your web browser.\n\nOther commands (these also run on GitHub Actions):\n- Formatter: `cargo fmt`\n- Linter: `cargo clippy`\n- Using `xterm` as a VT52 terminal emulator: `xterm -ti vt52 -tn vt52`\n\n\n## Deploying\n\nThese instructions are mostly for me.\nIf you want to run catris in a local network, see [local-playing.md](local-playing.md).\nIf you run a bigger catris server, please let me know by creating an issue :)\n\n```\n$ your_favorite_editor Cargo.toml   # Edit version number\n$ ./deploy1.sh  # Safe\n$ ./deploy2.sh  # Interrupts ongoing games\n```\n\nCurrently I run `./deploy2.sh` when nobody is currently playing.\nIf in the future there is always someone playing,\nuse `/home/catris/catris_motd.txt` to clearly announce the update beforehand.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakuli%2Fcatris","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fakuli%2Fcatris","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakuli%2Fcatris/lists"}