今年に入って Edx という、無償で大学レベルの授業を提供しているオンライン学習のプラットフォームの存在を知った。面白そうだったから、 UBCx’s Software Development MicroMasters® Program の How to Code: Simple Data の受講を始めた。このコースを受講し終えると、あらゆるプログラミング言語でうまくテストされた、可読性の高いプログラムが書けるようになるらしい。(本当か?)
Racket という Scheme から派生した、プログラミング教育のために作られた言語を使って、プログラムの設計方法やテストコードの書き方を教えてくれた。授業を進めると、適宜課題が出されるので、それに答えていく仕組み。課題の難易度は、簡単な選択問題から実際に ~200 行程度のコードを記述する問題まで色々あった。 最終課題はスペースインベーダーゲームのようなものを作るというものだった。
最後まで受講して、課題も一通り解いてみたけど、修了証明書を取得できなかった。
- コードの振る舞いや設計方針をコメントで説明するということが、実装やテストコードを書くのと同じぐらい重要視されていたが、うまく説明できなかった
- 操作ミスで、いくつかの課題を中途半端な状態で提出してしまって、不正解となった (再提出は不可)
- 英語力の低さが災いして、課題の意図を間違って解釈した結果、不正解となった
などが響いた。ただ、
- Racket を使って、簡単なプログラムを書けるようになった
- 状態をオブジェクトに持たせることができないため、関数の引数に何を渡して何を返すべきか、以前よりも注意深く考えるようになった
などの学びがあったから、受講して良かった。続編の How to Code: Complex Data も受講してみる。
以下は 最終課題 に提出したコード。課題ではゲームオーバー時(敵がスクリーン最下部に当たったとき)に何も起きないが、それだと面白く無いので、撃退した敵の数を画面に表示した。
(require 2htdp/image)
(require 2htdp/universe)
;; ===
;; Constants:
(define WIDTH 600)
(define HEIGHT 800)
(define MTS (empty-scene WIDTH HEIGHT))
(define THRESHOLD-HIT 5)
(define TANK-IMAGE (above (rectangle 10 15 "solid" "black")
(rectangle 30 10 "solid" "black")
(ellipse 45 10 "solid" "black")
))
(define TANK-IMAGE2 (above (rectangle 60 15 "solid" "black")
(rectangle 30 10 "solid" "black")
(ellipse 45 90 "solid" "black")
))
(define OFFSET 5)
(define MISSILE-IMAGE (ellipse 10 25 "solid" "red"))
(define INVADER-IMAGE (above (wedge 10 180 "outline" "purple")
(ellipse 60 10 "solid" "purple")))
;; ===
;; Data definitions:
(define-struct tank (x vx image))
(define (fn-for-tank t)
(... (tank-x t)
(tank-vx t)))
(define-struct missile (x y vy image died?))
(define-struct enemy (x y vx vy image died?))
(define-struct game (tank lom loe over? score))
;; ===
;; Functions:
;; Tank -> Natural
;; produce image width of tank
(define (tank-image-width t)
(image-width (tank-image t)))
(check-expect (tank-image-width (make-tank 0 0 TANK-IMAGE)) 45)
(check-expect (tank-image-width (make-tank 0 0 TANK-IMAGE2)) 60)
;; Tank -> Natural
;; produce image height of tank
(define (tank-image-height t)
(image-height (tank-image t)))
(check-expect (tank-image-height (make-tank 0 0 TANK-IMAGE)) 35)
(check-expect (tank-image-height (make-tank 0 0 TANK-IMAGE2)) 115)
;; Missile,ListOfEnemy -> Missile
;; produce moved missile
(define (move-missile m loe)
(if (any-hit-missile? m loe)
(make-missile (missile-x m) (- (missile-y m) (missile-vy m)) (missile-vy m) (missile-image m) true)
(make-missile (missile-x m) (- (missile-y m) (missile-vy m)) (missile-vy m) (missile-image m) (missile-died? m))))
(check-expect (move-missile (make-missile 0 0 0 MISSILE-IMAGE false) (cons (make-enemy 0 0 0 0 INVADER-IMAGE false) empty))
(make-missile 0 0 0 MISSILE-IMAGE true))
(check-expect (move-missile (make-missile 0 0 1 MISSILE-IMAGE false) (cons (make-enemy 0 0 0 0 INVADER-IMAGE true) empty))
(make-missile 0 -1 1 MISSILE-IMAGE false))
;; ListOfMissile,ListOfEnemy -> ListofMissile
;; produce moved missiles
(define (move-missiles lom loe)
(cond [(empty? lom) empty]
[else
(cons (move-missile (first lom) loe) (move-missiles (rest lom) loe))]))
(check-expect (move-missiles empty empty) empty)
(check-expect (move-missiles (cons (make-missile 0 0 1 MISSILE-IMAGE false) empty) empty)
(cons (make-missile 0 -1 1 MISSILE-IMAGE false) empty))
(check-expect (move-missiles (cons (make-missile 0 0 1 MISSILE-IMAGE false) empty) (cons (make-enemy 0 1 0 0 INVADER-IMAGE false) empty))
(cons (make-missile 0 -1 1 MISSILE-IMAGE true) empty))
;; ListOfEnemy,ListOfMissile -> ListOfEnemy
;; produce moved enemies
(define (move-enemies loe lom)
(cond [(empty? loe) empty]
[else
(cons (move-enemy (first loe) lom) (move-enemies (rest loe) lom))]))
(check-expect (move-enemies empty empty) empty)
(check-expect (move-enemies (cons (make-enemy 1 0 0 0 INVADER-IMAGE false) empty) empty)
(cons (make-enemy 1 0 0 0 INVADER-IMAGE false) empty))
(check-expect (move-enemies (cons (make-enemy 1 1 0 0 INVADER-IMAGE false) empty) (cons (make-missile 1 1 0 MISSILE-IMAGE false) empty))
(cons (make-enemy 1 1 0 0 INVADER-IMAGE true) empty))
;; Enemy -> Enemy
;; produce moved enemy
(define (move-enemy e lom)
(cond [(eq? (enemy-x e) WIDTH)
(flip-enemy e OFFSET)]
[(eq? (enemy-x e) 0)
(flip-enemy e (* -1 OFFSET))]
[else
(if (any-hit-enemy? e lom)
(make-enemy
(+ (enemy-x e) (enemy-vx e))
(+ (enemy-y e) (enemy-vy e))
(enemy-vx e)
(enemy-vy e)
INVADER-IMAGE
true
)
(make-enemy
(+ (enemy-x e) (enemy-vx e))
(+ (enemy-y e) (enemy-vy e))
(enemy-vx e)
(enemy-vy e)
INVADER-IMAGE
(enemy-died? e)
))
]))
(check-expect (move-enemy (make-enemy 1 0 0 0 INVADER-IMAGE false) empty)
(make-enemy 1 0 0 0 INVADER-IMAGE false))
(check-expect (move-enemy (make-enemy OFFSET 0 0 0 INVADER-IMAGE false) empty)
(make-enemy OFFSET 0 0 0 INVADER-IMAGE false))
;; Enemy -> Enemy
;; produce flipped enemy
(define (flip-enemy e offset)
(make-enemy
(- (enemy-x e) offset)
(enemy-y e)
(* -1 (enemy-vx e))
(enemy-vy e)
INVADER-IMAGE
(enemy-died? e)
))
(check-expect (flip-enemy (make-enemy 0 0 1 0 INVADER-IMAGE false) OFFSET)
(make-enemy (* -1 OFFSET) 0 -1 0 INVADER-IMAGE false))
;; ListOfEnemy -> Boolean
;; produce true if some enemies reach bottom of the screen otherwise produce false
(define (invade-enemies? loe)
(cond [(empty? loe) false]
[else
(if (invade-enemy? (first loe)) true (invade-enemies? (rest loe)))]))
(check-expect (invade-enemies? empty) false)
(check-expect (invade-enemies? (cons
(make-enemy 0 0 0 0 INVADER-IMAGE false) empty)) false)
(check-expect (invade-enemies? (cons
(make-enemy 0 0 0 0 INVADER-IMAGE false)
(cons
(make-enemy 0 (+ 1 HEIGHT) 0 0 INVADER-IMAGE false) empty)))
true)
;; Enemy -> Boolean
;; produce true if the enemy reach bottom of the screen otherwise produce false
(define (invade-enemy? e)
(cond [(empty? e) false]
[(enemy-died? e) false]
[else
(> (+ (enemy-y e) (/ (image-height (enemy-image e)) 2)) HEIGHT)]))
(check-expect (invade-enemy?
(make-enemy 0 0 0 0 INVADER-IMAGE false)) false)
(check-expect (invade-enemy?
(make-enemy 0 (+ 1 HEIGHT) 0 0 INVADER-IMAGE false)) true)
;; Game -> Game
;; produce next game
(define (tock g)
(if (invade-enemies? (game-loe g))
(make-game
(game-tank g)
(game-lom g)
(game-loe g)
true
(game-score g)
)
(make-game
(make-tank (+ (tank-vx (game-tank g)) (tank-x (game-tank g))) (tank-vx (game-tank g)) TANK-IMAGE)
(move-missiles (game-lom g) (game-loe g))
(if (eq? (random 100) 1)
(move-enemies (game-loe (append-enemy g (random WIDTH) (random (/ HEIGHT 2)))) (game-lom g))
(move-enemies (game-loe g) (game-lom g))
)
(invade-enemies? (game-loe g))
(count-died-enemies (game-loe g) 0)
)
)
)
;; Enemy, Missile -> Boolean
;; produce true if enemy intersect missile
(define (intersect? e m)
(cond
[(enemy-died? e) false]
[(missile-died? m) false]
[(< (+ (missile-x m) (image-width (missile-image m))) (enemy-x e)) false]
[(< (+ (enemy-x e) (image-width (enemy-image e))) (missile-x m)) false]
[(< (+ (missile-y m) (image-height (missile-image m))) (enemy-y e)) false]
[(< (+ (enemy-y e) (image-height (enemy-image e))) (missile-y m)) false]
[else true]))
;; Enemy , ListOfMissile -> Boolean
;; produce true if enemy intersects any missiles
(define (any-hit-enemy? e lom)
(cond [(empty? lom) false]
[(intersect? e (first lom)) true]
[else (any-hit-enemy? e (rest lom))])
)
(check-expect (any-hit-enemy? (make-enemy 0 0 0 0 INVADER-IMAGE false) empty) false)
(check-expect (any-hit-enemy? (make-enemy 0 0 0 0 INVADER-IMAGE false)
(cons (make-missile 0 0 0 MISSILE-IMAGE false) empty)) true)
(check-expect (any-hit-enemy? (make-enemy 0 0 0 0 INVADER-IMAGE false)
(cons (make-missile (image-width INVADER-IMAGE) (image-height INVADER-IMAGE) 0 MISSILE-IMAGE false) empty)) true)
(check-expect (any-hit-enemy? (make-enemy 0 0 0 0 INVADER-IMAGE false)
(cons (make-missile (+ 1 (image-width INVADER-IMAGE)) (+ 1 (image-height INVADER-IMAGE)) 0 MISSILE-IMAGE false) empty)) false)
;; Enemy , ListOfMissile -> Boolean
;; produce true if missile intersects any enemies
(define (any-hit-missile? m loe)
(cond [(empty? loe) false]
[(intersect? (first loe) m) true]
[else (any-hit-missile? m (rest loe))])
)
(check-expect (any-hit-missile? empty empty) false)
(check-expect (any-hit-missile? (make-missile 0 0 0 MISSILE-IMAGE false)
(cons (make-enemy 0 0 0 0 INVADER-IMAGE false) empty)) true)
(check-expect (any-hit-missile? (make-missile 0 0 0 MISSILE-IMAGE false)
(cons (make-enemy 0 0 0 0 INVADER-IMAGE false) empty)) true)
(check-expect (any-hit-missile? (make-missile 0 0 0 MISSILE-IMAGE false)
(cons (make-enemy 500 500 0 0 INVADER-IMAGE false) empty)) false)
;; Game -> Image
;; produce tank , missiles , enemies image
(define (render g)
(place-image TANK-IMAGE
(+ (/ (tank-image-width (game-tank g)) 2) (tank-x (game-tank g)))
(- HEIGHT (/ (tank-image-height (game-tank g)) 2))
(render-images (game-lom g) (game-loe g) (game-over? g) (game-score g))))
;; ListOfMissile -> Image
;; Render missiles
(define (render-missiles lom)
(cond [(empty? lom) MTS]
[else
(if (missile-died? (first lom)) (render-missiles (rest lom))
(place-image (missile-image (first lom)) (missile-x (first lom)) (missile-y (first lom)) (render-missiles (rest lom))))]))
;; ListOfEnemies, Image -> Image
;; render enemies on base image
;; (define (render-enemies loe base) base)
(define (render-enemies loe base)
(cond [(empty? loe) base]
[else
(if (enemy-died? (first loe)) (render-enemies (rest loe) base)
(place-image (enemy-image (first loe)) (+ (enemy-vx (first loe)) (enemy-x (first loe))) (+ (enemy-vy (first loe)) (enemy-y (first loe))) (render-enemies (rest loe) base)))])
)
;; Score -> String
;; produce enemy! if score > 1 otherwise enemies!
(define (enemy-text score)
(cond
[(< score 2) " invader"]
[else " invaders!"]))
(check-expect (enemy-text 0) " invader")
(check-expect (enemy-text 2) " invaders!")
;; ListOfMissile, ListOfEnemy -> Image
;; produce list of missile , list of enemy
(define (render-images lom loe over? score)
(if over?
(place-image (text (string-append "Game over!\n" "You killed " (number->string score) (enemy-text score)) 40 "black") 300 100 (render-enemies loe (render-missiles lom)))
(render-enemies loe (render-missiles lom))))
(check-expect (render-images empty empty false 0) MTS)
(check-expect (render-images (cons (make-missile 0 0 0 MISSILE-IMAGE false) empty) empty false 0)
(place-image MISSILE-IMAGE 0 0 MTS))
(check-expect (render-images empty (cons (make-enemy 0 0 0 0 INVADER-IMAGE false) empty) false 0)
(place-image INVADER-IMAGE 0 0 MTS))
(check-expect (render-images (cons (make-missile 0 0 0 MISSILE-IMAGE false) empty)
(cons (make-enemy 0 0 0 0 INVADER-IMAGE false) empty)
false 0)
(place-image INVADER-IMAGE 0 0 (place-image MISSILE-IMAGE 0 0 MTS)))
(check-expect (render-images empty
empty
true 0)
(place-image (text "Game over!\nYou killed 0 invader" 40 "black") 300 100 MTS))
(check-expect (render-images empty
empty
true 2)
(place-image (text "Game over!\nYou killed 2 invaders!" 40 "black") 300 100 MTS))
;; ListOfEnemy -> Natural
;; produce the number of enemies
(define (count-died-enemies loe acc)
(cond [(empty? loe) acc]
[(enemy-died? (first loe)) (count-died-enemies (rest loe) (+ 1 acc))]
[else (count-died-enemies (rest loe) acc)]))
(check-expect (count-died-enemies empty 0) 0)
(check-expect (count-died-enemies (cons (make-enemy 0 0 0 0 INVADER-IMAGE true) empty) 0) 1)
(check-expect (count-died-enemies (cons (make-enemy 0 0 0 0 INVADER-IMAGE true)
(cons (make-enemy 0 0 0 0 INVADER-IMAGE false) empty)
) 0) 1)
;; Game -> Tank
;; produce moved right tank
(define (move-right g)
(make-game
(make-tank (tank-x (game-tank g)) 1 TANK-IMAGE)
(game-lom g)
(game-loe g)
(game-over? g)
(game-score g)
))
(check-expect (move-right (make-game (make-tank 0 0 TANK-IMAGE) empty empty false 0))
(make-game (make-tank 0 1 TANK-IMAGE)
empty
empty
false
0
))
;; Game -> Tank
;; produce moved left tank
(define (move-left g)
(make-game
(make-tank (tank-x (game-tank g)) -1 TANK-IMAGE)
(game-lom g)
(game-loe g)
(game-over? g)
(game-score g)
))
(check-expect (move-left (make-game (make-tank 1 0 TANK-IMAGE) empty empty false 0))
(make-game (make-tank 1 -1 TANK-IMAGE)
empty
empty
false
0
))
;; Game -> Game
;; append the missile
(define (append-missile g x y)
(make-game
(game-tank g)
(cons (make-missile x y 1 MISSILE-IMAGE false) (game-lom g))
(game-loe g)
(game-over? g)
(game-score g)
))
(check-expect (append-missile (make-game (make-tank 0 0 TANK-IMAGE) empty empty false 0) 50 50)
(make-game
(make-tank 0 0 TANK-IMAGE)
(cons (make-missile 50 50 1 MISSILE-IMAGE false) empty)
empty false 0))
;; Game -> Game
;; append the enemy
(define (append-enemy g x y)
(make-game
(game-tank g)
(game-lom g)
(cons (make-enemy x y 1 1 INVADER-IMAGE false) (game-loe g))
(game-over? g)
(game-score g)
)
)
(check-expect (append-enemy (make-game empty empty empty false 0) 50 50)
(make-game
empty
empty
(cons (make-enemy 50 50 1 1 INVADER-IMAGE false) empty)
false
0
))
;; Game -> Game
;; handle key event
;; when pressed right key,move tank right
;; when pressed left key,move tank left
(define (handle-key g kev)
(cond
[(key=? kev "right") (move-right g)]
[(key=? kev "left") (move-left g)]
[(key=? kev " ") (append-missile
g
(+ (tank-x (game-tank g)) (/ (tank-image-width
(game-tank g)) 2))
(- HEIGHT (tank-image-height (game-tank g))))]
[else g]))
(check-expect (handle-key (make-game (make-tank 0 0 TANK-IMAGE) empty empty false 0) "up")
(make-game (make-tank 0 0 TANK-IMAGE) empty empty false 0))
(check-expect (handle-key (make-game (make-tank 0 0 TANK-IMAGE) empty empty false 0) "left")
(make-game (make-tank 0 -1 TANK-IMAGE)
empty
empty
false
0
))
(check-expect (handle-key (make-game (make-tank 0 0 TANK-IMAGE) empty empty false 0) "right")
(make-game (make-tank 0 1 TANK-IMAGE)
empty
empty
false
0
))
(define (main t)
(big-bang t
(on-tick tock 0.01)
(to-draw render)
(on-key handle-key)
))
(main (make-game (make-tank 0 0 TANK-IMAGE) empty empty false 0))