{"id":26678950,"url":"https://github.com/alakra/ticket-to-ride-core","last_synced_at":"2025-10-13T19:18:44.045Z","repository":{"id":48307500,"uuid":"145496015","full_name":"alakra/ticket-to-ride-core","owner":"alakra","description":"Ticket to Ride (Core Game Implementation)","archived":false,"fork":false,"pushed_at":"2021-08-02T12:15:24.000Z","size":167,"stargazers_count":8,"open_issues_count":4,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-12T10:40:19.718Z","etag":null,"topics":["10k","boardgame","elixir","multiplayer","otp","server"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":false,"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/alakra.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}},"created_at":"2018-08-21T02:33:00.000Z","updated_at":"2025-03-18T21:12:55.000Z","dependencies_parsed_at":"2022-08-24T04:20:29.420Z","dependency_job_id":null,"html_url":"https://github.com/alakra/ticket-to-ride-core","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/alakra/ticket-to-ride-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alakra%2Fticket-to-ride-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alakra%2Fticket-to-ride-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alakra%2Fticket-to-ride-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alakra%2Fticket-to-ride-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alakra","download_url":"https://codeload.github.com/alakra/ticket-to-ride-core/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alakra%2Fticket-to-ride-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279016892,"owners_count":26085884,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["10k","boardgame","elixir","multiplayer","otp","server"],"created_at":"2025-03-26T05:17:45.861Z","updated_at":"2025-10-13T19:18:44.032Z","avatar_url":"https://github.com/alakra.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ticket To Ride (Core Implementation, No Network)\n\n[![Build Status](https://travis-ci.org/alakra/ticket-to-ride-core.svg?branch=master)](https://travis-ci.org/alakra/ticket-to-ride-core)\n\nThis is an implementation of Ticket to Ride (the American version) in\nElixir. It not only has the mechanics of the board game, but also\nmanages player registration, session management and delegation of\nplayer actions to the correct game states.\n\nNOTE: This is a work in progress and not all goals have been met\nyet. When this message disappears, then it'll be done. :)\n\n### Why?\n\nThere are so many implementations of Ticket To Ride. Why build another\none? For me, it was about finding the limits of a new programming\nlanguage with a familiar board game.\n\nBoard games, in general, are good candidates for implementations\nbecause their feature sets can really push the limits of a language's\ndata structures, expressiveness and ability to model a domain in a\nhierarchy of code.\n\nMost board games have the following features:\n\n* They have to mantain state.\n* They have physical domain which makes them easier to reason about.\n* They have rules.\n* They (usually) have more than 1 player.\n* They have secrets.\n* They have limited effects due to randomness.\n\nBut the real draw for me was to build a proof-of-concept game server\nthat could manage the state of thousands of turn-based games on a\nsingle server without going to great lengths to configure the\nserver.\n\nThe Erlang BEAM is supposed to be really good at solving this sort of\nproblem and I wanted to leverage that strength. I wanted to do this\nwithout thinking about networking and transport-layer security, so I\nfocused on state management and scaling the game to over 10k\nconcurrent games.\n\n### What I Have Learned From This Project\n\nMy original implementation tried to do too much (core gameplay, state,\nnetworking and a terminal client), so I scaled it back to focus on the\ncore gameplay and managing state.\n\nI learned that:\n\n* Domain-Driven APIs are much easier to debug when you constrain\n  yourself to contexts (see [Phoenix Contexts](https://hexdocs.pm/phoenix/contexts.html#content))\n* `typespecs` will tell you how simple or insane your APIs are in\n  Elixir. Write them for every public function.\n* Not all domains are physical. New ones will appear as you see\n  consistent patterns on data manipulation.\n* Defining documentation on modules that are in the first-level of the\n  domain is really important. It's even more important to actually\n  render them using `mix docs` and expand the `Functions` drop-down\n  for every top-level domain module. It reveals a lot about what you\n  can do with a module. Verb-Noun structure (e.g. `get_card/2`) is\n  simple and clear.\n* Good tests make a foundation not for correctness, but for\n  identifying what effects your code changes will produce.\n* Not to return `nil` from functions. I won't do it. I will always try\n  to return something unambiguous.\n\n## Features\n\n* Only five top-level domains: `Board`, `Cards`, `Games`, `Mechanics` and `Players`\n* User registration (username and password)\n* Separation of player contexts vs complete state\n* Ownership is tied to the first player to join a new game. Will transfer to next player if first player leaves, etc.\n* Session management (provided concept for future implementations over a network to validate actions before hitting the core the API)\n* Can scale just over 10k concurrent games on a small virtual machine (1 cpu, 1 GB ram)\n* Single-time sliced timer for all turns (turns are limited to 60 seconds)\n* Randomly chooses which player goes first\n\n## Non-Features\n\n* No graphical UI\n* No persistent database\n* No network support\n* No transport-level security\n\nThese things belongs elsewhere.\n\n## Installation and Setup\n\n### Requirements\n\n* You must have `elixir` \u003e= `1.9` and `erlang` \u003e= `22`\n* You must have `git` installed\n* You must be running 64-bit linux or macos (\u003e= high sierra)\n\n### Installation\n\n```shell\ngit clone https://github.com/alakra/ticket-to-ride-core.git\ncd ticket-to-ride-core\nmix deps.get\nmix compile\n```\n\nThen start it up:\n\n```shell\niex -S mix\n```\n\n### Runbook\n\nYou can run the following in the `IEx` console. If you need more\ncontext, see the [official rules](https://www.daysofwonder.com/tickettoride/en/usa) for the game.\n\nMake sure you alias first:\n\n```elixir\nalias TtrCore.{\n  Board,\n  Games,\n  Players\n}\n\nalias TtrCore.Board.Route\nalias TtrCore.Mechanics.Context\n```\n\n#### Game Setup\n\n```elixir\n{:ok, user_id_a} = Players.register(\"playerA\", \"p@ssw0rd!\")\n{:ok, user_id_b} = Players.register(\"playerB\", \"p@ssw0rd!\")\n\n# NOTE: `login/2` is also supported with sessions, but only makes sense in the context of a network.\n\n{:ok, game_id, _pid} = Games.create(user_id_a)\n\n:ok = Games.join(game_id, user_id_b)\n:ok = Games.setup(game_id, user_id_a)\n\n{:ok, %{tickets_buffer: tickets_a}} = Games.get_context(game_id, user_id_a)\n:ok = Games.select_tickets(game_id, user_id_a, tickets_a)\n\n{:ok, %{tickets_buffer: tickets_b}} = Games.get_context(game_id, user_id_b)\nGames.select_tickets(game_id, user_id_b, tickets_b)\n\n:ok = Games.begin(game_id, user_id_a)\n```\n\n#### Game Turns\n\n##### Find out who goes first\n\n```elixir\n{:ok, context_a} = Games.get_context(game_id, user_id_a)\n{:ok, context_b} = Games.get_context(game_id, user_id_b)\n\nstarting_context = Enum.find([context_a, context_b], fn c -\u003e\n    c.current_player == c.id\nend)\n```\n\nLet's assume `user_id_a` gets to go first. Possible actions are\ndetailed in the following sections.\n\nAlso, make sure you get the latest context after each operation:\n\n```elixir\n{:ok, context_a} = Games.get_context(id, user_id_a)\n```\n\n##### Claim a route\n\n```elixir\ncontexts = [context_a, context_b]\nclaimed = Enum.flat_map(contexts, fn %{routes: routes} -\u003e routes end)\nroutes = Board.get_claimable_routes(claimed)\n\n# Take a look at your routes, then make sure selections\n\ntrain = hd(context_a.trains)\nroute_to_claim = {Seattle, Vancouver, 1, :any}\n\n:ok = Games.claim_route(game_id, user_id_a, route_to_claim, train)\n```\n\n##### Select trains from the display\n\n```elixir\n\n# Find out what trains are on display\n\n%Context{displayed_trains: displayed} = context_a\n[first|_] = displayed\n\n# Select the first train\n\n:ok = Games.select_trains(game_id, user_id_a, [first])\n```\n\n##### Draw trains from the deck\n\n```elixir\n%Context{train_deck: number_in_deck} = context_a\n\n# Draw a train if there are trains in the deck (can draw up to 2)\n\nif number_in_deck \u003e 0 do\n  :ok = Games.draw_trains(game_id, user_id_a, 1)\nend\n```\n\n##### Draw tickets from the deck\n\n```elixir\n:ok = Games.draw_tickets(game_id, user_id_a)\n```\n\n##### Select tickets that you have drawn\n\n```elixir\n%Context{tickets_buffer: buffer} = context_a\n:ok = Games.select_tickets(game_id, user_id_a, buffer)\n```\n\n#### Ending the Game\n\nThe game will automatically end when the context struct reports that\nthe key `:stage` has a value of `:finished`. The keys `winner_id` will\ncontain the `user_id` of the player who won the game.\n\n## License\n\nThis software is licensed under the [MIT License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falakra%2Fticket-to-ride-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falakra%2Fticket-to-ride-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falakra%2Fticket-to-ride-core/lists"}