Compare commits
10 Commits
8ef3f65ce9
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fc9f98aa3d | |||
| 6cae6a2ad8 | |||
| 033979c663 | |||
| 5e76bb2452 | |||
| bcca96e73f | |||
| e84aa30202 | |||
| 99fc0158b0 | |||
| 942bb1580d | |||
| 99ab005021 | |||
| b038e5ac29 |
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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`.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -1 +0,0 @@
|
||||
python3 pick_closets_flag.py ${CTF_PORT_BACKEND1}
|
||||
@@ -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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 675 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 646 B |
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 983 B |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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']
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
python3 -m http.server ${CTF_PORT_FRONTEND} --bind 0.0.0.0
|
||||
|
Before Width: | Height: | Size: 666 KiB |
@@ -1,103 +1,266 @@
|
||||
import asyncio
|
||||
import random
|
||||
from lib.game_engine import GameMap, run_game_server
|
||||
import threading
|
||||
import collections
|
||||
|
||||
|
||||
# 1. Initialize the global world model
|
||||
world = GameMap(show_gap_in_msec=1000.0)
|
||||
world = GameMap(show_gap_in_msec=1.0)
|
||||
lock = threading.Lock()
|
||||
last_updated_time = -1
|
||||
update_threshold = 100
|
||||
player_to_flag_assignments = {}
|
||||
update_threshold = 1
|
||||
player_to_flag_assign = {}
|
||||
my_side_is_left = None
|
||||
|
||||
class Map:
|
||||
def __init__(self):
|
||||
self.width = world.width
|
||||
self.height = world.height
|
||||
self.grid = [0] * (self.width * self.height)
|
||||
self.edge = [[] for _ in range(self.width * self.height)]
|
||||
self.in_safe_zone = None
|
||||
|
||||
def convert_pos_to_index(self, x, y):
|
||||
return y * self.width + x
|
||||
|
||||
def update(self,posx,posy):
|
||||
global my_side_is_left
|
||||
self.width = world.width
|
||||
self.height = world.height
|
||||
self.edge = [[] for _ in range(self.width * self.height)]
|
||||
self.grid = [0] * (self.width * self.height)
|
||||
walls = world.walls
|
||||
for wall in walls:
|
||||
x, y = wall
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
self.grid[idx] = 1
|
||||
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
|
||||
enemy_players = world.list_players(mine=False, inPrison=False, hasFlag=None)
|
||||
ally_players = world.list_players(mine=True, inPrison=False, hasFlag=None)
|
||||
my_pos = self.convert_pos_to_index(posx, posy)
|
||||
for ally in ally_players:
|
||||
x, y = ally["posX"], ally["posY"]
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
self.grid[idx] = 3
|
||||
for enemy in enemy_players:
|
||||
x, y = enemy["posX"], enemy["posY"]
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
if my_side_is_left:
|
||||
if not world.is_on_left((x+1,y)):
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
else:
|
||||
if world.is_on_left((x-1,y)):
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
if self.in_safe_zone:
|
||||
if (self.grid[idx] in (1,)) and idx != my_pos:
|
||||
continue
|
||||
else:
|
||||
if (self.grid[idx] in (1, 2)) and idx != my_pos:
|
||||
continue
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
n_idx = self.convert_pos_to_index(nx, ny)
|
||||
if self.in_safe_zone:
|
||||
if self.grid[n_idx] not in (1, ):
|
||||
self.edge[idx].append(n_idx)
|
||||
else:
|
||||
if self.grid[n_idx] not in (1, 2):
|
||||
self.edge[idx].append(n_idx)
|
||||
def guideance(self, posx_start, posy_start, posx_end, posy_end):
|
||||
self.update(posx_start,posy_start)
|
||||
src_idx = self.convert_pos_to_index(posx_start, posy_start)
|
||||
dst_idx = self.convert_pos_to_index(posx_end, posy_end)
|
||||
n = self.width * self.height
|
||||
dist = [float('inf')] * n
|
||||
prev = [None] * n
|
||||
dist[src_idx] = 0
|
||||
queue = collections.deque([src_idx])
|
||||
while queue:
|
||||
u = queue.popleft()
|
||||
if u == dst_idx:
|
||||
break
|
||||
for v in self.edge[u]:
|
||||
if dist[v] == float('inf'):
|
||||
dist[v] = dist[u] + 1
|
||||
prev[v] = u
|
||||
queue.append(v)
|
||||
if dist[dst_idx] == float('inf'):
|
||||
return ""
|
||||
path = []
|
||||
cur = dst_idx
|
||||
while cur is not None:
|
||||
path.append(cur)
|
||||
cur = prev[cur]
|
||||
path.reverse() # 现在是 [src, ..., dst]
|
||||
if len(path) < 2:
|
||||
return ""
|
||||
# ---- 计算第一步坐标并返回方向 ----
|
||||
next_idx = path[1]
|
||||
next_x = next_idx % (self.width)
|
||||
next_y = next_idx // (self.height)
|
||||
return world.get_direction((posx_start, posy_start), (next_x, next_y))
|
||||
def length(self, posx_start, posy_start, posx_end, posy_end):
|
||||
self.update(posx_start,posy_start)
|
||||
src_idx = self.convert_pos_to_index(posx_start, posy_start)
|
||||
dst_idx = self.convert_pos_to_index(posx_end, posy_end)
|
||||
n = self.width * self.height
|
||||
dist = [float('inf')] * n # 最短距离,初始为无穷大
|
||||
prev = [None] * n # 前驱节点,用于路径回溯
|
||||
dist[src_idx] = 0
|
||||
queue = collections.deque([src_idx])
|
||||
while queue:
|
||||
u = queue.popleft()
|
||||
if u == dst_idx:
|
||||
break
|
||||
for v in self.edge[u]:
|
||||
if dist[v] == float('inf'):
|
||||
dist[v] = dist[u] + 1
|
||||
prev[v] = u
|
||||
queue.append(v)
|
||||
|
||||
# ---- 若不可达,返回空字符串 ----
|
||||
if dist[dst_idx] == float('inf'):
|
||||
return -1
|
||||
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
|
||||
def closest_in_range(self, posx_start, posy_start, positions):
|
||||
closest_pos = None
|
||||
min_length = float('inf')
|
||||
for pos in positions:
|
||||
if my_side_is_left:
|
||||
if pos[0] >= self.width // 2:
|
||||
continue
|
||||
else:
|
||||
if pos[0] < self.width // 2:
|
||||
continue
|
||||
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,min_length
|
||||
myMap = Map()
|
||||
def start_game(req):
|
||||
global player_to_flag_assignments
|
||||
global player_to_flag_assign,my_side_is_left
|
||||
world.init(req)
|
||||
print("Start Game!!")
|
||||
player_to_flag_assignments = {}
|
||||
player_to_flag_assign = {}
|
||||
print(f"Game Started! Side: {'Left' if world.is_on_left(list(world.my_team_target)[0]) else 'Right'}")
|
||||
my_side_is_left = world.is_on_left(list(world.my_team_target)[0])
|
||||
|
||||
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)
|
||||
global player_to_flag_assign,myMap,my_side_is_left
|
||||
|
||||
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)
|
||||
my_flags = world.list_flags(mine=True, 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()
|
||||
flags_list = []
|
||||
regard_list = []
|
||||
pretend_list = []
|
||||
protect_list = []
|
||||
targets_list = []
|
||||
for flags in enemy_flags:
|
||||
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"]))
|
||||
for t in my_targets:
|
||||
targets_list.append((t[0],t[1]))
|
||||
player_to_flag_assign = {
|
||||
name: pos for name, pos in player_to_flag_assign.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"])
|
||||
if p["name"] not in active_player_names:
|
||||
continue
|
||||
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"]])
|
||||
continue
|
||||
f = myMap.closest(p["posX"],p["posY"],flags_list)
|
||||
if f is not None:
|
||||
player_to_flag_assign[p["name"]] = (f[0],f[1])
|
||||
flags_list.remove((f[0],f[1]))
|
||||
|
||||
# 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"]]
|
||||
dest,temp = myMap.closest_in_range(p["posX"],p["posY"],my_targets)
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],regard_list)
|
||||
if f is not None and lentime<6:
|
||||
dest = (f[0],f[1])
|
||||
elif p["name"] in player_to_flag_assign:
|
||||
dest = player_to_flag_assign[p["name"]]
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],regard_list)
|
||||
if f is not None and lentime<12:
|
||||
dest = (f[0],f[1])
|
||||
else :
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],pretend_list)
|
||||
if f is not None and lentime<6:
|
||||
dest = (f[0],f[1])
|
||||
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
|
||||
|
||||
f,temp = myMap.closest_in_range(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
|
||||
print(f"Player {p['name']} at ({p['posX']},{p['posY']}) moving towards ({dest[0]},{dest[1]})")
|
||||
player_moves[p["name"]] = myMap.guideance(p["posX"],p["posY"],dest[0],dest[1])
|
||||
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())
|
||||
asyncio.run(main())
|
||||
@@ -1,16 +1,17 @@
|
||||
import asyncio
|
||||
import random
|
||||
from lib.game_engine import GameMap, run_game_server
|
||||
import threading
|
||||
import collections
|
||||
import server0
|
||||
import server1
|
||||
|
||||
# 1. Initialize the global world model
|
||||
world = GameMap(show_gap_in_msec=10.0)
|
||||
world = GameMap(show_gap_in_msec=1.0)
|
||||
lock = threading.Lock()
|
||||
last_updated_time = -1
|
||||
update_threshold = 10
|
||||
update_threshold = 1
|
||||
player_to_flag_assign = {}
|
||||
my_side_is_left = None
|
||||
|
||||
class Map:
|
||||
def __init__(self):
|
||||
self.width = world.width
|
||||
@@ -23,6 +24,7 @@ class Map:
|
||||
return y * self.width + x
|
||||
|
||||
def update(self,posx,posy):
|
||||
global my_side_is_left
|
||||
self.width = world.width
|
||||
self.height = world.height
|
||||
self.edge = [[] for _ in range(self.width * self.height)]
|
||||
@@ -32,7 +34,10 @@ class Map:
|
||||
x, y = wall
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
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((min(posx+2,self.width),posy)) == my_side_is_left
|
||||
else:
|
||||
self.in_safe_zone = world.is_on_left((max(posx-2,0),posy)) == my_side_is_left
|
||||
enemy_players = world.list_players(mine=False, inPrison=False, hasFlag=None)
|
||||
ally_players = world.list_players(mine=True, inPrison=False, hasFlag=None)
|
||||
my_pos = self.convert_pos_to_index(posx, posy)
|
||||
@@ -43,10 +48,18 @@ class Map:
|
||||
for enemy in enemy_players:
|
||||
x, y = enemy["posX"], enemy["posY"]
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
if my_side_is_left:
|
||||
if not world.is_on_left((x+1,y)):
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
else:
|
||||
if world.is_on_left((x-1,y)):
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
@@ -61,10 +74,10 @@ class Map:
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
n_idx = self.convert_pos_to_index(nx, ny)
|
||||
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)
|
||||
else:
|
||||
if self.grid[n_idx] not in (1,):
|
||||
if self.grid[n_idx] not in (1, 2):
|
||||
self.edge[idx].append(n_idx)
|
||||
def guideance(self, posx_start, posy_start, posx_end, posy_end):
|
||||
self.update(posx_start,posy_start)
|
||||
@@ -84,11 +97,8 @@ class Map:
|
||||
dist[v] = dist[u] + 1
|
||||
prev[v] = u
|
||||
queue.append(v)
|
||||
|
||||
# ---- 若不可达,返回空字符串 ----
|
||||
if dist[dst_idx] == float('inf'):
|
||||
return ""
|
||||
# ---- 重建路径(逆序) ----
|
||||
path = []
|
||||
cur = dst_idx
|
||||
while cur is not None:
|
||||
@@ -125,31 +135,106 @@ class Map:
|
||||
if dist[dst_idx] == float('inf'):
|
||||
return -1
|
||||
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
|
||||
def closest_in_range(self, posx_start, posy_start, positions):
|
||||
closest_pos = None
|
||||
min_length = float('inf')
|
||||
for pos in positions:
|
||||
if my_side_is_left:
|
||||
if pos[0] >= self.width // 2:
|
||||
continue
|
||||
else:
|
||||
if pos[0] < self.width // 2:
|
||||
continue
|
||||
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,min_length
|
||||
def enemy_to_walls(self, enemy_posX,enemy_posY):
|
||||
idx = self.convert_pos_to_index(enemy_posX, enemy_posY)
|
||||
lengths = abs(enemy_posX-self.width//2)
|
||||
return lengths
|
||||
def choose_enemy(self, my_posX, my_posY,enemy_positions):
|
||||
chosen_enemy = None
|
||||
min_length = -1
|
||||
for pos in enemy_positions:
|
||||
if my_side_is_left:
|
||||
if pos[0] >= self.width // 2:
|
||||
continue
|
||||
else:
|
||||
if pos[0] < self.width // 2:
|
||||
continue
|
||||
temp = self.enemy_to_walls(pos[0], pos[1])
|
||||
length = self.length(my_posX, my_posY, pos[0], pos[1])
|
||||
if length > temp*2+4 : continue
|
||||
if temp < length:
|
||||
length = temp
|
||||
chosen_enemy = pos
|
||||
return chosen_enemy,min_length
|
||||
myMap = Map()
|
||||
rounds = 0
|
||||
def start_game(req):
|
||||
global player_to_flag_assign,my_side_is_left
|
||||
server1.start_game(req)
|
||||
server0.start_game(req)
|
||||
world.init(req)
|
||||
print("Start Game!!")
|
||||
player_to_flag_assign = {}
|
||||
print(f"Game Started! Side: {'Left' if world.is_on_left(list(world.my_team_target)[0]) else 'Right'}")
|
||||
my_side_is_left = world.is_on_left(list(world.my_team_target)[0])
|
||||
global rounds
|
||||
rounds = 0
|
||||
|
||||
|
||||
def plan_next_actions(req):
|
||||
global rounds
|
||||
rounds+=1
|
||||
# if rounds < 50:
|
||||
# return server0.plan_next_actions(req)
|
||||
# else:
|
||||
# return server1.plan_next_actions(req)
|
||||
# return server0.plan_next_actions(req)
|
||||
if not world.update(req):
|
||||
return
|
||||
global player_to_flag_assign,myMap,my_side_is_left
|
||||
|
||||
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)
|
||||
|
||||
my_flags = world.list_flags(mine=True, canPickup=True)
|
||||
my_targets = list(world.list_targets(mine=True))
|
||||
|
||||
|
||||
my_in_prison = len(world.list_players(mine=True, inPrison=True, hasFlag=None))
|
||||
op_in_prison = len(world.list_players(False,True,None))
|
||||
if my_in_prison <= 0 and op_in_prison <= 0:
|
||||
return server0.plan_next_actions(req)
|
||||
elif my_in_prison > op_in_prison:
|
||||
return server1.plan_next_actions(req)
|
||||
else:
|
||||
return server0.plan_next_actions(req)
|
||||
|
||||
|
||||
active_player_names = {p["name"] for p in my_players if not p["hasFlag"]}
|
||||
flags_list = []
|
||||
for flags in opponents:
|
||||
regard_list = []
|
||||
pretend_list = []
|
||||
protect_list = []
|
||||
targets_list = []
|
||||
for flags in enemy_flags:
|
||||
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"]))
|
||||
for t in my_targets:
|
||||
targets_list.append((t[0],t[1]))
|
||||
player_to_flag_assign = {
|
||||
name: pos for name, pos in player_to_flag_assign.items()
|
||||
if name in active_player_names
|
||||
@@ -158,42 +243,55 @@ def plan_next_actions(req):
|
||||
for p in my_players:
|
||||
if p["name"] not in active_player_names:
|
||||
continue
|
||||
if p["name"] in player_to_flag_assign and player_to_flag_assign[p["name"]] != None 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"]])
|
||||
continue
|
||||
closest_flag = None
|
||||
min_length = float('inf')
|
||||
for f in enemy_flags:
|
||||
temp = myMap.length(p["posX"],p["posY"],f["posX"],f["posY"])
|
||||
if temp != -1 and temp < min_length:
|
||||
min_length = temp
|
||||
closest_flag = f
|
||||
f = closest_flag if closest_flag is not None else random.choice(enemy_flags)
|
||||
player_to_flag_assign[p["name"]] = (f["posX"], f["posY"])
|
||||
|
||||
# 3. Plan moves for each player
|
||||
f = myMap.closest(p["posX"],p["posY"],flags_list)
|
||||
if f is not None:
|
||||
player_to_flag_assign[p["name"]] = (f[0],f[1])
|
||||
flags_list.remove((f[0],f[1]))
|
||||
player_moves = {}
|
||||
|
||||
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]
|
||||
dest = myMap.closest(p["posX"],p["posY"],my_targets)
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],regard_list)
|
||||
if f is not None and lentime<4:
|
||||
dest = (f[0],f[1])
|
||||
elif p["name"] in player_to_flag_assign:
|
||||
dest = player_to_flag_assign[p["name"]]
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],regard_list)
|
||||
if f is not None and lentime<14:
|
||||
dest = (f[0],f[1])
|
||||
else :
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],pretend_list)
|
||||
if f is not None and lentime<8:
|
||||
dest = (f[0],f[1])
|
||||
pretend_list.remove((f[0],f[1]))
|
||||
else:
|
||||
continue
|
||||
|
||||
# Determine Obstacles: Avoid opponents if we are in enemy territory
|
||||
is_safe = world.is_on_left(curr_pos) == my_side_is_left
|
||||
|
||||
f,temp = myMap.closest_in_range(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])
|
||||
pretend_list.remove((f[0],f[1]))
|
||||
else:
|
||||
f= myMap.closest(p["posX"],p["posY"],protect_list)
|
||||
if f is not None:
|
||||
dest = (f[0],f[1])
|
||||
protect_list.remove((f[0],f[1]))
|
||||
else:
|
||||
continue
|
||||
player_moves[p["name"]] = myMap.guideance(p["posX"],p["posY"],dest[0],dest[1])
|
||||
|
||||
return player_moves
|
||||
|
||||
def game_over(req):
|
||||
print("Game Over!")
|
||||
world.show(force=True)
|
||||
server1.game_over(req)
|
||||
server0.game_over(req)
|
||||
# print("Game Over!")
|
||||
# world.show(force=True)
|
||||
|
||||
async def main():
|
||||
import sys
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import asyncio
|
||||
import random
|
||||
from lib.game_engine import GameMap, run_game_server
|
||||
import threading
|
||||
import collections
|
||||
|
||||
# 1. Initialize the global world model
|
||||
world = GameMap(show_gap_in_msec=10.0)
|
||||
world = GameMap(show_gap_in_msec=1.0)
|
||||
lock = threading.Lock()
|
||||
last_updated_time = -1
|
||||
update_threshold = 10
|
||||
update_threshold = 1
|
||||
player_to_flag_assign = {}
|
||||
my_side_is_left = None
|
||||
|
||||
class Map:
|
||||
def __init__(self):
|
||||
self.width = world.width
|
||||
@@ -23,6 +22,7 @@ class Map:
|
||||
return y * self.width + x
|
||||
|
||||
def update(self,posx,posy):
|
||||
global my_side_is_left
|
||||
self.width = world.width
|
||||
self.height = world.height
|
||||
self.edge = [[] for _ in range(self.width * self.height)]
|
||||
@@ -32,7 +32,10 @@ class Map:
|
||||
x, y = wall
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
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((min(posx+2,self.width),posy)) == my_side_is_left
|
||||
else:
|
||||
self.in_safe_zone = world.is_on_left((max(posx-2,0),posy)) == my_side_is_left
|
||||
enemy_players = world.list_players(mine=False, inPrison=False, hasFlag=None)
|
||||
ally_players = world.list_players(mine=True, inPrison=False, hasFlag=None)
|
||||
my_pos = self.convert_pos_to_index(posx, posy)
|
||||
@@ -43,10 +46,18 @@ class Map:
|
||||
for enemy in enemy_players:
|
||||
x, y = enemy["posX"], enemy["posY"]
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
if my_side_is_left:
|
||||
if not world.is_on_left((x+1,y)):
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
else:
|
||||
if world.is_on_left((x-1,y)):
|
||||
self.grid[idx] = 2
|
||||
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
self.grid[self.convert_pos_to_index(nx,ny)] = 2
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
idx = self.convert_pos_to_index(x, y)
|
||||
@@ -61,10 +72,10 @@ class Map:
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
n_idx = self.convert_pos_to_index(nx, ny)
|
||||
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)
|
||||
else:
|
||||
if self.grid[n_idx] not in (1,):
|
||||
if self.grid[n_idx] not in (1, 2):
|
||||
self.edge[idx].append(n_idx)
|
||||
def guideance(self, posx_start, posy_start, posx_end, posy_end):
|
||||
self.update(posx_start,posy_start)
|
||||
@@ -84,11 +95,8 @@ class Map:
|
||||
dist[v] = dist[u] + 1
|
||||
prev[v] = u
|
||||
queue.append(v)
|
||||
|
||||
# ---- 若不可达,返回空字符串 ----
|
||||
if dist[dst_idx] == float('inf'):
|
||||
return ""
|
||||
# ---- 重建路径(逆序) ----
|
||||
path = []
|
||||
cur = dst_idx
|
||||
while cur is not None:
|
||||
@@ -125,7 +133,51 @@ class Map:
|
||||
if dist[dst_idx] == float('inf'):
|
||||
return -1
|
||||
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
|
||||
def closest_in_range(self, posx_start, posy_start, positions):
|
||||
closest_pos = None
|
||||
min_length = float('inf')
|
||||
for pos in positions:
|
||||
if my_side_is_left:
|
||||
if pos[0] >= self.width // 2:
|
||||
continue
|
||||
else:
|
||||
if pos[0] < self.width // 2:
|
||||
continue
|
||||
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,min_length
|
||||
def enemy_to_walls(self, enemy_posX,enemy_posY):
|
||||
idx = self.convert_pos_to_index(enemy_posX, enemy_posY)
|
||||
lengths = abs(enemy_posX-self.width//2)
|
||||
return lengths
|
||||
def choose_enemy(self, my_posX, my_posY,enemy_positions):
|
||||
chosen_enemy = None
|
||||
min_length = -1
|
||||
for pos in enemy_positions:
|
||||
if my_side_is_left:
|
||||
if pos[0] >= self.width // 2:
|
||||
continue
|
||||
else:
|
||||
if pos[0] < self.width // 2:
|
||||
continue
|
||||
temp = self.enemy_to_walls(pos[0], pos[1])
|
||||
length = self.length(my_posX, my_posY, pos[0], pos[1])
|
||||
if length > temp*2+4 : continue
|
||||
if temp < length:
|
||||
length = temp
|
||||
chosen_enemy = pos
|
||||
return chosen_enemy,min_length
|
||||
myMap = Map()
|
||||
def start_game(req):
|
||||
global player_to_flag_assign,my_side_is_left
|
||||
@@ -141,14 +193,27 @@ def plan_next_actions(req):
|
||||
global player_to_flag_assign,myMap,my_side_is_left
|
||||
|
||||
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)
|
||||
my_flags = world.list_flags(mine=True, canPickup=True)
|
||||
my_targets = list(world.list_targets(mine=True))
|
||||
active_player_names = {p["name"] for p in my_players if not p["hasFlag"]}
|
||||
flags_list = []
|
||||
for flags in my_flags:
|
||||
regard_list = []
|
||||
pretend_list = []
|
||||
protect_list = []
|
||||
targets_list = []
|
||||
for flags in enemy_flags:
|
||||
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"]))
|
||||
for t in my_targets:
|
||||
targets_list.append((t[0],t[1]))
|
||||
player_to_flag_assign = {
|
||||
name: pos for name, pos in player_to_flag_assign.items()
|
||||
if name in active_player_names
|
||||
@@ -160,35 +225,50 @@ def plan_next_actions(req):
|
||||
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"]])
|
||||
continue
|
||||
closest_flag = None
|
||||
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
|
||||
f = myMap.closest(p["posX"],p["posY"],flags_list)
|
||||
if f is not None:
|
||||
player_to_flag_assign[p["name"]] = (f[0],f[1])
|
||||
print(f[0], f[1])
|
||||
flags_list.remove((f[0],f[1]))
|
||||
|
||||
# 3. Plan moves for each player
|
||||
player_moves = {}
|
||||
|
||||
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]
|
||||
dest = myMap.closest(p["posX"],p["posY"],my_targets)
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],regard_list)
|
||||
if f is not None and lentime<4:
|
||||
dest = (f[0],f[1])
|
||||
elif p["name"] in player_to_flag_assign:
|
||||
dest = player_to_flag_assign[p["name"]]
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],regard_list)
|
||||
if f is not None and lentime<14:
|
||||
dest = (f[0],f[1])
|
||||
regard_list.remove((f[0],f[1]))
|
||||
else :
|
||||
f,lentime = myMap.closest_in_range(p["posX"],p["posY"],pretend_list)
|
||||
if f is not None and lentime<8:
|
||||
dest = (f[0],f[1])
|
||||
pretend_list.remove((f[0],f[1]))
|
||||
else:
|
||||
continue
|
||||
|
||||
player_moves[p["name"]] = myMap.guideance(p["posX"],p["posY"],dest[0],dest[1])
|
||||
|
||||
f,temp = myMap.closest_in_range(p["posX"],p["posY"],regard_list)
|
||||
if f is not None:
|
||||
dest = (f[0],f[1])
|
||||
regard_list.remove((f[0],f[1]))
|
||||
else:
|
||||
f = myMap.closest(p["posX"],p["posY"],pretend_list)
|
||||
if f is not None:
|
||||
dest = (f[0],f[1])
|
||||
pretend_list.remove((f[0],f[1]))
|
||||
else:
|
||||
f= myMap.closest(p["posX"],p["posY"],protect_list)
|
||||
if f is not None:
|
||||
dest = (f[0],f[1])
|
||||
protect_list.remove((f[0],f[1]))
|
||||
else:
|
||||
continue
|
||||
if dest is not None:
|
||||
player_moves[p["name"]] = myMap.guideance(p["posX"],p["posY"],dest[0],dest[1])
|
||||
else:
|
||||
player_moves[p["name"]] = myMap.guideance(p["posX"],p["posY"],p["posX"],p["posY"])
|
||||
return player_moves
|
||||
|
||||
def game_over(req):
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for Map class functions: enemy_to_walls and choose_enemy.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from server import Map, world
|
||||
from lib.game_engine import GameMap
|
||||
|
||||
def test_enemy_to_walls():
|
||||
"""Test enemy_to_walls function."""
|
||||
# Mock world with walls
|
||||
world.walls = {(0, 0), (5, 5), (10, 10)}
|
||||
world.width = 20
|
||||
world.height = 20
|
||||
|
||||
my_map = Map()
|
||||
|
||||
# Test case 1: enemy at (0, 0), nearest wall is itself
|
||||
assert my_map.enemy_to_walls(0, 0) == 0
|
||||
|
||||
# Test case 2: enemy at (1, 1), distance to (0,0) is 2
|
||||
assert my_map.enemy_to_walls(1, 1) == 2
|
||||
|
||||
# Test case 3: enemy at (6, 6), distance to (5,5) is 2
|
||||
assert my_map.enemy_to_walls(6, 6) == 2
|
||||
|
||||
# Test case 4: enemy at (15, 15), distance to (10,10) is 10
|
||||
assert my_map.enemy_to_walls(15, 15) == 10
|
||||
|
||||
print("test_enemy_to_walls passed!")
|
||||
|
||||
def test_choose_enemy():
|
||||
"""Test choose_enemy function logic without full map."""
|
||||
# Mock world
|
||||
world.walls = {(0, 0), (19, 19)}
|
||||
world.width = 20
|
||||
world.height = 20
|
||||
|
||||
my_map = Map()
|
||||
my_map.width = 20
|
||||
my_map.height = 20
|
||||
|
||||
# Mock my_side_is_left
|
||||
global my_side_is_left
|
||||
my_side_is_left = True # Left side
|
||||
|
||||
enemy_positions = [(5, 5)] # Only one enemy to avoid complexity
|
||||
|
||||
# Since length function is complex, we'll just check that the function runs without error
|
||||
# and returns the expected type
|
||||
result = my_map.choose_enemy(2, 2, enemy_positions)
|
||||
assert isinstance(result, tuple), "Should return a tuple"
|
||||
assert len(result) == 2, "Tuple should have 2 elements"
|
||||
print(f"choose_enemy returned: {result}")
|
||||
|
||||
print("test_choose_enemy passed!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_enemy_to_walls()
|
||||
test_choose_enemy()
|
||||
print("All tests passed!")
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"teams": [
|
||||
{ "name": "L", "who": "user11-1"},
|
||||
{ "name": "R", "who": "human"}
|
||||
{ "name": "L", "who": "user0-1"},
|
||||
{ "name": "R", "who": "user0-2"}
|
||||
],
|
||||
"setup": {
|
||||
"numPlayers": 1,
|
||||
"numFlags": 3,
|
||||
"useRandomFlags": false
|
||||
"numPlayers": 3,
|
||||
"numFlags": 9,
|
||||
"useRandomFlags": true
|
||||
},
|
||||
"servers": {
|
||||
"user0-1": "ws://115.191.4.103:34568",
|
||||
"user0-2": "ws://115.191.4.103:34569",
|
||||
"user0-1": "ws://0.0.0.0:11451",
|
||||
"user0-2": "ws://0.0.0.0:11452",
|
||||
"user1-1": "ws://115.191.4.103:34571",
|
||||
"user1-2": "ws://115.191.4.103:34572",
|
||||
"user2-1": "ws://115.191.4.103:34574",
|
||||
|
||||