mirror of
https://github.com/tcsenpai/fluffles.git
synced 2025-06-01 08:30:03 +00:00
first commit
This commit is contained in:
commit
6aeae4fdfd
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
logs
|
||||
bun.lockb
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
94
README.md
Normal 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
143
bun.lock
Normal 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
23
index.ts
Normal 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
17
package.json
Normal 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
324
src/animal.ts
Normal 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
400
src/animals/fluffles.ts
Normal 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
260
src/animals/rabbit.ts
Normal 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
234
src/dna.ts
Normal 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
80
src/logger.ts
Normal 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
424
src/renderer.ts
Normal 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
356
src/simulation.ts
Normal 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
251
src/world.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user