javascript

exercises

exercises.js
/**
 * ============================================
 * 8.2 CLASS INHERITANCE - EXERCISES
 * ============================================
 * Practice inheritance concepts
 */

/**
 * EXERCISE 1: Basic Inheritance
 * Difficulty: ⭐
 *
 * Create a class hierarchy:
 * - Animal class with name property and speak() method
 * - Dog class that extends Animal with bark() method
 * - Cat class that extends Animal with meow() method
 */
function exercise1() {
  // YOUR CODE HERE
  // TEST CASES (uncomment to test)
  // const dog = new Dog("Rex");
  // console.log(dog.name); // "Rex"
  // dog.speak(); // "[name] makes a sound"
  // dog.bark(); // "[name] says: Woof!"
  //
  // const cat = new Cat("Whiskers");
  // cat.speak(); // "[name] makes a sound"
  // cat.meow(); // "[name] says: Meow!"
}

/*
 * SOLUTION 1:
 *
 * class Animal {
 *     constructor(name) {
 *         this.name = name;
 *     }
 *
 *     speak() {
 *         console.log(`${this.name} makes a sound`);
 *     }
 * }
 *
 * class Dog extends Animal {
 *     bark() {
 *         console.log(`${this.name} says: Woof!`);
 *     }
 * }
 *
 * class Cat extends Animal {
 *     meow() {
 *         console.log(`${this.name} says: Meow!`);
 *     }
 * }
 */

/**
 * EXERCISE 2: Constructor with super()
 * Difficulty: ⭐
 *
 * Create:
 * - Person class with name and age
 * - Student class extending Person with grade property
 * - Student constructor must use super() and add grade
 */
function exercise2() {
  // YOUR CODE HERE
  // TEST CASES
  // const student = new Student("Alice", 20, "A");
  // console.log(student.name);  // "Alice"
  // console.log(student.age);   // 20
  // console.log(student.grade); // "A"
  // console.log(student instanceof Person); // true
}

/*
 * SOLUTION 2:
 *
 * class Person {
 *     constructor(name, age) {
 *         this.name = name;
 *         this.age = age;
 *     }
 * }
 *
 * class Student extends Person {
 *     constructor(name, age, grade) {
 *         super(name, age);
 *         this.grade = grade;
 *     }
 * }
 */

/**
 * EXERCISE 3: Method Overriding
 * Difficulty: ⭐
 *
 * Create Shape class with area() method that returns 0.
 * Create Circle and Rectangle that override area() with proper calculations.
 */
function exercise3() {
  // YOUR CODE HERE
  // TEST CASES
  // const circle = new Circle(5);
  // console.log(circle.area()); // ~78.54 (Math.PI * 25)
  //
  // const rect = new Rectangle(4, 6);
  // console.log(rect.area()); // 24
}

/*
 * SOLUTION 3:
 *
 * class Shape {
 *     area() {
 *         return 0;
 *     }
 * }
 *
 * class Circle extends Shape {
 *     constructor(radius) {
 *         super();
 *         this.radius = radius;
 *     }
 *
 *     area() {
 *         return Math.PI * this.radius ** 2;
 *     }
 * }
 *
 * class Rectangle extends Shape {
 *     constructor(width, height) {
 *         super();
 *         this.width = width;
 *         this.height = height;
 *     }
 *
 *     area() {
 *         return this.width * this.height;
 *     }
 * }
 */

/**
 * EXERCISE 4: Using super.method()
 * Difficulty: ⭐⭐
 *
 * Create:
 * - Vehicle class with describe() returning "Vehicle: [make] [model]"
 * - Car class with doors, describe() should include parent info + doors
 * - Use super.describe() in Car
 */
function exercise4() {
  // YOUR CODE HERE
  // TEST CASES
  // const car = new Car("Toyota", "Camry", 4);
  // console.log(car.describe());
  // // "Vehicle: Toyota Camry, Doors: 4"
}

