Esempio n. 1
0
    def setUp(self) -> None:
        self.tiles = []
        # these have to be added in the order of bottom to top, left to right in order for get_tile(x, y) to work
        # so (0, 0), (1, 0), .... (COLUMNS_NUMBER-1, 0), (0, 1), (1, 1), ...
        # this is a linear map with, from left to right, a column of water, plains, hills, plains, mountains
        for y in range(5):
            self.tiles.append(Tile(0, y, 1, 0))
            self.tiles.append(Tile(1, y, 1, 1))
            self.tiles.append(Tile(2, y, 1, 2))
            self.tiles.append(Tile(3, y, 1, 1))
            self.tiles.append(Tile(4, y, 1, 3))
        self.game_logic = GameLogic(self.tiles, 5, 5, players=[("one", CIV_ONE, "gray"), ("two", CIV_TWO, "red")],
                                    my_nick="one")
        self.game_logic.add_unit(2, 2, "one", 'Settler', 1)
        self.settler = self.game_logic.get_tile(2, 2).occupant

        self.area = []
        self.area.append(Tile(0, 1, 1, 0))
        self.area.append(Tile(1, 1, 1, 0))
        self.area.append(Tile(2, 1, 1, 1))
        self.area.append(Tile(2, 2, 1, 2))
        self.area.append(Tile(0, 0, 1, 2))
        self.area.append(Tile(1, 2, 1, 3))

        self.game_logic.build_city(self.settler, "test_name")
        self.tile = self.settler.tile
        self.city = self.tile.city
Esempio n. 2
0
    def __init__(self, width: int, height: int, tiles: list, client):
        """
        :param width: Max screen width.
        :param height: Max screen height.
        :param tiles: A 2D list of integer values representing tile types.
        :param client: A client object for server communication.
        """
        super().__init__()

        self.client = client
        self.my_turn = False
        self.cur_enemy = ""

        self.SCREEN_WIDTH = width
        self.SCREEN_HEIGHT = height
        self.SCROLL_STEP_X = SCROLL_STEP * width
        self.SCROLL_STEP_Y = SCROLL_STEP * height
        self.TILE_ROWS = len(tiles)
        self.TILE_COLS = len(tiles[0])
        self.zoom = 0

        self.top_bar = TopBar(None, TOP_BAR_SIZE)
        self.unit_popup = UnitPopup(4 * TOP_BAR_SIZE, 4 * TOP_BAR_SIZE)
        self.update_popup = False  # used to only update pop-up once per opponent's move, otherwise game is laggy
        self.update_topbar = False  # used to update top bar if my city has been taken
        self.update_diplo = None  # will be updated to a tuple of (message, sender, is_rejectable)
        self.diplo_answered = threading.Event()
        self.city_popup = CityCreationPopup(4 * TOP_BAR_SIZE, 5 * TOP_BAR_SIZE)
        self.diplo_popup = DiplomaticPopup(9 * TOP_BAR_SIZE, 3 * TOP_BAR_SIZE, self.diplo_answered)
        self.end_popup = EndingPopup(6 * TOP_BAR_SIZE, 6 * TOP_BAR_SIZE)
        self.ranking = None
        self.tiles = tiles

        self.tile_sprites = arcade.SpriteList()
        # needs to be smarter tbh but depends on the size of a real map
        self.tile_size = int((height - self.top_bar.height) / self.TILE_ROWS) - MARGIN
        # in order to center the tiles vertically and horizontally
        self.centering_x = (width - self.TILE_COLS * (self.tile_size + MARGIN)) / 2
        self.centering_y = ((height - self.top_bar.height) - self.TILE_ROWS * (self.tile_size + MARGIN)) / 2

        for row in range(self.TILE_ROWS):
            for col in range(self.TILE_COLS):
                tile = Tile(col, row, self.tile_size, tiles[row][col])
                tile.center_x = col * (self.tile_size + MARGIN) + (self.tile_size / 2) + MARGIN + self.centering_x
                tile.center_y = row * (self.tile_size + MARGIN) + (self.tile_size / 2) + MARGIN + self.centering_y
                self.tile_sprites.append(tile)

        self.game_logic = GameLogic(self.tile_sprites, self.TILE_ROWS, self.TILE_COLS, self.client.players,
                                    self.client.nick)
        self.top_bar.me = self.game_logic.me
        self.city_view = CityView(self.top_bar)
        self.enemy_city_view = EnemyCityView(self.top_bar, self.city_view.app, self.client, self.game_logic.me)
        self.top_bar.update_treasury()

        threading.Thread(target=self.wait_for_my_turn).start()
Esempio n. 3
0
class CityAreaTest(unittest.TestCase):
    def setUp_bigger(self):
        self.tiles = []
        for x in range(20):
            for y in range(20):
                self.tiles.append(Tile(x, y, 1, 1))
        self.game_logic = GameLogic(self.tiles,
                                    20,
                                    20,
                                    players=[("one", CIV_ONE, "gray"),
                                             ("two", CIV_TWO, "red")],
                                    my_nick="one")
        self.game_logic.add_unit(10, 10, "one", 'Settler', 1)
        settler = self.game_logic.get_tile(10, 10).occupant
        self.game_logic.build_city(settler, "stub")

    def test_city_resize(self):
        self.setUp_bigger()
        x_c, y_c = 10, 10
        city = self.game_logic.get_tile(x_c, y_c).city
        owner = city.owner
        for r in range(2, 9):
            self.game_logic.increase_area(x_c, y_c)
            self.assertEqual(city.current_radius, r)
            for x in range(x_c - r, x_c + r + 1):
                for y in range(y_c - r, y_c + r + 1):
                    tile = self.game_logic.get_tile(x, y)
                    if sqrt((x_c - x)**2 + (y_c - y)**2) <= r:
                        self.assertIn(tile, city.area)
                        self.assertEqual(tile.owner, owner)
                    else:
                        self.assertNotIn(tile, city.area)
                        self.assertIsNone(tile.owner)
