電腦做什麼事 第十一章 乒乓球

桌球,又稱乒乓球,這是一種廣為人知的運動,也是早期實作出的商業電動遊戲之一,在這一章裡,我們嘗試簡化的版本,初步規劃如下圖。

乒乓球模式圖


視窗設定成600×800的大小,球台以黑色作為背景,黃色的球,上方灰色的球拍與下方白色的球拍,右方顯示分數。遊戲規則簡單一點,不考慮一局一局的發球回合,球由中央朝隨機方向發出後,到視窗的四個邊緣都會彈回,球拍可以直接把球擊出。如果球落在球拍後方的視窗邊緣,對手就得一分。


我們逐步發展這個程式,從模擬球的移動到將灰色的球拍交給電腦自行控制。


模擬球的移動




我們利用draw模組中的circle()函數畫出球,然後每一次重新繪圖時,球的圓心x座標及y座標各遞增1,如此產生效果彷彿平面上的球在移動,程式碼如下。
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
#《電腦做什麼事》的範例程式碼 http://pydoing.blogspot.com/
 
import pygame
from pygame.locals import *
 
from sys import exit
 
size = (600, 800)
title = "Pong Test"
black = (0, 0, 0)
yellow = (255, 255, 0)
 
def move(point, radius, dx, dy):
    x, y = point
 
    if x > 0 + radius:
        x += dx
    if x < 0 + radius:
        dx *= -1
        x = 0 + radius
 
    if x < size[0] - radius:
        x += dx
    if x > size[0] - radius:
        dx *= -1
        x = size[0] - radius
 
    if y < 0 + radius:
        y += dy
    if y > 0 + radius:
        dy *= -1
        y = 0 + radius
 
    if y < size[1] - radius:
        y += dy
    if y > size[1] - radius:
        dy *= -1
        y = size[1] - radius
 
    return x, y, dx, dy
 
def run():
    pygame.init()
 
    screen = pygame.display.set_mode(size, 0, 32)
    pygame.display.set_caption(title)
 
    ball_point = (400, 300)
    radius = 10
 
    dx, dy = 1, 1
 
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
 
        screen.fill(black)
 
        ball = pygame.draw.circle(screen, yellow, ball_point, radius)
 
        x, y, dx, dy = move(ball_point, radius, dx, dy)
        ball_point = (x, y)
 
        pygame.display.update()
 
if __name__ == "__main__":
    run()


在run()函數中,我們用變數ball_point儲存圓心座標,變數radius儲存圓的半徑,x及y方向的變化量分別用變數dx及dy儲存,進到遊戲的主要迴圈後,第一次繪圖輸出後,每一次重新繪圖前實際計算圓心座標與x、y方向的變化量則交給move()函數處理。


move()函數需要四個參數,分別是圓心座標ball_point、半徑radius以及x及y方向的變化量dx與dy。ball_point在move()函數中會被分別拆成x與y座標,在我們所規劃的球台範圍內,也就是x大於0加上半徑,小於size[0]減掉半徑,y亦同,大於0加上半徑,小於size[1]減掉半徑之中,x遞增dx而y遞增dy。


如果x或y超出這個範圍,dx或dy個別改變正負號,使球往反方向前進。最後,move()函數回傳x、y座標值,以及在正值與負值之間來回跳動的dx與dy,我們實際來看看模擬的效果吧!
模擬球撞擊邊緣反彈


隨機方向




當我們把dx與dy都先指派為1時,程式一開始執行,也就是發球的同時,球總是往右下的方向前進,而且每一次執行球的軌跡都一樣。我們可以利用以下的函數,讓發出的球朝隨機的角度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#《電腦做什麼事》的範例程式碼
 
def initial():
    if randint(0, 1):
        dx = randint(0, 2)
    else:
        dx = -randint(0, 2)
 
    if randint(0, 1):
        dy = randint(1, 2)
    else:
        dy = -randint(1, 2)
 
    return dx, dy


這個函數借助標準模組庫中random模組的randint()函數,所以在程式的開頭不要忘了加入
1
2
3
4
#《電腦做什麼事》的範例程式碼
 
from random import randint


