Private
Public Access
1
0

chg: usr: add extended data to battle reports and sharing image to make viewable bonus points #5
All checks were successful
Deploy to Production / deploy (push) Successful in 12s

This commit is contained in:
2026-04-18 13:44:15 +02:00
parent 25f2aaab8c
commit dc9c5f6545
7 changed files with 431 additions and 44 deletions

View File

@@ -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"

View File

@@ -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;

View File

@@ -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 (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
<div style={{
width: 72, height: 72, borderRadius: '50%',
background: avatarUrl ? 'transparent' : gradient,
border: `2px solid ${border}`,
boxShadow: glow,
display: 'flex', alignItems: 'center', justifyContent: 'center',
font: '800 24px \'Rajdhani\', sans-serif',
color: textColor,
letterSpacing: 2,
overflow: 'hidden',
}}
>
{avatarUrl ? (
<img
src={avatarUrl}
alt={name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
) : (
initials
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, position: 'relative' }}>
<div style={{ position: 'relative' }}>
<div style={{
width: 72, height: 72, borderRadius: '50%',
background: avatarUrl ? 'transparent' : gradient,
border: `2px solid ${border}`,
boxShadow: glow,
display: 'flex', alignItems: 'center', justifyContent: 'center',
font: '800 24px \'Rajdhani\', sans-serif',
color: textColor,
letterSpacing: 2,
overflow: 'hidden',
}}
>
{avatarUrl ? (
<img
src={avatarUrl}
alt={name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
) : (
initials
)}
</div>
{0 < bonusPoints && (
<div style={{
position: 'absolute',
bottom: -6,
right: -6,
background: '#ffd700',
borderRadius: '50%',
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1)',
border: '2px solid rgba(0,0,0,0.5)',
zIndex: 10,
}}
>
<i className="fa fa-star" style={{ color: '#000', fontSize: 14 }} />
</div>
)}
</div>
<span style={{
@@ -210,13 +232,22 @@ export default function BattleDialog({ games }) {
</div>
</div>
<div className="bd-vs-panel">
<Avatar name={game.redName} color="red" avatarUrl={game.redAvatar} />
<Avatar name={game.redName} color="red" avatarUrl={game.redAvatar} bonusPoints={game.redBonusPoints > game.blueBonusPoints ? game.redBonusPoints : 0} />
<div className="bd-vs-center">
<div className="bd-vs-score">
<span className="bd-vs-score__red">{game.redPoints ?? '—'}</span>
<span className="bd-vs-score__sep">:</span>
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
</div>
<div className="bd-vs-score" style={{ marginBottom: 8 }}>
<span style={{ font: '700 13px \'Rajdhani\', sans-serif', color: '#f67d52', display: 'flex', alignItems: 'center', gap: 4 }}>
<i className="fa fa-star" style={{ fontSize: 11 }} /> {(game.redBonusPoints ?? 0).toFixed(1)}
</span>
<span className="bd-vs-score__sep">:</span>
<span style={{ font: '700 13px \'Rajdhani\', sans-serif', color: '#95cff5', display: 'flex', alignItems: 'center', gap: 4 }}>
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" style={{ fontSize: 11 }} />
</span>
</div>
<div className="bd-vs-label">VS</div>
<div
className="bd-result-badge"
@@ -225,7 +256,7 @@ export default function BattleDialog({ games }) {
<i className={`fa ${meta.icon}`} /> {meta.label}
</div>
</div>
<Avatar name={game.blueName} color="blue" avatarUrl={game.blueAvatar} />
<Avatar name={game.blueName} color="blue" avatarUrl={game.blueAvatar} bonusPoints={game.blueBonusPoints > game.redBonusPoints ? game.blueBonusPoints : 0} />
</div>
<div className="bd-stats">
<StatRow icon="fa-calendar" label="Date" value={game.date ?? '—'} />
@@ -244,6 +275,62 @@ export default function BattleDialog({ games }) {
<StatRow icon="fa-clock-o" label="Started" value={game.created} />
)}
</div>
{(0 < game.redBonusPoints
|| 0 < game.blueBonusPoints
|| game.redBonusStats?.blindHits
|| game.blueBonusStats?.blindHits
) && (
<div style={{ padding: '16px 20px 0', borderTop: '1px solid rgba(255,255,255,0.08)', marginTop: 16, marginBottom: 16 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{/* Red Bonus */}
<div style={{
padding: 16,
border: '1px solid rgba(173,10,5,0.2)',
borderRadius: 6,
background: 'rgba(173,10,5,0.05)',
}}
>
<span style={{ font: '700 12px \'Rajdhani\', sans-serif', textTransform: 'uppercase', letterSpacing: 2, color: '#ffd700', display: 'block', marginBottom: 12 }}>
<i className="fa fa-star" style={{ marginRight: 8 }} /> Red Bonus Statistics
</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
{0 < game.redBonusStats?.blindHits && <StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />}
{0 < game.redBonusStats?.chainBest && <StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />}
{0 < game.redBonusStats?.edgeMines && <StatRow icon="fa-border" label="Edge mines" value={game.redBonusStats.edgeMines} />}
{0 < game.redBonusStats?.lastMineHits && <StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.redBonusStats.lastMineHits} />}
{0 < game.redBonusStats?.biggestReveal && <StatRow icon="fa-expand" label="Biggest reveal" value={game.redBonusStats.biggestReveal} />}
{!game.redBonusStats?.blindHits && !game.redBonusStats?.chainBest && !game.redBonusStats?.edgeMines && !game.redBonusStats?.lastMineHits && !game.redBonusStats?.biggestReveal
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
</div>
</div>
{/* Blue Bonus */}
<div style={{
padding: 16,
border: '1px solid rgba(149,207,245,0.2)',
borderRadius: 6,
background: 'rgba(149,207,245,0.05)',
}}
>
<span style={{ font: '700 12px \'Rajdhani\', sans-serif', textTransform: 'uppercase', letterSpacing: 2, color: '#ffd700', display: 'block', marginBottom: 12 }}>
<i className="fa fa-star" style={{ marginRight: 8 }} /> Blue Bonus Statistics
</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
{0 < game.blueBonusStats?.blindHits && <StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />}
{0 < game.blueBonusStats?.chainBest && <StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />}
{0 < game.blueBonusStats?.edgeMines && <StatRow icon="fa-border" label="Edge mines" value={game.blueBonusStats.edgeMines} />}
{0 < game.blueBonusStats?.lastMineHits && <StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.blueBonusStats.lastMineHits} />}
{0 < game.blueBonusStats?.biggestReveal && <StatRow icon="fa-expand" label="Biggest reveal" value={game.blueBonusStats.biggestReveal} />}
{!game.blueBonusStats?.blindHits && !game.blueBonusStats?.chainBest && !game.blueBonusStats?.edgeMines && !game.blueBonusStats?.lastMineHits && !game.blueBonusStats?.biggestReveal
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
</div>
</div>
</div>
</div>
)}
</div>
</Dialog>
</ThemeProvider>

View File

@@ -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

View File

@@ -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.",
]);
}

View File

@@ -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);

View File

@@ -31,7 +31,7 @@
</div>
<div class="bshare-vs">
<div class="bshare-player bshare-player--red">
<div class="bshare-avatar bshare-avatar--red">
<div class="bshare-avatar bshare-avatar--red" style="position: relative;">
{% if redAvatar %}
<img src="{{ redAvatar|imagine_filter('avatar_thumb') }}"
alt="{{ redName }}"
@@ -39,6 +39,11 @@
{% else %}
{{ redName|slice(0,2)|upper }}
{% endif %}
{% if redBonusPoints > blueBonusPoints %}
<div style="position: absolute; bottom: -6px; right: -6px; background: #ffd700; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1); border: 2px solid rgba(0,0,0,0.5); z-index: 10;">
<i class="fas fa-star" style="color: #000; font-size: 14px;"></i>
</div>
{% endif %}
</div>
<span class="bshare-player__name">{{ redName }}</span>
<span class="bshare-player__side">Red</span>
@@ -53,6 +58,15 @@
{% else %}
<div class="bshare-score bshare-score--na">— : —</div>
{% endif %}
<div style="display: flex; justify-content: center; gap: 0; align-items: center; margin-bottom: 8px;">
<span style="font: 700 13px 'Rajdhani', sans-serif; color: #f67d52; display: flex; align-items: center; gap: 4px;">
<i class="fas fa-star" style="font-size: 11px;"></i> {{ (redBonusPoints ?? 0)|number_format(1, '.', '') }}
</span>
<span style="font: 700 13px 'Rajdhani', sans-serif; color: rgba(255,255,255,0.3); margin: 0 8px;">:</span>
<span style="font: 700 13px 'Rajdhani', sans-serif; color: #95cff5; display: flex; align-items: center; gap: 4px;">
{{ (blueBonusPoints ?? 0)|number_format(1, '.', '') }} <i class="fas fa-star" style="font-size: 11px;"></i>
</span>
</div>
<div class="bshare-vs__label">VS</div>
{% if resign == 'red' %}
<div class="bshare-badge bshare-badge--blue">
@@ -79,7 +93,7 @@
{% endif %}
</div>
<div class="bshare-player bshare-player--blue">
<div class="bshare-avatar bshare-avatar--blue">
<div class="bshare-avatar bshare-avatar--blue" style="position: relative;">
{% if blueAvatar %}
<img src="{{ blueAvatar|imagine_filter('avatar_thumb') }}"
alt="{{ blueName }}"
@@ -87,6 +101,11 @@
{% else %}
{{ blueName|slice(0,2)|upper }}
{% endif %}
{% if blueBonusPoints > redBonusPoints %}
<div style="position: absolute; bottom: -6px; right: -6px; background: #ffd700; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1); border: 2px solid rgba(0,0,0,0.5); z-index: 10;">
<i class="fas fa-star" style="color: #000; font-size: 14px;"></i>
</div>
{% endif %}
</div>
<span class="bshare-player__name">{{ blueName }}</span>
<span class="bshare-player__side">Blue</span>
@@ -118,6 +137,108 @@
</div>
{% endif %}
</div>
{# 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 %}
<div class="bshare-bonus">
<div class="bshare-bonus__title">
<i class="fas fa-star"></i> Bonus Statistics
</div>
<div class="bshare-bonus__grid">
{# Red Bonus #}
<div class="bshare-bonus__player bshare-bonus__player--red">
<div class="bshare-bonus__header">
<span class="bshare-bonus__points">{{ redBonusPoints|number_format(1, '.', '') }}</span>
<span class="bshare-bonus__label">pts</span>
</div>
<div class="bshare-bonus__stats">
{% if redBonusStats is not empty and redBonusStats.blindHits %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Blind hits</span>
<span class="bshare-bonus__stat-value">{{ redBonusStats.blindHits }}</span>
</div>
{% endif %}
{% if redBonusStats is not empty and redBonusStats.chainBest %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Best chain</span>
<span class="bshare-bonus__stat-value">{{ redBonusStats.chainBest }}</span>
</div>
{% endif %}
{% if redBonusStats is not empty and redBonusStats.edgeMines %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Edge mines</span>
<span class="bshare-bonus__stat-value">{{ redBonusStats.edgeMines }}</span>
</div>
{% endif %}
{% if redBonusStats is not empty and redBonusStats.lastMineHits %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Endgame mines</span>
<span class="bshare-bonus__stat-value">{{ redBonusStats.lastMineHits }}</span>
</div>
{% endif %}
{% if redBonusStats is not empty and redBonusStats.biggestReveal %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Biggest reveal</span>
<span class="bshare-bonus__stat-value">{{ redBonusStats.biggestReveal }}</span>
</div>
{% endif %}
{% if not hasRedStats %}
<div class="bshare-bonus__stat bshare-bonus__stat--empty">
<span class="bshare-bonus__stat-label">No bonuses earned</span>
</div>
{% endif %}
</div>
</div>
{# Blue Bonus #}
<div class="bshare-bonus__player bshare-bonus__player--blue">
<div class="bshare-bonus__header">
<span class="bshare-bonus__points">{{ blueBonusPoints|number_format(1, '.', '') }}</span>
<span class="bshare-bonus__label">pts</span>
</div>
<div class="bshare-bonus__stats">
{% if blueBonusStats is not empty and blueBonusStats.blindHits %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Blind hits</span>
<span class="bshare-bonus__stat-value">{{ blueBonusStats.blindHits }}</span>
</div>
{% endif %}
{% if blueBonusStats is not empty and blueBonusStats.chainBest %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Best chain</span>
<span class="bshare-bonus__stat-value">{{ blueBonusStats.chainBest }}</span>
</div>
{% endif %}
{% if blueBonusStats is not empty and blueBonusStats.edgeMines %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Edge mines</span>
<span class="bshare-bonus__stat-value">{{ blueBonusStats.edgeMines }}</span>
</div>
{% endif %}
{% if blueBonusStats is not empty and blueBonusStats.lastMineHits %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Endgame mines</span>
<span class="bshare-bonus__stat-value">{{ blueBonusStats.lastMineHits }}</span>
</div>
{% endif %}
{% if blueBonusStats is not empty and blueBonusStats.biggestReveal %}
<div class="bshare-bonus__stat">
<span class="bshare-bonus__stat-label">Biggest reveal</span>
<span class="bshare-bonus__stat-value">{{ blueBonusStats.biggestReveal }}</span>
</div>
{% endif %}
{% if not hasBlueStats %}
<div class="bshare-bonus__stat bshare-bonus__stat--empty">
<span class="bshare-bonus__stat-label">No bonuses earned</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<div class="bshare-cta">
<a href="{{ path('MineSeekerBundle_gamePlay') }}" class="bshare-btn">
<i class="fas fa-play"></i> Play MineSeeker