diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..870398e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +github: ovnanova +ko_fi: ovnanova +liberapay: ovnanova diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..9fd45e0 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/Cargo.toml b/Cargo.toml index 5616d5a..77fbac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,14 @@ edition = "2021" [dependencies] crossterm = "0.28.1" rand = "0.8.5" + +[dev-dependencies] +unicode-segmentation = "1.12.0" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +debug = false diff --git a/README.md b/README.md index 724cb71..1126676 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # AAAAAAAAA -A̴̖͚̭̟ͤ̀A͕̬͍̓͟҉͉Ą͖̜͍̭͗̕A̘͇͓͔ͫ̕͝Ä̷̲̯̗̩́͠Ḁ̸̸̬͎͚̍À̷̼̟̖͉̈́A̛̳̻̬̘ͤ͜A̧͍̲̠͕͊̕A̹͏̣̩̞ͦ͘A̸̪̤̤̘ͮ̕A̤͉̤͌́͡ͅÀ̤͍̘̜ͬ͡A̧̨͎͉̬̝ͩA̖̣̟͓ͯ͢͡A̴̻̗͕̥̅̕A̻̗̫̹͋͢͟Ä̛̘̬̺͈̕A̛̺̬̪̘ͤ͞A̶̗̹̝̳ͭ̕À̸͚̙̫̬̉Ą̸̤̖̺ͩͅA̧͕͉̫̤͗͡Ā̧̭̰̤̙͞A̡͙͈̱͙ͩ͝A̴͕̗̭̱̍̀A̼̜̖̰̐͟͞Ą̱͔͇̳̋̕A̖͉͚̬͑͞͝À̷̮͎͍͍́A̢̡̟͙̯̾ͅÁ̵̝̫̭͕̓A̢̲̦̟ͨ͏̳A͉͏̯̯̻ͫ͟Ā̛̰̞͕͡ͅẠ͏̯͉̲̊͞Ȁ̸̬͖͙͈͝Å̢̤͕̠̦͘A̹҉͎̜̦́͟A͕̬̟̤͆͟͞A̴͙͎͕̹ͫ͡A̖̗̬ͪ͝͏̬A̶͙̻̣̱͐͢Ą̦̮͙̰̀̃A̠͏̫̦̠̈́͟A̧̛̩̹͈͇̚A̡̬͉̝̭͒́Ä̷̸͚̹̥ͅẢ̸̛͖̼̗̼A̸̢̺̣͙̱ͤÁ̶̼̝̠̠̏A̬͙͈͇͗͘͝A̵̯͍̹̬͌͡A͙̯̯̓͘͏̣Ạ̸̼̪͓́̽A̙̤̠̫ͥ͡͠A̴̢͍̝̮̭̾Ą͈̳̗̼̀͞Ä̴̛͍̠̘́ͅA͙͚̝̗ͪ͢͞A͍̳̰͓̒̕͝Ǎ̸̡͇̯͎͇A̢̧̮͖͚͕̓Ą̧̭͎͕̳͌Å͚͖͉̬͘͜A̸̧͎͙̘̞̿Á̙̹̬̱̒̕A̙҉͈̠͉͋͝Ạ̰̱͖͛̕͜Ạ̶̭̗͔ͩ͠A̛̰̠̟͔̓͝A̯̳̠̣̅͜͜A̧̢̲̮̰͎̒A̷̙̫̫͔̒́A̵̹̬̗̟ͬ͡A͔̹̙͓ͬ̕͞A̛͉͓͕̫͂͢À̛͉̬̰͔̿A̜̳͈̬ͯ͡͝Å̸̻̣̥҉͍Ă̴̳̜̘͠ͅA̻̯͖ͮ͘͢ͅǍ̛̮̬ͅ͏̳Ą̛̞̥̫̟̂A̷͕̫̥̱͊͞Á͔̻͔͍̌͜A̶̯̩͎ͨ͜ͅÄ̧͉̘͚͖̀Ȧ͔̜̯͖́͘A̜̰̺̦ͬ́͝A̲̤͕̖͑́̕A̸̡̪̖̼ͥͅA̷̫͎̠̟ͪ͝Ạ̴̤͈̱̒̀Ă̡̩̙̲̤͞Á̸̳͖̤̞͞Ã̫̪͖͈̕͠A̵̳̬͉̿͝ͅÀ̖̘̪̾͠ͅĄ͍̩̣̀̂ͅA͙͏̵̘͚̻̓À̛̞̯̫̙͝A̳͚̪̦ͬ͠͡A̳̪͙̟̚͟͡A̡̭̬̬̟̅͢A̶̛̜̦̯ͣͅA̸̩͉̺͔ͪ͡A̼̙͖̰ͥ́͠Å̢̮͖̭̟͢A͈̟̲͛͜͟ͅÂ̪̣͎̣͘͢A͓̞͉̤̾͠͡A̧̗̘̖̲͂̕Ȧ̡͕̼̮̙͜Ȃ̶̦͚̞̫͢A̢̲͙̰͎ͦ́Ä̴̢͇̯̤̫Á̸͎̠͖̹͞Á͚̹͈̠̎́À̡̧̜͙͇̙Ą̛͕̲̲̬ͦĄ̹̻̥̖̊͠A͖̭̯̱̚͢͜A̴̜͍̫̐͏̜A̤̼̱͉͛͘͢A̞̫̠̯ͥ̀͢A̡̱͎̻͈̒͟Ä̶͈̟̹̤̀A͙͏̻̩̰̿͡A̦̖̼͍ͪ̕͡A̬͇͚͕ͤ͢͝A̸̧̩̠͔͈͊A͎҉̛͇̣͓̇Ą͓͓̖͋͜ͅA̧͇̱̰̱̔͡A̧̹̬͍̜̎͠Á͍͉̤̭̀͘A̧̪͖̠̼͊̀Ȃ̷̟̞̹͔͞A̴̫̥̫͛̀ͅĄ̝̞̙͎̄͢À͕̖͍̻͜͢A̷̢̻̞͕̰̾A̛͉͈̝ͨ́ͅÅ̗͚͙̲͟͝Ą͕̗̻̣͒͘À͙̼̲̥ͤ͜Ả͔̘̻̮͜͡Ả̴̞̳͕̫̕Ä̵̹͇͎̯͘A̳̱͓̼ͬ́̕A̠҉̴͇̥̤̀Ă̴̳̲̮̜͝A̶͔͇̻͙̒͝À̼̖̭̤ͬ͢Ḁ̛̬̩̏͘ͅA̷̛̱̣̼̯ͣÂ̛̹̗̭̱͠A̡͚͚̠̠ͪ͢A̺̰̖̠̓͝͡A̱̫̲̹̅̀͜A̧͖͙͓̼ͤ͢A̵̱͈̙̯͌͠A̩̮̫̻ͣ͘͝A̛̺̦̥͔̓͘A̵̖̟͉ͦ҉̣Ä̧̞̞͕̮͜A̻̮̪̲̅͞͡A̧̛̱̣̭̝̾A̦͏̵͔͖̘ͣÁ̷̯̮̞͈ͥA̘͍̒͢ͅ͏̠A̴̛̘̹̙͔͗A̢̨͔̝̺͙ͯA̖͏̺̜̫͋͡Å̸̩̟̞̖͘A̹̞͚̱̎͡͝A̷̸̩̤͍͈ͥȂ͙̭͎͘͡ͅA̹̱̠̣̔́͠A̵̡̝̤͇ͩͅḀ̧̞̦̣́̅Ą̛͚̖̬̫̈́Ǎ̡̧̠̘̪͈A̳͇̰̳ͤ͜͝Ã̰̲͉̦̕͝A̢̨̗̻̲̭̋A̸̧͚̟̖̮͋A̶̠͈̘ͯ͡ͅ +A̴̖͚̭̟ͤ̀A͕̬͍̓͟҉͉Ą͖̜͍̭͗̕A̘͇͓͔ͫ̕͝Ä̷̲̯̗̩́͠Ḁ̸̸̬͎͚̍À̷̼̟̖͉̈́A̛̳̻̬̘ͤ͜A̧͍̲̠͕͊̕A̹͏̣̩̞ͦ͘A̸̪̤̤̘ͮ̕A̤͉̤͌́͡ͅÀ̤͍̘̜ͬ͡A̧̨͎͉̬̝ͩA̖̣̟͓ͯ͢͡A̴̻̗͕̥̅̕A̻̗̫̹͋͢͟Ä̛̘̬̺͈̕A̛̺̬̪̘ͤ͞A̶̗̹̝̳ͭ̕À̸͚̙̫̬̉Ą̸̤̖̺ͩͅA̧͕͉̫̤͗͡Ā̧̭̰̤̙͞A̡͙͈̱͙ͩ͝A̴͕̗̭̱̍̀A̼̜̖̰̐͟͞Ą̱͔͇̳̋̕A̖͉͚̬͑͞͝À̷̮͎͍͍́A̢̡̟͙̯̾ͅÁ̵̝̫̭͕̓A̢̲̦̟ͨ͏̳A͉͏̯̯̻ͫ͟Ā̛̰̞͕͡ͅẠ͏̯͉̲̊͞Ȁ̸̬͖͙͈͝Å̢̤͕̠̦͘A̹҉͎̜̦́͟A͕̬̟̤͆͟͞A̴͙͎͕̹ͫ͡A̖̗̬ͪ͝͏̬A̶͙̻̣̱͐͢Ą̦̮͙̰̀̃A̠͏̫̦̠̈́͟A̧̛̩̹͈͇̚A̡̬͉̝̭͒́Ä̷̸͚̹̥ͅẢ̸̛͖̼̗̼A̸̢̺̣͙̱ͤÁ̶̼̝̠̠̏A̬͙͈͇͗͘͝A̵̯͍̹̬͌͡A͙̯̯̓͘͏̣Ạ̸̼̪͓́̽A̙̤̠̫ͥ͡͠A̴̢͍̝̮̭̾Ą͈̳̗̼̀͞Ä̴̛͍̠̘́ͅA͙͚̝̗ͪ͢͞A͍̳̰͓̒̕͝Ǎ̸̡͇̯͎͇A̢̧̮͖͚͕̓Ą̧̭͎͕̳͌Å͚͖͉̬͘͜A̸̧͎͙̘̞̿Á̙̹̬̱̒̕A̙҉͈̠͉͋͝Ạ̰̱͖͛̕͜Ạ̶̭̗͔ͩ͠A̛̰̠̟͔̓͝A̯̳̠̣̅͜͜A̧̢̲̮̰͎̒A̷̙̫̫͔̒́A̵̹̬̗̟ͬ͡A͔̹̙͓ͬ̕͞A̛͉͓͕̫͂͢À̛͉̬̰͔̿A̜̳͈̬ͯ͡͝Å̸̻̣̥҉͍Ă̴̳̜̘͠ͅA̻̯͖ͮ͘͢ͅǍ̛̮̬ͅ͏̳Ą̛̞̥̫̟̂A̷͕̫̥̱͊͞Á͔̻͔͍̌͜A̶̯̩͎ͨ͜ͅÄ̧͉̘͚͖̀Ȧ͔̜̯͖́͘A̜̰̺̦ͬ́͝A̲̤͕̖͑́̕A̸̡̪̖̼ͥͅA̷̫͎̠̟ͪ͝Ạ̴̤͈̱̒̀Ă̡̩̙̲̤͞Á̸̳͖̤̞͞Ã̫̪͖͈̕͠A̵̳̬͉̿͝ͅÀ̖̘̪̾͠ͅĄ͍̩̣̀̂ͅA͙͏̵̘͚̻̓À̛̞̯̫̙͝A̳͚̪̦ͬ͠͡A̳̪͙̟̚͟͡A̡̭̬̬̟̅͢A̶̛̜̦̯ͣͅA̸̩͉̺͔ͪ͡A̼̙͖̰ͥ́͠Å̢̮͖̭̟͢A͈̟̲͛͜͟ͅÂ̪̣͎̣͘͢A͓̞͉̤̾͠͡A̧̗̘̖̲͂̕Ȧ̡͕̼̮̙͜Ȃ̶̦͚̞̫͢A̢̲͙̰͎ͦ́Ä̴̢͇̯̤̫Á̸͎̠͖̹͞Á͚̹͈̠̎́À̡̧̜͙͇̙Ą̛͕̲̲̬ͦĄ̹̻̥̖̊͠A͖̭̯̱̚͢͜A̴̜͍̫̐͏̜A̤̼̱͉͛͘͢A̞̫̠̯ͥ̀͢A̡̱͎̻͈̒͟Ä̶͈̟̹̤̀A͙͏̻̩̰̿͡A̦̖̼͍ͪ̕͡A̬͇͚͕ͤ͢͝A̸̧̩̠͔͈͊A͎҉̛͇̣͓̇Ą͓͓̖͋͜ͅA̧͇̱̰̱̔͡A̧̹̬͍̜̎͠Á͍͉̤̭̀͘A̧̪͖̠̼͊̀Ȃ̷̟̞̹͔͞A̴̫̥̫͛̀ͅĄ̝̞̙͎̄͢À͕̖͍̻͜͢A̷̢̻̞͕̰̾A̛͉͈̝ͨ́ͅÅ̗͚͙̲͟͝Ą͕̗̻̣͒͘À͙̼̲̥ͤ͜Ả͔̘̻̮͜͡Ả̴̞̳͕̫̕Ä̵̹͇͎̯͘A̳̱͓̼ͬ́̕A̠҉̴͇̥̤̀Ă̴̳̲̮̜͝A̶͔͇̻͙̒͝À̼̖̭̤ͬ͢Ḁ̛̬̩̏͘ͅA̴̖͚̭̟ͤ̀A͕̬͍̓͟҉͉Ą͖̜͍̭͗̕A̘͇͓͔ͫ̕͝Ä̷̲̯̗̩́͠Ḁ̸̸̬͎͚̍À̷̼̟̖͉̈́A̛̳̻̬̘ͤ͜A̧͍̲̠͕͊̕A̹͏̣̩̞ͦ͘A̸̪̤̤̘ͮ̕A̤͉̤͌́͡ͅÀ̤͍̘̜ͬ͡A̧̨͎͉̬̝ͩA̖̣̟͓ͯ͢͡A̴̻̗͕̥̅̕A̻̗̫̹͋͢͟Ä̛̘̬̺͈̕A̛̺̬̪̘ͤ͞A̶̗̹̝̳ͭ̕À̸͚̙̫̬̉Ą̸̤̖̺ͩͅA̧͕͉̫̤͗͡Ā̧̭̰̤̙͞A̡͙͈̱͙ͩ͝A̴͕̗̭̱̍̀A̼̜̖̰̐͟͞Ą̱͔͇̳̋̕A̖͉͚̬͑͞͝À̷̮͎͍͍́A̢̡̟͙̯̾ͅÁ̵̝̫̭͕̓A̢̲̦̟ͨ͏̳A͉͏̯̯̻ͫ͟Ā̛̰̞͕͡ͅẠ͏̯͉̲̊͞Ȁ̸̬͖͙͈͝Å̢̤͕̠̦͘A̹҉͎̜̦́͟A͕̬̟̤͆͟͞A̴͙͎͕̹ͫ͡A̖̗̬ͪ͝͏̬A̶͙̻̣̱͐͢Ą̦̮͙̰̀̃A̠͏̫̦̠̈́͟A̧̛̩̹͈͇̚A̡̬͉̝̭͒́Ä̷̸͚̹̥ͅẢ̸̛͖̼̗̼A̸̢̺̣͙̱ͤÁ̶̼̝̠̠̏A̬͙͈͇͗͘͝A̵̯͍̹̬͌͡A͙̯̯̓͘͏̣Ạ̸̼̪͓́̽A̙̤̠̫ͥ͡͠A̴̢͍̝̮̭̾Ą͈̳̗̼̀͞Ä̴̛͍̠̘́ͅA͙͚̝̗ͪ͢͞A͍̳̰͓̒̕͝Ǎ̸̡͇̯͎͇A̢̧̮͖͚͕̓Ą̧̭͎͕̳͌Å͚͖͉̬͘͜A̸̧͎͙̘̞̿Á̙̹̬̱̒̕A̙҉͈̠͉͋͝Ạ̰̱͖͛̕͜Ạ̶̭̗͔ͩ͠A̛̰̠̟͔̓͝A̯̳̠̣̅͜͜A̧̢̲̮̰͎̒A̷̙̫̫͔̒́A̵̹̬̗̟ͬ͡A͔̹̙͓ͬ̕͞A̛͉͓͕̫͂͢À̛͉̬̰͔̿A̜̳͈̬ͯ͡͝Å̸̻̣̥҉͍Ă̴̳̜̘͠ͅA̻̯͖ͮ͘͢ͅǍ̛̮̬ͅ͏̳Ą̛̞̥̫̟̂A̷͕̫̥̱͊͞Á͔̻͔͍̌͜A̶̯̩͎ͨ͜ͅÄ̧͉̘͚͖̀Ȧ͔̜̯͖́͘A̜̰̺̦ͬ́͝A̲̤͕̖͑́̕A̸̡̪̖̼ͥͅA̷̫͎̠̟ͪ͝Ạ̴̤͈̱̒̀Ă̡̩̙̲̤͞Á̸̳͖̤̞͞Ã̫̪͖͈̕͠A̵̳̬͉̿͝ͅÀ̖̘̪̾͠ͅĄ͍̩̣̀̂ͅA͙͏̵̘͚̻̓À̛̞̯̫̙͝A̳͚̪̦ͬ͠͡A̳̪͙̟̚͟͡A̡̭̬̬̟̅͢A̶̛̜̦̯ͣͅA̸̩͉̺͔ͪ͡A̼̙͖̰ͥ́͠Å̢̮͖̭̟͢A͈̟̲͛͜͟ͅÂ̪̣͎̣͘͢A͓̞͉̤̾͠͡A̧̗̘̖̲͂̕Ȧ̡͕̼̮̙͜Ȃ̶̦͚̞̫͢A̢̲͙̰͎ͦ́Ä̴̢͇̯̤̫Á̸͎̠͖̹͞Á͚̹͈̠̎́À̡̧̜͙͇̙Ą̛͕̲̲̬ͦĄ̹̻̥̖̊͠A͖̭̯̱̚͢͜A̴̜͍̫̐͏̜A̤̼̱͉͛͘͢A̞̫̠̯ͥ̀͢A̡̱͎̻͈̒͟Ä̶͈̟̹̤̀A͙͏̻̩̰̿͡A̦̖̼͍ͪ̕͡A̬͇͚͕ͤ͢͝A̸̧̩̠͔͈͊A͎҉̛͇̣͓̇Ą͓͓̖͋͜ͅA̧͇̱̰̱̔͡A̧̹̬͍̜̎͠Á͍͉̤̭̀͘A̧̪͖̠̼͊̀Ȃ̷̟̞̹͔͞A̴̫̥̫͛̀ͅĄ̝̞̙͎̄͢À͕̖͍̻͜͢A̷̢̻̞͕̰̾A̛͉͈̝ͨ́ͅÅ̗͚͙̲͟͝Ą͕̗̻̣͒͘À͙̼̲̥ͤ͜Ả͔̘̻̮͜͡Ả̴̞̳͕̫̕Ä̵̹͇͎̯͘A̳̱͓̼ͬ́̕A̠҉̴͇̥̤̀Ă̴̳̲̮̜͝A̶͔͇̻͙̒͝À̼̖̭̤ͬ͢Ḁ̛̬̩̏͘ͅ + +[AAAAAAAAA.webm](https://github.com/user-attachments/assets/4c710cad-0067-4a95-8e51-7fab017c4bc1) diff --git a/src/main.rs b/src/main.rs index 4d16393..27634dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -252,3 +252,226 @@ fn main() -> io::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use unicode_segmentation::UnicodeSegmentation; + + #[test] + fn test_random_string_length() { + for _ in 0..1000000 { + let s = random_string(); + let grapheme_count = s.graphemes(true).count(); + assert!( + (1..=32).contains(&grapheme_count), + "Grapheme count out of bounds: {} (string: {})", + grapheme_count, + s + ); + } + } + + #[test] + fn test_all_chars_appear() { + let mut appearances = HashSet::<&'static str>::new(); + for _ in 0..10000 { + let s = random_string(); + appearances.extend(CHAR_SET.iter().filter(|&&c| s.contains(c))); + } + assert_eq!( + appearances.len(), + CHAR_SET.len(), + "Not all characters appeared in 10000 iterations" + ); + } + + #[test] + fn test_stream_direction_changes() { + let mut stream = Stream::new(80, 24); + let mut direction_changes = 0; + let mut last_direction = stream.direction.get_offset(); + + for _ in 0..1000 { + stream.update(80, 24); + let new_direction = stream.direction.get_offset(); + if new_direction != last_direction { + direction_changes += 1; + } + last_direction = new_direction; + } + + assert!( + direction_changes > 50, + "Stream should change direction frequently, only changed {} times", + direction_changes + ); + } + + #[test] + fn test_stream_bounds() { + let mut stream = Stream::new(80, 24); + for _ in 0..10000 { + stream.update(80, 24); + assert!( + stream.x >= 1 && stream.x <= 78, + "X out of bounds: {}", + stream.x + ); + assert!(stream.y >= 1, "Y below minimum: {}", stream.y); + } + } + + #[test] + fn test_color_distribution() { + let mut color_counts = std::collections::HashMap::new(); + for _ in 0..10000 { + let color = random_color(); + *color_counts.entry(format!("{:?}", color)).or_insert(0) += 1; + } + + // Check that each color appeared at least once + assert!( + color_counts.len() >= COLORS.len(), + "Not all colors appeared: {:?}", + color_counts + ); + + // Verify primary colors appear more often than accents + for weight in COLORS { + match weight { + Weight::Primary(c, _) => { + let count = color_counts.get(&format!("{:?}", c)).unwrap_or(&0); + assert!( + count > &500, + "Primary color {:?} appeared only {} times", + c, + count + ); + } + Weight::Accent(c, _) => { + let count = color_counts.get(&format!("{:?}", c)).unwrap_or(&0); + assert!( + count > &100, + "Accent color {:?} appeared only {} times", + c, + count + ); + } + } + } + } + + #[test] + fn test_chaos_probability() { + let mut new_streams = 0; + let trials = 10000; + + for _ in 0..trials { + if rand::thread_rng().gen_bool(CHAOS) { + new_streams += 1; + } + } + + let actual_probability = new_streams as f64 / trials as f64; + assert!( + (actual_probability - CHAOS).abs() < 0.02, + "Chaos probability {} significantly deviated from expected {}", + actual_probability, + CHAOS + ); + } + + #[test] + fn test_random_string_content() { + let s = random_string(); + assert!( + s.chars() + .all(|c| CHAR_SET.iter().any(|&set| set.contains(c))), + "Invalid characters in string: {}", + s + ); + } + + #[test] + fn test_color_weights() { + let total: u8 = COLORS + .iter() + .map(|c| match c { + Weight::Primary(_, w) | Weight::Accent(_, w) => w, + }) + .sum(); + assert!(total > 0, "Total color weights must be positive"); + + let mut counts = std::collections::HashMap::new(); + for _ in 0..1000 { + let color = random_color(); + *counts.entry(color).or_insert(0) += 1; + } + + // Verify primary colors appear more frequently than accents + for color_weight in COLORS { + match color_weight { + Weight::Primary(c, _) => { + let count = counts.get(c).unwrap_or(&0); + assert!(*count > 100, "Primary color {:?} appeared too rarely", c); + } + Weight::Accent(_, _) => {} + } + } + } + + #[test] + fn test_stream_boundaries() { + let mut stream = Stream::new(80, 24); + + // Test multiple updates to ensure boundaries are respected + for _ in 0..1000 { + stream.update(80, 24); + assert!( + stream.x > 0 && stream.x < 79, + "X position out of bounds: {}", + stream.x + ); + assert!(stream.y > 0, "Y position below zero: {}", stream.y); + } + } + + #[test] + fn test_direction_distribution() { + let mut counts = std::collections::HashMap::new(); + for _ in 0..1000 { + let dir = Direction::random(); + let offset = dir.get_offset(); + *counts.entry(offset).or_insert(0) += 1; + } + + // Check that all directions are used + assert_eq!(counts.len(), 8, "Not all directions were generated"); + + // Check for roughly even distribution + for (_offset, count) in counts { + assert!(count > 50, "Direction appeared too rarely: {} times", count); + } + } + + #[test] + fn test_stream_movement() { + let mut stream = Stream::new(80, 24); + let initial_pos = (stream.x, stream.y); + + // Store a few positions to verify movement + let mut positions = vec![initial_pos]; + for _ in 0..10 { + stream.update(80, 24); + positions.push((stream.x, stream.y)); + } + + // Verify that the stream actually moved + assert!( + positions.windows(2).any(|w| w[0] != w[1]), + "Stream didn't move from initial position" + ); + } +}