first commit

This commit is contained in:
tcsenpai 2025-03-30 22:46:27 +02:00
commit 6aeae4fdfd
14 changed files with 2630 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
logs
bun.lockb

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 tcsenpai (https://github.com/tcsenpai)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

94
README.md Normal file
View File

@ -0,0 +1,94 @@
# Fluffles Simulator
## Overview
Fluffles Simulator is an evolutionary simulation that models a population of small creatures called "Fluffles" in a dynamic ecosystem. The simulation demonstrates natural selection, genetic inheritance, and emergent behaviors as Fluffles compete for resources, reproduce, and adapt to their environment.
## Features
- **Genetic Evolution**: Fluffles inherit traits from their parents with slight mutations, allowing populations to evolve over time.
- **Complex Behaviors**: Fluffles make decisions based on their needs, genetic traits, and environmental conditions.
- **Life Cycle**: Fluffles are born, mature, reproduce, and eventually die of old age or other causes.
- **Predation**: Hungry or aggressive Fluffles may attack others to survive.
- **Environmental Challenges**: Natural disasters like earthquakes, droughts, disease, and cold snaps create evolutionary pressure.
- **Terrain Variety**: The world contains grass (food), water, and rocks, each affecting movement and survival.
- **Population Dynamics**: Overcrowding creates pressure that affects reproduction and resource availability.
## Genetic Traits
Fluffles have various genetic traits that influence their behavior and survival:
- **Size**: Affects energy consumption and appearance
- **Speed**: Determines movement efficiency and hunting/escape success
- **Metabolism**: Controls energy usage and hunger rate
- **Intelligence**: Affects decision-making quality
- **Aggression**: Influences tendency to attack other Fluffles
- **Friendship**: Counteracts aggression and promotes social behavior
- **Social Behavior**: Determines desire to be near other Fluffles
- **Reproductive Urge**: Affects frequency of mating attempts
- **Lifespan**: Determines maximum age
- **Maturity Age**: Sets when a Fluffles can reproduce
- **Fur Color**: Visual trait with no survival impact
## Controls
- **Left/Right Arrows**: Select previous/next animal
- **H**: Toggle help screen
- **+/-**: Increase/decrease simulation speed
- **Q** or **Esc**: Quit the simulation
## Visual Indicators
- **F, f**: Adult Fluffles (different sizes)
- **•**: Baby Fluffles (not mature yet)
- **♥**: Fluffles that are mating
- **♣, ♠, ·**: Grass (different densities)
- **≈**: Water
- **▲**: Rocks/Mountains
## Life Cycle
1. Fluffles are born as babies (•)
2. They mature around age 25 (varies by genetics)
3. They can mate only when mature (takes 4 turns)
4. Their lifespan is around 100 (varies by genetics)
5. Population pressure affects reproduction success
## Technical Details
The simulation is built with TypeScript and uses the Blessed library for terminal-based UI. It implements:
- A genetic system with inheritance and mutation
- A tile-based world with different terrain types
- A decision-making system based on needs and traits
- Environmental challenges and disasters
- Detailed logging and statistics
**NOTE:** This has been coded using Claude 3.7 Sonnet as a guide and is a fun project to play around with. Don't take it too seriously.
## Running the Simulation
To run the simulation:
1. Make sure you have Node.js and Bun installed
2. Clone the repository
3. Run `bun install` to install dependencies
4. Run `bun start` to start the simulation
Logs are saved to the `logs` directory for later analysis.
## Development
The codebase is organized into several key components:
- **Animal**: Base class for all creatures
- **Fluffles**: Implementation of the specific animal type
- **DNA**: Handles genetic traits, inheritance and mutation
- **World**: Manages the terrain, resources, and population
- **Simulation**: Controls the overall simulation flow
- **Renderer**: Handles the terminal-based UI
- **Logger**: Records events for analysis
## License
This project is licensed under the MIT License. See the LICENSE file for details.

143
bun.lock Normal file
View File

@ -0,0 +1,143 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "animal-simulator",
"dependencies": {
"blessed": "^0.1.81",
"blessed-contrib": "^4.11.0",
"chalk": "^5.3.0",
},
"devDependencies": {
"bun-types": "latest",
},
},
},
"packages": {
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
"@types/node": ["@types/node@22.13.14", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w=="],
"@types/ws": ["@types/ws@8.18.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw=="],
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
"ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="],
"ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="],
"ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="],
"ansi-term": ["ansi-term@0.0.2", "", { "dependencies": { "x256": ">=0.0.1" } }, "sha512-jLnGE+n8uAjksTJxiWZf/kcUmXq+cRWSl550B9NmQ8YiqaTM+lILcSe5dHdp8QkJPhaOghDjnMKwyYSMjosgAA=="],
"ansicolors": ["ansicolors@0.3.2", "", {}, "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="],
"blessed": ["blessed@0.1.81", "", { "bin": { "blessed": "./bin/tput.js" } }, "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ=="],
"blessed-contrib": ["blessed-contrib@4.11.0", "", { "dependencies": { "ansi-term": ">=0.0.2", "chalk": "^1.1.0", "drawille-canvas-blessed-contrib": ">=0.1.3", "lodash": "~>=4.17.21", "map-canvas": ">=0.1.5", "marked": "^4.0.12", "marked-terminal": "^5.1.1", "memory-streams": "^0.1.0", "memorystream": "^0.3.1", "picture-tuber": "^1.0.1", "sparkline": "^0.1.1", "strip-ansi": "^3.0.0", "term-canvas": "0.0.5", "x256": ">=0.0.1" } }, "sha512-P00Xji3xPp53+FdU9f74WpvnOAn/SS0CKLy4vLAf5Ps7FGDOTY711ruJPZb3/7dpFuP+4i7f4a/ZTZdLlKG9WA=="],
"bresenham": ["bresenham@0.0.3", "", {}, "sha512-wbMxoJJM1p3+6G7xEFXYNCJ30h2qkwmVxebkbwIl4OcnWtno5R3UT9VuYLfStlVNAQCmRjkGwjPFdfaPd4iNXw=="],
"buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
"bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="],
"cardinal": ["cardinal@2.1.1", "", { "dependencies": { "ansicolors": "~0.3.2", "redeyed": "~2.1.0" }, "bin": { "cdl": "./bin/cdl.js" } }, "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"charm": ["charm@0.1.2", "", {}, "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ=="],
"cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"drawille-blessed-contrib": ["drawille-blessed-contrib@1.0.0", "", {}, "sha512-WnHMgf5en/hVOsFhxLI8ZX0qTJmerOsVjIMQmn4cR1eI8nLGu+L7w5ENbul+lZ6w827A3JakCuernES5xbHLzQ=="],
"drawille-canvas-blessed-contrib": ["drawille-canvas-blessed-contrib@0.1.3", "", { "dependencies": { "ansi-term": ">=0.0.2", "bresenham": "0.0.3", "drawille-blessed-contrib": ">=0.0.1", "gl-matrix": "^2.1.0", "x256": ">=0.0.1" } }, "sha512-bdDvVJOxlrEoPLifGDPaxIzFh3cD7QH05ePoQ4fwnqfi08ZSxzEhOUpI5Z0/SQMlWgcCQOEtuw0zrwezacXglw=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"event-stream": ["event-stream@0.9.8", "", { "dependencies": { "optimist": "0.2" } }, "sha512-o5h0Mp1bkoR6B0i7pTCAzRy+VzdsRWH997KQD4Psb0EOPoKEIiaRx/EsOdUl7p1Ktjw7aIWvweI/OY1R9XrlUg=="],
"gl-matrix": ["gl-matrix@2.8.1", "", {}, "sha512-0YCjVpE3pS5XWlN3J4X7AiAx65+nqAI54LndtVFnQZB6G/FVLkZH8y8V6R3cIoOQR4pUdfwQGd1iwyoXHJ4Qfw=="],
"has-ansi": ["has-ansi@2.0.0", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"here": ["here@0.0.2", "", {}, "sha512-U7VYImCTcPoY27TSmzoiFsmWLEqQFaYNdpsPb9K0dXJhE6kufUqycaz51oR09CW85dDU9iWyy7At8M+p7hb3NQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"map-canvas": ["map-canvas@0.1.5", "", { "dependencies": { "drawille-canvas-blessed-contrib": ">=0.0.1", "xml2js": "^0.4.5" } }, "sha512-f7M3sOuL9+up0NCOZbb1rQpWDLZwR/ftCiNbyscjl9LUUEwrRaoumH4sz6swgs58lF21DQ0hsYOCw5C6Zz7hbg=="],
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"marked-terminal": ["marked-terminal@5.2.0", "", { "dependencies": { "ansi-escapes": "^6.2.0", "cardinal": "^2.1.1", "chalk": "^5.2.0", "cli-table3": "^0.6.3", "node-emoji": "^1.11.0", "supports-hyperlinks": "^2.3.0" }, "peerDependencies": { "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" } }, "sha512-Piv6yNwAQXGFjZSaiNljyNFw7jKDdGrw70FSbtxEyldLsyeuV5ZHm/1wW++kWbrOF1VPnUgYOhB2oLL0ZpnekA=="],
"memory-streams": ["memory-streams@0.1.3", "", { "dependencies": { "readable-stream": "~1.0.2" } }, "sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA=="],
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
"node-emoji": ["node-emoji@1.11.0", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A=="],
"nopt": ["nopt@2.1.2", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "./bin/nopt.js" } }, "sha512-x8vXm7BZ2jE1Txrxh/hO74HTuYZQEbo8edoRcANgdZ4+PCV+pbjd/xdummkmjjC7LU5EjPzlu8zEq/oxWylnKA=="],
"optimist": ["optimist@0.3.7", "", { "dependencies": { "wordwrap": "~0.0.2" } }, "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ=="],
"picture-tuber": ["picture-tuber@1.0.2", "", { "dependencies": { "buffers": "~0.1.1", "charm": "~0.1.0", "event-stream": "~0.9.8", "optimist": "~0.3.4", "png-js": "~0.1.0", "x256": "~0.0.1" }, "bin": { "picture-tube": "bin/tube.js" } }, "sha512-49/xq+wzbwDeI32aPvwQJldM8pr7dKDRuR76IjztrkmiCkAQDaWFJzkmfVqCHmt/iFoPFhHmI9L0oKhthrTOQw=="],
"png-js": ["png-js@0.1.1", "", {}, "sha512-NTtk2SyfjBm+xYl2/VZJBhFnTQ4kU5qWC7VC4/iGbrgiU4FuB4xC+74erxADYJIqZICOR1HCvRA7EBHkpjTg9g=="],
"readable-stream": ["readable-stream@1.0.34", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg=="],
"redeyed": ["redeyed@2.1.1", "", { "dependencies": { "esprima": "~4.0.0" } }, "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ=="],
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
"sparkline": ["sparkline@0.1.2", "", { "dependencies": { "here": "0.0.2", "nopt": "~2.1.2" }, "bin": { "sparkline": "bin/sparkline" } }, "sha512-t//aVOiWt9fi/e22ea1vXVWBDX+gp18y+Ch9sKqmHl828bRfvP2VtfTJVEcgWFBQHd0yDPNQRiHdqzCvbcYSDA=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
"strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="],
"supports-color": ["supports-color@2.0.0", "", {}, "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g=="],
"supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="],
"term-canvas": ["term-canvas@0.0.5", "", {}, "sha512-eZ3rIWi5yLnKiUcsW8P79fKyooaLmyLWAGqBhFspqMxRNUiB4GmHHk5AzQ4LxvFbJILaXqQZLwbbATLOhCFwkw=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"wordwrap": ["wordwrap@0.0.3", "", {}, "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw=="],
"x256": ["x256@0.0.2", "", {}, "sha512-ZsIH+sheoF8YG9YG+QKEEIdtqpHRA9FYuD7MqhfyB1kayXU43RUNBFSxBEnF8ywSUxdg+8no4+bPr5qLbyxKgA=="],
"xml2js": ["xml2js@0.4.23", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug=="],
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
"blessed-contrib/chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="],
"event-stream/optimist": ["optimist@0.2.8", "", { "dependencies": { "wordwrap": ">=0.0.1 <0.1.0" } }, "sha512-Wy7E3cQDpqsTIFyW7m22hSevyTLxw850ahYv7FWsw4G6MIKVTZ8NSA95KBrQ95a4SMsMr1UGUUnwEFKhVaSzIg=="],
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}

23
index.ts Normal file
View File

@ -0,0 +1,23 @@
import { Simulation } from './src/simulation';
// Create and start the simulation with a size that fits most terminals
const simulation = new Simulation(30, 15);
simulation.initialize(5); // Start with fewer fluffles for clarity
// Display startup message
console.log("Starting Fluffles Simulator...");
console.log("Press 'q' to quit");
simulation.start();
// Handle process termination
process.on('SIGINT', () => {
console.log('Shutting down simulation...');
simulation.stop();
process.exit(0);
});
process.on('exit', () => {
// Ensure simulation is stopped properly
simulation.stop();
});

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "animal-simulator",
"module": "index.ts",
"type": "module",
"scripts": {
"start": "bun run index.ts",
"dev": "bun --watch run index.ts"
},
"devDependencies": {
"bun-types": "latest"
},
"dependencies": {
"blessed": "^0.1.81",
"blessed-contrib": "^4.11.0",
"chalk": "^5.3.0"
}
}

324
src/animal.ts Normal file
View File

@ -0,0 +1,324 @@
import { DNA, Gene } from './dna';
import { Position, World } from './world';
import { Logger } from './logger';
export interface AnimalStats {
health: number;
energy: number;
age: number;
hunger: number;
}
export abstract class Animal {
protected id: string;
protected dna: DNA;
protected position: Position;
protected stats: AnimalStats;
protected world: World;
protected logger: Logger;
constructor(dna: DNA, position: Position, world: World, logger: Logger) {
this.id = crypto.randomUUID();
this.dna = dna;
this.position = position;
this.world = world;
this.logger = logger;
// Initialize stats based on DNA
this.stats = {
health: 100,
energy: 100,
age: 0,
hunger: 0
};
this.logger.log(`Animal ${this.id} was born at position (${position.x}, ${position.y})`);
}
getId(): string {
return this.id;
}
getDNA(): DNA {
return this.dna;
}
getPosition(): Position {
return {...this.position};
}
getStats(): AnimalStats {
return {...this.stats};
}
setPosition(position: Position): void {
this.position = position;
}
// Update the animal's state for one time step
update(): void {
// Increment age
this.stats.age += 1;
// Increase hunger
this.stats.hunger = Math.min(100, this.stats.hunger + 1);
// Decrease energy slightly
this.stats.energy = Math.max(0, this.stats.energy - 0.5);
// Check for death conditions
if (this.stats.health <= 0 || this.stats.hunger >= 100) {
this.die();
return;
}
// Check for old age death
const lifespanGene = this.dna.getGene('lifespan');
const baseLifespan = 100;
const lifespanModifier = lifespanGene ? lifespanGene.value * 50 : 0; // 0 to +50 range
const maxAge = baseLifespan + lifespanModifier;
// Chance of death increases with age
if (this.stats.age > maxAge * 0.7) {
const deathChance = (this.stats.age - (maxAge * 0.7)) / (maxAge * 0.3);
if (Math.random() < deathChance * 0.1) {
this.logger.log(`Animal ${this.id.substring(0, 6)} died of old age at ${this.stats.age}`);
this.die();
return;
}
}
// If still alive, perform actions
this.act();
}
protected calculateEnergyConsumption(): number {
// Base energy consumption
let consumption = 1;
// Size affects energy consumption
const sizeGene = this.dna.getGene('size');
if (sizeGene) {
consumption += sizeGene.value * 2; // Larger animals consume more energy
}
// Activity level affects energy consumption
const activityGene = this.dna.getGene('activity');
if (activityGene) {
consumption += activityGene.value * 1.5;
}
return consumption;
}
protected abstract act(): void;
protected move(direction: 'north' | 'east' | 'south' | 'west'): void {
const newPosition = {...this.position};
switch (direction) {
case 'north':
newPosition.y = Math.max(0, newPosition.y - 1);
break;
case 'east':
newPosition.x = Math.min(this.world.getWidth() - 1, newPosition.x + 1);
break;
case 'south':
newPosition.y = Math.min(this.world.getHeight() - 1, newPosition.y + 1);
break;
case 'west':
newPosition.x = Math.max(0, newPosition.x - 1);
break;
}
// Check if the new position is valid
if (this.world.isPositionValid(newPosition)) {
this.position = newPosition;
this.stats.energy -= 1; // Moving costs energy
// Only log movements occasionally to reduce spam
if (Math.random() < 0.05) {
this.logger.log(`Animal ${this.id.substring(0, 6)} moved to (${this.position.x}, ${this.position.y})`);
}
}
}
protected eat(foodValue: number): void {
this.stats.hunger = Math.max(0, this.stats.hunger - foodValue);
this.stats.energy = Math.min(100, this.stats.energy + foodValue / 2);
// Only log eating occasionally
if (Math.random() < 0.2) {
this.logger.log(`Animal ${this.id.substring(0, 6)} ate food with value ${foodValue}`);
}
}
protected die(): void {
this.world.removeAnimal(this.id);
this.logger.log(`Animal ${this.id} has died at age ${this.stats.age}`);
}
// Method for reproduction
reproduce(partner: Animal): Animal | null {
// Check if both animals are mature
if (!this.isAdult() || !partner.isAdult()) {
return null;
}
// Check if both animals have enough energy to reproduce
if (this.stats.energy < 50 || partner.getStats().energy < 50) {
return null;
}
// Get population pressure from the world
const populationPressure = this.world.getPopulationPressure();
// Make reproduction harder when population is high
const reproductiveUrgeGene = this.dna.getGene('reproductiveUrge');
const reproductiveUrge = reproductiveUrgeGene ? reproductiveUrgeGene.value : 0.5;
// Calculate success chance based on population pressure and reproductive urge
const successChance = Math.max(0.1, reproductiveUrge - (populationPressure * 0.08));
if (Math.random() > successChance) {
// Failed reproduction attempt due to environmental factors
this.stats.energy -= 10; // Still costs some energy to try
return null;
}
// Combine DNA
const childDNA = DNA.combine(this.dna, partner.getDNA());
// Possibly mutate
const mutatedDNA = childDNA.mutate();
// Create a new animal of the same type
const childPosition = {
x: Math.floor((this.position.x + partner.getPosition().x) / 2),
y: Math.floor((this.position.y + partner.getPosition().y) / 2)
};
// Consume energy for reproduction
this.stats.energy -= 30;
this.logger.log(`Animal ${this.id.substring(0, 6)} reproduced with ${partner.getId().substring(0, 6)}`);
// The specific animal type will be created by the derived class
const offspring = this.createOffspring(mutatedDNA, childPosition);
// Check if the world accepted the new animal
if (!this.world.addAnimal(offspring)) {
this.logger.log(`Offspring couldn't survive due to overpopulation`);
return null;
}
return offspring;
}
protected abstract createOffspring(dna: DNA, position: Position): Animal;
// Method for rendering the animal
abstract render(): string;
applyEnvironmentalStress(pressureLevel: number): void {
// Higher pressure means more competition for resources
if (pressureLevel > 7) {
// Severe pressure
this.stats.hunger += 0.5; // Animals get hungrier faster
this.stats.energy -= 0.3; // Animals tire more quickly
// Chance of health decline due to overcrowding
if (Math.random() < 0.05) {
this.stats.health -= 1;
if (Math.random() < 0.1) {
this.logger.log(`Animal ${this.id.substring(0, 6)} suffering from overcrowding`);
}
}
} else if (pressureLevel > 5) {
// Moderate pressure
this.stats.hunger += 0.3;
this.stats.energy -= 0.1;
}
}
protected isAdult(): boolean {
// Check if the animal has reached maturity
const lifespanGene = this.dna.getGene('lifespan');
const maturityGene = this.dna.getGene('maturityAge');
// Default maturity age is 25, but can be modified by genes
const baseMaturityAge = 25;
const maturityModifier = maturityGene ? maturityGene.value * 20 - 10 : 0; // -10 to +10 range
const maturityAge = Math.max(15, baseMaturityAge + maturityModifier);
return this.stats.age >= maturityAge;
}
protected attack(target: Animal): boolean {
// Check if we have enough energy to attack
if (this.stats.energy < 20) {
return false;
}
// Get relevant genes
const aggressionGene = this.dna.getGene('aggression');
const speedGene = this.dna.getGene('speed');
const targetSpeedGene = target.getDNA().getGene('speed');
// Calculate attack success chance based on genes
const aggression = aggressionGene ? aggressionGene.value : 0.3;
const speed = speedGene ? speedGene.value : 0.5;
const targetSpeed = targetSpeedGene ? targetSpeedGene.value : 0.5;
// Speed difference affects chance to catch prey
const speedAdvantage = Math.max(0, speed - targetSpeed);
// Calculate success chance
const successChance = 0.3 + (aggression * 0.4) + (speedAdvantage * 0.3);
// Attacking costs energy
this.stats.energy -= 15;
if (Math.random() < successChance) {
// Attack succeeded
this.logger.log(`Animal ${this.id.substring(0, 6)} successfully attacked ${target.getId().substring(0, 6)}`);
// Gain energy and reduce hunger from the kill
const energyGain = 30 + Math.floor(Math.random() * 20);
this.stats.energy = Math.min(100, this.stats.energy + energyGain);
this.stats.hunger = Math.max(0, this.stats.hunger - 40);
// Target dies
target.die();
return true;
} else {
// Attack failed
if (Math.random() < 0.3) {
this.logger.log(`Animal ${this.id.substring(0, 6)} failed to catch ${target.getId().substring(0, 6)}`);
}
return false;
}
}
protected shouldBeAggressive(): boolean {
// Get relevant genes
const aggressionGene = this.dna.getGene('aggression');
const friendshipGene = this.dna.getGene('friendship');
// Calculate base aggression
const aggression = aggressionGene ? aggressionGene.value : 0.3;
const friendship = friendshipGene ? friendshipGene.value : 0.5;
// Low energy increases aggression
const energyFactor = Math.max(0, 1 - (this.stats.energy / 100));
// Hunger increases aggression - this is now a stronger factor
const hungerFactor = this.stats.hunger / 100;
// Calculate overall aggression tendency
// Hunger now has a higher weight (0.4) compared to energy (0.2)
const aggressionTendency = (aggression * 0.3) + (energyFactor * 0.2) + (hungerFactor * 0.4) - (friendship * 0.3);
return Math.random() < aggressionTendency;
}
}

400
src/animals/fluffles.ts Normal file
View File

@ -0,0 +1,400 @@
import { Animal } from '../animal';
import { DNA } from '../dna';
import { Position, World } from '../world';
import { Logger } from '../logger';
export class Fluffles extends Animal {
private matingPartner: string | null = null;
private matingProgress: number = 0;
private readonly MATING_DURATION = 4;
constructor(dna: DNA, position: Position, world: World, logger: Logger) {
super(dna, position, world, logger);
// If no DNA is provided, initialize with standard genes
if (dna.getAllGenes().length === 0) {
// Create a new DNA with fluffles-specific tweaks to standard values
const flufflesDNA = DNA.createStandard({
size: 0.3, // Fluffles are small
speed: 0.7, // Fluffles are fast
metabolism: 0.7, // Fluffles have high metabolism
vision: 0.6, // Fluffles have good vision
intelligence: 0.4, // Fluffles are moderately intelligent
aggression: 0.2, // Fluffles are not aggressive
socialBehavior: 0.6, // Fluffles are somewhat social
reproductiveUrge: 0.8, // Fluffles reproduce quickly
maturityAge: 0.4 // Fluffles mature relatively quickly
});
// Copy all genes to this animal's DNA
flufflesDNA.getAllGenes().forEach(gene => {
this.dna.addGene(gene);
});
}
}
protected act(): void {
// If currently mating, continue the mating process
if (this.matingPartner !== null) {
this.continueMating();
return;
}
// Get relevant genes that affect behavior
const intelligenceGene = this.dna.getGene('intelligence');
const aggressionGene = this.dna.getGene('aggression');
const socialGene = this.dna.getGene('socialBehavior');
const reproductiveUrgeGene = this.dna.getGene('reproductiveUrge');
// Intelligence affects decision making
const intelligence = intelligenceGene ? intelligenceGene.value : 0.3;
// Get population pressure
const populationPressure = this.world.getPopulationPressure();
// Check if we should be aggressive (more likely when hungry or low energy)
const shouldAttack = this.shouldBeAggressive();
// Make smarter decisions based on intelligence
const randomFactor = Math.random();
// Higher intelligence means more likely to make optimal decisions
if (randomFactor < intelligence) {
// Make the best decision based on current needs
if (this.stats.hunger > 70) {
// Very hungry, prioritize food
if (shouldAttack && (this.stats.hunger > 80 || this.stats.energy < 50)) {
// Try hunting instead of foraging when very hungry
this.huntOthers();
} else {
this.searchForFood();
}
}
else if (this.stats.energy < 30) {
// Low energy, rest
this.rest();
}
else if (this.stats.hunger > 40) {
// Somewhat hungry, look for food
if (shouldAttack && (this.stats.hunger > 60 || this.stats.energy < 50)) {
// Try hunting instead of foraging
this.huntOthers();
} else {
this.searchForFood();
}
}
else if (reproductiveUrgeGene &&
reproductiveUrgeGene.value > 0.6 &&
this.stats.energy > 70 &&
populationPressure < 7 &&
this.isAdult()) { // Only seek mates when mature and population isn't too high
// High reproductive urge and enough energy, look for mates
this.searchForMates();
}
else if (socialGene && socialGene.value > 0.7 && populationPressure < 8 && !shouldAttack) {
// Highly social, seek other fluffles (unless very overcrowded or aggressive)
this.seekCompany();
}
else if (shouldAttack && this.stats.energy < 50) {
// Become aggressive when energy is low
this.huntOthers();
}
else {
// Otherwise, explore
this.wander();
}
} else {
// Less intelligent animals make more random choices
const choice = Math.random();
if (shouldAttack && this.stats.energy < 50 && choice < 0.4) {
this.huntOthers();
} else if (choice < 0.4) {
this.searchForFood();
} else if (choice < 0.6) {
this.rest();
} else {
this.wander();
}
}
}
// New method to handle the mating process
private continueMating(): void {
// Check if partner still exists
const partner = this.world.getAnimal(this.matingPartner!);
if (!partner) {
// Partner disappeared, cancel mating
this.matingPartner = null;
this.matingProgress = 0;
return;
}
// Increment mating progress
this.matingProgress++;
// Check if mating is complete
if (this.matingProgress >= this.MATING_DURATION) {
// Attempt reproduction
const offspring = this.reproduce(partner);
// Reset mating state
this.matingPartner = null;
this.matingProgress = 0;
if (offspring) {
this.logger.log(`Fluffles ${this.id.substring(0, 6)} and ${partner.getId().substring(0, 6)} successfully produced offspring!`);
}
} else {
// Stay close to partner during mating
const partnerPos = partner.getPosition();
if (Math.abs(partnerPos.x - this.position.x) + Math.abs(partnerPos.y - this.position.y) > 1) {
// Move toward partner if separated
this.moveToward(partnerPos);
}
}
}
// Update the searchForMates method to initiate mating
private searchForMates(): void {
const nearbyAnimals = this.world.getAnimalsInRadius(this.position, 2);
for (const animal of nearbyAnimals) {
if (animal.getId() !== this.id && animal instanceof Fluffles) {
// Check if the other animal is mature
if (!animal.isAdult()) continue;
const pos = animal.getPosition();
const distance = Math.abs(pos.x - this.position.x) + Math.abs(pos.y - this.position.y);
if (distance <= 1) {
// Adjacent animal, initiate mating
this.startMating(animal.getId());
return;
} else if (distance <= 2) {
// Nearby animal, move toward it
this.moveToward(pos);
return;
}
}
}
// If no potential mates found, just wander
this.wander();
}
// New method to start the mating process
private startMating(partnerId: string): void {
// Only start if not already mating
if (this.matingPartner === null) {
this.matingPartner = partnerId;
this.matingProgress = 0;
if (Math.random() < 0.3) {
this.logger.log(`Fluffles ${this.id.substring(0, 6)} started mating with ${partnerId.substring(0, 6)}`);
}
}
}
protected createOffspring(dna: DNA, position: Position): Animal {
return new Fluffles(dna, position, this.world, this.logger);
}
render(): string {
// Render the fluffles based on its fur color and other attributes
const furColorGene = this.dna.getGene('furColor');
const sizeGene = this.dna.getGene('size');
const energyLevel = this.stats.energy;
// Different symbols based on size
let symbol = 'f';
if (sizeGene) {
if (sizeGene.value > 0.7) symbol = 'F';
else if (sizeGene.value < 0.3) symbol = '°';
}
// If currently mating, use a special symbol
if (this.matingPartner !== null) {
symbol = '♥';
}
// If not mature yet, use a baby symbol
if (!this.isAdult()) {
symbol = '•';
}
// Color based on fur color gene
let color = 'white';
if (furColorGene) {
if (furColorGene.value > 0.8) color = 'red';
else if (furColorGene.value > 0.6) color = 'yellow';
else if (furColorGene.value > 0.4) color = 'magenta';
else if (furColorGene.value > 0.2) color = 'blue';
else color = 'white';
}
// Add brightness based on energy level
let brightness = '';
if (energyLevel < 30) brightness = '-fg';
else brightness = '-fg';
return `{${color}${brightness}}${symbol}{/${color}${brightness}}`;
}
private wander(): void {
// Simple random movement
const directions = ['north', 'east', 'south', 'west'] as const;
const randomDirection = directions[Math.floor(Math.random() * directions.length)];
this.move(randomDirection);
}
private searchForFood(): void {
// Look for food in adjacent tiles
const adjacentPositions = [
{ x: this.position.x, y: this.position.y - 1 }, // North
{ x: this.position.x + 1, y: this.position.y }, // East
{ x: this.position.x, y: this.position.y + 1 }, // South
{ x: this.position.x - 1, y: this.position.y } // West
];
// Check each adjacent position for food
let bestFoodPosition = null;
let bestFoodValue = 0;
for (const pos of adjacentPositions) {
if (this.world.isPositionValid(pos)) {
const tile = this.world.getTile(pos);
if (tile && tile.type === 'grass' && tile.foodValue > bestFoodValue) {
bestFoodValue = tile.foodValue;
bestFoodPosition = pos;
}
}
}
if (bestFoodPosition && bestFoodValue > 0) {
// Move toward the food
if (bestFoodPosition.y < this.position.y) {
this.move('north');
} else if (bestFoodPosition.x > this.position.x) {
this.move('east');
} else if (bestFoodPosition.y > this.position.y) {
this.move('south');
} else if (bestFoodPosition.x < this.position.x) {
this.move('west');
}
// Eat if we're on a grass tile
const currentTile = this.world.getTile(this.position);
if (currentTile && currentTile.type === 'grass' && currentTile.foodValue > 0) {
const foodEaten = this.world.consumeFood(this.position, Math.min(5, currentTile.foodValue));
this.eat(foodEaten);
}
} else {
// No food found, just wander
this.wander();
}
}
private rest(): void {
// Stay in place and recover energy
this.stats.energy = Math.min(100, this.stats.energy + 10);
if (Math.random() < 0.1) {
this.logger.log(`Fluffles ${this.id.substring(0, 6)} is resting and recovering energy`);
}
}
private seekCompany(): void {
const nearbyAnimals = this.world.getAnimalsInRadius(this.position, 3);
if (nearbyAnimals.length > 0) {
// Find the closest fluffles
let closestFluffles = null;
let closestDistance = Infinity;
for (const animal of nearbyAnimals) {
if (animal.getId() !== this.id) { // Not self
const pos = animal.getPosition();
const distance = Math.abs(pos.x - this.position.x) + Math.abs(pos.y - this.position.y);
if (distance < closestDistance) {
closestFluffles = animal;
closestDistance = distance;
}
}
}
if (closestFluffles) {
// Move toward the closest fluffles
const targetPos = closestFluffles.getPosition();
this.moveToward(targetPos);
return;
}
}
// If no fluffles found, just wander
this.wander();
}
// Helper method to move toward a target position
private moveToward(targetPos: Position): void {
// Determine which direction gets us closer to the target
const dx = targetPos.x - this.position.x;
const dy = targetPos.y - this.position.y;
if (Math.abs(dx) > Math.abs(dy)) {
// Move horizontally
if (dx > 0) {
this.move('east');
} else {
this.move('west');
}
} else {
// Move vertically
if (dy > 0) {
this.move('south');
} else {
this.move('north');
}
}
}
// Add a new method for hunting other animals
private huntOthers(): void {
const nearbyAnimals = this.world.getAnimalsInRadius(this.position, 2);
// Filter out self
const potentialTargets = nearbyAnimals.filter(animal => animal.getId() !== this.id);
if (potentialTargets.length > 0) {
// Find the closest target
let closestTarget = null;
let closestDistance = Infinity;
for (const animal of potentialTargets) {
const pos = animal.getPosition();
const distance = Math.abs(pos.x - this.position.x) + Math.abs(pos.y - this.position.y);
if (distance < closestDistance) {
closestTarget = animal;
closestDistance = distance;
}
}
if (closestTarget) {
if (closestDistance <= 1) {
// Adjacent animal, attack it
this.attack(closestTarget);
} else {
// Move toward the target
const targetPos = closestTarget.getPosition();
this.moveToward(targetPos);
}
return;
}
}
// If no targets found, just wander
this.wander();
}
}

260
src/animals/rabbit.ts Normal file
View File

@ -0,0 +1,260 @@
import { Animal } from '../animal';
import { DNA } from '../dna';
import { Position, World } from '../world';
import { Logger } from '../logger';
export class Rabbit extends Animal {
constructor(dna: DNA, position: Position, world: World, logger: Logger) {
super(dna, position, world, logger);
// If no DNA is provided, initialize with standard genes
if (dna.getAllGenes().length === 0) {
// Create a new DNA with rabbit-specific tweaks to standard values
const rabbitDNA = DNA.createStandard({
size: 0.3, // Rabbits are small
speed: 0.7, // Rabbits are fast
metabolism: 0.7, // Rabbits have high metabolism
vision: 0.6, // Rabbits have good vision
intelligence: 0.4, // Rabbits are moderately intelligent
aggression: 0.2, // Rabbits are not aggressive
socialBehavior: 0.6, // Rabbits are somewhat social
reproductiveUrge: 0.8 // Rabbits reproduce quickly
});
// Copy all genes to this animal's DNA
rabbitDNA.getAllGenes().forEach(gene => {
this.dna.addGene(gene);
});
}
}
protected act(): void {
// Get relevant genes that affect behavior
const intelligenceGene = this.dna.getGene('intelligence');
const aggressionGene = this.dna.getGene('aggression');
const socialGene = this.dna.getGene('socialBehavior');
const reproductiveUrgeGene = this.dna.getGene('reproductiveUrge');
// Intelligence affects decision making
const intelligence = intelligenceGene ? intelligenceGene.value : 0.3;
// Get population pressure
const populationPressure = this.world.getPopulationPressure();
// Make smarter decisions based on intelligence
const randomFactor = Math.random();
// Higher intelligence means more likely to make optimal decisions
if (randomFactor < intelligence) {
// Make the best decision based on current needs
if (this.stats.hunger > 70) {
// Very hungry, prioritize food
this.searchForFood();
}
else if (this.stats.energy < 30) {
// Low energy, rest
this.rest();
}
else if (this.stats.hunger > 40) {
// Somewhat hungry, look for food
this.searchForFood();
}
else if (reproductiveUrgeGene &&
reproductiveUrgeGene.value > 0.6 &&
this.stats.energy > 70 &&
populationPressure < 7) { // Only seek mates when population isn't too high
// High reproductive urge and enough energy, look for mates
this.searchForMates();
}
else if (socialGene && socialGene.value > 0.7 && populationPressure < 8) {
// Highly social, seek other rabbits (unless very overcrowded)
this.seekCompany();
}
else {
// Otherwise, explore
this.wander();
}
} else {
// Less intelligent animals make more random choices
const choice = Math.random();
if (choice < 0.4) {
this.searchForFood();
} else if (choice < 0.6) {
this.rest();
} else {
this.wander();
}
}
}
private searchForFood(): void {
// Check current position for food
const currentTile = this.world.getTile(this.position);
if (currentTile && currentTile.foodValue > 0) {
const consumed = this.world.consumeFood(this.position, 5);
this.eat(consumed);
return;
}
// Look in adjacent tiles for food
const directions: ('north' | 'east' | 'south' | 'west')[] = ['north', 'east', 'south', 'west'];
// Shuffle directions for randomness
for (let i = directions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[directions[i], directions[j]] = [directions[j], directions[i]];
}
// Check each direction
for (const direction of directions) {
const newPos = {...this.position};
switch (direction) {
case 'north': newPos.y = Math.max(0, newPos.y - 1); break;
case 'east': newPos.x = Math.min(this.world.getWidth() - 1, newPos.x + 1); break;
case 'south': newPos.y = Math.min(this.world.getHeight() - 1, newPos.y + 1); break;
case 'west': newPos.x = Math.max(0, newPos.x - 1); break;
}
const tile = this.world.getTile(newPos);
if (tile && tile.foodValue > 0) {
this.move(direction);
return;
}
}
// If no food found in adjacent tiles, just wander
this.wander();
}
private rest(): void {
// Resting recovers energy but increases hunger
this.stats.energy = Math.min(100, this.stats.energy + 5);
this.logger.log(`Rabbit ${this.id} is resting. Energy: ${this.stats.energy}`);
}
private wander(): void {
// Move in a random direction
const directions: ('north' | 'east' | 'south' | 'west')[] = ['north', 'east', 'south', 'west'];
const randomDirection = directions[Math.floor(Math.random() * directions.length)];
this.move(randomDirection);
}
protected createOffspring(dna: DNA, position: Position): Animal {
return new Rabbit(dna, position, this.world, this.logger);
}
render(): string {
// Render the rabbit based on its fur color and other attributes
const furColorGene = this.dna.getGene('furColor');
const sizeGene = this.dna.getGene('size');
const energyLevel = this.stats.energy;
// Different symbols based on size
let symbol = 'r';
if (sizeGene) {
if (sizeGene.value > 0.7) symbol = 'R';
else if (sizeGene.value < 0.3) symbol = '°';
}
// Color based on fur color gene
let color = 'white';
if (furColorGene) {
if (furColorGene.value > 0.8) color = 'red';
else if (furColorGene.value > 0.6) color = 'yellow';
else if (furColorGene.value > 0.4) color = 'magenta';
else if (furColorGene.value > 0.2) color = 'blue';
else color = 'white';
}
// Add brightness based on energy level
let brightness = '';
if (energyLevel < 30) brightness = '-fg';
else brightness = '-fg';
return `{${color}${brightness}}${symbol}{/${color}${brightness}}`;
}
// New method to seek company of other rabbits
private seekCompany(): void {
const nearbyAnimals = this.world.getAnimalsInRadius(this.position, 3);
if (nearbyAnimals.length > 0) {
// Find the closest rabbit
let closestRabbit = null;
let closestDistance = Infinity;
for (const animal of nearbyAnimals) {
if (animal.getId() !== this.id) { // Not self
const pos = animal.getPosition();
const distance = Math.abs(pos.x - this.position.x) + Math.abs(pos.y - this.position.y);
if (distance < closestDistance) {
closestRabbit = animal;
closestDistance = distance;
}
}
}
if (closestRabbit) {
// Move toward the closest rabbit
const targetPos = closestRabbit.getPosition();
this.moveToward(targetPos);
return;
}
}
// If no rabbits found, just wander
this.wander();
}
// New method to search for potential mates
private searchForMates(): void {
const nearbyAnimals = this.world.getAnimalsInRadius(this.position, 2);
for (const animal of nearbyAnimals) {
if (animal.getId() !== this.id) { // Not self
const pos = animal.getPosition();
const distance = Math.abs(pos.x - this.position.x) + Math.abs(pos.y - this.position.y);
if (distance <= 1) {
// Adjacent animal, try to reproduce
this.reproduce(animal);
return;
} else if (distance <= 2) {
// Nearby animal, move toward it
this.moveToward(pos);
return;
}
}
}
// If no potential mates found, just wander
this.wander();
}
// Helper method to move toward a target position
private moveToward(targetPos: Position): void {
// Determine which direction gets us closer to the target
const dx = targetPos.x - this.position.x;
const dy = targetPos.y - this.position.y;
if (Math.abs(dx) > Math.abs(dy)) {
// Move horizontally
if (dx > 0) {
this.move('east');
} else {
this.move('west');
}
} else {
// Move vertically
if (dy > 0) {
this.move('south');
} else {
this.move('north');
}
}
}
}

234
src/dna.ts Normal file
View File

@ -0,0 +1,234 @@
export interface Gene {
name: string;
value: number;
mutationRate: number;
}
export interface GeneDefinition {
name: string;
defaultValue: number;
defaultMutationRate: number;
description: string;
minValue?: number;
maxValue?: number;
}
export class DNA {
private genes: Map<string, Gene>;
constructor(initialGenes?: Gene[]) {
this.genes = new Map();
if (initialGenes) {
initialGenes.forEach(gene => {
this.genes.set(gene.name, gene);
});
}
}
addGene(gene: Gene): void {
this.genes.set(gene.name, gene);
}
getGene(name: string): Gene | undefined {
return this.genes.get(name);
}
getAllGenes(): Gene[] {
return Array.from(this.genes.values());
}
mutate(): DNA {
const newDNA = new DNA();
this.genes.forEach((gene, name) => {
const shouldMutate = Math.random() < gene.mutationRate;
if (shouldMutate) {
// Create a mutation by adjusting the value slightly
const mutationAmount = (Math.random() * 2 - 1) * 0.1; // -10% to +10%
const newValue = Math.max(0, Math.min(1, gene.value + mutationAmount));
newDNA.addGene({
name,
value: newValue,
mutationRate: gene.mutationRate
});
} else {
// Copy the gene as is
newDNA.addGene({...gene});
}
});
return newDNA;
}
// For creating offspring DNA by combining two parent DNAs
static combine(dna1: DNA, dna2: DNA): DNA {
const combinedDNA = new DNA();
// Get all unique gene names from both parents
const allGeneNames = new Set([
...dna1.genes.keys(),
...dna2.genes.keys()
]);
allGeneNames.forEach(name => {
const gene1 = dna1.getGene(name);
const gene2 = dna2.getGene(name);
if (gene1 && gene2) {
// If both parents have the gene, randomly select one or average them
const inheritanceType = Math.random();
if (inheritanceType < 0.4) {
// 40% chance to inherit from first parent
combinedDNA.addGene({...gene1});
} else if (inheritanceType < 0.8) {
// 40% chance to inherit from second parent
combinedDNA.addGene({...gene2});
} else {
// 20% chance to blend the genes
combinedDNA.addGene({
name,
value: (gene1.value + gene2.value) / 2,
mutationRate: (gene1.mutationRate + gene2.mutationRate) / 2
});
}
} else if (gene1) {
// Only first parent has the gene
combinedDNA.addGene({...gene1});
} else if (gene2) {
// Only second parent has the gene
combinedDNA.addGene({...gene2});
}
});
return combinedDNA;
}
// Standard gene definitions for animals
static STANDARD_GENES: GeneDefinition[] = [
{
name: 'size',
defaultValue: 0.5,
defaultMutationRate: 0.05,
description: 'Physical size of the animal',
minValue: 0.1,
maxValue: 1.0
},
{
name: 'speed',
defaultValue: 0.5,
defaultMutationRate: 0.1,
description: 'Movement speed and agility',
minValue: 0.1,
maxValue: 1.0
},
{
name: 'metabolism',
defaultValue: 0.5,
defaultMutationRate: 0.05,
description: 'Rate of energy consumption',
minValue: 0.1,
maxValue: 1.0
},
{
name: 'vision',
defaultValue: 0.5,
defaultMutationRate: 0.08,
description: 'Ability to detect food and threats at a distance',
minValue: 0.1,
maxValue: 1.0
},
{
name: 'intelligence',
defaultValue: 0.3,
defaultMutationRate: 0.03,
description: 'Problem-solving and decision-making ability',
minValue: 0.1,
maxValue: 1.0
},
{
name: 'aggression',
defaultValue: 0.3,
defaultMutationRate: 0.1,
description: 'Tendency to fight rather than flee',
minValue: 0,
maxValue: 1.0
},
{
name: 'socialBehavior',
defaultValue: 0.5,
defaultMutationRate: 0.07,
description: 'Tendency to group with others',
minValue: 0,
maxValue: 1.0
},
{
name: 'furColor',
defaultValue: 0.5,
defaultMutationRate: 0.15,
description: 'Color of fur/skin',
minValue: 0,
maxValue: 1.0
},
{
name: 'reproductiveUrge',
defaultValue: 0.5,
defaultMutationRate: 0.1,
description: 'Desire to reproduce when conditions are right',
minValue: 0.1,
maxValue: 1.0
},
{
name: 'lifespan',
defaultValue: 0.5,
defaultMutationRate: 0.05,
description: 'Natural maximum age',
minValue: 0.2,
maxValue: 1.0
},
{
name: 'maturityAge',
defaultValue: 0.5,
defaultMutationRate: 0.05,
description: 'Age at which the animal reaches maturity',
minValue: 0.2,
maxValue: 0.8
},
{
name: 'friendship',
defaultValue: 0.5,
defaultMutationRate: 0.1,
description: 'Tendency to be friendly rather than aggressive',
minValue: 0,
maxValue: 1.0
}
];
// Add a static method to create a DNA with standard genes
static createStandard(customValues?: Record<string, number>): DNA {
const dna = new DNA();
DNA.STANDARD_GENES.forEach(geneDef => {
const value = customValues && customValues[geneDef.name] !== undefined
? customValues[geneDef.name]
: geneDef.defaultValue;
dna.addGene({
name: geneDef.name,
value: value,
mutationRate: geneDef.defaultMutationRate
});
});
return dna;
}
// Add a method to get a gene's description
getGeneDescription(name: string): string | undefined {
const geneDef = DNA.STANDARD_GENES.find(g => g.name === name);
return geneDef?.description;
}
}

80
src/logger.ts Normal file
View File

@ -0,0 +1,80 @@
import * as fs from 'fs';
import * as path from 'path';
export class Logger {
private logs: string[] = [];
private logCallback: ((message: string) => void) | null = null;
private consoleOutput: boolean;
private fileOutput: boolean;
private logFile: fs.WriteStream | null = null;
constructor(consoleOutput: boolean = true, fileOutput: boolean = true) {
this.consoleOutput = consoleOutput;
this.fileOutput = fileOutput;
if (fileOutput) {
this.setupLogFile();
}
}
private setupLogFile(): void {
// Create logs directory if it doesn't exist
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir);
}
// Create a timestamped log file
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
const logFilePath = path.join(logsDir, `simulation-${timestamp}.log`);
this.logFile = fs.createWriteStream(logFilePath, { flags: 'a' });
this.log(`Log file created at ${logFilePath}`);
}
log(message: string): void {
const timestamp = new Date().toISOString();
const formattedMessage = `[${timestamp}] ${message}`;
this.logs.push(formattedMessage);
if (this.consoleOutput) {
console.log(formattedMessage);
}
if (this.fileOutput && this.logFile) {
this.logFile.write(formattedMessage + '\n');
}
if (this.logCallback) {
this.logCallback(formattedMessage);
}
}
setLogCallback(callback: (message: string) => void): void {
this.logCallback = callback;
// Send the last few logs to the callback to populate the log view
const lastLogs = this.getLastLogs(10);
lastLogs.forEach(log => callback(log));
}
getLogs(): string[] {
return [...this.logs];
}
getLastLogs(count: number): string[] {
return this.logs.slice(-count);
}
clearLogs(): void {
this.logs = [];
}
close(): void {
if (this.logFile) {
this.logFile.end();
this.logFile = null;
}
}
}

