UdemyでPythonを勉強した結果を残すブログ。

40歳でプログラミング始めて転職までいけるのかを実録してみます。

DAY20-21 タートルモジュールを使ってスネークゲームを作る

f:id:GO-py:20220403114146p:plain

snake game


DAY 19 20の二日間ではタートルモジュールを使って

レトロゲームのスネークゲームを作ります。

 

スネークを上下左右に操って、丸い餌を食べることで

体がとスコアが1つずつ増えていってどれだけスコアを上げられるかというゲームです。

 

ざっくりした流れは

  • 餌を食べるスネークを作る
  • ランダムに現れる餌を作成する
  • スネークが餌を食べたら増えるスコアを作成する
  • 四方の壁か自分の体に先頭が当たったらGameOver

こんな感じ。

 

それでは始めます。

 

初めにスネークから。

Turtleのshapeをsquareにして最初は3つに並べれば

体ができる。

ただ、餌を食べれば増えていくので初めにおきたい場所だけ設定しておいて、その後は最後尾に増えるようなコーディングにしたい。

 

snake.py

from turtle import Turtle
STARTING_POSITIONS = [(0, 0), (-20, 0), (-40, 0)]

class Snake:
def __init__(self):
self.snake_body = []
for i in STARTING_POSITIONS:
self.add_body(i)
self.head = self.snake_body[0]


def add_body(self, position):
new_body = Turtle("square")
new_body.color("white")
new_body.penup()
new_body.goto(position)
self.snake_body.append(new_body)

 

初期値でsnake_bodyというリストを作り、

add_bodyで四角をSTARTING_POSITIONにある3つの位置に置く。

 

4つ目以降は一番最後に置くようにする。


def increase_snake(self):
self.add_body(self.snake_body[-1].position())

def move(self):
for seg_num in range(len(self.snake_body)-1, 0, -1):
new_x = self.snake_body[seg_num - 1].xcor()
new_y = self.snake_body[seg_num - 1].ycor()
self.snake_body[seg_num].goto(new_x, new_y)
self.head.forward(20)

 

moveのrangeの内容はスネークの最後から順次20ずつ進むようにしているのは、アニメーションと体が増えるときに違和感のないようにしている。

 

というのも、最初に頭から移動すると、

1■■■

2■■ ■

3■ ■■

4 ■■■

と言う感じになって体1つ分隙間ができてしまう。

逆からやると、

1■■■

2 ■■ (左から2つ目が重なる)

3 ■■ (左から3つ目が重なる)

4 ■■■

と言う感じで体が離れずに進むことができる。

 

また、体が一つ増える分に関しては

snake_bodyの一番最後の場所を取得してから進み、

最後にその場所に四角を増やす、という仕組み。

同じような流れで言うと、

1■■■

2 ■■ (左から2つ目が重なる)

3 ■■ (左から3つ目が重なる)

4 ■■■

5■■■■(最後のpositionに新しい体を追加する)

6 ■■■(同じように進む)

 

最後に、上下左右に向きを変えるようための動作

 

UP = 90
DOWN = 270
RIGHT = 0
LEFT = 180

def up(self):
if self.head.heading() != DOWN:
self.head.setheading(UP)

def down(self):
if self.head.heading() != UP:
self.head.setheading(DOWN)


def left(self):
if self.head.heading() != RIGHT:
self.head.setheading(LEFT)


def right(self):
if self.head.heading() != LEFT:
self.head.setheading(RIGHT)

 

これでsnake.pyは終わり。

 

 

次に、food.pyとしてエサのクラスを作成する

food.py

これはシンプルに、エサの形状と

ランダムに表示される設定を作成する。

 

from turtle import Turtle
import random

class Food(Turtle):
def __init__(self):
super().__init__()
self.shape("circle")
self.color("yellow")
self.shapesize(0.3)
self.penup()
self.refresh()

def refresh(self):
random_x = random.randint(-280, 280)
random_y = random.randint(-280, 280)
self.goto(random_x, random_y)

 

food = Turtle()をつけなくてもclassのかっこにimportをいれて、

__init__下部分にsuper().__init__()

を入れることでTurtle()と同じことができるようになる。

 

次にスコアを表示

score_board.py

 

from turtle import Turtle

class ScoreBoard(Turtle):
def __init__(self):
super(ScoreBoard, self).__init__()
self.score = 0
self.penup()
self.ht()
self.goto(0, 270)
self.color("white")
self.get_score()

def get_score(self):
self.write(f"Score : {self.score}", font=("Sans", 20, "bold"), align="center")

def game_over(self):
self.goto(0, 0)
self.write("Game Over.", font=("Sans", 20, "bold"), align="center")

def increase_score(self):
self.clear()
self.score += 1
self.get_score()

 

スコアでは文字を取り扱うパーツにする。

スコアを初期値は0にして、get_scoreで表示をする。

 

スクリーンに文字を入れるには.writeメソッドを使う。

