From ec55444fec481346b1b81c3034109da54d80f2e9 Mon Sep 17 00:00:00 2001 From: kawakami-kohei-ai Date: Tue, 16 Dec 2025 07:59:43 +0000 Subject: [PATCH] =?UTF-8?q?=E3=83=9D=E3=83=A2=E3=83=89=E3=83=BC=E3=83=AD?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=9E=E3=83=BC=E6=A9=9F=E8=83=BD=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/agents/beastmode3.1.agent.md | 2 +- .vscode/mcp.json | 9 + app.py | 16 ++ architecture.md | 76 +++++++ deliverManager.py | 254 ----------------------- features.md | 44 ++++ main.py | 0 old/deliverManager.py | 296 +++++++++++++++++++++++++++ old/fibonacci.py | 15 ++ old/main.py | 7 + old/point.py | 16 ++ old/test_deliver_manager.py | 101 +++++++++ plan.md | 61 ++++++ point.py | 14 -- requirements.txt | 1 + static/css/style.css | 48 +++++ static/js/timer.js | 94 +++++++++ templates/index.html | 25 +++ tests/test_app.py | 23 +++ 19 files changed, 833 insertions(+), 269 deletions(-) create mode 100644 .vscode/mcp.json create mode 100644 app.py create mode 100644 architecture.md delete mode 100644 deliverManager.py create mode 100644 features.md delete mode 100644 main.py create mode 100644 old/deliverManager.py create mode 100644 old/fibonacci.py create mode 100644 old/main.py create mode 100644 old/point.py create mode 100644 old/test_deliver_manager.py create mode 100644 plan.md delete mode 100644 point.py create mode 100644 requirements.txt create mode 100644 static/css/style.css create mode 100644 static/js/timer.js create mode 100644 templates/index.html create mode 100644 tests/test_app.py diff --git a/.github/agents/beastmode3.1.agent.md b/.github/agents/beastmode3.1.agent.md index 8dee5d7d..b13d3a51 100644 --- a/.github/agents/beastmode3.1.agent.md +++ b/.github/agents/beastmode3.1.agent.md @@ -1,6 +1,6 @@ --- description: Beast Mode 3.1 -tools: ['extensions', 'codebase', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'terminalSelection', 'terminalLastCommand', 'openSimpleBrowser', 'fetch', 'findTestFiles', 'searchResults', 'githubRepo', 'runCommands', 'runTasks', 'editFiles', 'runNotebooks', 'search', 'new'] +tools: ['vscode', 'execute/getTerminalOutput', 'execute/runTask', 'execute/getTaskOutput', 'execute/createAndRunTask', 'execute/runInTerminal', 'execute/runNotebookCell', 'execute/testFailure', 'read', 'edit/editFiles', 'search', 'web'] --- # Beast Mode 3.1 diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000..b89dad06 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + "servers": { + "my-mcp-server-47aa837c": { + "url": "https://api.githubcopilot.com/mcp/", + "type": "http" + } + }, + "inputs": [] +} \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 00000000..8a1e4865 --- /dev/null +++ b/app.py @@ -0,0 +1,16 @@ +from flask import Flask, render_template +import os + +def create_app(): + app = Flask(__name__) + + @app.route('/') + def index(): + return render_template('index.html') + + return app + +if __name__ == "__main__": + app = create_app() + port = int(os.environ.get("PORT", 5000)) + app.run(debug=True, host="0.0.0.0", port=port) diff --git a/architecture.md b/architecture.md new file mode 100644 index 00000000..692e835e --- /dev/null +++ b/architecture.md @@ -0,0 +1,76 @@ +# PomodoroタイマーWebアプリケーション アーキテクチャ案 + +## 1. ディレクトリ構成例 + +``` +/workspaces/2025-Github-Copilot-Workshop-Python/ +├── app.py # Flaskアプリ本体(ファクトリパターン) +├── requirements.txt # 依存パッケージ +├── static/ +│ ├── css/ +│ │ └── style.css +│ └── js/ +│ └── timer.js +├── templates/ +│ └── index.html +├── timer_core.py # タイマーロジック(Pure Pythonクラス) +├── tests/ +│ └── test_app.py +└── architecture.md # 本ドキュメント +``` + +## 2. 構成要素と役割 + +- **Flask (app.py)** + - アプリケーションファクトリパターンで実装 + - ルーティング(トップページ、APIエンドポイント) + - 必要に応じてセッション管理やユーザー管理 + +- **HTML (templates/index.html)** + - UIモックに基づく画面レイアウト + - Jinja2テンプレートで動的要素を埋め込む場合も + +- **CSS (static/css/style.css)** + - UIモックに忠実なデザイン + +- **JavaScript (static/js/timer.js)** + - タイマーのカウントダウン・状態管理 + - スタート/ストップ/リセット等のUI操作 + - 必要に応じてAPIと非同期通信(fetch/AJAX) + - ロジックとUI操作を分離し、テストしやすく + +- **タイマーロジック (timer_core.py)** + - タイマーの状態遷移や計算ロジックをPythonクラスとして分離 + - 単体テスト容易 + +- **API層** + - DBや外部サービスへの依存はインターフェース化 + - テスト時はモックやインメモリ実装に差し替え可能 + +## 3. テスト容易性のための工夫 + +- Flaskアプリはファクトリパターンで作成し、テスト時に設定や依存を柔軟に差し替え可能 +- タイマーやビジネスロジックはクラス/関数として分離し、個別にユニットテスト可能 +- APIやDBアクセスはインターフェース化し、テスト時に差し替え可能 +- テスト用設定・テスト用DB(例: SQLite in-memory)を用意 +- テストコードは`/tests`ディレクトリで管理し、pytest等で自動テスト +- JavaScriptもロジック部分は関数化し、テストしやすく + +## 4. バックエンドとフロントエンドの役割分担 + +- タイマーのロジックは基本的にフロントエンド(JS)で管理 + - シンプルな場合はサーバー側の状態管理不要 + - ユーザーごとに進捗や履歴を保存したい場合はAPI設計(FlaskでREST APIを追加) +- サーバー側(Flask)は主にHTML配信と必要なAPIのみ + - 例:ポモドーロ履歴の保存/取得、ユーザー認証 + +## 5. 開発・運用の流れ + +1. UIモックに忠実なHTML/CSS/JSを作成し、ローカルで動くタイマーを実装 +2. Flaskで静的ファイル・テンプレート配信を構築 +3. 必要に応じてAPI設計・DB連携(履歴保存など)を追加 +4. テストコードも並行して整備 + +--- + +このアーキテクチャにより、保守性・拡張性・テスト容易性の高いWebアプリ開発が可能です。 diff --git a/deliverManager.py b/deliverManager.py deleted file mode 100644 index 6f07105d..00000000 --- a/deliverManager.py +++ /dev/null @@ -1,254 +0,0 @@ -import time -import random -from typing import List, Callable, Optional -from dataclasses import dataclass, field -from enum import Enum - - -class EventArgs: - """イベント引数の基底クラス""" - pass - - -class Event: - """C#のeventに相当するクラス""" - - def __init__(self): - self._handlers: List[Callable] = [] - - def add_handler(self, handler: Callable): - """イベントハンドラーを追加""" - if handler not in self._handlers: - self._handlers.append(handler) - - def remove_handler(self, handler: Callable): - """イベントハンドラーを削除""" - if handler in self._handlers: - self._handlers.remove(handler) - - def invoke(self, sender, args: EventArgs = None): - """イベントを発火""" - for handler in self._handlers: - handler(sender, args or EventArgs()) - - -@dataclass -class KitchenObjectSO: - """キッチンオブジェクトのデータクラス""" - name: str - object_id: int - - -@dataclass -class RecipeSO: - """レシピのデータクラス""" - name: str - kitchen_object_so_list: List[KitchenObjectSO] = field(default_factory=list) - - -@dataclass -class RecipeListSO: - """レシピリストのデータクラス""" - recipe_so_list: List[RecipeSO] = field(default_factory=list) - - -class PlateKitchenObject: - """皿のキッチンオブジェクト""" - - def __init__(self): - self._kitchen_object_so_list: List[KitchenObjectSO] = [] - - def add_kitchen_object(self, kitchen_object: KitchenObjectSO): - """キッチンオブジェクトを追加""" - self._kitchen_object_so_list.append(kitchen_object) - - def get_kitchen_object_so_list(self) -> List[KitchenObjectSO]: - """キッチンオブジェクトリストを取得""" - return self._kitchen_object_so_list.copy() - - -class KitchenGameManager: - """キッチンゲームマネージャー(Singleton)""" - - _instance: Optional['KitchenGameManager'] = None - - def __init__(self): - self._is_game_playing = False - - @classmethod - def get_instance(cls) -> 'KitchenGameManager': - """Singletonインスタンスを取得""" - if cls._instance is None: - cls._instance = cls() - return cls._instance - - def is_game_playing(self) -> bool: - """ゲームが進行中かどうか""" - return self._is_game_playing - - def start_game(self): - """ゲーム開始""" - self._is_game_playing = True - - def stop_game(self): - """ゲーム停止""" - self._is_game_playing = False - - -class DeliveryManager: - def get_recipe_by_name(self, user_input): - query = f"SELECT * FROM recipes WHERE name = '{user_input}'" - print(f"実行クエリ: {query}") - return query - - """配達管理クラス(Python版)""" - - _instance: Optional['DeliveryManager'] = None - - def __init__(self, recipe_list_so: RecipeListSO): - # イベント定義 - self.on_recipe_spawned = Event() - self.on_recipe_completed = Event() - self.on_recipe_success = Event() - self.on_recipe_failed = Event() - - # プライベート変数 - self._recipe_list_so = recipe_list_so - self._waiting_recipe_so_list: List[RecipeSO] = [] - self._spawn_recipe_timer = 0.0 - self._spawn_recipe_timer_max = 4.0 - self._waiting_recipes_max = 4 - self._successful_recipes_amount = 0 - self._last_update_time = time.time() - - @classmethod - def get_instance(cls, recipe_list_so: RecipeListSO = None) -> 'DeliveryManager': - """Singletonインスタンスを取得""" - if cls._instance is None: - if recipe_list_so is None: - raise ValueError("初回作成時にはrecipe_list_soが必要です") - cls._instance = cls(recipe_list_so) - return cls._instance - - def update(self): - """フレーム更新処理(UnityのUpdate相当)""" - current_time = time.time() - delta_time = current_time - self._last_update_time - self._last_update_time = current_time - - self._spawn_recipe_timer -= delta_time - - if self._spawn_recipe_timer <= 0.0: - self._spawn_recipe_timer = self._spawn_recipe_timer_max - - kitchen_game_manager = KitchenGameManager.get_instance() - if (kitchen_game_manager.is_game_playing() and - len(self._waiting_recipe_so_list) < self._waiting_recipes_max): - - # ランダムにレシピを選択 - waiting_recipe_so = random.choice(self._recipe_list_so.recipe_so_list) - self._waiting_recipe_so_list.append(waiting_recipe_so) - - # イベント発火 - self.on_recipe_spawned.invoke(self) - - def deliver_recipe(self, plate_kitchen_object: PlateKitchenObject): - """レシピの材料と皿の材料が一致しているかどうかを確認する""" - - for i, waiting_recipe_so in enumerate(self._waiting_recipe_so_list): - plate_ingredients = plate_kitchen_object.get_kitchen_object_so_list() - - # 材料数が一致するかチェック - if len(waiting_recipe_so.kitchen_object_so_list) == len(plate_ingredients): - plate_contents_matches_recipe = True - - # レシピの各材料をチェック - for recipe_kitchen_object_so in waiting_recipe_so.kitchen_object_so_list: - ingredient_found = False - - # 皿の材料と照合 - for plate_kitchen_object_so in plate_ingredients: - if plate_kitchen_object_so == recipe_kitchen_object_so: - ingredient_found = True - break - - if not ingredient_found: - plate_contents_matches_recipe = False - break - - # 材料が完全に一致した場合 - if plate_contents_matches_recipe: - self._successful_recipes_amount += 1 - self._waiting_recipe_so_list.pop(i) - - # 成功イベント発火 - self.on_recipe_completed.invoke(self) - self.on_recipe_success.invoke(self) - return - - # 一致するレシピが見つからなかった場合 - self.on_recipe_failed.invoke(self) - - def get_waiting_recipe_so_list(self) -> List[RecipeSO]: - """待機中のレシピリストを取得""" - return self._waiting_recipe_so_list.copy() - - def get_successful_recipes_amount(self) -> int: - """成功したレシピ数を取得""" - return self._successful_recipes_amount - - -# 使用例 -if __name__ == "__main__": - # サンプルデータ作成 - tomato = KitchenObjectSO("Tomato", 1) - lettuce = KitchenObjectSO("Lettuce", 2) - bread = KitchenObjectSO("Bread", 3) - - # サンプルレシピ - sandwich_recipe = RecipeSO("Sandwich", [bread, lettuce, tomato]) - salad_recipe = RecipeSO("Salad", [lettuce, tomato]) - - recipe_list = RecipeListSO([sandwich_recipe, salad_recipe]) - - # ゲームマネージャーとデリバリーマネージャーを初期化 - game_manager = KitchenGameManager.get_instance() - game_manager.start_game() - - delivery_manager = DeliveryManager.get_instance(recipe_list) - - # イベントハンドラーの設定 - def on_recipe_spawned(sender, args): - print("新しいレシピが生成されました!") - - def on_recipe_success(sender, args): - print("レシピ配達成功!") - - def on_recipe_failed(sender, args): - print("レシピ配達失敗...") - - delivery_manager.on_recipe_spawned.add_handler(on_recipe_spawned) - delivery_manager.on_recipe_success.add_handler(on_recipe_success) - delivery_manager.on_recipe_failed.add_handler(on_recipe_failed) - - # サンプル実行 - print("ゲーム開始...") - - # 5秒間更新処理を実行 - start_time = time.time() - while time.time() - start_time < 5: - delivery_manager.update() - time.sleep(0.1) # 100ms間隔で更新 - - print(f"待機中のレシピ数: {len(delivery_manager.get_waiting_recipe_so_list())}") - - # サンプル配達テスト - plate = PlateKitchenObject() - plate.add_kitchen_object(bread) - plate.add_kitchen_object(lettuce) - plate.add_kitchen_object(tomato) - - print("サンドイッチを配達...") - delivery_manager.deliver_recipe(plate) - - print(f"成功したレシピ数: {delivery_manager.get_successful_recipes_amount()}") \ No newline at end of file diff --git a/features.md b/features.md new file mode 100644 index 00000000..be56f56e --- /dev/null +++ b/features.md @@ -0,0 +1,44 @@ +# ポモドーロタイマーWebアプリケーション 実装機能一覧 + +## 必須機能 + +### 1. タイマー機能 +- ポモドーロ(作業)タイマーのカウントダウン +- 休憩タイマー(短・長)のカウントダウン +- スタート/一時停止/リセットボタン +- タイマーの残り時間表示 +- タイマー終了時の通知(アラート・音・バイブ等) + +### 2. セッション管理 +- ポモドーロ・休憩の自動切り替え +- 1サイクルごとの進捗表示(例:ポモドーロ4回で長休憩) + +### 3. UI/UX +- UIモックに基づく画面レイアウト +- ステータス表示(現在のモード:作業/休憩/長休憩) +- セッション数・進捗の可視化 +- レスポンシブデザイン(スマホ対応) + +### 4. 設定 +- 作業時間・休憩時間・長休憩時間のカスタマイズ +- サウンドON/OFF、通知ON/OFF + +### 5. 履歴・統計(任意/拡張) +- 日ごとのポモドーロ回数・履歴表示 +- 累計作業時間・達成数の表示 + +### 6. データ保存 +- ローカルストレージによる設定・履歴保存(最低限) +- サーバー側保存(ユーザー管理や多端末同期が必要な場合) + +### 7. テスト +- タイマー・状態遷移・APIのユニットテスト +- UIのE2Eテスト(必要に応じて) + +--- + +## 補足(拡張機能例) +- ユーザー認証(Googleログイン等) +- タグやタスク管理との連携 +- Slack/Discord等への通知連携 +- ダークモード切替 diff --git a/main.py b/main.py deleted file mode 100644 index e69de29b..00000000 diff --git a/old/deliverManager.py b/old/deliverManager.py new file mode 100644 index 00000000..cade9632 --- /dev/null +++ b/old/deliverManager.py @@ -0,0 +1,296 @@ +import time +import random +import threading +from typing import List, Callable, Optional +from dataclasses import dataclass, field +from collections import Counter + + + +class EventArgs: + """Base class for event arguments.""" + pass + + +from typing import TypeVar, Generic +TEventArgs = TypeVar('TEventArgs', bound=EventArgs) + + +class Event(Generic[TEventArgs]): + """C# event-like class for Python, type-safe.""" + def __init__(self): + self._handlers: List[Callable[[Any, TEventArgs], None]] = [] + + def add_handler(self, handler: Callable[[Any, TEventArgs], None]) -> None: + if handler not in self._handlers: + self._handlers.append(handler) + + def remove_handler(self, handler: Callable[[Any, TEventArgs], None]) -> None: + if handler in self._handlers: + self._handlers.remove(handler) + + def invoke(self, sender: Any, args: TEventArgs = None) -> None: + for handler in self._handlers: + try: + handler(sender, args or EventArgs()) + except Exception as e: + print(f"[Event] Handler exception: {e}") + + +class Event: + """C# event-like class for Python.""" + def __init__(self): + self._handlers: List[Callable] = [] + + def add_handler(self, handler: Callable): + if handler not in self._handlers: + self._handlers.append(handler) + + def remove_handler(self, handler: Callable): + if handler in self._handlers: + self._handlers.remove(handler) + + def invoke(self, sender, args: EventArgs = None): + for handler in self._handlers: + try: + handler(sender, args or EventArgs()) + except Exception as e: + print(f"[Event] Handler exception: {e}") + + +@dataclass +class KitchenObjectSO: + """Data class for kitchen objects.""" + name: str + object_id: int + + +@dataclass +class RecipeSO: + """Data class for recipes.""" + name: str + kitchen_object_so_list: List[KitchenObjectSO] = field(default_factory=list) + + +@dataclass +class RecipeListSO: + """Data class for recipe lists.""" + recipe_so_list: List[RecipeSO] = field(default_factory=list) + + + +class PlateKitchenObject: + """Plate kitchen object.""" + def __init__(self) -> None: + self._kitchen_object_so_list: List[KitchenObjectSO] = [] + + def add_kitchen_object(self, kitchen_object: KitchenObjectSO) -> None: + self._kitchen_object_so_list.append(kitchen_object) + + def get_kitchen_object_so_list(self) -> List[KitchenObjectSO]: + return self._kitchen_object_so_list.copy() + + + +class KitchenGameManager: + """Kitchen game manager (Singleton, thread-safe).""" + _instance: Optional['KitchenGameManager'] = None + _lock = threading.Lock() + + def __init__(self) -> None: + self._is_game_playing: bool = False + + @classmethod + def get_instance(cls) -> 'KitchenGameManager': + with cls._lock: + if cls._instance is None: + cls._instance = cls() + return cls._instance + + @classmethod + def _reset_instance(cls): + """テスト用: シングルトンインスタンスをリセット""" + with cls._lock: + cls._instance = None + + def is_game_playing(self) -> bool: + return self._is_game_playing + + def start_game(self) -> None: + self._is_game_playing = True + + def stop_game(self) -> None: + self._is_game_playing = False + + + +from typing import Protocol, runtime_checkable, Any + +@runtime_checkable +class RecipeRepositoryProtocol(Protocol): + def get_recipe_by_name(self, name: str) -> Any: + ... + + +class InMemoryRecipeRepository: + """In-memory implementation for RecipeRepositoryProtocol (for test/demo).""" + def __init__(self, recipe_list_so: RecipeListSO): + self._recipes = {r.name: r for r in recipe_list_so.recipe_so_list} + + def get_recipe_by_name(self, name: str) -> Optional[RecipeSO]: + return self._recipes.get(name) + + + +class DeliveryManager: + """Delivery manager class (Python version, improved).""" + _instance: Optional['DeliveryManager'] = None + _lock = threading.Lock() + + def __init__(self, recipe_list_so: RecipeListSO, recipe_repository: Optional[RecipeRepositoryProtocol] = None) -> None: + self.on_recipe_spawned: Event[EventArgs] = Event() + self.on_recipe_completed: Event[EventArgs] = Event() + self.on_recipe_success: Event[EventArgs] = Event() + self.on_recipe_failed: Event[EventArgs] = Event() + + self._recipe_list_so: RecipeListSO = recipe_list_so + self._waiting_recipe_so_list: List[RecipeSO] = [] + self._spawn_recipe_timer: float = 0.0 + self._spawn_recipe_timer_max: float = 4.0 + self._waiting_recipes_max: int = 4 + self._successful_recipes_amount: int = 0 + self._last_update_time: float = time.time() + self._recipe_repository: RecipeRepositoryProtocol = recipe_repository or InMemoryRecipeRepository(recipe_list_so) + + @classmethod + def get_instance(cls, recipe_list_so: RecipeListSO = None, recipe_repository: Optional[RecipeRepositoryProtocol] = None) -> 'DeliveryManager': + with cls._lock: + if cls._instance is None: + if recipe_list_so is None: + raise ValueError("recipe_list_so is required for the first instantiation") + cls._instance = cls(recipe_list_so, recipe_repository) + return cls._instance + + @classmethod + def _reset_instance(cls): + """テスト用: シングルトンインスタンスをリセット""" + with cls._lock: + cls._instance = None + + def get_recipe_by_name(self, name: str) -> Optional[RecipeSO]: + """Repository経由でレシピを安全に取得する。""" + return self._recipe_repository.get_recipe_by_name(name) + + def update(self) -> None: + """Frame update (equivalent to Unity's Update).""" + current_time = time.time() + delta_time = current_time - self._last_update_time + self._last_update_time = current_time + + self._spawn_recipe_timer -= delta_time + + if self._spawn_recipe_timer <= 0.0: + self._spawn_recipe_timer = self._spawn_recipe_timer_max + + kitchen_game_manager = KitchenGameManager.get_instance() + if (kitchen_game_manager.is_game_playing() and + len(self._waiting_recipe_so_list) < self._waiting_recipes_max): + + # Choose a recipe randomly + waiting_recipe_so = random.choice(self._recipe_list_so.recipe_so_list) + self._waiting_recipe_so_list.append(waiting_recipe_so) + + # Fire event + self._safe_invoke(self.on_recipe_spawned) + + def deliver_recipe(self, plate_kitchen_object: PlateKitchenObject) -> None: + """Check if the plate's ingredients match any waiting recipe.""" + if not isinstance(plate_kitchen_object, PlateKitchenObject): + print("Error: plate_kitchen_object must be a PlateKitchenObject instance") + self._safe_invoke(self.on_recipe_failed) + return + + plate_ingredients = plate_kitchen_object.get_kitchen_object_so_list() + + for i, waiting_recipe_so in enumerate(self._waiting_recipe_so_list): + recipe_ingredients = waiting_recipe_so.kitchen_object_so_list + if len(recipe_ingredients) != len(plate_ingredients): + continue + # 材料比較をCounterで簡素化(順序・重複非依存) + if Counter(recipe_ingredients) == Counter(plate_ingredients): + self._successful_recipes_amount += 1 + self._waiting_recipe_so_list.pop(i) + self._safe_invoke(self.on_recipe_completed) + self._safe_invoke(self.on_recipe_success) + return + + # No matching recipe found + self._safe_invoke(self.on_recipe_failed) + + def _safe_invoke(self, event: Event) -> None: + try: + event.invoke(self) + except Exception as e: + print(f"[DeliveryManager] Event invoke exception: {e}") + + def get_waiting_recipe_so_list(self) -> List[RecipeSO]: + return self._waiting_recipe_so_list.copy() + + def get_successful_recipes_amount(self) -> int: + return self._successful_recipes_amount + + +# 使用例 +if __name__ == "__main__": + # サンプルデータ作成 + tomato = KitchenObjectSO("Tomato", 1) + lettuce = KitchenObjectSO("Lettuce", 2) + bread = KitchenObjectSO("Bread", 3) + + # サンプルレシピ + sandwich_recipe = RecipeSO("Sandwich", [bread, lettuce, tomato]) + salad_recipe = RecipeSO("Salad", [lettuce, tomato]) + + recipe_list = RecipeListSO([sandwich_recipe, salad_recipe]) + + # ゲームマネージャーとデリバリーマネージャーを初期化 + game_manager = KitchenGameManager.get_instance() + game_manager.start_game() + + delivery_manager = DeliveryManager.get_instance(recipe_list) + + # イベントハンドラーの設定 + def on_recipe_spawned(sender, args): + print("新しいレシピが生成されました!") + + def on_recipe_success(sender, args): + print("レシピ配達成功!") + + def on_recipe_failed(sender, args): + print("レシピ配達失敗...") + + delivery_manager.on_recipe_spawned.add_handler(on_recipe_spawned) + delivery_manager.on_recipe_success.add_handler(on_recipe_success) + delivery_manager.on_recipe_failed.add_handler(on_recipe_failed) + + # サンプル実行 + print("ゲーム開始...") + + # 5秒間更新処理を実行 + start_time = time.time() + while time.time() - start_time < 5: + delivery_manager.update() + time.sleep(0.1) # 100ms間隔で更新 + + print(f"待機中のレシピ数: {len(delivery_manager.get_waiting_recipe_so_list())}") + + # サンプル配達テスト + plate = PlateKitchenObject() + plate.add_kitchen_object(bread) + plate.add_kitchen_object(lettuce) + plate.add_kitchen_object(tomato) + + print("サンドイッチを配達...") + delivery_manager.deliver_recipe(plate) + + print(f"成功したレシピ数: {delivery_manager.get_successful_recipes_amount()}") \ No newline at end of file diff --git a/old/fibonacci.py b/old/fibonacci.py new file mode 100644 index 00000000..db5332c4 --- /dev/null +++ b/old/fibonacci.py @@ -0,0 +1,15 @@ +# Fibonacci数列を計算する関数 +def fibonacci(n): + if n <= 0: + return [] + elif n == 1: + return [0] + elif n == 2: + return [0, 1] + + fib_sequence = [0, 1] + for i in range(2, n): + next_value = fib_sequence[-1] + fib_sequence[-2] + fib_sequence.append(next_value) + + return fib_sequence diff --git a/old/main.py b/old/main.py new file mode 100644 index 00000000..fb528555 --- /dev/null +++ b/old/main.py @@ -0,0 +1,7 @@ + +""" +main.py: テスト用エントリポイント +""" + +if __name__ == "__main__": + print("main.py: テスト用エントリポイントです。ユニットテストは test_deliver_manager.py を参照してください。") diff --git a/old/point.py b/old/point.py new file mode 100644 index 00000000..6d643fb2 --- /dev/null +++ b/old/point.py @@ -0,0 +1,16 @@ +import math + +class Point3D: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def distance_to(self, other): + dx = self.x - other.x + dy = self.y - other.y + dz = self.z - other.z + return math.sqrt(dx * dx + dy * dy + dz * dz) + + def __str__(self): + return f"Point3D({self.x}, {self.y}, {self.z})" diff --git a/old/test_deliver_manager.py b/old/test_deliver_manager.py new file mode 100644 index 00000000..451a37f7 --- /dev/null +++ b/old/test_deliver_manager.py @@ -0,0 +1,101 @@ +import pytest +from deliverManager import ( + KitchenObjectSO, RecipeSO, RecipeListSO, PlateKitchenObject, + DeliveryManager, KitchenGameManager, InMemoryRecipeRepository +) + +def make_sample_data(): + tomato = KitchenObjectSO("Tomato", 1) + lettuce = KitchenObjectSO("Lettuce", 2) + bread = KitchenObjectSO("Bread", 3) + sandwich_recipe = RecipeSO("Sandwich", [bread, lettuce, tomato]) + salad_recipe = RecipeSO("Salad", [lettuce, tomato]) + recipe_list = RecipeListSO([sandwich_recipe, salad_recipe]) + return bread, lettuce, tomato, sandwich_recipe, salad_recipe, recipe_list + +def setup_module(module): + # シングルトンをリセット + DeliveryManager._reset_instance() + KitchenGameManager._reset_instance() + +def teardown_module(module): + DeliveryManager._reset_instance() + KitchenGameManager._reset_instance() + +def test_recipe_repository(): + _, _, _, sandwich, salad, recipe_list = make_sample_data() + repo = InMemoryRecipeRepository(recipe_list) + assert repo.get_recipe_by_name("Sandwich") == sandwich + assert repo.get_recipe_by_name("Salad") == salad + assert repo.get_recipe_by_name("NotExist") is None + +def test_delivery_manager_spawn_and_deliver(): + bread, lettuce, tomato, sandwich, salad, recipe_list = make_sample_data() + dm = DeliveryManager(recipe_list) + kgm = KitchenGameManager.get_instance() + kgm.start_game() + # レシピ生成 + for _ in range(5): + dm.update() + waiting = dm.get_waiting_recipe_so_list() + assert 0 < len(waiting) <= 4 + # 正しい材料で配達 + plate = PlateKitchenObject() + for obj in sandwich.kitchen_object_so_list: + plate.add_kitchen_object(obj) + # waitingにサンドイッチがなければ追加 + if not any(r.name == "Sandwich" for r in waiting): + dm._waiting_recipe_so_list.append(sandwich) + before = dm.get_successful_recipes_amount() + dm.deliver_recipe(plate) + after = dm.get_successful_recipes_amount() + assert after == before + 1 + +def test_delivery_manager_failed_delivery(): + bread, lettuce, tomato, sandwich, salad, recipe_list = make_sample_data() + dm = DeliveryManager(recipe_list) + kgm = KitchenGameManager.get_instance() + kgm.start_game() + # waitingにサンドイッチを追加 + dm._waiting_recipe_so_list.append(sandwich) + # 間違った材料 + plate = PlateKitchenObject() + plate.add_kitchen_object(bread) + plate.add_kitchen_object(lettuce) + # tomatoが足りない + before = dm.get_successful_recipes_amount() + dm.deliver_recipe(plate) + after = dm.get_successful_recipes_amount() + assert after == before + +def test_event_handlers(): + _, _, _, sandwich, _, recipe_list = make_sample_data() + dm = DeliveryManager(recipe_list) + events = {"spawned": False, "success": False, "failed": False} + def on_spawned(sender, args): + events["spawned"] = True + def on_success(sender, args): + events["success"] = True + def on_failed(sender, args): + events["failed"] = True + dm.on_recipe_spawned.add_handler(on_spawned) + dm.on_recipe_success.add_handler(on_success) + dm.on_recipe_failed.add_handler(on_failed) + # spawn event + dm._waiting_recipe_so_list.clear() + kgm = KitchenGameManager.get_instance() + kgm.start_game() + dm.update() + assert events["spawned"] + # success event + plate = PlateKitchenObject() + for obj in sandwich.kitchen_object_so_list: + plate.add_kitchen_object(obj) + dm._waiting_recipe_so_list.append(sandwich) + dm.deliver_recipe(plate) + assert events["success"] + # failed event + plate2 = PlateKitchenObject() + plate2.add_kitchen_object(sandwich.kitchen_object_so_list[0]) + dm.deliver_recipe(plate2) + assert events["failed"] \ No newline at end of file diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..1446ae5f --- /dev/null +++ b/plan.md @@ -0,0 +1,61 @@ +# ポモドーロタイマーWebアプリケーション 段階的実装計画 + +## ステップ1:MVP(最小実用プロダクト) + +1. UI雛形の作成 + - UIモックに基づくHTML/CSS/JSの作成 + - タイマー表示・ボタン配置・モード表示 +2. フロントエンドタイマー機能 + - 作業タイマー・休憩タイマーのカウントダウン(JSのみ) + - スタート/一時停止/リセットボタン + - タイマー終了時のアラート +3. セッション管理(フロントのみ) + - 作業・休憩の自動切り替え + - 1サイクルごとの進捗表示 + +--- + +## ステップ2:基本機能の拡充 + +4. 設定機能 + - 作業・休憩・長休憩時間のカスタマイズUI + - サウンドON/OFF、通知ON/OFF +5. ローカルストレージ対応 + - 設定・進捗・履歴の保存(ブラウザローカル) +6. レスポンシブデザイン + - スマホ・タブレット対応 + +--- + +## ステップ3:サーバー連携・拡張 + +7. Flaskバックエンド導入 + - 静的ファイル・テンプレート配信 + - API設計(履歴保存・取得) +8. 履歴・統計機能 + - 日ごとのポモドーロ回数・累計作業時間の表示 +9. ユーザー管理(任意) + - サインアップ・ログイン + - サーバー側での履歴保存・多端末同期 + +--- + +## ステップ4:テスト・品質向上 + +10. ユニットテスト・E2Eテスト + - タイマー・状態遷移・APIの自動テスト + - UIのE2Eテスト + +--- + +## ステップ5:拡張・改善 + +11. 追加機能 + - タグ/タスク管理連携 + - Slack/Discord通知 + - ダークモード + - 多言語対応 + +--- + +各ステップは「UI→ロジック→保存→テスト」の順で小さく区切って進め、1機能ごとにPR/レビュー/テストを徹底することを推奨します。 diff --git a/point.py b/point.py deleted file mode 100644 index 6955e338..00000000 --- a/point.py +++ /dev/null @@ -1,14 +0,0 @@ -import math - -class Point2D: - def __init__(self, x, y): - self.x = x - self.y = y - - def distance_to(self, other): - dx = self.x - other.x - dy = self.y - other.y - return math.sqrt(dx * dx + dy * dy) - - def __str__(self): - return f"Point2D({self.x}, {self.y})" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e3e9a71d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 00000000..2d03ccf6 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,48 @@ +body { + font-family: 'Segoe UI', 'Meiryo', sans-serif; + background: #f7f7f7; + margin: 0; + padding: 0; +} +.container { + max-width: 400px; + margin: 40px auto; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + padding: 32px 24px 24px 24px; + text-align: center; +} +.timer-display { + font-size: 3em; + margin: 24px 0 8px 0; + font-weight: bold; + color: #e74c3c; +} +.mode-display { + font-size: 1.2em; + margin-bottom: 16px; + color: #555; +} +.controls button { + font-size: 1em; + margin: 0 8px; + padding: 8px 20px; + border: none; + border-radius: 6px; + background: #e74c3c; + color: #fff; + cursor: pointer; + transition: background 0.2s; +} +.controls button:hover { + background: #c0392b; +} +.session-progress { + margin-top: 20px; + font-size: 1.3em; + color: #aaa; +} +.session-progress span.active { + color: #e74c3c; +} diff --git a/static/js/timer.js b/static/js/timer.js new file mode 100644 index 00000000..7b9b302b --- /dev/null +++ b/static/js/timer.js @@ -0,0 +1,94 @@ +// timer.js +let workDuration = 25 * 60; // 25分 +let breakDuration = 5 * 60; // 5分 +let longBreakDuration = 15 * 60; // 15分 +let currentMode = 'work'; // 'work' | 'break' | 'longbreak' +let timer = null; +let timeLeft = workDuration; +let sessionCount = 0; + +const timerDisplay = document.getElementById('timer-display'); +const modeDisplay = document.getElementById('mode-display'); +const startBtn = document.getElementById('start-btn'); +const pauseBtn = document.getElementById('pause-btn'); +const resetBtn = document.getElementById('reset-btn'); +const sessionProgress = document.getElementById('session-progress'); + +function updateDisplay() { + const min = String(Math.floor(timeLeft / 60)).padStart(2, '0'); + const sec = String(timeLeft % 60).padStart(2, '0'); + timerDisplay.textContent = `${min}:${sec}`; + if (currentMode === 'work') { + modeDisplay.textContent = '作業中'; + } else if (currentMode === 'break') { + modeDisplay.textContent = '休憩中'; + } else { + modeDisplay.textContent = '長休憩中'; + } + // セッション進捗表示 + let html = ''; + for (let i = 0; i < 4; i++) { + html += `${i < sessionCount ? '●' : '○'}`; + } + sessionProgress.innerHTML = html; +} + +function startTimer() { + if (timer) return; + timer = setInterval(() => { + if (timeLeft > 0) { + timeLeft--; + updateDisplay(); + } else { + clearInterval(timer); + timer = null; + onTimerEnd(); + } + }, 1000); +} + +function pauseTimer() { + if (timer) { + clearInterval(timer); + timer = null; + } +} + +function resetTimer() { + pauseTimer(); + if (currentMode === 'work') { + timeLeft = workDuration; + } else if (currentMode === 'break') { + timeLeft = breakDuration; + } else { + timeLeft = longBreakDuration; + } + updateDisplay(); +} + +function onTimerEnd() { + if (currentMode === 'work') { + sessionCount++; + if (sessionCount >= 4) { + currentMode = 'longbreak'; + timeLeft = longBreakDuration; + sessionCount = 0; + } else { + currentMode = 'break'; + timeLeft = breakDuration; + } + alert('作業セッション終了!休憩しましょう。'); + } else { + currentMode = 'work'; + timeLeft = workDuration; + alert('休憩終了!作業を再開しましょう。'); + } + updateDisplay(); +} + +startBtn.onclick = startTimer; +pauseBtn.onclick = pauseTimer; +resetBtn.onclick = resetTimer; + +// 初期表示 +updateDisplay(); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..caeb812a --- /dev/null +++ b/templates/index.html @@ -0,0 +1,25 @@ + + + + + + ポモドーロタイマー + + + +
+

ポモドーロタイマー

+
25:00
+
作業中
+
+ + + +
+
+ +
+
+ + + diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 00000000..e32fa547 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,23 @@ +import pytest +from app import create_app + +@pytest.fixture +def app(): + app = create_app() + app.config.update({ + "TESTING": True, + }) + return app + +@pytest.fixture +def client(app): + return app.test_client() + +def test_index_route(client): + response = client.get("/") + assert response.status_code == 200 + assert b"ポモドーロタイマー" in response.data + assert b"timer-display" in response.data + assert b"start-btn" in response.data + assert b"pause-btn" in response.data + assert b"reset-btn" in response.data