Video Coming Soon...

Created by Zed A. Shaw Updated 2026-06-19 17:00:40

49: Ranges and Views

Rewrite our combat engine to use ranges, views, and lambdas.

References:

https://en.cppreference.com/cpp/ranges https://en.cppreference.com/cpp/algorithm/ranges

The Code

First give a brief intro to ranges:

View Source file ex49a.cpp Only

#include <fmt/core.h>
#include <string>
#include <vector>
#include <algorithm>
#include <ranges>
#include <iostream>
#include <list>
#include <map>

using std::string;

using Node = std::tuple<int, char, std::string>;

using namespace std::views;

int main() {
  std::vector<Node> nodes{
    {1, 'A', "α"},
    {2, 'B', "β"},
    {3, 'C', "γ"},
    {4, 'D', "δ"},
    {5, 'E', "ε"},
  };

  std::vector<std::string> names {
    "Zed",
    "Frank",
    "Joe",
  };

  for(auto e : nodes | take(2) | elements<1>) {
    fmt::println("{}", e);
  }

  for(auto [i, val] : nodes | elements<1> | enumerate) {
    fmt::println("i={}, val={}", i, val);
  }

  auto just_chars = nodes | elements<2>;

  for(auto [the_char, the_name] : zip(just_chars, names)) {
    fmt::println("char={}, name={}", the_char, the_name);
  }

  auto stringify = [](auto a, auto b) {
    return fmt::format("{}: {}", a, b);
  };

  auto new_list = zip_transform(stringify, just_chars, names);

  for(auto x : new_list) {
    fmt::println("{}", x);
  }
}

The Breakdown

line of code
Description.

The Discussion

Blah blah.

Updating the Engine

Not sure if I'll use both or have it as a challenge mode and just use one:

First the crazy version:

See how I did it.View Source file ex49_crazy.cpp Only

#include <fmt/core.h>
#include <string>
#include <random>
#include <memory>
#include <algorithm>
#include <unordered_map>
#include <vector>
#include <ranges>

using std::shared_ptr, std::make_shared, std::string;

std::random_device RNG;
std::mt19937 GENERATOR(RNG());

struct Combatant {
  string name;
  int damage{0};
  int hp{0};
  bool dead{false};
};

using ArenaID = int;
using Arena = std::unordered_map<ArenaID, shared_ptr<Combatant>>;
using CombatQueue = std::vector<size_t>;

int random_number(int from, int to) {
  std::uniform_int_distribution rand(from, to);
  return rand(GENERATOR);
}

void fight(shared_ptr<Combatant> attacker, shared_ptr<Combatant> defender) {
  int damage = random_number(0, attacker->damage);

  if(damage > 0) {
    defender->hp -= damage;
    fmt::println("{} hit {} for {}. {} now has {} hp",
        attacker->name, defender->name, damage,
        defender->name, defender->hp);
  } else {
    fmt::println("{} missed {}!",
        attacker->name, defender->name);
  }

  if(defender->hp <= 0) {
    fmt::println("{} died. {} wins! {} has {} HP",
        defender->name, attacker->name,
        attacker->name, attacker->hp);
  }
}

void fill_arena(Arena& arena, int combatants) {
  using namespace std::ranges;

  auto inserter = [&](size_t arena_id) {
    string name = fmt::format("Giant Rat #{}", arena_id);
    int hp = random_number(5,20);
    int damage = random_number(2,10);
    arena.try_emplace(arena_id, make_shared<Combatant>(name, damage, hp, 1.4f));
  };

  for_each(views::iota(0, combatants), inserter);
}

bool combat_active(Arena& arena, shared_ptr<Combatant> player) {
  return !arena.empty() && player->hp > 0;
}

CombatQueue queue_combat(Arena& arena) {
  using namespace std::ranges;

  CombatQueue queue;

  copy(views::keys(arena), std::back_inserter(queue));

  std::shuffle(queue.begin(), queue.end(), GENERATOR);

  return queue;
}

void run_combat(shared_ptr<Combatant> player, shared_ptr<Combatant> who) {
  bool player_turn = random_number(0,1);

  if(player_turn) {
    fight(who, player);
  } else {
    fight(player, who);
  }

  // now how to remove when dead
  if(who->hp <= 0) {
    who->dead = true;
  }
}

void cull_the_dead(Arena& arena) {
  using namespace std::ranges;

  CombatQueue dead;

  auto is_dead = [&](auto& tuple) -> bool { return tuple.second->dead; };
  auto eraser = [&](auto id) { arena.erase(id); };

  copy(arena | views::filter(is_dead) | views::elements<0>, std::back_inserter(dead));

  for_each(dead, eraser);
}

