#pragma once #include #include #include #include #include #include #include #include "point.hpp" #include "rand.hpp" #include "dbc.hpp" /* * # What is This Shit? * * Announcing the Shape Iterators, or "shiterators" for short. The best shite * for C++ for-loops since that [one youtube * video](https://www.youtube.com/watch?v=rX0ItVEVjHc) told everyone to * recreate SQL databases with structs. You could also say these are Shaw's * Iterators, but either way they are the _shite_. Or are they shit? You decide. * Maybe they're "shite"? * * A shiterator is a simple generator that converts 2D shapes into a 1D stream * of x/y coordinates. You give it a matrix, some parameters like start, end, * etc. and each time you call `next()` you get the next viable x/y coordinate to * complete the shape. This makes them far superior to _any_ existing for-loop * technology because shiterators operate _intelligently_ in shapes. Other * [programming pundits](https://www.youtube.com/watch?v=tD5NrevFtbU) will say * their 7000 line "easy to maintain" switch statements are better at drawing * shapes, but they're wrong. My way of making a for-loop do stuff is vastly * superior because it doesn't use a switch _or_ a virtual function _or_ * inheritance at all. That means they have to be the _fastest_. Feel free to run * them 1000 times and bask in the glory of 1 nanosecond difference performance. * * It's science and shite. * * More importantly, shiterators are simple and easy to use. They're so easy to * use you _don't even use the 3rd part of the for-loop_. What? You read that right, * not only have I managed to eliminate _both_ massive horrible to maintain switches, * and also avoided virtual functions, but I've also _eliminated one entire part * of the for-loop_. This obviously makes them way faster than other inferior * three-clause-loop-trash. Just look at this comparison: * * ```cpp * for(it = trash.begin(); it != trash.end(); it++) { * std::cout << it << std::endl; * } * ``` * * ```cpp * for(each_cell it{mat}; it.next();) { * std::cout << mat[it.y][it.x] << std::endl; * } * ``` * * Obviously this will outperform _any_ iterator invented in the last 30 years, but the best * thing about shiterators is their composability and ability to work simultaneously across * multiple matrices in one loop: * * ```cpp * for(line it{start, end}; it.next();) { * for(compass neighbor{walls, it.x, it.y}; neighbor.next();) { * if(walls[neighbor.y][neighbor.x] == 1) { * wall_update[it.y][it.x] = walls[it.y][it.x] + 10; * } * } * } * ``` * * This code sample (maybe, because I didn't run it) draws a line from * `start` to `end` then looks at each neighbor on a compass (north, south, east, west) * at each point to see if it's set to 1. If it is then it copies that cell over to * another matrix with +10. Why would you need this? Your Wizard just shot a fireball * down a corridor and you need to see if anything in the path is within 1 square of it. * * You _also_ don't even need to use a for-loop. Yes, you can harken back to the old * days when we did everything RAW inside a Duff's Device between a while-loop for * that PERFORMANCE because who cares about maintenance? You're a game developer! Tests? * Don't need a test if it runs fine on Sony Playstation only. Maintenance? You're moving * on to the next project in two weeks anyway right?! Use that while-loop and a shiterator * to really help that next guy: * * ```cpp * box it{walls, center_x, center_y, 20}; * while(it.next()) { * walls[it.y][it.x] = 1; * } * ``` * * ## Shiterator "Guarantees" * * Just like Rust [guarantees no memory leaks](https://github.com/pop-os/cosmic-comp/issues/1133), * a shiterator tries to ensure a few things, if it can: * * 1. All x/y values will be within the Matrix you give it. The `line` shiterator doesn't though. * 2. They try to not store anything and only calculate the math necessary to linearlize the shape. * 3. You can store them and incrementally call next to get the next value. * 4. You should be able to compose them together on the same Matrix or different matrices of the same dimensions. * 5. Most of them will only require 1 for-loop, the few that require 2 only do this so you can draw the inside of a shape. `circle` is like this. * 6. They don't assume any particular classes or require subclassing. As long as the type given enables `mat[y][x]` (row major) access then it'll work. * 7. The matrix given to a shiterator isn't actually attached to it, so you can use one matrix to setup an iterator, then apply the x/y values to any other matrix of the same dimensions. Great for smart copying and transforming. * 8. More importantly, shiterators _do not return any values from the matrix_. They only do the math for coordinates and leave it to you to work your matrix. * * These shiterators are used all over the game to do map rendering, randomization, drawing, nearly everything that involves a shape. * * ## Algorithms I Need * * I'm currently looking for a few algorithms, so if you know how to do these let me know: * * 1. _Flood fill_ This turns out to be really hard because most algorithms require keeping track of visited cells with a queue, recursion, etc. * 2. _Random rectangle fill_ I have something that mostly works but it's really only random across each y-axis, then separate y-axes are randomized. * 3. _Dijkstra Map_ I have a Dijkstra algorithm but it's not in this style yet. Look in `worldbuilder.cpp` for my current implementation. * 4. _Viewport_ Currently working on this but I need to have a rectangle I can move around as a viewport. * * * ## Usage * * Check the `matrix.hpp` for an example if you want to make it more conventient for your own type. * * ## Thanks * * Special thanks to Amit and hirdrac for their help with the math and for * giving me the initial idea. hirdrac doesn't want to be held responsible for * this travesty but he showed me that you can do iteration and _not_ use the * weird C++ iterators. Amit did a lot to show me how to do these calculations * without branching. Thanks to you both--and to everyone else--for helping me while I * stream my development. * * ### SERIOUS DISCLAIMER * * I am horribly bad at trigonometry and graphics algorithms, so if you've got an idea to improve them * or find a bug shoot me an email at help@learncodethehardway.com. */ namespace shiterator { using std::vector, std::queue, std::array; using std::min, std::max, std::floor; template using BaseRow = vector; template using Base = vector>; template inline Base make(size_t width, size_t height) { Base result(height, BaseRow(width)); return result; } /* * Just a quick thing to reset a matrix to a value. */ template inline void assign(MAT &out, VAL new_value) { for(auto &row : out) { row.assign(row.size(), new_value); } } /* * Tells you if a coordinate is in bounds of the matrix * and therefore safe to use. */ template inline bool inbounds(MAT &mat, size_t x, size_t y) { // since Point.x and Point.y are size_t any negatives are massive return (y < mat.size()) && (x < mat[0].size()); } /* * Gives the width of a matrix. Assumes row major (y/x) * and vector API .size(). */ template inline size_t width(MAT &mat) { return mat[0].size(); } /* * Same as shiterator::width but just the height. */ template inline size_t height(MAT &mat) { return mat.size(); } /* * These are internal calculations that help * with keeping track of the next x coordinate. */ inline size_t next_x(size_t x, size_t width) { return (x + 1) * ((x + 1) < width); } /* * Same as next_x but updates the next y coordinate. * It uses the fact that when x==0 you have a new * line so increment y. */ inline size_t next_y(size_t x, size_t y) { return y + (x == 0); } /* * Figures out if you're at the end of the shape, * which is usually when y > height. */ inline bool at_end(size_t y, size_t height) { return y < height; } /* * Determines if you're at the end of a row. */ inline bool end_row(size_t x, size_t width) { return x == width - 1; } /* * Most basic shiterator. It just goes through * every cell in the matrix in linear order * with not tracking of anything else. */ template struct each_cell_t { size_t x = ~0; size_t y = ~0; size_t width = 0; size_t height = 0; each_cell_t(MAT &mat) { height = shiterator::height(mat); width = shiterator::width(mat); } bool next() { x = next_x(x, width); y = next_y(x, y); return at_end(y, height); } }; /* * This is just each_cell_t but it sets * a boolean value `bool row` so you can * tell when you've reached the end of a * row. This is mostly used for printing * out a matrix and similar just drawing the * whole thing with its boundaries. */ template struct each_row_t { size_t x = ~0; size_t y = ~0; size_t width = 0; size_t height = 0; bool row = false; each_row_t(MAT &mat) { height = shiterator::height(mat); width = shiterator::width(mat); } bool next() { x = next_x(x, width); y = next_y(x, y); row = end_row(x, width); return at_end(y, height); } }; /* * This is a CENTERED box, that will create * a centered rectangle around a point of a * certain dimension. This kind of needs a * rewrite but if you want a rectangle from * a upper corner then use rectangle_t type. * * Passing 1 parameter for the size will make * a square. */ template struct box_t { size_t from_x; size_t from_y; size_t x = 0; // these are set in constructor size_t y = 0; // again, no fancy ~ trick needed size_t left = 0; size_t top = 0; size_t right = 0; size_t bottom = 0; box_t(MAT &mat, size_t at_x, size_t at_y, size_t size) : box_t(mat, at_x, at_y, size, size) { } box_t(MAT &mat, size_t at_x, size_t at_y, size_t width, size_t height) : from_x(at_x), from_y(at_y) { size_t h = shiterator::height(mat); size_t w = shiterator::width(mat); // keeps it from going below zero // need extra -1 to compensate for the first next() left = max(from_x, width) - width; x = left - 1; // must be -1 for next() // keeps it from going above width right = min(from_x + width + 1, w); // same for these two top = max(from_y, height) - height; y = top - (left == 0); bottom = min(from_y + height + 1, h); } bool next() { // calc next but allow to go to 0 for next x = next_x(x, right); // x will go to 0, which signals new line y = next_y(x, y); // this must go here // if x==0 then this moves it to min_x x = max(x, left); // and done return at_end(y, bottom); } /* * This was useful for doing quick lighting * calculations, and I might need to implement * it in other shiterators. It gives the distance * to the center from the current x/y. */ float distance() { int dx = from_x - x; int dy = from_y - y; return sqrt((dx * dx) + (dy * dy)); } }; /* * Stupid simple compass shape North/South/East/West. * This comes up a _ton_ when doing searching, flood * algorithms, collision, etc. Probably not the * fastest way to do it but good enough. */ template struct compass_t { size_t x = 0; // these are set in constructor size_t y = 0; // again, no fancy ~ trick needed array x_dirs{0, 1, 0, -1}; array y_dirs{-1, 0, 1, 0}; size_t max_dirs=0; size_t dir = ~0; compass_t(MAT &mat, size_t x, size_t y) : x(x), y(y) { array x_in{0, 1, 0, -1}; array y_in{-1, 0, 1, 0}; for(size_t i = 0; i < 4; i++) { int nx = x + x_in[i]; int ny = y + y_in[i]; if(shiterator::inbounds(mat, nx, ny)) { x_dirs[max_dirs] = nx; y_dirs[max_dirs] = ny; max_dirs++; } } } bool next() { dir++; if(dir < max_dirs) { x = x_dirs[dir]; y = y_dirs[dir]; return true; } else { return false; } } }; /* * Draws a line from start to end using a algorithm from * https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm * No idea if the one I picked is best but it's the one * that works in the shiterator requirements and produced * good results. * * _WARNING_: This one doesn't check if the start/end are * within your Matrix, as it's assumed _you_ did that * already. */ struct line { int x; int y; int x1; int y1; int sx; int sy; int dx; int dy; int error; line(Point start, Point end) : x(start.x), y(start.y), x1(end.x), y1(end.y) { dx = std::abs(x1 - x); sx = x < x1 ? 1 : -1; dy = std::abs(y1 - y) * -1; sy = y < y1 ? 1 : -1; error = dx + dy; } bool next() { if(x != x1 || y != y1) { int e2 = 2 * error; if(e2 >= dy) { error = error + dy; x = x + sx; } if(e2 <= dx) { error = error + dx; y = y + sy; } return true; } else { return false; } } }; /* * Draws a simple circle using a fairly naive algorithm * but one that actually worked. So, so, so, so many * circle drawing algorithms described online don't work * or are flat wrong. Even the very best I could find * did overdrawing of multiple lines or simply got the * math wrong. Keep in mind, _I_ am bad at this trig math * so if I'm finding errors in your circle drawing then * you got problems. * * This one is real simple, and works. If you got better * then take the challenge but be ready to get it wrong. */ template struct circle_t { float center_x; float center_y; float radius = 0.0f; int y = 0; int dx = 0; int dy = 0; int left = 0; int right = 0; int top = 0; int bottom = 0; int width = 0; int height = 0; circle_t(MAT &mat, Point center, float radius) : center_x(center.x), center_y(center.y), radius(radius) { width = shiterator::width(mat); height = shiterator::height(mat); top = max(int(floor(center_y - radius)), 0); bottom = min(int(floor(center_y + radius)), height - 1); y = top; } bool next() { y++; if(y <= bottom) { dy = y - center_y; dx = floor(sqrt(radius * radius - dy * dy)); left = max(0, int(center_x) - dx); right = min(width, int(center_x) + dx + 1); return true; } else { return false; } } }; /* * Basic rectangle shiterator, and like box and rando_rect_t you can * pass only 1 parameter for size to do a square. */ template struct rectangle_t { int x; int y; int top; int left; int width; int height; int right; int bottom; rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t size) : rectangle_t(mat, start_x, start_y, size, size) { } rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) : top(start_y), left(start_x), width(width), height(height) { size_t h = shiterator::height(mat); size_t w = shiterator::width(mat); y = start_y - 1; x = left - 1; // must be -1 for next() right = min(start_x + width, w); y = start_y; bottom = min(start_y + height, h); } bool next() { x = next_x(x, right); y = next_y(x, y); x = max(x, left); return at_end(y, bottom); } }; /* * WIP: This one is used to place entities randomly but * could be used for effects like random destruction of floors. * It simply "wraps" the rectangle_t but randomizes the x/y values * using a random starting point. This makes it random across the * x-axis but only partially random across the y. */ template struct rando_rect_t { int x; int y; int x_offset; int y_offset; rectangle_t it; rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t size) : rando_rect_t(mat, start_x, start_y, size, size) { } rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) : it{mat, start_x, start_y, width, height} { x_offset = Random::uniform(0, int(width)); y_offset = Random::uniform(0, int(height)); } bool next() { bool done = it.next(); x = it.left + ((it.x + x_offset) % it.width); y = it.top + ((it.y + y_offset) % it.height); return done; } }; /* * BROKEN: I'm actually not sure what I'm trying to * do here yet. */ template struct viewport_t { Point start; // this is the point in the map size_t x; size_t y; // this is the point inside the box, start at 0 size_t view_x = ~0; size_t view_y = ~0; // viewport width/height size_t width; size_t height; viewport_t(MAT &mat, Point start, int max_x, int max_y) : start(start), x(start.x-1), y(start.y-1) { width = std::min(size_t(max_x), shiterator::width(mat) - start.x); height = std::min(size_t(max_y), shiterator::height(mat) - start.y); fmt::println("viewport_t max_x, max_y {},{} vs matrix {},{}, x={}, y={}", max_x, max_y, shiterator::width(mat), shiterator::height(mat), x, y); } bool next() { y = next_y(x, y); x = next_x(x, width); view_x = next_x(view_x, width); view_y = next_y(view_x, view_y); return at_end(y, height); } }; }