diff options
author | Daniel Weipert <code@drogueronin.de> | 2023-09-24 13:40:25 +0200 |
---|---|---|
committer | Daniel Weipert <code@drogueronin.de> | 2023-09-24 13:40:25 +0200 |
commit | fa00b957378a393f8edbfc98ef111d35d18ecb09 (patch) | |
tree | 654e7dc5414f7f2795dbe996d3e1570793a5b1b8 |
initial commit
46 files changed, 2497 insertions, 0 deletions
diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0f3508d --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +APP_ENV= + +DB_NAME= +DB_USER= +DB_PASSWORD= + +BASE_BUILDING_BUILD_TIME_FACTOR=256 +BASE_UNIT_BUILD_TIME_FACTOR=256 +BASE_RESOURCE_GENERATION_FACTOR=128 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02166b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env + +/vendor/ diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..68a12c2 --- /dev/null +++ b/Readme.md @@ -0,0 +1 @@ +# Coop Game =} diff --git a/bin/villages.php b/bin/villages.php new file mode 100644 index 0000000..092a754 --- /dev/null +++ b/bin/villages.php @@ -0,0 +1,147 @@ +<?php + +use App\DB; +use Symfony\Component\Dotenv\Dotenv; + +require dirname(__DIR__) . '/vendor/autoload.php'; + +$dotenv = new Dotenv(); +$dotenv->load(dirname(__DIR__) . '/.env'); + +DB::init(); + +DB::query(<<<SQL + create table if not exists "villages" ( + "id" bigserial primary key, + + "name" character varying(255) not null, + + "x" integer not null, + "y" integer not null, + + "wood" bigint not null, + "clay" bigint not null, + "iron" bigint not null, + "food" bigint not null, + + "satisfaction" bigint not null, + "created_at" timestamp(0) not null, + "updated_at" timestamp(0) not null, + + unique ("x", "y") + ); +SQL); + +DB::query(<<<SQL + create table if not exists "village_buildings" ( + "id" bigserial primary key, + + "level" smallint not null, + "type" character varying(255) not null, + + "village_id" bigint not null, + constraint "relation_village" + foreign key ("village_id") references villages("id") on delete cascade, + + "created_at" timestamp(0) not null, + "updated_at" timestamp(0) not null, + + unique ("type", "village_id") + ); +SQL); + +DB::query(<<<SQL + create table if not exists "village_units" ( + "id" bigserial primary key, + + "amount" bigint not null, + "type" character varying(255) not null, + + "is_traveling" boolean not null default false, + + "home_village_id" bigint not null, + constraint "relation_village_home" + foreign key ("home_village_id") references villages("id") on delete cascade, + + "residence_village_id" bigint not null, + constraint "relation_village_residence" + foreign key ("residence_village_id") references villages("id") on delete cascade, + + "created_at" timestamp(0) not null, + "updated_at" timestamp(0) not null, + + unique ("type", "home_village_id", "residence_village_id") + ); +SQL); + +DB::query(<<<SQL + create table if not exists "village_storage_config" ( + "id" bigserial primary key, + + "wood" smallint not null, + "clay" smallint not null, + "iron" smallint not null, + "food" smallint not null, + + "village_id" bigint not null, + constraint "relation_village" + foreign key ("village_id") references villages("id") on delete cascade, + + "created_at" timestamp(0) not null, + "updated_at" timestamp(0) not null + ); +SQL); + +DB::query(<<<SQL + create table if not exists "events" ( + "id" bigserial primary key, + + "type" character varying(255) not null, + "time" timestamp(0) not null, + "payload" jsonb not null, + + "village_id" bigint not null, + constraint "relation_village" + foreign key ("village_id") references villages("id") on delete cascade, + + "created_at" timestamp(0) not null default current_timestamp, + "updated_at" timestamp(0) not null default current_timestamp + ); +SQL); + +DB::query(<<<SQL + create table if not exists "users" ( + "id" bigserial primary key, + + "username" character varying(255) not null, + "password" character varying(255) not null, + "email" character varying(255) not null, + + "created_at" timestamp(0) not null, + "updated_at" timestamp(0) not null, + + unique ("username") + ); +SQL); + +DB::query(<<<SQL + create table if not exists "user_settings" ( + "id" bigserial primary key, + + "display_name" character varying(255) not null, + + "user_id" bigint not null, + constraint "relation_user" + foreign key ("user_id") references users("id") on delete cascade, + + "created_at" timestamp(0) not null, + "updated_at" timestamp(0) not null + ); +SQL); + +DB::query(<<<SQL + create table if not exists "system" ( + "key" character varying(255) primary key, + "value" jsonb + ); +SQL); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c86117f --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "gemeinwohl-development/cooperative-common-good-game", + "authors": [ + { + "name": "Daniel Weipert", + "email": "code@drogueronin.de" + } + ], + "require": { + "php": "^8.0", + "symfony/config": "^6.3", + "symfony/dotenv": "^6.3", + "symfony/http-foundation": "^6.3", + "symfony/routing": "^6.3", + "twig/twig": "^3.7" + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..535f42f --- /dev/null +++ b/composer.lock @@ -0,0 +1,859 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ee17ea11ec110fee294c21a2ef86e5cc", + "packages": [ + { + "name": "symfony/config", + "version": "v6.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "b47ca238b03e7b0d7880ffd1cf06e8d637ca1467" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/b47ca238b03e7b0d7880ffd1cf06e8d637ca1467", + "reference": "b47ca238b03e7b0d7880ffd1cf06e8d637ca1467", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-19T20:22:16+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "ceadb434fe2a6763a03d2d110441745834f3dd1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/ceadb434fe2a6763a03d2d110441745834f3dd1e", + "reference": "ceadb434fe2a6763a03d2d110441745834f3dd1e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/process": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v6.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-21T14:41:17+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-06-01T08:30:39+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "cac1556fdfdf6719668181974104e6fcfa60e844" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/cac1556fdfdf6719668181974104e6fcfa60e844", + "reference": "cac1556fdfdf6719668181974104e6fcfa60e844", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.2" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3.0", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^5.4|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-22T08:20:46+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-php80": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-16T06:22:46+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/e7243039ab663822ff134fbc46099b5fdfa16f6a", + "reference": "e7243039ab663822ff134fbc46099b5fdfa16f6a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-31T07:08:24+00:00" + }, + { + "name": "twig/twig", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", + "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-08-28T11:09:02+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.0" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6d5f1c1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3" + +services: + app: + build: + context: docker/php + volumes: + - "./:/var/www/html" + + db: + image: postgres + restart: unless-stopped + environment: + - "POSTGRES_DB=${DB_NAME}" + - "POSTGRES_USER=${DB_USER}" + - "POSTGRES_PASSWORD=${DB_PASSWORD}" + volumes: + - "db:/var/lib/postgresql/data" + + web: + build: + context: docker/nginx + ports: + - "8080:80" + volumes: + - "./:/var/www/html" + + adminer: + image: adminer + ports: + - "8081:8080" + environment: + - "ADMINER_DEFAULT_SERVER=db" + +volumes: + db: diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..cad37d9 --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine + +ADD default.conf.template /etc/nginx/templates/ diff --git a/docker/nginx/default.conf.template b/docker/nginx/default.conf.template new file mode 100644 index 0000000..e8b82cd --- /dev/null +++ b/docker/nginx/default.conf.template @@ -0,0 +1,19 @@ +server { + listen 0.0.0.0:80; + + root /var/www/html/public; + index index.php; + + location / { + try_files $uri $uri/ /index.php?$args; + } + + location ~ \.php$ { + include fastcgi_params; + + fastcgi_pass app:9000; + + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; + } +} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..204d790 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,7 @@ +FROM php:fpm-alpine + +RUN : \ + && apk add libpq-dev icu-dev icu-data-full \ + && docker-php-ext-install pdo_pgsql intl + +WORKDIR /var/www/html diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..18edd19 --- /dev/null +++ b/public/index.php @@ -0,0 +1,11 @@ +<?php + +use App\App; +use Symfony\Component\Dotenv\Dotenv; + +require dirname(__DIR__) . '/vendor/autoload.php'; + +$dotenv = new Dotenv(); +$dotenv->load(dirname(__DIR__) . '/.env'); + +(new App())->run(); diff --git a/src/App.php b/src/App.php new file mode 100644 index 0000000..f1fa97e --- /dev/null +++ b/src/App.php @@ -0,0 +1,32 @@ +<?php + +namespace App; + +use Symfony\Component\HttpFoundation\Request; + +class App +{ + public function __construct() { + if ($_ENV['APP_ENV'] === 'development') { + error_reporting(E_ALL); + } + + // DB + DB::init(); + + // Router + Router::init(Request::createFromGlobals()); + + // View + View::init(); + + // Events + new EventRunner(); + } + + public function run(): void + { + $response = Router::execute(); + $response->send(); + } +} diff --git a/src/Controller/Building.php b/src/Controller/Building.php new file mode 100644 index 0000000..d8fe656 --- /dev/null +++ b/src/Controller/Building.php @@ -0,0 +1,49 @@ +<?php + +namespace App\Controller; + +use App\DB; +use App\Model\Building as Model; +use App\Model\Event; +use App\Model\Village; +use App\Router; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +class Building +{ + #[Route(path: '/village/{x}/{y}/building/{type}/level-up', methods: ['POST'])] + public function levelUp(Request $request): Response + { + $village = Village::getByCoordinates($request->get('x'), $request->get('y')); + $building = Model::getByVillage($village->id, $request->get('type')); + + // resources + foreach ($building->getResourceRequirements() as $resourceType => $resourceValue) { + $village->{$resourceType} -= $resourceValue; + } + $village->updateResources(); + + // event + $event = new Event(); + $event->type = 'UpgradeBuilding'; + $event->time = (new \DateTime())->add(\DateInterval::createFromDateString($building->getBuildTime() . ' seconds')); + $event->payload = json_encode([ + 'id' => $building->id, + ]); + + DB::query( + 'insert into events (type, time, payload, village_id) VALUES (:type, :time, :payload, :id)', + ['type' => $event->type, 'time' => $event->time->format('c'), 'payload' => $event->payload, 'id' => $village->id] + ); + + return new RedirectResponse( + Router::generate( + 'village.show', + ['x' => $request->get('x'), 'y' => $request->get('y')] + ) + ); + } +} diff --git a/src/Controller/Village.php b/src/Controller/Village.php new file mode 100644 index 0000000..3854d29 --- /dev/null +++ b/src/Controller/Village.php @@ -0,0 +1,54 @@ +<?php + +namespace App\Controller; + +use App\DB; +use App\Model\Event\UpgradeBuilding; +use App\Model\Village as Model; +use App\View; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +class Village +{ + #[Route(path: '/villages', methods: ['GET'])] + public function list(): Response + { + $villages = DB::fetch(Model::class, "select * from villages"); + + return new Response(View::render('villages.twig', [ + 'villages' => $villages, + ])); + } + + #[Route(path: '/village/{x}/{y}', methods: ['GET'])] + public function show(Request $request): Response + { + $village = Model::getByCoordinates($request->get('x'), $request->get('y')); + + $results = DB::query( + <<<SQL + select events.*, village_buildings.type as building from events + join village_buildings + on village_buildings.id=(events.payload->'id')::bigint + where events.village_id=:id and events.type=:type + SQL, ['id' => $village->id, 'type' => 'UpgradeBuilding'] + )->fetchAll(); + + $events = []; + foreach ($results as $row) { + $events[$row['type']][] = [ + 'event' => DB::convertToModel(UpgradeBuilding::class, $row), + 'data' => [ + 'building' => $row['building'], + ], + ]; + } + + return new Response(View::render('village.twig', [ + 'village' => $village, + 'events' => $events, + ])); + } +} diff --git a/src/DB.php b/src/DB.php new file mode 100644 index 0000000..4e34d32 --- /dev/null +++ b/src/DB.php @@ -0,0 +1,78 @@ +<?php + +namespace App; + +class DB { + private static \PDO $connection; + + public static function init(): void + { + $driver = $_ENV['DB_DRIVER'] ?? 'pgsql'; + $host = $_ENV['DB_HOST'] ?? 'db'; + $dbname = $_ENV['DB_NAME']; + $user = $_ENV['DB_USER']; + $password = $_ENV['DB_PASSWORD']; + + self::$connection = new \PDO("pgsql:host=$host;dbname=$dbname", $user, $password); + } + + /** + * @param string $query + * @param array $params + */ + public static function query(string $query, array $params = []): \PDOStatement|false + { + /**@var \PDOStatement $statement*/ + $statement = self::$connection->prepare($query); + $statement->execute($params); + + return $statement; + } + + /** + * @param string $class + * @param string $query + * @param array $params + * + * @return array<object> + */ + public static function fetch(string $class, string $query, array $params = []): array + { + $rows = DB::query($query, $params)->fetchAll(\PDO::FETCH_ASSOC); + + $results = []; + foreach ($rows as $row) { + $results[] = DB::convertToModel($class, $row); + } + + return $results; + } + + /** + * @param string $class + * @param array $row + * + * @return object + */ + public static function convertToModel(string $class, array $row): object + { + $object = new $class(); + + foreach ($row as $columnKey => $columnValue) { + $objectKey = explode('_', $columnKey); + $objectKey = $objectKey[0] . implode('', array_map('ucwords', array_slice($objectKey, 1))); + + if (property_exists($object, $objectKey)) { + $propertyType = (new \ReflectionProperty($object, $objectKey))->getType(); + + if (class_exists($propertyType->getName())) { + $object->$objectKey = new ($propertyType->getName())($columnValue); + } else { + $object->$objectKey = $columnValue; + } + } + } + + return $object; + } +} diff --git a/src/EventRunner.php b/src/EventRunner.php new file mode 100644 index 0000000..d2f1589 --- /dev/null +++ b/src/EventRunner.php @@ -0,0 +1,86 @@ +<?php + +namespace App; + +use App\Model\Building; +use App\Model\Building\ClayPit; +use App\Model\Building\Farm; +use App\Model\Building\IronMine; +use App\Model\Building\ResourceGenerator; +use App\Model\Building\Storage; +use App\Model\Building\WoodCutter; +use App\Model\Event; +use App\Model\Village; + +class EventRunner +{ + public function __construct() + { + $results = DB::query('select * from events where time < now()')->fetchAll(); + + foreach ($results as $row) { + /**@var Event $event*/ + $event = DB::convertToModel(Event::resolveType($row['type']), $row); + $event(); + + DB::query( + 'delete from events where id=:id', + ['id' => $event->id] + ); + } + + + // Resources + + $lastTick = json_decode(DB::query('select value from system where key=:key', ['key' => 'last_resource_tick'])->fetchColumn()); + if ($lastTick) { + $lastTick = new \DateTime($lastTick); + } else { + $lastTick = (new \DateTime())->modify('- 1 min'); + } + + $diff = (new \DateTime())->diff($lastTick); + $tickMultiplier = $diff->i; + + if ($tickMultiplier > 0) { + $villages = DB::fetch(Village::class, 'select id,wood,clay,iron,food from villages'); + foreach ($villages as $village) { + /**@var Village $village*/ + + /**@var array<int, WoodCutter|ClayPit|IronMine|Farm> $resourceGenerators*/ + $resourceGenerators = []; + + /**@var Storage $storage*/ + $storage = null; + + /**@var Building[] $buildings*/ + $buildings = DB::fetch(ResourceGenerator::class, 'select level,type,village_id from village_buildings where village_id=:id', ['id' => $village->id]); + foreach ($buildings as $building) { + if ($building->type == 'Storage') { + $storage = $building->cast(); + } + else if (in_array($building->type, ['WoodCutter', 'ClayPit', 'IronMine', 'Farm'])) { + $resourceGenerators[] = $building->cast(); + } + } + + $resources = []; + foreach ($resourceGenerators as $generator) { + $village->{$generator->resourceType} = min( + $village->{$generator->resourceType} + ($generator->getResourceIncrementor() * $tickMultiplier), + $storage->getResourceCapacity($generator->resourceType) + ); + } + + DB::query( + 'update villages set wood=:wood, clay=:clay, iron=:iron, food=:food where id=:id', + ['wood' => $village->wood, 'clay' => $village->clay, 'iron' => $village->iron, 'food' => $village->food, 'id' => $village->id] + ); + } + + DB::query('delete from system where key=:key', ['key' => 'last_resource_tick']); + $value = (new \DateTime((new \DateTime())->format('Y-m-d H:i')))->format('c'); + DB::query('insert into system (key,value) VALUES (:key,:value)', ['key' => 'last_resource_tick', 'value' => json_encode($value)]); + } + } +} diff --git a/src/Model.php b/src/Model.php new file mode 100644 index 0000000..5f45ed4 --- /dev/null +++ b/src/Model.php @@ -0,0 +1,19 @@ +<?php + +namespace App; + +class Model +{ + public static function castToType(object $original, string $cast): object + { + $object = new $cast(); + + foreach (get_class_vars(get_class($original)) as $property => $_) { + if (! empty($original->$property) && empty($object->$property)) { + $object->$property = $original->$property; + } + } + + return $object; + } +} diff --git a/src/Model/Building.php b/src/Model/Building.php new file mode 100644 index 0000000..eb166f9 --- /dev/null +++ b/src/Model/Building.php @@ -0,0 +1,102 @@ +<?php + +namespace App\Model; + +use App\DB; +use App\Model; + +class Building +{ + public int $id; + + public int $level; + public string $type; + public int $villageId; + + public \DateTime $createdAt; + public \DateTime $updatedAt; + + public string $unitType; + public int $buildTimeFactor; + public array $resourceRequirements; + public array $buildingRequirements; + public array $techRequirements; + public int $maxLevel; + + + public static function get(int $id): ?Building + { + $results = DB::fetch(Building::class, 'select * from village_buildings where id=:id', ['id' => $id]); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + public static function getByVillage(int $villageId, string $buildingType): ?Building + { + $results = DB::fetch(Building::class, 'select * from village_buildings where village_id=:id and type=:type', ['id' => $villageId, 'type' => $buildingType]); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + public static function getByVillageCoordinates(int $x, int $y, string $buildingType): ?Building + { + $results = DB::fetch( + Building::class, + <<<SQL + select village_buildings.* + from village_buildings + join villages on (villages.x=:x and villages.y=:y) + where village_buildings.village_id=villages.id and type=:type + SQL, + ['x' => $x, 'y' => $y, 'type' => $buildingType] + ); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + + public function getBuildTime(): int + { + $townHall = Village::getBuilding($this->villageId, 'TownHall'); + + $nextLevel = $this->level + 1; + + return intval($nextLevel * ($nextLevel / $townHall->level) * $_ENV['BASE_BUILDING_BUILD_TIME_FACTOR'] * $this->buildTimeFactor); + } + + /** + * @return array<string, int> + */ + public function getResourceRequirements(): array + { + return $this->getResourceRequirementsForLevel($this->level); + } + + /** + * @return array<string, int> + */ + public function getResourceRequirementsForLevel(int $level): array + { + $level += 1; + + return array_map( + fn ($resourceRequirement) => ceil(log($level * 2) * $resourceRequirement * 64 * $level), + $this->resourceRequirements + ); + } + + + /* OOP */ + + public function cast(): Building + { + $class = Building::resolveType($this->type); + + return Model::castToType($this, Building::resolveType($this->type)); + } + + public static function resolveType(string $type): string + { + return __NAMESPACE__ . '\\Building\\' . $type; + } +} diff --git a/src/Model/Building/ClayPit.php b/src/Model/Building/ClayPit.php new file mode 100644 index 0000000..8127818 --- /dev/null +++ b/src/Model/Building/ClayPit.php @@ -0,0 +1,16 @@ +<?php + +namespace App\Model\Building; + +class ClayPit extends ResourceGenerator +{ + public string $unitType = 'PitWorker'; + public int $buildTimeFactor = 1; + public int $maxLevel = 25; + + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; + + public string $resourceType = 'clay'; +} diff --git a/src/Model/Building/Farm.php b/src/Model/Building/Farm.php new file mode 100644 index 0000000..aaa58b5 --- /dev/null +++ b/src/Model/Building/Farm.php @@ -0,0 +1,31 @@ +<?php + +namespace App\Model\Building; + +use App\Model\Unit; +use App\Model\Village; + +class Farm extends ResourceGenerator +{ + public string $unitType = 'Farmer'; + public int $buildTimeFactor = 1; + public int $maxLevel = 25; + + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; + + public string $resourceType = 'food'; + + public function getResourceIncrementor(): int + { + $populationDemand = array_reduce( + Village::getUnits($this->villageId, Village::FETCH_UNIT_RESIDENCE), + function ($carry, Unit $unit) { + return $carry + $unit->getPopulationDemand(); + } + ); + + return parent::getResourceIncrementor() - $populationDemand; + } +} diff --git a/src/Model/Building/IronMine.php b/src/Model/Building/IronMine.php new file mode 100644 index 0000000..4bf5cc6 --- /dev/null +++ b/src/Model/Building/IronMine.php @@ -0,0 +1,16 @@ +<?php + +namespace App\Model\Building; + +class IronMine extends ResourceGenerator +{ + public string $unitType = 'Miner'; + public int $buildTimeFactor = 1; + public int $maxLevel = 25; + + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; + + public string $resourceType = 'iron'; +} diff --git a/src/Model/Building/ResourceGenerator.php b/src/Model/Building/ResourceGenerator.php new file mode 100644 index 0000000..5f1a6bb --- /dev/null +++ b/src/Model/Building/ResourceGenerator.php @@ -0,0 +1,22 @@ +<?php + +namespace App\Model\Building; + +use App\Model\Building; +use App\Model\Unit; + +class ResourceGenerator extends Building +{ + public string $resourceType; + + public function getResourceIncrementor(): int + { + $amountResiding = Unit::getAmountResiding($this->unitType, $this->villageId); + + return (int)ceil( + log( + ($this->level * $amountResiding) + 1 + ) * $_ENV['BASE_RESOURCE_GENERATION_FACTOR'] + ); + } +} diff --git a/src/Model/Building/Storage.php b/src/Model/Building/Storage.php new file mode 100644 index 0000000..fde4c4e --- /dev/null +++ b/src/Model/Building/Storage.php @@ -0,0 +1,28 @@ +<?php + +namespace App\Model\Building; + +use App\Model\Building; +use App\Model\Village; + +class Storage extends Building +{ + public int $buildTimeFactor = 1; + public int $maxLevel = 25; + + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; + + public function getCapacity(): int + { + return $this->level * 2560; + } + + public function getResourceCapacity(string $resourceType): int + { + $p = Village::getStorageConfig($this->villageId)->$resourceType / 100; + + return ceil($this->getCapacity() * $p); + } +} diff --git a/src/Model/Building/TownHall.php b/src/Model/Building/TownHall.php new file mode 100644 index 0000000..608f083 --- /dev/null +++ b/src/Model/Building/TownHall.php @@ -0,0 +1,17 @@ +<?php + +namespace App\Model\Building; + +use App\Model\Building; + +class TownHall extends Building +{ + public int $buildTimeFactor = 1; + public int $maxLevel = 25; + + public array $resourceRequirements = [ + 'wood' => 1.0, + 'clay' => 1.0, + 'iron' => 1.0, + ]; +} diff --git a/src/Model/Building/WoodCutter.php b/src/Model/Building/WoodCutter.php new file mode 100644 index 0000000..86bde9b --- /dev/null +++ b/src/Model/Building/WoodCutter.php @@ -0,0 +1,16 @@ +<?php + +namespace App\Model\Building; + +class WoodCutter extends ResourceGenerator +{ + public string $unitType = 'WoodCutter'; + public int $buildTimeFactor = 1; + public int $maxLevel = 25; + + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; + + public string $resourceType = 'wood'; +} diff --git a/src/Model/Event.php b/src/Model/Event.php new file mode 100644 index 0000000..aa235f9 --- /dev/null +++ b/src/Model/Event.php @@ -0,0 +1,32 @@ +<?php + +namespace App\Model; + +use App\Model; + +class Event +{ + public int $id; + + public string $type; + public \DateTime $time; + public string $payload; + + public int $villageId; + + + /* OOP */ + + public function cast(): Event + { + $class = Event::resolveType($this->type); + $object = new $class(); + + return Model::castToType($this, Event::resolveType($this->type)); + } + + public static function resolveType(string $type): string + { + return __NAMESPACE__ . '\\Event\\' . $type; + } +} diff --git a/src/Model/Event/TrainUnits.php b/src/Model/Event/TrainUnits.php new file mode 100644 index 0000000..0c7e0de --- /dev/null +++ b/src/Model/Event/TrainUnits.php @@ -0,0 +1,27 @@ +<?php + +namespace App\Model\Event; + +use App\DB; +use App\Model\Event; + +class TrainUnits extends Event +{ + /** + * @return void + */ + public function __invoke(): void + { + $payload = json_decode($this->payload, true); + + DB::query( + <<<SQL + insert into village_units (amount, type, is_traveling, home_village_id, residence_village_id) + values (:amount, :type, false, :id, :id) + on conflict (type, home_village_id, residence_village_id) + do update set amount = excluded.amount+:amount + SQL, + ['amount' => $payload['amount'], 'type' => $payload['type'], 'id' => $this->villageId] + ); + } +} diff --git a/src/Model/Event/UpgradeBuilding.php b/src/Model/Event/UpgradeBuilding.php new file mode 100644 index 0000000..c014cfe --- /dev/null +++ b/src/Model/Event/UpgradeBuilding.php @@ -0,0 +1,27 @@ +<?php + +namespace App\Model\Event; + +use App\DB; +use App\Model\Event; + +class UpgradeBuilding extends Event +{ + /** + * @return void + */ + public function __invoke(): void + { + $payload = json_decode($this->payload, true); + + DB::query( + 'update village_buildings set level=level+1 where id=:id', + ['id' => $payload['id']] + ); + + DB::query( + 'delete from events where id=:id', + ['id' => $this->id] + ); + } +} diff --git a/src/Model/Unit.php b/src/Model/Unit.php new file mode 100644 index 0000000..a0d1a35 --- /dev/null +++ b/src/Model/Unit.php @@ -0,0 +1,77 @@ +<?php + +namespace App\Model; + +use App\DB; +use App\Model; + +class Unit +{ + public int $id; + + public int $amount; + public string $type; + public bool $isTraveling; + + public int $homeVillageId; + public int $residenceVillageId; + + public string $createdAt; + public string $updatedAt; + + public string $buildingType; + public int $travelTime; + public int $populationDemandFactor; + public array $resourceRequirements = []; + + + public function getBuildTime(int $amount): int + { + return intval(($_ENV['BASE_UNIT_BUILD_TIME_FACTOR'] / ($this->getBuilding()->level ?: 1)) * $amount); + } + + public function getPopulationDemand(): int + { + return $this->getPopulationDemandForAmount($this->amount); + } + + public function getPopulationDemandForAmount(int $amount): int + { + return $amount * $this->populationDemandFactor; + } + + + /* Relations */ + + public function getBuilding(): ?Building + { + return Village::getBuilding($this->homeVillageId, $this->buildingType); + } + + public function cast(): Unit + { + $class = Unit::resolveType($this->type); + + return Model::castToType($this, Unit::resolveType($this->type)); + } + + public static function resolveType(string $type): string + { + return __NAMESPACE__ . '\\Unit\\' . $type; + } + + + /* Static */ + + public static function getAmountResiding(string $unitType, int $villageId): int + { + $statement = DB::query( + 'select SUM(amount) from village_units where type=:type and residence_village_id=:id', + ['type' => $unitType, 'id' => $villageId] + ); + $result = $statement->fetch()['sum']; + + return intval($result); + } + +} diff --git a/src/Model/Unit/Farmer.php b/src/Model/Unit/Farmer.php new file mode 100644 index 0000000..de37802 --- /dev/null +++ b/src/Model/Unit/Farmer.php @@ -0,0 +1,15 @@ +<?php + +namespace App\Model\Unit; + +use App\Model\Unit; + +class Farmer extends Unit +{ + public string $buildingType = 'Farm'; + public int $travelTime = 1; + public int $populationDemandFactor = 1; + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; +} diff --git a/src/Model/Unit/Miner.php b/src/Model/Unit/Miner.php new file mode 100644 index 0000000..ae6c00a --- /dev/null +++ b/src/Model/Unit/Miner.php @@ -0,0 +1,15 @@ +<?php + +namespace App\Model\Unit; + +use App\Model\Unit; + +class Miner extends Unit +{ + public string $buildingType = 'IronMine'; + public int $travelTime = 1; + public int $populationDemandFactor = 1; + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; +} diff --git a/src/Model/Unit/PitWorker.php b/src/Model/Unit/PitWorker.php new file mode 100644 index 0000000..4f873b4 --- /dev/null +++ b/src/Model/Unit/PitWorker.php @@ -0,0 +1,15 @@ +<?php + +namespace App\Model\Unit; + +use App\Model\Unit; + +class PitWorker extends Unit +{ + public string $buildingType = 'ClayPit'; + public int $travelTime = 1; + public int $populationDemandFactor = 1; + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; +} diff --git a/src/Model/Unit/WoodCutter.php b/src/Model/Unit/WoodCutter.php new file mode 100644 index 0000000..17923ca --- /dev/null +++ b/src/Model/Unit/WoodCutter.php @@ -0,0 +1,15 @@ +<?php + +namespace App\Model\Unit; + +use App\Model\Unit; + +class WoodCutter extends Unit +{ + public string $buildingType = 'WoodCutter'; + public int $travelTime = 1; + public int $populationDemandFactor = 1; + public array $resourceRequirements = [ + 'wood' => 1.0, + ]; +} diff --git a/src/Model/User.php b/src/Model/User.php new file mode 100644 index 0000000..fcb1869 --- /dev/null +++ b/src/Model/User.php @@ -0,0 +1,7 @@ +<?php + +namespace App\Model; + +class User +{ +} diff --git a/src/Model/Village.php b/src/Model/Village.php new file mode 100644 index 0000000..cd1c749 --- /dev/null +++ b/src/Model/Village.php @@ -0,0 +1,135 @@ +<?php + +namespace App\Model; + +use App\DB; +use App\Model\Building\Storage; +use App\Model\Village\StorageConfig; + +class Village +{ + public int $id; + + public string $name; + + public int $x; + public int $y; + + public int $wood; + public int $clay; + public int $iron; + public int $food; + + public int $satisfaction; + + public string $createdAt; + public string $updatedAt; + + public static function canBuild(Village $village, Building $building): bool + { + if ($building->level >= $building->maxLevel) { + return false; + } + + $resourceRequirements = $building->getResourceRequirements(); + foreach ($resourceRequirements as $resourceType => $requirement) { + if ($village->$resourceType < $requirement) { + return false; + } + } + + return true; + } + + /* DB - Actions */ + + public static function get(int $id): ?Village + { + return DB::fetch(Village::class, 'select * from villages where id=:id', ['id' => $id])[0] ?? null; + } + + public static function getByCoordinates(int $x, int $y): ?Village + { + return DB::fetch(Village::class, 'select * from villages where x=:x and y=:y', ['x' => $x, 'y' => $y])[0] ?? null; + } + + public function updateResources(): mixed + { + return DB::query( + 'update villages set wood=:wood,clay=:clay,iron=:iron,food=:food where id=:id', + ['wood' => $this->wood, 'clay' => $this->clay, 'iron' => $this->iron, 'food' => $this->food, 'id' => $this->id] + ); + } + + /* DB - Relations */ + + public static function getBuildings(int $villageId): array + { + $buildings = DB::fetch(Building::class, 'select * from village_buildings where village_id=:id', ['id' => $villageId]); + + return array_map(function (Building $building) { + return $building->cast(); + }, $buildings); + } + + public static function getBuilding(int $villageId, string $buildingType): ?Building + { + $results = DB::fetch( + Building::resolveType($buildingType), + 'select * from village_buildings where village_id=:id and type=:type', + ['id' => $villageId, 'type' => $buildingType] + ); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + public static function getStorage(int $villageId): ?Storage + { + return Village::getBuilding($villageId, 'Storage'); + } + + public static function getStorageConfig(int $villageId): ?StorageConfig + { + $results = DB::fetch( + StorageConfig::class, + 'select * from village_storage_config where village_id=:id', + ['id' => $villageId] + ); + + return $results[0] ?? null; + } + + public const FETCH_UNIT_HOME_AT_HOME = 1; + public const FETCH_UNIT_HOME_AT_SUPPORT = 2; + public const FETCH_UNIT_SUPPORT_AT_HOME = 3; + public const FETCH_UNIT_RESIDENCE = 4; + + public static function getUnit(string $unitType, int $flag): ?Unit + { + } + + /** + * @param int $flag + * + * @return array<int, Unit> + */ + public static function getUnits(int $villageId, $flag = Village::FETCH_UNIT_ALL): array + { + if ($flag == Village::FETCH_UNIT_HOME_AT_HOME) { + $units = DB::fetch(Unit::class, 'select * from village_units where home_village_id=:id and residence_village_id=:id', ['id' => $villageId]); + } + else if ($flag == Village::FETCH_UNIT_HOME_AT_SUPPORT) { + $units = DB::fetch(Unit::class, 'select * from village_units where home_village_id=:id and residence_village_id!=:id', ['id' => $villageId]); + } + else if ($flag == Village::FETCH_UNIT_SUPPORT_AT_HOME) { + $units = DB::fetch(Unit::class, 'select * from village_units where home_village_id!=:id and residence_village_id=:id', ['id' => $villageId]); + } + else if ($flag == Village::FETCH_UNIT_RESIDENCE) { + $units = DB::fetch(Unit::class, 'select * from village_units where residence_village_id=:id', ['id' => $villageId]); + } + + return array_map(function (Unit $unit) { + return $unit->cast(); + }, $units); + } +} diff --git a/src/Model/Village/StorageConfig.php b/src/Model/Village/StorageConfig.php new file mode 100644 index 0000000..272eaf5 --- /dev/null +++ b/src/Model/Village/StorageConfig.php @@ -0,0 +1,18 @@ +<?php + +namespace App\Model\Village; + +class StorageConfig +{ + public int $id; + + public int $wood; + public int $clay; + public int $iron; + public int $food; + + public int $villageId; + + public string $createdAt; + public string $updatedAt; +} diff --git a/src/Router.php b/src/Router.php new file mode 100644 index 0000000..8b24000 --- /dev/null +++ b/src/Router.php @@ -0,0 +1,80 @@ +<?php + +namespace App; + +use App\Support\RouteLoader; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\Routing\Loader\AnnotationFileLoader; +use Symfony\Component\Routing\Matcher\UrlMatcher; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RouteCollection; + +class Router +{ + public static Request $request; + public static RequestContext $context; + public static RouteCollection $routes; + + public static function init(Request $request): void + { + self::$request = $request; + + self::$context = new RequestContext(); + self::$context->fromRequest($request); + + self::$routes = new RouteCollection(); + $loader = new AnnotationFileLoader(new FileLocator(), new RouteLoader()); + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__ . '/Controller')); + foreach ($iterator as $file) { + /**@var \SplFileInfo $file*/ + if (in_array($file->getFilename(), ['.', '..'])) continue; + + $collection = $loader->load($file->getPathname(), 'attribute'); + self::$routes->addCollection($collection); + } + } + + public static function execute(): Response + { + try { + $matcher = new UrlMatcher(self::$routes, self::$context); + $match = $matcher->matchRequest(self::$request); + + foreach ($match as $key => $value) { + if (str_starts_with($key, '_')) continue; + + self::$request->query->set($key, $value); + } + + /**@var \ReflectionClass $class*/ + $class = $match['_']['class']; + /**@var \ReflectionMethod $method*/ + $method = $match['_']['method']; + + return ($class->newInstance())->{$method->getName()}(self::$request); + } catch (ResourceNotFoundException $exception) { + return new Response('404', 404); + } catch (MethodNotAllowedException $exception) { + return new Response('403', 403); + } catch (\Exception $exception) { + return new Response('500: ' . $exception->getMessage(), 500); + } + } + + /** + * @param string $name + * @param array $parameters + * @param int $referenceType + */ + public static function generate(string $name, array $parameters = [], int $referenceType = 1): string + { + $generator = new UrlGenerator(self::$routes, self::$context); + + return $generator->generate($name, $parameters, $referenceType); + } +} diff --git a/src/Support/ResourceType.php b/src/Support/ResourceType.php new file mode 100644 index 0000000..96c0f2c --- /dev/null +++ b/src/Support/ResourceType.php @@ -0,0 +1,11 @@ +<?php + +namespace App\Support; + +enum ResourceType: string +{ + case Wood = 'Wood'; + case Clay = 'Clay'; + case Iron = 'Iron'; + case Food = 'Food'; +} diff --git a/src/Support/RouteLoader.php b/src/Support/RouteLoader.php new file mode 100644 index 0000000..ba124c5 --- /dev/null +++ b/src/Support/RouteLoader.php @@ -0,0 +1,24 @@ +<?php + +namespace App\Support; + +use Symfony\Component\Routing\Loader\AnnotationClassLoader; +use Symfony\Component\Routing\Route; + +class RouteLoader extends AnnotationClassLoader +{ + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annotation) { + $route->setDefault('_', compact('class', 'method', 'annotation')); + } + + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + { + $name = parent::getDefaultRouteName($class, $method); + + return str_replace( + '_', + '.', + str_replace('app_controller_', '', $name) + ); + } +} diff --git a/src/Support/UnitType.php b/src/Support/UnitType.php new file mode 100644 index 0000000..5a53f24 --- /dev/null +++ b/src/Support/UnitType.php @@ -0,0 +1,11 @@ +<?php + +namespace App\Support; + +enum UnitType: string +{ + case WoodCutter = 'WoodCutter'; + case PitWorker = 'PitWorker'; + case Miner = 'Miner'; + case Farmer = 'Farmer'; +} diff --git a/src/View.php b/src/View.php new file mode 100644 index 0000000..5f9ca34 --- /dev/null +++ b/src/View.php @@ -0,0 +1,37 @@ +<?php + +namespace App; + +use Twig\Environment; +use Twig\Extension\DebugExtension; +use Twig\Loader\FilesystemLoader; +use Twig\TwigFilter; + +class View +{ + private static Environment $twig; + + public static function init(): void + { + $loader = new FilesystemLoader(dirname(__DIR__) . '/views'); + self::$twig = new Environment($loader, [ + 'debug' => $_ENV['APP_ENV'] === 'development', + ]); + + self::$twig->addExtension(new DebugExtension()); + // self::$twig->addExtension(new IntlExtension()); + + self::$twig->addFilter(new TwigFilter('buildTime', function ($buildTime) { + return @sprintf('%02d:%02d:%02d', $buildTime / 3600, ($buildTime / 60) % 60, $buildTime % 60); + })); + } + + /** + * @param string $name + * @param array $context + */ + public static function render(string $name, array $context = []): string + { + return self::$twig->render($name, $context); + } +} diff --git a/views/base.twig b/views/base.twig new file mode 100644 index 0000000..94e5037 --- /dev/null +++ b/views/base.twig @@ -0,0 +1,26 @@ +{% extends 'root.twig' %} + +{% block body %} +<div class="wrap"> + <header></header> + + <main> + {% block main %}{% endblock %} + </main> + + <footer></footer> + + <div class="global-timer"></div> + <script> + const timer = document.querySelectorAll('.global-timer'); + function setTime () { + const now = new Date(); + for (const t of timer) { + t.innerHTML = now; + } + } + setTime(); + setInterval(setTime, 1000); + </script> +</div> +{% endblock %} diff --git a/views/components/timer.twig b/views/components/timer.twig new file mode 100644 index 0000000..97977da --- /dev/null +++ b/views/components/timer.twig @@ -0,0 +1,25 @@ +<span class="timer" data-time="{{ time }}"> + <span class="timer__time"></span> +</span> +<script> +document.addEventListener('DOMContentLoaded', function (ev) { + const timer = document.querySelector('.timer[data-time="{{ time }}"] .timer__time'); + const time = new Date('{{ time }}'); + + const interval = setInterval(setTime, 1000); + function setTime() { + let diff = time - new Date(); + if (diff <= -1) { + clearInterval(interval); + } + + const hh = Math.floor(diff/1000/60/60); + diff -= hh*1000*60*60; + const mm = Math.floor(diff/1000/60); + diff -= mm*1000*60; + const ss = Math.floor(diff/1000); + timer.innerHTML = `${('00' + hh).slice(-2)}:${('00' + mm).slice(-2)}:${('00' + ss).slice(-2)}`; + } + setTime(); +}); +</script> diff --git a/views/root.twig b/views/root.twig new file mode 100644 index 0000000..11dc665 --- /dev/null +++ b/views/root.twig @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> +</head> +<body> + {% block body %}{% endblock %} +</body> +</html> diff --git a/views/village.twig b/views/village.twig new file mode 100644 index 0000000..7bb55b2 --- /dev/null +++ b/views/village.twig @@ -0,0 +1,169 @@ +{% extends 'base.twig' %} + +{% block main %} +<div class="village"> + + <div class="village__top"> + <div><span>{{ village.x }} x {{ village.y }}</span> — {{ village.name }}</div> + + <div class="resources"> + <span>wood: <b>{{ village.wood }}</b> / {{ village.getStorage(village.id).getResourceCapacity('wood') }} – {{ village.getBuilding(village.id, 'WoodCutter').getResourceIncrementor() }}</span> + <span>clay: <b>{{ village.clay }}</b> / {{ village.getStorage(village.id).getResourceCapacity('clay') }} – {{ village.getBuilding(village.id, 'ClayPit').getResourceIncrementor() }}</span> + <span>iron: <b>{{ village.iron }}</b> / {{ village.getStorage(village.id).getResourceCapacity('iron') }} – {{ village.getBuilding(village.id, 'IronMine').getResourceIncrementor() }}</span> + <span>food: <b>{{ village.food }}</b> / {{ village.getStorage(village.id).getResourceCapacity('food') }} – {{ village.getBuilding(village.id, 'Farm').getResourceIncrementor() }}</span> + + <span>capacity: {{ village.getStorage(village.id).getCapacity() }}</span> + </div> + </div> + + <div class="village__events"> + <h3>Events</h3> + + {% if events['UpgradeBuilding'] %} + <h4>Upgrade Buildings</h4> + <table> + <thead> + <tr> + <th>Building</th> + <th>Time</th> + <th></th> + </tr> + </thead> + <tbody> + {% for event in events['UpgradeBuilding'] %} + <tr> + <td>{{ event.data.building }}</td> + <td class="timer"> + {% include 'components/timer.twig' with { 'time': event.event.time|date('c') } %} + </td> + <td> + <a class="btn" href="/village/{{ village.x }}/{{ village.y }}/building/{{ event.data.building }}/build/cancel"> + Cancel + </a> + </td> + </tr> + {% endfor %} + </tbody> + </table> + {% endif %} + + {% if events.train %} + <h4>Train Units</h4> + {% endif %} + + {% if events.send %} + <h4>Send Resources / Units</h4> + {% endif %} + </div> + + <div class="village__main"> + <div class="village__buildings"> + <h3>Buildings</h3> + <table> + <thead> + <tr> + <th>Type</th> + <th>Level</th> + <th>Build Time</th> + <th>Resources</th> + <th></th> + </tr> + </thead> + <tbody> + {% for building in village.getBuildings(village.id) %} + <tr class="village__buildings__row"> + <td>{{ building.type }}</td> + <td>{{ building.level }}</td> + <td>{{ building.getBuildTime() | buildTime }}</td> + <td class="resources"> + <span>wood: {{ building.getResourceRequirements()['wood'] }}</span> + + <span>clay: {{ building.getResourceRequirements()['clay'] }}</span> + + <span>iron: {{ building.getResourceRequirements()['iron'] }}</span> + </td> + <td> + <form action="/village/{{ village.x }}/{{ village.y }}/building/{{ building.type }}/level-up" method="post"> + <input type="submit" value="Level up" {{ village.canBuild(village, building) ? '' : 'disabled' }}> + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + + <div class="village_units"> + <h3>Units</h3> + <table> + <thead> + <tr> + <th>Type</th> + <th>Amount</th> + <th>Build Time</th> + <th>Resources</th> + <th></th> + </tr> + </thead> + <tbody> + {% for unit in village.getUnits(village.id, 1) %} + <tr> + <td>{{ unit.type }}</td> + <td>{{ unit.amount }}</td> + <td> + {{ unit.getBuildTime(1) | buildTime }} + </td> + <td> + <span>wood: {{ unit.getResourceRequirements()['wood'] }}</span> + + <span>clay: {{ unit.getResourceRequirements()['clay'] }}</span> + + <span>iron: {{ unit.getResourceRequirements()['iron'] }}</span> + + <span>food: {{ unit.getResourceRequirements()['food'] ?? 0 }}</span> + </td> + <td> + <form action="/village/{{ village.x }}/{{ village.y }}/unit/{{ unit.type }}/create" method="post" class="inline"> + <input type="number" min="0" name="amount" placeholder="Amount"> + <input type="submit" value="Create"> + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> + + <h4>Supporting Units</h4> + <table> + <thead> + <tr> + <th>Type</th> + <th>Amount</th> + <th>Origin</th> + <th>Location</th> + <th></th> + </tr> + </thead> + <tbody> + {% for unit in village.getUnits(village.id, 2) | merge(village.getUnits(village.id, 3)) %} + <tr> + <td>{{ unit.type }}</td> + <td>{{ unit.amount }}</td> + <td>{{ village.get(unit.homeVillageId).name }}</td> + <td>{{ not unit.isTraveling ? village.get(unit.residenceVillageId).name : '~traveling~' }}</td> + <td> + {% if not unit.isTraveling %} + <form action="/village/{{ village.id }}/unit/{{ unit.id }}/send-back" method="post" class="inline"> + <input type="number" min="1" max="{{ unit.amount }}" name="amount" placeholder="Amount" required> + <input type="submit" value="{{ (unit.homeVillageId != unit.residenceVillageId) ? 'Send Back' : 'Recall Home' }}"> + </form> + {% endif %} + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> +</div> +{% endblock %} diff --git a/views/villages.twig b/views/villages.twig new file mode 100644 index 0000000..21d8cec --- /dev/null +++ b/views/villages.twig @@ -0,0 +1,38 @@ +{% extends 'base.twig' %} + +{% block main %} +<table> + <thead> + <tr> + <td>Name</td> + <td>Coordinates</td> + <td>Wood</td> + <td>Clay</td> + <td>Iron</td> + <td>Food</td> + <td>Storage</td> + <td>Reputation</td> + </tr> + </thead> + <tbody> + {% for village in villages %} + <tr> + <td> + <a href="/village/{{ village.x}}/{{ village.y }}"> + {{ village.name }} + </a> + </td> + <td> + { x: {{ village.x }}, y: {{ village.y }} } + </td> + <td>{{ village.wood }}</td> + <td>{{ village.clay }}</td> + <td>{{ village.iron }}</td> + <td>{{ village.food }}</td> + <td>{{ village.getStorage(village.id).getCapacity() * (25 / 100) }}</td> + <td>{{ village.reputation }}</td> + </tr> + {% endfor %} + </tbody> +</table> +{% endblock %} |