python：pygameによるSPI試験みたいなアプリの試作

## 概要
この記事内では，pythonのpygameライブラリを用いて計算問題のアプリケーション（アプリ）の作成方法を紹介します．アプリ内では以下の2個の特徴を含みますので，似たような機能を作成したい方はぜひご参照ください．

1. 2桁×1桁の計算の自動生成
1. プロセスサークル（プロセスバーの円形のもの）の作り方

動作の様子は以下です．
<iframe width="560" height="315" src="https://www.youtube.com/embed/vrd5Oc9W99s?si=HygDR4_5Ic_ceCFp" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

右上のプロセスバーは7sで一周します．また正解と不正解によって正解数と不正解数が増えます．

### 作成したプログラム
はじめにコード全体を載せます．
```python
import pygame
import random
import time
import numpy as np
import sys
PI = np.pi
class CalculationApp:
    
    def __init__(self):
        
        #initialization
        pygame.init()
        
        #screen
        self.screen_width = 400
        self.screen_height = 250
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        pygame.display.set_caption("Calculation App")

        #font
        self.font = pygame.font.Font(None, 50)
        self.font_text = pygame.font.Font(None,35)
        self.font_answer = pygame.font.Font(None,22)

        # create questions
        self.num1 = random.randint(10, 99)
        self.num2 = random.randint(1, 9)
        self.answer = ""
        
        #limit
        self.time_limit = 10
        self.remaining_time = self.time_limit
        
        # arc param
        self.center = (350, 45)
        self.aRadius = 20
        self.start_angle = -2*PI/2
        self.end_angle = PI/2 
        self.arc_color = (0,255, 0)
        self.width = 15
        
        # answer
        self.correct_num = 0
        self.incorrect_num = 0
        self.num = 0
        
        # time
        self.start_time = time.time()
        self.end_time = time.time()
        self.elapsed_time = self.end_time-self.start_time
        

    def update(self):
        # fill white
        self.screen.fill((255, 255, 255))
        # question
        question_text = self.font.render(f"{self.num1} × {self.num2} =", True, (0, 0, 0))
        self.screen.blit(question_text, (70, 130))
        # answer
        answer_text = self.font.render(self.answer, True, (0, 0, 0))
        self.screen.blit(answer_text, (210, 130))
        # text
        issue_text = self.font_text.render("Calculate 100 questions!!",True,(0,0,0))
        self.screen.blit(issue_text,(10,40))
        remain_text = self.font_answer.render(f"{self.num}/ 100",True,(0,0,0))
        self.screen.blit(remain_text,(340,230))
        # correct and incorrect 
        correct = self.font_answer.render(f"Correct Answers:{self.correct_num} Incorrect Answers:{self.incorrect_num}",True,(0,0,0))
        self.screen.blit(correct,(10,230))
        # process circle
        self.start_angle,self.end_angle = self.updateprocesscircle()
        pygame.draw.arc(
            self.screen, self.arc_color, 
            (self.center[0] - self.aRadius, self.center[1] - self.aRadius, self.aRadius * 2, self.aRadius * 2), 
            self.start_angle, 
            self.end_angle,
            width = self.width)
        # draw
        pygame.display.flip()
        
    def updateprocesscircle(self):
        self.end_time = time.time()
        self.elapsed_time = self.end_time-self.start_time
        # 7 seconds
        if(self.elapsed_time>7.0):
            self.start_time = time.time()
            self.end_time = time.time()
            rate = PI/2
            self.timeup()
        else:
            rate = PI/2-(self.elapsed_time/7.0)*2*PI
            
        return rate,PI/2
    
    def timeclear(self):
        self.start_time = time.time()
        self.end_time = time.time()
        
    def timeup(self):
        self.num1 = random.randint(10, 99)
        self.num2 = random.randint(1, 9)
        self.incorrect_num += 1
        self.num += 1
        self.answer = ""
        
    def run(self):
        # ゲームループ
        running = True
        while(running):
            for event in pygame.event.get():
                if(event.type == pygame.QUIT):
                    running = False
                if(event.type == pygame.KEYDOWN):
                    if(event.key == pygame.K_ESCAPE):
                        running = False
                    elif(event.key == pygame.K_RETURN):
                        try:
                            user_answer = int(self.answer)
                            correct_answer = self.num1 * self.num2
                            # correct 
                            if(user_answer == correct_answer and self.num<100):
                                self.num1 = random.randint(10, 99)
                                self.num2 = random.randint(1, 9)
                                self.correct_num += 1
                                self.num += 1
                                self.answer = ""
                                self.timeclear()
                            # incorrect
                            elif(user_answer != correct_answer and self.num<100):
                                self.incorrect_num += 1
                                self.num += 1
                                self.num1 = random.randint(10, 99)
                                self.num2 = random.randint(1, 9)
                                self.answer = ""
                                self.timeclear()
                            else:
                                running = False
                
                        except ValueError:
                            # when the input is wrong
                            self.answer = ""
                    elif(event.key in (pygame.K_0, pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, pygame.K_5, pygame.K_6, pygame.K_7, pygame.K_8, pygame.K_9)):
                        # add
                        self.answer += event.unicode
                    elif(event.key == pygame.K_BACKSPACE):
                        self.answer = ""
                        
            self.update()

        # when runnning is false
        pygame.quit()
        sys.exit()

if __name__ == "__main__":
    app = CalculationApp()
    app.run()
```
以上が全体のコードです．以下では計算方法の追加部分と視覚的な残り時間の提示方法を書きます．

### 2桁×1桁の計算の自動生成

コードの主な該当部分はrun()とupdate()です．

```python
    ###########
    try:
        user_answer = int(self.answer)
        correct_answer = self.num1 * self.num2
    ###########
```
ここでは正答のチェックを行っています．
```python
def update(self):
        ######################
        # question
        question_text = self.font.render(f"{self.num1} × {self.num2} =", True, (0, 0, 0))
        self.screen.blit(question_text, (70, 130))
        ######################
```
update内ではtextの描画を担当しています．question_textを変更することで任意の問題に変更できます．

### 視覚的な残り時間の提示（プロセスサークル）
pygameのpygame.draw.arc()がややプロセスサークルを作成するのに不便だったので注意です．
draw.arcのパラメータはpygame.draw.arc(スクリーン, 色, 描画する領域（四角形）, start angle, end angle)が必須になるのですが，angleに基本的に正の値を与えると，例えば start angle = 0, end angle = PIとしてend angleを増加させると反時計回りになってしまいます．
そこでプログラムではend angleを残り時間によって減少させています．

```python
def updateprocesscircle(self):
        self.end_time = time.time()
        self.elapsed_time = self.end_time-self.start_time
        # 7 seconds
        if(self.elapsed_time>7.0):
            self.start_time = time.time()
            self.end_time = time.time()
            rate = PI/2
            self.timeup()
        else:
            rate = PI/2-(self.elapsed_time/7.0)*2*PI
            
        return rate,PI/2
```
## なぜ作ったか?（追記）

作成した背景としては，ストレスがかかりそうなタスクを行う必要があり，かつプロセスバーみたいなものを作成したかったからです．