#include #include #include "GLFW/glfw3.h" #include "data/Array.h" #include "data/ArrayList.h" #include "data/HashMap.h" #include "data/List.h" #include "math/Vector.h" #include "rendering/Shader.h" #include "rendering/Texture.h" #include "rendering/VertexBuffer.h" #include "rendering/Window.h" #include "utils/Color.h" #include "utils/Logger.h" #include "utils/Random.h" static Shader shader; static Shader fontShader; static Buffer buffer; static Buffer fontBuffer; static VertexBuffer vertexBuffer; static VertexBuffer fontVertexBuffer; static Texture fontTexture; static Random rng(0); static int restartButton = 0; static int restartMapButton = 0; static int turns = 0; enum class Resource { NOTHING, CLAY, ORE, WHEAT, SHEEP, WOOD }; static const char* RESOURCE_NAME[] = {"Nothing", "Clay", "Ore", "Wheat", "Sheep", "Wood"}; static const Color4 RESOURCE_COLOR[] = { Color4(160, 120, 0, 255), Color4(220, 96, 0, 255), Color4(128, 128, 128, 255), Color4(255, 200, 0, 255), Color4(40, 200, 40, 255), Color4(0, 120, 0, 255)}; static const Vector2 MIN_BORDER(-0.9f, -0.9f); static const Vector2 MAX_BORDER(0.9f, 0.9f); static const Vector2 AREA = MAX_BORDER - MIN_BORDER; static const float RADIUS = AREA[0] / (5.0f * 2.0f * cosf(30.0f * static_cast(M_PI) / 180.0f)); static const float LINE_LENGTH = RADIUS * sinf(30.0f * static_cast(M_PI) / 180.0f) * 2.0f; static const float WIDTH = AREA[0] / 5.0f; static void addTriangle(const Vector2 a, const Vector2 b, const Vector2 c, const Color4& color) { buffer.add(a).add(color); buffer.add(b).add(color); buffer.add(c).add(color); } static void addSquare(const Vector2 mid, float size, const Color4& color) { size *= 0.5f; Vector2 a = mid + Vector2(-size, size); Vector2 b = mid + Vector2(size, size); Vector2 c = mid + Vector2(-size, -size); Vector2 d = mid + Vector2(size, -size); addTriangle(a, b, c, color); addTriangle(b, d, c, color); } static void addString(const char* s, const Vector2& pos, float size, const Color4& color) { int index = 0; Vector2 topLeft = pos; while(s[index] != '\0') { Vector2 topRight = topLeft + Vector2(size, 0.0f); Vector2 bottomLeft = topLeft + Vector2(0.0f, -size); Vector2 bottomRight = topRight + Vector2(0.0f, -size); int c = s[index]; float minX = static_cast(c % 16) / 16.0f + 1.0f / 128.0f; float maxX = minX + 6.0f / 128.0f; float minY = static_cast(c / 16) / 16.0f; float maxY = minY + 1.0f / 16.0f; fontBuffer.add(topLeft).add(Vector2(minX, minY)).add(color); fontBuffer.add(bottomLeft).add(Vector2(minX, maxY)).add(color); fontBuffer.add(topRight).add(Vector2(maxX, minY)).add(color); fontBuffer.add(bottomLeft).add(Vector2(minX, maxY)).add(color); fontBuffer.add(topRight).add(Vector2(maxX, minY)).add(color); fontBuffer.add(bottomRight).add(Vector2(maxX, maxY)).add(color); topLeft[0] += size; index++; } } static int getQualityOfNumber(int number) { if(number < 2 || number > 12 || number == 7) { return 0; } return number < 7 ? number - 1 : 13 - number; } struct Player { int id = -1; Color4 color; HashMap resources; int settlements = 5; int paths = 15; int cities = 4; Player() { giveResource(Resource::CLAY, 0); giveResource(Resource::ORE, 0); giveResource(Resource::SHEEP, 0); giveResource(Resource::WHEAT, 0); giveResource(Resource::WOOD, 0); } int getResource(Resource r) const { const int* i = resources.search(static_cast(r)); return i == nullptr ? 0 : *i; } void placeStartSettlement(); void giveResource(Resource r, int amount) { LOG_INFO(StringBuffer<256>("Player ") .append(id) .append(" gets ") .append(amount) .append(" ") .append(RESOURCE_NAME[static_cast(r)])); int* i = resources.search(static_cast(r)); if(i == nullptr) { resources.tryEmplace(static_cast(r), amount); } else { *i += amount; } LOG_INFO(StringBuffer<256>(resources)); } void removeResource(Resource r, int amount) { LOG_INFO(StringBuffer<256>("Player ") .append(id) .append(" pays ") .append(amount) .append(" ") .append(RESOURCE_NAME[static_cast(r)])); int* i = resources.search(static_cast(r)); if(i == nullptr) { return; } *i -= amount; LOG_INFO(StringBuffer<256>(resources)); } void takeTurn(); bool swap() { int max = 0; int maxResource = 0; for(const auto& entry : resources.entries()) { if(entry.value > max) { max = entry.value; maxResource = entry.getKey(); } } int min = max; int minResource = 0; for(const auto& entry : resources.entries()) { if(entry.value < min) { min = entry.value; minResource = entry.getKey(); } } if(max >= 4) { removeResource(static_cast(maxResource), 4); giveResource(static_cast(minResource), 1); return true; } return false; } int getPoints() const; void reset() { removeResource(Resource::CLAY, getResource(Resource::CLAY)); removeResource(Resource::ORE, getResource(Resource::ORE)); removeResource(Resource::SHEEP, getResource(Resource::SHEEP)); removeResource(Resource::WHEAT, getResource(Resource::WHEAT)); removeResource(Resource::WOOD, getResource(Resource::WOOD)); settlements = 5; paths = 15; cities = 4; } }; static Array players; static int currentPlayer = 0; struct Hexagon { Resource resource = Resource::NOTHING; Vector2 mid; Array corners; int number = 0; Hexagon() : corners(-1) { } void addToBuffer() const { Color4 color = RESOURCE_COLOR[static_cast(resource)]; float angle = 2.0f * static_cast(M_PI) / 6.0f; for(int i = 0; i < 6; i++) { Vector2 a(sinf(angle * static_cast(i)) * RADIUS, cosf(angle * static_cast(i)) * RADIUS); Vector2 b(sinf(angle * static_cast(i + 1)) * RADIUS, cosf(angle * static_cast(i + 1)) * RADIUS); addTriangle(a + mid, b + mid, mid, color); } } void addStringToBuffer() const { if(number == 0) { return; } StringBuffer<16> s(number); constexpr float SIZE = 0.1f; addString(s, mid + Vector2(-SIZE * static_cast(s.getLength()), SIZE) * 0.5f, SIZE, Color4(0, 0, 0, 0)); } Vector2 getTopCorner() const { return mid + Vector2(0.0f, RADIUS); } Vector2 getBottomCorner() const { return mid - Vector2(0.0f, RADIUS); } Vector2 getLeftTopCorner() const { return mid - Vector2(WIDTH, -LINE_LENGTH) * 0.5f; } Vector2 getLeftBottomCorner() const { return mid - Vector2(WIDTH, LINE_LENGTH) * 0.5f; } Vector2 getRightTopCorner() const { return mid + Vector2(WIDTH, LINE_LENGTH) * 0.5f; } Vector2 getRightBottomCorner() const { return mid + Vector2(WIDTH, -LINE_LENGTH) * 0.5f; } }; static Array gHexagons; struct Corner { int player = -1; bool city = false; Vector2 mid; ArrayList hexagons; ArrayList paths; void addToBuffer() const { if(player < 0 || player >= players.getLength()) { return; } addSquare(mid, 0.075f * (static_cast(city) + 1.0f), players[player].color); } bool validForSettlement() const; bool doesAnyPathBelongToPlayer(int player) const; bool isFree() const { return player < 0 || player >= players.getLength(); } void reset() { player = -1; city = false; } int getQuality() { int q = 0; for(int h : hexagons) { q += getQualityOfNumber(gHexagons[h].number); } return q; } }; static Array gCorners; struct Path { int player = -1; Vector2 from; Vector2 to; int cornerA = -1; int cornerB = -1; void addToBuffer() const { if(player < 0 || player >= players.getLength()) { return; } Vector2 diff = to - from; Vector2 normal(diff[1], -diff[0]); normal.normalize(); normal *= 0.01f; Vector2 a = from + normal; Vector2 b = from - normal; Vector2 c = to + normal; Vector2 d = to - normal; addTriangle(a, c, b, players[player].color); addTriangle(c, d, b, players[player].color); } Vector2 getMid() const { return (to + from) * 0.5f; } const Corner& getOtherCorner(const Corner& c) const { if(&(gCorners[cornerA]) == &c) { return gCorners[cornerB]; } else if(&(gCorners[cornerB]) == &c) { return gCorners[cornerA]; } return c; } bool isFree() const { return player < 0 || player >= players.getLength(); } void reset() { player = -1; } }; static List gPaths; static int findBestCorner() { int current = -1; int quality = 0; for(int i = 0; i < gCorners.getLength(); i++) { if(!gCorners[i].validForSettlement()) { continue; } int newQuality = 0; for(int h : gCorners[i].hexagons) { newQuality += getQualityOfNumber(gHexagons[h].number); } if(newQuality > quality) { quality = newQuality; current = i; } } return current; } void Player::placeStartSettlement() { int c = findBestCorner(); if(c == -1) { LOG_ERROR("Cannot place start settlement"); return; } gCorners[c].player = id; settlements--; while(true) { int p = gCorners[c].paths[rng.next(0, gCorners[c].paths.getLength() - 1)]; if(gPaths[p].player == -1) { gPaths[p].player = id; paths--; return; } } } static List getPossiblePaths(int player) { List paths; for(int i = 0; i < gPaths.getLength(); i++) { if(!gPaths[i].isFree()) { continue; } const Corner& cornerA = gCorners[gPaths[i].cornerA]; const Corner& cornerB = gCorners[gPaths[i].cornerB]; if(cornerA.player == player || cornerB.player == player || cornerA.doesAnyPathBelongToPlayer(player) || cornerB.doesAnyPathBelongToPlayer(player)) { paths.add(i); } } return paths; } static List getPossibleCorners(int player) { List corners; for(int i = 0; i < gCorners.getLength(); i++) { if(!gCorners[i].isFree()) { continue; } if(gCorners[i].doesAnyPathBelongToPlayer(player) && gCorners[i].validForSettlement()) { corners.add(i); } } return corners; } static List getPossibleCityCorners(int player) { List corners; for(int i = 0; i < gCorners.getLength(); i++) { if(gCorners[i].player == player && !gCorners[i].city) { corners.add(i); } } return corners; } void Player::takeTurn() { while(true) { int clay = getResource(Resource::CLAY); int ore = getResource(Resource::ORE); int sheep = getResource(Resource::SHEEP); int wheat = getResource(Resource::WHEAT); int wood = getResource(Resource::WOOD); if(wood >= 1 && clay >= 1 && wheat >= 1 && sheep >= 1 && settlements > 0) { LOG_INFO("I can build a settlement."); List possibleCorners = getPossibleCorners(id); if(possibleCorners.getLength() >= 1) { int index = rng.next(0, possibleCorners.getLength() - 1); gCorners[possibleCorners[index]].player = id; removeResource(Resource::CLAY, 1); removeResource(Resource::WOOD, 1); removeResource(Resource::SHEEP, 1); removeResource(Resource::WHEAT, 1); LOG_INFO("I build a settlement."); settlements--; continue; } } if(wood >= 1 && clay >= 1 && paths > 0) { LOG_INFO("I can build a path."); List possiblePaths = getPossiblePaths(id); if(possiblePaths.getLength() >= 1 && getPossibleCorners(id).getLength() == 0) { int index = rng.next(0, possiblePaths.getLength() - 1); gPaths[possiblePaths[index]].player = id; removeResource(Resource::CLAY, 1); removeResource(Resource::WOOD, 1); LOG_INFO("I build a path."); paths--; continue; } } if(ore >= 3 && wheat >= 2 && cities > 0) { LOG_INFO("I can build a city."); List possibleCityCorners = getPossibleCityCorners(id); if(possibleCityCorners.getLength() >= 1) { int bestIndex = -1; int quality = -1; for(int index : possibleCityCorners) { int newQuality = gCorners[index].getQuality(); if(newQuality > quality) { bestIndex = index; quality = newQuality; } } gCorners[bestIndex].city = true; removeResource(Resource::ORE, 3); removeResource(Resource::WHEAT, 2); LOG_INFO("I build a city."); cities--; settlements++; continue; } } if(swap()) { continue; } break; } } bool Corner::doesAnyPathBelongToPlayer(int playerId) const { for(int p : paths) { if(gPaths[p].player == playerId) { return true; } } return false; } bool Corner::validForSettlement() const { if(!isFree()) { return false; } for(int p : paths) { if(!gPaths[p].getOtherCorner(*this).isFree()) { return false; } } return true; } int Player::getPoints() const { return (4 - cities) * 2 + 5 - settlements; } static void initResources() { for(int i = 0; i < 4; i++) { gHexagons[i].resource = Resource::WHEAT; gHexagons[i + 4].resource = Resource::WOOD; gHexagons[i + 8].resource = Resource::SHEEP; } for(int i = 0; i < 3; i++) { gHexagons[i + 12].resource = Resource::ORE; gHexagons[i + 15].resource = Resource::CLAY; } gHexagons[18].resource = Resource::NOTHING; for(int i = 0; i < gHexagons.getLength(); i++) { std::swap(gHexagons[i].resource, gHexagons[rng.next(i, gHexagons.getLength() - 1)].resource); } } static bool invalidNumbers(int a, int b) { a = a == 8 ? 6 : a; b = b == 8 ? 6 : b; return a == b; } static bool invalidNumbersExist() { for(const Corner& c : gCorners) { if(c.hexagons.getLength() == 2) { int numberA = gHexagons[c.hexagons[0]].number; int numberB = gHexagons[c.hexagons[1]].number; if(invalidNumbers(numberA, numberB)) { return true; } } else if(c.hexagons.getLength() == 3) { int numberA = gHexagons[c.hexagons[0]].number; int numberB = gHexagons[c.hexagons[1]].number; int numberC = gHexagons[c.hexagons[2]].number; if(invalidNumbers(numberA, numberB) || invalidNumbers(numberA, numberC) || invalidNumbers(numberB, numberC)) { return true; } } } return false; } static void initNumbers() { int index = 0; int numbers[] = {2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 3, 4, 5, 6, 8, 9, 10, 11}; for(Hexagon& h : gHexagons) { if(h.resource != Resource::NOTHING) { h.number = numbers[index++]; } else { h.number = 0; } } for(int i = 0; i < gHexagons.getLength(); i++) { if(gHexagons[i].resource == Resource::NOTHING) { continue; } int ni = rng.next(i, gHexagons.getLength() - 1); while(gHexagons[ni].resource == Resource::NOTHING) { ni = rng.next(i, gHexagons.getLength() - 1); } std::swap(gHexagons[i].number, gHexagons[ni].number); } while(invalidNumbersExist()) { int a = rng.next(0, gHexagons.getLength() - 1); while(gHexagons[a].resource == Resource::NOTHING) { a = rng.next(0, gHexagons.getLength() - 1); } int b = rng.next(0, gHexagons.getLength() - 1); while(gHexagons[b].resource == Resource::NOTHING) { b = rng.next(0, gHexagons.getLength() - 1); } std::swap(gHexagons[a].number, gHexagons[b].number); } } static void initHexagonMid() { Vector2 mid = MIN_BORDER + AREA * 0.5f; for(int i = 0; i < 3; i++) { gHexagons[i].mid = mid - Vector2(WIDTH * static_cast(i - 1), -RADIUS * 2.0f - LINE_LENGTH); gHexagons[i + 16].mid = mid - Vector2(WIDTH * static_cast(i - 1), RADIUS * 2.0f + LINE_LENGTH); } for(int i = 3; i < 7; i++) { gHexagons[i].mid = mid - Vector2(WIDTH * (static_cast(i) - 4.5f), -RADIUS - LINE_LENGTH * 0.5f); gHexagons[i + 9].mid = mid - Vector2(WIDTH * (static_cast(i) - 4.5f), RADIUS + LINE_LENGTH * 0.5f); } for(int i = 7; i < 12; i++) { gHexagons[i].mid = mid - Vector2(WIDTH * static_cast(i - 9), 0.0f); } } static void initCornerMid() { for(int i = 0; i < gHexagons.getLength(); i++) { gCorners[i].mid = gHexagons[i].getLeftTopCorner(); gCorners[i + 19].mid = gHexagons[i].getLeftBottomCorner(); } gCorners[38].mid = gHexagons[0].getTopCorner(); gCorners[39].mid = gHexagons[1].getTopCorner(); gCorners[40].mid = gHexagons[2].getTopCorner(); gCorners[41].mid = gHexagons[16].getBottomCorner(); gCorners[42].mid = gHexagons[17].getBottomCorner(); gCorners[43].mid = gHexagons[18].getBottomCorner(); gCorners[44].mid = gHexagons[0].getRightTopCorner(); gCorners[45].mid = gHexagons[0].getRightBottomCorner(); gCorners[46].mid = gHexagons[3].getRightTopCorner(); gCorners[47].mid = gHexagons[3].getRightBottomCorner(); gCorners[48].mid = gHexagons[7].getRightTopCorner(); gCorners[49].mid = gHexagons[7].getRightBottomCorner(); gCorners[50].mid = gHexagons[12].getRightTopCorner(); gCorners[51].mid = gHexagons[12].getRightBottomCorner(); gCorners[52].mid = gHexagons[16].getRightTopCorner(); gCorners[53].mid = gHexagons[16].getRightBottomCorner(); } static int findCorner(const Vector2& mid) { for(int i = 0; i < gCorners.getLength(); i++) { if((mid - gCorners[i].mid).squareLength() < 0.00001f) { return i; } } return -1; } static void initHexagongCorners() { for(Hexagon& h : gHexagons) { h.corners[0] = findCorner(h.getTopCorner()); h.corners[1] = findCorner(h.getBottomCorner()); h.corners[2] = findCorner(h.getLeftTopCorner()); h.corners[3] = findCorner(h.getLeftBottomCorner()); h.corners[4] = findCorner(h.getRightTopCorner()); h.corners[5] = findCorner(h.getRightBottomCorner()); } for(int i = 0; i < gHexagons.getLength(); i++) { for(int c = 0; c < gHexagons[c].corners.getLength(); c++) { if(gHexagons[i].corners[c] == -1) { LOG_WARNING("Could not find a hexagon corner"); } else { if(gCorners[gHexagons[i].corners[c]].hexagons.add(i)) { LOG_WARNING("Corner hexagon overflow"); } } } } } static bool doesPathExist(const Path& p) { Vector2 mid = p.getMid(); for(const Path& po : gPaths) { if((mid - po.getMid()).squareLength() < 0.0001f) { return true; } } return false; } static void initPaths() { for(int i = 0; i < gCorners.getLength(); i++) { for(int k = 0; k < gCorners.getLength(); k++) { if(i == k || (gCorners[i].mid - gCorners[k].mid).length() >= LINE_LENGTH * 1.01f) { continue; } Path p; p.from = gCorners[i].mid; p.to = gCorners[k].mid; if(doesPathExist(p)) { continue; } gPaths.add(p); } } LOG_INFO( StringBuffer<256>("Got ").append(gPaths.getLength()).append(" paths")); } static void initCornerPaths() { for(int c = 0; c < gCorners.getLength(); c++) { for(int i = 0; i < gPaths.getLength(); i++) { Vector2 mid = gPaths[i].getMid(); if((gCorners[c].mid - mid).length() >= RADIUS) { continue; } if(gCorners[c].paths.add(i)) { LOG_WARNING("Corner path overflow"); } if(gPaths[i].cornerA == -1) { gPaths[i].cornerA = c; } else if(gPaths[i].cornerB == -1) { gPaths[i].cornerB = c; } else { LOG_WARNING("Path got too much gCorners"); } } } for(const Path& p : gPaths) { if(p.cornerA == -1 || p.cornerB == -1) { LOG_WARNING("Path is missing gCorners"); } } } static void initPlayers() { static const Color4 PLAYER_COLOR[] = { Color4(255, 0, 0, 255), Color4(0, 0, 255, 255), Color4(255, 128, 0, 255), Color4(255, 255, 255, 255)}; for(int i = 0; i < players.getLength(); i++) { players[i].id = i; players[i].color = PLAYER_COLOR[i]; } } static void placeStartSettlement() { for(int i = 0; i < players.getLength(); i++) { players[i].placeStartSettlement(); } for(int i = players.getLength() - 1; i >= 0; i--) { players[i].placeStartSettlement(); } } static void init() { initResources(); initHexagonMid(); initCornerMid(); initHexagongCorners(); initPaths(); initCornerPaths(); initNumbers(); initPlayers(); placeStartSettlement(); } static void buildBuffer() { buffer.clear(); for(const Hexagon& h : gHexagons) { h.addToBuffer(); } for(const Path& p : gPaths) { p.addToBuffer(); } for(const Corner& c : gCorners) { c.addToBuffer(); } vertexBuffer.setData(buffer, GL::BufferUsage::DYNAMIC); } static void buildFontBuffer() { fontBuffer.clear(); for(const Hexagon& h : gHexagons) { h.addStringToBuffer(); } for(int i = 0; i < players.getLength(); i++) { StringBuffer<16> s(players[i].getPoints()); if(i == currentPlayer) { s.append(" <"); } addString(s, Vector2(-0.9f, 0.9f) - Vector2(0.0f, 0.1f * static_cast(i)), 0.1f, players[i].color); } addString(StringBuffer<16>(turns), Vector2(0.6f, 0.9f), 0.1f, Color4(255, 255, 255, 255)); fontVertexBuffer.setData(fontBuffer, GL::BufferUsage::DYNAMIC); } static void rebuild() { buildBuffer(); buildFontBuffer(); } static void giveResources(int dice) { for(const Hexagon& h : gHexagons) { if(h.number != dice) { continue; } for(int c : h.corners) { const Corner& corner = gCorners[c]; if(corner.player < 0 || corner.player >= players.getLength()) { continue; } players[corner.player].giveResource(h.resource, 1 + corner.city); } } } static void doTurn() { int dice = rng.next(1, 6) + rng.next(1, 6); LOG_INFO(StringBuffer<256>("Dice: ").append(dice)); giveResources(dice); players[currentPlayer].takeTurn(); } static int waitTicks = 0; static bool checkForWinner() { bool won = false; for(const Player& p : players) { if(p.getPoints() >= 10) { return true; } } return won; } static void restart() { for(Corner& c : gCorners) { c.reset(); } for(Path& p : gPaths) { p.reset(); } for(Player& p : players) { p.reset(); } currentPlayer = 0; turns = 0; placeStartSettlement(); } static void restartMap() { restart(); initResources(); initNumbers(); } static void tick() { if(Window::Controls::wasReleased(restartButton)) { restart(); } if(Window::Controls::wasReleased(restartMapButton)) { restartMap(); } waitTicks++; if(waitTicks < 1) { return; } waitTicks = 0; if(checkForWinner()) { return; } turns++; doTurn(); rebuild(); currentPlayer = (currentPlayer + 1) % players.getLength(); } static void render(float lag) { GL::setViewport(Window::getSize()[0], Window::getSize()[1]); GL::clear(); shader.use(); vertexBuffer.draw(vertexBuffer.getSize() / static_cast(sizeof(Vector2) + sizeof(Color4))); fontShader.use(); fontTexture.bindTo(0); GL::enableBlending(); fontVertexBuffer.draw( vertexBuffer.getSize() / static_cast(sizeof(Vector2) * 2 + sizeof(Color4))); (void)lag; } static bool shouldRun() { return !Window::shouldClose(); } int main() { Error e = Window::open( Window::Options(4, 6, IntVector2(400, 300), false, "Catan Simulator")); if(e.has()) { e.message.printLine(); return 0; } e = shader.compile("resources/vertex.vs", "resources/fragment.fs"); if(e.has()) { e.message.printLine(); return 0; } e = fontShader.compile("resources/font.vs", "resources/font.fs"); if(e.has()) { e.message.printLine(); return 0; } e = fontTexture.load("resources/font8x8.png", 1); if(e.has()) { e.message.printLine(); return 0; } vertexBuffer.init(VertexBuffer::Attributes().addFloat(2).addColor4()); fontVertexBuffer.init( VertexBuffer::Attributes().addFloat(2).addFloat(2).addColor4()); restartButton = Window::Controls::add("Restart"); Window::Controls::bindKey(restartButton, GLFW_KEY_R); restartMapButton = Window::Controls::add("Restart Map"); Window::Controls::bindKey(restartMapButton, GLFW_KEY_M); init(); rebuild(); Window::show(); Window::run(10'000'000); return 0; }