1+ // Smooth 2048 with stable tile IDs (drop-in replacement)
12let board = [ ] ;
23let score = 0 ;
34let best = 0 ;
45const size = 4 ;
6+ let nextId = 1 ; // unique id generator
7+ const tilesMap = new Map ( ) ; // id -> DOM element
58
9+ // ---------- init / new game ----------
610function init ( ) {
7- board = Array ( size ) . fill ( ) . map ( ( ) => Array ( size ) . fill ( 0 ) ) ;
8- score = 0 ;
9- updateScore ( ) ;
10- addRandomTile ( ) ;
11- addRandomTile ( ) ;
12- render ( ) ;
11+ board = Array ( size ) . fill ( ) . map ( ( ) => Array ( size ) . fill ( null ) ) ;
12+ score = 0 ;
13+ nextId = 1 ;
14+ tilesMap . clear ( ) ;
15+ document . getElementById ( 'grid' ) . querySelectorAll ( '.tile' ) . forEach ( t => t . remove ( ) ) ;
16+ updateScore ( ) ;
17+ addRandomTile ( ) ;
18+ addRandomTile ( ) ;
19+ render ( true ) ;
1320}
1421
1522function newGame ( ) {
16- document . getElementById ( 'gameOver' ) . classList . remove ( 'show' ) ;
17- init ( ) ;
23+ document . getElementById ( 'gameOver' ) . classList . remove ( 'show' ) ;
24+ init ( ) ;
25+ }
26+
27+ // ---------- helpers ----------
28+ function makeCell ( value ) {
29+ return { v : value , id : nextId ++ } ;
1830}
1931
2032function addRandomTile ( ) {
21- let empty = [ ] ;
22- for ( let r = 0 ; r < size ; r ++ ) {
23- for ( let c = 0 ; c < size ; c ++ ) {
24- if ( board [ r ] [ c ] === 0 ) empty . push ( { r, c} ) ;
25- }
26- }
27- if ( empty . length > 0 ) {
28- let { r, c} = empty [ Math . floor ( Math . random ( ) * empty . length ) ] ;
29- board [ r ] [ c ] = Math . random ( ) < 0.9 ? 2 : 4 ;
33+ const empty = [ ] ;
34+ for ( let r = 0 ; r < size ; r ++ ) {
35+ for ( let c = 0 ; c < size ; c ++ ) {
36+ if ( board [ r ] [ c ] === null ) empty . push ( { r, c } ) ;
3037 }
38+ }
39+ if ( empty . length === 0 ) return ;
40+ const { r, c } = empty [ Math . floor ( Math . random ( ) * empty . length ) ] ;
41+ board [ r ] [ c ] = makeCell ( Math . random ( ) < 0.9 ? 2 : 4 ) ;
3142}
3243
33- function render ( ) {
44+ // ---------- rendering ----------
45+ function render ( animated = true ) {
3446 const grid = document . getElementById ( 'grid' ) ;
35- const tiles = grid . querySelectorAll ( '.tile' ) ;
36- tiles . forEach ( tile => tile . remove ( ) ) ;
37-
47+ const gridRect = grid . getBoundingClientRect ( ) ;
48+ const gap = 10 ; // same as CSS
49+ const cellSize = ( gridRect . width - gap * ( size - 1 ) - 20 ) / size ;
50+
51+ const existingTiles = { } ;
52+ grid . querySelectorAll ( '.tile' ) . forEach ( tile => {
53+ existingTiles [ String ( tile . dataset . id ) ] = tile ;
54+ } ) ;
55+ const newIds = new Set ( ) ;
56+
3857 for ( let r = 0 ; r < size ; r ++ ) {
39- for ( let c = 0 ; c < size ; c ++ ) {
40- if ( board [ r ] [ c ] !== 0 ) {
41- const tile = document . createElement ( 'div' ) ;
42- tile . className = `tile tile-${ board [ r ] [ c ] } ` ;
43- if ( board [ r ] [ c ] > 2048 ) tile . className = 'tile tile-super' ;
44- tile . textContent = board [ r ] [ c ] ;
45- tile . style . left = `${ c * 100 + 10 } px` ;
46- tile . style . top = `${ r * 100 + 10 } px` ;
47- grid . appendChild ( tile ) ;
48- }
58+ for ( let c = 0 ; c < size ; c ++ ) {
59+ const cell = board [ r ] [ c ] ;
60+ if ( ! cell ) continue ;
61+
62+ const value = cell . v ;
63+ const idStr = String ( cell . id ) ;
64+ newIds . add ( idStr ) ;
65+
66+ let tile = existingTiles [ idStr ] ;
67+ if ( ! tile ) {
68+ tile = document . createElement ( 'div' ) ;
69+ tile . className = `tile tile-${ value } ` ;
70+ tile . textContent = value ;
71+ tile . dataset . id = idStr ;
72+ grid . appendChild ( tile ) ;
73+
74+ tile . style . transform = "scale(0)" ;
75+ requestAnimationFrame ( ( ) => {
76+ tile . style . transform = "scale(1)" ;
77+ } ) ;
78+ } else if ( parseInt ( tile . textContent , 10 ) !== value ) {
79+ tile . textContent = value ;
80+ tile . className = `tile tile-${ value } merged` ;
81+ tile . addEventListener ( 'animationend' , ( ) => tile . classList . remove ( 'merged' ) , { once : true } ) ;
4982 }
83+
84+ // RESPONSIVE POSITIONING
85+ const left = c * ( cellSize + gap ) + 10 ; // +10 for grid padding
86+ const top = r * ( cellSize + gap ) + 10 ;
87+ tile . style . width = `${ cellSize } px` ;
88+ tile . style . height = `${ cellSize } px` ;
89+ tile . style . left = `${ left } px` ;
90+ tile . style . top = `${ top } px` ;
91+
92+ if ( animated ) {
93+ tile . style . transition = "top 0.15s ease, left 0.15s ease, transform 0.15s, opacity 0.15s" ;
94+ } else {
95+ tile . style . transition = "none" ;
96+ }
97+
98+ if ( value > 2048 ) tile . classList . add ( "tile-super" ) ;
99+ }
100+ }
101+
102+ for ( const idStr in existingTiles ) {
103+ if ( ! newIds . has ( idStr ) ) {
104+ const tile = existingTiles [ idStr ] ;
105+ tile . style . transform = "scale(0)" ;
106+ tile . style . opacity = "0" ;
107+ tile . addEventListener ( "transitionend" , ( ) => tile . remove ( ) , { once : true } ) ;
108+ setTimeout ( ( ) => { if ( tile . parentNode ) tile . remove ( ) ; } , 300 ) ;
109+ }
50110 }
111+ }
112+
113+
114+
115+
116+ function findCellById ( id ) {
117+ for ( let r = 0 ; r < size ; r ++ )
118+ for ( let c = 0 ; c < size ; c ++ )
119+ if ( board [ r ] [ c ] && board [ r ] [ c ] . id === id ) return { r, c } ;
120+ return null ;
51121}
52122
123+ // ---------- score ----------
53124function updateScore ( ) {
54- document . getElementById ( 'score' ) . textContent = score ;
55- if ( score > best ) {
56- best = score ;
57- document . getElementById ( 'best' ) . textContent = best ;
125+ document . getElementById ( 'score' ) . textContent = score ;
126+ if ( score > best ) {
127+ best = score ;
128+ document . getElementById ( 'best' ) . textContent = best ;
129+ }
130+ }
131+
132+ // ---------- movement utilities ----------
133+ // slide & merge a line of cell objects (keeps ids appropriately)
134+ function slideAndMergeLine ( line ) {
135+ // line: array of cell objects or null, length = size
136+ const comps = line . filter ( x => x !== null ) ; // compacted left
137+ const merged = [ ] ;
138+ for ( let i = 0 ; i < comps . length ; i ++ ) {
139+ if ( i + 1 < comps . length && comps [ i ] . v === comps [ i + 1 ] . v ) {
140+ // merge into comps[i]; keep comps[i].id (so DOM element of first tile remains)
141+ comps [ i ] . v *= 2 ;
142+ score += comps [ i ] . v ;
143+ // mark the second tile's id for removal by not carrying it forward
144+ // (we simply skip the next item)
145+ merged . push ( comps [ i ] . id ) ; // for optional UI if needed
146+ comps . splice ( i + 1 , 1 ) ;
58147 }
148+ }
149+ while ( comps . length < size ) comps . push ( null ) ;
150+ return comps ;
59151}
60152
153+ // ---------- moves ----------
61154function move ( direction ) {
62- let moved = false ;
63- let newBoard = board . map ( row => [ ...row ] ) ;
64-
65- if ( direction === 'left' || direction === 'right' ) {
66- for ( let r = 0 ; r < size ; r ++ ) {
67- let row = newBoard [ r ] . filter ( val => val !== 0 ) ;
68- if ( direction === 'right' ) row . reverse ( ) ;
69-
70- for ( let i = 0 ; i < row . length - 1 ; i ++ ) {
71- if ( row [ i ] === row [ i + 1 ] ) {
72- row [ i ] *= 2 ;
73- score += row [ i ] ;
74- row . splice ( i + 1 , 1 ) ;
75- }
76- }
77-
78- while ( row . length < size ) row . push ( 0 ) ;
79- if ( direction === 'right' ) row . reverse ( ) ;
80-
81- if ( JSON . stringify ( newBoard [ r ] ) !== JSON . stringify ( row ) ) moved = true ;
82- newBoard [ r ] = row ;
83- }
84- } else {
85- for ( let c = 0 ; c < size ; c ++ ) {
86- let col = [ ] ;
87- for ( let r = 0 ; r < size ; r ++ ) {
88- if ( newBoard [ r ] [ c ] !== 0 ) col . push ( newBoard [ r ] [ c ] ) ;
89- }
90- if ( direction === 'down' ) col . reverse ( ) ;
91-
92- for ( let i = 0 ; i < col . length - 1 ; i ++ ) {
93- if ( col [ i ] === col [ i + 1 ] ) {
94- col [ i ] *= 2 ;
95- score += col [ i ] ;
96- col . splice ( i + 1 , 1 ) ;
97- }
98- }
99-
100- while ( col . length < size ) col . push ( 0 ) ;
101- if ( direction === 'down' ) col . reverse ( ) ;
102-
103- for ( let r = 0 ; r < size ; r ++ ) {
104- if ( newBoard [ r ] [ c ] !== col [ r ] ) moved = true ;
105- newBoard [ r ] [ c ] = col [ r ] ;
106- }
155+ let moved = false ;
156+ let newBoard = Array ( size ) . fill ( ) . map ( ( ) => Array ( size ) . fill ( null ) ) ;
157+
158+ if ( direction === 'left' || direction === 'right' ) {
159+ for ( let r = 0 ; r < size ; r ++ ) {
160+ const line = board [ r ] . slice ( ) ; // row
161+ // map to objects or null already
162+ let working = line . slice ( ) ;
163+ if ( direction === 'right' ) working = working . reverse ( ) ;
164+ const compact = working . filter ( x => x !== null ) ;
165+ // create shallow copies to avoid mutating original objects except value changes
166+ const mergedLine = slideAndMergeLine ( compact . map ( x => x ? { v : x . v , id : x . id } : null ) ) ;
167+ // place back
168+ let final = mergedLine ;
169+ if ( direction === 'right' ) final = final . reverse ( ) ;
170+ for ( let c = 0 ; c < size ; c ++ ) {
171+ // if final[c] is an object keep its id and updated value; else null
172+ newBoard [ r ] [ c ] = final [ c ] ? { v : final [ c ] . v , id : final [ c ] . id } : null ;
173+ }
174+ if ( ! arraysRowEqual ( board [ r ] , newBoard [ r ] ) ) moved = true ;
175+ }
176+ } else { // up / down
177+ for ( let c = 0 ; c < size ; c ++ ) {
178+ const col = [ ] ;
179+ for ( let r = 0 ; r < size ; r ++ ) col . push ( board [ r ] [ c ] ) ;
180+ let working = col . slice ( ) ;
181+ if ( direction === 'down' ) working = working . reverse ( ) ;
182+ const compact = working . filter ( x => x !== null ) ;
183+ const mergedCol = slideAndMergeLine ( compact . map ( x => x ? { v : x . v , id : x . id } : null ) ) ;
184+ let final = mergedCol ;
185+ if ( direction === 'down' ) final = final . reverse ( ) ;
186+ for ( let r = 0 ; r < size ; r ++ ) {
187+ newBoard [ r ] [ c ] = final [ r ] ? { v : final [ r ] . v , id : final [ r ] . id } : null ;
188+ }
189+ // compare original column with new column
190+ for ( let r = 0 ; r < size ; r ++ ) {
191+ const a = board [ r ] [ c ] ;
192+ const b = newBoard [ r ] [ c ] ;
193+ if ( ( a === null && b !== null ) || ( a !== null && b === null ) || ( a && b && ( a . v !== b . v || a . id !== b . id ) ) ) {
194+ moved = true ;
195+ break ;
107196 }
197+ }
108198 }
199+ }
109200
110- if ( moved ) {
111- board = newBoard ;
112- addRandomTile ( ) ;
113- updateScore ( ) ;
114- render ( ) ;
115-
116- if ( checkGameOver ( ) ) {
117- setTimeout ( ( ) => {
118- document . getElementById ( 'gameOver' ) . classList . add ( 'show' ) ;
119- } , 300 ) ;
120- }
201+ if ( moved ) {
202+ board = newBoard ;
203+ addRandomTile ( ) ;
204+ updateScore ( ) ;
205+ render ( ) ;
206+ if ( checkGameOver ( ) ) {
207+ setTimeout ( ( ) => {
208+ document . getElementById ( 'gameOver' ) . classList . add ( 'show' ) ;
209+ } , 300 ) ;
121210 }
211+ }
122212}
123213
214+ // helper to compare row arrays of cell objects/null by value+id
215+ function arraysRowEqual ( oldRow , newRow ) {
216+ for ( let i = 0 ; i < size ; i ++ ) {
217+ const a = oldRow [ i ] , b = newRow [ i ] ;
218+ if ( a === null && b === null ) continue ;
219+ if ( ( a === null ) !== ( b === null ) ) return false ;
220+ if ( a . v !== b . v || a . id !== b . id ) return false ;
221+ }
222+ return true ;
223+ }
224+
225+ // ---------- game over ----------
124226function checkGameOver ( ) {
125- for ( let r = 0 ; r < size ; r ++ ) {
126- for ( let c = 0 ; c < size ; c ++ ) {
127- if ( board [ r ] [ c ] === 0 ) return false ;
128- if ( c < size - 1 && board [ r ] [ c ] === board [ r ] [ c + 1 ] ) return false ;
129- if ( r < size - 1 && board [ r ] [ c ] === board [ r + 1 ] [ c ] ) return false ;
130- }
227+ for ( let r = 0 ; r < size ; r ++ ) {
228+ for ( let c = 0 ; c < size ; c ++ ) {
229+ if ( board [ r ] [ c ] === null ) return false ;
230+ if ( c < size - 1 && board [ r ] [ c ] . v === board [ r ] [ c + 1 ] . v ) return false ;
231+ if ( r < size - 1 && board [ r ] [ c ] . v === board [ r + 1 ] [ c ] . v ) return false ;
131232 }
132- return true ;
233+ }
234+ return true ;
133235}
134236
237+ // ---------- keyboard ----------
135238document . addEventListener ( 'keydown' , ( e ) => {
136- if ( e . key === 'ArrowLeft' ) move ( 'left' ) ;
137- else if ( e . key === 'ArrowRight' ) move ( 'right' ) ;
138- else if ( e . key === 'ArrowUp' ) move ( 'up' ) ;
139- else if ( e . key === 'ArrowDown' ) move ( 'down' ) ;
239+ if ( e . key === 'ArrowLeft' ) move ( 'left' ) ;
240+ else if ( e . key === 'ArrowRight' ) move ( 'right' ) ;
241+ else if ( e . key === 'ArrowUp' ) move ( 'up' ) ;
242+ else if ( e . key === 'ArrowDown' ) move ( 'down' ) ;
140243} ) ;
141244
142- init ( ) ;
245+ // ---------- start ----------
246+ init ( ) ;
0 commit comments