fix(games): Tetris controls centered + Snake icon visibility + digital score-dashboard

User-Wünsche:
1. Tetris bedien-buttons mittig zum Spielfeld (war off-center)
2. Snake geklickte button-icons NICHT weiß (sonst light-theme unsichtbar)
3. Beide games: digital score-counter über playfield

Tetris:
- Controls in alignItems:'center'-wrapper mit width:boardWidth child +
  justifyContent:'space-between' → Move-Pad+Action-Pad bündig zum Feld
  unabhängig von screen-width
- Old Score/Level/Lines header entfernt → DigitalScore übernimmt

Snake:
- DPadBtn: ALWAYS color={tint} (#007aff iOS-blue) für Ionicons
- Active-state via borderColor + scale(1.04), NICHT mehr durch white-icon
- Semi-transparent blue bg (rgba) sichtbar in beiden themes
- Android-Branches + elevation entfernt (überall einheitlich)

DigitalScore (neu):
- 7-segment-feel via Courier New monospace + letterSpacing 2 + tabular-nums
- padStart(5,'0') Score+Best, padStart(2,'0') Level/Length
- Dunkles Panel (#0d1117) + border #1f2937, intentional contrast
- width:boardWidth, alignSelf:center
- Snake: SCORE+BEST | Tetris: SCORE+BEST+LVL

TS clean. Frontend-only, Metro reload reicht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-09 15:46:17 +02:00
parent 417191c90a
commit c9029b8fb5

View File

@ -9,6 +9,7 @@ import * as Haptics from 'expo-haptics';
import Slider from '@react-native-community/slider'; import Slider from '@react-native-community/slider';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { memorySvg, snakeSvg, tetrisSvg, tictactoeSvg } from './gameSvgs'; import { memorySvg, snakeSvg, tetrisSvg, tictactoeSvg } from './gameSvgs';
import { useColors } from '../../lib/theme';
// Haptic helper — fire-and-forget, swallow errors on platforms without taptic engine // Haptic helper — fire-and-forget, swallow errors on platforms without taptic engine
function tapHaptic() { function tapHaptic() {
@ -282,22 +283,15 @@ export function SnakeGame({
return ( return (
<View style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: Math.max(insets.bottom, 16) }}> <View style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: Math.max(insets.bottom, 16) }}>
{/* Header */} {/* Header */}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<Text style={{ fontSize: 11, color: '#6b7280', flex: 1, marginRight: 8 }} numberOfLines={2}>{lyraMessage}</Text> <Text style={{ fontSize: 11, color: '#6b7280', flex: 1, marginRight: 8 }} numberOfLines={2}>{lyraMessage}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 14 }}>
<View style={{ alignItems: 'center' }}>
<Text style={{ fontSize: 9, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: 0.5 }}>Score</Text>
<Text style={{ fontSize: 18, fontFamily: 'Nunito_800ExtraBold', color: '#16a34a' }}>{score}</Text>
</View>
<View style={{ alignItems: 'center' }}>
<Text style={{ fontSize: 9, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: 0.5 }}>Best</Text>
<Text style={{ fontSize: 18, fontFamily: 'Nunito_800ExtraBold', color: '#111827' }}>{highScore}</Text>
</View>
<Pressable onPress={onAbandon} hitSlop={10} style={{ width: 28, height: 28, alignItems: 'center', justifyContent: 'center' }}> <Pressable onPress={onAbandon} hitSlop={10} style={{ width: 28, height: 28, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ fontSize: 18, color: '#6b7280' }}></Text> <Text style={{ fontSize: 18, color: '#6b7280' }}></Text>
</Pressable> </Pressable>
</View> </View>
</View>
{/* Digital score dashboard */}
<DigitalScore score={score} best={highScore} boardWidth={boardW} />
{/* Board */} {/* Board */}
<View style={{ alignItems: 'center' }} {...panResponder.panHandlers}> <View style={{ alignItems: 'center' }} {...panResponder.panHandlers}>
@ -352,8 +346,6 @@ export function SnakeGame({
); );
} }
// Platform-native D-Pad button: iOS uses system-blue tinted circle (SF-symbol look),
// Android uses Material elevated surface with ripple.
function DPadBtn({ dir, active, onPress }: { dir: Dir; active: boolean; onPress: () => void }) { function DPadBtn({ dir, active, onPress }: { dir: Dir; active: boolean; onPress: () => void }) {
const icons: Record<Dir, 'chevron-up' | 'chevron-down' | 'chevron-back' | 'chevron-forward'> = { const icons: Record<Dir, 'chevron-up' | 'chevron-down' | 'chevron-back' | 'chevron-forward'> = {
up: 'chevron-up', down: 'chevron-down', left: 'chevron-back', right: 'chevron-forward', up: 'chevron-up', down: 'chevron-down', left: 'chevron-back', right: 'chevron-forward',
@ -366,29 +358,24 @@ function DPadBtn({ dir, active, onPress }: { dir: Dir; active: boolean; onPress:
hitSlop={12} hitSlop={12}
android_ripple={{ color: 'rgba(0,122,255,0.22)', borderless: true, radius: 32 }} android_ripple={{ color: 'rgba(0,122,255,0.22)', borderless: true, radius: 32 }}
style={({ pressed }) => { style={({ pressed }) => {
const bgIdle = isIOS ? 'rgba(0,122,255,0.10)' : '#ffffff'; const bgIdle = 'rgba(0,122,255,0.10)';
const bgPressed = isIOS ? 'rgba(0,122,255,0.22)' : '#f5f5f5'; const bgPressed = 'rgba(0,122,255,0.22)';
const bgActive = tint; const bgActive = 'rgba(0,122,255,0.22)';
const bg = active ? bgActive : (pressed && isIOS ? bgPressed : bgIdle); const bg = active ? bgActive : pressed ? bgPressed : bgIdle;
return { return {
width: 60, height: 60, borderRadius: 30, width: 60, height: 60, borderRadius: 30,
backgroundColor: bg, backgroundColor: bg,
borderWidth: 1.5,
borderColor: active ? tint : 'rgba(0,122,255,0.30)',
alignItems: 'center', justifyContent: 'center', alignItems: 'center', justifyContent: 'center',
...(isIOS ? {} : { transform: [{ scale: pressed ? 0.96 : active ? 1.04 : 1 }],
elevation: active ? 4 : 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.15,
shadowRadius: 2,
}),
transform: [{ scale: pressed && isIOS ? 0.96 : 1 }],
}; };
}} }}
> >
<Ionicons <Ionicons
name={icons[dir]} name={icons[dir]}
size={28} size={28}
color={active ? '#ffffff' : tint} color={tint}
/> />
</Pressable> </Pressable>
); );
@ -966,19 +953,18 @@ export function TetrisGame({
const speedColors = ['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444']; const speedColors = ['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444'];
const boardWidth = TETRIS_COLS * CELL;
return ( return (
<View style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: Math.max(insets.bottom, 16) }}> <View style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: Math.max(insets.bottom, 16) }}>
{/* Header */} {/* Header */}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<Text style={{ fontSize: 11, color: '#6b7280', flex: 1, marginRight: 8 }} numberOfLines={2}>{lyraMessage}</Text> <Text style={{ fontSize: 11, color: '#6b7280', flex: 1, marginRight: 8 }} numberOfLines={2}>{lyraMessage}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Stat label="Score" value={score} color="#111827" />
{highScore > 0 && <Stat label="Best" value={highScore} color="#f59e0b" />}
<Stat label="Lvl" value={level} color="#3b82f6" />
<Stat label="Lines" value={lines} color="#111827" />
<Pressable onPress={onAbandon} hitSlop={10}><Text style={{ fontSize: 18, color: '#6b7280' }}></Text></Pressable> <Pressable onPress={onAbandon} hitSlop={10}><Text style={{ fontSize: 18, color: '#6b7280' }}></Text></Pressable>
</View> </View>
</View>
{/* Digital score dashboard */}
<DigitalScore score={score} best={highScore} extra={level} extraLabel="LVL" boardWidth={boardWidth} />
{/* Board */} {/* Board */}
<View style={{ alignItems: 'center', marginVertical: 4 }}> <View style={{ alignItems: 'center', marginVertical: 4 }}>
@ -1037,8 +1023,14 @@ export function TetrisGame({
/> />
</View> </View>
{/* Controls — Move Pad (links) + Action Pad (rechts) */} {/* Controls — aligned to board width, centered on screen */}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 18 }}> <View style={{ alignItems: 'center', marginTop: 18 }}>
<View style={{
width: boardWidth,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}>
{/* Move Pad */} {/* Move Pad */}
<View style={{ flexDirection: 'row', gap: 14 }}> <View style={{ flexDirection: 'row', gap: 14 }}>
<DPadBtn dir="left" active={false} onPress={moveLeft} /> <DPadBtn dir="left" active={false} onPress={moveLeft} />
@ -1050,6 +1042,7 @@ export function TetrisGame({
<TetrisActionBtn icon="arrow-down" label="Drop" accent="#0ea5e9" onPress={softDrop} /> <TetrisActionBtn icon="arrow-down" label="Drop" accent="#0ea5e9" onPress={softDrop} />
</View> </View>
</View> </View>
</View>
{gameOver && ( {gameOver && (
<View style={{ marginTop: 14, alignItems: 'center', gap: 6 }}> <View style={{ marginTop: 14, alignItems: 'center', gap: 6 }}>
@ -1062,10 +1055,75 @@ export function TetrisGame({
} }
function Stat({ label, value, color }: { label: string; value: number; color: string }) { function Stat({ label, value, color }: { label: string; value: number; color: string }) {
const colors = useColors();
return ( return (
<View style={{ alignItems: 'center' }}> <View style={{ alignItems: 'center' }}>
<Text style={{ fontSize: 9, color: '#9ca3af', textTransform: 'uppercase', letterSpacing: 0.5 }}>{label}</Text> <Text style={{ fontSize: 9, color: colors.textMuted, textTransform: 'uppercase', letterSpacing: 0.5 }}>{label}</Text>
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color }}>{value}</Text> <Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color }}>{value}</Text>
</View> </View>
); );
} }
function DigitalScore({
score,
best,
extra,
extraLabel,
boardWidth,
}: {
score: number;
best: number;
extra?: number;
extraLabel?: string;
boardWidth: number;
}) {
const fmt = (n: number, digits = 5) => String(n).padStart(digits, '0');
return (
<View style={{
width: boardWidth,
alignSelf: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#0d1117',
borderRadius: 10,
borderWidth: 1,
borderColor: '#1f2937',
paddingHorizontal: 12,
paddingVertical: 7,
marginBottom: 6,
}}>
<ScoreCell label="SCORE" value={fmt(score)} bright />
<View style={{ width: 1, height: 28, backgroundColor: '#1f2937' }} />
<ScoreCell label="BEST" value={fmt(best)} />
{extra !== undefined && extraLabel !== undefined && (
<>
<View style={{ width: 1, height: 28, backgroundColor: '#1f2937' }} />
<ScoreCell label={extraLabel} value={fmt(extra, 2)} />
</>
)}
</View>
);
}
function ScoreCell({ label, value, bright }: { label: string; value: string; bright?: boolean }) {
return (
<View style={{ alignItems: 'center', flex: 1 }}>
<Text style={{
fontSize: 9,
color: '#4b5563',
letterSpacing: 1.5,
fontFamily: 'Nunito_600SemiBold',
textTransform: 'uppercase',
}}>{label}</Text>
<Text style={{
fontSize: 18,
fontFamily: 'Courier New' as any,
fontVariant: ['tabular-nums'],
color: bright ? '#00e680' : '#6b7280',
letterSpacing: 2,
lineHeight: 22,
}}>{value}</Text>
</View>
);
}