/*
 * SOLUTION 4:
 *
 * class Vehicle {
 *     constructor(make, model) {
 *         this.make = make;
 *         this.model = model;
 *     }
 *
 *     describe() {
 *         return `Vehicle: ${this.make} ${this.model}`;
 *     }
 * }
 *
 * class Car extends Vehicle {
 *     constructor(make, model, doors) {
 *         super(make, model);
 *         this.doors = doors;
 *     }
 *
 *     describe() {
 *         return `${super.describe()}, Doors: ${this.doors}`;
 *     }
 * }
 */

/**
 * EXERCISE 5: Custom Error Class
 * Difficulty: ⭐⭐
 *
 * Create a ValidationError class that:
 * - Extends Error
 * - Has field property (which field failed)
 * - Has value property (the invalid value)
 * - Sets appropriate error name
 */
function exercise5() {
  // YOUR CODE HERE
  // TEST CASES
  // const error = new ValidationError("email", "invalid", "Invalid email format");
  // console.log(error.name);    // "ValidationError"
  // console.log(error.message); // "Invalid email format"
  // console.log(error.field);   // "email"
  // console.log(error.value);   // "invalid"
  // console.log(error instanceof Error); // true
}

/*
 * SOLUTION 5:
 *
 * class ValidationError extends Error {
 *     constructor(field, value, message) {
 *         super(message);
 *         this.name = "ValidationError";
 *         this.field = field;
 *         this.value = value;
 *     }
 * }
 */

/**
 * EXERCISE 6: Extending Array
 * Difficulty: ⭐⭐
 *
 * Create a Queue class that extends Array:
 * - enqueue(item) - adds to end
 * - dequeue() - removes and returns from front
 * - peek() - returns front without removing
 * - isEmpty() - check if empty
 */
function exercise6() {
  // YOUR CODE HERE
  // TEST CASES
  // const queue = new Queue();
  // queue.enqueue(1);
  // queue.enqueue(2);
  // queue.enqueue(3);
  // console.log(queue.peek());    // 1
  // console.log(queue.dequeue()); // 1
  // console.log(queue.dequeue()); // 2
  // console.log(queue.isEmpty()); // false
  // console.log(queue.dequeue()); // 3
  // console.log(queue.isEmpty()); // true
}

/*
 * SOLUTION 6:
 *
 * class Queue extends Array {
 *     enqueue(item) {
 *         this.push(item);
 *         return this;
 *     }
 *
 *     dequeue() {
 *         return this.shift();
 *     }
 *
 *     peek() {
 *         return this[0];
 *     }
 *
 *     isEmpty() {
 *         return this.length === 0;
 *     }
 * }
 */

/**
 * EXERCISE 7: Three-Level Inheritance
 * Difficulty: ⭐⭐
 *
 * Create:
 * - Entity: id, createdAt
 * - User: extends Entity, adds name, email
 * - Admin: extends User, adds permissions array and hasPermission(perm)
 */
function exercise7() {
  // YOUR CODE HERE
  // TEST CASES
  // const admin = new Admin(1, "Alice", "alice@example.com", ["read", "write", "delete"]);
  // console.log(admin.id);         // 1
  // console.log(admin.name);       // "Alice"
  // console.log(admin.createdAt);  // Date object
  // console.log(admin.hasPermission("write"));  // true
  // console.log(admin.hasPermission("admin"));  // false
  // console.log(admin instanceof Entity); // true
  // console.log(admin instanceof User);   // true
  // console.log(admin instanceof Admin);  // true
}

/*
 * SOLUTION 7:
 *
 * class Entity {
 *     constructor(id) {
 *         this.id = id;
 *         this.createdAt = new Date();
 *     }
 * }
 *
 * class User extends Entity {
 *     constructor(id, name, email) {
 *         super(id);
 *         this.name = name;
 *         this.email = email;
 *     }
 * }
 *
 * class Admin extends User {
 *     constructor(id, name, email, permissions) {
 *         super(id, name, email);
 *         this.permissions = permissions;
 *     }
 *
 *     hasPermission(perm) {
 *         return this.permissions.includes(perm);
 *     }
 * }
 */

