# מערכת הדיבור המקבילית במשחק ## תקציר מערכת הדיבור המקבילית מוסיפה למשחק שכבת תקשורת חברתית שאינה תלויה ישירות בתור הפעיל. לפני השינוי, שחקן AI היה יכול לדבר בעיקר בזמן התור שלו: הוא בחר פעולה, צירף אליה `say_outloud`, והמשחק שידר את המשפט לשאר השחקנים. המשמעות היתה ששיחה חברתית היתה קיימת, אבל לא באמת תגובתית: אם בוב עקץ את אליס, אליס יכלה לענות רק כשיגיע התור שלה. המודל החדש מפריד בין שני ערוצים: 1. ערוץ המשחק: רק שחקן אחד מחזיק תור, ורק הוא רשאי לשנות את מצב הלוח. 2. ערוץ השיחה: כל שחקן שאינו בתור יכול לקבל אירוע חברתי ולבחור אם להגיב, בלי לבצע פעולה על הלוח. ההפרדה הזאת מאפשרת לשיחה להרגיש חיה יותר בלי לפגוע באינבריאנט המרכזי של קטאן: מצב הלוח מתקדם באופן סדרתי, פעולה חוקית אחת בכל פעם. ## השאלה המחקרית הבעיה אינה רק "איך לתת לשחקנים להגיב". אם כל משפט יוצר פרומפט לכל שחקן, וכל תגובה יוצרת עוד פרומפטים, מתקבלת מערכת לא יציבה: פיצוץ פרומפטים, דיבור יתר, TTS חופף, ואפשרות ששחקן יקבל במקביל גם פרומפט תור וגם פרומפט תגובה. לכן השאלה המדויקת היא: איך מאפשרים תגובות חברתיות בזמן אמת, תוך שמירה על ארבעה גבולות: - אין שינוי מצב לוח מחוץ לתור. - אין יותר מתהליך LLM אחד לאותו שחקן באותו רגע. - אין פיצוץ תגובות. - אין חפיפה קולית ב-TTS. המימוש הנוכחי עונה על זה באמצעות תור תגובות פר-שחקן, worker ברקע לכל שחקן, batching של אירועים, prompt שמעדיף שתיקה, ונעילות סביב שידור מדיה וסביב בקשות של אותו שחקן. ## המודל הקונספטואלי ### 1. פעולה פעילה כאשר שחקן נמצא בתורו, הוא מקבל פרומפט מלא: מצב לוח, זיכרון, היסטוריית שיחה, פעולות מותרות, ולעיתים כלים. התשובה שלו יכולה לכלול: - `internal_thinking`: מחשבה פנימית. - `note_to_self`: זיכרון אסטרטגי/חברתי. - `say_outloud`: משפט לשולחן. - `action`: פעולה אמיתית במשחק. זהו ערוץ סמכותי: רק מכאן חוזרת פעולה שיכולה לשנות את הלוח. ### 2. תצפית חברתית כאשר מישהו אחר אומר משהו או מבצע פעולה חברתית/עוינת, שחקנים אחרים יכולים לקבל פרומפט מסוג תצפית. בפרומפט כזה `is_active_turn=false`, וההוראה מפורשת: אין לבחור פעולת לוח. מותר רק לעדכן זיכרון ואולי לדבר. התגובה האפשרית כוללת את אותה מעטפת רגשית-קוגניטיבית: - `internal_thinking` - `note_to_self` - `say_outloud` אבל אין `action` שמוחזרת למשחק. ### 3. שתיקה היא פעולה תקינה הנחת היסוד החשובה ביותר היא שלא כל הודעה דורשת תשובה. הפרומפט אומר במפורש שהתגובה המועדפת לשיחה כללית היא שתיקה. השחקן אמור לדבר רק אם: - פנו אליו ישירות. - העליבו אותו או איימו עליו. - פגעו בו ישירות, למשל שודד. - הוצעה לו עסקה משמעותית. - האירוע חשוב ליחסים או לאסטרטגיה ארוכת טווח. כך מתקבלת שיחה שמסוגלת להתפתח, אבל אינה חייבת להתפתח מכל משפט. ## המימוש הטכני ### נקודת הכניסה: GameManager אחרי כל פעולה, `GameManager` קורא ל-`_process_ai_reactions` אם הפעולה הצליחה או אם יש לה `reaction_events`. הפונקציה אוספת שני סוגי טריגרים: - דיבור פומבי מתוך `_ai_say_outloud`. - אירועים ייעודיים מתוך `result.reaction_events`, למשל הצעת טרייד או גניבה עם השודד. בשלב הזה כבר יש מצב לוח עדכני. זה חשוב: שחקן שמקבל תגובת תצפית רואה את מצב הלוח האמיתי אחרי הפעולה, לא snapshot תאורטי לפני שהפעולה בוצעה. ### מניעת כפילות בטריידים טרייד הוא מקרה מיוחד. אם שון מציע לזיו עסקה, זיו לא אמור לקבל גם prompt תור לקבלת/דחיית טרייד וגם prompt תצפית חברתי על אותה הצעה. לכן המימוש מדלג על target של `TRADE_PROPOSE` בתור צופה חברתי. התוצאה: - שון מציע לזיו: זיו מקבל פרומפט אקטיבי מסוג קבלת/דחיית טרייד. - הדר, שאינה target, יכולה לקבל prompt תצפית. - אם זיו עונה בקול, התשובה שלו יכולה להפוך לאירוע חברתי נפרד לשאר השולחן. זה שומר על תגובה אחת לכל תפקיד: target מקבל החלטת משחק, observers מקבלים תצפית חברתית. ### תור תגובות פר-שחקן כאשר `async_reactions=true`, תגובה לא נשלחת מיד ל-LLM מתוך הלולאה הראשית. במקום זאת היא נכנסת ל-mailbox של השחקן: - לכל שחקן יש רשימת אירועים משלו. - אם אין worker פעיל לשחקן, נפתח thread רק עבורו. - worker מנקז את ה-mailbox, מאחד אירועים שהצטברו, ושולח פרומפט אחד. המשמעות היא ששחקנים שונים יכולים לעבד תגובות במקביל, אבל אותו שחקן לא יקבל שתי בקשות LLM חופפות. ### Batching אם בזמן שהדר מחכה לתשובת LLM נכנסות אליה עוד הודעות, הן נשמרות בתור. כשה-worker חוזר, הוא לוקח את ההודעות שהצטברו ושולח אותן יחד כ: `Event 1`, `Event 2`, וכן הלאה. הגודל נשלט על ידי: `reaction_max_batch_messages` ברירת המחדל היא 5. אם הגיעו יותר הודעות, המערכת שומרת את האחרונות ומציינת שאירועים ישנים יותר דולגו. זה מונע backlog אינסופי ושומר על רלוונטיות. ### נעילה פר-שחקן לכל שחקן יש `agent_request_lock`. גם תור פעיל וגם תגובת תצפית משתמשים באותה נעילה. לכן לא יכול לקרות מצב שבו הדר מקבלת במקביל: - פרומפט תור לביצוע פעולה. - פרומפט תגובה חברתית. אם שניהם מגיעים באותו זמן, אחד מחכה לשני. זה חשוב במיוחד במצב async, כי אחרת הזיכרון וה-chat של אותו שחקן היו יכולים להתעדכן בסדר לא צפוי. ### שידור מדיה ו-TTS גם אם כמה שחקנים חוזרים מה-LLM בערך באותו זמן, השידור עובר דרך `_broadcast_chat`, שמוגן על ידי `media_broadcast_lock`. הנעילה מכסה: - הוספה ל-chat history. - כתיבה ללוג. - callback ל-web viewer. - שליחה ל-TTS עם הקול המתאים לשחקן. - הדפסה לקונסול. בנוסף, ספקי ה-TTS עובדים עם תור השמעה יחיד. לכן גם אם שתי תגובות נוצרו במקביל, הן לא אמורות להישמע אחת על השנייה. ### כיבוי, סנכרון והרצה המערכת נשלטת דרך קונפיגורציה ו-flags: - `--async-reactions`: תגובות חברתיות ברקע. - `--sync-reactions`: תגובות חברתיות בסינכרון. - `--no-reactions`: כיבוי שכבת התגובות. - `--reaction-batch-size N`: כמה אירועים לאחד בפרומפט אחד. לדוגמה: ```powershell .\play_ai_auto.bat --replay-session session_20260516_011543 --hebrew-chat --async-reactions --reaction-batch-size 5 ``` בסוף הרצה עם async, המשחק קורא `wait_for_reactions` לזמן קצר כדי לא לאבד תגובות שעוד רצות ברקע לפני שמירת הסשן. ## מנגנוני בלימה נגד פיצוץ פרומפטים המערכת מונעת פיצוץ באמצעות כמה שכבות בלתי תלויות: - prompt policy: שתיקה היא ברירת המחדל. - observer schema: תגובת תצפית אינה יכולה לבחור פעולה על הלוח. - mailbox per player: אותו שחקן מעבד תגובות בסדר, לא במקביל. - batching: כמה אירועים מתאחדים לפרומפט אחד. - max batch size: אין צבירת אינסוף הודעות. - reaction keys: אותה תגובה לא נשלחת שוב לאותו שחקן. - trade target skip: target של טרייד לא מקבל prompt כפול. - replay guard: פעולות replay לא יוצרות שוב תגובות חברתיות. - media lock: גם אם התגובות חושבו במקביל, השידור וה-TTS מסודרים. מבחינה מערכתית, זו לא חסימה מוחלטת של שרשרת תגובות. שרשרת עדיין יכולה לקרות, וזה רצוי: אם הדר אומרת משהו שמכעיס את זיו, זיו יכול להגיב, והדר יכולה לשמוע את זיו. אבל השרשרת עוברת דרך הסתברות/רלוונטיות חברתית ודרך תורים מבוקרים, ולכן היא נוטה לדעוך במקום להתפוצץ. ## ניתוח הסשן האחרון: `session_20260516_014931` הסשן האחרון מציג דוגמה טובה למערכת בפעולה. עשר ההודעות האחרונות הן: 1. שון: "יופי, ה-11 הזה בא בדיוק בזמן. סוף סוף אני יכול להקים את היישוב בנמל." 2. הדר: "לגמרי, ה-11 הזה בא בטוב גם לי. בהצלחה עם הנמל!" 3. שון לזיו: "זיו, ה-11 הזה באמת בא בטוב... יש לך לבנה אחת מיותרת... אני מוכן לתת לך עץ..." 4. זיו: "יאללה שון, שכנעת אותי... בוא נסגור." 5. שון: "תודה על הטרייד זיו... הדר, אל תדאגי..." 6. זיו: "בכיף שון... נראה שכולם פה מתקדמים יפה, לא רק אני." 7. הדר: "כל עוד אתה משאיר לי קצת אוויר לנשימה שם, אני רגועה..." 8. שון לזיו ולהדר: "זיו... אולי תרצה כבשה בתמורה לאחת?... והדר, אם יש לך עץ פנוי..." 9. זיו: "לא הפעם שון, אני חייב לשמור את הלבנה הזאת..." 10. הדר: "יש לי עץ, נראה כשיגיע התור שלי אם זה יהיה רלוונטי." ### מה קרה מבחינת המערכת הודעה 1 היא דיבור מתוך תור פעיל של שון. בעקבותיה הדר קיבלה prompt תצפית (`Active Turn: False`) ובחרה לענות בהודעה 2. זה מראה את השכבה החברתית הפשוטה: לא היתה פנייה ישירה, אבל היה אירוע רלוונטי ליחסים ולתכנון, כי ה-11 נתן גם לה עץ ושון התקרב לנמל. הודעה 3 היא גם דיבור וגם הצעת טרייד. זיו, כ-target של הטרייד, קיבל prompt אקטיבי לקבל או לדחות. הדר, כ-observer, קיבלה prompt תצפית נפרד שבו נאמר לה ששון אמר את המשפט ושיש הצעת טרייד של עץ תמורת לבנה. היא לא קיבלה אפשרות להתערב בלוח, רק לעדכן זיכרון או לדבר. בהודעה 4 זיו קיבל את הטרייד ודיבר בקול. כאן רואים את היתרון של המודל: זיו מגיב בזמן אמת להצעה, אבל עדיין בתוך מנגנון חוקי של טרייד. זה אינו prompt תצפית, אלא action prompt של target. בהודעה 5 שון ממשיך בתורו, אחרי שהטרייד הצליח, ופונה גם לזיו וגם להדר. בעקבות זה הדר וזיו מקבלים תגובות תצפית. זיו עונה בהודעה 6, והדר עונה בהודעה 7. זו שרשרת חברתית אמיתית: משפט של שון יצר שתי תגובות, אחת מצד השותף לטרייד ואחת מצד מי שמרגישה איום מרחבי. הודעה 8 היא שוב הצעה עם שני רבדים: שון מציע לזיו עסקה, ובאותו משפט גם פותח פתח מול הדר לגבי עץ. זיו מקבל prompt אקטיבי לטרייד ועונה בהודעה 9 בדחייה. הדר מקבלת prompt תצפית, ובוחרת לענות בהודעה 10 בלי להתחייב: יש לה עץ, אבל היא רוצה לחכות לתור שלה. ### מה מעניין בהתנהגות המערכת הצליחה ליצור שיחה עם זיכרון חברתי ולא רק הודעות מבודדות. הדר לא מגיבה לכל דבר. למשל, בפרומפט על הצעת הטרייד בין שון לזיו היא זיהתה שזה משמעותי, שמרה note לעצמה, אבל בחרה `say_outloud=""`. כלומר היא "שמעה" בלי לדבר. אחר כך, כששון פנה אליה ישירות והרגיע אותה לגבי ההתרחבות שלו, היא כן ענתה. זה בדיוק הדפוס הרצוי: השחקנים מאזינים כל הזמן, אבל מדברים רק כשיש רלוונטיות חברתית או אסטרטגית. ### עדות לראיית מצב הלוח בתגובות של הדר וזיו יש שימוש במידע לוח אמיתי: - הדר יודעת שיש לה עץ אחד מה-11. - זיו יודע שה-11 לא נתן לו משאב ושעליו לשמור את הלבנה. - שון מבין שהלבנה של זיו מגיעה מהעיר ומהפקות קודמות. - הדר מזהה ששון מתקדם לאזור הגבעות ושזה עלול להגביל אותה. זה חשוב כי תגובות חברתיות אינן רק "צ'אט על טקסט". הן מקבלות את מצב המשחק ומפרשות את הדיבור ביחס אליו. ## מסקנה המערכת הנוכחית יוצרת הבחנה בריאה בין משחק לשיחה. המשחק נשאר סדרתי וחוקי; השיחה נעשית מקבילית, רכה, ומבוססת רלוונטיות. מבחינה התנהגותית, הסשן האחרון מראה שהשחקנים מצליחים: - לשמוע דיבור מחוץ לתור. - לענות כשפונים אליהם. - לשתוק כשעדיף אסטרטגית. - לעדכן זיכרון גם בלי לדבר. - להגיב לטריידים בלי כפילות בין target ל-observer. - לשמור על רצף חברתי בין כמה הודעות. הכיוון הבא למחקר יהיה למדוד כמותית את קצב הדיבור: כמה prompts נוצרים לכל action, כמה מהם מסתיימים בשתיקה, כמה שרשראות תגובה נוצרות, ומה אורך השרשרת הממוצע. אם נראה שהמודל מדבר יותר מדי, כדאי להוסיף threshold מפורש יותר או cooldown חברתי קצר. אם נראה שהוא שותק מדי, אפשר לרכך את ההוראה סביב פניות עקיפות או הצעות שמשפיעות על יחסי כוח.