Skip to content

Commit 175645e

Browse files
Keith RieckKeith Rieck
Keith Rieck
authored and
Keith Rieck
committedSep 25, 2022
Snake game
1 parent 152e5fa commit 175645e

10 files changed

+1302
-1
lines changed
 

‎.gitignore

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
venv/
2+
__target__/
3+
.idea/
4+
5+
# Created by https://www.gitignore.io/api/macos,python
6+
# Edit at https://www.gitignore.io/?templates=macos,python
7+
8+
### macOS ###
9+
# General
10+
.DS_Store
11+
.AppleDouble
12+
.LSOverride
13+
14+
# Icon must end with two \r
15+
Icon
16+
17+
# Thumbnails
18+
._*
19+
20+
# Files that might appear in the root of a volume
21+
.DocumentRevisions-V100
22+
.fseventsd
23+
.Spotlight-V100
24+
.TemporaryItems
25+
.Trashes
26+
.VolumeIcon.icns
27+
.com.apple.timemachine.donotpresent
28+
29+
# Directories potentially created on remote AFP share
30+
.AppleDB
31+
.AppleDesktop
32+
Network Trash Folder
33+
Temporary Items
34+
.apdisk
35+
36+
### Python ###
37+
# Byte-compiled / optimized / DLL files
38+
__pycache__/
39+
*.py[cod]
40+
*$py.class
41+
42+
# C extensions
43+
*.so
44+
45+
# Distribution / packaging
46+
.Python
47+
build/
48+
develop-eggs/
49+
dist/
50+
downloads/
51+
eggs/
52+
.eggs/
53+
lib/
54+
lib64/
55+
parts/
56+
sdist/
57+
var/
58+
wheels/
59+
pip-wheel-metadata/
60+
share/python-wheels/
61+
*.egg-info/
62+
.installed.cfg
63+
*.egg
64+
MANIFEST
65+
66+
# PyInstaller
67+
# Usually these files are written by a python script from a template
68+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
69+
*.manifest
70+
*.spec
71+
72+
# Installer logs
73+
pip-log.txt
74+
pip-delete-this-directory.txt
75+
76+
# Unit test / coverage reports
77+
htmlcov/
78+
.tox/
79+
.nox/
80+
.coverage
81+
.coverage.*
82+
.cache
83+
nosetests.xml
84+
coverage.xml
85+
*.cover
86+
.hypothesis/
87+
.pytest_cache/
88+
89+
# Translations
90+
*.mo
91+
*.pot
92+
93+
# Scrapy stuff:
94+
.scrapy
95+
96+
# Sphinx documentation
97+
docs/_build/
98+
99+
# PyBuilder
100+
target/
101+
102+
# pyenv
103+
.python-version
104+
105+
# pipenv
106+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
107+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
108+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
109+
# install all needed dependencies.
110+
#Pipfile.lock
111+
112+
# celery beat schedule file
113+
celerybeat-schedule
114+
115+
# SageMath parsed files
116+
*.sage.py
117+
118+
# Spyder project settings
119+
.spyderproject
120+
.spyproject
121+
122+
# Rope project settings
123+
.ropeproject
124+
125+
# Mr Developer
126+
.mr.developer.cfg
127+
.project
128+
.pydevproject
129+
130+
# mkdocs documentation
131+
/site
132+
133+
# mypy
134+
.mypy_cache/
135+
.dmypy.json
136+
dmypy.json
137+
138+
# Pyre type checker
139+
.pyre/
140+
141+
# End of https://www.gitignore.io/api/macos,python

‎LICENSE

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
BSD 2-Clause License
2+
3+
Copyright (c) 2020, Keith Rieck
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

