Compare commits

..

2 Commits

Author SHA1 Message Date
ydy0615 99ab005021 feat(ctf): increase map update frequency and enhance path planning
- Reduce `show_gap_in_msec` from 10 ms to 1 ms and `update_threshold` from 10 to 1 for faster game state updates.
- Use `global my_side_is_left` in `Map.update` and adjust safe‑zone calculation with a one‑cell offset, fixing zone detection.
- Correct edge generation logic to properly handle safe and unsafe zones.
- Add tracking of enemy players without flags (`pretend_list`) and own flags (`protect_list`) to broaden target options.
- Implement fallback destination selection: prioritize enemy flags, then enemy players, then own flags.
- Clean up stray debug prints and minor formatting issues.
2025-12-27 23:30:22 +08:00
ydy0615 b038e5ac29 refactor(game): add Map.closest and improve flag assignment logic
- Introduced `Map.closest` to encapsulate nearest‑position search and replaced duplicated distance loops in flag targeting.
- Updated flag assignment to consider enemy players carrying flags and to use the new helper for both ally and enemy flag selection.
- Cleaned up unused imports and renamed `mypolicy` to `mypolicy.py` for clarity.
- Adjusted frontend `game_config.json` to use new team identifiers, increase players to 3, raise flag count to 9, and enable random flag placement.
2025-12-27 22:10:46 +08:00
38 changed files with 55 additions and 255584 deletions
-95
View File
@@ -1,95 +0,0 @@
# Capture the Flag
Your job is to implement your own algorithm (see `backend/server.cpp`) to control
a team to compete in "Capture the Flag" Game.
## Game Rules
Capture the Flag is a popular outdoor game where two teams compete in an open field.
Each team has a territory and a set of flags located within the territory. Each team's
goal is to collect flags of the opponent team and bring them back to the target area.
A player can tag the opponent team's player within his territory.
When tagged, the opponent team's player will be put into the prison.
The player will stay in the prison for a period of time, unless he is saved by
his teammate earlier.
The initial positions of flags, the target area and the prison are within the team's
territory.
A player can only pick the flags of the opponent team. That is, he cannot pick up
his team's flag and put it in a different place.
Our game has two teams: "L" and "R" team. The field is a rectangle area,
where the left half is "L" team's territory and the other is "R" team's.
There are obstacles and walls within the map.
![Capture The Flag Map](./map_example.png)
## Play
The game consists of 2 parts:
- __frontend/__: starts the game web server, written in Javascript. It optionally connects to 2 backend servers to move the players. You should NOT change the code, but you may read the code to understand how it generates the map and communicates with the backend.
- __backend/__: is the backend server which sends back instructions to frontend to move players. This is where you implement your algorithms. Note that in real competition, your server controls one team and the other is your opponent team's implementation.
To play it manually, you can use `w a s d` keys to control L team and `↑ ← ↓ →` keys to control R team. The keys override backend server's decisions. Note that the pressed keys move all players in one direction while your code can move each player independently.
Press SPACE KEY to start, pause or continue the game.
1. Install dependency
```
brew update;
brew install boost nlohmann-json
```
2. Compile server.cpp
```
cd backend/;
g++ -std=c++17 server.cpp -I/opt/homebrew/include -L/opt/homebrew/lib -lpthread -o server
```
3. Run server on port 8081 (can run on other ports)
```
./server 8081
```
4. Update `assets/remote_config.json` to the local port. Update `ws_url` with your port.
```
{
"teams": [
{ "name": "L", "ws_url": "ws://localhost:8080" },
{ "name": "R", "ws_url": "ws://localhost:8081" }
]
}
```
5. Start frontend website
```
cd frontend/;
python3 -m http.server 8000
```
6. In your browser, open "http://localhost:8000/index.html" to play.
- Press (Cmd + Option + I on macOS) to open DevTools
- Go to the Network tab
- Check ✅ “Disable cache” (upper-left toolbar) to ensure all your updated remote_config.json is loaded properly.
## Your Job
In `backend/server.cpp`, your need to implement `startGame(req)` and `planNextActions(req, ws)` functions.
- `startGame(req)` is called once when the game starts. `req` contains the game information, such as map (e.g., height, width, obstacle positions) and team (e.g., name, number of players and number of flags).
- `planNextActions(req, ws)` is called periodically to update you all the player and flags' information. You should use `ws` to send back the actions taken for your team player. The current implementation returns random actions for every team player.
- `gameOver(req)` is called once the game finishes and a winner is determined. You may clean up the state for the next `startGame`.
## Write up (Important!)
You must submit a markdown writeup consisting of the following:
1. The top 3-5 "strategic" decisions to compete against the opponents? Explain the intuition,
the core idea and the technical details (such as the data structure & algorithms).
2. Some interesting and funny moments when you are testing your implementations, or competing
with your friends. What changes did you make after the test?
## Sample Team (in Python)
We provide a "not-so-dummy" python backend as a competitive opponent for you to develop your own algorithm.
DO NOT translate the python algorithm into C++ as your own implementation.
To run the python example,
```
python3 pick_closest_flag.py 8081
```
You'll need to `pip3 install` dependency, such as `asyncio`.
-534
View File
@@ -1,534 +0,0 @@
{
"action": "init",
"map": {
"width": 20,
"height": 20,
"walls": [
{
"x": 0,
"y": 0
},
{
"x": 19,
"y": 0
},
{
"x": 0,
"y": 19
},
{
"x": 19,
"y": 19
},
{
"x": 1,
"y": 0
},
{
"x": 2,
"y": 0
},
{
"x": 3,
"y": 0
},
{
"x": 4,
"y": 0
},
{
"x": 5,
"y": 0
},
{
"x": 6,
"y": 0
},
{
"x": 7,
"y": 0
},
{
"x": 8,
"y": 0
},
{
"x": 9,
"y": 0
},
{
"x": 10,
"y": 0
},
{
"x": 11,
"y": 0
},
{
"x": 12,
"y": 0
},
{
"x": 13,
"y": 0
},
{
"x": 14,
"y": 0
},
{
"x": 15,
"y": 0
},
{
"x": 16,
"y": 0
},
{
"x": 17,
"y": 0
},
{
"x": 18,
"y": 0
},
{
"x": 1,
"y": 19
},
{
"x": 2,
"y": 19
},
{
"x": 3,
"y": 19
},
{
"x": 4,
"y": 19
},
{
"x": 5,
"y": 19
},
{
"x": 6,
"y": 19
},
{
"x": 7,
"y": 19
},
{
"x": 8,
"y": 19
},
{
"x": 9,
"y": 19
},
{
"x": 10,
"y": 19
},
{
"x": 11,
"y": 19
},
{
"x": 12,
"y": 19
},
{
"x": 13,
"y": 19
},
{
"x": 14,
"y": 19
},
{
"x": 15,
"y": 19
},
{
"x": 16,
"y": 19
},
{
"x": 17,
"y": 19
},
{
"x": 18,
"y": 19
},
{
"x": 0,
"y": 1
},
{
"x": 0,
"y": 2
},
{
"x": 0,
"y": 3
},
{
"x": 0,
"y": 4
},
{
"x": 0,
"y": 5
},
{
"x": 0,
"y": 6
},
{
"x": 0,
"y": 7
},
{
"x": 0,
"y": 8
},
{
"x": 0,
"y": 9
},
{
"x": 0,
"y": 10
},
{
"x": 0,
"y": 11
},
{
"x": 0,
"y": 12
},
{
"x": 0,
"y": 13
},
{
"x": 0,
"y": 14
},
{
"x": 0,
"y": 15
},
{
"x": 0,
"y": 16
},
{
"x": 0,
"y": 17
},
{
"x": 0,
"y": 18
},
{
"x": 19,
"y": 1
},
{
"x": 19,
"y": 2
},
{
"x": 19,
"y": 3
},
{
"x": 19,
"y": 4
},
{
"x": 19,
"y": 5
},
{
"x": 19,
"y": 6
},
{
"x": 19,
"y": 7
},
{
"x": 19,
"y": 8
},
{
"x": 19,
"y": 9
},
{
"x": 19,
"y": 10
},
{
"x": 19,
"y": 11
},
{
"x": 19,
"y": 12
},
{
"x": 19,
"y": 13
},
{
"x": 19,
"y": 14
},
{
"x": 19,
"y": 15
},
{
"x": 19,
"y": 16
},
{
"x": 19,
"y": 17
},
{
"x": 19,
"y": 18
}
],
"obstacles": [
{
"x": 10,
"y": 6
},
{
"x": 15,
"y": 6
},
{
"x": 9,
"y": 5
},
{
"x": 13,
"y": 12
},
{
"x": 15,
"y": 9
},
{
"x": 10,
"y": 1
},
{
"x": 14,
"y": 11
},
{
"x": 10,
"y": 18
},
{
"x": 14,
"y": 13
},
{
"x": 9,
"y": 11
},
{
"x": 11,
"y": 4
},
{
"x": 11,
"y": 14
},
{
"x": 14,
"y": 14
},
{
"x": 9,
"y": 12
},
{
"x": 11,
"y": 5
},
{
"x": 11,
"y": 15
}
]
},
"numPlayers": 3,
"numFlags": 6,
"myteamName": "L",
"myteamPrison": [
{
"x": 1,
"y": 16
},
{
"x": 2,
"y": 16
},
{
"x": 3,
"y": 16
},
{
"x": 1,
"y": 17
},
{
"x": 2,
"y": 17
},
{
"x": 3,
"y": 17
},
{
"x": 1,
"y": 18
},
{
"x": 2,
"y": 18
},
{
"x": 3,
"y": 18
}
],
"myteamTarget": [
{
"x": 1,
"y": 9
},
{
"x": 2,
"y": 9
},
{
"x": 3,
"y": 9
},
{
"x": 1,
"y": 10
},
{
"x": 2,
"y": 10
},
{
"x": 3,
"y": 10
},
{
"x": 1,
"y": 11
},
{
"x": 2,
"y": 11
},
{
"x": 3,
"y": 11
}
],
"opponentPrison": [
{
"x": 16,
"y": 16
},
{
"x": 17,
"y": 16
},
{
"x": 18,
"y": 16
},
{
"x": 16,
"y": 17
},
{
"x": 17,
"y": 17
},
{
"x": 18,
"y": 17
},
{
"x": 16,
"y": 18
},
{
"x": 17,
"y": 18
},
{
"x": 18,
"y": 18
}
],
"opponentTarget": [
{
"x": 16,
"y": 9
},
{
"x": 17,
"y": 9
},
{
"x": 18,
"y": 9
},
{
"x": 16,
"y": 10
},
{
"x": 17,
"y": 10
},
{
"x": 18,
"y": 10
},
{
"x": 16,
"y": 11
},
{
"x": 17,
"y": 11
},
{
"x": 18,
"y": 11
}
]
}
@@ -1,134 +0,0 @@
{
"action": "status",
"time": 11067.9,
"myteamPlayer": [
{
"name": "L0",
"team": "L",
"hasFlag": false,
"posX": 2,
"posY": 2,
"inPrison": false,
"inPrisonTimeLeft": 0,
"inPrisonDuration": 20000
},
{
"name": "L1",
"team": "L",
"hasFlag": false,
"posX": 2,
"posY": 3,
"inPrison": false,
"inPrisonTimeLeft": 0,
"inPrisonDuration": 20000
},
{
"name": "L2",
"team": "L",
"hasFlag": false,
"posX": 2,
"posY": 4,
"inPrison": false,
"inPrisonTimeLeft": 0,
"inPrisonDuration": 20000
}
],
"myteamFlag": [
{
"canPickup": true,
"posX": 1,
"posY": 1
},
{
"canPickup": true,
"posX": 1,
"posY": 2
},
{
"canPickup": true,
"posX": 1,
"posY": 3
},
{
"canPickup": true,
"posX": 1,
"posY": 4
},
{
"canPickup": true,
"posX": 1,
"posY": 5
},
{
"canPickup": true,
"posX": 1,
"posY": 6
}
],
"myteamScore": 0,
"opponentPlayer": [
{
"name": "R0",
"team": "R",
"hasFlag": false,
"posX": 16,
"posY": 1,
"inPrison": false,
"inPrisonTimeLeft": 0,
"inPrisonDuration": 20000
},
{
"name": "R1",
"team": "R",
"hasFlag": false,
"posX": 16,
"posY": 2,
"inPrison": false,
"inPrisonTimeLeft": 0,
"inPrisonDuration": 20000
},
{
"name": "R2",
"team": "R",
"hasFlag": false,
"posX": 16,
"posY": 3,
"inPrison": false,
"inPrisonTimeLeft": 0,
"inPrisonDuration": 20000
}
],
"opponentFlag": [
{
"canPickup": true,
"posX": 18,
"posY": 1
},
{
"canPickup": true,
"posX": 18,
"posY": 2
},
{
"canPickup": true,
"posX": 18,
"posY": 3
},
{
"canPickup": true,
"posX": 18,
"posY": 4
},
{
"canPickup": true,
"posX": 18,
"posY": 5
},
{
"canPickup": true,
"posX": 18,
"posY": 6
}
],
"opponentScore": 0
}
-202
View File
@@ -1,202 +0,0 @@
import asyncio
from abc import ABC
import collections
import json
import threading
import random
import websockets
from IPython.display import clear_output
class GameMap(ABC):
def __init__(self, show_gap_in_msec = 1000.0):
"""
@show_gap: how many milliseconds when show can be invoked
"""
self.width = 0
self.height = 0
self.middle_line = self.width / 2
self.walls = set()
self.players = []
self.flags = []
self.current_time = 0.0
self.next_show_time = -1.0
self.my_team_name = ""
self.show_gap_in_msec = show_gap_in_msec
def init(self, req):
map_data = req["map"]
self.current_time = 0.0
self.next_show_time = -1.0
self.width = map_data["width"]
self.height = map_data["height"]
self.middle_line = self.width / 2
self.my_team_name = req.get("myteamName", "")
self.walls = {(w["x"], w["y"]) for w in (map_data.get("walls", []) + map_data.get("obstacles", []))}
self.my_team_prison = {(w["x"], w["y"]) for w in (req.get("myteamPrison", []))}
self.opponent_team_prison = {(w["x"], w["y"]) for w in (req.get("opponentPrison", []))}
self.my_team_target = {(w["x"], w["y"]) for w in (req.get("myteamTarget", []))}
self.opponent_team_target = {(w["x"], w["y"]) for w in (req.get("opponentTarget", []))}
def update(self, req):
if req["time"] < self.current_time:
return False
self.current_time = req["time"]
self.players = []
# Combine and tag players
for p in req.get("myteamPlayer", []):
p['mine'] = True
self.players.append(p)
for p in req.get("opponentPlayer", []):
p['mine'] = False
self.players.append(p)
self.flags = []
for f in req.get("myteamFlag", []):
f['mine'] = True
self.flags.append(f)
for f in req.get("opponentFlag", []):
f['mine'] = False
self.flags.append(f)
return True
def list_players(self, mine, inPrison, hasFlag):
"""
mine: True or False. If True, return players on my side, otherwise return opponent players;
inPrison: True or False or None. If True, return players that are in prison; if false, return players that can move around freely; if none, return all of them.
hasFlag: True or False or None. If True, return players that have flags; if false, return players that do not have flags; if none, return all of them.
"""
return [p for p in self.players if p['mine'] == mine and (inPrison == None or p["inPrison"] == inPrison) and (hasFlag == None or p["hasFlag"] == hasFlag)]
def list_flags(self, mine, canPickup):
"""
mine: True or False. If True, return flags on my side (i.e., flags I should protect), otherwise return opponent's flags (i.e., flags I should pick up);
canPickup: True or False or None. If True, return flags that can be picked up; if false, return flags that are already placed in my camp; if none, return all of them.
"""
return [f for f in self.flags if f['mine'] == mine and (canPickup == None or f["canPickup"] == canPickup)]
def list_targets(self, mine):
if mine:
return self.my_team_target
else:
return self.opponent_team_target
def list_prisons(self, mine):
if mine:
return self.my_team_prison
else:
return self.opponent_team_prison
def get_object_at_XY(self, x, y, flag_over_target=False, player_over_prison=False):
"""
flags could overlap with targets, and players could overlap with prisons.
These are controlled by @flag_over_target and @player_over_prison.
"""
if (x, y) in self.walls: return "██ "
if not player_over_prison:
if (x, y) in self.my_team_prison: return "PP "
if (x, y) in self.opponent_team_prison: return "PP "
if not flag_over_target:
if (x, y) in self.my_team_target: return "TT "
if (x, y) in self.opponent_team_target: return "TT "
# Check Players
for p in self.players:
if p["posX"] == x and p["posY"] == y:
return p["name"] + " "
# Check Flags
for f in self.flags:
if f["posX"] == x and f["posY"] == y:
if f['mine']:
team = self.my_team_name
else:
team = "R" if self.my_team_name == "L" else "L"
return f"{team}F "
if player_over_prison:
if (x, y) in self.my_team_prison: return "PP "
if (x, y) in self.opponent_team_prison: return "PP "
if flag_over_target:
if (x, y) in self.my_team_target: return "TT "
if (x, y) in self.opponent_team_target: return "TT "
return " . "
def show(self, force=False, do_not_clear=False, flag_over_target=False, player_over_prison=False):
"""
Prints the grid with L1, R2, LF, RF labels.
flags could overlap with targets, and players could overlap with prisons.
These are controlled by @flag_over_target and @player_over_prison.
"""
if self.current_time < self.next_show_time and (not force):
return
if not do_not_clear:
clear_output()
header = " " + " ".join([f"{x:2}" for x in range(self.width)])
print(header)
for y in range(self.height):
row = f"{y:2} "
for x in range(self.width):
row += self.get_object_at_XY(x, y, flag_over_target, player_over_prison)
print(row)
self.next_show_time = self.current_time + self.show_gap_in_msec
def route_to(self, srcXY, dstXY, extra_obstacles=None):
extras = set(extra_obstacles) if extra_obstacles else set()
queue = collections.deque([[srcXY]])
seen = {srcXY}
while queue:
path = queue.popleft()
curr = path[-1]
if curr == dstXY:
return path
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: # Up, Down, Left, Right
nxt = (curr[0] + dx, curr[1] + dy)
if (0 <= nxt[0] < self.width and 0 <= nxt[1] < self.height and
nxt not in self.walls and nxt not in extras and nxt not in seen):
queue.append(path + [nxt])
seen.add(nxt)
return []
def is_on_left(self, srcXY):
return srcXY[0] < self.middle_line
@staticmethod
def get_direction(currentXY, nextXY):
"""Helper to convert two points into a direction string."""
dx = nextXY[0] - currentXY[0]
dy = nextXY[1] - currentXY[1]
if dx == 1: return "right"
if dx == -1: return "left"
if dy == 1: return "down"
if dy == -1: return "up"
return ""
async def run_game_server(port, start_fn, plan_fn, end_fn):
lock = threading.Lock()
async def handler(websocket):
print(f"Connected on port {port}")
async for msg in websocket:
req = json.loads(msg)
action = req.get("action")
if action == "init":
with lock:
start_fn(req)
elif action == "status":
with lock:
moves = plan_fn(req)
await websocket.send(json.dumps({"players": moves}))
elif action == "finished":
with lock:
end_fn(req)
print(f"Starting server on port {port}...")
async with websockets.serve(handler, "0.0.0.0", port):
await asyncio.Future()
-282
View File
@@ -1,282 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "21c8be3f-c57b-439c-8ca8-5991dd0da465",
"metadata": {},
"source": [
"# 夺旗赛 Capture The Flag"
]
},
{
"cell_type": "markdown",
"id": "0dfeabca-44d9-4906-abc6-cba1e4fc8a35",
"metadata": {},
"source": [
"## 初始化 (使用上排目录栏的的 ▶️ 运行,▪️停止, ⟳ 重启)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "098c5853-135d-4eb8-ab51-bbf660fc09e9",
"metadata": {},
"outputs": [],
"source": [
"import importlib\n",
"import lib.game_engine\n",
"\n",
"# Force the reload manually\n",
"importlib.reload(lib.game_engine)\n",
"\n",
"# Re-import the specific classes/functions\n",
"from lib.game_engine import GameMap, run_game_server\n",
"\n",
"# Now initialize your objects\n",
"world = GameMap()"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "e9437ef2-777a-4694-b1b2-428e654be4ab",
"metadata": {},
"outputs": [],
"source": [
"from IPython.display import clear_output\n",
"import os\n",
"import json\n",
"import random"
]
},
{
"cell_type": "markdown",
"id": "a1d9d9f6-3644-41bf-a60d-0148a74140c4",
"metadata": {},
"source": [
"## 以下是你要编写的代码。 \n",
"- start_game:初始化游戏。\n",
"- game_over:游戏结束。\n",
"- plan_next_actions:每一时刻,告诉你目前小人们的位置。\n",
" \n",
"每次代码更新,一定要使用上排目录栏的的 ▶️ 运行"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "021ea24a-9a8f-4a4d-a086-f3b80b882c95",
"metadata": {},
"outputs": [],
"source": [
"## 这是你要编写的策略\n",
"def start_game(req):\n",
" \"\"\"Called when the game begins.\"\"\"\n",
" world.init(req)\n",
" print(f\"Map initialized: {world.width}x{world.height}\")\n",
"\n",
"def game_over(req):\n",
" \"\"\"Called when the game ends.\"\"\"\n",
" print(\"Game Over!\")"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "22e04cb6-87cb-486d-a580-a1ae5fbe3a98",
"metadata": {},
"outputs": [],
"source": [
"## 这是你要编写的策略。以下always_move_right和walk_to_first_flag_and_return是两个例子\n",
"def plan_next_actions(req):\n",
" \"\"\"\n",
" Called every tick. \n",
" Return a dictionary: {\"playerName\": \"direction\"}\n",
" direction is \"up\", \"down\", \"right\", \"left, \"\" . \"\" means the player should stand still.\n",
" \"\"\"\n",
" world.update(req) \n",
" world.show() \n",
" actions = dict()\n",
" # TODO:请在这里写下你的代码来控制小人\n",
" \n",
" return actions\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "a8561aec-4246-40dc-b833-1d2b8991edd3",
"metadata": {},
"outputs": [],
"source": [
"## 这是一个超简单策略:即把所有的小人都向右移动\n",
"## Python小测试 - 将以下程序改成:如果是L team,就往右走;如果是R team,就往左走。\n",
"def always_move_right(req):\n",
" \"\"\"\n",
" Called every tick. \n",
" Return a dictionary: {\"playerName\": \"direction\"}\n",
" direction is \"up\", \"down\", \"right\", \"left, \"\" . \"\" means the player should stand still.\n",
" \"\"\"\n",
" if not world.update(req):\n",
" return\n",
"\n",
" world.show() \n",
"\n",
" # List all players that can move freely (set `hasFlag=True`)\n",
" my_players = world.list_players(mine=True, inPrison=False, hasFlag=None)\n",
"\n",
" actions = dict()\n",
" for p in my_players:\n",
" actions[p[\"name\"]] = \"right\"\n",
"\n",
" return actions"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "e4b51cda-514b-4264-b8af-53acc4a76120",
"metadata": {},
"outputs": [],
"source": [
"## 这是一个超简单策略:所有小人都向着第一个小旗子走\n",
"## Python小测试 - 将以下程序改成:如果在对方的领地里,将对方的player位置设为extra_obstacles\n",
"\n",
"def walk_to_first_flag_and_return(req):\n",
" \"\"\"\n",
" Called every tick. \n",
" Return a dictionary: {\"playerName\": \"direction\"}\n",
" direction is \"up\", \"down\", \"right\", \"left, \"\" . \"\" means the player should stand still.\n",
" \"\"\"\n",
" if not world.update(req):\n",
" return\n",
"\n",
" # world.show() always show targets and prisons, regardless whether flags and players are not there or not\n",
" world.show(flag_over_target=True, player_over_prison=True) \n",
"\n",
" # List all players that can move freely (set `hasFlag=True`)\n",
" my_players_go = world.list_players(mine=True, inPrison=False, hasFlag=False)\n",
" my_players_return = world.list_players(mine=True, inPrison=False, hasFlag=True)\n",
" # List a\n",
" opponents = world.list_players(mine=False, inPrison=False, hasFlag=None)\n",
" enemy_flags = world.list_flags(mine=False, canPickup=None)\n",
" \n",
" actions = {}\n",
" \n",
" # Everyone wo/ a flag rushes the first flag\n",
" if enemy_flags:\n",
" target_flag = enemy_flags[0]\n",
" dest = (target_flag[\"posX\"], target_flag[\"posY\"])\n",
" for p in my_players_go:\n",
" start = (p[\"posX\"], p[\"posY\"])\n",
" path = world.route_to(start, dest, extra_obstacles=[])\n",
" \n",
" if len(path) > 1:\n",
" # Convert the next coordinate in path to a direction string\n",
" next_step = path[1]\n",
" actions[p[\"name\"]] = GameMap.get_direction(start, next_step)\n",
" # Everyone w/ a flag returns the target zone\n",
" target_zone = list(world.list_targets(mine=True))[0]\n",
" for p in my_players_return:\n",
" start = (p[\"posX\"], p[\"posY\"])\n",
" path = world.route_to(start, target_zone, extra_obstacles=[])\n",
" if len(path) > 1:\n",
" # Convert the next coordinate in path to a direction string\n",
" next_step = path[1]\n",
" actions[p[\"name\"]] = GameMap.get_direction(start, next_step)\n",
" return actions"
]
},
{
"cell_type": "markdown",
"id": "322772ab-a0b8-4da3-906f-212926ad24fb",
"metadata": {},
"source": [
"## 运行以下cell启动你的server (使用上排目录栏的的 ▶️ 运行,使用▪️停止)\n",
"[*] 表示cell正在运行中"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e0898ab6-bd6e-4bf1-9a29-d802933dd91a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19\n",
" 0 ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ \n",
" 1 ██ . . . . . . . ██ . . . . . . ██ . . RF ██ \n",
" 2 ██ . . . ██ . . . ██ . . . . . . . . . RF ██ \n",
" 3 ██ LF . . . . . . ██ . . . . . . . . . RF ██ \n",
" 4 ██ LF . . . . . . . . . . . . . . . . RF ██ \n",
" 5 ██ LF . . . . . . . . . . . . . . . . . ██ \n",
" 6 ██ LF . . . . . . . . . ██ . . . . . . . ██ \n",
" 7 ██ . . . . . . . . . . . . . . ██ . . . ██ \n",
" 8 ██ . . . . . . . . . . . . . . . . . . ██ \n",
" 9 ██ RF RF TT ██ . . . . . . . . . . . TT TT TT ██ \n",
"10 ██ TT L1 TT ██ . . . . . R0 . . . . . TT TT TT ██ \n",
"11 ██ TT TT TT . . . . . . . . . . . . TT TT TT ██ \n",
"12 ██ . . . . . . . . . . . . . . . . . . ██ \n",
"13 ██ . . . . . . . . . ██ . . . . . . . . ██ \n",
"14 ██ . . . . . . ██ . . ██ . . . . . . . . ██ \n",
"15 ██ . . . . . . . . . . . . . . . . . . ██ \n",
"16 ██ L0 PP PP . . . . . ██ . . . . ██ . R2 PP PP ██ \n",
"17 ██ PP PP PP . . . ██ . ██ . . . . . . PP PP PP ██ \n",
"18 ██ PP PP PP . . . . . . . . . . . . PP PP PP ██ \n",
"19 ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ \n"
]
}
],
"source": [
"# Change the port to match your game settings\n",
"import os\n",
"\n",
"PORT_ID = 2 # 或者 2\n",
"PORT = \"CTF_PORT_BACKEND\" + str(PORT_ID)\n",
"\n",
"try:\n",
" PORT_VALUE = os.environ[PORT]\n",
" print(f\"PORT: {PORT_VALUE}\")\n",
"except KeyError:\n",
" print(\"Error: PORT environment variable not set.\")\n",
"try:\n",
" # 将`plan_fn=`改成你的plan_next_actions,如always_move_right, walk_to_first_flag_and_return 等等\n",
" await run_game_server(PORT_VALUE, start_fn=start_game, plan_fn=walk_to_first_flag_and_return, end_fn=game_over)\n",
"except Exception as e:\n",
" print(f\"Server stopped: {e}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b869dde3-cf94-4d25-8fe8-25096bab1f37",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
-249
View File
@@ -1,249 +0,0 @@
import asyncio
import json
import random
import traceback
import websockets
ACTIONS = ["up", "down", "left", "right", ""]
class GameMap:
EMPTY = 0
OBSTACLE = -1
FLAG = 2
ACTIONS = ["up", "down", "left", "right", ""]
ACTIONS_IN_MOVE = [(0, -1), (0, 1), (-1, 0), (1, 0)]
def __init__(self, map_json):
self.w = map_json["width"]
self.h = map_json["height"]
self.grids = [[GameMap.EMPTY for _ in range(0, self.h)] for _ in range(0, self.w)]
self.obstacles = map_json["walls"] + map_json["obstacles"]
for o in self.obstacles:
self.grids[o["x"]][o["y"]] = GameMap.OBSTACLE
def show_map(self):
for y in range(0, self.h):
for x in range(0, self.w):
print(self.grids[x][y], end=" ")
print("")
def find_closest_goal_from_pos(self, pos_x, pos_y, goals, blockers):
"""
Find the closest goal(@goal_x, @goal_y) in @goals from (@pos_x, @pos_y).
Return the moving direction for (@pos_x, @pos_y).
If none is reachable, return "".
"""
goals_pos = {(g["x"], g["y"]) for g in goals}
blocker_pos = set()
for (bx, by) in blockers:
for (dx, dy) in GameMap.ACTIONS_IN_MOVE:
x = bx + dx
y = by + dy
if (x, y) not in goals_pos:
blocker_pos.add((bx, by))
# visited != -1 means the grid is reached from (pos_x, pos_y)
# it stores the previous grid's direction to reach the current grid
visited = [[-1 for _ in range(0, self.h)] for _ in range(0, self.w)]
bfs = [(pos_x, pos_y)]
st = 0
goal_x = -1
goal_y = -1
while st < len(bfs):
for d, (dx, dy) in enumerate(GameMap.ACTIONS_IN_MOVE):
x = bfs[st][0] + dx
y = bfs[st][1] + dy
if (self.grids[x][y] != GameMap.OBSTACLE and
((x, y) not in blocker_pos) and
visited[x][y] < 0):
visited[x][y] = d
bfs.append((x, y))
if (x, y) in goals_pos:
goal_x = x
goal_y = y
break
st = st + 1
# we need to find the very first direction taken by (pos_x, pos_y) to reach (goal_x, goal_y)
if goal_x < 0 or goal_y < 0:
return GameMap.ACTIONS[-1]
cur_x = goal_x
cur_y = goal_y
first_direction = -1
while cur_x != pos_x or cur_y != pos_y:
first_direction = visited[cur_x][cur_y]
cur_x = cur_x - GameMap.ACTIONS_IN_MOVE[first_direction][0]
cur_y = cur_y - GameMap.ACTIONS_IN_MOVE[first_direction][1]
return GameMap.ACTIONS[first_direction]
class Game:
def __init__(self):
self.map = None
self.team_name = None
self.team_target = None
self.num_flags = 0
self.num_players = 0
self.game_started = False
self.player_to_flag_assignments = None
def startGame(self, game_json):
self.map = GameMap(game_json["map"])
self.team_name = game_json["myteamName"]
self.team_target = game_json["myteamTarget"]
self.num_flags = game_json["numFlags"]
self.num_players = game_json["numPlayers"]
self.game_started = True
self.middle_line = self.map.w / 2;
self.my_team_on_the_left = self.team_target[0]['x'] < self.middle_line;
# playerName -> (flagX, flagY)
self.player_to_flag_assignments = dict()
def endGame(self, game_json):
self.map = None
self.team_name = None
self.team_target = None
self.num_flags = 0
self.num_players = 0
self.game_started = False
self.player_to_flag_assignments = None
def is_player_safe(self, player):
return (player["posX"] < self.middle_line) == self.my_team_on_the_left
def assign_flags_to_players(self, players, flags):
"""
assign all pickable flags to all eligible players (i.e., !hasFlag and !inPrison)
"""
# remove the prison player and player with flags
for p in players:
if (p["hasFlag"] or p["inPrison"]) and p["name"] in self.player_to_flag_assignments:
del self.player_to_flag_assignments[p["name"]]
pickable_flags = {
(f["posX"], f["posY"]): False for f in flags if f["canPickup"]
}
players_wo_flag = set([p["name"] for p in players if (not p["hasFlag"]) and (not p["inPrison"])])
for p, f in self.player_to_flag_assignments.items():
if f in pickable_flags:
pickable_flags[f] = True
players_wo_flag.remove(p)
# randomly match unassigned flags and players
flags_wo_player = [f for (f, m) in pickable_flags.items() if not m]
if len(flags_wo_player) > 0:
for i, p in enumerate(players_wo_flag):
self.player_to_flag_assignments[p] = flags_wo_player[i % len(flags_wo_player)]
elif len(pickable_flags) > 0:
pickable_flags_list = list(pickable_flags)
for i, p in enumerate(players_wo_flag):
self.player_to_flag_assignments[p] = random.choice(pickable_flags_list)
def find_next_move(self, player, opponents):
if player["inPrison"]:
return ""
blockers = [] if self.is_player_safe(player) else [(o["posX"], o["posY"]) for o in opponents]
if player["hasFlag"]:
return self.map.find_closest_goal_from_pos(
player["posX"], player["posY"],
[GAME.team_target[0]],
blockers)
if player["name"] not in self.player_to_flag_assignments:
return ""
flag = self.player_to_flag_assignments[player["name"]]
return self.map.find_closest_goal_from_pos(
player["posX"], player["posY"],
[{"x": flag[0], "y": flag[1]}],
blockers
)
# SINGLETON
GAME = Game()
async def startGame(req):
print("Start Game")
global GAME
GAME.startGame(req)
async def planNextActions(req, websocket):
global GAME
player_moves = dict()
players = req.get("myteamPlayer", [])
opponents = req.get("opponentPlayer", [])
flags = req.get("opponentFlag", [])
GAME.assign_flags_to_players(players, flags)
for p in players:
action = GAME.find_next_move(p, opponents)
if action != "":
player_moves[p["name"]] = action
result = {"players": player_moves}
await websocket.send(json.dumps(result))
async def gameOver(req):
global GAME
GAME.endGame(req)
async def handle_client(websocket):
print("New session started")
try:
async for msg in websocket:
try:
req = json.loads(msg)
if req.get("action") == "status":
await planNextActions(req, websocket)
elif req.get("action") == "init":
await startGame(req)
elif req.get("action") == "finished":
await gameOver(req)
except json.JSONDecodeError:
print("JSON parse error")
await websocket.send(json.dumps({"error": "invalid json"}))
except websockets.exceptions.ConnectionClosedOK:
print("Client closed connection normally")
except websockets.exceptions.ConnectionClosedError as e:
print(f"Connection error: {e}")
except Exception as e:
print(f"Exception: {e}")
traceback.print_exc()
async def main():
import sys
if len(sys.argv) != 2:
print(f"Usage: python3 {sys.argv[0]} <port>")
print(f"Example: python3 {sys.argv[0]} 8080")
return
port = int(sys.argv[1])
print(f"AI backend running on port {port} ...")
async with websockets.serve(handle_client, "0.0.0.0", port):
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())
-103
View File
@@ -1,103 +0,0 @@
import asyncio
import random
from lib.game_engine import GameMap, run_game_server
import threading
# 1. Initialize the global world model
world = GameMap(show_gap_in_msec=1000.0)
lock = threading.Lock()
last_updated_time = -1
update_threshold = 100
player_to_flag_assignments = {}
def start_game(req):
global player_to_flag_assignments
world.init(req)
print("Start Game!!")
player_to_flag_assignments = {}
print(f"Game Started! Side: {'Left' if world.is_on_left(list(world.my_team_target)[0]) else 'Right'}")
def plan_next_actions(req):
if not world.update(req):
return
global player_to_flag_assignments
# Render the map
# world.show(do_not_clear=False)
my_players = world.list_players(mine=True, inPrison=False, hasFlag=None)
opponents = world.list_players(mine=False, inPrison=False, hasFlag=None)
enemy_flags = world.list_flags(mine=False, canPickup=True)
my_targets = list(world.list_targets(mine=True))
# 2. Logic: Assign flags to players without flags
# We maintain the original logic of matching players to specific flag coordinates
active_player_names = {p["name"] for p in my_players if not p["hasFlag"]}
# Cleanup assignments for players captured or flags already taken
player_to_flag_assignments = {
name: pos for name, pos in player_to_flag_assignments.items()
if name in active_player_names
}
if enemy_flags:
for p in my_players:
if not p["hasFlag"] and p["name"] not in player_to_flag_assignments:
# Randomly assign one of the available enemy flags
f = random.choice(enemy_flags)
player_to_flag_assignments[p["name"]] = (f["posX"], f["posY"])
# 3. Plan moves for each player
player_moves = {}
my_side_is_left = world.is_on_left(my_targets[0])
for p in my_players:
curr_pos = (p["posX"], p["posY"])
# Determine Target: Either the assigned flag or the home target
if p["hasFlag"]:
dest = my_targets[0]
elif p["name"] in player_to_flag_assignments:
dest = player_to_flag_assignments[p["name"]]
else:
continue
# Determine Obstacles: Avoid opponents if we are in enemy territory
is_safe = world.is_on_left(curr_pos) == my_side_is_left
blockers = [] if is_safe else [(o["posX"], o["posY"]) for o in opponents]
# Calculate Path
path = world.route_to(curr_pos, dest, extra_obstacles=blockers)
if len(path) > 1:
move = world.get_direction(curr_pos, path[1])
player_moves[p["name"]] = move
return player_moves
def game_over(req):
print("Game Over!")
world.show(force=True)
async def main():
import sys
if len(sys.argv) != 2:
print(f"Usage: python3 {sys.argv[0]} <port>")
print(f"Example: python3 {sys.argv[0]} 8080")
sys.exit(1)
port = int(sys.argv[1])
print(f"AI backend running on port {port} ...")
try:
await run_game_server(port, start_game, plan_next_actions, game_over)
except Exception as e:
print(f"Server Stopped: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
python3 pick_closets_flag.py ${CTF_PORT_BACKEND1}
-177
View File
@@ -1,177 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "21c8be3f-c57b-439c-8ca8-5991dd0da465",
"metadata": {},
"source": [
"# 夺旗赛 Capture The Flag Test"
]
},
{
"cell_type": "markdown",
"id": "0dfeabca-44d9-4906-abc6-cba1e4fc8a35",
"metadata": {},
"source": [
"## 初始化 (使用上排目录栏的的 ▶️ 运行,▪️停止,⟳ 重启)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "dfde1bf2-03b7-4f8c-8eff-07f486bfe520",
"metadata": {},
"outputs": [],
"source": [
"import importlib\n",
"import lib.game_engine\n",
"\n",
"# Force the reload manually\n",
"importlib.reload(lib.game_engine)\n",
"\n",
"# Re-import the specific classes/functions\n",
"from lib.game_engine import GameMap, run_game_server\n",
"\n",
"# Now initialize your objects\n",
"world = GameMap()"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "a3f08c96-ce25-43c5-97e6-955e6fc67746",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"\n",
"def load_example_json():\n",
" with open(\"example_init.json\", \"r\") as fin:\n",
" init_data = json.load(fin)\n",
" world.init(init_data)\n",
"\n",
" with open(\"example_plan_next_actions.json\", \"r\") as fin:\n",
" status_data = json.load(fin)\n",
" \n",
" world.init(init_data)\n",
" world.update(status_data)\n",
" world.show()\n",
" # world.show(flag_over_target=True, player_over_prison=True) "
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "b0eeca91-8a45-4c1f-8418-d5593968dd99",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19\n",
" 0 ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ \n",
" 1 ██ LF . . . . . . . . ██ . . . . . R0 . RF ██ \n",
" 2 ██ LF L0 . . . . . . . . . . . . . R1 . RF ██ \n",
" 3 ██ LF L1 . . . . . . . . . . . . . R2 . RF ██ \n",
" 4 ██ LF L2 . . . . . . . . ██ . . . . . . RF ██ \n",
" 5 ██ LF . . . . . . . ██ . ██ . . . . . . RF ██ \n",
" 6 ██ LF . . . . . . . . ██ . . . . ██ . . RF ██ \n",
" 7 ██ . . . . . . . . . . . . . . . . . . ██ \n",
" 8 ██ . . . . . . . . . . . . . . . . . . ██ \n",
" 9 ██ TT TT TT . . . . . . . . . . . ██ TT TT TT ██ \n",
"10 ██ TT TT TT . . . . . . . . . . . . TT TT TT ██ \n",
"11 ██ TT TT TT . . . . . ██ . . . . ██ . TT TT TT ██ \n",
"12 ██ . . . . . . . . ██ . . . ██ . . . . . ██ \n",
"13 ██ . . . . . . . . . . . . . ██ . . . . ██ \n",
"14 ██ . . . . . . . . . . ██ . . ██ . . . . ██ \n",
"15 ██ . . . . . . . . . . ██ . . . . . . . ██ \n",
"16 ██ PP PP PP . . . . . . . . . . . . PP PP PP ██ \n",
"17 ██ PP PP PP . . . . . . . . . . . . PP PP PP ██ \n",
"18 ██ PP PP PP . . . . . . ██ . . . . . PP PP PP ██ \n",
"19 ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ \n"
]
}
],
"source": [
"load_example_json()"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "854f32c5-e6cd-420e-ac44-6bf3dfbee646",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"11067.9\n"
]
}
],
"source": [
"print(world.current_time)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "e6bca36b-bb07-44c6-8b0a-d740987e5811",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1000.0\n"
]
}
],
"source": [
"print(world.show_gap_in_msec)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "a425410f-c733-4240-a032-0f852f7b8d39",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'width': 20, 'height': 20, 'middle_line': 10.0, 'walls': {(4, 0), (19, 0), (8, 0), (14, 13), (19, 9), (10, 6), (0, 5), (5, 19), (19, 18), (11, 5), (0, 14), (11, 14), (7, 19), (18, 19), (19, 2), (19, 11), (0, 7), (9, 19), (0, 16), (13, 19), (19, 4), (0, 0), (11, 0), (9, 12), (0, 9), (15, 0), (13, 12), (15, 9), (1, 19), (17, 0), (19, 6), (9, 5), (3, 19), (14, 19), (0, 2), (16, 19), (5, 0), (11, 4), (7, 0), (18, 0), (9, 0), (14, 14), (10, 1), (13, 0), (19, 13), (15, 6), (10, 19), (0, 18), (12, 19), (1, 0), (19, 15), (0, 11), (3, 0), (14, 0), (19, 8), (0, 4), (16, 0), (19, 17), (0, 13), (2, 19), (6, 19), (14, 11), (4, 19), (19, 1), (19, 10), (0, 6), (19, 19), (8, 19), (0, 15), (11, 15), (19, 3), (10, 0), (19, 12), (9, 11), (0, 8), (10, 18), (0, 17), (12, 0), (19, 5), (0, 1), (19, 14), (0, 10), (0, 19), (11, 19), (15, 19), (19, 7), (0, 3), (2, 0), (19, 16), (17, 19), (0, 12), (6, 0)}, 'players': [{'name': 'L0', 'team': 'L', 'hasFlag': False, 'posX': 2, 'posY': 2, 'inPrison': False, 'inPrisonTimeLeft': 0, 'inPrisonDuration': 20000, 'mine': True}, {'name': 'L1', 'team': 'L', 'hasFlag': False, 'posX': 2, 'posY': 3, 'inPrison': False, 'inPrisonTimeLeft': 0, 'inPrisonDuration': 20000, 'mine': True}, {'name': 'L2', 'team': 'L', 'hasFlag': False, 'posX': 2, 'posY': 4, 'inPrison': False, 'inPrisonTimeLeft': 0, 'inPrisonDuration': 20000, 'mine': True}, {'name': 'R0', 'team': 'R', 'hasFlag': False, 'posX': 16, 'posY': 1, 'inPrison': False, 'inPrisonTimeLeft': 0, 'inPrisonDuration': 20000, 'mine': False}, {'name': 'R1', 'team': 'R', 'hasFlag': False, 'posX': 16, 'posY': 2, 'inPrison': False, 'inPrisonTimeLeft': 0, 'inPrisonDuration': 20000, 'mine': False}, {'name': 'R2', 'team': 'R', 'hasFlag': False, 'posX': 16, 'posY': 3, 'inPrison': False, 'inPrisonTimeLeft': 0, 'inPrisonDuration': 20000, 'mine': False}], 'flags': [{'canPickup': True, 'posX': 1, 'posY': 1, 'mine': True}, {'canPickup': True, 'posX': 1, 'posY': 2, 'mine': True}, {'canPickup': True, 'posX': 1, 'posY': 3, 'mine': True}, {'canPickup': True, 'posX': 1, 'posY': 4, 'mine': True}, {'canPickup': True, 'posX': 1, 'posY': 5, 'mine': True}, {'canPickup': True, 'posX': 1, 'posY': 6, 'mine': True}, {'canPickup': True, 'posX': 18, 'posY': 1, 'mine': False}, {'canPickup': True, 'posX': 18, 'posY': 2, 'mine': False}, {'canPickup': True, 'posX': 18, 'posY': 3, 'mine': False}, {'canPickup': True, 'posX': 18, 'posY': 4, 'mine': False}, {'canPickup': True, 'posX': 18, 'posY': 5, 'mine': False}, {'canPickup': True, 'posX': 18, 'posY': 6, 'mine': False}], 'current_time': 11067.9, 'next_show_time': 12067.9, 'my_team_name': 'L', 'show_gap_in_msec': 1000.0, 'my_team_prison': {(1, 18), (2, 17), (3, 17), (2, 16), (3, 16), (1, 17), (2, 18), (1, 16), (3, 18)}, 'opponent_team_prison': {(17, 17), (18, 17), (16, 16), (17, 16), (18, 16), (18, 18), (16, 18), (17, 18), (16, 17)}, 'my_team_target': {(1, 11), (2, 10), (3, 10), (2, 9), (3, 9), (1, 10), (2, 11), (1, 9), (3, 11)}, 'opponent_team_target': {(16, 10), (17, 10), (18, 10), (16, 9), (17, 9), (18, 9), (17, 11), (18, 11), (16, 11)}}\n"
]
}
],
"source": [
"# print all the variables in `world`\n",
"print(world.__dict__)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

-37
View File
@@ -1,37 +0,0 @@
{ "compressionlevel":0,
"editorsettings":
{
"export":
{
"format":"json",
"target":"tilemap.json"
}
},
"height":20,
"infinite":false,
"layers":[],
"nextlayerid":4,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.3.0",
"tileheight":32,
"tilesets":[
{
"columns":12,
"firstgid":1,
"image":"tiles.png",
"imageheight":352,
"imagewidth":384,
"margin":0,
"name":"tiles",
"spacing":0,
"tilecount":132,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.2,
"width":20
}
-28
View File
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.3.0" orientation="orthogonal" renderorder="right-down" compressionlevel="0" width="21" height="15" tilewidth="32" tileheight="32" infinite="0" nextlayerid="4" nextobjectid="1">
<editorsettings>
<export target="tilemap.json" format="json"/>
</editorsettings>
<tileset firstgid="1" name="tiles" tilewidth="32" tileheight="32" tilecount="132" columns="12">
<image source="tiles.png" width="384" height="352"/>
</tileset>
<layer id="2" name="level" width="21" height="15">
<data encoding="csv">
45,46,46,46,46,46,46,46,46,47,5,45,46,46,46,46,46,46,46,46,47,
57,94,94,94,94,94,94,94,94,60,17,60,94,94,94,94,94,94,94,94,59,
57,106,81,83,94,81,46,47,94,69,46,71,94,45,46,83,94,81,83,106,59,
57,94,94,94,94,94,94,60,94,94,96,94,94,60,94,94,94,94,94,94,59,
57,94,81,83,94,48,94,69,46,83,0,81,46,71,94,48,94,81,83,94,59,
60,94,94,94,94,60,94,94,0,0,0,0,0,94,94,60,94,94,94,94,60,
69,46,46,83,94,69,46,47,0,53,56,55,0,45,46,71,94,81,46,46,71,
104,0,0,0,94,94,94,60,0,65,68,67,0,60,94,94,94,0,0,0,104,
45,46,46,83,94,48,94,72,0,77,79,80,0,72,94,48,94,81,46,46,47,
60,94,94,94,94,60,94,0,0,0,95,0,0,0,94,60,94,94,94,94,60,
57,94,81,47,94,72,94,48,94,81,46,83,94,48,94,72,94,45,83,94,59,
57,106,94,60,94,94,94,60,94,94,84,94,94,60,94,94,94,60,94,106,59,
69,47,94,72,94,81,83,69,83,94,94,94,81,71,81,83,94,72,94,45,71,
5,60,94,94,94,94,94,94,94,94,58,94,94,94,94,94,94,94,94,60,5,
17,69,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,71,17
</data>
</layer>
</map>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

-213
View File
@@ -1,213 +0,0 @@
{
"teams": [
{ "name": "L", "who": "user0-1"},
{ "name": "R", "who": "user0-2"}
],
"setup": {
"numPlayers": 1,
"numFlags": 3,
"useRandomFlags": false
},
"servers": {
"user0-1": "ws://115.191.4.103:34568",
"user0-2": "ws://115.191.4.103:34569",
"user1-1": "ws://115.191.4.103:34571",
"user1-2": "ws://115.191.4.103:34572",
"user2-1": "ws://115.191.4.103:34574",
"user2-2": "ws://115.191.4.103:34575",
"user3-1": "ws://115.191.4.103:34577",
"user3-2": "ws://115.191.4.103:34578",
"user4-1": "ws://115.191.4.103:34580",
"user4-2": "ws://115.191.4.103:34581",
"user5-1": "ws://115.191.4.103:34583",
"user5-2": "ws://115.191.4.103:34584",
"user6-1": "ws://115.191.4.103:34586",
"user6-2": "ws://115.191.4.103:34587",
"user7-1": "ws://115.191.4.103:34589",
"user7-2": "ws://115.191.4.103:34590",
"user8-1": "ws://115.191.4.103:34592",
"user8-2": "ws://115.191.4.103:34593",
"user9-1": "ws://115.191.4.103:34595",
"user9-2": "ws://115.191.4.103:34596",
"user10-1": "ws://115.191.4.103:34598",
"user10-2": "ws://115.191.4.103:34599",
"user11-1": "ws://115.191.4.103:34601",
"user11-2": "ws://115.191.4.103:34602",
"user12-1": "ws://115.191.4.103:34604",
"user12-2": "ws://115.191.4.103:34605",
"user13-1": "ws://115.191.4.103:34607",
"user13-2": "ws://115.191.4.103:34608",
"user14-1": "ws://115.191.4.103:34610",
"user14-2": "ws://115.191.4.103:34611",
"user15-1": "ws://115.191.4.103:34613",
"user15-2": "ws://115.191.4.103:34614",
"user16-1": "ws://115.191.4.103:34616",
"user16-2": "ws://115.191.4.103:34617",
"user17-1": "ws://115.191.4.103:34619",
"user17-2": "ws://115.191.4.103:34620",
"user18-1": "ws://115.191.4.103:34622",
"user18-2": "ws://115.191.4.103:34623",
"user19-1": "ws://115.191.4.103:34625",
"user19-2": "ws://115.191.4.103:34626",
"user20-1": "ws://115.191.4.103:34628",
"user20-2": "ws://115.191.4.103:34629",
"user21-1": "ws://115.191.4.103:34631",
"user21-2": "ws://115.191.4.103:34632",
"user22-1": "ws://115.191.4.103:34634",
"user22-2": "ws://115.191.4.103:34635",
"user23-1": "ws://115.191.4.103:34637",
"user23-2": "ws://115.191.4.103:34638",
"user24-1": "ws://115.191.4.103:34640",
"user24-2": "ws://115.191.4.103:34641",
"user25-1": "ws://115.191.4.103:34643",
"user25-2": "ws://115.191.4.103:34644",
"user26-1": "ws://115.191.4.103:34646",
"user26-2": "ws://115.191.4.103:34647",
"user27-1": "ws://115.191.4.103:34649",
"user27-2": "ws://115.191.4.103:34650",
"user28-1": "ws://115.191.4.103:34652",
"user28-2": "ws://115.191.4.103:34653",
"user29-1": "ws://115.191.4.103:34655",
"user29-2": "ws://115.191.4.103:34656",
"user30-1": "ws://115.191.4.103:34658",
"user30-2": "ws://115.191.4.103:34659",
"user31-1": "ws://115.191.4.103:34661",
"user31-2": "ws://115.191.4.103:34662",
"user32-1": "ws://115.191.4.103:34664",
"user32-2": "ws://115.191.4.103:34665",
"user33-1": "ws://115.191.4.103:34667",
"user33-2": "ws://115.191.4.103:34668",
"user34-1": "ws://115.191.4.103:34670",
"user34-2": "ws://115.191.4.103:34671",
"user35-1": "ws://115.191.4.103:34673",
"user35-2": "ws://115.191.4.103:34674",
"user36-1": "ws://115.191.4.103:34676",
"user36-2": "ws://115.191.4.103:34677",
"user37-1": "ws://115.191.4.103:34679",
"user37-2": "ws://115.191.4.103:34680",
"user38-1": "ws://115.191.4.103:34682",
"user38-2": "ws://115.191.4.103:34683",
"user39-1": "ws://115.191.4.103:34685",
"user39-2": "ws://115.191.4.103:34686",
"user40-1": "ws://115.191.4.103:34688",
"user40-2": "ws://115.191.4.103:34689",
"user41-1": "ws://115.191.4.103:34691",
"user41-2": "ws://115.191.4.103:34692",
"user42-1": "ws://115.191.4.103:34694",
"user42-2": "ws://115.191.4.103:34695",
"user43-1": "ws://115.191.4.103:34697",
"user43-2": "ws://115.191.4.103:34698",
"user44-1": "ws://115.191.4.103:34700",
"user44-2": "ws://115.191.4.103:34701",
"user45-1": "ws://115.191.4.103:34703",
"user45-2": "ws://115.191.4.103:34704",
"user46-1": "ws://115.191.4.103:34706",
"user46-2": "ws://115.191.4.103:34707",
"user47-1": "ws://115.191.4.103:34709",
"user47-2": "ws://115.191.4.103:34710",
"user48-1": "ws://115.191.4.103:34712",
"user48-2": "ws://115.191.4.103:34713",
"user49-1": "ws://115.191.4.103:34715",
"user49-2": "ws://115.191.4.103:34716",
"user50-1": "ws://115.191.4.103:34718",
"user50-2": "ws://115.191.4.103:34719",
"user51-1": "ws://115.191.4.103:34721",
"user51-2": "ws://115.191.4.103:34722",
"user52-1": "ws://115.191.4.103:34724",
"user52-2": "ws://115.191.4.103:34725",
"user53-1": "ws://115.191.4.103:34727",
"user53-2": "ws://115.191.4.103:34728",
"user54-1": "ws://115.191.4.103:34730",
"user54-2": "ws://115.191.4.103:34731",
"user55-1": "ws://115.191.4.103:34733",
"user55-2": "ws://115.191.4.103:34734",
"user56-1": "ws://115.191.4.103:34736",
"user56-2": "ws://115.191.4.103:34737",
"user57-1": "ws://115.191.4.103:34739",
"user57-2": "ws://115.191.4.103:34740",
"user58-1": "ws://115.191.4.103:34742",
"user58-2": "ws://115.191.4.103:34743",
"user59-1": "ws://115.191.4.103:34745",
"user59-2": "ws://115.191.4.103:34746",
"user60-1": "ws://115.191.4.103:34748",
"user60-2": "ws://115.191.4.103:34749",
"user61-1": "ws://115.191.4.103:34751",
"user61-2": "ws://115.191.4.103:34752",
"user62-1": "ws://115.191.4.103:34754",
"user62-2": "ws://115.191.4.103:34755",
"user63-1": "ws://115.191.4.103:34757",
"user63-2": "ws://115.191.4.103:34758",
"user64-1": "ws://115.191.4.103:34760",
"user64-2": "ws://115.191.4.103:34761",
"user65-1": "ws://115.191.4.103:34763",
"user65-2": "ws://115.191.4.103:34764",
"user66-1": "ws://115.191.4.103:34766",
"user66-2": "ws://115.191.4.103:34767",
"user67-1": "ws://115.191.4.103:34769",
"user67-2": "ws://115.191.4.103:34770",
"user68-1": "ws://115.191.4.103:34772",
"user68-2": "ws://115.191.4.103:34773",
"user69-1": "ws://115.191.4.103:34775",
"user69-2": "ws://115.191.4.103:34776",
"user70-1": "ws://115.191.4.103:34778",
"user70-2": "ws://115.191.4.103:34779",
"user71-1": "ws://115.191.4.103:34781",
"user71-2": "ws://115.191.4.103:34782",
"user72-1": "ws://115.191.4.103:34784",
"user72-2": "ws://115.191.4.103:34785",
"user73-1": "ws://115.191.4.103:34787",
"user73-2": "ws://115.191.4.103:34788",
"user74-1": "ws://115.191.4.103:34790",
"user74-2": "ws://115.191.4.103:34791",
"user75-1": "ws://115.191.4.103:34793",
"user75-2": "ws://115.191.4.103:34794",
"user76-1": "ws://115.191.4.103:34796",
"user76-2": "ws://115.191.4.103:34797",
"user77-1": "ws://115.191.4.103:34799",
"user77-2": "ws://115.191.4.103:34800",
"user78-1": "ws://115.191.4.103:34802",
"user78-2": "ws://115.191.4.103:34803",
"user79-1": "ws://115.191.4.103:34805",
"user79-2": "ws://115.191.4.103:34806",
"user80-1": "ws://115.191.4.103:34808",
"user80-2": "ws://115.191.4.103:34809",
"user81-1": "ws://115.191.4.103:34811",
"user81-2": "ws://115.191.4.103:34812",
"user82-1": "ws://115.191.4.103:34814",
"user82-2": "ws://115.191.4.103:34815",
"user83-1": "ws://115.191.4.103:34817",
"user83-2": "ws://115.191.4.103:34818",
"user84-1": "ws://115.191.4.103:34820",
"user84-2": "ws://115.191.4.103:34821",
"user85-1": "ws://115.191.4.103:34823",
"user85-2": "ws://115.191.4.103:34824",
"user86-1": "ws://115.191.4.103:34826",
"user86-2": "ws://115.191.4.103:34827",
"user87-1": "ws://115.191.4.103:34829",
"user87-2": "ws://115.191.4.103:34830",
"user88-1": "ws://115.191.4.103:34832",
"user88-2": "ws://115.191.4.103:34833",
"user89-1": "ws://115.191.4.103:34835",
"user89-2": "ws://115.191.4.103:34836",
"user90-1": "ws://115.191.4.103:34838",
"user90-2": "ws://115.191.4.103:34839",
"user91-1": "ws://115.191.4.103:34841",
"user91-2": "ws://115.191.4.103:34842",
"user92-1": "ws://115.191.4.103:34844",
"user92-2": "ws://115.191.4.103:34845",
"user93-1": "ws://115.191.4.103:34847",
"user93-2": "ws://115.191.4.103:34848",
"user94-1": "ws://115.191.4.103:34850",
"user94-2": "ws://115.191.4.103:34851",
"user95-1": "ws://115.191.4.103:34853",
"user95-2": "ws://115.191.4.103:34854",
"user96-1": "ws://115.191.4.103:34856",
"user96-2": "ws://115.191.4.103:34857",
"user97-1": "ws://115.191.4.103:34859",
"user97-2": "ws://115.191.4.103:34860",
"user98-1": "ws://115.191.4.103:34862",
"user98-2": "ws://115.191.4.103:34863",
"user99-1": "ws://115.191.4.103:34865",
"user99-2": "ws://115.191.4.103:34866"
}
}
-51
View File
@@ -1,51 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>capture_the_flag</title>
<style>
/* Reset CSS: Please avoid making changes here unless you know what you're doing :) */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
}
body {
line-height: 1;
font-family: Arial, sans-serif;
background-color: #040218;
}
ol, ul {
list-style: none;
}
img, video {
max-width: 100%;
height: auto;
display: block;
}
a {
text-decoration: none;
color: inherit;
}
</style>
</head>
<body>
<div id="game-container">
</div>
<!--script src="./phaser.js"></script-->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
<script type="module" src="./src/main.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
-11
View File
@@ -1,11 +0,0 @@
[game]
title = "capture_the_flag"
width = 1280
height = 720
[user]
id = "c2d44495-b627-44a5-8415-f3cfe6b96dcb"
[editor]
editor_version = "1.1.2"
phaser_version = "3.88.2"
creation_at = 1756965006742
code_font_size = 16
-74
View File
@@ -1,74 +0,0 @@
export default {
// 'audio': {
// score: {
// key: 'sound',
// args: ['assets/sound.mp3', 'assets/sound.m4a', 'assets/sound.ogg']
// },
// },
// 'image': {
// spikes: {
// key: 'spikes',
// args: ['assets/spikes.png']
// },
// },
'image': {
red_flag_img: {
key: 'red_flag_img',
args: ['assets/red_flag_32_32.png']
},
yellow_flag_img: {
key: 'yellow_flag_img',
args: ['assets/yellow_flag_32_32.png']
},
},
'spritesheet': {
tiles: {
key: 'tiles',
args: ['assets/tiles.png', {
frameWidth: 32,
frameHeight: 32
}]
},
characters: {
key: 'characters',
args: ['assets/characters.png', {
frameWidth: 32,
frameHeight: 32
}]
},
characters_L_flag: {
key: 'characters_L_flag',
args: ['assets/characters_yellow_flag.png', {
frameWidth: 32,
frameHeight: 32
}]
},
characters_R_flag: {
key: 'characters_R_flag',
args: ['assets/characters_red_flag.png', {
frameWidth: 32,
frameHeight: 32
}]
},
L_flag: {
key: 'L_flag',
args: ['assets/red_flag_32_32.png', {
frameWidth: 32,
frameHeight: 32
}]
},
R_flag: {
key: 'R_flag',
args: ['assets/yellow_flag_32_32.png', {
frameWidth: 32,
frameHeight: 32
}]
},
},
'tilemapTiledJSON': {
map: {
key: 'map',
args: ['assets/tilemap.json']
}
},
};
-42
View File
@@ -1,42 +0,0 @@
import ASSETS from '../assets.js';
export default class Flag extends Phaser.Physics.Arcade.Sprite
{
constructor(scene, x, y, team, canPickup)
{
if (team == "L") {
super(scene, x, y, ASSETS.spritesheet.L_flag.key);
} else {
super(scene, x, y, ASSETS.spritesheet.R_flag.key);
}
scene.add.existing(this);
scene.physics.add.existing(this);
this.team = team;
this.mapOffset = scene.getMapOffset();
this.posX = x;
this.posY = y;
this.setPosition(this.mapOffset.x + (x * this.mapOffset.tileSize), this.mapOffset.y + (y * this.mapOffset.tileSize));
this.setDepth(90);
this.scene = scene;
this.canPickup = canPickup;
}
collect() {
if (!this.canPickup) {
return false;
}
this.scene.removeFlagItem(this);
return true;
}
// return player status to send to remote backend
getStatus() {
return {
"canPickup": this.canPickup,
"posX": this.posX,
"posY": this.posY,
}
}
}
-166
View File
@@ -1,166 +0,0 @@
import ASSETS from '../assets.js';
const PlayerDirection = Object.freeze({
UP: "up",
DOWN: "down",
LEFT: "left",
RIGHT: "right",
});
export default class Player extends Phaser.Physics.Arcade.Sprite {
moveSpeed = 300; // time in milliseconds to move from one tile to another
frameDuration = 0;
accumulator = 0;
target = { x: 0, y: 0 };
constructor(scene, name, x, y, team, spriteChoice = 1, useAWSD = true) {
super(scene, x, y, ASSETS.spritesheet.characters.key, (spriteChoice - 1) * 12 + 1);
scene.add.existing(this);
scene.physics.add.existing(this);
this.name = name;
this.team = team;
this.inPrison = false;
this.inPrisonTimeLeft = 0;
this.inPrisonDuration = 20000; // time in milliseconds to stay in prison unless a teammate saves the player.
this.hasFlag = false;
this.spriteChoice = spriteChoice;
this.mapOffset = scene.getMapOffset();
this.target.x = this.mapOffset.x + (x * this.mapOffset.tileSize);
this.target.y = this.mapOffset.y + (y * this.mapOffset.tileSize);
this.setPosition(this.target.x, this.target.y);
this.setCollideWorldBounds(true);
this.setDepth(100);
this.scene = scene;
this.frameDuration = this.moveSpeed / this.mapOffset.tileSize;
this.remoteControl = null;
// key control
if (useAWSD) {
this.keys = this.scene.awsd_keys;
} else {
this.keys = this.scene.cursors;
}
this.can_go_next_tile = false; // will go next tile only if this is true
}
collectFlag() {
this.hasFlag = true;
}
dropFlag() {
this.hasFlag = false;
}
setRemoteControl(remoteControl) {
this.remoteControl = remoteControl;
}
update(time, delta) {
this.accumulator += delta;
while (this.accumulator > this.frameDuration) {
this.accumulator -= this.frameDuration;
if (this.inPrison) {
this.inPrisonTimeLeft -= this.frameDuration;
if (this.inPrisonTimeLeft <= 0) {
this.inPrison = false;
this.inPrisonTimeLeft = 0;
}
}
if (!this.inPrison) {
this.checkInput();
this.move();
} else {
this.showStaticImage();
}
}
}
checkInput() {
// check if player is at target position
if (this.can_go_next_tile && this.target.x === this.x && this.target.y === this.y) {
this.can_go_next_tile = false;
const moveDirection = { x: 0, y: 0 }; // default move direction
// Keys take priority over heuristics
if (this.keys.left.isDown) moveDirection.x--;
else if (this.keys.right.isDown) moveDirection.x++;
else if (this.keys.up.isDown) moveDirection.y--;
else if (this.keys.down.isDown) moveDirection.y++;
else if (this.remoteControl == PlayerDirection.LEFT) moveDirection.x--;
else if (this.remoteControl == PlayerDirection.RIGHT) moveDirection.x++;
else if (this.remoteControl == PlayerDirection.UP) moveDirection.y--;
else if (this.remoteControl == PlayerDirection.DOWN) moveDirection.y++;
// set next tile coordinates to move towards
const nextPosition = {
x: this.x + (moveDirection.x * this.mapOffset.tileSize),
y: this.y + (moveDirection.y * this.mapOffset.tileSize)
};
// check if next tile to move towards is walkable
if (!this.scene.isWall(nextPosition.x, nextPosition.y)) {
// set target position to move towards
this.target.x = nextPosition.x;
this.target.y = nextPosition.y;
}
}
}
// move player towards target position
move() {
let animation_key = "player" + this.spriteChoice + (this.hasFlag ? "-characters_"+this.team+"_flag-": "-characters-");
if (this.x < this.target.x) {
this.x ++;
this.anims.play(animation_key + "right", true);
}
else if (this.x > this.target.x) {
this.x --;
this.anims.play(animation_key + "left", true);
}
if (this.y < this.target.y) {
this.y ++;
this.anims.play(animation_key + "down", true);
}
else if (this.y > this.target.y) {
this.y --;
this.anims.play(animation_key + "up", true);
}
}
showStaticImage() {
let animation_key = "player" + this.spriteChoice + "-characters-down";
this.anims.play(animation_key, true);
}
toPrison(prisonX, prisonY) {
this.target.x = this.mapOffset.x + (prisonX * this.mapOffset.tileSize);
this.target.y = this.mapOffset.y + (prisonY * this.mapOffset.tileSize);
this.setPosition(this.target.x, this.target.y);
this.inPrison = true;
this.inPrisonTimeLeft = this.inPrisonDuration;
}
hit() {
this.destroy();
}
// return player status to send to remote backend
getStatus() {
return {
"name": this.name,
"team": this.team,
"hasFlag": this.hasFlag,
"posX": (this.target.x - this.mapOffset.x) / this.mapOffset.tileSize,
"posY": (this.target.y - this.mapOffset.y) / this.mapOffset.tileSize,
"inPrison": this.inPrison,
"inPrisonTimeLeft": this.inPrisonTimeLeft,
"inPrisonDuration": this.inPrisonDuration,
}
}
}
-34
View File
@@ -1,34 +0,0 @@
import { Boot } from './scenes/Boot.js';
import { Preloader } from './scenes/Preloader.js';
import { Game } from './scenes/Game.js';
import { GameOver } from './scenes/GameOver.js';
// Find out more information about the Game Config at:
// https://newdocs.phaser.io/docs/3.70.0/Phaser.Types.Core.GameConfig
const config = {
type: Phaser.AUTO,
// REMEMBER to keep consistent with tilemap.json
width: (20 + 10) * 32,
height: (20 + 10) * 32,
parent: 'game-container',
backgroundColor: '#2d3436',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
physics: {
default: 'arcade',
arcade: {
debug: false,
gravity: { y: 0 }
}
},
scene: [
Boot,
Preloader,
Game,
GameOver
]
};
new Phaser.Game(config);
-16
View File
@@ -1,16 +0,0 @@
export class Boot extends Phaser.Scene {
constructor() {
super('Boot');
}
preload() {
// The Boot Scene is typically used to load in any assets you require for your Preloader, such as a game logo or background.
// The smaller the file size of the assets, the better, as the Boot Scene itself has no preloader.
// this.load.image('background', 'assets/background.png');
}
create() {
this.scene.start('Preloader');
}
}
-821
View File
@@ -1,821 +0,0 @@
/*
* Asset from: https://kenney.nl/assets/pixel-platformer
*/
import ASSETS from '../assets.js';
import Player from '../gameObjects/Player.js';
import PlayerDirection from '../gameObjects/Player.js';
import Flag from '../gameObjects/Flag.js';
export class Game extends Phaser.Scene {
constructor() {
super('Game');
}
create() {
// do nothing
// we only do in create_after_preload()
}
create_after_preload() {
// constant
// this.NUM_PLAYERS = 3;
// this.NUM_FLAGS = 9;
// this.useRandomFlags = false;
this.NUM_OBSTACLES_1 = 8;
this.NUM_OBSTACLES_2 = 4;
this.stageSent = false;
this.initVariables();
this.initGameUi();
this.initAnimations();
this.initInput();
this.initMap();
this.initBoundary();
this.initTeams();
this.initPhysics();
}
startOrPauseOrContinue() {
if (!this.gameStarted) {
this.startGame();
} else {
this.gamePaused = !this.gamePaused;
}
}
update(time, delta) {
if (!this.gameStarted || this.gamePaused) return;
// move players
let players_ready = 0, total_players = 0;
let tick = (/** @type {string} */ can_go_next_tile) => {
this.lteamPlayers.getChildren().forEach( player => {
++total_players;
player.can_go_next_tile |= can_go_next_tile;
player.update(time, delta);
if(player.x == player.target.x && player.y == player.target.y)
++players_ready;
});
this.rteamPlayers.getChildren().forEach( player => {
++total_players;
player.can_go_next_tile |= can_go_next_tile;
player.update(time, delta);
if(player.x == player.target.x && player.y == player.target.y)
++players_ready;
});
};
tick(false);
if(players_ready !== total_players) {
this.stageSent = false;
return;
}
tick(true);
if (this.stageSent && time - this.lastSendTime < 600)
return;
setTimeout(() => {
this.stageSent = true;
// notify server backend
let lteamPlayerStatus = this.lteamPlayers.getChildren().map( player => player.getStatus() );
let lteamFlagStatus = this.lteamFlags.getChildren().map( flag => flag.getStatus() );
let rteamPlayerStatus = this.rteamPlayers.getChildren().map( player => player.getStatus() );
let rteamFlagStatus = this.rteamFlags.getChildren().map( flag => flag.getStatus() );
// each team gets its own perspective
if (this.lteamSocket && this.lteamSocket.readyState == WebSocket.OPEN) {
const lteamPlayerStatus = this.lteamPlayers.getChildren().map( player => player.getStatus() );
const payload = {
action: "status",
time: time,
myteamPlayer: lteamPlayerStatus,
myteamFlag: lteamFlagStatus,
myteamScore: this.lteamState.score,
opponentPlayer: rteamPlayerStatus,
opponentFlag: rteamFlagStatus,
opponentScore: this.rteamState.score,
}
this.lteamSocket.send(JSON.stringify(payload));
}
if (this.rteamSocket && this.rteamSocket.readyState == WebSocket.OPEN) {
const payload = {
action: "status",
time: time,
myteamPlayer: rteamPlayerStatus,
myteamFlag: rteamFlagStatus,
myteamScore: this.rteamState.score,
opponentPlayer: lteamPlayerStatus,
opponentFlag: lteamFlagStatus,
opponentScore: this.lteamState.score,
}
this.rteamSocket.send(JSON.stringify(payload));
}
this.lastSendTime = time;
}, 50);
}
async preload() {
// load team info from JSON file
const resp = await fetch("game_config.json");
const data = await resp.json();
this.team_config = data.teams;
this.team_servers = data.servers;
this.initSockets();
this.NUM_PLAYERS = data.setup.numPlayers;
this.NUM_FLAGS = data.setup.numFlags;
this.useRandomFlags = data.setup.useRandomFlags;
this.create_after_preload();
}
updatePlayerInfo(teamName, data) {
try {
const actions = JSON.parse(data);
Object.keys(actions.players).forEach (p => {
let d = actions.players[p];
console.assert(p.startsWith(teamName), `Invalid operation to control player ${p} for team ${teamName}`);
console.assert(d == "up" || d == "down" || d == "left" || d == "right",
`Invalid operation to move player to direction ${d}`);
});
let teamPlayers = null;
if (teamName === "L") {
teamPlayers = this.lteamPlayers.getChildren();
} else if (teamName === "R") {
teamPlayers = this.rteamPlayers.getChildren();
}
// For each team player, we will set its direction.
teamPlayers.forEach ( player => {
let remoteControl = actions.players[player.name];
player.setRemoteControl(remoteControl);
})
} catch (e) {
console.error("Invalid JSON from backend:", e, data);
}
}
initSockets() {
this.lteamSocket = null;
this.rteamSocket = null;
let lTeamWho = "-";
let rTeamWho = "-"
// connect to LTeam and RTeam backends
for (let i = 0; i < this.team_config.length; ++i) {
const team = this.team_config[i];
if (team.name != "L" && team.name != "R") {
console.log(`Unknown team ${team.name} found in remote_config.json. Skip.`);
continue;
}
if (team["ws_url"] == null && (team["who"] == null || this.team_servers[team.who] == null)) {
console.log(`Unknown server ${team["who"]} for ${team.name} found in remote_config.json. Skip.`);
continue;
}
if (team["who"] != null) {
if (team.name == "L") {
lTeamWho = team.who;
} else {
rTeamWho = team.who;
}
}
let ws = team["ws_url"] == null ? new WebSocket(this.team_servers[team.who]) : new WebSocket(team.ws_url);
ws.onopen = () => console.log(`${team.name} connected`);
ws.onmessage = (msg) => this.updatePlayerInfo(team.name, msg.data);
ws.onerror = (err) => console.error("WebSocket error", err);
if (team.name === "L") {
this.lteamSocket = ws;
}
else if (team.name === "R") {
this.rteamSocket = ws;
}
};
this.lastSendTime = 0;
this.lTeamWhoText = this.add.text(30, 60, `${lTeamWho}`, {
fontFamily: 'Arial Black', fontSize: 36, color: '#ffffff',
stroke: '#000000', strokeThickness: 8, align: 'left'
})
.setDepth(100);
this.rTeamWhoText = this.add.text(this.scale.width - 450, 60, `${rTeamWho}`, {
fontFamily: 'Arial Black', fontSize: 36, color: '#ffffff',
stroke: '#000000', strokeThickness: 8, align: 'right'
})
.setDepth(100);
}
initVariables() {
this.gameStarted = false;
this.gamePaused = false;
this.centerX = this.scale.width * 0.5;
this.centerY = this.scale.height * 0.5;
this.tileSize = 32; // width and height of a tile in pixels
this.halfTileSize = this.tileSize * 0.5; // width and height of a tile in pixels
this.mapHeight = (this.scale.height / this.tileSize) - 5 * 2; // height of the tile map (in tiles)
this.mapWidth = (this.scale.width / this.tileSize) - 5 * 2; // width of the tile map (in tiles)
this.mapX = this.centerX - (this.mapWidth * this.tileSize * 0.5); // x position of the top-left corner of the tile map
this.mapY = this.centerY - (this.mapHeight * this.tileSize * 0.5); // y position of the top-left corner of the tile map
// used to generate random background image
this.backgroundTiles = [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 44 ];
this.targetTiles = [13, 14, 15, 25, 26, 27, 37, 38, 39];
this.prisonTiles = [97, 98, 99, 109, 110, 111, 121, 122, 123];
this.wallTiles = [45, 46, 47, 57, 59, 69, 70, 71];
this.tree1Tiles = [6, 18, 30, 29, 28];
this.tree2Tiles = [[4, 16], [5, 17]];
// generate walls
this.walls = [
{x: 0, y: 0, tileId: 45}, {x: this.mapWidth -1, y: 0, tileId: 47}, {x: 0, y: this.mapHeight - 1, tileId:69}, {x: this.mapWidth - 1, y: this.mapHeight - 1, tileId: 71}
].concat(
Array.from({ length: this.mapWidth - 2 }, (_, i) => ({ x: i + 1, y: 0, tileId: 46 }))
).concat(
Array.from({ length: this.mapWidth - 2 }, (_, i) => ({ x: i + 1, y: this.mapHeight - 1, tileId: 46 }))
).concat(
Array.from({ length: this.mapHeight - 2 }, (_, i) => ({ x: 0, y: i + 1, tileId: 57 }))
).concat(
Array.from({ length: this.mapHeight - 2 }, (_, i) => ({ x: this.mapWidth - 1, y: i + 1, tileId: 59 }))
);
function notContains(xyArrays, x, y) {
const ret = xyArrays.find(obj => (obj.x === x && obj.y === y));
return ret == null;
}
// generate obstacles
this.obstacles1 = [];
for (let i = 0; i < this.NUM_OBSTACLES_1; ++i) {
while (true) {
const x = Phaser.Math.RND.integerInRange(4, this.mapWidth - 5);
const y = Phaser.Math.RND.integerInRange(1, this.mapHeight - 2);
if (notContains(this.obstacles1, x, y)) {
this.obstacles1.push({x: x, y: y});
break;
}
}
}
this.obstacles2 = [];
for (let i = 0; i< this.NUM_OBSTACLES_2; ++i) {
while (true) {
const x = Phaser.Math.RND.integerInRange(4, this.mapWidth - 5);
const y = Phaser.Math.RND.integerInRange(1, this.mapHeight - 3);
if (notContains(this.obstacles1, x, y)
&& notContains(this.obstacles1, x, y + 1)
&& notContains(this.obstacles2, x, y - 1)
&& notContains(this.obstacles2, x, y)) {
this.obstacles2.push({x: x, y: y});
break;
}
}
}
// Randomly generate flags
let lFlags = [];
for (let i = 0; i< this.NUM_FLAGS; ++i) {
while (true) {
const x = Phaser.Math.RND.integerInRange(2, this.mapWidth / 2 - 1);
const y = Phaser.Math.RND.integerInRange(1, this.mapHeight - 3);
if (notContains(this.obstacles1, x, y)
&& notContains(this.obstacles2, x, y - 1)
&& notContains(this.obstacles2, x, y)
&& notContains(lFlags, x, y)) {
lFlags.push({x: x, y: y});
break;
}
}
}
let rFlags = [];
for (let i = 0; i< this.NUM_FLAGS; ++i) {
while (true) {
const x = Phaser.Math.RND.integerInRange(this.mapWidth / 2, this.mapWidth - 2);
const y = Phaser.Math.RND.integerInRange(1, this.mapHeight - 3);
if (notContains(this.obstacles1, x, y)
&& notContains(this.obstacles2, x, y - 1)
&& notContains(this.obstacles2, x, y)
&& notContains(rFlags, x, y)) {
rFlags.push({x: x, y: y});
break;
}
}
}
// generate flag and player position for LTeam and RTeam
// left team
this.lteamState = {
score: 0,
player_sprite_choice: 1,
flags: this.useRandomFlags ? lFlags : Array.from({ length: this.NUM_FLAGS }, (_, i) => ({ x: 1, y: i + 1})),
players: this.useRandomFlags? Array.from({ length: this.NUM_PLAYERS }, (_, i) => ({ x: 1, y: i + 1, name: "L" + i})) : Array.from({ length: this.NUM_PLAYERS }, (_, i) => ({ x: 2, y: i + 1, name: "L" + i})),
target: this.create3x3grid(2, this.mapHeight / 2),
prison: this.create3x3grid(2, this.mapHeight - 3),
};
// right team
this.rteamState = {
score: 0,
player_sprite_choice: 4,
// flags: Array.from({ length: this.NUM_FLAGS }, (_, i) => ({ x: this.mapWidth - 2, y: i + 1})),
flags: this.useRandomFlags ? rFlags : Array.from({ length: this.NUM_FLAGS }, (_, i) => ({ x: this.mapWidth - 2, y: i + 1})),
players: this.useRandomFlags ? Array.from({ length: this.NUM_PLAYERS }, (_, i) => ({ x: this.mapWidth - 2, y: i + 1, name: "R" + i})) : Array.from({ length: this.NUM_PLAYERS }, (_, i) => ({ x: this.mapWidth - 3, y: i + 1, name: "R" + i})),
target: this.create3x3grid(this.mapWidth - 3, this.mapHeight / 2),
prison: this.create3x3grid(this.mapWidth - 3, this.mapHeight - 3),
};
this.map; // rference to tile map
this.groundLayer; // used to create background layer of tile map
this.levelLayer; // reference to level layer of tile map
}
initGameUi() {
// Create tutorial text
this.tutorialText = this.add.text(this.centerX, this.centerY, 'Arrow keys to move!\nPress Spacebar to Start', {
fontFamily: 'Arial Black', fontSize: 48, color: '#ffffff',
stroke: '#000000', strokeThickness: 8,
align: 'center'
})
.setOrigin(0.5)
.setDepth(100);
// Create score text
this.lScoreText = this.add.text(30, 20, 'LTeam #Flags: 0', {
fontFamily: 'Arial Black', fontSize: 36, color: '#ffffff',
stroke: '#000000', strokeThickness: 8, align: 'left'
})
.setDepth(100);
this.rScoreText = this.add.text(this.scale.width - 450, 20, 'RTeam #Flags: 0', {
fontFamily: 'Arial Black', fontSize: 36, color: '#ffffff',
stroke: '#000000', strokeThickness: 8, align: 'right'
})
.setDepth(100);
// Create game over text
this.gameOverText = this.add.text(this.scale.width * 0.5, this.scale.height * 0.5, 'Game Over', {
fontFamily: 'Arial Black', fontSize: 64, color: '#ffffff',
stroke: '#000000', strokeThickness: 8,
align: 'center'
})
.setOrigin(0.5)
.setDepth(100)
.setVisible(false);
}
initAnimations() {
const flag_choices = ["characters", "characters_L_flag", "characters_R_flag"];
const dir_choices = ["left", "down", "up", "right"]
for (let k = 0; k < 3; ++k) {
for (let i = 1; i <= 6; ++i) {
for (let j = 0; j < 4; ++j) {
const key = "player" + i + "-" + flag_choices[k] + "-" + dir_choices[j];
const config = { frames: [(i - 1) * 12 + j, (i - 1) * 12 + j + 4, (i - 1) * 12 + j + 8] };
this.anims.create({
key: key,
frames: this.anims.generateFrameNumbers(flag_choices[k], config),
frameRate: 10,
repeat: 0
});
}
}
}
}
initPhysics() {
this.physics.add.overlap(this.lteamPlayers, this.rteamPlayers, this.hitPlayer, null, this);
this.physics.add.overlap(this.lteamPlayers, this.rteamFlags, this.collectFlag, null, this);
this.physics.add.overlap(this.rteamPlayers, this.lteamFlags, this.collectFlag, null, this);
this.physics.add.overlap(this.lteamPlayers, this.lteamTargetZone, this.dropFlag, null, this);
this.physics.add.overlap(this.rteamPlayers, this.rteamTargetZone, this.dropFlag, null, this);
this.physics.add.overlap(this.lteamPlayers, this.lteamPrisonZone, this.freePlayer, null, this);
this.physics.add.overlap(this.rteamPlayers, this.rteamPrisonZone, this.freePlayer, null, this);
}
initTeams() {
// init L Team
this.lteamFlags = this.add.group();
this.lteamPlayers = this.add.group();
this.lteamState.flags.forEach( flag => {
const flagObj = new Flag(this, flag.x, flag.y, "L", true);
this.lteamFlags.add(flagObj);
});
this.lteamState.players.forEach( player => {
const playerObj = new Player(this, player.name, player.x, player.y, "L", this.lteamState.player_sprite_choice, true);
this.lteamPlayers.add(playerObj);
});
// this.lteamTargetZone = this.add.zone(this.lteamState.target[0].x * this.tileSize, this.lteamState.target[0].y * this.tileSize, 3 * this.tileSize, 3 * this.tileSize);
this.lteamTargetZone = this.add.zone(
this.mapX + (this.lteamState.target[0].x * this.tileSize + 1.5 * this.tileSize),
this.mapY + (this.lteamState.target[0].y * this.tileSize + 1.5 * this.tileSize),
3 * this.tileSize, 3 * this.tileSize);
this.physics.add.existing(this.lteamTargetZone);
this.lteamTargetZone.body.setAllowGravity(false);
this.lteamTargetZone.body.setImmovable(true);
// this.lteamPrisonZone = this.add.zone(this.lteamState.prison[0].x * this.tileSize, this.lteamState.prison[0].y * this.tileSize, 3 * this.tileSize, 3 * this.tileSize);
this.lteamPrisonZone = this.add.zone(
this.mapX + (this.lteamState.prison[0].x * this.tileSize + 1.5 * this.tileSize),
this.mapY + (this.lteamState.prison[0].y * this.tileSize + 1.5 * this.tileSize),
3 * this.tileSize, 3 * this.tileSize);
this.physics.add.existing(this.lteamPrisonZone);
this.lteamPrisonZone.body.setAllowGravity(false);
this.lteamPrisonZone.body.setImmovable(true);
// init R Team
this.rteamFlags = this.add.group();
this.rteamPlayers = this.add.group();
this.rteamState.flags.forEach( flag => {
const flagObj = new Flag(this, flag.x, flag.y, "R", true);
this.rteamFlags.add(flagObj);
});
this.rteamState.players.forEach( player => {
const playerObj = new Player(this, player.name, player.x, player.y, "R", this.rteamState.player_sprite_choice, false);
this.rteamPlayers.add(playerObj);
});
this.rteamTargetZone = this.add.zone(
this.mapX + (this.rteamState.target[0].x * this.tileSize + 1.5 * this.tileSize),
this.mapY + (this.rteamState.target[0].y * this.tileSize + 1.5 * this.tileSize),
3 * this.tileSize, 3 * this.tileSize);
this.physics.add.existing(this.rteamTargetZone);
this.rteamTargetZone.body.setAllowGravity(false);
this.rteamTargetZone.body.setImmovable(true);
this.rteamPrisonZone = this.add.zone(
this.mapX + (this.rteamState.prison[0].x * this.tileSize + 1.5 * this.tileSize),
this.mapY + (this.rteamState.prison[0].y * this.tileSize + 1.5 * this.tileSize),
3 * this.tileSize, 3 * this.tileSize);
this.physics.add.existing(this.rteamPrisonZone);
this.rteamPrisonZone.body.setAllowGravity(false);
this.rteamPrisonZone.body.setImmovable(true);
}
initInput() {
this.cursors = this.input.keyboard.createCursorKeys();
this.awsd_keys = this.input.keyboard.addKeys({
up: Phaser.Input.Keyboard.KeyCodes.W,
left: Phaser.Input.Keyboard.KeyCodes.A,
down: Phaser.Input.Keyboard.KeyCodes.S,
right: Phaser.Input.Keyboard.KeyCodes.D
});
// check for spacebar press only once
// this.cursors.space.once('down', (key, event) => {
// this.startGame();
// });
this.cursors.space.on('down', (key, event) => {
this.startOrPauseOrContinue();
});
}
// create tile map data
initMap() {
this.map = this.make.tilemap({ key: ASSETS.tilemapTiledJSON.map.key });
const tileset = this.map.addTilesetImage(ASSETS.spritesheet.tiles.key);
// create background layer, randomly pick the tiles
this.groundLayer = this.map.createBlankLayer('ground', tileset, this.mapX, this.mapY);
for (let y = 0; y < this.mapHeight; y++) {
for (let x = 0; x < this.mapWidth; x++) {
// randomly choose a tile id from this.tiles
// weightedPick favours items earlier in the array
const tileIndex = Phaser.Math.RND.pick(this.backgroundTiles);
this.groundLayer.putTileAt(tileIndex, x, y);
}
}
// create level layer to show game level elements
this.levelLayer = this.map.createBlankLayer('level', tileset, this.mapX, this.mapY);
this.levelLayer.fill(0, 0, 0, this.mapWidth, this.mapHeight);
// show prison tiles
for (let i = 0; i < this.lteamState.prison.length; ++i) {
const prison = this.lteamState.prison[i];
const tile = this.levelLayer.getTileAt(prison.x, prison.y);
tile.index = this.prisonTiles[i];
}
for (let i = 0; i < this.rteamState.prison.length; ++i) {
const prison = this.rteamState.prison[i];
const tile = this.levelLayer.getTileAt(prison.x, prison.y);
tile.index = this.prisonTiles[i];
}
for (let i = 0; i < this.lteamState.target.length; ++i) {
const target = this.lteamState.target[i];
const tile = this.levelLayer.getTileAt(target.x, target.y);
tile.index = this.targetTiles[i];
}
for (let i = 0; i < this.rteamState.target.length; ++i) {
const target = this.rteamState.target[i];
const tile = this.levelLayer.getTileAt(target.x, target.y);
tile.index = this.targetTiles[i];
}
// create wall
for (let i = 0; i < this.walls.length; ++i) {
const wall = this.walls[i];
const tile = this.levelLayer.getTileAt(wall.x, wall.y);
tile.index = wall.tileId;
const collisionId = (wall.x - 1) * this.mapWidth + wall.y;
this.map.setCollision(collisionId);
}
// create obstacles
for (let i = 0; i < this.obstacles1.length; ++i) {
const obs = this.obstacles1[i];
const tile = this.levelLayer.getTileAt(obs.x, obs.y);
tile.index = Phaser.Math.RND.pick(this.tree1Tiles);
const collisionId = (obs.x - 1) * this.mapWidth + obs.y;
this.map.setCollision(collisionId);
}
for (let i = 0; i < this.obstacles2.length; ++i) {
const obs = this.obstacles2[i];
const treeTile = Phaser.Math.RND.pick(this.tree2Tiles);
const tile1 = this.levelLayer.getTileAt(obs.x, obs.y);
tile1.index = treeTile[0];
const tile2 = this.levelLayer.getTileAt(obs.x, obs.y + 1);
tile2.index = treeTile[1];
const collisionId = (obs.x - 1) * this.mapWidth + obs.y;
this.map.setCollision(collisionId, collisionId + 1);
}
}
// create a thin black line in the middle
initBoundary() {
const startY = this.centerY - this.mapHeight * this.tileSize / 2;
const endY = this.centerY + this.mapHeight * this.tileSize / 2;
this.add.line(0, 0, this.centerX, startY, this.centerX, endY, 0x000000)
.setOrigin(0, 0)
.setLineWidth(1);
}
startGame() {
this.gameStarted = true;
this.tutorialText.setVisible(false);
const mapPayload = {
"width": this.mapWidth,
"height": this.mapHeight,
"walls": this.walls.map(w => {return {x: w.x, y: w.y}}),
"obstacles": this.obstacles1.concat(this.obstacles2).concat(
this.obstacles2.map(w => {return {"x": w.x, "y": w.y + 1}})
),
};
if (this.lteamSocket && this.lteamSocket.readyState == WebSocket.OPEN) {
const payload = {
"action": "init",
"map": mapPayload,
"numPlayers": this.NUM_PLAYERS,
"numFlags": this.NUM_FLAGS,
"myteamName": "L",
// this is where you will be sent to, if you were caught by opponent
"myteamPrison": this.lteamState.prison,
// this is where you will drop the flags
"myteamTarget": this.lteamState.target,
"opponentPrison": this.rteamState.prison,
"opponentTarget": this.rteamState.target,
}
this.lteamSocket.send(JSON.stringify(payload));
}
if (this.rteamSocket && this.rteamSocket.readyState == WebSocket.OPEN) {
const payload = {
"action": "init",
"map": mapPayload,
"numPlayers": this.NUM_PLAYERS,
"numFlags": this.NUM_FLAGS,
"myteamName": "R",
// this is where you will send the opponent to prison
"myteamPrison": this.rteamState.prison,
// this is where you will drop the flags
"myteamTarget": this.rteamState.target,
"opponentPrison": this.lteamState.prison,
"opponentTarget": this.lteamState.target,
}
this.rteamSocket.send(JSON.stringify(payload));
}
}
hitPlayer(player1, player2) {
// does not matter if player1 is not in the same team as player2
if (player1.team === player2.team) {
return;
}
if (player1.inPrison || player2.inPrison) {
return;
}
// When collision happens around the center, we use the middle X
let playerCenterX = (player1.x + player2.x) / 2;
// in L team's side
if (playerCenterX < this.centerX) {
// find the prison tile to send the R team player
// If the flag is held by the R team player, drop the flag at where it was caught.
const spot = this.findAvailablePrisonTile(this.rteamPlayers.getChildren(), this.rteamState.prison);
const caughtPlayer = player1.team === "R" ? player1 : player2;
if (caughtPlayer.hasFlag) {
const tile = this.getTileAt(caughtPlayer.x, caughtPlayer.y);
const flag = new Flag(this, tile.x, tile.y, "L", true);
this.lteamFlags.add(flag);
caughtPlayer.hasFlag = false;
}
caughtPlayer.toPrison(spot.x, spot.y);
} else {
const spot = this.findAvailablePrisonTile(this.lteamPlayers.getChildren(), this.lteamState.prison);
const caughtPlayer = player1.team === "L" ? player1 : player2;
if (caughtPlayer.hasFlag) {
const tile = this.getTileAt(caughtPlayer.x, caughtPlayer.y);
const flag = new Flag(this, tile.x, tile.y, "R", true);
this.rteamFlags.add(flag);
caughtPlayer.hasFlag = false;
}
caughtPlayer.toPrison(spot.x, spot.y);
}
}
findAvailablePrisonTile(players, prisons) {
for (let i = 0; i < prisons.length; ++i) {
let isAvailable = true;
for (let j = 0; j < players.length; ++j) {
if (!players[j].inPrison) {
continue;
}
const tile = this.getTileAt(players[j].x, players[j].y);
if (tile.x == prisons[i].x && tile.y == prisons[i].y) {
isAvailable = false;
break;
}
}
if (isAvailable) {
return {x: prisons[i].x, y: prisons[i].y};
}
}
}
dropFlag(player) {
if (!player.hasFlag) {
return;
}
player.dropFlag();
if (player.team == "L") {
const spot = this.findAvailableFlagTile(this.rteamFlags.getChildren(), this.lteamState.target);
const flag = new Flag(this, spot.x, spot.y, "R", false);
this.rteamFlags.add(flag);
this.updateTeamScore("L");
} else {
const spot = this.findAvailableFlagTile(this.lteamFlags.getChildren(), this.rteamState.target);
const flag = new Flag(this, spot.x, spot.y, "L", false);
this.lteamFlags.add(flag);
this.updateTeamScore("R");
}
}
findAvailableFlagTile(flags, targets) {
for (let i = 0; i < targets.length; ++i) {
let isAvailable = true;
for (let j = 0; j < flags.length; ++j) {
if (flags[j].canPickup) {
continue;
}
const tile = this.getTileAt(flags[j].x, flags[j].y);
if (tile.x == targets[i].x && tile.y == targets[i].y) {
isAvailable = false;
break;
}
}
if (isAvailable) {
return {x: targets[i].x, y: targets[i].y};
}
}
}
collectFlag(player, flag) {
// cannot collect flag from my team
if (player.team == flag.team) {
return;
}
if (player.inPrison) {
return;
}
// a player cannot collect >1 flag
if (player.hasFlag) {
return;
}
// cannot collect flag that cannot be collected
if (!flag.canPickup) {
return;
}
flag.collect();
player.collectFlag();
}
removeFlagItem(flag) {
if (flag.team == "L") {
this.lteamFlags.remove(flag, true, true);
} else if (flag.team == "R") {
this.rteamFlags.remove(flag, true, true);
}
}
freePlayer(player) {
// player sent to prison cannot free others
if (player.inPrison) {
return;
}
if (player.team == "L") {
this.lteamPlayers.getChildren().forEach( player => {
if (player.inPrison) { player.inPrison = false; }
})
} else {
this.rteamPlayers.getChildren().forEach( player => {
if (player.inPrison) { player.inPrison = false; }
})
}
}
updateTeamScore(team) {
if (team == "L") {
++this.lteamState.score;
this.lScoreText.setText(`LTeam #Flags: ${this.lteamState.score}`);
if (this.lteamState.score == this.NUM_FLAGS) {
this.GameOver(team);
}
} else if (team == "R") {
++this.rteamState.score;
this.rScoreText.setText(`RTeam #Flags: ${this.rteamState.score}`);
if (this.rteamState.score == this.NUM_FLAGS) {
this.GameOver(team);
}
}
}
getMapOffset() {
return {
x: this.mapX + this.halfTileSize,
y: this.mapY + this.halfTileSize,
width: this.mapWidth,
height: this.mapHeight,
tileSize: this.tileSize
}
}
getTileAt(x, y) {
const tile = this.levelLayer.getTileAtWorldXY(x, y, true);
return tile;
}
isWall(x, y) {
const tile = this.levelLayer.getTileAtWorldXY(x, y, true);
return this.wallTiles.indexOf(tile.index) >= 0 ||
this.tree1Tiles.indexOf(tile.index) >= 0 ||
this.tree2Tiles[0].indexOf(tile.index) >= 0 ||
this.tree2Tiles[1].indexOf(tile.index) >= 0
;
}
// return a 3x3 grid from x, y
create3x3grid(x, y) {
return [
{x: x - 1, y: y - 1}, {x: x, y: y - 1}, {x: x + 1, y: y - 1},
{x: x - 1, y: y}, {x: x, y: y}, {x: x + 1, y: y},
{x: x - 1, y: y + 1}, {x: x, y: y + 1}, {x: x + 1, y: y + 1},
]
}
GameOver(team) {
this.gameStarted = false;
this.gameOverText.setText(team+"Team Won!")
this.gameOverText.setVisible(true);
if (this.lteamSocket && this.lteamSocket.readyState == WebSocket.OPEN) {
const payload = {
action: "finished",
myteamScore: this.lteamState.score,
opponentScore: this.rteamState.score,
}
this.lteamSocket.send(JSON.stringify(payload));
}
if (this.rteamSocket && this.rteamSocket.readyState == WebSocket.OPEN) {
const payload = {
action: "finished",
myteamScore: this.rteamState.score,
opponentScore: this.lteamState.score,
}
this.rteamSocket.send(JSON.stringify(payload));
}
}
}
-16
View File
@@ -1,16 +0,0 @@
export class GameOver extends Phaser.Scene {
constructor() {
super('GameOver');
}
create() {
this.background1 = this.add.image(0, 0, 'background').setOrigin(0);
this.add.text(this.scale.width * 0.5, this.scale.height * 0.5, 'Game Over', {
fontFamily: 'Arial Black', fontSize: 64, color: '#ffffff',
stroke: '#000000', strokeThickness: 8,
align: 'center'
}).setOrigin(0.5);
}
}
-49
View File
@@ -1,49 +0,0 @@
import ASSETS from '../assets.js';
export class Preloader extends Phaser.Scene {
constructor() {
super('Preloader');
}
init() {
const centreX = this.scale.width * 0.5;
const centreY = this.scale.height * 0.5;
const barWidth = 468;
const barHeight = 32;
const barMargin = 4;
// We loaded this image in our Boot Scene, so we can display it here
// A simple progress bar. This is the outline of the bar.
this.add.rectangle(centreX, centreY, barWidth, barHeight).setStrokeStyle(1, 0xffffff);
// This is the progress bar itself. It will increase in size from the left based on the % of progress.
const bar = this.add.rectangle(centreX - (barWidth * 0.5) + barMargin, centreY, barMargin, barHeight - barMargin, 0xffffff);
// Use the 'progress' event emitted by the LoaderPlugin to update the loading bar
this.load.on('progress', (progress) => {
// Update the progress bar (our bar is 464px wide, so 100% = 464px)
bar.width = barMargin + ((barWidth - (barMargin * 2)) * progress);
});
}
preload() {
// Load the assets for the game - see ./src/assets.js
for (let type in ASSETS) {
for (let key in ASSETS[type]) {
let args = ASSETS[type][key].args.slice();
args.unshift(ASSETS[type][key].key);
this.load[type].apply(this.load, args);
}
}
}
create() {
// When all the assets have loaded, it's often worth creating global objects here that the rest of the game can use.
// For example, you can define global animations here, so we can use them in other scenes.
// Move to the MainMenu. You could also swap this for a Scene Transition, such as a camera fade.
this.scene.start('Game');
}
}
-1
View File
@@ -1 +0,0 @@
python3 -m http.server ${CTF_PORT_FRONTEND} --bind 0.0.0.0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 KiB

@@ -1,16 +1,15 @@
import asyncio import asyncio
import random
from lib.game_engine import GameMap, run_game_server from lib.game_engine import GameMap, run_game_server
import threading import threading
import collections import collections
# 1. Initialize the global world model world = GameMap(show_gap_in_msec=1.0)
world = GameMap(show_gap_in_msec=10.0)
lock = threading.Lock() lock = threading.Lock()
last_updated_time = -1 last_updated_time = -1
update_threshold = 10 update_threshold = 1
player_to_flag_assign = {} player_to_flag_assign = {}
my_side_is_left = None my_side_is_left = None
class Map: class Map:
def __init__(self): def __init__(self):
self.width = world.width self.width = world.width
@@ -23,6 +22,7 @@ class Map:
return y * self.width + x return y * self.width + x
def update(self,posx,posy): def update(self,posx,posy):
global my_side_is_left
self.width = world.width self.width = world.width
self.height = world.height self.height = world.height
self.edge = [[] for _ in range(self.width * self.height)] self.edge = [[] for _ in range(self.width * self.height)]
@@ -32,7 +32,11 @@ class Map:
x, y = wall x, y = wall
idx = self.convert_pos_to_index(x, y) idx = self.convert_pos_to_index(x, y)
self.grid[idx] = 1 self.grid[idx] = 1
self.in_safe_zone = world.is_on_left((posx,posy)) == my_side_is_left if my_side_is_left:
self.in_safe_zone = world.is_on_left((posx+1,posy)) == my_side_is_left
else:
self.in_safe_zone = world.is_on_left((posx-1,posy)) == my_side_is_left
print(my_side_is_left)
enemy_players = world.list_players(mine=False, inPrison=False, hasFlag=None) enemy_players = world.list_players(mine=False, inPrison=False, hasFlag=None)
ally_players = world.list_players(mine=True, inPrison=False, hasFlag=None) ally_players = world.list_players(mine=True, inPrison=False, hasFlag=None)
my_pos = self.convert_pos_to_index(posx, posy) my_pos = self.convert_pos_to_index(posx, posy)
@@ -61,10 +65,10 @@ class Map:
if 0 <= nx < self.width and 0 <= ny < self.height: if 0 <= nx < self.width and 0 <= ny < self.height:
n_idx = self.convert_pos_to_index(nx, ny) n_idx = self.convert_pos_to_index(nx, ny)
if self.in_safe_zone: if self.in_safe_zone:
if self.grid[n_idx] not in (1, 2): if self.grid[n_idx] not in (1, ):
self.edge[idx].append(n_idx) self.edge[idx].append(n_idx)
else: else:
if self.grid[n_idx] not in (1,): if self.grid[n_idx] not in (1, 2):
self.edge[idx].append(n_idx) self.edge[idx].append(n_idx)
def guideance(self, posx_start, posy_start, posx_end, posy_end): def guideance(self, posx_start, posy_start, posx_end, posy_end):
self.update(posx_start,posy_start) self.update(posx_start,posy_start)
@@ -84,11 +88,8 @@ class Map:
dist[v] = dist[u] + 1 dist[v] = dist[u] + 1
prev[v] = u prev[v] = u
queue.append(v) queue.append(v)
# ---- 若不可达,返回空字符串 ----
if dist[dst_idx] == float('inf'): if dist[dst_idx] == float('inf'):
return "" return ""
# ---- 重建路径(逆序) ----
path = [] path = []
cur = dst_idx cur = dst_idx
while cur is not None: while cur is not None:
@@ -125,7 +126,15 @@ class Map:
if dist[dst_idx] == float('inf'): if dist[dst_idx] == float('inf'):
return -1 return -1
return dist[dst_idx] return dist[dst_idx]
def closest(self, posx_start, posy_start, positions):
closest_pos = None
min_length = float('inf')
for pos in positions:
temp = self.length(posx_start, posy_start, pos[0], pos[1])
if temp != -1 and temp < min_length:
min_length = temp
closest_pos = pos
return closest_pos
myMap = Map() myMap = Map()
def start_game(req): def start_game(req):
global player_to_flag_assign,my_side_is_left global player_to_flag_assign,my_side_is_left
@@ -141,14 +150,24 @@ def plan_next_actions(req):
global player_to_flag_assign,myMap,my_side_is_left global player_to_flag_assign,myMap,my_side_is_left
my_players = world.list_players(mine=True, inPrison=False, hasFlag=None) my_players = world.list_players(mine=True, inPrison=False, hasFlag=None)
opponents = world.list_players(mine=False, inPrison=False, hasFlag=None) enemy_players_flags = world.list_players(mine=False, inPrison=False, hasFlag=True)
enemy_players = world.list_players(mine=False, inPrison=False, hasFlag=False)
enemy_flags = world.list_flags(mine=False, canPickup=True) enemy_flags = world.list_flags(mine=False, canPickup=True)
my_flags = world.list_flags(mine=True, canPickup=True) my_flags = world.list_flags(mine=True, canPickup=True)
my_targets = list(world.list_targets(mine=True)) my_targets = list(world.list_targets(mine=True))
active_player_names = {p["name"] for p in my_players if not p["hasFlag"]} active_player_names = {p["name"] for p in my_players if not p["hasFlag"]}
flags_list = [] flags_list = []
for flags in my_flags: regard_list = []
pretend_list = []
protect_list = []
for flags in enemy_flags:
flags_list.append((flags["posX"],flags["posY"])) flags_list.append((flags["posX"],flags["posY"]))
for p in enemy_players_flags:
regard_list.append((p["posX"],p["posY"]))
for p in enemy_players:
pretend_list.append((p["posX"],p["posY"]))
for flags in my_flags:
protect_list.append((flags["posX"],flags["posY"]))
player_to_flag_assign = { player_to_flag_assign = {
name: pos for name, pos in player_to_flag_assign.items() name: pos for name, pos in player_to_flag_assign.items()
if name in active_player_names if name in active_player_names
@@ -160,33 +179,32 @@ def plan_next_actions(req):
if p["name"] in player_to_flag_assign and player_to_flag_assign[p["name"]] in flags_list: if p["name"] in player_to_flag_assign and player_to_flag_assign[p["name"]] in flags_list:
flags_list.remove(player_to_flag_assign[p["name"]]) flags_list.remove(player_to_flag_assign[p["name"]])
continue continue
closest_flag = None f = myMap.closest(p["posX"],p["posY"],flags_list)
min_length = float('inf')
for f in flags_list:
temp = myMap.length(p["posX"],p["posY"],f[0],f[1])
if temp != -1 and temp < min_length:
min_length = temp
closest_flag = f
f = closest_flag
if f is not None: if f is not None:
player_to_flag_assign[p["name"]] = (f[0],f[1]) player_to_flag_assign[p["name"]] = (f[0],f[1])
print(f[0], f[1])
flags_list.remove((f[0],f[1])) flags_list.remove((f[0],f[1]))
# 3. Plan moves for each player
player_moves = {} player_moves = {}
for p in my_players: for p in my_players:
curr_pos = (p["posX"], p["posY"])
# Determine Target: Either the assigned flag or the home target
if p["hasFlag"]: if p["hasFlag"]:
dest = my_targets[0] dest = my_targets[0]
elif p["name"] in player_to_flag_assign: elif p["name"] in player_to_flag_assign:
dest = player_to_flag_assign[p["name"]] dest = player_to_flag_assign[p["name"]]
else: else:
continue f = myMap.closest(p["posX"],p["posY"],regard_list)
if f is not None:
dest = (f[0],f[1])
else:
f = myMap.closest(p["posX"],p["posY"],pretend_list)
if f is not None:
dest = (f[0],f[1])
else:
f= myMap.closest(p["posX"],p["posY"],protect_list)
if f is not None:
dest = (f[0],f[1])
else:
continue
player_moves[p["name"]] = myMap.guideance(p["posX"],p["posY"],dest[0],dest[1]) player_moves[p["name"]] = myMap.guideance(p["posX"],p["posY"],dest[0],dest[1])
return player_moves return player_moves
+7 -7
View File
@@ -1,16 +1,16 @@
{ {
"teams": [ "teams": [
{ "name": "L", "who": "user11-1"}, { "name": "L", "who": "user0-1"},
{ "name": "R", "who": "human"} { "name": "R", "who": "user0-2"}
], ],
"setup": { "setup": {
"numPlayers": 1, "numPlayers": 3,
"numFlags": 3, "numFlags": 9,
"useRandomFlags": false "useRandomFlags": true
}, },
"servers": { "servers": {
"user0-1": "ws://115.191.4.103:34568", "user0-1": "ws://0.0.0.0:11451",
"user0-2": "ws://115.191.4.103:34569", "user0-2": "ws://0.0.0.0:11452",
"user1-1": "ws://115.191.4.103:34571", "user1-1": "ws://115.191.4.103:34571",
"user1-2": "ws://115.191.4.103:34572", "user1-2": "ws://115.191.4.103:34572",
"user2-1": "ws://115.191.4.103:34574", "user2-1": "ws://115.191.4.103:34574",