.write("表示したい文字", font=("フォント". 文字の大きさ,"太字とかイタリックとか", align="centerとか")

 

で文字の装飾も可能。

 

 

main.pyでゲームをまとめていく

それぞれのクラスが設定できたので、まとめる。

 

まずはそれぞれを呼び出して、ウィンドウの設定をしていく。

 

from turtle import Turtle, Screen
from snake import Snake
from food import Food
from score_board import ScoreBoard
import random
import time

screen = Screen()
screen.setup(600, 600)
screen.bgcolor("black")
screen.title("Snake Game")
screen.tracer(0)

snake = Snake()
food = Food()
score = ScoreBoard()

 

.tracerについては後述。

前回のタートルゲームでやったキーを押すことでウィンドウ内にアクションができる機能 listen()とonkey()を入力して上下左右に向きを変えられるようにする。

 


screen.listen()
screen.onkey(snake.up, "Up")
screen.onkey(snake.down, "Down")
screen.onkey(snake.left, "Left")
screen.onkey(snake.right, "Right")

 

ゲーム開始部分に。

ゲームが終わるまではずっとスネークが動かせるようにする。

スネークが四方にはみ出たらゲームオーバー。

また、スネークが自分の体にあたってもゲームオーバー。

スネークがエサを食べたら体が伸びてスコアもアップする。

この3つを盛り込めば完成。


start_game = True
while start_game:
screen.update()
time.sleep(0.5)
snake.move()
if snake.head.distance(food) < 20:
food.refresh()
score.increase_score()
snake.increase_snake()

if snake.head.xcor() >= 300 or snake.head.xcor() <= -300 or \
snake.head.ycor() >= 300 or snake.head.ycor() <= -300:
score.game_over()
start_game = False

for snake_body in snake.snake_body:
if snake_body == snake.head:
pass
elif snake.head.distance(snake_body) < 10:
score.game_over()
start_game = False

screen.exitonclick()

 

エサを食べるというのはどう言う判定をするか、というのは

スネークの頭とエサの距離が20以下になったら、と判定する。

(スネークの体1つが20なので)

それができるのがdistance関数。1つ目のifがそのコード。

 

2つ目のコードは四方の壁からスネークがはみ出たらゲームオーバーになるコード。

壁からはみ出るのを判定するのは、

スネークの頭をxcor()、ycor()で場所を取得して、

ウィンドウサイズより大きい、または少ないを判定している。

 

for部分では頭が体に追突したらゲームオーバーになるコード。

snake_body == snake.headの場合はpassとしているが、

これをしないとsnake.headの距離は0なのですぐゲームオーバーになってしまうため。

 

先ほどの.tracer()と今回の.update()について

これがないとgoto()や向きを変更するときに律儀にくるっと回転するアニメーションまで出てきてしまうので、

time.sleepと合わせて使用する。

伝え方が難しいので、コードをない場合とある場合で試してみるといいかも。

 

 

これで完成。

 

それぞれのファイルまとめ

main.py

from turtle import Turtle, Screen
from snake import Snake
from food import Food
from score_board import ScoreBoard
import random
import time

screen = Screen()
screen.setup(600, 600)
screen.bgcolor("black")
screen.title("Snake Game")
screen.tracer(0)

snake = Snake()
food = Food()
score = ScoreBoard()

screen.listen()
screen.onkey(snake.up, "Up")
screen.onkey(snake.down, "Down")
screen.onkey(snake.left, "Left")
screen.onkey(snake.right, "Right")

def count_num(num):
count_nums = Turtle()
count_nums.ht()
count_nums.color("white")
count_nums.penup()
count_nums.goto(0, 20)
for i in range(1,num):
count_nums.clear()
count_nums.write(num - i, font=("Sans", 20, "bold"), align="center")
time.sleep(1)
count_nums.clear()

count_num(4)

start_game = True
while start_game:
screen.update()
time.sleep(0.1)
snake.move()
if snake.head.distance(food) < 20:
food.refresh()
score.increase_score()
snake.increase_snake()

if snake.head.xcor() >= 300 or snake.head.xcor() <= -300 or \
snake.head.ycor() >= 300 or snake.head.ycor() <= -300:
score.game_over()
start_game = False

for snake_body in snake.snake_body:
if snake_body == snake.head:
pass
elif snake.head.distance(snake_body) < 10:
score.game_over()
start_game = False

screen.exitonclick()

 

 

snake.py

from turtle import Turtle
STARTING_POSITIONS = [(0, 0), (-20, 0), (-40, 0)]
UP = 90
DOWN = 270
RIGHT = 0
LEFT = 180

class Snake:
def __init__(self):
self.snake_body = []
for i in STARTING_POSITIONS:
self.add_body(i)
self.head = self.snake_body[0]

def add_body(self, position):
new_body = Turtle("square")
new_body.color("white")
new_body.penup()
new_body.goto(position)
self.snake_body.append(new_body)

def increase_snake(self):
self.add_body(self.snake_body[-1].position())

def move(self):
for seg_num in range(len(self.snake_body)-1, 0, -1):
new_x = self.snake_body[seg_num - 1].xcor()
new_y = self.snake_body[seg_num - 1].ycor()
self.snake_body[seg_num].goto(new_x, new_y)
self.head.forward(20)



def up(self):
if self.head.heading() != DOWN:
self.head.setheading(UP)

def down(self):
if self.head.heading() != UP:
self.head.setheading(DOWN)


def left(self):
if self.head.heading() != RIGHT:
self.head.setheading(LEFT)


def right(self):
if self.head.heading() != LEFT:
self.head.setheading(RIGHT)

 

score_board.py

from turtle import Turtle

class ScoreBoard(Turtle):
def __init__(self):
super(ScoreBoard, self).__init__()
self.score = 0
self.penup()
self.ht()
self.goto(0, 270)
self.color("white")
self.get_score()

def get_score(self):
self.write(f"Score : {self.score}", font=("Sans", 20, "bold"), align="center")

def game_over(self):
self.goto(0, 0)
self.write("Game Over.", font=("Sans", 20, "bold"), align="center")

def increase_score(self):
self.clear()
self.score += 1
self.get_score()

 

food.py

from turtle import Turtle
import random

class Food(Turtle):
def __init__(self):
super().__init__()
self.shape("circle")
self.color("yellow")
self.shapesize(0.3)
self.penup()
self.refresh()

def refresh(self):
random_x = random.randint(-280, 280)
random_y = random.randint(-280, 280)
self.goto(random_x, random_y)