• בלוג
  • יום 5: משחק איקס עיגול נגד הסוכן

יום 5: משחק איקס עיגול נגד הסוכן

17/08/2026

שיחות עם סוכן לא חייבות לכלול רק הודעות טקסט. בדוגמה של היום נבנה משחק איקס עיגול נגד סוכן ונראה איך להעביר את מצב המשחק בהודעה כדי שהסוכן יחליט על המהלך הבא. דוגמה זו מהווה בסיס לדוגמאות שנראה בהמשך בהן הסוכן משולב במערכת קיימת ומאפשר שני ממשקים: ממשק שיחה וממשק גרפי המשתלבים ביניהם.

1. מה אנחנו בונים

נבנה משחק איקס עיגול בו שחקן המחשב הוא סוכן חכם שמשתמש במודל של Gemini. המשחק כולל שני ממשקים: ממשק ווב וממשק מסוף למשחק משורת הפקודה.

בשני המקרים המשחק יעבוד לפי אותו מנגנון:

  1. מחכים למהלך של השחקן האנושי.

  2. שולחים את לוח המשחק לסוכן בצירוף הוראות שמבקשות את המהלך הבא.

  3. אם הסוכן לא מחזיר מהלך או מחזיר מהלך לא תקין נבחר מהלך באקראי מתוך המשבצות הריקות על הלוח.

למערכת יהיה סט בדיקות שישתמש במחלקה TestModel כך שנוכל לראות שהכל עובד על מודל בדיקה בדיקה שאנחנו יודעים מראש מה יחזיר.

2. קוד הסוכן

אני מתחיל את הסיור בדוגמה בקובץ הסוכן agent.py.

הקובץ מגדיר את מבנה הפלט שהוא בסך הכל מהלך במשחק:

class Move(BaseModel):
    """The move the AI wants to play."""

    index: int = Field(
        description="Index of the cell to play (0-8), counting left-to-right, "
        "top-to-bottom. Must be an empty cell.",
        ge=0,
        le=8,
    )

מהלך הוא רק מספר בין 0 ל-8 שמייצג את התא בו צריך לשחק. העטיפה ב Pydantic Model מאפשרת להוסיף לו תיאור וגם להגדיר גבולות.

הקלאס המרכזי בקובץ הוא TicTacToeAgent:

class TicTacToeAgent:
    """A tic-tac-toe player backed by a Pydantic AI model."""

    def __init__(self, model: Model):
        self.model = model

    def _agent(self) -> Agent[None, Move]:
        return Agent(self.model, output_type=Move, instructions=INSTRUCTIONS)

    @staticmethod
    def _result(board: list[str], move: Move | None) -> MoveResult:
        """Use a legal agent move, otherwise fall back to a random one."""
        if move is not None and game.is_valid_move(board, move.index):
            return MoveResult(index=move.index, source="agent")
        return _random_fallback(board)

    async def choose_move(self, board: list[str]) -> MoveResult:
        """Ask the model for a move without blocking the current event loop."""
        try:
            move = (await self._agent().run(_render_board(board))).output
        except Exception:
            move = None
        return self._result(board, move)

    def choose_move_sync(self, board: list[str]) -> MoveResult:
        """Ask the model for a move from synchronous code."""
        try:
            move = self._agent().run_sync(_render_board(board)).output
        except Exception:
            move = None
        return self._result(board, move)

מחלקה זו מאותחלת עם מודל מבחוץ בשביל שיהיה קל לבדוק את הקוד. קוד הבדיקות יעביר כאן את מודל הבדיקה וקוד המשחק יעביר את המודל האמיתי, ג'מיני או כל מודל אחר שנרצה. הכנסתי לתוכה גם את פונקציית הפעולה choose_move שפונה לסוכן לבקש מהלך וגם גרסה סינכרונית שלה choose_move_sync. בגרסה האסינכרונית אני משתמש מתוך ממשק ה Web, בגרסה הסינכרונית מתוך ממשק שורת הפקודה.

3. הממשקים

מכאן שני קבצי הממשק הם כבר פשוטים. זאת בסך הכל לולאת המשחק בממשק שורת הפקודה:

def main() -> None:
    load_dotenv()
    agent = build_agent(build_primary_model())
    board = game.new_board()
    print("Tic-Tac-Toe — you are X, the AI is O.\n")
    print(render(board))

    while not game.is_over(board):
        # 1. Human move.
        board[ask_human_move(board)] = game.HUMAN
        print("\n" + render(board) + "\n")
        if game.is_over(board):
            break

        # 2. AI move.
        print("AI is thinking...")
        result = agent.choose_move_sync(board)
        board[result.index] = game.AI
        note = "" if result.source == "agent" else "  (random fallback)"
        print(f"AI plays cell {result.index}.{note}\n")
        print(render(board) + "\n")

בממשק הווב (בקובץ server.py) הסיפור יותר מורכב כי צריך HTML, CSS ו JavaScript וכי המתנה למהלך היא אסינכרונית אבל גם שם הכל מתנקז לפונקציה אחת שמטפלת במהלך שמשתמש מבצע מתוך הממשק:

@app.post("/play")
async def play(req: PlayRequest) -> dict:
    """Apply the human's move, then let the AI respond."""
    if game.is_over(BOARD):
        return state({"error": "Game is already over."})
    if not game.is_valid_move(BOARD, req.index):
        return state({"error": "Illegal move."})

    # 1. Human plays.
    BOARD[req.index] = game.HUMAN

    # 2. AI responds (if the game isn't already decided).
    ai_move = None
    if not game.is_over(BOARD):
        result = await agent.choose_move(BOARD)
        BOARD[result.index] = game.AI
        ai_move = {"index": result.index, "source": result.source}

    return state({"ai_move": ai_move})

משתמש שולח מהלך, אנחנו מעדכנים את הלוח ושולחים את הלוח כולו לסוכן כדי לקבל את המהלך הנגדי.

4. בדיקות

החלק הכי מעניין בדוגמה היום הוא הבדיקות. מחלקת TestModel המובנית בתוך pydantic-ai מקלה מאוד על כתיבת בדיקות. במקום לדרוס פונקציות של הספריה אנחנו פשוט מעבירים TestModel שנבנה מראש עם פלט שאנחנו רוצים וכשנבקש השלמה דרך הסוכן נקבל בדיוק את הטקסט שביקשנו.

קובץ הבדיקות מגדיר מספר פונקציות עזר:

def forced_move(index: int) -> TestModel:
    """Return a model that always asks to play the given cell."""
    return TestModel(custom_output_args={"index": index})


def play_human(board: list[str], index: int) -> None:
    assert game.is_valid_move(board, index)
    board[index] = game.HUMAN


def play_ai(board: list[str], index: int):
    result = build_agent(forced_move(index)).choose_move_sync(board)
    assert game.is_valid_move(board, result.index)
    board[result.index] = game.AI
    return result

שימו לב לפונקציה play_ai. היא מקבלת את לוח המשחק ואינדקס איפה לשחק, בונה סוכן עם מודל הבדיקה שמכוון מראש עם האינדקס, מוודאת שהמהלך שקיבלנו תקין ומשחקת אותו. הפוקנציה choose_move_sync כבר כוללת את המנגנון שבודק את הפלט של הסוכן ומחליף אותו במספר אקראי אם הסוכן החזיר מהלך לא הגיוני.

כך נראית בדיקה שמוודאת את ההתנהגות הזאת ומחליפה פלט לא הגיוני של AI במהלך אקראי:

def test_invalid_ai_move_uses_and_registers_random_fallback():
    board = game.new_board()
    play_human(board, 0)
    empty_before = set(game.empty_cells(board))

    result = play_ai(board, 9)

    assert result.source == "random"
    assert result.index in empty_before
    assert board[result.index] == game.AI
    assert board.count(game.AI) == 1

5. עכשיו אתם

  1. הריצו את המשחק על המכונה שלכם ושחקו נגד ג'מיני.

  2. עדכנו את הוראות הסוכן כדי שג'מיני לא ינצח כל הזמן.

  3. כתבו משחק בין שני סוכנים ושימו לב שהם תמיד יוצאים בתיקו (כי שניהם אלופים).

  4. עדכנו את המשחק והפכו אותו לקשה יותר כדי שמשחק בין שני סוכנים יהיה מעניין - אפשר להגדיל את הלוח או לשחק על כמה לוחות במקביל. נסו לסבך אותם מספיק כדי שנראה הבדלים בין המודלים.