int main(int argc, char* argv[]) {
  using namespace std::ranges;

  Arena arena;
  auto player = make_shared<Combatant>("Player", 20, 50, 1.4f);

  fill_arena(arena, 10);

  // now with multiple combatants we have to keep going until the player dies
  while(combat_active(arena, player)) {
    auto whos_next = queue_combat(arena);

    for_each(whos_next, [&](auto enemy_id) {
      run_combat(player, arena[enemy_id]);
    });

    cull_the_dead(arena);
  }

  if(arena.empty()) {
    fmt::println("The Player Wins! You defeated all of the rats.");
  } else {
    fmt::println("You Died! The Rats Win.");
  }
}

Then tone it down and do a more sane one:

See how I did it.View Source file ex49_sane.cpp Only

#include <fmt/core.h>
#include <string>
#include <random>
#include <memory>
#include <algorithm>
#include <unordered_map>
#include <vector>
#include <ranges>

using std::shared_ptr, std::make_shared, std::string;

std::random_device RNG;
std::mt19937 GENERATOR(RNG());

struct Combatant {
  string name;
  int damage{0};
  int hp{0};
  bool dead{false};
};

using ArenaID = int;
using Arena = std::unordered_map<ArenaID, shared_ptr<Combatant>>;
using CombatQueue = std::vector<size_t>;

int random_number(int from, int to) {
  std::uniform_int_distribution rand(from, to);
  return rand(GENERATOR);
}

void fight(shared_ptr<Combatant> attacker, shared_ptr<Combatant> defender) {
  int damage = random_number(0, attacker->damage);

  if(damage > 0) {
    defender->hp -= damage;
    fmt::println("{} hit {} for {}. {} now has {} hp",
        attacker->name, defender->name, damage,
        defender->name, defender->hp);
  } else {
    fmt::println("{} missed {}!",
        attacker->name, defender->name);
  }

  if(defender->hp <= 0) {
    fmt::println("{} died. {} wins! {} has {} HP",
        defender->name, attacker->name,
        attacker->name, attacker->hp);
  }
}

void fill_arena(Arena& arena, int combatants) {
  using namespace std::ranges;

  for(auto arena_id : views::iota(0, combatants)) {
    string name = fmt::format("Giant Rat #{}", arena_id);
    int hp = random_number(5,20);
    int damage = random_number(2,10);
    arena.try_emplace(arena_id, make_shared<Combatant>(name, damage, hp, 1.4f));
  }
}


bool combat_active(Arena& arena, shared_ptr<Combatant> player) {
  return !arena.empty() && player->hp > 0;
}

CombatQueue queue_combat(Arena& arena) {
  using namespace std::ranges;

  auto queue = arena | views::keys | to<CombatQueue>();

  shuffle(queue, GENERATOR);

  return queue;
}

void run_combat(shared_ptr<Combatant> player, shared_ptr<Combatant> who) {
  bool player_turn = random_number(0,1);

  if(player_turn) {
    fight(who, player);
  } else {
    fight(player, who);
  }

  // now how to remove when dead
  if(who->hp <= 0) {
    who->dead = true;
  }
}

void cull_the_dead(Arena& arena) {
  using namespace std::ranges;

  auto is_dead = [&](auto& tuple) -> bool { return tuple.second->dead; };

  auto dead = arena | views::filter(is_dead) | views::elements<0> | to<CombatQueue>();

  for(auto id : dead) {
    arena.erase(id);
  }
}

int main(int argc, char* argv[]) {
  using namespace std::ranges;

  Arena arena;
  auto player = make_shared<Combatant>("Player", 20, 50, 1.4f);

  fill_arena(arena, 10);

  // now with multiple combatants we have to keep going until the player dies
  while(combat_active(arena, player)) {
    auto whos_next = queue_combat(arena);

    for_each(whos_next, [&](auto enemy_id) {
      run_combat(player, arena[enemy_id]);
    });

    cull_the_dead(arena);
  }

  if(arena.empty()) {
    fmt::println("The Player Wins! You defeated all of the rats.");
  } else {
    fmt::println("You Died! The Rats Win.");
  }
}

Further Study

Previous Lesson Next Lesson

Register for Learn C++ the Hard Way

Register to gain access to additional videos which demonstrate each exercise. Videos are priced to cover the cost of hosting.