fa
Feedback
ToCode

ToCode

رفتن به کانال در Telegram

טיפים קצרים למתכנתים מאת ינון פרק

نمایش بیشتر
1 417
مشترکین
اطلاعاتی وجود ندارد24 ساعت
-17 روز
-430 روز
آرشیو پست ها
ToCode
1 417
function moveBody(body: Array<Coordinates>, direction: Direction, grow: number) {
  const head = body[0];
  const newHead = {
    'right': () => ({x: head.x + 1, y: head.y}),
    'left': () => ({x: head.x - 1, y: head.y}),
    'up': () => ({x: head.x, y: head.y - 1}),
    'down': () => ({x: head.x, y: head.y + 1}),
  }[direction]();
  if (grow > 0) {
    return [newHead, ...body]
  } else {
    return [newHead, ...body.slice(0, body.length - 1)]
  }
}
קובץ קומפוננטה של נחש בשם snake.tsx:
import type { Snake, Coordinates } from "@/types/game";
import { coordinatesToStyle } from '@/lib/utils';
/**
 * Renders the snake using DOM objects
 * Each snake part should be a div with a class name of 'snake'
 */


export default function Snake({ snake }: { snake: Snake }) {
  return (
    <>
      {snake.body.map((c, i) => (
        <div        
          key={i}
          className='w-5 h-5 bg-amber-900 absolute m-0 p-0'
          style={coordinatesToStyle(c)}
        />
      ))}
    </>
  )
}
וקובץ משחק game.tsx שמייצר נחש, תפוח ו Game Loop ומתחיל להזיז דברים על המסך:
'use client';
import isColliding from "@/lib/is-colliding";
import useSnake from "@/hooks/use-snake";
import useGameLoop from "@/hooks/use-game-loop";
import useApple from "@/hooks/use-apple";
import Snake from "@/components/dom/snake";
import Apple from "@/components/dom/apple";
import PlayButton from "@/components/dom/play-button";
import { useState } from "react";
/**
 * Create a Snake game client component
 */
export default function Game() {
  const [isPlaying, setIsPlaying] = useState(false);
  const snake = useSnake();
  const apple = useApple();
  const snakeEatsApple = isColliding(snake.body, apple.body);
  const GAME_SPEED = 2;  // move the snake 1 step every second
  
  const gameLoop = useGameLoop(isPlaying ? GAME_SPEED : 0, () => {
    snake.update();
  });
  
  if (snakeEatsApple) {
  }

  return (
    <div>
      <h1>Snake</h1>    
      <PlayButton isPlaying={isPlaying} toggle={() => setIsPlaying(p => !p)} />
      <div className="border-purple-600 border w-[800px] h-[600px] relative mx-auto">
        <Snake snake={snake} />
        <Apple apple={apple} />
      </div>        
    </div>
  )    
}
פה כבר היתה מספיק מסה של קוד כדי שה AI יבין איך המשחק צריך להיות בנוי ותוך רגע קיבלתי משחק סנייק שעובד. ה AI הוסיף את כל המנגנונים: 1. נחש יכול לאכול תפוח ולגדול. 2. שליטה בנחש עם כפתורי החצים או w, s, a, d. 3. כשנחש אוכל תפוח מקבלים נקודות, ומהירות המשחק לאט לאט עולה. 4. פסילה כשנחש נוגע בעצמו או בקירות העולם. אפשר לראות את כל הקוד שקלוד כתב בצורת diff בקישור הזה: https://github.com/ynonp/vibe-coding-snake-game/commit/7476986d0f7cc35ab70aa670c349dfdf5a4c531e בעבודה עם AI יש נקודה שהחל ממנה ה AI כבר מסתדר להמשיך לבד וכותב קוד שמתאים לסגנון שאתם רוצים, אבל בשביל ששיתוף הפעולה הזה יעבוד טוב אתם צריכים להיות אלה שמבינים איך המערכת בנויה ומה התפקיד של כל קובץ וכל שורת קוד. אחד הפרמטרים שמעניין להסתכל עליהם בניסיונות מהסוג הזה הוא הפרדיקטביליות של הקוד, כלומר אנחנו נותנים לקרסר ליצור את הקוד ואז שמים אותו ב branch, ואז נותנים לו ליצור שוב עוד גירסה עם אותו פרומפט, וככה ממשיכים 3-4 פעמים. ככל שנראה יותר שינויים בין הגירסאות שנוצרו זו אינדיקציה שהבסיס שלנו עדיין לא מספיק ברור והאלמנט האקראי בעבודה של ה AI משחק תפקיד יותר משמעותי.

