From d32f8c8db9fcd5ebeb7bc21aeb56f974e09f3587 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 01:57:25 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Sprint=20pedag=C3=B3gico=20C#=20POO=20I:=20?= =?UTF-8?q?U1=20rehecha,=20correcciones=20concretas=20U2-U8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U1 (rehecha, 4 → 6 lecciones): separa la lección comprimida en seis pasos que agregan una idea a la vez. pensar-en-objetos ya sólo enseña class/ campos con dominio Producto (sin new/objeto/método); crear-un-objeto es nueva y aísla new/objeto/instancia/acceso con punto; clase-objeto- instancia deja de usar un método Mostrar() para demostrar independencia de estado; estado-y-comportamiento se queda sólo con la traza antes/ después; responsabilidad-del-objeto es nueva y aísla "quién debe hacer la operación" (con if, sin private); abstraccion-con-criterio no cambia. U2: visibilidad ya no mezcla símbolos UML (+/-/#) con la explicación de public/private; protected se menciona en una frase y se remite a U5. Práctica Conversor (utilidad sin estado, instanciada porque sí) se reemplaza por Cronometro, con estado real acumulado por las dos sobrecargas de Registrar. U3: el paso final de csharp-a-uml ya no es "usa Semaforo desde Main" (sólo evaluaba C#); ahora hay un fill_blank que evalúa extracción real de UML antes del challenge de uso. Práctica de Estudiante ya no exige un constructor que el contrato UML no declaraba. Práctica codigo-a-uml-pelicula deja de evaluar sólo C# y ahora pide imprimir el diagrama UML de una clase ya escrita (código→UML real, verificado por stdout). U4: elegir-relacion ya no nombra la relación en el enunciado (antes decía "se asocia con"/"compone"); ahora describe el dominio y el alumno decide. Práctica "Equipo y entrenador" deja de construir el Entrenador inline dentro de new Equipo(...) — ahora se crea aparte, para visualizar una parte preexistente e independiente, como pide la lección. U5: generalizacion-herencia usaba base(...) antes de que base-protected lo explicara — se corrigió a constructores vacíos + campos públicos, aislando sólo la relación es-un. virtual-override-polimorfismo ahora abre con una predicción (Empleado e = new Vendedor(); e.Pago(); SIN virtual/override) antes de introducir el mecanismo, y separa el salto a arreglo+ciclo en su propio paso guiado. Práctica Medicion (Temperatura/ Distancia, abstracción sin significado compartido) se reemplaza por Figura (Rectangulo/Circulo, Area()). U6: Cuenta ahora valida el saldo inicial no negativo desde el constructor (antes prometía el invariante y no lo exigía al nacer). RenglonPedido corrige su vocabulario: es asociación con un Articulo de catálogo, no composición. miniproyecto-dominio corrige "composición" → "asociación" para Alumno/Laboratorio recibidos por Reserva. U7: Saludador, Cotizador y ContadorServicio ganan structureContract (antes se aprobaban con sólo stdout, sin verificar que fueran clases). U8 (unpublished): requisitos-uml-aceptacion ya no promete Cliente y CalculadorDescuento para luego implementar sólo Producto/Cotizacion — el modelo es uno solo, consistente en toda la unidad. Corrige "Cotizacion contiene sus renglones" (composición) → asociación con un Producto existente. implementar-capa-dominio gana structureContract obligatorio (antes el reto final de dominio se aprobaba reproduciendo stdout desde un Main sin clases). Las cuatro prácticas independientes ganan structureContract; la de Orden corrige "compone" → asociación. Verificación: 79/79 piezas de C# compilan y producen la salida esperada (mcs/mono), 96/96 tests de contenido/estructura, 440/440 suite completa, typecheck y lint limpios, build compila (falla sólo por DATABASE_URL/ BETTER_AUTH_SECRET ausentes del sandbox, confirmado con valores dummy). U7/U8 siguen published: false. Sin POO II, sin colecciones genéricas, sin interfaces C#. Slugs existentes conservados; sólo se añadieron los dos slugs de lección que pedía el spec de U1. --- prisma/content/csharp/unidad-01-modelar.ts | 593 +++++++++++++----- prisma/content/csharp/unidad-02-encapsular.ts | 7 +- prisma/content/csharp/unidad-03-uml.ts | 33 +- prisma/content/csharp/unidad-04-relaciones.ts | 2 +- prisma/content/csharp/unidad-05-herencia.ts | 116 +++- .../csharp/unidad-06-diseno-robusto.ts | 6 +- prisma/content/csharp/unidad-08-integrador.ts | 33 +- .../exercises/csharp/u02-encapsular.ts | 63 +- prisma/content/exercises/csharp/u03-uml.ts | 78 +-- .../exercises/csharp/u04-relaciones.ts | 12 +- .../content/exercises/csharp/u05-herencia.ts | 84 +-- .../exercises/csharp/u06-diseno-robusto.ts | 17 +- prisma/content/exercises/csharp/u07-gui.ts | 25 + .../exercises/csharp/u08-integrador.ts | 70 ++- tests/content/structure-contracts.test.ts | 7 +- 15 files changed, 846 insertions(+), 300 deletions(-) diff --git a/prisma/content/csharp/unidad-01-modelar.ts b/prisma/content/csharp/unidad-01-modelar.ts index d61b628..2a213e4 100644 --- a/prisma/content/csharp/unidad-01-modelar.ts +++ b/prisma/content/csharp/unidad-01-modelar.ts @@ -3,9 +3,10 @@ import type { UnitDefinition } from "../types"; // ===================================================================== // De problemas a objetos // -// La unidad va de lo concreto a lo formal, en este orden y sin saltos: -// 1. un objeto concreto del problema (qué recuerda / qué hace) -// 2. del ejemplar al modelo: clase, objeto, instancia, `new` +// La unidad va de lo concreto a lo formal, en SEIS pasos pequeños y sin +// saltos — cada uno agrega UNA idea nueva: +// 1. datos repetidos → class, tipo propio, campos (sin new, sin objeto) +// 2. new, objeto, instancia, acceso con `.` // 3. una clase, dos objetos independientes // 4. estado y comportamiento, con traza antes → llamada → después // 5. responsabilidad: quién debe hacer la operación @@ -29,141 +30,294 @@ export const unidad01: UnitDefinition = { published: true, lessons: [ /** - * Etapas 1 y 2: un objeto concreto y, sólo después, los nombres - * formales (clase, objeto, instancia, `new`). + * Etapa 1: SÓLO class / tipo propio / campos. Nada de new, objeto, + * instancia, métodos, responsabilidad, referencias ni encapsulación + * — eso viene en las lecciones siguientes, una idea a la vez. */ { slug: "pensar-en-objetos", - title: "Pensar en objetos, no en una lista de instrucciones", - description: "Un objeto concreto: qué necesita recordar y qué necesita hacer.", - estimatedMinutes: 9, - xpReward: 30, + title: "De variables sueltas a una clase", + description: "Agrupa datos que se repiten en un molde propio: la clase.", + estimatedMinutes: 8, + xpReward: 25, steps: [ { type: "theory", - markdown: `# Un taladro del taller + markdown: `# Los mismos dos datos, otra vez -En el taller del CETI hay un taladro. Para controlar los préstamos, del taladro sólo importan dos cosas: +En la tienda del taller cada producto tiene un nombre y unas existencias: -- **cómo se llama**: \`Taladro\` -- **cuántas piezas quedan**: \`3\` +\`\`\`csharp +string nombre = "Taladro"; +int existencias = 3; +\`\`\` -Y una sola acción: **mostrar su ficha**, algo como \`Taladro: 3\`. +Pero la tienda no tiene un solo producto. Tiene muchos: -Eso es todo por ahora. Nada de herencia, nada de diagramas. +\`\`\`csharp +string nombre1 = "Taladro"; +int existencias1 = 3; -Fíjate en cómo se parte en dos: hay cosas que el taladro **recuerda** (nombre, existencias) y cosas que **hace** (mostrar su ficha). Esa separación es la idea central de la unidad; los nombres formales vienen en la pantalla siguiente, cuando ya la hayas visto funcionando.`, +string nombre2 = "Sierra"; +int existencias2 = 8; +\`\`\` + +Es el mismo par de datos, copiado, con un número al final del nombre para no chocar. Con diez productos serían veinte variables sueltas, sin nada que diga que \`nombre1\` y \`existencias1\` van juntos. + +Falta un molde que diga: **todo Producto tiene un nombre y unas existencias.**`, }, { - type: "code_example", - code: `using System; + type: "theory", + markdown: `# \`class\`: un tipo que tú defines + +C# ya trae tipos como \`string\` e \`int\`. Con \`class\` defines uno **tuyo**: -class Herramienta +\`\`\`csharp +class Producto { public string Nombre; public int Existencias; +} +\`\`\` - public void Mostrar() +- \`class Producto\` declara el tipo nuevo. +- \`Nombre\` y \`Existencias\` son sus **campos**: los datos que va a tener todo Producto. +- \`public\` va antes de cada campo por ahora de forma mecánica — cópialo tal cual. En U2 verás qué significa exactamente y cuándo conviene cambiarlo. + +Esto todavía **no crea ningún producto**. Es sólo el molde: dice qué tendrá un Producto, no cuál. Eso viene en la siguiente lección.`, + }, + { + type: "code_example", + code: `class Producto +{ + public string Nombre; + public int Existencias; +}`, + explanation: "Dos campos, un molde. No hay Main aquí a propósito: una clase por sí sola no es un programa completo, y todavía no estamos creando ningún producto — sólo describiendo la forma que tendrán todos.", + runnable: false, + localOnlyNote: "Es sólo la declaración del molde: no se ejecuta todavía, se lee.", + }, + { + type: "quiz", + question: "Dentro de `class Producto { public string Nombre; public int Existencias; }`, ¿qué es `Existencias`?", + options: [ + "Un campo: un dato que cada Producto va a tener", + "Una variable local dentro de Main", + "El nombre de la clase", + "Un valor concreto, como 3", + ], + feedbackPerOption: [ + "", + "No está dentro de ningún método; está dentro de la clase.", + "El nombre de la clase es Producto.", + "Todavía no hay ningún valor: la clase sólo describe qué campo va a existir.", + ], + correctIndex: 0, + explanation: "Los campos describen los datos que tendrá cada objeto de esa clase. Por ahora es sólo descripción: ningún Producto existe todavía.", + }, + { + type: "fill_blank", + prompt: "Completa la declaración de una clase Herramienta con dos campos.", + template: `{{0}} Herramienta +{ + public string {{1}}; + public int {{2}}; +}`, + blanks: [ + { answer: "class", hint: "La palabra que declara un tipo propio." }, + { answer: "Nombre", hint: "Un campo de texto, con mayúscula inicial." }, + { answer: "Existencias", hint: "Un campo numérico, con mayúscula inicial." }, + ], + explanation: "class declara el molde; dentro van los campos con su tipo y su nombre, cada uno con `public` por ahora.", + }, + { + type: "code_challenge", + exercise: { + prompt: `## Tu turno: sólo el molde + +El taller de robótica quiere modelar un \`Curso\`: cada curso tiene un **nombre** y un **cupo disponible**. + +Declara la clase \`Curso\` con: + +- un campo público \`Nombre\` de tipo \`string\` +- un campo público \`CupoDisponible\` de tipo \`int\` + +Todavía no vas a crear ningún objeto — sólo el molde. \`Main\` se queda vacío.`, + starterCode: `using System; + +// Declara aquí la clase Curso + +class Program +{ + static void Main() { - Console.WriteLine($"{Nombre}: {Existencias}"); } +}`, + solutionCode: `using System; + +class Curso +{ + public string Nombre; + public int CupoDisponible; } class Program { static void Main() { - Herramienta taladro = new Herramienta(); - taladro.Nombre = "Taladro"; - taladro.Existencias = 3; - taladro.Mostrar(); } }`, - explanation: "Arriba está el taladro del que hablamos. `Nombre` y `Existencias` son lo que recuerda; `Mostrar()` es lo que hace. Córrelo y cambia el 3 por otro número: verás que la ficha cambia con él. Los campos son públicos sólo para ver la mecánica primero; en la unidad siguiente protegeremos el estado.", - runnable: true, - expectedOutput: "Taladro: 3", + hints: [ + "Empieza con class Curso, fuera de Program.", + "Dos campos públicos: uno string, uno int.", + "No necesitas new ni Main con contenido: sólo el molde.", + ], + difficulty: "easy", + xpReward: 20, + structure: { + classes: [ + { + name: "Curso", + fields: [ + { name: "Nombre", visibility: "public", type: "string" }, + { name: "CupoDisponible", visibility: "public", type: "int" }, + ], + }, + ], + }, + testCases: [ + { + stdin: "", + expectedStdout: "", + visible: true, + description: "Sólo se evalúa la estructura de la clase", + }, + ], + }, }, + ], + }, + /** + * Etapa 2 (NUEVA): new, objeto, instancia, acceso con `.`. Reutiliza + * la clase Producto de la lección anterior. Nada de "referencia" ni + * de métodos: sólo crear y usar. + */ + { + slug: "crear-un-objeto", + title: "Crear y usar un objeto", + description: "El molde no basta: hay que construir uno con new.", + estimatedMinutes: 9, + xpReward: 28, + steps: [ { type: "theory", - markdown: `# Ahora sí, los nombres + markdown: `# La clase describe, el objeto existe -Ya viste el código funcionando. Ponle nombre a cada pieza: +Con la clase \`Producto\` de la lección anterior todavía no tienes ningún producto: sólo el molde. Para tener uno de verdad: -- \`class Herramienta\` es la **clase**: el molde. Describe qué recordará y qué hará *cualquier* herramienta. -- \`taladro\` es un **objeto** (también se le dice **instancia**): una herramienta concreta, con sus propios datos. -- \`new Herramienta()\` es lo que **crea** el objeto. Sin \`new\` sólo tienes el molde, no la pieza. +\`\`\`csharp +Producto taladro = new Producto(); +taladro.Nombre = "Taladro"; +taladro.Existencias = 3; +\`\`\` -Dicho corto: **la clase describe, el objeto existe.** +- \`new Producto()\` **construye** un producto concreto siguiendo el molde de la clase. +- \`taladro\` es la variable que guarda ese producto. También se le llama **objeto** o **instancia**. +- Con el punto \`.\` accedes a sus campos: \`taladro.Nombre\`, \`taladro.Existencias\`. -La clase no guarda "Taladro" ni el 3. Esos datos viven en el objeto.`, +Dicho corto: **la clase describe, el objeto existe.** Sin \`new\` sólo tienes el molde, no la pieza.`, }, { - type: "fill_blank", - prompt: "Completa la creación de un objeto a partir de la clase `Herramienta` y la asignación de su nombre.", - template: `{{0}} llave = {{1}} Herramienta(); -llave.{{2}} = "Llave";`, - blanks: [ - { answer: "Herramienta", hint: "El tipo de la variable es la clase, igual que en el ejemplo." }, - { answer: "new", hint: "La palabra que crea la instancia." }, - { answer: "Nombre", hint: "El campo que guarda cómo se llama. Respeta la mayúscula." }, - ], - explanation: "Mismo patrón del ejemplo: tipo, `new`, y luego se llenan los datos del objeto.", + type: "code_example", + code: `using System; + +class Producto +{ + public string Nombre; + public int Existencias; +} + +class Program +{ + static void Main() + { + Producto taladro = new Producto(); + taladro.Nombre = "Taladro"; + taladro.Existencias = 3; + + Console.WriteLine(taladro.Nombre); + Console.WriteLine(taladro.Existencias); + } +}`, + explanation: "new Producto() crea el objeto; las dos líneas siguientes llenan sus campos con el punto. Cambia el 3 por otro número y verás que la segunda línea impresa cambia con él.", + runnable: true, + expectedOutput: `Taladro +3`, }, { type: "quiz", - question: "En el código de la lección, ¿cuál de estos es un objeto (una instancia)?", + question: "¿Cuál de estas líneas CREA el objeto (no sólo lo usa)?", options: [ - "class Herramienta", - "public int Existencias", - "taladro", - "Mostrar()", + "taladro.Nombre = \"Taladro\";", + "Producto taladro = new Producto();", + "Console.WriteLine(taladro.Nombre);", + "class Producto { public string Nombre; public int Existencias; }", ], feedbackPerOption: [ - "Eso describe; todavía no existe ninguna herramienta concreta.", - "Eso es uno de los datos que la clase dice que habrá.", + "Eso asigna un campo de un objeto que ya existe.", "", - "Eso es una de las acciones que la clase describe.", + "Eso sólo lee y muestra un campo.", + "Eso es el molde; no crea ningún objeto por sí solo.", ], - correctIndex: 2, - explanation: "`new Herramienta()` crea el objeto y `taladro` es la variable que lo referencia: la clase describe, el objeto existe.", + correctIndex: 1, + explanation: "`new Producto()` es lo que construye el objeto. Todo lo demás asume que ya existe.", + }, + { + type: "fill_blank", + prompt: "Completa la creación de un objeto Producto y la asignación de sus dos campos.", + template: `{{0}} llave = {{1}} Producto(); +llave.{{2}} = "Llave"; +llave.{{3}} = 12;`, + blanks: [ + { answer: "Producto", hint: "El tipo de la variable es la clase." }, + { answer: "new", hint: "La palabra que crea la instancia." }, + { answer: "Nombre", hint: "El campo de texto. Respeta la mayúscula." }, + { answer: "Existencias", hint: "El campo numérico. Respeta la mayúscula." }, + ], + explanation: "Mismo patrón del ejemplo: tipo, new, y luego se llenan los dos campos del objeto con el punto.", }, { type: "code_challenge", exercise: { - prompt: `## Tu turno: la clase + prompt: `## Sólo crear y usar -\`Main\` ya está escrito: lee el nombre, crea el objeto, le asigna el nombre y llama a \`Presentar()\`. **Falta la clase.** +La clase \`Alumno\` ya está escrita, con dos campos: \`Nombre\` y \`Grupo\`. \`Main\` ya lee ambos datos. -Escribe \`FichaHerramienta\` con: - -- un campo público \`Nombre\` de tipo \`string\` -- un método \`Presentar()\` que escriba exactamente \`Herramienta: Nombre\` - -Es el mismo patrón del ejemplo, con un solo dato en vez de dos.`, +Te toca: crea el objeto, asígnale los datos leídos y muestra cada campo con \`Console.WriteLine\`, uno por línea. La clase no cambia — sólo la usas.`, starterCode: `using System; -// Escribe aquí la clase FichaHerramienta +class Alumno +{ + public string Nombre; + public string Grupo; +} class Program { static void Main() { string nombre = Console.ReadLine(); - FichaHerramienta ficha = new FichaHerramienta(); - ficha.Nombre = nombre; - ficha.Presentar(); + string grupo = Console.ReadLine(); + + // Crea el objeto, asígnale los datos y muestra Nombre y Grupo } }`, solutionCode: `using System; -class FichaHerramienta +class Alumno { public string Nombre; - - public void Presentar() - { - Console.WriteLine($"Herramienta: {Nombre}"); - } + public string Grupo; } class Program @@ -171,46 +325,47 @@ class Program static void Main() { string nombre = Console.ReadLine(); - FichaHerramienta ficha = new FichaHerramienta(); - ficha.Nombre = nombre; - ficha.Presentar(); + string grupo = Console.ReadLine(); + + Alumno alumno = new Alumno(); + alumno.Nombre = nombre; + alumno.Grupo = grupo; + + Console.WriteLine(alumno.Nombre); + Console.WriteLine(alumno.Grupo); } }`, hints: [ - "La clase va FUERA de Program, igual que Herramienta en el ejemplo.", - "Adentro: un campo `public string Nombre;` y un método `public void Presentar()`.", - "Presentar imprime con interpolación: $\"Herramienta: {Nombre}\".", + "Usa new Alumno() para crear el objeto.", + "Asigna cada campo con el punto: alumno.Nombre = nombre;", + "Imprime cada campo con su propio Console.WriteLine.", ], difficulty: "easy", - xpReward: 20, + xpReward: 22, structure: { classes: [ { - name: "FichaHerramienta", - fields: [{ name: "Nombre", visibility: "public", type: "string" }], - methods: [{ name: "Presentar", visibility: "public" }], + name: "Alumno", + fields: [ + { name: "Nombre", visibility: "public", type: "string" }, + { name: "Grupo", visibility: "public", type: "string" }, + ], }, ], }, testCases: [ { - stdin: "Martillo\n", - expectedStdout: "Herramienta: Martillo\n", + stdin: "Luna\n3A\n", + expectedStdout: "Luna\n3A\n", visible: true, description: "Caso visible", }, { - stdin: "Broca 1/4\n", - expectedStdout: "Herramienta: Broca 1/4\n", + stdin: "Iris Nava\n2B\n", + expectedStdout: "Iris Nava\n2B\n", visible: false, description: "Nombre con espacio", }, - { - stdin: "Nivel\n", - expectedStdout: "Herramienta: Nivel\n", - visible: false, - description: "Otro nombre", - }, ], }, }, @@ -218,7 +373,9 @@ class Program }, /** * Etapa 3: una clase, dos objetos independientes. Se manipula UNO y se - * comparan las dos salidas. Nada de `b = a`: eso llega al final de U2. + * comparan las dos salidas, con campos + Console.WriteLine directo + * (sin métodos: `Mostrar()` llega hasta la etapa 4). Nada de `b = a`: + * eso llega al final de U2. */ { slug: "clase-objeto-instancia", @@ -245,11 +402,6 @@ class Locker { public int Numero; public string Responsable; - - public void Mostrar() - { - Console.WriteLine($"Locker {Numero}: {Responsable}"); - } } class Program @@ -267,29 +419,29 @@ class Program // Le cambiamos el responsable SÓLO al primero. a.Responsable = "Sofia"; - a.Mostrar(); - b.Mostrar(); + Console.WriteLine($"Locker {a.Numero}: {a.Responsable}"); + Console.WriteLine($"Locker {b.Numero}: {b.Responsable}"); } }`, - explanation: "Dos `new`, dos objetos. Le cambiamos el responsable a `a` y `b` sigue con Luis: cada objeto tiene sus propios datos. Cambia la línea de Sofia por una que toque a `b` y verás moverse la otra línea, nunca las dos.", + explanation: "Dos `new`, dos objetos. Le cambiamos el responsable a `a` y `b` sigue con Luis: cada objeto tiene sus propios campos. Cambia la línea de Sofia por una que toque a `b` y verás moverse la otra línea, nunca las dos.", runnable: true, expectedOutput: `Locker 12: Sofia Locker 13: Luis`, }, { type: "quiz", - question: "Con dos objetos creados por separado (`Locker a = new Locker();` y `Locker b = new Locker();`), ¿qué imprime `b.Mostrar()` después de ejecutar `a.Numero = 99;`?", + question: "Con dos objetos creados por separado (`Locker a = new Locker();` y `Locker b = new Locker();`), ¿qué imprime `Console.WriteLine(b.Numero);` después de ejecutar `a.Numero = 99;`?", options: [ "El número que se le dio a b: el cambio en a no lo toca.", "99, porque comparten la clase Locker.", "0, porque asignar a un objeto borra el otro.", - "Nada: sólo se puede mostrar un objeto por programa.", + "Nada: un campo sólo se puede leer una vez.", ], feedbackPerOption: [ "", "La clase es el molde compartido, pero los datos no: viven en cada objeto.", "Asignar a un objeto no toca a los demás; nada se borra.", - "Puedes crear y mostrar tantos objetos como quieras.", + "Un campo se puede leer tantas veces como quieras.", ], correctIndex: 0, explanation: "Cada `new` reserva su propio estado. `a` y `b` comparten la clase, no los datos.", @@ -312,12 +464,12 @@ Locker 13: Luis`, exercise: { prompt: `## Dos libros, un molde -\`Main\` ya lee los datos y ya está escrito hasta la lectura. Te toca: +\`Main\` ya lee los datos. Te toca: -1. escribir la clase \`Libro\` con campos públicos \`Titulo\` (\`string\`) y \`Paginas\` (\`int\`) y un método \`Resumen()\` que escriba \`Titulo (N paginas)\`; -2. crear **dos** objetos distintos en \`Main\`, darle sus datos a cada uno y mostrar ambos. +1. escribir la clase \`Libro\` con campos públicos \`Titulo\` (\`string\`) y \`Paginas\` (\`int\`); +2. crear **dos** objetos distintos en \`Main\`, darle sus datos a cada uno y mostrar ambos con \`Console.WriteLine\`, formato \`Titulo (N paginas)\`. -La dimensión nueva frente al reto anterior es una sola: ahora son dos objetos.`, +La dimensión nueva frente al reto anterior es una sola: ahora son dos objetos. Todavía sin métodos: imprime los campos directamente.`, starterCode: `using System; // Escribe aquí la clase Libro @@ -340,11 +492,6 @@ class Libro { public string Titulo; public int Paginas; - - public void Resumen() - { - Console.WriteLine($"{Titulo} ({Paginas} paginas)"); - } } class Program @@ -364,14 +511,14 @@ class Program segundo.Titulo = titulo2; segundo.Paginas = paginas2; - primero.Resumen(); - segundo.Resumen(); + Console.WriteLine($"{primero.Titulo} ({primero.Paginas} paginas)"); + Console.WriteLine($"{segundo.Titulo} ({segundo.Paginas} paginas)"); } }`, hints: [ "La clase es como la del reto anterior, pero con dos campos.", "Cada objeto necesita su propio new: dos libros, dos new.", - "Asigna los datos al objeto correspondiente y llama Resumen una vez por objeto.", + "Imprime cada libro con su propia línea, usando sus campos directamente.", ], difficulty: "easy", xpReward: 22, @@ -383,7 +530,6 @@ class Program { name: "Titulo", visibility: "public", type: "string" }, { name: "Paginas", visibility: "public", type: "int" }, ], - methods: [{ name: "Resumen", visibility: "public" }], }, ], }, @@ -406,15 +552,16 @@ class Program ], }, /** - * Etapas 4 y 5: estado y comportamiento con traza antes → después, y - * luego responsabilidad (quién debe hacer la operación). + * Etapa 4: estado y comportamiento con traza antes → después. La + * responsabilidad (quién debe hacer la operación) se movió a su + * propia lección para no mezclar dos ideas nuevas en una sola. */ { slug: "estado-y-comportamiento", title: "Los métodos cambian el estado", - description: "Predice el valor antes y después de la llamada, y decide quién debe hacer la operación.", - estimatedMinutes: 11, - xpReward: 35, + description: "Predice el valor antes y después de la llamada a un método.", + estimatedMinutes: 9, + xpReward: 32, steps: [ { type: "theory", @@ -430,9 +577,7 @@ Sigue la traza con los ojos antes de correr nada: | se ejecuta \`tarjeta.Consumir(25)\` | — | | después de la llamada | \`75\` | -El dato que el objeto **recuerda** se llama **estado**. La acción que lo **cambia** se llama **comportamiento** (un método). - -Un método no es una función suelta: vive junto al dato que modifica.`, +El dato que el objeto **recuerda** se llama **estado**. La acción que lo **cambia** se llama **comportamiento** (un método).`, }, { type: "code_example", @@ -460,7 +605,7 @@ class Program Console.WriteLine(tarjeta.Saldo); // despues } }`, - explanation: "El programa imprime el estado antes y después de la llamada: 100 y 75. `Consumir` vive dentro de `TarjetaComedor` porque es esa clase la que sabe qué es el saldo. La validación (no dejar el saldo en negativo) llegará con encapsulamiento.", + explanation: "El programa imprime el estado antes y después de la llamada: 100 y 75. Consumir recibe un parámetro y usa ese valor para cambiar Saldo.", runnable: true, expectedOutput: `100 75`, @@ -478,26 +623,6 @@ class Program correctIndex: 2, explanation: "El método cambió el estado: 40 − 15 = 25. Después de la llamada, el objeto recuerda el nuevo valor.", }, - { - type: "theory", - markdown: `# ¿Quién debe hacer la operación? - -Hay dos formas de descontar 25 pesos: - -\`\`\`csharp -// A: lo hace Program, por fuera -tarjeta.Saldo = tarjeta.Saldo - 25; - -// B: lo hace la tarjeta -tarjeta.Consumir(25); -\`\`\` - -Las dos dan 75 hoy. No son lo mismo mañana. - -Cuando la escuela decida que el saldo no puede quedar negativo, en **A** hay que buscar y corregir cada línea del programa que resta saldo. En **B** se corrige en un solo lugar: dentro de \`Consumir\`. - -A eso se le llama **responsabilidad**: la operación pertenece a la clase que es dueña del dato.`, - }, { type: "code_completion", prompt: "Ordena el cuerpo de un método que registra una entrada: primero cambia el estado, luego muestra el valor ya actualizado.", @@ -620,6 +745,180 @@ class Program }, ], }, + /** + * Etapa 5 (NUEVA): responsabilidad. Contrasta cambiar el campo desde + * fuera contra pedírselo al objeto. Puede usar `if` (conocimiento + * estructurado previo); todavía sin `private`. + */ + { + slug: "responsabilidad-del-objeto", + title: "La operación pertenece al objeto", + description: "Decide quién debe hacer el cambio: el objeto, no quien lo usa.", + estimatedMinutes: 9, + xpReward: 34, + steps: [ + { + type: "theory", + markdown: `# ¿Quién debe hacer la operación? + +Hay dos formas de descontar 25 pesos de una tarjeta: + +\`\`\`csharp +// A: lo hace Program, por fuera +tarjeta.Saldo = tarjeta.Saldo - 25; + +// B: lo hace la tarjeta +tarjeta.Consumir(25); +\`\`\` + +Las dos dan 75 hoy. No son lo mismo mañana. + +Cuando la escuela decida que el saldo no puede quedar negativo, en **A** hay que buscar y corregir cada línea del programa que resta saldo. En **B** se corrige en un solo lugar: dentro de \`Consumir\`. + +A eso se le llama **responsabilidad**: la operación pertenece a la clase que es dueña del dato. Todavía no vamos a *impedir* el acceso directo desde fuera — eso lo hace \`private\`, y lo verás en la unidad siguiente. Por ahora se trata de decidir bien dónde vive el código, aunque nada te obligue todavía.`, + }, + { + type: "quiz", + question: "El sistema del comedor va a agregar la regla “el saldo nunca puede quedar negativo”. ¿Cuál diseño hace ese cambio más fácil de aplicar de forma consistente?", + options: [ + "Que cada parte del programa que descuenta escriba tarjeta.Saldo = tarjeta.Saldo - monto directamente.", + "Que sólo TarjetaComedor.Consumir cambie Saldo, y todo el programa llame a Consumir.", + "Da igual: el resultado numérico es el mismo.", + "Convertir Saldo en una constante.", + ], + feedbackPerOption: [ + "Cada lugar que resta tendría que recordar agregar la misma regla por su cuenta.", + "", + "El resultado de hoy es igual, pero mantener la regla en un solo lugar es más fácil y más seguro.", + "Una constante no podría cambiar nunca, ni siquiera válidamente.", + ], + correctIndex: 1, + explanation: "Cuando la operación vive en un solo método, agregar o corregir una regla se hace en un solo lugar — y todo el que llama a Consumir queda protegido automáticamente.", + }, + { + type: "code_completion", + prompt: "Ordena el cuerpo de Agregar: primero decide con un if si hay espacio, y sólo entonces cambia el estado.", + lines: [ + "public void Agregar(int cantidad)", + "{", + " if (Ocupados + cantidad <= Capacidad)", + " {", + " Ocupados = Ocupados + cantidad;", + " }", + "}", + ], + explanation: "La decisión (if) va antes del cambio de estado: el objeto sólo se actualiza cuando la regla lo permite.", + }, + { + type: "code_challenge", + exercise: { + prompt: `## La alberca decide, no Main + +La alberca del CETI tiene un cupo máximo. Modela \`Alberca\` con: + +- campo público \`Personas\` (cuántas hay ahora) +- campo público \`Capacidad\` (el cupo) +- método \`Entrar()\` que aumente \`Personas\` en 1 **sólo si todavía hay lugar** (\`Personas < Capacidad\`) — la decisión la toma la alberca, no quien la usa. + +\`Main\` ya lee la capacidad, las personas iniciales y cuántas veces alguien intenta entrar; llama \`Entrar()\` esa cantidad de veces y al final imprime \`Personas\`. Todavía sin \`private\`: el campo sigue siendo público, pero nadie fuera de \`Alberca\` decide si se puede entrar.`, + starterCode: `using System; + +// Escribe aquí la clase Alberca + +class Program +{ + static void Main() + { + int capacidad = int.Parse(Console.ReadLine()); + int personas = int.Parse(Console.ReadLine()); + int intentos = int.Parse(Console.ReadLine()); + + Alberca alberca = new Alberca(); + alberca.Capacidad = capacidad; + alberca.Personas = personas; + + // Llama Entrar() "intentos" veces y muestra Personas al final. + } +}`, + solutionCode: `using System; + +class Alberca +{ + public int Personas; + public int Capacidad; + + public void Entrar() + { + if (Personas < Capacidad) + { + Personas = Personas + 1; + } + } +} + +class Program +{ + static void Main() + { + int capacidad = int.Parse(Console.ReadLine()); + int personas = int.Parse(Console.ReadLine()); + int intentos = int.Parse(Console.ReadLine()); + + Alberca alberca = new Alberca(); + alberca.Capacidad = capacidad; + alberca.Personas = personas; + + for (int i = 0; i < intentos; i++) + { + alberca.Entrar(); + } + + Console.WriteLine(alberca.Personas); + } +}`, + hints: [ + "Entrar no recibe parámetros: siempre intenta sumar uno.", + "El if va DENTRO de Entrar, no en Main.", + "Main sólo llama Entrar() en un ciclo; no revisa el cupo por su cuenta.", + ], + difficulty: "medium", + xpReward: 30, + structure: { + classes: [ + { + name: "Alberca", + fields: [ + { name: "Personas", visibility: "public", type: "int" }, + { name: "Capacidad", visibility: "public", type: "int" }, + ], + methods: [{ name: "Entrar", visibility: "public", paramCount: 0 }], + }, + ], + }, + testCases: [ + { + stdin: "5\n3\n4\n", + expectedStdout: "5\n", + visible: true, + description: "Se llena y bloquea los intentos de más", + }, + { + stdin: "10\n0\n3\n", + expectedStdout: "3\n", + visible: false, + description: "Con espacio de sobra", + }, + { + stdin: "2\n2\n1\n", + expectedStdout: "2\n", + visible: false, + description: "Ya está llena", + }, + ], + }, + }, + ], + }, /** * Etapa 6: abstracción con propósito. El reto evalúa SELECCIÓN de * miembros y responsabilidad; nada de formato nuevo ni de conversiones diff --git a/prisma/content/csharp/unidad-02-encapsular.ts b/prisma/content/csharp/unidad-02-encapsular.ts index e1ce64f..6ab26d9 100644 --- a/prisma/content/csharp/unidad-02-encapsular.ts +++ b/prisma/content/csharp/unidad-02-encapsular.ts @@ -27,13 +27,12 @@ export const unidad02: UnitDefinition = { type: "theory", markdown: `# Encapsular es controlar el acceso -- \`public\`: cualquier código con acceso al objeto puede usar el miembro. En UML: \`+\`. -- \`private\`: sólo la propia clase puede usarlo. En UML: \`-\`. -- \`protected\`: la clase y sus clases derivadas pueden usarlo. En UML: \`#\`. +- \`public\`: cualquier código con acceso al objeto puede usar el miembro. +- \`private\`: sólo la propia clase puede usarlo. El estado que sostiene una regla debe ser \`private\`. Si \`saldo\` es público, cualquier línea puede volverlo negativo sin pasar por \`Retirar\`. Haz públicas las operaciones que el resto del programa necesita, no todos los datos. -\`protected\` se reconoce ahora, pero se usará con sentido al estudiar herencia. No lo elijas como un \`private\` “menos estricto”.`, +Existe una tercera opción, \`protected\`, que sólo tiene sentido cuando hay herencia de por medio — la verás con propósito en U5. Por ahora, si dudas entre \`private\` y \`protected\`, usa \`private\`.`, }, { type: "code_example", diff --git a/prisma/content/csharp/unidad-03-uml.ts b/prisma/content/csharp/unidad-03-uml.ts index d0e5ed6..861e877 100644 --- a/prisma/content/csharp/unidad-03-uml.ts +++ b/prisma/content/csharp/unidad-03-uml.ts @@ -413,10 +413,39 @@ Puerta correctIndex: 2, explanation: "El diagrama de clase muestra estructura, no el algoritmo interno completo.", }, + { + // Antes este paso era "usa Semaforo desde Main": no evaluaba UML, + // sólo llamar métodos. La unidad promete C#→UML, así que el reto + // debe pedir el diagrama, no una llamada más. + type: "fill_blank", + prompt: "A partir de esta clase, completa su diagrama UML.", + template: `class Semaforo +{ + private string color; + public Semaforo(string inicial) { color = inicial; } + public void Cambiar(string nuevo) { color = nuevo; } + public string ColorActual() { return color; } +} + +// Diagrama: +// Semaforo +// ----------------- +// {{0}}: string +// ----------------- +// +Semaforo(inicial: string) +// +Cambiar(nuevo: string): {{1}} +// +ColorActual(): {{2}}`, + blanks: [ + { answer: "-color", hint: "El campo es privado: usa el signo `-` seguido del nombre exacto del campo." }, + { answer: "void", hint: "Cambiar no devuelve nada." }, + { answer: "string", hint: "ColorActual devuelve el color." }, + ], + explanation: "El diagrama registra visibilidad, atributos y firmas — nunca el cuerpo de los métodos. `Cambiar` no retorna nada (`void`); `ColorActual` retorna `string`.", + }, { type: "code_challenge", exercise: { - prompt: "El programa contiene una clase Semaforo. Completa sólo Main para demostrar su contrato: lee color inicial, crea el objeto, llama Cambiar una vez con el segundo color e imprime ColorActual(). No modifiques la clase.", + prompt: "Ahora al revés: usa el contrato de Semaforo (ya escrito, no lo modifiques) desde Main. Lee color inicial, crea el objeto, llama Cambiar una vez con el segundo color e imprime ColorActual().", starterCode: `using System; class Semaforo @@ -463,7 +492,7 @@ class Program "No agregues acceso directo al campo.", ], difficulty: "easy", - xpReward: 24, + xpReward: 20, structure: { classes: [ { diff --git a/prisma/content/csharp/unidad-04-relaciones.ts b/prisma/content/csharp/unidad-04-relaciones.ts index f29fa32..192788a 100644 --- a/prisma/content/csharp/unidad-04-relaciones.ts +++ b/prisma/content/csharp/unidad-04-relaciones.ts @@ -557,7 +557,7 @@ Las relaciones no son etiquetas decorativas. Deben coincidir con campos, paráme { type: "code_challenge", exercise: { - prompt: "Modela una OrdenServicio que se asocia con un Cliente existente y compone un Diagnostico creado internamente desde texto. Cliente tiene Nombre; Diagnostico tiene Detalle; OrdenServicio recibe folio, Cliente y detalle. Mostrar imprime Folio | Cliente | Detalle. Lee un cliente y dos órdenes; ambas comparten el mismo Cliente, cada orden crea su Diagnostico.", + prompt: "El taller registra órdenes de servicio. El Cliente existe por su cuenta y puede tener varias órdenes abiertas al mismo tiempo. Cada orden, en cambio, redacta y posee su propio diagnóstico: nadie más lo usa y no tiene sentido fuera de esa orden. Decide qué relación le corresponde a cada parte y modélala así: Cliente tiene Nombre; Diagnostico tiene Detalle; OrdenServicio recibe folio, el Cliente y el texto del diagnóstico. Mostrar imprime Folio | Cliente | Detalle. Lee un cliente y dos órdenes para ese mismo cliente.", starterCode: `using System; class Cliente { /* ... */ } diff --git a/prisma/content/csharp/unidad-05-herencia.ts b/prisma/content/csharp/unidad-05-herencia.ts index d79d16d..59e401b 100644 --- a/prisma/content/csharp/unidad-05-herencia.ts +++ b/prisma/content/csharp/unidad-05-herencia.ts @@ -32,32 +32,36 @@ export const unidad05: UnitDefinition = { Prueba de sustitución: si una operación espera \`Empleado\`, ¿aceptar un \`Becario\` conserva el sentido? Si sí, la generalización puede ser adecuada.`, }, { + // Constructores vacíos a propósito: la relación es-un y "qué se + // hereda" es lo que se evalúa aquí. Encadenar constructores con + // base(...) llega en la lección siguiente, por su cuenta. type: "code_example", code: `using System; class Empleado { - public string Nombre { get; private set; } - public Empleado(string nombre) { Nombre = nombre; } + public string Nombre; public void Identificarse() { Console.WriteLine("Empleado: " + Nombre); } } class Becario : Empleado { - public string Escuela { get; private set; } - public Becario(string nombre, string escuela) : base(nombre) { Escuela = escuela; } + public string Escuela; } class Program { static void Main() { - Becario b = new Becario("Ana", "CETI"); + Becario b = new Becario(); + b.Nombre = "Ana"; + b.Escuela = "CETI"; + b.Identificarse(); Console.WriteLine(b.Escuela); } }`, - explanation: "Becario hereda Identificarse y satisface el contrato de Empleado; Escuela es su especialización.", + explanation: "Becario no declara Nombre ni Identificarse: los hereda de Empleado. Escuela es su especialización, lo que lo hace un Becario y no cualquier Empleado.", runnable: true, expectedOutput: `Empleado: Ana CETI`, @@ -74,7 +78,7 @@ CETI`, { type: "code_challenge", exercise: { - prompt: "Crea Persona con Nombre y método Presentar(). Crea Alumno : Persona con Registro. Lee nombre y registro; imprime \"Persona: N\" y \"Registro: R\".", + prompt: "Crea Persona con campo público Nombre y método Presentar() que imprima \"Persona: N\". Crea Alumno : Persona con campo público Registro (sin constructores todavía). En Main lee nombre y registro, crea UN Alumno, asígnale ambos campos, llama Presentar() (heredado) e imprime \"Registro: R\".", starterCode: `using System; class Persona { /* completa */ } class Alumno : Persona { /* completa */ } @@ -82,28 +86,29 @@ class Program { static void Main() { /* lee, crea y muestra */ } }`, solutionCode: `using System; class Persona { - public string Nombre { get; private set; } - public Persona(string nombre) { Nombre = nombre; } + public string Nombre; public void Presentar() { Console.WriteLine("Persona: " + Nombre); } } class Alumno : Persona { - public string Registro { get; private set; } - public Alumno(string nombre, string registro) : base(nombre) { Registro = registro; } + public string Registro; } class Program { static void Main() { - Alumno a = new Alumno(Console.ReadLine(), Console.ReadLine()); + Alumno a = new Alumno(); + a.Nombre = Console.ReadLine(); + a.Registro = Console.ReadLine(); + a.Presentar(); Console.WriteLine("Registro: " + a.Registro); } }`, hints: [ "Usa : Persona.", - "Invoca base(nombre) en el constructor.", - "El método heredado se llama sobre Alumno.", + "Alumno no declara Nombre ni Presentar: los hereda.", + "Todavía sin constructores: asigna los campos después de new.", ], difficulty: "medium", xpReward: 28, @@ -111,15 +116,13 @@ class Program classes: [ { name: "Persona", - properties: [{ name: "Nombre", visibility: "public", type: "string" }], - constructors: [{ paramCount: 1 }], + fields: [{ name: "Nombre", visibility: "public", type: "string" }], methods: [{ name: "Presentar", visibility: "public" }], }, { name: "Alumno", extends: "Persona", - properties: [{ name: "Registro", visibility: "public", type: "string" }], - constructors: [{ paramCount: 2, callsBase: true }], + fields: [{ name: "Registro", visibility: "public", type: "string" }], }, ], }, @@ -341,9 +344,54 @@ class Program steps: [ { type: "theory", - markdown: `# Un mensaje, varias respuestas + markdown: `# Un mensaje, ¿varias respuestas? + +Piensa en este caso antes de ver ninguna palabra nueva: -La base declara un punto de extensión \`virtual\`; la subclase lo redefine con \`override\`. Una variable de tipo base puede referirse a cualquier subtipo y C# elige la implementación según el objeto real. En POO I se practica con un arreglo de tamaño fijo; las colecciones genéricas corresponden a POO II.`, +\`\`\`csharp +class Empleado +{ + public decimal Pago() { return 1000m; } +} +class Vendedor : Empleado +{ + public decimal Pago() { return 1300m; } +} +... +Empleado e = new Vendedor(); +Console.WriteLine(e.Pago()); +\`\`\` + +\`e\` está **declarada** como \`Empleado\`, pero el objeto que guarda es, en realidad, un \`Vendedor\`. ¿Qué \`Pago()\` corre: el de \`Empleado\` o el de \`Vendedor\`?`, + }, + { + type: "quiz", + question: "Con el código de arriba (sin ninguna palabra clave nueva todavía), ¿qué imprime `e.Pago()`?", + options: [ + "1300.00, porque el objeto real es un Vendedor", + "1000.00, porque C# usa el tipo con el que se declaró e, no el tipo del objeto", + "Un error de compilación", + "1000.00 y 1300.00, una por línea", + ], + feedbackPerOption: [ + "Es la respuesta intuitiva, pero no es lo que hace C# sin más: sigue leyendo.", + "", + "El código compila: Vendedor sí es un Empleado.", + "Sólo hay una llamada a Pago(); sólo puede imprimir un valor.", + ], + correctIndex: 1, + explanation: "Sin marcar nada especial, C# resuelve Pago() por el TIPO DECLARADO de la variable (Empleado), no por el objeto real. Para que corra la versión de Vendedor hace falta decírselo explícitamente: eso es lo que sigue.", + }, + { + type: "theory", + markdown: `# virtual + override: el contrato explícito + +Para que \`e.Pago()\` corra la versión de \`Vendedor\` —lo que probablemente esperabas— hay que decirle a C# dos cosas: + +- en la base, que el método **puede redefinirse**: \`public virtual decimal Pago()\` +- en la derivada, que **lo redefine**: \`public override decimal Pago()\` + +Con las dos palabras presentes, C# elige la implementación según el **objeto real**, sin importar el tipo con el que declaraste la variable. Eso es polimorfismo.`, }, { type: "code_example", @@ -360,17 +408,13 @@ class Program { static void Main() { - Empleado[] equipo = new Empleado[2]; - equipo[0] = new Empleado(); - equipo[1] = new Vendedor(); - for (int i = 0; i < equipo.Length; i++) - Console.WriteLine(equipo[i].Pago().ToString("0.00")); + Empleado e = new Vendedor(); + Console.WriteLine(e.Pago().ToString("0.00")); } }`, - explanation: "La segunda referencia tiene tipo declarado Empleado, pero el objeto Vendedor decide la respuesta.", + explanation: "Misma variable declarada Empleado, mismo objeto Vendedor que antes — pero ahora Pago es virtual/override y sí corre la versión redefinida: 1300.00.", runnable: true, - expectedOutput: `1000.00 -1300.00`, + expectedOutput: "1300.00", }, { type: "quiz", @@ -378,12 +422,26 @@ class Program options: [ "Que Pago sea static.", "La combinación virtual en la base y override en la derivada.", - "Que el arreglo tenga dos posiciones.", + "Que la variable se llame e.", "El método Main.", ], correctIndex: 1, explanation: "virtual/override forman el contrato de redefinición.", }, + { + // Segunda dimensión, por separado: ahora un ARREGLO de la base + // con varios objetos reales. Todavía un solo par virtual/override. + type: "code_completion", + prompt: "Ordena el ciclo que recorre un arreglo de Empleado (con un Empleado y un Vendedor adentro) e imprime el Pago de cada uno con el método ya definido arriba.", + lines: [ + "Empleado[] equipo = new Empleado[2];", + "equipo[0] = new Empleado();", + "equipo[1] = new Vendedor();", + "for (int i = 0; i < equipo.Length; i++)", + " Console.WriteLine(equipo[i].Pago().ToString(\"0.00\"));", + ], + explanation: "El arreglo se declara del tipo base; cada posición puede guardar cualquier subtipo, y el ciclo no necesita saber cuál es cuál: cada Pago() se resuelve solo.", + }, { type: "code_challenge", exercise: { diff --git a/prisma/content/csharp/unidad-06-diseno-robusto.ts b/prisma/content/csharp/unidad-06-diseno-robusto.ts index 5d824a1..3f9525c 100644 --- a/prisma/content/csharp/unidad-06-diseno-robusto.ts +++ b/prisma/content/csharp/unidad-06-diseno-robusto.ts @@ -251,7 +251,7 @@ class Program { slug: "miniproyecto-dominio", title: "Miniproyecto: reservas de laboratorio", - description: "Coordina entidades, composición, validación y presentación sin mezclar responsabilidades.", + description: "Coordina entidades, asociación, validación y presentación sin mezclar responsabilidades.", estimatedMinutes: 18, xpReward: 65, steps: [ @@ -259,7 +259,9 @@ class Program type: "theory", markdown: `# Del requisito a las responsabilidades -Requisito: “Un alumno reserva un laboratorio por cierto número de horas; el costo debe ser positivo”. \`Alumno\` conserva identidad, \`Laboratorio\` tarifa y \`Reserva\` coordina fecha lógica/costo. \`Program\` sólo recibe y muestra. Antes de codificar, dibuja las tres clases y marca asociaciones.`, +Requisito: “Un alumno reserva un laboratorio por cierto número de horas; el costo debe ser positivo”. \`Alumno\` conserva identidad, \`Laboratorio\` tarifa y \`Reserva\` coordina fecha lógica/costo. \`Program\` sólo recibe y muestra. Antes de codificar, dibuja las tres clases y marca asociaciones. + +Fíjate en cómo llega cada colaborador a \`Reserva\`: el \`Alumno\` y el \`Laboratorio\` ya existen antes de la reserva y se le pasan hechos. Eso es **asociación** (como en U4), no composición — \`Reserva\` no los crea ni los posee, sólo los referencia mientras dura.`, }, { type: "code_example", diff --git a/prisma/content/csharp/unidad-08-integrador.ts b/prisma/content/csharp/unidad-08-integrador.ts index 575a8dc..21a3640 100644 --- a/prisma/content/csharp/unidad-08-integrador.ts +++ b/prisma/content/csharp/unidad-08-integrador.ts @@ -31,19 +31,19 @@ export const unidad08: UnitDefinition = { type: "theory", markdown: `# Caso guía: cotizador de papelería -El negocio necesita capturar cliente, producto, precio y cantidad; calcular subtotal y descuento; rechazar importes o cantidades no positivas; y mostrar un resumen. Alcance POO I: una sesión en memoria, arreglo fijo si hace falta, sin base de datos, red, XML, concurrencia ni colecciones genéricas. +El negocio necesita capturar producto, precio, cantidad y porcentaje de descuento; calcular el total; rechazar precios, cantidades o descuentos fuera de rango; y mostrar un resumen. Alcance POO I: una sesión en memoria, sin base de datos, red, XML, concurrencia ni colecciones genéricas. -Clases candidatas: \`Cliente\`, \`Producto\`, \`Cotizacion\` y \`CalculadorDescuento\`. El formulario es frontera, no entidad. Cada criterio debe describir una entrada y un resultado observable.`, +Clases del modelo — y sólo ésas se implementan en toda la unidad, sin agregar ni quitar ninguna después: \`Producto\` (nombre, precio; valida precio positivo) y \`Cotizacion\` (referencia a un Producto existente, cantidad, descuento; valida cantidad y descuento, y calcula el total). El formulario es frontera, no entidad. Cada criterio debe describir una entrada y un resultado observable.`, }, { type: "matching", pairs: [ { left: "“Cantidad mayor que cero”", right: "Invariante" }, - { left: "Cotizacion contiene sus renglones del alcance", right: "Composición" }, + { left: "Cotizacion referencia un Producto que ya existía antes de cotizar", right: "Asociación" }, { left: "Formulario", right: "Frontera de interfaz" }, { left: "Precio 100, cantidad 2, descuento 10% → 180", right: "Criterio de aceptación" }, ], - explanation: "El diagrama y los casos deben contar la misma historia.", + explanation: "El diagrama y los casos deben contar la misma historia. Producto no nace ni muere con la Cotizacion que lo usa — el mismo producto podría cotizarse varias veces — así que la relación es asociación, no composición.", }, { type: "quiz", @@ -148,6 +148,31 @@ class Program ], difficulty: "hard", xpReward: 48, + // Reto final de dominio de la unidad integradora: no puede + // aprobarse reproduciendo la salida esperada desde un Main + // sin clases. El contrato exige la forma real del modelo + // acordado en requisitos-uml-aceptacion. + structure: { + classes: [ + { + name: "Producto", + properties: [ + { name: "Nombre", visibility: "public", type: "string" }, + { name: "Precio", visibility: "public", type: "decimal" }, + ], + constructors: [{ paramCount: 2 }], + }, + { + name: "Cotizacion", + constructors: [{ paramCount: 3 }], + methods: [ + { name: "Total", visibility: "public", returnType: "decimal" }, + { name: "Resumen", visibility: "public", returnType: "string" }, + ], + stores: [{ type: "Producto" }], + }, + ], + }, testCases: [ { stdin: "Papel\n100\n2\n10\n", diff --git a/prisma/content/exercises/csharp/u02-encapsular.ts b/prisma/content/exercises/csharp/u02-encapsular.ts index 6f2dd3a..f0095e6 100644 --- a/prisma/content/exercises/csharp/u02-encapsular.ts +++ b/prisma/content/exercises/csharp/u02-encapsular.ts @@ -250,14 +250,19 @@ class Program ], }, { + // Antes esto era `Conversor`: una utilidad sin estado, instanciada + // arbitrariamente sólo para tener un objeto sobre el cual llamar la + // sobrecarga. Ahora la sobrecarga sirve a un objeto con estado real: + // el cronómetro ACUMULA lo que se le registra, con dos formas de + // decírselo. slug: "csharp-poo-sobrecarga-conversion", - title: "Sobrecarga de conversiones", - description: "Usa una intención común con entradas distintas.", - prompt: "Conversor tiene Convertir(int minutos) que devuelve segundos y Convertir(int horas,int minutos) que devuelve minutos totales. Lee m, h, m2 e imprime ambos resultados.", + title: "Sobrecarga con intención", + description: "Dos formas de registrar tiempo en el mismo cronómetro.", + prompt: "Cronometro guarda AcumuladoMinutos (privado). Registrar(int minutos) le suma minutos directos. Registrar(int horas, int minutos) primero convierte a minutos y también los suma. Lee minutos sueltos, luego horas y minutos, registra ambos en el mismo cronómetro e imprime el acumulado final.", starterCode: `using System; -class Conversor +class Cronometro { - /* sobrecargas */ + /* estado privado + dos Registrar */ } class Program @@ -268,15 +273,23 @@ class Program } }`, solutionCode: `using System; -class Conversor +class Cronometro { - public int Convertir(int minutos) + private int acumuladoMinutos; + + public void Registrar(int minutos) + { + acumuladoMinutos += minutos; + } + + public void Registrar(int horas, int minutos) { - return minutos*60; + acumuladoMinutos += horas * 60 + minutos; } - public int Convertir(int horas, int minutos) + + public int AcumuladoMinutos() { - return horas*60+minutos; + return acumuladoMinutos; } } @@ -284,26 +297,32 @@ class Program { static void Main() { - int m=int.Parse(Console.ReadLine()), h=int.Parse(Console.ReadLine()), m2=int.Parse(Console.ReadLine()); - Conversor c=new Conversor(); - Console.WriteLine(c.Convertir(m)); - Console.WriteLine(c.Convertir(h, m2)); + int m = int.Parse(Console.ReadLine()); + int h = int.Parse(Console.ReadLine()); + int m2 = int.Parse(Console.ReadLine()); + + Cronometro c = new Cronometro(); + c.Registrar(m); + c.Registrar(h, m2); + Console.WriteLine(c.AcumuladoMinutos()); } }`, hints: [ - "Mismo nombre, distinta lista de parámetros.", - "La primera conversión produce segundos.", - "La segunda produce minutos.", + "Mismo nombre `Registrar`, distinta lista de parámetros: eso es sobrecarga.", + "Las dos versiones sólo difieren en cómo llegan los minutos; las dos suman al mismo campo.", + "El acumulado es estado del cronómetro: no lo calcules en Main.", ], difficulty: "hard", xpReward: 36, structure: { classes: [ { - name: "Conversor", + name: "Cronometro", + fields: [{ name: "acumuladoMinutos", visibility: "private", type: "int" }], methods: [ - { name: "Convertir", visibility: "public", paramCount: 1, returnType: "int" }, - { name: "Convertir", visibility: "public", paramCount: 2, returnType: "int" }, + { name: "Registrar", visibility: "public", paramCount: 1 }, + { name: "Registrar", visibility: "public", paramCount: 2 }, + { name: "AcumuladoMinutos", visibility: "public", returnType: "int" }, ], }, ], @@ -311,12 +330,12 @@ class Program testCases: [ { stdin: "3\n2\n15\n", - expectedStdout: "180\n135\n", + expectedStdout: "138\n", visible: true, }, { stdin: "0\n10\n0\n", - expectedStdout: "0\n600\n", + expectedStdout: "600\n", visible: false, }, ], diff --git a/prisma/content/exercises/csharp/u03-uml.ts b/prisma/content/exercises/csharp/u03-uml.ts index dcdb5ed..05616f5 100644 --- a/prisma/content/exercises/csharp/u03-uml.ts +++ b/prisma/content/exercises/csharp/u03-uml.ts @@ -18,7 +18,7 @@ export const u03UmlExercises: PracticeUnitSetDefinition = { slug: "csharp-poo-uml-estudiante", title: "UML a código: Estudiante", description: "Traduce visibilidad, atributo y operación.", - prompt: "Del UML Estudiante(-registro:string, +Nombre:string, +Presentar():string), implementa constructor y Presentar que devuelva \"registro - Nombre\". Lee ambos e imprime.", + prompt: "Del UML Estudiante(-registro:string, +Nombre:string, +Estudiante(registro:string, nombre:string), +Presentar():string), implementa el constructor y Presentar que devuelva \"registro - Nombre\". Lee ambos e imprime.", starterCode: `using System; class Estudiante { @@ -170,40 +170,52 @@ class Program ], }, { + // Antes pedía implementar Pelicula y nunca tocaba UML: sólo evaluaba + // C#. Ahora la clase ya está completa y lo que se evalúa es la + // extracción — el alumno produce el diagrama de ESA clase como texto, + // con el mismo formato de compartimentos usado en la unidad. slug: "csharp-poo-codigo-a-uml-pelicula", - title: "Código coherente con diagrama", - description: "Completa código a partir de una lectura UML inversa.", - prompt: "Implementa Pelicula con Titulo público de lectura, duración privada, constructor y EsLarga():bool (más de 120). Lee datos e imprime \"Titulo | larga/corta\".", + title: "De la clase al diagrama", + description: "Extrae el diagrama UML de una clase ya escrita.", + prompt: "La clase Pelicula ya está completa; no la modifiques. Completa Main para que imprima su diagrama UML con el mismo formato de compartimentos visto en la unidad: nombre, atributos con visibilidad, separador, y operaciones con parámetros y retorno.", starterCode: `using System; class Pelicula { - /* luego dibuja su UML */ + private int duracion; + public string Titulo { get; private set; } + public Pelicula(string titulo, int duracion) + { + Titulo = titulo; + this.duracion = duracion; + } + public bool EsLarga() + { + return duracion > 120; + } } class Program { static void Main() { - /* completa */ + // Imprime el diagrama de Pelicula: nombre, línea de atributos + // ("-duracion: int" y "+Titulo: string"), separador, y las dos + // operaciones públicas con su firma y retorno. } }`, solutionCode: `using System; class Pelicula { private int duracion; - public string Titulo - { - get; - private set; - } - public Pelicula(string t, int d) + public string Titulo { get; private set; } + public Pelicula(string titulo, int duracion) { - Titulo=t; - duracion=d; + Titulo = titulo; + this.duracion = duracion; } public bool EsLarga() { - return duracion>120; + return duracion > 120; } } @@ -211,39 +223,27 @@ class Program { static void Main() { - Pelicula p=new Pelicula(Console.ReadLine(), int.Parse(Console.ReadLine())); - Console.WriteLine(p.Titulo+" | "+(p.EsLarga()?"larga":"corta")); + Console.WriteLine("Pelicula"); + Console.WriteLine("-duracion: int"); + Console.WriteLine("+Titulo: string"); + Console.WriteLine("-----------------"); + Console.WriteLine("+Pelicula(titulo: string, duracion: int)"); + Console.WriteLine("+EsLarga(): bool"); } }`, hints: [ - "120 exactos no es larga.", - "duracion permanece private.", - "Dibuja después la firma de EsLarga.", + "No copies el cuerpo de EsLarga; el diagrama registra la firma, no el algoritmo.", + "`Titulo` es una propiedad pública de lectura: se anota igual que un atributo público.", + "El constructor también es una operación pública: aparece con sus dos parámetros.", ], difficulty: "medium", xpReward: 28, - structure: { - classes: [ - { - name: "Pelicula", - fields: [{ name: "duracion", visibility: "private", type: "int" }], - properties: [{ name: "Titulo", visibility: "public", type: "string" }], - constructors: [{ paramCount: 2 }], - methods: [{ name: "EsLarga", visibility: "public", returnType: "bool" }], - }, - ], - }, - testCases: [ + testCases: [ { - stdin: "Norte\n121\n", - expectedStdout: "Norte | larga\n", + stdin: "", + expectedStdout: "Pelicula\n-duracion: int\n+Titulo: string\n-----------------\n+Pelicula(titulo: string, duracion: int)\n+EsLarga(): bool\n", visible: true, }, - { - stdin: "Límite\n120\n", - expectedStdout: "Límite | corta\n", - visible: false, - }, ], }, ], diff --git a/prisma/content/exercises/csharp/u04-relaciones.ts b/prisma/content/exercises/csharp/u04-relaciones.ts index e7bb54d..fd4aa09 100644 --- a/prisma/content/exercises/csharp/u04-relaciones.ts +++ b/prisma/content/exercises/csharp/u04-relaciones.ts @@ -148,13 +148,17 @@ class Program { static void Main() { - string e=Console.ReadLine(), n=Console.ReadLine(); - new Equipo(e, new Entrenador(n)).Mostrar(); + string nombreEquipo = Console.ReadLine(); + string nombreEntrenador = Console.ReadLine(); + + Entrenador entrenador = new Entrenador(nombreEntrenador); + Equipo equipo = new Equipo(nombreEquipo, entrenador); + equipo.Mostrar(); } }`, hints: [ - "Entrenador existe antes de Equipo.", - "Equipo guarda la referencia.", + "Entrenador existe antes de Equipo: créalo primero, en su propia variable.", + "Equipo sólo guarda la referencia que ya recibe, no crea su propio entrenador.", "No heredes.", ], difficulty: "easy", diff --git a/prisma/content/exercises/csharp/u05-herencia.ts b/prisma/content/exercises/csharp/u05-herencia.ts index 6dca8be..29a20dd 100644 --- a/prisma/content/exercises/csharp/u05-herencia.ts +++ b/prisma/content/exercises/csharp/u05-herencia.ts @@ -295,20 +295,25 @@ class Program ], }, { + // Antes esto era `Medicion` con Temperatura/Distancia: una + // abstracción sin significado compartido real (convertir grados no es + // la misma operación que convertir kilómetros; sólo compartían la + // forma de la firma). Area() sí es una operación con el mismo + // significado en cualquier figura. slug: "csharp-poo-abstract-medicion", - title: "Mediciones abstractas", - description: "Implementa dos fórmulas detrás de un contrato.", - prompt: "Medicion abstracta declara Valor(). Temperatura recibe Celsius y devuelve C*1.8+32; Distancia recibe km y devuelve km*1000. Lee C y km, usa Medicion[2] e imprime dos valores con dos decimales.", + title: "Figuras abstractas", + description: "Implementa dos fórmulas de área detrás de un contrato.", + prompt: "Figura abstracta declara Area(). Rectangulo recibe base y altura y devuelve base*altura; Circulo recibe radio y devuelve Math.PI*radio*radio. Lee base, altura y radio; usa Figura[2] e imprime las dos áreas con dos decimales.", starterCode: `using System; -abstract class Medicion +abstract class Figura { } -class Temperatura:Medicion +class Rectangulo:Figura { } -class Distancia:Medicion +class Circulo:Figura { } @@ -319,34 +324,36 @@ class Program } }`, solutionCode: `using System; -abstract class Medicion +abstract class Figura { - public abstract double Valor(); + public abstract double Area(); } -class Temperatura:Medicion +class Rectangulo:Figura { - private double c; - public Temperatura(double c) + private double baseR; + private double altura; + public Rectangulo(double baseR, double altura) { - this.c=c; + this.baseR=baseR; + this.altura=altura; } - public override double Valor() + public override double Area() { - return c*1.8+32; + return baseR*altura; } } -class Distancia:Medicion +class Circulo:Figura { - private double km; - public Distancia(double k) + private double radio; + public Circulo(double radio) { - km=k; + this.radio=radio; } - public override double Valor() + public override double Area() { - return km*1000; + return Math.PI*radio*radio; } } @@ -354,49 +361,50 @@ class Program { static void Main() { - Medicion[] m=new Medicion[] + Figura[] f=new Figura[] { - new Temperatura(double.Parse(Console.ReadLine())), new Distancia(double.Parse(Console.ReadLine())) + new Rectangulo(double.Parse(Console.ReadLine()), double.Parse(Console.ReadLine())), + new Circulo(double.Parse(Console.ReadLine())) } ; - for(int i=0;i { it("todo reto de lección de C# publicado declara su objetivo", () => { // "Declara su objetivo" = o tiene contrato (comportamiento + estructura) // o está en la lista de los que sólo evalúan comportamiento. - const soloComportamiento = new Set([]); + const soloComportamiento = new Set([ + // La clase ya viene completa y sin cambios: el reto evalúa que el + // alumno EXTRAIGA el diagrama UML correcto como texto, no la + // estructura de clases (que no escribe). + "csharp-poo-1/csharp-poo-03-uml/csharp-poo-codigo-a-uml-pelicula", + ]); const sinContrato = challenges .filter((c) => c.language === "csharp" && c.structure === undefined) .map((c) => c.id) From 7db0485682972251c9a934dd1976a897b808d355 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 02:08:51 +0000 Subject: [PATCH 2/3] =?UTF-8?q?Cierra=207=20omisiones=20del=20sprint=20C#?= =?UTF-8?q?=20POO=20I=20se=C3=B1aladas=20en=20revisi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. U1 prácticas (u01-modelar.ts, no tocado antes): Mascota ya no exige clase+método+objeto+I/O a la vez — la clase y Describir() ya vienen escritos, sólo se evalúa crear+usar el objeto. Lampara deja de usar Encender() para demostrar independencia: ahora reasigna un campo y compara con Console.WriteLine directo, como la lección. Bateria conserva Main/parsing ya resueltos (el objetivo es dónde vive la regla, no leer datos). Casillero plantea una decisión real (5 candidatos, sólo 3 pertenecen al modelo) antes de dar la forma exacta que se califica. 2. U1 estado-y-comportamiento: progresión mínima añadida entre "ordenar un método" y "escribir Marcador con dos métodos": un fill_blank que separa completar CUERPO (firma dada) de completar FIRMA (cuerpo dado), y un code_challenge autónomo de un solo método (Reiniciar()) antes de integrar Sumar+Restar. 3. U3 modelar-requerimientos: la quiz genérica ("qué demuestra mejor abstracción") se reemplazó por una decisión concreta sobre ESTE requerimiento — elegir el conjunto correcto de miembros de Articulo entre cuatro opciones (correcta, con dato inventado, con dato faltante que la regla necesita, con responsabilidad de GUI) — antes de implementar exactamente ese modelo. 4. U3/U8 multiplicidades: nunca se enseñaron en U3/U4, así que se quitaron como requisito del UML final de U8 en vez de enseñarlas tarde; se documenta explícitamente por qué no se piden. 5. U6 instancia-y-static: secuencia reordenada a instancia-vs-clase → caso static SEGURO (constante Pi + función pura Area) → caso static MUTABLE (contador Creados) → costo de usar static como almacén global accidental. El reto del contador (Entrada/Total) se mantiene al final, sin cambios. 6. U7 (unidad-07-gui.ts, no tocada antes): la primera lección ahora explica, en términos concretos para quien abre WinForms por primera vez: qué es Form, qué es el Toolbox y cómo arrastrar controles, Name vs Text, cómo conectar un Click, y Form1.cs vs el código del Designer — sin volverse tutorial gigante. La última lección separa cognitivamente Parte 1 (estado/referencia que sobrevive entre eventos) de Parte 2 (organización visual y publicación), cada una con su propia teoría y evaluación, dentro del mismo slug. Sigue published: false. No se reabrió U2/U4/U5 ni el resto de U6/U8 fuera de lo señalado. Verificación: 81/81 piezas de C# compilan y producen la salida esperada (mcs/mono), 97/97 tests de contenido/estructura, 441/441 suite completa, typecheck y lint limpios. No se tocó infraestructura: no se repitió build. --- prisma/content/csharp/unidad-01-modelar.ts | 127 ++++++++++++++- prisma/content/csharp/unidad-03-uml.ts | 27 ++- .../csharp/unidad-06-diseno-robusto.ts | 55 ++++++- prisma/content/csharp/unidad-07-gui.ts | 95 ++++++++--- prisma/content/csharp/unidad-08-integrador.ts | 4 +- .../content/exercises/csharp/u01-modelar.ts | 154 ++++++++++++------ 6 files changed, 367 insertions(+), 95 deletions(-) diff --git a/prisma/content/csharp/unidad-01-modelar.ts b/prisma/content/csharp/unidad-01-modelar.ts index 2a213e4..518faf6 100644 --- a/prisma/content/csharp/unidad-01-modelar.ts +++ b/prisma/content/csharp/unidad-01-modelar.ts @@ -560,8 +560,8 @@ class Program slug: "estado-y-comportamiento", title: "Los métodos cambian el estado", description: "Predice el valor antes y después de la llamada a un método.", - estimatedMinutes: 9, - xpReward: 32, + estimatedMinutes: 12, + xpReward: 36, steps: [ { type: "theory", @@ -635,10 +635,129 @@ class Program ], explanation: "Primero cambia el estado; después muestra el estado ya actualizado.", }, + { + // Dos huecos, dos habilidades distintas y pequeñas antes de pedir + // escribir un método completo: primero sólo el CUERPO (la firma + // ya está), después sólo la FIRMA (el cuerpo ya está). + type: "fill_blank", + prompt: "Marcador va a tener dos métodos. Primero completa sólo el CUERPO de Sumar — la firma ya está escrita. Luego completa sólo la FIRMA de Restar — el cuerpo ya está escrito.", + template: `class Marcador +{ + public int Puntos; + + public void Sumar(int cantidad) + { + {{0}} = {{1}} + cantidad; + } + + public void {{2}}({{3}} cantidad) + { + Puntos = Puntos - cantidad; + } +}`, + blanks: [ + { answer: "Puntos", hint: "El campo que Sumar debe cambiar." }, + { answer: "Puntos", hint: "El cuerpo lee el valor actual antes de sumarle." }, + { answer: "Restar", hint: "El nombre del segundo método." }, + { answer: "int", hint: "El tipo del parámetro `cantidad`." }, + ], + explanation: "Sumar y Restar tienen la misma forma: reciben una cantidad y cambian Puntos. Completar el cuerpo o la firma por separado te obliga a leer ambas partes con cuidado.", + }, + { + type: "code_challenge", + exercise: { + prompt: `## Un solo método, desde cero + +Antes de juntar dos métodos en una clase, escribe uno solo y compruébalo. + +\`Marcador\` ya tiene el campo \`Puntos\` y \`Main\` ya está completo. Falta un único método: \`Reiniciar()\`, sin parámetros, que ponga \`Puntos\` en \`0\`.`, + starterCode: `using System; + +class Marcador +{ + public int Puntos; + + // Escribe aquí el método Reiniciar() +} + +class Program +{ + static void Main() + { + int inicial = int.Parse(Console.ReadLine()); + + Marcador marcador = new Marcador(); + marcador.Puntos = inicial; + + Console.WriteLine(marcador.Puntos); + marcador.Reiniciar(); + Console.WriteLine(marcador.Puntos); + } +}`, + solutionCode: `using System; + +class Marcador +{ + public int Puntos; + + public void Reiniciar() + { + Puntos = 0; + } +} + +class Program +{ + static void Main() + { + int inicial = int.Parse(Console.ReadLine()); + + Marcador marcador = new Marcador(); + marcador.Puntos = inicial; + + Console.WriteLine(marcador.Puntos); + marcador.Reiniciar(); + Console.WriteLine(marcador.Puntos); + } +}`, + hints: [ + "Reiniciar no recibe ningún parámetro.", + "El cuerpo tiene una sola línea: Puntos = 0;", + "No cambies nada en Main: ya está listo.", + ], + difficulty: "easy", + xpReward: 16, + structure: { + classes: [ + { + name: "Marcador", + fields: [{ name: "Puntos", visibility: "public", type: "int" }], + methods: [{ name: "Reiniciar", visibility: "public", paramCount: 0 }], + }, + ], + }, + testCases: [ + { + stdin: "12\n", + expectedStdout: "12\n0\n", + visible: true, + description: "Reinicia a cero", + }, + { + stdin: "0\n", + expectedStdout: "0\n0\n", + visible: false, + description: "Ya estaba en cero", + }, + ], + }, + }, { type: "code_challenge", exercise: { - prompt: `## Un marcador que se mueve solo + prompt: `## Ahora dos métodos juntos + +Ya escribiste un método solo (\`Reiniciar\`). Ahora integra dos que sí necesitan trabajar juntos: uno que suma y otro que resta, cada uno con su propio parámetro. \`Main\` ya lee los tres números y ya crea el marcador con su valor inicial. Te toca la clase y las dos llamadas. @@ -648,7 +767,7 @@ Escribe \`Marcador\` con: - un método \`Sumar(int cantidad)\` que le sume al estado - un método \`Restar(int cantidad)\` que le reste al estado -Y en \`Main\`, pídele al objeto que sume y que reste — **no cambies \`Puntos\` desde fuera**. La dimensión nueva es una: métodos que reciben un dato y modifican el estado.`, +Y en \`Main\`, pídele al objeto que sume y que reste — **no cambies \`Puntos\` desde fuera**. La dimensión nueva es una: dos métodos que reciben un dato cada uno y modifican el mismo estado.`, starterCode: `using System; // Escribe aquí la clase Marcador diff --git a/prisma/content/csharp/unidad-03-uml.ts b/prisma/content/csharp/unidad-03-uml.ts index 861e877..51b7938 100644 --- a/prisma/content/csharp/unidad-03-uml.ts +++ b/prisma/content/csharp/unidad-03-uml.ts @@ -559,21 +559,32 @@ No crees \`Papeleria\`, \`Sistema\`, \`Usuario\`, \`Pantalla\` y \`BaseDeDatos\` explanation: "Separar dominio de interfaz desde el modelo evita una clase que haga todo.", }, { + // Antes de programar, decisión explícita sobre ESTE requerimiento: + // el conjunto exacto de miembros que Articulo va a tener, contra + // tres alternativas plausibles pero equivocadas (dato inventado, + // dato faltante que la regla necesita, responsabilidad de GUI). + // El challenge que sigue implementa justo lo que aquí se decide. type: "quiz", - question: "¿Qué decisión demuestra mejor abstracción para este requerimiento?", + question: "Según el requerimiento de la papelería, ¿qué conjunto de miembros debe tener Articulo?", options: [ - "Agregar 20 datos “por si acaso”", - "Modelar sólo datos y operaciones usados por venta", - "Poner todo en Main", - "Hacer públicos todos los campos", + "Codigo, Precio, Existencias, Vender(cantidad), ConsultarExistencias()", + "Codigo, Precio, Existencias, NombreProveedor, Vender(cantidad)", + "Codigo, Precio, Vender(cantidad), ConsultarExistencias() (sin Existencias)", + "Codigo, Precio, Existencias, Vender(cantidad), MostrarBotonVender()", ], - correctIndex: 1, - explanation: "El modelo mínimo cubre las reglas observables sin inventar alcance.", + feedbackPerOption: [ + "", + "NombreProveedor no aparece en el requerimiento ni lo necesita ninguna regla: es un dato inventado.", + "Sin Existencias no hay con qué comparar la cantidad ni qué reducir: la regla central queda sin dónde vivir.", + "Mostrar un botón es responsabilidad de la interfaz, no del artículo del dominio.", + ], + correctIndex: 0, + explanation: "Cada miembro de este conjunto responde a algo que el requerimiento pide o que la regla de venta necesita — ni un dato de más, ni uno de menos.", }, { type: "code_challenge", exercise: { - prompt: "Implementa Articulo con Codigo de sólo lectura externa, Precio y Existencias privados, constructor, Vender(int):bool y ConsultarExistencias():int. El constructor convierte precio/existencias negativos a 0. Lee artículo y dos ventas; por cada venta imprime OK/NO; al final Stock: N.", + prompt: "Implementa el modelo que acabas de decidir: Articulo con Codigo de sólo lectura externa, Precio y Existencias privados, constructor, Vender(int):bool y ConsultarExistencias():int. El constructor convierte precio/existencias negativos a 0. Lee artículo y dos ventas; por cada venta imprime OK/NO; al final Stock: N.", starterCode: `using System; class Articulo diff --git a/prisma/content/csharp/unidad-06-diseno-robusto.ts b/prisma/content/csharp/unidad-06-diseno-robusto.ts index 3f9525c..d69f674 100644 --- a/prisma/content/csharp/unidad-06-diseno-robusto.ts +++ b/prisma/content/csharp/unidad-06-diseno-robusto.ts @@ -20,14 +20,48 @@ export const unidad06: UnitDefinition = { slug: "instancia-y-static", title: "Responsabilidades de instancia y de clase", description: "Evita convertir static en almacenamiento global accidental.", - estimatedMinutes: 14, - xpReward: 45, + estimatedMinutes: 16, + xpReward: 48, steps: [ { + // a) instancia vs clase, primero — sin static todavía. type: "theory", markdown: `# ¿De quién es el dato? -Cada objeto posee sus miembros de instancia. Un miembro \`static\` pertenece a la clase y se comparte. Una constante o una función pura de utilidad puede ser estática; el nombre o saldo de una entidad no. Estado global compartido introduce dependencias ocultas y hace las pruebas frágiles.`, +Cada objeto tiene sus propios miembros de **instancia**: el \`Nombre\` de un alumno no es el mismo que el de otro, aunque los dos sean \`Alumno\`. Eso ya lo conoces desde U1: cada \`new\` reserva su propio estado. + +Un miembro \`static\` es distinto: pertenece a la **clase**, no a cada objeto. Sólo existe una copia, compartida por todos. Antes de usarlo hay que preguntar: ¿este dato es de un alumno en particular, o es algo que describe a la clase completa?`, + }, + { + // b) primer caso static: SEGURO, sin estado mutable compartido. + type: "code_example", + code: `using System; +class Circulo +{ + public const double Pi = 3.1416; // constante: igual para todo Circulo + + public static double Area(double radio) + { + return Pi * radio * radio; // no lee ni cambia el estado de ningún objeto + } +} +class Program +{ + static void Main() + { + Console.WriteLine(Circulo.Area(2).ToString("0.00")); + } +}`, + explanation: "Pi es una constante compartida: no cambia, y no tiene sentido que cada Circulo cargue su propia copia. Area es una función pura: recibe un dato, calcula y regresa un resultado, sin recordar nada entre una llamada y otra. Ninguno de los dos necesita un objeto para existir — por eso son static seguros.", + runnable: true, + expectedOutput: "12.57", + }, + { + // c) ahora sí, el caso static MUTABLE — sólo después del caso seguro. + type: "theory", + markdown: `# Un caso distinto: static que SÍ cambia + +\`Pi\` y \`Area\` nunca cambian entre llamadas. Pero hay otro uso legítimo de \`static\`: un contador compartido por todos los objetos, que sí muta con el tiempo.`, }, { type: "code_example", @@ -46,16 +80,25 @@ class Program Console.WriteLine(Alumno.Creados); } }`, - explanation: "Nombre pertenece a cada objeto; Creados describe a la clase completa.", + explanation: "Nombre pertenece a cada objeto; Creados describe a la clase completa y SÍ cambia — cada new lo incrementa. Es static, pero ya no es un cálculo sin estado como Area: ahora es un dato compartido que muta.", runnable: true, expectedOutput: "2", }, + { + // d) coste breve: por qué no convertirlo en almacén global accidental. + type: "theory", + markdown: `# El costo de un static mutable + +Un contador como \`Creados\` es deliberado y acotado: sólo cuenta, y su regla es clara. El riesgo aparece cuando \`static\` se usa como bodega general — guardar ahí "por comodidad" datos que en realidad pertenecen a un objeto (como el saldo o el nombre de una entidad concreta). + +Eso crea una dependencia oculta: cualquier parte del programa puede leer o cambiar ese valor sin pasar por ningún objeto, lo que hace las pruebas frágiles (el resultado de una prueba depende de qué corrió antes) y el comportamiento difícil de rastrear. Usa \`static\` para lo que de verdad es de la clase — constantes, funciones puras, contadores explícitos — nunca como atajo para no crear el campo de instancia correcto.`, + }, { type: "matching", pairs: [ { left: "Saldo de una cuenta", right: "Instancia" }, - { left: "Número de objetos creados", right: "static" }, - { left: "Convertir Celsius a Fahrenheit sin estado", right: "static" }, + { left: "Número de objetos creados", right: "static (mutable, deliberado)" }, + { left: "Convertir Celsius a Fahrenheit sin estado", right: "static (función pura)" }, { left: "Nombre de un alumno", right: "Instancia" }, ], explanation: "La propiedad semántica del dato, no la comodidad de acceso, decide.", diff --git a/prisma/content/csharp/unidad-07-gui.ts b/prisma/content/csharp/unidad-07-gui.ts index 386e2ca..2383196 100644 --- a/prisma/content/csharp/unidad-07-gui.ts +++ b/prisma/content/csharp/unidad-07-gui.ts @@ -25,24 +25,47 @@ export const unidad07: UnitDefinition = { slug: "formularios-controles-eventos", title: "Formulario, controles y eventos", description: "Comprende el ciclo de interacción y crea la primera interfaz local.", - estimatedMinutes: 20, - xpReward: 45, + estimatedMinutes: 24, + xpReward: 48, steps: [ + { + // Primer contacto con WinForms: qué es la ventana, de dónde + // salen los controles, y por qué Name y Text son cosas distintas + // — todo lo que alguien que abre Visual Studio por primera vez + // necesita para no perderse en el resto de la unidad. + type: "theory", + markdown: `# Tu primera ventana + +Hasta ahora tus programas de C# corrían en la consola: leían texto y escribían texto. **Windows Forms** es otra forma de correr un programa de C#: en vez de texto, el usuario ve una **ventana** con botones y cajas. + +- **\`Form\`** es la ventana misma. Cuando creas un proyecto Windows Forms, Visual Studio te da una ventana vacía llamada \`Form1\`. +- El **Toolbox** (la caja de herramientas, normalmente a un lado del editor) es la lista de controles disponibles: \`Button\`, \`TextBox\`, \`Label\`, etc. **Arrastras** uno del Toolbox al \`Form\` para agregarlo — no lo escribes a mano. +- Cada control que arrastras tiene, entre muchas propiedades, dos que vale la pena distinguir desde ya: + - **\`Name\`**: el identificador que vas a usar en tu código C# para referirte a ese control (\`txtNombre\`, \`btnSaludar\`). Nadie lo ve en la ventana. + - **\`Text\`**: lo que el usuario SÍ ve dibujado — el texto de un botón, el contenido inicial de una caja. + +Cambiar \`Text\` de un botón a "Saludar" no cambia su \`Name\`; puedes seguir llamándolo \`btnSaludar\` en el código aunque diga otra cosa en pantalla.`, + }, { type: "theory", - markdown: `# La interfaz reacciona a eventos + markdown: `# De un clic a una respuesta + +Un control puede disparar **eventos**: cosas que le pasan, como que le hagan \`Click\`. Tú no revisas "¿ya hicieron clic?" en un ciclo — en vez de eso, **conectas** un método tuyo a ese evento, y Windows Forms lo llama automáticamente cuando ocurre. + +Para conectar un \`Click\`: seleccionas el botón en el diseñador y das doble clic sobre él (o usas la ventana de Propiedades → el ícono de rayo → \`Click\`). Visual Studio genera un método vacío con el nombre \`nombreDelBoton_Click\` y te deja escribiendo dentro de él. -Un \`Form\` es una ventana/contenedor; \`TextBox\`, \`Button\` y \`Label\` son controles. El usuario dispara un evento como \`Click\`; un manejador lee la entrada, llama al dominio y actualiza la salida. +Ese método es el **manejador** (handler). El flujo completo de la unidad es siempre el mismo: -Flujo: **entrada del control → manejador → objeto de dominio → resultado → control de salida**. El diseñador genera parte de la inicialización; no copies ese código al ejecutor web.`, +**entrada del control → manejador → objeto de dominio → resultado → control de salida**`, }, { type: "matching", pairs: [ { left: "Form", right: "Ventana y contenedor principal" }, + { left: "Toolbox", right: "De aquí arrastras los controles al Form" }, { left: "TextBox", right: "Entrada de texto" }, { left: "Button.Click", right: "Evento" }, - { left: "btnGuardar_Click", right: "Manejador" }, + { left: "btnGuardar_Click", right: "Manejador conectado al evento" }, { left: "Label", right: "Salida breve" }, ], explanation: "El evento no contiene por sí mismo la regla de negocio; sólo activa el manejador.", @@ -60,14 +83,25 @@ private void btnSaludar_Click(object sender, EventArgs e) localOnlyNote: "Requiere Visual Studio en Windows. Es un fragmento de Windows Forms, no un programa de consola: se ejecuta en el laboratorio local de esta unidad, no en el navegador.", }, + { + type: "theory", + markdown: `# Form1.cs y el código del Designer + +Cada formulario en realidad son DOS archivos: + +- **\`Form1.cs\`**: donde tú escribes, incluidos los manejadores como \`btnSaludar_Click\`. +- **\`Form1.Designer.cs\`**: lo genera automáticamente Visual Studio cada vez que arrastras un control o cambias una propiedad desde el diseñador visual. Ahí es donde vive la línea que crea \`txtNombre\`, le pone su \`Name\`, su posición, etc. + +No necesitas escribir el \`.Designer.cs\` a mano ni entender cada línea — sólo saber que existe, que lo genera el diseñador, y que tu código de verdad (los manejadores, la lógica) va en \`Form1.cs\`.`, + }, { type: "theory", markdown: `## Laboratorio local verificable 1. En Visual Studio crea **Windows Forms App (.NET)**, C#, con .NET 10 LTS si está instalado; .NET 8 es aceptable. -2. Agrega \`txtNombre\`, \`btnSaludar\` y \`lblResultado\`; asigna esos valores a \`Name\`. -3. Enlaza \`btnSaludar.Click\` al manejador mostrado. -4. Ejecuta, escribe \`Franco\` y pulsa el botón. +2. Desde el Toolbox, arrastra un \`TextBox\`, un \`Button\` y un \`Label\` al \`Form1\`. Con cada uno seleccionado, cambia su \`Name\` en la ventana de Propiedades a \`txtNombre\`, \`btnSaludar\` y \`lblResultado\` — y el \`Text\` del botón a "Saludar" (el \`Name\` no cambia por eso). +3. Da doble clic sobre \`btnSaludar\` para generar su manejador de \`Click\`, y escribe dentro el código mostrado arriba. +4. Ejecuta, escribe \`Franco\` en la caja y pulsa el botón. **Evidencia observable:** la ventana permanece abierta y \`lblResultado\` muestra \`Hola, Franco\`. Entrega captura y el archivo \`Form1.cs\`; explica en una frase qué control originó el evento.`, }, @@ -211,27 +245,18 @@ Implementa \`Pedido\` con la regla \`cantidad > 0\` y el manejador mostrado. Ver estimatedMinutes: 20, xpReward: 45, steps: [ + // --- Parte A: estado/referencias que sobreviven entre eventos --- { type: "theory", - markdown: `# Una ventana con flujo visible + markdown: `# Parte 1: lo que sobrevive entre clics -Usa \`Panel\`, \`GroupBox\` o \`TableLayoutPanel\` para agrupar entrada, acciones y resultado. El formulario puede conservar una referencia privada a un servicio de dominio; no debe recrearlo si el estado necesita sobrevivir entre clics. Dibuja antes el flujo de datos y de procesos. +Cada método manejador (\`btnX_Click\`) corre y termina. Sus variables locales mueren con él. Pero a veces necesitas que algo **recuerde** su valor de un clic al siguiente — por ejemplo, un servicio de dominio que no tiene sentido recrear cada vez. -Para entregar, usa **Publish** de Visual Studio o \`dotnet publish -c Release\`. Define sistema operativo, arquitectura y modo dependiente del framework o autónomo. Prueba el resultado en otra carpeta o equipo; publicar no sustituye probar.`, - }, - { - type: "matching", - pairs: [ - { left: "TableLayoutPanel", right: "Alineación adaptable de controles" }, - { left: "GroupBox", right: "Agrupación con título" }, - { left: "Campo privado del Form", right: "Referencia que sobrevive entre eventos" }, - { left: "Publish", right: "Salida desplegable" }, - ], - explanation: "La organización visual y la vida de los objetos son decisiones distintas pero coordinadas.", +Para eso, el \`Form\` puede tener sus propios **campos privados**, igual que cualquier otra clase que ya conoces. Un campo del \`Form\` se crea una sola vez (normalmente en el constructor, después de \`InitializeComponent();\`) y cada manejador puede leerlo y usarlo sin volver a construirlo.`, }, { type: "quiz", - question: "¿Cuál referencia debe ser campo del formulario?", + question: "¿Cuál referencia debe ser campo privado del formulario, en vez de una variable local dentro del manejador?", options: [ "Una variable temporal usada en una sola línea.", "El servicio que conserva el estado entre varios clics.", @@ -239,13 +264,33 @@ Para entregar, usa **Publish** de Visual Studio o \`dotnet publish -c Release\`. "Cada argumento de un método.", ], correctIndex: 1, - explanation: "Su ciclo de vida coincide con el de la ventana.", + explanation: "Su ciclo de vida coincide con el de la ventana: se crea una vez y varios manejadores lo usan, en vez de recrearlo en cada clic.", + }, + // --- Parte B: organización visual y publicación --- + { + type: "theory", + markdown: `# Parte 2: organizar la ventana y publicarla + +Con controles y manejadores ya resueltos, falta ordenar la ventana y entregarla como programa ejecutable. Son dos decisiones distintas de las de la Parte 1: aquí no se trata de qué recuerda el formulario, sino de cómo se ve y cómo se comparte. + +Usa \`Panel\`, \`GroupBox\` o \`TableLayoutPanel\` para agrupar entrada, acciones y resultado — así el usuario reconoce de un vistazo qué hace cada zona. Dibuja antes el flujo de datos y de procesos. + +Para entregar, usa **Publish** de Visual Studio o \`dotnet publish -c Release\`. Define sistema operativo, arquitectura y modo dependiente del framework o autónomo. Prueba el resultado en otra carpeta o equipo; publicar no sustituye probar.`, + }, + { + type: "matching", + pairs: [ + { left: "TableLayoutPanel", right: "Alineación adaptable de controles" }, + { left: "GroupBox", right: "Agrupación con título" }, + { left: "Publish", right: "Salida desplegable" }, + ], + explanation: "Organizar la ventana y publicarla son pasos posteriores a resolver el estado que conserva el formulario.", }, { type: "theory", markdown: `## Laboratorio local verificable -Reorganiza la aplicación de pedidos en tres contenedores: Entrada, Acciones y Resultado. Conserva un \`ServicioPedidos\` como campo privado. Publica en Release para \`win-x64\` con el modo acordado por el docente. +Reorganiza la aplicación de pedidos en tres contenedores: Entrada, Acciones y Resultado. Conserva un \`ServicioPedidos\` como campo privado (Parte 1). Publica en Release para \`win-x64\` con el modo acordado por el docente (Parte 2). **Evidencia observable:** captura de la interfaz, diagrama de flujo de datos, carpeta publicada y prueba desde el ejecutable publicado. Incluye \`README.txt\` con requisitos, pasos y versión de .NET. No subas \`bin/\` o \`obj/\` al contenido del curso.`, }, diff --git a/prisma/content/csharp/unidad-08-integrador.ts b/prisma/content/csharp/unidad-08-integrador.ts index 21a3640..e753f20 100644 --- a/prisma/content/csharp/unidad-08-integrador.ts +++ b/prisma/content/csharp/unidad-08-integrador.ts @@ -61,7 +61,9 @@ Clases del modelo — y sólo ésas se implementan en toda la unidad, sin agrega type: "theory", markdown: `## Entregable de diseño -Produce: (1) alcance de cinco a ocho requisitos; (2) diagrama UML con visibilidad, atributos, operaciones, multiplicidades y relaciones; (3) flujo de proceso; (4) seis criterios de aceptación, incluidos dos inválidos; (5) matriz requisito → clase responsable → prueba. +Produce: (1) alcance de cinco a ocho requisitos; (2) diagrama UML con visibilidad, atributos, operaciones y relaciones; (3) flujo de proceso; (4) seis criterios de aceptación, incluidos dos inválidos; (5) matriz requisito → clase responsable → prueba. + +No se exigen multiplicidades (1, 0..1, 0..*, etc.): no se enseñaron en U3/U4, así que no forman parte de lo que este diagrama tiene que mostrar. Revisión obligatoria: ninguna regla vive únicamente en el formulario; toda relación del UML aparece en código; ningún elemento “futuro” se implementa por accidente.`, }, diff --git a/prisma/content/exercises/csharp/u01-modelar.ts b/prisma/content/exercises/csharp/u01-modelar.ts index ede387b..036a2d1 100644 --- a/prisma/content/exercises/csharp/u01-modelar.ts +++ b/prisma/content/exercises/csharp/u01-modelar.ts @@ -15,21 +15,34 @@ export const u01ModelarExercises: PracticeUnitSetDefinition = { unitIcon: "🧱", exercises: [ { + // Objetivo único: crear y usar un objeto (etapa `crear-un-objeto`). + // La clase y la lectura ya están escritas para que la única + // habilidad evaluada sea new + asignación de campos + llamada a un + // método que YA existe — no clase+método+objeto+I/O a la vez. slug: "csharp-poo-objeto-mascota", title: "Una mascota como objeto", - description: "Modela dos datos y un comportamiento.", - prompt: "Lee nombre y especie. Crea Mascota y llama Describir para imprimir \"N es E\".", + description: "Crea el objeto, asígnale sus datos y úsalo.", + prompt: "La clase Mascota ya está completa, con Nombre, Especie y el método Describir() (que imprime \"N es E\"). Main ya lee ambos datos. Te toca: crea el objeto, asígnale los dos campos y llama Describir().", starterCode: `using System; class Mascota { - /* estado y método */ + public string Nombre; + public string Especie; + + public void Describir() + { + Console.WriteLine(Nombre + " es " + Especie); + } } class Program { static void Main() { - /* lee y usa el objeto */ + string nombre = Console.ReadLine(); + string especie = Console.ReadLine(); + + // Crea el objeto, asígnale los datos y llama Describir() } }`, solutionCode: `using System; @@ -37,9 +50,10 @@ class Mascota { public string Nombre; public string Especie; + public void Describir() { - Console.WriteLine(Nombre+" es "+Especie); + Console.WriteLine(Nombre + " es " + Especie); } } @@ -47,19 +61,22 @@ class Program { static void Main() { - Mascota m=new Mascota(); - m.Nombre=Console.ReadLine(); - m.Especie=Console.ReadLine(); + string nombre = Console.ReadLine(); + string especie = Console.ReadLine(); + + Mascota m = new Mascota(); + m.Nombre = nombre; + m.Especie = especie; m.Describir(); } }`, hints: [ - "Los datos pertenecen a Mascota.", - "Crea exactamente una instancia.", - "Describir imprime el estado.", + "Usa new Mascota() para crear el objeto.", + "Asigna cada campo con el punto: m.Nombre = nombre;", + "Describir ya está escrito: sólo llámalo.", ], difficulty: "easy", - xpReward: 18, + xpReward: 16, structure: { classes: [ { @@ -86,49 +103,58 @@ class Program ], }, { + // Objetivo único: independencia de dos objetos (etapa + // `clase-objeto-instancia`). Sin método `Encender()`: la prueba de + // independencia es reasignar un campo y mostrar con + // Console.WriteLine directo, igual que en la lección. slug: "csharp-poo-dos-lamparas", title: "Dos lámparas independientes", - description: "Demuestra que dos instancias conservan estados distintos.", - prompt: "Lee dos colores. Crea dos Lampara con campo Color y método Encender; imprime \"Luz COLOR\" para cada una.", + description: "Cambia una y comprueba que la otra no se entera.", + prompt: "Lee dos colores. Crea dos objetos Lampara (campo público Color, sin métodos) y asígnales esos colores. Después cambia el Color de la PRIMERA lámpara a \"apagada\" y muestra las dos con Console.WriteLine, formato \"Luz COLOR\" — para comprobar que la segunda conserva su color original.", starterCode: `using System; class Lampara { - /* completa */ + /* completa: sólo un campo, sin métodos */ } class Program { static void Main() { - /* dos new */ + string color1 = Console.ReadLine(); + string color2 = Console.ReadLine(); + + /* dos new, apaga sólo la primera, muestra las dos */ } }`, solutionCode: `using System; class Lampara { public string Color; - public void Encender() - { - Console.WriteLine("Luz "+Color); - } } class Program { static void Main() { - Lampara a=new Lampara(); - a.Color=Console.ReadLine(); - Lampara b=new Lampara(); - b.Color=Console.ReadLine(); - a.Encender(); - b.Encender(); + string color1 = Console.ReadLine(); + string color2 = Console.ReadLine(); + + Lampara a = new Lampara(); + a.Color = color1; + Lampara b = new Lampara(); + b.Color = color2; + + a.Color = "apagada"; + + Console.WriteLine("Luz " + a.Color); + Console.WriteLine("Luz " + b.Color); } }`, hints: [ - "Usa dos expresiones new.", - "No uses un campo static.", - "Llama el método en orden.", + "Usa dos expresiones new: una lámpara no puede compartirse con la otra.", + "Sólo reasigna el Color de la primera, después de crear ambas.", + "Imprime cada una con su propio Console.WriteLine, sin método.", ], difficulty: "easy", xpReward: 20, @@ -137,28 +163,31 @@ class Program { name: "Lampara", fields: [{ name: "Color", visibility: "public", type: "string" }], - methods: [{ name: "Encender", visibility: "public" }], }, ], }, testCases: [ { stdin: "azul\nroja\n", - expectedStdout: "Luz azul\nLuz roja\n", + expectedStdout: "Luz apagada\nLuz roja\n", visible: true, }, { stdin: "blanco cálido\nverde\n", - expectedStdout: "Luz blanco cálido\nLuz verde\n", + expectedStdout: "Luz apagada\nLuz verde\n", visible: false, }, ], }, { + // Transferencia final de comportamiento/responsabilidad de la + // unidad. El objetivo es dónde vive la regla (dentro de Usar), no + // leer ni convertir datos — así que Main ya viene con la lectura y + // el parseo resueltos. slug: "csharp-poo-bateria-comportamiento", title: "Batería que se descarga", description: "Coloca el cambio de estado en el objeto.", - prompt: "Bateria inicia con carga leída. Usar(int puntos) resta sin bajar de cero. Lee carga y dos consumos; imprime el valor final.", + prompt: "Bateria inicia con carga leída. Usar(int puntos) resta sin bajar de cero — la regla la aplica la batería, no Main. Main ya lee la carga y los dos consumos, y ya crea el objeto e imprime el resultado; sólo falta la clase Bateria.", starterCode: `using System; class Bateria { @@ -169,7 +198,16 @@ class Program { static void Main() { - /* completa */ + int cargaInicial = int.Parse(Console.ReadLine()); + int consumo1 = int.Parse(Console.ReadLine()); + int consumo2 = int.Parse(Console.ReadLine()); + + Bateria b = new Bateria(); + b.Carga = cargaInicial; + b.Usar(consumo1); + b.Usar(consumo2); + + Console.WriteLine(b.Carga); } }`, solutionCode: `using System; @@ -178,8 +216,8 @@ class Bateria public int Carga; public void Usar(int puntos) { - Carga-=puntos; - if(Carga<0) Carga=0; + Carga -= puntos; + if (Carga < 0) Carga = 0; } } @@ -187,17 +225,22 @@ class Program { static void Main() { - Bateria b=new Bateria(); - b.Carga=int.Parse(Console.ReadLine()); - b.Usar(int.Parse(Console.ReadLine())); - b.Usar(int.Parse(Console.ReadLine())); + int cargaInicial = int.Parse(Console.ReadLine()); + int consumo1 = int.Parse(Console.ReadLine()); + int consumo2 = int.Parse(Console.ReadLine()); + + Bateria b = new Bateria(); + b.Carga = cargaInicial; + b.Usar(consumo1); + b.Usar(consumo2); + Console.WriteLine(b.Carga); } }`, hints: [ - "Usar cambia Carga.", - "Corrige el límite después de restar.", - "Main no debe calcular el saldo.", + "Usar cambia Carga; no lo hagas desde Main.", + "Corrige el límite (Carga < 0) después de restar, dentro de Usar.", + "Main ya está completo: no necesitas tocarlo.", ], difficulty: "medium", xpReward: 28, @@ -229,19 +272,28 @@ class Program ], }, { - // Alineado con la nueva secuencia de U1 (`CS-01`): esta unidad enseña - // campos públicos y métodos. `private` y los constructores llegan en - // U2, así que el ejercicio pide DECIDIR qué miembros entran al modelo - // —que es la etapa 6 de la unidad— sin sintaxis que todavía no se ha - // visto. + // Alineado con la nueva secuencia de U1 (etapa `abstraccion-con- + // criterio`): esta unidad enseña campos públicos y métodos. `private` + // y los constructores llegan en U2. El enunciado plantea una + // decisión real —cinco candidatos, sólo tres pertenecen al modelo— + // ANTES de dar la forma exacta que se va a calificar; no dicta el + // modelo desde la primera línea. slug: "csharp-poo-abstraer-casillero", title: "Abstracción de un casillero", - description: "Selecciona sólo el estado necesario para prestar y liberar.", - prompt: "El taller sólo necesita saber, de cada casillero, su número, quién lo tiene y si está ocupado. Modela `Casillero` con campos públicos `Numero` (int), `Propietario` (string) y `Ocupado` (bool), y con los métodos `Ocupar(string quien)` —guarda al propietario y marca ocupado— y `Liberar()` —vacía el propietario con \"\" y marca libre—. `Mostrar()` imprime \"NUM | PROPIETARIO | ocupado\" o \"NUM | libre\" según el estado. Lee número, primer propietario y una orden (\"liberar\" o cualquier otra cosa); ocupa el casillero y, si la orden es liberar, libéralo. Después muestra el casillero.", + description: "Decide qué pertenece al modelo antes de programarlo.", + prompt: `El taller de casilleros va a registrar préstamos: quién tiene cada casillero y si está libre u ocupado. Antes de programar, decide cuáles de estos cinco datos pertenecen a ese problema: + +- el número del casillero +- quién lo tiene ahora mismo +- si está ocupado +- el color de la puerta +- la marca del candado + +Sólo tres son relevantes para registrar préstamos; los otros dos no ayudan a identificar el casillero, a saber quién lo ocupa ni a decidir si está libre. Modela \`Casillero\` con exactamente esos tres como campos públicos: \`Numero\` (int), \`Propietario\` (string) y \`Ocupado\` (bool). Agrega \`Ocupar(string quien)\` —guarda al propietario y marca ocupado— y \`Liberar()\` —vacía el propietario con "" y marca libre—. \`Mostrar()\` imprime "NUM | PROPIETARIO | ocupado" o "NUM | libre" según el estado. Lee número, primer propietario y una orden ("liberar" o cualquier otra cosa); ocupa el casillero y, si la orden es liberar, libéralo. Después muestra el casillero.`, starterCode: `using System; class Casillero { - /* Sólo lo que este sistema necesita: numero, propietario, ocupado */ + /* Sólo los tres datos que decidiste que pertenecen al modelo */ } class Program From 2bd1b78dfe4c591ad7304d74221bb1223b8ab7d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 02:12:28 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Casillero=20deja=20de=20revelar=20la=20deci?= =?UTF-8?q?si=C3=B3n=20de=20abstracci=C3=B3n=20en=20el=20enunciado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit csharp-poo-abstraer-casillero enumeraba los cinco candidatos y, en la misma respiración, revelaba cuáles tres eran correctos y la estructura exacta a implementar (Numero/Propietario/Ocupado/Ocupar/Liberar/ Mostrar). El alumno nunca tenía que derivar nada. El nuevo enunciado plantea los datos posibles de un casillero real, pide identificar cuáles ayudan a registrar un préstamo sin decir cuáles son, y describe el comportamiento requerido (identificar, consultar, ocupar, liberar, mostrar con el formato exacto) en términos de la acción, no del nombre del miembro — remitiendo a la convención PascalCase del curso para que el alumno derive los nombres. El structureContract sigue exigiendo internamente la estructura correcta. Slug, starterCode, solutionCode, structure y testCases sin cambios. Verificación dirigida: tests/content/structure-contracts.test.ts (53/53) y verify-content.ts csharp-poo-1 (81/81 piezas compilan y producen la salida esperada). --- prisma/content/exercises/csharp/u01-modelar.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/prisma/content/exercises/csharp/u01-modelar.ts b/prisma/content/exercises/csharp/u01-modelar.ts index 036a2d1..a966509 100644 --- a/prisma/content/exercises/csharp/u01-modelar.ts +++ b/prisma/content/exercises/csharp/u01-modelar.ts @@ -275,21 +275,19 @@ class Program // Alineado con la nueva secuencia de U1 (etapa `abstraccion-con- // criterio`): esta unidad enseña campos públicos y métodos. `private` // y los constructores llegan en U2. El enunciado plantea una - // decisión real —cinco candidatos, sólo tres pertenecen al modelo— - // ANTES de dar la forma exacta que se va a calificar; no dicta el - // modelo desde la primera línea. + // decisión real: el enunciado NO revela cuáles candidatos pertenecen + // al modelo ni enumera la estructura exacta a implementar — eso lo + // tiene que derivar el alumno del requisito. El structureContract de + // abajo sigue exigiendo la estructura correcta internamente, para + // calificar sin regalar la respuesta en el enunciado. slug: "csharp-poo-abstraer-casillero", title: "Abstracción de un casillero", description: "Decide qué pertenece al modelo antes de programarlo.", - prompt: `El taller de casilleros va a registrar préstamos: quién tiene cada casillero y si está libre u ocupado. Antes de programar, decide cuáles de estos cinco datos pertenecen a ese problema: + prompt: `El taller de casilleros va a registrar préstamos. Un casillero real podría tener número, color de puerta, marca de candado, quién lo tiene ahora mismo, si está ocupado, fecha de instalación... Antes de programar, decide cuáles de esos datos ayudan realmente a registrar un préstamo y déjalos fuera si no ayudan: no modeles nada que no necesites para este problema. -- el número del casillero -- quién lo tiene ahora mismo -- si está ocupado -- el color de la puerta -- la marca del candado +Tu clase \`Casillero\` tiene que permitir: identificar el casillero por su número; saber quién lo tiene ahora mismo y si está ocupado; ocuparlo (queda registrado quién lo ocupa); y liberarlo (queda sin propietario y libre otra vez). Necesitas además una forma de mostrarlo: cuando está ocupado, "NUM | PROPIETARIO | ocupado"; cuando está libre, "NUM | libre". Usa nombres de miembro en PascalCase que reflejen directamente cada dato u operación que acabas de identificar, siguiendo la convención del resto del curso. -Sólo tres son relevantes para registrar préstamos; los otros dos no ayudan a identificar el casillero, a saber quién lo ocupa ni a decidir si está libre. Modela \`Casillero\` con exactamente esos tres como campos públicos: \`Numero\` (int), \`Propietario\` (string) y \`Ocupado\` (bool). Agrega \`Ocupar(string quien)\` —guarda al propietario y marca ocupado— y \`Liberar()\` —vacía el propietario con "" y marca libre—. \`Mostrar()\` imprime "NUM | PROPIETARIO | ocupado" o "NUM | libre" según el estado. Lee número, primer propietario y una orden ("liberar" o cualquier otra cosa); ocupa el casillero y, si la orden es liberar, libéralo. Después muestra el casillero.`, +\`Main\` ya lee número, primer propietario y una orden ("liberar" o cualquier otra cosa). Complétalo para: ocupar el casillero y, si la orden es liberar, liberarlo; después mostrarlo.`, starterCode: `using System; class Casillero {