{"id":15010105,"url":"https://github.com/ketupia/ecspanse_state_machine","last_synced_at":"2025-04-09T21:23:27.690Z","repository":{"id":225440694,"uuid":"766000738","full_name":"ketupia/ecspanse_state_machine","owner":"ketupia","description":null,"archived":false,"fork":false,"pushed_at":"2024-08-13T02:20:47.000Z","size":250,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-06T13:11:34.646Z","etag":null,"topics":["ecspanse","elixir","state-machine"],"latest_commit_sha":null,"homepage":"","language":"Elixir","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/ketupia.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}},"created_at":"2024-03-02T04:18:16.000Z","updated_at":"2024-12-04T08:59:44.000Z","dependencies_parsed_at":"2024-03-31T20:24:16.212Z","dependency_job_id":null,"html_url":"https://github.com/ketupia/ecspanse_state_machine","commit_stats":null,"previous_names":["ketupia/ecspanse_state_machine"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ketupia%2Fecspanse_state_machine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ketupia%2Fecspanse_state_machine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ketupia%2Fecspanse_state_machine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ketupia%2Fecspanse_state_machine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ketupia","download_url":"https://codeload.github.com/ketupia/ecspanse_state_machine/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248008631,"owners_count":21032556,"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":["ecspanse","elixir","state-machine"],"created_at":"2024-09-24T19:30:15.448Z","updated_at":"2025-04-09T21:23:27.666Z","avatar_url":"https://github.com/ketupia.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EcspanseStateMachine\n\n[![Hex Version](https://img.shields.io/hexpm/v/ecspanse_state_machine.svg)](https://hex.pm/packages/ecspanse_state_machine)\n![GitHub CI](https://github.com/ketupia/ecspanse_state_machine/actions/workflows/elixir.yml/badge.svg)\n[![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/ecspanse_state_machine)\n\n\u003c!-- [![License](https://img.shields.io/hexpm/l/ecspanse_state_machine.svg)](https://github.com/ketupia/ecspanse_state_machine/blob/72dd2045a8ca217b7b07529ac43780d0a3145e50/README.md) --\u003e\n\n\u003c!-- ![Elixir](https://img.shields.io/badge/dynamic/yaml?url=https://raw.githubusercontent.com/ketupia/ecspanse_state_machine/master/.github/workflows/elixir.yml\u0026query=$.jobs.build.steps[1].with[\"elixir-version\"]\u0026label=Elixir) --\u003e\n\n`ECSpanse State Machine` is a component level state machine implementation for [`ECSpanse`](https://hexdocs.pm/ecspanse). It is an Ecspanse component you include in your entities.\n\n[![](https://mermaid.ink/img/pako:eNpVkDEOwjAMRa8SeUTNwpgBCYmFgYkRM0SNaSOStEpdJFT1DNyFifNwAa5Amg6UJcp7_pFjD1A2hkCBlBIDW3akxNH61pHY7jFkjaFjzbSzuoray9t6UqfVWUi5EdY4wnCxVc2Zjf3DuTydGXNhwa3m2Dgl3o_n5_XAMPMyujDTq180Nco2fQQK8BS9tiZNMmAQAoFr8oSg0tXoeEXAMKac7rk53kMJimNPBfSt-Y0G6qJdlywZy008zKvJGxq_xZBnJQ?type=png)](https://mermaid.live/edit#pako:eNpVkDEOwjAMRa8SeUTNwpgBCYmFgYkRM0SNaSOStEpdJFT1DNyFifNwAa5Amg6UJcp7_pFjD1A2hkCBlBIDW3akxNH61pHY7jFkjaFjzbSzuoray9t6UqfVWUi5EdY4wnCxVc2Zjf3DuTydGXNhwa3m2Dgl3o_n5_XAMPMyujDTq180Nco2fQQK8BS9tiZNMmAQAoFr8oSg0tXoeEXAMKac7rk53kMJimNPBfSt-Y0G6qJdlywZy008zKvJGxq_xZBnJQ)\n\n## Features\n\n- Every entity can have a state machine executing simultaneously - create, start, and stop independently\n- Validation - all states must be defined and reachable\n- State changes on command or timeout\n- Observable: Events and Telemetry\n- Mermaid state diagram generation\n\n## Installation\n\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed\nby adding `ecspanse_state_machine` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:ecspanse_state_machine, \"~\u003e 0.3.3\"}\n  ]\nend\n```\n\n## How to Use\n\n1. [Systems Setup](#systems-setup)\n2. [Add a state machine](#add-a-state-machine)\n3. [Listen for state changes](#listen-for-state-changes)\n4. [Command a state change](#command-a-state-change)\n5. [Stopping a state machine](#stopping-a-state-machine)\n\n### Systems Setup\n\nAs part of your ESCpanse setup, you will have defined a `manager` with a `setup(data)` function. In that function, chain a call to `ESCpanseStateMachine.setup`\n\n```elixir\n  def setup(data) do\n    data\n    # register the state machine's systems\n    |\u003e EcspanseStateMachine.setup()\n\n    # Be sure to register the Ecspanse System Timer!\n    |\u003e Ecspanse.add_frame_end_system(Ecspanse.System.Timer)\n\n    # register your systems too\n```\n\nECSpanseStateMachine will add the systems it needs for you.\n\n### Add a state machine\n\nThe state machine is an ECSpanse component. You add it to your entity's spec in the components list. `EcspanseStateMachine.new` is a convenience API function to create the state machine component.\n\n1. Create a state machine component spec\n\n```elixir\n    state_machine =\n      EcspanseStateMachine.new(\n        :idle,\n        [\n          [name: :idle, exits: [:patrol, :fight], timeout: 5_000],\n          [name: :patrol, exits: [:fight, :idle], timeout: 10_000, default_exit: :idle],\n          [name: :fight, exits: [:idle, :die]],\n          [name: :die]\n        ]\n      )\n```\n\n2. Include the component in your entity\n\n```elixir\n    Ecspanse.Command.spawn_entity!({\n      Ecspanse.Entity,\n      components: [state_machine]\n    })\n```\n\n#### Defining States\n\nA state definition is a keyword list with the following keys. :name is the only required key but most states will also include :exits.\n\nStates that have at least one exit have a **default exit**. The default exit is the first exit in the :exits list unless specified by the :default_exit keyword.\n\nStates that have timeout will transition to the default exit. The Api provides a convenience function for transitioning to the default exit.\n\n- **:name** - A State must have a unique name (an atom or String).\n- **:exits** - Exits is a list of state names that can be transitioned to from this state. The majority of your states will have at least one value. Terminal states will not have any.\n- **:default_exit** - The state to transition to by default. The default exit must be in the list of exits.\n- **:timeout** - The number of milliseconds to be in this state before automatically transitioning to the default exit.\n\n##### Examples\n\n```elixir\n  # This is a terminal state since it has no exits.  The state machine will stop once it enters a terminal state.\n  [name: :die]\n\n  # The :fight state can transition to :idle or :die. You must call a transition function on the api to change from the :fight state since there is no :timeout.\n  [name: :fight, exits: [:idle, :die]],\n\n  # :idle can transition to :patrol or :fight.  You can use the api to transition to either state.\n  # After 5 seconds (the :timeout), the state will transition to :patrol.\n  # :patrol is the default exit state since it is first in the :exits list and :default_exit isn't specified\n  [name: :idle, exits: [:patrol, :fight], timeout: 5_000]\n\n  # :patrol can transition to :fight or :idle.  You can use the api to transition to either state.\n  # After 10 seconds (the :timeout), the state will transition to :idle.\n  # :idle is the default exit state since it is specified as the :default_exit.\n  [name: :patrol, exits: [:fight, :idle], timeout: 10_000, default_exit: :idle]\n```\n\n#### Starting your state machine\n\nThe default behavior is to automatically start a state machine. If you don't want that behavior, then you can 'set auto_start to false' and call `EcspanseStateMachine.start` when you're ready.\n\nAuto start is an option, the third parameter to EcspanseStateMachine.new(). Here's an example of turning off auto_start and then starting the state machine later.\n\n```elixir\n  state_machine =\n    EcspanseStateMachine.new(\n      :idle,\n      [\n        [name: :idle, exits: [:patrol, :fight], timeout: 5_000],\n        [name: :patrol, exits: [:fight, :idle], timeout: 10_000, default_exit: :idle],\n        [name: :fight, exits: [:idle, :die]],\n        [name: :die]\n      ],\n      auto_start: false\n    )\n\n  entity = Ecspanse.Command.spawn_entity!({\n    Ecspanse.Entity,\n    components: [ state_machine]\n  })\n\n  # some time later\n  EcspanseStateMachine.start(entity.id)\n```\n\n### Listen for state changes\n\nECSpanseStateMachine publishes `Started`, `Stopped`, and `StateChanged` events. State changed is the primary event. It's your chance to take action after a transition.\n\n```elixir\ndefmodule OnStateChanged do\n  use Ecspanse.System,\n    event_subscriptions: [EcspanseStateMachine.Events.StateChanged]\n\n  def run(\n        %EcspanseStateMachine.Events.StateChanged{\n          entity_id: entity_id,\n          from: from,\n          to: to,\n          trigger: _trigger\n        },\n        _frame\n      ) do\n      # respond to the transition\n  end\nend\n```\n\n### Command a state change\n\nState changes happen when a timeout elapses or upon request. Call `EcspanseStateMachine.transition` to trigger a transition.\n\n```elixir\n  EcspanseStateMachine.transition(entity_id, :fight, :idle)\n```\n\nHere were changing state from :fight to :idle.\n\n### Stopping a state machine\n\nThe state machine will automatically stop when it reaches a state no exits.\n\nYou can stop a state machine anytime by calling `EcspanseStateMachine.stop`.\n\n```elixir\n  EcspanseStateMachine.stop(entity_id)\n```\n\n## Telemetry\n\nECSpanse State Machine implements telemetry for the following events.\n\n| event name                         | measurement | metadata             | description                     |\n| ---------------------------------- | ----------- | -------------------- | ------------------------------- |\n| ecspanse_state_machine.start       | system_time | state_machine        | Executed on state machine start |\n| ecspanse_state_machine_stop        | duration    | state_machine        | Executed on state machine stop  |\n| ecspanse_state_machine.state.start | system_time | state_machine, state | Executed on entering a state    |\n| ecspanse_state_machine.state.stop  | duration    | state_machine, state | Executed on exiting a state     |\n\n## Mermaid State Diagrams\n\nECSpanseStateMachine generates [Mermaid.js](https://mermaid.js.org/) state diagrams for your state machines.\n\n```elixir\n  EcspanseStateMachine.format_as_mermaid_diagram(entity_id)\n```\n\nHere's an example output.\n\n```\n---\ntitle: Simple AI\n---\nstateDiagram-v2\n[*] --\u003e idle\nfight --\u003e die\nfight --\u003e idle\nidle --\u003e fight\nidle --\u003e patrol: ⏲️\npatrol --\u003e fight\npatrol --\u003e idle: ⏲️\ndie --\u003e [*]\n```\n\nWhich produces the following state diagram when rendered\n\n[![](https://mermaid.ink/img/pako:eNpVkD0OwjAMha8SeURkYcyAhMTCwMSIGSLilojErYKLhFDP0LswcR4uwBUI6UBZLH_Pz_LPHY6NIzCgtUYWL4GM2vnYBlKrDXKRL2KF1t7WyUZ9XSDvZwel9VJ5Fwi58vVJCjv_h2P5GwuWwoRbK6kJRr2Gx_s5II88tU6Ub9fPmgcVNS8Cc4iUovUuX3FHVgpBThQJweTU2XRGQO6zz3bS7G58BCOpozl0rfsdBqay4ZJVcl6atB3fUr7TfwD9smWR?type=png)](https://mermaid.live/edit#pako:eNpVkD0OwjAMha8SeURkYcyAhMTCwMSIGSLilojErYKLhFDP0LswcR4uwBUI6UBZLH_Pz_LPHY6NIzCgtUYWL4GM2vnYBlKrDXKRL2KF1t7WyUZ9XSDvZwel9VJ5Fwi58vVJCjv_h2P5GwuWwoRbK6kJRr2Gx_s5II88tU6Ub9fPmgcVNS8Cc4iUovUuX3FHVgpBThQJweTU2XRGQO6zz3bS7G58BCOpozl0rfsdBqay4ZJVcl6atB3fUr7TfwD9smWR)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fketupia%2Fecspanse_state_machine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fketupia%2Fecspanse_state_machine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fketupia%2Fecspanse_state_machine/lists"}