All work

Software · NP-004

Oasis Wildlife Simulator

A C++ program that simulates 27 Australian animals month by month, season by season. It’s built on a three-level class hierarchy, and I traced 14 bugs back to their causes and fixed them along the way.

Context
SEP105 Introduction to Programming for Engineers · Deakin University
When
T2 2025
My role
Final assignment: class design, code and debugging
Tools & standards
C++ · Classes & inheritance · std::vector · Header / source split
Animal8 private stats · clamped 0–100Mammal12 animalsReptile6 animalsBird9 animalsKoala× 5Wombat× 7Blue-tongue× 3Frilled× 3Kookaburra× 5Emu× 4▷ inherits from

Class hierarchy · 10 classes · 27 animals

01 · The brief

Twenty-seven animals, one month at a time

The final assignment was a C++ simulation of an oasis full of Australian animals. The user, greeted by the program as Dr. Elara, enters how many months to simulate and which month to start in. For every month, the program works out the season, applies each zone’s habitat to every animal and its food to the mammals and birds, and removes any that die. Then it prints a table of every animal’s health, hunger, hydration, energy and comfort, followed by that month’s deaths.

The population starts at 27 animals: six species in three families, living across five zones. The family rules were based on the brief’s guidelines (AnimalFamilies.txt), with a few additions of my own. The assignment was turning them into a working program, with classes, a monthly loop, death checks and output someone could read.

Starting population by species
ClassFamilyAnimalsZoneHabitatZone food / month
KoalaMammal5Whispering Woodsforest350
WombatMammal7Savannah Safarigrassland400
EBTLizardReptile3Coral Covecoastal500
FrilledLizardReptile3Scrub Countrydesert300
KookaburraBird5Lagoon Landingwetlands200
EmuBird4Coral Covecoastal500

Zone food is what GetZoneFood() returns for each zone. Reptiles don’t eat zone food in the model, and Scrub Country isn’t listed in GetZoneFood(), so it gets the 300 default.

10

Classes in the hierarchy, three levels deep

7

Source files, headers kept apart from code

14

Bugs traced to a cause and documented

0

Compiler warnings, with strict g++ flags on

02 · Class design

One base class, three families, six species

Animal holds what every animal shares: eight private stats (health, hunger, hydration, energy, comfort and three more), each one behind a getter and a setter. Mammal, Reptile and Bird add their family’s fields and behaviour. Each species then adds fields of its own, plus its own survival check, CheckLife().

Two jobs are built into the classes, so the code that uses them can’t get them wrong. Every setter goes through KeepInRange() in Animal, so no rule can push a stat outside 0–100. And at every level (Animal, family and species), the constructor adds one to that class’s counter and the destructor takes one away. That keeps the counts in each table header equal to the number of objects that actually exist.

Base class

Animal

  • −HP, HungerLvl, HydrationLvl, EnergyLvl, ComfyLvl + 3 more
  • +get…() / set…() for each, clamped by KeepInRange()
  • +Animal() / ~Animal() keep Animal_count

Family

Mammal

  • +dailyFoodRequirement, preferredHabitatType, nocturnalActivity
  • +CheckFoodAvailability(), CheckHabitat()
  • Koala × 5

    + climbingDistance

    + CheckFeeding()

    + CheckLife()

  • Wombat × 7

    + burrowingDepth

    + CheckBurrowingAbility()

    + CheckLife()

Family

Reptile

  • +coldBlooded, shedsSkin, preferredSunningSpot
  • +CheckTemperature(), ShedSkin()
  • EBTLizard × 3

    + prefBaskingTemp

    + CheckThermoregulation()

    + CheckLife()

  • FrilledLizard × 3

    + frillSize

    + CheckThreatResponse()

    + CheckLife()

Family

Bird

  • +flyingAbility, nestingPreference, migratorySeasons[4]
  • +CheckNestingSite(), FeedingBehaviour(), Migrate(), ActivityCycle()
  • Kookaburra × 5

    + huntingEfficiency

    + CheckHuntingSuccess()

    + CheckLife()

  • Emu × 4

    + foragingSkill

    + CheckForagingSuccess()

    + CheckLife()

The clamp started out as a fix (bug 03 in the log below). Before it existed, a well-fed mammal’s hunger kept falling to −270 within a year, and a lizard reached 235% health. Because every setter goes through the same function, adding it meant changing one file. Nothing that calls setHP() had to change.

Globals follow the one-definition rule: extern int Koala_count; in the header, and exactly one int Koala_count = 0; in a .cpp file. That rule came from link errors over Koala_count and Kookaburra_count (bug 14).