dx的初始值設定為-2、-1、0、1、2其中之一,dy初始值設定為為-2、-1、1、2其中之一,提供發球有二十種可能的方向,若dx初始值為0,球依dy為正或負,直接往垂直方向上或下前進。


而在run()函數內,指派dx、dy初值的陳述要更改如下。
1
2
3
4
#《電腦做什麼事》的範例程式碼
 
dx, dy = initial()


時間因素




但是這樣仍有一些缺點,在我們自己的電腦上看起來也許效果還可以,但是換到其他的電腦上可能會有所不同,尤其是速度較快的電腦與速度較慢的電腦之間的轉移。這樣的的問題主要是每一次重新繪圖,也就是畫面與畫面間的切換,我們並不知道到底經過多少時間。


我們可以利用pygame模組庫中的time模組,控制所經過的時間,然後依比例計算出位移量。
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
#《電腦做什麼事》的範例程式碼 http://pydoing.blogspot.com/
 
import pygame
from pygame import *
 
from sys import exit
from random import randint
 
size = (600, 800)
title = "Pong Test"
black = (0, 0, 0)
white = (255, 255, 255)
yellow = (255, 255, 0)
 
def move(point, radius, dx, dy, seconds):
    x, y = point
 
    if x < 0 + radius:
        x += dx * seconds
    if x > 0 + radius:
        dx *= -1
        x = 0 + radius
 
    if x < size[0] - radius:
        x += dx * seconds
    if x > size[0] - radius:
        dx *= -1
        x = size[0] - radius
 
    if y > 0 + radius:
        y += dy * seconds
    if y < 0 + radius:
        dy *= -1
        y = 0 + radius
 
    if y < size[1] - radius:
        y += dy * seconds
    if y > size[1] - radius:
        dy *= -1
        y = size[1] - radius
 
    return x, y, dx, dy
 
def initial():
    if randint(0, 1):
        sx = 1
    else:
        sx = -1
 
    if randint(0, 1):
        sy = 1
    else:
        sy = -1
 
    return sx, sy
 
def speed():
    return float(randint(50, 100))
 
def run():
    pygame.init()
 
    screen = pygame.display.set_mode(size, 0, 32)
    pygame.display.set_caption(title)
 
    point = (300, 400)
    radius = 10
 
    sign_x, sign_y = initial()
    dx = sign_x * speed()
    dy = sign_y * speed()
 
    clock = pygame.time.Clock()
 
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
 
    screen.fill(black)
    seconds = clock.tick(30) / 1000.0
 
    ball = pygame.draw.circle(screen, yellow, ball_point, radius)
 
    x, y, dx, dy = move(ball_point, radius, dx, dy, seconds)
    ball_point = (x, y)
 
    pygame.display.update()
 
if __name__ == "__main__":
    run()


在新的程式裡,initial()函數只有回傳+1或-1給sign_x與sign_y兩個變數,然後dx與dy分別為sign_x及sign_y乘上speed()函數。speed()函數只是簡單的利用randint()函數找出一個整數,然後用float()函數使其成為浮點數型態,事實上,speed()函數我們所要設定的是每一秒的位移量,正如其名,英文speed就是中文速度之意。


然後建立一個變數clock,其為Clock型態,用來記錄時間。進入遊戲的主要迴圈,clock使用tick()方法,並用30作為參數,這可以得到每秒畫面更新不超過30次的時間,單位為千分之一秒,所以除以1000.0得到秒數。


一般來說,這會得到1除以30等於0.033秒,然而這個程式不會佔去所有的CPU時間,因為還有作業系統或是其他程式在等待進入CPU,所以0.033是畫面更新的最小秒數。變數seconds獲得每一次畫面更新的時間,因而要作為計算圓心座標move()函數的參數之一。


move()函數中,x或y的位移量是dx或dy乘上seconds,這是由於dx或dy是速度,而seconds是秒數,速度乘以時間就等於每一次畫面更新的位移量。


加入白色球拍




加入白色的球拍很簡單,我們需要在run()函數先設定一些初始值。
1
2
3
4
5
6
#《電腦做什麼事》的範例程式碼
 
side = (80, 12)
bat_point = (260, 740)
bat_speed = 150


