diff --git a/docs/artificialintelligence/assignments/flocking/flocking.cpp b/docs/artificialintelligence/assignments/flocking/flocking.cpp deleted file mode 100644 index 4e0f283e..00000000 --- a/docs/artificialintelligence/assignments/flocking/flocking.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include -#include -#include -#include -#include - -using namespace std; - -struct Vector2 { - double x=0, y=0; - Vector2() : x(0), y(0){}; - Vector2(double x, double y) : x(x), y(y){}; - Vector2(const Vector2& v) = default; - - // unary operations - Vector2 operator-() const { return {-x, -y}; } - Vector2 operator+() const { return {x, y}; } - - // binary operations - Vector2 operator-(const Vector2& rhs) const { return {x - rhs.x, y - rhs.y}; } - Vector2 operator+(const Vector2& rhs) const { return {x + rhs.x, y + rhs.y}; } - Vector2 operator*(const double& rhs) const { return {x * rhs, y * rhs}; } - friend Vector2 operator*(const double& lhs, const Vector2& rhs) { return {lhs * rhs.x, lhs * rhs.y}; } - Vector2 operator/(const double& rhs) const { return {x / rhs, y / rhs}; } - Vector2 operator/(const Vector2& rhs) const { return {x / rhs.x, y / rhs.y}; } - bool operator!=(const Vector2& rhs) const { return (*this - rhs).sqrMagnitude() >= 1.0e-6; }; - bool operator==(const Vector2& rhs) const { return (*this - rhs).sqrMagnitude() < 1.0e-6; }; - - // assignment operation - Vector2& operator=(Vector2 const& rhs) = default; - Vector2& operator=(Vector2&& rhs) = default; - - // compound assignment operations - Vector2& operator+=(const Vector2& rhs) { - x += rhs.x; - y += rhs.y; - return *this; - } - Vector2& operator-=(const Vector2& rhs) { - x -= rhs.x; - y -= rhs.y; - return *this; - } - Vector2& operator*=(const double& rhs) { - x *= rhs; - y *= rhs; - return *this; - } - Vector2& operator/=(const double& rhs) { - x /= rhs; - y /= rhs; - return *this; - } - Vector2& operator*=(const Vector2& rhs) { - x *= rhs.x; - y *= rhs.y; - return *this; - } - Vector2& operator/=(const Vector2& rhs) { - x /= rhs.x; - y /= rhs.y; - return *this; - } - - double sqrMagnitude() const { return x * x + y * y; } - double getMagnitude() const { return sqrt(sqrMagnitude()); } - static double getMagnitude(const Vector2& vector) { return vector.getMagnitude(); } - - static double Distance(const Vector2& a, const Vector2& b) { return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); }; - double Distance(const Vector2& b) const { return sqrt((x - b.x) * (x - b.x) + (y - b.y) * (y - b.y)); }; - static double DistanceSquared(const Vector2& a, const Vector2& b) { return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); }; - double DistanceSquared(const Vector2& b) const { return (x - b.x) * (x - b.x) + (y - b.y) * (y - b.y); }; - - static Vector2 normalized(const Vector2& v) { return v.normalized(); }; - Vector2 normalized() const { - auto magnitude = getMagnitude(); - - // If the magnitude is not null - if (magnitude > 0.) - return Vector2(x, y) / magnitude; - else - return {x, y}; - }; - - static const Vector2 zero; -}; - -const Vector2 Vector2::zero = {0, 0}; - -struct Boid { - Boid(const Vector2& pos, const Vector2& vel): position(pos), velocity(vel){}; - Boid():position({0,0}), velocity({0,0}){}; - Vector2 position; - Vector2 velocity; -}; - -struct Cohesion { - double radius; - double k; - - Cohesion() = default; - - Vector2 ComputeForce(const vector& boids, int boidAgentIndex) { - return {}; - } -}; - -struct Alignment { - double radius; - double k; - - Alignment() = default; - - Vector2 ComputeForce(const vector& boids, int boidAgentIndex) { - return {}; - } -}; - -struct Separation { - double radius; - double k; - double maxForce; - - Separation() = default; - - Vector2 ComputeForce(const vector& boids, int boidAgentIndex) { - return {}; - } -}; - -// feel free to edit this main function to meet your needs -int main() { - // Variable declaration - Separation separation{}; - Alignment alignment{}; - Cohesion cohesion{}; - int numberOfBoids; - string line; // for reading until EOF - vector currentState, newState; - // Input Reading - cin >> cohesion.radius >> separation.radius >> separation.maxForce >> alignment.radius >> cohesion.k >> separation.k >> alignment.k >> numberOfBoids; - for (int i = 0; i < numberOfBoids; i++) { - Boid b; - cin >> b.position.x >> b.position.y >> b.velocity.x >> b.velocity.y; - //cout << "b.y: " << b.y << endl; - currentState.push_back(b); - newState.push_back(b); - } - cin.ignore(256, '\n'); - // Final input reading and processing - // todo: edit this. probably my code will be different than yours. - while (getline(cin, line)) { // game loop - // Use double buffer! you should read from the current and store changes in the new state. - currentState = newState; - double deltaT = stod(line); - // a vector of the sum of forces for each boid. - vector allForces = vector(numberOfBoids, {0, 0}); - // Compute Forces - for (int i = 0; i < numberOfBoids; i++) // for every boid - { - for (int j = 0; j < numberOfBoids; j++) // for every boid combination. Pre-processing loop. - { - // Process Cohesion Forces - auto dist = (currentState[i].position-currentState[j].position).getMagnitude(); - if (i != j && dist <= cohesion.radius) { - allForces[i] += cohesion.ComputeForce(currentState, i); - } - // Process Separation Forces - if (i != j && dist <= separation.radius) { - allForces[i] += separation.ComputeForce(currentState, i); - } - // Process Alignment Forces - if (i != j && dist <= alignment.radius) { - allForces[i] += alignment.ComputeForce(currentState, i); - } - } - } - // Tick Time and Output - // todo: edit this. probably my code will be different than yours. - cout << fixed << setprecision(3); // set 3 decimal places precision for output - for (int i = 0; i < numberOfBoids; i++) // for every boid - { - newState[i].velocity += allForces[i] * deltaT; - newState[i].position += currentState[i].velocity * deltaT; - cout << newState[i].position.x << " " << newState[i].position.y << " " - << newState[i].velocity.x << " " << newState[i].velocity.y << endl; - } - } - - return 0; -} diff --git a/docs/artificialintelligence/assignments/maze/README.md b/docs/artificialintelligence/assignments/maze/README.md index d97ff57f..e6fe8508 100644 --- a/docs/artificialintelligence/assignments/maze/README.md +++ b/docs/artificialintelligence/assignments/maze/README.md @@ -32,7 +32,7 @@ In order to be consistent with all languages and random functions the pseudo ran Every call to the random function should return the current number the index is pointing to, and then increment the index. If the index is greater than 99, it should be reset to 0. -## Direction decision making +## Direction decision-making In order to give consistency on how to decide the direction of the next cell, the following procedure should be followed: @@ -41,6 +41,10 @@ In order to give consistency on how to decide the direction of the next cell, th 3. If there is one visitable, do not call random, just return the first neighbor found; 4. If there are two or more visitable neighbors, call random and return the neighbor at the index of the random number modulo the number of visitable neighbors. `vec[i]%visitableCount` +!!! example "Data Structure" + + Read the [Data Structure](maze-datastructure.md) page to understand how the maze could be represented in memory. + ## Input The input is a single line with three `32 bits` unsigned integer numbers, `C`, `L` and `I`, where `C` and `L` are the number of columns and lines of the maze, respectively, and `I` is the index of the first random number to be used> `I` can varies from `0` to `99`. diff --git a/docs/artificialintelligence/assignments/maze/img.png b/docs/artificialintelligence/assignments/maze/img.png new file mode 100644 index 00000000..4281c1a8 Binary files /dev/null and b/docs/artificialintelligence/assignments/maze/img.png differ diff --git a/docs/artificialintelligence/assignments/maze/img_1.png b/docs/artificialintelligence/assignments/maze/img_1.png new file mode 100644 index 00000000..6ee34a80 Binary files /dev/null and b/docs/artificialintelligence/assignments/maze/img_1.png differ diff --git a/docs/artificialintelligence/assignments/maze/maze-datastructure.md b/docs/artificialintelligence/assignments/maze/maze-datastructure.md new file mode 100644 index 00000000..e4ce95c3 --- /dev/null +++ b/docs/artificialintelligence/assignments/maze/maze-datastructure.md @@ -0,0 +1,280 @@ +# Maze Data structures + +Mazes are a pretty common type of scenario for game development, and they can be represented in many ways. In this document, we will explore some of the most common data structures used to represent mazes. + +![img.png](img.png) + +## Grid of Rooms + +The most common way to represent a maze is a grid of rooms. It usually can be a squared grid, or rarely, a hexagonal grid. Here I will talk about the squared grid, but you can adapt the concepts to a hexagonal grid too. + +Let's simplify the maze to a grid of rooms where each room can have walls in any of the four directions (north, south, east, west) and the data the room should store. + +``` +Example: of a 3x3 grid maze + _ _ _ +|_|_|_| +|_|_|_| +|_|_|_| +``` + +## Data Structure + +The abstract idea of the room could be something like this: + +```c++ +struct RoomInfo { + // data of the room +}; + +struct Room { + RoomInfo data; + bool northWall; + bool southWall; + bool eastWall; + bool westWall; +}; +``` + +We can store the rooms into a simple 2D array: + +```c++ +Room maze[3][3]; +``` + +But 2D arrays are a bit worse in terms of cache locality compared to a 1D array, so we can flatten the 2D array into a 1D array: + +```c++ +Room maze[3*3]; +Room& getRoom(int x, int y) { + return maze[y*3 + x]; +} +``` + +To further improve the cache locality, applying concepts of Data Oriented Programming we could create a registry for our maze and isolate the room data from the walls: + +```c++ +struct RoomInfo { + // data of the room +}; +struct RoomWall { + bool north; + bool south; + bool east; + bool west; +}; +struct RoomRegistry { + int width, height; + vector data; + vector walls; + RoomInfo& getRoomInfo(int x, int y) { + return data[y*width + x]; + } + RoomWall& getRoomWall(int x, int y) { + return walls[y*width + x]; + } +}; +``` + +If you have a sparse maze, you can use a hash map to store the rooms, and be a bit more memory efficient: + +```c++ +struct RoomRegistry { + unordered_map, RoomInfo> data; + unordered_map, RoomWall> walls; + RoomInfo& getRoomInfo(int x, int y) { + return data[{x, y}]; + } + RoomWall& getRoomWall(int x, int y) { + return walls[{x, y}]; + } +}; +``` + +Or you can use pointers for the neighbors, if the pointer is null, it means there is no neighbor in that direction. But it will use more memory (pointers usually uses 8 bytes), will be less cache efficient (data information would be scattered in the heap), and you will have extra effort to query rooms at position (X,Y). + +```c++ +struct Room { + RoomInfo data; + Room* north; + Room* south; + Room* east; + Room* west; +}; +``` + +Let's assume we don't have a sparse maze, so we will use the 2D array representation. + +Now I will try to reduce the amount of memory used by the walls. Consider the current state of the RoomWall struct: + +```c++ +struct RoomWall { + bool north; // uses 1 byte + bool south; // uses 1 byte + bool east; // uses 1 byte + bool west; // uses 1 byte +}; +``` + +Can we make it more memory efficient? Yes, we can use bitfields to store the walls in a single byte: + +```c++ +struct RoomWall { + uint8_t walls; // one byte + bool hasNorthWall() const { + return walls & 1; + } + bool hasSouthWall() const { + return walls & 2; + } + bool hasEastWall() const { + return walls & 4; + } + bool hasWestWall() const { + return walls & 8; + } + void setNorthWall(bool value) { + if (value) walls |= 1; + else walls &= ~1; + } + void setSouthWall(bool value) { + if (value) walls |= 2; + else walls &= ~2; + } + void setEastWall(bool value) { + if (value) walls |= 4; + else walls &= ~4; + } + void setWestWall(bool value) { + if (value) walls |= 8; + else walls &= ~8; + } +}; // one byte +``` + +Or we can simplify it by just using data layout to do the same thing but with less code, and left the compiler to do the work: + +```c++ +struct RoomWall { + bool north: 1; // uses 1 bit + bool south: 1; // uses 1 bit + bool east: 1; // uses 1 bit + bool west: 1; // uses 1 bit +}; // uses 1 byte because byte is the smallest unit of addressable memory +``` + +There is another issue with that representation: two adjacent rooms will have duplicated walls. Ex.: the north wall of a given room is the same as the south wall of the room above it. + +To fix that issue, we will need to not use the abstraction of RoomWall anymore and store the walls directly in the RoomRegistry struct: + +```c++ +struct RoomRegistry { + int width, height; + vector data; + vector walls; +}; +``` + +Before going deep into how can we address the indexes for the walls, you need to know vector are not common vectors where each element returns a reference to a bool. Instead, it returns a proxy object that behaves like a bool. This is because the standard vector is a specialization of the vector class that is optimized for space efficiency. + +```c++ +// Example of how vector works +template +stuct vector { + // other controlling fields and methods + uint_t* data; + bool operator[](size_t index) { + return data[index / 8] & (1 << (index % 8)); + } +}; +``` + +![img_1.png](img_1.png) + +Now we have a way to address bits directly using vector, but you need to remember that for an X x Y grid, we will need X+1 vertical walls and Y+1 horizontal walls. Check the following example below for a 2x2 grid: + +``` + _ _ +|_|_| +|_|_| +``` + +So we will need 3 vertical walls and 3 horizontal walls. + +Now, we reached to the next issue. How can we address the walls in the std::vector? We will need to change our point of view from addressing Rooms at position (X,Y) to WallIntersections. Every intersection will be 2 bits to represent vertical and horizontal walls. + +``` + _ _ +|_|_| +|_|_| +``` + +In the previous example of a 2x2 grid, we will have the following intersections: + +| y | x | vertical | horizontal | +|---|---|----------|------------| +| 0 | 0 | false | true | +| 0 | 1 | false | true | +| 0 | 2 | false | false | +| 1 | 0 | true | true | +| 1 | 1 | true | true | +| 1 | 2 | true | false | +| 2 | 0 | true | true | +| 2 | 1 | true | true | +| 2 | 2 | true | false | + +So that grid is represented as an array of 18 bits like this: + +``` +01 01 00 11 11 10 11 11 10 +``` + +So if we address it via index, + +| index | value | y | x | orientation | +|-------|-------|---|---|-------------| +| 0 | 0 | 0 | 0 | vertical | +| 1 | 1 | 0 | 0 | horizontal | +| 2 | 0 | 0 | 1 | vertical | +| 3 | 1 | 0 | 1 | horizontal | +| 4 | 0 | 0 | 2 | vertical | +| 5 | 0 | 0 | 2 | horizontal | +| 6 | 1 | 1 | 0 | vertical | +| 7 | 1 | 1 | 0 | horizontal | +| 8 | 1 | 1 | 1 | vertical | +| 9 | 1 | 1 | 1 | horizontal | +| 10 | 1 | 1 | 2 | vertical | +| 11 | 0 | 1 | 2 | horizontal | +| 12 | 1 | 2 | 0 | vertical | +| 13 | 1 | 2 | 0 | horizontal | +| 14 | 1 | 2 | 1 | vertical | +| 15 | 1 | 2 | 1 | horizontal | +| 16 | 1 | 2 | 2 | vertical | +| 17 | 0 | 2 | 2 | horizontal | + +Now all we need to do is to create functions to get and set the walls for a specific room at position (X,Y). In this world reference, we will consider the top-left corner as the origin (0,0) and the bottom-right corner as (width-1, height-1). + +```c++ +struct RoomRegistry { + int width, height; + vector data; + vector walls; + bool getNorthWall(int x, int y) { + return walls[2*(y*(width+1)+x)+1]; + } + bool getSouthWall(int x, int y) { + return walls[2*((y+1)*(width+1) + x)+1]; + } + bool getWestWall(int x, int y) { + return walls[2*((y+1)*(width+1)+x)]; + } + bool getEastWall(int x, int y) { + return walls[2*((y+1)*(width+1)+x+1)]; + } + // set functions +}; +``` + +## Conclusion + +Now we are using the most memory efficient way to represent a dense maze. We learned matrix flattening, bit index addressing, data layout, and discovered why vector is a bit different from other vectors. \ No newline at end of file