Snake
Snake is a game where the player maneuvers a line which grows in length every time the snake reaches a food source.
This page uses content from Wikipedia. The original article was at Snake_(video_game). The list of authors can be seen in the page history. As with Rosetta Code, the text of Wikipedia is available under the GNU FDL. (See links for details on variance) |
- Task
Implement a variant of the Snake game, in any interactive environment, in which a sole player attempts to eat items by running into them with the head of the snake. Each item eaten makes the snake longer and a new item is randomly generated somewhere else on the plane. The game ends when the snake attempts to eat himself.
BASIC
ZX Spectrum Basic
By ancient tradition, the controls are Q for up, A for down, O for left, and P for right.
A screenshot is here.
Note that lines 10
to 210
and 580
to 890
—more than half the program—define graphics characters for the snake's head (facing in different directions) and for its food. If you're happy to make do with characters from the standard character set, you can easily adapt lines 220
to 570
to work on their own. The things the snake eats are supposed to be apples, although they don't look too much like them.
<lang zxbasic> 10 FOR i=0 TO 7
20 READ bits 30 POKE USR "L"+i,bits 40 NEXT i 50 FOR i=0 TO 7 60 READ bits 70 POKE USR "R"+i,bits 80 NEXT i 90 FOR i=0 TO 7
100 READ bits 110 POKE USR "P"+i,bits 120 NEXT i 130 RESTORE 740 140 FOR i=7 TO 0 STEP -1 150 READ bits 160 POKE USR "D"+i,bits 170 NEXT i 180 FOR i=0 TO 7 190 READ bits 200 POKE USR "F"+i, bits 210 NEXT i 220 PAPER 0 230 CLS 240 LET snakex=19 250 LET snakey=15 260 LET dx=-1 270 LET dy=0 280 LET s$=CHR$ 15+CHR$ 20+CHR$ 15+CHR$ 21 290 LET foodx=INT (RND*32) 300 LET foody=INT (RND*22) 310 IF SCREEN$ (foody,foodx)<>" " THEN GO TO 290 320 INK 2 330 PRINT AT foody,foodx;CHR$ 149 340 INK 4 350 INVERSE 1 360 PRINT AT CODE s$,CODE s$(1);"#" 370 INVERSE 0 380 IF INKEY$="q" AND dy=0 THEN LET dx=0: LET dy=-1 390 IF INKEY$="a" AND dy=0 THEN LET dx=0: LET dy=1 400 IF INKEY$="o" AND dx=0 THEN LET dx=-1: LET dy=0 410 IF INKEY$="p" AND dx=0 THEN LET dx=1: LET dy=0 420 IF dx=-1 THEN PRINT AT snakey,snakex;CHR$ 155 430 IF dx=1 THEN PRINT AT snakey,snakex;CHR$ 161 440 IF dy=1 THEN PRINT AT snakey,snakex;CHR$ 159 450 IF dy=-1 THEN PRINT AT snakey,snakex;CHR$ 147 460 LET s$=CHR$ snakey+CHR$ snakex+s$ 470 IF snakex=foodx AND snakey=foody THEN GO TO 290 480 PRINT AT CODE s$(LEN s$-1),CODE s$(LEN s$);" " 490 LET s$=s$( TO LEN s$-2) 500 LET snakex=snakex+dx 510 LET snakey=snakey+dy 520 IF snakex=-1 THEN LET snakex=31 530 IF snakex=32 THEN LET snakex=0 540 IF snakey=-1 THEN LET snakey=21 550 IF snakey=22 THEN LET snakey=0 560 IF SCREEN$ (snakey,snakex)="#" THEN STOP 570 GO TO 340 580 DATA BIN 00001111 590 DATA BIN 00111111 600 DATA BIN 01110011 610 DATA BIN 11110011 620 DATA BIN 11111111 630 DATA BIN 01111111 640 DATA BIN 00000111 650 DATA BIN 00011111 660 DATA BIN 11110000 670 DATA BIN 11111100 680 DATA BIN 11001110 690 DATA BIN 11001111 700 DATA BIN 11111111 710 DATA BIN 11111110 720 DATA BIN 11100000 730 DATA BIN 11111000 740 DATA BIN 00011000 750 DATA BIN 00111100 760 DATA BIN 01111100 770 DATA BIN 01111101 780 DATA BIN 11001101 790 DATA BIN 11001111 800 DATA BIN 11111111 810 DATA BIN 11111111 820 DATA BIN 00000100 830 DATA BIN 00001000 840 DATA BIN 01101011 850 DATA BIN 11111100 860 DATA BIN 11111100 870 DATA BIN 11111100 880 DATA BIN 01111111 890 DATA BIN 00110110</lang>
C
As some implementation below (C++) works on Windows console, let it work on Linux. Other implementations could be added as well, reusing the api. <lang c>// Snake
// The problem with implementing this task in C is, the language standard // does not cover some details essential for interactive games: // a nonblocking keyboard input, a positional console output, // a and millisecond-precision timer: these things are all system-dependent.
// Therefore the program is split in two pieces, a system-independent // game logic, and a system-dependent UI, separated by a tiny API: char nonblocking_getch(); void positional_putch(int x, int y, char ch); void millisecond_sleep(int n); void init_screen(); void update_screen(); void close_screen();
// The implementation of a system-dependent part. // Requires POSIX IEEE 1003.1-2008 compliant system and ncurses library.
- ifdef __linux__
- define _POSIX_C_SOURCE 200809L
- include <time.h> // nanosleep
- include <ncurses.h> // getch, mvaddch, and others
char nonblocking_getch() { return getch(); } void positional_putch(int x, int y, char ch) { mvaddch(x, y, ch); } void millisecond_sleep(int n) { struct timespec t = { 0, n * 1000000 }; nanosleep(&t, 0); // for older POSIX standards, consider usleep() } void update_screen() { refresh(); } void init_screen() { initscr(); noecho(); cbreak(); nodelay(stdscr, TRUE); } void close_screen() { endwin(); }
- endif
// An implementation for some other system...
- ifdef _WIN32
- error "not implemented"
- endif
// The game logic, system-independent
- include <time.h> // time
- include <stdlib.h> // rand, srand
- define w 80
- define h 40
int board[w * h]; int head; enum Dir { N, E, S, W } dir; int quit;
enum State { SPACE=0, FOOD=1, BORDER=2 }; // negative values denote the snake (a negated time-to-live in given cell)
// reduce a time-to-live, effectively erasing the tail void age() {
int i;
for(i = 0; i < w * h; ++i) if(board[i] < 0) ++board[i]; }
// put a piece of food at random empty position void plant() { int r; do r = rand() % (w * h); while(board[r] != SPACE); board[r] = FOOD; }
// initialize the board, plant a very first food item void start(void) {
int i;
for(i = 0; i < w; ++i) board[i] = board[i + (h - 1) * w] = BORDER; for(i = 0; i < h; ++i) board[i * w] = board[i * w + w - 1] = BORDER; head = w * (h - 1 - h % 2) / 2; // screen center for any h board[head] = -5; dir = N; quit = 0; srand(time(0)); plant(); }
void step() { int len = board[head]; switch(dir) { case N: head -= w; break; case S: head += w; break; case W: --head; break; case E: ++head; break; } switch(board[head]) { case SPACE: board[head] = len - 1; // keep in mind len is negative age(); break; case FOOD: board[head] = len - 1; plant(); break; default: quit = 1; } }
void show() { const char * symbol = " @.";
int i;
for(i = 0; i < w * h; ++i) positional_putch(i / w, i % w, board[i] < 0 ? '#' : symbol[board[i]]); update_screen(); }
int main (int argc, char * argv[]) { init_screen(); start(); do { show(); switch(nonblocking_getch()) { case 'i': dir = N; break; case 'j': dir = W; break; case 'k': dir = S; break; case 'l': dir = E; break; case 'q': quit = 1; break; } step(); millisecond_sleep(100); // beware, this approach // is not suitable for anything but toy projects like this } while(!quit); millisecond_sleep(999); close_screen(); return 0; }</lang>
C++
Simple Windows console implementation.
<lang cpp>
- include <windows.h>
- include <ctime>
- include <iostream>
- include <string>
const int WID = 60, HEI = 30, MAX_LEN = 600; enum DIR { NORTH, EAST, SOUTH, WEST };
class snake { public:
snake() { console = GetStdHandle( STD_OUTPUT_HANDLE ); SetConsoleTitle( "Snake" ); COORD coord = { WID + 1, HEI + 2 }; SetConsoleScreenBufferSize( console, coord ); SMALL_RECT rc = { 0, 0, WID, HEI + 1 }; SetConsoleWindowInfo( console, TRUE, &rc ); CONSOLE_CURSOR_INFO ci = { 1, false }; SetConsoleCursorInfo( console, &ci ); } void play() { std::string a; while( 1 ) { createField(); alive = true; while( alive ) { drawField(); readKey(); moveSnake(); Sleep( 50 ); } COORD c = { 0, HEI + 1 }; SetConsoleCursorPosition( console, c ); SetConsoleTextAttribute( console, 0x000b ); std::cout << "Play again [Y/N]? "; std::cin >> a; if( a.at( 0 ) != 'Y' && a.at( 0 ) != 'y' ) return; } }
private:
void createField() { COORD coord = { 0, 0 }; DWORD c; FillConsoleOutputCharacter( console, ' ', ( HEI + 2 ) * 80, coord, &c ); FillConsoleOutputAttribute( console, 0x0000, ( HEI + 2 ) * 80, coord, &c ); SetConsoleCursorPosition( console, coord ); int x = 0, y = 1; for( ; x < WID * HEI; x++ ) brd[x] = 0; for( x = 0; x < WID; x++ ) { brd[x] = brd[x + WID * ( HEI - 1 )] = '+'; } for( ; y < HEI; y++ ) { brd[0 + WID * y] = brd[WID - 1 + WID * y] = '+'; } do { x = rand() % WID; y = rand() % ( HEI >> 1 ) + ( HEI >> 1 ); } while( brd[x + WID * y] ); brd[x + WID * y] = '@'; tailIdx = 0; headIdx = 4; x = 3; y = 2; for( int c = tailIdx; c < headIdx; c++ ) { brd[x + WID * y] = '#'; snk[c].X = 3 + c; snk[c].Y = 2; } head = snk[3]; dir = EAST; points = 0; } void readKey() { if( GetAsyncKeyState( 39 ) & 0x8000 ) dir = EAST; if( GetAsyncKeyState( 37 ) & 0x8000 ) dir = WEST; if( GetAsyncKeyState( 38 ) & 0x8000 ) dir = NORTH; if( GetAsyncKeyState( 40 ) & 0x8000 ) dir = SOUTH; } void drawField() { COORD coord; char t; for( int y = 0; y < HEI; y++ ) { coord.Y = y; for( int x = 0; x < WID; x++ ) { t = brd[x + WID * y]; if( !t ) continue; coord.X = x; SetConsoleCursorPosition( console, coord ); if( coord.X == head.X && coord.Y == head.Y ) { SetConsoleTextAttribute( console, 0x002e ); std::cout << 'O'; SetConsoleTextAttribute( console, 0x0000 ); continue; } switch( t ) { case '#': SetConsoleTextAttribute( console, 0x002a ); break; case '+': SetConsoleTextAttribute( console, 0x0019 ); break; case '@': SetConsoleTextAttribute( console, 0x004c ); break; } std::cout << t; SetConsoleTextAttribute( console, 0x0000 ); } } std::cout << t; SetConsoleTextAttribute( console, 0x0007 ); COORD c = { 0, HEI }; SetConsoleCursorPosition( console, c ); std::cout << "Points: " << points; } void moveSnake() { switch( dir ) { case NORTH: head.Y--; break; case EAST: head.X++; break; case SOUTH: head.Y++; break; case WEST: head.X--; break; } char t = brd[head.X + WID * head.Y]; if( t && t != '@' ) { alive = false; return; } brd[head.X + WID * head.Y] = '#'; snk[headIdx].X = head.X; snk[headIdx].Y = head.Y; if( ++headIdx >= MAX_LEN ) headIdx = 0; if( t == '@' ) { points++; int x, y; do { x = rand() % WID; y = rand() % ( HEI >> 1 ) + ( HEI >> 1 ); } while( brd[x + WID * y] ); brd[x + WID * y] = '@'; return; } SetConsoleCursorPosition( console, snk[tailIdx] ); std::cout << ' '; brd[snk[tailIdx].X + WID * snk[tailIdx].Y] = 0; if( ++tailIdx >= MAX_LEN ) tailIdx = 0; } bool alive; char brd[WID * HEI]; HANDLE console; DIR dir; COORD snk[MAX_LEN]; COORD head; int tailIdx, headIdx, points;
}; int main( int argc, char* argv[] ) {
srand( static_cast<unsigned>( time( NULL ) ) ); snake s; s.play(); return 0;
} </lang>
Delphi
<lang Delphi> unit SnakeGame;
interface
uses
Winapi.Windows, System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Forms, Vcl.Dialogs, System.Generics.Collections, Vcl.ExtCtrls;
type
TSnakeApp = class(TForm) procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure FormPaint(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure DoFrameStep(Sender: TObject); procedure Reset; private { Private declarations } FrameTimer: TTimer; public { Public declarations } end;
TSnake = class len: Integer; alive: Boolean; pos: TPoint; posArray: TList<TPoint>; dir: Byte; private function Eat(Fruit: TPoint): Boolean; function Overlap: Boolean; procedure update; public procedure Paint(Canvas: TCanvas); procedure Reset; constructor Create; destructor Destroy; override; end;
TFruit = class FruitTime: Boolean; pos: TPoint; constructor Create; procedure Reset; procedure Paint(Canvas: TCanvas); private procedure SetFruit; end;
const
L = 1; R = 2; D = 4; U = 8;
var
SnakeApp: TSnakeApp; block: Integer = 24; wid: Integer = 30; hei: Integer = 20; fruit: TFruit; snake: TSnake;
implementation
{$R *.dfm}
function Rect(x, y, w, h: Integer): TRect; begin
Result := TRect.Create(x, y, x + w, y + h);
end;
{ TSnake }
constructor TSnake.Create; begin
posArray := TList<TPoint>.Create; Reset;
end;
procedure TSnake.Paint(Canvas: TCanvas); var
pos: TPoint; i, l: Integer; r: TRect;
begin
with Canvas do begin Brush.Color := rgb(130, 190, 0);
i := posArray.count - 1; l := posArray.count; while True do begin pos := posArray[i]; dec(i); r := rect(pos.x * block, pos.y * block, block, block); FillRect(r); dec(l); if l = 0 then Break; end; end;
end;
procedure TSnake.Reset; begin
alive := true; pos := Tpoint.Create(1, 1); posArray.Clear; posArray.Add(Tpoint.Create(pos)); len := posArray.Count; dir := r;
end;
destructor TSnake.Destroy; begin
posArray.Free; inherited;
end;
function TSnake.Eat(Fruit: TPoint): Boolean; begin
result := (pos.X = Fruit.X) and (pos.y = Fruit.y); if result then begin inc(len); if len > 5000 then len := 500; end;
end;
function TSnake.Overlap: Boolean; var
aLen: Integer; tp: TPoint; i: Integer;
begin
aLen := posArray.count - 1;
for i := 0 to aLen - 1 do begin tp := posArray[i]; if (tp.x = pos.x) and (tp.y = pos.y) then Exit(True); end; Result := false;
end;
procedure TSnake.update; begin
if not alive then exit; case dir of l: begin dec(pos.X); if pos.X < 1 then pos.x := wid - 2 end; r: begin inc(pos.x); if (pos.x > (wid - 2)) then pos.x := 1; end; U: begin dec(pos.y);
if (pos.y < 1) then pos.y := hei - 2 end; D: begin inc(pos.y);
if (pos.y > hei - 2) then pos.y := 1; end; end; if Overlap then alive := False else begin posArray.Add(TPoint(pos));
if len < posArray.Count then posArray.Delete(0); end;
end;
{ TFruit }
constructor TFruit.Create; begin
Reset;
end;
procedure TFruit.Paint(Canvas: TCanvas); var
r: TRect;
begin
with Canvas do begin Brush.Color := rgb(200, 50, 20);
r := Rect(pos.x * block, pos.y * block, block, block);
FillRect(r); end;
end;
procedure TFruit.Reset; begin
fruitTime := true; pos := Tpoint.Create(0, 0);
end;
procedure TFruit.SetFruit; begin
pos.x := Trunc(Random(wid - 2) + 1); pos.y := Trunc(Random(hei - 2) + 1); fruitTime := false;
end;
procedure TSnakeApp.DoFrameStep(Sender: TObject); begin
Invalidate;
end;
procedure TSnakeApp.FormClose(Sender: TObject; var Action: TCloseAction); begin
FrameTimer.Free; snake.Free; Fruit.Free;
end;
procedure TSnakeApp.FormCreate(Sender: TObject); begin
Canvas.pen.Style := psClear; ClientHeight := block * hei; ClientWidth := block * wid; DoubleBuffered := True; KeyPreview := True;
OnClose := FormClose; OnKeyDown := FormKeyDown; OnPaint := FormPaint;
snake := TSnake.Create; Fruit := TFruit.Create(); FrameTimer := TTimer.Create(nil); FrameTimer.Interval := 250; FrameTimer.OnTimer := DoFrameStep; FrameTimer.Enabled := True;
end;
procedure TSnakeApp.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
function ValidDir(value: Byte): Byte; var combination: Byte; begin combination := (value or snake.dir); if (combination = 3) or (combination = 12) then Result := snake.dir else Result := value; end;
begin
case Key of VK_LEFT: snake.dir := ValidDir(l); VK_RIGHT: snake.dir := ValidDir(r); VK_UP: snake.dir := ValidDir(U); VK_DOWN: snake.dir := ValidDir(D); VK_ESCAPE: Reset; end;
end;
procedure TSnakeApp.FormPaint(Sender: TObject); var
i: Integer; r: TRect; frameR: Double;
begin
with Canvas do begin Brush.Color := rgb(0, $22, 0); FillRect(ClipRect); Brush.Color := rgb(20, 50, 120);
for i := 0 to wid - 1 do begin r := rect(i * block, 0, block, block); FillRect(r); r := rect(i * block, ClientHeight - block, block, block); FillRect(r); end;
for i := 1 to hei - 2 do begin r := Rect(1, i * block, block, block); FillRect(r); r := Rect(ClientWidth - block, i * block, block, block); FillRect(r); end;
if (Fruit.fruitTime) then begin Fruit.setFruit(); frameR := FrameTimer.Interval * 0.95; if frameR < 30 then frameR := 30; FrameTimer.Interval := trunc(frameR); end;
Fruit.Paint(Canvas); snake.update();
if not snake.alive then begin FrameTimer.Enabled := False; Application.ProcessMessages; ShowMessage('Game over'); Reset; exit; end;
if (snake.eat(Fruit.pos)) then Fruit.fruitTime := true; snake.Paint(Canvas);
Brush.Style := bsClear; Font.Color := rgb(200, 200, 200); Font.Size := 18; TextOut(50, 0, (snake.len - 1).ToString); end;
end;
procedure TSnakeApp.Reset; begin
snake.Reset; Fruit.Reset; FrameTimer.Interval := 250; FrameTimer.Enabled := True;
end; end.</lang> Form resources: <lang Delphi> object SnakeApp: TSnakeApp
OnCreate = FormCreate
end </lang>
Go
This uses the termbox package for terminal input and output. This makes the code fairly cross-platform, it successfully built for FreeBSD, OpenBSD, NetBSD, DragonFly BSD, Linux, MS Windows, and MacOS (tested on FreeBSD and MS Windows). <lang Go>package main
import ( "errors" "fmt" "log" "math/rand" "time"
termbox "github.com/nsf/termbox-go" )
func main() { rand.Seed(time.Now().UnixNano()) score, err := playSnake() if err != nil { log.Fatal(err) } fmt.Println("Final score:", score) }
type snake struct { body []position // tail to head positions of the snake heading direction width, height int cells []termbox.Cell }
type position struct { X int Y int }
type direction int
const ( North direction = iota East South West )
func (p position) next(d direction) position { switch d { case North: p.Y-- case East: p.X++ case South: p.Y++ case West: p.X-- } return p }
func playSnake() (int, error) { err := termbox.Init() if err != nil { return 0, err } defer termbox.Close()
termbox.Clear(fg, bg) termbox.HideCursor() s := &snake{ // It would be more efficient to use a circular // buffer instead of a plain slice for s.body. body: make([]position, 0, 32), cells: termbox.CellBuffer(), } s.width, s.height = termbox.Size() s.drawBorder() s.startSnake() s.placeFood() s.flush()
moveCh, errCh := s.startEventLoop() const delay = 125 * time.Millisecond for t := time.NewTimer(delay); ; t.Reset(delay) { var move direction select { case err = <-errCh: return len(s.body), err case move = <-moveCh: if !t.Stop() { <-t.C // handles race between moveCh and t.C } case <-t.C: move = s.heading } if s.doMove(move) { time.Sleep(1 * time.Second) break } }
return len(s.body), err }
func (s *snake) startEventLoop() (<-chan direction, <-chan error) { moveCh := make(chan direction) errCh := make(chan error, 1) go func() { defer close(errCh) for { switch ev := termbox.PollEvent(); ev.Type { case termbox.EventKey: switch ev.Ch { // WSAD and HJKL movement case 'w', 'W', 'k', 'K': moveCh <- North case 'a', 'A', 'h', 'H': moveCh <- West case 's', 'S', 'j', 'J': moveCh <- South case 'd', 'D', 'l', 'L': moveCh <- East case 0: switch ev.Key { // Cursor key movement case termbox.KeyArrowUp: moveCh <- North case termbox.KeyArrowDown: moveCh <- South case termbox.KeyArrowLeft: moveCh <- West case termbox.KeyArrowRight: moveCh <- East case termbox.KeyEsc: // Quit return } } case termbox.EventResize: // TODO errCh <- errors.New("terminal resizing unsupported") return case termbox.EventError: errCh <- ev.Err return case termbox.EventInterrupt: return } } }() return moveCh, errCh }
func (s *snake) flush() { termbox.Flush() s.cells = termbox.CellBuffer() }
func (s *snake) getCellRune(p position) rune { i := p.Y*s.width + p.X return s.cells[i].Ch } func (s *snake) setCell(p position, c termbox.Cell) { i := p.Y*s.width + p.X s.cells[i] = c }
func (s *snake) drawBorder() { for x := 0; x < s.width; x++ { s.setCell(position{x, 0}, border) s.setCell(position{x, s.height - 1}, border) } for y := 0; y < s.height-1; y++ { s.setCell(position{0, y}, border) s.setCell(position{s.width - 1, y}, border) } }
func (s *snake) placeFood() { for { // a random empty cell x := rand.Intn(s.width-2) + 1 y := rand.Intn(s.height-2) + 1 foodp := position{x, y} r := s.getCellRune(foodp) if r != ' ' { continue } s.setCell(foodp, food) return } }
func (s *snake) startSnake() { // a random cell somewhat near the center x := rand.Intn(s.width/2) + s.width/4 y := rand.Intn(s.height/2) + s.height/4 head := position{x, y} s.setCell(head, snakeHead) s.body = append(s.body[:0], head) s.heading = direction(rand.Intn(4)) }
func (s *snake) doMove(move direction) bool { head := s.body[len(s.body)-1] s.setCell(head, snakeBody) head = head.next(move) s.heading = move s.body = append(s.body, head) r := s.getCellRune(head) s.setCell(head, snakeHead) gameOver := false switch r { case food.Ch: s.placeFood() case border.Ch, snakeBody.Ch: gameOver = true fallthrough case empty.Ch: s.setCell(s.body[0], empty) s.body = s.body[1:] default: panic(r) } s.flush() return gameOver }
const ( fg = termbox.ColorWhite bg = termbox.ColorBlack )
// Symbols to use. // Could use Unicode instead of simple ASCII. var ( empty = termbox.Cell{Ch: ' ', Bg: bg, Fg: fg} border = termbox.Cell{Ch: '+', Bg: bg, Fg: termbox.ColorBlue} snakeBody = termbox.Cell{Ch: '#', Bg: bg, Fg: termbox.ColorGreen} snakeHead = termbox.Cell{Ch: 'O', Bg: bg, Fg: termbox.ColorYellow | termbox.AttrBold} food = termbox.Cell{Ch: '@', Bg: bg, Fg: termbox.ColorRed} )</lang>
Haskell
<lang haskell>{-# LANGUAGE TemplateHaskell #-} import Control.Monad.Random (getRandomRs) import Graphics.Gloss.Interface.Pure.Game import Lens.Micro ((%~), (^.), (&), set) import Lens.Micro.TH (makeLenses)
-- all data types
data Snake = Snake { _body :: [Point], _direction :: Point } makeLenses Snake
data World = World { _snake :: Snake , _food :: [Point]
, _score :: Int , _maxScore :: Int }
makeLenses World
-- everything snake can do
moves (Snake b d) = Snake (step b d : init b) d eats (Snake b d) = Snake (step b d : b) d bites (Snake b _) = any (== head b) step ((x,y):_) (a,b) = (x+a, y+b)
turn (x',y') (Snake b (x,y)) | (x+x',y+y') == (0,0) = Snake b (x,y)
| otherwise = Snake b (x',y')
-- all randomness
createWorld = do xs <- map fromIntegral <$> getRandomRs (2, 38 :: Int)
ys <- map fromIntegral <$> getRandomRs (2, 38 :: Int) return (Ok, World snake (zip xs ys) 0 0) where snake = Snake [(20, 20)] (1,0)
-- A tyny DSL for declarative description of business logic
data Status = Ok | Fail | Stop
continue = \x -> (Ok, x) stop = \x -> (Stop, x) f >>> g = \x -> case f x of { (Ok, y) -> g y; b -> b } -- chain composition f <|> g = \x -> case f x of { (Fail, _) -> g x; b -> b } -- alternative p ==> f = \x -> if p x then f x else (Fail, x) -- condition l .& f = continue . (l %~ f) -- modification l .= y = continue . set l y -- setting
-- all business logic
updateWorld _ = id >>> (snakeEats <|> snakeMoves)
where snakeEats = (snakeFindsFood ==> (snake .& eats)) >>> (score .& (+1)) >>> (food .& tail)
snakeMoves = (snakeBitesTail ==> stop) <|> (snakeHitsWall ==> stop) <|> (snake .& moves)
snakeFindsFood w = (w^.snake & moves) `bites` (w^.food & take 1) snakeBitesTail w = (w^.snake) `bites` (w^.snake.body & tail) snakeHitsWall w = (w^.snake.body) & head & isOutside isOutside (x,y) = or [x <= 0, 40 <= x, y <= 0, 40 <= y]
-- all event handing
handleEvents e (s,w) = f w
where f = case s of Ok -> case e of EventKey (SpecialKey k) _ _ _ -> case k of KeyRight -> snake .& turn (1,0) KeyLeft -> snake .& turn (-1,0) KeyUp -> snake .& turn (0,1) KeyDown -> snake .& turn (0,-1) _-> continue _-> continue _-> \w -> w & ((snake.body) .= [(20,20)]) >>> (maxScore .& max (w^.score)) >>> (score .= 0)
-- all graphics
renderWorld (s, w) = pictures [frame, color c drawSnake, drawFood, showScore]
where c = case s of { Ok -> orange; _-> red } drawSnake = foldMap (rectangleSolid 10 10 `at`) (w^.snake.body) drawFood = color blue $ circleSolid 5 `at` (w^.food & head) frame = color black $ rectangleWire 400 400 showScore = color orange $ scale 0.2 0.2 $ txt `at` (-80,130) txt = Text $ mconcat ["Score: ", w^.score & show ," Maximal score: ", w^.maxScore & show] at p (x,y) = Translate (10*x-200) (10*y-200) p
main = do world <- createWorld
play inW white 7 world renderWorld handleEvents updateWorld where inW = InWindow "The Snake" (400, 400) (10, 10)</lang>
Extra credit
It is easy to make snake to seek food automatically. Just change the first line of the updateWorld
definition:
<lang haskell>updateWorld _ = id >>> snakeSeeksFood >>> (snakeEats <|> snakeMoves) </lang>
and add local definition:
<lang haskell> snakeSeeksFood w = w & snake .& turns optimalDirection
where optimalDirection = minimumBy (comparing distanceToFood) safeTurns safeTurns = filter safe [(x,y),(-y,x),(y,-x)] `ifEmpty` [(x,y)] where (x,y) = w^.snake.direction safe d = let w = w & snake %~ moves . turns d in not (snakeBitesTail w || snakeHitsWall w) lst `ifEmpty` x = if null lst then x else lst distanceToFood d = let (a,b) = w^.snake & turns d & moves & (^.body) & head (x,y) = w^.food & head in (a-x)^2 + (b-y)^2</lang>
Java
See Snake/Java.
JavaScript
You need the P5 Library to run this code! <lang javascript> const L = 1, R = 2, D = 4, U = 8; var block = 24, wid = 30, hei = 20, frameR = 7, fruit, snake; function Snake() {
this.length = 1; this.alive = true; this.pos = createVector( 1, 1 ); this.posArray = []; this.posArray.push( createVector( 1, 1 ) ); this.dir = R; this.draw = function() { fill( 130, 190, 0 ); var pos, i = this.posArray.length - 1, l = this.length; while( true ){ pos = this.posArray[i--]; rect( pos.x * block, pos.y * block, block, block ); if( --l == 0 ) break; } } this.eat = function( frut ) { var b = this.pos.x == frut.x && this.pos.y == frut.y; if( b ) this.length++; return b; } this.overlap = function() { var len = this.posArray.length - 1; for( var i = len; i > len - this.length; i-- ) { tp = this.posArray[i]; if( tp.x === this.pos.x && tp.y === this.pos.y ) return true; } return false; } this.update = function() { if( !this.alive ) return; switch( this.dir ) { case L: this.pos.x--; if( this.pos.x < 1 ) this.pos.x = wid - 2; break; case R: this.pos.x++; if( this.pos.x > wid - 2 ) this.pos.x = 1; break; case U: this.pos.y--; if( this.pos.y < 1 ) this.pos.y = hei - 2; break; case D: this.pos.y++; if( this.pos.y > hei - 2 ) this.pos.y = 1; break; } if( this.overlap() ) { this.alive = false; } else { this.posArray.push( createVector( this.pos.x, this.pos.y ) ); if( this.posArray.length > 5000 ) { this.posArray.splice( 0, 1 ); } } }
} function Fruit() {
this.fruitTime = true; this.pos = createVector(); this.draw = function() { fill( 200, 50, 20 ); rect( this.pos.x * block, this.pos.y * block, block, block ); }
this.setFruit = function() { this.pos.x = floor( random( 1, wid - 1 ) ); this.pos.y = floor( random( 1, hei - 1 ) ); this.fruitTime = false; }
} function setup() {
createCanvas( block * wid, block * hei ); noStroke(); frameRate( frameR ); snake = new Snake();fruit = new Fruit();
} function keyPressed() {
switch( keyCode ) { case LEFT_ARROW: snake.dir = L; break; case RIGHT_ARROW: snake.dir = R; break; case UP_ARROW: snake.dir = U; break; case DOWN_ARROW: snake.dir = D; }
} function draw() {
background( color( 0, 0x22, 0 ) ); fill( 20, 50, 120 ); for( var i = 0; i < wid; i++ ) { rect( i * block, 0, block, block ); rect( i * block, height - block, block, block ); } for( var i = 1; i < hei - 1; i++ ) { rect( 1, i * block, block, block ); rect( width - block, i * block, block, block ); } if( fruit.fruitTime ) { fruit.setFruit(); frameR += .2; frameRate( frameR ); } fruit.draw(); snake.update(); if( snake.eat( fruit.pos ) ) { fruit.fruitTime = true; } snake.draw(); fill( 200 ); textStyle( BOLD ); textAlign( RIGHT ); textSize( 120 ); text( ""+( snake.length - 1 ), 690, 440 ); if( !snake.alive ) text( "THE END", 630, 250 );
} </lang>
Julia
Makie version in 99 lines. <lang julia>using Makie
mutable struct SnakeGame
height width snake food
end
function SnakeGame(;height=6, width=8)
snake = [rand(CartesianIndices((height, width)))] food = rand(CartesianIndices((height, width))) while food == snake[1] food = rand(CartesianIndices((height, width))) end SnakeGame(height, width, snake, food)
end
function step!(game, direction)
next_head = game.snake[1] + direction next_head = CartesianIndex(mod.(next_head.I, Base.OneTo.((game.height, game.width)))) # allow crossing boundry if is_valid(game, next_head) pushfirst!(game.snake, next_head) if next_head == game.food length(game.snake) < game.height * game.width && init_food!(game) else pop!(game.snake) end true else false end
end
is_valid(game, position) = position ∉ game.snake
function init_food!(game)
p = rand(CartesianIndices((game.height, game.width))) while !is_valid(game, p) p = rand(CartesianIndices((game.height, game.width))) end game.food = p
end
function play(;n=10,t=0.5)
game = Node(SnakeGame(;width=n,height=n)) scene = Scene(resolution = (1000, 1000), raw = true, camera = campixel!) display(scene)
area = scene.px_area poly!(scene, area)
grid_size = @lift((widths($area)[1] / $game.height, widths($area)[2] / $game.width))
snake_boxes = @lift([FRect2D((p.I .- (1,1)) .* $grid_size , $grid_size) for p in $game.snake]) poly!(scene, snake_boxes, color=:blue, strokewidth = 5, strokecolor = :black)
snake_head_box = @lift(FRect2D(($game.snake[1].I .- (1,1)) .* $grid_size , $grid_size)) poly!(scene, snake_head_box, color=:black) snake_head = @lift((($game.snake[1].I .- 0.5) .* $grid_size)) scatter!(scene, snake_head, marker='◉', color=:blue, markersize=@lift(minimum($grid_size)))
food_position = @lift(($game.food.I .- (0.5,0.5)) .* $grid_size) scatter!(scene, food_position, color=:red, marker='♥', markersize=@lift(minimum($grid_size)))
score_text = @lift("Score: $(length($game.snake)-1)") text!(scene, score_text, color=:gray, position = @lift((widths($area)[1]/2, widths($area)[2])), textsize = 50, align = (:center, :top))
direction = Ref{Any}(nothing)
on(scene.events.keyboardbuttons) do but if ispressed(but, Keyboard.left) direction[] = CartesianIndex(-1,0) elseif ispressed(but, Keyboard.up) direction[] = CartesianIndex(0,1) elseif ispressed(but, Keyboard.down) direction[] = CartesianIndex(0,-1) elseif ispressed(but, Keyboard.right) direction[] = CartesianIndex(1,0) end end
last_dir = nothing while true # avoid turn back if !isnothing(direction[]) && (isnothing(last_dir) || direction[] != -last_dir) last_dir = direction[] end if !isnothing(last_dir) if step!(game[], last_dir) game[] = game[] else break end end sleep(t) end
end
play() </lang>
Kotlin
<lang scala>// Kotlin Native v0.5
import kotlinx.cinterop.* import platform.posix.* import platform.windows.*
const val WID = 60 const val HEI = 30 const val MAX_LEN = 600 const val NUL = '\u0000'
enum class Dir { NORTH, EAST, SOUTH, WEST }
class Snake {
val console: HANDLE var alive = false val brd = CharArray(WID * HEI) var dir = Dir.NORTH val snk = nativeHeap.allocArray<COORD>(MAX_LEN) lateinit var head: COORD var tailIdx = 0 var headIdx = 0 var points = 0
init { console = GetStdHandle(STD_OUTPUT_HANDLE)!! SetConsoleTitleW("Snake") memScoped { val coord = alloc<COORD>().apply { X = (WID + 1).toShort(); Y = (HEI + 2).toShort() } SetConsoleScreenBufferSize(console, coord.readValue()) val rc = alloc<SMALL_RECT>().apply { Left = 0; Top = 0; Right = WID.toShort(); Bottom = (HEI + 1).toShort() } SetConsoleWindowInfo(console, TRUE, rc.ptr) val ci = alloc<CONSOLE_CURSOR_INFO>().apply { dwSize = 1; bVisible = FALSE } SetConsoleCursorInfo(console, ci.ptr) } }
fun play() { while (true) { createfield() alive = true while (alive) { drawfield() readKey() moveSnake() Sleep(50) } memScoped { val c = alloc<COORD>().apply { X = 0; Y = (HEI + 1).toShort() } SetConsoleCursorPosition(console, c.readValue()) } SetConsoleTextAttribute(console, 0x000b) print("Play again [Y/N]? ") val a = readLine()!!.toLowerCase() if (a.length > 0 && a[0] != 'y') { nativeHeap.free(snk) return } } }
private fun createfield() { memScoped { val coord = alloc<COORD>().apply { X = 0; Y = 0 } val c = alloc<DWORDVar>() FillConsoleOutputCharacterW(console, 32, (HEI + 2) * 80, coord.readValue(), c.ptr) FillConsoleOutputAttribute(console, 0x0000, (HEI + 2) * 80, coord.readValue(), c.ptr) SetConsoleCursorPosition(console, coord.readValue()) } for (x in 0 until WID * HEI) brd[x] = NUL for (x in 0 until WID) { brd[x + WID * (HEI - 1)] = '+' brd[x] = '+' } for (y in 1 until HEI) { brd[WID - 1 + WID * y] = '+' brd[WID * y] = '+' } var xx: Int var yy: Int do { xx = rand() % WID yy = rand() % (HEI shr 1) + (HEI shr 1) } while (brd[xx + WID * yy] != NUL) brd[xx + WID * yy] = '@' tailIdx = 0 headIdx = 4 xx = 3 yy = 2 for (cc in tailIdx until headIdx) { brd[xx + WID * yy] = '#' snk[cc].X = (3 + cc).toShort() snk[cc].Y = 2 } head = snk[3] dir = Dir.EAST points = 0 }
private fun readKey() { if ((GetAsyncKeyState(39).toInt() and 0x8000) != 0) dir = Dir.EAST if ((GetAsyncKeyState(37).toInt() and 0x8000) != 0) dir = Dir.WEST if ((GetAsyncKeyState(38).toInt() and 0x8000) != 0) dir = Dir.NORTH if ((GetAsyncKeyState(40).toInt() and 0x8000) != 0) dir = Dir.SOUTH }
private fun drawfield() { memScoped { val coord = alloc<COORD>() var t = NUL for (y in 0 until HEI) { coord.Y = y.toShort() for (x in 0 until WID) { t = brd[x + WID * y] if (t == NUL) continue coord.X = x.toShort() SetConsoleCursorPosition(console, coord.readValue()) if (coord.X == head.X && coord.Y == head.Y) { SetConsoleTextAttribute(console, 0x002e) print('O') SetConsoleTextAttribute(console, 0x0000) continue } when (t) { '#' -> SetConsoleTextAttribute(console, 0x002a) '+' -> SetConsoleTextAttribute(console, 0x0019) '@' -> SetConsoleTextAttribute(console, 0x004c) } print(t) SetConsoleTextAttribute(console, 0x0000) } } print(t) SetConsoleTextAttribute(console, 0x0007) val c = alloc<COORD>().apply { X = 0; Y = HEI.toShort() } SetConsoleCursorPosition(console, c.readValue()) print("Points: $points") } }
private fun moveSnake() { when (dir) { Dir.NORTH -> head.Y-- Dir.EAST -> head.X++ Dir.SOUTH -> head.Y++ Dir.WEST -> head.X-- } val t = brd[head.X + WID * head.Y] if (t != NUL && t != '@') { alive = false return } brd[head.X + WID * head.Y] = '#' snk[headIdx].X = head.X snk[headIdx].Y = head.Y if (++headIdx >= MAX_LEN) headIdx = 0 if (t == '@') { points++ var x: Int var y: Int do { x = rand() % WID y = rand() % (HEI shr 1) + (HEI shr 1) } while (brd[x + WID * y] != NUL) brd[x + WID * y] = '@' return } SetConsoleCursorPosition(console, snk[tailIdx].readValue()) print(' ') brd[snk[tailIdx].X + WID * snk[tailIdx].Y] = NUL if (++tailIdx >= MAX_LEN) tailIdx = 0 }
}
fun main(args: Array<String>) {
srand(time(null).toInt()) Snake().play()
}</lang>
- Output:
Similar to C++ entry
OCaml
<lang ocaml>(* A simple Snake Game *) open Sdl
let width, height = (640, 480)
type pos = int * int
type game_state = {
pos_snake: pos; seg_snake: pos list; dir_snake: [`left | `right | `up | `down]; pos_fruit: pos; sleep_time: int; game_over: bool;
}
let red = (255, 0, 0) let blue = (0, 0, 255) let green = (0, 255, 0) let black = (0, 0, 0) let alpha = 255
let fill_rect renderer (x, y) =
let rect = Rect.make4 x y 20 20 in Render.fill_rect renderer rect;
let display_game renderer state =
let bg_color, snake_color, fruit_color = if state.game_over then (red, black, green) else (black, blue, red) in Render.set_draw_color renderer bg_color alpha; Render.clear renderer; Render.set_draw_color renderer fruit_color alpha; fill_rect renderer state.pos_fruit; Render.set_draw_color renderer snake_color alpha; List.iter (fill_rect renderer) state.seg_snake; Render.render_present renderer;
let proc_events dir_snake = function
| Event.KeyDown { Event.keycode = Keycode.Left } -> `left | Event.KeyDown { Event.keycode = Keycode.Right } -> `right | Event.KeyDown { Event.keycode = Keycode.Up } -> `up | Event.KeyDown { Event.keycode = Keycode.Down } -> `down | Event.KeyDown { Event.keycode = Keycode.Q } | Event.KeyDown { Event.keycode = Keycode.Escape } | Event.Quit _ -> Sdl.quit (); exit 0 | _ -> (dir_snake)
let rec event_loop dir_snake =
match Event.poll_event () with | None -> (dir_snake) | Some ev -> let dir = proc_events dir_snake ev in event_loop dir
let rec pop = function
| [_] -> [] | hd :: tl -> hd :: (pop tl) | [] -> invalid_arg "pop"
let rec new_pos_fruit seg_snake =
let new_pos = (20 * Random.int 32, 20 * Random.int 24) in if List.mem new_pos seg_snake then new_pos_fruit seg_snake else (new_pos)
let update_state req_dir ({
pos_snake; seg_snake; pos_fruit; dir_snake; sleep_time; game_over; } as state) = if game_over then state else let dir_snake = match dir_snake, req_dir with | `left, `right -> dir_snake | `right, `left -> dir_snake | `up, `down -> dir_snake | `down, `up -> dir_snake | _ -> req_dir in let pos_snake = let x, y = pos_snake in match dir_snake with | `left -> (x - 20, y) | `right -> (x + 20, y) | `up -> (x, y - 20) | `down -> (x, y + 20) in let game_over = let x, y = pos_snake in List.mem pos_snake seg_snake || x < 0 || y < 0 || x >= width || y >= height in let seg_snake = pos_snake :: seg_snake in let seg_snake, pos_fruit, sleep_time = if pos_snake = pos_fruit then (seg_snake, new_pos_fruit seg_snake, sleep_time - 1) else (pop seg_snake, pos_fruit, sleep_time) in { pos_snake; seg_snake; pos_fruit; dir_snake; sleep_time; game_over; }
let () =
Random.self_init (); Sdl.init [`VIDEO]; let window, renderer = Render.create_window_and_renderer ~width ~height ~flags:[] in Window.set_title ~window ~title:"Snake OCaml-SDL2"; let initial_state = { pos_snake = (100, 100); seg_snake = [ (100, 100); ( 80, 100); ( 60, 100); ]; pos_fruit = (200, 200); dir_snake = `right; sleep_time = 120; game_over = false; } in
let rec main_loop state = let req_dir = event_loop state.dir_snake in let state = update_state req_dir state in display_game renderer state; Timer.delay state.sleep_time; main_loop state in main_loop initial_state</lang>
Perl
<lang perl>use utf8; use Time::HiRes qw(sleep); use Term::ANSIColor qw(colored); use Term::ReadKey qw(ReadMode ReadLine);
binmode(STDOUT, ':utf8');
use constant {
VOID => 0, HEAD => 1, BODY => 2, TAIL => 3, FOOD => 4, };
use constant {
LEFT => [+0, -1], RIGHT => [+0, +1], UP => [-1, +0], DOWN => [+1, +0], };
use constant {
BG_COLOR => "on_black", SLEEP_SEC => 0.05, };
use constant {
SNAKE_COLOR => ('bold green' . ' ' . BG_COLOR), FOOD_COLOR => ('red' . ' ' . BG_COLOR), };
use constant {
U_HEAD => colored('▲', SNAKE_COLOR), D_HEAD => colored('▼', SNAKE_COLOR), L_HEAD => colored('◀', SNAKE_COLOR), R_HEAD => colored('▶', SNAKE_COLOR),
U_BODY => colored('╹', SNAKE_COLOR), D_BODY => colored('╻', SNAKE_COLOR), L_BODY => colored('╴', SNAKE_COLOR), R_BODY => colored('╶', SNAKE_COLOR),
U_TAIL => colored('╽', SNAKE_COLOR), D_TAIL => colored('╿', SNAKE_COLOR), L_TAIL => colored('╼', SNAKE_COLOR), R_TAIL => colored('╾', SNAKE_COLOR),
A_VOID => colored(' ', BG_COLOR), A_FOOD => colored('❇', FOOD_COLOR), };
local $| = 1;
my $w = eval { `tput cols` } || 80; my $h = eval { `tput lines` } || 24; my $r = "\033[H";
my @grid = map {
[map { [VOID] } 1 .. $w]
} 1 .. $h;
my $dir = LEFT; my @head_pos = ($h / 2, $w / 2); my @tail_pos = ($head_pos[0], $head_pos[1] + 1);
$grid[$head_pos[0]][$head_pos[1]] = [HEAD, $dir]; # head $grid[$tail_pos[0]][$tail_pos[1]] = [TAIL, $dir]; # tail
sub create_food {
my ($food_x, $food_y);
do { $food_x = rand($w); $food_y = rand($h); } while ($grid[$food_y][$food_x][0] != VOID);
$grid[$food_y][$food_x][0] = FOOD;
}
create_food();
sub display {
my $i = 0;
print $r, join("\n", map { join("", map { my $t = $_->[0]; if ($t != FOOD and $t != VOID) { my $p = $_->[1]; $i = $p eq UP ? 0 : $p eq DOWN ? 1 : $p eq LEFT ? 2 : 3; } $t == HEAD ? (U_HEAD, D_HEAD, L_HEAD, R_HEAD)[$i] : $t == BODY ? (U_BODY, D_BODY, L_BODY, R_BODY)[$i] : $t == TAIL ? (U_TAIL, D_TAIL, L_TAIL, R_TAIL)[$i] : $t == FOOD ? (A_FOOD) : (A_VOID);
} @{$_} ) } @grid );
}
sub move {
my $grew = 0;
# Move the head { my ($y, $x) = @head_pos;
my $new_y = ($y + $dir->[0]) % $h; my $new_x = ($x + $dir->[1]) % $w;
my $cell = $grid[$new_y][$new_x]; my $t = $cell->[0];
if ($t == BODY or $t == TAIL) { die "Game over!\n"; } elsif ($t == FOOD) { create_food(); $grew = 1; }
# Create a new head $grid[$new_y][$new_x] = [HEAD, $dir];
# Replace the current head with body $grid[$y][$x] = [BODY, $dir];
# Save the position of the head @head_pos = ($new_y, $new_x); }
# Move the tail if (not $grew) { my ($y, $x) = @tail_pos;
my $pos = $grid[$y][$x][1]; my $new_y = ($y + $pos->[0]) % $h; my $new_x = ($x + $pos->[1]) % $w;
$grid[$y][$x][0] = VOID; # erase the current tail $grid[$new_y][$new_x][0] = TAIL; # create a new tail
# Save the position of the tail @tail_pos = ($new_y, $new_x); }
}
ReadMode(3); while (1) {
my $key; until (defined($key = ReadLine(-1))) { move(); display(); sleep(SLEEP_SEC); }
if ($key eq "\e[A" and $dir ne DOWN ) { $dir = UP } elsif ($key eq "\e[B" and $dir ne UP ) { $dir = DOWN } elsif ($key eq "\e[C" and $dir ne LEFT ) { $dir = RIGHT } elsif ($key eq "\e[D" and $dir ne RIGHT) { $dir = LEFT }
}</lang>
Phix
<lang Phix>constant W = 60, H = 30, MAX_LEN = 600 enum NORTH, EAST, SOUTH, WEST
sequence board, snake bool alive integer tailIdx, headIdx, hdX, hdY, d, points
procedure createField()
clear_screen() board = repeat("+"&repeat(' ',W-2)&'+',H) for x=1 to W do board[1,x] = '+' end for board[H] = board[1] board[1+rand(H-2),1+rand(W-2)] = '@'; snake = repeat(0,MAX_LEN) board[3,4] = '#'; tailIdx = 1; headIdx = 5; for c=tailIdx to headIdx do snake[c] = {3,3+c} end for {hdY,hdX} = snake[headIdx-1]; d = EAST; points = 0;
end procedure
procedure drawField()
for y=1 to H do for x=1 to W do integer t = board[y,x] if t!=' ' then position(y,x) if x=hdX and y=hdY then text_color(14); puts(1,'O'); else text_color({10,9,12}[find(t,"#+@")]); puts(1,t); end if end if end for end for position(H+1,1); text_color(7); printf(1,"Points: %d",points)
end procedure
procedure readKey()
integer k = find(get_key(),{333,331,328,336}) if k then d = {EAST,WEST,NORTH,SOUTH}[k] end if
end procedure
procedure moveSnake() integer x,y
switch d do case NORTH: hdY -= 1 case EAST: hdX += 1 case SOUTH: hdY += 1 case WEST: hdX -= 1 end switch integer t = board[hdY,hdX]; if t!=' ' and t!='@' then alive = false; return; end if board[hdY,hdX] = '#'; snake[headIdx] = {hdY,hdX}; headIdx += 1; if headIdx>MAX_LEN then headIdx = 1 end if if t=='@' then points += 1 while 1 do x = 1+rand(W-2); y = 1+rand(H-2); if board[y,x]=' ' then board[y,x] = '@' return end if end while end if {y,x} = snake[tailIdx]; position(y,x); puts(1,' '); board[y,x] = ' '; tailIdx += 1; if tailIdx>MAX_LEN then tailIdx = 1 end if
end procedure
procedure play()
while true do createField(); alive = true; cursor(NO_CURSOR) while alive do drawField(); readKey(); moveSnake(); sleep(0.05) end while cursor(BLOCK_CURSOR); position(H+2,1); bk_color(0); text_color(11); puts(1,"Play again [Y/N]? ") if upper(wait_key())!='Y' then return end if end while
end procedure play()</lang>
Raku
(formerly Perl 6)
This is a variation of a demo script included in the examples folder for the Raku SDL2::Raw library bindings.
<lang perl6>use SDL2::Raw; use Cairo;
constant W = 1280; constant H = 960;
constant FIELDW = W div 32; constant FIELDH = H div 32;
SDL_Init(VIDEO);
my $window = SDL_CreateWindow(
'Snake', SDL_WINDOWPOS_CENTERED_MASK, SDL_WINDOWPOS_CENTERED_MASK, W, H, OPENGL
);
my $render = SDL_CreateRenderer($window, -1, ACCELERATED +| PRESENTVSYNC);
my $snake_image = Cairo::Image.record(
-> $_ { .save; .rectangle: 0, 0, 64, 64; .clip; .rgb: 0, 1, 0; .rectangle: 0, 0, 64, 64; .fill :preserve; .rgb: 0, 0, 0; .stroke; .restore;
.save; .translate: 64, 0; .rectangle: 0, 0, 64, 64; .clip; .rgb: 1, 0, 0; .arc: 32, 32, 30, 0, 2 * pi; .fill :preserve; .rgb: 0, 0, 0; .stroke; .restore; }, 128, 128, Cairo::FORMAT_ARGB32);
my $snake_texture = SDL_CreateTexture(
$render, %PIXELFORMAT<ARGB8888>, STATIC, 128, 128
);
SDL_UpdateTexture(
$snake_texture, SDL_Rect.new( :x(0), :y(0), :w(128), :h(128) ), $snake_image.data, $snake_image.stride // 128 * 4
);
SDL_SetTextureBlendMode($snake_texture, 1);
SDL_SetRenderDrawBlendMode($render, 1);
my $snakepiece_srcrect = SDL_Rect.new(:w(64), :h(64)); my $nompiece_srcrect = SDL_Rect.new(:w(64), :h(64), :x(64));
my $event = SDL_Event.new;
enum GAME_KEYS (
K_UP => 82, K_DOWN => 81, K_LEFT => 80, K_RIGHT => 79,
);
my Complex @snakepieces = 10+10i; my Complex @noms; my Complex $snakedir = 1+0i; my $nomspawn = 0; my $snakespeed = 0.1; my $snakestep = 0; my $nom = 4;
my $last_frame_start = now; main: loop {
my $start = now; my $dt = $start - $last_frame_start // 0.00001; while SDL_PollEvent($event) { my $casted_event = SDL_CastEvent($event); given $casted_event { when *.type == QUIT { last main } when *.type == KEYDOWN { if GAME_KEYS(.scancode) -> $comm { given $comm { when 'K_LEFT' { $snakedir = -1+0i unless $snakedir == 1+0i } when 'K_RIGHT' { $snakedir = 1+0i unless $snakedir == -1+0i } when 'K_UP' { $snakedir = 0-1i unless $snakedir == 0+1i } when 'K_DOWN' { $snakedir = 0+1i unless $snakedir == 0-1i } } } } } }
if ($nomspawn -= $dt) < 0 { $nomspawn += 1; @noms.push: (^FIELDW).pick + (^FIELDH).pick * i unless @noms > 3; @noms.pop if @noms[*-1] == any(@snakepieces); }
if ($snakestep -= $dt) < 0 { $snakestep += $snakespeed;
@snakepieces.unshift: do given @snakepieces[0] { ($_.re + $snakedir.re) % FIELDW + (($_.im + $snakedir.im) % FIELDH) * i }
if @snakepieces[2..*].first( * == @snakepieces[0], :k ) -> $idx { @snakepieces = @snakepieces[0..($idx + 1)]; }
@noms .= grep( { $^piece == @snakepieces[0] ?? ($nom += 1) && False !! True } );
if $nom == 0 { @snakepieces.pop; } else { $nom = $nom - 1; } }
for @snakepieces { SDL_SetTextureColorMod( $snake_texture, 255, (cos((++$) / 2) * 100 + 155).round, 255 );
SDL_RenderCopy( $render, $snake_texture, $snakepiece_srcrect, SDL_Rect.new(.re * 32, .im * 32, 32, 32) ); }
SDL_SetTextureColorMod($snake_texture, 255, 255, 255);
for @noms { SDL_RenderCopy( $render, $snake_texture, $nompiece_srcrect, SDL_Rect.new(.re * 32, .im * 32, 32, 32) ) }
SDL_RenderPresent($render); SDL_SetRenderDrawColor($render, 0, 0, 0, 0); SDL_RenderClear($render);
$last_frame_start = $start; sleep(1 / 50);
}
SDL_Quit();</lang>
Sidef
<lang ruby>class SnakeGame(w, h) {
const readkey = frequire('Term::ReadKey') const ansi = frequire('Term::ANSIColor')
enum (VOID, HEAD, BODY, TAIL, FOOD)
define ( LEFT = [+0, -1], RIGHT = [+0, +1], UP = [-1, +0], DOWN = [+1, +0], )
define BG_COLOR = "on_black" define FOOD_COLOR = ("red" + " " + BG_COLOR) define SNAKE_COLOR = ("bold green" + " " + BG_COLOR) define SLEEP_SEC = 0.02
const ( A_VOID = ansi.colored(' ', BG_COLOR), A_FOOD = ansi.colored('❇', FOOD_COLOR), A_BLOCK = ansi.colored('■', SNAKE_COLOR), )
has dir = LEFT has grid = [[]] has head_pos = [0, 0] has tail_pos = [0, 0]
method init { grid = h.of { w.of { [VOID] } }
head_pos = [h//2, w//2] tail_pos = [head_pos[0], head_pos[1]+1]
grid[head_pos[0]][head_pos[1]] = [HEAD, dir] # head grid[tail_pos[0]][tail_pos[1]] = [TAIL, dir] # tail
self.make_food() }
method make_food { var (food_x, food_y)
do { food_x = w.rand.int food_y = h.rand.int } while (grid[food_y][food_x][0] != VOID)
grid[food_y][food_x][0] = FOOD }
method display { print("\033[H", grid.map { |row| row.map { |cell| given (cell[0]) { when (VOID) { A_VOID } when (FOOD) { A_FOOD } default { A_BLOCK } } }.join() }.join("\n") ) }
method move { var grew = false
# Move the head var (y, x) = head_pos...
var new_y = (y+dir[0] % h) var new_x = (x+dir[1] % w)
var cell = grid[new_y][new_x]
given (cell[0]) { when (BODY) { die "\nYou just bit your own body!\n" } when (TAIL) { die "\nYou just bit your own tail!\n" } when (FOOD) { grew = true; self.make_food() } }
# Create a new head grid[new_y][new_x] = [HEAD, dir]
# Replace the current head with body grid[y][x] = [BODY, dir]
# Update the head position head_pos = [new_y, new_x]
# Move the tail if (!grew) { var (y, x) = tail_pos...
var pos = grid[y][x][1] var new_y = (y+pos[0] % h) var new_x = (x+pos[1] % w)
grid[y][x][0] = VOID # erase the current tail grid[new_y][new_x][0] = TAIL # create a new tail
tail_pos = [new_y, new_x] } }
method play { STDOUT.autoflush(true) readkey.ReadMode(3)
try { loop { var key while (!defined(key = readkey.ReadLine(-1))) { self.move() self.display() Sys.sleep(SLEEP_SEC) }
given (key) { when ("\e[A") { if (dir != DOWN ) { dir = UP } } when ("\e[B") { if (dir != UP ) { dir = DOWN } } when ("\e[C") { if (dir != LEFT ) { dir = RIGHT } } when ("\e[D") { if (dir != RIGHT) { dir = LEFT } } } } } catch { readkey.ReadMode(0) } }
}
var w = `tput cols`.to_i var h = `tput lines`.to_i
SnakeGame(w || 80, h || 24).play</lang>