«snake» by eli.fieldsteel

on 28 Apr'21 06:35 in gamesnakearcade

a SuperCollider recreation of the the arcade game "snake"

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
(
//cmd-enter to run, esc to close

var height=20, width=30,
body = (1..6),
size = body.size,
grow_inc = 2, //get 1 food = +2 size
framerate = 16,
dir = \east,

//keycodes for left/right/down/up
keycodes = Platform.case(
	\osx, {[123,124,125,126]},
	\linux, {[65361,65363,65364,65362]},
	\windows, {[37,39,40,38]}
);

//snake + board are represented as 2D array,
//0 = empty space
//positive integers = snake body, value represents
//number of frames until snake segment disappears
//e.g. 6-segment snake heading east looks like:
// [0, 0, 0, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0]
var board = (0!(height*width)).clump(width);

//current position of snake "head"
var pos = round(height/2) @ (size + 1);

//amount/direction to shift on each frame
var shift = 0 @ 1;

//for creating snake food
var foodpos, foodcolor, tryfoodpos;

//gui
var win, u, deadbox;

//functions that handle snake crash and food generation
var crash, makefood;

Window.closeAll;

makefood = {

	//colorful snacks
	foodcolor = Color.rand(0.25,1.0);
	foodpos = nil;

	//make sure food isn't placed on snake body
	while(
		{foodpos == nil},
		{
			tryfoodpos = rand(height) @ rand(width);
			if(
				//food generation only successful if blank space is chosen
				board[tryfoodpos.x][tryfoodpos.y] == 0,
				{foodpos = tryfoodpos.copy}
			);
		}
	);
};

win = Window.new("snake", Rect(
	Window.screenBounds.width/2-(width*10),
	Window.screenBounds.height/2-(height*10),
	width*20,height*20
), false, false)
.alwaysOnTop_(true);

u = UserView(win, win.view.bounds)
.background_(Color.gray(0.1));

//game over & score box, only visible when you crash
//(score = snake body size)
deadbox = StaticText(win, Rect(
	win.bounds.width/2 - 50,
	win.bounds.height/2 - 20,
	100, 40
))
.visible_(false)
.background_(Color.red(0.7))
.stringColor_(Color.gray(0.8))
.align_(\center);

//when crash, stop animation and display game over & score
crash = {
	u.animate_(false);
	deadbox
	.string_("you died\nscore: "++size)
	.visible_(true);
};

//place snake body integers on 2D array
body.do({
	arg n,i;
	board[round(height/2)].put(2+i,n);
});

//generate a food location
makefood.();

//game animation handled by userview drawfunc
u.drawFunc_({

	//decrement board, lower bound at zero
	board = (board - 1).max(0);

	//adjust shift based on movement direction
	case
	{dir == \east} {shift = 0 @ 1}
	{dir == \west} {shift = 0 @ -1}
	{dir == \north} {shift = -1 @ 0}
	{dir == \south} {shift = 1 @ 0}
	{true} {nil};

	//new target position for snake head
	pos = pos + shift;

	if(
		//check for boundary collision, crash if so
		(pos.y >= width) || (pos.y < 0) ||
		(pos.x >= height) || (pos.x < 0),

		crash,

		{
			//if no boundary crash, check for self-collision, crash if so
			if(
				(board[pos.x][pos.y] > 0),

				crash,

				//if no collision, place snake head on board
				{board[pos.x][pos.y] = size}
			);
		}
	);

	//check if food was acquired
	if(
		pos == foodpos,
		{
			//get bigger
			size = size + grow_inc;
			board = board.deepCollect(2, {
				arg n;
				if (n!=0, {n + grow_inc}, {0})
			});

			//generate new food
			makefood.();
		}
	);

	//draw next frame
	board.do({
		arg row, j;
		row.do({
			arg val, i;
			Pen.fillColor_(Color.gray(
				//positive numbers are light gray (snake body)
				//0s are dark grey (game board background)
				(val>0).asInteger*0.6+0.1
			));
			Pen.addRect(Rect(i*20, j*20, 20, 20));
			Pen.fill;
		});
	});

	//draw food
	Pen.fillColor_(foodcolor);
	Pen.addRect(Rect(foodpos.y*20,foodpos.x*20, 20, 20));
	Pen.fill;
});

//userview updates snake direction in response to arrow keys
u.keyDownAction_({
	arg view, char, mod, uni, keycode;
	case
	{keycode == keycodes[0]} {dir = \west}
	{keycode == keycodes[1]} {dir = \east}
	{keycode == keycodes[2]} {dir = \south}
	{keycode == keycodes[3]} {dir = \north}
	{uni == 27} {u.animate_(false); win.close}
	{true} {nil};
});

//set framerate and start game
u.frameRate_(framerate);
u.animate_(true);
win.front;
)
raw 4145 chars (focus & ctrl+a+c to copy)
reception
fun
comments
56228375 user 30 Apr'21 09:07

This works quite well, but on my laptop running linux the keycodes are completely different: \west: 65361 \east: 65363 \south: 65364 \north: 65362

eli.fieldsteel user 06 Jun'21 19:47

Thanks for this comment, I totally overlooked this discrepancy. I modified the code, it should now be fully cross-platform for mac/win/linux.