ToCode
1 417
const gameLoop = useGameLoop();
  
  if (areColliding) {

  }

  return (
    <div>
      <h1>Snake</h1>
      <Snake snake={snake} />
      <Apple apple={apple} />
    </div>
  )
}
שלושה קבצים ריקים עבור הלוגיקה שרציתי שתישמר בתור hooks עם השמות use-apple.ts, use-collision-detection.ts, use-game-loop.ts ו use-snake.ts. יצרתי גם קובץ טיפוסים בשם types/game.ts עם התוכן הבא:
export type GameObject = Coordinates | Array<Coordinates>;

export type Snake = {
  body: Array<Coordinates>;
  direction: Direction;
};

export type Coordinates = {
  x: number;
  y: number;
};

export type Direction = 'up' | 'down' | 'left' | 'right';

export type GameState = {
  snake: Snake;
  food: Coordinates;
  gameOver: boolean;
};

export type GameConfig = {
  width: number;
  height: number;
  gameOver: boolean;
  speed: number;
}
יותר מזה, רציתי לוודא שבלולאת המשחק ה UI ישתמש ב requestAnimationFrame אז כתבתי בקובץ use-game-loop.ts את התוכן הבא:
import { useEffect } from "react";

export default function useGameLoop({
  update,
  isRunning
}: {
  update: () => void;
  isRunning: boolean;
}) {
  let animationFrame: number;

  function tick() {
    if (isRunning) {
      update();
      animationFrame = requestAnimationFrame(tick);
    } 
  }

  useEffect(() => {
    tick();
    return () => cancelAnimationFrame(animationFrame);
  }, [isRunning]);
}
למרות שלי היה ברור לגמרי איך להמשיך את המשחק, התוצאה שקיבלתי מה AI לא היתה טובה. כן זה היה יותר טוב מהסיבוב הראשון: הקבצים היו קטנים יותר, היתה התחלה של בדיקות, החלוקה לקבצים היתה לפי מה שאני הגדרתי והוא לא ניסה לממש את המשחק בערבוב מוזר של Canvas ו State. אבל, המשחק עדיין כלל באגים, הבדיקות לא היו טובות, ובאופן כללי המימוש לא איפשר הרחבה של המשחק בלי לשבור דברים. ניסיון שלישי - מבנה בסיסי עובד, AI משלים אחרי שנתתי למספר מודלים ליצור משחק סנייק לפי ההתחלה של הבסיס שכתבתי בחלק הקודם הבנתי שהבעיה לא במודלים אלא בפרומפט וחזרתי לשולחן השרטוטים. הפעם המשכתי את המשחק בעצמי עד שהגעתי ל Game Loop שמזיזה נחש. הקוד כלל את הקובץ use-game-loop.ts השלם:
import type { RefObject } from 'react';
import { useRef, useEffect } from "react";

function gameLoop(
  nextAnimationFrameRef: RefObject<number>,
  lastTime: number,
  accumulatorRef: RefObject<number>,
  updatesPerSecond: number,
  update: () => void,
) {
  const timeStep = 1 / updatesPerSecond;
  const currentTime = performance.now();
  let deltaTime = (currentTime - lastTime) / 1000;
  accumulatorRef.current += deltaTime;

  while (accumulatorRef.current >= timeStep) {
    update();
    accumulatorRef.current -= timeStep;
  }
  nextAnimationFrameRef.current = requestAnimationFrame(() => gameLoop(nextAnimationFrameRef, currentTime, accumulatorRef, updatesPerSecond, update));
}

