-
Notifications
You must be signed in to change notification settings - Fork 3
/
Tcl 2048.tcl
276 lines (241 loc) · 7.47 KB
/
Tcl 2048.tcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# A minimal implementation of the game 2048 in Tcl.
# For a maintained version with expanded functionality see
# https://tcl.wiki/40557.
package require Tcl 8.5
package require struct::matrix
package require struct::list
# Board size.
set size 4
# Iterate over all cells of the game board and run script for each.
#
# The game board is a 2D matrix of a fixed size that consists of elements
# called "cells" that each can contain a game tile (corresponds to numerical
# values of 2, 4, 8, ..., 2048) or nothing (zero).
#
# - cellList is a list of cell indexes (coordinates), which are
# themselves lists of two numbers each. They each represent the location
# of a given cell on the board.
# - varName1 are varName2 are names of the variables the will be assigned
# the index values.
# - cellVarName is the name of the variable that at each step of iteration
# will contain the numerical value of the present cell. Assigning to it will
# change the cell's value.
# - script is the script to run.
proc forcells {cellList varName1 varName2 cellVarName script} {
upvar $varName1 i
upvar $varName2 j
upvar $cellVarName c
foreach cell $cellList {
set i [lindex $cell 0]
set j [lindex $cell 1]
set c [cell-get $cell]
uplevel $script
cell-set "$i $j" $c
}
}
# Generate a list of cell indexes for all cells on the board, i.e.,
# {{0 0} {0 1} ... {0 size-1} {1 0} {1 1} ... {size-1 size-1}}.
proc cell-indexes {} {
global size
set list {}
foreach i [::struct::list iota $size] {
foreach j [::struct::list iota $size] {
lappend list [list $i $j]
}
}
return $list
}
# Check if a number is a valid cell index (is 0 to size-1).
proc valid-index {i} {
global size
expr {0 <= $i && $i < $size}
}
# Return 1 if the predicate pred is true when applied to all items on the list
# or 0 otherwise.
proc map-and {list pred} {
set res 1
foreach item $list {
set res [expr {$res && [$pred $item]}]
if {! $res} break
}
return $res
}
# Check if list represents valid cell coordinates.
proc valid-cell? cell {
map-and $cell valid-index
}
# Get the value of a game board cell.
proc cell-get cell {
board get cell {*}$cell
}
# Set the value of a game board cell.
proc cell-set {cell value} {
board set cell {*}$cell $value
}
# Filter a list of board cell indexes cellList to only have those indexes
# that correspond to empty board cells.
proc empty {cellList} {
::struct::list filterfor x $cellList {[cell-get $x] == 0}
}
# Pick a random item from the given list.
proc pick list {
lindex $list [expr {int(rand() * [llength $list])}]
}
# Put a "2" into an empty cell on the board.
proc spawn-new {} {
set emptyCell [pick [empty [cell-indexes]]]
if {[llength $emptyCell] > 0} {
forcells [list $emptyCell] i j cell {
set cell 2
}
}
return $emptyCell
}
# Return vector sum of lists v1 and v2.
proc vector-add {v1 v2} {
set result {}
foreach a $v1 b $v2 {
lappend result [expr {$a + $b}]
}
return $result
}
# If checkOnly is false try to shift all cells one step in the direction of
# directionVect. If checkOnly is true just say if that move is possible.
proc move-all {directionVect {checkOnly 0}} {
set changedCells 0
forcells [cell-indexes] i j cell {
set newIndex [vector-add "$i $j" $directionVect]
set removedStar 0
# For every nonempty source cell and valid destination cell...
if {$cell != 0 && [valid-cell? $newIndex]} {
if {[cell-get $newIndex] == 0} {
# Destination is empty.
if {$checkOnly} {
# -level 2 is to return from both forcells and move-all.
return -level 2 true
} else {
# Move tile to empty cell.
cell-set $newIndex $cell
set cell 0
incr changedCells
}
} elseif {([cell-get $newIndex] eq $cell) &&
[string first + $cell] == -1} {
# Destination is the same number as source.
if {$checkOnly} {
return -level 2 true
} else {
# When merging two tiles into one mark the new tile with
# the marker of "+" to ensure it doesn't get combined
# again this turn.
cell-set $newIndex [expr {2 * $cell}]+
set cell 0
incr changedCells
}
}
}
}
if {$checkOnly} {
return false
}
# Remove "changed this turn" markers at the end of the turn.
if {$changedCells == 0} {
forcells [cell-indexes] i j cell {
set cell [string trim $cell +]
}
}
return $changedCells
}
# Is it possible to move any tiles in the direction of directionVect?
proc can-move? {directionVect} {
move-all $directionVect 1
}
# Check win condition. The player wins when there's a 2048 tile.
proc check-win {} {
forcells [cell-indexes] i j cell {
if {$cell == 2048} {
puts "You win!"
exit 0
}
}
}
# Check lose condition. The player loses when the win condition isn't met and
# there are no possible moves.
proc check-lose {possibleMoves} {
set values [dict values $possibleMoves]
if {!(true in $values || 1 in $values)} {
puts "You lose."
exit 0
}
}
# Pretty-print the board. Specify an index in highlight to highlight a cell.
proc print-board {{highlight {-1 -1}}} {
forcells [cell-indexes] i j cell {
if {$j == 0} {
puts ""
}
puts -nonewline [
if {$cell != 0} {
if {[::struct::list equal "$i $j" $highlight]} {
format "\[%4s\]" $cell*
} else {
format "\[%4s\]" $cell
}
} else {
lindex "......"
}
]
}
puts "\n"
}
proc main {} {
global size
struct::matrix board
# Generate an empty board of a given size.
board add columns $size
board add rows $size
forcells [cell-indexes] i j cell {
set cell 0
}
set controls {
h {0 -1}
j {1 0}
k {-1 0}
l {0 1}
}
# Game loop.
while true {
set playerMove 0
set possibleMoves {}
# Add new tile to the board and print the board highlighting this tile.
print-board [spawn-new]
check-win
# Find possible moves.
foreach {button vector} $controls {
dict set possibleMoves $button [can-move? $vector]
}
check-lose $possibleMoves
# Get valid input from the player.
while {$playerMove == 0} {
# Print prompt.
puts -nonewline "Move ("
foreach {button vector} $controls {
if {[dict get $possibleMoves $button]} {
puts -nonewline $button
}
}
puts ")?"
set playerInput [gets stdin]
# Validate input.
if {[dict exists $possibleMoves $playerInput] &&
[dict get $possibleMoves $playerInput]} {
set playerMove [dict get $controls $playerInput]
}
}
# Apply current move until no changes occur on the board.
while true {
if {[move-all $playerMove] == 0} break
}
}
}
main