Esempio n. 4
0
 def setUp(self) -> None:
     self.tiles = []
     # these have to be added in the order of bottom to top, left to right in order for get_tile(x, y) to work
     # so (0, 0), (1, 0), .... (COLUMNS_NUMBER-1, 0), (0, 1), (1, 1), ...
     # this is a linear map with, from left to right, a column of water, plains, hills, plains, mountains
     for y in range(5):
         self.tiles.append(Tile(0, y, 1, 0))
         self.tiles.append(Tile(1, y, 1, 1))
         self.tiles.append(Tile(2, y, 1, 2))
         self.tiles.append(Tile(3, y, 1, 1))
         self.tiles.append(Tile(4, y, 1, 3))
     self.game_logic = GameLogic(self.tiles, 5, 5, players=[("one", CIV_ONE, "gray"), ("two", CIV_TWO, "red")],
                                 my_nick="one")
Esempio n. 5
0
 def setUp_bigger(self):
     self.tiles = []
     for x in range(20):
         for y in range(20):
             self.tiles.append(Tile(x, y, 1, 1))
     self.game_logic = GameLogic(self.tiles,
                                 20,
                                 20,
                                 players=[("one", CIV_ONE, "gray"),
                                          ("two", CIV_TWO, "red")],
                                 my_nick="one")
     self.game_logic.add_unit(10, 10, "one", 'Settler', 1)
     settler = self.game_logic.get_tile(10, 10).occupant
     self.game_logic.build_city(settler, "stub")
Esempio n. 6
0
class BuildingsTest(unittest.TestCase):
    def setUp(self) -> None:
        self.tiles = []
        # these have to be added in the order of bottom to top, left to right in order for get_tile(x, y) to work
        # so (0, 0), (1, 0), .... (COLUMNS_NUMBER-1, 0), (0, 1), (1, 1), ...
        # this is a linear map with, from left to right, a column of water, plains, hills, plains, mountains
        for y in range(5):
            self.tiles.append(Tile(0, y, 1, 0))
            self.tiles.append(Tile(1, y, 1, 1))
            self.tiles.append(Tile(2, y, 1, 2))
            self.tiles.append(Tile(3, y, 1, 1))
            self.tiles.append(Tile(4, y, 1, 3))
        self.game_logic = GameLogic(self.tiles, 5, 5, players=[("one", CIV_ONE, "gray"), ("two", CIV_TWO, "red")],
                                    my_nick="one")
        self.game_logic.add_unit(2, 2, "one", 'Settler', 1)
        self.settler = self.game_logic.get_tile(2, 2).occupant

        self.area = []
        self.area.append(Tile(0, 1, 1, 0))
        self.area.append(Tile(1, 1, 1, 0))
        self.area.append(Tile(2, 1, 1, 1))
        self.area.append(Tile(2, 2, 1, 2))
        self.area.append(Tile(0, 0, 1, 2))
        self.area.append(Tile(1, 2, 1, 3))

        self.game_logic.build_city(self.settler, "test_name")
        self.tile = self.settler.tile
        self.city = self.tile.city

    def test_mines(self):
        before_goods = self.city.calculate_goods()
        self.city.buildings["Mines"] = True
        after_goods = self.city.calculate_goods()
        self.assertEqual(after_goods["stone"], before_goods["stone"] + 20)

    def test_free_market(self):
        before_goods = self.city.calculate_goods()
        self.city.buildings["Free Market"] = True
        after_goods = self.city.calculate_goods()
        self.assertEqual(after_goods["gold"], before_goods["gold"] + len(self.city.area) * 3)

    def test_free_market_2(self):
        self.city.area.append(Tile(0, 0, 1, 0))

        before_goods = self.city.calculate_goods()
        self.city.buildings["Free Market"] = True
        after_goods = self.city.calculate_goods()
        self.assertEqual(after_goods["gold"], before_goods["gold"] + len(self.city.area) * 3)

    def test_astronomic_tower(self):
        self.game_logic.increase_area(*self.tile.coords)
        self.assertEqual(len(self.city.area), 13)
        self.game_logic.increase_area(*self.tile.coords)
        self.assertEqual(len(self.city.area), 25)