export default function useGameLoop(
 updatesPerSecond: number,
 update: () => void,
) {
  let accumulatorRef = useRef(0); 
  const nextAnimationFrameRef = useRef(0);

  useEffect(() => {
    if (updatesPerSecond > 0) {
      gameLoop(nextAnimationFrameRef, performance.now(), accumulatorRef, updatesPerSecond, update) 
      return () => {
        cancelAnimationFrame(nextAnimationFrameRef.current);
      }
    }
  }, [updatesPerSecond, update])
}
קובץ use-snake עם הרבה יותר תוכן:
import { Coordinates, Direction, Snake } from "@/types/game";
import { useState } from "react";

/**
 * Creates a new snake object
 * Bind keyboard events to move the snake
 */
export default function useSnake(): Snake {
  const [body, setBody] = useState([{x: 20, y: 20}]);
  const [direction, setDirection] = useState<Direction>('right');
  const [grow, setGrow] = useState(2);

  return {
    body: body,
    direction,
    update: () => {
      setBody(body => moveBody(body, direction, grow))
      if (grow > 0) {
        setGrow(g => g - 1);
      }
    },
    grow: (addition: number) => {
      setGrow(g => g + addition)
    }
  }
}

ToCode
1 417
פיתוח משחק סנייק עם AI - מה כן עבד לפני כמה ימים כתבתי כאן על הכישלון שלי בפיתוח משחק סנייק עם קרסר. הפרומפט "תפתח לי משחק סנייק", באופן די צפוי, יצר קוד גרוע ומשחק שרק נראה עובד אבל מכיל באגים וארכיטקטורה גרועה, כך שכל תיקון של אחד שבר 3 אחרים. בפוסט היום אני רוצה להראות מה כן לעשות - איך לגרום ל AI לפתח קוד ממש בסדר, ומה זה אומר על התפקיד של מתכנתים בעולם של Vibe Coding. ניסיון ראשון - תבנה לי סנייק אז מה בעצם הבעיה? למה ה AI לא יכול לקבל פרומפט כמו "תכתוב משחק סנייק" ופשוט לכתוב משחק נורמלי? ה AI הוא מכונה שמתוכננת "להמשיך" טקסטים. הוא עובר על הפרומפט והמילים בפרומפט משנות את המצב הפנימי של ה LLM. בסוף הפרומפט המצב הפנימי שלו הוא נקודת ההתחלה, ממנה הוא מוצא את המילה הבאה, שגם היא בתורה מעדכנת את המצב הפנימי ומובילה למילה שאחריה וכך הלאה. זה אומר שמה שנכנס לחלון הקונטקסט לפני ששולחים ל AI את ההודעה חייב לשים אותו בנקודה מאוד ספציפית ממנה הוא יבנה קוד נכון. אפשר לחשוב על זה כאילו הפרומפט מפעיל אזורים שונים באימון של ה AI וגורם לו להמשיך את ההשלמה בצורות אחרות. אפשר לחשוב על זה כאילו הפרומפט "תבנה לי משחק סנייק" מפעיל את האזורים באימון של ה AI שהיו קרובים למילים בפרומפט, כלומר למילים של בנייה, משחק וסנייק. הבעיה היא שהאינטרנט מלא בקוד גרוע שקשור לאנשים שכתבו משחקי סנייק לא טובים מכל מיני סיבות ומכל מיני סוגים. שלחתי את ה AI להשלים על בסיס פרומפט כללי מדי ולכן אני מקבל השלמה לא רלוונטית ולא טובה. ניסיון שני - בסיס בינוני, תבנה לי סנייק אז מה כן צריך? אם מדובר במכונת השלמה אז אנחנו צריכים לוודא שאנחנו כותבים בקוד את המבנה שכן נרצה בתוצאה הסופית - כלומר הארכיטקטורה, מבנה הקבצים ואפילו חתימות הפונקציות. לסיבוב הזה הגעתי יותר מוכן - אפשר למצוא את קוד הסטארטר שכתבתי בריפו כאן: https://github.com/ynonp/vibe-coding-snake-game/tree/17c67dcee0cc3e6514440f09bb6449478dae0108 זה מתחיל בקובץ חוקים כללי לפרויקט שמתאר את מבנה התיקיות והטכנולוגיות:
---
description: 
globs: 
alwaysApply: true
---
* Project Overview *

