シェルスクリプトマガジン

香川大学SLPからお届け!(Vol.98掲載)

著者:井上竜輔

最終回では、筆者が作成したプログラミング学習アプリの概要を紹介します。このアプリは、AIなどの外部APIを活用して、オンラインで動的に問題を生成、実行、評価できるものです。安全性を考慮して、ユーザーが提出したプログラムは「Judge0 CE」というオンラインコード実行システムで、実行、評価します。

シェルスクリプトマガジン Vol.98は以下のリンク先でご購入できます。

図7 「components/CodeEditor.tsx」ファイルのコード(抜粋)

import React from 'react';
import Editor from '@monaco-editor/react';
import { Select, MenuItem, FormControl, InputLabel, Box } from '@mui/material';
(略)
const CodeEditor: React.FC<Props> = ({ language, setLanguage, code, setCode }) => {
    return (
        <Box sx={{ mt: 2 }}>
            <FormControl sx={{ mb: 1, minWidth: 120 }}>
                <InputLabel>Language</InputLabel>
                <Select
                    value={language}
                    onChange={(e) => setLanguage(e.target.value)}
                    label="Language"
                >
                    <MenuItem value="python">Python</MenuItem>
                    <MenuItem value="cpp">C++</MenuItem>
                    <MenuItem value="javascript">JavaScript</MenuItem>
                </Select>
            </FormControl>
            <Editor
                height="40vh"
                language={language}
                value={code}
                onChange={(value) => setCode(value || '')}
                theme="vs-dark"
            />
        </Box>
    );
};

図8 handleSubmitCode関数の定義コード

const handleSubmitCode = async () => {
    if (!problem) return;
    setIsSubmitting(true);
    let finalVerdict = "AC (Accepted)";
    // サンプルケースごとにコードを実行、検証
    for (let i = 0; i < problem.samples.length; i++) {
        const sample = problem.samples[i];
        try {
            // APIを呼び出してコードを実行
            const judgeResult = await submitCode(language, code, sample.input);
            // 結果を検証
            if (judgeResult.status.id === 3) {
                const userOutput = (judgeResult.stdout || '').trim();
                const expectedOutput = sample.output.trim();
                if (userOutput !== expectedOutput) {
                    finalVerdict = "WA (Wrong Answer)";
                }
            } else {
                finalVerdict = judgeResult.status.description;
            }
        } catch (error) {
        }
        // 最初の失敗で処理を中断
        if (finalVerdict !== "AC (Accepted)") {
            break;
        }
        // APIのレート制限を避けるための待ち時間
        if (i < problem.samples.length - 1) {
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    }
    setResult(finalVerdict);
    setIsSubmitting(false);
};

図9 フロントエンドからのリクエストを受け取るコード

import type { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
// Judge0 CEの言語IDをマッピング
const LANGUAGE_MAP: { [key: string]: number } = {
    python: 71, // Python 3.8.1
    cpp: 54,    // C++ (GCC 9.2.0)
    javascript: 63, // JavaScript (Node.js 12.14.0)
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    if (req.method !== 'POST') {
        return res.status(405).json({ error: 'メソッドはポストではありません。' });
    }
    const { language, code, stdin } = req.body;
    if (!language || !code || !stdin) {
        return res.status(400).json({ error: '要素が足りません。' });
    }
    const languageId = LANGUAGE_MAP[language];
    if (!languageId) {
        return res.status(400).json({ error: 対応していない言語です。 ${language} });
    }
(略)
}

図10 Judge0 CEのAPIにリクエストを送り、結果をフロントエンドに返すコード

const judge0ApiKey = process.env.NEXT_PUBLIC_JUDGE0_API_KEY;
const options = {
    method: 'POST',
    url: 'https://judge0-ce.p.rapidapi.com/submissions',
    params: {
        base64_encoded: 'false',
        wait: 'true', // 実行完了まで待機する
    },
    headers: {
        'x-rapidapi-host': 'judge0-ce.p.rapidapi.com',
        'x-rapidapi-key': judge0ApiKey, // 環境変数から読み込んだAPIキー
        'content-type': 'application/json',
    },
    data: {
        language_id: languageId,
        source_code: code,
        stdin: stdin,
    },
};
try {
    const submissionResponse = await axios.request(options);
    // Judge0 CEからの結果をフロントエンドに返す
    res.status(200).json(submissionResponse.data);
} catch (error) {/* エラーハンドリング*/}

図11 Geminiに送信するプロンプトの例

あなたは優秀な競技プログラミングの問題作成者です。
以下の制約に従って、新しいプログラミング問題を1つ作成してください。

# 制約
- 難易度: AtCoder ProblemsのDifficulty基準で「B」レベル
- トピック: 配列の操作、または文字列の探索
- 入力: 標準入力から受け取ること
- 出力: 標準出力へ書き出すこと
- 形式: 以下のJSONフォーマットに従うこと

# JSONフォーマット
{
  "title": "問題タイトル",
  "statement": "問題文。背景やストーリーを簡潔に記述。",
  "constraints": [
    "Nは1以上100以下の整数",
    "Sは英小文字からなる長さNの文字列"
  ],
  "input_format": "入力形式の説明",
  "output_format": "出力形式の説明",
  "samples": [
    { "input": "入力例1", "output": "出力例1" },
    { "input": "入力例2", "output": "出力例2" },
    { "input": "入力例3", "output": "出力例3" }
  ],
  "solution_code": "Pythonによる解答例のコード"
}