/**
 * EXERCISE 8: Abstract Class Pattern
 * Difficulty: ⭐⭐⭐
 *
 * Create an abstract Formatter class:
 * - Cannot be instantiated directly (throw error if new.target === Formatter)
 * - Has abstract format(data) method
 * Create JSONFormatter and XMLFormatter implementations
 */
function exercise8() {
  // YOUR CODE HERE
  // TEST CASES
  // try {
  //     const formatter = new Formatter(); // Should throw
  // } catch (e) {
  //     console.log("Correctly throws:", e.message);
  // }
  //
  // const json = new JSONFormatter();
  // console.log(json.format({ name: "Alice", age: 30 }));
  // // '{"name":"Alice","age":30}'
  //
  // const xml = new XMLFormatter("user");
  // console.log(xml.format({ name: "Alice", age: 30 }));
  // // '<user><name>Alice</name><age>30</age></user>'
}

/*
 * SOLUTION 8:
 *
 * class Formatter {
 *     constructor() {
 *         if (new.target === Formatter) {
 *             throw new Error("Formatter is abstract and cannot be instantiated directly");
 *         }
 *     }
 *
 *     format(data) {
 *         throw new Error("format() must be implemented by subclass");
 *     }
 * }
 *
 * class JSONFormatter extends Formatter {
 *     format(data) {
 *         return JSON.stringify(data);
 *     }
 * }
 *
 * class XMLFormatter extends Formatter {
 *     constructor(rootElement = "root") {
 *         super();
 *         this.rootElement = rootElement;
 *     }
 *
 *     format(data) {
 *         const inner = Object.entries(data)
 *             .map(([key, value]) => `<${key}>${value}</${key}>`)
 *             .join('');
 *         return `<${this.rootElement}>${inner}</${this.rootElement}>`;
 *     }
 * }
 */

/**
 * EXERCISE 9: Polymorphic Processing
 * Difficulty: ⭐⭐⭐
 *
 * Create a payment system:
 * - PaymentMethod base class with process(amount) and getName()
 * - CreditCard, PayPal, BankTransfer implementations
 * - Create processPayment(method, amount) function that works with any payment type
 */
function exercise9() {
  // YOUR CODE HERE
  // TEST CASES
  // const methods = [
  //     new CreditCard("1234-5678-9012-3456"),
  //     new PayPal("user@example.com"),
  //     new BankTransfer("123456789")
  // ];
  //
  // for (const method of methods) {
  //     processPayment(method, 100);
  //     // Credit Card (*3456): Processing $100
  //     // PayPal (user@example.com): Processing $100
  //     // Bank Transfer (******789): Processing $100
  // }
}

/*
 * SOLUTION 9:
 *
 * class PaymentMethod {
 *     process(amount) {
 *         throw new Error("process() must be implemented");
 *     }
 *
 *     getName() {
 *         throw new Error("getName() must be implemented");
 *     }
 * }
 *
 * class CreditCard extends PaymentMethod {
 *     constructor(cardNumber) {
 *         super();
 *         this.cardNumber = cardNumber;
 *     }
 *
 *     process(amount) {
 *         return `Processing $${amount} with Credit Card`;
 *     }
 *
 *     getName() {
 *         return `Credit Card (*${this.cardNumber.slice(-4)})`;
 *     }
 * }
 *
 * class PayPal extends PaymentMethod {
 *     constructor(email) {
 *         super();
 *         this.email = email;
 *     }
 *
 *     process(amount) {
 *         return `Processing $${amount} with PayPal`;
 *     }
 *
 *     getName() {
 *         return `PayPal (${this.email})`;
 *     }
 * }
 *
 * class BankTransfer extends PaymentMethod {
 *     constructor(accountNumber) {
 *         super();
 *         this.accountNumber = accountNumber;
 *     }
 *
 *     process(amount) {
 *         return `Processing $${amount} with Bank Transfer`;
 *     }
 *
 *     getName() {
 *         return `Bank Transfer (******${this.accountNumber.slice(-3)})`;
 *     }
 * }
 *
 * function processPayment(method, amount) {
 *     console.log(`${method.getName()}: ${method.process(amount)}`);
 * }
 */

