
2048 puzzle
Join tiles by swiping to create the 2048 tile and beyond - alone or with friends
Created with prompt
2048 game. Use this as reference import React from 'react'; import { View, StyleSheet, Dimensions } from 'react-native'; import { PanGestureHandler, State, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import { GameTile } from '$components'; import { GRID_SIZE } from '../constants'; const { width: screenWidth } = Dimensions.get('window'); const BOARD_PADDING = 20; const BOARD_SIZE = screenWidth - (BOARD_PADDING * 2); const TILE_SIZE = (BOARD_SIZE - (GRID_SIZE + 1) * 8) / GRID_SIZE; const TILE_GAP = 8; interface GameBoardProps { grid: (number | null)[][]; onMove: (direction: 'up' | 'down' | 'left' | 'right') => void; disabled?: boolean; } const GameBoard: React.FC<GameBoardProps> = ({ grid, onMove, disabled = false }) => { const handleGestureEvent = (event: PanGestureHandlerGestureEvent) => { if (disabled || event.nativeEvent.state !== State.END) { return; } const { translationX, translationY } = event.nativeEvent; const MIN_SWIPE_DISTANCE = 50; // Determine if swipe is primarily horizontal or vertical if (Math.abs(translationX) > Math.abs(translationY)) { // Horizontal swipe if (Math.abs(translationX) > MIN_SWIPE_DISTANCE) { if (translationX > 0) { onMove('right'); } else { onMove('left'); } } } else { // Vertical swipe if (Math.abs(translationY) > MIN_SWIPE_DISTANCE) { if (translationY > 0) { onMove('down'); } else { onMove('up'); } } } }; const renderGrid = () => { const rows = []; // Handle case where grid is undefined or invalid if (!grid || !Array.isArray(grid) || grid.length !== GRID_SIZE) { // Render empty grid for (let row = 0; row < GRID_SIZE; row++) { const columns = []; for (let col = 0; col < GRID_SIZE; col++) { columns.push( <View key={`${row}-${col}`} style={[ styles.tileContainer, { width: TILE_SIZE, height: TILE_SIZE, } ]} > <GameTile value={null} size={TILE_SIZE} /> </View> ); } rows.push( <View key={row} style={styles.row}> {columns} </View> ); } return rows; } for (let row = 0; row < GRID_SIZE; row++) { const columns = []; for (let col = 0; col < GRID_SIZE; col++) { const tileValue = grid?.[row]?.[col] ?? null; columns.push( <View key={`${row}-${col}`} style={[ styles.tileContainer, { width: TILE_SIZE, height: TILE_SIZE, } ]} > <GameTile value={tileValue} size={TILE_SIZE} /> </View> ); } rows.push( <View key={row} style={styles.row}> {columns} </View> ); } return rows; }; return ( <PanGestureHandler onHandlerStateChange={handleGestureEvent}> <View style={[ styles.board, { width: BOARD_SIZE, height: BOARD_SIZE } ]}> <View style={styles.gridContainer}> {renderGrid()} </View> </View> </PanGestureHandler> ); }; const styles = StyleSheet.create({ board: { backgroundColor: '#bbada0', borderRadius: 12, padding: TILE_GAP, alignSelf: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, gridContainer: { flex: 1, justifyContent: 'space-between', }, row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: TILE_GAP, }, tileContainer: { borderRadius: 6, backgroundColor: '#cdc1b4', justifyContent: 'center', alignItems: 'center', }, }); export default GameBoard; --- import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { TYPOGRAPHY } from '../styles/typography'; interface GameTileProps { value: number | null; position?: { row: number; col: number }; size?: number; } const GameTile: React.FC<GameTileProps> = ({ value, position, size }) => { const tileStyle = getTileStyle(value); const textStyle = getTextStyle(value); if (!value) { return ( <View style={[ styles.container, styles.empty, size && { width: size, height: size } ]} /> ); } return ( <View style={[ styles.container, tileStyle, size && { width: size, height: size } ]}> <Text style={[styles.text, textStyle]}> {value} </Text> </View> ); }; const getTileStyle = (value: number | null) => { if (!value) return styles.empty; switch (value) { case 2: return styles.tile2; case 4: return styles.tile4; case 8: return styles.tile8; case 16: return styles.tile16; case 32: return styles.tile32; case 64: return styles.tile64; case 128: return styles.tile128; case 256: return styles.tile256; case 512: return styles.tile512; case 1024: return styles.tile1024; case 2048: return styles.tile2048; default: return styles.tileSuper; } }; const getTextStyle = (value: number | null) => { if (!value) return null; if (value <= 4) return styles.textLight; if (value >= 128) return styles.textWhite; return styles.textDark; }; const styles = StyleSheet.create({ container: { width: 70, height: 70, borderRadius: 8, justifyContent: 'center', alignItems: 'center', margin: 2, }, text: { ...TYPOGRAPHY.h3, fontWeight: 'bold', textAlign: 'center', }, // Empty tile empty: { backgroundColor: '#CDC1B4', opacity: 0.4, }, // Tile colors based on value tile2: { backgroundColor: '#EEE4DA', }, tile4: { backgroundColor: '#EDE0C8', }, tile8: { backgroundColor: '#F2B179', }, tile16: { backgroundColor: '#F59563', }, tile32: { backgroundColor: '#F67C5F', }, tile64: { backgroundColor: '#F65E3B', }, tile128: { backgroundColor: '#EDCF72', }, tile256: { backgroundColor: '#EDCC61', }, tile512: { backgroundColor: '#EDC850', }, tile1024: { backgroundColor: '#EDC53F', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 4, }, tile2048: { backgroundColor: '#EDC22E', shadowColor: '#000', shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 6, }, tileSuper: { backgroundColor: '#3C3A32', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.4, shadowRadius: 8, elevation: 8, }, // Text colors textLight: { color: '#776E65', }, textDark: { color: '#F9F6F2', }, textWhite: { color: '#F9F6F2', }, }); export default GameTile; --- import React, { useEffect, useRef } from 'react'; import { View, Text, StyleSheet, Animated } from 'react-native'; import { TYPOGRAPHY } from '../styles/typography'; import { useWabiTheme } from '$wabi-components'; interface ScoreDisplayProps { currentScore: number; bestScore: number; isNewBest?: boolean; } const ScoreDisplay: React.FC<ScoreDisplayProps> = ({ currentScore, bestScore, isNewBest = false }) => { const { colors } = useWabiTheme(); const animationValue = useRef(new Animated.Value(1)).current; const bestScoreAnimation = useRef(new Animated.Value(1)).current; // Format numbers with commas for readability const formatScore = (score: number): string => { return score.toLocaleString(); }; // Animate score changes useEffect(() => { Animated.sequence([ Animated.timing(animationValue, { toValue: 1.1, duration: 150, useNativeDriver: true, }), Animated.timing(animationValue, { toValue: 1, duration: 150, useNativeDriver: true, }), ]).start(); // eslint-disable-next-line wabi-components/warn-useeffect-object-deps }, [currentScore, animationValue]); // Animate best score highlight useEffect(() => { if (isNewBest) { Animated.sequence([ Animated.timing(bestScoreAnimation, { toValue: 1.2, duration: 200, useNativeDriver: true, }), Animated.timing(bestScoreAnimation, { toValue: 1, duration: 300, useNativeDriver: true, }), ]).start(); } }, [isNewBest]); return ( <View style={styles.container}> <View style={styles.scoreContainer}> <Text style={[styles.label, { color: '#666' }]}>SCORE</Text> <Animated.View style={{ transform: [{ scale: animationValue }] }}> <Text style={[styles.scoreValue, { color: colors.primary }]}> {formatScore(currentScore)} </Text> </Animated.View> </View> <View style={styles.scoreContainer}> <Text style={[styles.label, { color: '#666' }]}>BEST</Text> <Animated.View style={{ transform: [{ scale: bestScoreAnimation }], backgroundColor: isNewBest ? colors.primary + '20' : 'transparent', borderRadius: 8, paddingHorizontal: isNewBest ? 8 : 0, paddingVertical: isNewBest ? 4 : 0, }}> <Text style={[ styles.scoreValue, { color: isNewBest ? colors.primary : '#333', fontWeight: isNewBest ? '600' : '500' } ]}> {formatScore(bestScore)} </Text> </Animated.View> </View> </View> ); }; const styles = StyleSheet.create({ container: { flexDirection: 'row', backgroundColor: '#f8f9fa', borderRadius: 12, padding: 20, marginBottom: 20, justifyContent: 'space-between', alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, scoreContainer: { alignItems: 'center', flex: 1, }, label: { ...TYPOGRAPHY.meta, marginBottom: 8, fontWeight: '600', letterSpacing: 1, }, scoreValue: { ...TYPOGRAPHY.numberLg, fontWeight: '500', textAlign: 'center', }, }); export default ScoreDisplay; --- import { createStore, configureStorage, userSessionInfo } from '$sdk'; import type { GameState, ScoreRecord } from '../types'; import { GameStateDef, ScoreRecordDef } from '../types'; import { GRID_SIZE, INITIAL_TILES, WIN_TILE, GAME_STATUS, FOUR_TILE_PROBABILITY } from '../constants'; configureStorage('2048-game', { partialize: (state: GameStore) => ({ currentGame: state.currentGame, scoreRecords: state.scoreRecords, gamesPlayed: state.gamesPlayed }) }); interface GameStore { // State currentGame: GameState | null; scoreRecords: ScoreRecord[]; isLoading: boolean; error: string | null; gamesPlayed: number; gameStartTime: number | null; newTilePositions: { row: number; col: number }[]; mergedTilePositions: { row: number; col: number }[]; // Getters getCurrentGame: () => GameState | null; getTopScores: (limit?: number) => ScoreRecord[]; getFormattedScores: () => ScoreRecord[]; canMakeMove: () => boolean; isGameWon: () => boolean; isGameOver: () => boolean; getBestScore: () => number; getCurrentScore: () => number; getNewTilePositions: () => { row: number; col: number }[]; getMergedTilePositions: () => { row: number; col: number }[]; // Actions initializeGame: () => void; startNewGame: () => void; makeMove: (direction: 'up' | 'down' | 'left' | 'right') => boolean; undoLastMove: () => void; resetGame: () => void; saveCurrentScore: () => void; } export const useAppStore = createStore<GameStore>((set, get) => ({ // State currentGame: null, scoreRecords: [], isLoading: false, error: null, gamesPlayed: 0, gameStartTime: null, newTilePositions: [], mergedTilePositions: [], // Getters /** * Gets the current active game state * @state_dependency: currentGame */ getCurrentGame: () => { const { currentGame } = get(); return currentGame; }, /** * Gets top score records sorted by score value * @state_dependency: scoreRecords */ getTopScores: (limit = 10) => { const { scoreRecords } = get(); return [...scoreRecords] .sort((a, b) => b.scoreValue - a.scoreValue) .slice(0, limit); }, /** * Gets formatted score records with human-readable dates * @state_dependency: scoreRecords */ getFormattedScores: () => { const { scoreRecords } = get(); return scoreRecords.map(record => ({ ...record, formattedDate: userSessionInfo.formatDate({ formatString: 'MMM dd, yyyy', timestampMs: record.dateAchieved }) })); }, /** * Checks if any moves are possible on the current grid * @state_dependency: currentGame */ canMakeMove: () => { const { currentGame } = get(); if (!currentGame || currentGame.gameStatus !== GAME_STATUS.PLAYING) { return false; } const grid = currentGame.grid; // Check for empty cells for (let row = 0; row < GRID_SIZE; row++) { for (let col = 0; col < GRID_SIZE; col++) { if (grid[row][col] === null) { return true; } } } // Check for possible merges (horizontal and vertical) for (let row = 0; row < GRID_SIZE; row++) { for (let col = 0; col < GRID_SIZE; col++) { const current = grid[row][col]; if (current !== null) { // Check right neighbor if (col < GRID_SIZE - 1 && grid[row][col + 1] === current) { return true; } // Check bottom neighbor if (row < GRID_SIZE - 1 && grid[row + 1][col] === current) { return true; } } } } return false; }, /** * Checks if the game has been won (2048 tile reached) * @state_dependency: currentGame */ isGameWon: () => { const { currentGame } = get(); return currentGame?.gameStatus === GAME_STATUS.WON; }, /** * Checks if the game is over (lost or won) * @state_dependency: currentGame */ isGameOver: () => { const { currentGame } = get(); return currentGame?.gameStatus === GAME_STATUS.LOST || currentGame?.gameStatus === GAME_STATUS.WON; }, /** * Gets the best score from all score records * @state_dependency: scoreRecords, currentGame */ getBestScore: () => { const { scoreRecords, currentGame } = get(); const recordBest = Math.max(0, ...scoreRecords.map(r => r.scoreValue)); const currentBest = currentGame?.bestScore || 0; return Math.max(recordBest, currentBest); }, /** * Gets the current game score * @state_dependency: currentGame */ getCurrentScore: () => { const { currentGame } = get(); return currentGame?.currentScore || 0; }, /** * Gets positions of newly added tiles for animations * @state_dependency: newTilePositions */ getNewTilePositions: () => { const { newTilePositions } = get(); return newTilePositions; }, /** * Gets positions of tiles that were merged for animations * @state_dependency: mergedTilePositions */ getMergedTilePositions: () => { const { mergedTilePositions } = get(); return mergedTilePositions; }, // Actions /** * Initializes the game with starting state */ initializeGame: () => { set({ isLoading: true, error: null }); // Create initial empty grid const emptyGrid: (number | null)[][] = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null) as (number | null)[] ); // Add two initial tiles const firstTile = addRandomTile(emptyGrid); const gridWithTiles = addRandomTile(firstTile.grid); const initialGame: GameState = { id: `game_${Date.now()}`, currentScore: 0, bestScore: get().getBestScore(), gameStatus: GAME_STATUS.PLAYING, moveCount: 0, grid: gridWithTiles.grid, timestamp: userSessionInfo.getTimestampInMs(), canUndo: false, previousGrid: undefined, previousScore: undefined }; set({ currentGame: initialGame, isLoading: false, gameStartTime: userSessionInfo.getTimestampInMs() }); }, /** * Starts a new game, saving current score if game was completed */ startNewGame: () => { const currentGame = get().currentGame; // Save score if game ended with a score > 0 if (currentGame && currentGame.currentScore > 0 && (currentGame.gameStatus === GAME_STATUS.LOST || currentGame.gameStatus === GAME_STATUS.WON)) { get().saveCurrentScore(); } get().initializeGame(); set(state => ({ gamesPlayed: state.gamesPlayed + 1 })); }, /** * Makes a move in the specified direction */ makeMove: (direction: 'up' | 'down' | 'left' | 'right') => { const currentGame = get().currentGame; if (!currentGame || currentGame.gameStatus !== GAME_STATUS.PLAYING) { return false; } const { grid: oldGrid, currentScore: oldScore } = currentGame; const { grid: newGrid, score: scoreGained, moved, mergedPositions } = moveGrid(oldGrid, direction); if (!moved) { return false; // No valid move } // Add random tile to the new grid const { grid: finalGrid, newTilePosition } = addRandomTile(newGrid); const newScore = oldScore + scoreGained; const bestScore = Math.max(get().getBestScore(), newScore); // Update animation state const newTilePositions = newTilePosition ? [newTilePosition] : []; // Check for win condition let newStatus: 'playing' | 'won' | 'lost' = currentGame.gameStatus; if (newStatus === GAME_STATUS.PLAYING && hasWinTile(finalGrid)) { newStatus = GAME_STATUS.WON; } // Check for game over const tempGame = { ...currentGame, grid: finalGrid, gameStatus: newStatus }; set({ currentGame: tempGame, mergedTilePositions: mergedPositions, newTilePositions: newTilePositions }); if (newStatus === GAME_STATUS.PLAYING && !get().canMakeMove()) { newStatus = GAME_STATUS.LOST; } const updatedGame: GameState = { ...currentGame, currentScore: newScore, bestScore: bestScore, gameStatus: newStatus, moveCount: currentGame.moveCount + 1, grid: finalGrid, timestamp: userSessionInfo.getTimestampInMs(), canUndo: true, previousGrid: oldGrid, previousScore: oldScore }; set({ currentGame: updatedGame }); // Clear animation states after a delay to reset for next move setTimeout(() => { set({ newTilePositions: [], mergedTilePositions: [] }); }, 300); return true; }, /** * Undos the last move if available */ undoLastMove: () => { const currentGame = get().currentGame; if (!currentGame || !currentGame.canUndo || !currentGame.previousGrid) { return; } const restoredGame: GameState = { ...currentGame, currentScore: currentGame.previousScore || 0, gameStatus: GAME_STATUS.PLAYING, moveCount: Math.max(0, currentGame.moveCount - 1), grid: currentGame.previousGrid, timestamp: userSessionInfo.getTimestampInMs(), canUndo: false, previousGrid: undefined, previousScore: undefined }; set({ currentGame: restoredGame }); }, /** * Resets the current game to initial state */ resetGame: () => { get().initializeGame(); }, /** * Saves the current game score to score records */ saveCurrentScore: () => { const { currentGame, gameStartTime } = get(); if (!currentGame || currentGame.currentScore === 0) { return; } const timePlayed = gameStartTime ? userSessionInfo.getTimestampInMs() - gameStartTime : 0; const scoreRecord: ScoreRecord = { id: `score_${userSessionInfo.getTimestampInMs()}`, scoreValue: currentGame.currentScore, dateAchieved: userSessionInfo.getTimestampInMs(), movesTaken: currentGame.moveCount, timePlayed: timePlayed, gridSnapshot: currentGame.grid.map(row => [...row]) }; set(state => ({ scoreRecords: [...state.scoreRecords, scoreRecord] })); } })); // Helper functions for game logic /** * Adds a random tile (2 or 4) to an empty cell in the grid */ function addRandomTile(grid: (number | null)[][]): { grid: (number | null)[][], newTilePosition: { row: number; col: number } | null } { if (!grid || !Array.isArray(grid)) { const emptyGrid: (number | null)[][] = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null) as (number | null)[] ); return { grid: emptyGrid, newTilePosition: null }; } const emptyCells: { row: number; col: number }[] = []; // Find empty cells for (let row = 0; row < GRID_SIZE; row++) { for (let col = 0; col < GRID_SIZE; col++) { if (grid[row][col] === null) { emptyCells.push({ row, col }); } } } if (emptyCells.length === 0) { return { grid, newTilePosition: null }; // No empty cells } // Choose random empty cell const randomIndex = Math.floor(Math.random() * emptyCells.length); const { row, col } = emptyCells[randomIndex]; // Choose tile value (90% chance for 2, 10% chance for 4) const tileValue = Math.random() < FOUR_TILE_PROBABILITY ? 4 : 2; // Create new grid with the tile const newGrid = grid.map((gridRow, r) => gridRow.map((cell, c) => (r === row && c === col) ? tileValue : cell) ); return { grid: newGrid, newTilePosition: { row, col } }; } /** * Moves the grid in the specified direction and returns new grid, score gained, and whether anything moved */ function moveGrid(grid: (number | null)[][], direction: string): { grid: (number | null)[][]; score: number; moved: boolean; mergedPositions: { row: number; col: number }[]; } { const rotatedGrid = rotateGrid(grid, direction); const { grid: movedGrid, score, moved, mergedPositions: rotatedMerged } = moveLeft(rotatedGrid); const finalGrid = rotateGridBack(movedGrid, direction); // Transform merged positions back to original orientation const mergedPositions = transformMergedPositions(rotatedMerged, direction); return { grid: finalGrid, score, moved, mergedPositions }; } /** * Transforms merged positions back to original grid orientation */ function transformMergedPositions(positions: { row: number; col: number }[], direction: string): { row: number; col: number }[] { return positions.map(pos => { switch (direction) { case 'up': return { row: pos.col, col: pos.row }; case 'down': return { row: GRID_SIZE - 1 - pos.col, col: pos.row }; case 'right': return { row: pos.row, col: GRID_SIZE - 1 - pos.col }; case 'left': default: return pos; } }); } /** * Rotates grid based on direction (to normalize all moves to "left") */ function rotateGrid(grid: (number | null)[][], direction: string): (number | null)[][] { switch (direction) { case 'up': return transposeGrid(grid); case 'down': return reverseGrid(transposeGrid(grid)); case 'right': return reverseGrid(grid); case 'left': default: return grid; } } /** * Rotates grid back to original orientation after move */ function rotateGridBack(grid: (number | null)[][], direction: string): (number | null)[][] { switch (direction) { case 'up': return transposeGrid(grid); case 'down': return transposeGrid(reverseGrid(grid)); case 'right': return reverseGrid(grid); case 'left': default: return grid; } } /** * Transposes a grid (swap rows and columns) */ function transposeGrid(grid: (number | null)[][]): (number | null)[][] { if (!grid || !grid[0]) { return Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null) as (number | null)[] ); } return grid[0].map((_, colIndex) => grid.map(row => row[colIndex])); } /** * Reverses each row of the grid */ function reverseGrid(grid: (number | null)[][]): (number | null)[][] { return grid.map(row => [...row].reverse()); } /** * Moves all tiles left, merging when possible */ function moveLeft(grid: (number | null)[][]): { grid: (number | null)[][]; score: number; moved: boolean; mergedPositions: { row: number; col: number }[]; } { let totalScore = 0; let moved = false; const mergedPositions: { row: number; col: number }[] = []; const newGrid = grid.map((row, rowIndex) => { const { row: newRow, score, rowMoved, mergedCols } = moveRowLeft([...row]); totalScore += score; if (rowMoved) moved = true; // Add merged positions for this row mergedCols.forEach(col => { mergedPositions.push({ row: rowIndex, col }); }); return newRow; }); return { grid: newGrid, score: totalScore, moved, mergedPositions }; } /** * Moves a single row left, merging tiles and calculating score */ function moveRowLeft(row: (number | null)[]): { row: (number | null)[]; score: number; rowMoved: boolean; mergedCols: number[]; } { const originalRow = [...row]; // Remove nulls and compact const compacted = row.filter(cell => cell !== null); let score = 0; const mergedCols: number[] = []; // Merge adjacent equal tiles for (let i = 0; i < compacted.length - 1; i++) { if (compacted[i] === compacted[i + 1]) { compacted[i] = (compacted[i] as number) * 2; score += compacted[i] as number; mergedCols.push(i); // Track which column had a merge compacted.splice(i + 1, 1); // Remove merged tile } } // Fill remaining with nulls while (compacted.length < GRID_SIZE) { compacted.push(null); } const rowMoved = !arraysEqual(originalRow, compacted); return { row: compacted, score, rowMoved, mergedCols }; } /** * Checks if the grid contains the winning tile */ function hasWinTile(grid: (number | null)[][]): boolean { for (let row = 0; row < GRID_SIZE; row++) { for (let col = 0; col < GRID_SIZE; col++) { if (grid[row][col] === WIN_TILE) { return true; } } } return false; } /** * Compares two arrays for equality */ function arraysEqual(a: (number | null)[], b: (number | null)[]): boolean { return a.length === b.length && a.every((val, index) => val === b[index]); } --- import { defineType, Generators } from '$sdk'; import { GAME_STATUS } from '../constants'; export interface GameState { id: string; streamingId?: string; currentScore: number; bestScore: number; gameStatus: 'playing' | 'won' | 'lost'; moveCount: number; grid: (number | null)[][]; timestamp: number; canUndo: boolean; previousGrid?: (number | null)[][]; previousScore?: number; } export interface ScoreRecord { id: string; streamingId?: string; scoreValue: number; dateAchieved: number; movesTaken: number; timePlayed: number; gridSnapshot: (number | null)[][]; } export const GameStateDef = defineType<GameState>({ typeName: "GameState", isComplexObject: true, fields: { id: { backendManaged: false, description: "Unique identifier for the game state", type: 'string', required: true, defaultValue: Generators.ids.random }, streamingId: { backendManaged: false, description: "Unique identifier for streaming updates", type: 'string', defaultValue: Generators.ids.streaming }, currentScore: { backendManaged: false, description: "Current score in the game", type: 'number', required: true, defaultValue: 0, previewContent: true }, bestScore: { backendManaged: false, description: "Highest score achieved", type: 'number', required: true, defaultValue: 0, previewContent: true }, gameStatus: { backendManaged: false, description: "Current status of the game", type: 'string', literalValues: ["playing", "won", "lost"] as const, defaultValue: "playing", previewContent: true }, moveCount: { backendManaged: false, description: "Number of moves made in current game", type: 'number', required: true, defaultValue: 0 }, grid: { backendManaged: false, description: "4x4 grid representing the game board state", type: 'array', required: true, defaultValue: [] }, timestamp: { backendManaged: false, description: "When the game state was last updated", type: 'number', required: true, defaultValue: Generators.timestamps.current }, canUndo: { backendManaged: false, description: "Whether undo is available for current state", type: 'boolean', defaultValue: false }, previousGrid: { backendManaged: false, description: "Previous grid state for undo functionality", type: 'array', defaultValue: [] }, previousScore: { backendManaged: false, description: "Previous score for undo functionality", type: 'number', defaultValue: 0 } } }); export const ScoreRecordDef = defineType<ScoreRecord>({ typeName: "ScoreRecord", isComplexObject: false, fields: { id: { backendManaged: false, description: "Unique identifier for the score record", type: 'string', required: true, defaultValue: Generators.ids.random }, streamingId: { backendManaged: false, description: "Unique identifier for streaming updates", type: 'string', defaultValue: Generators.ids.streaming }, scoreValue: { backendManaged: false, description: "The achieved score value", type: 'number', required: true, previewContent: true }, dateAchieved: { backendManaged: false, description: "Timestamp when the score was achieved", type: 'number', required: true, defaultValue: Generators.timestamps.current, previewContent: true }, movesTaken: { backendManaged: false, description: "Number of moves taken to achieve this score", type: 'number', required: true }, timePlayed: { backendManaged: false, description: "Time played in milliseconds to achieve this score", type: 'number', required: true }, gridSnapshot: { backendManaged: false, description: "Final grid state when score was achieved", type: 'array', required: true, defaultValue: [] } } });