Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stability++ #23

Merged
merged 1 commit into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
- 'www/'

jobs:
composer-check:
server-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -43,7 +43,7 @@ jobs:
- name: "Check code coverage min percentage"
timeout-minutes: 5
run: |
echo '<?php preg_match("~Lines:\s+([\d.]+)%~", stream_get_contents(STDIN), $m);exit((int)((float)$m[1] < 99.0));' > cc.php
echo '<?php preg_match("~Lines:\s+([\d.]+)%~", stream_get_contents(STDIN), $m);exit((int)((float)$m[1] < 99.87));' > cc.php
export XDEBUG_MODE=coverage
composer unit -- --stderr --no-progress --colors=never \
--coverage-xml=www/coverage/coverage-xml --log-junit=www/coverage/junit.xml \
Expand All @@ -52,7 +52,7 @@ jobs:
grep 'Lines: ' cc.txt | php -d error_reporting=E_ALL cc.php

- name: "Check infection mutation framework min percentage"
timeout-minutes: 10
timeout-minutes: 8
run: |
export XDEBUG_MODE=off
grep '"timeout": 20,' infection.json5
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Counter-Strike: Football [![Tests](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml/badge.svg)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml)
# Counter-Strike: Football [![Tests](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml/badge.svg)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml) [![Code coverage](https://img.shields.io/badge/Code%20coverage-100%25-green?style=flat)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml)

Competitive multiplayer FPS game where two football fan teams fight with the goal of winning more rounds than the opponent team.

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"scripts": {
"stan": "php vendor/bin/phpstan --memory-limit=300M analyze",
"unit": "php vendor/bin/phpunit -d memory_limit=70M",
"infection": "php -d memory_limit=180M vendor/bin/infection --only-covered --threads=6 --min-covered-msi=87",
"infection": "php -d memory_limit=180M vendor/bin/infection --only-covered --threads=6 --min-covered-msi=99",
"infection-cache": "@infection --coverage=www/coverage/",
"dev": "php cli/server.php 1 8080 --debug & php cli/udp-ws-bridge.php",
"dev2": "php cli/server.php 2 8080 --debug & php cli/udp-ws-bridge.php & php cli/udp-ws-bridge.php 8082",
Expand Down
12 changes: 12 additions & 0 deletions infection.json5
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,19 @@
"@operator": false,
"@regex": true,
"@removal": true,
"MatchArmRemoval": {
"ignoreSourceCodeByRegex": [
".+GameException::invalid\\(.+",
],
},
"ArrayItemRemoval": {
"ignore": [
"cs\\Event\\*::serialize",
],
},
"MethodCallRemoval": {
"ignoreSourceCodeByRegex": [
"\\$this->setActiveFloor\\(.+\\);",
"\\$prevPos->setFrom\\(\\$candidate\\);",
"\\$prevPos->setFrom\\(\\$newPos\\);",
"\\$this->makeSound\\(.+\\);",
Expand All @@ -44,6 +55,7 @@
"\\$soundEvent->setSurface\\(.+\\);",
"\\$soundEvent->addExtra\\(.+\\);",
"\\$this->addSoundEvent\\(.+\\);",
"\\$bullet->addPlayerIdSkip\\(\\$playerId\\);",
]
},
"@return_value": true,
Expand Down
17 changes: 5 additions & 12 deletions server/src/Core/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,15 @@ public function tick(int $tickId): ?GameOverEvent
}
}
$this->backtrack->finishState();
$this->checkRoundEnd($alivePlayers[0], $alivePlayers[1]);
if (!$this->roundEndCoolDown) {
$this->checkRoundEnd($alivePlayers[0], $alivePlayers[1]);
}
$this->processEvents($tickId);
return null;
}