/**
 * EXERCISE 10: Template Method Pattern
 * Difficulty: ⭐⭐⭐
 *
 * Create a Report base class with generate() template method:
 * - fetchData() - abstract, must be implemented
 * - processData(data) - can be overridden, default returns data as-is
 * - formatOutput(data) - abstract, must be implemented
 * - generate() calls these in order and returns result
 *
 * Create SalesReport and InventoryReport implementations
 */
function exercise10() {
  // YOUR CODE HERE
  // TEST CASES
  // const salesReport = new SalesReport();
  // console.log(salesReport.generate());
  // // Should fetch sales data, optionally process, and format
  //
  // const inventoryReport = new InventoryReport();
  // console.log(inventoryReport.generate());
}

/*
 * SOLUTION 10:
 *
 * class Report {
 *     fetchData() {
 *         throw new Error("fetchData() must be implemented");
 *     }
 *
 *     processData(data) {
 *         return data; // Default: no processing
 *     }
 *
 *     formatOutput(data) {
 *         throw new Error("formatOutput() must be implemented");
 *     }
 *
 *     generate() {
 *         const data = this.fetchData();
 *         const processed = this.processData(data);
 *         return this.formatOutput(processed);
 *     }
 * }
 *
 * class SalesReport extends Report {
 *     fetchData() {
 *         return [
 *             { product: "Widget", sales: 100 },
 *             { product: "Gadget", sales: 50 }
 *         ];
 *     }
 *
 *     processData(data) {
 *         const total = data.reduce((sum, item) => sum + item.sales, 0);
 *         return { items: data, total };
 *     }
 *
 *     formatOutput(data) {
 *         return `Sales Report\n${data.items.map(i =>
 *             `${i.product}: ${i.sales}`).join('\n')}\nTotal: ${data.total}`;
 *     }
 * }
 *
 * class InventoryReport extends Report {
 *     fetchData() {
 *         return [
 *             { item: "Widget", quantity: 200 },
 *             { item: "Gadget", quantity: 75 }
 *         ];
 *     }
 *
 *     formatOutput(data) {
 *         return `Inventory Report\n${data.map(i =>
 *             `${i.item}: ${i.quantity} in stock`).join('\n')}`;
 *     }
 * }
 */

/**
 * EXERCISE 11: Extending Map with Expiry
 * Difficulty: ⭐⭐⭐
 *
 * Create ExpiringMap that extends Map:
 * - set(key, value, ttl) - ttl in milliseconds
 * - get(key) - returns undefined if expired
 * - cleanup() - removes all expired entries
 */
function exercise11() {
  // YOUR CODE HERE
  // TEST CASES (use setTimeout to test expiry)
  // const cache = new ExpiringMap();
  // cache.set("key1", "value1", 100); // expires in 100ms
  // cache.set("key2", "value2", 1000); // expires in 1000ms
  //
  // console.log(cache.get("key1")); // "value1"
  // console.log(cache.get("key2")); // "value2"
  //
  // setTimeout(() => {
  //     console.log(cache.get("key1")); // undefined (expired)
  //     console.log(cache.get("key2")); // "value2" (still valid)
  // }, 200);
}

/*
 * SOLUTION 11:
 *
 * class ExpiringMap extends Map {
 *     constructor() {
 *         super();
 *         this._expiries = new Map();
 *     }
 *
 *     set(key, value, ttl) {
 *         super.set(key, value);
 *         if (ttl) {
 *             this._expiries.set(key, Date.now() + ttl);
 *         }
 *         return this;
 *     }
 *
 *     get(key) {
 *         if (this._expiries.has(key)) {
 *             if (Date.now() > this._expiries.get(key)) {
 *                 this.delete(key);
 *                 return undefined;
 *             }
 *         }
 *         return super.get(key);
 *     }
 *
 *     delete(key) {
 *         this._expiries.delete(key);
 *         return super.delete(key);
 *     }
 *
 *     cleanup() {
 *         const now = Date.now();
 *         for (const [key, expiry] of this._expiries) {
 *             if (now > expiry) {
 *                 this.delete(key);
 *             }
 *         }
 *     }
 * }
 */