You are an experienced game developer implementing a DOM based snake game using:
  1. React
  2. Next.JS 15 (App Router)
  3. Tailwind CSS 4
  4. Vitest

* Directory Structure *

1. \/src\ contains source files
2. \/tests\ contains test files, in a top-level test separation
3. \/src/components/dom\ are components that render game items to DOM
4. \/src/app\ is the root for the application pages
5. \/src/lib\ stores general utilities or logic, including server actions
6. \/src/hooks\ stores custom hooks

* Components *
* Store DOM rendering logic in components
* Component should be small (less than 200 lines). Their logic is saved in hooks
* Think carefully before using \useEffect\. There's probably a better way.
* Do not use \useCallback\ or \useMemo\. Be smart about the location of component state.
* Create new components as needed

* Hooks *
* Store the client side component logic in custom hooks
* Hooks help us maintain small components
* Create new hooks as needed

* Tests *
* vitest config file is /vitest.config.mts
* Create tests for any feature you implement.
* Use existing mocks where available, for example use fake timers in the tests.
* Do not create new mocks or stubs for the tests.

* Style *
* Prefer using built-in tailwind 4 classes
* All tailwind theme settings is defined in \src/app/globals.css\.
* Do not use a dedicated config file.
לא תמיד ה AI לוקח ברצינות את הבקשות כאן, אבל הרוב כן נקלט והוא הרבה פחות להוט להתקין ספריות חדשות או להמציא שיטות עבודה. אחרי זה הלכתי לתיקיית קבצי המקור ויצרתי כמה קבצים. תחילה קובץ game.tsx בשביל הקומפוננטה של המשחק:
'use client';

import useSnake from "@/hooks/use-snake";
import useGameLoop from "@/hooks/use-game-loop";
/**
 * Create a Snake game client component
 */
export default function Game() {
  const snake = useSnake();
  const apple = useApple();
  const areColliding = useCollisionDetection(snake.body, apple.body);

ToCode
1 417
פייתון 3.14 עשוי לחסוך לכם שאלת ראיונות עבודה מוזרה רוצים לבלבל מועמד למשרת פייתון? שאלו אותו מה מדפיסה התוכנית הבאה:
def return_example():
    try:
        print("In try block")
        return "Try block return"
    except Exception as e:
        print(f"Exception: {e}")
    finally:
        print("In finally block")
        return "Finally block return"
    
result = return_example()
print(f"Result: {result}") 
נו טוב, אולי זה לא כזה מבלבל כי יש רק שתי פקודות return בפונקציה ואם אנחנו שואלים על זה אז בטח התשובה לא יכולה להיות התשובה הצפויה. ואכן בפייתון בגלל שקוד שכתוב בבלוק finally תמיד ירוץ התוצאה תהיה:
In try block
In finally block
Result: Finally block return
זה קצת מוזר כי אנחנו מדמיינים שאחרי return אין כלום, אבל זה גם קצת הגיוני כי finally תמיד צריך לרוץ. בכל מקרה אם קוד מהסוג הזה בלבל גם אתכם תשמחו לשמוע שהחל מגירסה 3.14 של פייתון אי אפשר יהיה לסיים בלוק finally בפקודות return, break או continue וכך לפחות מהשטות הזאת לא נצטרך להתבלבל. כל הפרטים ב PEP: https://peps.python.org/pep-0765/ אגב דוגמה מעניינת מה PEP של קוד אמיתי שנופל בבור הזה היתה:
try:
    ...
except:
    return NotImplemented
finally:
    return some_value
הרעיון כאן היה שמישהו רצה להחזיר ערך מסוים אם היה Exception או ערך אחר אם לא היה, אבל השימוש ב return בתוך finally ממסך על ה return של ה except וכך תמיד מוחזר הערך שמוגדר ב finally.

ToCode
1 417
import * as Phaser from 'phaser';

export default class MainScene extends Phaser.Scene {
  constructor() {
    super('MainScene');    
  }

  preload() {
    const graphics = this.add.graphics();
    graphics.fillStyle(0x00ff00, 1);
    graphics.fillCircle(25, 25, 25); // Draw circle at the center of a 50x50 texture
    graphics.generateTexture('greenBall', 50, 50);  // Generate texture
    graphics.destroy(); // Clean up graphics after texture is created
  }
  
  create() {
    // Now you can safely use the texture
    this.ball = this.physics.add.sprite(100, 100, 'greenBall');
    this.ball
    .setVelocity(200, 200)
    .setCollideWorldBounds(true);    
  }

  update() {
    const {width, height} = this.game.config;
    
    if (this.ball.body.left <= 0) {
      this.ball.body.velocity.x = 200;
    }
    if (this.ball.body.right >= width) {
      this.ball.body.velocity.x = -200;
    }
    if (this.ball.body.top <= 0) {
      this.ball.body.velocity.y = 200;
    }
    if (this.ball.body.bottom >= height ) {
      this.ball.body.velocity.y = -200;
    }
  }
}
ובקובץ main.js אני מעדכן את הקונפיגורציה כדי להציג את הסצינה:
import * as Phaser from 'phaser';
import MainScene from './main-scene';
const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  pixelArt: true,
  transparent: false,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false
    }
  },
  scene: [MainScene]
};