424
src/renderer.ts Normal file
View File

@ -0,0 +1,424 @@
import { World } from './world';
import { Logger } from './logger';
import * as blessed from 'blessed';
import * as contrib from 'blessed-contrib';
import { Animal } from './animal';
export class Renderer {
private screen: blessed.Widgets.Screen;
private worldBox: blessed.Widgets.BoxElement;
private worldInfoBox: blessed.Widgets.BoxElement;
private logBox: blessed.Widgets.Log;
private statsBox: blessed.Widgets.BoxElement;
private helpBox: blessed.Widgets.BoxElement;
private world: World;
private logger: Logger;
private selectedAnimalIndex: number = 0;
private showHelp: boolean = false;
private disasterInfo: { active: boolean, type: string, duration: number, intensity: number } = {
active: false,
type: '',
duration: 0,
intensity: 0
};
constructor(world: World, logger: Logger) {
this.world = world;
this.logger = logger;
// Initialize blessed screen
this.screen = blessed.screen({
smartCSR: true,
title: 'Animal Simulator',
fullUnicode: true
});
// Create layout
this.worldBox = blessed.box({
top: 0,
left: 0,
width: '70%',
height: '60%', // Reduced height to make room for world info
content: '',
tags: true,
label: ' World ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'cyan'
}
}
});
// Add a separate world info box
this.worldInfoBox = blessed.box({
top: '60%',
left: 0,
width: '70%',
height: '10%', // Small box for world info and legend
content: '',
tags: true,
label: ' World Info ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'green'
}
}
});
// Use a Log widget instead of a Box for better log handling
this.logBox = blessed.log({
top: '70%',
left: 0,
width: '100%',
height: '30%',
tags: true,
label: ' Animal Logs ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'cyan'
}
},
scrollable: true,
scrollbar: {
ch: '│',
track: {
bg: 'black'
},
style: {
inverse: true
}
},
mouse: true,
alwaysScroll: true
});
this.statsBox = blessed.box({
top: 0,
left: '70%',
width: '30%',
height: '70%',
content: '',
tags: true,
label: ' Statistics ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'cyan'
}
},
scrollable: true,
scrollbar: {
ch: '│',
track: {
bg: 'black'
},
style: {
inverse: true
}
},
mouse: true,
alwaysScroll: true
});
// Create help box (initially hidden)
this.helpBox = blessed.box({
top: 'center',
left: 'center',
width: '80%',
height: '80%',
content: this.getHelpContent(),
tags: true,
label: ' Help ',
border: {
type: 'line'
},
style: {
fg: 'white',
border: {
fg: 'yellow'
}
},
hidden: true,
scrollable: true,
scrollbar: {
ch: '│',
track: {
bg: 'black'
},
style: {
inverse: true
}
},
mouse: true,
alwaysScroll: true,
keys: true,
vi: true
});
// Add components to screen
this.screen.append(this.worldBox);
this.screen.append(this.worldInfoBox); // Add the new box
this.screen.append(this.logBox);
this.screen.append(this.statsBox);
this.screen.append(this.helpBox);
// Set up key handlers
this.setupKeyHandlers();
// Register with logger to receive log events
logger.setLogCallback((message) => {
this.logBox.log(message);
this.screen.render();
});
// Render the screen
this.screen.render();
}
private setupKeyHandlers(): void {
// Quit on Escape, q, or Control-C
this.screen.key(['escape', 'q', 'C-c'], () => process.exit(0));
// Toggle help screen with 'h'
this.screen.key('h', () => {
this.showHelp = !this.showHelp;
this.helpBox.hidden = !this.showHelp;
this.screen.render();
});
// Animal selection with arrow keys
this.screen.key('right', () => {
const animals = this.world.getAllAnimals();
if (animals.length > 0) {
this.selectedAnimalIndex = (this.selectedAnimalIndex + 1) % animals.length;
this.render();
}
});
this.screen.key('left', () => {
const animals = this.world.getAllAnimals();
if (animals.length > 0) {
this.selectedAnimalIndex = (this.selectedAnimalIndex - 1 + animals.length) % animals.length;
this.render();
}
});
// Speed controls
this.screen.key('+', () => {
this.logger.log("Simulation speed increased");
this.screen.emit('speed', 0.75); // Emit custom event for speed change
});
this.screen.key('-', () => {
this.logger.log("Simulation speed decreased");
this.screen.emit('speed', 1.25); // Emit custom event for speed change
});
// Add pause/unpause key
this.screen.key('p', () => {
this.screen.emit('pause'); // Emit custom event for pause toggle
this.logger.log("Simulation pause toggled");
});
}
onSpeedChange(callback: (factor: number) => void): void {
this.screen.on('speed', callback);
}
onPauseToggle(callback: () => void): void {
this.screen.on('pause', callback);
}
private getHelpContent(): string {
return `{bold}{green-fg}Animal Simulator Help{/green-fg}{/bold}
{yellow-fg}Navigation Controls:{/yellow-fg}
{bold}Left/Right Arrows{/bold}: Select previous/next animal
{bold}H{/bold}: Toggle this help screen
{bold}Q{/bold} or {bold}Esc{/bold}: Quit the simulation
{yellow-fg}Simulation Controls:{/yellow-fg}
{bold}P{/bold}: Pause/Unpause simulation
{bold}+{/bold}: Increase simulation speed
{bold}{/bold}: Decrease simulation speed
{yellow-fg}World Symbols:{/yellow-fg}
{bold}F, f, °{/bold}: Adult Fluffles (different sizes)
{bold}{/bold}: Baby Fluffles (not mature yet)
{bold}{/bold}: Fluffles that are mating
{bold}, , ·{/bold}: Grass (different densities)
{bold}{/bold}: Water
{bold}{/bold}: Rocks/Mountains
{cyan-fg}Life Cycle:{/cyan-fg}
Fluffles are born as babies ()
They mature around age 25 (varies by genetics)
They can mate only when mature (takes 4 turns)
Their lifespan is around 100 (varies by genetics)
Population pressure affects reproduction success
{cyan-fg}Tip:{/cyan-fg} Watch how animals with different DNA behave differently!`;
}
updateDisasterInfo(active: boolean, type: string, duration: number, intensity: number): void {
this.disasterInfo.active = active;
this.disasterInfo.type = type;
this.disasterInfo.duration = duration;
this.disasterInfo.intensity = intensity;
}
render(): void {
// Render world with improved visuals and highlight selected animal
const worldGrid = this.world.render();
const animals = this.world.getAllAnimals();
// Make sure the selected animal index is valid
if (this.selectedAnimalIndex >= animals.length && animals.length > 0) {
this.selectedAnimalIndex = animals.length - 1;
}
// Highlight the selected animal if any
if (animals.length > 0) {
const selectedAnimal = animals[this.selectedAnimalIndex];
if (selectedAnimal) { // Make sure the animal exists
const pos = selectedAnimal.getPosition();
// Add a highlight marker around the selected animal
if (this.world.isPositionValid(pos)) {
worldGrid[pos.y][pos.x] = `{inverse}${worldGrid[pos.y][pos.x]}{/inverse}`;
}
}
}
// Remove legend from world content and put it in the world info box
let worldContent = '';
for (const row of worldGrid) {
worldContent += row.join('') + '\n';
}
this.worldBox.setContent(worldContent);
// Create world info content with legend and terrain stats
let worldInfoContent = '{bold}Legend:{/bold} ';
worldInfoContent += '{green-fg}♣{/green-fg}=Grass ';
worldInfoContent += '{blue-fg}≈{/blue-fg}=Water ';
worldInfoContent += '{white-fg}▲{/white-fg}=Rock ';
worldInfoContent += '{magenta-fg}F{/magenta-fg}=Adult ';
worldInfoContent += '{cyan-fg}•{/cyan-fg}=Baby ';
worldInfoContent += '{red-fg}♥{/red-fg}=Mating ';
worldInfoContent += '{yellow-fg}⚔{/yellow-fg}=Fighting\n';
// Add terrain statistics
const terrainStats = this.world.getTerrainStats();
worldInfoContent += `{bold}Terrain:{/bold} `;
worldInfoContent += `Grass: ${terrainStats.grass} tiles | `;
worldInfoContent += `Water: ${terrainStats.water} tiles | `;
worldInfoContent += `Rock: ${terrainStats.rock} tiles | `;
worldInfoContent += `Avg. Food: ${terrainStats.averageFood.toFixed(1)}`;
// Add disaster info if active
if (this.disasterInfo.active) {
worldInfoContent += `\n{bold}{red-fg}DISASTER:{/red-fg}{/bold} ${this.disasterInfo.type} `;
worldInfoContent += `(${Math.round(this.disasterInfo.intensity * 100)}% intensity, `;
worldInfoContent += `${this.disasterInfo.duration} ticks remaining)`;
}
this.worldInfoBox.setContent(worldInfoContent);
// Render stats with improved formatting
let statsContent = '{bold}Animal Simulator Stats{/bold}\n\n';
const populationPressure = this.world.getPopulationPressure();
const maxPopulation = this.world.getMaxPopulation();
// Show population status with color coding
let populationColor = 'green';
if (populationPressure > 7) populationColor = 'red';
else if (populationPressure > 5) populationColor = 'yellow';
statsContent += `{yellow-fg}Total Animals:{/yellow-fg} ${animals.length}/${maxPopulation}\n`;
statsContent += `{yellow-fg}Population Pressure:{/yellow-fg} ${this.createProgressBar(populationPressure, 10, true)}\n\n`;
if (animals.length > 0 && this.selectedAnimalIndex < animals.length) {
const animal = animals[this.selectedAnimalIndex];
const stats = animal.getStats();
const pos = animal.getPosition();
statsContent += `{green-fg}Selected Animal:{/green-fg} ${animal.getId().substring(0, 8)}...\n`;
statsContent += `{green-fg}Animal ${this.selectedAnimalIndex + 1} of ${animals.length}{/green-fg}\n`;
statsContent += `{cyan-fg}Position:{/cyan-fg} (${pos.x}, ${pos.y})\n`;
statsContent += `{cyan-fg}Health:{/cyan-fg} ${this.createProgressBar(stats.health, 100)}\n`;
statsContent += `{cyan-fg}Energy:{/cyan-fg} ${this.createProgressBar(stats.energy, 100)}\n`;
statsContent += `{cyan-fg}Age:{/cyan-fg} ${stats.age}\n`;
statsContent += `{cyan-fg}Hunger:{/cyan-fg} ${this.createProgressBar(stats.hunger, 100, true)}\n\n`;
statsContent += '{bold}DNA:{/bold}\n';
const dna = animal.getDNA();
dna.getAllGenes().forEach(gene => {
statsContent += `{magenta-fg}${gene.name}:{/magenta-fg} ${gene.value.toFixed(2)}\n`;
});
// Add controls reminder
statsContent += '\n{bold}Controls:{/bold} ← → to select animals | H for help';
} else if (animals.length === 0) {
// No animals left
statsContent += '{red-fg}No animals alive in the simulation!{/red-fg}\n';
statsContent += 'The population has gone extinct.\n\n';
statsContent += 'Press Q to quit or restart the simulation.';
}
// Add disaster info to stats if active
if (this.disasterInfo.active) {
statsContent += '\n{bold}{red-fg}DISASTER ACTIVE{/red-fg}{/bold}\n';
statsContent += `{red-fg}Type:{/red-fg} ${this.disasterInfo.type}\n`;
statsContent += `{red-fg}Intensity:{/red-fg} ${Math.round(this.disasterInfo.intensity * 100)}%\n`;
statsContent += `{red-fg}Remaining:{/red-fg} ${this.disasterInfo.duration} ticks\n`;
}
// Add key commands reminder at the bottom of the stats box
statsContent += '\n\n{bold}Key Commands:{/bold}\n';
statsContent += '← → : Select animal | H: Help | +/-: Speed | Q: Quit';
this.statsBox.setContent(statsContent);
// Render the screen
this.screen.render();
}
private createProgressBar(value: number, max: number, inverse: boolean = false): string {
const width = 15;
const filledWidth = Math.round((value / max) * width);
const emptyWidth = width - filledWidth;
let color = 'green';
if (inverse) {
// For inverse bars like hunger, red means high (bad)
if (value > 70) color = 'red';
else if (value > 40) color = 'yellow';
} else {
// For normal bars like health, red means low (bad)
if (value < 30) color = 'red';
else if (value < 60) color = 'yellow';
}
const filled = `{${color}-fg}${'█'.repeat(filledWidth)}{/${color}-fg}`;
const empty = '░'.repeat(emptyWidth);
return `[${filled}${empty}] ${value}/${max}`;
}
}