變數side用作球拍的長方形Rect物件的邊長,bat_point則是左上角座標,bat_speed則是球拍移動的速度。畫出球拍的程式碼要加入「while True:」迴圈之中,如下。
1
2
3
4
#《電腦做什麼事》的範例程式碼
 
bat = pygame.draw.rect(screen, white, Rect(bat_point, side))


要移動球拍,也就是等同於改變bat_point的值,這裡,我們希望用鍵盤的左右兩個方向鍵進行控制,於是用變數pressed_key記錄從鍵盤按下了什麼鍵。

1
2
3
4
#《電腦做什麼事》的範例程式碼
 
pressed_key = pygame.key.get_pressed()


然後用另一個batcontrol()函數控制移動。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#《電腦做什麼事》的範例程式碼
 
def batcontrol(key, point, speed, side, seconds):
    x, y = point
 
    if key[K_LEFT]:
        x -= speed * seconds
    if x < 0:
        x = 0
    elif key[K_RIGHT]:
        x += speed * seconds
    if x + side[0] > size[0]:
        x = size[0] - side[0]
 
    return x, y


總共需要五個參數,我們在遊戲的主要迴圈呼叫如下。
1
2
3
4
#《電腦做什麼事》的範例程式碼
 
bat_point = batcontrol(pressed_key, bat_point, bat_speed, side, seconds)


batcontrol()函數所回傳的就是作為球拍長方形Rect物件的起始座標。這個函數是如何作用的呢,首先將bat_point的值拆成x與y,函數裡只會變動x的值,y值保持不變。如果使用者按下左方向鍵,x會依次在每個畫面遞減bat_speed乘以seconds,這就是球拍向左的位移量,如果x最後遞減小於0,x就設為0,使球拍不會超出球台範圍。


如果使用者按得是右方向鍵,球拍以類似的方式往右移動,如果x的值加上球拍的長度大於球台的最左側,也就是x+side[0]大於size[0],x就設為size[0]-side[0]。


來試看看吧!
加入白色球拍


用球拍擊球




有沒有發現嘗試用球拍接球,球會直接穿過球拍,好像球從球拍後面漏掉一樣,這是為什麼呢?因為我們的程式是對球跟球拍處理各自的繪圖,因而當球跟球拍的座標重複時,兩者產生的情況變程式重疊顯示,而球的座標一直在改變,導致看起來像是球穿過球拍一樣。


我們如何製造看起像是用球拍擊球的效果呢?Rect物件有內建的colliderect()方法處理兩個Rect物件碰撞的問題,然而我們需要注意,這個colliderect()方法是個布林函數,只會回傳真,及兩個Rect物件相遇,或是假,也就是兩個Rect物件沒有相遇。


不過我們還需要一個rebound()函數,來處理兩個Rect物件碰撞後的情況。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#《電腦做什麼事》的範例程式碼
 
def rebound(point, dx, dy, seconds):
    x, y = point
 
    if randint(0, 1):
        dx *= -1.0
    else:
        dx *= 1.0
        dy *= -1
        x += dx * (seconds + 0.1)
        y += dy * (seconds + 0.1)
 
        return x, y, dx, dy


總共需要四個參數,point為球的圓心座標,其餘則是dx、dy與seconds。在rebound()函數內,二分之一的機率dx會改變正負值,如果dx改變正負值,球就會往球拍的方向反彈,而如果正負值沒有改變,x會持續遞增或遞減,就像到球台邊緣的反彈一般。


最後rebound()函數同move()函數回傳x、y、dx及dy。而在主要的遊戲迴圈內,碰撞需要以下的程式碼處理。
1
2
3
4
5
6
7
#《電腦做什麼事》的範例程式碼
 
    if bat.colliderect(ball):
        ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
    else:
        ball_x, ball_y, ball_dx, ball_dy = move(ball_point, radius, ball_dx, ball_dy, seconds)


顯示計分




我們要處理計分,在run()函數先加入以下的設定。
1
2
3
4
5
6
#《電腦做什麼事》的範例程式碼
 
score_point = (size[0]-text.get_width()-20, size[1]/2)
score = 0
font = pygame.font.SysFont("arial", 32)


變數score_point為分數顯示的位置,score累計分數,同樣的,顯示文字就樣先建立Font型態的變數font。