new Phaser.Game(config);
מה הלאה כל הכבוד! כתבתם את המשחק הראשון עם Phaser. נו, לא בדיוק משחק אבל צריך להתחיל איפשהו. הרחבה מתבקשת למשחק הכדור הקופץ היא להוסיף מקל בתחתית המסך שיזוז ימינה ושמאלה עם החצים וצריך לתפוס את הכדור, ואם הכדור נופל למטה אז נפסלים. הרחבה הבאה היא ליצור שני מקלות בשני צידי המסך ולעשות את זה משחק לשני שחקנים ומיטיבי לכת יוכלו לבנות מזה משחק לשני שחקנים על שני מחשבים שונים. אפשר ללמוד עוד על פייזר דרך אינסוף מדריכים באתר שלהם כאן: https://phaser.io/ ולא לשכוח לשתף משחקים מעניינים שתכתבו איתו.

ToCode
1 417
צעדים ראשונים עם פייזר - דוגמת כדור קופץ פייזר הוא ספריה לפיתוח משחקים פשוטים בדפדפן. יש בו כל מה שאפשר לרצות והעבודה היא בקוד לכן לא צריך מיומנות עם כלים גרפיים בשביל להסתדר. בואו נראה איך לבנות איתו פרויקט שמראה כדור מקפץ על המסך. יצירת פרויקט ראשון לא צריך הרבה בשביל פייזר מספיק פרויקט חדש ב Vite ולבחור Vanilla JavaScript. אין בעיה גם עם טייפסקריפט אם אתם מעדיפים. אחרי יצירת הפרויקט אני מעדכן את הקובץ main.js בתבנית לקוד הבא:
import * as Phaser from 'phaser';

const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  pixelArt: true,
  transparent: false,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false
    }
  },
  scene: []
};

new Phaser.Game(config);
מתקין את פייזר עם:
npm add phaser
ונכנס למסך הפיתוח עם:
npm run dev
ואם הכל הלך כמו שצריך תראו על המסך ריבוע שחור ברוחב 800 פיקסלים וגובה 600. זה לוח המשחק בו פייזר עובד. הוספת הכדור בשביל לכתוב משהו שיופיע על המסך השחור אנחנו צריכים סצינה. סצינה בפייזר היא בסך הכל מחלקה שמגדירה מספר פונקציות עם שמות שמורים ויורשת ממחלקת הסצינה המובנית של פייזר:
import * as Phaser from 'phaser';

export default class MainScene extends Phaser.Scene {}
השמות שנראה בדוגמה הם: 1. הפונקציה preload שנקראת לפני שטוענים את הסצינה ונועדה לאתחל רכיבים גרפיים של הסצינה. 2. הפונקציה create שנקראת כשצריך להפעיל את הסצינה. 3. הפונקציה update שקורית 24 פעמים בשניה ואחראית על הזזת הפריטים שעל המסך, בדרך כלל בהתבסס על קלט מהמשתמש - למשל אם לוחצים חץ ימינה אז השחקן יזוז ימינה. בדוגמה שלנו אני רוצה לבנות כדור קטן שיקפוץ על המסך לכן בפונקציה preload אני מייצר את הגרפיקה של הכדור, ב create אני שם אותו על המסך וב update מעדכן את הכיוון שלו. בואו נראה את זה ונבין איך פייזר עוזר לי בכל אחד מהשלבים. מתחילים עם preload:
  preload() {
    const graphics = this.add.graphics();
    graphics.fillStyle(0x00ff00, 1);
    graphics.fillCircle(25, 25, 25);
    graphics.generateTexture('greenBall', 50, 50);
    graphics.destroy(); // Clean up graphics after texture is created
  }