‎Makefile

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
COMPILE_FLAGS=-b -m -n
2+
PYTHON_HOME=/usr/local/opt/python@3.8
3+
DEPLOY_DIR=${HOME}/Sites/bedlam
4+
5+
build: build/snake.js
6+
7+
build/snake.js: snake.py
8+
transcrypt $(COMPILE_FLAGS) snake.py
9+
mkdir -p build
10+
cp -r __target__/* build/
11+
12+
clean:
13+
rm -rf __target__
14+
rm -rf /__javascript__
15+
rm -rf build
16+
17+
deploy: build LICENSE
18+
mkdir -p $(DEPLOY_DIR)
19+
cp -rf __target__/ build/
20+
cp *.html $(DEPLOY_DIR)
21+
cp -r build $(DEPLOY_DIR)
22+
cp -r assets $(DEPLOY_DIR)
23+
cp LICENSE $(DEPLOY_DIR)
24+
cp *.png $(DEPLOY_DIR)
25+
cp style.css $(DEPLOY_DIR)
26+
cp *.json $(DEPLOY_DIR)
27+
cp *.js $(DEPLOY_DIR)
28+
29+
setup:
30+
virtualenv venv --python=${PYTHON_HOME}/bin/python3
31+
venv/bin/python -m pip install transcrypt mypy
32+
echo "Enter virtual environment with: . venv/bin/activate"

‎README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
Hosted at: https://keithrieck.github.io/snake/snake.html
2+
13
# snake
2-
Simple Snake game written in Python using the bedlam framework
4+
Simple Snake game written in Python using the [bedlam](https://github.com/KeithRieck/bedlam) game framework.
5+
6+
This program was written in Python and compiled with [Transcrypt](https://transcrypt.org/).

‎bedlam.py

+678
Large diffs are not rendered by default.

‎favicon.ico

7.23 KB
Binary file not shown.

‎snake.html

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8"/>
5+
<title>Snake</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<link rel="apple-touch-icon" sizes="128x128" href="snake_apple-touch-icon.png">
8+
<meta name="description" content="Snake game written in Python and compiled to JavaScript.">
9+
<link rel="stylesheet" href="style.css">
10+
<meta name="theme-color" content="white"/>
11+
</head>
12+
<body class="fullscreen">
13+
14+
<div class="container">
15+
<canvas id="canvas" width="640" height="480"></canvas>
16+
</div>
17+
18+
<script type="module">
19+
import * as game from "./build/snake.js";
20+
window.game = game;
21+
const urlParams = new URLSearchParams(window.location.search);
22+
var app = new window.game.SnakeGame();
23+
app.set_debug(urlParams.get('debug'))
24+
app.start()
25+
</script>
26+
27+
<div style="display:none;">
28+
<audio id="add_apple" src="assets/snake_add_apple.wav"></audio>
29+
<audio id="collision" src="assets/snake_collision.mp3"></audio>
30+
<audio id="eat_apple" src="assets/snake_eat_apple.wav"></audio>
31+
</div>
32+
</body>
33+
</html>

‎snake.py

+365
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
from bedlam import Game
2+
from bedlam import GameObject
3+
from bedlam import Scene
4+
from bedlam import Sprite
5+
6+
# __pragma__('skip')
7+
document = window = Math = Date = console = 0 # Prevent complaints by optional static checker
8+
9+
# __pragma__('noskip')
10+
# __pragma__('noalias', 'clear')
11+
12+
BOARD_SIZE = 16
13+
CELL_SIZE = 28
14+
BOARD_MARGIN_LEFT = 50
15+
BOARD_MARGIN_TOP = 5
16+
SNAKE_TIME_STEP = 300
17+
SNAKE_TIME_STEP_MIN = 150
18+
GAME_BEGIN = "GAME_BEGIN"
19+
GAME_RUNNING = "GAME_RUNNING"
20+
GAME_OVER = "GAME_OVER"
21+
UP = "UP"
22+
DOWN = "DOWN"
23+
LEFT = "LEFT"
24+
RIGHT = "RIGHT"
25+
DEBUG = False
26+
COLOR_TIME = 1500
27+
COLOR_APPLE = ['#ff0000', '#ff3333', '#ff6666', '#ff9999', '#ffcccc', '#ffffff']
28+
29+
30+
class Apple(GameObject):
31+
def __init__(self, game, cx, cy):
32+
GameObject.__init__(self, game)
33+
self.cell_x = cx
34+
self.cell_y = cy
35+
36+
def update(self, delta_time: float):
37+
GameObject.update(self, delta_time)
38+
39+
def draw(self, ctx):
40+
GameObject.draw(self, ctx)
41+
Sprite.draw(self, ctx)
42+
ctx.save()
43+
# ctx.globalCompositeOperation = 'source-over'
44+
ctx.strokeStyle = 'red'
45+
ctx.fillStyle = COLOR_APPLE[0]
46+
ctx.lineWidth = COLOR_APPLE[0]
47+
ctx.beginPath()
48+
radius = (CELL_SIZE - 4) // 2
49+
apple_x = self.cell_x * CELL_SIZE + BOARD_MARGIN_LEFT + CELL_SIZE // 2
50+
apple_y = self.cell_y * CELL_SIZE + BOARD_MARGIN_TOP + CELL_SIZE // 2
51+
ctx.arc(apple_x, apple_y, radius, 0, 2 * Math.PI)
52+
ctx.fill()
53+
ctx.stroke()
54+
ctx.restore()
55+
56+
57+
class Snake(GameObject):
58+
def __init__(self, game):
59+
GameObject.__init__(self, game)
60+
self.segments = []
61+
self.time_used = 0
62+
63+
def append(self, snakesegment):
64+
if len(self.segments) > 0:
65+
snakesegment.prev_seg = self.segments[len(self.segments) - 1]
66+
self.segments.append(snakesegment)
67+
68+
def reset(self):
69+
cx = 2
70+
cy = BOARD_SIZE // 2
71+
self.segments = []
72+
self.append(SnakeSegment(self.game, cx, cy, RIGHT))
73+
self.append(SnakeSegment(self.game, cx - 1, cy, RIGHT))
74+
for snakesegment in self.segments:
75+
snakesegment.scene = self.scene
76+
snakesegment.reset()
77+
78+
def set_direction(self, dr):
79+
snakesegment = self.segments[0]
80+
if not self.opposite(snakesegment.direction, dr):
81+
snakesegment.set_direction(dr)
82+
83+
def opposite(self, d1, d2):
84+
return (d1 == LEFT and d2 == RIGHT) or (d1 == RIGHT and d2 == LEFT) \
85+
or (d1 == UP and d2 == DOWN) or (d1 == DOWN and d2 == UP)
86+
87+
def update(self, delta_time: float):
88+
global SNAKE_TIME_STEP
89+
GameObject.update(self, delta_time)
90+
if self.scene.mode == GAME_RUNNING:
91+
self.time_used = self.time_used + delta_time
92+
while self.time_used > SNAKE_TIME_STEP:
93+
self.time_used = self.time_used - SNAKE_TIME_STEP
94+
self.move_at_cell()
95+
SNAKE_TIME_STEP = Math.max(SNAKE_TIME_STEP_MIN, SNAKE_TIME_STEP-1)
96+
for snakesegment in self.segments:
97+
snakesegment.update(delta_time)
98+
99+
def move_at_cell(self):
100+
for snakesegment in self.segments:
101+
if snakesegment.prev_seg is not None:
102+
snakesegment.next_direction = snakesegment.prev_seg.direction
103+
for snakesegment in self.segments:
104+
snakesegment.move_at_cell()
105+
seg = self.segments[0]
106+
nx, ny = seg.next_cell(seg.cell_x, seg.cell_y, seg.direction)
107+
for apple in self.scene.apples:
108+
if apple.cell_x == nx and apple.cell_y == ny:
109+
self.eat_apple(apple)
110+
break
111+
112+
def draw(self, ctx):
113+
GameObject.draw(self, ctx)
114+
for snakesegment in self.segments:
115+
snakesegment.draw(ctx)
116+
117+
def eat_apple(self, apple):
118+
global DEBUG
119+
seg = self.segments[0]
120+
snakesegment = SnakeSegment(self.game, apple.cell_x, apple.cell_y)
121+
snakesegment.scene = self.scene
122+
snakesegment.color_time = COLOR_TIME
123+
seg.prev_seg = snakesegment
124+
snakesegment.set_cell(apple.cell_x, apple.cell_y, seg.direction)
125+
self.scene.apples.remove(apple)
126+
self.scene.remove(apple)
127+
self.segments.insert(0, snakesegment)
128+
nx, ny = snakesegment.next_cell(snakesegment.cell_x, snakesegment.cell_y, snakesegment.direction)
129+
self.scene.sound_eat_apple.play()
130+
if nx < 0 or nx > BOARD_SIZE - 1 or ny < 0 or ny > BOARD_SIZE - 1:
131+
self.scene.collision()
132+
if DEBUG:
133+
console.log("Collision with wall at " + nx + "," + ny)
134+
if len(self.scene.apples) == 0 and self.scene.mode == GAME_RUNNING:
135+
self.schedule(self.scene.add_apple, 1000)
136+
137+
138+
class SnakeSegment(GameObject):
139+
def __init__(self, game, cx, cy, dr=RIGHT):
140+
GameObject.__init__(self, game)
141+
self.cell_x = cx
142+
self.cell_y = cy
143+
self.start_x = cx
144+
self.start_y = cy
145+
self.start_dir = dr
146+
self.x = 0
147+
self.y = 0
148+
self.direction = dr
149+
self.next_direction = dr
150+
self.prev_seg = None
151+
self.color_time = 0
152+
153+
def set_direction(self, dr):
154+
self.next_direction = dr
155+
if self.scene.mode == GAME_BEGIN:
156+
self.scene.mode = GAME_RUNNING
157+
self.direction = dr
158+
159+
def set_cell(self, cx, cy, dr=None):
160+
self.cell_x = cx
161+
self.cell_y = cy
162+
self.x = self.cell_x * CELL_SIZE
163+
self.y = self.cell_y * CELL_SIZE
164+
if dr is not None:
165+
self.direction = dr
166+
self.next_direction = dr
167+
168+
def reset(self):
169+
self.set_cell(self.start_x, self.start_y, self.start_dir)
170+
171+
def update(self, delta_time: float):
172+
GameObject.update(self, delta_time)
173+
if self.scene.mode == GAME_RUNNING:
174+
dist = Math.round(delta_time * (CELL_SIZE / SNAKE_TIME_STEP))
175+
if self.direction == LEFT:
176+
self.x = self.x - dist
177+
elif self.direction == RIGHT:
178+
self.x = self.x + dist
179+
elif self.direction == UP:
180+
self.y = self.y - dist
181+
elif self.direction == DOWN:
182+
self.y = self.y + dist
183+
self.color_time = Math.max(0, self.color_time - delta_time)
184+
185+
def move_at_cell(self):
186+
global DEBUG
187+
collision = False
188+
cx = self.cell_x
189+
cy = self.cell_y
190+
if self.direction == LEFT and self.cell_x <= 1:
191+
cx = 0
192+
if self.next_direction == self.direction:
193+
collision = True
194+
else:
195+
self.direction = self.next_direction
196+
elif self.direction == RIGHT and self.cell_x >= (BOARD_SIZE - 2):
197+
cx = BOARD_SIZE - 1
198+
if self.next_direction == self.direction:
199+
collision = True
200+
else:
201+
self.direction = self.next_direction
202+
elif self.direction == UP and self.cell_y <= 1:
203+
cy = 0
204+
if self.next_direction == self.direction:
205+
collision = True
206+
else:
207+
self.direction = self.next_direction
208+
elif self.direction == DOWN and self.cell_y >= (BOARD_SIZE - 2):
209+
cy = BOARD_SIZE - 1
210+
if self.next_direction == self.direction:
211+
collision = True
212+
else:
213+
self.direction = self.next_direction
214+
else:
215+
cx, cy = self.next_cell(self.cell_x, self.cell_y, self.direction)
216+
if DEBUG and collision:
217+
console.log("collision with wall at " + cx + "," + cy)
218+
if self.prev_seg is None:
219+
nx, ny = self.next_cell(cx, cy, self.next_direction)
220+
for seg in self.scene.snake.segments:
221+
if self != seg and seg.cell_x == nx and seg.cell_y == ny:
222+
collision = True
223+
if (DEBUG):
224+
console.log("collision with snake at " + nx + "," + ny)
225+
if DEBUG:
226+
console.log(self.cell_x + ", " + self.cell_y + " : " + self.direction + " : " + self.scene.mode)
227+
if collision:
228+
self.scene.collision()
229+
else:
230+
self.set_cell(cx, cy, self.next_direction)
231+
232+
def next_cell(self, cx, cy, dr):
233+
nx, ny = cx, cy
234+
if dr == LEFT:
235+
nx = nx - 1
236+
elif dr == RIGHT:
237+
nx = nx + 1
238+
elif dr == UP:
239+
ny = ny - 1
240+
elif dr == DOWN:
241+
ny = ny + 1
242+
return nx, ny
243+
244+
def draw(self, ctx):
245+
GameObject.draw(self, ctx)
246+
Sprite.draw(self, ctx)
247+
ctx.save()
248+
ctx.strokeStyle = 'black'
249+
ctx.lineWidth = 3
250+
ctx.beginPath()
251+
radius = (CELL_SIZE - 4) // 2
252+
snake_x = self.x + BOARD_MARGIN_LEFT + CELL_SIZE // 2
253+
snake_y = self.y + BOARD_MARGIN_TOP + CELL_SIZE // 2
254+
if self.color_time > 0:
255+
t = Math.floor(len(COLOR_APPLE) * (COLOR_TIME - self.color_time) / COLOR_TIME)
256+
ctx.fillStyle = COLOR_APPLE[t]
257+
ctx.arc(snake_x, snake_y, radius, 0, 2 * Math.PI)
258+
ctx.fill()
259+
ctx.stroke()
260+
else:
261+
ctx.arc(snake_x, snake_y, radius, 0, 2 * Math.PI)
262+
ctx.stroke()
263+
ctx.restore()
264+
265+
266+
class SnakeScene(Scene):
267+
def __init__(self, game, name):
268+
Scene.__init__(self, game, name)
269+
self.sound_add_apple = None
270+
self.mode = GAME_BEGIN
271+
self.snake = Snake(game)
272+
self.append(self.snake)
273+
self.apples = []
274+
self.add_apple(BOARD_SIZE - 3, BOARD_SIZE // 2)
275+
self.reset_board()
276+
self.sound_add_apple = self.game.load_audio('add_apple')
277+
self.sound_collision = self.game.load_audio('collision')
278+
self.sound_eat_apple = self.game.load_audio('eat_apple')
279+
280+
def reset_board(self):
281+
self.mode = GAME_BEGIN
282+
self.snake.reset()
283+
284+
def add_apple(self, cx=None, cy=None):
285+
while cx is None:
286+
xx = Math.floor(Math.random() * BOARD_SIZE)
287+
yy = Math.floor(Math.random() * BOARD_SIZE)
288+
if not self._occupied(xx, yy) and not self._bad(xx, yy):
289+
cx = xx
290+
cy = yy
291+
apple = Apple(self.game, cx, cy)
292+
self.apples.append(apple)
293+
self.append(apple)
294+
if self.sound_add_apple is not None:
295+
self.sound_add_apple.play()
296+
297+
def _occupied(self, cx, cy):
298+
for apple in self.apples:
299+
if apple.cell_x == cx and apple.cell_y == cy:
300+
return True
301+
for snakesegment in self.snake.segments:
302+
if snakesegment.cell_x == cx and snakesegment.cell_y == cy:
303+
return True
304+
return False
305+
306+
def _bad(self, cx, cy):
307+
return (cx <= 0 or cx >= BOARD_SIZE - 1) and (cy <= 0 or cy >= BOARD_SIZE - 1)
308+
309+
def _set_direction(self, d):
310+
self.snake.set_direction(d)
311+
312+
def handle_keydown(self, event):
313+
Scene.handle_keydown(self, event)
314+
if event.key == 'a' or event.key == 'ArrowLeft':
315+
self._set_direction(LEFT)
316+
elif event.key == 'd' or event.key == 'ArrowRight':
317+
self._set_direction(RIGHT)
318+
elif event.key == 'w' or event.key == 'ArrowUp':
319+
self._set_direction(UP)
320+
elif event.key == 's' or event.key == 'ArrowDown':
321+
self._set_direction(DOWN)
322+
323+
def collision(self):
324+
self.sound_collision.play()
325+
self.mode = GAME_OVER
326+
327+
def update(self, delta_time: float):
328+
Scene.update(self, delta_time)
329+
pass
330+
331+
def draw(self, ctx):
332+
global DEBUG
333+
Scene.draw(self, ctx)
334+
ctx.save()
335+
ctx.globalCompositeOperation = 'source-over'
336+
ctx.strokeStyle = 'black'
337+
ctx.lineWidth = 2
338+
ctx.beginPath()
339+
board_width = BOARD_SIZE * CELL_SIZE
340+
ctx.rect(BOARD_MARGIN_LEFT, BOARD_MARGIN_TOP, board_width, board_width)
341+
ctx.stroke()
342+
if DEBUG:
343+
ctx.strokeStyle = '#CCCCCC'
344+
ctx.lineWidth = 1
345+
ctx.beginPath()
346+
for n in range(1, BOARD_SIZE):
347+
ctx.moveTo(BOARD_MARGIN_LEFT, BOARD_MARGIN_TOP + n * CELL_SIZE)
348+
ctx.lineTo(BOARD_MARGIN_LEFT + board_width, BOARD_MARGIN_TOP + n * CELL_SIZE)
349+
ctx.moveTo(BOARD_MARGIN_LEFT + n * CELL_SIZE, BOARD_MARGIN_TOP)
350+
ctx.lineTo(BOARD_MARGIN_LEFT + n * CELL_SIZE, BOARD_MARGIN_TOP + board_width)
351+
ctx.stroke()
352+
ctx.restore()
353+
354+
355+
class SnakeGame(Game):
356+
def __init__(self, loop_time=20):
357+
Game.__init__(self, 'Snake', loop_time)
358+
self.append(SnakeScene(self, 'MAIN'))
359+
360+
@staticmethod
361+
def set_debug(b):
362+
global DEBUG, SNAKE_TIME_STEP
363+
if b is not None and b == 'true':
364+
DEBUG = True
365+
SNAKE_TIME_STEP = SNAKE_TIME_STEP * 2

‎snake_apple-touch-icon.png

10.7 KB
Loading

‎style.css

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
body {
2+
font-family: sans-serif;
3+
}
4+
5+
/* Make content area fill the entire browser window */
6+
html,
7+
.fullscreen {
8+
display: flex;
9+
height: 100%;
10+
margin: 0;
11+
padding: 0;
12+
width: 100%;
13+
}
14+
15+
/* Center the content in the browser window */
16+
.container {
17+
margin: auto;
18+
text-align: center;
19+
}
20+
21+
.title {
22+
font-size: 3rem;
23+
}

0 commit comments

Comments
 (0)
Please sign in to comment.