2048: Difference between revisions
(+ D entry) |
(→{{header|Go}}: Add implementation) |
||
Line 429: | Line 429: | ||
}</lang> |
}</lang> |
||
The output is the same as the C++ version. |
The output is the same as the C++ version. |
||
=={{header|Go}}== |
|||
{{works with|Linux}} |
|||
<lang Go>package main |
|||
import ( |
|||
"bufio" |
|||
"fmt" |
|||
"log" |
|||
"math/rand" |
|||
"os" |
|||
"os/exec" |
|||
"strconv" |
|||
"strings" |
|||
"text/template" |
|||
"time" |
|||
"unicode" |
|||
"golang.org/x/crypto/ssh/terminal" |
|||
) |
|||
const maxPoints = 2048 |
|||
const ( |
|||
fieldSizeX = 4 |
|||
fieldSizeY = 4 |
|||
) |
|||
const tilesAtStart = 2 |
|||
const probFor2 = 0.9 |
|||
type button int |
|||
const ( |
|||
_ button = iota |
|||
up |
|||
down |
|||
right |
|||
left |
|||
quit |
|||
) |
|||
var labels = func() map[button]rune { |
|||
m := make(map[button]rune, 4) |
|||
m[up] = 'W' |
|||
m[down] = 'S' |
|||
m[right] = 'D' |
|||
m[left] = 'A' |
|||
return m |
|||
}() |
|||
var keybinding = func() map[rune]button { |
|||
m := make(map[rune]button, 8) |
|||
for b, r := range labels { |
|||
m[r] = b |
|||
if unicode.IsUpper(r) { |
|||
r = unicode.ToLower(r) |
|||
} else { |
|||
r = unicode.ToUpper(r) |
|||
} |
|||
m[r] = b |
|||
} |
|||
m[0x03] = quit |
|||
return m |
|||
}() |
|||
var model = struct { |
|||
Score int |
|||
Field [fieldSizeY][fieldSizeX]int |
|||
}{} |
|||
var view = func() *template.Template { |
|||
maxWidth := 1 |
|||
for i := maxPoints; i >= 10; i /= 10 { |
|||
maxWidth++ |
|||
} |
|||
w := maxWidth + 3 |
|||
r := make([]byte, fieldSizeX*w+1) |
|||
for i := range r { |
|||
if i%w == 0 { |
|||
r[i] = '+' |
|||
} else { |
|||
r[i] = '-' |
|||
} |
|||
} |
|||
rawBorder := string(r) |
|||
v, err := template.New("").Parse(`SCORE: {{.Score}} |
|||
{{range .Field}} |
|||
` + rawBorder + ` |
|||
|{{range .}} {{if .}}{{printf "%` + strconv.Itoa(maxWidth) + `d" .}}{{else}}` + |
|||
strings.Repeat(" ", maxWidth) + `{{end}} |{{end}}{{end}} |
|||
` + rawBorder + ` |
|||
(` + string(labels[up]) + `)Up (` + |
|||
string(labels[down]) + `)Down (` + |
|||
string(labels[left]) + `)Left (` + |
|||
string(labels[right]) + `)Right |
|||
`) |
|||
check(err) |
|||
return v |
|||
}() |
|||
func check(err error) { |
|||
if err != nil { |
|||
log.Panicln(err) |
|||
} |
|||
} |
|||
func clear() { |
|||
c := exec.Command("clear") |
|||
c.Stdout = os.Stdout |
|||
check(c.Run()) |
|||
} |
|||
func draw() { |
|||
clear() |
|||
check(view.Execute(os.Stdout, model)) |
|||
} |
|||
func addRandTile() (full bool) { |
|||
free := make([]*int, 0, fieldSizeX*fieldSizeY) |
|||
for x := 0; x < fieldSizeX; x++ { |
|||
for y := 0; y < fieldSizeY; y++ { |
|||
if model.Field[y][x] == 0 { |
|||
free = append(free, &model.Field[y][x]) |
|||
} |
|||
} |
|||
} |
|||
val := 4 |
|||
if rand.Float64() < probFor2 { |
|||
val = 2 |
|||
} |
|||
*free[rand.Intn(len(free))] = val |
|||
return len(free) == 1 |
|||
} |
|||
type point struct{ x, y int } |
|||
func (p point) get() int { return model.Field[p.y][p.x] } |
|||
func (p point) set(n int) { model.Field[p.y][p.x] = n } |
|||
func (p point) inField() bool { return p.x >= 0 && p.y >= 0 && p.x < fieldSizeX && p.y < fieldSizeY } |
|||
func (p *point) next(n point) { p.x += n.x; p.y += n.y } |
|||
func controller(key rune) (gameOver bool) { |
|||
b := keybinding[key] |
|||
if b == 0 { |
|||
return false |
|||
} |
|||
if b == quit { |
|||
return true |
|||
} |
|||
var starts []point |
|||
var next point |
|||
switch b { |
|||
case up: |
|||
next = point{0, 1} |
|||
starts = make([]point, fieldSizeX) |
|||
for x := 0; x < fieldSizeX; x++ { |
|||
starts[x] = point{x, 0} |
|||
} |
|||
case down: |
|||
next = point{0, -1} |
|||
starts = make([]point, fieldSizeX) |
|||
for x := 0; x < fieldSizeX; x++ { |
|||
starts[x] = point{x, fieldSizeY - 1} |
|||
} |
|||
case right: |
|||
next = point{-1, 0} |
|||
starts = make([]point, fieldSizeY) |
|||
for y := 0; y < fieldSizeY; y++ { |
|||
starts[y] = point{fieldSizeX - 1, y} |
|||
} |
|||
case left: |
|||
next = point{1, 0} |
|||
starts = make([]point, fieldSizeY) |
|||
for y := 0; y < fieldSizeY; y++ { |
|||
starts[y] = point{0, y} |
|||
} |
|||
} |
|||
moved := false |
|||
winning := false |
|||
for _, s := range starts { |
|||
n := s |
|||
move := func(set int) { |
|||
moved = true |
|||
s.set(set) |
|||
n.set(0) |
|||
} |
|||
for n.next(next); n.inField(); n.next(next) { |
|||
if s.get() != 0 { |
|||
if n.get() == s.get() { |
|||
score := s.get() * 2 |
|||
model.Score += score |
|||
winning = score >= maxPoints |
|||
move(score) |
|||
s.next(next) |
|||
} else if n.get() != 0 { |
|||
s.next(next) |
|||
if s.get() == 0 { |
|||
move(n.get()) |
|||
} |
|||
} |
|||
} else if n.get() != 0 { |
|||
move(n.get()) |
|||
} |
|||
} |
|||
} |
|||
if !moved { |
|||
return false |
|||
} |
|||
lost := false |
|||
if addRandTile() { |
|||
lost = true |
|||
Out: |
|||
for x := 0; x < fieldSizeX; x++ { |
|||
for y := 0; y < fieldSizeY; y++ { |
|||
if (y > 0 && model.Field[y][x] == model.Field[y-1][x]) || |
|||
(x > 0 && model.Field[y][x] == model.Field[y][x-1]) { |
|||
lost = false |
|||
break Out |
|||
} |
|||
} |
|||
} |
|||
} |
|||
draw() |
|||
if winning { |
|||
fmt.Println("You win!") |
|||
return true |
|||
} |
|||
if lost { |
|||
fmt.Println("Game Over") |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
func main() { |
|||
oldState, err := terminal.MakeRaw(0) |
|||
check(err) |
|||
defer terminal.Restore(0, oldState) |
|||
rand.Seed(time.Now().Unix()) |
|||
for i := tilesAtStart; i > 0; i-- { |
|||
addRandTile() |
|||
} |
|||
draw() |
|||
stdin := bufio.NewReader(os.Stdin) |
|||
readKey := func() rune { |
|||
r, _, err := stdin.ReadRune() |
|||
check(err) |
|||
return r |
|||
} |
|||
for !controller(readKey()) { |
|||
} |
|||
} |
|||
</lang> |
|||
=={{header|Tcl}}== |
=={{header|Tcl}}== |
Revision as of 22:19, 3 April 2015
Implement a 2D sliding block puzzle game where blocks with numbers are combined to add their values.
The rules are that each turn the player must perform a valid move shifting all tiles in one direction (up, down, left or right). A move is valid when at least one tile can be moved in that direction. When moved against each other tiles with the same number on them combine into one. A new tile with the value of 2 is spawned at the end of each turn if there is an empty spot for it. To win the player must create a tile with the number 2048. The player loses if no valid moves are possible.
The name comes from the popular open-source implementation of this game mechanic, 2048.
Requirements:
- "Non-greedy" movement. The tiles that were created by combining other tiles should not be combined again during the same turn (move). That is to say that moving the tile row of
[2][2][2][2]
to the right should result in
......[4][4]
and not
.........[8]
- Check for valid moves. The player shouldn't be able to skip their turn by trying a move that doesn't change the board.
- Win condition.
- Lose condition.
C++
<lang cpp>
- include <time.h>
- include <iostream>
- include <string>
- include <iomanip>
typedef unsigned int uint; using namespace std; enum movDir { UP, DOWN, LEFT, RIGHT };
class tile { public:
tile() : val( 0 ), blocked( false ) {} uint val; bool blocked;
};
class g2048 { public:
g2048() : done( false ), win( false ), moved( true ), score( 0 ) {} void loop() {
addTile(); while( true ) { if( moved ) addTile(); drawBoard(); if( done ) break; waitKey(); } string s = "Game Over!"; if( win ) s = "You've made it!"; cout << s << endl << endl;
}
private:
void drawBoard() {
system( "cls" ); cout << "SCORE: " << score << endl << endl; for( int y = 0; y < 4; y++ ) { cout << "+------+------+------+------+" << endl << "| "; for( int x = 0; x < 4; x++ ) { if( !board[x][y].val ) cout << setw( 4 ) << " "; else cout << setw( 4 ) << board[x][y].val; cout << " | "; } cout << endl; } cout << "+------+------+------+------+" << endl << endl;
} void waitKey() {
moved = false; char c; cout << "(W)Up (S)Down (A)Left (D)Right "; cin >> c; c &= 0x5F; switch( c ) { case 'W': move( UP );break; case 'A': move( LEFT ); break; case 'S': move( DOWN ); break; case 'D': move( RIGHT ); } for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) board[x][y].blocked = false;
} void addTile() {
for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) if( !board[x][y].val ) { uint a, b; do { a = rand() % 4; b = rand() % 4; } while( board[a][b].val );
int s = rand() % 100; if( s > 89 ) board[a][b].val = 4; else board[a][b].val = 2; if( canMove() ) return; } done = true;
} bool canMove() {
for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) if( !board[x][y].val ) return true;
for( int y = 0; y < 4; y++ ) for( int x = 0; x < 4; x++ ) { if( testAdd( x + 1, y, board[x][y].val ) ) return true; if( testAdd( x - 1, y, board[x][y].val ) ) return true; if( testAdd( x, y + 1, board[x][y].val ) ) return true; if( testAdd( x, y - 1, board[x][y].val ) ) return true; } return false;
} bool testAdd( int x, int y, uint v ) {
if( x < 0 || x > 3 || y < 0 || y > 3 ) return false; return board[x][y].val == v;
} void moveVert( int x, int y, int d ) {
if( board[x][y + d].val && board[x][y + d].val == board[x][y].val && !board[x][y].blocked && !board[x][y + d].blocked ) { board[x][y].val = 0; board[x][y + d].val *= 2; score += board[x][y + d].val; board[x][y + d].blocked = true; moved = true; } else if( !board[x][y + d].val && board[x][y].val ) { board[x][y + d].val = board[x][y].val; board[x][y].val = 0; moved = true; } if( d > 0 ) { if( y + d < 3 ) moveVert( x, y + d, 1 ); } else { if( y + d > 0 ) moveVert( x, y + d, -1 ); }
} void moveHori( int x, int y, int d ) {
if( board[x + d][y].val && board[x + d][y].val == board[x][y].val && !board[x][y].blocked && !board[x + d][y].blocked ) { board[x][y].val = 0; board[x + d][y].val *= 2; score += board[x + d][y].val; board[x + d][y].blocked = true; moved = true; } else if( !board[x + d][y].val && board[x][y].val ) { board[x + d][y].val = board[x][y].val; board[x][y].val = 0; moved = true; } if( d > 0 ) { if( x + d < 3 ) moveHori( x + d, y, 1 ); } else { if( x + d > 0 ) moveHori( x + d, y, -1 ); }
} void move( movDir d ) {
switch( d ) { case UP: for( int x = 0; x < 4; x++ ) { int y = 1; while( y < 4 ) { if( board[x][y].val ) moveVert( x, y, -1 ); y++;} } break; case DOWN: for( int x = 0; x < 4; x++ ) { int y = 2; while( y >= 0 ) { if( board[x][y].val ) moveVert( x, y, 1 ); y--;} } break; case LEFT: for( int y = 0; y < 4; y++ ) { int x = 1; while( x < 4 ) { if( board[x][y].val ) moveHori( x, y, -1 ); x++;} } break; case RIGHT: for( int y = 0; y < 4; y++ ) { int x = 2; while( x >= 0 ) { if( board[x][y].val ) moveHori( x, y, 1 ); x--;} } }
} tile board[4][4]; bool win, done, moved; uint score;
}; int main( int argc, char* argv[] ) {
srand( static_cast<uint>( time( NULL ) ) ); g2048 g; g.loop(); return system( "pause" );
} </lang>
- Output:
SCORE: 2024 +------+------+------+------+ | 2 | 8 | 32 | 256 | +------+------+------+------+ | | | 4 | 32 | +------+------+------+------+ | | | 2 | 8 | +------+------+------+------+ | | | | 2 | +------+------+------+------+ (W)Up (S)Down (A)Left (D)Right
D
<lang d>import std.stdio, std.string, std.random; import core.stdc.stdlib: exit;
struct G2048 {
public void gameLoop() /*@safe @nogc*/ { addTile; while (true) { if (moved) addTile; drawBoard; if (done) break; waitKey; } writeln(win ? "You win!" : "Game Over!"); }
private:
static struct Tile { uint val = 0; bool blocked = false; }
enum moveDir { up, down, left, right } enum uint side = 4;
Tile[side][side] board; bool win = false, done = false, moved = true; uint score = 0;
void drawBoard() const /*@safe @nogc*/ { writeln("SCORE: ", score, "\n"); foreach (immutable y; 0 .. side) { write("+------+------+------+------+\n| "); foreach (immutable x; 0 .. side) { if (board[x][y].val) writef("%4d", board[x][y].val); else writef("%4s", " "); write(" | "); } writeln; } "+------+------+------+------+\n".writeln; }
void waitKey() /*@safe*/ { moved = false; "(W)Up (S)Down (A)Left (D)Right (Q)Quit: ".write; immutable c = readln.strip.toLower;
switch (c) { case "w": move(moveDir.up); break; case "a": move(moveDir.left); break; case "s": move(moveDir.down); break; case "d": move(moveDir.right); break; case "q": endGame; break; default: break; }
foreach (immutable y; 0 .. side) foreach (immutable x; 0 .. side) board[x][y].blocked = false; }
void endGame() const { writeln("Game ended with score: ", score); exit(0); }
void addTile() /*nothrow*/ @safe /*@nogc*/ { foreach (immutable y; 0 .. side) { foreach (immutable x; 0 .. side) { if (!board[x][y].val) { uint a, b; do { a = uniform(0, side); b = uniform(0, side); } while (board[a][b].val);
board[a][b].val = (uniform01 > 0.89) ? side : 2; if (canMove) return; } } } done = true; }
bool canMove() const pure nothrow @safe @nogc { foreach (immutable y; 0 .. side) foreach (immutable x; 0 .. side) if (!board[x][y].val) return true;
foreach (immutable y; 0 .. side) { foreach (immutable x; 0 .. side) { if (testAdd(x + 1, y, board[x][y].val) || testAdd(x - 1, y, board[x][y].val) || testAdd(x, y + 1, board[x][y].val) || testAdd(x, y - 1, board[x][y].val)) return true; } } return false; }
bool testAdd(in uint x, in uint y, in uint v) const pure nothrow @safe @nogc { if (x > 3 || y > 3) return false; return board[x][y].val == v; }
void moveVertically(in uint x, in uint y, in uint d) pure nothrow @safe @nogc { if (board[x][y + d].val && board[x][y + d].val == board[x][y].val && !board[x][y].blocked && !board[x][y + d].blocked) { board[x][y].val = 0; board[x][y + d].val *= 2; score += board[x][y + d].val; board[x][y + d].blocked = true; moved = true; } else if (!board[x][y + d].val && board[x][y].val) { board[x][y + d].val = board[x][y].val; board[x][y].val = 0; moved = true; }
if (d > 0) { if (y + d < 3) moveVertically(x, y + d, 1); } else { if (y + d > 0) moveVertically(x, y + d, -1); } }
void moveHorizontally(in uint x, in uint y, in uint d) pure nothrow @safe @nogc { if (board[x + d][y].val && board[x + d][y].val == board[x][y].val && !board[x][y].blocked && !board[x + d][y].blocked) { board[x][y].val = 0; board[x + d][y].val *= 2; score += board[x + d][y].val; board[x + d][y].blocked = true; moved = true; } else if (!board[x + d][y].val && board[x][y].val) { board[x + d][y].val = board[x][y].val; board[x][y].val = 0; moved = true; }
if (d > 0) { if (x + d < 3) moveHorizontally(x + d, y, 1); } else { if (x + d > 0) moveHorizontally(x + d, y, -1); } }
void move(in moveDir d) pure nothrow @safe @nogc { final switch (d) with(moveDir) { case up: foreach (immutable x; 0 .. side) foreach (immutable y; 1 .. side) if (board[x][y].val) moveVertically(x, y, -1); break; case down: foreach (immutable x; 0 .. side) foreach_reverse (immutable y; 0 .. 3) if (board[x][y].val) moveVertically(x, y, 1); break; case left: foreach (immutable y; 0 .. side) foreach (immutable x; 1 .. side) if (board[x][y].val) moveHorizontally(x, y, -1); break; case right: foreach (immutable y; 0 .. side) foreach_reverse (immutable x; 0 .. 3) if (board[x][y].val) moveHorizontally(x, y, 1); } }
}
void main() /*safe*/ {
G2048 g; g.gameLoop;
}</lang> The output is the same as the C++ version.
Go
<lang Go>package main
import ( "bufio" "fmt" "log" "math/rand" "os" "os/exec" "strconv" "strings" "text/template" "time" "unicode"
"golang.org/x/crypto/ssh/terminal" )
const maxPoints = 2048 const ( fieldSizeX = 4 fieldSizeY = 4 ) const tilesAtStart = 2 const probFor2 = 0.9
type button int
const ( _ button = iota up down right left quit )
var labels = func() map[button]rune { m := make(map[button]rune, 4) m[up] = 'W' m[down] = 'S' m[right] = 'D' m[left] = 'A' return m }() var keybinding = func() map[rune]button { m := make(map[rune]button, 8) for b, r := range labels { m[r] = b if unicode.IsUpper(r) { r = unicode.ToLower(r) } else { r = unicode.ToUpper(r) } m[r] = b } m[0x03] = quit return m }()
var model = struct { Score int Field [fieldSizeY][fieldSizeX]int }{}
var view = func() *template.Template { maxWidth := 1 for i := maxPoints; i >= 10; i /= 10 { maxWidth++ }
w := maxWidth + 3 r := make([]byte, fieldSizeX*w+1) for i := range r { if i%w == 0 { r[i] = '+' } else { r[i] = '-' } } rawBorder := string(r)
v, err := template.New("").Parse(`SCORE: Template:.Score Template:Range .Field ` + rawBorder + ` |Template:Range . Template:If .Template:Printf "%` + strconv.Itoa(maxWidth) + `d" .Template:Else` + strings.Repeat(" ", maxWidth) + `Template:End |Template:EndTemplate:End ` + rawBorder + `
(` + string(labels[up]) + `)Up (` + string(labels[down]) + `)Down (` + string(labels[left]) + `)Left (` + string(labels[right]) + `)Right `) check(err) return v }()
func check(err error) { if err != nil { log.Panicln(err) } }
func clear() { c := exec.Command("clear") c.Stdout = os.Stdout check(c.Run()) }
func draw() { clear() check(view.Execute(os.Stdout, model)) }
func addRandTile() (full bool) { free := make([]*int, 0, fieldSizeX*fieldSizeY)
for x := 0; x < fieldSizeX; x++ { for y := 0; y < fieldSizeY; y++ { if model.Field[y][x] == 0 { free = append(free, &model.Field[y][x]) } } }
val := 4 if rand.Float64() < probFor2 { val = 2 } *free[rand.Intn(len(free))] = val
return len(free) == 1 }
type point struct{ x, y int }
func (p point) get() int { return model.Field[p.y][p.x] } func (p point) set(n int) { model.Field[p.y][p.x] = n } func (p point) inField() bool { return p.x >= 0 && p.y >= 0 && p.x < fieldSizeX && p.y < fieldSizeY } func (p *point) next(n point) { p.x += n.x; p.y += n.y }
func controller(key rune) (gameOver bool) { b := keybinding[key]
if b == 0 { return false } if b == quit { return true }
var starts []point var next point
switch b { case up: next = point{0, 1} starts = make([]point, fieldSizeX) for x := 0; x < fieldSizeX; x++ { starts[x] = point{x, 0} } case down: next = point{0, -1} starts = make([]point, fieldSizeX) for x := 0; x < fieldSizeX; x++ { starts[x] = point{x, fieldSizeY - 1} } case right: next = point{-1, 0} starts = make([]point, fieldSizeY) for y := 0; y < fieldSizeY; y++ { starts[y] = point{fieldSizeX - 1, y} } case left: next = point{1, 0} starts = make([]point, fieldSizeY) for y := 0; y < fieldSizeY; y++ { starts[y] = point{0, y} } }
moved := false winning := false
for _, s := range starts { n := s move := func(set int) { moved = true s.set(set) n.set(0) } for n.next(next); n.inField(); n.next(next) { if s.get() != 0 { if n.get() == s.get() { score := s.get() * 2 model.Score += score winning = score >= maxPoints
move(score) s.next(next) } else if n.get() != 0 { s.next(next) if s.get() == 0 { move(n.get()) } } } else if n.get() != 0 { move(n.get()) } } }
if !moved { return false }
lost := false if addRandTile() { lost = true Out: for x := 0; x < fieldSizeX; x++ { for y := 0; y < fieldSizeY; y++ { if (y > 0 && model.Field[y][x] == model.Field[y-1][x]) || (x > 0 && model.Field[y][x] == model.Field[y][x-1]) { lost = false break Out } } } }
draw()
if winning { fmt.Println("You win!") return true } if lost { fmt.Println("Game Over") return true }
return false }
func main() { oldState, err := terminal.MakeRaw(0) check(err) defer terminal.Restore(0, oldState)
rand.Seed(time.Now().Unix())
for i := tilesAtStart; i > 0; i-- { addRandTile() } draw()
stdin := bufio.NewReader(os.Stdin)
readKey := func() rune { r, _, err := stdin.ReadRune() check(err) return r }
for !controller(readKey()) { } } </lang>
Tcl
Text mode
<lang tcl>
- A minimal implementation of the game 2048 in Tcl.
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 </lang>