אני מייצר אוביקט גרפי בגודל 50 על 50, מצייר באמצע שלו עיגול ושומר אותו בשם greenBall. הפונקציה fillCircle היא פונקציה שמורה של מנגנון הגרפיקה של פייזר וכמוה גם fillStyle ויש שם עוד הרבה, ובמשחקים אמיתיים בדרך כלל נטען את הגרפיקה מתמונה. הפונקציה create כבר יותר מעניינת:
create() {
  // Now you can safely use the texture
  this.ball = this.physics.add.sprite(100, 100, 'greenBall');
  this.ball
  .setVelocity(200, 200)
  .setCollideWorldBounds(true);
}
פה אנחנו עוברים לדבר על פיזיקה - מגדירים ספרייט, שזה דמות במשחק, מתוך אותו כדור ירוק שיצרנו. לספרייט יש מיקום על המסך וזה ה 100,100 שאני מעביר, זו בעצם נקודת ההתחלה של הכדור. לספרייט אפשר לתת גם מהירות ואני מגדיר לו מהירות 200,200 כלומר 200 פיקסלים בשניה ימינה ו 200 פיקסלים בשניה למטה. הפקודה הבאה, setCollideWorldBounds, גורמת לכדור לעצור כשהוא מתנגש באחד הקירות של העולם. רק עד לפה אפשר להריץ את התוכנית ואנחנו נראה כדור ירוק זז באלכסון למטה, מתנגש בקיר התחתון של המשחק ואז עובר להתגלגל ימינה עד הפינה הימנית תחתונה של העולם. אני מעדיף לתפוס את ההתנגשות בין הכדור לקיר ולשנות לו את הכיוון, ובשביל זה מוסיף את הפונקציה השלישית update:
  update() {
    const {width, height} = this.game.config;
    
    if (this.ball.body.left <= 0) {
      this.ball.body.velocity.x = 200;
    }
    if (this.ball.body.right >= width) {
      this.ball.body.velocity.x = -200;
    }
    if (this.ball.body.top <= 0) {
      this.ball.body.velocity.y = 200;
    }
    if (this.ball.body.bottom >= height ) {
      this.ball.body.velocity.y = -200;
    }
  }
במשחקים update נקראת עבור כל פריים ותפקידה לעדכן את המידע של הדמויות השונות לקראת ציור הפריים הבא. מאחר ופייזר מטפל לבד בהזזה של פריטים מספיק לי להבין אם הכדור שלי התנגש בקיר ואם כן לשנות לו את המהירות כדי שישנה כיוון, למשל אם הוא פגע בקיר התחתון אז אני משנה את המהירות שלו כך שיתחיל לזוז למעלה. את הקוד המלא שמרתי בקובץ main-scene.js:

ToCode
1 417
מיקרו החלטות, קוד שמשתכפל, למידה. הי קרסר - כתוב לי בבקשה משחק סנייק. אין בעיה בוס, מתחילים בקובץ הזה:
import dynamic from 'next/dynamic'

