diff --git a/Makefile b/Makefile
index 62d5770..515f36e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt
+.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt cache-clear og-cache-clear
.DEFAULT_GOAL := help
@@ -12,6 +12,8 @@ help:
@echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)"
@echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)"
@echo " make ccp - Clear the production cache"
+ @echo " make cache-clear - Clear all caches (Vite, Symfony, node_modules)"
+ @echo " make og-cache-clear - Clear Open Graph cache only"
start:
docker compose up -d
@@ -55,3 +57,28 @@ db-reset:
ccp:
bin/console cache:clear --no-warmup --env=prod
+
+cache-clear:
+ @echo "Clearing all caches..."
+ @rm -rf node_modules/.vite
+ @rm -rf .vite
+ @rm -rf var/og-cache
+ @php bin/console cache:clear --no-warmup
+ @echo "✓ Vite cache cleared"
+ @echo "✓ OG cache cleared"
+ @echo "✓ Symfony cache cleared"
+ @echo ""
+ @echo "Rebuilding assets..."
+ @bun run build
+ @echo ""
+ @echo "✓ All caches cleared and assets rebuilt!"
+ @echo " Next step: Refresh browser with Ctrl+Shift+R"
+
+og-cache-clear:
+ @echo "Clearing Open Graph cache..."
+ @rm -rf var/og-cache
+ @echo "✓ OG cache cleared!"
+ @echo " Battle card images will be regenerated on next access"
+
+
+
diff --git a/assets/css/homepage/_profile.scss b/assets/css/homepage/_profile.scss
index e9d8537..2280962 100644
--- a/assets/css/homepage/_profile.scss
+++ b/assets/css/homepage/_profile.scss
@@ -859,6 +859,104 @@
flex-wrap: wrap;
}
+.bshare-bonus {
+ padding: 28px 28px 0;
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.bshare-bonus__title {
+ font: 700 13px 'Rajdhani', sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ color: #ffd700;
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ i { font-size: 14px; }
+}
+
+.bshare-bonus__grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+ margin-bottom: 28px;
+}
+
+.bshare-bonus__player {
+ padding: 16px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.02);
+
+ &--red {
+ border-color: rgba(246, 125, 82, 0.15);
+ background: rgba(246, 125, 82, 0.04);
+ }
+
+ &--blue {
+ border-color: rgba(149, 207, 245, 0.15);
+ background: rgba(149, 207, 245, 0.04);
+ }
+}
+
+.bshare-bonus__header {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.bshare-bonus__points {
+ font: 700 24px 'Rajdhani', sans-serif;
+ background: linear-gradient(135deg, #ffd700, #ffed4e);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.bshare-bonus__label {
+ font: 700 11px 'Rajdhani', sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: rgba(255, 215, 0, 0.7);
+}
+
+.bshare-bonus__stats {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.bshare-bonus__stat {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 12px;
+ gap: 8px;
+}
+
+.bshare-bonus__stat-label {
+ color: rgba(255, 255, 255, 0.6);
+ font: 500 11px 'Rajdhani', sans-serif;
+ text-transform: capitalize;
+}
+
+.bshare-bonus__stat-value {
+ font: 700 13px 'Rajdhani', sans-serif;
+ color: rgba(255, 215, 0, 0.9);
+ min-width: 24px;
+ text-align: right;
+}
+
+.bshare-bonus__stat--empty {
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 11px;
+}
+
.bshare-btn {
display: inline-flex;
align-items: center;
diff --git a/assets/js/components/BattleDialog.jsx b/assets/js/components/BattleDialog.jsx
index 775d115..0991af1 100644
--- a/assets/js/components/BattleDialog.jsx
+++ b/assets/js/components/BattleDialog.jsx
@@ -50,7 +50,7 @@ const RESULT_META = {
},
};
-function Avatar({ name, color, avatarUrl }) {
+function Avatar({ name, color, avatarUrl, bonusPoints = 0 }) {
const isRed = 'red' === color;
const initials = (name || '?').slice(0, 2).toUpperCase();
@@ -66,31 +66,53 @@ function Avatar({ name, color, avatarUrl }) {
const textColor = isRed ? '#f67d52' : '#95cff5';
return (
-
-
- {avatarUrl ? (
-

- ) : (
- initials
+
+
+
+ {avatarUrl ? (
+

+ ) : (
+ initials
+ )}
+
+ {0 < bonusPoints && (
+
+
+
)}
-
+
game.blueBonusPoints ? game.redBonusPoints : 0} />
{game.redPoints ?? '—'}
:
{game.bluePoints ?? '—'}
+
+
+ {(game.redBonusPoints ?? 0).toFixed(1)}
+
+ :
+
+ {(game.blueBonusPoints ?? 0).toFixed(1)}
+
+
VS
{meta.label}
-
+
game.redBonusPoints ? game.blueBonusPoints : 0} />
@@ -244,6 +275,62 @@ export default function BattleDialog({ games }) {
)}
+
+ {(0 < game.redBonusPoints
+ || 0 < game.blueBonusPoints
+ || game.redBonusStats?.blindHits
+ || game.blueBonusStats?.blindHits
+ ) && (
+
+
+ {/* Red Bonus */}
+
+
+ Red Bonus Statistics
+
+
+
+ {0 < game.redBonusStats?.blindHits && }
+ {0 < game.redBonusStats?.chainBest && }
+ {0 < game.redBonusStats?.edgeMines && }
+ {0 < game.redBonusStats?.lastMineHits && }
+ {0 < game.redBonusStats?.biggestReveal && }
+ {!game.redBonusStats?.blindHits && !game.redBonusStats?.chainBest && !game.redBonusStats?.edgeMines && !game.redBonusStats?.lastMineHits && !game.redBonusStats?.biggestReveal
+ && }
+
+
+
+ {/* Blue Bonus */}
+
+
+ Blue Bonus Statistics
+
+
+
+ {0 < game.blueBonusStats?.blindHits && }
+ {0 < game.blueBonusStats?.chainBest && }
+ {0 < game.blueBonusStats?.edgeMines && }
+ {0 < game.blueBonusStats?.lastMineHits && }
+ {0 < game.blueBonusStats?.biggestReveal && }
+ {!game.blueBonusStats?.blindHits && !game.blueBonusStats?.chainBest && !game.blueBonusStats?.edgeMines && !game.blueBonusStats?.lastMineHits && !game.blueBonusStats?.biggestReveal
+ && }
+
+
+
+
+ )}
diff --git a/docs/game-mechanics/BONUS_POINTS_SYSTEM.md b/docs/game-mechanics/BONUS_POINTS_SYSTEM.md
index bf43b93..edb4c7b 100644
--- a/docs/game-mechanics/BONUS_POINTS_SYSTEM.md
+++ b/docs/game-mechanics/BONUS_POINTS_SYSTEM.md
@@ -124,10 +124,45 @@ The Mine-Seeker game includes a bonus points system that rewards skilled play. B
---
+## Battle Report Display Components
+
+**IMPORTANT**: The Bonus Statistics display appears in **two places** that must be kept in sync:
+
+### 1. Public Battle Share Page
+**File**: `/templates/Game/battle_share.html.twig`
+- Displays via `bshare-bonus` CSS classes
+- Backend data passed from `ProfileController::battleShare()`
+- Shows bonus stats as HTML table format
+
+### 2. Profile Dialog (BattleDialog component)
+**File**: `/assets/js/components/BattleDialog.jsx`
+- React component using Material-UI Dialog
+- Displays inside the match details modal on profile page
+- Shows bonus stats using `StatRow` components in side-by-side boxes
+
+### Synchronization Requirements
+
+When making changes to the bonus statistics display, update **BOTH** files:
+
+1. **Update logic/data** → Edit both template and component
+2. **Change stat names** → Update both BONUS_LABELS and both display files
+3. **Modify formatting** → Keep visual consistency between both displays
+4. **Add new stats** → Add to both the `.twig` template AND the `.jsx` component
+
+**Checklist for changes**:
+- [ ] Update `/src/Util/TopicManager.php` if bonus calculation changes
+- [ ] Update `/templates/Game/battle_share.html.twig` for public display
+- [ ] Update `/assets/js/components/BattleDialog.jsx` for profile dialog
+- [ ] Update `/assets/js/mine-seeker/utils/constants.jsx` if adding new stats
+- [ ] Test both displays show identical data
+
+---
+
## Quick Checklist for Changes
- [ ] Code changes implemented
- [ ] This documentation updated
- [ ] `/docs/README.md` Quick Reference table updated
- [ ] Code comments added/updated
- [ ] Examples updated to match new behavior
+- [ ] Both battle report displays tested
diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php
index d823990..1fc24a0 100644
--- a/src/Controller/ProfileController.php
+++ b/src/Controller/ProfileController.php
@@ -163,6 +163,10 @@ class ProfileController extends AbstractController
'result' => $result,
'myPoints' => $myPts,
'oppPoints' => $oppPts,
+ 'redBonusPoints' => $game->getRedBonusPoints() ?? 0,
+ 'blueBonusPoints' => $game->getBlueBonusPoints() ?? 0,
+ 'redBonusStats' => $game->getRedBonusStats() ?? [],
+ 'blueBonusStats' => $game->getBlueBonusStats() ?? [],
];
}, $recent),
'chartData' => [
@@ -197,6 +201,10 @@ class ProfileController extends AbstractController
$resign = $game->getResign();
$redAvatar = $game->getRed()?->getAvatarPath();
$blueAvatar = $game->getBlue()?->getAvatarPath();
+ $redBonusPoints = $game->getRedBonusPoints() ?? 0;
+ $blueBonusPoints = $game->getBlueBonusPoints() ?? 0;
+ $redBonusStats = $game->getRedBonusStats() ?? [];
+ $blueBonusStats = $game->getBlueBonusStats() ?? [];
if ($resign === 'red') {
$summary = "$redName resigned — $blueName wins";
@@ -215,16 +223,20 @@ class ProfileController extends AbstractController
}
return $this->render('Game/battle_share.html.twig', [
- 'game' => $game,
- 'redName' => $redName,
- 'blueName' => $blueName,
- 'redPts' => $redPts,
- 'bluePts' => $bluePts,
- 'resign' => $resign,
- 'redAvatar' => $redAvatar,
- 'blueAvatar' => $blueAvatar,
- 'ogTitle' => "MineSeeker · $summary",
- 'ogDesc' => "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.",
+ 'game' => $game,
+ 'redName' => $redName,
+ 'blueName' => $blueName,
+ 'redPts' => $redPts,
+ 'bluePts' => $bluePts,
+ 'resign' => $resign,
+ 'redAvatar' => $redAvatar,
+ 'blueAvatar' => $blueAvatar,
+ 'redBonusPoints' => $redBonusPoints,
+ 'blueBonusPoints' => $blueBonusPoints,
+ 'redBonusStats' => $redBonusStats,
+ 'blueBonusStats' => $blueBonusStats,
+ 'ogTitle' => "MineSeeker · $summary",
+ 'ogDesc' => "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.",
]);
}
diff --git a/src/Service/BattleCardGenerator.php b/src/Service/BattleCardGenerator.php
index e3f8151..1d90c0d 100644
--- a/src/Service/BattleCardGenerator.php
+++ b/src/Service/BattleCardGenerator.php
@@ -56,8 +56,9 @@ class BattleCardGenerator
{
$path = $this->cachePath((int)$game->getId());
+ // Always regenerate to ensure bonus points are included
if (is_file($path)) {
- return $path;
+ unlink($path);
}
if (!is_dir($this->cacheDir)) {
@@ -154,6 +155,12 @@ class BattleCardGenerator
$scoreText = $redPts !== null && $bluePts !== null ? $redPts . ' : ' . $bluePts : 'VS';
$this->centeredText($im, $scoreText, 72, self::WIDTH / 2, 390, $white);
+ /** Bonus points below score*/
+ $redBonusPoints = $game->getRedBonusPoints() ?? 0;
+ $blueBonusPoints = $game->getBlueBonusPoints() ?? 0;
+ $bonusText = number_format((float)$redBonusPoints, 1, '.', '') . ' * : * ' . number_format((float)$blueBonusPoints, 1, '.', '');
+ $this->centeredText($im, $bonusText, 24, self::WIDTH / 2, 425, $gold);
+
if ($winner === 'red') {
$resultText = $redName . ' wins';
$resultColor = $gold;
@@ -169,11 +176,11 @@ class BattleCardGenerator
}
if ($resultText !== '') {
- $this->centeredText($im, $resultText, 30, self::WIDTH / 2, 460, $resultColor);
+ $this->centeredText($im, $resultText, 30, self::WIDTH / 2, 475, $resultColor);
}
if ($resign) {
- $this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::WIDTH / 2, 498, $muted);
+ $this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::WIDTH / 2, 508, $muted);
}
$this->centeredText($im, 'mineseeker.hu', 16, self::WIDTH / 2, self::HEIGHT - 20, $muted);
diff --git a/templates/Game/battle_share.html.twig b/templates/Game/battle_share.html.twig
index f9fed9d..878d137 100644
--- a/templates/Game/battle_share.html.twig
+++ b/templates/Game/battle_share.html.twig
@@ -31,7 +31,7 @@
-
+
{% if redAvatar %}
 }})
