Skip to content

Commit 7eec8b1

Browse files
authored
math,examples: make 2048 use sliding animation for the tile movement (#23268)
1 parent 66caf94 commit 7eec8b1

File tree

5 files changed

+325
-83
lines changed

5 files changed

+325
-83
lines changed

examples/2048/2048.v

+150-66
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import gg
22
import gx
33
import math
4+
import math.easing
45
import os.asset
56
import rand
67
import time
78

9+
const zooming_percent_per_frame = 5
10+
const movement_percent_per_frame = 10
11+
12+
const window_title = 'V 2048'
13+
const default_window_width = 544
14+
const default_window_height = 560
15+
16+
const possible_moves = [Direction.up, .right, .down, .left]
17+
const predictions_per_move = 300
18+
const prediction_depth = 8
19+
820
struct App {
921
mut:
1022
gg &gg.Context = unsafe { nil }
@@ -14,7 +26,8 @@ mut:
1426
theme_idx int
1527
board Board
1628
undo []Undo
17-
atickers [4][4]int
29+
atickers [4][4]f64
30+
mtickers [4][4]f64
1831
state GameState = .play
1932
tile_format TileFormat = .normal
2033
moves int
@@ -111,14 +124,6 @@ const themes = [
111124
]
112125
},
113126
]
114-
const window_title = 'V 2048'
115-
const default_window_width = 544
116-
const default_window_height = 560
117-
const animation_length = 10 // frames
118-
119-
const possible_moves = [Direction.up, .right, .down, .left]
120-
const predictions_per_move = 300
121-
const prediction_depth = 8
122127