/**
 * EXERCISE 12: Event-Driven Inheritance
 * Difficulty: ⭐⭐⭐⭐
 *
 * Create:
 * - EventEmitter base class with on(), off(), emit()
 * - ObservableCollection extending EventEmitter
 *   - Emits 'add', 'remove', 'clear' events
 *   - Has add(), remove(), clear(), items getter
 */
function exercise12() {
  // YOUR CODE HERE
  // TEST CASES
  // const collection = new ObservableCollection();
  //
  // collection.on('add', (item) => console.log(`Added: ${item}`));
  // collection.on('remove', (item) => console.log(`Removed: ${item}`));
  // collection.on('clear', () => console.log('Collection cleared'));
  //
  // collection.add('A'); // Logs: "Added: A"
  // collection.add('B'); // Logs: "Added: B"
  // collection.remove('A'); // Logs: "Removed: A"
  // console.log(collection.items); // ['B']
  // collection.clear(); // Logs: "Collection cleared"
}

/*
 * SOLUTION 12:
 *
 * class EventEmitter {
 *     constructor() {
 *         this._events = new Map();
 *     }
 *
 *     on(event, callback) {
 *         if (!this._events.has(event)) {
 *             this._events.set(event, []);
 *         }
 *         this._events.get(event).push(callback);
 *         return this;
 *     }
 *
 *     off(event, callback) {
 *         if (this._events.has(event)) {
 *             const callbacks = this._events.get(event);
 *             const index = callbacks.indexOf(callback);
 *             if (index > -1) {
 *                 callbacks.splice(index, 1);
 *             }
 *         }
 *         return this;
 *     }
 *
 *     emit(event, ...args) {
 *         if (this._events.has(event)) {
 *             for (const callback of this._events.get(event)) {
 *                 callback(...args);
 *             }
 *         }
 *         return this;
 *     }
 * }
 *
 * class ObservableCollection extends EventEmitter {
 *     constructor() {
 *         super();
 *         this._items = [];
 *     }
 *
 *     add(item) {
 *         this._items.push(item);
 *         this.emit('add', item);
 *         return this;
 *     }
 *
 *     remove(item) {
 *         const index = this._items.indexOf(item);
 *         if (index > -1) {
 *             this._items.splice(index, 1);
 *             this.emit('remove', item);
 *         }
 *         return this;
 *     }
 *
 *     clear() {
 *         this._items = [];
 *         this.emit('clear');
 *         return this;
 *     }
 *
 *     get items() {
 *         return [...this._items];
 *     }
 * }
 */

/**
 * EXERCISE 13: Mixin Factory
 * Difficulty: ⭐⭐⭐⭐
 *
 * Create mixin factories:
 * - Timestamped(Base) - adds createdAt, updatedAt, touch()
 * - Identifiable(Base) - adds auto-generated id
 * - Comparable(Base) - adds compareTo(other), requires implementing compareKey()
 *
 * Create a Task class using all three mixins
 */
function exercise13() {
  // YOUR CODE HERE
  // TEST CASES
  // const task1 = new Task("Learn JavaScript", 1);
  // const task2 = new Task("Build Project", 2);
  //
  // console.log(task1.id);        // Auto-generated unique ID
  // console.log(task1.createdAt); // Date object
  //
  // task1.touch();
  // console.log(task1.updatedAt); // Updated date
  //
  // console.log(task1.compareTo(task2)); // -1 (priority comparison)
}