Animal.cppC++ · excerpt
double KeepInRange(double value) {    if (value < 0.0) {        return 0.0;    }    else if (value > 100.0) {        return 100.0;    }    else {        return value;    }}

03 · The monthly loop

One month, in order

One_Month() updates every animal in a fixed order, then runs a death pass. Order matters here: CheckHabitat() and CheckTemperature() set values outright instead of adding to them, so running them last would overwrite the changes made before them.

  1. 01

    Habitat

    • CheckHabitat()
    • CheckTemperature()
    • CheckNestingSite()

    CheckHabitat() and CheckTemperature() set comfort (and, for reptiles, energy and hydration) outright, so they run first. Birds’ CheckNestingSite() also runs first, but only adjusts comfort.

  2. 02

    Food, season & species

    • CheckFoodAvailability()
    • FeedingBehaviour()
    • ShedSkin()
    • Migrate()
    • CheckFeeding()
    • CheckBurrowingAbility()
    • …

    Adds to and takes from the stats: food against need, the season’s effects and each species’ own rules. Each mammal gets its zone’s food divided by the total number of mammals; birds run their hunting or foraging check first, because it sets how much they catch.

  3. 03

    Death pass

    • CheckLife()
    • BasicDeathCheck()
    • erase()

    Runs after every animal has been updated. The species’ own rule comes first, then the shared one. The dead are erased, and their destructors correct the counts.

The death rules every animal shares live in a single function that takes an Animal&. A Koala is an Animal, so the same function checks all six species through inheritance. It used to be copy-pasted into six separate loops. Removal uses erase(), and the loop index only moves on when nothing was removed. That keeps the survivors in the same order, so each animal can be followed from month to month.