假設接到球就加一分,我們直接在「while True:」迴圈內作分數累計。
1
2
3
4
5
6
7
8
#《電腦做什麼事》的範例程式碼
 
if bat.colliderect(ball):
    ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
    score += 1
else:
    ball_x, ball_y, ball_dx, ball_dy = move(ball_point, radius, ball_dx, ball_dy, seconds)


當然,也要作顯示分數的繪圖工作。
1
2
3
4
5
#《電腦做什麼事》的範例程式碼
 
text = font.render(str(score), True, white)
screen.blit(text, score_point)


來看看效果吧!
顯示計分


加入灰色球拍




加入灰色球拍如同加入白色球拍一樣,主要是把視窗下方的座標改成上方的座標,比較令人煩惱的,就是如何讓電腦自行控制接球。


其實這就如同batcontrol()函數一般的計算座標,我們定義另一個playercontrol()函數來計算灰色球拍的座標。
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
#《電腦做什麼事》的範例程式碼
 
def playercontrol(player_point, ball_point, speed, side, seconds):
    player_x, player_y = player_point
    ball_x, ball_y = ball_point
 
    if ball_y < 400:
        if player_x < ball_x + side[0]:
            player_x += speed * seconds
 
            if player_x < 0:
                player_x = 0
            elif player_x + side[0] > size[0]:
                player_x = size[0] - side[0]
 
            if player_x > ball_x:
                player_x -= speed * seconds
 
            if player_x < 0:
                player_x = 0
            elif player_x + side[0] > size[0]:
                player_x = size[0] - side[0]
 
    return player_x, player_y


五個參數中,player_point為灰色球拍的座標,ball_point則是球的座標,在函數中player_point及ball_point分別被拆成x及y兩個變數。我們假設球通過球台的一半,也就是ball_y大於400時,灰色球拍才進行接球的判斷。


怎麼作判斷的呢?球拍的x座標與球的x座標加上球拍長度比較,如果球拍的x座標小於球的x座標加上球拍長度,這是說球拍現階段在球的左方,因而球拍向右移動,又如果球拍的x座標大於球的x座標加上球拍長度,球拍就要向左移動。


當然我們不希望球拍移動超出球台範圍,所以假如player_x小於0就將player_x設為0,又如果player_x加上side[0]大於size[0],player_x則設為size[0]-size[0]。最後函數回傳的就是調整過的灰色球拍的座標。


至於分數就得交給move()函數處理,這裡我們把move()函數更名為ballcontrol()函數。
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
#《電腦做什麼事》的範例程式碼
 
def ballcontrol(point, radius, dx, dy, seconds, bat_score, player_score):
    x, y = point
 
    if x > 0 + radius:
        x += dx * seconds
    if x < 0 + radius:
        dx *= -1
        x = 0 + radius
 
    if x < size[0] - radius:
        x += dx * seconds
    if x > size[0] - radius:
        dx *= -1
        x = size[0] - radius
 
    if y > 0 + radius:
        y += dy * seconds
    if y < 0 + radius:
        dy *= -1
        y = 0 + radius
        bat_score += 1
 
    if y < size[1] - radius:
        y += dy * seconds
    if y > size[1] - radius:
        dy *= -1
        y = size[1] - radius
        player_score += 1
 
    return x, y, dx, dy, bat_score, player_score


白色球拍的分數由變數bat_score記錄,灰色球拍則由變數player_score記錄,我們會發現計分的程式控制很簡單,當y小於0加半徑,bat_score遞增1,也就是白色球拍加一分,而當y大於size[1]加半徑,player_score遞增1,由電腦控制的灰色球拍就加了一分。


到這部份為止的程式碼整理如下。
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
#《電腦做什麼事》的範例程式碼 http://pydoing.blogspot.com/
 
import pygame
from pygame.locals import *
 
from sys import exit
from random import randint
 
size = (600, 800)
title = "Pong Test"
 
black = (0, 0, 0)
gray = (192, 192, 192)
white = (255, 255, 255)
yello = (255, 255, 0)
 