/*
 * SOLUTION 13:
 *
 * const Timestamped = (Base) => class extends Base {
 *     constructor(...args) {
 *         super(...args);
 *         this.createdAt = new Date();
 *         this.updatedAt = new Date();
 *     }
 *
 *     touch() {
 *         this.updatedAt = new Date();
 *         return this;
 *     }
 * };
 *
 * const Identifiable = (Base) => class extends Base {
 *     static _nextId = 1;
 *
 *     constructor(...args) {
 *         super(...args);
 *         this.id = this.constructor._nextId++;
 *     }
 * };
 *
 * const Comparable = (Base) => class extends Base {
 *     compareKey() {
 *         throw new Error("compareKey() must be implemented");
 *     }
 *
 *     compareTo(other) {
 *         const a = this.compareKey();
 *         const b = other.compareKey();
 *         if (a < b) return -1;
 *         if (a > b) return 1;
 *         return 0;
 *     }
 * };
 *
 * class BaseTask {
 *     constructor(title, priority) {
 *         this.title = title;
 *         this.priority = priority;
 *     }
 * }
 *
 * class Task extends Comparable(Identifiable(Timestamped(BaseTask))) {
 *     compareKey() {
 *         return this.priority;
 *     }
 * }
 */

/**
 * EXERCISE 14: Repository Pattern with Inheritance
 * Difficulty: ⭐⭐⭐⭐
 *
 * Create:
 * - Repository<T> abstract class with:
 *   - Abstract: findById(id), findAll(), save(entity), delete(id)
 *   - Concrete: exists(id), count()
 * - InMemoryRepository implementation storing data in Map
 * - UserRepository extending InMemoryRepository with findByEmail()
 */
function exercise14() {
  // YOUR CODE HERE
  // TEST CASES
  // const userRepo = new UserRepository();
  //
  // userRepo.save({ id: 1, name: "Alice", email: "alice@example.com" });
  // userRepo.save({ id: 2, name: "Bob", email: "bob@example.com" });
  //
  // console.log(userRepo.count());       // 2
  // console.log(userRepo.exists(1));     // true
  // console.log(userRepo.findById(1));   // { id: 1, name: "Alice", ... }
  // console.log(userRepo.findByEmail("bob@example.com")); // { id: 2, name: "Bob", ... }
  // console.log(userRepo.findAll().length); // 2
  //
  // userRepo.delete(1);
  // console.log(userRepo.count());       // 1
}

/*
 * SOLUTION 14:
 *
 * class Repository {
 *     constructor() {
 *         if (new.target === Repository) {
 *             throw new Error("Repository is abstract");
 *         }
 *     }
 *
 *     findById(id) { throw new Error("Not implemented"); }
 *     findAll() { throw new Error("Not implemented"); }
 *     save(entity) { throw new Error("Not implemented"); }
 *     delete(id) { throw new Error("Not implemented"); }
 *
 *     exists(id) {
 *         return this.findById(id) !== null;
 *     }
 *
 *     count() {
 *         return this.findAll().length;
 *     }
 * }
 *
 * class InMemoryRepository extends Repository {
 *     constructor() {
 *         super();
 *         this._store = new Map();
 *     }
 *
 *     findById(id) {
 *         return this._store.get(id) || null;
 *     }
 *
 *     findAll() {
 *         return [...this._store.values()];
 *     }
 *
 *     save(entity) {
 *         this._store.set(entity.id, entity);
 *         return entity;
 *     }
 *
 *     delete(id) {
 *         return this._store.delete(id);
 *     }
 * }
 *
 * class UserRepository extends InMemoryRepository {
 *     findByEmail(email) {
 *         return this.findAll().find(user => user.email === email) || null;
 *     }
 * }
 */

/**
 * EXERCISE 15: Complete Application Example
 * Difficulty: ⭐⭐⭐⭐⭐
 *
 * Build a game entity system:
 * - GameObject: id, position {x, y}, update(), render()
 * - MovableObject extends GameObject: velocity, move()
 * - CollidableObject extends MovableObject: hitbox, collidesWith(other)
 * - Player extends CollidableObject: health, takeDamage(), isAlive()
 * - Enemy extends CollidableObject: damage, attack(target)
 *
 * Create a simple game loop simulation
 */
function exercise15() {
  // YOUR CODE HERE
  // TEST CASES
  // const player = new Player(1, { x: 0, y: 0 }, 100);
  // const enemy = new Enemy(2, { x: 5, y: 0 }, 10);
  //
  // // Move player
  // player.velocity = { x: 1, y: 0 };
  // player.move();
  // console.log(player.position); // { x: 1, y: 0 }
  //
  // // Check collision (simplified)
  // enemy.position = { x: 1, y: 0 };
  // if (player.collidesWith(enemy)) {
  //     enemy.attack(player);
  //     console.log(player.health); // 90
  // }
  //
  // // Game loop simulation
  // console.log(player.isAlive()); // true
}