private function checkRoundEnd(int $defendersAlive, int $attackersAlive): void
{
if ($this->roundEndCoolDown) {
return;
}

if ($this->playersCountAttackers > 0 && $attackersAlive === 0) {
$this->roundEnd(false, RoundEndReason::ALL_ENEMIES_ELIMINATED);
return;
Expand Down Expand Up @@ -372,10 +370,6 @@ public function bombPlanted(Player $planter): void

public function roundEnd(bool $attackersWins, RoundEndReason $reason): void
{
if ($this->roundEndCoolDown) {
return;
}

$this->roundEndCoolDown = true;
$roundEndEvent = new RoundEndEvent($this, $attackersWins, $reason);
$roundEndEvent->onComplete[] = fn() => $this->endRound($roundEndEvent);
Expand Down Expand Up @@ -467,8 +461,7 @@ private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $
$amount += match ($roundEndEvent->reason) {
RoundEndReason::ALL_ENEMIES_ELIMINATED => 3250,
RoundEndReason::BOMB_EXPLODED => 3500,
RoundEndReason::TIME_RUNS_OUT,
RoundEndReason::BOMB_DEFUSED => throw new GameException('Invalid? ' . $roundEndEvent->reason->value), // @codeCoverageIgnore
RoundEndReason::TIME_RUNS_OUT, RoundEndReason::BOMB_DEFUSED => GameException::invalid((string)$roundEndEvent->reason->value), // @codeCoverageIgnore
};
} elseif (!$player->isAlive()) {
$amount += $this->score->getMoneyLossBonus(true);
Expand All @@ -482,7 +475,7 @@ private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $
$amount += match ($roundEndEvent->reason) {
RoundEndReason::ALL_ENEMIES_ELIMINATED, RoundEndReason::TIME_RUNS_OUT => 3250,
RoundEndReason::BOMB_DEFUSED => 3500,
RoundEndReason::BOMB_EXPLODED => throw new GameException('Invalid? ' . $roundEndEvent->reason->value), // @codeCoverageIgnore
RoundEndReason::BOMB_EXPLODED => GameException::invalid((string)$roundEndEvent->reason->value), // @codeCoverageIgnore
};
} else {
$amount += $this->score->getMoneyLossBonus(false);
Expand Down
5 changes: 5 additions & 0 deletions server/src/Core/GameException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ public static function notImplementedYet(string $msg = ''): never
throw new self("Not implemented yet! " . $msg);
}

public static function invalid(string $msg = ''): never
{
throw new self("This should not be called! " . $msg);
}

}
42 changes: 25 additions & 17 deletions server/src/Core/Inventory.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ public function reset(bool $isAttackerSide, bool $respawn): void
];
$this->equippedSlot = InventorySlot::SLOT_SECONDARY->value;
$this->lastEquippedSlotId = InventorySlot::SLOT_KNIFE->value;
$this->lastEquippedGrenadeSlots = [
InventorySlot::SLOT_GRENADE_SMOKE->value, InventorySlot::SLOT_GRENADE_MOLOTOV->value, InventorySlot::SLOT_GRENADE_HE->value,
InventorySlot::SLOT_GRENADE_FLASH->value, InventorySlot::SLOT_GRENADE_DECOY->value,
];
$this->lastEquippedGrenadeSlots = InventorySlot::getGrenadeSlotIds();
} else {
foreach ($this->items as $item) {
$item->reset();
Expand All @@ -52,12 +49,15 @@ public function reset(bool $isAttackerSide, bool $respawn): void
}
}

$this->removeBomb();
if ($this->has(InventorySlot::SLOT_BOMB->value)) {
$this->removeBomb();
}
$this->store->reset($isAttackerSide, $this->items);
}