Esempio n. 7
0
class MovementTests(unittest.TestCase):
    def setUp(self) -> None:
        self.tiles = []
        # these have to be added in the order of bottom to top, left to right in order for get_tile(x, y) to work
        # so (0, 0), (1, 0), .... (COLUMNS_NUMBER-1, 0), (0, 1), (1, 1), ...
        # this is a linear map with, from left to right, a column of water, plains, hills, plains, mountains
        for y in range(5):
            self.tiles.append(Tile(0, y, 1, 0))
            self.tiles.append(Tile(1, y, 1, 1))
            self.tiles.append(Tile(2, y, 1, 2))
            self.tiles.append(Tile(3, y, 1, 1))
            self.tiles.append(Tile(4, y, 1, 3))
        self.game_logic = GameLogic(self.tiles,
                                    5,
                                    5,
                                    players=[("one", CIV_ONE, "gray"),
                                             ("two", CIV_TWO, "red")],
                                    my_nick="one")
        self.game_logic.add_unit(2, 2, "one", 'Settler', 1)
        self.settler = self.game_logic.get_tile(2, 2).occupant

        self.area = []
        self.area.append(Tile(0, 1, 1, 0))
        self.area.append(Tile(1, 1, 1, 0))
        self.area.append(Tile(2, 1, 1, 1))
        self.area.append(Tile(2, 2, 1, 2))
        self.area.append(Tile(0, 0, 1, 2))
        self.area.append(Tile(1, 2, 1, 3))

    def test_city_building(self):
        # a city is created
        # the settler should be deleted and the tile unoccupied
        # the city should be owned by the owner of the settler
        # and should have a proper name and tile set
        player = self.settler.owner
        self.game_logic.build_city(self.settler, "test_name")
        tile = self.settler.tile
        self.assertNotIn(self.settler, player.units)
        self.assertFalse(tile.occupied())
        city = tile.city
        self.assertIsNotNone(city)
        self.assertEqual(city.owner, player)
        self.assertEqual(city.name, "test_name")

    def test_city_area(self):
        # a city is created
        # if all tiles around it are not owned, it gets a 3x3 area where the middle point is the city
        self.game_logic.build_city(self.settler, "test_name")
        player = self.settler.owner
        tile = self.settler.tile
        city = tile.city
        expected_area = {(x, y) for x in range(1, 4) for y in range(1, 4)}
        real_area = {t.coords for t in city.area}
        self.assertEqual(expected_area, real_area)
        for x, y in expected_area:
            self.assertEqual(self.game_logic.get_tile(x, y).owner, player)

    def test_two_cities_area(self):
        # two cities are created, and the second one is so close that the 3x3 square around it contains
        # the territory of the first one
        # we'll make sure that this territory won't be taken by the new city
        self.game_logic.add_unit(2, 0, "two", 'Settler', 1)
        self.game_logic.build_city(self.settler, "one_city")
        self.game_logic.build_opponents_city(2, 0, "two_city")
        expected_area = {(1, 0), (2, 0), (3, 0)}
        city = self.game_logic.get_tile(2, 0).city
        player = city.owner
        real_area = {t.coords for t in city.area}
        self.assertEqual(expected_area, real_area)
        for x, y in expected_area:
            self.assertEqual(self.game_logic.get_tile(x, y).owner, player)

    def test_simple_border(self):
        # a city is created
        # since the basic area for a city is a 3x3 square, it's borders are all it's tiles except the middle point
        self.game_logic.build_city(self.settler, "one_city")
        expected_border = {(x, y)
                           for x in range(1, 4) for y in range(1, 4)
                           if not x == y == 2}
        player = self.settler.owner
        real_border = {border.tile.coords for border in player.borders}
        self.assertEqual(expected_border, real_border)

    def test_separated_border(self):
        # two cities whose borders don't touch
        # and they are on screen edges, so their areas are 2x3, which means all their tiles should be borders
        self.game_logic.add_unit(1, 0, "one", 'Settler', 1)
        self.game_logic.build_opponents_city(1, 0, "city1")
        self.game_logic.add_unit(3, 4, "one", 'Settler', 1)
        self.game_logic.build_opponents_city(3, 4, "city2")
        city1 = self.game_logic.get_tile(1, 0).city
        city2 = self.game_logic.get_tile(3, 4).city
        player = city1.owner
        expected_border = {
            tile.coords
            for city in [city1, city2] for tile in city.area
        }
        real_border = {border.tile.coords for border in player.borders}
        self.assertEqual(expected_border, real_border)

    def test_border_update(self):
        # we will create two cities so that their borders connect
        # some tiles should stop being borders then
        player = self.settler.owner
        self.game_logic.build_city(self.settler, "city1")
        self.assertIn((2, 1),
                      [border.tile.coords for border in player.borders])
        # another city that's very close so the borders touch
        self.game_logic.add_unit(2, 0, "one", 'Settler', 1)
        self.game_logic.build_opponents_city(2, 0, "city2")
        expected_border = {(x, y)
                           for x in range(1, 4) for y in range(0, 4)
                           if not (x == 2 and 0 < y < 3)}  # no (2, 1)!
        real_border = {border.tile.coords for border in player.borders}
        self.assertEqual(expected_border, real_border)

    def test_calculating_goods_no_city(self):
        # just check based on known output
        self.assertEqual({
            'gold': 2,
            'wood': 25,
            'stone': 5,
            'food': 27
        }, City.calculate_goods_no_city(self.area))

    def test_calculating_goods_based_on_city(self):
        # building city and calculating based on his area.
        self.game_logic.build_city(self.settler, "test_name")
        city = self.settler.tile.city
        assert city is not None
        city.set_area(self.area)
        self.assertEqual({
            'gold': 2,
            'wood': 25,
            'stone': 5,
            'food': 27
        }, city.calculate_goods())

    def test_city_capture(self):
        # player two's forces have made it to the players one's stub_city and are ready to capture it
        self.setUp()
        x, y = self.settler.tile.coords
        player1 = self.settler.owner
        player2 = self.game_logic.players['two']
        self.game_logic.build_city(self.settler, "stub_city")
        city = player1.cities[0]

        self.assertNotIn(city, player2.cities)
        self.game_logic.give_opponents_city(x, y, 'two')
        self.assertNotIn(city, player1.cities)
        self.assertIn(city, player2.cities)
        for t in city.area:
            self.assertEqual(t.owner, player2)

        # fortunately, it was just player one's sneaky trap and he immediately recovered his city

        self.game_logic.give_opponents_city(x, y, 'one')
        for t in city.area:
            self.assertEqual(t.owner, player1)
