From 8e101f8bfd995321ac08a16fea9a171b549a0ae4 Mon Sep 17 00:00:00 2001
From: Steph Enders <smenders@gmail.com>
Date: Sat, 17 Dec 2022 11:04:20 -0500
Subject: Support doors with keys

Add initial support for doors and keys via pre-defined mappings:
k || d
------
1 -> a
2 -> b
3 -> c
4 -> d

Any key can open any door of its mapping, but is spent once used.
May require additional testing
---
 README.md            |  17 +++++++++
 dnglib/algs.lua      |  12 +++++-
 dnglib/constants.lua |   2 +-
 dnglib/defaults.lua  |  33 +++++++++++++---
 maps/lvl3/dng.map    |  14 +++----
 src/CApi.h           | 104 +++++++++++++++++++++++++++++++++++++--------------
 src/Level.cpp        |  53 ++++++++++++++++++++++++--
 src/Level.h          |  11 ++++++
 src/SfmlUtils.h      |  38 ++++++++++++++++++-
 src/main.cpp         |   7 +++-
 10 files changed, 241 insertions(+), 50 deletions(-)

diff --git a/README.md b/README.md
index 3c17630..8158279 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,23 @@ The map format is just a text file with the following key tokens:
 | t     | Treasure (0 or more)     |
 | 0     | Empty space              |
 
+Doors and Keys: 
+
+Doors and keys are single use and map to specific doors.
+Each key (1-4) maps to a door type (a-d)
+
+| Token | Description              |
+|:------|:-------------------------|
+| 1     | Key 1, for Door a        |
+| 2     | Key 2, for Door b        |
+| 3     | Key 3, for Door c        |
+| 4     | Key 4, for Door d        |
+| a     | Door a, opened by key 1  |
+| a     | Door b, opened by key 2  |
+| a     | Door c, opened by key 3  |
+| d     | Door d, opened by key 4  |
+
+
 #### Tips
 
 Space your map out using whitespace between every token (`w w w w` instead of `wwww`) for better readability.
diff --git a/dnglib/algs.lua b/dnglib/algs.lua
index d2e6252..b5f331f 100644
--- a/dnglib/algs.lua
+++ b/dnglib/algs.lua
@@ -150,10 +150,12 @@ end
 ---@param target_pos table [x, y]
 -- @param enemies list of enemy positions (cannot pass thru enemy)
 -- @param treasures list of treasure positions (cannot pass thru treasure)
+-- @param door_keys list of key positions (cannot pass thru treasure)
+-- @param doors list of door positions (cannot pass thru treasure)
 ---@param map table 2D map array
 ---@return table best move to target [x, y]
 ---
-local function pathfind(start_pos, target_pos, enemies, treasures, map)
+local function pathfind(start_pos, target_pos, enemies, treasures, door_keys, doors, map)
     local queue = Queue:new()
     
     local visit_map = {}
@@ -173,7 +175,13 @@ local function pathfind(start_pos, target_pos, enemies, treasures, map)
     for _, t in ipairs(treasures) do
       visit_map[t.y][t.x] = MAP_WALL -- use wall value for impass
     end
-    
+    for _, k in ipairs(door_keys) do
+      visit_map[k.y][k.x] = MAP_WALL -- use wall value for impass
+    end
+    for _, d in ipairs(doors) do
+      visit_map[d.y][d.x] = MAP_WALL -- use wall value for impass
+    end
+
     -- since we mutate the visit_map let's calc this early if need be
     local best_effort = best_effort_move(start_pos, target_pos, visit_map)
 
diff --git a/dnglib/constants.lua b/dnglib/constants.lua
index 098f308..2e1fa52 100644
--- a/dnglib/constants.lua
+++ b/dnglib/constants.lua
@@ -77,7 +77,7 @@ KEY_UP = 73
 KEY_DOWN = 74
 
 -- map values
-MAP_WALL = 1
+MAP_WALL = 9
 MAP_SPACE = 0
 MAP_VISITED = -1
 