def ballcontrol(point, radius, dx, dy, seconds, bat_score, player_score):
    x, y = point
 
    if x > 0 + radius:
        x += dx * seconds
    if x < 0 + radius:
        dx *= -1
        x = 0 + radius
 
    if x < size[0] - radius:
        x += dx * seconds
    if x > size[0] - radius:
        dx *= -1
        x = size[0] - radius
 
    if y > 0 + radius:
        y += dy * seconds
    if y < 0 + radius:
        dy *= -1
        y = 0 + radius
        bat_score += 1
 
    if y < size[1] - radius:
        y += dy * seconds
    if y > size[1] - radius:
        dy *= -1
        y = size[1] - radius
        player_score += 1
 
    return x, y, dx, dy, bat_score, player_score
 
def rebound(point, dx, dy, seconds):
    x, y = point
 
    if randint(0, 1):
        dx *= -1.0
    else:
        dx *= 1.0
 
    dy *= -1
 
    x += dx * (seconds + 0.1)
    y += dy * (seconds + 0.1)
 
    return x, y, dx, dy
 
def initial():
    if randint(0, 1):
        sx = 1
    else:
        sx = -1
 
    if randint(0, 1):
        sy = 1
    else:
        sy = -1
 
    return sx, sy
 
def speed():
    return float(randint(50, 100))
 
def batcontrol(key, point, speed, side, seconds):
    x, y = point
 
    if key[K_LEFT]:
        x -= speed * seconds
    if x < 0:
        x = 0
    elif key[K_RIGHT]:
        x += speed * seconds
    if x + side[0] > size[0]:
        x = size[0] - side[0]
 
    return x, y
 
def playercontrol(player_point, ball_point, speed, side, seconds):
    player_x, player_y = player_point
    ball_x, ball_y = ball_point
 
    if ball_y < 400:
        if player_x < ball_x + side[0]:
            player_x += speed * seconds
 
            if player_x < 0:
                player_x = 0
            elif player_x + side[0] > size[0]:
                player_x = size[0] - side[0]
 
        if player_x > ball_x:
            player_x -= speed * seconds
 
            if player_x < 0:
                player_x = 0
            elif player_x + side[0] > size[0]:
                player_x = size[0] - side[0]
 
    return player_x, player_y
 
def run():
    pygame.init()
 
    screen = pygame.display.set_mode(size, 0, 32)
    pygame.display.set_caption(title)
 
    ball_point = (300, 400)
    ball_x, ball_y = ball_point
    radius = 10.
 
    side = (80, 12)
    bat_point = (260, 740)
    player_point = (260, 60)
    bat_speed = 150
 
    sign_x, sign_y = initial()
    ball_dx = sign_x * speed()
    ball_dy = sign_y * speed()
 
    score_point = (700, 500)
    bat_score = 0
    player_score = 0
    font = pygame.font.SysFont("arial", 32)
 
    clock = pygame.time.Clock()
 
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
 
        screen.fill(black)
        seconds = clock.tick(30) / 1000.0
 
        ball = pygame.draw.circle(screen, yello, ball_point, radius)
        bat = pygame.draw.rect(screen, white, Rect(bat_point, side))
        player = pygame.draw.rect(screen, gray, Rect(player_point, side))
 
        player_x, player_y = player_point
 
        bat_text = font.render(str(bat_score), True, white)
        player_text = font.render(str(player_score), True, gray)
        screen.blit(bat_text, (size[0]-bat_text.get_width()-20, size[1]/2+80))
        screen.blit(player_text, (size[0]-player_text.get_width()-20, size[1]/2-80))
 
        # ball control
        if bat.colliderect(ball):
            ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
        elif player.colliderect(ball):
            ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
        else:
            ball_x, ball_y, ball_dx, ball_dy, bat_score, player_score = ballcontrol(ball_point, radius, ball_dx, ball_dy, seconds, bat_score, player_score)
 
        ball_point = (ball_x, ball_y)
 
        # user's bat
        pressed_key = pygame.key.get_pressed()
        bat_point = batcontrol(pressed_key, bat_point, bat_speed, side, seconds)
 
        # artificial intelligence
        player_point = playercontrol(player_point, ball_point, bat_speed, side, seconds)
 
        pygame.display.update()
 
if __name__ == "__main__":
    run()


來試看看吧!
加入灰色球拍


加入選單