private function updateEquippedSlot(): int
private function updateEquippedSlot(Item $item): int
{
$this->tryRemoveLastEquippedGrenade($item);
if (isset($this->items[$this->equippedSlot])) {
return $this->equippedSlot;
}
Expand All @@ -72,15 +72,29 @@ private function updateEquippedSlot(): int

public function removeBomb(): InventorySlot
{
unset($this->items[InventorySlot::SLOT_BOMB->value]);
return InventorySlot::from($this->updateEquippedSlot());
$bomb = $this->items[InventorySlot::SLOT_BOMB->value] ?? null;
if ($bomb) {
unset($this->items[InventorySlot::SLOT_BOMB->value]);
return InventorySlot::from($this->updateEquippedSlot($bomb));
}

GameException::invalid('You do not have bomb!'); // @codeCoverageIgnore
}

public function getEquipped(): Item
{
return $this->items[$this->equippedSlot];
}

private function tryRemoveLastEquippedGrenade(Item $item): void
{
if ($item instanceof Grenade) {
$index = array_search($item->getSlot()->value, $this->lastEquippedGrenadeSlots, true);
assert(is_int($index));
unset($this->lastEquippedGrenadeSlots[$index]);
}
}

public function removeEquipped(): ?Item
{
if (!$this->getEquipped()->isUserDroppable()) {
Expand All @@ -90,10 +104,7 @@ public function removeEquipped(): ?Item
$item = $this->items[$this->equippedSlot];
if ($item->getQuantity() === 1) {
unset($this->items[$this->equippedSlot]);
if ($item instanceof Grenade) {
unset($this->lastEquippedGrenadeSlots[$this->equippedSlot]);
}
$this->updateEquippedSlot();
$this->updateEquippedSlot($item);
$item->unEquip();

return $item;
Expand All @@ -116,10 +127,7 @@ public function removeSlot(int $slot): void
}

unset($this->items[$slot]);
if ($item instanceof Grenade) {
unset($this->lastEquippedGrenadeSlots[$slot]);
}
$this->updateEquippedSlot();
$this->updateEquippedSlot($item);
}

public function canBuy(Item $item): bool
Expand Down Expand Up @@ -176,7 +184,7 @@ public function equip(InventorySlot $slot): ?EquipEvent
$this->lastEquippedSlotId = $this->equippedSlot;
$this->equippedSlot = $slot->value;
if ($item instanceof Grenade) {
unset($this->lastEquippedGrenadeSlots[$slot->value]);
$this->tryRemoveLastEquippedGrenade($item);
array_unshift($this->lastEquippedGrenadeSlots, $slot->value);
}
return $item->equip();
Expand Down
2 changes: 1 addition & 1 deletion server/src/Core/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public function canPurchaseMultipleTime(self $newSlotItem): bool
{
return match ($this->getType()) {
ItemType::TYPE_WEAPON_PRIMARY, ItemType::TYPE_WEAPON_SECONDARY => true,
default => GameException::notImplementedYet('New item? ' . get_class($this)) // @codeCoverageIgnore
default => GameException::invalid('New item? ' . get_class($this)) // @codeCoverageIgnore
};
}

Expand Down
2 changes: 1 addition & 1 deletion server/src/Core/Plane.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function getHitAntiForce(Point $point): int
$hit = $point->to2D($this->axis2d);
if ($hit->x < $this->point2DStart->x || $hit->x > $this->point2DEnd->x
|| $hit->y < $this->point2DStart->y || $hit->y > $this->point2DEnd->y) {
throw new GameException("Hit '{$hit}' out of plane boundary '{$this}'");
throw new GameException("Hit '{$hit}' ({$point}) out of plane boundary '{$this}'");
}

$margin = $this->wallBangEdgeMarginDistance;
Expand Down
2 changes: 1 addition & 1 deletion server/src/Core/Score.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public function getPlayerStat(int $playerId): PlayerStat
}

/**
* @return array<mixed>
* @return array<string,mixed>
*/
public function toArray(): array
{
Expand Down
38 changes: 18 additions & 20 deletions server/src/Core/World.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ final class World
private Bomb $bomb;
private int $lastBombActionTick = -1;
private int $lastBombPlayerId = -1;
private int $bombActionTickBuffer = 1;
private int $playerPotentialDistanceSquared;
private ?PathFinder $grenadeNavMesh = null;

Expand Down Expand Up @@ -314,20 +315,21 @@ public function playerUse(Player $player): void
&& $this->canBeSeen($player, $this->bomb->getPosition(), self::BOMB_RADIUS, self::BOMB_DEFUSE_MAX_DISTANCE)
) {
$bomb = $this->bomb;
if ($this->lastBombActionTick + Util::millisecondsToFrames(50) < $this->getTickId()) {
$bomb->reset();
$tickId = $this->getTickId();
$playerId = $player->getId();
if ($playerId !== $this->lastBombPlayerId || $this->lastBombActionTick + $this->bombActionTickBuffer < $tickId) {
$player->stop();
$bomb->startDefusing($tickId, $player->hasDefuseKit());
$soundEvent = new SoundEvent($player->getPositionClone()->addY(10), SoundType::BOMB_DEFUSING);
$this->makeSound($soundEvent->setPlayer($player)->setItem($bomb));
}
$this->lastBombActionTick = $this->getTickId();
$this->lastBombPlayerId = $player->getId();
$this->lastBombActionTick = $tickId;
$this->lastBombPlayerId = $playerId;

$defused = $this->bomb->defuse($player->hasDefuseKit());
if ($defused) {
$this->game->bombDefused($player);
if ($bomb->isDefused($tickId)) {
$this->lastBombActionTick = -1;
$this->lastBombPlayerId = -1;
$this->game->bombDefused($player);
}
return;
}
Expand Down Expand Up @@ -827,25 +829,24 @@ private function playerHit(Point $hitPoint, Player $playerHit, Player $playerCul
}
}

public function tryPlantBomb(Player $player): void
public function tryPlantBomb(Player $player, Bomb $bomb): void
{
if (!$this->canPlant($player)) {
return;
}

/** @var Bomb $bomb */
$bomb = $player->getEquippedItem();
if ($this->lastBombActionTick + Util::millisecondsToFrames(200) < $this->getTickId()) {
$bomb->reset();
$tickId = $this->getTickId();
$playerId = $player->getId();
if ($playerId !== $this->lastBombPlayerId || $this->lastBombActionTick + $this->bombActionTickBuffer < $tickId) {
$player->stop();
$bomb->startPlanting($tickId);
$soundEvent = new SoundEvent($player->getPositionClone()->addY(10), SoundType::BOMB_PLANTING);
$this->makeSound($soundEvent->setPlayer($player)->setItem($bomb));
}
$this->lastBombActionTick = $this->getTickId();
$this->lastBombPlayerId = $player->getId();
$this->lastBombPlayerId = $playerId;

$planted = $bomb->plant();
if ($planted) {
if ($bomb->isPlanted($tickId)) {
$player->equip($player->getInventory()->removeBomb());
$bomb->setPosition($player->getPositionClone());
$this->game->bombPlanted($player);
Expand All @@ -858,10 +859,7 @@ public function tryPlantBomb(Player $player): void

public function isPlantingOrDefusing(Player $player): bool
{
return (
$this->lastBombPlayerId === $player->getId() &&
($this->lastBombActionTick === $this->getTickId() || $this->lastBombActionTick + 1 === $this->getTickId())
);
return ($this->lastBombPlayerId === $player->getId() && $this->bombActionTickBuffer >= $this->getTickId() - $this->lastBombActionTick);
}

public function isWallOrFloorCollision(Point $start, Point $candidate, int $radius): bool
Expand Down Expand Up @@ -896,7 +894,7 @@ public function isCollisionWithOtherPlayers(int $playerIdSkip, Point $point, int
}

if ($collider->collide($point, $radius, $height)) {
return $this->game->getPlayer($collider->playerId);
return $collider->getPlayer();
}
}

Expand Down
12 changes: 12 additions & 0 deletions server/src/Enum/InventorySlot.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,16 @@ enum InventorySlot: int
case SLOT_KEVLAR = 10;
case SLOT_KIT = 11;

/** @return list<int> */
public static function getGrenadeSlotIds(): array
{
return [
self::SLOT_GRENADE_SMOKE->value,
self::SLOT_GRENADE_MOLOTOV->value,
self::SLOT_GRENADE_HE->value,
self::SLOT_GRENADE_FLASH->value,
self::SLOT_GRENADE_DECOY->value,
];
}

}
Loading
Loading