356
src/simulation.ts Normal file
View File

@ -0,0 +1,356 @@
import { World } from './world';
import { Logger } from './logger';
import { Renderer } from './renderer';
import { DNA } from './dna';
import { Fluffles } from './animals/fluffles';
export class Simulation {
private world: World;
private logger: Logger;
private renderer: Renderer;
private running: boolean = false;
private tickInterval: number = 800; // milliseconds
private timer: NodeJS.Timeout | null = null;
private disasterActive: boolean = false;
private disasterType: string = '';
private disasterDuration: number = 0;
private disasterIntensity: number = 0;
private paused: boolean = false;
constructor(worldWidth: number, worldHeight: number) {
// Create logger with console output disabled but file output enabled
this.logger = new Logger(false, true);
this.world = new World(worldWidth, worldHeight, this.logger);
this.renderer = new Renderer(this.world, this.logger);
// Slower tick rate for better visualization
this.tickInterval = 800; // milliseconds
// Connect speed controls
this.renderer.onSpeedChange((factor: number) => {
this.tickInterval = Math.max(100, Math.min(2000, this.tickInterval * factor));
this.logger.log(`Simulation speed changed to ${Math.round(1000/this.tickInterval)} ticks/second`);
// Restart the timer if running
if (this.running && this.timer) {
clearInterval(this.timer);
this.timer = setInterval(() => {
this.tick();
}, this.tickInterval);
}
});
// Connect disaster info to renderer
this.renderer.updateDisasterInfo(this.disasterActive, this.disasterType, this.disasterDuration, this.disasterIntensity);
// Connect pause toggle
this.renderer.onPauseToggle(() => {
this.paused = !this.paused;
this.logger.log(`Simulation ${this.paused ? 'paused' : 'resumed'}`);
});
this.logger.log('Simulation initialized');
}
initialize(numFluffles: number): void {
// Limit initial population to the world's capacity
const maxPopulation = this.world.getMaxPopulation();
const actualFluffles = Math.min(numFluffles, maxPopulation);
if (actualFluffles < numFluffles) {
this.logger.log(`Limiting initial population to ${actualFluffles} (world capacity)`);
}
// Add initial animals
for (let i = 0; i < actualFluffles; i++) {
const dna = new DNA();
const position = {
x: Math.floor(Math.random() * this.world.getWidth()),
y: Math.floor(Math.random() * this.world.getHeight())
};
const fluffles = new Fluffles(dna, position, this.world, this.logger);
this.world.addAnimal(fluffles);
}
this.logger.log(`Added ${actualFluffles} fluffles to the world`);
}
start(): void {
if (this.running) return;
this.running = true;
this.logger.log('Simulation started');
// Start the simulation loop
this.timer = setInterval(() => {
this.tick();
}, this.tickInterval);
}
stop(): void {
if (!this.running) return;
this.running = false;
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.logger.log('Simulation stopped');
this.logger.close();
}
tick(): void {
// Skip updates if paused
if (this.paused) {
// Still render to show the paused state
this.renderer.render();
return;
}
// Update the world
this.world.update();
// Check for reproduction opportunities
this.checkReproduction();
// Handle ongoing disasters
if (this.disasterActive) {
this.updateDisaster();
} else {
// Occasionally introduce environmental challenges or disasters
if (Math.random() < 0.005) { // 0.5% chance per tick
this.introduceChallenge();
}
// Even more rarely, trigger a major disaster
if (Math.random() < 0.001) { // 0.1% chance per tick
this.triggerDisaster();
}
}
// Render the current state
this.renderer.render();
}
private checkReproduction(): void {
// This is now handled by the Fluffles class's mating behavior
// We don't need to check for reproduction here anymore
}
private introduceChallenge(): void {
const challengeType = Math.random();
if (challengeType < 0.3) {
// Food shortage
this.logger.log("Environmental event: Food shortage affecting the ecosystem");
this.reduceWorldFood(0.3); // Reduce food by 30%
} else if (challengeType < 0.6) {
// Disease
this.logger.log("Environmental event: Disease spreading among animals");
this.applyDisease();
} else {
// Harsh weather
this.logger.log("Environmental event: Harsh weather conditions");
this.applyHarshWeather();
}
}
private reduceWorldFood(percentage: number): void {
// Implementation to reduce food in the world
for (let y = 0; y < this.world.getHeight(); y++) {
for (let x = 0; x < this.world.getWidth(); x++) {
const position = { x, y };
const tile = this.world.getTile(position);
if (tile && tile.type === 'grass') {
const reduction = tile.foodValue * percentage;
this.world.consumeFood(position, reduction);
}
}
}
}
private applyDisease(): void {
// Implementation to apply disease effects to some animals
const animals = this.world.getAllAnimals();
const affectedCount = Math.ceil(animals.length * 0.2); // Affect 20% of population
for (let i = 0; i < affectedCount; i++) {
const randomIndex = Math.floor(Math.random() * animals.length);
const animal = animals[randomIndex];
// Apply health reduction
const stats = animal.getStats();
stats.health -= 20;
if (Math.random() < 0.3) {
this.logger.log(`Animal ${animal.getId().substring(0, 6)} affected by disease`);
}
}
}
private applyHarshWeather(): void {
// Implementation to apply harsh weather effects
const animals = this.world.getAllAnimals();
animals.forEach(animal => {
// Harsh weather affects energy levels
const stats = animal.getStats();
stats.energy = Math.max(10, stats.energy - 10);
});
}
setTickInterval(interval: number): void {
this.tickInterval = interval;
// Restart the timer if running
if (this.running && this.timer) {
clearInterval(this.timer);
this.timer = setInterval(() => {
this.tick();
}, this.tickInterval);
}
}
// Add a method to limit log frequency
private shouldLog(animalId: string): boolean {
// Only log about 10% of animal actions to reduce log spam
return Math.random() < 0.1;
}
// Add a method to trigger major disasters
private triggerDisaster(): void {
const disasterTypes = ['earthquake', 'drought', 'disease', 'coldSnap'];
const disasterType = disasterTypes[Math.floor(Math.random() * disasterTypes.length)];
// Set disaster parameters
this.disasterActive = true;
this.disasterType = disasterType;
this.disasterDuration = 20 + Math.floor(Math.random() * 30); // 20-50 ticks
this.disasterIntensity = 0.5 + (Math.random() * 0.5); // 0.5-1.0 intensity
this.logger.log(`MAJOR DISASTER: A ${disasterType} has struck the world! (Intensity: ${Math.round(this.disasterIntensity * 100)}%)`);
// Apply initial disaster effects
this.applyDisasterEffects();
// Update renderer with new disaster info
this.renderer.updateDisasterInfo(this.disasterActive, this.disasterType, this.disasterDuration, this.disasterIntensity);
}
// Add a method to update ongoing disasters
private updateDisaster(): void {
// Continue applying effects
this.applyDisasterEffects();
// Decrease duration
this.disasterDuration--;
// Check if disaster has ended
if (this.disasterDuration <= 0) {
this.logger.log(`The ${this.disasterType} has ended. The world begins to recover.`);
this.disasterActive = false;
this.disasterType = '';
}
// Update renderer with current disaster info
this.renderer.updateDisasterInfo(this.disasterActive, this.disasterType, this.disasterDuration, this.disasterIntensity);
}
// Add a method to apply disaster effects
private applyDisasterEffects(): void {
const animals = this.world.getAllAnimals();
switch (this.disasterType) {
case 'earthquake':
// Earthquakes damage terrain and injure animals
if (Math.random() < 0.3) {
// Damage random terrain
for (let i = 0; i < 5; i++) {
const x = Math.floor(Math.random() * this.world.getWidth());
const y = Math.floor(Math.random() * this.world.getHeight());
const position = { x, y };
const tile = this.world.getTile(position);
if (tile && tile.type === 'grass') {
// Reduce food value
this.world.consumeFood(position, tile.foodValue * this.disasterIntensity);
}
}
}
// Injure random animals
animals.forEach(animal => {
if (Math.random() < this.disasterIntensity * 0.2) {
const stats = animal.getStats();
stats.health -= Math.floor(20 * this.disasterIntensity);
if (Math.random() < 0.1) {
this.logger.log(`Animal ${animal.getId().substring(0, 6)} was injured by the earthquake`);
}
}
});
break;
case 'drought':
// Droughts reduce food everywhere
if (this.disasterDuration % 5 === 0) {
for (let y = 0; y < this.world.getHeight(); y++) {
for (let x = 0; x < this.world.getWidth(); x++) {
const position = { x, y };
const tile = this.world.getTile(position);
if (tile && tile.type === 'grass') {
// Reduce food value
this.world.consumeFood(position, tile.foodValue * 0.1 * this.disasterIntensity);
}
}
}
if (Math.random() < 0.3) {
this.logger.log("The drought continues to wither the vegetation...");
}
}
// Animals get thirstier
animals.forEach(animal => {
const stats = animal.getStats();
stats.hunger += this.disasterIntensity * 2;
});
break;
case 'disease':
// Disease spreads among animals
animals.forEach(animal => {
if (Math.random() < this.disasterIntensity * 0.15) {
const stats = animal.getStats();
stats.health -= Math.floor(10 * this.disasterIntensity);
if (Math.random() < 0.05) {
this.logger.log(`Animal ${animal.getId().substring(0, 6)} is suffering from the disease`);
}
}
});
break;
case 'coldSnap':
// Cold snap reduces energy and can kill animals
animals.forEach(animal => {
const stats = animal.getStats();
// Energy drains faster in cold
stats.energy -= this.disasterIntensity * 2;
// Very cold can cause health damage
if (Math.random() < this.disasterIntensity * 0.1) {
stats.health -= Math.floor(5 * this.disasterIntensity);
if (Math.random() < 0.05) {
this.logger.log(`Animal ${animal.getId().substring(0, 6)} is suffering from the cold`);
}
}
});
break;
}
}
}

