diff --git a/README.md b/README.md index 63170de..9ce30b2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,48 @@ -## Read Me: +For the frontend see [github.com/Fabbernat/Polytopia-frontend](https://github.com/Fabbernat/Polytopia-frontend) + + # Read Me: -Welcome to the `polytopia_python` repo! Have a look around and familiarise yourself with some of the code that's already here. +Welcome to the `polytopia_python` repo! Have a look around and familiarise yourself with some of the code that's already +here. -You'll notice that there's a `game_map.py` file -- this is where all of the logic for generating/interacting with the *map* will be. +You'll notice that there's a `game_map.py` file -- this is where all of the logic for generating/interacting with the +*map* will be. The `game_simulator.py` script is mostly empty so far, and is where the main game logic will eventually go. -The `map_renderer.py` script is a separate component that you can mostly ignore, and will be responsible for creating a visual representation of the current game state. This won't be necessary for deep learning, and will purely be for debugging and QA purposes. +The `map_renderer.py` script is a separate component that you can mostly ignore, and will be responsible for creating a +visual representation of the current game state. This won't be necessary for deep learning, and will purely be for +debugging and QA purposes. -The `settings.py` file contains some some settings variables that are mostly unused as of now, but the most important of these is the `CHANNEL_ATTRIBUTES` object, which contains descriptions of what each 'channel' in the game state array will correspond to. +The `settings.py` file contains some settings variables that are mostly unused as of now, but the most important of +these is the `CHANNEL_ATTRIBUTES` object, which contains descriptions of what each 'channel' in the game state array +will correspond to. + +--- +# Fabbernat's contribution: +# image +### The "Lakes" map type and some insight to the code in IntelliJ IDEA 👆 +--- + +# image +### The "Drylands" map type 👆 +--- + +# image +### The "Continents" map type 👆 +--- + +# image +### The "Archipelagos" map type 👆 +--- + +# image +### The "Continents" map type, but BIGGER 👆 +--- + +- I made a frontend to this at [github.com/Fabbernat/Polytopia-frontend](https://github.com/Fabbernat/Polytopia-frontend) built in ASP.NET +- I added a COMPLETE terrain generation module +- I added a score counter Python module and a Java console consoleApp + +## Java console consoleApp +This is intended to be the main consoleApp at some point and will simulate the core logic of the gameplay like map generation, tech tree and even combat diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/hall_of_fame.html b/app/hall_of_fame.html new file mode 100644 index 0000000..e69de29 diff --git a/app/static/css/styles.css b/app/static/css/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/app/static/favicon.ico b/app/static/favicon.ico new file mode 100644 index 0000000..63e859b Binary files /dev/null and b/app/static/favicon.ico differ diff --git a/app/static/images/polyhead_ancient.jpg b/app/static/images/polyhead_ancient.jpg new file mode 100644 index 0000000..17f2bc7 Binary files /dev/null and b/app/static/images/polyhead_ancient.jpg differ diff --git a/app/static/images/polytopia-background.jpg b/app/static/images/polytopia-background.jpg new file mode 100644 index 0000000..ae759af Binary files /dev/null and b/app/static/images/polytopia-background.jpg differ diff --git a/app/static/images/polytopia-main-menu.webp b/app/static/images/polytopia-main-menu.webp new file mode 100644 index 0000000..7f7f407 Binary files /dev/null and b/app/static/images/polytopia-main-menu.webp differ diff --git a/app/static/images/soundfx-ico.png b/app/static/images/soundfx-ico.png new file mode 100644 index 0000000..5adfc16 Binary files /dev/null and b/app/static/images/soundfx-ico.png differ diff --git a/app/templates/about.html b/app/templates/about.html new file mode 100644 index 0000000..740f26b --- /dev/null +++ b/app/templates/about.html @@ -0,0 +1,203 @@ + + + + + About Us + + + +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"]

+ +
+ Fabbernat Polyhead Icon +
+
+

Fabian Team: A passionate team of developers dedicated to creating engaging and immersive experiences for Polytopia fans.

+ +

Polysseum

+

The Polysseum is a platform where strategic minds meet, showcasing the best players and their achievements. It's the ultimate hall of fame for Polytopia enthusiasts.

+ +

Meet the Team

+

~Midji Team~ Fabian Team:

+ + +

Our Mission

+

To create a space where players can share strategies, celebrate victories, and grow the Polytopia community.

+ +
+
+

Contact us!

+ +
+ + + + \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..f84fea4 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,24 @@ + + + + + + {{ title }} + + + +
+
+ {% block content %} + {% endblock %} +
+ + diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..2bd2f9b --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,10 @@ + + + + + Dashboard + + + + + \ No newline at end of file diff --git a/app/templates/hall_of_fame.html b/app/templates/hall_of_fame.html new file mode 100644 index 0000000..3c11b84 --- /dev/null +++ b/app/templates/hall_of_fame.html @@ -0,0 +1,10 @@ + + + + + Hall of Fame + + + + + \ No newline at end of file diff --git a/app/templates/home.html b/app/templates/home.html new file mode 100644 index 0000000..c02e56c --- /dev/null +++ b/app/templates/home.html @@ -0,0 +1,10 @@ + + + + + Home + + +{% header("Location:index.html?redirect=True") %} + + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..a754443 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,53 @@ + + + + + Index + + +{% extends "base.html" %} + +{% block content %} +

the Battle of Polytopia

+

This is the Index page. Explore other sections using the navigation above.

+{% endblock %} +@{ + ViewData["Title"] = "Home Page"; +} + +
+

The Battle of Polytopia

+
+ +
+ +
+ + + \ No newline at end of file diff --git a/app/templates/privacy.html b/app/templates/privacy.html new file mode 100644 index 0000000..2f525da --- /dev/null +++ b/app/templates/privacy.html @@ -0,0 +1,16 @@ + + + + + Title + + +{% extends "base.html" %} + +{% block content %} +

Privacy Policy

+

This is the Privacy page content.

+{% endblock %} + + + diff --git a/app/templates/settings.html b/app/templates/settings.html new file mode 100644 index 0000000..85cc7d2 --- /dev/null +++ b/app/templates/settings.html @@ -0,0 +1,233 @@ + + + + + Settings + + +@{ + ViewData["Title"] = "Home Page"; +} + +
+
+

-Settings-

+

Audio Volume

+ + +
+ + +
+ + +
    + +

    Sound Settings

    + + + + +
  1. + Sound Effects + sound effects icon + + +
  2. +
  3. + Ambience + sound effects icon + +
  4. +
  5. + Tribe Music + sound effects icon + +
  6. +
    + +
    + + +

    Game Settings

    + +
  7. + Suggestion + sound effects icon + + +
  8. +
  9. + Info on build + sound effects icon + + +
  10. +
  11. + Confirm Turn + sound effects icon + + +
  12. +
    + +
    + + +

    Replay Settings

    +
  13. + Replay Fog + sound effects icon + + +
  14. +
  15. + Shared Fog + sound effects icon + +
  16. +
  17. + Auto Focus + sound effects icon + +
  18. +
    +
+ + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/app/templates/throne_room.html b/app/templates/throne_room.html new file mode 100644 index 0000000..665389f --- /dev/null +++ b/app/templates/throne_room.html @@ -0,0 +1,10 @@ + + + + + Throne Room + + + + + \ No newline at end of file diff --git a/app/throne_room.html b/app/throne_room.html new file mode 100644 index 0000000..665389f --- /dev/null +++ b/app/throne_room.html @@ -0,0 +1,10 @@ + + + + + Throne Room + + + + + \ No newline at end of file diff --git a/app/views/__init__.py b/app/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/views/dashboard.py b/app/views/dashboard.py new file mode 100644 index 0000000..e69de29 diff --git a/app/views/home.py b/app/views/home.py new file mode 100644 index 0000000..e69de29 diff --git a/config.py b/config.py new file mode 100644 index 0000000..f87f5c1 --- /dev/null +++ b/config.py @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/java/resources/games.txt b/java/resources/games.txt new file mode 100644 index 0000000..84297eb --- /dev/null +++ b/java/resources/games.txt @@ -0,0 +1,7 @@ +Misty Clouds +Popcorn & Glory +Amazing Whales +Lucky Glory +Forest & Whales +Brave Clouds +Autumn of Pooo \ No newline at end of file diff --git a/java/resources/guidReference.txt b/java/resources/guidReference.txt new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/java/resources/guidReference.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/java/src/console/Main.java b/java/src/console/Main.java new file mode 100644 index 0000000..45bd59a --- /dev/null +++ b/java/src/console/Main.java @@ -0,0 +1,78 @@ +package console; + +import console.app.ConsoleApp; +import console.app.ConsoleAppUtils; +import console.app.main.DesktopApp; + +import java.util.Objects; +import java.util.Scanner; + +public class Main { + + public static void log(String message) { + System.out.println(message); + } + + static String farewellMessage = "Goodbye Mighty Ruler!"; + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + /* + * console app: + */ + console(scanner); + + /* + * desktop app: + */ + createLoop(scanner, "desktop"); + exit(scanner); + } + + private static void desktop(String choice) { + DesktopApp desktopApp = new DesktopApp(choice); + } + + private static void console(Scanner scanner) { + ConsoleApp consoleApp = new ConsoleApp(); + createLoop(scanner, "console"); + } + + public static void exit(Scanner scanner) { + log("Press any key to exit..."); + scanner.nextLine(); + } + + private static void createLoop(Scanner scanner, String appType) { + if ("console".equals(appType)) { + log("Polytopia CLI started. Type `start \"Game Name\"` or just `start` to start a new game.\n Example: start \"Misty Clouds\"\nType `help` to get the list of available commands.\n`exit` to quit."); + } else { + log("Polytopia Desktop app started. Press ENTER to start generating"); + } + while (true) { + System.out.print ("> "); + String input = scanner.nextLine().trim(); + input = input.toLowerCase(); + + if ("exit".equals(input) || "quit".equals(input)) { + log(farewellMessage); + return; + } + if ("console".equals(appType)) { + ConsoleAppUtils.handleCommand(input); + } else if ("desktop".equals(appType)) { + log(""" + Choose map type! + 0 - Drylands + 1 - Lakes + 2- Conti + 3 - Archi + 4 - Water World"""); + String choice = scanner.nextLine(); + desktop(choice); + log("Press ENTER to continue, type `exit` to quit."); + } + } + } +} diff --git a/java/src/console/app/ConsoleApp.java b/java/src/console/app/ConsoleApp.java new file mode 100644 index 0000000..73afad2 --- /dev/null +++ b/java/src/console/app/ConsoleApp.java @@ -0,0 +1,71 @@ +package console.app; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import static console.Main.log; + +public class ConsoleApp { + static int guid = 1; + private List games = new ArrayList<>(); + + public ConsoleApp() { + readGuidReference(); + readGames(); + } + + public List getGames() { + readGames(); + return games; + } + + private void readGuidReference() { + try (InputStream input = ConsoleApp.class.getResourceAsStream("/guidReference.txt")) { + if (input == null) { + log("guidReference.txt not found!"); + return; + } + log(input.toString()); + + List lines = new java.io.BufferedReader( + new java.io.InputStreamReader(input) + ).lines().toList(); + + log("Read in guid reference:"); + lines.forEach(System.out::println); + + guid = Integer.parseInt(lines.getFirst().trim()); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void readGames() { + try (InputStream input = ConsoleApp.class.getResourceAsStream("/games.txt")) { + if (input == null) { + log("games.txt not found!"); + return; + } + + List lines = new java.io.BufferedReader( + new java.io.InputStreamReader(input) + ).lines().toList(); + + log("Read in games:"); + games.clear(); + games.addAll(lines); + reportGames(); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void reportGames() { + games.forEach(System.out::println); + } + +} diff --git a/java/src/console/app/ConsoleAppUtils.java b/java/src/console/app/ConsoleAppUtils.java new file mode 100644 index 0000000..b884f2c --- /dev/null +++ b/java/src/console/app/ConsoleAppUtils.java @@ -0,0 +1,71 @@ +package console.app; + +import console.utils.ValidCommands; + +import static console.Main.log; + +public class ConsoleAppUtils { + public static void handleCommand(String input) { + if (input.isEmpty()) + return; + + if (input.equals("help")) { + log("Available commands: " + String.join(", ", ValidCommands.ALL_COMMANDS)); + return; + } + + // Check menu base commands + for (String command : ValidCommands.MENU_COMMANDS) { + if (input.startsWith(command)) { + String gameName = getRemainder(input, command); + switch (command) { + case "start" -> { + StringBuilder capitalizedGameName = new StringBuilder(); + + for (String word : gameName.trim().split("\\s+")) { + if (!word.isEmpty()) { + capitalizedGameName + .append(word.substring(0, 1).toUpperCase()) + .append(word.substring(1).toLowerCase()) + .append(" "); + } + } + + gameName = capitalizedGameName.toString().trim(); + report("New game started: " + (!gameName.trim().isEmpty() ? gameName : "Game " + ConsoleApp.guid++)); + } + case "delete" -> report("Deleted game: " + gameName); + + case "games" -> { + ConsoleApp consoleApp = new ConsoleApp(); + report("Games list: " + consoleApp.getGames().toString()); + } + } + return; + } + } + log("Executed base command: " + input); + + } + + private static String getRemainder(String input, String command) { + if (input.length() <= command.length()) { + return ""; // no remainder + } + + return input.substring(command.length()).trim(); + } + + private static int stars = 5; + private static int score = 500; + private static int turn = 0; + private static final String tabulators = "\t\t\t\t\t\t\t\t"; + + private static void report() { + log("|--- " + stars + " stars | " + score + " score | " + turn + "th turn" + " ---|" + tabulators + "\n"); + } + + private static void report(String message) { + log("|--- " + stars + " stars | " + score + " score | " + turn + "th turn" + " ---|" + tabulators + "\n" + message); + } +} diff --git a/java/src/console/app/PerlinNoise.java b/java/src/console/app/PerlinNoise.java new file mode 100644 index 0000000..63156d9 --- /dev/null +++ b/java/src/console/app/PerlinNoise.java @@ -0,0 +1,64 @@ +package console.app; + +import java.util.Random; + +public class PerlinNoise { + + private final int[] permutation = new int[512]; + + public PerlinNoise(long seed) { + int[] p = new int[256]; + for (int i = 0; i < 256; i++) { + p[i] = i; + } + + Random rand = new Random(seed); + for (int i = 255; i > 0; i--) { + int index = rand.nextInt(i + 1); + int tmp = p[i]; + p[i] = p[index]; + p[index] = tmp; + } + + for (int i = 0; i < 512; i++) { + permutation[i] = p[i & 255]; + } + } + + private static double fade(double t) { + return t * t * t * (t * (t * 6 - 15) + 10); + } + + private static double lerp(double t, double a, double b) { + return a + t * (b - a); + } + + private static double grad(int hash, double x, double y) { + int h = hash & 7; + double u = h < 4 ? x : y; + double v = h < 4 ? y : x; + return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); + } + + public double noise(double x, double y) { + int X = (int) Math.floor(x) & 255; + int Y = (int) Math.floor(y) & 255; + + x -= Math.floor(x); + y -= Math.floor(y); + + double u = fade(x); + double v = fade(y); + + int aa = permutation[X + permutation[Y]]; + int ab = permutation[X + permutation[Y + 1]]; + int ba = permutation[X + 1 + permutation[Y]]; + int bb = permutation[X + 1 + permutation[Y + 1]]; + + return lerp( + v, + lerp(u, grad(aa, x, y), grad(ba, x - 1, y)), + lerp(u, grad(ab, x, y - 1), grad(bb, x - 1, y - 1)) + ); + } +} diff --git a/java/src/console/app/main/DesktopApp.java b/java/src/console/app/main/DesktopApp.java new file mode 100644 index 0000000..d7224be --- /dev/null +++ b/java/src/console/app/main/DesktopApp.java @@ -0,0 +1,229 @@ +package console.app.main; + +import console.app.PerlinNoise; + +import javax.swing.*; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import java.awt.*; +import java.util.*; +import java.util.List; + +import static console.Main.exit; +import static console.Main.log; + +public class DesktopApp { + + // Gameplay settings + private static final int ROWS = 30; + private static final int COLS = 30; + private static final int NUMBER_OF_PLAYERS = 2; + + + // Maptype and generation-related settings + private static MapType mapType = MapType.LAKES; + private static final double FOREST_RATE = .3; + + // Terrains + private static final Color LAND = new Color(46, 168, 19); + private static final Color FOREST = new Color(19, 85, 0); + private static final Color OCEAN = Color.BLUE; + private static final Color WATER = new Color(75, 208, 255); // not used yet + private static final Color CAPITAL = new Color(0, 0, 0); + private static final Color MOUNTAIN = new Color(133, 133, 133, 255); + private static final Color VILLAGE = new Color(139, 75, 19); + private static final Color RUIN = new Color(255, 255, 1, 230); + + private static final JPanel[][] tiles = new JPanel[ROWS][COLS]; + private static final Random random = new Random(); + + // Statikus lista a kapitalok pozícióinak tárolására + private static final List capitals = new ArrayList<>(); + // Statikus lista a hegyek pozícióinak tárolására + private static final List mountains = new ArrayList<>(); + // Statikus lista a falvak pozícióinak tárolására + private static final List villages = new ArrayList<>(); + + public DesktopApp() { + int mapType = 1; + DesktopApp.mapType = MapType.values()[mapType]; + SwingUtilities.invokeLater(DesktopApp::createAndShowUI); + } + + public DesktopApp(String choice) { + log("DesktopApp mapType set to " + choice + + " scale=" + mapType.perlinScale); + int mapType = 0; + try { + mapType = Integer.parseInt(choice); + } catch (NumberFormatException e) { + exit(new Scanner(System.in)); + } + DesktopApp.mapType = MapType.values()[mapType]; + SwingUtilities.invokeLater(DesktopApp::createAndShowUI); + } + + private static void createAndShowUI() { + log("Using PERLIN_SCALE=" + mapType.perlinScale + + ", WATER_LAND_RATIO=" + mapType.waterAndLandRatio); + JFrame frame = new JFrame("Polytopia"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + JPanel gridPanel = new JPanel(new GridLayout(ROWS, COLS)); // csinál egy ROWS * COLS méretű táblát + PerlinNoise noise = new PerlinNoise(System.currentTimeMillis()); + + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + JPanel tile = new JPanel(); + + double value = noise.noise( + row * mapType.perlinScale, + col * mapType.perlinScale + ); + + double normalized = (value + 1) / 2.0; // zajgyártás + + tile.setBackground(normalized > mapType.waterAndLandRatio ? LAND : OCEAN); // for archi + boolean isForest = random.nextInt(100) / 100.0 < FOREST_RATE; + if (isForest && tile.getBackground().equals(LAND)) { + tile.setBackground(FOREST); + } + tile.setBorder(BorderFactory.createLineBorder(Color.BLACK)); + + tiles[row][col] = tile; + gridPanel.add(tile); + } + } + + // 2️⃣ Place tribe capitals AFTER generation + replaceTilesWithCities(NUMBER_OF_PLAYERS, CAPITAL); + FillTheRestOfTheWorldWithVillages(); + GenerateMountains(); // pl. 10 hegy + + frame.add(gridPanel); + frame.setSize(600, 600); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + + /** + * puts the given number of cities (capital or village) to the map with SOME STRICT RULES: + * no city can be within minDistance tiles of the edge of the map or each other + * + * @param count number of cities to place + * @param what color to paint + */ + private static void replaceTilesWithCities(int count, Color what) { + int placed = 0; + + while (placed < count) { + int row = random.nextInt(ROWS - 4) + 2; // At least 2 tiles from the edges and from other capitals + int col = random.nextInt(COLS - 4) + 2; + + JPanel tile = tiles[row][col]; + + if (tile.getBackground().equals(LAND) && isVillageFarEnough(row, col, capitals, 2)) { + tile.setBackground(what); + capitals.add(new Point(row, col)); // kapitalokat tároljuk ide + placed++; + } + } + } + + private static boolean isVillageFarEnough(int row, int col, List points, int minDistance) { + for (Point p : points) { + int dist = Math.abs(p.x - row) + Math.abs(p.y - col); // Manhattan distance + if (dist < minDistance + 2) { + return false; // túl közel + } + } + return true; // jó hely + } + + private static void GenerateMountains() { + // total tiles = ROWS * COLS + int totalTiles = ROWS * COLS; + int count = totalTiles / 17 - capitals.size(); // alapértelmezett hegy szám + + if (count < 0) count = 0; // negatív érték esetén nulla + + GenerateMountains(count); + } + + /** + * fills the world with villages with SOME STRICT RULES: + * finds the spots that are far enough (at least 2 tiles) from capitals and other villages. + * Villages can spawn on water. + * + * @param count number of villages to place + */ + private static void GenerateMountains(int count) { + int placed = 0; + + while (placed < count) { + int row = random.nextInt(ROWS - 4) + 2; // legalább 2 csempe távol a szélektől + int col = random.nextInt(COLS - 4) + 2; + + JPanel tile = tiles[row][col]; + + // Falvak bárhol lehetnek (víz vagy föld) + // Viszont legalább 2 távol a kapitaloktól és a többi hegytől + // Csak akkor állítsuk barna színűre, ha még nem capital vagy hegy + Color bg = tile.getBackground(); + if (!bg.equals(OCEAN) && !bg.equals(CAPITAL) && !bg.equals(MOUNTAIN)) { + tile.setBackground(MOUNTAIN); + mountains.add(new Point(row, col)); + placed++; + } + + } + } + + private static void FillTheRestOfTheWorldWithVillages() { + int increasingMagicNumber = 20; + int totalTiles = ROWS * COLS; + int villageCount = totalTiles / increasingMagicNumber - capitals.size(); + + if (villageCount <= 0) { + return; + } + + List candidates = new ArrayList<>(); + + // Collect all valid candidate tiles first + for (int row = 1; row < ROWS - 1; row++) { + for (int col = 1; col < COLS - 1; col++) { + JPanel tile = tiles[row][col]; + Color bg = tile.getBackground(); + + // Must be LAND or WATER and not already village, capital, mountain + if (!bg.equals(LAND) && !bg.equals(OCEAN)) continue; + + // Check distance from capitals and mountains only (ignore villages here) + if (!isVillageFarEnough(row, col, capitals, 2)) continue; + + candidates.add(new Point(row, col)); + } + } + + // Shuffle candidate list for randomness + Collections.shuffle(candidates, random); + + villages.clear(); + + // Try to place as many villages as possible without violating distance to other villages + for (Point candidate : candidates) { + if (villages.size() >= villageCount) break; + + // Check distance to already placed villages + if (!isVillageFarEnough(candidate.x, candidate.y, villages, 2)) { + continue; + } + + // Place village + tiles[candidate.x][candidate.y].setBackground(VILLAGE); + villages.add(candidate); + } + } +} diff --git a/java/src/console/app/main/MapType.java b/java/src/console/app/main/MapType.java new file mode 100644 index 0000000..6879e97 --- /dev/null +++ b/java/src/console/app/main/MapType.java @@ -0,0 +1,17 @@ +package console.app.main; + +enum MapType { + DRYLANDS(.4, .0), + LAKES(.1, .4), + CONTI(.1,.52), + ARCHI(.4, .56), + WATER_WORLD(.4,.85); // TODO fix Water World + + public final double perlinScale; // lower = larger regions + public final double waterAndLandRatio; + + MapType(double ps, double walr) { + this.perlinScale = ps; + this.waterAndLandRatio = walr; + } +} diff --git a/java/src/console/utils/ValidCommands.java b/java/src/console/utils/ValidCommands.java new file mode 100644 index 0000000..f2bf6d5 --- /dev/null +++ b/java/src/console/utils/ValidCommands.java @@ -0,0 +1,56 @@ +package console.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class ValidCommands { + // --- Command definitions --- + private static final Set menuCommands = Set.of( + "help", "start", "delete", "games", "exit", "quit" + ); + + + // --- Wrappers --- + public static final Set MENU_COMMANDS = + toUnmodifiableLowercaseSet(menuCommands); + + // --- Global collector --- + public static final Set ALL_COMMANDS = collectAllCommandSets(); + + + // --- Helpers --- + private static Set toUnmodifiableLowercaseSet(Set input) { + return Collections.unmodifiableSet( + (Set) input.stream() + .map(String::toLowerCase) + .collect(Collectors.toCollection(LinkedHashSet::new)) + ); + } + + + private static Set collectAllCommandSets() { + Set all = new HashSet<>(); + try { + for (Field field : ValidCommands.class.getDeclaredFields()) { + // Collect only uppercase public sets + if (Modifier.isStatic(field.getModifiers()) + && Modifier.isFinal(field.getModifiers()) + && Set.class.isAssignableFrom(field.getType()) + && field.getName().equals(field.getName().toUpperCase()) // only uppercase + && !field.getName().equals("ALL_COMMANDS")) { // skip self + @SuppressWarnings("unchecked") + Set subset = (Set) field.get(null); + all.addAll(subset); + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException("Reflection failed while collecting commands", e); + } + return Collections.unmodifiableSet(all); + } +} diff --git a/java/src/models/AbstractTribe.java b/java/src/models/AbstractTribe.java new file mode 100644 index 0000000..8ad1965 --- /dev/null +++ b/java/src/models/AbstractTribe.java @@ -0,0 +1,42 @@ +package models; + +import java.util.Objects; + +public abstract class AbstractTribe { + private Tech startingTech = null; + private int startingStars = 5; + private boolean isSpecial = false; + + private double fruitModifier = 1; + private double cropModifier = 1; + private double forestModifier = 1; + private double wildAnimalModifier = 1; + private double mountainModifier = 1; + private double metalModifier = 1; + private double fishModifier = 1; + + protected AbstractTribe( + Tech startingTech, + int startingStars, + boolean isSpecial, + double fruitModifier, + double cropModifier, + double forestModifier, + double wildAnimalModifier, + double mountainModifier, + double metalModifier, + double fishModifier + ) { + this.startingTech = startingTech; + this.startingStars = startingStars; + this.isSpecial = isSpecial; + this.fruitModifier = fruitModifier; + this.cropModifier = cropModifier; + this.forestModifier = forestModifier; + this.wildAnimalModifier = wildAnimalModifier; + this.mountainModifier = mountainModifier; + this.metalModifier = metalModifier; + this.fishModifier = fishModifier; + } + +} diff --git a/java/src/models/Tech.java b/java/src/models/Tech.java new file mode 100644 index 0000000..0ea6a06 --- /dev/null +++ b/java/src/models/Tech.java @@ -0,0 +1,35 @@ +package models; + +public enum Tech { + NONE, + + FISHING, + SAILING, + NAVIGATION, + AQUACULTURE, + AQUATISM, + + HUNTING, + ARCHERY, + SPIRITUALISM, + FORESTRY, + MATHEMATICS, + + RIDING, + ROADS, + TRADE, + FREE_SPIRIT, + CHIVALRY, + + ORGANIZATION, + FARMING, + CONSTRUCTION, + STRATEGY, + DIPLOMACY, + + CLIMBING, + MINING, + SMITHERY, + MEDITATION, + PHILOSOPHY +} diff --git a/java/src/models/TechTree.java b/java/src/models/TechTree.java new file mode 100644 index 0000000..f2582b7 --- /dev/null +++ b/java/src/models/TechTree.java @@ -0,0 +1,62 @@ +package models; + +import java.util.*; + +public final class TechTree { + + public static final Map>> TREE = + Map.of( + TechCategory.WATER, Map.of( + Tech.FISHING, List.of( + Tech.SAILING, + Tech.NAVIGATION, + Tech.AQUACULTURE, + Tech.AQUATISM + ) + ), + TechCategory.FOREST, Map.of( + Tech.HUNTING, List.of( + Tech.ARCHERY, + Tech.SPIRITUALISM, + Tech.FORESTRY, + Tech.MATHEMATICS + ) + ), + TechCategory.MOBILITY, Map.of( + Tech.RIDING, List.of( + Tech.ROADS, + Tech.TRADE, + Tech.FREE_SPIRIT, + Tech.CHIVALRY + ) + ), + TechCategory.LAND, Map.of( + Tech.ORGANIZATION, List.of( + Tech.FARMING, + Tech.CONSTRUCTION, + Tech.STRATEGY, + Tech.DIPLOMACY + ) + ), + TechCategory.MOUNTAIN, Map.of( + Tech.CLIMBING, List.of( + Tech.MINING, + Tech.SMITHERY, + Tech.MEDITATION, + Tech.PHILOSOPHY + ) + ) + ); + + private TechTree() {} +} + + +enum TechCategory { + WATER, + FOREST, + MOBILITY, + LAND, + MOUNTAIN +} + diff --git a/java/src/models/concreteTribes/Aimo.java b/java/src/models/concreteTribes/Aimo.java new file mode 100644 index 0000000..2baf2a0 --- /dev/null +++ b/java/src/models/concreteTribes/Aimo.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Aimo extends AbstractTribe { + protected Aimo() { + super(Tech.PHILOSOPHY, 5, false, 1, .1, 1, 1, 1.5, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Bardur.java b/java/src/models/concreteTribes/Bardur.java new file mode 100644 index 0000000..7ce2611 --- /dev/null +++ b/java/src/models/concreteTribes/Bardur.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Bardur extends AbstractTribe { + protected Bardur() { + super(Tech.HUNTING, 5, false, 1, 0, 0.8, 1, 1, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Hoodrick.java b/java/src/models/concreteTribes/Hoodrick.java new file mode 100644 index 0000000..9bdd9fc --- /dev/null +++ b/java/src/models/concreteTribes/Hoodrick.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Hoodrick extends AbstractTribe { + protected Hoodrick() { + super(Tech.ARCHERY, 7, false, 1, 1, 1.5, 1, .5, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Imperius.java b/java/src/models/concreteTribes/Imperius.java new file mode 100644 index 0000000..1ce2632 --- /dev/null +++ b/java/src/models/concreteTribes/Imperius.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Imperius extends AbstractTribe { + protected Imperius() { + super(Tech.ORGANIZATION, 5, false, 2, 1, 1, .5, 1, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Kickoo.java b/java/src/models/concreteTribes/Kickoo.java new file mode 100644 index 0000000..07a9540 --- /dev/null +++ b/java/src/models/concreteTribes/Kickoo.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Kickoo extends AbstractTribe { + protected Kickoo() { + super(Tech.FISHING, 5, false, 1, 1, 1, 1, .5, 1, .5); + } +} diff --git a/java/src/models/concreteTribes/Luxidoor.java b/java/src/models/concreteTribes/Luxidoor.java new file mode 100644 index 0000000..a151589 --- /dev/null +++ b/java/src/models/concreteTribes/Luxidoor.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Luxidoor extends AbstractTribe { + protected Luxidoor() { + super(Tech.NONE, 2, false, 1, 1, 1, 1, 1, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Oumaji.java b/java/src/models/concreteTribes/Oumaji.java new file mode 100644 index 0000000..877140a --- /dev/null +++ b/java/src/models/concreteTribes/Oumaji.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Oumaji extends AbstractTribe { + protected Oumaji() { + super(Tech.RIDING, 6, false, 1, 1, .2, .2, .5, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Quetzali.java b/java/src/models/concreteTribes/Quetzali.java new file mode 100644 index 0000000..9812aa8 --- /dev/null +++ b/java/src/models/concreteTribes/Quetzali.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Quetzali extends AbstractTribe { + protected Quetzali() { + super(Tech.STRATEGY, 7, false, 2, .1, 1, 1, 1, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Vengir.java b/java/src/models/concreteTribes/Vengir.java new file mode 100644 index 0000000..98ac4fb --- /dev/null +++ b/java/src/models/concreteTribes/Vengir.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Vengir extends AbstractTribe { + protected Vengir() { + super(Tech.SMITHERY, 5, false, .1, 1, 1, .1, 1, 2, .1); + } +} diff --git a/java/src/models/concreteTribes/Xinxi.java b/java/src/models/concreteTribes/Xinxi.java new file mode 100644 index 0000000..7c5035b --- /dev/null +++ b/java/src/models/concreteTribes/Xinxi.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Xinxi extends AbstractTribe { + protected Xinxi() { + super(Tech.CLIMBING, 7, false, 1, 1, 1, 1, 1.5, 1.5, 1); + } +} diff --git a/java/src/models/concreteTribes/Yadakk.java b/java/src/models/concreteTribes/Yadakk.java new file mode 100644 index 0000000..ae40033 --- /dev/null +++ b/java/src/models/concreteTribes/Yadakk.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Yadakk extends AbstractTribe { + protected Yadakk() { + super(Tech.ROADS, 7, false, 1.5, 1, .5, 1, .5, 1, 1); + } +} diff --git a/java/src/models/concreteTribes/Zebasi.java b/java/src/models/concreteTribes/Zebasi.java new file mode 100644 index 0000000..f88d092 --- /dev/null +++ b/java/src/models/concreteTribes/Zebasi.java @@ -0,0 +1,10 @@ +package models.concreteTribes; + +import models.AbstractTribe; +import models.Tech; + +public class Zebasi extends AbstractTribe { + protected Zebasi() { + super(Tech.FARMING, 5, false, .5, 1, .5, 1, .5, 1, 1); + } +} diff --git a/out/production/polytopia-backend/console/Main.class b/out/production/polytopia-backend/console/Main.class new file mode 100644 index 0000000..e765362 Binary files /dev/null and b/out/production/polytopia-backend/console/Main.class differ diff --git a/out/production/polytopia-backend/console/utils/ValidCommands.class b/out/production/polytopia-backend/console/utils/ValidCommands.class new file mode 100644 index 0000000..6ce4393 Binary files /dev/null and b/out/production/polytopia-backend/console/utils/ValidCommands.class differ diff --git a/out/production/src/Main.class b/out/production/src/Main.class new file mode 100644 index 0000000..c15ac94 Binary files /dev/null and b/out/production/src/Main.class differ diff --git a/polytopia-backend.iml b/polytopia-backend.iml new file mode 100644 index 0000000..e13cb1e --- /dev/null +++ b/polytopia-backend.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/polytopia_algorithms/__init__.py b/polytopia_algorithms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polytopia_algorithms/alfa_beta.py b/polytopia_algorithms/alfa_beta.py new file mode 100644 index 0000000..5bc5fdb --- /dev/null +++ b/polytopia_algorithms/alfa_beta.py @@ -0,0 +1,83 @@ +def vegallapot(n): + """ + Ellenőrzi, hogy a játék végállapot-e. + Ha valamelyik játékos összes városát és egységét elvesztette, akkor vége a játéknak. + """ + return n["player1_cities"] == 0 or n["player2_cities"] == 0 + + +def hasznossag(n): + """ + Hasznossági függvény: értékeli az állapotot. + - Pozitív érték: előnyben van az első játékos. + - Negatív érték: előnyben van a második játékos. + """ + return (n["player1_cities"] * 10 + n["player1_units"] * 2) - (n["player2_cities"] * 10 + n["player2_units"] * 2) + + +def neighbors_of_n(n): + """ + Generálja az összes lehetséges következő állapotot az aktuális játékos számára. + - Egy játékos mozgathatja az egységeit, támadhat, vagy új egységeket képezhet. + - Itt egyszerűsített lépéseket modellezünk. + """ + allapotok = [] + + # Pl. az első játékos új egységeket képezhet vagy mozgathat + if n["current_player"] == 1: + if n["player1_cities"] > 0: # Ha van városa, képezhet egységet + uj_allapot = n.copy() + uj_allapot["player1_units"] += 1 + uj_allapot["current_player"] = 2 + allapotok.append(uj_allapot) + + if n["player2_units"] > 0: # Ha van ellenséges egység, támadhat + uj_allapot = n.copy() + uj_allapot["player2_units"] -= 1 + uj_allapot["current_player"] = 2 + allapotok.append(uj_allapot) + + # Második játékos hasonló akciókat végezhet + else: + if n["player2_cities"] > 0: + uj_allapot = n.copy() + uj_allapot["player2_units"] += 1 + uj_allapot["current_player"] = 1 + allapotok.append(uj_allapot) + + if n["player1_units"] > 0: + uj_allapot = n.copy() + uj_allapot["player1_units"] -= 1 + uj_allapot["current_player"] = 1 + allapotok.append(uj_allapot) + + return allapotok + +INF = float('inf') + +def MaxErtek(n, alfa, beta): + if vegallapot(n): + return hasznossag(n) + + max = -INF + for a in neighbors_of_n: + max = max(max, MinErtek(a, alfa, beta)) + if max >= beta: + return max + + alfa = max(max, alfa) + return max + + +def MinErtek(n, alfa, beta): + if vegallapot(n): + return hasznossag(n) + + min = +INF + for a in neighbors_of_n: + min = min(min, MaxErtek(a, alfa, beta)) + if alfa >= min: + return min + beta = min(min, beta) + return min + diff --git a/polytopia_algorithms/game_runner.py b/polytopia_algorithms/game_runner.py new file mode 100644 index 0000000..969df80 --- /dev/null +++ b/polytopia_algorithms/game_runner.py @@ -0,0 +1,33 @@ +from typing import Any + +from polytopia_game.settings import Settings +import random + +class Game: + def __init__(self, maximizing_player, minimizing_player, tree, map_settings, players): + self.maximizing_player = maximizing_player + self.minimizing_player = minimizing_player + self.tree = tree + self.map_settings = map_settings # Currently unused, but can store game configuration. + + self.players = players + self.player_names = ['Zoy', 'Midjiwan', 'Midjitone', 'GullYY', 'Espark', 'Tolbby', 'CadeTheFrogger', 'Innofunni', + 'Finiite', 'Guardian', 'Generukk', 'McGoon', 'Sljivovica'] + random.shuffle(self.player_names) + + + def run(self): + print(f"Initialized players: {self.players}") + + # Shuffle player names + random.shuffle(self.player_names) + + # Create player scores + player_scores: dict[Any, int] = {} + for player in self.players: + player_scores[player] = random.randint(90, 1000) * 10 + 5 + + # Random MinMax score + minmax_score = random.randint(-100, 100) + + return player_scores, minmax_score \ No newline at end of file diff --git a/polytopia_algorithms/min_max.py b/polytopia_algorithms/min_max.py new file mode 100644 index 0000000..9dcc63e --- /dev/null +++ b/polytopia_algorithms/min_max.py @@ -0,0 +1,78 @@ +def vegallapot(n): + """ + Ellenőrzi, hogy a játék végállapot-e. + Ha valamelyik játékos összes városát és egységét elvesztette, akkor vége a játéknak. + """ + return n["player1_cities"] == 0 or n["player2_cities"] == 0 + + +def hasznossag(n): + """ + Hasznossági függvény: értékeli az állapotot. + - Pozitív érték: előnyben van az első játékos. + - Negatív érték: előnyben van a második játékos. + """ + return (n["player1_cities"] * 10 + n["player1_units"] * 2) - (n["player2_cities"] * 10 + n["player2_units"] * 2) + + +def neighbors_of_n(n): + """ + Generálja az összes lehetséges következő állapotot az aktuális játékos számára. + - Egy játékos mozgathatja az egységeit, támadhat, vagy új egységeket képezhet. + - Itt egyszerűsített lépéseket modellezünk. + """ + allapotok = [] + + # Pl. az első játékos új egységeket képezhet vagy mozgathat + if n["current_player"] == 1: + if n["player1_cities"] > 0: # Ha van városa, képezhet egységet + uj_allapot = n.copy() + uj_allapot["player1_units"] += 1 + uj_allapot["current_player"] = 2 + allapotok.append(uj_allapot) + + if n["player2_units"] > 0: # Ha van ellenséges egység, támadhat + uj_allapot = n.copy() + uj_allapot["player2_units"] -= 1 + uj_allapot["current_player"] = 2 + allapotok.append(uj_allapot) + + # Második játékos hasonló akciókat végezhet + else: + if n["player2_cities"] > 0: + uj_allapot = n.copy() + uj_allapot["player2_units"] += 1 + uj_allapot["current_player"] = 1 + allapotok.append(uj_allapot) + + if n["player1_units"] > 0: + uj_allapot = n.copy() + uj_allapot["player1_units"] -= 1 + uj_allapot["current_player"] = 1 + allapotok.append(uj_allapot) + + return allapotok + +INF = float('inf') + +def minmax(n): + def max_value(n): + if vegallapot(n): + return hasznossag(n) + + max = -INF + for a in neighbors_of_n: + max = max(max, min_value(a)) + return max + + def min_value(n): + if vegallapot(n): + return hasznossag(n) + + min = +INF + for a in neighbors_of_n: + min = min(min, max_value(a)) + + return min + + return min_value(n), max_value(n) diff --git a/polytopia_algorithms/run_game.py b/polytopia_algorithms/run_game.py new file mode 100644 index 0000000..eb3d760 --- /dev/null +++ b/polytopia_algorithms/run_game.py @@ -0,0 +1,35 @@ +import random + +from polytopia_game.settings import Settings +from game_runner import Game + +# Players +maximalizalo_jatekos = "Player 1 (Max)" +minimalizalo_jatekos = "Player 2 (Min)" + +# Game tree (scores at the leaf nodes) +tree = [None, None, None, None, None, None, None, 5, 6, 7, 1, -13, 9, -8, -4] + +map_settings = { + "game_mode": Settings.GAME_MODE, + "map_size": Settings.MAP_SIZE, + "map_type": Settings.MAP_TYPE.name, +} + +players = ['Zoy', 'Midjiwan'] +game = Game(maximalizalo_jatekos, minimalizalo_jatekos, tree, map_settings, players) + +(player_scores, minmax_score) = game.run() +winner = maximalizalo_jatekos if minmax_score > 0 else minimalizalo_jatekos + +print(f"Players: {game.players}") +print(f"Player names: {game.player_names}") +print(f"Length of player names: {len(game.player_names)}") +print(f"Player scores: {player_scores}") + + +if game.player_names: + winner = random.choice([game.player_names[0], game.player_names[-1]]) +else: + winner = "Unknown" # or any other default behavior +print(f"The winner is: {winner}! Personal score: {player_scores}. Game score: {minmax_score}") diff --git a/polytopia_game/__init__.py b/polytopia_game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/game_map.py b/polytopia_game/game_map.py similarity index 99% rename from game_map.py rename to polytopia_game/game_map.py index d42c7ed..49d69ad 100644 --- a/game_map.py +++ b/polytopia_game/game_map.py @@ -8,6 +8,7 @@ # Local import settings + MAP_HEIGHT = settings.MAP_HEIGHT MAP_WIDTH = settings.MAP_WIDTH diff --git a/game_simulator.py b/polytopia_game/game_simulator.py similarity index 100% rename from game_simulator.py rename to polytopia_game/game_simulator.py diff --git a/map_renderer.py b/polytopia_game/map_renderer.py similarity index 99% rename from map_renderer.py rename to polytopia_game/map_renderer.py index c834240..9218824 100644 --- a/map_renderer.py +++ b/polytopia_game/map_renderer.py @@ -4,7 +4,7 @@ # Third-party import numpy as np -from PIL import Image +from Pillow import Image # Local import settings diff --git a/polytopia_game/settings.py b/polytopia_game/settings.py new file mode 100644 index 0000000..cd19d09 --- /dev/null +++ b/polytopia_game/settings.py @@ -0,0 +1,113 @@ +import random +import pathlib +from enum import Enum + +class GameMode(Enum): + GLORY = 0 + MIGHT = 1 + FREE_ROAMING_MODE = 2 # FNAF reference + +class MapSizes(Enum): + TINY = 11 + SMALL = 14 + NORMAL = 16 + LARGE = 18 + HUGE = 20 + MASSIVE = 30 + + def __mul__(self, other): + if isinstance(other, (int, float)): # Multiply with numbers + return self.value * other + elif isinstance(other, MapSizes): # Multiply with another enum member + return self.value * other.value + else: + raise TypeError(f"Unsupported operand type(s) for *: '{type(self)}' and '{type(other)}'") + +class MapTypes(Enum): + DRYLAND = 0 + LAKES = 1 + PANGEA = 2 + CONTINENTS = 3 + ARCHIPELAGO = 4 + WATER_WORLD = 5 + + +def setNumberOfPlayers(number_of_desired_players): + if number_of_desired_players < 2: + raise ValueError("The number of players must be at least 2!") + if number_of_desired_players > 16: + raise ValueError("There cannot be more than 16 players in a game!") + return number_of_desired_players + + +class Settings: + GAME_MODE = GameMode.MIGHT + + MAP_HEIGHT = MapSizes.NORMAL + MAP_WIDTH = MapSizes.NORMAL + + MAP_SIZE = MAP_WIDTH * MAP_HEIGHT + + MAP_TYPE = MapTypes.DRYLAND + + NUMBER_OF_PLAYERS = setNumberOfPlayers(2) + + + TILE_PROPERTY_CHANNEL = 0 + UNIT_TYPE_CHANNEL = 1 + UNIT_STATE_CHANNEL = 2 + UNIT_TEAM_CHANNEL = 3 + UNIT_HEALTH_CHANNEL = 4 + + CHANNEL_ATTRIBUTES = {0: {"channel name": "tile property", + "description": """ + What 'property' is on the tile, such as a resource type (forest, animal, etc.). + Needed for the V0 example because a tile can either have nothing on it, or a + city. + """, + "possible values": {0: "nothing", + 1: "team 1 city", + 2: "team 2 city"}}, + 1: {"channel name": "unit type", + "description": """ + The kind of unit that is on the tile (warrior, rider, etc.). Even though V0 + will feature warriors as the only unit, this channel is necessary to + highlight the *absence* of a unit. + """, + "possible values": {0: "no unit", + 1: "warrior"}}, + 2: {"channel name": "unit state", + "description": """ + What 'state' that unit is currently in. Examples include unit being able to + move/attack, being able to just attack, or being able to just move. This will + mostly be necessary for V0 to distinguish between units that have already moved + in this turn, or those that can still move. + """, + "possible values": {0: "unit has not taken action", + 1: "unit has moved, but can still attack", + 2: "unit can no longer take action"}}, + 3: {"channel name": "unit team", + "description": """ + What team the unit is on. In V0 there will only be two possible teams. This is + separate from the concept of a 'tribe', which is absent in V0. + """, + "possible values": {0: "no team", + 1: "team 1", + 2: "team 2"}}, + 4: {"channel name": "unit health", + "description": """ + How much health the unit has remaining. This is an int value (instead of a + category). The maximum possible health value in V0 would be 15 (for a veteran + warrior), and the minimum would be 1. + """, + "possible values": {0: "no unit", + 1-15: "unit health value"}} + } + + ## OTHER THINGS THAT NEED TO BE TRACKED FOR THE GAME STATE: + # - Current player team (0/1) + # - Number of Stars + # - Number of units (0-2) + # - Score of players + + BASE_DIR = str(pathlib.Path(__file__).parent.resolve()) \ No newline at end of file diff --git a/polytopia_score_counter/__init__.py b/polytopia_score_counter/__init__.py new file mode 100644 index 0000000..02c814b --- /dev/null +++ b/polytopia_score_counter/__init__.py @@ -0,0 +1,4 @@ +# polytopia_score_counter/__init__.py + +from .rewards import calculate_reward +from .actions import ACTIONS diff --git a/polytopia_score_counter/__pycache__/__init__.cpython-312.pyc b/polytopia_score_counter/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..0aa7e9d Binary files /dev/null and b/polytopia_score_counter/__pycache__/__init__.cpython-312.pyc differ diff --git a/polytopia_score_counter/__pycache__/actions.cpython-312.pyc b/polytopia_score_counter/__pycache__/actions.cpython-312.pyc new file mode 100644 index 0000000..18923ac Binary files /dev/null and b/polytopia_score_counter/__pycache__/actions.cpython-312.pyc differ diff --git a/polytopia_score_counter/__pycache__/rewards.cpython-312.pyc b/polytopia_score_counter/__pycache__/rewards.cpython-312.pyc new file mode 100644 index 0000000..363af20 Binary files /dev/null and b/polytopia_score_counter/__pycache__/rewards.cpython-312.pyc differ diff --git a/polytopia_score_counter/actions.py b/polytopia_score_counter/actions.py new file mode 100644 index 0000000..062dee0 --- /dev/null +++ b/polytopia_score_counter/actions.py @@ -0,0 +1,32 @@ +# polytopia_score_counter/actions.py + +# List of valid actions for reference +ACTIONS = [ + "train_unit", + "lose_unit", + "upgrade_city", + "place_structure", + "capture_city", + "lose_city", + "gain_territory", + "lose_territory", + "research_tech", + "discover_fog", + "park", +] + +_2D_ACTIONS = [ + {"action": "train_unit", "unit_type": "archer"}, + {"action": "lose_unit", "unit_type": "warrior"}, + {"action": "upgrade_city", "city_population_gain": 3}, + {"action": "place_structure", "structure": "monument"}, + {"action": "capture_city", "city_population_gain": 1}, + {"action": "lose_city", "city_population_loss": 2}, + {"action": "gain_territory"}, + {"action": "lose_territory"}, + {"action": "research_tech", "tech_tier": 1}, + {"action": "research_tech", "tech_tier": 2}, + {"action": "research_tech", "tech_tier": 3}, + {"action": "discover_fog"}, + {"action": "park"}, +] diff --git a/polytopia_score_counter/rewards.py b/polytopia_score_counter/rewards.py new file mode 100644 index 0000000..a08818c --- /dev/null +++ b/polytopia_score_counter/rewards.py @@ -0,0 +1,58 @@ +# polytopia_score_counter/rewards.py + +# Star cost for common units +UNIT_STAR_COST = { + "warrior": 2, + "rider": 3, + "archer": 3, + "defender": 3, + "swordsman": 5, + "catapult": 8, + "cloak": 8, + "knight": 8, + "giant": 10, +} + + +def calculate_reward(action, **kwargs): + if action == "train_unit": + unit_type = kwargs.get("unit_type", None) + return 5 * UNIT_STAR_COST.get(unit_type, 0) + + if action == "lose_unit": + unit_type = kwargs.get("unit_type", None) + return -5 * UNIT_STAR_COST.get(unit_type, 0) + + if action == "upgrade_city": + city_population_gain = kwargs.get("city_population_gain", 0) + return 50 + (5 * city_population_gain) + + if action == "place_structure": + structure = kwargs.get("structure", None) + if structure == "monument": + return 400 + + if action == "capture_city": + return 100 + + if action == "lose_city": + city_population_loss = kwargs.get("city_population_loss", 0) + return -(50 + (5 * city_population_loss)) + + if action == "gain_territory": + return 20 + + if action == "lose_territory": + return -20 + + if action == "research_tech": + tech_tier = kwargs.get("tech_tier", 1) + return 100 * tech_tier + + if action == "discover_fog": + return 5 + + if action == "park": + return 250 + + return 0 # Default reward for neutral actions diff --git a/polytopia_tests/__init__.py b/polytopia_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polytopia_tests/polytopia_game_test.py b/polytopia_tests/polytopia_game_test.py new file mode 100644 index 0000000..e69de29 diff --git a/polytopia_tests/polytopia_score_counter_test.py b/polytopia_tests/polytopia_score_counter_test.py new file mode 100644 index 0000000..89abf83 --- /dev/null +++ b/polytopia_tests/polytopia_score_counter_test.py @@ -0,0 +1,20 @@ +from polytopia_score_counter.rewards import calculate_reward +from polytopia_score_counter.actions import _2D_ACTIONS + + +def main(): + score = 0 + + # Loop through each action and calculate the reward + for action_info in _2D_ACTIONS: + action = action_info["action"] + # Call calculate_reward dynamically with the appropriate arguments + reward = calculate_reward(action, **{key: value for key, value in action_info.items() if key != "action"}) + score += reward + print(f"Reward for {action}: {reward}") + + print(f'Total score: {score}') + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 405ec23..08c39ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,7 @@ -numpy==1.19.5 -Pillow==8.2.0 +numpy~=2.2.1 +Pillow~=11.1.0 + +pip~=24.3.1 +Flask~=3.1.0 + +jinja2 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..a012ea7 --- /dev/null +++ b/run.py @@ -0,0 +1,135 @@ +# Windows HP templates route: C:\Users\HP\PycharmProjects\polytopia_python\consoleApp\templates +# Windows truncated templates route: C:\PycharmProjects\polytopia_python\consoleApp\templates +# relative templates route: polytopia_python\consoleApp\templates +from flask import Flask, render_template + +# Create a Flask consoleApp +consoleApp = Flask(__name__) + +# add Index page, Privacy, Dashboard, About, Settings, HallOfFame and ThroneRoom routes! I have the content for them, you just make the routes and the navigation! + +# Route for the homepage +@consoleApp.route("/") +def home(): + return ( + """ + + + + + Index + + +{% extends "base.html" %} + +{% block content %} +

The Battle of Polytopia

The server is running at 127.0.0.1:5000

" +

This is the Index page. Explore other sections using the navigation above.

+{% endblock %} +@{ + ViewData["Title"] = "Home Page"; +} +
+ polyhead + polytopia-background.jpg + polytopia-main-menu.webp + soundfx-ico.png + + + + +
+
+

The Battle of Polytopia

+
+ +
+ +
+ + + + """ + ) + +@consoleApp.route("/game") +def game(): + return "" +# Route for the Privacy page +# TODO Fix error Template file 'polytopia_python' not found +@consoleApp.route("/privacy") +def privacy(): + return +""" + + + + + Title + + +{% extends "base.html" %} + +{% block content %} +

Privacy Policy

+

This is the Privacy page content.

+{% endblock %} + + + +""" + +# Route for the Dashboard page +# TODO Fix error Template file 'dashboard. html' not found +@consoleApp.route("/dashboard") +def dashboard(): + return render_template("dashboard.html") + +# Route for the About page +@consoleApp.route("/about") +def about(): + return render_template("about.html") + +# Route for the Settings page +@consoleApp.route("/settings") +def settings(): + return render_template("settings.html") + +# Route for the Hall of Fame page +@consoleApp.route("/hall-of-fame") +def hall_of_fame(): + return render_template("hall_of_fame.html") + +# Route for the Throne Room page +@consoleApp.route("/throne-room") +def throne_room(): + return render_template("throne_room.html") + +if __name__ == "__main__": + consoleApp.run(debug=True, host="127.0.0.1", port=5000) diff --git a/settings.py b/settings.py deleted file mode 100644 index 9ff706e..0000000 --- a/settings.py +++ /dev/null @@ -1,62 +0,0 @@ -import pathlib - -MAP_HEIGHT = 6 -MAP_WIDTH = 6 - -TILE_PROPERTY_CHANNEL = 0 -UNIT_TYPE_CHANNEL = 1 -UNIT_STATE_CHANNEL = 2 -UNIT_TEAM_CHANNEL = 3 -UNIT_HEALTH_CHANNEL = 4 - -CHANNEL_ATTRIBUTES = {0: {"channel name": "tile property", - "description": """ - What 'property' is on the tile, such as a resource type (forest, animal, etc.). - Needed for the V0 example because a tile can either have nothing on it, or a - city. - """, - "possible values": {0: "nothing", - 1: "team 1 city", - 2: "team 2 city"}}, - 1: {"channel name": "unit type", - "description": """ - The kind of unit that is on the tile (warrior, rider, etc.). Even though V0 - will feature warriors as the only unit, this channel is necessary to - highlight the *absence* of a unit. - """, - "possible values": {0: "no unit", - 1: "warrior"}}, - 2: {"channel name": "unit state", - "description": """ - What 'state' that unit is currently in. Examples include unit being able to - move/attack, being able to just attack, or being able to just move. This will - mostly be necessary for V0 to distinguish between units that have already moved - in this turn, or those that can still move. - """, - "possible values": {0: "unit has not taken action", - 1: "unit has moved, but can still attack", - 2: "unit can no longer take action"}}, - 3: {"channel name": "unit team", - "description": """ - What team the unit is on. In V0 there will only be two possible teams. This is - separate from the concept of a 'tribe', which is absent in V0. - """, - "possible values": {0: "no team", - 1: "team 1", - 2: "team 2"}}, - 4: {"channel name": "unit health", - "description": """ - How much health the unit has remaining. This is an int value (instead of a - category). The maximum possible health value in V0 would be 15 (for a veteran - warrior), and the minimum would be 1. - """, - "possible values": {0: "no unit", - 1-15: "unit health value"}} - } - -## OTHER THINGS THAT NEED TO BE TRACKED FOR THE GAME STATE: -# - Current player team (0/1) -# - Number of Stars -# - Number of units (0-2) - -BASE_DIR = str(pathlib.Path(__file__).parent.resolve()) \ No newline at end of file