// Use dynamic import to prevent hydration errors with canvas
const SnakeGame = dynamic(() => import('@/components/SnakeGame'), {
  ssr: false
})

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-4 bg-gray-100">
      <h1 className="text-3xl font-bold mb-6">Snake Game</h1>
      <SnakeGame />
    </main>
  )
}
הי קרסר הקוד לא מתקמפל - הוא מתלונן שאי אפשר לקרוא ל dynamic בלי SSR מתוך קומפוננטת צד שרת. אין בעיה בוס אני על זה. בוא נעשה ככה: 1. ניצור קובץ חדש בשם SnakeGameWrapper שיתחיל ב use client ונעביר אליו את כל הקוד מ page. 2. נעדכן את page שיטען את הקובץ החדש הזה. --- קרסר מסיים. הקוד עובד. אבל מי נשאר להגיד לקרסור שאפשר היה לכתוב use client פשוט בהתחלה של page.tsx במקום להוסיף קומפוננטה מיותרת? ומי יקרא את הקוד שבוע הבא ויחשוב בטעות ש page.tsx חייב להיות קומפוננטת צד שרת? ולמה בכלל משחק הסנייק צריך את dynamic? למה שלא יהיה קומפוננטת צד לקוח רגילה?

ToCode
1 417
גם השמות הלא נכונים שלכם הם חוב טכני שמות תמיד היו חשובים. שם לא נכון של עמודה בבסיס נתונים יכול לבלבל משתמשים חדשים וגם וותיקים, ואותו דבר לגבי פונקציות, שמות קבצים, שמות מחלקות ואפילו שמות של פרויקטים. כן לתת שמות לדברים תמיד היה קשה. ואז הגיע AI. שימו לב לשלושת הפרומפטים הכמעט זהים:
Write tests for the following python function:

def count_lines(x):
  ...
Write tests for the following python function:

def count_lines(url):
  ...
Write tests for the following python function:

def count_lines(file_path):
  ...
Write tests for the following python function:

def count_lines(fp):
  ...
ההבדל בין הפרומפטים הוא "רק" שם המשתנה בסוגריים, פרט שלחלוטין לא משנה לפייתון אבל אומר עולם ומלואו ל AI. גם השמות הלא נכונים שלכם הם חוב טכני. זה תמיד היה רעיון טוב לשנות שמות של דברים כדי שיהיה לנו יותר קל להבין מה קורה בקוד, ועכשיו הקפדה על השמות יכולה להיות הזדמנות לשפר את חווית העבודה שלנו עם ה AI.

ToCode
1 417
משפט שתפס אותי מהפוסט הזה Coding, however, is not the boring part, it’s my craft and I want to enjoy it and stay sharp. https://cekrem.github.io/posts/coding-as-craft-going-back-to-the-old-gym/

ToCode
1 417
היום למדתי: דריסת הפונקציה import ב Python בהשראת טראמפ, מישהו כתב חבילת פייתון שגורמת לטעינת חבילות חיצוניות להיות איטית יותר (כי חבילות חיצוניות גנבו לנו את זמן המעבד או שטות כזאת). בכל מקרה זה נראה ככה:
import tariff

* Set your tariff rates (package_name: percentage) *
tariff.set({
    "numpy": 50,     # 50% tariff on numpy
    "pandas": 200,   # 200% tariff on pandas
    "requests": 150  # 150% tariff on requests
})

* Now when you import these packages, they'll be TARIFFED! *
import numpy   # This will be 50% slower
import pandas  # This will be 200% slower
וזה מעניין! כי איך הם גרמו ל import שיבוא אחריהם לעבוד יותר לאט? אני מודה שקצת הפתיע אותי לגלות שמנגנון בסיסי כמו import הוא גם קל כל כך לדריסה. ככה נגמרת הפונקציה set שלהם:
builtins.__import__ = _tariffed_import
זה מקור החבילה בגיטהאב: https://github.com/hxu296/tariff/tree/main ועכשיו אתם - מה הייתם משנים ב import? ולמה?