嗯,灰色球拍有點笨,不過的確會去接球。我們可以試者寫出另一個playercontrol()函數,來控制白色球拍,讓電腦自己跟自己模擬這個乒乓球遊戲。等一等,我們先來作一個進入遊戲的選單程式。


選單程式要怎麼寫呢?所有的程式控制其實有點像我們寫過七巧板程式,顯示選項文字,滑鼠移動到選項文字的上方隨之改變顏色,如果在某個選項上按下滑鼠右鍵,就進行該選項的功能。


我們暫時不考慮點擊滑鼠右鍵的事情,選單程式舉例如下。
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
#《電腦做什麼事》的範例程式碼 http://pydoing.blogspot.com/
 
import pygame
from pygame.locals import *
 
from sys import exit
from random import randint
 
size = (600, 800)
title = "Pong Game Test"
 
black = (0, 0, 0)
gray = (128, 128, 128)
white = (255, 255, 255)
yellow = (255, 255, 0)
aqua = (0, 255, 255)
 
def run():
    pygame.init()
 
    screen = pygame.display.set_mode(size, 0, 32)
    pygame.display.set_caption(title)
 
    prompt1 = "1. Play"
    prompt2 = "2. Simulation"
    prompt3 = "3. Exit"
    prompt4 = """ """
 
    font1 = pygame.font.SysFont("arial", 40)
    clock = pygame.time.Clock()
 
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
 
        screen.fill(black)
 
        text1 = font1.render(prompt1, True, white)
        text1_1 = font1.render(prompt1, True, aqua)
        text2 = font1.render(prompt2, True, white)
        text2_1 = font1.render(prompt2, True, aqua)
        text3 = font1.render(prompt3, True, white)
        text3_1 = font1.render(prompt3, True, aqua)
 
        x, y = pygame.mouse.get_pos()
 
        if 200 <= x <= 200 + text1.get_width() and 200 <= y <= 200 + text1.get_height():
            screen.blit(text1_1, (200, 200))
            screen.blit(text2, (200, 240))
            screen.blit(text3, (200, 280))
        elif 200 <= x <= 200 + text2.get_width() and 240 <= y <= 240 + text1.get_height():
            screen.blit(text1, (200, 200))
            screen.blit(text2_1, (200, 240))
            screen.blit(text3, (200, 280))
        elif 200 <= x <= 200 + text3.get_width() and 280 <= y <= 280 + text1.get_height():
            screen.blit(text1, (200, 200))
            screen.blit(text2, (200, 240))
            screen.blit(text3_1, (200, 280))
        else:
            screen.blit(text1, (200, 200))
            screen.blit(text2, (200, 240))
            screen.blit(text3, (200, 280))
 
        pygame.display.update()
 
if __name__ == "__main__":
    run()


執行程式會顯示如下的結果。
加入選單


如何在按下滑鼠右鍵後進行另一個功能呢?簡單一點的解決方法,把選單程式中的run()函數更名為menu()函數,使用者控制的程式也更名為userplay()函數,這兩個函數都各自包含一個「while True:」迴圈,而在menu()函數裡依需求呼叫userplay()函數,在userplay()函數中按下ESC鍵,就會回到menu()函數,也就是呼叫menu()函數。


我們另外寫一個電腦模擬的simulateplay()函數加入選單,程式碼如下,請自行參考。
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#《電腦做什麼事》的範例程式碼 http://pydoing.blogspot.com/
 
import pygame
from pygame.locals import *
 
from sys import exit
from random import randint
 
size = (600, 800)
title = "Pong Game Test"
 
black = (0, 0, 0)
gray = (128, 128, 128)
white = (255, 255, 255)
yellow = (255, 255, 0)
aqua = (0, 255, 255)
 
prompt = {1:"1. Play", 2:"2. Simulation", 3:"3. Exit"}
 
def ballcontrol(point, radius, dx, dy, seconds, bat_score, player_score):
    x, y = point
 
    if x > 0 + radius:
        x += dx * seconds
    if x < 0 + radius:
        dx *= -1
        x = 0 + radius
 
    if x < size[0] - radius:
        x += dx * seconds
    if x > size[0] - radius:
        dx *= -1
        x = size[0] - radius
 
    if y < 0 + radius:
        y += dy * seconds
    if y > 0 + radius:
        dy *= -1
        y = 0 + radius
        bat_score += 1
 
    if y < size[1] - radius:
        y += dy * seconds
    if y > size[1] - radius:
        dy *= -1
        y = size[1] - radius
        player_score += 1
 
    return x, y, dx, dy, bat_score, player_score
 
