diff --git a/app-src/.gitignore b/app-src/.gitignore new file mode 100644 index 0000000..8c4e233 --- /dev/null +++ b/app-src/.gitignore @@ -0,0 +1,4 @@ +Cargo.lock +target/ +.idea/ +*.iml \ No newline at end of file diff --git a/app-src/Cargo.toml b/app-src/Cargo.toml new file mode 100644 index 0000000..3c1bfd8 --- /dev/null +++ b/app-src/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "aicup2019" +version = "1.3.0" +authors = ["kuviman "] +edition = "2018" +default-run = "aicup2019" + +[features] +default = ["rendering"] +rendering = ["codegame/rendering"] + +[dependencies] +codegame = { git = "https://github.com/codeforces/codegame", default-features = false } +serde = "1" +trans-gen = { git = "https://github.com/kuviman/trans-gen" } +structopt = "0.3" + +[dev-dependencies] +tempfile = "3" \ No newline at end of file diff --git a/app-src/README.md b/app-src/README.md new file mode 100644 index 0000000..c04ed77 --- /dev/null +++ b/app-src/README.md @@ -0,0 +1,19 @@ +To build, you'll need to install [Rust](https://rustup.rs/). + +To run native build: + +```shell +cargo run --release +``` + +To build web version, first install [`cargo-web`](https://github.com/koute/cargo-web): + +```shell +cargo install cargo-web +``` + +Then build, start local server and open browser: + +```shell +cargo web start --release --open +``` \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/CMakeLists.txt b/app-src/src/bin/client_gen/cpp/CMakeLists.txt new file mode 100644 index 0000000..e2a609b --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.5) +project(aicup2019) + +# OS and compiler checks. +if(WIN32) + add_definitions(-DWIN32) + SET(PROJECT_LIBS Ws2_32.lib) +endif() + +file(GLOB HEADERS "*.hpp" "model/*.hpp" "csimplesocket/*.h") +SET_SOURCE_FILES_PROPERTIES(${HEADERS} PROPERTIES HEADER_FILE_ONLY TRUE) +file(GLOB SRC "*.cpp" "model/*.cpp" "csimplesocket/*.cpp") +add_executable(aicup2019 ${HEADERS} ${SRC}) +TARGET_LINK_LIBRARIES(aicup2019 ${PROJECT_LIBS}) \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/Debug.cpp b/app-src/src/bin/client_gen/cpp/Debug.cpp new file mode 100644 index 0000000..a00f0e5 --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/Debug.cpp @@ -0,0 +1,11 @@ +#include "Debug.hpp" +#include "model/PlayerMessageGame.hpp" + +Debug::Debug(const std::shared_ptr &outputStream) + : outputStream(outputStream) {} + +void Debug::draw(const CustomData &customData) { + outputStream->write(PlayerMessageGame::CustomDataMessage::TAG); + customData.writeTo(*outputStream); + outputStream->flush(); +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/Debug.hpp b/app-src/src/bin/client_gen/cpp/Debug.hpp new file mode 100644 index 0000000..c5ef180 --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/Debug.hpp @@ -0,0 +1,17 @@ +#ifndef _DEBUG_HPP_ +#define _DEBUG_HPP_ + +#include "Stream.hpp" +#include "model/CustomData.hpp" +#include + +class Debug { +public: + Debug(const std::shared_ptr &outputStream); + void draw(const CustomData &customData); + +private: + std::shared_ptr outputStream; +}; + +#endif \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/Dockerfile b/app-src/src/bin/client_gen/cpp/Dockerfile new file mode 100644 index 0000000..88fe725 --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/Dockerfile @@ -0,0 +1,6 @@ +FROM gcc + +RUN apt-get update && apt-get install -y cmake + +COPY . /project +WORKDIR /project \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/MyStrategy.cpp b/app-src/src/bin/client_gen/cpp/MyStrategy.cpp new file mode 100644 index 0000000..205e98f --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/MyStrategy.cpp @@ -0,0 +1,65 @@ +#include "MyStrategy.hpp" + +MyStrategy::MyStrategy() {} + +double distanceSqr(Vec2Double a, Vec2Double b) { + return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); +} + +UnitAction MyStrategy::getAction(const Unit &unit, const Game &game, + Debug &debug) { + const Unit *nearestEnemy = nullptr; + for (const Unit &other : game.units) { + if (other.playerId != unit.playerId) { + if (nearestEnemy == nullptr || + distanceSqr(unit.position, other.position) < + distanceSqr(unit.position, nearestEnemy->position)) { + nearestEnemy = &other; + } + } + } + const LootBox *nearestWeapon = nullptr; + for (const LootBox &lootBox : game.lootBoxes) { + if (std::dynamic_pointer_cast(lootBox.item)) { + if (nearestWeapon == nullptr || + distanceSqr(unit.position, lootBox.position) < + distanceSqr(unit.position, nearestWeapon->position)) { + nearestWeapon = &lootBox; + } + } + } + Vec2Double targetPos = unit.position; + if (unit.weapon == nullptr && nearestWeapon != nullptr) { + targetPos = nearestWeapon->position; + } else if (nearestEnemy != nullptr) { + targetPos = nearestEnemy->position; + } + debug.draw(CustomData::Log( + std::string("Target pos: ") + targetPos.toString())); + Vec2Double aim = Vec2Double(0, 0); + if (nearestEnemy != nullptr) { + aim = Vec2Double(nearestEnemy->position.x - unit.position.x, + nearestEnemy->position.y - unit.position.y); + } + bool jump = targetPos.y > unit.position.y; + if (targetPos.x > unit.position.x && + game.level.tiles[size_t(unit.position.x + 1)][size_t(unit.position.y)] == + Tile::WALL) { + jump = true; + } + if (targetPos.x < unit.position.x && + game.level.tiles[size_t(unit.position.x - 1)][size_t(unit.position.y)] == + Tile::WALL) { + jump = true; + } + UnitAction action; + action.velocity = targetPos.x - unit.position.x; + action.jump = jump; + action.jumpDown = !action.jump; + action.aim = aim; + action.shoot = true; + action.reload = false; + action.swapWeapon = false; + action.plantMine = false; + return action; +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/MyStrategy.hpp b/app-src/src/bin/client_gen/cpp/MyStrategy.hpp new file mode 100644 index 0000000..05085ab --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/MyStrategy.hpp @@ -0,0 +1,16 @@ +#ifndef _MY_STRATEGY_HPP_ +#define _MY_STRATEGY_HPP_ + +#include "Debug.hpp" +#include "model/CustomData.hpp" +#include "model/Game.hpp" +#include "model/Unit.hpp" +#include "model/UnitAction.hpp" + +class MyStrategy { +public: + MyStrategy(); + UnitAction getAction(const Unit &unit, const Game &game, Debug &debug); +}; + +#endif \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/TcpStream.cpp b/app-src/src/bin/client_gen/cpp/TcpStream.cpp new file mode 100644 index 0000000..4d5288a --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/TcpStream.cpp @@ -0,0 +1,131 @@ +#include "TcpStream.hpp" +#include +#include +#include +#include + +#ifdef _WIN32 +typedef int RECV_SEND_T; +#else +typedef ssize_t RECV_SEND_T; +#endif + +TcpStream::TcpStream(const std::string &host, int port) { +#ifdef _WIN32 + WSADATA wsa_data; + if (WSAStartup(MAKEWORD(1, 1), &wsa_data) != 0) { + throw std::runtime_error("Failed to initialize sockets"); + } +#endif + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == -1) { + throw std::runtime_error("Failed to create socket"); + } + int yes = 1; + if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&yes, sizeof(int)) < + 0) { + throw std::runtime_error("Failed to set TCP_NODELAY"); + } + addrinfo hints, *servinfo; + std::memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + if (getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, + &servinfo) != 0) { + throw std::runtime_error("Failed to get addr info"); + } + if (connect(sock, servinfo->ai_addr, servinfo->ai_addrlen) == -1) { + throw std::runtime_error("Failed to connect"); + } + freeaddrinfo(servinfo); +} + +class TcpInputStream : public InputStream { +public: + TcpInputStream(std::shared_ptr tcpStream) + : tcpStream(tcpStream), bufferPos(0), bufferSize(0) {} + void readBytes(char *buffer, size_t byteCount) { + while (byteCount > 0) { + if (bufferSize > 0) { + if (bufferSize >= byteCount) { + memcpy(buffer, this->buffer + bufferPos, byteCount); + bufferPos += byteCount; + bufferSize -= byteCount; + return; + } + memcpy(buffer, this->buffer + bufferPos, bufferSize); + buffer += bufferSize; + byteCount -= bufferSize; + bufferPos += bufferSize; + bufferSize = 0; + } + if (bufferPos == BUFFER_CAPACITY) { + bufferPos = 0; + } + RECV_SEND_T received = + recv(tcpStream->sock, this->buffer + bufferPos + bufferSize, + BUFFER_CAPACITY - bufferPos - bufferSize, 0); + if (received < 0) { + throw std::runtime_error("Failed to read from socket"); + } + bufferSize += received; + } + } + +private: + static const size_t BUFFER_CAPACITY = 8 * 1024; + char buffer[BUFFER_CAPACITY]; + size_t bufferPos; + size_t bufferSize; + std::shared_ptr tcpStream; +}; + +class TcpOutputStream : public OutputStream { +public: + TcpOutputStream(std::shared_ptr tcpStream) + : tcpStream(tcpStream), bufferPos(0), bufferSize(0) {} + void writeBytes(const char *buffer, size_t byteCount) { + while (byteCount > 0) { + size_t capacity = BUFFER_CAPACITY - bufferPos - bufferSize; + if (capacity >= byteCount) { + memcpy(this->buffer + bufferPos + bufferSize, buffer, byteCount); + bufferSize += byteCount; + return; + } + memcpy(this->buffer + bufferPos + bufferSize, buffer, capacity); + bufferSize += capacity; + byteCount -= capacity; + buffer += capacity; + flush(); + } + } + void flush() { + while (bufferSize > 0) { + RECV_SEND_T sent = + send(tcpStream->sock, buffer + bufferPos, bufferSize, 0); + if (sent < 0) { + throw std::runtime_error("Failed to write to socket"); + } + bufferPos += sent; + bufferSize -= sent; + } + bufferPos = 0; + } + +private: + static const size_t BUFFER_CAPACITY = 8 * 1024; + char buffer[BUFFER_CAPACITY]; + size_t bufferPos; + size_t bufferSize; + std::shared_ptr tcpStream; +}; + +std::shared_ptr +getInputStream(std::shared_ptr tcpStream) { + return std::shared_ptr(new TcpInputStream(tcpStream)); +} + +std::shared_ptr +getOutputStream(std::shared_ptr tcpStream) { + return std::shared_ptr(new TcpOutputStream(tcpStream)); +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/TcpStream.hpp b/app-src/src/bin/client_gen/cpp/TcpStream.hpp new file mode 100644 index 0000000..eb51719 --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/TcpStream.hpp @@ -0,0 +1,35 @@ +#ifndef _TCP_STREAM_HPP_ +#define _TCP_STREAM_HPP_ + +#ifdef _WIN32 +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0501 +#endif +#include +#include +#else +#include +#include +#include +#include +#include +typedef int SOCKET; +#endif + +#include "Stream.hpp" +#include +#include + +class TcpStream { +public: + TcpStream(const std::string &host, int port); + SOCKET sock; +}; + +std::shared_ptr +getInputStream(std::shared_ptr tcpStream); + +std::shared_ptr +getOutputStream(std::shared_ptr tcpStream); + +#endif \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/compile.sh b/app-src/src/bin/client_gen/cpp/compile.sh new file mode 100644 index 0000000..9428988 --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/compile.sh @@ -0,0 +1,14 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/MyStrategy.cpp MyStrategy.cpp + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +cmake -DCMAKE_CXX_STANDARD=17 -DCMAKE_BUILD_TYPE=RELEASE -DCMAKE_VERBOSE_MAKEFILE=ON . +cmake --build . --config Release +cp aicup2019 /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/main.cpp b/app-src/src/bin/client_gen/cpp/main.cpp new file mode 100644 index 0000000..a067297 --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/main.cpp @@ -0,0 +1,52 @@ +#include "Debug.hpp" +#include "MyStrategy.hpp" +#include "TcpStream.hpp" +#include "model/PlayerMessageGame.hpp" +#include "model/ServerMessageGame.hpp" +#include +#include +#include + +class Runner { +public: + Runner(const std::string &host, int port, const std::string &token) { + std::shared_ptr tcpStream(new TcpStream(host, port)); + inputStream = getInputStream(tcpStream); + outputStream = getOutputStream(tcpStream); + outputStream->write(token); + outputStream->flush(); + } + void run() { + MyStrategy myStrategy; + Debug debug(outputStream); + while (true) { + auto message = ServerMessageGame::readFrom(*inputStream); + const auto& playerView = message.playerView; + if (!playerView) { + break; + } + std::unordered_map actions; + for (const Unit &unit : playerView->game.units) { + if (unit.playerId == playerView->myId) { + actions.emplace(std::make_pair( + unit.id, + myStrategy.getAction(unit, playerView->game, debug))); + } + } + PlayerMessageGame::ActionMessage(Versioned(actions)).writeTo(*outputStream); + outputStream->flush(); + } + } + +private: + std::shared_ptr inputStream; + std::shared_ptr outputStream; +}; + +int main(int argc, char *argv[]) { + std::string host = argc < 2 ? "127.0.0.1" : argv[1]; + int port = argc < 3 ? 31001 : atoi(argv[2]); + std::string token = argc < 4 ? "0000000000000000" : argv[3]; + Runner(host, port, token).run(); + return 0; +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/cpp/run.sh b/app-src/src/bin/client_gen/cpp/run.sh new file mode 100644 index 0000000..e94c24a --- /dev/null +++ b/app-src/src/bin/client_gen/cpp/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +./aicup2019 "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/csharp/Debug.cs b/app-src/src/bin/client_gen/csharp/Debug.cs new file mode 100644 index 0000000..56549c7 --- /dev/null +++ b/app-src/src/bin/client_gen/csharp/Debug.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace AiCup2019 +{ + public class Debug + { + private BinaryWriter writer; + public Debug(BinaryWriter writer) + { + this.writer = writer; + } + public void Draw(Model.CustomData customData) + { + new Model.PlayerMessageGame.CustomDataMessage(customData).WriteTo(writer); + writer.Flush(); + } + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/csharp/Dockerfile b/app-src/src/bin/client_gen/csharp/Dockerfile new file mode 100644 index 0000000..125a71b --- /dev/null +++ b/app-src/src/bin/client_gen/csharp/Dockerfile @@ -0,0 +1,20 @@ +FROM debian + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + libunwind8 \ + wget \ + gpg \ + apt-transport-https \ + ca-certificates \ + zlib1g libicu63 libcurl4 +RUN wget https://dot.net/v1/dotnet-install.sh && chmod +x dotnet-install.sh +RUN ./dotnet-install.sh -Channel 2.1 && ./dotnet-install.sh -Channel 3.0 && ./dotnet-install.sh -Channel 3.1 + +ENV PATH="/root/.dotnet:$PATH" + +COPY . /project +WORKDIR /project + +RUN dotnet build -c Release \ No newline at end of file diff --git a/app-src/src/bin/client_gen/csharp/MyStrategy.cs b/app-src/src/bin/client_gen/csharp/MyStrategy.cs new file mode 100644 index 0000000..22c865a --- /dev/null +++ b/app-src/src/bin/client_gen/csharp/MyStrategy.cs @@ -0,0 +1,71 @@ +using AiCup2019.Model; + +namespace AiCup2019 +{ + public class MyStrategy + { + static double DistanceSqr(Vec2Double a, Vec2Double b) + { + return (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y); + } + public UnitAction GetAction(Unit unit, Game game, Debug debug) + { + Unit? nearestEnemy = null; + foreach (var other in game.Units) + { + if (other.PlayerId != unit.PlayerId) + { + if (!nearestEnemy.HasValue || DistanceSqr(unit.Position, other.Position) < DistanceSqr(unit.Position, nearestEnemy.Value.Position)) + { + nearestEnemy = other; + } + } + } + LootBox? nearestWeapon = null; + foreach (var lootBox in game.LootBoxes) + { + if (lootBox.Item is Item.Weapon) + { + if (!nearestWeapon.HasValue || DistanceSqr(unit.Position, lootBox.Position) < DistanceSqr(unit.Position, nearestWeapon.Value.Position)) + { + nearestWeapon = lootBox; + } + } + } + Vec2Double targetPos = unit.Position; + if (!unit.Weapon.HasValue && nearestWeapon.HasValue) + { + targetPos = nearestWeapon.Value.Position; + } + else if (nearestEnemy.HasValue) + { + targetPos = nearestEnemy.Value.Position; + } + debug.Draw(new CustomData.Log("Target pos: " + targetPos)); + Vec2Double aim = new Vec2Double(0, 0); + if (nearestEnemy.HasValue) + { + aim = new Vec2Double(nearestEnemy.Value.Position.X - unit.Position.X, nearestEnemy.Value.Position.Y - unit.Position.Y); + } + bool jump = targetPos.Y > unit.Position.Y; + if (targetPos.X > unit.Position.X && game.Level.Tiles[(int)(unit.Position.X + 1)][(int)(unit.Position.Y)] == Tile.Wall) + { + jump = true; + } + if (targetPos.X < unit.Position.X && game.Level.Tiles[(int)(unit.Position.X - 1)][(int)(unit.Position.Y)] == Tile.Wall) + { + jump = true; + } + UnitAction action = new UnitAction(); + action.Velocity = targetPos.X - unit.Position.X; + action.Jump = jump; + action.JumpDown = !jump; + action.Aim = aim; + action.Shoot = true; + action.Reload = false; + action.SwapWeapon = false; + action.PlantMine = false; + return action; + } + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/csharp/Runner.cs b/app-src/src/bin/client_gen/csharp/Runner.cs new file mode 100644 index 0000000..3e8d4bb --- /dev/null +++ b/app-src/src/bin/client_gen/csharp/Runner.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net.Sockets; +using System.Text; + +namespace AiCup2019 +{ + public class Runner + { + private BinaryReader reader; + private BinaryWriter writer; + public Runner(string host, int port, string token) + { + var client = new TcpClient(host, port) { NoDelay = true }; + var stream = new BufferedStream(client.GetStream()); + reader = new BinaryReader(stream); + writer = new BinaryWriter(stream); + var tokenData = System.Text.Encoding.UTF8.GetBytes(token); + writer.Write(tokenData.Length); + writer.Write(tokenData); + writer.Flush(); + } + public void Run() + { + var myStrategy = new MyStrategy(); + var debug = new Debug(writer); + while (true) + { + Model.ServerMessageGame message = Model.ServerMessageGame.ReadFrom(reader); + if (!message.PlayerView.HasValue) + { + break; + } + Model.PlayerView playerView = message.PlayerView.Value; + var actions = new Dictionary(); + foreach (var unit in playerView.Game.Units) + { + if (unit.PlayerId == playerView.MyId) + { + actions.Add(unit.Id, myStrategy.GetAction(unit, playerView.Game, debug)); + } + } + new Model.PlayerMessageGame.ActionMessage(new Model.Versioned(actions)).WriteTo(writer); + writer.Flush(); + } + } + public static void Main(string[] args) + { + string host = args.Length < 1 ? "127.0.0.1" : args[0]; + int port = args.Length < 2 ? 31001 : int.Parse(args[1]); + string token = args.Length < 3 ? "0000000000000000" : args[2]; + new Runner(host, port, token).Run(); + } + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/csharp/aicup2019.csproj b/app-src/src/bin/client_gen/csharp/aicup2019.csproj new file mode 100644 index 0000000..04bd451 --- /dev/null +++ b/app-src/src/bin/client_gen/csharp/aicup2019.csproj @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp2.1 + + + \ No newline at end of file diff --git a/app-src/src/bin/client_gen/csharp/compile.sh b/app-src/src/bin/client_gen/csharp/compile.sh new file mode 100644 index 0000000..5636631 --- /dev/null +++ b/app-src/src/bin/client_gen/csharp/compile.sh @@ -0,0 +1,12 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/MyStrategy.cs MyStrategy.cs + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +dotnet publish -c Release -o /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/csharp/run.sh b/app-src/src/bin/client_gen/csharp/run.sh new file mode 100644 index 0000000..5a0174e --- /dev/null +++ b/app-src/src/bin/client_gen/csharp/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +dotnet ./aicup2019.dll "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/dlang/Dockerfile b/app-src/src/bin/client_gen/dlang/Dockerfile new file mode 100644 index 0000000..bc56eca --- /dev/null +++ b/app-src/src/bin/client_gen/dlang/Dockerfile @@ -0,0 +1,6 @@ +FROM dlanguage/dmd + +COPY . /project +WORKDIR /project + +RUN dub build -b release \ No newline at end of file diff --git a/app-src/src/bin/client_gen/dlang/app.d b/app-src/src/bin/client_gen/dlang/app.d new file mode 100644 index 0000000..831cb77 --- /dev/null +++ b/app-src/src/bin/client_gen/dlang/app.d @@ -0,0 +1,75 @@ +import model; +import my_strategy; +import stream; +import debugger; +import std.socket; +import std.conv; +import std.exception; + +class SocketStream : Stream { + this(Socket socket) { + this.socket = socket; + } + override ubyte[] readBytes(size_t byteCount) { + ubyte[] data = new ubyte[byteCount]; + size_t offset = 0; + while (offset < byteCount) { + auto received = socket.receive(data[offset..data.length]); + enforce(received > 0); + offset += received; + } + return data; + } + override void writeBytes(const ubyte[] data) { + size_t offset = 0; + while (offset < data.length) { + auto sent = socket.send(data[offset..data.length]); + enforce(sent > 0); + offset += sent; + } + } + override void flush() { } +private: + Socket socket; +} + +class Runner { + this(string host, ushort port, string token) { + auto addr = getAddress(host, port)[0]; + auto socket = new Socket(addr.addressFamily, SocketType.STREAM); + socket.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, true); + socket.connect(addr); + stream = new SocketStream(socket); + stream.write(token); + stream.flush(); + } + void run() { + auto myStrategy = new MyStrategy(); + auto debugger = new Debugger(stream); + while (true) { + ServerMessageGame message = ServerMessageGame.readFrom(stream); + if (message.playerView.isNull()) { + break; + } + PlayerView playerView = message.playerView.get; + UnitAction[int] actions; + foreach (unit; playerView.game.units) { + if (unit.playerId == playerView.myId) { + actions[unit.id] = myStrategy.getAction(unit, playerView.game, debugger); + } + } + new PlayerMessageGame.ActionMessage(Versioned(actions)).writeTo(stream); + stream.flush(); + } + } +private: + Stream stream; +} + +void main(string[] args) { + string host = args.length < 2 ? "127.0.0.1" : args[1]; + ushort port = args.length < 3 ? 31001 : to!ushort(args[2]); + string token = args.length < 4 ? "0000000000000000" : args[3]; + + new Runner(host, port, token).run(); +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/dlang/compile.sh b/app-src/src/bin/client_gen/dlang/compile.sh new file mode 100644 index 0000000..edd2741 --- /dev/null +++ b/app-src/src/bin/client_gen/dlang/compile.sh @@ -0,0 +1,13 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/my_strategy.d source/my_strategy.d + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +dub build -b release +cp aicup2019 /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/dlang/debugger.d b/app-src/src/bin/client_gen/dlang/debugger.d new file mode 100644 index 0000000..1c670c1 --- /dev/null +++ b/app-src/src/bin/client_gen/dlang/debugger.d @@ -0,0 +1,14 @@ +import model; +import stream; + +class Debugger { + this(Stream stream) { + this.stream = stream; + } + void draw(const CustomData data) { + stream.write(PlayerMessageGame.CustomDataMessage.TAG); + data.writeTo(stream); + } +private: + Stream stream; +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/dlang/dub.json b/app-src/src/bin/client_gen/dlang/dub.json new file mode 100644 index 0000000..17dec29 --- /dev/null +++ b/app-src/src/bin/client_gen/dlang/dub.json @@ -0,0 +1,3 @@ +{ + "name": "aicup2019" +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/dlang/my_strategy.d b/app-src/src/bin/client_gen/dlang/my_strategy.d new file mode 100644 index 0000000..e36f122 --- /dev/null +++ b/app-src/src/bin/client_gen/dlang/my_strategy.d @@ -0,0 +1,64 @@ +import model; +import debugger; +import std.typecons; +import std.conv; + +class MyStrategy { + UnitAction getAction(Unit unit, Game game, Debugger debugger) { + double distanceSqr(Vec2Double a, Vec2Double b) { + return (a.x - b.x) * (a.x - b.x) + + (a.y - b.y) * (a.y - b.y); + } + Nullable!Unit nearestEnemy; + foreach (other; game.units) { + if (other.playerId != unit.playerId) { + if (nearestEnemy.isNull() || + distanceSqr(unit.position, other.position) < + distanceSqr(unit.position, nearestEnemy.get.position)) { + nearestEnemy = other; + } + } + } + Nullable!LootBox nearestWeapon; + foreach (lootBox; game.lootBoxes) { + if (cast(Item.Weapon)(lootBox.item)) { + if (nearestWeapon.isNull() || + distanceSqr(unit.position, lootBox.position) < + distanceSqr(unit.position, nearestWeapon.get.position)) { + nearestWeapon = lootBox; + } + } + } + Vec2Double targetPos = unit.position; + if (unit.weapon.isNull() && !nearestWeapon.isNull()) { + targetPos = nearestWeapon.get.position; + } else if (!nearestEnemy.isNull()) { + targetPos = nearestEnemy.get.position; + } + debugger.draw(new CustomData.Log( + "Target pos: " ~ to!string(targetPos))); + Vec2Double aim = Vec2Double(0, 0); + if (!nearestEnemy.isNull()) { + aim = Vec2Double( + nearestEnemy.get.position.x - unit.position.x, + nearestEnemy.get.position.y - unit.position.y); + } + bool jump = targetPos.y > unit.position.y; + if (targetPos.x > unit.position.x && game.level.tiles[(unit.position.x + 1).to!size_t][(unit.position.y).to!size_t] == Tile.Wall) { + jump = true; + } + if (targetPos.x < unit.position.x && game.level.tiles[(unit.position.x - 1).to!size_t][(unit.position.y).to!size_t] == Tile.Wall) { + jump = true; + } + UnitAction action; + action.velocity = targetPos.x - unit.position.x; + action.jump = jump; + action.jumpDown = !jump; + action.aim = aim; + action.shoot = true; + action.reload = false; + action.swapWeapon = false; + action.plantMine = false; + return action; + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/dlang/run.sh b/app-src/src/bin/client_gen/dlang/run.sh new file mode 100644 index 0000000..e94c24a --- /dev/null +++ b/app-src/src/bin/client_gen/dlang/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +./aicup2019 "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/fsharp/Debug.fs b/app-src/src/bin/client_gen/fsharp/Debug.fs new file mode 100644 index 0000000..102b7fc --- /dev/null +++ b/app-src/src/bin/client_gen/fsharp/Debug.fs @@ -0,0 +1,7 @@ +namespace AiCup2019 + +type Debug(writer) = + member this.draw(customData) = + let message : Model.PlayerMessageGameCustomDataMessage = {Data = customData} + message.writeTo writer + writer.Flush() diff --git a/app-src/src/bin/client_gen/fsharp/Dockerfile b/app-src/src/bin/client_gen/fsharp/Dockerfile new file mode 100644 index 0000000..125a71b --- /dev/null +++ b/app-src/src/bin/client_gen/fsharp/Dockerfile @@ -0,0 +1,20 @@ +FROM debian + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + libunwind8 \ + wget \ + gpg \ + apt-transport-https \ + ca-certificates \ + zlib1g libicu63 libcurl4 +RUN wget https://dot.net/v1/dotnet-install.sh && chmod +x dotnet-install.sh +RUN ./dotnet-install.sh -Channel 2.1 && ./dotnet-install.sh -Channel 3.0 && ./dotnet-install.sh -Channel 3.1 + +ENV PATH="/root/.dotnet:$PATH" + +COPY . /project +WORKDIR /project + +RUN dotnet build -c Release \ No newline at end of file diff --git a/app-src/src/bin/client_gen/fsharp/MyStrategy.fs b/app-src/src/bin/client_gen/fsharp/MyStrategy.fs new file mode 100644 index 0000000..6e12fc8 --- /dev/null +++ b/app-src/src/bin/client_gen/fsharp/MyStrategy.fs @@ -0,0 +1,47 @@ +namespace AiCup2019 + +open AiCup2019.Model + +type MyStrategy() = + static member DistanceSqr (a: Vec2Double, b: Vec2Double) = + (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y) + + member this.getAction(unit: Unit, game: Game, debug: Debug) = + let nearestEnemy = game.Units |> Array.filter(fun u -> u.PlayerId <> unit.PlayerId) + |> Array.sortBy(fun u -> MyStrategy.DistanceSqr(u.Position, unit.Position)) + |> Seq.tryFind(fun _ -> true) + + let nearestWeapon = game.LootBoxes |> Array.choose(fun b -> match b.Item with + | Item.Weapon _ -> Some b.Position + | _ -> None) + |> Array.sortBy(fun p -> MyStrategy.DistanceSqr(p, unit.Position)) + |> Seq.tryFind(fun _ -> true) + + let mutable targetPos = unit.Position + + if not unit.Weapon.IsSome && nearestWeapon.IsSome then + targetPos <- nearestWeapon.Value + else if nearestEnemy.IsSome then + targetPos <- nearestEnemy.Value.Position + + debug.draw(CustomData.Log {Text = sprintf "Target pos: %A" targetPos }) + + let aim: Vec2Double = match nearestEnemy with + | Some x -> { X = x.Position.X - unit.Position.X; Y = x.Position.Y - unit.Position.Y} + | None -> { X = 0.0; Y = 0.0 } + + let mutable jump = targetPos.Y > unit.Position.Y + + if targetPos.X > unit.Position.X && game.Level.Tiles.[(int unit.Position.X + 1)].[(int unit.Position.Y)] = Tile.Wall then jump <- true + if targetPos.X < unit.Position.X && game.Level.Tiles.[(int unit.Position.X - 1)].[(int unit.Position.Y)] = Tile.Wall then jump <- true + + { + Velocity = targetPos.X - unit.Position.X + Jump = jump + JumpDown = not jump + Aim = aim + Shoot = true + Reload = false + SwapWeapon = false + PlantMine = false + } diff --git a/app-src/src/bin/client_gen/fsharp/Runner.fs b/app-src/src/bin/client_gen/fsharp/Runner.fs new file mode 100644 index 0000000..47e1d7b --- /dev/null +++ b/app-src/src/bin/client_gen/fsharp/Runner.fs @@ -0,0 +1,55 @@ +namespace AiCup2019 + +open System +open System.IO; +open System.Net.Sockets; + +module Runner = + type T(host, port, token: string) = + let client = new TcpClient(host, port) + let stream = new BufferedStream(client.GetStream()) + let reader = new BinaryReader(stream) + let writer = new BinaryWriter(stream) + let tokenData = System.Text.Encoding.UTF8.GetBytes token + do + client.NoDelay <- true + writer.Write tokenData.Length + writer.Write tokenData + writer.Flush() + + member this.run = + let myStrategy = new MyStrategy() + let debug = new Debug(writer) + + let rec loop() = + let message = Model.ServerMessageGame.readFrom reader + + match message.PlayerView with + | Some playerView -> + let actions = playerView.Game.Units + |> Array.filter(fun x -> x.PlayerId = playerView.MyId) + |> Array.map(fun x -> + (x.Id, myStrategy.getAction(x, playerView.Game, debug))) + |> Map.ofArray + (Model.PlayerMessageGame.ActionMessage {Action = {Inner = actions}}).writeTo writer + writer.Flush() + loop() + | None -> () + + loop() + + [] + let main argv = + let host = match argv with + | x when x.Length >=1 -> x.[0] + | _-> "127.0.0.1" + let port = match argv with + | x when x.Length >=2 -> x.[1] |> Int32.Parse + | _ -> 31001 + let token = match argv with + | x when x.Length >=3 -> x.[2] + | _ -> "0000000000000000" + + (new T(host, port, token)).run + + 0 // return an integer exit code diff --git a/app-src/src/bin/client_gen/fsharp/aicup2019.fsproj b/app-src/src/bin/client_gen/fsharp/aicup2019.fsproj new file mode 100644 index 0000000..72dec00 --- /dev/null +++ b/app-src/src/bin/client_gen/fsharp/aicup2019.fsproj @@ -0,0 +1,42 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-src/src/bin/client_gen/fsharp/compile.sh b/app-src/src/bin/client_gen/fsharp/compile.sh new file mode 100644 index 0000000..d218661 --- /dev/null +++ b/app-src/src/bin/client_gen/fsharp/compile.sh @@ -0,0 +1,12 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/MyStrategy.fs MyStrategy.fs + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +dotnet publish -c Release -o /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/fsharp/run.sh b/app-src/src/bin/client_gen/fsharp/run.sh new file mode 100644 index 0000000..5a0174e --- /dev/null +++ b/app-src/src/bin/client_gen/fsharp/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +dotnet ./aicup2019.dll "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/go/Dockerfile b/app-src/src/bin/client_gen/go/Dockerfile new file mode 100644 index 0000000..4546e9b --- /dev/null +++ b/app-src/src/bin/client_gen/go/Dockerfile @@ -0,0 +1,6 @@ +FROM golang + +COPY . /project +WORKDIR /project + +RUN go build -o aicup2019 && rm aicup2019 \ No newline at end of file diff --git a/app-src/src/bin/client_gen/go/compile.sh b/app-src/src/bin/client_gen/go/compile.sh new file mode 100644 index 0000000..b88da7b --- /dev/null +++ b/app-src/src/bin/client_gen/go/compile.sh @@ -0,0 +1,13 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/my_strategy.go my_strategy.go + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +go build -o aicup2019 +cp aicup2019 /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/go/debug.go b/app-src/src/bin/client_gen/go/debug.go new file mode 100644 index 0000000..d1d3f35 --- /dev/null +++ b/app-src/src/bin/client_gen/go/debug.go @@ -0,0 +1,18 @@ +package main + +import . "aicup2019/model" +import "bufio" + +type Debug struct { + Writer *bufio.Writer +} + +func (debug Debug) Draw(data CustomData) { + PlayerMessageGameCustomDataMessage { + Data: data, + }.Write(debug.Writer) + err := debug.Writer.Flush() + if err != nil { + panic(err) + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/go/go.mod b/app-src/src/bin/client_gen/go/go.mod new file mode 100644 index 0000000..334f778 --- /dev/null +++ b/app-src/src/bin/client_gen/go/go.mod @@ -0,0 +1,3 @@ +module aicup2019 + +go 1.13 diff --git a/app-src/src/bin/client_gen/go/main.go b/app-src/src/bin/client_gen/go/main.go new file mode 100644 index 0000000..799d303 --- /dev/null +++ b/app-src/src/bin/client_gen/go/main.go @@ -0,0 +1,91 @@ +package main + +import ( + . "aicup2019/model" + . "aicup2019/stream" + "os" + "strconv" + "net" + "bufio" +) + +type Runner struct { + conn net.Conn + reader *bufio.Reader + writer *bufio.Writer +} + +func NewRunner(host string, port uint16, token string) Runner { + conn, err := net.Dial("tcp", host + ":" + strconv.Itoa(int(port))) + if err != nil { + panic(err) + } + writer := bufio.NewWriter(conn) + WriteString(writer, token) + err = writer.Flush() + if err != nil { + panic(err) + } + return Runner { + conn: conn, + reader: bufio.NewReader(conn), + writer: writer, + } +} + +func (runner Runner) Run() { + myStrategy := NewMyStrategy() + debug := Debug { + Writer: runner.writer, + } + for { + message := ReadServerMessageGame(runner.reader) + if message.PlayerView == nil { + break + } + playerView := *message.PlayerView + actions := make(map[int32]UnitAction) + for _, unit := range playerView.Game.Units { + if (unit.PlayerId == playerView.MyId) { + actions[unit.Id] = myStrategy.getAction(unit, playerView.Game, debug) + } + } + PlayerMessageGameActionMessage { + Action: Versioned { + Inner: actions, + }, + }.Write(runner.writer) + err := runner.writer.Flush() + if err != nil { + panic(err) + } + } +} + +func main() { + var host string + if len(os.Args) < 2 { + host = "localhost" + } else { + host = os.Args[1] + } + var port uint16 + if len(os.Args) < 3 { + port = 31001 + } else { + portInt, err := strconv.Atoi(os.Args[2]) + port = uint16(portInt) + if err != nil { + panic(err) + } + } + var token string + if len(os.Args) < 4 { + token = "0000000000000000" + } else { + token = os.Args[3] + } + + runner := NewRunner(host, port, token) + runner.Run() +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/go/my_strategy.go b/app-src/src/bin/client_gen/go/my_strategy.go new file mode 100644 index 0000000..eebc450 --- /dev/null +++ b/app-src/src/bin/client_gen/go/my_strategy.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + + . "aicup2019/model" +) + +type MyStrategy struct{} + +func NewMyStrategy() MyStrategy { + return MyStrategy{} +} + +func distanceSqr(a Vec2Float64, b Vec2Float64) float64 { + return (a.X-b.X)*(a.X-b.X) + (a.Y-b.Y)*(a.Y-b.Y) +} + +func (strategy MyStrategy) getAction(unit Unit, game Game, debug Debug) UnitAction { + var nearestEnemy *Unit + for i, other := range game.Units { + if other.PlayerId != unit.PlayerId { + if nearestEnemy == nil || distanceSqr(unit.Position, other.Position) < distanceSqr(unit.Position, nearestEnemy.Position) { + nearestEnemy = &game.Units[i] + } + } + } + var nearestWeapon *LootBox + for i, lootBox := range game.LootBoxes { + switch lootBox.Item.(type) { + case ItemWeapon: + if nearestWeapon == nil || distanceSqr(unit.Position, lootBox.Position) < distanceSqr(unit.Position, nearestWeapon.Position) { + nearestWeapon = &game.LootBoxes[i] + } + } + } + targetPos := unit.Position + if unit.Weapon == nil && nearestWeapon != nil { + targetPos = nearestWeapon.Position + } else if nearestEnemy != nil { + targetPos = nearestEnemy.Position + } + debug.Draw(CustomDataLog{ + Text: fmt.Sprintf("Target pos: %v", targetPos), + }) + aim := Vec2Float64{ + X: 0, + Y: 0, + } + if nearestEnemy != nil { + aim = Vec2Float64{ + X: nearestEnemy.Position.X - unit.Position.X, + Y: nearestEnemy.Position.Y - unit.Position.Y, + } + } + jump := targetPos.Y > unit.Position.Y + if targetPos.X > unit.Position.X && game.Level.Tiles[int(unit.Position.X+1)][int(unit.Position.Y)] == TileWall { + jump = true + } + if targetPos.X < unit.Position.X && game.Level.Tiles[int(unit.Position.X-1)][int(unit.Position.Y)] == TileWall { + jump = true + } + return UnitAction{ + Velocity: targetPos.X - unit.Position.X, + Jump: jump, + JumpDown: !jump, + Aim: aim, + SwapWeapon: false, + PlantMine: false, + Shoot: true, + Reload: false, + } +} diff --git a/app-src/src/bin/client_gen/go/run.sh b/app-src/src/bin/client_gen/go/run.sh new file mode 100644 index 0000000..e94c24a --- /dev/null +++ b/app-src/src/bin/client_gen/go/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +./aicup2019 "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/java/Debug.java b/app-src/src/bin/client_gen/java/Debug.java new file mode 100644 index 0000000..0efef8f --- /dev/null +++ b/app-src/src/bin/client_gen/java/Debug.java @@ -0,0 +1,19 @@ +import java.io.IOException; +import java.io.OutputStream; + +public class Debug { + private OutputStream stream; + + public Debug(OutputStream stream) { + this.stream = stream; + } + + public void draw(model.CustomData customData) { + try { + new model.PlayerMessageGame.CustomDataMessage(customData).writeTo(stream); + stream.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/java/Dockerfile b/app-src/src/bin/client_gen/java/Dockerfile new file mode 100644 index 0000000..f62366a --- /dev/null +++ b/app-src/src/bin/client_gen/java/Dockerfile @@ -0,0 +1,6 @@ +FROM maven:3-jdk-8 + +COPY . /project +WORKDIR /project + +RUN mvn package --batch-mode \ No newline at end of file diff --git a/app-src/src/bin/client_gen/java/MyStrategy.java b/app-src/src/bin/client_gen/java/MyStrategy.java new file mode 100644 index 0000000..d034187 --- /dev/null +++ b/app-src/src/bin/client_gen/java/MyStrategy.java @@ -0,0 +1,59 @@ +import model.*; + +public class MyStrategy { + static double distanceSqr(Vec2Double a, Vec2Double b) { + return (a.getX() - b.getX()) * (a.getX() - b.getX()) + (a.getY() - b.getY()) * (a.getY() - b.getY()); + } + + public UnitAction getAction(Unit unit, Game game, Debug debug) { + Unit nearestEnemy = null; + for (Unit other : game.getUnits()) { + if (other.getPlayerId() != unit.getPlayerId()) { + if (nearestEnemy == null || distanceSqr(unit.getPosition(), + other.getPosition()) < distanceSqr(unit.getPosition(), nearestEnemy.getPosition())) { + nearestEnemy = other; + } + } + } + LootBox nearestWeapon = null; + for (LootBox lootBox : game.getLootBoxes()) { + if (lootBox.getItem() instanceof Item.Weapon) { + if (nearestWeapon == null || distanceSqr(unit.getPosition(), + lootBox.getPosition()) < distanceSqr(unit.getPosition(), nearestWeapon.getPosition())) { + nearestWeapon = lootBox; + } + } + } + Vec2Double targetPos = unit.getPosition(); + if (unit.getWeapon() == null && nearestWeapon != null) { + targetPos = nearestWeapon.getPosition(); + } else if (nearestEnemy != null) { + targetPos = nearestEnemy.getPosition(); + } + debug.draw(new CustomData.Log("Target pos: " + targetPos)); + Vec2Double aim = new Vec2Double(0, 0); + if (nearestEnemy != null) { + aim = new Vec2Double(nearestEnemy.getPosition().getX() - unit.getPosition().getX(), + nearestEnemy.getPosition().getY() - unit.getPosition().getY()); + } + boolean jump = targetPos.getY() > unit.getPosition().getY(); + if (targetPos.getX() > unit.getPosition().getX() && game.getLevel() + .getTiles()[(int) (unit.getPosition().getX() + 1)][(int) (unit.getPosition().getY())] == Tile.WALL) { + jump = true; + } + if (targetPos.getX() < unit.getPosition().getX() && game.getLevel() + .getTiles()[(int) (unit.getPosition().getX() - 1)][(int) (unit.getPosition().getY())] == Tile.WALL) { + jump = true; + } + UnitAction action = new UnitAction(); + action.setVelocity(targetPos.getX() - unit.getPosition().getX()); + action.setJump(jump); + action.setJumpDown(!jump); + action.setAim(aim); + action.setShoot(true); + action.setReload(false); + action.setSwapWeapon(false); + action.setPlantMine(false); + return action; + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/java/Runner.java b/app-src/src/bin/client_gen/java/Runner.java new file mode 100644 index 0000000..cfd11e7 --- /dev/null +++ b/app-src/src/bin/client_gen/java/Runner.java @@ -0,0 +1,51 @@ +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.io.InputStream; +import java.util.Map; +import java.util.HashMap; +import java.io.BufferedOutputStream; + +import util.StreamUtil; + +public class Runner { + private final InputStream inputStream; + private final OutputStream outputStream; + + Runner(String host, int port, String token) throws IOException { + Socket socket = new Socket(host, port); + socket.setTcpNoDelay(true); + inputStream = new BufferedInputStream(socket.getInputStream()); + outputStream = new BufferedOutputStream(socket.getOutputStream()); + StreamUtil.writeString(outputStream, token); + outputStream.flush(); + } + + void run() throws IOException { + MyStrategy myStrategy = new MyStrategy(); + Debug debug = new Debug(outputStream); + while (true) { + model.ServerMessageGame message = model.ServerMessageGame.readFrom(inputStream); + model.PlayerView playerView = message.getPlayerView(); + if (playerView == null) { + break; + } + Map actions = new HashMap<>(); + for (model.Unit unit : playerView.getGame().getUnits()) { + if (unit.getPlayerId() == playerView.getMyId()) { + actions.put(unit.getId(), myStrategy.getAction(unit, playerView.getGame(), debug)); + } + } + new model.PlayerMessageGame.ActionMessage(new model.Versioned(actions)).writeTo(outputStream); + outputStream.flush(); + } + } + + public static void main(String[] args) throws IOException { + String host = args.length < 1 ? "127.0.0.1" : args[0]; + int port = args.length < 2 ? 31001 : Integer.parseInt(args[1]); + String token = args.length < 3 ? "0000000000000000" : args[2]; + new Runner(host, port, token).run(); + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/java/compile.sh b/app-src/src/bin/client_gen/java/compile.sh new file mode 100644 index 0000000..88cd179 --- /dev/null +++ b/app-src/src/bin/client_gen/java/compile.sh @@ -0,0 +1,13 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/MyStrategy.java src/main/java/MyStrategy.java + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +mvn package --batch-mode +cp target/aicup2019-jar-with-dependencies.jar /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/java/pom.xml b/app-src/src/bin/client_gen/java/pom.xml new file mode 100644 index 0000000..fc06f2f --- /dev/null +++ b/app-src/src/bin/client_gen/java/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + com.codegame.codeside2019.devkit + aicup2019 + jar + 1.0-SNAPSHOT + + aicup2019 + + + UTF-8 + + + + aicup2019 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + UTF-8 + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + jar-with-dependencies + + + + Runner + + + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/app-src/src/bin/client_gen/java/run.sh b/app-src/src/bin/client_gen/java/run.sh new file mode 100644 index 0000000..0abe2cc --- /dev/null +++ b/app-src/src/bin/client_gen/java/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +java -Xmx250m -jar ./aicup2019-jar-with-dependencies.jar "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/javascript/Dockerfile b/app-src/src/bin/client_gen/javascript/Dockerfile new file mode 100644 index 0000000..450600a --- /dev/null +++ b/app-src/src/bin/client_gen/javascript/Dockerfile @@ -0,0 +1,4 @@ +FROM node + +COPY . /project +WORKDIR /project \ No newline at end of file diff --git a/app-src/src/bin/client_gen/javascript/compile.sh b/app-src/src/bin/client_gen/javascript/compile.sh new file mode 100644 index 0000000..6c67e41 --- /dev/null +++ b/app-src/src/bin/client_gen/javascript/compile.sh @@ -0,0 +1,12 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/my-strategy.js my-strategy.js + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +cp -r * /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/javascript/debug.js b/app-src/src/bin/client_gen/javascript/debug.js new file mode 100644 index 0000000..279ff0a --- /dev/null +++ b/app-src/src/bin/client_gen/javascript/debug.js @@ -0,0 +1,13 @@ +const PlayerMessageGame = require('./model/player-message-game').PlayerMessageGame; + +class Debug { + constructor (streamWrapper) { + this.streamWrapper = streamWrapper; + } + + async draw (data) { + await (new PlayerMessageGame.CustomDataMessage(data)).writeTo(this.streamWrapper); + } +} + +module.exports.Debug = Debug; diff --git a/app-src/src/bin/client_gen/javascript/index.js b/app-src/src/bin/client_gen/javascript/index.js new file mode 100644 index 0000000..6f25838 --- /dev/null +++ b/app-src/src/bin/client_gen/javascript/index.js @@ -0,0 +1,74 @@ +'use strict'; + +const StreamWrapper = require('./stream-wrapper').StreamWrapper; +const Socket = require('net').Socket; + +const ServerMessageGame = require('./model/server-message-game').ServerMessageGame; +const PlayerMessageGame = require('./model/player-message-game').PlayerMessageGame; +const Versioned = require('./model/versioned').Versioned; +const MyStrategy = require('./my-strategy').MyStrategy; +const Debug = require('./debug').Debug; + +class Runner { + constructor (host, port, token) { + this.socket = new Socket({readable: true, writable: true}); + this.socket + .setNoDelay(true) + .on('error', (error) => { + console.error('Socket error: ' + error.message); + process.exit(1); + }); + this.streamWrapper = new StreamWrapper(this.socket); + this.host = host; + this.port = port; + this.token = token; + } + + async connect () { + const _this = this; + await new Promise(function(resolve, reject) { + _this.socket.connect({ + host: _this.host, + port: _this.port + }, function() { + resolve(); + }); + }); + await this.streamWrapper.writeString(this.token); + } + + async run () { + try { + await this.connect(); + let message, playerView, actions; + const strategy = new MyStrategy(); + const debug = new Debug(this.streamWrapper); + while (true) { + message = await ServerMessageGame.readFrom(this.streamWrapper); + if (message.playerView === null) { + break; + } + playerView = message.playerView; + actions = new Map(); + for (let i = 0, unitsSize = playerView.game.units.length; i < unitsSize; i++) { + let unit = playerView.game.units[i]; + if (unit.playerId === playerView.myId) { + actions.set(unit.id, await strategy.getAction(unit, playerView.game, debug)); + } + } + await (new PlayerMessageGame.ActionMessage(new Versioned(actions)).writeTo(this.streamWrapper)); + } + } catch (e) { + console.error(e); + process.exit(1); + } + } +} + + +const argv = process.argv; +const argc = argv.length; +const host = argc < 3 ? '127.0.0.1' : argv[2]; +const port = argc < 4 ? 31001 : parseInt(argv[3]); +const token = argc < 5 ? '0000000000000000' : argv[4]; +(new Runner(host, port, token)).run(); diff --git a/app-src/src/bin/client_gen/javascript/my-strategy.js b/app-src/src/bin/client_gen/javascript/my-strategy.js new file mode 100644 index 0000000..6faefd7 --- /dev/null +++ b/app-src/src/bin/client_gen/javascript/my-strategy.js @@ -0,0 +1,79 @@ +const UnitAction = require('./model/unit-action').UnitAction; +const Vec2Double = require('./model/vec2-double').Vec2Double; +const Item = require('./model/item').Item; +const Tile = require('./model/tile'); +const CustomData = require('./model/custom-data').CustomData; + +class MyStrategy { + async getAction (unit, game, debug) { + const distanceSqr = function (a, b) { + return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2); + }; + + let minDistance = Number.POSITIVE_INFINITY; + const nearestEnemy = game.units + .filter((u) => { + return u.playerId !== unit.playerId; + }) + .reduce(function (prev, u) { + let currentDistance = distanceSqr(u.position, unit.position); + if (currentDistance < minDistance) { + minDistance = currentDistance; + return u; + } + return prev; + }); + + minDistance = Number.POSITIVE_INFINITY; + const nearestWeapon = game.lootBoxes + .filter ((box) => { + return box.item instanceof Item.Weapon; + }) + .reduce(function (prev, box) { + let currentDistance = distanceSqr(box.position, unit.position); + if (currentDistance < minDistance) { + minDistance = currentDistance; + return box; + } + return prev; + }); + + let targetPos = unit.position; + if (unit.weapon === null && nearestWeapon) { + targetPos = nearestWeapon.position; + } else { + targetPos = nearestEnemy.position; + } + await debug.draw(new CustomData.Log(`Target pos: ${targetPos.toString()}`)); + + let aim = new Vec2Double(0, 0); + if (nearestEnemy) { + aim = new Vec2Double( + nearestEnemy.position.x - unit.position.x, + nearestEnemy.position.y - unit.position.y + ); + } + + let jump = targetPos.y > unit.position.y; + if (targetPos.x > unit.position.x && game.level.tiles[parseInt(unit.position.x + 1)][parseInt(unit.position.y)] === Tile.Wall) { + jump = true; + } + + if (targetPos.x < unit.position.x && game.level.tiles[parseInt(unit.position.x - 1)][parseInt(unit.position.y)] === Tile.Wall) { + jump = true; + } + + return new UnitAction( + targetPos.x - unit.position.x, + jump, + !jump, + aim, + true, + false, + false, + false + ); + } +} + +module.exports.MyStrategy = MyStrategy; diff --git a/app-src/src/bin/client_gen/javascript/run.sh b/app-src/src/bin/client_gen/javascript/run.sh new file mode 100644 index 0000000..584943d --- /dev/null +++ b/app-src/src/bin/client_gen/javascript/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +node ./index.js "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/kotlin/Debug.kt b/app-src/src/bin/client_gen/kotlin/Debug.kt new file mode 100644 index 0000000..e98dcae --- /dev/null +++ b/app-src/src/bin/client_gen/kotlin/Debug.kt @@ -0,0 +1,14 @@ +import java.io.IOException +import java.io.OutputStream + +class Debug(private val stream: OutputStream) { + + fun draw(customData: model.CustomData) { + try { + model.PlayerMessageGame.CustomDataMessage(customData).writeTo(stream) + stream.flush() + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/app-src/src/bin/client_gen/kotlin/Dockerfile b/app-src/src/bin/client_gen/kotlin/Dockerfile new file mode 100644 index 0000000..f62366a --- /dev/null +++ b/app-src/src/bin/client_gen/kotlin/Dockerfile @@ -0,0 +1,6 @@ +FROM maven:3-jdk-8 + +COPY . /project +WORKDIR /project + +RUN mvn package --batch-mode \ No newline at end of file diff --git a/app-src/src/bin/client_gen/kotlin/MyStrategy.kt b/app-src/src/bin/client_gen/kotlin/MyStrategy.kt new file mode 100644 index 0000000..8354932 --- /dev/null +++ b/app-src/src/bin/client_gen/kotlin/MyStrategy.kt @@ -0,0 +1,60 @@ +import model.* + +class MyStrategy { + + fun getAction(unit: model.Unit, game: Game, debug: Debug): UnitAction { + var nearestEnemy: model.Unit? = null + for (other in game.units) { + if (other.playerId != unit.playerId) { + if (nearestEnemy == null || distanceSqr(unit.position, + other.position) < distanceSqr(unit.position, nearestEnemy.position)) { + nearestEnemy = other + } + } + } + var nearestWeapon: LootBox? = null + for (lootBox in game.lootBoxes) { + if (lootBox.item is Item.Weapon) { + if (nearestWeapon == null || distanceSqr(unit.position, + lootBox.position) < distanceSqr(unit.position, nearestWeapon.position)) { + nearestWeapon = lootBox + } + } + } + var targetPos: Vec2Double = unit.position + if (unit.weapon == null && nearestWeapon != null) { + targetPos = nearestWeapon.position + } else if (nearestEnemy != null) { + targetPos = nearestEnemy.position + } + debug.draw(CustomData.Log("Target pos: $targetPos")) + var aim = Vec2Double(0.0, 0.0) + if (nearestEnemy != null) { + aim = Vec2Double(nearestEnemy.position.x - unit.position.x, + nearestEnemy.position.y - unit.position.y) + } + var jump = targetPos.y > unit.position.y; + if (targetPos.x > unit.position.x && game.level.tiles[(unit.position.x + 1).toInt()][(unit.position.y).toInt()] == Tile.WALL) { + jump = true + } + if (targetPos.x < unit.position.x && game.level.tiles[(unit.position.x - 1).toInt()][(unit.position.y).toInt()] == Tile.WALL) { + jump = true + } + val action = UnitAction() + action.velocity = targetPos.x - unit.position.x + action.jump = jump + action.jumpDown = !jump + action.aim = aim + action.shoot = true + action.reload = false + action.swapWeapon = false + action.plantMine = false + return action + } + + companion object { + internal fun distanceSqr(a: Vec2Double, b: Vec2Double): Double { + return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + } + } +} diff --git a/app-src/src/bin/client_gen/kotlin/Runner.kt b/app-src/src/bin/client_gen/kotlin/Runner.kt new file mode 100644 index 0000000..7bb1c23 --- /dev/null +++ b/app-src/src/bin/client_gen/kotlin/Runner.kt @@ -0,0 +1,53 @@ +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.Socket +import java.util.HashMap +import util.StreamUtil + +class Runner @Throws(IOException::class) +internal constructor(host: String, port: Int, token: String) { + private val inputStream: InputStream + private val outputStream: OutputStream + + init { + val socket = Socket(host, port) + socket.tcpNoDelay = true + inputStream = BufferedInputStream(socket.getInputStream()) + outputStream = BufferedOutputStream(socket.getOutputStream()) + StreamUtil.writeString(outputStream, token) + outputStream.flush() + } + + @Throws(IOException::class) + internal fun run() { + val myStrategy = MyStrategy() + val debug = Debug(outputStream) + while (true) { + val message = model.ServerMessageGame.readFrom(inputStream) + val playerView = message.playerView ?: break + val actions = HashMap() + for (unit in playerView.game.units) { + if (unit.playerId == playerView.myId) { + actions[unit.id] = myStrategy.getAction(unit, playerView.game, debug) + } + } + model.PlayerMessageGame.ActionMessage(model.Versioned(actions)).writeTo(outputStream) + outputStream.flush() + } + } + + companion object { + + @Throws(IOException::class) + @JvmStatic + fun main(args: Array) { + val host = if (args.size < 1) "127.0.0.1" else args[0] + val port = if (args.size < 2) 31001 else Integer.parseInt(args[1]) + val token = if (args.size < 3) "0000000000000000" else args[2] + Runner(host, port, token).run() + } + } +} diff --git a/app-src/src/bin/client_gen/kotlin/compile.sh b/app-src/src/bin/client_gen/kotlin/compile.sh new file mode 100644 index 0000000..f9bef2e --- /dev/null +++ b/app-src/src/bin/client_gen/kotlin/compile.sh @@ -0,0 +1,13 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/MyStrategy.kt src/main/kotlin/MyStrategy.kt + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +mvn package --batch-mode +cp target/aicup2019-jar-with-dependencies.jar /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/kotlin/pom.xml b/app-src/src/bin/client_gen/kotlin/pom.xml new file mode 100644 index 0000000..4afa367 --- /dev/null +++ b/app-src/src/bin/client_gen/kotlin/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + com.codegame.codeside2019.devkit + aicup2019 + jar + 1.0-SNAPSHOT + + aicup2019 + + + 1.3.11 + Runner + UTF-8 + + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + aicup2019 + ${project.basedir}/src/main/kotlin + + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + + + compile + compile + + compile + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + jar-with-dependencies + + + + Runner + + + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/app-src/src/bin/client_gen/kotlin/run.sh b/app-src/src/bin/client_gen/kotlin/run.sh new file mode 100644 index 0000000..0abe2cc --- /dev/null +++ b/app-src/src/bin/client_gen/kotlin/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +java -Xmx250m -jar ./aicup2019-jar-with-dependencies.jar "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/main.rs b/app-src/src/bin/client_gen/main.rs new file mode 100644 index 0000000..8c17841 --- /dev/null +++ b/app-src/src/bin/client_gen/main.rs @@ -0,0 +1,749 @@ +use aicup2019::*; +use std::fs::File; +use std::path::{Path, PathBuf}; + +const VERSION: &str = "0.1.0"; + +#[derive(StructOpt)] +struct Opt { + path: PathBuf, +} + +fn write_file>(path: P, content: &str) -> std::io::Result<()> { + if let Some(dir) = path.as_ref().parent() { + std::fs::create_dir_all(dir)?; + } + File::create(path)?.write_all(content.as_bytes())?; + Ok(()) +} + +fn gen_python(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("python"); + let mut gen = trans_gen::Python::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&path)?; + write_file(path.join("Dockerfile"), include_str!("python/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("python/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("python/run.sh"))?; + write_file(path.join("main.py"), include_str!("python/main.py"))?; + write_file(path.join("debug.py"), include_str!("python/debug.py"))?; + write_file( + path.join("my_strategy.py"), + include_str!("python/my_strategy.py"), + )?; + Ok(()) +} + +fn gen_javascript(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("javascript"); + let mut gen = trans_gen::JavaScript::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&path)?; + write_file( + path.join("Dockerfile"), + include_str!("javascript/Dockerfile"), + )?; + write_file( + path.join("compile.sh"), + include_str!("javascript/compile.sh"), + )?; + write_file(path.join("run.sh"), include_str!("javascript/run.sh"))?; + write_file(path.join("index.js"), include_str!("javascript/index.js"))?; + write_file(path.join("debug.js"), include_str!("javascript/debug.js"))?; + write_file( + path.join("my-strategy.js"), + include_str!("javascript/my-strategy.js"), + )?; + Ok(()) +} + +fn gen_ruby(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("ruby"); + let mut gen = trans_gen::Ruby::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&path)?; + write_file(path.join("Dockerfile"), include_str!("ruby/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("ruby/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("ruby/run.sh"))?; + write_file(path.join("main.rb"), include_str!("ruby/main.rb"))?; + write_file(path.join("debug.rb"), include_str!("ruby/debug.rb"))?; + write_file( + path.join("my_strategy.rb"), + include_str!("ruby/my_strategy.rb"), + )?; + Ok(()) +} + +fn gen_rust(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("rust"); + let mut gen = trans_gen::Rust::new("aicup2019-model", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(path.join("model"))?; + write_file(path.join("Dockerfile"), include_str!("rust/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("rust/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("rust/run.sh"))?; + write_file( + path.join("Cargo.toml"), + &include_str!("rust/Cargo.toml.template").replace("$version", VERSION), + )?; + write_file(path.join("src/main.rs"), include_str!("rust/src/main.rs"))?; + write_file( + path.join("src/my_strategy.rs"), + include_str!("rust/src/my_strategy.rs"), + )?; + Ok(()) +} + +fn gen_java(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("java"); + let src_path = path.join("src").join("main").join("java"); + let mut gen = trans_gen::Java::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&src_path)?; + write_file(path.join("Dockerfile"), include_str!("java/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("java/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("java/run.sh"))?; + write_file( + src_path.join("MyStrategy.java"), + &include_str!("java/MyStrategy.java"), + )?; + write_file( + src_path.join("Debug.java"), + &include_str!("java/Debug.java"), + )?; + write_file( + src_path.join("Runner.java"), + &include_str!("java/Runner.java"), + )?; + write_file(path.join("pom.xml"), include_str!("java/pom.xml"))?; + Ok(()) +} + +fn gen_scala(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("scala"); + let src_path = path.join("src").join("main").join("scala"); + let mut gen = trans_gen::Scala::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&src_path)?; + write_file(path.join("Dockerfile"), include_str!("scala/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("scala/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("scala/run.sh"))?; + write_file( + src_path.join("MyStrategy.scala"), + &include_str!("scala/MyStrategy.scala"), + )?; + write_file( + src_path.join("Debug.scala"), + &include_str!("scala/Debug.scala"), + )?; + write_file( + src_path.join("Runner.scala"), + &include_str!("scala/Runner.scala"), + )?; + write_file(path.join("pom.xml"), include_str!("scala/pom.xml"))?; + Ok(()) +} + +fn gen_kotlin(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("kotlin"); + let src_path = path.join("src").join("main").join("kotlin"); + let mut gen = trans_gen::Kotlin::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&src_path)?; + write_file(path.join("Dockerfile"), include_str!("kotlin/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("kotlin/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("kotlin/run.sh"))?; + write_file( + src_path.join("MyStrategy.kt"), + &include_str!("kotlin/MyStrategy.kt"), + )?; + write_file(src_path.join("Debug.kt"), &include_str!("kotlin/Debug.kt"))?; + write_file( + src_path.join("Runner.kt"), + &include_str!("kotlin/Runner.kt"), + )?; + write_file(path.join("pom.xml"), include_str!("kotlin/pom.xml"))?; + Ok(()) +} + +fn gen_csharp(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("csharp"); + let mut gen = trans_gen::CSharp::new("AiCup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&path)?; + write_file(path.join("Dockerfile"), include_str!("csharp/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("csharp/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("csharp/run.sh"))?; + write_file(path.join("Runner.cs"), &include_str!("csharp/Runner.cs"))?; + write_file( + path.join("MyStrategy.cs"), + &include_str!("csharp/MyStrategy.cs"), + )?; + write_file(path.join("Debug.cs"), &include_str!("csharp/Debug.cs"))?; + write_file( + path.join("aicup2019.csproj"), + &include_str!("csharp/aicup2019.csproj"), + )?; + Ok(()) +} + +fn gen_fsharp(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("fsharp"); + let mut gen = trans_gen::FSharp::new("AiCup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&path)?; + write_file(path.join("Dockerfile"), include_str!("fsharp/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("fsharp/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("fsharp/run.sh"))?; + write_file(path.join("Runner.fs"), &include_str!("fsharp/Runner.fs"))?; + write_file( + path.join("MyStrategy.fs"), + &include_str!("fsharp/MyStrategy.fs"), + )?; + write_file(path.join("Debug.fs"), &include_str!("fsharp/Debug.fs"))?; + write_file( + path.join("aicup2019.fsproj"), + &include_str!("fsharp/aicup2019.fsproj"), + )?; + Ok(()) +} + +fn gen_dlang(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("dlang"); + let mut gen = trans_gen::Dlang::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(path.join("source"))?; + write_file(path.join("Dockerfile"), include_str!("dlang/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("dlang/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("dlang/run.sh"))?; + write_file(path.join("dub.json"), &include_str!("dlang/dub.json"))?; + write_file( + path.join("source").join("app.d"), + &include_str!("dlang/app.d"), + )?; + write_file( + path.join("source").join("debugger.d"), + &include_str!("dlang/debugger.d"), + )?; + write_file( + path.join("source").join("my_strategy.d"), + &include_str!("dlang/my_strategy.d"), + )?; + Ok(()) +} + +fn gen_go(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("go"); + let mut gen = trans_gen::Go::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&path)?; + write_file(path.join("Dockerfile"), include_str!("go/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("go/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("go/run.sh"))?; + write_file(path.join("go.mod"), &include_str!("go/go.mod"))?; + write_file(path.join("main.go"), &include_str!("go/main.go"))?; + write_file(path.join("debug.go"), &include_str!("go/debug.go"))?; + write_file( + path.join("my_strategy.go"), + &include_str!("go/my_strategy.go"), + )?; + Ok(()) +} + +fn gen_cpp(opt: &Opt) -> std::io::Result<()> { + let path = opt.path.join("cpp"); + let mut gen = trans_gen::Cpp::new("aicup2019", VERSION); + gen.add(&trans_schema::schema::>()); + gen.add(&trans_schema::schema::>()); + gen.write_to(&path)?; + write_file(path.join("Dockerfile"), include_str!("cpp/Dockerfile"))?; + write_file(path.join("compile.sh"), include_str!("cpp/compile.sh"))?; + write_file(path.join("run.sh"), include_str!("cpp/run.sh"))?; + write_file( + path.join("CMakeLists.txt"), + &include_str!("cpp/CMakeLists.txt"), + )?; + write_file( + path.join("TcpStream.hpp"), + &include_str!("cpp/TcpStream.hpp"), + )?; + write_file( + path.join("TcpStream.cpp"), + &include_str!("cpp/TcpStream.cpp"), + )?; + write_file( + path.join("MyStrategy.hpp"), + &include_str!("cpp/MyStrategy.hpp"), + )?; + write_file( + path.join("MyStrategy.cpp"), + &include_str!("cpp/MyStrategy.cpp"), + )?; + write_file(path.join("Debug.hpp"), &include_str!("cpp/Debug.hpp"))?; + write_file(path.join("Debug.cpp"), &include_str!("cpp/Debug.cpp"))?; + write_file(path.join("main.cpp"), &include_str!("cpp/main.cpp"))?; + Ok(()) +} + +fn run(opt: Opt) -> std::io::Result<()> { + std::fs::remove_dir_all(&opt.path)?; + gen_python(&opt)?; + gen_javascript(&opt)?; + gen_ruby(&opt)?; + gen_rust(&opt)?; + gen_java(&opt)?; + gen_kotlin(&opt)?; + gen_scala(&opt)?; + gen_csharp(&opt)?; + gen_fsharp(&opt)?; + gen_dlang(&opt)?; + gen_cpp(&opt)?; + gen_go(&opt)?; + Ok(()) +} + +fn main() -> std::io::Result<()> { + let opt = Opt::from_args(); + run(opt) +} + +#[cfg(test)] +mod tests { + use crate::*; + #[test] + fn test_client_gen() -> std::io::Result<()> { + let dir = tempfile::tempdir()?; + run(crate::Opt { + path: dir.path().to_owned(), + }) + } + + fn command(cmd: &str) -> std::process::Command { + let mut parts = cmd.split_whitespace(); + let mut command = if cfg!(windows) { + let mut command = std::process::Command::new("cmd"); + command.arg("/C").arg(parts.next().unwrap()); + command + } else { + std::process::Command::new(parts.next().unwrap()) + }; + for part in parts { + command.arg(part); + } + command + } + + trait CommandExt { + fn run(&mut self) -> std::io::Result<()>; + } + + impl CommandExt for std::process::Command { + fn run(&mut self) -> std::io::Result<()> { + let status = self.status()?; + if status.success() { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + status.to_string(), + )) + } + } + } + + fn test_client( + build_command: impl FnOnce(&std::path::Path) -> std::io::Result<()>, + command: impl FnOnce(&std::path::Path, u16) -> std::io::Result<()>, + ) -> std::io::Result<()> { + let _ = logger::init(); + let dir = tempfile::tempdir()?; + run(crate::Opt { + path: dir.path().to_owned(), + })?; + build_command(dir.path())?; + if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { + std::env::set_current_dir(dir)?; + } + struct AppHandle { + handle: Option>, + } + impl Drop for AppHandle { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } + } + static NEXT_PORT: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| std::sync::atomic::AtomicU16::new(31001)); + let port = NEXT_PORT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let mut app_handle = AppHandle { + handle: Some(std::thread::spawn(move || { + aicup2019::run_with(aicup2019::Opt { + actual_config: Some(codegame::FullOptions { + seed: None, + options_preset: OptionsPreset::Custom(model::Options { + properties: Some(model::Properties { + max_tick_count: 1000, + ..default() + }), + ..default() + }), + players: vec![ + PlayerOptions::Quickstart, + PlayerOptions::Tcp(codegame::TcpPlayerOptions { + host: None, + port, + accept_timeout: Some(5.0), + timeout: Some(1.0), + token: Some("token".to_owned()), + }), + ], + }), + batch_mode: true, + ..default() + }) + })), + }; + std::thread::sleep(std::time::Duration::from_secs(1)); + info!("Starting client"); + let start = std::time::Instant::now(); + command(dir.path(), port)?; + if let Err(_) = app_handle.handle.take().unwrap().join() { + return Err(std::io::Error::new(std::io::ErrorKind::Other, "App failed")); + } + let passed = std::time::Instant::now().duration_since(start).as_millis(); + info!("Finished in {}.{:03} secs", passed / 1000, passed % 1000); + Ok(()) + } + + #[test] + #[ignore] + fn test_client_python() -> std::io::Result<()> { + test_client( + |_| Ok(()), + |dir, port| { + command(if cfg!(windows) { "py -3" } else { "python3" }) + .arg(dir.join("python").join("main.py")) + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_javascript() -> std::io::Result<()> { + test_client( + |_| Ok(()), + |dir, port| { + command("node") + .arg("index.js") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("javascript")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_ruby() -> std::io::Result<()> { + test_client( + |_| Ok(()), + |dir, port| { + command("ruby") + .arg(dir.join("ruby").join("main.rb")) + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_rust() -> std::io::Result<()> { + test_client( + |dir| { + command("cargo") + .arg("build") + .arg("--release") + .current_dir(dir.join("rust")) + .run() + }, + |dir, port| { + command("cargo") + .arg("run") + .arg("--release") + .arg("--") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("rust")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_java() -> std::io::Result<()> { + test_client( + |dir| { + command("mvn") + .arg("package") + .arg("--batch-mode") + .current_dir(dir.join("java")) + .run() + }, + |dir, port| { + command("java") + .arg("-jar") + .arg("target/aicup2019-jar-with-dependencies.jar") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("java")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_kotlin() -> std::io::Result<()> { + test_client( + |dir| { + command("mvn") + .arg("--batch-mode") + .arg("package") + .current_dir(dir.join("kotlin")) + .run() + }, + |dir, port| { + command("java") + .arg("-jar") + .arg("target/aicup2019-jar-with-dependencies.jar") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("kotlin")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_scala() -> std::io::Result<()> { + test_client( + |dir| { + command("mvn") + .arg("--batch-mode") + .arg("package") + .current_dir(dir.join("scala")) + .run() + }, + |dir, port| { + command("java") + .arg("-jar") + .arg("target/aicup2019-jar-with-dependencies.jar") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("scala")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_csharp() -> std::io::Result<()> { + test_client( + |dir| { + command("dotnet") + .arg("build") + .arg("-c") + .arg("Release") + .current_dir(dir.join("csharp")) + .run() + }, + |dir, port| { + command("dotnet") + .arg("run") + .arg("-c") + .arg("Release") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("csharp")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_fsharp() -> std::io::Result<()> { + test_client( + |dir| { + command("dotnet") + .arg("build") + .arg("-c") + .arg("Release") + .current_dir(dir.join("fsharp")) + .run() + }, + |dir, port| { + command("dotnet") + .arg("run") + .arg("-c") + .arg("Release") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("fsharp")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_dlang() -> std::io::Result<()> { + test_client( + |dir| { + command("dub") + .arg("build") + .arg("-b") + .arg("release") + .current_dir(dir.join("dlang")) + .run() + }, + |dir, port| { + command("dub") + .arg("run") + .arg("-b") + .arg("release") + .arg("--") + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("dlang")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_go() -> std::io::Result<()> { + test_client( + |dir| { + command("go") + .arg("build") + .arg("-o") + .arg(if cfg!(windows) { + "aicup2019.exe" + } else { + "aicup2019" + }) + .current_dir(dir.join("go")) + .run() + }, + |dir, port| { + command( + std::path::PathBuf::from(".") + .join(if cfg!(windows) { + "aicup2019.exe" + } else { + "aicup2019" + }) + .to_str() + .unwrap(), + ) + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("go")) + .run() + }, + ) + } + + fn test_client_cpp_with_standard(standard: &str) -> std::io::Result<()> { + let _ = logger::init(); + info!("Testing with C++{}", standard); + test_client( + |dir| { + command("cmake") + .arg(format!("-DCMAKE_CXX_STANDARD={}", standard)) + .arg("-DCMAKE_BUILD_TYPE=RELEASE") + .arg("-DCMAKE_VERBOSE_MAKEFILE=ON") + .arg(".") + .current_dir(dir.join("cpp")) + .run()?; + command("cmake") + .arg("--build") + .arg(".") + .arg("--config") + .arg("Release") + .current_dir(dir.join("cpp")) + .run() + }, + |dir, port| { + command( + std::path::PathBuf::from(if cfg!(windows) { "Release" } else { "." }) + .join(if cfg!(windows) { + "aicup2019.exe" + } else { + "aicup2019" + }) + .to_str() + .unwrap(), + ) + .arg("127.0.0.1") + .arg(port.to_string()) + .arg("token") + .current_dir(dir.join("cpp")) + .run() + }, + ) + } + + #[test] + #[ignore] + fn test_client_cpp11() -> std::io::Result<()> { + test_client_cpp_with_standard("11") + } + + #[test] + #[ignore] + fn test_client_cpp14() -> std::io::Result<()> { + test_client_cpp_with_standard("14") + } + + #[test] + #[ignore] + fn test_client_cpp17() -> std::io::Result<()> { + test_client_cpp_with_standard("17") + } +} diff --git a/app-src/src/bin/client_gen/python/Dockerfile b/app-src/src/bin/client_gen/python/Dockerfile new file mode 100644 index 0000000..158fe56 --- /dev/null +++ b/app-src/src/bin/client_gen/python/Dockerfile @@ -0,0 +1,6 @@ +FROM python + +RUN pip install numpy cython sklearn lightgbm catboost + +COPY . /project +WORKDIR /project \ No newline at end of file diff --git a/app-src/src/bin/client_gen/python/compile.sh b/app-src/src/bin/client_gen/python/compile.sh new file mode 100644 index 0000000..34636eb --- /dev/null +++ b/app-src/src/bin/client_gen/python/compile.sh @@ -0,0 +1,14 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/my_strategy.py my_strategy.py + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +find . -name '*.pyx' -exec cythonize -i {} \; +python -m py_compile main.py +cp -r * /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/python/debug.py b/app-src/src/bin/client_gen/python/debug.py new file mode 100644 index 0000000..ff951e1 --- /dev/null +++ b/app-src/src/bin/client_gen/python/debug.py @@ -0,0 +1,10 @@ +import model + + +class Debug: + def __init__(self, writer): + self.writer = writer + + def draw(self, data): + model.PlayerMessageGame.CustomDataMessage(data).write_to(self.writer) + self.writer.flush() diff --git a/app-src/src/bin/client_gen/python/main.py b/app-src/src/bin/client_gen/python/main.py new file mode 100644 index 0000000..0c423a4 --- /dev/null +++ b/app-src/src/bin/client_gen/python/main.py @@ -0,0 +1,44 @@ +import model +from stream_wrapper import StreamWrapper +from debug import Debug +from my_strategy import MyStrategy +import socket +import sys + + +class Runner: + def __init__(self, host, port, token): + self.socket = socket.socket() + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + self.socket.connect((host, port)) + socket_stream = self.socket.makefile('rwb') + self.reader = StreamWrapper(socket_stream) + self.writer = StreamWrapper(socket_stream) + self.token = token + self.writer.write_string(self.token) + self.writer.flush() + + def run(self): + strategy = MyStrategy() + debug = Debug(self.writer) + + while True: + message = model.ServerMessageGame.read_from(self.reader) + if message.player_view is None: + break + player_view = message.player_view + actions = {} + for unit in player_view.game.units: + if unit.player_id == player_view.my_id: + actions[unit.id] = strategy.get_action( + unit, player_view.game, debug) + model.PlayerMessageGame.ActionMessage( + model.Versioned(actions)).write_to(self.writer) + self.writer.flush() + + +if __name__ == "__main__": + host = "127.0.0.1" if len(sys.argv) < 2 else sys.argv[1] + port = 31001 if len(sys.argv) < 3 else int(sys.argv[2]) + token = "0000000000000000" if len(sys.argv) < 4 else sys.argv[3] + Runner(host, port, token).run() diff --git a/app-src/src/bin/client_gen/python/my_strategy.py b/app-src/src/bin/client_gen/python/my_strategy.py new file mode 100644 index 0000000..be24e51 --- /dev/null +++ b/app-src/src/bin/client_gen/python/my_strategy.py @@ -0,0 +1,45 @@ +import model + + +class MyStrategy: + def __init__(self): + pass + + def get_action(self, unit, game, debug): + # Replace this code with your own + def distance_sqr(a, b): + return (a.x - b.x) ** 2 + (a.y - b.y) ** 2 + nearest_enemy = min( + filter(lambda u: u.player_id != unit.player_id, game.units), + key=lambda u: distance_sqr(u.position, unit.position), + default=None) + nearest_weapon = min( + filter(lambda box: isinstance( + box.item, model.Item.Weapon), game.loot_boxes), + key=lambda box: distance_sqr(box.position, unit.position), + default=None) + target_pos = unit.position + if unit.weapon is None and nearest_weapon is not None: + target_pos = nearest_weapon.position + elif nearest_enemy is not None: + target_pos = nearest_enemy.position + debug.draw(model.CustomData.Log("Target pos: {}".format(target_pos))) + aim = model.Vec2Double(0, 0) + if nearest_enemy is not None: + aim = model.Vec2Double( + nearest_enemy.position.x - unit.position.x, + nearest_enemy.position.y - unit.position.y) + jump = target_pos.y > unit.position.y + if target_pos.x > unit.position.x and game.level.tiles[int(unit.position.x + 1)][int(unit.position.y)] == model.Tile.WALL: + jump = True + if target_pos.x < unit.position.x and game.level.tiles[int(unit.position.x - 1)][int(unit.position.y)] == model.Tile.WALL: + jump = True + return model.UnitAction( + velocity=target_pos.x - unit.position.x, + jump=jump, + jump_down=not jump, + aim=aim, + shoot=True, + reload=False, + swap_weapon=False, + plant_mine=False) diff --git a/app-src/src/bin/client_gen/python/run.sh b/app-src/src/bin/client_gen/python/run.sh new file mode 100644 index 0000000..4ce474b --- /dev/null +++ b/app-src/src/bin/client_gen/python/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +python ./main.py "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/ruby/Dockerfile b/app-src/src/bin/client_gen/ruby/Dockerfile new file mode 100644 index 0000000..2a8f553 --- /dev/null +++ b/app-src/src/bin/client_gen/ruby/Dockerfile @@ -0,0 +1,4 @@ +FROM ruby + +COPY . /project +WORKDIR /project \ No newline at end of file diff --git a/app-src/src/bin/client_gen/ruby/compile.sh b/app-src/src/bin/client_gen/ruby/compile.sh new file mode 100644 index 0000000..f5b7504 --- /dev/null +++ b/app-src/src/bin/client_gen/ruby/compile.sh @@ -0,0 +1,13 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/my_strategy.rb my_strategy.rb + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +ruby -c main.rb +cp -r * /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/ruby/debug.rb b/app-src/src/bin/client_gen/ruby/debug.rb new file mode 100644 index 0000000..8f2ae65 --- /dev/null +++ b/app-src/src/bin/client_gen/ruby/debug.rb @@ -0,0 +1,12 @@ +require_relative 'model' + +class Debug + def initialize(writer) + @writer = writer + end + + def draw(data) + PlayerMessageGame::CustomDataMessage.new(data).write_to(@writer) + @writer.flush() + end +end diff --git a/app-src/src/bin/client_gen/ruby/main.rb b/app-src/src/bin/client_gen/ruby/main.rb new file mode 100644 index 0000000..ff60b9f --- /dev/null +++ b/app-src/src/bin/client_gen/ruby/main.rb @@ -0,0 +1,67 @@ +require 'socket' +require_relative 'stream_wrapper' +require_relative 'model' +require_relative 'my_strategy' +require_relative 'debug' + +class SocketWrapper + def initialize(socket) + @socket = socket + end + def read_bytes(byte_count) + result = '' + while result.length < byte_count + chunk = @socket.recv(byte_count - result.length) + if chunk.length <= 0 + raise "Can't read from socket" + end + result << chunk + end + result + end + def write_bytes(data) + @socket.write(data) + end + def flush() + # TODO + end +end + +class Runner + def initialize(host, port, token) + socket = TCPSocket.open(host, port) + socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + stream = SocketWrapper.new(socket) + @reader = StreamWrapper.new(stream) + @writer = StreamWrapper.new(stream) + @token = token + @writer.write_string(@token) + @writer.flush() + end + + def run() + strategy = MyStrategy.new() + debug = Debug.new(@writer) + + while true + message = ServerMessageGame.read_from(@reader) + player_view = message.player_view + if player_view == nil + break + end + actions = Hash.new + player_view.game.units.each do |unit| + if unit.player_id == player_view.my_id + actions[unit.id] = strategy.get_action(unit, player_view.game, debug) + end + end + PlayerMessageGame::ActionMessage.new(Versioned.new(actions)).write_to(@writer) + @writer.flush() + end + end +end + +host = ARGV.length < 1 ? "127.0.0.1" : ARGV[0] +port = ARGV.length < 2 ? 31001 : ARGV[1].to_i +token = ARGV.length < 3 ? "0000000000000000" : ARGV[2] +Runner.new(host, port, token).run() diff --git a/app-src/src/bin/client_gen/ruby/my_strategy.rb b/app-src/src/bin/client_gen/ruby/my_strategy.rb new file mode 100644 index 0000000..684f3e9 --- /dev/null +++ b/app-src/src/bin/client_gen/ruby/my_strategy.rb @@ -0,0 +1,54 @@ +require_relative 'model' + + +class MyStrategy + def get_action(unit, game, debug) + def distance_sqr(a, b) + (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + end + nearest_enemy = nil + game.units.each do |other| + if other.player_id != unit.player_id + if nearest_enemy == nil || distance_sqr(unit.position, other.position) < distance_sqr(unit.position, nearest_enemy.position) + nearest_enemy = other + end + end + end + nearest_weapon = nil + game.loot_boxes.each do |loot_box| + if loot_box.item.instance_of? Item::Weapon + if nearest_weapon == nil || distance_sqr(unit.position, loot_box.position) < distance_sqr(unit.position, nearest_weapon.position) + nearest_weapon = loot_box + end + end + end + target_pos = unit.position + if unit.weapon == nil && nearest_weapon != nil + target_pos = nearest_weapon.position + elsif nearest_enemy != nil + target_pos = nearest_enemy.position + end + debug.draw(CustomData::Log.new("Target pos: #{target_pos}")) + aim = Vec2Double.new(0, 0) + if nearest_enemy != nil + aim = Vec2Double.new( + nearest_enemy.position.x - unit.position.x, + nearest_enemy.position.y - unit.position.y) + end + velocity = target_pos.x - unit.position.x + jump = target_pos.y > unit.position.y + if target_pos.x > unit.position.x and game.level.tiles[(unit.position.x + 1).to_i][(unit.position.y).to_i] == Tile::WALL + jump = true + end + if target_pos.x < unit.position.x and game.level.tiles[(unit.position.x - 1).to_i][(unit.position.y).to_i] == Tile::WALL + jump = true + end + jump_down = !jump + aim = aim + shoot = true + reload = false + swap_weapon = false + plant_mine = false + UnitAction.new(velocity, jump, jump_down, aim, shoot, reload, swap_weapon, plant_mine) + end +end \ No newline at end of file diff --git a/app-src/src/bin/client_gen/ruby/run.sh b/app-src/src/bin/client_gen/ruby/run.sh new file mode 100644 index 0000000..7b83032 --- /dev/null +++ b/app-src/src/bin/client_gen/ruby/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +ruby ./main.rb "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/rust/Cargo.toml.template b/app-src/src/bin/client_gen/rust/Cargo.toml.template new file mode 100644 index 0000000..3c49330 --- /dev/null +++ b/app-src/src/bin/client_gen/rust/Cargo.toml.template @@ -0,0 +1,8 @@ +[package] +name = "aicup2019" +version = "$version" +edition = "2018" + +[dependencies] +model = { path = "model", package = "aicup2019-model" } +trans = { path = "model/trans" } \ No newline at end of file diff --git a/app-src/src/bin/client_gen/rust/Dockerfile b/app-src/src/bin/client_gen/rust/Dockerfile new file mode 100644 index 0000000..30d26c9 --- /dev/null +++ b/app-src/src/bin/client_gen/rust/Dockerfile @@ -0,0 +1,6 @@ +FROM rust + +COPY . /project +WORKDIR /project + +RUN cargo build --release \ No newline at end of file diff --git a/app-src/src/bin/client_gen/rust/compile.sh b/app-src/src/bin/client_gen/rust/compile.sh new file mode 100644 index 0000000..2913ab2 --- /dev/null +++ b/app-src/src/bin/client_gen/rust/compile.sh @@ -0,0 +1,13 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/my_strategy.rs src/my_strategy.rs + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +cargo build --release --offline +cp target/release/aicup2019 /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/rust/run.sh b/app-src/src/bin/client_gen/rust/run.sh new file mode 100644 index 0000000..e94c24a --- /dev/null +++ b/app-src/src/bin/client_gen/rust/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +./aicup2019 "$@" \ No newline at end of file diff --git a/app-src/src/bin/client_gen/rust/src/main.rs b/app-src/src/bin/client_gen/rust/src/main.rs new file mode 100644 index 0000000..efbaf66 --- /dev/null +++ b/app-src/src/bin/client_gen/rust/src/main.rs @@ -0,0 +1,88 @@ +mod my_strategy; + +use my_strategy::MyStrategy; + +struct Args { + host: String, + port: u16, + token: String, +} + +impl Args { + fn parse() -> Self { + let mut args = std::env::args(); + args.next().unwrap(); + let host = args.next().unwrap_or("127.0.0.1".to_owned()); + let port = args + .next() + .map_or(31001, |s| s.parse().expect("Can't parse port")); + let token = args.next().unwrap_or("0000000000000000".to_string()); + Self { host, port, token } + } +} + +struct Runner { + reader: Box, + writer: Box, +} + +pub struct Debug<'a>(&'a mut dyn std::io::Write); + +impl Debug<'_> { + fn draw(&mut self, data: model::CustomData) { + use trans::Trans; + model::PlayerMessageGame::CustomDataMessage { data } + .write_to(&mut self.0) + .expect("Failed to write custom debug data"); + } +} + +impl Runner { + fn new(args: &Args) -> std::io::Result { + use std::io::Write; + use trans::Trans; + let stream = std::net::TcpStream::connect((args.host.as_str(), args.port))?; + stream.set_nodelay(true)?; + let stream_clone = stream.try_clone()?; + let reader = std::io::BufReader::new(stream); + let mut writer = std::io::BufWriter::new(stream_clone); + args.token.write_to(&mut writer)?; + writer.flush()?; + Ok(Self { + reader: Box::new(reader), + writer: Box::new(writer), + }) + } + fn run(mut self) -> std::io::Result<()> { + use trans::Trans; + let mut strategy = MyStrategy::new(); + loop { + let message = model::ServerMessageGame::read_from(&mut self.reader)?; + let player_view = match message.player_view { + Some(view) => view, + None => break, + }; + let mut actions = std::collections::HashMap::new(); + for unit in player_view + .game + .units + .iter() + .filter(|unit| unit.player_id == player_view.my_id) + { + let action = + strategy.get_action(unit, &player_view.game, &mut Debug(&mut self.writer)); + actions.insert(unit.id, action); + } + let message = model::PlayerMessageGame::ActionMessage { + action: model::Versioned { inner: actions }, + }; + message.write_to(&mut self.writer)?; + self.writer.flush()?; + } + Ok(()) + } +} + +fn main() -> std::io::Result<()> { + Runner::new(&Args::parse())?.run() +} diff --git a/app-src/src/bin/client_gen/rust/src/my_strategy.rs b/app-src/src/bin/client_gen/rust/src/my_strategy.rs new file mode 100644 index 0000000..7e42075 --- /dev/null +++ b/app-src/src/bin/client_gen/rust/src/my_strategy.rs @@ -0,0 +1,84 @@ +pub struct MyStrategy {} + +impl MyStrategy { + pub fn new() -> Self { + Self {} + } + pub fn get_action( + &mut self, + unit: &model::Unit, + game: &model::Game, + debug: &mut crate::Debug, + ) -> model::UnitAction { + fn distance_sqr(a: &model::Vec2F64, b: &model::Vec2F64) -> f64 { + (a.x - b.x).powi(2) + (a.y - b.y).powi(2) + } + let nearest_enemy = game + .units + .iter() + .filter(|other| other.player_id != unit.player_id) + .min_by(|a, b| { + std::cmp::PartialOrd::partial_cmp( + &distance_sqr(&a.position, &unit.position), + &distance_sqr(&b.position, &unit.position), + ) + .unwrap() + }); + let nearest_weapon = game + .loot_boxes + .iter() + .filter(|loot| { + if let model::Item::Weapon { .. } = loot.item { + true + } else { + false + } + }) + .min_by(|a, b| { + std::cmp::PartialOrd::partial_cmp( + &distance_sqr(&a.position, &unit.position), + &distance_sqr(&b.position, &unit.position), + ) + .unwrap() + }); + let mut target_pos = unit.position.clone(); + if let (&None, Some(weapon)) = (&unit.weapon, nearest_weapon) { + target_pos = weapon.position.clone(); + } else if let Some(enemy) = nearest_enemy { + target_pos = enemy.position.clone(); + } + debug.draw(model::CustomData::Log { + text: format!("Target pos: {:?}", target_pos), + }); + let mut aim = model::Vec2F64 { x: 0.0, y: 0.0 }; + if let Some(enemy) = nearest_enemy { + aim = model::Vec2F64 { + x: enemy.position.x - unit.position.x, + y: enemy.position.y - unit.position.y, + }; + } + let mut jump = target_pos.y > unit.position.y; + if target_pos.x > unit.position.x + && game.level.tiles[(unit.position.x + 1.0) as usize][(unit.position.y) as usize] + == model::Tile::Wall + { + jump = true + } + if target_pos.x < unit.position.x + && game.level.tiles[(unit.position.x - 1.0) as usize][(unit.position.y) as usize] + == model::Tile::Wall + { + jump = true + } + model::UnitAction { + velocity: target_pos.x - unit.position.x, + jump, + jump_down: target_pos.y < unit.position.y, + aim, + shoot: true, + reload: false, + swap_weapon: false, + plant_mine: false, + } + } +} diff --git a/app-src/src/bin/client_gen/scala/Debug.scala b/app-src/src/bin/client_gen/scala/Debug.scala new file mode 100644 index 0000000..afb69da --- /dev/null +++ b/app-src/src/bin/client_gen/scala/Debug.scala @@ -0,0 +1,8 @@ +import java.io.OutputStream + +class Debug(private val stream: OutputStream) { + def draw(customData: model.CustomData) { + model.PlayerMessageGame.CustomDataMessage(customData).writeTo(stream) + stream.flush() + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/scala/Dockerfile b/app-src/src/bin/client_gen/scala/Dockerfile new file mode 100644 index 0000000..f62366a --- /dev/null +++ b/app-src/src/bin/client_gen/scala/Dockerfile @@ -0,0 +1,6 @@ +FROM maven:3-jdk-8 + +COPY . /project +WORKDIR /project + +RUN mvn package --batch-mode \ No newline at end of file diff --git a/app-src/src/bin/client_gen/scala/MyStrategy.scala b/app-src/src/bin/client_gen/scala/MyStrategy.scala new file mode 100644 index 0000000..9497783 --- /dev/null +++ b/app-src/src/bin/client_gen/scala/MyStrategy.scala @@ -0,0 +1,50 @@ +import model.CustomData.Log +import model.{Tile, UnitAction, Vec2Double} + +class MyStrategy { + def getAction(unit: model.Unit, game: model.Game, debug: Debug): model.UnitAction = { + val nearestEnemy = game.units.filter(_.playerId != unit.playerId) + .sortBy(x => distanceSqr(unit.position, x.position)) + .headOption + + val nearestWeapon = game.lootBoxes + .sortBy(x => distanceSqr(unit.position, x.position)) + .headOption + + var targetPos: Vec2Double = unit.position + if (unit.weapon.isEmpty && nearestWeapon.isDefined) { + targetPos = nearestWeapon.get.position + } else if (nearestEnemy.isDefined) { + targetPos = nearestEnemy.get.position + } + + debug.draw(Log(s"Target pos: $targetPos")) + val aim = nearestEnemy match { + case Some(enemy) => + Vec2Double(enemy.position.x - unit.position.x, + enemy.position.y - unit.position.y) + case None => Vec2Double(0, 0) + } + + var jump = targetPos.y > unit.position.y + if (targetPos.x > unit.position.x && game.level.tiles((unit.position.x + 1).toInt)(unit.position.y.toInt) == Tile.WALL) { + jump = true + } + if (targetPos.x < unit.position.x && game.level.tiles((unit.position.x - 1).toInt)(unit.position.y.toInt) == Tile.WALL) { + jump = true + } + + UnitAction(velocity = targetPos.x - unit.position.x, + jump = jump, + jumpDown = !jump, + aim = aim, + shoot = true, + reload = false, + swapWeapon = false, + plantMine = false) + } + + private def distanceSqr(a: Vec2Double, b: Vec2Double): Double = { + (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + } +} \ No newline at end of file diff --git a/app-src/src/bin/client_gen/scala/Runner.scala b/app-src/src/bin/client_gen/scala/Runner.scala new file mode 100644 index 0000000..c17e7cc --- /dev/null +++ b/app-src/src/bin/client_gen/scala/Runner.scala @@ -0,0 +1,43 @@ +import java.io.{BufferedInputStream, BufferedOutputStream} +import java.net.Socket + +import model.PlayerMessageGame.ActionMessage +import util.StreamUtil + +object Runner extends App { + + val host = if (args.length < 1) "127.0.0.1" else args(0) + val port = if (args.length < 2) 31001 else args(1).toInt + val token = if (args.length < 3) "0000000000000000" else args(2) + + run(host, port, token) + + def run(host: String, port: Int, token: String) { + val socket = new Socket(host, port) + socket.setTcpNoDelay(true) + val inputStream = new BufferedInputStream(socket.getInputStream) + val outputStream = new BufferedOutputStream(socket.getOutputStream) + + StreamUtil.writeString(outputStream, token) + outputStream.flush() + + + val myStrategy = new MyStrategy() + val debug = new Debug(outputStream) + while (true) { + val message = model.ServerMessageGame.readFrom(inputStream) + + message.playerView match { + case None => return + case Some(playerView) => + val actions = playerView.game.units + .filter(_.playerId == playerView.myId) + .map(x => (x.id, myStrategy.getAction(x, playerView.game, debug))) + .toMap + + ActionMessage(model.Versioned(actions)).writeTo(outputStream) + outputStream.flush() + } + } + } +} diff --git a/app-src/src/bin/client_gen/scala/compile.sh b/app-src/src/bin/client_gen/scala/compile.sh new file mode 100644 index 0000000..5a5183c --- /dev/null +++ b/app-src/src/bin/client_gen/scala/compile.sh @@ -0,0 +1,13 @@ +set -ex + +if [ "$1" != "base" ]; then + if [[ `ls -1 /src/ | wc -l` -eq 1 ]]; then + cp -f /src/MyStrategy.scala src/main/scala/MyStrategy.scala + else + rm -rf ./* + cp -rf /src/* ./ + fi +fi + +mvn package --batch-mode +cp target/aicup2019-jar-with-dependencies.jar /output/ \ No newline at end of file diff --git a/app-src/src/bin/client_gen/scala/pom.xml b/app-src/src/bin/client_gen/scala/pom.xml new file mode 100644 index 0000000..8bcf85f --- /dev/null +++ b/app-src/src/bin/client_gen/scala/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + com.codegame.codeside2019.devkit + aicup2019 + jar + 1.0-SNAPSHOT + + aicup2019 + + + UTF-8 + Runner + 2.13.1 + + + + + org.scala-lang + scala-library + ${scala.version} + + + + + aicup2019 + ${project.basedir}/src/main/scala + + + + net.alchim31.maven + scala-maven-plugin + 3.4.4 + + ${scala.version} + + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + jar-with-dependencies + + + + Runner + + + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/app-src/src/bin/client_gen/scala/project/plugins.sbt b/app-src/src/bin/client_gen/scala/project/plugins.sbt new file mode 100644 index 0000000..09c90ca --- /dev/null +++ b/app-src/src/bin/client_gen/scala/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") \ No newline at end of file diff --git a/app-src/src/bin/client_gen/scala/run.sh b/app-src/src/bin/client_gen/scala/run.sh new file mode 100644 index 0000000..0abe2cc --- /dev/null +++ b/app-src/src/bin/client_gen/scala/run.sh @@ -0,0 +1,4 @@ +set -ex + +cd /output +java -Xmx250m -jar ./aicup2019-jar-with-dependencies.jar "$@" \ No newline at end of file diff --git a/app-src/src/keyboard_player.rs b/app-src/src/keyboard_player.rs new file mode 100644 index 0000000..ac489a4 --- /dev/null +++ b/app-src/src/keyboard_player.rs @@ -0,0 +1,199 @@ +use crate::*; + +#[derive(Clone)] +pub struct Input { + pub pressed_keys: HashSet, + pub pressed_buttons: HashSet, + pub mouse_pos: Vec2, +} + +pub struct KeyboardPlayer { + pub input: Arc>, + pub left: geng::Key, + pub right: geng::Key, + pub jump: geng::Key, + pub jump_down: geng::Key, + pub shoot: geng::MouseButton, + pub reload: geng::Key, + pub swap_weapon: geng::Key, + pub last_swap_weapon: bool, + pub plant_mine: geng::Key, + pub last_plant_mine: bool, +} + +impl KeyboardPlayer { + pub fn new(input: &Arc>) -> Self { + Self { + input: input.clone(), + left: geng::Key::A, + right: geng::Key::D, + jump: geng::Key::W, + jump_down: geng::Key::S, + shoot: geng::MouseButton::Left, + reload: geng::Key::R, + swap_weapon: geng::Key::E, + last_swap_weapon: false, + plant_mine: geng::Key::Q, + last_plant_mine: false, + } + } +} + +#[derive(Clone)] +pub struct Config { + input: Arc>, + geng: Rc, + theme: Rc, +} + +impl codegame::PlayerConfig for Config { + fn name(&self) -> &str { + translate("keyboard") + } + fn ui<'a>(&'a mut self) -> Box { + use ui::*; + let ui = ui::text( + translate("Keyboard player"), + &self.theme.font, + 16.0, + Color::GRAY, + ) + .align(vec2(0.5, 1.0)); + Box::new(ui) + } + fn ready(&mut self) -> bool { + true + } + fn get(&mut self) -> Box> { + Box::new(KeyboardPlayer::new(&self.input)) + } + fn to_options(&self) -> PlayerOptions { + PlayerOptions::Keyboard + } +} + +impl Config { + pub fn new(geng: &Rc, theme: &Rc, input: &Arc>) -> Self { + Self { + geng: geng.clone(), + theme: theme.clone(), + input: input.clone(), + } + } + pub fn constructor( + geng: &Rc, + theme: &Rc, + input: &Arc>, + ) -> Box Box>> { + let geng = geng.clone(); + let theme = theme.clone(); + let input = input.clone(); + Box::new(move || Box::new(Self::new(&geng, &theme, &input))) + } +} + +impl codegame::Player for KeyboardPlayer { + fn get_action( + &mut self, + view: &model::PlayerView, + custom_data_handler: Option<&dyn Fn(model::CustomData)>, + ) -> Result { + Ok({ + let input = self.input.lock().unwrap().clone(); + if let Some(handler) = custom_data_handler { + if let Some(unit) = view + .game + .units + .iter() + .find(|unit| unit.player_id == view.my_id) + { + if let Some(weapon) = &unit.weapon { + let width = weapon.params.bullet.size; + let gap_size = width + r64(0.5) * weapon.spread / weapon.params.max_spread; + let radius = gap_size / r64(2.0) + r64(0.5); + let color = Color::rgba(0.0, 1.0, 0.0, 0.5); + handler(model::CustomData::Rect { + pos: (input.mouse_pos + vec2(-width / r64(2.0), gap_size / r64(2.0))) + .map(|x| x.raw() as f32), + size: vec2(width, radius).map(|x| x.raw() as f32), + color, + }); + handler(model::CustomData::Rect { + pos: (input.mouse_pos + vec2(-width / r64(2.0), -gap_size / r64(2.0))) + .map(|x| x.raw() as f32), + size: vec2(width, -radius).map(|x| x.raw() as f32), + color, + }); + handler(model::CustomData::Rect { + pos: (input.mouse_pos + vec2(gap_size / r64(2.0), -width / r64(2.0))) + .map(|x| x.raw() as f32), + size: vec2(radius, width).map(|x| x.raw() as f32), + color, + }); + handler(model::CustomData::Rect { + pos: (input.mouse_pos + vec2(-gap_size / r64(2.0), -width / r64(2.0))) + .map(|x| x.raw() as f32), + size: vec2(-radius, width).map(|x| x.raw() as f32), + color, + }); + } + } + } + let mut velocity = r64(0.0); + if input.pressed_keys.contains(&self.left) { + velocity -= r64(1.0); + } + if input.pressed_keys.contains(&self.right) { + velocity += r64(1.0); + } + let jump = input.pressed_keys.contains(&self.jump); + let jump_down = input.pressed_keys.contains(&self.jump_down); + + let mut unit_actions = HashMap::new(); + for unit in view + .game + .units + .iter() + .filter(|unit| unit.player_id == view.my_id) + { + let unit_pos = unit.position + vec2(r64(0.0), unit.size.y / r64(2.0)); + unit_actions.insert( + unit.id, + model::UnitAction { + velocity: velocity * view.game.properties.unit_max_horizontal_speed, + jump, + jump_down, + aim: input.mouse_pos - unit_pos, + shoot: input.pressed_buttons.contains(&self.shoot), + swap_weapon: if input.pressed_keys.contains(&self.swap_weapon) { + if !self.last_swap_weapon { + self.last_swap_weapon = true; + true + } else { + false + } + } else { + self.last_swap_weapon = false; + false + }, + reload: input.pressed_keys.contains(&self.reload), + plant_mine: if input.pressed_keys.contains(&self.plant_mine) { + if !self.last_plant_mine { + self.last_plant_mine = true; + true + } else { + false + } + } else { + self.last_plant_mine = false; + false + }, + }, + ); + } + model::ActionWrapper::V1 { + actions: unit_actions, + } + }) + } +} diff --git a/app-src/src/level_editor.rs b/app-src/src/level_editor.rs new file mode 100644 index 0000000..e96f629 --- /dev/null +++ b/app-src/src/level_editor.rs @@ -0,0 +1,200 @@ +use crate::*; + +struct Data { + close: bool, + change_tile: bool, + tile_button: ui::TextButton, + save_button: ui::TextButton, +} + +impl Data { + fn ui<'a>(&'a mut self) -> impl ui::Widget + 'a { + use ui::*; + let change_tile = &mut self.change_tile; + let close = &mut self.close; + stack![ + self.tile_button + .ui(Box::new(move || { + *change_tile = true; + })) + .align(vec2(0.5, 0.0)) + .uniform_padding(8.0), + self.save_button + .ui(Box::new(move || { + *close = true; + })) + .align(vec2(1.0, 0.0)) + .uniform_padding(8.0), + ] + } +} + +pub struct LevelEditor { + geng: Rc, + simple_renderer: Rc, + camera: Camera, + level: model::Level, + spawn_points: HashSet>, + level_renderer: LevelRenderer, + level_options: Rc>>, + mouse_pos: Vec2, + ui_controller: ui::Controller, + data: Data, + tile_options: Vec, + current_tile: usize, +} + +impl LevelEditor { + pub fn new( + geng: &Rc, + simple_renderer: &Rc, + theme: &Rc, + level_options: &Rc>>, + size: Vec2, + ) -> Self { + Self { + geng: geng.clone(), + simple_renderer: simple_renderer.clone(), + camera: Camera::new(), + level: level_options.borrow().as_ref().map_or_else( + || model::Level { + tiles: vec![vec![model::Tile::Empty; size.y]; size.x], + }, + |options| options.clone().create().0, + ), + spawn_points: HashSet::new(), + level_renderer: LevelRenderer::new(geng, simple_renderer), + mouse_pos: vec2(r64(0.0), r64(0.0)), + ui_controller: ui::Controller::new(), + data: Data { + close: false, + change_tile: false, + tile_button: ui::TextButton::new(geng, theme, String::new(), 32.0), + save_button: ui::TextButton::new(geng, theme, translate("save").to_owned(), 32.0), + }, + tile_options: vec![ + model::Tile::Empty, + model::Tile::Wall, + model::Tile::Platform, + model::Tile::Ladder, + model::Tile::JumpPad, + ], + current_tile: 0, + level_options: level_options.clone(), + } + } +} + +impl geng::State for LevelEditor { + fn update(&mut self, delta_time: f64) { + if self + .geng + .window() + .is_button_pressed(geng::MouseButton::Left) + { + if self.mouse_pos.x >= r64(0.0) && self.mouse_pos.y >= r64(0.0) { + let pos = self.mouse_pos.map(|x| x.raw() as usize); + if let Some(tile) = self + .level + .tiles + .get_mut(pos.x) + .and_then(|col| col.get_mut(pos.y)) + { + *tile = self.tile_options[self.current_tile].clone(); + } + } + } + self.ui_controller.update(self.data.ui(), delta_time); + } + fn draw(&mut self, framebuffer: &mut ugli::Framebuffer) { + ugli::clear(framebuffer, Some(Color::BLACK), None); + self.camera.center = self.level.size().map(|x| r64(x as _)) / r64(2.0); + self.camera.fov = r64(self.level.size().y as f64 + 5.0); + self.mouse_pos = self + .camera + .screen_to_world(framebuffer, self.geng.window().mouse_pos().map(|x| r64(x))); + self.data.tile_button.text = format!("{:?}", self.tile_options[self.current_tile]); + self.level_renderer + .draw(framebuffer, &self.camera, &self.level, None, None); + for pos in &self.spawn_points { + self.simple_renderer.frame( + framebuffer, + &self.camera, + AABB::pos_size(pos.map(|x| r64(x as _)), vec2(r64(1.0), r64(2.0))), + r64(0.1), + Color::rgba(1.0, 0.5, 0.5, 0.7), + ); + } + if self.mouse_pos.x >= r64(0.0) + && self.mouse_pos.y >= r64(0.0) + && self + .level + .tiles + .get(self.mouse_pos.x.raw() as usize) + .and_then(|col| col.get(self.mouse_pos.y.raw() as usize)) + .is_some() + { + self.simple_renderer.frame( + framebuffer, + &self.camera, + AABB::pos_size(self.mouse_pos.map(|x| x.floor()), vec2(r64(1.0), r64(1.0))), + r64(0.1), + Color::rgba(0.5, 0.5, 1.0, 0.7), + ); + } + self.ui_controller.draw(self.data.ui(), framebuffer); + } + fn handle_event(&mut self, event: geng::Event) { + match event { + geng::Event::KeyDown { key } => match key { + geng::Key::Escape => { + self.data.close = true; + } + #[cfg(not(any(target_arch = "asmjs", target_arch = "wasm32")))] + geng::Key::S if self.geng.window().is_key_pressed(geng::Key::LCtrl) => { + save_file(translate("save level"), "level.txt", |writer| { + self.level.save(writer, &self.spawn_points) + }) + .expect("Failed to save level"); + } + _ => {} + }, + geng::Event::MouseDown { button, .. } => match button { + geng::MouseButton::Right => { + if self.mouse_pos.x >= r64(0.0) && self.mouse_pos.y >= r64(0.0) { + let pos = self.mouse_pos.map(|x| x.raw() as usize); + if pos.x < self.level.size().x && pos.y < self.level.size().y { + if self.spawn_points.contains(&pos) { + self.spawn_points.remove(&pos); + } else { + self.spawn_points.insert(pos); + } + } + } + } + _ => {} + }, + _ => {} + } + self.ui_controller.handle_event(self.data.ui(), event); + if self.data.change_tile { + self.data.change_tile = false; + self.current_tile = (self.current_tile + 1) % self.tile_options.len(); + } + } + fn transition(&mut self) -> Option { + if self.data.close { + *self.level_options.borrow_mut() = Some(model::LevelOptions::Ready { + level: self.level.clone(), + spawn_points: self + .spawn_points + .iter() + .map(|pos| vec2(r64(pos.x as f64 + 0.5), r64(pos.y as _))) + .collect(), + }); + Some(geng::Transition::Pop) + } else { + None + } + } +} diff --git a/app-src/src/lib.rs b/app-src/src/lib.rs new file mode 100644 index 0000000..21ae195 --- /dev/null +++ b/app-src/src/lib.rs @@ -0,0 +1,369 @@ +pub use codegame::prelude::*; +#[cfg(feature = "rendering")] +use geng::ui; + +#[cfg(feature = "rendering")] +mod keyboard_player; +// #[cfg(feature = "rendering")] +// mod level_editor; +pub mod model; +mod quickstart_player; +mod random_player; +#[cfg(feature = "rendering")] +mod renderer; + +// #[cfg(feature = "rendering")] +// use level_editor::*; +#[cfg(feature = "rendering")] +use renderer::*; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum OptionsPreset { + Round1, + Round2, + Finals, + Custom(model::Options), +} + +impl From for model::Options { + fn from(preset: OptionsPreset) -> model::Options { + match preset { + // TODO + OptionsPreset::Round1 => model::Options { + level: model::LevelOptions::Simple, + properties: Some(model::Properties { + team_size: 1, + ..default() + }), + }, + OptionsPreset::Round2 => model::Options { + level: model::LevelOptions::Simple, + properties: Some(model::Properties { + team_size: 2, + ..default() + }), + }, + OptionsPreset::Finals => model::Options { + level: model::LevelOptions::Complex, + properties: Some(model::Properties { + team_size: 2, + ..default() + }), + }, + OptionsPreset::Custom(options) => options, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum PlayerOptions { + #[cfg(feature = "rendering")] + Keyboard, + Tcp(codegame::TcpPlayerOptions), + Quickstart, + Empty(codegame::EmptyPlayerOptions), + Random(random_player::RandomPlayerOptions), +} + +impl From for PlayerOptions { + fn from(options: codegame::TcpPlayerOptions) -> Self { + Self::Tcp(options) + } +} + +impl From for PlayerOptions { + fn from(options: codegame::EmptyPlayerOptions) -> Self { + Self::Empty(options) + } +} + +pub struct PlayerExtraData { + #[cfg(feature = "rendering")] + keyboard_input: Arc>, +} + +impl codegame::PlayerOptions for PlayerOptions { + fn get( + &self, + extra_data: &PlayerExtraData, + ) -> Pin< + Box< + dyn Future< + Output = Result>, codegame::PlayerError>, + >, + >, + > { + match self { + #[cfg(feature = "rendering")] + Self::Keyboard => futures::future::ready(Ok(Box::new( + keyboard_player::KeyboardPlayer::new(&extra_data.keyboard_input), + ) + as Box>)) + .boxed_local(), + Self::Tcp(options) => codegame::TcpPlayer::new(options.clone()) + .map(|result| match result { + Ok(player) => Ok(Box::new(player) as Box>), + Err(e) => Err(codegame::PlayerError::IOError(e)), + }) + .boxed_local(), + Self::Quickstart => { + futures::future::ready(Ok(Box::new(quickstart_player::QuickstartPlayer::new()) + as Box>)) + .boxed_local() + } + Self::Empty(_) => futures::future::ready(Ok( + Box::new(codegame::EmptyPlayer) as Box> + )) + .boxed_local(), + Self::Random(options) => futures::future::ready(Ok(Box::new( + random_player::RandomPlayer::new(options.clone()), + ) + as Box>)) + .boxed_local(), + } + } +} + +#[cfg(feature = "rendering")] +#[cfg(target_arch = "wasm32")] +#[derive(geng::Assets)] +pub struct Levels { + #[asset(path = "level.txt")] + level: String, +} + +#[cfg(feature = "rendering")] +#[derive(geng::Assets)] +pub struct Assets { + #[cfg(target_arch = "wasm32")] + #[asset(path = "levels")] + levels: Levels, + #[asset(path = "assets")] + renderer_assets: renderer::Assets, +} + +#[derive(Debug, StructOpt, Default)] +pub struct Opt { + #[structopt(long)] + pub lang: Option, + #[structopt(long)] + pub replay: Option, + #[structopt(long)] + pub repeat: Option, + #[structopt(long)] + pub save_replay: Option, + #[structopt(long)] + pub save_results: Option, + #[structopt(long)] + pub config: Option, + #[structopt(skip = None)] + pub actual_config: Option>, + #[structopt(long)] + pub batch_mode: bool, + #[structopt(long)] + pub log_level: Option, + #[structopt(long)] + pub player_names: Vec, +} + +pub fn run_with(opt: Opt) { + let mut config: Option> = opt.actual_config; + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(path) = opt.config { + config = Some( + codegame::FullOptions::::load( + std::fs::File::open(path).expect("Could not open file"), + ) + .expect("Could not load config"), + ); + } + } + + let player_extra_data = PlayerExtraData { + #[cfg(feature = "rendering")] + keyboard_input: Arc::new(Mutex::new(keyboard_player::Input { + mouse_pos: vec2(r64(0.0), r64(0.0)), + pressed_keys: HashSet::new(), + pressed_buttons: HashSet::new(), + })), + }; + + let processor = if let Some(config) = config { + let mut processor = if let Some(path) = opt.repeat { + codegame::GameProcessor::::repeat_full( + config, + &player_extra_data, + std::fs::File::open(path).expect("Failed to open game log file"), + ) + } else { + codegame::GameProcessor::::new_full(config, &player_extra_data) + }; + if let Some(path) = &opt.save_replay { + processor.set_tick_handler(codegame::save_replay_tick_handler( + std::io::BufWriter::new( + std::fs::File::create(path).expect("Failed to create replay file"), + ), + )); + } + if let Some(path) = &opt.save_results { + let writer = std::io::BufWriter::new( + std::fs::File::create(path).expect("Failed to create results file"), + ); + processor.set_results_handler(Box::new(move |results| { + serde_json::to_writer_pretty(writer, &results).expect("Failed to write results"); + })); + } + Some(processor) + } else { + None + }; + + if opt.batch_mode { + match processor { + Some(processor) => processor.run(), + None => panic!("Batch mode can only be used with --config option"), + } + return; + } + + #[cfg(not(feature = "rendering"))] + panic!("Rendering feature not enabled"); + + #[cfg(feature = "rendering")] + { + let geng = Rc::new(Geng::new(geng::ContextOptions { + title: "AI Cup 2019".to_owned(), + ..default() + })); + let theme = Rc::new(geng::ui::Theme::default(&geng)); + + let replay: futures::future::OptionFuture<_> = opt + .replay + .map(|path| codegame::History::::load(&path)) + .into(); + + let player_names = opt.player_names; + + let preferences = Rc::new(RefCell::new(AutoSave::< + codegame::AppPreferences, + >::load("aicup2019-preferences.json"))); + + let state = geng::LoadingScreen::new( + &geng, + geng::EmptyLoadingScreen, + futures::future::join(::load(&geng, "."), replay), + { + let geng = geng.clone(); + move |(assets, replay)| { + let assets = assets.expect("Failed to load assets"); + let renderer = Renderer::new( + &geng, + preferences.clone(), + &player_extra_data.keyboard_input, + player_names, + assets.renderer_assets, + ); + if let Some(processor) = processor { + Box::new(codegame::GameScreen::new( + &geng, + processor, + renderer, + preferences, + )) as Box + } else if let Some(replay) = replay { + Box::new(codegame::GameScreen::replay( + &geng, + replay, + renderer, + preferences, + )) as Box + } else { + let player_config_options = vec![ + keyboard_player::Config::constructor( + &geng, + &theme, + &player_extra_data.keyboard_input, + ), + #[cfg(not(target_arch = "wasm32"))] + codegame::TcpPlayerConfig::constructor(&geng, &theme), + quickstart_player::Config::constructor(&geng, &theme), + random_player::Config::constructor(&geng, &theme), + codegame::EmptyPlayerConfig::constructor(&geng, &theme), + ]; + let mut levels = vec![ + (translate("simple").to_owned(), model::LevelOptions::Simple), + ( + translate("complex").to_owned(), + model::LevelOptions::Complex, + ), + ]; + #[cfg(target_arch = "wasm32")] + { + levels.push(( + "level.txt".to_owned(), + model::LevelOptions::parse(&assets.levels.level), + )); + } + #[cfg(not(target_arch = "wasm32"))] + { + fn add_levels( + levels: &mut Vec<(String, model::LevelOptions)>, + ) -> std::io::Result<()> { + for entry in std::fs::read_dir("levels")? { + let entry = entry?; + if entry.path().is_file() { + levels.push(( + entry.path().to_str().unwrap().to_owned(), + model::LevelOptions::LoadFrom { path: entry.path() }, + )); + } + } + Ok(()) + } + add_levels(&mut levels).expect("Failed to get list of levels"); + } + let game_options_config = model::OptionsConfig::new(&geng, &theme, levels); + Box::new(codegame::ConfigScreen::new( + &geng, + &theme, + Box::new(game_options_config), + player_config_options, + vec![0, 1], + renderer, + preferences, + )) + } + } + }, + ); + geng::run(geng, state); + } +} + +pub fn run() { + let opt: Opt = program_args::parse(); + if let Some(level) = opt.log_level { + logger::init_with_level(level) + } else { + logger::init() + } + .expect("Failed to initialize logger"); + #[cfg(feature = "rendering")] + logger::add_logger(Box::new(geng::logger())); + add_translations(include_str!("translations.txt")); + if let Some(lang) = &opt.lang { + set_locale(lang); + } + if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { + std::env::set_current_dir(std::path::Path::new(&dir).join("static")).unwrap(); + } else { + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(path) = std::env::current_exe().unwrap().parent() { + std::env::set_current_dir(path).unwrap(); + } + } + } + run_with(program_args::parse()); +} diff --git a/app-src/src/main.rs b/app-src/src/main.rs new file mode 100644 index 0000000..9b109f6 --- /dev/null +++ b/app-src/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + aicup2019::run(); +} diff --git a/app-src/src/model/action.rs b/app-src/src/model/action.rs new file mode 100644 index 0000000..e11dac8 --- /dev/null +++ b/app-src/src/model/action.rs @@ -0,0 +1,55 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +#[schematic(rename = "UnitAction")] +pub struct OldUnitAction { + pub velocity: R64, + pub jump: bool, + pub jump_down: bool, + pub aim: Vec2, + pub shoot: bool, + pub swap_weapon: bool, + pub plant_mine: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct UnitAction { + pub velocity: R64, + pub jump: bool, + pub jump_down: bool, + pub aim: Vec2, + pub shoot: bool, + pub reload: bool, + pub swap_weapon: bool, + pub plant_mine: bool, +} + +impl From for UnitAction { + fn from(old: OldUnitAction) -> Self { + Self { + velocity: old.velocity, + jump: old.jump, + jump_down: old.jump_down, + aim: old.aim, + shoot: old.shoot, + reload: false, + swap_weapon: old.swap_weapon, + plant_mine: old.plant_mine, + } + } +} + +impl Default for UnitAction { + fn default() -> Self { + Self { + velocity: r64(0.0), + jump: false, + jump_down: false, + aim: vec2(r64(0.0), r64(0.0)), + shoot: false, + reload: false, + swap_weapon: false, + plant_mine: false, + } + } +} diff --git a/app-src/src/model/bullet.rs b/app-src/src/model/bullet.rs new file mode 100644 index 0000000..48fe610 --- /dev/null +++ b/app-src/src/model/bullet.rs @@ -0,0 +1,22 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct Bullet { + pub weapon_type: WeaponType, + pub unit_id: Id, + pub player_id: Id, + pub position: Vec2, + pub velocity: Vec2, + pub damage: i32, + pub size: R64, + pub explosion_params: Option, +} + +impl Bullet { + pub fn rect(&self) -> AABB { + AABB::pos_size( + self.position - vec2(self.size, self.size) / r64(2.0), + vec2(self.size, self.size), + ) + } +} diff --git a/app-src/src/model/custom_data.rs b/app-src/src/model/custom_data.rs new file mode 100644 index 0000000..56e47a4 --- /dev/null +++ b/app-src/src/model/custom_data.rs @@ -0,0 +1,43 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +#[cfg_attr(feature = "rendering", derive(ugli::Vertex))] +pub struct ColoredVertex { + pub position: Vec2, + pub color: Color, +} + +#[derive(Debug, Serialize, Deserialize, Copy, Clone, Trans, Schematic)] +pub enum TextAlignment { + Left, + Center, + Right, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub enum CustomData { + Log { + text: String, + }, + Rect { + pos: Vec2, + size: Vec2, + color: Color, + }, + Line { + p1: Vec2, + p2: Vec2, + width: f32, + color: Color, + }, + Polygon { + vertices: Vec, + }, + PlacedText { + text: String, + pos: Vec2, + alignment: TextAlignment, + size: f32, + color: Color, + }, +} diff --git a/app-src/src/model/game.rs b/app-src/src/model/game.rs new file mode 100644 index 0000000..1797068 --- /dev/null +++ b/app-src/src/model/game.rs @@ -0,0 +1,556 @@ +use super::*; + +fn ladder_rect(unit: &Unit) -> AABB { + AABB::pos_size(unit.position, vec2(r64(0.0), unit.size.y / r64(2.0))) +} + +struct Explosion { + position: Vec2, + params: ExplosionParams, + player_id: Id, +} + +impl Game { + fn process_shooting( + &mut self, + rng: &mut dyn rand::RngCore, + events: &mut Vec, + unit_actions: &HashMap, + delta_time: R64, + ) { + for unit in &mut self.units { + let action = unit_actions.get(&unit.id).cloned().unwrap_or_else(default); + + let unit_center = unit.center(); + + if let Some(weapon) = &mut unit.weapon { + if action.aim.len() > r64(0.5) { + weapon.aim(action.aim); + weapon.set_shoot(action.shoot); + if action.reload { + weapon.reload(); + } + if action.shoot { + if let Some(bullet) = weapon.fire( + self.current_tick, + unit.id, + unit.player_id, + unit_center, + action.aim, + rng, + ) { + events.push(Event::Shot { + weapon_type: bullet.weapon_type, + }); + self.bullets.push(bullet); + } + } + } + weapon.update(delta_time); + } + } + } + fn process_movement(&mut self, unit_actions: &HashMap, delta_time: R64) { + fn update(old: R64, direction: R64, to: R64) -> R64 { + if direction > old { + max(old, to) + } else { + min(old, to) + } + } + let properties = &self.properties; + for unit_index in 0..self.units.len() { + let mut unit_position; + let mut unit_jump_state; + let mut on_ground = false; + { + let unit = &self.units[unit_index]; + let action = unit_actions.get(&unit.id).cloned().unwrap_or_else(default); + + unit_position = unit.position; + unit_jump_state = unit.jump_state.clone(); + let mut new_position = unit_position; + + new_position.x += + clamp_abs(action.velocity, properties.unit_max_horizontal_speed) * delta_time; + + if unit_jump_state.can_cancel && !action.jump { + unit_jump_state = JumpState::falling(); + } + if unit_jump_state.can_jump { + new_position.y += unit_jump_state.speed * delta_time; + unit_jump_state.max_time -= delta_time; + if unit_jump_state.max_time < r64(0.0) { + unit_jump_state = JumpState::falling(); + } + } else if action.jump_down + || !self + .level + .find_tiles(ladder_rect(unit)) + .any(|(_, tile)| *tile == Tile::Ladder) + { + new_position.y -= properties.unit_fall_speed * delta_time; + } + + let eps = r64(1e-9); + + // Horizontal + for other in &self.units { + if other.id == unit.id { + continue; + } + if unit + .rect_at(vec2(new_position.x, unit_position.y)) + .intersects(&other.rect()) + { + new_position.x = update( + unit_position.x, + new_position.x, + other.position.x + + (unit_position.x - other.position.x).signum() + * ((unit.size.x + other.size.x) / r64(2.0) + eps), + ); + } + } + if self + .level + .find_tiles(unit.rect_at(vec2(new_position.x, unit_position.y))) + .any(|(_, tile)| tile.is_blocking()) + { + if new_position.x > unit_position.x { + unit_position.x = update( + unit_position.x, + new_position.x, + unit_position.x + (unit.rect().x_max - eps).ceil() + - unit.rect().x_max + - eps, + ); + } else { + unit_position.x = update( + unit_position.x, + new_position.x, + unit_position.x + (unit.rect().x_min + eps).floor() - unit.rect().x_min + + eps, + ); + } + } else { + unit_position.x = new_position.x; + } + new_position.x = unit_position.x; + + // Vertical + for other in &self.units { + if other.id == unit.id { + continue; + } + if unit.rect_at(new_position).intersects(&other.rect()) { + if new_position.y > unit_position.y { + new_position.y = update( + unit_position.y, + new_position.y, + other.position.y - unit.size.y - eps, + ); + unit_jump_state = JumpState::falling(); + } else { + new_position.y = update( + unit_position.y, + new_position.y, + other.position.y + other.size.y + eps, + ); + unit_jump_state = JumpState { + can_jump: true, + speed: properties.unit_jump_speed, + max_time: properties.unit_jump_time, + can_cancel: true, + }; + } + } + } + if self + .level + .find_tiles(unit.rect_at(new_position)) + .any(|(pos, tile)| { + let falling_from = if action.jump_down { + None + } else { + Some(unit_position.y) + }; + match tile { + Tile::Empty | Tile::JumpPad => false, + Tile::Wall => true, + Tile::Platform => { + if let Some(height) = falling_from { + height > r64(0.0) && height.raw() as usize > pos.y + } else { + false + } + } + Tile::Ladder => { + if let Some(height) = falling_from { + height > r64(0.0) + && height.raw() as usize > pos.y + && unit_position.x > r64(0.0) + && unit_position.x.raw() as usize == pos.x + } else { + false + } + } + } + }) + { + if new_position.y < unit_position.y { + unit_jump_state = JumpState { + can_jump: true, + speed: properties.unit_jump_speed, + max_time: properties.unit_jump_time, + can_cancel: true, + }; + on_ground = true; + } else { + unit_jump_state = JumpState::falling(); + } + if new_position.y > unit_position.y { + unit_position.y = update( + unit_position.y, + new_position.y, + unit_position.y + (unit.rect().y_max - eps).ceil() + - unit.rect().y_max + - eps, + ); + } else { + unit_position.y = update( + unit_position.y, + new_position.y, + unit_position.y + (unit.rect().y_min + eps).floor() - unit.rect().y_min + + eps, + ); + } + } else { + unit_position.y = new_position.y; + } + new_position.y = unit_position.y; + } + let unit = &mut self.units[unit_index]; + unit.on_ground = on_ground; + unit.stand = unit_position.x == unit.position.x; + if !unit.stand { + unit.walked_right = unit_position.x > unit.position.x; + } + unit.position = unit_position; + unit.jump_state = unit_jump_state; + } + } + fn process_level_jumps(&mut self, events: &mut Vec) { + let properties = &self.properties; + for unit in &mut self.units { + unit.on_ladder = false; + let jump_pad_pos = self.level.find_tiles(unit.rect()).find_map(|(pos, tile)| { + if *tile == Tile::JumpPad { + Some(pos) + } else { + None + } + }); + if let Some(pos) = jump_pad_pos { + unit.jump_state = JumpState { + can_jump: true, + speed: properties.jump_pad_jump_speed, + max_time: properties.jump_pad_jump_time, + can_cancel: false, + }; + events.push(Event::LevelEvent { used_tile: pos }); + } + if self + .level + .find_tiles(ladder_rect(unit)) + .any(|(_, tile)| *tile == Tile::Ladder) + { + unit.jump_state = JumpState { + can_jump: true, + speed: properties.unit_jump_speed, + max_time: unit.jump_state.max_time.max(properties.unit_jump_time), + can_cancel: true, + }; + unit.on_ground = true; + unit.on_ladder = true; + } + } + } + fn process_bullets( + &mut self, + events: &mut Vec, + explosions: &mut Vec, + delta_time: R64, + ) { + { + let players = &mut self.players; + let units = &mut self.units; + let mines = &mut self.mines; + self.bullets.retain(|bullet| { + for unit in units.iter_mut() { + if unit.id == bullet.unit_id { + continue; + } + if bullet.rect().intersects(&unit.rect()) { + events.push(Event::Hit); + let score = bullet.damage.min(unit.health).max(0) as usize; + unit.health -= bullet.damage; + if unit.player_id != bullet.player_id { + players + .iter_mut() + .find(|player| player.id == bullet.player_id) + .unwrap() + .score += score; + } + if let Some(params) = &bullet.explosion_params { + explosions.push(Explosion { + player_id: bullet.player_id, + position: bullet.position, + params: params.clone(), + }); + } + return false; + } + } + for mine in mines.iter_mut() { + if bullet.rect().intersects(&mine.rect()) { + mine.state = MineState::Exploded; + if let Some(params) = &bullet.explosion_params { + explosions.push(Explosion { + player_id: bullet.player_id, + position: bullet.position, + params: params.clone(), + }); + } + return false; + } + } + true + }); + } + for bullet in &mut self.bullets { + bullet.position += bullet.velocity * delta_time; + } + let level = &self.level; + self.bullets.retain(|bullet| { + if level + .find_tiles(bullet.rect()) + .any(|(_, tile)| tile.is_blocking()) + { + if let Some(params) = &bullet.explosion_params { + explosions.push(Explosion { + player_id: bullet.player_id, + position: bullet.position, + params: params.clone(), + }); + } + false + } else { + true + } + }); + } + fn process_loot_boxes( + &mut self, + events: &mut Vec, + unit_actions: &HashMap, + unit_swapped: &mut HashSet, + ) { + let properties = &self.properties; + for unit in &mut self.units { + let swap_weapon = unit_actions + .get(&unit.id) + .map(|action| action.swap_weapon) + .unwrap_or(false) + && !unit_swapped.contains(&unit.id); + let loot_boxes = self + .loot_boxes + .drain(..) + .filter_map(|loot_box| { + if !loot_box.rect().intersects(&unit.rect()) { + return Some(loot_box); + } + let mut handle_item = |item: Item| -> Option { + match item { + Item::HealthPack { health } => { + if unit.health < properties.unit_max_health { + events.push(Event::Heal); + unit.health = + (unit.health + health).min(properties.unit_max_health); + None + } else { + Some(Item::HealthPack { health }) + } + } + Item::Weapon { mut weapon_type } => { + if let Some(unit_weapon) = &mut unit.weapon { + if swap_weapon { + let new_unit_weapon = Weapon::new(properties, weapon_type); + weapon_type = unit_weapon.typ; + *unit_weapon = new_unit_weapon; + unit_swapped.insert(unit.id); + } + Some(Item::Weapon { weapon_type }) + } else { + events.push(Event::PickupWeapon); + unit.weapon = Some(Weapon::new(properties, weapon_type)); + None + } + } + Item::Mine {} => { + events.push(Event::PickupMine); + unit.mines += 1; + None + } + } + }; + if let Some(item) = handle_item(loot_box.item) { + Some(LootBox { + position: loot_box.position, + size: loot_box.size, + item, + }) + } else { + None + } + }) + .collect::>(); + self.loot_boxes.extend(loot_boxes); + } + } + fn process_planting_mines( + &mut self, + unit_actions: &HashMap, + unit_planted_mine: &mut HashSet, + ) { + for unit in &mut self.units { + let action = unit_actions.get(&unit.id).cloned().unwrap_or_else(default); + if action.plant_mine && !unit_planted_mine.contains(&unit.id) { + if unit.mines > 0 + && unit.on_ground + && !unit.on_ladder + && self + .level + .get( + unit.position.x.raw().max(0.0) as usize, + (unit.position.y.raw().max(0.0) as usize).max(1) - 1, + ) + .map_or(false, |tile| tile.can_plant_mine()) + { + unit.mines -= 1; + self.mines + .push(Mine::spawn(unit.player_id, &self.properties, unit.position)); + } + unit_planted_mine.insert(unit.id); + } + } + } + fn process_mines(&mut self, explosions: &mut Vec, delta_time: R64) { + for mine in &mut self.mines { + mine.timer = match mine.timer { + Some(mut timer) => { + timer -= delta_time; + if timer > r64(0.0) { + Some(timer) + } else { + None + } + } + None => None, + }; + match mine.state { + MineState::Preparing => { + if mine.timer.is_none() { + mine.state = MineState::Idle; + } + } + MineState::Idle => { + for unit in &self.units { + let distance = unit.rect().distance_to(&mine.rect()); + if distance < mine.trigger_radius { + mine.state = MineState::Triggered; + mine.timer = Some(self.properties.mine_trigger_time); + break; + } + } + } + MineState::Triggered => { + if mine.timer.is_none() { + mine.state = MineState::Exploded; + } + } + MineState::Exploded => {} + }; + if mine.state == MineState::Exploded { + explosions.push(Explosion { + position: mine.rect().center(), + params: mine.explosion_params.clone(), + player_id: mine.player_id, + }); + } + } + self.mines.retain(|mine| mine.state != MineState::Exploded); + } + fn process_explosions(&mut self, explosions: &[Explosion]) { + for explosion in explosions { + let rect = AABB::from_corners( + explosion.position - vec2(explosion.params.radius, explosion.params.radius), + explosion.position + vec2(explosion.params.radius, explosion.params.radius), + ); + for unit in &mut self.units { + if unit.rect().intersects(&rect) { + let score = explosion.params.damage.min(unit.health).max(0) as usize; + unit.health -= explosion.params.damage; + if unit.player_id != explosion.player_id { + self.players + .iter_mut() + .find(|player| player.id == explosion.player_id) + .unwrap() + .score += score; + } + } + } + for mine in &mut self.mines { + if mine.rect().intersects(&rect) { + mine.state = MineState::Exploded; + } + } + } + } + pub fn update( + &mut self, + rng: &mut dyn rand::RngCore, + events: &mut Vec, + unit_planted_mine: &mut HashSet, + unit_swapped: &mut HashSet, + unit_actions: &HashMap, + delta_time: R64, + ) { + let mut explosions: Vec = Vec::new(); + self.process_planting_mines(unit_actions, unit_planted_mine); + self.process_shooting(rng, events, unit_actions, delta_time); + self.process_loot_boxes(events, unit_actions, unit_swapped); + self.process_movement(unit_actions, delta_time); + self.process_level_jumps(events); + self.process_bullets(events, &mut explosions, delta_time); + self.process_mines(&mut explosions, delta_time); + self.process_explosions(&explosions); + for unit in &self.units { + if unit.health <= 0 { + self.players + .iter_mut() + .find(|player| player.id != unit.player_id) + .unwrap() + .score += self.properties.kill_score; + } + } + self.units.retain(|unit| unit.health > 0); + for explosion in explosions { + events.push(Event::Explosion { + player_id: explosion.player_id, + position: explosion.position, + params: explosion.params, + }); + } + } +} diff --git a/app-src/src/model/level/complex.rs b/app-src/src/model/level/complex.rs new file mode 100644 index 0000000..a03620a --- /dev/null +++ b/app-src/src/model/level/complex.rs @@ -0,0 +1,152 @@ +use super::*; + +pub fn gen(rng: &mut dyn rand::RngCore) -> (Level, Vec) { + let width = 40; + let height = 30; + + let w = 5; + let h = 5; + assert_eq!(width % w, 0); + assert_eq!(height % h, 0); + + #[derive(Debug, Clone)] + struct BigTile { + left: bool, + down: bool, + }; + impl BigTile { + fn new(left: bool, down: bool) -> Self { + Self { left, down } + } + } + let sw = width / w / 2; + let sh = height / h; + let mut big = vec![vec![BigTile::new(false, false); sh]; sw]; + let mut nx = vec![vec![vec2(0, 0); sh]; sw]; + for i in 0..sw { + for j in 0..sh { + nx[i][j] = vec2(i, j); + } + } + fn get_root(nx: &[Vec>], p: Vec2) -> Vec2 { + let mut p = p; + while nx[p.x][p.y] != p { + p = nx[p.x][p.y]; + } + p + } + fn connected(nx: &[Vec>]) -> bool { + for i in 0..nx.len() { + for j in 0..nx[0].len() { + if get_root(nx, vec2(i, j)) != get_root(nx, vec2(0, 0)) { + return false; + } + } + } + true + } + fn unite(nx: &mut [Vec>], p1: Vec2, p2: Vec2) { + let r1 = get_root(nx, p1); + let r2 = get_root(nx, p2); + nx[r1.x][r1.y] = r2; + } + while !connected(&nx) { + let mut options = Vec::new(); + for i in 0..sw { + for j in 0..sh { + if i > 0 && !big[i][j].left { + options.push((i, j, 0)); + } + if j > 0 && !big[i][j].down { + options.push((i, j, 1)); + } + } + } + let (i, j, side) = options[rng.gen_range(0, options.len())]; + match side { + 0 => { + big[i][j].left = true; + unite(&mut nx, vec2(i, j), vec2(i - 1, j)); + } + 1 => { + big[i][j].down = true; + unite(&mut nx, vec2(i, j), vec2(i, j - 1)); + } + _ => unreachable!(), + } + } + + let mut level = Level { + tiles: vec![vec![Tile::Empty; height]; width], + }; + for i in 0..sw { + let mut j = 0; + while j < sh { + let mut nj = j + 1; + let mx = rng.gen_range(1, 3); + while nj < j + mx + 1 && nj < sh && big[i][nj].down { + nj += 1; + } + let nj = nj - 1; + let y = nj * h; + if nj > j { + if nj + 1 < sh && big[i][nj + 1].down { + for x in i * w..(i + 1) * w { + level.tiles[x][y] = Tile::Platform; + } + } + match nj - j { + 1 => {} + 2 => { + let x = rng.gen_range(i * w + 2, (i + 1) * w - 1); + if !(nj + 1 < sh && big[i][nj + 1].down) || rng.gen() { + let y = j * h + 1; + level.tiles[x][y] = Tile::JumpPad; + } else { + for y in j * h + 1..=nj * h { + level.tiles[x][y] = Tile::Ladder; + } + } + } + _ => unreachable!(), + } + j = nj; + } else { + j += 1; + } + } + } + for i in 0..sw { + for j in 0..sh { + if !big[i][j].left { + let x = i * w; + for y in j * h..(j + 1) * h + 1 { + if let Some(tile) = level.tiles[x].get_mut(y) { + *tile = Tile::Wall; + } + } + } + if !big[i][j].down { + let y = j * h; + for x in i * w..(i + 1) * w + 1 { + level.tiles[x][y] = Tile::Wall; + } + } + } + } + for x in 0..width / 2 { + for y in 0..height { + level.tiles[width - 1 - x][y] = level.tiles[x][y].clone(); + } + } + for x in 0..width { + level.tiles[x][height - 1] = Tile::Wall; + } + + let mut spawn_points = Vec::new(); + for i in 1..3 { + spawn_points.push(vec2(r64(i as f64 + 0.5), r64(1.0))); + spawn_points.push(vec2(r64(width as f64 - i as f64 - 0.5), r64(1.0))); + } + (level, spawn_points) +} diff --git a/app-src/src/model/level/mod.rs b/app-src/src/model/level/mod.rs new file mode 100644 index 0000000..50419de --- /dev/null +++ b/app-src/src/model/level/mod.rs @@ -0,0 +1,319 @@ +use super::*; + +mod complex; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum LevelOptions { + EmptyBox { + width: usize, + height: usize, + }, + LoadFrom { + path: std::path::PathBuf, + }, + Ready { + level: Level, + spawn_points: Vec, + }, + Simple, + Complex, +} + +impl LevelOptions { + pub fn parse(s: &str) -> Self { + let mut spawn_points = Vec::new(); + let mut extra_spawn_points = Vec::new(); + let mut tiles = Vec::new(); + for (y, line) in s.lines().enumerate() { + for (x, c) in line.chars().enumerate() { + while x >= tiles.len() { + tiles.push(Vec::new()); + } + while y >= tiles[x].len() { + tiles[x].push(Tile::Empty); + } + tiles[x][y] = match c { + '#' => Tile::Wall, + '.' => Tile::Empty, + '^' => Tile::Platform, + 'H' => Tile::Ladder, + 'T' => Tile::JumpPad, + 'P' => { + spawn_points.push(vec2(r64(x as f64 + 0.5), r64(y as _))); + Tile::Empty + } + _ => { + if let Some(digit) = c.to_digit(10) { + extra_spawn_points + .push((digit, vec2(r64(x as f64 + 0.5), r64(y as _)))); + Tile::Empty + } else { + panic!("Unexpected character"); + } + } + } + } + } + extra_spawn_points.sort_by_key(|&(digit, _)| digit); + for (_, point) in extra_spawn_points { + spawn_points.push(point); + } + let size = vec2(tiles.len(), tiles[0].len()); + for row in &mut tiles { + assert_eq!(row.len(), size.y); + row.reverse(); + } + for spawn_point in &mut spawn_points { + spawn_point.y = r64(size.y as _) - spawn_point.y - r64(1.0); + } + LevelOptions::Ready { + level: Level::new(tiles), + spawn_points, + } + } +} + +pub type SpawnPoint = Vec2; + +impl LevelOptions { + pub fn create(self, rng: &mut dyn rand::RngCore) -> (Level, Vec) { + match self { + LevelOptions::EmptyBox { width, height } => { + assert!(width > 2 && height > 2); + let mut tiles = vec![vec![Tile::Empty; height]; width]; + for (x, row) in tiles.iter_mut().enumerate() { + for (y, tile) in row.iter_mut().enumerate() { + if x == 0 || x + 1 == width || y == 0 || y + 1 == height { + *tile = Tile::Wall; + } + } + } + let mut spawn_points = Vec::new(); + for i in 1..width / 2 { + spawn_points.push(vec2(r64(i as f64 + 0.5), r64(1.0))); + spawn_points.push(vec2(r64(width as f64 - i as f64 - 0.5), r64(1.0))); + } + (Level::new(tiles), spawn_points) + } + LevelOptions::LoadFrom { path } => { + fn load(path: std::path::PathBuf) -> std::io::Result { + let mut content = String::new(); + std::fs::File::open(path)?.read_to_string(&mut content)?; + Ok(LevelOptions::parse(&content)) + } + load(path).expect("Failed to load map").create(rng) + } + LevelOptions::Ready { + level, + spawn_points, + } => (level, spawn_points), + LevelOptions::Simple => { + let width = 40; + let height = 30; + let mut tiles = vec![vec![Tile::Empty; height]; width]; + for y in 0..height { + tiles[0][y] = Tile::Wall; + } + let step = 5; + let mut y = 0; + let mut spawn_points = Vec::new(); + for x in 0..width / 2 { + tiles[x][height - 1] = Tile::Wall; + if x % step == 0 { + let next_y = rng.gen_range(0, height - 5); + for y in min(y, next_y)..=max(y, next_y) { + tiles[x][y] = Tile::Wall; + } + if x != 0 { + let options = if max(next_y, y) - min(next_y, y) < 10 { + 3 + } else { + 2 + }; + match rng.gen_range(0, options) { + 0 => { + let x = if next_y > y { x - 1 } else { x + 1 }; + for y in (min(y, next_y) + 1)..=max(y, next_y) { + tiles[x][y] = Tile::Ladder; + } + } + 1 => { + let x_range = if next_y > y { + (x - 2)..x + } else { + (x + 1)..(x + 3) + }; + for x in x_range { + for y in + ((min(y, next_y))..=max(y, next_y)).step_by(3).skip(1) + { + tiles[x][y] = Tile::Platform; + } + } + } + 2 => { + let x = if next_y > y { x - 1 } else { x + 1 }; + tiles[x][min(y, next_y) + 1] = Tile::JumpPad; + } + _ => unreachable!(), + } + } + y = next_y; + } + tiles[x][y] = Tile::Wall; + if x > 0 { + spawn_points.push(vec2(r64(x as f64 + 0.5), r64(y as f64 + 1.0))); + spawn_points.push(vec2( + r64(width as f64 - x as f64 - 0.5), + r64(y as f64 + 1.0), + )); + } + } + for x in 0..width / 2 { + for y in 0..height { + tiles[width - 1 - x][y] = tiles[x][y].clone(); + } + } + (Level::new(tiles), spawn_points) + } + LevelOptions::Complex => complex::gen(rng), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Trans, Schematic)] +pub enum Tile { + Empty, + Wall, + Platform, + Ladder, + JumpPad, +} + +impl Tile { + pub fn is_blocking(&self) -> bool { + match self { + Tile::Empty => false, + Tile::Wall => true, + Tile::Platform => false, + Tile::Ladder => false, + Tile::JumpPad => false, + } + } + pub fn can_plant_mine(&self) -> bool { + match self { + Tile::Empty => false, + Tile::Wall => true, + Tile::Platform => true, + Tile::Ladder => false, + Tile::JumpPad => false, + } + } + pub fn is_walkable(&self) -> bool { + match self { + Tile::Empty => false, + Tile::Wall => true, + Tile::Platform => true, + Tile::Ladder => true, + Tile::JumpPad => false, + } + } + pub fn is_destructible(&self) -> bool { + match self { + _ => true, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Diff, PartialEq, Eq, Trans, Schematic)] +pub struct Level { + #[diff = "eq"] + pub tiles: Vec>, +} + +impl<'a> IntoIterator for &'a Level { + type Item = (Vec2, &'a Tile); + type IntoIter = Box + 'a>; + fn into_iter(self) -> Self::IntoIter { + Box::new(self.tiles.iter().enumerate().flat_map(move |(x, row)| { + row.iter() + .enumerate() + .map(move |(y, tile)| (vec2(x, y), tile)) + })) + } +} + +impl Level { + fn new(tiles: Vec>) -> Self { + Self { tiles } + } + pub fn get(&self, x: usize, y: usize) -> Option<&Tile> { + self.tiles.get(x).and_then(|row| row.get(y)) + } + pub fn iter(&self) -> impl Iterator, &Tile)> { + self.into_iter() + } + pub fn size(&self) -> Vec2 { + vec2(self.tiles.len(), self.tiles[0].len()) + } + pub fn find_tiles(&self, rect: AABB) -> impl Iterator, &Tile)> { + let start = rect + .bottom_left() + .map(|t| t.raw().floor().max(0.0) as usize); + let end = rect.top_right().map(|t| t.raw().ceil().max(0.0) as usize); + let start = vec2(min(start.x, self.size().x), min(start.y, self.size().y)); + let end = vec2(min(end.x, self.size().x), min(end.y, self.size().y)); + self.tiles[start.x..end.x] + .iter() + .enumerate() + .flat_map(move |(dx, row)| { + row[start.y..end.y] + .iter() + .enumerate() + .map(move |(dy, tile)| (vec2(start.x + dx, start.y + dy), tile)) + }) + } + pub fn find_tiles_mut( + &mut self, + rect: AABB, + ) -> impl Iterator, &mut Tile)> { + let start = rect + .bottom_left() + .map(|t| t.raw().floor().max(0.0) as usize); + let end = rect.top_right().map(|t| t.raw().ceil().max(0.0) as usize); + let end = vec2(min(end.x, self.size().x), min(end.y, self.size().y)); + self.tiles[start.x..end.x] + .iter_mut() + .enumerate() + .flat_map(move |(dx, row)| { + row[start.y..end.y] + .iter_mut() + .enumerate() + .map(move |(dy, tile)| (vec2(start.x + dx, start.y + dy), tile)) + }) + } + pub fn save( + &self, + mut out: impl Write, + spawn_points: &HashSet>, + ) -> std::io::Result<()> { + for row in (0..self.size().y).rev() { + for col in 0..self.size().x { + let c = if spawn_points.contains(&vec2(col, row)) { + 'P' + } else { + match self.tiles[col][row] { + Tile::Empty => '.', + Tile::Wall => '#', + Tile::Ladder => 'H', + Tile::JumpPad => 'T', + Tile::Platform => '^', + } + }; + write!(out, "{}", c)? + } + writeln!(out)? + } + Ok(()) + } +} diff --git a/app-src/src/model/loot.rs b/app-src/src/model/loot.rs new file mode 100644 index 0000000..4e47c18 --- /dev/null +++ b/app-src/src/model/loot.rs @@ -0,0 +1,31 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub enum Item { + HealthPack { health: i32 }, + Weapon { weapon_type: WeaponType }, + Mine {}, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct LootBox { + pub position: Vec2, + pub size: Vec2, + pub item: Item, +} + +impl LootBox { + pub fn spawn(properties: &Properties, position: Vec2, item: Item) -> Self { + Self { + position, + size: properties.loot_box_size, + item, + } + } + pub fn rect(&self) -> AABB { + AABB::pos_size( + self.position - vec2(self.size.x / r64(2.0), r64(0.0)), + self.size, + ) + } +} diff --git a/app-src/src/model/mine.rs b/app-src/src/model/mine.rs new file mode 100644 index 0000000..bd46a57 --- /dev/null +++ b/app-src/src/model/mine.rs @@ -0,0 +1,40 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Trans, Schematic)] +pub enum MineState { + Preparing, + Idle, + Triggered, + Exploded, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct Mine { + pub player_id: Id, + pub position: Vec2, + pub size: Vec2, + pub state: MineState, + pub timer: Option, + pub trigger_radius: R64, + pub explosion_params: ExplosionParams, +} + +impl Mine { + pub fn spawn(player_id: Id, properties: &Properties, position: Vec2) -> Self { + Self { + player_id, + position, + size: properties.mine_size, + state: MineState::Preparing, + timer: Some(properties.mine_prepare_time), + trigger_radius: properties.mine_trigger_radius, + explosion_params: properties.mine_explosion_params.clone(), + } + } + pub fn rect(&self) -> AABB { + AABB::pos_size( + self.position - vec2(self.size.x / r64(2.0), r64(0.0)), + self.size, + ) + } +} diff --git a/app-src/src/model/mod.rs b/app-src/src/model/mod.rs new file mode 100644 index 0000000..ee60631 --- /dev/null +++ b/app-src/src/model/mod.rs @@ -0,0 +1,323 @@ +use codegame::prelude::*; + +mod action; +mod bullet; +mod custom_data; +mod game; +mod level; +mod loot; +mod mine; +mod options; +mod properties; +mod unit; +mod weapon; + +pub use action::*; +pub use bullet::*; +pub use custom_data::*; +pub use game::*; +pub use level::*; +pub use loot::*; +pub use mine::*; +pub use options::*; +pub use properties::*; +pub use unit::*; +pub use weapon::*; + +#[derive(Debug, Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Copy, Trans, Schematic)] +pub struct Id(usize); + +impl Id { + pub fn new() -> Self { + static NEXT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1); + Self(NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst)) + } + pub fn raw(&self) -> usize { + self.0 + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct Player { + pub id: Id, + pub score: usize, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Diff, Trans, Schematic)] +pub struct Game { + #[diff = "clone"] + pub current_tick: usize, + #[diff = "eq"] + pub properties: Properties, + pub level: Level, + #[diff = "clone"] + pub players: Vec, + #[diff = "clone"] + pub units: Vec, + #[diff = "clone"] + pub bullets: Vec, + #[diff = "clone"] + pub mines: Vec, + #[diff = "clone"] + pub loot_boxes: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct PlayerView { + pub my_id: Id, + pub game: Game, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum ActionWrapper { + V0 { actions: HashMap }, + V1 { actions: HashMap }, +} + +impl ActionWrapper { + const MAGIC: i32 = 0xabcd; + + fn to_new(&self) -> HashMap { + match self { + Self::V0 { actions } => actions + .clone() + .into_iter() + .map(|(id, action)| (id, action.into())) + .collect(), + Self::V1 { actions } => actions.clone(), + } + } +} + +impl Default for ActionWrapper { + fn default() -> Self { + Self::V1 { actions: default() } + } +} + +impl Trans for ActionWrapper { + fn read_from(mut reader: impl std::io::Read) -> std::io::Result { + let x = i32::read_from(&mut reader)?; + Ok(match x { + ActionWrapper::MAGIC => Self::V1 { + actions: Trans::read_from(&mut reader)?, + }, + _ => { + let mut actions = HashMap::new(); + for _ in 0..x { + let key = Id::read_from(&mut reader)?; + let value = OldUnitAction::read_from(&mut reader)?; + actions.insert(key, value); + } + Self::V0 { actions } + } + }) + } + fn write_to(&self, mut writer: impl std::io::Write) -> std::io::Result<()> { + unimplemented!("Not used") + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Schematic)] +#[schematic(rename = "Versioned", magic = "ActionWrapper::MAGIC")] +struct LastVersionUnitAction { + inner: HashMap, +} + +impl Schematic for ActionWrapper { + fn create_schema() -> trans_schema::Schema { + LastVersionUnitAction::create_schema() + } +} + +pub type Action = ActionWrapper; +pub type Results = Vec; + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub enum Event { + LevelEvent { + used_tile: Vec2, + }, + Explosion { + position: Vec2, + player_id: Id, + params: ExplosionParams, + }, + Shot { + weapon_type: WeaponType, + }, + Heal, + PickupWeapon, + PickupMine, + Hit, +} + +impl codegame::Game for Game { + type Options = Options; + type OptionsPreset = crate::OptionsPreset; + type PlayerOptions = crate::PlayerOptions; + type PlayerExtraData = crate::PlayerExtraData; + type Action = Action; + type PlayerView = PlayerView; + type Event = Event; + type Results = Results; + type CustomData = CustomData; + + fn init(rng: &mut dyn rand::RngCore, player_count: usize, options: Options) -> Self { + let properties = options.properties.unwrap_or_default(); + let (level, spawn_points) = options.level.create(rng); + let mut players = Vec::new(); + let mut units = Vec::new(); + for i in 0..player_count { + let player = Player { + id: Id::new(), + score: 0, + }; + players.push(player); + } + let mut spawn_points = spawn_points.into_iter(); + for _ in 0..properties.team_size { + for player in &players { + units.push(Unit::spawn( + &properties, + player.id, + spawn_points.next().unwrap(), + )); + } + } + let mut loot_boxes = Vec::new(); + { + let mut possible_positions = Vec::new(); + for (pos, tile) in &level { + if pos.x < level.size().x / 2 + && pos.y > 0 + && tile == &Tile::Empty + && level.get(pos.x, pos.y - 1).unwrap().is_walkable() + && level.get(pos.x, pos.y) == level.get(level.size().x - 1 - pos.x, pos.y) + && level.get(pos.x, pos.y - 1) + == level.get(level.size().x - 1 - pos.x, pos.y - 1) + { + possible_positions.push(pos); + } + } + let loot_count = possible_positions.len() / 4; + for (i, &mut pos) in possible_positions + .partial_shuffle(rng, loot_count) + .0 + .into_iter() + .enumerate() + { + let item = if i < 3 { + Item::Weapon { + weapon_type: match i { + 0 => WeaponType::Pistol, + 1 => WeaponType::AssaultRifle, + 2 => WeaponType::RocketLauncher, + _ => unreachable!(), + }, + } + } else if rng.gen::() { + Item::HealthPack { + health: properties.health_pack_health, + } + } else { + Item::Mine {} + }; + for &pos in &[pos, vec2(level.size().x - 1 - pos.x, pos.y)] { + loot_boxes.push(LootBox::spawn( + &properties, + vec2(r64(pos.x as f64 + 0.5), r64(pos.y as f64)), + item.clone(), + )); + } + } + let mut used_positions = HashSet::new(); + for _ in 0..15 { + let pos = loop { + let pos = vec2( + rng.gen_range(0, level.size().x), + rng.gen_range(1, level.size().y), + ); + if level.tiles[pos.x][pos.y] == Tile::Empty + && level.tiles[pos.x][pos.y - 1].is_walkable() + && !used_positions.contains(&pos) + { + break pos; + } + }; + used_positions.insert(pos); + } + for (i, pos) in used_positions.into_iter().enumerate() {} + } + Self { + current_tick: 0, + properties, + level, + players, + units, + bullets: Vec::new(), + mines: Vec::new(), + loot_boxes, + } + } + + fn player_view(&self, player_index: usize) -> PlayerView { + PlayerView { + my_id: self.players[player_index].id, + game: self.clone(), + } + } + + fn process_turn( + &mut self, + rng: &mut dyn rand::RngCore, + actions: HashMap, + ) -> Vec { + let mut unit_actions = HashMap::new(); + for (player_index, action) in actions { + let player = &self.players[player_index]; + for (unit_id, action) in action.to_new() { + let unit = self.units.iter().find(|unit| unit.id == unit_id); + if let Some(unit) = unit { + if unit.player_id != player.id { + warn!("Received action for a unit not owned by the player"); + continue; + } + unit_actions.insert(unit_id, action); + } else { + warn!("Received action for non-existent unit"); + } + } + } + let mut events = Vec::new(); + let mut unit_swapped = HashSet::new(); + let mut unit_planted_mine = HashSet::new(); + for _ in 0..self.properties.updates_per_tick { + let delta_time = r64(1.0) + / self.properties.ticks_per_second + / r64(self.properties.updates_per_tick as _); + self.update( + rng, + &mut events, + &mut unit_planted_mine, + &mut unit_swapped, + &unit_actions, + delta_time, + ); + } + self.current_tick += 1; + events + } + fn finished(&self) -> bool { + self.units + .iter() + .map(|unit| unit.player_id) + .collect::>() + .len() + <= 1 + || self.current_tick >= self.properties.max_tick_count + } + fn results(&self) -> Self::Results { + self.players.iter().map(|player| player.score).collect() + } +} diff --git a/app-src/src/model/options.rs b/app-src/src/model/options.rs new file mode 100644 index 0000000..98dff2c --- /dev/null +++ b/app-src/src/model/options.rs @@ -0,0 +1,143 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Options { + pub level: LevelOptions, + pub properties: Option, +} + +impl Default for Options { + fn default() -> Self { + Self { + level: LevelOptions::EmptyBox { + width: 40, + height: 30, + }, + properties: None, + } + } +} + +use crate::*; + +#[cfg(feature = "rendering")] +mod config { + use super::*; + + pub struct OptionsConfig { + geng: Rc, + theme: Rc, + predefined_maps: Vec<(String, model::LevelOptions)>, + team_size: usize, + team_size_button: ui::TextButton, + current_option: usize, + level_button: ui::TextButton, + custom_level: Rc>>, + level_editor_button: ui::TextButton, + request_level_editor: bool, + } + + impl OptionsConfig { + pub fn new( + geng: &Rc, + theme: &Rc, + predefined_maps: Vec<(String, model::LevelOptions)>, + ) -> Self { + Self { + geng: geng.clone(), + theme: theme.clone(), + predefined_maps, + team_size: 1, + team_size_button: ui::TextButton::new(geng, theme, String::new(), 32.0), + current_option: 0, + level_button: ui::TextButton::new(geng, theme, String::new(), 32.0), + custom_level: Rc::new(RefCell::new(None)), + level_editor_button: ui::TextButton::new( + geng, + theme, + translate("level editor").to_owned(), + 32.0, + ), + request_level_editor: false, + } + } + } + + impl ui::Config for OptionsConfig { + fn get(&self) -> OptionsPreset { + OptionsPreset::Custom(model::Options { + level: self.predefined_maps[self.current_option].1.clone(), + properties: Some(model::Properties { + team_size: self.team_size, + ..default() + }), + }) + } + fn ui<'a>(&'a mut self) -> Box { + use ui::*; + if let Some(options) = &*self.custom_level.borrow() { + if self.predefined_maps.last().unwrap().0 == "custom" { + self.predefined_maps.last_mut().unwrap().1 = options.clone(); + } else { + self.current_option = self.predefined_maps.len(); + self.predefined_maps + .push(("custom".to_owned(), options.clone())); + } + } + self.level_button.text = self.predefined_maps[self.current_option].0.clone(); + self.team_size_button.text = self.team_size.to_string(); + let request_level_editor = &mut self.request_level_editor; + let ui = ui::column![ + row![ + text(translate("Team size:"), &self.theme.font, 32.0, Color::GRAY) + .padding_right(32.0), + self.team_size_button.ui(Box::new({ + let team_size = &mut self.team_size; + let change = ((*team_size) % 2) + 1; + move || { + *team_size = change; + } + })), + ] + .align(vec2(0.5, 0.5)), + row![ + text(translate("Level:"), &self.theme.font, 32.0, Color::GRAY) + .padding_right(32.0), + self.level_button.ui(Box::new({ + let current_option = &mut self.current_option; + let change = (*current_option + 1) % self.predefined_maps.len(); + move || { + *current_option = change; + } + })), + ] + .align(vec2(0.5, 0.5)), + // self.level_editor_button + // .ui(Box::new(move || { + // *request_level_editor = true; + // })) + // .align(vec2(0.5, 0.5)), + // self.properties.ui().fixed_size(vec2(300.0, 200.0)), + ]; + Box::new(ui) + } + } + + impl codegame::DeepConfig for OptionsConfig { + fn transition(&mut self) -> Option> { + // if self.request_level_editor { + // self.request_level_editor = false; + // return Some(Box::new(LevelEditor::new( + // &self.geng, + // &Rc::new(SimpleRenderer::new(&self.geng)), + // &self.theme, + // &self.custom_level, + // vec2(40, 30), + // ))); + // } + None + } + } +} +#[cfg(feature = "rendering")] +pub use config::OptionsConfig; diff --git a/app-src/src/model/properties.rs b/app-src/src/model/properties.rs new file mode 100644 index 0000000..e742285 --- /dev/null +++ b/app-src/src/model/properties.rs @@ -0,0 +1,109 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Trans, Schematic)] +pub struct Properties { + pub max_tick_count: usize, + pub team_size: usize, + pub ticks_per_second: R64, + pub updates_per_tick: usize, + pub loot_box_size: Vec2, + pub unit_size: Vec2, + pub unit_max_horizontal_speed: R64, + pub unit_fall_speed: R64, + pub unit_jump_time: R64, + pub unit_jump_speed: R64, + pub jump_pad_jump_time: R64, + pub jump_pad_jump_speed: R64, + pub unit_max_health: i32, + pub health_pack_health: i32, + pub weapon_params: HashMap, + pub mine_size: Vec2, + pub mine_explosion_params: ExplosionParams, + pub mine_prepare_time: R64, + pub mine_trigger_time: R64, + pub mine_trigger_radius: R64, + pub kill_score: usize, +} + +impl Default for Properties { + fn default() -> Self { + Self { + max_tick_count: 3_600, + team_size: 1, + ticks_per_second: r64(60.0), + #[cfg(target_arch = "wasm32")] + updates_per_tick: 1, + #[cfg(not(target_arch = "wasm32"))] + updates_per_tick: 100, + loot_box_size: vec2(r64(0.5), r64(0.5)), + unit_size: vec2(r64(0.9), r64(1.8)), + unit_max_horizontal_speed: r64(10.0), + unit_fall_speed: r64(10.0), + unit_jump_time: r64(0.55), + unit_jump_speed: r64(10.0), + jump_pad_jump_time: r64(0.525), + jump_pad_jump_speed: r64(20.0), + unit_max_health: 100, + health_pack_health: 50, + mine_size: vec2(r64(0.5), r64(0.5)), + mine_explosion_params: ExplosionParams { + radius: r64(3.0), + damage: 50, + }, + mine_prepare_time: r64(1.0), + mine_trigger_time: r64(0.5), + mine_trigger_radius: r64(1.0), + weapon_params: hashmap![ + WeaponType::Pistol => WeaponParams { + fire_rate: r64(0.4), + magazine_size: 8, + reload_time: r64(1.0), + min_spread: r64(0.05), + max_spread: r64(0.5), + recoil: r64(0.5), + aim_speed: r64(1.0), + bullet: BulletParams { + speed: r64(50.0), + size: r64(0.2), + damage: 20, + }, + explosion: None, + }, + WeaponType::AssaultRifle => WeaponParams { + fire_rate: r64(0.1), + magazine_size: 20, + reload_time: r64(1.0), + min_spread: r64(0.1), + max_spread: r64(0.5), + recoil: r64(0.2), + aim_speed: r64(1.9), + bullet: BulletParams { + speed: r64(50.0), + size: r64(0.2), + damage: 5, + }, + explosion: None, + }, + WeaponType::RocketLauncher => WeaponParams { + fire_rate: r64(1.0), + magazine_size: 1, + reload_time: r64(1.0), + min_spread: r64(0.1), + max_spread: r64(0.5), + recoil: r64(1.0), + aim_speed: r64(1.0), + bullet: BulletParams { + speed: r64(20.0), + size: r64(0.4), + damage: 30, + }, + explosion: Some(ExplosionParams{ + radius: r64(3.0), + damage: 50, + }), + }, + ], + kill_score: 1000, + } + } +} diff --git a/app-src/src/model/unit.rs b/app-src/src/model/unit.rs new file mode 100644 index 0000000..c3a529c --- /dev/null +++ b/app-src/src/model/unit.rs @@ -0,0 +1,64 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct JumpState { + pub can_jump: bool, + pub speed: R64, + pub max_time: R64, + pub can_cancel: bool, +} + +impl JumpState { + pub fn falling() -> Self { + Self { + can_jump: false, + speed: r64(0.0), + max_time: r64(0.0), + can_cancel: false, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct Unit { + pub player_id: Id, + pub id: Id, + pub health: i32, + pub position: Vec2, + pub size: Vec2, + pub jump_state: JumpState, + pub walked_right: bool, + pub stand: bool, + pub on_ground: bool, + pub on_ladder: bool, + pub mines: usize, + pub weapon: Option, +} + +impl Unit { + pub(crate) fn spawn(properties: &Properties, player_id: Id, position: Vec2) -> Self { + Self { + player_id, + id: Id::new(), + health: properties.unit_max_health, + position, + size: properties.unit_size, + jump_state: JumpState::falling(), + mines: 0, + weapon: None, + walked_right: true, + stand: true, + on_ground: false, + on_ladder: false, + } + } + pub fn rect(&self) -> AABB { + self.rect_at(self.position) + } + pub fn rect_at(&self, position: Vec2) -> AABB { + AABB::pos_size(position - vec2(self.size.x / r64(2.0), r64(0.0)), self.size) + } + pub fn center(&self) -> Vec2 { + self.position + vec2(r64(0.0), self.size.y / r64(2.0)) + } +} diff --git a/app-src/src/model/weapon.rs b/app-src/src/model/weapon.rs new file mode 100644 index 0000000..1c0fbed --- /dev/null +++ b/app-src/src/model/weapon.rs @@ -0,0 +1,137 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Trans, Schematic)] +pub struct ExplosionParams { + pub radius: R64, + pub damage: i32, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Trans, Schematic)] +pub struct BulletParams { + pub speed: R64, + pub size: R64, + pub damage: i32, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Trans, Schematic)] +pub struct WeaponParams { + pub magazine_size: usize, + pub fire_rate: R64, + pub reload_time: R64, + pub min_spread: R64, + pub max_spread: R64, + pub recoil: R64, + pub aim_speed: R64, + pub bullet: BulletParams, + pub explosion: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash, Trans, Schematic)] +pub enum WeaponType { + Pistol, + AssaultRifle, + RocketLauncher, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Trans, Schematic)] +pub struct Weapon { + pub typ: WeaponType, + pub params: WeaponParams, + pub magazine: usize, + pub was_shooting: bool, + pub spread: R64, + pub fire_timer: Option, + pub last_angle: Option, + pub last_fire_tick: Option, +} + +impl Weapon { + pub fn new(properties: &Properties, typ: WeaponType) -> Self { + let params = properties.weapon_params.get(&typ).unwrap().clone(); + Self { + typ, + spread: params.min_spread, + magazine: params.magazine_size, + was_shooting: false, + fire_timer: Some(params.reload_time), + params, + last_angle: None, + last_fire_tick: None, + } + } + pub fn reload(&mut self) { + if self.magazine == self.params.magazine_size { + return; + } + self.fire_timer = Some(self.params.reload_time); + self.magazine = self.params.magazine_size; + } + pub fn set_shoot(&mut self, shoot: bool) { + self.was_shooting = shoot; + } + pub fn update(&mut self, delta_time: R64) { + self.spread = max( + self.spread - self.params.aim_speed * delta_time, + self.params.min_spread, + ); + if let Some(timer) = self.fire_timer { + let timer = timer - delta_time; + if timer > r64(0.0) { + self.fire_timer = Some(timer); + } else { + self.fire_timer = None; + } + } + } + pub fn aim(&mut self, direction: Vec2) { + let angle = direction.arg(); + if let Some(last_angle) = self.last_angle { + let mut extra_spread = angle - last_angle; + while extra_spread >= R64::PI { + extra_spread -= r64(2.0) * R64::PI; + } + while extra_spread < -R64::PI { + extra_spread += r64(2.0) * R64::PI; + } + extra_spread = extra_spread.abs(); + self.spread = min(self.spread + extra_spread, self.params.max_spread); + } + self.last_angle = Some(angle); + } + pub fn fire( + &mut self, + current_tick: usize, + unit_id: Id, + player_id: Id, + position: Vec2, + direction: Vec2, + rng: &mut dyn rand::RngCore, + ) -> Option { + if self.fire_timer.is_some() || self.magazine == 0 { + return None; + } + self.last_fire_tick = Some(current_tick); + self.magazine -= 1; + let direction = Vec2::rotated( + direction, + r64(distributions::Uniform::from(-self.spread.raw()..=self.spread.raw()).sample(rng)), + ); + self.spread = min(self.spread + self.params.recoil, self.params.max_spread); + if self.magazine == 0 { + self.magazine = self.params.magazine_size; + self.fire_timer = Some(self.params.reload_time); + } else { + self.fire_timer = Some(self.params.fire_rate); + } + Some(Bullet { + weapon_type: self.typ, + unit_id, + player_id, + position, + velocity: direction.normalize() * self.params.bullet.speed, + damage: self.params.bullet.damage, + size: self.params.bullet.size, + explosion_params: self.params.explosion.clone(), + }) + } +} diff --git a/app-src/src/quickstart_player.rs b/app-src/src/quickstart_player.rs new file mode 100644 index 0000000..a0ef59c --- /dev/null +++ b/app-src/src/quickstart_player.rs @@ -0,0 +1,145 @@ +use crate::*; + +pub struct QuickstartPlayer {} + +impl QuickstartPlayer { + pub fn new() -> Self { + Self {} + } +} + +impl codegame::Player for QuickstartPlayer { + fn get_action( + &mut self, + view: &model::PlayerView, + _custom_data_handler: Option<&dyn Fn(model::CustomData)>, + ) -> Result { + Ok(model::ActionWrapper::V1 { + actions: view + .game + .units + .iter() + .filter(|unit| unit.player_id == view.my_id) + .map(|unit| { + let nearest_enemy = view + .game + .units + .iter() + .filter(|other| other.player_id != unit.player_id) + .min_by(|a, b| { + std::cmp::PartialOrd::partial_cmp( + &(a.position - unit.position).len(), + &(b.position - unit.position).len(), + ) + .unwrap() + }); + let nearest_weapon = view + .game + .loot_boxes + .iter() + .filter(|loot| { + if let model::Item::Weapon { .. } = loot.item { + true + } else { + false + } + }) + .min_by(|a, b| { + std::cmp::PartialOrd::partial_cmp( + &(a.position - unit.position).len(), + &(b.position - unit.position).len(), + ) + .unwrap() + }); + let target_pos = if unit.weapon.is_none() { + nearest_weapon.map(|loot| loot.position) + } else { + nearest_enemy.map(|enemy| enemy.position) + } + .unwrap_or(unit.position); + let direction = if target_pos.x > unit.position.x { + 1 + } else { + -1 + }; + let jump = target_pos.y > unit.position.y + || view.game.level.get( + (unit.position.x.raw() as i32 + direction) as usize, + unit.position.y.raw() as usize, + ) == Some(&model::Tile::Wall); + ( + unit.id, + model::UnitAction { + velocity: target_pos.x - unit.position.x, + jump, + jump_down: !jump, + aim: nearest_enemy + .map(|enemy| enemy.position - unit.position) + .unwrap_or(vec2(r64(0.0), r64(0.0))), + shoot: true, + reload: false, + swap_weapon: false, + plant_mine: false, + }, + ) + }) + .collect(), + }) + } +} + +#[cfg(feature = "rendering")] +mod config { + use super::*; + + #[derive(Clone)] + pub struct Config { + geng: Rc, + theme: Rc, + } + + impl codegame::PlayerConfig for Config { + fn name(&self) -> &str { + "quickstart" + } + fn ui<'a>(&'a mut self) -> Box { + use ui::*; + let ui = ui::text( + translate("Quickstart strategy"), + &self.theme.font, + 16.0, + Color::GRAY, + ) + .align(vec2(0.5, 1.0)); + Box::new(ui) + } + fn ready(&mut self) -> bool { + true + } + fn get(&mut self) -> Box> { + Box::new(QuickstartPlayer::new()) + } + fn to_options(&self) -> PlayerOptions { + PlayerOptions::Quickstart + } + } + + impl Config { + pub fn new(geng: &Rc, theme: &Rc) -> Self { + Self { + geng: geng.clone(), + theme: theme.clone(), + } + } + pub fn constructor( + geng: &Rc, + theme: &Rc, + ) -> Box Box>> { + let geng = geng.clone(); + let theme = theme.clone(); + Box::new(move || Box::new(Self::new(&geng, &theme))) + } + } +} +#[cfg(feature = "rendering")] +pub use config::Config; diff --git a/app-src/src/random_player.rs b/app-src/src/random_player.rs new file mode 100644 index 0000000..cfe86a4 --- /dev/null +++ b/app-src/src/random_player.rs @@ -0,0 +1,138 @@ +use crate::*; + +pub struct RandomPlayer { + think_ticks: usize, + next_think: usize, + last_action: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RandomPlayerOptions { + pub think_ticks: usize, +} + +impl RandomPlayer { + pub fn new(options: RandomPlayerOptions) -> Self { + Self { + think_ticks: options.think_ticks, + next_think: 0, + last_action: None, + } + } +} + +impl codegame::Player for RandomPlayer { + fn get_action( + &mut self, + view: &model::PlayerView, + _custom_data_handler: Option<&dyn Fn(model::CustomData)>, + ) -> Result { + if self.next_think == 0 || self.last_action.is_none() { + self.last_action = Some(model::ActionWrapper::V1 { + actions: view + .game + .units + .iter() + .filter(|unit| unit.player_id == view.my_id) + .map(|unit| { + ( + unit.id, + model::UnitAction { + velocity: r64(global_rng().gen_range( + -view.game.properties.unit_max_horizontal_speed.raw(), + view.game.properties.unit_max_horizontal_speed.raw(), + )), + jump: global_rng().gen(), + jump_down: global_rng().gen(), + aim: Vec2::rotated( + vec2(r64(1.0), r64(0.0)), + r64(global_rng().gen_range(0.0, 2.0 * std::f64::consts::PI)), + ), + shoot: global_rng().gen(), + reload: global_rng().gen(), + swap_weapon: global_rng().gen(), + plant_mine: global_rng().gen(), + }, + ) + }) + .collect(), + }); + self.next_think = self.think_ticks; + } + self.next_think -= 1; + Ok(self.last_action.clone().unwrap()) + } +} + +#[cfg(feature = "rendering")] +mod config { + use super::*; + + #[derive(Clone)] + pub struct Config { + geng: Rc, + theme: Rc, + slider: ui::Slider, + options: RandomPlayerOptions, + } + + impl codegame::PlayerConfig for Config { + fn name(&self) -> &str { + translate("random") + } + fn ui<'a>(&'a mut self) -> Box { + use ui::*; + let think_ticks = &mut self.options.think_ticks; + let ui = row![ + text( + translate("thinking speed"), + &self.theme.font, + 16.0, + Color::GRAY + ) + .padding_right(16.0), + self.slider + .ui( + *think_ticks as f64, + 1.0..=120.0, + Box::new(move |new_value| { + *think_ticks = new_value as usize; + }), + ) + .fixed_size(vec2(50.0, 16.0)), + ] + .align(vec2(0.5, 1.0)); + Box::new(ui) + } + fn ready(&mut self) -> bool { + true + } + fn get(&mut self) -> Box> { + Box::new(RandomPlayer::new(self.options.clone())) + } + fn to_options(&self) -> PlayerOptions { + PlayerOptions::Random(self.options.clone()) + } + } + + impl Config { + pub fn new(geng: &Rc, theme: &Rc) -> Self { + Self { + geng: geng.clone(), + theme: theme.clone(), + options: RandomPlayerOptions { think_ticks: 60 }, + slider: geng::ui::Slider::new(geng, theme), + } + } + pub fn constructor( + geng: &Rc, + theme: &Rc, + ) -> Box Box>> { + let geng = geng.clone(); + let theme = theme.clone(); + Box::new(move || Box::new(Self::new(&geng, &theme))) + } + } +} +#[cfg(feature = "rendering")] +pub use config::Config; diff --git a/app-src/src/renderer/background.rs b/app-src/src/renderer/background.rs new file mode 100644 index 0000000..1654950 --- /dev/null +++ b/app-src/src/renderer/background.rs @@ -0,0 +1,63 @@ +use super::*; + +#[derive(geng::Assets)] +pub struct Assets { + #[asset(path = "city.png")] + city: ugli::Texture, + #[asset(path = "cloud.png")] + cloud: ugli::Texture, +} + +pub struct BackgroundRenderer { + geng: Rc, + assets: Assets, +} + +impl BackgroundRenderer { + const CLOUDS_Y: f32 = 160.0 / 576.0; + const CLOUD_SPEED: f32 = 1.0 / 60.0; + pub fn new(geng: &Rc, mut assets: Assets) -> Self { + assets.city.set_filter(ugli::Filter::Nearest); + assets.cloud.set_filter(ugli::Filter::Nearest); + Self { + geng: geng.clone(), + assets, + } + } + pub fn draw(&mut self, framebuffer: &mut ugli::Framebuffer, game: &model::Game) { + let framebuffer_size = framebuffer.size().map(|x| x as f32); + self.geng.draw_2d().textured_quad( + framebuffer, + AABB::pos_size(vec2(0.0, 0.0), framebuffer_size), + &self.assets.city, + Color::WHITE, + ); + let pos = (game.current_tick as f32 / game.properties.ticks_per_second.raw() as f32 + * Self::CLOUD_SPEED) + .fract(); + self.geng.draw_2d().textured_quad( + framebuffer, + AABB::pos_size( + vec2( + pos * framebuffer_size.x, + framebuffer_size.y * (1.0 - Self::CLOUDS_Y), + ), + vec2(framebuffer_size.x, framebuffer_size.y * Self::CLOUDS_Y), + ), + &self.assets.cloud, + Color::WHITE, + ); + self.geng.draw_2d().textured_quad( + framebuffer, + AABB::pos_size( + vec2( + pos * framebuffer_size.x - framebuffer_size.x, + framebuffer_size.y * (1.0 - Self::CLOUDS_Y), + ), + vec2(framebuffer_size.x, framebuffer_size.y * Self::CLOUDS_Y), + ), + &self.assets.cloud, + Color::WHITE, + ); + } +} diff --git a/app-src/src/renderer/bullet.rs b/app-src/src/renderer/bullet.rs new file mode 100644 index 0000000..914facf --- /dev/null +++ b/app-src/src/renderer/bullet.rs @@ -0,0 +1,24 @@ +use super::*; + +pub struct BulletRenderer { + simple_renderer: Rc, +} + +impl BulletRenderer { + pub fn new(simple_renderer: &Rc) -> Self { + Self { + simple_renderer: simple_renderer.clone(), + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + for bullet in &game.bullets { + self.simple_renderer + .quad(framebuffer, camera, bullet.rect(), Color::WHITE); + } + } +} diff --git a/app-src/src/renderer/camera.rs b/app-src/src/renderer/camera.rs new file mode 100644 index 0000000..34b0a8b --- /dev/null +++ b/app-src/src/renderer/camera.rs @@ -0,0 +1,47 @@ +use super::*; + +pub struct Camera { + pub center: Vec2, + pub fov: R64, +} + +impl Camera { + pub fn new() -> Self { + Self { + center: vec2(r64(0.0), r64(0.0)), + fov: r64(100.0), + } + } + fn view_matrix(&self) -> Mat4 { + Mat4::scale_uniform(r64(1.0) / self.fov) * Mat4::translate(-self.center.extend(r64(0.0))) + } + fn projection_matrix(&self, framebuffer: &ugli::Framebuffer) -> Mat4 { + let framebuffer_size = framebuffer.size().map(|x| r64(x as _)); + Mat4::scale(vec3(framebuffer_size.y / framebuffer_size.x, r64(1.0), r64(1.0)) * r64(2.0)) + } + pub fn uniforms(&self, framebuffer: &ugli::Framebuffer) -> impl ugli::Uniforms { + ugli::uniforms! { + u_projection_matrix: self.projection_matrix(framebuffer).map(|x| x.raw() as f32), + u_view_matrix: self.view_matrix().map(|x| x.raw() as f32), + } + } + pub fn screen_to_world(&self, framebuffer: &ugli::Framebuffer, pos: Vec2) -> Vec2 { + let framebuffer_size = framebuffer.size().map(|x| r64(x as _)); + let pos = vec2( + pos.x / framebuffer_size.x * r64(2.0) - r64(1.0), + pos.y / framebuffer_size.y * r64(2.0) - r64(1.0), + ); + let pos = (self.projection_matrix(framebuffer) * self.view_matrix()).inverse() + * pos.extend(r64(0.0)).extend(r64(1.0)); + pos.xy() + } + pub fn world_to_screen(&self, framebuffer: &ugli::Framebuffer, pos: Vec2) -> Vec2 { + let framebuffer_size = framebuffer.size().map(|x| r64(x as _)); + let pos = (self.projection_matrix(framebuffer) * self.view_matrix()) + * pos.extend(r64(0.0)).extend(r64(1.0)); + vec2( + (pos.x + r64(1.0)) / r64(2.0) * framebuffer_size.x, + (pos.y + r64(1.0)) / r64(2.0) * framebuffer_size.y, + ) + } +} diff --git a/app-src/src/renderer/custom/mod.rs b/app-src/src/renderer/custom/mod.rs new file mode 100644 index 0000000..109ff34 --- /dev/null +++ b/app-src/src/renderer/custom/mod.rs @@ -0,0 +1,152 @@ +use super::*; + +use model::ColoredVertex as Vertex; + +pub struct CustomRenderer { + geng: Rc, + program: ugli::Program, + geometry: ugli::VertexBuffer, +} + +impl CustomRenderer { + pub fn new(geng: &Rc) -> Self { + Self { + geng: geng.clone(), + program: geng + .shader_lib() + .compile(include_str!("program.glsl")) + .unwrap(), + geometry: ugli::VertexBuffer::new_dynamic(geng.ugli(), Vec::new()), + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera_uniforms: impl ugli::Uniforms, + geometry: &[Vertex], + ) { + self.geometry.clear(); + self.geometry.extend_from_slice(geometry); + ugli::draw( + framebuffer, + &self.program, + ugli::DrawMode::TriangleFan, + &self.geometry, + camera_uniforms, + ugli::DrawParameters { + blend_mode: Some(default()), + ..default() + }, + ); + } + + pub fn draw_all( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + custom_data: &HashMap>, + ) { + let camera_uniforms = camera.uniforms(framebuffer); + let mut y = framebuffer.size().y as f32 - 100.0; + for player_custom_data in custom_data.values() { + for data in player_custom_data { + match *data { + model::CustomData::Log { ref text } => { + self.geng.default_font().draw( + framebuffer, + text, + vec2(10.0, y), + 20.0, + Color::WHITE, + ); + y += 20.0; + } + model::CustomData::PlacedText { + ref text, + pos, + size, + alignment, + color, + } => { + if size > 1.0 { + let pos = camera + .world_to_screen(framebuffer, pos.map(|x| r64(x as f64))) + .map(|x| x.raw() as f32); + let align = match alignment { + model::TextAlignment::Left => 0.0, + model::TextAlignment::Center => 0.5, + model::TextAlignment::Right => 1.0, + }; + self.geng.default_font().draw_aligned( + framebuffer, + text, + pos, + align, + size, + color, + ); + } + } + model::CustomData::Rect { pos, size, color } => { + self.draw( + framebuffer, + &camera_uniforms, + &[ + Vertex { + position: pos, + color, + }, + Vertex { + position: pos + vec2(size.x, 0.0), + color, + }, + Vertex { + position: pos + size, + color, + }, + Vertex { + position: pos + vec2(0.0, size.y), + color, + }, + ], + ); + } + model::CustomData::Line { + p1, + p2, + width, + color, + } => { + let n = (p2 - p1).rotate_90().normalize(); + let width = width / 2.0; + self.draw( + framebuffer, + &camera_uniforms, + &[ + Vertex { + position: p1 + n * width, + color, + }, + Vertex { + position: p2 + n * width, + color, + }, + Vertex { + position: p2 - n * width, + color, + }, + Vertex { + position: p1 - n * width, + color, + }, + ], + ); + } + model::CustomData::Polygon { ref vertices } => { + self.draw(framebuffer, &camera_uniforms, vertices); + } + } + } + } + } +} diff --git a/app-src/src/renderer/custom/program.glsl b/app-src/src/renderer/custom/program.glsl new file mode 100644 index 0000000..aa871f1 --- /dev/null +++ b/app-src/src/renderer/custom/program.glsl @@ -0,0 +1,20 @@ +varying vec4 v_color; + +#ifdef VERTEX_SHADER +attribute vec2 position; +attribute vec4 color; + +uniform mat4 u_projection_matrix; +uniform mat4 u_view_matrix; + +void main() { + v_color = color; + gl_Position = u_projection_matrix * u_view_matrix * vec4(position, 0.0, 1.0); +} +#endif + +#ifdef FRAGMENT_SHADER +void main() { + gl_FragColor = v_color; +} +#endif \ No newline at end of file diff --git a/app-src/src/renderer/fancy_bullet.rs b/app-src/src/renderer/fancy_bullet.rs new file mode 100644 index 0000000..7bc0815 --- /dev/null +++ b/app-src/src/renderer/fancy_bullet.rs @@ -0,0 +1,44 @@ +use super::*; + +pub struct FancyBulletRenderer { + sprite_renderer: Rc, + weapon_assets: Rc, +} + +impl FancyBulletRenderer { + pub fn new( + sprite_renderer: &Rc, + weapon_assets: &Rc, + ) -> Self { + Self { + sprite_renderer: sprite_renderer.clone(), + weapon_assets: weapon_assets.clone(), + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + let camera_uniforms = camera.uniforms(framebuffer); + for bullet in &game.bullets { + let texture = match bullet.weapon_type { + model::WeaponType::Pistol => &self.weapon_assets.pistol_bullet, + model::WeaponType::AssaultRifle => &self.weapon_assets.assault_rifle_bullet, + model::WeaponType::RocketLauncher => &self.weapon_assets.rocket_launcher_bullet, + }; + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + texture, + vec2(0.5, 0.5), + 0.04, + bullet.position.map(|x| x.raw() as f32), + bullet.velocity.arg().raw() as f32, + false, + Color::WHITE, + ); + } + } +} diff --git a/app-src/src/renderer/fancy_explosion.rs b/app-src/src/renderer/fancy_explosion.rs new file mode 100644 index 0000000..f8e482e --- /dev/null +++ b/app-src/src/renderer/fancy_explosion.rs @@ -0,0 +1,61 @@ +use super::*; + +#[derive(geng::Assets)] +pub struct Assets { + #[asset(path = "explosion.png")] + pub explosion: ugli::Texture, +} + +impl Assets { + fn setup(&mut self) { + self.explosion.set_filter(ugli::Filter::Nearest); + } +} + +pub struct FancyExplosionRenderer { + sprite_renderer: Rc, + assets: Assets, +} + +impl FancyExplosionRenderer { + const DURATION: f32 = 0.2; + pub fn new(sprite_renderer: &Rc, mut assets: Assets) -> Self { + assets.setup(); + Self { + sprite_renderer: sprite_renderer.clone(), + assets, + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + explosions: &[Explosion], + game: &model::Game, + ) { + let camera_uniforms = camera.uniforms(framebuffer); + let texture = &self.assets.explosion; + let texture_size = texture.size().map(|x| x as f32); + for explosion in explosions { + let size = 2.0 * explosion.params.radius.raw() as f32 / texture_size.y; + let t = (game.current_tick - explosion.tick) as f32 + / game.properties.ticks_per_second.raw() as f32 + / Self::DURATION; + if t > 1.0 { + continue; + } + let size = size * t; + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + texture, + vec2(0.5, 0.5), + size, + explosion.position.map(|x| x.raw() as f32), + 0.0, + false, + Color::rgba(1.0, 1.0, 1.0, 1.0 - t), + ); + } + } +} diff --git a/app-src/src/renderer/fancy_level/mod.rs b/app-src/src/renderer/fancy_level/mod.rs new file mode 100644 index 0000000..3785e1b --- /dev/null +++ b/app-src/src/renderer/fancy_level/mod.rs @@ -0,0 +1,387 @@ +use super::*; + +#[derive(geng::Assets)] +#[allow(non_snake_case)] +pub struct Assets { + #[asset(path = "jumper_1.png")] + jumper_1: ugli::Texture, + #[asset(path = "jumper_2.png")] + jumper_2: ugli::Texture, + #[asset(path = "jumper_3.png")] + jumper_3: ugli::Texture, + #[asset(path = "wall_down_L.png")] + wall_down_L: ugli::Texture, + #[asset(path = "wall_down_R.png")] + wall_down_R: ugli::Texture, + #[asset(path = "wall_down_C.png")] + wall_down_C: ugli::Texture, + #[asset(path = "wall_down_solo.png")] + wall_down_solo: ugli::Texture, + #[asset(path = "wall_L.png")] + wall_L: ugli::Texture, + #[asset(path = "wall_C.png")] + wall_C: ugli::Texture, + #[asset(path = "wall_R.png")] + wall_R: ugli::Texture, + #[asset(path = "wall_solo.png")] + wall_solo: ugli::Texture, + #[asset(path = "window_L.png")] + window_L: ugli::Texture, + #[asset(path = "window_R.png")] + window_R: ugli::Texture, + #[asset(path = "window_C.png")] + window_C: ugli::Texture, + #[asset(path = "window_solo.png")] + window_solo: ugli::Texture, + #[asset(path = "railing_L.png")] + railing_L: ugli::Texture, + #[asset(path = "railing_C.png")] + railing_C: ugli::Texture, + #[asset(path = "railing_R.png")] + railing_R: ugli::Texture, + #[asset(path = "platform_C.png")] + platform_C: ugli::Texture, + #[asset(path = "platform_L.png")] + platform_L: ugli::Texture, + #[asset(path = "platform_R.png")] + platform_R: ugli::Texture, + #[asset(path = "platform_solo.png")] + platform_solo: ugli::Texture, + #[asset(path = "floor_1.png")] + floor_1: ugli::Texture, + #[asset(path = "floor_2.png")] + floor_2: ugli::Texture, + #[asset(path = "door.png")] + door: ugli::Texture, + #[asset(path = "platform_small_L.png")] + platform_small_L: ugli::Texture, + #[asset(path = "platform_small_C.png")] + platform_small_C: ugli::Texture, + #[asset(path = "platform_small_R.png")] + platform_small_R: ugli::Texture, + #[asset(path = "platform_small_solo.png")] + platform_small_solo: ugli::Texture, + #[asset(path = "stairs.png")] + stairs: ugli::Texture, +} + +#[allow(non_snake_case)] +struct TextureAtlas { + atlas: geng::TextureAtlas, + jumper_1: usize, + jumper_2: usize, + jumper_3: usize, + wall_down_L: usize, + wall_down_R: usize, + wall_down_C: usize, + wall_down_solo: usize, + wall_L: usize, + wall_C: usize, + wall_R: usize, + wall_solo: usize, + window_L: usize, + window_R: usize, + window_C: usize, + window_solo: usize, + railing_L: usize, + railing_C: usize, + railing_R: usize, + platform_C: usize, + platform_L: usize, + platform_R: usize, + platform_solo: usize, + floor_1: usize, + floor_2: usize, + door: usize, + platform_small_L: usize, + platform_small_C: usize, + platform_small_R: usize, + platform_small_solo: usize, + stairs: usize, +} + +impl TextureAtlas { + fn new(ugli: &Rc, assets: Assets) -> Self { + let mut atlas = geng::TextureAtlas::new( + ugli, + &[ + &assets.jumper_1, + &assets.jumper_2, + &assets.jumper_3, + &assets.wall_down_L, + &assets.wall_down_R, + &assets.wall_down_C, + &assets.wall_down_solo, + &assets.wall_L, + &assets.wall_C, + &assets.wall_R, + &assets.wall_solo, + &assets.window_L, + &assets.window_R, + &assets.window_C, + &assets.window_solo, + &assets.railing_L, + &assets.railing_C, + &assets.railing_R, + &assets.platform_C, + &assets.platform_L, + &assets.platform_R, + &assets.platform_solo, + &assets.floor_1, + &assets.floor_2, + &assets.door, + &assets.platform_small_L, + &assets.platform_small_C, + &assets.platform_small_R, + &assets.platform_small_solo, + &assets.stairs, + ], + ); + atlas.set_filter(ugli::Filter::Nearest); + Self { + atlas, + jumper_1: 0, + jumper_2: 1, + jumper_3: 2, + wall_down_L: 3, + wall_down_R: 4, + wall_down_C: 5, + wall_down_solo: 6, + wall_L: 7, + wall_C: 8, + wall_R: 9, + wall_solo: 10, + window_L: 11, + window_R: 12, + window_C: 13, + window_solo: 14, + railing_L: 15, + railing_C: 16, + railing_R: 17, + platform_C: 18, + platform_L: 19, + platform_R: 20, + platform_solo: 21, + floor_1: 22, + floor_2: 23, + door: 24, + platform_small_L: 25, + platform_small_C: 26, + platform_small_R: 27, + platform_small_solo: 28, + stairs: 29, + } + } +} + +#[derive(ugli::Vertex)] +struct Vertex { + a_pos: Vec2, +} + +#[derive(ugli::Vertex)] +struct Instance { + i_pos: Vec2, + i_color: Color, + i_uv_pos: Vec2, + i_uv_size: Vec2, +} + +pub struct FancyLevelRenderer { + program: ugli::Program, + texture_atlas: TextureAtlas, + vertices: ugli::VertexBuffer, + instances: ugli::VertexBuffer, +} + +impl FancyLevelRenderer { + const JUMP_PAD_ANIMATION_TIME: f32 = 0.2; + pub fn new(geng: &Rc, assets: Assets) -> Self { + Self { + program: geng + .shader_lib() + .compile(include_str!("program.glsl")) + .unwrap(), + texture_atlas: TextureAtlas::new(geng.ugli(), assets), + vertices: ugli::VertexBuffer::new_static( + geng.ugli(), + vec![ + Vertex { + a_pos: vec2(0.0, 0.0), + }, + Vertex { + a_pos: vec2(1.0, 0.0), + }, + Vertex { + a_pos: vec2(1.0, 1.0), + }, + Vertex { + a_pos: vec2(0.0, 1.0), + }, + ], + ), + instances: ugli::VertexBuffer::new_dynamic(geng.ugli(), Vec::new()), + } + } + fn add_quad(&mut self, x: usize, y: usize, texture: usize) { + self.add_colored_quad(x, y, texture, Color::WHITE); + } + fn add_colored_quad(&mut self, x: usize, y: usize, texture: usize, color: Color) { + let uv_rect = self.texture_atlas.atlas.uv(texture); + self.instances.push(Instance { + i_pos: vec2(x as f32, y as f32), + i_color: color, + i_uv_pos: uv_rect.bottom_left(), + i_uv_size: uv_rect.size(), + }); + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + level: &model::Level, + game: Option<&model::Game>, + extra_data: Option<&RendererExtraData>, + ) { + self.instances.clear(); + let mut has_ceiling = vec![false; level.size().x]; + for y in (0..level.size().y - 1).rev() { + for x in 1..level.size().x - 1 { + if level.get(x, y) == Some(&model::Tile::Wall) { + has_ceiling[x] = true; + } + } + for x in 0..level.size().x { + if has_ceiling[x] { + let has_left = x > 0 && has_ceiling[x - 1]; + let has_right = has_ceiling.get(x + 1) == Some(&true); + let texture = + if y == 1 || y > 0 && level.get(x, y - 1) == Some(&model::Tile::Wall) { + match (has_left, has_right) { + (true, true) => self.texture_atlas.wall_down_C, + (true, false) => self.texture_atlas.wall_down_R, + (false, true) => self.texture_atlas.wall_down_L, + (false, false) => self.texture_atlas.wall_down_solo, + } + } else { + match (has_left, has_right) { + (true, true) => self.texture_atlas.wall_C, + (true, false) => self.texture_atlas.wall_R, + (false, true) => self.texture_atlas.wall_L, + (false, false) => self.texture_atlas.wall_solo, + } + }; + self.add_quad(x, y, texture); + } + } + } + for (pos, tile) in level { + if pos.y == 0 { + let texture = if pos.x % 3 == 0 { + self.texture_atlas.floor_1 + } else { + self.texture_atlas.floor_2 + }; + self.add_quad(pos.x, pos.y, texture); + continue; + } else if pos.x == 0 || pos.x + 1 == level.size().x || pos.y + 1 == level.size().y { + continue; + } + match tile { + model::Tile::Empty => {} + model::Tile::Wall => { + let has_left = + pos.x > 0 && level.get(pos.x - 1, pos.y) == Some(&model::Tile::Wall); + let has_right = level.get(pos.x + 1, pos.y) == Some(&model::Tile::Wall); + let texture = match (has_left, has_right) { + (true, true) => self.texture_atlas.platform_C, + (true, false) => self.texture_atlas.platform_R, + (false, true) => self.texture_atlas.platform_L, + (false, false) => self.texture_atlas.platform_solo, + }; + self.add_quad(pos.x, pos.y, texture); + } + model::Tile::Platform => { + let has_left = + pos.x > 0 && level.get(pos.x - 1, pos.y) == Some(&model::Tile::Platform); + let has_right = level.get(pos.x + 1, pos.y) == Some(&model::Tile::Platform); + let texture = match (has_left, has_right) { + (true, true) => self.texture_atlas.platform_small_C, + (true, false) => self.texture_atlas.platform_small_R, + (false, true) => self.texture_atlas.platform_small_L, + (false, false) => self.texture_atlas.platform_small_solo, + }; + self.add_quad(pos.x, pos.y, texture); + } + model::Tile::Ladder => {} + model::Tile::JumpPad => {} + } + } + for y in (2..level.size().y).rev() { + let mut has_rail = vec![false; level.size().x]; + for x in 0..level.size().x { + has_rail[x] = level.get(x, y) != Some(&model::Tile::Wall) + && y > 0 + && level.get(x, y - 1) == Some(&model::Tile::Wall); + } + for x in 0..level.size().x { + if has_rail[x] { + let has_left = x > 0 && has_rail[x - 1]; + let has_right = has_rail.get(x + 1) == Some(&true); + let texture = match (has_left, has_right) { + (true, true) => self.texture_atlas.railing_C, + (true, false) => self.texture_atlas.railing_R, + (false, true) => self.texture_atlas.railing_L, + (false, false) => continue, + }; + self.add_quad(x, y, texture); + } + } + } + for (pos, tile) in level { + match tile { + model::Tile::Ladder => { + self.add_quad(pos.x, pos.y, self.texture_atlas.stairs); + } + model::Tile::JumpPad => { + let mut texture = self.texture_atlas.jumper_1; + if let Some(game) = game { + if let Some(last_used_tick) = extra_data.and_then(|extra_data| { + extra_data.level_last_used_tick.get(&pos).copied() + }) { + let t = ((game.current_tick as f32 - 1.0 - last_used_tick as f32) + / game.properties.ticks_per_second.raw() as f32 + / Self::JUMP_PAD_ANIMATION_TIME) + .min(1.0); + let t = if t < 0.0 { 1.0 } else { t }; + if t < 0.33 { + texture = self.texture_atlas.jumper_3; + } else if t < 0.66 { + texture = self.texture_atlas.jumper_2; + } + } + } + self.add_quad(pos.x, pos.y, texture); + } + _ => {} + } + } + let camera_uniforms = camera.uniforms(framebuffer); + ugli::draw( + framebuffer, + &self.program, + ugli::DrawMode::TriangleFan, + ugli::instanced(&self.vertices, &self.instances), + ( + camera_uniforms, + ugli::uniforms! { + u_texture: self.texture_atlas.atlas.texture(), + }, + ), + ugli::DrawParameters { + blend_mode: Some(default()), + ..default() + }, + ); + } +} diff --git a/app-src/src/renderer/fancy_level/program.glsl b/app-src/src/renderer/fancy_level/program.glsl new file mode 100644 index 0000000..295fbd4 --- /dev/null +++ b/app-src/src/renderer/fancy_level/program.glsl @@ -0,0 +1,27 @@ +varying vec4 v_color; +varying vec2 v_uv; + +#ifdef VERTEX_SHADER +attribute vec2 a_pos; + +attribute vec2 i_pos; +attribute vec4 i_color; +attribute vec2 i_uv_pos; +attribute vec2 i_uv_size; + +uniform mat4 u_projection_matrix; +uniform mat4 u_view_matrix; + +void main() { + v_color = i_color; + v_uv = i_uv_pos + vec2(a_pos.x, 1.0 - a_pos.y) * i_uv_size; + gl_Position = u_projection_matrix * u_view_matrix * vec4(i_pos + a_pos, 0.0, 1.0); +} +#endif + +#ifdef FRAGMENT_SHADER +uniform sampler2D u_texture; +void main() { + gl_FragColor = texture2D(u_texture, v_uv) * v_color; +} +#endif \ No newline at end of file diff --git a/app-src/src/renderer/fancy_loot.rs b/app-src/src/renderer/fancy_loot.rs new file mode 100644 index 0000000..a7f2cfd --- /dev/null +++ b/app-src/src/renderer/fancy_loot.rs @@ -0,0 +1,70 @@ +use super::*; + +#[derive(geng::Assets)] +pub struct Assets { + #[asset(path = "health_pack.png")] + pub health_pack: ugli::Texture, + #[asset(path = "mine.png")] + pub mine: ugli::Texture, + #[asset(path = "pistol.png")] + pub pistol: ugli::Texture, + #[asset(path = "assault_rifle.png")] + pub assault_rifle: ugli::Texture, + #[asset(path = "rocket_launcher.png")] + pub rocket_launcher: ugli::Texture, +} + +impl Assets { + fn setup(&mut self) { + self.health_pack.set_filter(ugli::Filter::Nearest); + self.mine.set_filter(ugli::Filter::Nearest); + self.pistol.set_filter(ugli::Filter::Nearest); + self.assault_rifle.set_filter(ugli::Filter::Nearest); + self.rocket_launcher.set_filter(ugli::Filter::Nearest); + } +} + +pub struct FancyLootRenderer { + sprite_renderer: Rc, + assets: Assets, +} + +impl FancyLootRenderer { + pub fn new(sprite_renderer: &Rc, mut assets: Assets) -> Self { + assets.setup(); + Self { + sprite_renderer: sprite_renderer.clone(), + assets, + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + let camera_uniforms = camera.uniforms(framebuffer); + for loot_box in &game.loot_boxes { + let texture = match &loot_box.item { + model::Item::HealthPack { .. } => &self.assets.health_pack, + model::Item::Weapon { weapon_type } => match weapon_type { + model::WeaponType::Pistol => &self.assets.pistol, + model::WeaponType::AssaultRifle => &self.assets.assault_rifle, + model::WeaponType::RocketLauncher => &self.assets.rocket_launcher, + }, + model::Item::Mine {} => &self.assets.mine, + }; + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + texture, + vec2(0.5, 0.0), + 0.04, + loot_box.position.map(|x| x.raw() as f32), + 0.0, + false, + Color::WHITE, + ); + } + } +} diff --git a/app-src/src/renderer/fancy_mine.rs b/app-src/src/renderer/fancy_mine.rs new file mode 100644 index 0000000..f7d4655 --- /dev/null +++ b/app-src/src/renderer/fancy_mine.rs @@ -0,0 +1,62 @@ +use super::*; + +#[derive(geng::Assets)] +pub struct Assets { + #[asset(path = "idle.png")] + idle: ugli::Texture, + #[asset(path = "preparing.png")] + preparing: ugli::Texture, + #[asset(path = "triggered.png")] + triggered: ugli::Texture, +} + +impl Assets { + fn setup(&mut self) { + self.idle.set_filter(ugli::Filter::Nearest); + self.preparing.set_filter(ugli::Filter::Nearest); + self.triggered.set_filter(ugli::Filter::Nearest); + } +} + +pub struct FancyMineRenderer { + sprite_renderer: Rc, + assets: Assets, +} + +impl FancyMineRenderer { + pub fn new(sprite_renderer: &Rc, mut assets: Assets) -> Self { + assets.setup(); + Self { + sprite_renderer: sprite_renderer.clone(), + assets, + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + let camera_uniforms = camera.uniforms(framebuffer); + for mine in &game.mines { + let texture = match mine.state { + model::MineState::Preparing { .. } => &self.assets.preparing, + model::MineState::Idle => &self.assets.idle, + model::MineState::Triggered { .. } | model::MineState::Exploded => { + &self.assets.triggered + } + }; + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + texture, + vec2(0.5, 0.0), + 0.04, + mine.position.map(|x| x.raw() as f32), + 0.0, + false, + Color::WHITE, + ); + } + } +} diff --git a/app-src/src/renderer/fancy_unit.rs b/app-src/src/renderer/fancy_unit.rs new file mode 100644 index 0000000..3a91f9c --- /dev/null +++ b/app-src/src/renderer/fancy_unit.rs @@ -0,0 +1,251 @@ +use super::*; + +#[derive(geng::Assets)] +pub struct PlayerAssets { + #[asset(path = "jump/*.png", range = "1..=3")] + jump: Vec, + #[asset(path = "rise/*.png", range = "1..=6")] + rise: Vec, + #[asset(path = "walk/*.png", range = "1..=6")] + walk: Vec, + #[asset(path = "hand.png")] + hand: ugli::Texture, + #[asset(path = "head.png")] + head: ugli::Texture, + #[asset(path = "stay.png")] + stay: ugli::Texture, +} + +impl PlayerAssets { + fn setup(&mut self) { + self.head.set_filter(ugli::Filter::Nearest); + self.hand.set_filter(ugli::Filter::Nearest); + self.stay.set_filter(ugli::Filter::Nearest); + for texture in &mut self.jump { + texture.set_filter(ugli::Filter::Nearest); + } + for texture in &mut self.rise { + texture.set_filter(ugli::Filter::Nearest); + } + for texture in &mut self.walk { + texture.set_filter(ugli::Filter::Nearest); + } + } +} + +#[derive(geng::Assets)] +pub struct Assets { + #[asset(path = "*", range = "1..=3")] + players: Vec, +} + +impl Assets { + fn setup(&mut self) { + for player in &mut self.players { + player.setup(); + } + } +} + +pub struct FancyUnitRenderer { + simple_renderer: Rc, + sprite_renderer: Rc, + assets: Assets, + weapon_assets: Rc, +} + +impl FancyUnitRenderer { + const ANIMATION_SPEED: f64 = 1.2; + pub fn new( + geng: &Rc, + simple_renderer: &Rc, + sprite_renderer: &Rc, + mut assets: Assets, + weapon_assets: &Rc, + ) -> Self { + assets.setup(); + Self { + simple_renderer: simple_renderer.clone(), + sprite_renderer: sprite_renderer.clone(), + assets, + weapon_assets: weapon_assets.clone(), + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + let camera_uniforms = camera.uniforms(framebuffer); + for unit in &game.units { + let player_assets = &self.assets.players[unit.id.raw() % self.assets.players.len()]; + let texture = &player_assets.walk[0]; + let mut angle = unit + .weapon + .as_ref() + .and_then(|weapon| weapon.last_angle) + .map(|angle| angle.raw() as f32) + .unwrap_or(if unit.walked_right { + 0.0 + } else { + std::f32::consts::PI + }); + let flip = angle > std::f32::consts::PI / 2.0 || angle < -std::f32::consts::PI / 2.0; + if flip { + if angle < 0.0 { + angle = angle + std::f32::consts::PI; + } else { + angle = angle - std::f32::consts::PI; + } + } + let height = unit.size.y.raw() as f32 * 0.6; + let texture_scale = height / texture.size().y as f32; + // body + { + let mut y_off = 0.0; + let texture = if unit.on_ladder { + y_off = height * 0.8; + let index = (unit.position.y.raw() * Self::ANIMATION_SPEED) as usize + % player_assets.rise.len(); + &player_assets.rise[index] + } else if unit.on_ground { + if unit.stand { + &player_assets.stay + } else { + let index = (unit.position.x.raw() * Self::ANIMATION_SPEED) as usize + % player_assets.walk.len(); + &player_assets.walk[index] + } + } else { + &player_assets.jump[if unit.jump_state.can_jump { 0 } else { 2 }] + }; + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + texture, + vec2(0.5, 1.0), + texture_scale, + vec2( + unit.position.x.raw() as f32, + unit.position.y.raw() as f32 + + unit.size.y.raw() as f32 * 0.6 + + y_off as f32, + ), + 0.0, + flip, + Color::WHITE, + ); + } + // head + { + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + &player_assets.head, + vec2(0.5, 0.3), + texture_scale, + vec2( + unit.position.x.raw() as f32, + unit.position.y.raw() as f32 + unit.size.y.raw() as f32 * 0.75, + ), + angle / 2.0, + flip, + Color::WHITE, + ); + } + // weapon + if let Some(weapon) = &unit.weapon { + let (texture, origin) = match weapon.typ { + model::WeaponType::Pistol => (&self.weapon_assets.pistol, vec2(-0.3, 0.8)), + model::WeaponType::AssaultRifle => { + (&self.weapon_assets.assault_rifle, vec2(-0.2, 0.6)) + } + model::WeaponType::RocketLauncher => { + (&self.weapon_assets.rocket_launcher, vec2(0.3, 0.5)) + } + }; + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + texture, + origin, + texture_scale, + vec2( + unit.position.x.raw() as f32 + - if flip { -1.0 } else { 1.0 } * unit.size.x.raw() as f32 * 0.1, + unit.position.y.raw() as f32 + unit.size.y.raw() as f32 * 0.55, + ), + angle, + flip, + Color::WHITE, + ); + if let Some(tick) = weapon.last_fire_tick { + let muzzle_flash_index = (game.current_tick - tick) / 2; + if muzzle_flash_index < 2 { + let (texture, origin) = match weapon.typ { + model::WeaponType::Pistol => { + (&self.weapon_assets.pistol_muzzle_flash, vec2(-0.9, 0.5)) + } + model::WeaponType::AssaultRifle => ( + &self.weapon_assets.assault_rifle_muzzle_flash[muzzle_flash_index], + vec2(-1.2, 0.5), + ), + model::WeaponType::RocketLauncher => ( + &self.weapon_assets.rocket_launcher_muzzle_flash, + vec2(-1.8, 0.5), + ), + }; + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + texture, + origin, + texture_scale, + vec2( + unit.position.x.raw() as f32 + - if flip { -1.0 } else { 1.0 } + * unit.size.x.raw() as f32 + * 0.1, + unit.position.y.raw() as f32 + unit.size.y.raw() as f32 * 0.55, + ), + angle, + flip, + Color::WHITE, + ); + } + } + } + //hand + self.sprite_renderer.draw( + framebuffer, + &camera_uniforms, + &player_assets.hand, + vec2(0.1, 0.9), + texture_scale, + vec2( + unit.position.x.raw() as f32 + - if flip { -1.0 } else { 1.0 } * unit.size.x.raw() as f32 * 0.1, + unit.position.y.raw() as f32 + unit.size.y.raw() as f32 * 0.55, + ), + angle, + flip, + Color::WHITE, + ); + self.simple_renderer.quad( + framebuffer, + camera, + AABB::pos_size( + unit.position - vec2(unit.size.x / r64(2.0), r64(0.0)), + vec2( + unit.size.x, + unit.size.y + * (r64(1.0) + - r64(unit.health as f64 / game.properties.unit_max_health as f64)), + ), + ), + Color::rgba(1.0, 0.0, 0.0, 0.5), + ); + } + } +} diff --git a/app-src/src/renderer/fancy_weapon.rs b/app-src/src/renderer/fancy_weapon.rs new file mode 100644 index 0000000..f9e665d --- /dev/null +++ b/app-src/src/renderer/fancy_weapon.rs @@ -0,0 +1,41 @@ +use super::*; + +#[derive(geng::Assets)] +pub struct Assets { + #[asset(path = "assault_rifle/gun.png")] + pub assault_rifle: ugli::Texture, + #[asset(path = "assault_rifle/bullet.png")] + pub assault_rifle_bullet: ugli::Texture, + #[asset(path = "assault_rifle/muzzle_flash_*.png", range = "1..=2")] + pub assault_rifle_muzzle_flash: Vec, + #[asset(path = "pistol/gun.png")] + pub pistol: ugli::Texture, + #[asset(path = "pistol/bullet.png")] + pub pistol_bullet: ugli::Texture, + #[asset(path = "pistol/muzzle_flash.png")] + pub pistol_muzzle_flash: ugli::Texture, + #[asset(path = "rocket_launcher/gun.png")] + pub rocket_launcher: ugli::Texture, + #[asset(path = "rocket_launcher/bullet.png")] + pub rocket_launcher_bullet: ugli::Texture, + #[asset(path = "rocket_launcher/muzzle_flash.png")] + pub rocket_launcher_muzzle_flash: ugli::Texture, +} + +impl Assets { + pub fn setup(&mut self) { + self.assault_rifle.set_filter(ugli::Filter::Nearest); + for texture in &mut self.assault_rifle_muzzle_flash { + texture.set_filter(ugli::Filter::Nearest); + } + self.pistol.set_filter(ugli::Filter::Nearest); + self.pistol_muzzle_flash.set_filter(ugli::Filter::Nearest); + self.rocket_launcher.set_filter(ugli::Filter::Nearest); + self.rocket_launcher_muzzle_flash + .set_filter(ugli::Filter::Nearest); + self.pistol_bullet.set_filter(ugli::Filter::Nearest); + self.assault_rifle_bullet.set_filter(ugli::Filter::Nearest); + self.rocket_launcher_bullet + .set_filter(ugli::Filter::Nearest); + } +} diff --git a/app-src/src/renderer/level.rs b/app-src/src/renderer/level.rs new file mode 100644 index 0000000..776ab40 --- /dev/null +++ b/app-src/src/renderer/level.rs @@ -0,0 +1,112 @@ +use super::*; + +pub struct LevelRenderer { + simple_renderer: Rc, + quads: ugli::VertexBuffer, + last_drawn_level: Option, +} + +impl LevelRenderer { + const JUMP_PAD_ANIMATION_TIME: f32 = 0.2; + pub fn new(geng: &Rc, simple_renderer: &Rc) -> Self { + Self { + simple_renderer: simple_renderer.clone(), + quads: ugli::VertexBuffer::new_static(geng.ugli(), vec![]), + last_drawn_level: None, + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + level: &model::Level, + game: Option<&model::Game>, + extra_data: Option<&RendererExtraData>, + ) { + if true { + // TODO: caching breaks jump pad animation + // self.last_drawn_map.as_ref() != Some(&game.map) { + let quads: &mut Vec = &mut self.quads; + quads.clear(); + for (pos, tile) in level { + match tile { + model::Tile::Empty => {} + model::Tile::Wall => { + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32), + i_size: vec2(1.0, 1.0), + i_color: Color::GRAY, + }); + } + model::Tile::Platform => { + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32) + vec2(0.0, 0.9), + i_size: vec2(1.0, 0.1), + i_color: Color::GRAY, + }); + } + model::Tile::Ladder => { + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32), + i_size: vec2(0.1, 1.0), + i_color: Color::GRAY, + }); + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32) + vec2(1.0, 0.0), + i_size: vec2(-0.1, 1.0), + i_color: Color::GRAY, + }); + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32) + vec2(0.0, 0.4), + i_size: vec2(1.0, 0.1), + i_color: Color::GRAY, + }); + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32) + vec2(0.0, 0.9), + i_size: vec2(1.0, 0.1), + i_color: Color::GRAY, + }); + } + model::Tile::JumpPad => { + let height = if let Some(game) = game { + if let Some(last_used_tick) = extra_data.and_then(|extra_data| { + extra_data.level_last_used_tick.get(&pos).copied() + }) { + let t = ((game.current_tick as f32 - 1.0 - last_used_tick as f32) + / game.properties.ticks_per_second.raw() as f32 + / Self::JUMP_PAD_ANIMATION_TIME) + .min(1.0); + let t = if t < 0.0 { 1.0 } else { t }; + 1.0 - t * 0.5 + } else { + 0.5 + } + } else { + 0.5 + }; + + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32) + vec2(0.3, 0.0), + i_size: vec2(0.4, height - 0.1), + i_color: Color::GRAY, + }); + quads.push(simple::Instance { + i_pos: pos.map(|x| x as f32) + vec2(0.0, height - 0.2), + i_size: vec2(1.0, 0.2), + i_color: Color::YELLOW, + }); + } + } + } + self.last_drawn_level = Some(level.clone()); + } + self.simple_renderer.frame( + framebuffer, + camera, + AABB::pos_size(vec2(r64(0.0), r64(0.0)), level.size().map(|x| r64(x as _))), + r64(0.1), + Color::GRAY, + ); + self.simple_renderer.quads(framebuffer, camera, &self.quads); + } +} diff --git a/app-src/src/renderer/loot.rs b/app-src/src/renderer/loot.rs new file mode 100644 index 0000000..6926fc8 --- /dev/null +++ b/app-src/src/renderer/loot.rs @@ -0,0 +1,38 @@ +use super::*; + +pub struct LootRenderer { + simple_renderer: Rc, + weapon_renderer: Rc, +} + +impl LootRenderer { + pub fn new(simple_renderer: &Rc, weapon_renderer: &Rc) -> Self { + Self { + simple_renderer: simple_renderer.clone(), + weapon_renderer: weapon_renderer.clone(), + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + for loot_box in &game.loot_boxes { + match &loot_box.item { + model::Item::HealthPack { .. } => { + self.simple_renderer + .quad(framebuffer, camera, loot_box.rect(), Color::GREEN); + } + model::Item::Weapon { weapon_type } => { + self.weapon_renderer + .draw(framebuffer, camera, loot_box.rect(), *weapon_type); + } + model::Item::Mine {} => { + self.simple_renderer + .quad(framebuffer, camera, loot_box.rect(), Color::YELLOW); + } + } + } + } +} diff --git a/app-src/src/renderer/mine.rs b/app-src/src/renderer/mine.rs new file mode 100644 index 0000000..2961533 --- /dev/null +++ b/app-src/src/renderer/mine.rs @@ -0,0 +1,37 @@ +use super::*; + +pub struct MineRenderer { + simple_renderer: Rc, +} + +impl MineRenderer { + pub fn new(simple_renderer: &Rc) -> Self { + Self { + simple_renderer: simple_renderer.clone(), + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + for mine in &game.mines { + self.simple_renderer + .quad(framebuffer, camera, mine.rect(), Color::YELLOW); + self.simple_renderer.quad( + framebuffer, + camera, + AABB::pos_size( + mine.position - vec2(mine.size.x / r64(4.0), r64(0.0)), + mine.size / r64(2.0), + ), + match mine.state { + model::MineState::Preparing { .. } => Color::GREEN, + model::MineState::Idle => Color::rgb(1.0, 0.5, 1.0), + model::MineState::Triggered { .. } | model::MineState::Exploded => Color::RED, + }, + ); + } + } +} diff --git a/app-src/src/renderer/mod.rs b/app-src/src/renderer/mod.rs new file mode 100644 index 0000000..f051a05 --- /dev/null +++ b/app-src/src/renderer/mod.rs @@ -0,0 +1,475 @@ +use crate::*; + +mod background; +mod bullet; +mod camera; +mod custom; +mod fancy_bullet; +mod fancy_explosion; +mod fancy_level; +mod fancy_loot; +mod fancy_mine; +mod fancy_unit; +mod fancy_weapon; +mod level; +mod loot; +mod mine; +mod simple; +mod sprite; +mod unit; +mod weapon; + +use background::BackgroundRenderer; +use bullet::BulletRenderer; +pub use camera::Camera; +use custom::CustomRenderer; +use fancy_bullet::FancyBulletRenderer; +use fancy_explosion::FancyExplosionRenderer; +pub use fancy_level::FancyLevelRenderer; +use fancy_loot::FancyLootRenderer; +use fancy_mine::FancyMineRenderer; +use fancy_unit::FancyUnitRenderer; +pub use level::LevelRenderer; +use loot::LootRenderer; +use mine::MineRenderer; +pub use simple::SimpleRenderer; +use sprite::SpriteRenderer; +use unit::UnitRenderer; +use weapon::WeaponRenderer; + +#[derive(geng::Assets)] +pub struct Sounds { + #[asset(path = "explosion.wav")] + explosion: geng::Sound, + #[asset(path = "pickup.wav")] + pickup: geng::Sound, + #[asset(path = "heal.wav")] + heal: geng::Sound, + #[asset(path = "hit.wav")] + hit: geng::Sound, + #[asset(path = "guns/pistol/shoot.wav")] + pistol_shoot: geng::Sound, + #[asset(path = "guns/assault_rifle/shoot.wav")] + assault_rifle_shoot: geng::Sound, + #[asset(path = "guns/rocket_launcher/shoot.wav")] + rocket_launcher_shoot: geng::Sound, +} + +#[derive(geng::Assets)] +pub struct Assets { + #[asset(path = "level")] + level: fancy_level::Assets, + #[asset(path = ".")] + background: background::Assets, + #[asset(path = ".")] + explosion: fancy_explosion::Assets, + #[asset(path = "unit")] + unit: fancy_unit::Assets, + #[asset(path = "guns")] + weapon: fancy_weapon::Assets, + #[asset(path = "loot")] + loot: fancy_loot::Assets, + #[asset(path = "mine")] + mine: fancy_mine::Assets, + #[asset(path = ".")] + sounds: Sounds, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Hash)] +enum Mode { + Fancy, + Schematic, + Blank, +} + +impl Mode { + fn switch_next(&mut self) { + let new_mode = match self { + Self::Fancy => Self::Schematic, + Self::Schematic => Self::Blank, + Self::Blank => Self::Fancy, + }; + *self = new_mode; + } +} + +pub struct Renderer { + geng: Rc, + default_tps: f64, + keyboard_player_input: Arc>, + simple_renderer: Rc, + sprite_renderer: Rc, + camera: Camera, + level_renderer: LevelRenderer, + fancy_level_renderer: FancyLevelRenderer, + unit_renderer: UnitRenderer, + bullet_renderer: BulletRenderer, + loot_renderer: LootRenderer, + fancy_loot_renderer: FancyLootRenderer, + fancy_bullet_renderer: FancyBulletRenderer, + mine_renderer: MineRenderer, + background: BackgroundRenderer, + fancy_unit: FancyUnitRenderer, + fancy_mine: FancyMineRenderer, + fancy_explosion: FancyExplosionRenderer, + custom: CustomRenderer, + sounds: Sounds, + player_names: Vec, + preferences: Rc>>>, +} + +impl Renderer { + pub fn new( + geng: &Rc, + preferences: Rc>>>, + keyboard_player_input: &Arc>, + mut player_names: Vec, + mut assets: Assets, + ) -> Self { + assets.weapon.setup(); + let weapon_assets = Rc::new(assets.weapon); + let simple_renderer = Rc::new(SimpleRenderer::new(geng)); + let sprite_renderer = Rc::new(SpriteRenderer::new(geng)); + let weapon_renderer = Rc::new(WeaponRenderer::new(&simple_renderer)); + if player_names.len() == 0 { + player_names.push(format!("{} 1", translate("Player"))); + } + if player_names.len() == 1 { + player_names.push(format!("{} 2", translate("Player"))); + } + Self { + geng: geng.clone(), + default_tps: 1.0, + keyboard_player_input: keyboard_player_input.clone(), + simple_renderer: simple_renderer.clone(), + sprite_renderer: sprite_renderer.clone(), + camera: Camera::new(), + level_renderer: LevelRenderer::new(geng, &simple_renderer), + fancy_level_renderer: FancyLevelRenderer::new(geng, assets.level), + unit_renderer: UnitRenderer::new(geng, &simple_renderer, &weapon_renderer), + bullet_renderer: BulletRenderer::new(&simple_renderer), + loot_renderer: LootRenderer::new(&simple_renderer, &weapon_renderer), + mine_renderer: MineRenderer::new(&simple_renderer), + background: BackgroundRenderer::new(geng, assets.background), + fancy_unit: FancyUnitRenderer::new( + geng, + &simple_renderer, + &sprite_renderer, + assets.unit, + &weapon_assets, + ), + fancy_loot_renderer: FancyLootRenderer::new(&sprite_renderer, assets.loot), + fancy_bullet_renderer: FancyBulletRenderer::new(&sprite_renderer, &weapon_assets), + fancy_mine: FancyMineRenderer::new(&sprite_renderer, assets.mine), + fancy_explosion: FancyExplosionRenderer::new(&sprite_renderer, assets.explosion), + custom: CustomRenderer::new(geng), + sounds: assets.sounds, + player_names, + preferences: preferences.clone(), + } + } +} + +#[derive(Clone, Serialize, Deserialize, Trans, PartialEq, Eq)] +pub struct Explosion { + tick: usize, + player_id: model::Id, + position: Vec2, + params: model::ExplosionParams, +} + +#[derive(Clone, Diff, Serialize, Deserialize, Trans)] +pub struct RendererExtraData { + #[diff = "eq"] + pub level_last_used_tick: HashMap, usize>, + #[diff = "eq"] + pub explosions: Vec, +} + +impl RendererExtraData { + const EXPLOSION_TICKS: usize = 30; +} + +impl codegame::RendererExtraData for RendererExtraData { + fn new(game: &model::Game) -> Self { + Self { + level_last_used_tick: HashMap::new(), + explosions: Vec::new(), + } + } + fn update(&mut self, events: &[model::Event], game: &model::Game) { + for event in events { + match event { + model::Event::LevelEvent { used_tile } => { + self.level_last_used_tick + .insert(*used_tile, game.current_tick); + } + model::Event::Explosion { + player_id, + position, + params, + } => { + self.explosions.push(Explosion { + tick: game.current_tick, + player_id: *player_id, + position: *position, + params: params.clone(), + }); + } + model::Event::Shot { .. } + | model::Event::PickupMine + | model::Event::PickupWeapon + | model::Event::Heal + | model::Event::Hit => {} + } + } + self.explosions + .retain(|explosion| explosion.tick + Self::EXPLOSION_TICKS > game.current_tick); + } +} + +impl Renderer { + const SCHEMATIC_EXPLOSION_TICKS: usize = 10; +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RendererPreferences { + mode: Mode, +} + +impl Default for RendererPreferences { + fn default() -> Self { + Self { mode: Mode::Fancy } + } +} + +impl codegame::Renderer for Renderer { + type ExtraData = RendererExtraData; + type Preferences = RendererPreferences; + fn default_tps(&self) -> f64 { + self.default_tps + } + fn draw( + &mut self, + game: &model::Game, + extra_data: &RendererExtraData, + custom_data: &HashMap>, + framebuffer: &mut ugli::Framebuffer, + ) { + let framebuffer_size = framebuffer.size(); + self.default_tps = game.properties.ticks_per_second.raw(); + self.camera.center = game.level.size().map(|x| r64(x as _)) / r64(2.0); + self.camera.fov = r64(game.level.size().y as f64 + 5.0); + { + let mut input = self.keyboard_player_input.lock().unwrap(); + input.mouse_pos = self + .camera + .screen_to_world(framebuffer, self.geng.window().mouse_pos().map(|x| r64(x))); + input.pressed_keys = self.geng.window().pressed_keys().clone(); + input.pressed_buttons = self.geng.window().pressed_buttons().clone(); + } + match self.preferences.borrow().renderer.mode { + Mode::Fancy => { + ugli::clear(framebuffer, Some(Color::BLACK), None); + self.background.draw(framebuffer, game); + self.fancy_level_renderer.draw( + framebuffer, + &self.camera, + &game.level, + Some(game), + Some(extra_data), + ); + self.fancy_loot_renderer + .draw(framebuffer, &self.camera, game); + self.fancy_bullet_renderer + .draw(framebuffer, &self.camera, game); + self.fancy_unit.draw(framebuffer, &self.camera, game); + self.fancy_mine.draw(framebuffer, &self.camera, game); + self.fancy_explosion + .draw(framebuffer, &self.camera, &extra_data.explosions, game); + } + Mode::Schematic => { + ugli::clear(framebuffer, Some(Color::BLACK), None); + self.level_renderer.draw( + framebuffer, + &self.camera, + &game.level, + Some(game), + Some(extra_data), + ); + self.loot_renderer.draw(framebuffer, &self.camera, game); + self.bullet_renderer.draw(framebuffer, &self.camera, game); + self.unit_renderer.draw(framebuffer, &self.camera, game); + self.mine_renderer.draw(framebuffer, &self.camera, game); + for explosion in &extra_data.explosions { + if explosion.tick + Self::SCHEMATIC_EXPLOSION_TICKS > game.current_tick { + let size = vec2(explosion.params.radius, explosion.params.radius); + self.simple_renderer.quad( + framebuffer, + &self.camera, + AABB::from_corners( + explosion.position - size, + explosion.position + size, + ), + Color::rgba(1.0, 0.0, 0.0, 0.5), + ); + } + } + } + Mode::Blank => { + ugli::clear(framebuffer, Some(Color::BLACK), None); + } + } + self.custom.draw_all(framebuffer, &self.camera, custom_data); + let font_size = 20.0; + let player_colors = [Color::rgb(1.0, 0.7, 0.7), Color::rgb(0.7, 0.7, 1.0)]; + if self.preferences.borrow().renderer.mode != Mode::Blank { + for unit in &game.units { + let player_index = game + .players + .iter() + .position(|player| player.id == unit.player_id) + .unwrap(); + if let Some(name) = self.player_names.get(player_index) { + let pos = self.camera.world_to_screen( + framebuffer, + vec2(unit.position.x, unit.position.y + unit.size.y), + ); + self.geng.default_font().draw_aligned( + framebuffer, + name, + pos.map(|x| x.raw() as f32), + 0.5, + 20.0, + player_colors[player_index], + ); + } + } + let mid_width = partial_max( + self.geng + .default_font() + .measure(translate("score"), font_size) + .width(), + self.geng + .default_font() + .measure(translate("versus"), font_size) + .width(), + ); + let off = mid_width / 2.0 + font_size; + self.geng.default_font().draw_aligned( + framebuffer, + &format!("{}", game.players[0].score), + vec2( + framebuffer_size.x as f32 / 2.0 - off, + framebuffer_size.y as f32 - 40.0 - font_size, + ), + 1.0, + font_size, + Color::WHITE, + ); + self.geng.default_font().draw_aligned( + framebuffer, + translate("score"), + vec2( + framebuffer_size.x as f32 / 2.0, + framebuffer_size.y as f32 - 40.0 - font_size, + ), + 0.5, + font_size, + Color::GRAY, + ); + self.geng.default_font().draw_aligned( + framebuffer, + &format!("{}", game.players[1].score), + vec2( + framebuffer_size.x as f32 / 2.0 + off, + framebuffer_size.y as f32 - 40.0 - font_size, + ), + 0.0, + font_size, + Color::WHITE, + ); + if self.player_names.len() == 2 { + self.geng.default_font().draw_aligned( + framebuffer, + &self.player_names[0], + vec2( + framebuffer_size.x as f32 / 2.0 - off, + framebuffer_size.y as f32 - 40.0, + ), + 1.0, + font_size, + player_colors[0], + ); + self.geng.default_font().draw_aligned( + framebuffer, + translate("versus"), + vec2( + framebuffer_size.x as f32 / 2.0, + framebuffer_size.y as f32 - 40.0, + ), + 0.5, + font_size, + Color::GRAY, + ); + self.geng.default_font().draw_aligned( + framebuffer, + &self.player_names[1], + vec2( + framebuffer_size.x as f32 / 2.0 + off, + framebuffer_size.y as f32 - 40.0, + ), + 0.0, + font_size, + player_colors[1], + ); + } + } + } + fn process_event(&mut self, event: &model::Event) { + match event { + model::Event::Explosion { .. } => { + let mut effect = self.sounds.explosion.effect(); + effect.set_volume(self.preferences.borrow().volume); + effect.play(); + } + model::Event::LevelEvent { .. } => {} + model::Event::Shot { weapon_type } => { + let sound = match weapon_type { + model::WeaponType::Pistol => &self.sounds.pistol_shoot, + model::WeaponType::AssaultRifle => &self.sounds.assault_rifle_shoot, + model::WeaponType::RocketLauncher => &self.sounds.rocket_launcher_shoot, + }; + let mut effect = sound.effect(); + effect.set_volume(self.preferences.borrow().volume); + effect.play(); + } + model::Event::PickupMine | model::Event::PickupWeapon => { + let mut effect = self.sounds.pickup.effect(); + effect.set_volume(self.preferences.borrow().volume); + effect.play(); + } + model::Event::Heal => { + let mut effect = self.sounds.heal.effect(); + effect.set_volume(self.preferences.borrow().volume); + effect.play(); + } + model::Event::Hit => { + let mut effect = self.sounds.hit.effect(); + effect.set_volume(self.preferences.borrow().volume); + effect.play(); + } + } + } + fn handle_event(&mut self, event: &geng::Event) { + match event { + geng::Event::KeyDown { key, .. } => match key { + geng::Key::M => { + self.preferences.borrow_mut().renderer.mode.switch_next(); + } + _ => {} + }, + _ => {} + } + } +} diff --git a/app-src/src/renderer/simple/mod.rs b/app-src/src/renderer/simple/mod.rs new file mode 100644 index 0000000..2859c0b --- /dev/null +++ b/app-src/src/renderer/simple/mod.rs @@ -0,0 +1,158 @@ +use super::*; + +#[derive(ugli::Vertex)] +pub struct Vertex { + pub a_pos: Vec2, +} + +#[derive(ugli::Vertex)] +pub struct Instance { + pub i_pos: Vec2, + pub i_size: Vec2, + pub i_color: Color, +} + +pub struct SimpleRenderer { + quad_geometry: ugli::VertexBuffer, + instances: RefCell>, + program: ugli::Program, + white_texture: ugli::Texture, +} + +impl SimpleRenderer { + pub fn new(geng: &Rc) -> Self { + Self { + quad_geometry: ugli::VertexBuffer::new_static( + geng.ugli(), + vec![ + Vertex { + a_pos: vec2(0.0, 0.0), + }, + Vertex { + a_pos: vec2(1.0, 0.0), + }, + Vertex { + a_pos: vec2(1.0, 1.0), + }, + Vertex { + a_pos: vec2(0.0, 1.0), + }, + ], + ), + instances: RefCell::new(ugli::VertexBuffer::new_dynamic(geng.ugli(), vec![])), + program: geng + .shader_lib() + .compile(include_str!("program.glsl")) + .unwrap(), + white_texture: ugli::Texture::new_with(geng.ugli(), vec2(1, 1), |_| Color::WHITE), + } + } + pub fn textured_quad( + &self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + rect: AABB, + texture: &ugli::Texture, + ) { + let rect = rect.map(|x| x.raw() as f32); + let mut instances = self.instances.borrow_mut(); + instances.clear(); + instances.push(Instance { + i_pos: rect.bottom_left(), + i_size: rect.size(), + i_color: Color::WHITE, + }); + self.draw( + framebuffer, + camera, + &self.quad_geometry, + &instances, + Some(texture), + ); + } + pub fn quad( + &self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + rect: AABB, + color: Color, + ) { + let rect = rect.map(|x| x.raw() as f32); + let mut instances = self.instances.borrow_mut(); + instances.clear(); + instances.push(Instance { + i_pos: rect.bottom_left(), + i_size: rect.size(), + i_color: color, + }); + self.quads(framebuffer, camera, &instances); + } + pub fn frame( + &self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + rect: AABB, + width: R64, + color: Color, + ) { + self.quad( + framebuffer, + camera, + AABB::pos_size(rect.bottom_left(), vec2(-width, rect.height())), + color, + ); + self.quad( + framebuffer, + camera, + AABB::pos_size(rect.bottom_left(), vec2(rect.width(), -width)), + color, + ); + self.quad( + framebuffer, + camera, + AABB::pos_size(rect.top_right(), vec2(-rect.width(), width)), + color, + ); + self.quad( + framebuffer, + camera, + AABB::pos_size(rect.top_right(), vec2(width, -rect.height())), + color, + ); + } + pub fn quads( + &self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + instances: &ugli::VertexBuffer, + ) { + self.draw(framebuffer, camera, &self.quad_geometry, instances, None); + } + pub fn draw( + &self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + vertices: &ugli::VertexBuffer, + instances: &ugli::VertexBuffer, + texture: Option<&ugli::Texture>, + ) { + let texture = texture.unwrap_or(&self.white_texture); + let camera_uniforms = camera.uniforms(framebuffer); + ugli::draw( + framebuffer, + &self.program, + ugli::DrawMode::TriangleFan, + ugli::instanced(vertices, instances), + ( + camera_uniforms, + ugli::uniforms! { + u_texture: texture, + }, + ), + ugli::DrawParameters { + blend_mode: Some(default()), + ..default() + }, + ); + } +} diff --git a/app-src/src/renderer/simple/program.glsl b/app-src/src/renderer/simple/program.glsl new file mode 100644 index 0000000..8c60b46 --- /dev/null +++ b/app-src/src/renderer/simple/program.glsl @@ -0,0 +1,27 @@ +varying vec4 v_color; +varying vec2 v_pos; + +#ifdef VERTEX_SHADER +attribute vec2 a_pos; + +attribute vec2 i_pos; +attribute vec2 i_size; + +attribute vec4 i_color; + +uniform mat4 u_projection_matrix; +uniform mat4 u_view_matrix; + +void main() { + v_pos = a_pos; + v_color = i_color; + gl_Position = u_projection_matrix * u_view_matrix * vec4(i_pos + a_pos * i_size, 0.0, 1.0); +} +#endif + +#ifdef FRAGMENT_SHADER +uniform sampler2D u_texture; +void main() { + gl_FragColor = texture2D(u_texture, vec2(v_pos.x, 1.0 - v_pos.y)) * v_color; +} +#endif \ No newline at end of file diff --git a/app-src/src/renderer/sprite/mod.rs b/app-src/src/renderer/sprite/mod.rs new file mode 100644 index 0000000..1111bd8 --- /dev/null +++ b/app-src/src/renderer/sprite/mod.rs @@ -0,0 +1,80 @@ +use super::*; + +#[derive(ugli::Vertex)] +pub struct Vertex { + pub a_pos: Vec2, +} + +pub struct SpriteRenderer { + program: ugli::Program, + quad: ugli::VertexBuffer, +} + +impl SpriteRenderer { + pub fn new(geng: &Rc) -> Self { + Self { + program: geng + .shader_lib() + .compile(include_str!("program.glsl")) + .unwrap(), + quad: ugli::VertexBuffer::new_static( + geng.ugli(), + vec![ + Vertex { + a_pos: vec2(0.0, 0.0), + }, + Vertex { + a_pos: vec2(1.0, 0.0), + }, + Vertex { + a_pos: vec2(1.0, 1.0), + }, + Vertex { + a_pos: vec2(0.0, 1.0), + }, + ], + ), + } + } + pub fn draw( + &self, + framebuffer: &mut ugli::Framebuffer, + camera_uniforms: impl ugli::Uniforms, + texture: &ugli::Texture, + origin: Vec2, + scale: f32, + pos: Vec2, + rotation: f32, + flip: bool, + color: Color, + ) { + let texture_size = texture.size().map(|x| x as f32) * scale; + let origin = if flip { + vec2(1.0 - origin.x, origin.y) + } else { + origin + }; + ugli::draw( + framebuffer, + &self.program, + ugli::DrawMode::TriangleFan, + &self.quad, + ( + camera_uniforms, + ugli::uniforms! { + u_texture: texture, + u_rotation: rotation, + u_pos: pos, + u_flip: if flip { 0.0 } else { 1.0 }, + u_texture_size: texture_size, + u_origin: origin, + u_color: color, + }, + ), + ugli::DrawParameters { + blend_mode: Some(default()), + ..default() + }, + ); + } +} diff --git a/app-src/src/renderer/sprite/program.glsl b/app-src/src/renderer/sprite/program.glsl new file mode 100644 index 0000000..3c25419 --- /dev/null +++ b/app-src/src/renderer/sprite/program.glsl @@ -0,0 +1,34 @@ +varying vec2 v_vt; + +#ifdef VERTEX_SHADER +attribute vec2 a_pos; + +uniform mat4 u_projection_matrix; +uniform mat4 u_view_matrix; + +uniform vec2 u_texture_size; +uniform vec2 u_origin; +uniform vec2 u_pos; +uniform float u_rotation; +uniform float u_flip; + +vec2 rotate_vec(vec2 v, float angle) { + float s = sin(angle); + float c = cos(angle); + return vec2(v.x * c - v.y * s, v.x * s + v.y * c); +} + +void main() { + v_vt = vec2(a_pos.x * u_flip + (1.0 - a_pos.x) * (1.0 - u_flip), a_pos.y); + vec2 world_pos = u_pos + rotate_vec((a_pos - u_origin) * u_texture_size, u_rotation); + gl_Position = u_projection_matrix * u_view_matrix * vec4(world_pos, 0.0, 1.0); +} +#endif + +#ifdef FRAGMENT_SHADER +uniform sampler2D u_texture; +uniform vec4 u_color; +void main() { + gl_FragColor = texture2D(u_texture, vec2(v_vt.x, 1.0 - v_vt.y)) * u_color; +} +#endif \ No newline at end of file diff --git a/app-src/src/renderer/unit.rs b/app-src/src/renderer/unit.rs new file mode 100644 index 0000000..efffba1 --- /dev/null +++ b/app-src/src/renderer/unit.rs @@ -0,0 +1,97 @@ +use super::*; + +pub struct UnitRenderer { + geng: Rc, + simple_renderer: Rc, + weapon_renderer: Rc, +} + +impl UnitRenderer { + pub fn new( + geng: &Rc, + simple_renderer: &Rc, + weapon_renderer: &Rc, + ) -> Self { + Self { + geng: geng.clone(), + simple_renderer: simple_renderer.clone(), + weapon_renderer: weapon_renderer.clone(), + } + } + pub fn draw( + &mut self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + game: &model::Game, + ) { + for unit in &game.units { + self.simple_renderer.quad( + framebuffer, + camera, + AABB::pos_size( + unit.position - vec2(unit.size.x / r64(2.0), r64(0.0)), + unit.size, + ), + Color::WHITE, + ); + self.simple_renderer.quad( + framebuffer, + camera, + AABB::pos_size( + unit.position - vec2(unit.size.x / r64(2.0), r64(0.0)), + vec2( + unit.size.x, + unit.size.y + * (r64(1.0) + - r64(unit.health as f64) + / r64(game.properties.unit_max_health as f64)), + ), + ), + Color::RED, + ); + if let Some(weapon) = &unit.weapon { + self.weapon_renderer.draw( + framebuffer, + camera, + AABB::pos_size( + unit.rect().center() - vec2(r64(0.25), r64(0.25)), + vec2(r64(0.5), r64(0.5)), + ), + weapon.typ, + ); + if let Some(angle) = weapon.last_angle { + let vertices = ugli::VertexBuffer::new_static( + &self.geng.ugli(), + vec![ + simple::Vertex { + a_pos: vec2(0.0, 0.0), + }, + simple::Vertex { + a_pos: Vec2::rotated( + vec2(100.0, 0.0), + (angle + weapon.spread).raw() as f32, + ), + }, + simple::Vertex { + a_pos: Vec2::rotated( + vec2(100.0, 0.0), + (angle - weapon.spread).raw() as f32, + ), + }, + ], + ); + let instances = ugli::VertexBuffer::new_static( + &self.geng.ugli(), + vec![simple::Instance { + i_pos: unit.center().map(|x| x.raw() as f32), + i_size: vec2(1.0, 1.0), + i_color: Color::rgba(1.0, 1.0, 1.0, 0.1), + }], + ); + self.simple_renderer + .draw(framebuffer, camera, &vertices, &instances, None); + } + } + } + } +} diff --git a/app-src/src/renderer/weapon.rs b/app-src/src/renderer/weapon.rs new file mode 100644 index 0000000..6a10bf2 --- /dev/null +++ b/app-src/src/renderer/weapon.rs @@ -0,0 +1,32 @@ +use super::*; + +pub struct WeaponRenderer { + simple_renderer: Rc, +} + +impl WeaponRenderer { + pub fn new(simple_renderer: &Rc) -> Self { + Self { + simple_renderer: simple_renderer.clone(), + } + } + pub fn draw( + &self, + framebuffer: &mut ugli::Framebuffer, + camera: &Camera, + rect: AABB, + typ: model::WeaponType, + ) { + use model::WeaponType::*; + self.simple_renderer.quad( + framebuffer, + camera, + rect, + match typ { + Pistol => Color::rgb(0.0, 0.0, 0.5), + AssaultRifle => Color::rgb(0.0, 0.0, 1.0), + RocketLauncher => Color::rgb(1.0, 0.5, 0.0), + }, + ); + } +} diff --git a/app-src/src/translations.txt b/app-src/src/translations.txt new file mode 100644 index 0000000..a708da2 --- /dev/null +++ b/app-src/src/translations.txt @@ -0,0 +1,41 @@ +en=keyboard +ru=клавиатура + +en=Keyboard player +ru=Игрок с клавиатуры + +en=Quickstart strategy +ru=Стратегия быстрого старта + +en=random +ru=случайный + +en=thinking speed +ru=скорость действий + +en=Level: +ru=Уровень: + +en=level editor +ru=редактор уровней + +en=save +ru=сохранить + +en=score +ru=счёт + +en=simple +ru=простой + +en=versus +ru=против + +en=Player +ru=Игрок + +en=Team size: +ru=Размер команды: + +en=complex +ru=сложный \ No newline at end of file diff --git a/app-src/static/aicup2019-preferences.json b/app-src/static/aicup2019-preferences.json new file mode 100644 index 0000000..26c4001 --- /dev/null +++ b/app-src/static/aicup2019-preferences.json @@ -0,0 +1,7 @@ +( + view_speed: 1, + volume: 0.5, + renderer: ( + mode: Fancy, + ), +) \ No newline at end of file diff --git a/app-src/static/assets/city.png b/app-src/static/assets/city.png new file mode 100644 index 0000000..8673927 Binary files /dev/null and b/app-src/static/assets/city.png differ diff --git a/app-src/static/assets/cloud.png b/app-src/static/assets/cloud.png new file mode 100644 index 0000000..36b5419 Binary files /dev/null and b/app-src/static/assets/cloud.png differ diff --git a/app-src/static/assets/explosion.png b/app-src/static/assets/explosion.png new file mode 100644 index 0000000..70d8547 Binary files /dev/null and b/app-src/static/assets/explosion.png differ diff --git a/app-src/static/assets/explosion.wav b/app-src/static/assets/explosion.wav new file mode 100644 index 0000000..f55494d Binary files /dev/null and b/app-src/static/assets/explosion.wav differ diff --git a/app-src/static/assets/guns/assault_rifle/bullet.png b/app-src/static/assets/guns/assault_rifle/bullet.png new file mode 100644 index 0000000..74ccfb6 Binary files /dev/null and b/app-src/static/assets/guns/assault_rifle/bullet.png differ diff --git a/app-src/static/assets/guns/assault_rifle/gun.png b/app-src/static/assets/guns/assault_rifle/gun.png new file mode 100644 index 0000000..dbec8c3 Binary files /dev/null and b/app-src/static/assets/guns/assault_rifle/gun.png differ diff --git a/app-src/static/assets/guns/assault_rifle/muzzle_flash_1.png b/app-src/static/assets/guns/assault_rifle/muzzle_flash_1.png new file mode 100644 index 0000000..6536e95 Binary files /dev/null and b/app-src/static/assets/guns/assault_rifle/muzzle_flash_1.png differ diff --git a/app-src/static/assets/guns/assault_rifle/muzzle_flash_2.png b/app-src/static/assets/guns/assault_rifle/muzzle_flash_2.png new file mode 100644 index 0000000..af04859 Binary files /dev/null and b/app-src/static/assets/guns/assault_rifle/muzzle_flash_2.png differ diff --git a/app-src/static/assets/guns/assault_rifle/shoot.wav b/app-src/static/assets/guns/assault_rifle/shoot.wav new file mode 100644 index 0000000..8cf8eba Binary files /dev/null and b/app-src/static/assets/guns/assault_rifle/shoot.wav differ diff --git a/app-src/static/assets/guns/pistol/bullet.png b/app-src/static/assets/guns/pistol/bullet.png new file mode 100644 index 0000000..bfc8caa Binary files /dev/null and b/app-src/static/assets/guns/pistol/bullet.png differ diff --git a/app-src/static/assets/guns/pistol/gun.png b/app-src/static/assets/guns/pistol/gun.png new file mode 100644 index 0000000..f5c340a Binary files /dev/null and b/app-src/static/assets/guns/pistol/gun.png differ diff --git a/app-src/static/assets/guns/pistol/muzzle_flash.png b/app-src/static/assets/guns/pistol/muzzle_flash.png new file mode 100644 index 0000000..af04859 Binary files /dev/null and b/app-src/static/assets/guns/pistol/muzzle_flash.png differ diff --git a/app-src/static/assets/guns/pistol/shoot.wav b/app-src/static/assets/guns/pistol/shoot.wav new file mode 100644 index 0000000..8751535 Binary files /dev/null and b/app-src/static/assets/guns/pistol/shoot.wav differ diff --git a/app-src/static/assets/guns/rocket_launcher/bullet.png b/app-src/static/assets/guns/rocket_launcher/bullet.png new file mode 100644 index 0000000..fcbe446 Binary files /dev/null and b/app-src/static/assets/guns/rocket_launcher/bullet.png differ diff --git a/app-src/static/assets/guns/rocket_launcher/gun.png b/app-src/static/assets/guns/rocket_launcher/gun.png new file mode 100644 index 0000000..bcd8f94 Binary files /dev/null and b/app-src/static/assets/guns/rocket_launcher/gun.png differ diff --git a/app-src/static/assets/guns/rocket_launcher/muzzle_flash.png b/app-src/static/assets/guns/rocket_launcher/muzzle_flash.png new file mode 100644 index 0000000..13cdd7a Binary files /dev/null and b/app-src/static/assets/guns/rocket_launcher/muzzle_flash.png differ diff --git a/app-src/static/assets/guns/rocket_launcher/shoot.wav b/app-src/static/assets/guns/rocket_launcher/shoot.wav new file mode 100644 index 0000000..457b6ab Binary files /dev/null and b/app-src/static/assets/guns/rocket_launcher/shoot.wav differ diff --git a/app-src/static/assets/heal.wav b/app-src/static/assets/heal.wav new file mode 100644 index 0000000..fd055df Binary files /dev/null and b/app-src/static/assets/heal.wav differ diff --git a/app-src/static/assets/hit.wav b/app-src/static/assets/hit.wav new file mode 100644 index 0000000..1b6936d Binary files /dev/null and b/app-src/static/assets/hit.wav differ diff --git a/app-src/static/assets/level/door.png b/app-src/static/assets/level/door.png new file mode 100644 index 0000000..969db95 Binary files /dev/null and b/app-src/static/assets/level/door.png differ diff --git a/app-src/static/assets/level/floor_1.png b/app-src/static/assets/level/floor_1.png new file mode 100644 index 0000000..25ca8af Binary files /dev/null and b/app-src/static/assets/level/floor_1.png differ diff --git a/app-src/static/assets/level/floor_2.png b/app-src/static/assets/level/floor_2.png new file mode 100644 index 0000000..f3e5bb5 Binary files /dev/null and b/app-src/static/assets/level/floor_2.png differ diff --git a/app-src/static/assets/level/jumper_1.png b/app-src/static/assets/level/jumper_1.png new file mode 100644 index 0000000..fcf541b Binary files /dev/null and b/app-src/static/assets/level/jumper_1.png differ diff --git a/app-src/static/assets/level/jumper_2.png b/app-src/static/assets/level/jumper_2.png new file mode 100644 index 0000000..d13d8f2 Binary files /dev/null and b/app-src/static/assets/level/jumper_2.png differ diff --git a/app-src/static/assets/level/jumper_3.png b/app-src/static/assets/level/jumper_3.png new file mode 100644 index 0000000..cadb9da Binary files /dev/null and b/app-src/static/assets/level/jumper_3.png differ diff --git a/app-src/static/assets/level/platform_C.png b/app-src/static/assets/level/platform_C.png new file mode 100644 index 0000000..9bb9936 Binary files /dev/null and b/app-src/static/assets/level/platform_C.png differ diff --git a/app-src/static/assets/level/platform_L.png b/app-src/static/assets/level/platform_L.png new file mode 100644 index 0000000..4ddf0fe Binary files /dev/null and b/app-src/static/assets/level/platform_L.png differ diff --git a/app-src/static/assets/level/platform_R.png b/app-src/static/assets/level/platform_R.png new file mode 100644 index 0000000..661477e Binary files /dev/null and b/app-src/static/assets/level/platform_R.png differ diff --git a/app-src/static/assets/level/platform_small_C.png b/app-src/static/assets/level/platform_small_C.png new file mode 100644 index 0000000..c45def2 Binary files /dev/null and b/app-src/static/assets/level/platform_small_C.png differ diff --git a/app-src/static/assets/level/platform_small_L.png b/app-src/static/assets/level/platform_small_L.png new file mode 100644 index 0000000..6ec288d Binary files /dev/null and b/app-src/static/assets/level/platform_small_L.png differ diff --git a/app-src/static/assets/level/platform_small_R.png b/app-src/static/assets/level/platform_small_R.png new file mode 100644 index 0000000..277f8d1 Binary files /dev/null and b/app-src/static/assets/level/platform_small_R.png differ diff --git a/app-src/static/assets/level/platform_small_solo.png b/app-src/static/assets/level/platform_small_solo.png new file mode 100644 index 0000000..d87952b Binary files /dev/null and b/app-src/static/assets/level/platform_small_solo.png differ diff --git a/app-src/static/assets/level/platform_solo.png b/app-src/static/assets/level/platform_solo.png new file mode 100644 index 0000000..2af3d2c Binary files /dev/null and b/app-src/static/assets/level/platform_solo.png differ diff --git a/app-src/static/assets/level/railing_C.png b/app-src/static/assets/level/railing_C.png new file mode 100644 index 0000000..8de9edd Binary files /dev/null and b/app-src/static/assets/level/railing_C.png differ diff --git a/app-src/static/assets/level/railing_L.png b/app-src/static/assets/level/railing_L.png new file mode 100644 index 0000000..089fcc9 Binary files /dev/null and b/app-src/static/assets/level/railing_L.png differ diff --git a/app-src/static/assets/level/railing_R.png b/app-src/static/assets/level/railing_R.png new file mode 100644 index 0000000..2b8d9a5 Binary files /dev/null and b/app-src/static/assets/level/railing_R.png differ diff --git a/app-src/static/assets/level/stairs.png b/app-src/static/assets/level/stairs.png new file mode 100644 index 0000000..9c58771 Binary files /dev/null and b/app-src/static/assets/level/stairs.png differ diff --git a/app-src/static/assets/level/wall_C.png b/app-src/static/assets/level/wall_C.png new file mode 100644 index 0000000..51410c7 Binary files /dev/null and b/app-src/static/assets/level/wall_C.png differ diff --git a/app-src/static/assets/level/wall_L.png b/app-src/static/assets/level/wall_L.png new file mode 100644 index 0000000..4dee1dc Binary files /dev/null and b/app-src/static/assets/level/wall_L.png differ diff --git a/app-src/static/assets/level/wall_R.png b/app-src/static/assets/level/wall_R.png new file mode 100644 index 0000000..ca16373 Binary files /dev/null and b/app-src/static/assets/level/wall_R.png differ diff --git a/app-src/static/assets/level/wall_down_C.png b/app-src/static/assets/level/wall_down_C.png new file mode 100644 index 0000000..d3adbdf Binary files /dev/null and b/app-src/static/assets/level/wall_down_C.png differ diff --git a/app-src/static/assets/level/wall_down_L.png b/app-src/static/assets/level/wall_down_L.png new file mode 100644 index 0000000..3e4388b Binary files /dev/null and b/app-src/static/assets/level/wall_down_L.png differ diff --git a/app-src/static/assets/level/wall_down_R.png b/app-src/static/assets/level/wall_down_R.png new file mode 100644 index 0000000..a86d7a0 Binary files /dev/null and b/app-src/static/assets/level/wall_down_R.png differ diff --git a/app-src/static/assets/level/wall_down_solo.png b/app-src/static/assets/level/wall_down_solo.png new file mode 100644 index 0000000..46eb0df Binary files /dev/null and b/app-src/static/assets/level/wall_down_solo.png differ diff --git a/app-src/static/assets/level/wall_solo.png b/app-src/static/assets/level/wall_solo.png new file mode 100644 index 0000000..0ce975b Binary files /dev/null and b/app-src/static/assets/level/wall_solo.png differ diff --git a/app-src/static/assets/level/window_C.png b/app-src/static/assets/level/window_C.png new file mode 100644 index 0000000..702489a Binary files /dev/null and b/app-src/static/assets/level/window_C.png differ diff --git a/app-src/static/assets/level/window_L.png b/app-src/static/assets/level/window_L.png new file mode 100644 index 0000000..637d1ee Binary files /dev/null and b/app-src/static/assets/level/window_L.png differ diff --git a/app-src/static/assets/level/window_R.png b/app-src/static/assets/level/window_R.png new file mode 100644 index 0000000..cc0a589 Binary files /dev/null and b/app-src/static/assets/level/window_R.png differ diff --git a/app-src/static/assets/level/window_solo.png b/app-src/static/assets/level/window_solo.png new file mode 100644 index 0000000..8f47122 Binary files /dev/null and b/app-src/static/assets/level/window_solo.png differ diff --git a/app-src/static/assets/loot/assault_rifle.png b/app-src/static/assets/loot/assault_rifle.png new file mode 100644 index 0000000..681534e Binary files /dev/null and b/app-src/static/assets/loot/assault_rifle.png differ diff --git a/app-src/static/assets/loot/health_pack.png b/app-src/static/assets/loot/health_pack.png new file mode 100644 index 0000000..2b30ccf Binary files /dev/null and b/app-src/static/assets/loot/health_pack.png differ diff --git a/app-src/static/assets/loot/mine.png b/app-src/static/assets/loot/mine.png new file mode 100644 index 0000000..b7d482b Binary files /dev/null and b/app-src/static/assets/loot/mine.png differ diff --git a/app-src/static/assets/loot/pistol.png b/app-src/static/assets/loot/pistol.png new file mode 100644 index 0000000..e809db0 Binary files /dev/null and b/app-src/static/assets/loot/pistol.png differ diff --git a/app-src/static/assets/loot/rocket_launcher.png b/app-src/static/assets/loot/rocket_launcher.png new file mode 100644 index 0000000..107ce47 Binary files /dev/null and b/app-src/static/assets/loot/rocket_launcher.png differ diff --git a/app-src/static/assets/mine/idle.png b/app-src/static/assets/mine/idle.png new file mode 100644 index 0000000..f06f3d8 Binary files /dev/null and b/app-src/static/assets/mine/idle.png differ diff --git a/app-src/static/assets/mine/preparing.png b/app-src/static/assets/mine/preparing.png new file mode 100644 index 0000000..dd4211a Binary files /dev/null and b/app-src/static/assets/mine/preparing.png differ diff --git a/app-src/static/assets/mine/triggered.png b/app-src/static/assets/mine/triggered.png new file mode 100644 index 0000000..4ec8ba4 Binary files /dev/null and b/app-src/static/assets/mine/triggered.png differ diff --git a/app-src/static/assets/pickup.wav b/app-src/static/assets/pickup.wav new file mode 100644 index 0000000..96519fc Binary files /dev/null and b/app-src/static/assets/pickup.wav differ diff --git a/app-src/static/assets/unit/1/hand.png b/app-src/static/assets/unit/1/hand.png new file mode 100644 index 0000000..060ed8a Binary files /dev/null and b/app-src/static/assets/unit/1/hand.png differ diff --git a/app-src/static/assets/unit/1/head.png b/app-src/static/assets/unit/1/head.png new file mode 100644 index 0000000..f300868 Binary files /dev/null and b/app-src/static/assets/unit/1/head.png differ diff --git a/app-src/static/assets/unit/1/jump/1.png b/app-src/static/assets/unit/1/jump/1.png new file mode 100644 index 0000000..63fbd0d Binary files /dev/null and b/app-src/static/assets/unit/1/jump/1.png differ diff --git a/app-src/static/assets/unit/1/jump/2.png b/app-src/static/assets/unit/1/jump/2.png new file mode 100644 index 0000000..ff48037 Binary files /dev/null and b/app-src/static/assets/unit/1/jump/2.png differ diff --git a/app-src/static/assets/unit/1/jump/3.png b/app-src/static/assets/unit/1/jump/3.png new file mode 100644 index 0000000..f115e57 Binary files /dev/null and b/app-src/static/assets/unit/1/jump/3.png differ diff --git a/app-src/static/assets/unit/1/rise/1.png b/app-src/static/assets/unit/1/rise/1.png new file mode 100644 index 0000000..52e344e Binary files /dev/null and b/app-src/static/assets/unit/1/rise/1.png differ diff --git a/app-src/static/assets/unit/1/rise/2.png b/app-src/static/assets/unit/1/rise/2.png new file mode 100644 index 0000000..9ccb253 Binary files /dev/null and b/app-src/static/assets/unit/1/rise/2.png differ diff --git a/app-src/static/assets/unit/1/rise/3.png b/app-src/static/assets/unit/1/rise/3.png new file mode 100644 index 0000000..769b29d Binary files /dev/null and b/app-src/static/assets/unit/1/rise/3.png differ diff --git a/app-src/static/assets/unit/1/rise/4.png b/app-src/static/assets/unit/1/rise/4.png new file mode 100644 index 0000000..fea7365 Binary files /dev/null and b/app-src/static/assets/unit/1/rise/4.png differ diff --git a/app-src/static/assets/unit/1/rise/5.png b/app-src/static/assets/unit/1/rise/5.png new file mode 100644 index 0000000..e820d89 Binary files /dev/null and b/app-src/static/assets/unit/1/rise/5.png differ diff --git a/app-src/static/assets/unit/1/rise/6.png b/app-src/static/assets/unit/1/rise/6.png new file mode 100644 index 0000000..799ee9a Binary files /dev/null and b/app-src/static/assets/unit/1/rise/6.png differ diff --git a/app-src/static/assets/unit/1/stay.png b/app-src/static/assets/unit/1/stay.png new file mode 100644 index 0000000..6b9af63 Binary files /dev/null and b/app-src/static/assets/unit/1/stay.png differ diff --git a/app-src/static/assets/unit/1/walk/1.png b/app-src/static/assets/unit/1/walk/1.png new file mode 100644 index 0000000..897cd3b Binary files /dev/null and b/app-src/static/assets/unit/1/walk/1.png differ diff --git a/app-src/static/assets/unit/1/walk/2.png b/app-src/static/assets/unit/1/walk/2.png new file mode 100644 index 0000000..41cb2fb Binary files /dev/null and b/app-src/static/assets/unit/1/walk/2.png differ diff --git a/app-src/static/assets/unit/1/walk/3.png b/app-src/static/assets/unit/1/walk/3.png new file mode 100644 index 0000000..a2aea1e Binary files /dev/null and b/app-src/static/assets/unit/1/walk/3.png differ diff --git a/app-src/static/assets/unit/1/walk/4.png b/app-src/static/assets/unit/1/walk/4.png new file mode 100644 index 0000000..495293a Binary files /dev/null and b/app-src/static/assets/unit/1/walk/4.png differ diff --git a/app-src/static/assets/unit/1/walk/5.png b/app-src/static/assets/unit/1/walk/5.png new file mode 100644 index 0000000..2cf2a02 Binary files /dev/null and b/app-src/static/assets/unit/1/walk/5.png differ diff --git a/app-src/static/assets/unit/1/walk/6.png b/app-src/static/assets/unit/1/walk/6.png new file mode 100644 index 0000000..7bd62f6 Binary files /dev/null and b/app-src/static/assets/unit/1/walk/6.png differ diff --git a/app-src/static/assets/unit/2/hand.png b/app-src/static/assets/unit/2/hand.png new file mode 100644 index 0000000..c68e4cb Binary files /dev/null and b/app-src/static/assets/unit/2/hand.png differ diff --git a/app-src/static/assets/unit/2/head.png b/app-src/static/assets/unit/2/head.png new file mode 100644 index 0000000..da6054f Binary files /dev/null and b/app-src/static/assets/unit/2/head.png differ diff --git a/app-src/static/assets/unit/2/jump/1.png b/app-src/static/assets/unit/2/jump/1.png new file mode 100644 index 0000000..09b774d Binary files /dev/null and b/app-src/static/assets/unit/2/jump/1.png differ diff --git a/app-src/static/assets/unit/2/jump/2.png b/app-src/static/assets/unit/2/jump/2.png new file mode 100644 index 0000000..cabedcc Binary files /dev/null and b/app-src/static/assets/unit/2/jump/2.png differ diff --git a/app-src/static/assets/unit/2/jump/3.png b/app-src/static/assets/unit/2/jump/3.png new file mode 100644 index 0000000..3387019 Binary files /dev/null and b/app-src/static/assets/unit/2/jump/3.png differ diff --git a/app-src/static/assets/unit/2/rise/1.png b/app-src/static/assets/unit/2/rise/1.png new file mode 100644 index 0000000..8d3af99 Binary files /dev/null and b/app-src/static/assets/unit/2/rise/1.png differ diff --git a/app-src/static/assets/unit/2/rise/2.png b/app-src/static/assets/unit/2/rise/2.png new file mode 100644 index 0000000..96945e2 Binary files /dev/null and b/app-src/static/assets/unit/2/rise/2.png differ diff --git a/app-src/static/assets/unit/2/rise/3.png b/app-src/static/assets/unit/2/rise/3.png new file mode 100644 index 0000000..01346e3 Binary files /dev/null and b/app-src/static/assets/unit/2/rise/3.png differ diff --git a/app-src/static/assets/unit/2/rise/4.png b/app-src/static/assets/unit/2/rise/4.png new file mode 100644 index 0000000..2c600a4 Binary files /dev/null and b/app-src/static/assets/unit/2/rise/4.png differ diff --git a/app-src/static/assets/unit/2/rise/5.png b/app-src/static/assets/unit/2/rise/5.png new file mode 100644 index 0000000..3da5618 Binary files /dev/null and b/app-src/static/assets/unit/2/rise/5.png differ diff --git a/app-src/static/assets/unit/2/rise/6.png b/app-src/static/assets/unit/2/rise/6.png new file mode 100644 index 0000000..b595a5f Binary files /dev/null and b/app-src/static/assets/unit/2/rise/6.png differ diff --git a/app-src/static/assets/unit/2/stay.png b/app-src/static/assets/unit/2/stay.png new file mode 100644 index 0000000..75b31cb Binary files /dev/null and b/app-src/static/assets/unit/2/stay.png differ diff --git a/app-src/static/assets/unit/2/walk/1.png b/app-src/static/assets/unit/2/walk/1.png new file mode 100644 index 0000000..e907071 Binary files /dev/null and b/app-src/static/assets/unit/2/walk/1.png differ diff --git a/app-src/static/assets/unit/2/walk/2.png b/app-src/static/assets/unit/2/walk/2.png new file mode 100644 index 0000000..16cdad7 Binary files /dev/null and b/app-src/static/assets/unit/2/walk/2.png differ diff --git a/app-src/static/assets/unit/2/walk/3.png b/app-src/static/assets/unit/2/walk/3.png new file mode 100644 index 0000000..b69ea91 Binary files /dev/null and b/app-src/static/assets/unit/2/walk/3.png differ diff --git a/app-src/static/assets/unit/2/walk/4.png b/app-src/static/assets/unit/2/walk/4.png new file mode 100644 index 0000000..9c7febf Binary files /dev/null and b/app-src/static/assets/unit/2/walk/4.png differ diff --git a/app-src/static/assets/unit/2/walk/5.png b/app-src/static/assets/unit/2/walk/5.png new file mode 100644 index 0000000..0f5a766 Binary files /dev/null and b/app-src/static/assets/unit/2/walk/5.png differ diff --git a/app-src/static/assets/unit/2/walk/6.png b/app-src/static/assets/unit/2/walk/6.png new file mode 100644 index 0000000..18a390b Binary files /dev/null and b/app-src/static/assets/unit/2/walk/6.png differ diff --git a/app-src/static/assets/unit/3/hand.png b/app-src/static/assets/unit/3/hand.png new file mode 100644 index 0000000..65a70b7 Binary files /dev/null and b/app-src/static/assets/unit/3/hand.png differ diff --git a/app-src/static/assets/unit/3/head.png b/app-src/static/assets/unit/3/head.png new file mode 100644 index 0000000..3b0120e Binary files /dev/null and b/app-src/static/assets/unit/3/head.png differ diff --git a/app-src/static/assets/unit/3/jump/1.png b/app-src/static/assets/unit/3/jump/1.png new file mode 100644 index 0000000..7dac8fe Binary files /dev/null and b/app-src/static/assets/unit/3/jump/1.png differ diff --git a/app-src/static/assets/unit/3/jump/2.png b/app-src/static/assets/unit/3/jump/2.png new file mode 100644 index 0000000..f649269 Binary files /dev/null and b/app-src/static/assets/unit/3/jump/2.png differ diff --git a/app-src/static/assets/unit/3/jump/3.png b/app-src/static/assets/unit/3/jump/3.png new file mode 100644 index 0000000..6b4e322 Binary files /dev/null and b/app-src/static/assets/unit/3/jump/3.png differ diff --git a/app-src/static/assets/unit/3/rise/1.png b/app-src/static/assets/unit/3/rise/1.png new file mode 100644 index 0000000..1166aa5 Binary files /dev/null and b/app-src/static/assets/unit/3/rise/1.png differ diff --git a/app-src/static/assets/unit/3/rise/2.png b/app-src/static/assets/unit/3/rise/2.png new file mode 100644 index 0000000..36d0f9b Binary files /dev/null and b/app-src/static/assets/unit/3/rise/2.png differ diff --git a/app-src/static/assets/unit/3/rise/3.png b/app-src/static/assets/unit/3/rise/3.png new file mode 100644 index 0000000..81c351c Binary files /dev/null and b/app-src/static/assets/unit/3/rise/3.png differ diff --git a/app-src/static/assets/unit/3/rise/4.png b/app-src/static/assets/unit/3/rise/4.png new file mode 100644 index 0000000..5d9cec5 Binary files /dev/null and b/app-src/static/assets/unit/3/rise/4.png differ diff --git a/app-src/static/assets/unit/3/rise/5.png b/app-src/static/assets/unit/3/rise/5.png new file mode 100644 index 0000000..56da834 Binary files /dev/null and b/app-src/static/assets/unit/3/rise/5.png differ diff --git a/app-src/static/assets/unit/3/rise/6.png b/app-src/static/assets/unit/3/rise/6.png new file mode 100644 index 0000000..b07665a Binary files /dev/null and b/app-src/static/assets/unit/3/rise/6.png differ diff --git a/app-src/static/assets/unit/3/stay.png b/app-src/static/assets/unit/3/stay.png new file mode 100644 index 0000000..bfad3cd Binary files /dev/null and b/app-src/static/assets/unit/3/stay.png differ diff --git a/app-src/static/assets/unit/3/walk/1.png b/app-src/static/assets/unit/3/walk/1.png new file mode 100644 index 0000000..c365baa Binary files /dev/null and b/app-src/static/assets/unit/3/walk/1.png differ diff --git a/app-src/static/assets/unit/3/walk/2.png b/app-src/static/assets/unit/3/walk/2.png new file mode 100644 index 0000000..8e435da Binary files /dev/null and b/app-src/static/assets/unit/3/walk/2.png differ diff --git a/app-src/static/assets/unit/3/walk/3.png b/app-src/static/assets/unit/3/walk/3.png new file mode 100644 index 0000000..7b91d8a Binary files /dev/null and b/app-src/static/assets/unit/3/walk/3.png differ diff --git a/app-src/static/assets/unit/3/walk/4.png b/app-src/static/assets/unit/3/walk/4.png new file mode 100644 index 0000000..f628e29 Binary files /dev/null and b/app-src/static/assets/unit/3/walk/4.png differ diff --git a/app-src/static/assets/unit/3/walk/5.png b/app-src/static/assets/unit/3/walk/5.png new file mode 100644 index 0000000..c365baa Binary files /dev/null and b/app-src/static/assets/unit/3/walk/5.png differ diff --git a/app-src/static/assets/unit/3/walk/6.png b/app-src/static/assets/unit/3/walk/6.png new file mode 100644 index 0000000..79fd0b5 Binary files /dev/null and b/app-src/static/assets/unit/3/walk/6.png differ diff --git a/app-src/static/index.html b/app-src/static/index.html new file mode 100644 index 0000000..f4da52d --- /dev/null +++ b/app-src/static/index.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-src/static/levels/level.txt b/app-src/static/levels/level.txt new file mode 100644 index 0000000..26ccb7a --- /dev/null +++ b/app-src/static/levels/level.txt @@ -0,0 +1,30 @@ +######################################## +#.................#....................# +#......................................# +#.................#....................# +#...........H#############.............# +#...........H..........................# +#...........H..........................# +#...........H..........................# +#.....########^^^^^^^^^^########.......# +#..........#.............#.............# +#..........#.............#.............# +#..........#............##.............# +#..........##..........................# +#.................^^#..................# +#...................#..................# +#...................#..................# +#.....T..................^^^###H###....# +#....########..................H.......# +#....#......#..................H.......# +#...........#..................H.......# +#..#...............T...................# +#..#............#######^^^^^...........# +#..####^^H^............................# +#........H.............................# +#........H.............................# +#....H#######^^^^#^^^^^^^^######.......# +#....H.................H...............# +#....H.................H...............# +#.13.H.......T...#.#...H.........T..42.# +######################################## \ No newline at end of file