Esempio n. 8
0
class MyTestCase(unittest.TestCase):
    def setUp(self) -> None:
        self.tiles = []
        # these have to be added in the order of bottom to top, left to right in order for get_tile(x, y) to work
        # so (0, 0), (1, 0), .... (COLUMNS_NUMBER-1, 0), (0, 1), (1, 1), ...
        # this is a linear map with, from left to right, a column of water, plains, hills, plains, mountains
        for y in range(5):
            self.tiles.append(Tile(0, y, 1, 0))
            self.tiles.append(Tile(1, y, 1, 1))
            self.tiles.append(Tile(2, y, 1, 2))
            self.tiles.append(Tile(3, y, 1, 1))
            self.tiles.append(Tile(4, y, 1, 3))
        self.game_logic = GameLogic(self.tiles, 5, 5, players=[("one", CIV_ONE, "gray"), ("two", CIV_TWO, "red")],
                                    my_nick="one")

    def test_only_fight(self):
        # test only method invoking combat between military units
        self.game_logic.add_unit(2, 2, 'one', 'Poor Infantry', 10)
        self.game_logic.add_unit(2, 3, 'two', 'Archers', 2)
        u1 = self.game_logic.get_tile(2, 2).occupant
        u2 = self.game_logic.get_tile(2, 3).occupant
        winner = u1.attack(u2, seed=10)

        self.assertEqual(winner, u1)
        self.assertEqual(winner.health, 130)
        self.assertLessEqual(u2.health, 0)

    def test_move_and_fight(self):
        # test combat resulting from one unit moving to the place occupied by another
        self.setUp()
        self.game_logic.add_unit(2, 2, 'one', 'Poor Infantry', 10)
        self.game_logic.add_unit(2, 3, 'two', 'Archers', 2)
        loser = self.game_logic.players['two']
        u1 = self.game_logic.get_tile(2, 2).occupant
        u2 = self.game_logic.get_tile(2, 3).occupant

        self.assertIn(u2, loser.units)
        self.game_logic.move_unit(u1, 2, 3, 0)
        self.assertNotIn(u2, loser.units)

    def test_slaughter_helpless_settlers(self):
        # test combat against non military units
        self.setUp()
        self.game_logic.add_unit(2, 2, 'one', 'Poor Infantry', 10)
        self.game_logic.add_unit(2, 3, 'two', 'Settler', 1)
        loser = self.game_logic.players['two']

        u1 = self.game_logic.get_tile(2, 2).occupant
        u2 = self.game_logic.get_tile(2, 3).occupant

        self.assertIn(u2, loser.units)
        self.game_logic.move_unit(u1, 2, 3, 0)
        self.assertNotIn(u2, loser.units)
Esempio n. 9
0
class MovementTests(unittest.TestCase):
    def setUp(self) -> None:
        self.tiles = []
        # these have to be added in the order of bottom to top, left to right in order for get_tile(x, y) to work
        # so (0, 0), (1, 0), .... (COLUMNS_NUMBER-1, 0), (0, 1), (1, 1), ...
        # this is a linear map with, from left to right, a column of water, plains, hills, plains, mountains
        for y in range(5):
            self.tiles.append(Tile(0, y, 1, 0))
            self.tiles.append(Tile(1, y, 1, 1))
            self.tiles.append(Tile(2, y, 1, 2))
            self.tiles.append(Tile(3, y, 1, 1))
            self.tiles.append(Tile(4, y, 1, 3))
        self.game_logic = GameLogic(self.tiles,
                                    5,
                                    5,
                                    players=[("one", CIV_ONE, "gray"),
                                             ("two", CIV_TWO, "red")],
                                    my_nick="one")
        self.game_logic.add_unit(2, 2, "one", 'Poor Infantry', 1)
        self.unit = self.game_logic.get_tile(2, 2).occupant
        self.unit.max_movement = 2
        self.unit.reset_movement()

    def test_cost_assignment(self):
        # Tile(x, y, n) should have a cost of n if 0 < n < 3, else inf
        # this means that water and mountains have a cost of inf
        # plains cost 1 and hills cost 2 to move to them
        # let's check if these costs have been correctly assigned
        for y in range(5):
            self.assertEqual(self.game_logic.get_tile(0, y).cost, inf)
            self.assertEqual(self.game_logic.get_tile(1, y).cost, 1)
            self.assertEqual(self.game_logic.get_tile(2, y).cost, 2)
            self.assertEqual(self.game_logic.get_tile(3, y).cost, 1)
            self.assertEqual(self.game_logic.get_tile(4, y).cost, inf)

    def test_unit_placement(self):
        # a unit has been placed on tile (2, 2) in the setup
        # let's check if it is set as the occupant
        tile = self.game_logic.get_tile(2, 2)
        self.assertEqual(tile.occupant, self.unit)
        # and it has the correct tile assigned
        self.assertEqual(self.unit.tile, tile)

    def test_movement_range(self):
        # the unit is standing on the central tile
        # the map, labeling each tile with its individual cost, looks like this
        # inf 1 2 1 inf
        # inf 1 2 1 inf
        # inf 1 * 1 inf
        # inf 1 2 1 inf
        # inf 1 2 1 inf
        # we have 2 movement points to spare
        # the unit should be able to move one tile up and down at cost 2 (hill cost)
        # and one tile left and right at cost 1 (plains cost)
        # and one tile diagonally at cost 2 (1 to move left/right + 1 to move up/down because these are plains too)
        # and nowhere else
        expected_range = {(x, y): 2 for x in range(1, 4) for y in range(1, 4)}
        expected_range[2, 2] = 0  # also it's free to stay where it is
        expected_range[1, 2] = expected_range[3, 2] = 1

        unit_range = self.game_logic.get_unit_range(self.unit)
        self.assertEqual(unit_range, expected_range)

    def test_simple_move(self):
        # we move one up
        unit_range = self.game_logic.get_unit_range(self.unit)
        self.game_logic.move_unit(self.unit, 2, 3, unit_range[2, 3])
        # the unit should have the cost subtracted from its movement points
        self.assertEqual(self.unit.movement,
                         self.unit.max_movement - unit_range[2, 3])
        # the previous tile should be unoccupied
        self.assertFalse(self.game_logic.get_tile(2, 2).occupied())
        # and the occupant of the new tile should be out unit
        # and the unit should have this tile assigned
        new_tile = self.game_logic.get_tile(2, 3)
        self.assertEqual(new_tile.occupant, self.unit)
        self.assertEqual(self.unit.tile, new_tile)

    def test_sequential_move(self):
        # let's assume we move one tile left
        # then we should be able to move again - one tile up or down, and nowhere else
        unit_range = self.game_logic.get_unit_range(self.unit)
        self.game_logic.move_unit(self.unit, 1, 2, unit_range[1, 2])
        new_range = self.game_logic.get_unit_range(self.unit)
        self.assertEqual(len(new_range), 3)  # up, down, or stay in place
        self.assertEqual(self.unit.movement, 1)
        # let's move one down now
        self.assertIn((1, 1), new_range)
        self.assertEqual(new_range[1, 1], 1)
        self.game_logic.move_unit(self.unit, 1, 1, new_range[1, 1])
        self.assertEqual(self.unit.movement, 0)
        new_tile = self.game_logic.get_tile(1, 1)
        self.assertEqual(new_tile.occupant, self.unit)
        self.assertEqual(self.unit.tile, new_tile)