/*
 * SOLUTION 15:
 *
 * class GameObject {
 *     static nextId = 1;
 *
 *     constructor(position = { x: 0, y: 0 }) {
 *         this.id = GameObject.nextId++;
 *         this.position = { ...position };
 *     }
 *
 *     update() {
 *         // Override in subclasses
 *     }
 *
 *     render() {
 *         console.log(`Object ${this.id} at (${this.position.x}, ${this.position.y})`);
 *     }
 * }
 *
 * class MovableObject extends GameObject {
 *     constructor(position) {
 *         super(position);
 *         this.velocity = { x: 0, y: 0 };
 *     }
 *
 *     move() {
 *         this.position.x += this.velocity.x;
 *         this.position.y += this.velocity.y;
 *         return this;
 *     }
 *
 *     update() {
 *         super.update();
 *         this.move();
 *     }
 * }
 *
 * class CollidableObject extends MovableObject {
 *     constructor(position, hitboxSize = 1) {
 *         super(position);
 *         this.hitboxSize = hitboxSize;
 *     }
 *
 *     collidesWith(other) {
 *         const dx = Math.abs(this.position.x - other.position.x);
 *         const dy = Math.abs(this.position.y - other.position.y);
 *         const minDist = (this.hitboxSize + other.hitboxSize) / 2;
 *         return dx < minDist && dy < minDist;
 *     }
 * }
 *
 * class Player extends CollidableObject {
 *     constructor(position, health = 100) {
 *         super(position, 1);
 *         this.health = health;
 *         this.maxHealth = health;
 *     }
 *
 *     takeDamage(amount) {
 *         this.health = Math.max(0, this.health - amount);
 *         console.log(`Player took ${amount} damage! Health: ${this.health}`);
 *     }
 *
 *     isAlive() {
 *         return this.health > 0;
 *     }
 *
 *     render() {
 *         console.log(`Player [HP: ${this.health}/${this.maxHealth}] at (${this.position.x}, ${this.position.y})`);
 *     }
 * }
 *
 * class Enemy extends CollidableObject {
 *     constructor(position, damage = 10) {
 *         super(position, 1);
 *         this.damage = damage;
 *     }
 *
 *     attack(target) {
 *         if (target.takeDamage) {
 *             target.takeDamage(this.damage);
 *         }
 *     }
 *
 *     render() {
 *         console.log(`Enemy [DMG: ${this.damage}] at (${this.position.x}, ${this.position.y})`);
 *     }
 * }
 *
 * // Game loop simulation
 * function gameLoop(entities, frames = 5) {
 *     for (let frame = 0; frame < frames; frame++) {
 *         console.log(`\n--- Frame ${frame + 1} ---`);
 *
 *         // Update all entities
 *         for (const entity of entities) {
 *             entity.update();
 *         }
 *
 *         // Check collisions
 *         for (let i = 0; i < entities.length; i++) {
 *             for (let j = i + 1; j < entities.length; j++) {
 *                 if (entities[i].collidesWith(entities[j])) {
 *                     console.log(`Collision between ${entities[i].constructor.name} and ${entities[j].constructor.name}`);
 *                 }
 *             }
 *         }
 *
 *         // Render all entities
 *         for (const entity of entities) {
 *             entity.render();
 *         }
 *     }
 * }
 */

// Run exercises
console.log('=== Class Inheritance Exercises ===\n');

// Uncomment to run individual exercises
// exercise1();
// exercise2();
// exercise3();
// exercise4();
// exercise5();
// exercise6();
// exercise7();
// exercise8();
// exercise9();
// exercise10();
// exercise11();
// exercise12();
// exercise13();
// exercise14();
// exercise15();

console.log('\nUncomment exercises to practice!');
Exercises - JavaScript Tutorial | DeepML