diff --git a/dnglib/defaults.lua b/dnglib/defaults.lua
index f33e603..f2251a2 100644
--- a/dnglib/defaults.lua
+++ b/dnglib/defaults.lua
@@ -60,6 +60,12 @@ keys = {
 --- setup random
 --math.randomseed(os.time())
 
+-- Checks if x,y equals for both objects
+local function is_collision(a, b)
+  return a.x == b.x and a.y == b.y
+end
+
+
 ---@param pressedKey number
 function onKeyPress(pressedKey)
     scene = c_get_scene()
@@ -102,35 +108,52 @@ function onUpdate(dt)
     treasures = c_get_treasures()
     assert(type(treasures) == "table", "treasures is not a table")
 
+    door_keys = c_get_keys()
+    assert(type(door_keys) == "table", "keys is not a table")
+
+    doors = c_get_doors()
+    assert(type(doors) == "table", "doors is not a table")
+
     map = c_get_map();
     assert(type(map) == "table", "map is not a table")
 
     for i, v in ipairs(enemies) do
         local next;
         if diff_time >= MOV_TIME then
-            next = algs.pathfind(v, player, enemies, treasures, map)
+            next = algs.pathfind(v, player, enemies, treasures, door_keys, doors, map)
         else
             next = { dx = 0, dy = 0 }
         end
 
         new_pos = c_move_enemy(v.id, next.dx, next.dy)
         assert(type(new_pos) == "table", "new_pos is not a table")
-        if new_pos.x == player.x and new_pos.y == player.y then
+        if is_collision(new_pos, player)  then
             c_trigger_loss()
         end
         enemies[i] = new_pos -- update new position for pathfinding
     end
-    treasures = c_get_treasures()
-    assert(type(treasures) == "table", "treasures is not a table")
 
     for _, t in ipairs(treasures) do
-        if t.x == player.x and t.y == player.y then
+        if is_collision(t, player) then
             c_score_treasure(t.id)
             if #treasures == 1 then
                 c_trigger_win()
             end
         end
     end
+
+    for _, k in ipairs(door_keys) do
+      if is_collision(k, player) then
+        c_take_key(k.id)
+      end
+    end
+
+    for _, d in ipairs(doors) do
+      if is_collision(d, player) then
+        c_open_door(d.id)
+      end
+    end
+
     if diff_time > MOV_TIME then
         diff_time = 0
     end
diff --git a/maps/lvl3/dng.map b/maps/lvl3/dng.map
index d250f9f..3905121 100644
--- a/maps/lvl3/dng.map
+++ b/maps/lvl3/dng.map
@@ -2,22 +2,22 @@ w w w w w w w w w w w w w w w w w w w w w
 w 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 w
 w 0 w w w w w w w w w w w w w w w w w 0 w
 w p w 0 0 0 0 0 0 0 w 0 0 0 0 0 0 w w 0 w
-w 0 w e t 0 0 0 0 0 w 0 0 0 0 t 0 w w 0 w
-w 0 w 0 0 0 0 0 0 0 w 0 0 0 0 0 0 w w 0 w
+w 0 w 0 t 0 0 0 0 0 w 0 0 0 0 t 0 w w 0 w
+w 0 w 0 e 0 0 0 0 0 w 0 0 0 0 0 0 w w 0 w
 w 0 w 0 t 0 0 0 0 0 w 0 0 0 0 0 0 w w 0 w
 w 0 w 0 0 0 0 w w w w w w 0 0 0 0 w t 0 w
-w 0 w 0 0 0 0 w t t t t w 0 0 0 0 0 w 0 w
-w 0 w 0 0 0 0 0 t t t t w e 0 0 0 0 w 0 w
-w 0 w 0 0 0 0 w t t t t 0 0 0 0 0 0 w 0 w
+w 0 w 0 0 0 0 w t t t t w 0 e 0 0 0 w 0 w
+w 0 w 0 0 0 0 b t t t t w 0 0 0 0 0 w 0 w
+w 0 a 0 0 0 0 w t t t t 0 0 0 0 0 0 w 0 w
 w 0 w 0 0 0 0 w t t t t w 0 0 0 0 0 w 0 w
 w 0 w 0 0 0 0 w w w w w w 0 0 0 0 0 w 0 w
 w 0 w 0 t 0 0 0 0 0 w 0 0 0 0 0 0 0 w 0 w
 w 0 w 0 0 0 0 0 0 0 w 0 0 0 0 0 0 0 w 0 w
-w 0 w 0 0 0 0 0 0 0 w 0 0 0 0 0 0 0 w 0 w
+w 0 w 0 0 0 2 0 0 0 w 0 0 0 0 0 0 0 w 0 w
 w 0 w 0 t 0 0 0 0 0 w 0 0 0 0 t 0 0 w 0 w
 w 0 w 0 0 0 0 0 0 0 w 0 0 0 0 0 0 0 w 0 w
 w 0 w t t t t t t t w t t t t t t t t 0 w
 w 0 w w w w w w w w t w w w w w w w 0 0 w
-w 0 t 0 0 0 0 0 0 0 t 0 0 0 0 0 0 0 0 0 w
+w 0 t 0 0 0 0 0 1 0 t 0 0 0 0 0 0 0 0 0 w
 w 0 t 0 0 0 0 0 0 0 w 0 0 0 0 0 0 0 0 0 w
 w w w w w w w w w w w w w w w w w w w w w
diff --git a/src/CApi.h b/src/CApi.h
index 4d35939..c6ffea3 100644
--- a/src/CApi.h
+++ b/src/CApi.h
@@ -35,6 +35,25 @@
 extern std::shared_ptr<Level> lvl;
 extern Scene scene;
 
+void push_position_table(lua_State *L, std::vector<Pos> positions) {
+  lua_createtable(L, int(positions.size()), 0);
+  int idx = 0;
+
+  for (auto &pos : positions) {
+    lua_pushnumber(L, ++idx);
+    lua_createtable(L, 0, 3);
+    lua_pushnumber(L, pos.token);
+    lua_setfield(L, -2, "token");
+    lua_pushnumber(L, pos.id);
+    lua_setfield(L, -2, "id");
+    lua_pushnumber(L, pos.x + 1);
+    lua_setfield(L, -2, "x");
+    lua_pushnumber(L, pos.y + 1);
+    lua_setfield(L, -2, "y");
+    lua_settable(L, -3);
+  }
+}
+
 /*
  * c_get_player_position(int x, int y)
  */
@@ -103,22 +122,7 @@ static int c_move_enemy(lua_State *L) {
  * c_get_enemies()
  */
 static int c_get_enemies(lua_State *L) {
-  lua_createtable(L, int(lvl->enemyPositions.size()), 0);
-
-  int idx = 0;
-
-  for (auto &pos : lvl->enemyPositions) {
-    lua_pushnumber(L, ++idx);
-    lua_createtable(L, 0, 3);
-    lua_pushnumber(L, pos.id);
-    lua_setfield(L, -2, "id");
-    lua_pushnumber(L, pos.x + 1);
-    lua_setfield(L, -2, "x");
-    lua_pushnumber(L, pos.y + 1);
-    lua_setfield(L, -2, "y");
-    lua_settable(L, -3);
-  }
-
+  push_position_table(L, lvl->enemyPositions);
   return 1;
 }
 
@@ -184,19 +188,7 @@ static int c_get_scene(lua_State *L) {
 }
 
 static int c_get_treasures(lua_State *L) {
-  lua_createtable(L, static_cast<int>(lvl->treasurePositions.size()), 0);
-  int idx = 0;
-  for (auto &t : lvl->treasurePositions) {
-    lua_pushnumber(L, ++idx);
-    lua_createtable(L, 0, 3);
-    lua_pushnumber(L, t.y + 1);
-    lua_setfield(L, -2, "y");
-    lua_pushnumber(L, t.x + 1);
-    lua_setfield(L, -2, "x");
-    lua_pushnumber(L, t.id);
-    lua_setfield(L, -2, "id");
-    lua_settable(L, -3);
-  }
+  push_position_table(L, lvl->treasurePositions);
   return 1;
 }
 
@@ -223,6 +215,56 @@ static int c_trigger_restart(lua_State *L) {
   return 1;
 }
 
+static int c_get_doors(lua_State *L) {
+  push_position_table(L, lvl->doorPositions);
+  return 1;
+}
+
+/**
+ * c_open_door(id)
+ * if you have a key it will open the door and use the key
+ */
+static int c_open_door(lua_State *L) {
+  int id = static_cast<int>(lua_tonumber(L, -1));
+  bool can_open = false;
+  for (int i = 0; i < lvl->doorPositions.size(); i++) {
+    if (lvl->doorPositions[i].id == id) {
+      char c = lvl->doorPositions[i].token;
+      for (int k = lvl->heldKeys.size() - 1; k >= 0; k--) {
+        char mapped_door = KEY_DOOR_MAPPING[lvl->heldKeys[k] - KEY_TKN_START];
+        if (mapped_door == c) {
+          can_open = true;
+          // erase key
+          lvl->heldKeys.erase(lvl->heldKeys.begin() + k);
+          lvl->doorPositions.erase(lvl->doorPositions.begin() + i);
+          lvl->map[lvl->doorPositions[i].y][lvl->doorPositions[i].x] =
+              BLANK_SPACE;
+          break;
+        }
+      }
+    }
+  }
+
+  return 1;
+}
+
+static int c_get_keys(lua_State *L) {
+  push_position_table(L, lvl->keyPositions);
+  return 1;
+}
+
+static int c_take_key(lua_State *L) {
+  int id = static_cast<int>(lua_tonumber(L, -1));
+  for (int i = 0; i < lvl->keyPositions.size(); i++) {
+    if (lvl->keyPositions[i].id == id) {
+      lvl->heldKeys.push_back(lvl->keyPositions[i].token);
+      lvl->keyPositions.erase(lvl->keyPositions.begin() + i);
+      break;
+    }
+  }
+  return 1;
+}
+
 // not for lua use
 void init_c_api(lua_State *L) {
   lua_register(L, "c_move_player", c_move_player);
@@ -239,6 +281,10 @@ void init_c_api(lua_State *L) {
   lua_register(L, "c_score_treasure", c_score_treasure);
   lua_register(L, "c_get_treasures", c_get_treasures);
   lua_register(L, "c_trigger_restart", c_trigger_restart);
+  lua_register(L, "c_get_doors", c_get_doors);
+  lua_register(L, "c_open_door", c_open_door);
+  lua_register(L, "c_get_keys", c_get_keys);
+  lua_register(L, "c_take_key", c_take_key);
 }
 
 #endif // DNG_CAPI_H
diff --git a/src/Level.cpp b/src/Level.cpp
index 5c8fbe0..3a013b7 100644
--- a/src/Level.cpp
+++ b/src/Level.cpp
@@ -63,17 +63,28 @@ void Level::load() {
         } else if (c == ENEMY_TKN) {
           auto e = create_enemy(x, y);
           this->enemyPositions.push_back(
-              {.id = this->nextId(), .x = x, .y = y, .sprite = e});
+              {.token = c, .id = this->nextId(), .x = x, .y = y, .sprite = e});
           this->map[y].push_back(BLANK_SPACE);
         } else if (c == PLAYER_TKN) {
           auto p = create_player(x, y);
-          this->player = {.id = playerId, .x = x, .y = y, .sprite = p};
+          this->player = {
+              .token = c, .id = playerId, .x = x, .y = y, .sprite = p};
           this->map[y].push_back(BLANK_SPACE);
         } else if (c == TREASURE_TKN) {
           auto t = create_treasure(x, y);
           this->treasurePositions.push_back(
-              {.id = this->nextId(), .x = x, .y = y, .sprite = t});
+              {.token = c, .id = this->nextId(), .x = x, .y = y, .sprite = t});
           this->map[y].push_back(BLANK_SPACE);
+        } else if (c >= KEY_TKN_START && c <= KEY_TKN_END) {
+          auto k = create_key(c, x, y);
+          this->keyPositions.push_back(
+              {.token = c, .id = this->nextId(), .x = x, .y = y, .sprite = k});
+          this->map[y].push_back(BLANK_SPACE);
+        } else if (c >= DOOR_TKN_START && c <= DOOR_TKN_END) {
+          auto d = create_door(c, x, y);
+          this->doorPositions.push_back(
+              {.token = c, .id = this->nextId(), .x = x, .y = y, .sprite = d});
+          this->map[y].push_back(WALL_SPACE);
         } else {
           continue;
         }
@@ -99,11 +110,45 @@ void Level::reset() {
   this->treasurePositions.clear();
   this->displayMap.clear();
   this->enemyPositions.clear();
+  this->keyPositions.clear();
+  this->doorPositions.clear();
   this->load();
 }
 
 bool Level::playerCanStep(int dx, int dy) const {
-  return canStep(player, dx, dy, map);
+  bool check_wall = canStep(player, dx, dy, map);
+
+  auto new_pos_x = player.x + dx;
+  auto new_pos_y = player.y + dy;
+  return check_wall ||
+         (isDoor(new_pos_x, new_pos_y) && tryDoor(new_pos_x, new_pos_y));
+}
+
+bool Level::isDoor(int x, int y) const {
+  for (auto &d : doorPositions) {
+    if (d.x == x && d.y == y) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool Level::tryDoor(int x, int y) const {
+
+  for (auto &d : doorPositions) {
+    if (d.x == x && d.y == y) {
+      char door_token = d.token;
+      for (auto &k : heldKeys) {
+        if (KEY_DOOR_MAPPING[k - KEY_TKN_START] == door_token) {
+          return true;
+        }
+      }
+      // matched door pos but not openable
+      return false;
+    }
+  }
+  // not a door?
+  return false;
 }
 
 int Level::nextId() { return idCounter++; }
diff --git a/src/Level.h b/src/Level.h
index 7b25d0b..7c2c74b 100644
--- a/src/Level.h
+++ b/src/Level.h
@@ -39,8 +39,14 @@ static const char TREASURE_TKN = 't';
 static const char ENEMY_TKN = 'e';
 static const char BLANK_SPACE = '\0';
 static const char WALL_SPACE = '#';
+static const char KEY_TKN_START = '1';  // inclusive (1, 2, 3, 4)
+static const char KEY_TKN_END = '4';    // inclusive (1, 2, 3, 4)
+static const char DOOR_TKN_START = 'a'; // inclusive (a, b, c, d)
+static const char DOOR_TKN_END = 'd';   // inclusive (a, b, c, d)
+static const char KEY_DOOR_MAPPING[4] = {'a', 'b', 'c', 'd'};
 
 struct Pos {
+  char token;
   int id;
   int x;
   int y;
@@ -54,6 +60,8 @@ public:
   ~Level() = default;
   void load();
   bool playerCanStep(int dx, int dy) const;
+  bool tryDoor(int x, int y) const;
+  bool isDoor(int x, int y) const;
   int getEnemyIndex(int id);
   bool enemyCanStep(const Pos &pos, int dx, int dy) const;
   void reset();
@@ -64,8 +72,11 @@ public:
   std::vector<std::vector<char>> map; // source copy of map
   std::vector<sf::RectangleShape> displayMap;
   Pos player;
+  std::vector<char> heldKeys;
   std::vector<Pos> enemyPositions;
   std::vector<Pos> treasurePositions;
+  std::vector<Pos> doorPositions;
+  std::vector<Pos> keyPositions;
 
 private:
   int idCounter = 1; // defaults at 1 (player always 0)
diff --git a/src/SfmlUtils.h b/src/SfmlUtils.h
index dd63cd6..86d5f62 100644
--- a/src/SfmlUtils.h
+++ b/src/SfmlUtils.h
@@ -63,6 +63,14 @@ inline sf::RectangleShape create_square(sf::Color color, int x, int y) {
   return rect;
 }
 
+inline sf::RectangleShape create_small_square(sf::Color color, int x, int y) {
+  sf::RectangleShape rect({SPRITE_SIZE - 4.f, SPRITE_SIZE - 4.f});
+  rect.setFillColor(color);
+  auto pos = to_position_xy(x, y);
+  rect.setPosition({pos.x + 2.f, pos.y + 2.f});
+  return rect;
+}
+
 inline sf::RectangleShape create_wall(int x, int y) {
   return create_square(WALL_COLOR, x, y);
 }
@@ -79,6 +87,34 @@ inline sf::RectangleShape create_treasure(int x, int y) {
   return create_square(sf::Color::Yellow, x, y);
 }
 
+inline sf::RectangleShape create_key(char t, int x, int y) {
+  switch (t) {
+  case '1':
+    return create_small_square(sf::Color::Blue, x, y);
+  case '2':
+    return create_small_square(sf::Color::Green, x, y);
+  case '3':
+    return create_small_square(sf::Color::Black, x, y);
+  case '4':
+  default:
+    return create_small_square(sf::Color::White, x, y);
+  }
+}
+
+inline sf::RectangleShape create_door(char t, int x, int y) {
+  switch (t) {
+  case 'a':
+    return create_square(sf::Color::Blue, x, y);
+  case 'b':
+    return create_square(sf::Color::Green, x, y);
+  case 'c':
+    return create_square(sf::Color::Black, x, y);
+  case 'd':
+  default:
+    return create_square(sf::Color::White, x, y);
+  }
+}
+
 inline sf::Vector2f round(const sf::Vector2f vector) {
   return sf::Vector2f{std::round(vector.x), std::round(vector.y)};
 }
@@ -107,4 +143,4 @@ inline sf::Text write_text(const char *msg, unsigned int fontSize,
   return text;
 }
 
-#endif // DNG_SFML_UTILS_H
\ No newline at end of file
+#endif // DNG_SFML_UTILS_H
diff --git a/src/main.cpp b/src/main.cpp
index 38208ec..796c3f9 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -238,9 +238,14 @@ int main(int argc, char **argv) {
         window.draw(enemy.sprite);
       }
       for (auto &treasure : lvl->treasurePositions) {
-        treasure.sprite.setPosition(to_position(treasure));
         window.draw(treasure.sprite);
       }
+      for (auto &key : lvl->keyPositions) {
+        window.draw(key.sprite);
+      }
+      for (auto &door : lvl->doorPositions) {
+        window.draw(door.sprite);
+      }
     }
 
     if (scene != Scene::LOSS) {
-- 
cgit v1.2.3-54-g00ecf