123128
struct Pos {
124129
x int = -1
@@ -128,6 +133,7 @@ struct Pos {
128133
struct Board {
129134
mut:
130135
field [4][4]int
136+
oidxs [4][4]u32 // old indexes of the fields, when != 0; each index is an encoding of its y,x coordinates = y << 16 | x
131137
points int
132138
shifts int
133139
}
@@ -141,6 +147,7 @@ struct TileLine {
141147
ypos int
142148
mut:
143149
field [5]int
150+
oidxs [5]u32
144151
points int
145152
shifts int
146153
}
@@ -201,6 +208,7 @@ fn (b Board) transpose() Board {
201208
for y in 0 .. 4 {
202209
for x in 0 .. 4 {
203210
res.field[y][x] = b.field[x][y]
211+
res.oidxs[y][x] = b.oidxs[x][y]
204212
}
205213
}
206214
return res
@@ -211,6 +219,7 @@ fn (b Board) hmirror() Board {
211219
for y in 0 .. 4 {
212220
for x in 0 .. 4 {
213221
res.field[y][x] = b.field[y][3 - x]
222+
res.oidxs[y][x] = b.oidxs[y][3 - x]
214223
}
215224
}
216225
return res
@@ -241,6 +250,7 @@ fn (t TileLine) to_left() TileLine {
241250
res.shifts++
242251
for k := x; k < right_border_idx; k++ {
243252
res.field[k] = res.field[k + 1]
253+
res.oidxs[k] = res.oidxs[k + 1]
244254
}
245255
remaining_zeros--
246256
}
@@ -255,6 +265,7 @@ fn (t TileLine) to_left() TileLine {
255265
if res.field[x] == res.field[x + 1] {
256266
for k := x; k < right_border_idx; k++ {
257267
res.field[k] = res.field[k + 1]
268+
res.oidxs[k] = res.oidxs[k + 1]
258269
}
259270
res.shifts++
260271
res.field[x]++
@@ -272,28 +283,39 @@ fn (b Board) to_left() Board {
272283
}
273284
for x in 0 .. 4 {
274285
hline.field[x] = b.field[y][x]
286+
hline.oidxs[x] = b.oidxs[y][x]
275287
}
276288
reshline := hline.to_left()
277289
res.shifts += reshline.shifts
278290
res.points += reshline.points
279291
for x in 0 .. 4 {
280292
res.field[y][x] = reshline.field[x]
293+
res.oidxs[y][x] = reshline.oidxs[x]
281294
}
282295
}
283296
return res
284297
}
285298

286-
fn (b Board) move(d Direction) (Board, bool) {
299+
fn yx2i(y int, x int) u32 {
300+
return u32(y) << 16 | u32(x)
301+
}
302+
303+
fn (mut b Board) move(d Direction) (Board, bool) {
304+
for y in 0 .. 4 {
305+
for x in 0 .. 4 {
306+
b.oidxs[y][x] = yx2i(y, x)
307+
}
308+
}
287309
new := match d {
288310
.left { b.to_left() }
289311
.right { b.hmirror().to_left().hmirror() }
290312
.up { b.transpose().to_left().transpose() }
291313
.down { b.transpose().hmirror().to_left().hmirror().transpose() }
292314
}
293315
// If the board hasn't changed, it's an illegal move, don't allow it.
294-
for x in 0 .. 4 {
295-
for y in 0 .. 4 {
296-
if b.field[x][y] != new.field[x][y] {
316+
for y in 0 .. 4 {
317+
for x in 0 .. 4 {
318+
if b.field[y][x] != new.field[y][x] {
297319
return new, true
298320
}
299321
}
@@ -324,11 +346,10 @@ fn (mut b Board) is_game_over() bool {
324346
fn (mut app App) update_tickers() {
325347
for y in 0 .. 4 {
326348
for x in 0 .. 4 {
327-
mut old := app.atickers[y][x]
328-
if old > 0 {
329-
old--
330-
app.atickers[y][x] = old
331-
}
349+
app.atickers[y][x] = math.clip(app.atickers[y][x] - f64(zooming_percent_per_frame) / 100.0,
350+
0.0, 1.0)
351+
app.mtickers[y][x] = math.clip(app.mtickers[y][x] - f64(movement_percent_per_frame) / 100.0,
352+
0.0, 1.0)
332353
}
333354
}
334355
}
@@ -339,6 +360,7 @@ fn (mut app App) new_game() {
339360
for x in 0 .. 4 {
340361
app.board.field[y][x] = 0
341362
app.atickers[y][x] = 0
363+
app.mtickers[y][x] = 0
342364
}
343365
}
344366
app.state = .play
@@ -387,23 +409,26 @@ fn (mut b Board) place_random_tile() (Pos, int) {
387409
value := rand.f64n(1.0) or { 0.0 }
388410
random_value := if value < 0.9 { 1 } else { 2 }
389411
b.field[empty_pos.y][empty_pos.x] = random_value
412+
b.oidxs[empty_pos.y][empty_pos.x] = yx2i(empty_pos.y, empty_pos.x)
390413
return empty_pos, random_value
391414
}
392415
return Pos{}, 0
393416
}
394417

395418
fn (mut app App) new_random_tile() {
419+
// do not animate empty fields:
396420
for y in 0 .. 4 {
397421
for x in 0 .. 4 {
398422
fidx := app.board.field[y][x]
399423
if fidx == 0 {
400424
app.atickers[y][x] = 0
425+
app.board.oidxs[y][x] = 0xFFFF_FFFF
401426
}
402427
}
403428
}
404429
empty_pos, random_value := app.board.place_random_tile()
405430
if random_value > 0 {
406-
app.atickers[empty_pos.y][empty_pos.x] = animation_length
431+
app.atickers[empty_pos.y][empty_pos.x] = 1.0
407432
}
408433
if app.state != .freeplay {
409434
app.check_for_victory()
@@ -414,6 +439,13 @@ fn (mut app App) new_random_tile() {
414439
fn (mut app App) apply_new_board(new Board) {
415440
old := app.board
416441
app.moves++
442+
for y in 0 .. 4 {
443+
for x in 0 .. 4 {
444+
if old.oidxs[y][x] != new.oidxs[y][x] {
445+
app.mtickers[y][x] = 1.0
446+
}
447+
}
448+
}
417449
app.board = new
418450
app.undo << Undo{old, app.state}
419451
app.new_random_tile()
@@ -623,59 +655,111 @@ fn (app &App) draw_tiles() {
623655
// Draw the padding around the tiles
624656
app.gg.draw_rounded_rect_filled(xstart, ystart, tiles_size, tiles_size, tiles_size / 24,
625657
app.theme.padding_color)
626-
// Draw the actual tiles
658+
659+
// Draw empty tiles:
660+
for y in 0 .. 4 {
661+
for x in 0 .. 4 {
662+
tw := app.ui.tile_size
663+
th := tw // square tiles, w == h
664+
xoffset := xstart + app.ui.padding_size + x * toffset
665+
yoffset := ystart + app.ui.padding_size + y * toffset
666+
app.gg.draw_rounded_rect_filled(xoffset, yoffset, tw, th, tw / 8, app.theme.tile_colors[0])
667+
}
668+
}
669+
670+
// Draw the already placed and potentially moving tiles:
627671
for y in 0 .. 4 {
628672
for x in 0 .. 4 {
629673
tidx := app.board.field[y][x]
630-
tile_color := if tidx < app.theme.tile_colors.len {
631-
app.theme.tile_colors[tidx]
632-
} else {
633-
// If there isn't a specific color for this tile, reuse the last color available
634-
app.theme.tile_colors.last()
674+
oidx := app.board.oidxs[y][x]
675+
if tidx == 0 || oidx == 0xFFFF_FFFF {
676+
continue
635677
}
636-
anim_size := animation_length - app.atickers[y][x]
637-
tw := int(f64(app.ui.tile_size) / animation_length * anim_size)
638-
th := tw // square tiles, w == h
639-
xoffset := xstart + app.ui.padding_size + x * toffset + (app.ui.tile_size - tw) / 2
640-
yoffset := ystart + app.ui.padding_size + y * toffset + (app.ui.tile_size - th) / 2
641-
app.gg.draw_rounded_rect_filled(xoffset, yoffset, tw, th, tw / 8, tile_color)
642-
if tidx != 0 { // 0 == blank spot
643-
xpos := xoffset + tw / 2
644-
ypos := yoffset + th / 2
645-
mut fmt := app.label_format(.tile)
646-
fmt = gx.TextCfg{
678+
app.draw_one_tile(x, y, tidx)
679+
}
680+
}
681+
682+
// Draw the newly placed random tiles on top of everything else:
683+
for y in 0 .. 4 {
684+
for x in 0 .. 4 {
685+
tidx := app.board.field[y][x]
686+
oidx := app.board.oidxs[y][x]
687+
if oidx == 0xFFFF_FFFF && tidx != 0 {
688+
app.draw_one_tile(x, y, tidx)
689+
}
690+
}
691+
}
692+
}
693+
694+
fn (app &App) draw_one_tile(x int, y int, tidx int) {
695+
xstart := app.ui.x_padding + app.ui.border_size
696+
ystart := app.ui.y_padding + app.ui.border_size + app.ui.header_size
697+
toffset := app.ui.tile_size + app.ui.padding_size
698+
oidx := app.board.oidxs[y][x]
699+
oy := oidx >> 16
700+
ox := oidx & 0xFFFF
701+
mut dx := 0
702+
mut dy := 0
703+
if oidx != 0xFFFF_FFFF {
704+
scaling := app.ui.tile_size * easing.in_out_quint(app.mtickers[y][x])
705+
if ox != x {
706+
dx = math.clip(int(scaling * (f64(ox) - f64(x))), -4 * app.ui.tile_size, 4 * app.ui.tile_size)
707+
}
708+
if oy != y {
709+
dy = math.clip(int(scaling * (f64(oy) - f64(y))), -4 * app.ui.tile_size, 4 * app.ui.tile_size)
710+
}
711+
}
712+
tile_color := if tidx < app.theme.tile_colors.len {
713+
app.theme.tile_colors[tidx]
714+
} else {
715+
// If there isn't a specific color for this tile, reuse the last color available
716+
app.theme.tile_colors.last()
717+
}
718+
anim_size := 1.0 - app.atickers[y][x]
719+
tw := int(f64(anim_size * app.ui.tile_size))
720+
th := tw // square tiles, w == h
721+
xoffset := dx + xstart + app.ui.padding_size + x * toffset + (app.ui.tile_size - tw) / 2
722+
yoffset := dy + ystart + app.ui.padding_size + y * toffset + (app.ui.tile_size - th) / 2
723+
app.gg.draw_rounded_rect_filled(xoffset, yoffset, tw, th, tw / 8, tile_color)
724+
if tidx != 0 { // 0 == blank spot
725+
xpos := xoffset + tw / 2
726+
ypos := yoffset + th / 2
727+
mut fmt := app.label_format(.tile)
728+
fmt = gx.TextCfg{
729+
...fmt
730+
size: int(anim_size * (fmt.size - 1))
731+
}
732+
match app.tile_format {
733+
.normal {
734+
app.gg.draw_text(xpos, ypos, '${1 << tidx}', fmt)
735+
}
736+
.log {
737+
app.gg.draw_text(xpos, ypos, '${tidx}', fmt)
738+
}
739+
.exponent {
740+
app.gg.draw_text(xpos, ypos, '2', fmt)
741+
fs2 := int(f32(fmt.size) * 0.67)
742+
app.gg.draw_text(xpos + app.ui.tile_size / 10, ypos - app.ui.tile_size / 8,
743+
'${tidx}', gx.TextCfg{
647744
...fmt
648-
size: int(f32(fmt.size - 1) / animation_length * anim_size)
649-
}
650-
match app.tile_format {
651-
.normal {
652-
app.gg.draw_text(xpos, ypos, '${1 << tidx}', fmt)
653-
}
654-
.log {
655-
app.gg.draw_text(xpos, ypos, '${tidx}', fmt)
656-
}
657-
.exponent {
658-
app.gg.draw_text(xpos, ypos, '2', fmt)
659-
fs2 := int(f32(fmt.size) * 0.67)
660-
app.gg.draw_text(xpos + app.ui.tile_size / 10, ypos - app.ui.tile_size / 8,
661-
'${tidx}', gx.TextCfg{
662-
...fmt
663-
size: fs2
664-
align: gx.HorizontalAlign.left
665-
})
666-
}
667-
.shifts {
668-
fs2 := int(f32(fmt.size) * 0.6)
669-
app.gg.draw_text(xpos, ypos, '2<<${tidx - 1}', gx.TextCfg{
670-
...fmt
671-
size: fs2
672-
})
673-
}
674-
.none {} // Don't draw any text here, colors only
675-
.end {} // Should never get here
676-
}
745+
size: fs2
746+
align: gx.HorizontalAlign.left
747+
})
748+
}
749+
.shifts {
750+
fs2 := int(f32(fmt.size) * 0.6)
751+
app.gg.draw_text(xpos, ypos, '2<<${tidx - 1}', gx.TextCfg{
752+
...fmt
753+
size: fs2
754+
})
677755
}
756+
.none {} // Don't draw any text here, colors only
757+
.end {} // Should never get here
678758
}
759+
// oidx_fmt := gx.TextCfg{...fmt,size: 14}
760+
// app.gg.draw_text(xoffset + 50, yoffset + 15, 'y:${oidx >> 16}|x:${oidx & 0xFFFF}|m:${app.mtickers[y][x]:5.3f}', oidx_fmt)
761+
// app.gg.draw_text(xoffset + 52, yoffset + 30, 'ox:${ox}|oy:${oy}', oidx_fmt)
762+
// app.gg.draw_text(xoffset + 52, yoffset + 85, 'dx:${dx}|dy:${dy}', oidx_fmt)
679763
}
680764
}
681765

@@ -733,7 +817,7 @@ fn (mut app App) handle_swipe() {
733817
adx, ady := math.abs(dx), math.abs(dy)
734818
dmin := if math.min(adx, ady) > 0 { math.min(adx, ady) } else { 1 }
735819
dmax := if math.max(adx, ady) > 0 { math.max(adx, ady) } else { 1 }
736-
tdiff := int(e.time.unix_milli() - s.time.unix_milli())
820+
tdiff := (e.time - s.time).milliseconds()
737821
// TODO: make this calculation more accurate (don't use arbitrary numbers)
738822
min_swipe_distance := int(math.sqrt(math.min(w, h) * tdiff / 100)) + 20
739823
if dmax < min_swipe_distance {

0 commit comments

Comments
 (0)