main.cppC++ · excerpt
bool BasicDeathCheck(Animal& a) {    if (a.getHP() <= 0.0) {// HP at or below 0 means dead        return true;    }    else if (a.getEnergyLvl() <= 0.0) {// Energy at or below 0 also means dead        return true;    }    else if (a.getHungerLvl() >= 100.0) {// if hunger is 100 or higher then dead (starved)        return true;    }    else {        return false;// anything in between is alive    }}
main.cppC++ · excerpt
// KOALASfor (int i = 0; i < Koala_count; ) {    bool dead = false;// start by assuming this is alive    //species-specific first (calls Koala::CheckLife on this object)    if (!koalas[i].CheckLife()) {        dead = true;    }    else if (BasicDeathCheck(koalas[i])) {// then the general rules (a Koala IS an Animal)        dead = true;    }     if (dead) {// if this died this month        d.koala = d.koala + 1;// record 1 death in the Deaths struct return to main()        koalas.erase(koalas.begin() + i);// remove koala number i, the rest slide up one    }    else {// still alive        i = i + 1;// move to the next index only when we didn't delete    }}

04 · Debug log

Fourteen bugs, each traced to a cause

Every fix is written up in a comment where it was made: what went wrong, why, and what changed. The log below follows those comments, symptom first, because that’s how each bug showed up.

  1. BUG-01Animal_count fell 27 → 0 → −27 (every other counter by its own population on each call), and the tables stopped printing rows after the first one.main.cpp

    CausePrintProperties() took the six vectors by value. The copies were made by the compiler’s own copy constructor, which never runs Animal(), so the counters never went up for them. But ~Animal(), ~Mammal() and ~Koala() still ran when the copies were thrown away, so every call took each counter down by the number of animals it counts.

    FixPass the vectors by reference (vector<Koala>&). The same fix meant One_Month() finally changed the real animals instead of copies.

  2. BUG-02Species counts went negative after deaths.main.cpp

    CauseThe death loop decremented Koala_count by hand, and erase() destroyed the object, which ran the destructors that decrement it again. Every death was counted twice.

    FixDeleted the manual decrements. Each count now goes up in a constructor and down in a destructor, and nowhere else.

  3. BUG-03Every koala and wombat “died” in April while eating perfectly.main.cpp · Animal.cpp

    CauseNothing limited the stats. A well-fed mammal’s hunger kept falling (−270 after a year), and a “hunger ≤ −100 is dead” rule was killing the best-fed animals. Lizard HP reached 235 “percent”.

    FixKeepInRange() clamps every setter to 0–100, and that rule is gone. The death checks now work cleanly because HP or energy can land exactly on 0.

  4. BUG-04Every kookaburra and emu was starving, every single month.main.cpp

    CauseTheir daily food needs (400–480 for kookaburras, 550–700 for emus) were more than their zones held: 200 at Lagoon Landing for the kookaburras, 500 at Coral Cove for the emus.

    FixA bird now eats what it catches: zone food × hunting or foraging efficiency. Food needs were rescaled to that scale, in the same order as before.

  5. BUG-05Kookaburras got +40 comfort every month, wherever they lived.main.cpp

    CauseCheckNestingSite() was passed the bird’s own nesting preference, so it compared the preference with itself. And once it was compared with the zone’s habitat, "Wetlands" would never have equalled "wetlands".

    FixIt now gets the zone’s real habitat from GetZoneHabitat(), with lower-case habitat words throughout. A kookaburra that wants grassland but lives in the wetlands is now uncomfortable, as it should be.

  6. BUG-06Over two or three years, every kookaburra slowly got too weak to hunt and starved.Species.cpp

    CauseThe seasonal multipliers (×0.9 in winter, ×1.1 in summer) were saved back into the stat each month, so they compounded, and integer maths rounded down every time.

    FixFrilled lizards, kookaburras and emus now keep a “normal” value. Each month the season adjusts a copy of it for that month only.

  7. BUG-07Animals with no energy left didn’t die.Species.cpp

    CauseThe check was getEnergyLvl() == 0.0. Energy moves in steps like −15, so it can jump from 10 to −5 and never be exactly zero.

    FixgetEnergyLvl() <= 0.0.

  8. BUG-08Weak but well-fed kookaburras died, and starving ones lived.Species.cpp

    CauseThe hunger test in Kookaburra::CheckLife() was backwards (≤ 60), even though a higher HungerLvl means hungrier everywhere else.

    Fix> 60, consistent with the mammals’ > 70.

  9. BUG-09Every kookaburra ran out of energy by its second month.AnimalFamilies.cpp

    CauseBirds start on 50 energy and migrating costs 40 a month in a migratory season, but nothing ever gave any back. ActivityCycle() was optional in the brief, so it had been left empty.

    FixActivityCycle() now models roosting: +20 energy a month, and +10 more for a bird that’s comfortable in its nest.

  10. BUG-10Latent undefined behaviour in the death loop.AnimalFamilies.cpp

    CauseMammal() left its bool, int and double fields uninitialised until main() seeded them. erase() copies animals, and copying an uninitialised bool is undefined behaviour. Because main() happened to seed everything first it never fired, but nothing guaranteed that.

    FixMammal() now sets defaults, the same way Reptile() and Bird() already did.

  11. BUG-11A mammal’s comfort ignored whether it was living in its preferred habitat.main.cpp

    CauseCheckHabitat() was written but never called. It also sets comfort outright (= 100) instead of adding to it, so calling it last would wipe out the month’s other comfort changes.

    FixIt’s called first in each mammal’s monthly update, and the same rule applies to CheckTemperature() for reptiles. Order matters.

  12. BUG-12Table rows reshuffled after a death, so one animal couldn’t be followed from month to month.main.cpp

    CauseDead animals were removed by copying the last animal into their slot and calling pop_back().

    Fixerase() keeps everyone in order, and the loop index only moves on when nothing was removed.

  13. BUG-13Bad input quietly ran a simulation nobody asked for.main.cpp

    CauseInvalid entries were forced to the nearest valid value (0 months became 1, month 15 became December), and typing letters jammed cin for every read after it.

    FixThe program re-prompts until the input is valid, using cin.clear() and cin.ignore() to recover from a failed read.

  14. BUG-14The program wouldn’t link: “undefined reference to EBTLizard::CheckLife()”, plus “undefined symbol” errors for counters such as Koala_count and Kookaburra_count.Species.cpp

    CauseEach one was declared in a header but never defined anywhere.

    FixOne definition per symbol in its .cpp file, with extern in the header. That’s the rule every global counter in the program follows.

Bug 06 is the subtlest. Taking last month’s hunting efficiency and multiplying it by the season compounds: three winter months is ×0.9³, and int maths rounded down every time on top of that. A year of seasons left each bird slightly worse than it started, so the decline only showed up as kookaburras starving two or three years into a run.

The fix is to keep each animal’s normal value and apply the season to a copy for that month only. Winter hurts for the winter months, and things go back to normal afterwards.

Species.cppC++ · excerpt
// (same "normal value" idea as CheckThreatResponse above)void Kookaburra::CheckHuntingSuccess(string season) {    int hEff = normalHuntingEfficiency;     if (season == "Winter") {        hEff = (hEff * 90) / 100;    }    else if (season == "Summer") {        hEff = (hEff * 110) / 100;    }     if (hEff > 100) {// it's a percentage, so it can't go past 100%        hEff = 100;    }     huntingEfficiency = hEff;

05 · Run it

The real program, every start month

Interactive · the program’s own output

Choose a start month and how long to run it

These are the two inputs the program asks for. Every number below comes from its printed tables. The compiled program was run for all 12 start months, 120 months each, and the counts were read from its output, so the chart only shows what the real program printed.

Start month
Alive at the end
21 of 27
Deaths
6
Last death
Month 20
09182703691215182124months simulated

Shaded months are winter

Terminalwhat the program prints for these inputs

$ ./oasis
Hello Dr. Elara, please enter how many months you would like to simulate: 24

Next, please enter the month you want to start this simulation:
[ menu: 1 - January … 12 - December ]
Enter your choice (1-12): 1
[ initial values + 24 monthly tables ]

 ################################# Simulation Summary ###################################
 Months simulated: 24
 Animals still alive: 21 of 27
 Total deaths: 6
0 Koala Deaths in total (5 left)
0 Wombat Deaths in total (7 left)
1 Eastern Blue-Tongued Lizard Deaths in total (2 left)
1 Frilled Lizard Deaths in total (2 left)
3 Kookaburra Deaths in total (2 left)
1 Emu Deaths in total (3 left)
Show the numbers as a table
MonthKoalasWombatsBlue-tonguesFrilledKookaburrasEmusTotal
Start57335427
12 · December57233323
24 · December57222321

Counters

1,452 / 1,452

Tables where every header count matched the rows printed under it: 12 start months × 121 tables. The fixes for bugs 01 and 02 hold for ten simulated years.

Compiler

0

Warnings from g++ with -Wall -Wextra -Wpedantic -Wshadow -Wconversion, on the unmodified source.

Memory

0

Errors from AddressSanitizer and UndefinedBehaviorSanitizer across all 1,440 simulated months, including every erase() in the death loops.

06 · Ten years in

Every start month ends the same way

Run any start month for ten years and the oasis settles on the same 14 survivors: five wombats, two of each lizard, two kookaburras and three emus. The last death always falls between months 74 and 83, in a winter of the seventh year. After that the head count never moves: every survivor settles into the same yearly cycle, and nothing in the model adds animals.

The health traces show why the mammals die. Nothing in the koala’s rules ever adds health back. Poor eucalyptus costs 5 HP in each winter month, and in autumn as well for the two koalas that eat the fewest leaves and climb too low to reach the fresh ones. So every koala is on a countdown. Wombats live or die on one number: an effective burrow depth under 100 cm costs 3 HP a month, and 100 cm or deeper gives 3 back. The two shallowest burrowers die, and the deeper ones stay at or near full health.

14 of 27

Survivors after 120 months, from every start month

0 of 5

Koalas left: no rule ever restores their health

100 cm

Effective burrow depth that decides a wombat’s fate

Koalas HP, 10-year run from January

 

050100024487296120months#1–2#3–5

#1–2 died in month 40 · #3–5 died in month 79

Wombats HP, 10-year run from January

 

050100024487296120months#1#2#3, #4–7

#1 died in month 34 · #2 died in month 66 · #3, #4–7 alive at month 120

Show every animal as a table
AnimalHP at 122460120Outcome
Koala #1 · climbing distance 207040——Died in month 40 (April)
Koala #2 · climbing distance 357040——Died in month 40 (April)
Koala #3 · climbing distance 50857025—Died in month 79 (July)
Koala #4 · climbing distance 65857025—Died in month 79 (July)
Koala #5 · climbing distance 80857025—Died in month 79 (July)
Wombat #1 · burrow 80 cm6428——Died in month 34 (October)
Wombat #2 · burrow 93.5 cm76584—Died in month 66 (June)
Wombat #3 · burrow 107 cm100100100100Alive
Wombat #4 · burrow 120.5 cm100100100100Alive
Wombat #5 · burrow 134 cm100100100100Alive
Wombat #6 · burrow 147.5 cm100100100100Alive
Wombat #7 · burrow 150 cm100100100100Alive

07 · Next steps

Where it could go next

  1. N1Let well-fed animals recover health.Nothing gives a koala HP back, so the model guarantees every koala dies. A small gain for a good feed would let it settle into a real balance.
  2. N2Add births.The brief marked BirthProcess() as “omitted”, so it was left empty and the population can only fall. With births, a long run would mean something.
  3. N3One loop instead of six.Make CheckLife() and a monthly update virtual in Animal and keep every animal in one vector<unique_ptr<Animal>>. Then adding a species no longer means editing main().
  4. N4Rules as data, with tests.Put zone food, season multipliers and death thresholds in tables, with unit tests on the edge cases behind bugs 03, 07 and 08.