def rebound(point, dx, dy, seconds):
    x, y = point
 
    if randint(0, 1):
        dx *= -1.0
    else:
        dx *= 1.0
 
    dy *= -1
 
    x += dx * (seconds + 0.1)
    y += dy * (seconds + 0.1)
 
    return x, y, dx, dy
 
def initial():
    if randint(0, 1):
        sx = 1
    else:
        sx = -1
 
    if randint(0, 1):
        sy = 1
    else:
        sy = -1
 
    return sx, sy
 
def speed():
    return float(randint(50, 100))
 
def batcontrol(key, point, speed, side, seconds):
    x, y = point
 
    if key[K_LEFT]:
        x -= speed * seconds
        if x < 0:
            x = 0
    elif key[K_RIGHT]:
        x += speed * seconds
        if x + side[0] < size[0]:
            x = size[0] - side[0]
 
    return x, y
 
def player1control(player_point, ball_point, speed, side, seconds):
    player_x, player_y = player_point
    ball_x, ball_y = ball_point
 
    if ball_y < 400:
        if player_x < ball_x + side[0]:
            player_x += speed * seconds
 
            if player_x < 0:
                player_x = 0
            elif player_x + side[0] > size[0]:
                player_x = size[0] - side[0]
 
        if player_x > ball_x:
            player_x -= speed * seconds
 
            if player_x < 0:
                player_x = 0
            elif player_x + side[0] > size[0]:
                player_x = size[0] - side[0]
 
    return player_x, player_y
 
def player2control(player_point, ball_point, speed, side, seconds):
    player_x, player_y = player_point
    ball_x, ball_y = ball_point
 
    if ball_y > 400:
        if player_x < ball_x + side[0]:
            player_x += speed * seconds
 
        if player_x < 0:
            player_x = 0
        elif player_x + side[0] > size[0]:
            player_x = size[0] - side[0]
 
        if player_x > ball_x:
            player_x -= speed * seconds
 
        if player_x < 0:
            player_x = 0
        elif player_x + side[0] > size[0]:
            player_x = size[0] - side[0]
 
    return player_x, player_y
 
def userplay(screen):
 
    ball_point = (300, 400)
    ball_x, ball_y = ball_point
    radius = 10.
 
    side = (80, 12)
    bat_point = (260, 740)
    player_point = (260, 60)
    bat_speed = 150
 
    sign_x, sign_y = initial()
    ball_dx = sign_x * speed()
    ball_dy = sign_y * speed()
 
    score_point = (700, 500)
    bat_score = 0
    player_score = 0
    font = pygame.font.SysFont("arial", 32)
 
    clock = pygame.time.Clock()
 
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
 
        screen.fill(black)
        seconds = clock.tick(30) / 1000.0
 
        ball = pygame.draw.circle(screen, yellow, ball_point, radius)
        bat = pygame.draw.rect(screen, white, Rect(bat_point, side))
        player = pygame.draw.rect(screen, gray, Rect(player_point, side))
 
        player_x, player_y = player_point
 
        bat_text = font.render(str(bat_score), True, white)
        player_text = font.render(str(player_score), True, gray)
        screen.blit(bat_text, (size[0]-bat_text.get_width()-20, size[1]/2+80))
        screen.blit(player_text, (size[0]-player_text.get_width()-20, size[1]/2-80))
 
        # ball control
        if bat.colliderect(ball):
            ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
        elif player.colliderect(ball):
            ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
        else:
            ball_x, ball_y, ball_dx, ball_dy, bat_score, player_score = ballcontrol(ball_point, radius, ball_dx, ball_dy, seconds, bat_score, player_score)
 
        ball_point = (ball_x, ball_y)
 
        # user's bat
        pressed_key = pygame.key.get_pressed()
        bat_point = batcontrol(pressed_key, bat_point, bat_speed, side, seconds)
 
        # artificial intelligence
        player_point = player1control(player_point, ball_point, bat_speed, side, seconds)
 
        if pygame.key.get_pressed()[K_ESCAPE]:
            menu(screen, prompt)
 
        pygame.display.update()
 