Esempio n. 10
0
class GameView(arcade.View):
    """
    The view of the map of the game.
    """

    def __init__(self, width: int, height: int, tiles: list, client):
        """
        :param width: Max screen width.
        :param height: Max screen height.
        :param tiles: A 2D list of integer values representing tile types.
        :param client: A client object for server communication.
        """
        super().__init__()

        self.client = client
        self.my_turn = False
        self.cur_enemy = ""

        self.SCREEN_WIDTH = width
        self.SCREEN_HEIGHT = height
        self.SCROLL_STEP_X = SCROLL_STEP * width
        self.SCROLL_STEP_Y = SCROLL_STEP * height
        self.TILE_ROWS = len(tiles)
        self.TILE_COLS = len(tiles[0])
        self.zoom = 0

        self.top_bar = TopBar(None, TOP_BAR_SIZE)
        self.unit_popup = UnitPopup(4 * TOP_BAR_SIZE, 4 * TOP_BAR_SIZE)
        self.update_popup = False  # used to only update pop-up once per opponent's move, otherwise game is laggy
        self.update_topbar = False  # used to update top bar if my city has been taken
        self.update_diplo = None  # will be updated to a tuple of (message, sender, is_rejectable)
        self.diplo_answered = threading.Event()
        self.city_popup = CityCreationPopup(4 * TOP_BAR_SIZE, 5 * TOP_BAR_SIZE)
        self.diplo_popup = DiplomaticPopup(9 * TOP_BAR_SIZE, 3 * TOP_BAR_SIZE, self.diplo_answered)
        self.end_popup = EndingPopup(6 * TOP_BAR_SIZE, 6 * TOP_BAR_SIZE)
        self.ranking = None
        self.tiles = tiles

        self.tile_sprites = arcade.SpriteList()
        # needs to be smarter tbh but depends on the size of a real map
        self.tile_size = int((height - self.top_bar.height) / self.TILE_ROWS) - MARGIN
        # in order to center the tiles vertically and horizontally
        self.centering_x = (width - self.TILE_COLS * (self.tile_size + MARGIN)) / 2
        self.centering_y = ((height - self.top_bar.height) - self.TILE_ROWS * (self.tile_size + MARGIN)) / 2

        for row in range(self.TILE_ROWS):
            for col in range(self.TILE_COLS):
                tile = Tile(col, row, self.tile_size, tiles[row][col])
                tile.center_x = col * (self.tile_size + MARGIN) + (self.tile_size / 2) + MARGIN + self.centering_x
                tile.center_y = row * (self.tile_size + MARGIN) + (self.tile_size / 2) + MARGIN + self.centering_y
                self.tile_sprites.append(tile)

        self.game_logic = GameLogic(self.tile_sprites, self.TILE_ROWS, self.TILE_COLS, self.client.players,
                                    self.client.nick)
        self.top_bar.me = self.game_logic.me
        self.city_view = CityView(self.top_bar)
        self.enemy_city_view = EnemyCityView(self.top_bar, self.city_view.app, self.client, self.game_logic.me)
        self.top_bar.update_treasury()

        threading.Thread(target=self.wait_for_my_turn).start()

    def relative_to_absolute(self, x: float, y: float):
        """
        Converts relative coordinates to absolute ones. Coordinates provided by mouse events are relative to the
        current zoom, so for some uses (like determining what tile has been clicked) they need to be scaled and shifted.
        """
        current = arcade.get_viewport()
        real_y = y * (current[3] - current[2]) / self.SCREEN_HEIGHT + current[2] - self.centering_y
        real_x = x * (current[1] - current[0]) / self.SCREEN_WIDTH + current[0] - self.centering_x
        return real_x, real_y

    def absolute_to_tiles(self, x: float, y: float):
        """
        Converts absolute coordinates to map matrix indices of the tile they lay on.
        """
        return map(lambda a: int(a // (self.tile_size + MARGIN)), (x, y))

    def get_tile(self, x, y):
        return self.tile_sprites[y * self.TILE_COLS + x]

    def on_show(self):
        arcade.set_background_color(arcade.color.BLACK)
        self.top_bar.adjust()
        self.unit_popup.adjust()

    def on_update(self, delta_time: float):
        self.game_logic.update()
        if self.update_popup:
            self.unit_popup.update()
            self.update_popup = False
        if self.update_topbar:
            self.top_bar.update_treasury()
            self.update_topbar = False
        if self.update_diplo:
            self.diplo_popup.display(*self.update_diplo)
            self.update_diplo = None
        if self.ranking:
            self.top_bar.game_ended()
            self.end_popup.display(self.ranking)
            self.ranking = None

    def on_draw(self):
        self.top_bar.turn_change(self.cur_enemy)
        arcade.start_render()
        # units, cities, move ranges
        self.game_logic.draw()
        # top bar
        self.top_bar.draw_background()
        # unit popup
        self.unit_popup.draw_background()
        self.city_popup.draw_background()
        self.diplo_popup.draw_background()
        self.end_popup.draw_background()

    def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
        if self.city_popup.visible() or self.diplo_popup.visible() or self.end_popup.visible():
            return
        if 0 <= self.zoom + scroll_y < MAX_ZOOM:
            self.zoom += scroll_y
            current = arcade.get_viewport()

            new_width = (current[1] - current[0]) - 2 * scroll_y * self.SCROLL_STEP_X
            new_height = (current[3] - current[2]) - 2 * scroll_y * self.SCROLL_STEP_Y

            # we need to check if zooming will cross the borders of the map, if so - snap them back
            x, y = self.relative_to_absolute(x, y)
            new_left = x - new_width / 2
            if new_left > 0:
                x_shift = self.SCREEN_WIDTH - (new_left + new_width)
                if x_shift < 0:
                    new_left += x_shift
            else:
                new_left = 0

            new_bottom = y - new_height / 2
            if new_bottom > 0:
                # now, the size of the top bar changes and the zoom has to adjust
                # this means that in no zoom we can move closer to camera_top = screen_height than if we zoom in
                # and that's because the height of the top bar in pixels is the largest when zoomed out
                # actually, in no zoom we can move all the way up there
                # so the max camera_top for when zoomed in n times is:
                # screen_height - (max_top_bar_height - top_bar_height_after_zoom_change)
                y_shift = (self.SCREEN_HEIGHT - (self.zoom * 2 * self.SCROLL_STEP_Y) * self.top_bar.size_y) - (
                        new_bottom + new_height)
                if y_shift < 0:
                    new_bottom += y_shift
            else:
                new_bottom = 0

            arcade.set_viewport(new_left, new_left + new_width, new_bottom, new_bottom + new_height)
            self.top_bar.adjust()
            self.unit_popup.adjust()

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        if self.city_popup.visible() or self.diplo_popup.visible() or self.end_popup.visible():
            return
        if buttons == 4:
            current = arcade.get_viewport()
            # slow the movement down a lil bit
            dx /= 4
            dy /= 4

            if current[0] - dx < 0 or current[1] - dx > self.SCREEN_WIDTH:
                dx = 0
            # max_top = zoomed out map height + current top bar height,
            # cause we want to be able to see the edge of the map
            max_top = self.SCREEN_HEIGHT - self.top_bar.max_height + self.top_bar.height
            if current[2] - dy < 0 or current[3] - dy > max_top:
                dy = 0

            arcade.set_viewport(current[0] - dx, current[1] - dx, current[2] - dy, current[3] - dy)
            self.top_bar.adjust()
            self.unit_popup.adjust()

    def on_mouse_press(self, x, y, button, modifiers):
        if self.city_popup.visible() or self.diplo_popup.visible() or self.end_popup.visible():
            return
        if button == 1:
            # don't let the player click through the unit pop-up or the top bar
            if self.unit_popup.is_hit(x, y) or self.top_bar.is_hit(x, y):
                pass
            else:
                # the x and y arguments are relative to the current zoom, so we need to scale and shift them
                x, y = self.relative_to_absolute(x, y)
                # aaand then turn them into grid coords
                tile_col, tile_row = self.absolute_to_tiles(x, y)

                if tile_col < self.TILE_COLS and tile_row < self.TILE_ROWS:
                    # sprite list is 1d so we need to turn coords into a single index
                    tile = self.tile_sprites[tile_row * self.TILE_COLS + tile_col]

                    if self.unit_popup.visible():
                        unit = self.unit_popup.unit
                        cost = self.game_logic.can_unit_move(unit, tile_col, tile_row)
                        if self.my_turn and cost:
                            # if it's my turn and my unit and i clicked within its range, move it
                            old_tile = unit.tile
                            participants = self.game_logic.move_unit(unit, tile_col, tile_row, cost)
                            if len(participants) > 1:
                                # if there was some combat, update healths
                                for participant, (x, y) in participants:
                                    messages = self.client.update_health(x, y, participant.health)
                                    self.handle_additional_messages(messages)
                            if unit.health > 0:
                                # if my unit survived, move it
                                messages = self.client.move_unit(*old_tile.coords, tile_col, tile_row, cost)
                                self.handle_additional_messages(messages)
                                self.unit_popup.update()
                                city = unit.tile.city
                                if city and city.owner != self.game_logic.me:
                                    opponent = city.owner
                                    # if i moved to a city that's wasn't mine, it's mine now uwu
                                    self.game_logic.give_city(city, self.game_logic.me)
                                    messages = self.client.get_city(city)
                                    self.handle_additional_messages(messages)
                                    self.top_bar.update_treasury()
                                    # if the city was the user's last, they're defeated
                                    if len(opponent.cities) == 0:
                                        messages = self.client.kill(opponent.nick)
                                        self.handle_additional_messages(messages)
                                        # if that was the last opponent, the game is over
                                        if len(self.game_logic.players) - len(
                                                self.game_logic.disconnected_players) == 1:
                                            self.game_logic.hide_unit_range()
                                            self.unit_popup.hide()
                                            messages = self.client.end_game_by_host()
                                            self.handle_additional_messages(messages)
                            else:
                                # i died :(
                                self.game_logic.hide_unit_range()
                                self.unit_popup.hide()
                        else:
                            # otherwise it means that i "unclicked" the popup
                            self.unit_popup.hide()
                            self.game_logic.hide_unit_range()

                    elif tile.occupied():
                        unit = tile.occupant
                        # hide the range cause we might have clicked another unit while it was displayed
                        self.game_logic.hide_unit_range()
                        if self.my_turn and self.game_logic.is_unit_mine(unit):
                            # show me where can i move it
                            self.unit_popup.display(unit, mine=True)
                            self.game_logic.display_unit_range(unit)
                        else:
                            # just show the info
                            self.unit_popup.display(unit, mine=False)

                    elif self.my_turn:
                        if tile.city:
                            print("inside")
                            if tile.city.owner == self.game_logic.me:
                                self.city_view.set_city(tile.city)
                                self.window.show_view(self.city_view)
                            else:
                                self.enemy_city_view.set_city(tile.city)
                                self.window.show_view(self.enemy_city_view)
                        # some cheats, TODO make them activate by typing 'AEZAKMI'
                        else:
                            if modifiers & arcade.key.MOD_SHIFT:
                                # shift + click creates a settler on the tile
                                self.game_logic.add_unit(tile_col, tile_row, self.client.nick, 'Settler', 1)
                                messages = self.client.add_unit(tile_col, tile_row, "Settler", 1)
                            else:
                                # regular click creates some garrison (see GameLogic.add_unit)
                                self.game_logic.add_unit(tile_col, tile_row, self.client.nick, 'Cavalry', 10)
                                messages = self.client.add_unit(tile_col, tile_row, "Cavalry", 10)
                            self.handle_additional_messages(messages)

    def on_key_press(self, symbol, modifiers):
        if self.my_turn:
            if self.city_popup.visible():
                # ACCEPTING OR DECLINING TO BUILD A CITY
                if symbol == arcade.key.ESCAPE:
                    self.city_popup.hide()
                elif symbol == arcade.key.ENTER:
                    city_name = self.city_popup.hide()
                    unit = self.unit_popup.unit
                    messages = self.client.add_city(*unit.tile.coords, city_name)
                    self.handle_additional_messages(messages)
                    self.game_logic.build_city(self.unit_popup.unit, city_name)
                    self.top_bar.update_treasury()
                    self.unit_popup.hide()
                    self.game_logic.hide_unit_range()
            elif self.diplo_popup.visible():
                pass
            else:
                # END TURN
                if symbol == arcade.key.SPACE:
                    self.my_turn = False
                    self.unit_popup.hide()
                    self.game_logic.end_turn()

                    units = self.game_logic.get_deployed_units()
                    for u in units:
                        x, y = u.tile.coords
                        messages = self.client.add_unit(x, y, u.type, u.count)
                        self.handle_additional_messages(messages)

                    self.game_logic.update_building()
                    cities = self.game_logic.get_enhanced_cities()
                    for c in cities:
                        messages = self.client.enhance_city_area(c[0], c[1])
                        self.handle_additional_messages(messages)

                    for city in self.game_logic.me.cities:
                        city.goods = city.calculate_goods()
                    self.top_bar.update_treasury()

                    for player in self.game_logic.players.values():  # forgetting that message was sent
                        player.deputation = None

                    messages = self.client.end_turn()
                    self.handle_additional_messages(messages)
                    threading.Thread(target=self.wait_for_my_turn, daemon=True).start()
                # BUILD A CITY
                elif symbol == arcade.key.N and self.unit_popup.can_build_city():
                    unit = self.unit_popup.unit
                    if self.game_logic.is_unit_mine(unit):
                        area = self.game_logic.get_city_area(unit)
                        stats = City.calculate_goods_no_city(area)
                        self.city_popup.display(unit, stats)
                # TODO DELETE THIS
                # END GAME EXAMPLE
                elif symbol == arcade.key.P:
                    messages = self.client.end_game_by_host()
                    self.handle_additional_messages(messages)
                # DEFEAT PLAYER EXAMPLE
                elif symbol == arcade.key.D and self.unit_popup.visible():
                    player_to_kill = self.unit_popup.unit.owner
                    if player_to_kill != self.game_logic.me:
                        messages = self.client.kill(player_to_kill.nick)
                        self.handle_additional_messages(messages)

    def handle_additional_messages(self, messages):
        """
        A method that allows players to disconnect anytime, sort of a "panic mode". Has to be called everytime a request
        is sent to the server! Reads and handles all messages that the client didn't expect at this particular moment
        (for now just disconnecting). For instance, a client sends a request to add a unit to the map, and they expect
        to receive that same message as confirmation. Before they sent their request, another player has disconnected.
        This method will remove the disconnected player and then allow the game to go back to normal.
        :param messages: a generator of unexpected messages (see Client.unexpected_messages)
        """
        ranking = []
        for mes in messages:
            print(mes)

            if mes[0] == "DISCONNECT" or mes[0] == "DEFEAT":
                nick = mes[1]
                self.game_logic.disconnected_players.append(nick)
                if self.unit_popup.visible() and self.unit_popup.unit.owner.nick == nick:
                    self.unit_popup.hide()

            elif mes[0] == "RANK":
                ranking.append((mes[1], int(mes[2])))

            elif mes[0] == "END_GAME":
                self.my_turn = False
                self.ranking = ranking

    def wait_for_my_turn(self):
        """
        A prototype function for handling server messages about other players' actions. Will probably be renamed and
        maybe split later on.
        """
        ranking = []
        while True:
            self.diplo_answered.clear()
            message = self.client.get_opponents_move()
            print(message)
            if message[0] == "TURN":
                if message[1] == self.client.nick:
                    self.my_turn = True
                    self.cur_enemy = ""
                    return
                else:
                    self.cur_enemy = message[1]
                    self.game_logic.reset_movement(self.cur_enemy)

            elif message[0] == "ADD_UNIT":
                nick = message[1]
                x, y = eval(message[2])
                unit_type = message[3]
                count = int(message[4])
                self.game_logic.add_unit(x, y, nick, unit_type, count)

            elif message[0] == "MOVE_UNIT":
                x0, y0 = eval(message[1])
                x1, y1 = eval(message[2])
                cost = int(message[3])
                self.game_logic.move_opponents_unit(x0, y0, x1, y1, cost)
                self.update_popup = True

            elif message[0] == "HEALTH":
                x, y = eval(message[1])
                health = int(message[2])
                if health <= 0:
                    self.game_logic.kill_opponents_unit(x, y)
                    self.unit_popup.hide_if_on_tile(x, y)
                else:
                    tile = self.game_logic.get_tile(x, y)
                    if tile:
                        tile.occupant.health = health

            elif message[0] == "ADD_CITY":
                nick = message[1]
                x, y = eval(message[2])
                city_name = message[3]
                self.game_logic.build_opponents_city(x, y, city_name)
                self.unit_popup.hide_if_on_tile(x, y)

            elif message[0] == "GIVE_CITY":
                x, y = eval(message[1])
                recipient = message[2]
                self.game_logic.give_opponents_city(x, y, recipient)
                """ "Niech tak będzie" ~ PM """
                self.update_topbar = True

            elif message[0] == "MORE_AREA":
                x, y = eval(message[1])
                self.game_logic.increase_area(x, y)

            elif message[0] == "DIPLOMACY_ANSWER":
                involved = False
                sender_is_me = False
                other = None
                new_message = message.copy()
                if message[2] == self.client.nick:
                    involved = sender_is_me = True
                    other = message[3]
                    new_message[2] = "You"
                elif message[3] == self.client.nick:
                    involved = True
                    other = message[2]
                    new_message[3] = "you"

                if involved:
                    if message[1] == "DECLARE_WAR":
                        self.game_logic.me.enemies.append(self.game_logic.players[other])
                    elif message[1] == "ALLIANCE" and eval(message[-1]):
                        self.game_logic.me.allies.append(self.game_logic.players[other])
                    elif message[1] == "END_ALLIANCE":
                        self.game_logic.me.allies.remove(self.game_logic.players[other])
                    elif message[1] == "TRUCE" and eval(message[-1]):
                        self.game_logic.me.enemies.remove(self.game_logic.players[other])
                    elif message[1] == "BUY_CITY":
                        x, y = eval(message[4])
                        new_message[4] = self.game_logic.get_tile(x, y).city.name
                        if eval(message[-1]):
                            self.game_logic.give_opponents_city(x, y, message[3])
                        elif message[3] == self.client.nick:
                            self.game_logic.me.granary.change_resource('gold', int(message[5]))
                        self.update_topbar = True
                    elif message[1] == "BUY_RESOURCE":
                        if message[3] == self.client.nick:
                            self.game_logic.me.granary.change_resource('gold', int(message[5]))
                            self.game_logic.me.granary.change_resource(message[4], int(message[6]))
                            self.update_topbar = True
                    self.update_diplo = new_message, sender_is_me
                    self.diplo_answered.wait()

                else:
                    if message[1] == "BUY_CITY" and eval(message[-1]):
                        x, y = eval(message[4])
                        new_message[4] = self.game_logic.get_tile(x, y).city.name
                        self.game_logic.give_opponents_city(*eval(message[4]), message[3])
                    if message[1] in ["DECLARE_WAR", "END_ALLIANCE"] or message[1] != "BUY_RESOURCE" and eval(message[-1]):
                        self.update_diplo = new_message, sender_is_me
                        self.diplo_answered.wait()

            elif message[0] == "DIPLOMACY":
                if message[3] == self.client.nick:
                    if message[1] == "BUY_RESOURCE":
                        resource = message[4]
                        price = float(message[5])
                        quantity = int(message[6])
                        max_price = ceil(price * quantity)
                        in_storage = self.game_logic.me.granary.get_resource(resource)
                        actual_quantity = min(quantity, in_storage)
                        actual_price = ceil(price * actual_quantity)
                        change = max_price - actual_price

                        new_message = message.copy()
                        new_message[5] = actual_price
                        new_message[6] = actual_quantity
                        self.update_diplo = new_message, False
                        self.diplo_answered.wait()
                        response = self.diplo_popup.accepted
                        if response:
                            self.game_logic.me.granary.change_resource('gold', actual_price)
                            self.game_logic.me.granary.change_resource(resource, -actual_quantity)
                            self.update_topbar = True

                        self.client.only_send(
                            f"DIPLOMACY_ANSWER:{message[1]}:{message[3]}:{message[2]}:{message[4]}:{change if response else max_price}:{actual_quantity if response else 0}:{response}")

                    elif message[1] == "BUY_CITY":
                        x, y = eval(message[4])
                        city_name = self.game_logic.get_tile(x, y).city.name
                        price = int(message[5])
                        new_message = message.copy()
                        new_message[4] = city_name
                        self.update_diplo = new_message, False
                        self.diplo_answered.wait()
                        response = self.diplo_popup.accepted
                        if response:
                            self.game_logic.me.granary.change_resource('gold', price)
                            self.update_topbar = True
                        self.client.only_send(
                            f"DIPLOMACY_ANSWER:{message[1]}:{message[3]}:{message[2]}:" + ":".join(
                                message[4:]) + f":{response}")
                    else:
                        self.update_diplo = message, False
                        self.diplo_answered.wait()
                        response = self.diplo_popup.accepted
                        self.client.only_send(f"DIPLOMACY_ANSWER:{message[1]}:{message[3]}:{message[2]}:{response}")

            elif message[0] == "DISCONNECT" or message[0] == "DEFEAT":
                nick = message[1]
                self.game_logic.disconnected_players.append(nick)
                if self.unit_popup.visible() and self.unit_popup.unit.owner.nick == nick:
                    self.unit_popup.hide()

            elif message[0] == "RANK":
                ranking.append((message[1], int(message[2])))

            elif message[0] == "END_GAME":
                self.ranking = ranking
                return

            elif message[0] == "DISCONNECTED":
                return