251
src/world.ts Normal file
View File

@ -0,0 +1,251 @@
import { Animal } from './animal';
import { Logger } from './logger';
export interface Position {
x: number;
y: number;
}
export interface Tile {
type: 'grass' | 'water' | 'rock';
foodValue: number;
}
export class World {
private width: number;
private height: number;
private grid: Tile[][];
private animals: Map<string, Animal>;
private logger: Logger;
private maxPopulation: number;
private populationPressure: number = 0; // Tracks environmental stress
constructor(width: number, height: number, logger: Logger) {
this.width = width;
this.height = height;
this.animals = new Map();
this.logger = logger;
// Set maximum population based on world size
// A reasonable limit is about 30% of the total tiles
this.maxPopulation = Math.floor(width * height * 0.3);
// Initialize the grid
this.grid = [];
for (let y = 0; y < height; y++) {
const row: Tile[] = [];
for (let x = 0; x < width; x++) {
// Random tile type
const rand = Math.random();
let type: Tile['type'] = 'grass';
let foodValue = 0;
if (rand < 0.7) {
type = 'grass';
foodValue = 10;
} else if (rand < 0.9) {
type = 'water';
foodValue = 5;
} else {
type = 'rock';
foodValue = 0;
}
row.push({ type, foodValue });
}
this.grid.push(row);
}
this.logger.log(`Created world with dimensions ${width}x${height}`);
}
getWidth(): number {
return this.width;
}
getHeight(): number {
return this.height;
}
getTile(position: Position): Tile | null {
if (this.isPositionValid(position)) {
return this.grid[position.y][position.x];
}
return null;
}
isPositionValid(position: Position): boolean {
return position.x >= 0 && position.x < this.width &&
position.y >= 0 && position.y < this.height;
}
addAnimal(animal: Animal): boolean {
// Check if we're at population capacity
if (this.animals.size >= this.maxPopulation) {
// Only allow adding if it's a special case (like initial setup)
if (this.populationPressure < 5) {
this.populationPressure++;
this.logger.log(`Warning: Population approaching maximum capacity (${this.animals.size}/${this.maxPopulation})`);
}
return false;
}
this.animals.set(animal.getId(), animal);
// Calculate population pressure (0-10 scale)
this.populationPressure = Math.min(10, Math.floor((this.animals.size / this.maxPopulation) * 10));
if (this.animals.size % 5 === 0) {
this.logger.log(`Population now at ${this.animals.size} animals (${Math.round((this.animals.size / this.maxPopulation) * 100)}% of capacity)`);
}
return true;
}
removeAnimal(id: string): void {
this.animals.delete(id);
this.logger.log(`Removed animal ${id} from the world`);
}
getAnimal(id: string): Animal | undefined {
return this.animals.get(id);
}
getAllAnimals(): Animal[] {
return Array.from(this.animals.values());
}
update(): void {
// Update all animals
this.animals.forEach(animal => {
// Apply population pressure effects
if (this.populationPressure > 5) {
// High population pressure makes food scarcer
animal.applyEnvironmentalStress(this.populationPressure);
}
animal.update();
});
// Regrow food in grass tiles, but slower when overpopulated
const regrowthRate = Math.max(0.01, 0.1 - (this.populationPressure * 0.01));
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const tile = this.grid[y][x];
if (tile.type === 'grass' && tile.foodValue < 10) {
tile.foodValue += regrowthRate;
}
}
}
}
consumeFood(position: Position, amount: number): number {
const tile = this.getTile(position);
if (!tile) return 0;
const consumed = Math.min(tile.foodValue, amount);
tile.foodValue -= consumed;
return consumed;
}
// Get a string representation of the world for rendering
render(): string[][] {
const representation: string[][] = [];
// Initialize with terrain
for (let y = 0; y < this.height; y++) {
const row: string[] = [];
for (let x = 0; x < this.width; x++) {
const tile = this.grid[y][x];
switch (tile.type) {
case 'grass':
// Show different grass density based on food value with colors
if (tile.foodValue > 7) {
row.push('{green-fg}♣{/green-fg}'); // Lush grass
} else if (tile.foodValue > 3) {
row.push('{green-fg}♠{/green-fg}'); // Medium grass
} else {
row.push('{green-fg}·{/green-fg}'); // Sparse grass
}
break;
case 'water':
row.push('{blue-fg}≈{/blue-fg}'); // Water
break;
case 'rock':
row.push('{white-fg}▲{/white-fg}'); // Rock/mountain
break;
}
}
representation.push(row);
}
// Add animals with colors based on their attributes
this.animals.forEach(animal => {
const pos = animal.getPosition();
if (this.isPositionValid(pos)) {
representation[pos.y][pos.x] = animal.render();
}
});
return representation;
}
getAnimalsInRadius(center: Position, radius: number): Animal[] {
const result: Animal[] = [];
this.animals.forEach(animal => {
const pos = animal.getPosition();
const distance = Math.abs(pos.x - center.x) + Math.abs(pos.y - center.y);
if (distance <= radius) {
result.push(animal);
}
});
return result;
}
getPopulationPressure(): number {
return this.populationPressure;
}
getMaxPopulation(): number {
return this.maxPopulation;
}
getTerrainStats(): { grass: number, water: number, rock: number, averageFood: number } {
let grassCount = 0;
let waterCount = 0;
let rockCount = 0;
let totalFood = 0;
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const tile = this.grid[y][x];
switch (tile.type) {
case 'grass':
grassCount++;
totalFood += tile.foodValue;
break;
case 'water':
waterCount++;
break;
case 'rock':
rockCount++;
break;
}
}
}
const averageFood = grassCount > 0 ? totalFood / grassCount : 0;
return {
grass: grassCount,
water: waterCount,
rock: rockCount,
averageFood
};
}
}