ToCode
1 417
ניסוי: נתתי לסונט לכתוב כמה בדיקות לאתר אחרי שלמדנו ש AI יודע לכתוב קוד חדש וגם לעדכן קוד ולתקן באגים רציתי לתת לו לכתוב כמה בדיקות לאתר כדי לראות איך זה עובד ואיך הוא מסתדר עם ריילס. התוצאות לא רעות - בואו נראה מה יצא טוב ומה היה צריך עוד דחיפה. נקודת התחלה - מה בודקים הפרומפט הראשון שהפתיע אותי לטובה היה כשנתתי ל AI (דרך קרסור) את כל קוד האתר ופשוט ביקשתי שיציע לי מה לבדוק. קלוד ידע להשוות בין הקוד לבין הבדיקות הקיימות והצביע על מספר מקומות שלא נבדקו או שלא נבדקו בצורה מספיק מקיפה. הפרומפט התחיל ב:
Context: I'm a new developer starting to work on this code
Create a roadmap for getting to know the most important parts of it
אחרי שהוא מיפה את הקוד המשכתי עם:
find 5 flows that are not tested
ה AI באמת הדפיס 5 אזורים בקוד שלא נבדקו מספיק טוב. בחרתי אחד מהם וביקשתי לקבל 5 הצעות לבדיקות שאפשר לכתוב. את כל זה עשיתי במצב Ask כדי שלא יתחיל עדיין לכתוב קוד. מימוש הבדיקות אחרי שקיבלתי רשימת Test Cases למימוש המשכתי עם הפרומפט הבא:
Implement the following tests using my current rubocop settings. 
Instead of mocking, when you need to work with PDF files generate the real ones and verify they were generated as planned.
ופה צריך להבהיר - מניסויים שעשיתי עד עכשיו עם AI הוא מאוד אוהב ליצור מימושי Stub להמון דברים כדי לחדד את הבדיקה, גם במחיר של בדיקה פחות טובה. אני מוסיף את הבקשה שלא יבנה Mock-ים כשאני צריך ממנו בדיקות. אם אני כן רוצה מימוש Stub למשהו במערכת אני מחלק את זה לשני שלבים, קודם כל בונה מימוש Stub בצורה שאפשר להשתמש בה במספר בדיקות בתשתית הבדיקה שלי ואז במימוש הבדיקה ממליץ ל AI להשתמש בתשתית הקיימת. תוצאות ותיקונים האתר כתוב בריילס ולכן גם הבדיקות וגם הבעיות בהן ספציפיות לריילס. אני ממליץ לכם לבצע ניסויים דומים עם המערכות שלכם כדי לראות איך הסיפור עובד בעולם הספציפי שלכם, אבל לריילס כן יש כמה מאפיינים כלליים שאני חושב שעושים פה חיים קלים ל AI, הכי חשוב זה שיש דרך מאוד סטנדרטית לכתוב בדיקות. מאפיין נוסף ספציפי של האתר שלי הוא שיש כבר בדיקות קיימות ולכן היה לקלוד על מה להתבסס. למרות זאת ולמרות שהמימוש בגדול היה נכון היו כמה בעיות: 1. קלוד המציא שמות של פונקציות שנראה לו הגיוני שיהיו. הוא צדק כי היו פונקציות שעשו את מה שהוא רצה פשוט קראו להן בשמות אחרים. בכל אופן את זה היה קל לתקן. 2. בריילס הפקודה update מעדכנת מידע בבסיס הנתונים ואם מוסיפים לה סימן קריאה היא גם זורקת Exception אם העדכון נכשל. בשלב ה Setup של בדיקה מאוד חשוב לשים סימן קריאה אחרי update כי אחרת העדכון נכשל בצורה שקטה ואתם לא מבינים למה הבדיקה נכשלה. קלוד לא חשב על זה. 3. את הערכים ל enum-ים הוא לפעמים כתב נכון ולפעמים המציא ערכים. הפעם אני לוקח אחריות על הטעות כי ההגדרה של ה enum היתה קצת מעורפלת. 4. ההערות שלו לא תמיד התאימו לקוד. במצבים כאלה ההערה היתה נכונה אבל הקוד שהוא יצר לא היה נכון. 5. למרות שביקשתי 5 בדיקות הוא מימש בסוף 8. מתוכן אחת כללה מימוש ריק עם הערת Todo שמסבירה מה צריך לבדוק. סך הכל החוויה לא היתה רעה. הייתי רוצה ש Cursor יציעו מנגנון למחוק אוטומטית חלק או את רוב ההערות של ה AI, אבל אין ספק שההכוונה של ה AI גם בהחלטה מה לבדוק וגם במימוש המבנה הבסיסי של הבדיקות על בסיס תשתית בדיקות קיימת במערכת דחפה אותי לכתוב בדיקות שביום אחר לא הייתי טורח לכתוב.