blueBonusPoints %}
+
+
+
+ {% endif %}
{{ redName }}
Red
@@ -53,6 +58,15 @@
{% else %}
— : —
{% endif %}
+
+
+ {{ (redBonusPoints ?? 0)|number_format(1, '.', '') }}
+
+ :
+
+ {{ (blueBonusPoints ?? 0)|number_format(1, '.', '') }}
+
+
VS
{% if resign == 'red' %}
@@ -79,7 +93,7 @@
{% endif %}
-
+
{% if blueAvatar %}
 }})
redBonusPoints %}
+
+
+
+ {% endif %}
{{ blueName }}
Blue
@@ -118,6 +137,108 @@
{% endif %}
+
+ {# Bonus Stats Section #}
+ {% set hasRedStats = redBonusStats is not empty and (redBonusStats.blindHits or redBonusStats.chainBest or redBonusStats.edgeMines or redBonusStats.lastMineHits or redBonusStats.biggestReveal) %}
+ {% set hasBlueStats = blueBonusStats is not empty and (blueBonusStats.blindHits or blueBonusStats.chainBest or blueBonusStats.edgeMines or blueBonusStats.lastMineHits or blueBonusStats.biggestReveal) %}
+ {% if redBonusPoints > 0 or blueBonusPoints > 0 or hasRedStats or hasBlueStats %}
+
+
+ Bonus Statistics
+
+
+ {# Red Bonus #}
+
+
+
+ {% if redBonusStats is not empty and redBonusStats.blindHits %}
+
+ Blind hits
+ {{ redBonusStats.blindHits }}
+
+ {% endif %}
+ {% if redBonusStats is not empty and redBonusStats.chainBest %}
+
+ Best chain
+ {{ redBonusStats.chainBest }}
+
+ {% endif %}
+ {% if redBonusStats is not empty and redBonusStats.edgeMines %}
+
+ Edge mines
+ {{ redBonusStats.edgeMines }}
+
+ {% endif %}
+ {% if redBonusStats is not empty and redBonusStats.lastMineHits %}
+
+ Endgame mines
+ {{ redBonusStats.lastMineHits }}
+
+ {% endif %}
+ {% if redBonusStats is not empty and redBonusStats.biggestReveal %}
+
+ Biggest reveal
+ {{ redBonusStats.biggestReveal }}
+
+ {% endif %}
+ {% if not hasRedStats %}
+
+ No bonuses earned
+
+ {% endif %}
+
+
+
+ {# Blue Bonus #}
+
+
+
+ {% if blueBonusStats is not empty and blueBonusStats.blindHits %}
+
+ Blind hits
+ {{ blueBonusStats.blindHits }}
+
+ {% endif %}
+ {% if blueBonusStats is not empty and blueBonusStats.chainBest %}
+
+ Best chain
+ {{ blueBonusStats.chainBest }}
+
+ {% endif %}
+ {% if blueBonusStats is not empty and blueBonusStats.edgeMines %}
+
+ Edge mines
+ {{ blueBonusStats.edgeMines }}
+
+ {% endif %}
+ {% if blueBonusStats is not empty and blueBonusStats.lastMineHits %}
+
+ Endgame mines
+ {{ blueBonusStats.lastMineHits }}
+
+ {% endif %}
+ {% if blueBonusStats is not empty and blueBonusStats.biggestReveal %}
+
+ Biggest reveal
+ {{ blueBonusStats.biggestReveal }}
+
+ {% endif %}
+ {% if not hasBlueStats %}
+
+ No bonuses earned
+
+ {% endif %}
+
+
+
+
+ {% endif %}