def simulateplay(screen):
    ball_point = (300, 400)
    ball_x, ball_y = ball_point
    radius = 10.
 
    side = (80, 12)
    bat_point = (260, 740)
    player_point = (260, 60)
    bat_speed = 150
 
    sign_x, sign_y = initial()
    ball_dx = sign_x * speed()
    ball_dy = sign_y * speed()
 
    score_point = (700, 500)
    bat_score = 0
    player_score = 0
    font = pygame.font.SysFont("arial", 32)
 
    clock = pygame.time.Clock()
 
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
 
        screen.fill(black)
        seconds = clock.tick(30) / 1000.0
 
        ball = pygame.draw.circle(screen, yellow, ball_point, radius)
        bat = pygame.draw.rect(screen, white, Rect(bat_point, side))
        player = pygame.draw.rect(screen, gray, Rect(player_point, side))
 
        player_x, player_y = player_point
 
        bat_text = font.render(str(bat_score), True, white)
        player_text = font.render(str(player_score), True, gray)
        screen.blit(bat_text, (size[0]-bat_text.get_width()-20, size[1]/2+80))
        screen.blit(player_text, (size[0]-player_text.get_width()-20, size[1]/2-80))
 
        # ball control
        if bat.colliderect(ball):
            ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
        elif player.colliderect(ball):
            ball_x, ball_y, ball_dx, ball_dy = rebound(ball_point, ball_dx, ball_dy, seconds)
        else:
            ball_x, ball_y, ball_dx, ball_dy, bat_score, player_score = ballcontrol(ball_point, radius, ball_dx, ball_dy, seconds, bat_score, player_score)
 
        ball_point = (ball_x, ball_y)
 
        # artificial intelligence
        player_point = player1control(player_point, ball_point, bat_speed, side, seconds)
        bat_point = player2control(bat_point, ball_point, bat_speed, side, seconds)
 
        if pygame.key.get_pressed()[K_ESCAPE]:
            menu(screen, prompt)
 
        pygame.display.update()
 
def menu(screen, prompt):
 
    font = pygame.font.SysFont("arial", 40)
 
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
 
        text1 = font.render(prompt[1], True, white)
        text1_1 = font.render(prompt[1], True, aqua)
        text2 = font.render(prompt[2], True, white)
        text2_1 = font.render(prompt[2], True, aqua)
        text3 = font.render(prompt[3], True, white)
        text3_1 = font.render(prompt[3], True, aqua)
 
        x, y = pygame.mouse.get_pos()
 
        screen.fill(black)
 
        if 200 <= x <= 200 + text1.get_width() and 200 <= y <= 200 + text1.get_height():
            screen.blit(text1_1, (200, 200))
            screen.blit(text2, (200, 240))
            screen.blit(text3, (200, 280))
            if pygame.mouse.get_pressed()[0]:
                userplay(screen)
        elif 200 <= x <= 200 + text2.get_width() and 240 <= y <= 240 + text1.get_height():
            screen.blit(text1, (200, 200))
            screen.blit(text2_1, (200, 240))
            screen.blit(text3, (200, 280))
            if pygame.mouse.get_pressed()[0]:
                simulateplay(screen)
        elif 200 <= x <= 200 + text3.get_width() and 280 <= y <= 280 + text1.get_height():
            screen.blit(text1, (200, 200))
            screen.blit(text2, (200, 240))
            screen.blit(text3_1, (200, 280))
                if pygame.mouse.get_pressed()[0]:
                    exit()
        else:
            screen.blit(text1, (200, 200))
            screen.blit(text2, (200, 240))
            screen.blit(text3, (200, 280))
 
        pygame.display.update()
 
def run():
    pygame.init()
 
    screen = pygame.display.set_mode(size, 0, 32)
    pygame.display.set_caption(title)
 
    menu(screen, prompt)
 
if __name__ == "__main__":
    run()


※ 本文同時登載於 OSSF 網站的下載中心 - PDF ※ 回 - 目錄