{"id":29573242,"url":"https://github.com/datavorous/dunefetch","last_synced_at":"2025-07-22T08:02:07.684Z","repository":{"id":304869414,"uuid":"1020320817","full_name":"datavorous/dunefetch","owner":"datavorous","description":"neofetch + falling sand engine for your terminal ","archived":false,"fork":false,"pushed_at":"2025-07-16T02:27:00.000Z","size":13,"stargazers_count":24,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-16T15:04:59.054Z","etag":null,"topics":["command-line","curses","neofetch","python","terminal"],"latest_commit_sha":null,"homepage":"","language":"Python","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/datavorous.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":"2025-07-15T17:22:08.000Z","updated_at":"2025-07-16T14:09:50.000Z","dependencies_parsed_at":"2025-07-16T21:09:12.870Z","dependency_job_id":"e001343a-068d-402d-a61b-5a9a6d7418f5","html_url":"https://github.com/datavorous/dunefetch","commit_stats":null,"previous_names":["datavorous/dunefetch"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/datavorous/dunefetch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datavorous%2Fdunefetch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datavorous%2Fdunefetch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datavorous%2Fdunefetch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datavorous%2Fdunefetch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/datavorous","download_url":"https://codeload.github.com/datavorous/dunefetch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datavorous%2Fdunefetch/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265893947,"owners_count":23845183,"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":["command-line","curses","neofetch","python","terminal"],"created_at":"2025-07-19T05:32:28.458Z","updated_at":"2025-07-19T05:32:37.244Z","avatar_url":"https://github.com/datavorous.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dunefetch\n\nneofetch + falling sand engine for your terminal \n\n![Preview](https://raw.githubusercontent.com/datavorous/datavorous/refs/heads/main/3j044t.png)\n\n\n[**Video Demo**](https://www.youtube.com/watch?v=tfzAKLan-mQ)\n\n\nFuture improvements will focus on customizability.\n\n\n# Install \n\nMake sure to have [Python](https://www.python.org/downloads/) installed.\n\nRecommended Terminal Emulator: [kitty](https://sw.kovidgoyal.net/kitty/binary/)\n\n```\ngit clone https://github.com/datavorous/dunefetch\ncd dunefetch-main\ncd dunefetch-main\npython -m venv .venv\nsource .venv/bin/activate\npip install .\ndunefetch\n```\n\n# Help\n\n```\ndunefetch --help\ndunefetch --help-controls\ndunefetch --version\n```\n\n| Key(s)       | Action                         |\n|--------------|--------------------------------|\n| Arrow Keys   | Move spawn cursor              |\n| `Space`      | Place selected material        |\n| `1`          | Select SAND                    |\n| `2`          | Select WATER                   |\n| `3`          | Select WALL                    |\n| `4`          | Select OIL                     |\n| `5`          | Select FIRE                    |\n| `6`          | Select PLANT                   |\n| `7`          | Select STEAM                   |\n| `8`          | Select WOOD                    |\n| `p`          | Pause/unpause simulation       |\n| `c`          | Clear particles near cursor    |\n| `r`          | Reset simulation               |\n| `q`          | Quit                           |\n\n\n# Explanation\n\n## Components\n\nThere are 3 important modules: \n\n1. `cursu` -\u003e visual I/O, color, character rendering\n2. `sand_core` -\u003e simulation logic, material specific behaviour\n3. `sand_utils` -\u003e grid setup, access helpers\n4. `elements` -\u003e pariticle definitions (name, symbol, color, index)\n\n`cursu` is a small wrapper around python's curses to provide an easier api for drawing elements on the terminal, more info can be found [here](https://docs.python.org/3/howto/curses.html).\n\n`sand_utils` handles grid creation, index validation, and get, set, swap cell values functionalities.\n\n`sand_core` is the main heart. The class SandCore inherits the properites from SandUtils class. \nIt contains the update rules for each type of element, allows adding particles, and updating them.\n\n```bash\n+----------------------+\n|   Terminal Display   |  -\u003e (cursu.py)\n+----------------------+\n|   Particle Engine    |  -\u003e (sand_core.py)\n+----------------------+\n|     Grid Manager     |  -\u003e (sand_utils.py)\n+----------------------+\n|   Material Database  |  -\u003e (elements.py)\n+----------------------+\n```\n\n## Data Structures\n\nThe grid is the main data structure around which everything revolves around. \n\n```py\nself.grid = [[EMPTY for x in range(width)] for y in range(height)]\n```\n\nIt is a 2D array of dimensions height x width; each cell is an integer containing the index number of the particle in `ELEMENTS`, viz.\n\n```py\nEMPTY = 0\nSAND = 1\nWATER = 2\n```\n\nAdditionally there is a life state buffer used for managing fire life time,\n\n```py\nself.life = [[0 for x in range(width)] for y in range(height)]\n```\n\n## Core Update Cycle\n\nEvery tick/frame, the engine runs the following:\n\n```py\nfor y in reversed(range(height)):\n    for x in range(width):\n        update_cell(y, x)\n```\n\nBottom up traversal prevents particles (which go downwar) to directly teleport at the bottom. Similarly for the particles of type `STEAM`, we do top down traveral, to prevent teleporting directly at the top.\n\nEach cell's type is checked, and corresponding update logic is run.\n\nThe system avoids out-of-bounds errors via `is_valid(y, x)` checks.\n\nEach particle has a set of local rules based on its neighborhood:\n\n```py\nneighbors = {\n    \"below\": (y+1, x),\n    \"left\": (y, x-1),\n    \"right\": (y, x+1),\n    \"below_left\": (y+1, x-1),\n    \"below_right\": (y+1, x+1),\n}\n```\nThen, based on the material type at y, x, we apply material-specific rules.\n\n## Particle Update Logic\n\nWe'll take the example of `SAND`, `WATER` and `FIRE` here.\n\n### Sand\n\nWe check three neighbouring cells (below, below_left, below_right), if there is any `EMPTY` cell, we swap that with our sand particle.\n```py\nif cell_below == EMPTY:\n    swap(y, x, y+1, x)\nelif below_left == EMPTY:\n    swap(y, x, y+1, x-1)\nelif below_right == EMPTY:\n    swap(y, x, y+1, x+1)\n```\n\nThis gives those natural looking piles or _dunes_.\n\n### Water \n\nThese flow downward, then sideways, and seeks equilibrium (spreads horizontally). We added randomness to prevent gridlocked water. \nAdditionally, checking column wise water level would be another idea to make it more natural, but we are yet to try that out.\n\n```py\nif below == EMPTY:\n    swap(y, x, y+1, x)\nelif left == EMPTY and right == EMPTY:\n    swap with random(left or right)\nelif left == EMPTY:\n    swap(y, x, y, x-1)\nelif right == EMPTY:\n    swap(y, x, y, x+1)\n```\n\n### Fire\n\nFire is perhaps the hardest one to implement. It burns for a few frames and spreads to flammable neighbors. Eventually leaves empty cell behind.\nThe life time grid is updated accordingly.\n\n```py\n    def update_fire(self, y, x):\n        if self.life[y][x] \u003c= 0:\n            self.set(y, x, EMPTY)\n            return False\n\n        for dy, dx in [(-1, -1), (-1, 1), (1, -1), (1, 1), (-1,  0), (1,  0), (0, -1), (0,  1)]:\n            # checking the neighbours\n            target = self.get(y + dy, x + dx)\n            if target == WATER:\n                # changing the particle type upon interaction\n                self.set(y + dy, x + dx, STEAM)\n                \n            if target in (OIL, PLANT):\n                self.set(y+dy, x+dx, FIRE)\n                # giving fire a full life or a boosted one\n                self.life[y+dy][x+dx] = self.max_fire_life + 2\n                \n            if target == WOOD:\n                if random.random() \u003c 0.7:\n                    self.set(y+dy, x+dx, FIRE)\n                    self.life[y+dy][x+dx] = self.max_fire_life + 5\n                    # wood burns longer hence this\n        \n        # occasionally it can leap two cells, the more randomised the more chaotic\n        if random.random() \u003c 0.0001:\n            dy, dx = random.choice([(-2,0), (2,0), (0,-2), (0,2)])\n            ny, nx = y + dy, x + dx\n            if 0 \u003c ny \u003c self.height-1 and 0 \u003c nx \u003c self.width-1:\n                if self.get(ny, nx) == EMPTY:\n                    self.set(ny, nx, FIRE)\n                    self.life[ny][nx] = self.max_fire_life\n                    \n        decay = random.randint(1, 2)\n        self.life[y][x] = max(0, self.life[y][x] - decay)\n\n        return True\n```\n\nThe `sand_core.py` is pretty much self explanatory, do check out the code for a better grasp of the concepts.\n\n## Interaction\n\nTotally dependent upon keys(pressed) matching.\n\n## Extras\n\nWe had tried to develop a sand based game 4 years ago (but failed, unfortunately), and had watched/read these videos/articles:\n\n1. [MARF's Youtube Video](https://youtu.be/5Ka3tbbT-9E?si=vabzB_Z2n9OEhH2E) :: _How To Code a Falling Sand Simulation (like Noita) with Cellular Automata_\n\n2. [Winterdev's Youtube Video](https://youtu.be/wZJCQQPaGZI?si=o7YyMqOzug5BFUx9) :: _Making games with Falling Sand part 1_\n\n3. [John Jackson's Youtube Video](https://youtu.be/VLZjd_Y1gJ8?si=lecmiGLE74tPjtsf) :: _Recreating Noita's Sand Simulation in C and OpenGL | Game Engineering_\n\nSome recent additions:\n\n4. [Jason's Blog](https://jason.today/falling-fire) :: _Adding fire to our falling sand simulator_\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatavorous%2Fdunefetch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatavorous%2Fdunefetch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatavorous%2Fdunefetch/lists"}