{"id":21368291,"url":"https://github.com/chrisvilches/card-game-kernel","last_synced_at":"2025-03-16T08:12:46.979Z","repository":{"id":94022548,"uuid":"147780143","full_name":"ChrisVilches/Card-Game-Kernel","owner":"ChrisVilches","description":"A flexible engine for creating card games (Pokemon, Magic, etc) where the behavior of each card can be completely arbitrary.","archived":false,"fork":false,"pushed_at":"2018-09-11T19:00:57.000Z","size":154,"stargazers_count":2,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-22T20:29:46.534Z","etag":null,"topics":["card-game","design-patterns","extensible","framework","game"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ChrisVilches.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2018-09-07T06:24:05.000Z","updated_at":"2021-09-07T21:42:16.000Z","dependencies_parsed_at":"2023-03-13T17:08:14.472Z","dependency_job_id":null,"html_url":"https://github.com/ChrisVilches/Card-Game-Kernel","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChrisVilches%2FCard-Game-Kernel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChrisVilches%2FCard-Game-Kernel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChrisVilches%2FCard-Game-Kernel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ChrisVilches%2FCard-Game-Kernel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ChrisVilches","download_url":"https://codeload.github.com/ChrisVilches/Card-Game-Kernel/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243841217,"owners_count":20356446,"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","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":["card-game","design-patterns","extensible","framework","game"],"created_at":"2024-11-22T07:23:39.871Z","updated_at":"2025-03-16T08:12:46.960Z","avatar_url":"https://github.com/ChrisVilches.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Card Game Kernel\n\n\u003c!-- toc --\u003e\n\n- [Introduction](#introduction)\n  * [The problem](#the-problem)\n  * [The solution](#the-solution)\n- [Proof of concept](#proof-of-concept)\n  * [When card A is present, prevent the opponent from using cards of type B](#when-card-a-is-present-prevent-the-opponent-from-using-cards-of-type-b)\n  * [Increment a card's counter every turn](#increment-a-cards-counter-every-turn)\n  * [Attack the opponent's card](#attack-the-opponents-card)\n  * [Pushing a state that makes the application change its course (must be handled by the application logic)](#pushing-a-state-that-makes-the-application-change-its-course-must-be-handled-by-the-application-logic)\n- [API Example](#api-example)\n  * [Global data store](#global-data-store)\n  * [Containers](#containers)\n  * [Cards](#cards)\n  * [Hooks](#hooks)\n  * [Triggering events](#triggering-events)\n- [Install](#install)\n- [Tests](#tests)\n\n\u003c!-- tocstop --\u003e\n\n## Introduction\n\nA flexible engine for creating card games (Pokemon, Magic, etc) where the behavior of each card can be completely arbitrary.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"magic.jpg\" width=\"256\" title=\"Magic card game\"\u003e\n\u003c/p\u003e\n\n\n### The problem\n\nCard games like Pokemon, Magic, Mitos y Leyendas, etc. are very difficult to code because every card can have any arbitrary behavior designed to be\nunderstood verbally by a human being (and therefore difficult to understand for computers).\n\n### The solution\n\nThis little software contains a framework where it's possible to design and code nearly any card game you can imagine. Some of the features:\n\n* Cards have **events**, which are the main mechanism of communicating between cards, and achieving the desired behavior. Events can be triggered from many places, and a lot of customization is possible.\n* Cards are divided into **containers**, which is a generic way of dividing a deck into your hand, the opponent's hand, disposed cards, etc. Each card of the entire set of cards present in the game fall in one container.\n* Containers can be nested, and they can be found by their path, e.g. `[:player1, :hand]` or `[:player2, :cemetery]`\n* It manages data and state globally using Redux (or a similar data store).\n* Attributes can be added dynamically to cards.\n* Events can have `pre` and `post` hooks so you can control the execution with detail.\n* Generic, unopinionated solution.\n* It can also be used for games that require a similar mechanism, like Final Fantasy or Pokemon battle systems, which require to implement many techniques and movements which behavior differs significantly from each other and there's no standard reusable model.\n\nThis is a very low level software, and therefore in order to work with it, further layers of abstraction must be written by the developer. This engine doesn't even force you to implement the concept of `turns` or `deck`. You simply have a few data structures that communicate with each other, and you must write the rest of the logic yourself.\n\nSince this software is a low level framework, one class has to be created for each different card (usually by inheriting from the base `Card` class). In order to make it easier to work with, a wrapper can be written in a similar way ORMs like Hibernate or ActiveRecord help developers work easily with databases. However, this project won't include a wrapper, since the goal is to keep it low level.\n\n## Proof of concept\n\nIn order to prove this software can achieve a flexible way of developing any card game, where cards can have any imaginable behavior, a few proof of concept test cases were developed. Some of these were coded as tests and can be found in the following link.\n\n[Code of some of these examples](spec/proof_of_concept)\n\n### When card A is present, prevent the opponent from using cards of type B\n\nEach card has a `transfer` event that executes whenever a card is transferred from one card container to another. In this case, the first container, and the next container could for instance be the attacking line, where you place all the cards that are going to attack the opponent.\n\nOnce the card is transferred from the first container to the next, the `transfer` event is executed, and then optionally validate that the card is in the correct container, and then setup a `pre` hook for the `transfer` event where you only focus on the opponent's containers. In this case, this `pre` hook will return `false` in case the opponent is trying to transfer a card from one container to another (for example again, from his hand to his attacking line) and therefore achieving the objective.\n\nOnce the card that originated the `pre` hook is transferred away, the hook may be eliminated.\n\n### Increment a card's counter every turn\n\nFirst, no card has any default `counter` attribute, so it has to be added dynamically. Then create a `new_turn` event, which when triggered, a `pre` hook will increment the counter inside the card.\n\nThere are many ways to trigger the event only for the card you want. One way is to setup the hook globally, and then execute it for all cards inside a container, or also it's possible to setup a hook only for one card.\n\n### Attack the opponent's card\n\nMake a card respond to a `receive_attack` event. Since triggered events can also have arguments, a `damage` attribute can also be included.\n\nThis event can be triggered from many places, and it depends on how you want to establish your application's logic.\n\n*Bonus:* If you include the attacking card reference as part of the event arguments, you'll have access to that card from the card that receives damage, so you could also create something along the lines of:\n* If A receives damage by B, it will counterattack with 10% of the total damage.\n* If A dies (HP=0) while being attacked by B, B also dies (by triggering one of B's events).\n* More.\n\n### Pushing a state that makes the application change its course (must be handled by the application logic)\n\nEach turn has several stages, `stage1`, `stage2`, `stage3`, and so on. Suddenly, something happens and a card has an event triggered, and within the event handler, it pushes a new state onto the history stack. This state makes the game change its course, and one of the players is now forced to withdraw a card from his deck. The game will only continue once he's done with this.\n\nThere's a global data store (Redux is encouraged, although any can be used as long as it supplies the correct interface), and this can store any kind of data you want. In this case we need a state stack.\n\nSince cards can communicate with the global data, a card can directly push a new state onto the stack. The next part consists of handling each state by applying user defined logic, and this can be done *from outside* this framework. In other words, the user must use this as a library and build on top of it by adding logic. If we change the state from `stage3` to `choosing_card`, it's the duty of the user to implement, let's say, a GUI menu that shows every possible card to pick, and when it's done, pop the state and go back to `stage3`.\n\nIf the card that triggered this state change wants to limit or filter out some cards (for example, choosing cards that are stronger than 120 is prohibited), we can also use the global data store and set a predicate (lambda function) there, so it can be accessed and used from outside. Just make sure the application logic knows about that predicate, so it can find and use it.\n\n## API Example\n\n### Global data store\n\nLet's create a global data store using the Rydux gem. You can create your own data store as long as it implements the methods defined in the `DataStore` class.\n\n```ruby\nclass PlayerReducer \u003c Rydux::Reducer\n  def self.map_state(action, state = { gold: 0, level: 1, bonus: 0 })\n    case action[:type]\n    when :increment_gold\n      gold = state[:gold]\n      state.merge(gold: gold + 1)\n    when :decrement_gold\n      gold = state[:gold]\n      state.merge(gold: gold - 1)\n    when :level_up\n      level = state[:level]\n      bonus = state[:bonus]\n      bonus = bonus + 1 if level % 10 == 0 # Bonus +1 each time it goes 10 levels up\n      state.merge(level: level + 1, bonus: bonus)\n    else\n      state\n    end\n  end\nend\n\n# And then\n\nclass MyDataStorage \u003c DataStore\n\n  def initialize\n    @store = Rydux::Store.new(player1: PlayerReducer, player2: PlayerReducer)\n  end\n\n  def set_data(action:, arguments: {})\n    @store.dispatch(type: action, payload: arguments)\n    get_data_lambda = lambda { Store.state }\n  end\n\n  def get_data\n    {\n      player1: @store.player1,\n      player2: @store.player2\n    }    \n  end\nend\n```\n\n### Containers\n\nHere we'll make some card containers. All the cards in the game are separated into containers, and these can be nested.\n\n```ruby\nkernel = CardKernel.new\n\nkernel.create_container [:player1]\nkernel.create_container [:player1, :hand]\nkernel.create_container [:player1, :deck]\n\nkernel.create_container [:player2]\nkernel.create_container [:player2, :hand]\nkernel.create_container [:player2, :deck]\n\nkernel.create_container [:shared_cards]\n```\n\nLet's now add some cards to the containers.\n\n```ruby\n\ncontainer = kernel.create_container [:player1, :hand]\n\n# Use the data storage class we created before\n\nmy_data = MyDataStorage.new\n\ncontainer.add_card(Card.new(id: 1, data_store: my_data))\ncontainer.add_card(Card.new(id: 2, data_store: my_data))\ncontainer.add_card(Card.new(id: 3, data_store: my_data))\n```\n\n### Cards\n\nThe `Card` class can be overridden and you can define detailed behavior by registering events (and its handlers) to which this class of cards will react to.\n\nHere we create a type of card that can receive damage.\n\n```ruby\nclass AttackerCard \u003c Card\n\n  def initialize(id:)\n    super(id: id)\n\n    # This card has a custom attribute, health points (HP)\n    set_attributes({ hp: 100 })\n\n    # Event handler for when it receives an attack.\n    # We register the event by its name, and then define an event handler for when it's triggered.\n    on(:receive_attack, lambda { |args|\n\n      # Decrement its health points (HP)\n      current_hp = self.attributes[:hp]\n      set_attributes({ hp: current_hp - args[:damage] })\n\n      # Counter attack by triggering the same event on the card that attacked first\n      if args.has_key?(:can_counterattack) \u0026\u0026 args[:can_counterattack] == true\n        args[:attacker_card].trigger_event(event: :receive_attack,\n          arguments: {\n            damage: 3,\n            can_counterattack: false # If this is true, it'd become an infinite loop\n          })\n      end\n\n      return {\n        current_hp: self.attributes[:hp]\n      }\n    })\n\n  end\nend\n```\n\nOf course instances of this newly created class can also be added to containers.\n\n### Hooks\n\nHooks are functions that execute before and after the main event handler. This allows you to control more precisely what happens before and after an event. You can do things like blocking an event from happening, or doing some pre-processing logic that will change the way the main event handler behaves.\n\nHooks can be configured at a global scope, and a per card scope. Once an event is triggered, the order in which these execute is as follows.\n\n```\nglobal pre hook → card pre hook → main event handler → global post hook → card post hook\n```\n\nCreating a global hook. This hook will be executed for each card that has the `transfer` event triggered. Since it was\nregistered using the `pre` symbol, it will execute before the main event handler for `transfer`. Also note that `transfer` is\na special event that executes automatically (no need to execute `card.trigger_event(...)`) when a card is moved from one container\nto another.\n\n```ruby\nglobal_hooks = GlobalHooks.new\ncard = Card.new(id: 1, global_hooks: global_hooks)\n\nlambda_hook = lambda { |args|\n\n  # The transfer event will contain the \"prev_container\" and the \"next_container\" attributes in its argument object.\n  # \"prev_container\" won't be included if it's the first time the card is put into a container.\n  # The \"card\" attribute will be the card where the hooks are executing.\n  if (!args[:prev_container].nil? \u0026\u0026 args[:prev_container].id == [:b]) \u0026\u0026 args[:next_container].id == [:b, :c] \u0026\u0026 args[:card].type == :my_type\n    return false\n  end\n\n  return true\n}\n\n# \"card_owner_id\" is optional, as some global hooks don't need to be associated with any card in particular.\nglobal_hooks.append_hook(:pre, event_name: :transfer, fn: lambda_hook, card_owner_id: 1)\n```\n\nCreating a card scoped hook. Similarly, you can register a lambda function as a `pre` hook inside a card.\nIt will execute only for instances of this card class.\n\n```ruby\nclass PreHookCard \u003c Card\n  def initialize(id:)\n    super(id: id)\n    @pre[:transfer] = lambda { |args_|\n      # ...\n    }\n  end\nend\n```\n\n\n### Triggering events\n\nTrigger an event for one card.\n\n```ruby\ncard_instance.trigger_event(event: :event_name_goes_here, arguments: { arg1: 0, arg2: \"hoge\", arg3: \"piyo\" })\n```\n\nTrigger an event for all cards in a container.\n\n```ruby\ncontainer_instance.trigger_event(event: :event_name_goes_here, arguments: { arg1: 0, arg2: \"hoge\", arg3: \"piyo\" }, recursive: false)\n```\n\nNote the `recursive` argument when triggering an event in a container. If it's `false`, it will only trigger the event for all cards inside that container, but will not trigger it for the cards inside the nested containers. If it's `true`, the event will be triggered for every card in the container and all cards in every container nested to it.\n\n## Install\n\nInstall gems using the following command.\n\n```bash\nbundle\n```\n\n## Tests\n\nTests can be found at the `spec` folder, and can be run all at once by executing the following command (it needs to `gem install rspec` first).\n\n```bash\nrspec spec/\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisvilches%2Fcard-game-kernel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrisvilches%2Fcard-game-kernel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisvilches%2Fcard-game-kernel/lists"}